PageSourceSearch

https://wheatlandcounty.ca/wp-content/plugins/escribe-agenda/public/js/escribe-agenda-public.js?ver=1.2.3

js wheatlandcounty.ca collected 2026-10-02 13:44:31 UTC 44,053 bytes, 1,769 lines download raw bytes

1jQuery(document).ready(function($) {
2
3    'use strict';
4
5
6
7    // First check if Vue is available
8
9    if (typeof Vue === 'undefined') {
10
11        console.error('Vue is not defined. Make sure Vue is loaded before this script.');
12
13        return;
14
15    }
16
17
18
19    if (typeof EscribeAgendaData === 'undefined') {
20
21        console.error('eScribe: shared data helpers are not loaded.');
22
23        return;
24
25    }
26
27
28
29    // Initialize a separate Vue app for each eScribe widget on the page
30
31    const escribeMounts = document.querySelectorAll('.escribe-mount');
32
33    escribeMounts.forEach(function(mountEl) {
34
35        const instanceId = mountEl.dataset.instanceId;
36
37        const carouselSel = '#escribeCarousel_' + instanceId;
38
39        const pastModalSel = '#eScribePastModal_' + instanceId;
40
41        const searchModalSel = '#eScribeSearchModal_' + instanceId;
42
43        const searchModalId = 'eScribeSearchModal_' + instanceId;
44
45        const docsModalSel = '#eScribeDocsModal_' + instanceId;
46
47        const docsModalId = 'eScribeDocsModal_' + instanceId;
48
49        const resizeNs = '.escribeResize_' + instanceId;
50
51        // Per-instance, not window-global: two carousels on one page would
52        // otherwise clobber each other's debounce timer.
53
54        let carouselResizeTimer = null;
55
56        let lastCarouselWidth = window.innerWidth;
57
58
59
60        const app = Vue.createApp({
61
62            data() {
63
64                return {
65
66                    instanceId: instanceId,
67
68                    loading: false,
69
70                    loadError: '',
71
72                    searchInitialized: true, // NEW: Always keep search visible regardless of loading state
73
74                    raw: {},
75
76                    meetings: {
77
78                        MeetingGetByQueryResult: {
79
80                            Agendas: {
81
82                                'Agenda.Record': []
83
84                            },
85
86                            FutureMeetings: {
87
88                                'Meeting.Record': []
89
90                            },
91
92                            PastMeetings: {
93
94                                'Meeting.Record': []
95
96                            }
97
98                        }
99
100                    },
101
102                    emeetings: {
103
104                        past: [],
105
106                        future: []
107
108                    },
109
110                    documentNames: {}, // Document.Record Id -> document name
111
112                    selectedDocuments: [], // Supplemental docs shown in the documents modal
113
114                    selectedDocumentsTitle: '',
115
116                    meetingTypeFilter: [], // Array of meeting type IDs to filter by
117
118                    years: [],
119
120                    selectedYear: null,
121
122                    selectedMonth: null,
123
124                    searchText: '',
125
126                    searching: false,
127
128                    searchResults: [],
129
130                    showSearchResults: false,
131
132                    currentSearchPage: 1,
133
134                    searchPageSize: 10,
135
136                    totalSearchResults: 0,
137
138                    totalSearchPages: 1
139
140                };
141
142            },
143
144            methods: {
145
146                getType(Id) {
147
148                    let name = '';
149
150                    const meetingTypes = this.meetings.MeetingGetByQueryResult.MeetingTypes['MeetingType.Record'];
151
152                    
153
154                    if (meetingTypes) {
155
156                        $.each(meetingTypes, function(k, v) {
157
158                            if (v.Id.Id === Id) {
159
160                                name = v.Name.KeyValueOfintstring[1].Value;
161
162                                return false;
163
164                            }
165
166                        });
167
168                    }
169
170                    
171
172                    return name;
173
174                },
175
176                // Meeting/document resolution lives in escribe-agenda-data.js so the
177                // carousel and the calendar cannot drift apart. These stay as
178                // methods because the templates call them directly.
179
180                asArray(value) {
181
182                    return EscribeAgendaData.asArray(value);
183
184                },
185
186                dataIndex() {
187
188                    // Rebuilt when the payload changes; cheap enough to memoise
189                    // against the current result object.
190
191                    const result = this.meetings.MeetingGetByQueryResult;
192
193                    if (!this._idx || this._idxFor !== result) {
194
195                        this._idx = EscribeAgendaData.index(result);
196
197                        this._idxFor = result;
198
199                    }
200
201                    return this._idx;
202
203                },
204
205                fileStreamUrl(documentId) {
206
207                    return EscribeAgendaData.fileStreamUrl(this.dataIndex(), documentId);
208
209                },
210
211                meetingUrl(meetingId) {
212
213                    return EscribeAgendaData.meetingUrl(this.dataIndex(), meetingId);
214
215                },
216
217                buildDocumentNames(result) {
218
219                    this._idx = EscribeAgendaData.index(result);
220
221                    this._idxFor = result;
222
223                    this.documentNames = this._idx.documentNames;
224
225                },
226
227                documentName(pkg) {
228
229                    return EscribeAgendaData.documentName(this.dataIndex(), pkg);
230
231                },
232
233                getPackage(meeting) {
234
235                    const agenda = EscribeAgendaData.getAgenda(this.dataIndex(), meeting);
236
237                    if (agenda) {
238
239                        meeting.Link = this.meetingUrl(meeting.Id);
240
241                        meeting.PDF = this.fileStreamUrl(agenda.DocumentId);
242
243                    }
244
245                    return agenda;
246
247                },
248
249                getMinutes(meeting) {
250
251                    const minutes = EscribeAgendaData.getMinutes(this.dataIndex(), meeting);
252
253                    if (minutes) {
254
255                        meeting.MinuteLink = this.fileStreamUrl(minutes.DocumentId);
256
257                    }
258
259                    return minutes;
260
261                },
262
263                getSupplementalDocuments(meeting) {
264
265                    return EscribeAgendaData.getSupplementalDocuments(this.dataIndex(), meeting, meeting.Minutes);
266
267                },
268
269                showDocuments(meeting) {
270
271                    this.selectedDocuments = meeting.Documents || [];
272
273                    this.selectedDocumentsTitle = this.getType(meeting.MeetingTypeId) + ' – ' + this.niceDate(meeting.Start);
274
275                    const modal = bootstrap.Modal.getOrCreateInstance(document.getElementById(docsModalId));
276
277                    modal.show();
278
279                },
280
281                GetData() {
282
283                    this.loading = true;
284
285                    
286
287                    // Create URL parameters
288
289                    const params = new URLSearchParams();
290
291                    params.append('action', 'geteScribeMeetings');
292
293                    
294
295                    // Always include the validate parameter
296
297                    // (it won't be checked for non-logged in users with our updated PHP)
298
299                    params.append('validate', eScribeajaxObj.validate);
300
301                    
302
303                    axios.post(eScribeajaxObj.ajax_url, params)
304
305                        .then(response => this.loadFields(response))
306
307                        .catch(error => {
308
309                            console.error('Error fetching data:', error);
310
311                            this.loadError = 'Meeting information is temporarily unavailable.';
312
313                            this.loading = false;
314
315                        });
316
317                },
318
319                rebuildObject(meeting) {
320
321                    meeting.Link = '#';
322
323                    meeting.PDF = '#';
324
325                    meeting.MinuteLink = '#';
326
327                    meeting.Name = this.getType(meeting.MeetingTypeId);
328
329                    meeting.Agenda = this.getPackage(meeting);
330
331                    meeting.Minutes = this.getMinutes(meeting);
332
333                    // Must run after getMinutes(): it excludes the minutes PDF.
334
335                    meeting.Documents = this.getSupplementalDocuments(meeting);
336
337                },
338
339                getYear(value) {
340
341                    return new Date(value).getFullYear();
342
343                },
344
345                getMonth(value) {
346
347                    return new Date(value).getMonth() + 1;
348
349                },
350
351                niceDate(value) {
352
353                    return new Date(value).toLocaleDateString("en-US", {
354
355                        weekday: 'long',
356
357                        year: 'numeric',
358
359                        month: 'long',
360
361                        day: 'numeric',
362
363                        hour12: true,
364
365                        hour: 'numeric',
366
367                        minute: '2-digit'
368
369                    });
370
371                },
372
373                loadFields(response) {
374
375                    if (response == null || typeof response.data.error !== "undefined") {
376
377                        console.error('Error in response:', response?.data?.error);
378
379                        // The plugin no longer halts the page on a service
380                        // failure, so say something rather than render nothing.
381
382                        this.loadError = (response && response.data && response.data.message)
383
384                            || 'Meeting information is temporarily unavailable.';
385
386                        this.loading = false;
387
388                        return;
389
390                    }
391
392
393
394                    this.loadError = '';
395
396
397
398                    this.meetings = response.data;
399
400                    
401
402                    // Check if the expected data structure exists
403
404                    if (!response.data.MeetingGetByQueryResult) {
405
406                        console.error('Unexpected response structure:', response.data);
407
408                        this.loading = false;
409
410                        return;
411
412                    }
413
414
415
416                    // Must run before rebuildObject(): getMinutes() resolves
417                    // document names through this lookup.
418
419                    // Must read through this.meetings, not response.data: Vue
420                    // wraps the assigned object, so response.data.X and
421                    // this.meetings.X are different references and the memo in
422                    // dataIndex() would miss and rebuild the whole index.
423
424                    this.buildDocumentNames(this.meetings.MeetingGetByQueryResult);
425
426
427
428                    this.emeetings.past = response.data.MeetingGetByQueryResult.PastMeetings['Meeting.Record'] || [];
429
430                    this.emeetings.future = response.data.MeetingGetByQueryResult.FutureMeetings['Meeting.Record'] || [];
431
432					//console.log(this.emeetings.future);
433
434					this.emeetings.future.sort((a, b) => {
435
436						return new Date(a.Start) - new Date(b.Start);
437
438					});	
439
440									  
441
442					this.emeetings.future.sort((a, b) => {
443
444						return new Date(a.Start) - new Date(b.Start);
445
446					});
447
448				    //console.log(this.emeetings.future);			  
449
450                    // Process future meetings
451
452                    for (let i = 0; i < this.emeetings.future.length; i++) {
453
454                        this.rebuildObject(this.emeetings.future[i]);
455
456                    }
457
458
459
460                    // Sort past meetings by date (newest first)
461
462                    this.emeetings.past.sort((a, b) => {
463
464                        return new Date(b.Start) - new Date(a.Start);
465
466                    });
467
468
469
470                    // Process past meetings and collect years
471
472                    for (let i = 0; i < this.emeetings.past.length; i++) {
473
474                        const meeting = this.emeetings.past[i];
475
476                        this.rebuildObject(meeting);
477
478
479
480                        const year = this.getYear(meeting.Start);
481
482                        meeting.year = year;
483
484
485
486                        const month = this.getMonth(meeting.Start);
487
488                        meeting.month = month;
489
490
491
492                        if (!this.years.includes(year)) {
493
494                            this.years.push(year);
495
496                        }
497
498                    }
499
500
501
502                    // Sort years in descending order (latest to oldest)
503
504                    this.years.sort((a, b) => b - a);
505
506
507
508                    // Set selected year and month to most recent past meeting
509
510                    this.setMostRecentPastDate();
511
512
513
514                    this.loading = false;
515
516
517
518                    this.$nextTick(() => {
519
520                        this.initTooltips();
521
522                    });
523
524                    
525
526                    // Initialize carousel after data is loaded
527
528                    if (!this.loading) {
529
530                        setTimeout(() => {
531
532                            this.SetCarousel();
533
534                        }, 1000);
535
536                    }
537
538                },
539
540                loadPast() {
541
542                    this.$nextTick(() => {
543
544                        this.initTooltips();
545
546                        this.$nextTick(() => {
547
548                            // First dispose any existing tooltips in the modal
549
550                            var modalTooltips = [].slice.call(document.querySelectorAll(pastModalSel + ' [data-bs-toggle="tooltip"]'));
551
552                            modalTooltips.forEach(function (tooltipEl) {
553
554                                let tooltip = bootstrap.Tooltip.getInstance(tooltipEl);
555
556                                if (tooltip) {
557
558                                    tooltip.dispose();
559
560                                }
561
562                            });
563
564                        });
565
566
567
568                    });
569
570                },
571
572                setMostRecentPastDate() {
573
574                    // Get meetings filtered by meeting type only
575
576                    let meetings = this.emeetings.past;
577
578
579
580                    if (this.meetingTypeFilter && this.meetingTypeFilter.length > 0) {
581
582                        meetings = this.emeetings.past.filter(meeting => {
583
584                            const meetingTypeId = parseInt(meeting.MeetingTypeId);
585
586                            return this.meetingTypeFilter.includes(meetingTypeId);
587
588                        });
589
590                    }
591
592
593
594                    // If we have filtered meetings, set to the most recent one
595
596                    if (meetings.length > 0) {
597
598                        // Meetings are already sorted by date (most recent first)
599
600                        const mostRecent = meetings[0];
601
602                        this.selectedYear = mostRecent.year;
603
604                        this.selectedMonth = mostRecent.month;
605
606                    } else if (this.filteredYears.length > 0) {
607
608                        // No meetings found, but we have years - set to first available year
609
610                        this.selectedYear = this.filteredYears[0];
611
612                        // Set month to 1 (January) as a fallback
613
614                        this.selectedMonth = 1;
615
616                    }
617
618                },
619
620				SetCarousel() {
621
622					var multipleCardCarousel = document.querySelector(carouselSel);
623
624					if (!multipleCardCarousel) return;
625
626					
627
628					// First, remove any previous event handlers and carousel initialization
629
630					$(carouselSel + " .carousel-control-next, " + carouselSel + " .carousel-control-prev").off("click");
631
632					
633
634					try {
635
636					   // Destroy existing carousel if it exists
637
638					   var existingCarousel = bootstrap.Carousel.getInstance(multipleCardCarousel);
639
640					   if (existingCarousel) {
641
642						  existingCarousel.dispose();
643
644					   }
645
646					} catch(e) {
647
648					   // Ignore errors if carousel wasn't initialized
649
650					}
651
652					
653
654					// Handle the different view modes based on screen width
655
656					if (window.matchMedia("(max-width: 575px)").matches) {
657
658					   // Mobile view (1 card)
659
660					   
661
662					   // Reset any custom styles
663
664					   $(carouselSel + " .carousel-inner").css({
665
666						  "display": "block",
667
668						  "overflow": ""
669
670					   });
671
672					   $(carouselSel + " .carousel-inner").scrollLeft(0);
673
674					   
675
676					   // Make only first item active, remove active class from others
677
678					   $(carouselSel + " .carousel-item").removeClass("active");
679
680					   $(carouselSel + " .carousel-item:first").addClass("active");
681
682					   
683
684					   // Enable standard Bootstrap carousel with slide
685
686					   $(multipleCardCarousel).addClass("slide");
687
688					   
689
690					   // Initialize Bootstrap carousel
691
692					   var mobileCarousel = new bootstrap.Carousel(multipleCardCarousel, {
693
694						  interval: false
695
696					   });
697
698					   $(carouselSel + " .carousel-control-prev").on("click", function () {
699
700						  mobileCarousel.prev();
701
702					   });
703
704					   $(carouselSel + " .carousel-control-next").on("click", function () {
705
706						  mobileCarousel.next();
707
708					   });
709
710					} 
711
712					else {
713
714					   // Tablet/Desktop view (2 or 3 cards)
715
716					   $(multipleCardCarousel).removeClass("slide");
717
718					   
719
720					   // Make all items active for the multi-card display
721
722					   $(carouselSel + " .carousel-item").addClass("active");
723
724					   
725
726					   // Setup custom horizontal scrolling
727
728					   $(carouselSel + " .carousel-inner").css({
729
730						  "display": "flex",
731
732						  "overflow": "hidden"
733
734					   });
735
736					   
737
738					   var carouselWidth = $(carouselSel + " .carousel-inner")[0].scrollWidth;
739
740					   var cardWidth = $(carouselSel + " .carousel-item").first().width();
741
742					   
743
744					   var scrollPosition = 0;
745
746					   
747
748					   // Always scroll 1 card at a time for better control
749
750					   var scrollCards = 1;
751
752					   
753
754					   $(carouselSel + " .carousel-control-next").on("click", function () {
755
756						  if (scrollPosition < carouselWidth - cardWidth * 3) {
757
758							 scrollPosition += cardWidth * scrollCards;
759
760							 $(carouselSel + " .carousel-inner").animate(
761
762								{ scrollLeft: scrollPosition },
763
764								600
765
766							 );
767
768						  }
769
770					   });
771
772					   
773
774					   $(carouselSel + " .carousel-control-prev").on("click", function () {
775
776						  if (scrollPosition > 0) {
777
778							 scrollPosition -= cardWidth * scrollCards;
779
780							 $(carouselSel + " .carousel-inner").animate(
781
782								{ scrollLeft: scrollPosition },
783
784								600
785
786							 );
787
788						  }
789
790					   });
791
792					}
793
794					
795
796					// Update carousel when window is resized
797
798					const self = this; // Preserve 'this' context
799
800					// Only WIDTH changes matter here — the layout is chosen from
801					// width breakpoints. Re-running on height changes caused a
802					// feedback loop with any other widget that resizes with the
803					// page: relayout -> page height changes -> scrollbar toggles
804					// -> window resize -> relayout, forever.
805
806					lastCarouselWidth = window.innerWidth;
807
808					$(window).off('resize' + resizeNs).on('resize' + resizeNs, function() {
809
810					   if (window.innerWidth === lastCarouselWidth) {
811
812						  return;
813
814					   }
815
816					   lastCarouselWidth = window.innerWidth;
817
818					   // Wait a moment for the resize to complete before recalculating
819
820					   clearTimeout(carouselResizeTimer);
821
822					   carouselResizeTimer = setTimeout(function() {
823
824						  self.SetCarousel();
825
826					   }, 250);
827
828					});
829
830				 },
831
832
833
834                 performSearch(page = 1) {
835
836                    if (!this.searchText || this.searchText.trim() === '') {
837
838                        return;
839
840                    }
841
842
843
844                    if (page && typeof page === 'object' && page.target) {
845
846                        // Handle event object - try to extract page number if possible
847
848                        const targetValue = page.target.textContent || page.target.value;
849
850                        page = parseInt(targetValue, 10);
851
852                        if (isNaN(page)) {
853
854                            page = 1; // Default to page 1 if we couldn't extract a valid number
855
856                        }
857
858                    }                    
859
860                    
861
862                    this.searching = true;
863
864                    this.showSearchResults = false;
865
866                    this.currentSearchPage = page;
867
868  
869
870                    // Calculate start index for pagination
871
872                    const startIndex = (page - 1) * this.searchPageSize;
873
874                    
875
876                    // Create URL parameters
877
878                    const params = new URLSearchParams();
879
880                    params.append('action', 'eScribeMeetingsSearch');
881
882                    params.append('searchText', this.searchText);
883
884                    params.append('Page', startIndex);
885
886                    params.append('Length', this.searchPageSize);
887
888                    
889
890                    // Always include the validate parameter
891
892                    params.append('validate', eScribeajaxObj.validate);
893
894                    
895
896                    axios.post(eScribeajaxObj.ajax_url, params)
897
898                        .then(response => {
899
900                            this.searching = false;
901
902                            if (response.data && response.data.aaData) {
903
904                                // Store total counts for pagination
905
906                                this.totalSearchResults = response.data.iTotalRecords || 0;
907
908                                this.totalSearchPages = Math.ceil(this.totalSearchResults / this.searchPageSize);
909
910                                
911
912                                // Parse the search results
913
914                                this.searchResults = this.parseMeetingData(response.data);
915
916                                this.showSearchResults = true;
917
918                                
919
920                                // Open the modal to show results if not already open
921
922                                if (!$(searchModalSel).hasClass('show')) {
923
924                                    const searchModal = new bootstrap.Modal(document.getElementById(searchModalId));
925
926                                    searchModal.show();
927
928                                }
929
930                            } else {
931
932                                console.error('Unexpected search response format:', response.data);
933
934                                // Show a message to the user
935
936                                alert('No search results found or unexpected response format.');
937
938                            }
939
940                        })
941
942                        .catch(error => {
943
944                            console.error('Error performing search:', error);
945
946                            this.searching = false;
947
948                            // Surface the server's own message (rate limited,
949                            // service unavailable) rather than a generic one.
950
951                            const message = error.response && error.response.data && error.response.data.message;
952
953                            alert(message || 'Error performing search. Please try again later.');
954
955                        });
956
957                },
958
959
960
961                changePageSize() {
962
963                    // Reset to first page when changing page size
964
965                    this.currentSearchPage = 1;
966
967                    this.performSearch(1);
968
969                },
970
971
972
973                goToSearchPage(page) {
974
975                    if (page < 1 || page > this.totalSearchPages) {
976
977                        return;
978
979                    }
980
981                    this.performSearch(page);
982
983                },
984
985                
986
987                // Method to parse search results
988
989                parseMeetingData(jsonData) {
990
991                    const results = [];
992
993
994
995                    if (jsonData.aaData && Array.isArray(jsonData.aaData)) {
996
997                        for (const htmlString of jsonData.aaData) {
998
999                            // Each aaData entry is a *group* of documents that
1000                            // share a meeting, not a single document, so one
1001                            // entry can expand to several rows.
1002
1003                            results.push(...this.extractInfoFromHTML(htmlString));
1004
1005                        }
1006
1007                    }
1008
1009
1010
1011                    return results;
1012
1013                },
1014
1015
1016
1017                // Vue does not block javascript: URLs in :href bindings, and
1018                // these come from HTML the eSCRIBE portal returns. Only allow
1019                // links we would actually navigate to.
1020
1021                safeUrl(url) {
1022
1023                    if (!url) {
1024
1025                        return '#';
1026
1027                    }
1028
1029                    try {
1030
1031                        const parsed = new URL(url, window.location.origin);
1032
1033                        return (parsed.protocol === 'http:' || parsed.protocol === 'https:') ? parsed.href : '#';
1034
1035                    } catch (e) {
1036
1037                        return '#';
1038
1039                    }
1040
1041                },
1042
1043                // Method to extract information from HTML strings in search results
1044
1045                extractInfoFromHTML(htmlString) {
1046
1047                    const text = el => (el ? el.textContent.trim() : '');
1048
1049                    const doc = new DOMParser().parseFromString(htmlString, 'text/html');
1050
1051
1052
1053                    // Group header row: meeting type and meeting date.
1054
1055                    const groupRow = doc.querySelector('.srch-grp-row');
1056
1057                    const groupCells = groupRow ? groupRow.querySelectorAll('td') : [];
1058
1059                    const meetingType = text(groupCells[0]) || 'Unknown meeting type';
1060
1061                    const meetingDate = text(groupCells[1]) || 'Unknown date';
1062
1063
1064
1065                    const results = [];
1066
1067
1068
1069                    doc.querySelectorAll('.srch-item').forEach(item => {
1070
1071                        const link = item.querySelector('a');
1072
1073                        const row = item.closest('tr');
1074
1075                        const cells = row ? row.querySelectorAll('td') : [];
1076
1077
1078
1079                        results.push({
1080
1081                            // The title attribute carries the full document
1082                            // name; the link text is truncated for display.
1083
1084                            title: (link && link.getAttribute('title')) || text(link) || 'No title found',
1085
1086                            url: this.safeUrl(link && link.getAttribute('href')),
1087
1088                            docType: text(cells[cells.length - 1]) || 'Unknown document',
1089
1090                            meetingDate,
1091
1092                            meetingType
1093
1094                        });
1095
1096                    });
1097
1098
1099
1100                    return results;
1101
1102                },
1103
1104                clearSearch() {
1105
1106                    this.searchText = '';
1107
1108                    this.searchResults = [];
1109
1110                    this.showSearchResults = false;
1111
1112                    this.currentSearchPage = 1;
1113
1114                    this.totalSearchResults = 0;
1115
1116                    this.totalSearchPages = 1;
1117
1118                    this.clearBackdrops();
1119
1120                },
1121
1122                clearBackdrops() {
1123
1124                    // Keep one backdrop per modal that is still open and strip
1125                    // only the excess. Removing them all would leave a stacked
1126                    // modal (documents opened over past meetings) with no
1127                    // backdrop once the top one closes.
1128
1129                    const openModals = $('.modal.show').length;
1130
1131                    $('.modal-backdrop').slice(openModals).remove();
1132
1133
1134
1135                    // Remove modal-open class from body and restore scrolling
1136
1137                    if (!openModals) {
1138
1139                        $('body').removeClass('modal-open');
1140
1141                        $('body').css({
1142
1143                            'overflow': '',
1144
1145                            'padding-right': ''
1146
1147                        });
1148
1149                    }
1150
1151                },
1152
1153                setupModalEvents() {
1154
1155                    // Handle search modal hidden event (when modal is fully closed)
1156
1157                    $(searchModalSel).on('hidden.bs.modal', () => {
1158
1159                      // Remove any lingering backdrop elements
1160
1161                      this.clearBackdrops();
1162
1163                    });
1164
1165                    
1166
1167                    // Handle past meetings modal hidden event (when modal is fully closed)
1168
1169                    $(pastModalSel).on('hidden.bs.modal', () => {
1170
1171                      // Remove any lingering backdrop elements
1172
1173                      this.clearBackdrops();
1174
1175                    });
1176
1177                    
1178
1179                    // Backup cleanup for any remaining backdrops
1180
1181                    $(document).on('click', '.btn-close, [data-bs-dismiss="modal"]', () => {
1182
1183                      // Add a slight delay to ensure modal is in the process of closing
1184
1185                      setTimeout(() => {
1186
1187                        // Check if no modals are currently being shown
1188
1189                        if (!$('.modal.show').length) {
1190
1191                          this.clearBackdrops();
1192
1193                        }
1194
1195                      }, 300);
1196
1197                    });
1198
1199                },
1200
1201
1202
1203                ensureSearchVisibility() {
1204
1205                    // Force search component to be visible by updating DOM directly if needed
1206
1207                    const searchGroup = $('.escribe-search-input-group');
1208
1209                    if (searchGroup.length && !searchGroup.is(':visible')) {
1210
1211                        searchGroup.show();
1212
1213                    }
1214
1215                },
1216
1217                initTooltips() {
1218
1219                    this.$nextTick(() => {
1220
1221                        // Initialize all tooltips
1222
1223                        var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
1224
1225                        tooltipTriggerList.map(function (tooltipTriggerEl) {
1226
1227                            // Destroy existing tooltip if it exists
1228
1229                            let tooltip = bootstrap.Tooltip.getInstance(tooltipTriggerEl);
1230
1231                            if (tooltip) {
1232
1233                                tooltip.dispose();
1234
1235                            }
1236
1237                            
1238
1239                            // Create new tooltip
1240
1241                            return new bootstrap.Tooltip(tooltipTriggerEl, {
1242
1243                                trigger: 'hover focus', // Works for both hover and focus (tap on mobile)
1244
1245                                container: tooltipTriggerEl.closest('.modal') ? '.modal-content' : 'body'
1246
1247                            });
1248
1249                        });
1250
1251                    });
1252
1253                },
1254
1255                setupModalTooltipHandlers() {
1256
1257                    // Setup past meetings modal event handler
1258
1259                    $(pastModalSel).on('shown.bs.modal', () => {
1260
1261                        this.$nextTick(() => {
1262
1263                            // Initialize tooltips inside the modal
1264
1265                            var modalTooltips = [].slice.call(document.querySelectorAll(pastModalSel + ' [data-bs-toggle="tooltip"]'));
1266
1267                            modalTooltips.map(function (tooltipEl) {
1268
1269                                // Destroy existing tooltip if it exists
1270
1271                                let tooltip = bootstrap.Tooltip.getInstance(tooltipEl);
1272
1273                                if (tooltip) {
1274
1275                                    tooltip.dispose();
1276
1277                                }
1278
1279                                
1280
1281                                // Create new tooltip with modal-specific container
1282
1283                                return new bootstrap.Tooltip(tooltipEl, {
1284
1285                                    trigger: 'hover focus',
1286
1287                                    container: '.modal-content'
1288
1289                                });
1290
1291                            });
1292
1293                        });
1294
1295                    });
1296
1297                    
1298
1299                    // When modal is hidden, dispose of tooltips to prevent memory leaks
1300
1301                    $(pastModalSel).on('hidden.bs.modal', () => {
1302
1303                        var modalTooltips = [].slice.call(document.querySelectorAll(pastModalSel + ' [data-bs-toggle="tooltip"]'));
1304
1305                        modalTooltips.forEach(function (tooltipEl) {
1306
1307                            let tooltip = bootstrap.Tooltip.getInstance(tooltipEl);
1308
1309                            if (tooltip) {
1310
1311                                tooltip.dispose();
1312
1313                            }
1314
1315                        });
1316
1317                    });
1318
1319                },
1320
1321  
1322
1323            },
1324
1325            mounted() {
1326
1327                // Read meeting type filter from data attribute
1328
1329                if (mountEl && mountEl.dataset.meetingTypes) {
1330
1331                    try {
1332
1333                        this.meetingTypeFilter = JSON.parse(mountEl.dataset.meetingTypes);
1334
1335                        if (this.meetingTypeFilter.length > 0) {
1336
1337                            console.log('eScribe: Filtering by meeting types:', this.meetingTypeFilter);
1338
1339                        }
1340
1341                    } catch (e) {
1342
1343                        console.error('eScribe: Failed to parse meeting type filter', e);
1344
1345                        this.meetingTypeFilter = [];
1346
1347                    }
1348
1349                }
1350
1351
1352
1353                this.searchInitialized = true;
1354
1355
1356
1357                // Get the meeting data
1358
1359                this.GetData();
1360
1361
1362
1363                this.clearBackdrops();
1364
1365
1366
1367                // Setup proper modal event handling
1368
1369                this.setupModalEvents();
1370
1371
1372
1373                this.initTooltips();
1374
1375
1376
1377                this.setupModalTooltipHandlers();
1378
1379
1380
1381                setTimeout(() => {
1382
1383                    this.ensureSearchVisibility();
1384
1385                    this.$forceUpdate();
1386
1387                }, 500);
1388
1389
1390
1391
1392
1393                
1394
1395
1396
1397                // Handle modal z-index issues
1398
1399                $(pastModalSel).on('show.bs.modal', function() {
1400
1401                    setTimeout(function() {
1402
1403                        $('.modal-backdrop').css('z-index', '1040');
1404
1405                        $(pastModalSel).css('z-index', '1050');
1406
1407                        $(pastModalSel + ' .modal-dialog').css('z-index', '1051');
1408
1409                    }, 0);
1410
1411                });
1412
1413
1414
1415                // Additional fix for persistent modal issues
1416
1417                $(document).on('shown.bs.modal', pastModalSel, function() {
1418
1419                    $(pastModalSel).appendTo('body');
1420
1421                    $('.modal-backdrop').css('z-index', '1040');
1422
1423                    $(pastModalSel).css('z-index', '1050');
1424
1425                });
1426
1427
1428
1429                $(searchModalSel).on('show.bs.modal', function() {
1430
1431                    setTimeout(function() {
1432
1433                        $('.modal-backdrop').css('z-index', '1040');
1434
1435                        $(searchModalSel).css('z-index', '1050');
1436
1437                        $(searchModalSel + ' .modal-dialog').css('z-index', '1051');
1438
1439                    }, 0);
1440
1441                });
1442
1443
1444
1445                // The documents modal can be opened on top of the past meetings
1446                // modal, so it has to sit above both that modal and its
1447                // backdrop rather than at the shared 1040/1050 level.
1448
1449                $(docsModalSel).on('show.bs.modal', function() {
1450
1451                    $(docsModalSel).appendTo('body');
1452
1453                    setTimeout(function() {
1454
1455                        $('.modal-backdrop').last().css('z-index', '1060');
1456
1457                        $(docsModalSel).css('z-index', '1070');
1458
1459                        $(docsModalSel + ' .modal-dialog').css('z-index', '1071');
1460
1461                    }, 0);
1462
1463                });
1464
1465
1466
1467                $(docsModalSel).on('hidden.bs.modal', () => {
1468
1469                    this.clearBackdrops();
1470
1471                    // Bootstrap drops .modal-open when any modal closes; restore
1472                    // it if the past meetings modal is still underneath.
1473
1474                    if ($('.modal.show').length) {
1475
1476                        $('body').addClass('modal-open');
1477
1478                    }
1479
1480                });
1481
1482                
1483
1484                // Additional fix for persistent search modal issues
1485
1486                $(document).on('shown.bs.modal', searchModalSel, function() {
1487
1488                    $(searchModalSel).appendTo('body');
1489
1490                    $('.modal-backdrop').css('z-index', '1040');
1491
1492                    $(searchModalSel).css('z-index', '1050');
1493
1494                    $(searchModalSel + ' .modal-dialog').css('z-index', '1051');
1495
1496                });
1497
1498                
1499
1500                // Set up watchers for search functionality
1501
1502                this.$watch('searchResults', function(newVal) {
1503
1504                    if (newVal.length > 0 && this.showSearchResults) {
1505
1506                        // Open the search modal when there are results
1507
1508                        var searchModal = new bootstrap.Modal(document.getElementById(searchModalId));
1509
1510                        searchModal.show();
1511
1512                    }
1513
1514                });   
1515
1516                
1517
1518                this.originalClearSearch = this.clearSearch;
1519
1520                this.clearSearch = function() {
1521
1522                    this.originalClearSearch();
1523
1524                    this.clearBackdrops();
1525
1526                };
1527
1528            },
1529
1530            computed: {
1531
1532                visiblePageNumbers() {
1533
1534                    let pages = [];
1535
1536                    const totalPages = this.totalSearchPages;
1537
1538                    const currentPage = this.currentSearchPage;
1539
1540                    
1541
1542                    // For small number of pages, show all
1543
1544                    if (totalPages <= 7) {
1545
1546                        for (let i = 1; i <= totalPages; i++) {
1547
1548                            pages.push(i);
1549
1550                        }
1551
1552                    } else {
1553
1554                        // Always include first page
1555
1556                        pages.push(1);
1557
1558                        
1559
1560                        // For current pages near the beginning
1561
1562                        if (currentPage <= 4) {
1563
1564                            for (let i = 2; i <= 5; i++) {
1565
1566                                pages.push(i);
1567
1568                            }
1569
1570                            pages.push('...');
1571
1572                            pages.push(totalPages);
1573
1574                        } 
1575
1576                        // For current pages near the end
1577
1578                        else if (currentPage >= totalPages - 3) {
1579
1580                            pages.push('...');
1581
1582                            for (let i = totalPages - 4; i <= totalPages; i++) {
1583
1584                                pages.push(i);
1585
1586                            }
1587
1588                        } 
1589
1590                        // For current pages in the middle
1591
1592                        else {
1593
1594                            pages.push('...');
1595
1596                            for (let i = currentPage - 1; i <= currentPage + 1; i++) {
1597
1598                                pages.push(i);
1599
1600                            }
1601
1602                            pages.push('...');
1603
1604                            pages.push(totalPages);
1605
1606                        }
1607
1608                    }
1609
1610                    
1611
1612                    return pages;
1613
1614                },
1615
1616                filteredFutureMeetings() {
1617
1618                    if (!this.meetingTypeFilter || this.meetingTypeFilter.length === 0) {
1619
1620                        // No filter, return all
1621
1622                        return this.emeetings.future;
1623
1624                    }
1625
1626
1627
1628                    // Filter by meeting type
1629
1630                    return this.emeetings.future.filter(meeting => {
1631
1632                        const meetingTypeId = parseInt(meeting.MeetingTypeId);
1633
1634                        return this.meetingTypeFilter.includes(meetingTypeId);
1635
1636                    });
1637
1638                },
1639
1640                filteredPastMeetings() {
1641
1642                    // First filter by year/month selection
1643
1644                    let filteredByDate = this.emeetings.past;
1645
1646
1647
1648                    if (this.selectedMonth != null && this.selectedYear != null) {
1649
1650                        const selectedYearNum = parseInt(this.selectedYear);
1651
1652                        const selectedMonthNum = parseInt(this.selectedMonth);
1653
1654
1655
1656                        filteredByDate = this.emeetings.past.filter(meeting => {
1657
1658                            return meeting.year === selectedYearNum && meeting.month === selectedMonthNum;
1659
1660                        });
1661
1662                    }
1663
1664
1665
1666                    // Then apply meeting type filter if set
1667
1668                    if (!this.meetingTypeFilter || this.meetingTypeFilter.length === 0) {
1669
1670                        return filteredByDate;
1671
1672                    }
1673
1674
1675
1676                    return filteredByDate.filter(meeting => {
1677
1678                        const meetingTypeId = parseInt(meeting.MeetingTypeId);
1679
1680                        return this.meetingTypeFilter.includes(meetingTypeId);
1681
1682                    });
1683
1684                },
1685
1686                filteredYears() {
1687
1688                    // Get meetings filtered by meeting type only (not by date)
1689
1690                    let meetings = this.emeetings.past;
1691
1692
1693
1694                    if (this.meetingTypeFilter && this.meetingTypeFilter.length > 0) {
1695
1696                        meetings = this.emeetings.past.filter(meeting => {
1697
1698                            const meetingTypeId = parseInt(meeting.MeetingTypeId);
1699
1700                            return this.meetingTypeFilter.includes(meetingTypeId);
1701
1702                        });
1703
1704                    }
1705
1706
1707
1708                    // Extract unique years
1709
1710                    const yearsSet = new Set();
1711
1712                    meetings.forEa
1712ch(meeting => {
1713
1714                        if (meeting.year) {
1715
1716                            yearsSet.add(meeting.year);
1717
1718                        }
1719
1720                    });
1721
1722
1723
1724                    // Convert to array and sort descending
1725
1726                    return Array.from(yearsSet).sort((a, b) => b - a);
1727
1728                }
1729
1730            },
1731
1732            watch: {
1733
1734                meetingTypeFilter: {
1735
1736                    handler() {
1737
1738                        // When meeting type filter changes, update to most recent filtered date
1739
1740                        this.setMostRecentPastDate();
1741
1742                    },
1743
1744                    deep: true
1745
1746                }
1747
1748            }
1749
1750        });
1751
1752
1753
1754        // Mount the Vue app to this specific element
1755
1756        try {
1757
1758            app.mount(mountEl);
1759
1760        } catch (error) {
1761
1762            console.error('Error mounting eScribe Vue app:', error);
1763
1764        }
1765
1766    });
1767
1768});
1769

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.