PageSourceSearch

https://dillonyachtclub.com/

html dillonyachtclub.com collected 2026-10-06 11:15:10 UTC 1,176,744 bytes, 25,292 lines download raw bytes

1<!DOCTYPE html>
2<html lang="en">
3    
4    <head>
5    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
6    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
7    <!-- set up Sentry integration -->
8    
9    
9<script type="text/javascript">
10    (function(){
11        const page_version = "v-5de77e9587-20261005t210552";
12        const reload_key = 'reloaded_for_missing_asset_at';
13        //a failure this soon after a reload means the reload didn't help, later ones are a new mismatch
14        const repeat_window_ms = 30 * 1000;
15        let has_reported_repeat = false;
16        let is_leaving_page = false;
17
18        //resolves once the event is sent, so a reload right after can't drop it
19        function report_missing_asset(url, type) {
20            //offline the event can't send, so waiting on it would only delay the reload
21            if (typeof Sentry === 'undefined' || navigator.onLine === false) {
22                return Promise.resolve();
23            }
24            const asset_path = new URL(url, window.location.href).pathname;
25            // so we can track if a certain asset is problematic
26            const missing_asset = asset_path.replace('/public/dist/', '').replace(/\.[0-9a-f]{12}(\.\w+)$/, '$1');
27            const metric = {
28                attributes: {
29                    type: type,
30                    missing_asset: missing_asset,
31                    page_version: page_version,
32                },
33            };
34            Sentry.metrics.count('missing_asset', 1, metric);
35            //we already reloaded and it didn't recover the page
36            if (type === 'repeat') {
37                Sentry.captureMessage('Missing asset after reload', {
38                    level: 'error',
39                    fingerprint: ['missing-asset-after-reload'],
40                    tags: {
41                        missing_asset: missing_asset,
42                        page_version: page_version,
43                    },
44                    extra: {
45                        missing_asset_url: asset_path,
46                    },
47                });
48            }
49            return Sentry.flush(2000);
50        }
51
52        //only failed script and stylesheet loads of hashed files count
53        function return_missing_asset_url(event) {
54            const element = event.target;
55            if (!element || (element.tagName !== 'SCRIPT' && element.tagName !== 'LINK')) {
56                return null;
57            }
58            const url = element.src || element.href || '';
59            if (!url.includes('/public/dist/')) {
60                return null;
61            }
62            return url;
63        }
64
65        //without storage we can't tell a first failure from a repeat, so we don't reload at all
66        function remember_reload() {
67            try {
68                window.sessionStorage.setItem(reload_key, String(Date.now()));
69                return true;
70            } catch(error) {
71                return false;
72            }
73        }
74
75        function has_reloaded_recently() {
76            try {
77                const reloaded_at = Number(window.sessionStorage.getItem(reload_key));
78                return Date.now() - reloaded_at < repeat_window_ms;
79            } catch(error) {
80                return false;
81            }
82        }
83
84        //errors from loads the browser abandons on navigation aren't missing files
85        window.addEventListener('pagehide', function(){
86            is_leaving_page = true;
87        });
88
89        //html from one deploy can reach an instance of the next, which doesn't have this page's hashed files
90        // .. in which case we reload to force them onto the new instance
91        window.addEventListener('error', function(event){
92            if (window.is_reloading_for_missing_asset || is_leaving_page) {
93                return;
94            }
95            const url = return_missing_asset_url(event);
96            if (!url) {
97                return;
98            }
99            //at most one reload per window so we don't loop
100            if (!has_reloaded_recently()) {
101                if (remember_reload()) {
102                    window.is_reloading_for_missing_asset = true;
103                    report_missing_asset(url, 'reload').finally(() => {
104                        window.location.reload();
105                    });
106                }
107            } else if (!has_reported_repeat) {
108                has_reported_repeat = true;
109                report_missing_asset(url, 'repeat');
110            }
111        }, true);
112    })();
113</script>
113
114    
115    
115<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
115
116    <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
117    
117<script src="/public/assets/js/jquery-ui.min.js"></script>
117
118    <link href="/public/assets/css/jquery-ui.min.css" rel="stylesheet">
119    
119<script type="text/javascript" src="/parse-sdk-js"></script>
119
120    
120<script type="text/javascript" src="/public/dist/sc-common.628889f40a86.js"></script>
120
121    <!-- start vue -->
122    
123        <!-- prod minified vue js -->
124        
124<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.4.21/vue.global.prod.min.js"></script>
124
125    
126    
126<script src="/public/dist/catch-all/vue.99a908762ece.js"></script>
126
127    <!-- end vue -->
128    <!-- library for image orientation -->
129    
129<script src="/public/assets/js/load-image-all.min.js"></script>
129
130    <!-- moment.js -->
131    
131<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js"></script>
131
132    
132<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment-with-locales.min.js"></script>
132
133    
133<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.45/moment-timezone-with-data.min.js"></script>
133
134    <!-- library for charts -->
135    
135<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.js"></script>
135
136    <link rel="stylesheet" href="/public/assets/css/material-icons.css">
137    <!-- <link rel="stylesheet" href="https://kit.fontawesome.com/a59cae6e7b.css" crossorigin="anonymous"> -->
138    <link rel="stylesheet" href="/public/assets/css/font-awesome.css">
139    <!-- more js -->
140    
140<script src="/public/dist/js/cookies.184eea104904.js"></script>
140
141    
142
143    
144        <!-- web builder -->
145        
146<link href="https://fonts.googleapis.com/css2?family=Boogaloo&family=Bree+Serif&family=Carlito:ital,wght@0,400;0,700;1,400;1,700&family=Cinzel:wght@400;900&family=Cormorant+Garamond&family=Halant:wght@400;500;600;700&family=Inknut+Antiqua:wght@400;700&family=JetBrains+Mono:wght@400;500&family=Karla&family=Krona+One&family=Lato&family=Libre+Baskerville:ital,wght@0,400;0,700;1,400&family=Lora:wght@400;500&family=Montserrat:wght@400;500;600&family=Mrs+Saint+Delafield&family=Mukta:wght@400;700;800&family=Nunito+Sans&family=Open+Sans:ital,wght@0,300;0,800&family=Poller+One&family=PT+Sans:wght@400;700&family=Raleway:wght@400;500;700;800&family=Roboto:wght@400;500&family=Rubik:wght@400;500;700&family=Special+Gothic&family=Spectral+SC:wght@400;500;600;700&family=Spinnaker&family=Vollkorn&family=Zalando+Sans+Expanded:wght@400;500;700&display=swap" rel="stylesheet">
147
148        <!-- Juice browserified for inline css -->
149        
149<script src="/public/dist/browserified/juice-browserified.4ae70b219fc7.js"></script>
149
150        <!-- start calendar stuff (tui-calendar) -->
151        <link rel="stylesheet" type="text/css" href="https://uicdn.toast.com/tui-calendar/v1.15.0/tui-calendar.css" />
152        <link rel="stylesheet" type="text/css" href="https://uicdn.toast.com/tui.date-picker/latest/tui-date-picker.css" />
153        <link rel="stylesheet" type="text/css" href="https://uicdn.toast.com/tui.time-picker/latest/tui-time-picker.css" />
154        
154<script src="https://uicdn.toast.com/tui.code-snippet/v1.5.2/tui-code-snippet.min.js"></script>
154
155        
155<script src="https://uicdn.toast.com/tui-calendar/v1.15.0/tui-calendar.js"></script>
155
156        <!-- end calendar stuff -->
157        
157<script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>
157
158        
158<script src="/public/dist/js/browserify_src/bundle_browserify.078d255a7e2b.js"></script>
158
159        
159<script src="/public/dist/catch-all/common-web-builder.6885f73389b0.js"></script>
159
160        <!-- Cropper.js -->
161        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.min.css">
162        
162<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.6/cropper.min.js"></script>
162
163        <!-- unlayer (newer email builder, to replace tinymce) -->
164        
164<script src="https://editor.unlayer.com/embed.js"></script>
164
165        <!-- CodeMirror code editor (for custom-code component) -->
166        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
167        
167<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
167
168        
168<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/xml/xml.min.js"></script>
168
169        
169<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/javascript/javascript.min.js"></script>
169
170        
170<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/css/css.min.js"></script>
170
171        
171<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/htmlmixed/htmlmixed.min.js"></script>
171
172        
172<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/edit/closebrackets.min.js"></script>
172
173        
173<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/edit/closetag.min.js"></script>
173
174        
174<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/addon/edit/matchbrackets.min.js"></script>
174
175        <!-- end CodeMirror -->
176        
177    
178
179    
180    
181    <!-- weglot translations -->
182    
183         
184    
185
186    
187
188    
189    
190    <!-- userway.org accessibility snippet -->
191    
192         
193    
194
195
196    
197        
197<script src="/public/dist/catch-all/hosted-sites.7bbc5790960f.js"></script>
197
198        <link href="/public/dist/css/hosted.4b026a3f2a07.css" rel="stylesheet">
199        
199<script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>
199
200        
201        
201<script>
202            window.__cs_web_builder_default_animation = "";
203        </script>
203
204        <!-- custom styles -->
205        <style>
206
207        
208
209	
210
211	
212
213	
214	
215    
216
217	/* set base fonts */
218	
219	
220	
221	
222	
223	
224
225	/* set base colors */
226	
227	
228	
229    
230
231
232	/* now write the styles */
233
234	/* NOTE: the wb-content class needs to prepend all the styles so they are ONLY applied to hosted website content, not other content on the page - eg. for the website builder, it shouldn't impact the builder menu on the left */
235
236	.wb-content .cs-header p, 
237	.wb-content .cs-header div, 
238	.cs-footer-sublink-label,
239	.cs-header-sublink-label{
240		font-family: Montserrat !important;  
241	}
242
243	.wb-content h1{
244		font-size: 64px;
245		font-weight: normal;
246		font-family: Montserrat;
247	}
248
249    .custom-font-wrap h1{
250		font-family: Montserrat;
251	}
252
253	.wb-content h2 {
254		font-size: 42px;
255		font-weight: normal;
256		font-family: Montserrat;
257	}
258
259    .custom-font-wrap h2{
260		font-family: Montserrat;
261	}
262
263	.wb-content h3{
264		font-size: 28px;
265		font-weight: normal;
266		font-family: Montserrat;
267	}
268
269	.wb-content select{
270		font-size: 28px;
271		font-weight: normal;
272		font-family: Montserrat;
273	}
274
275    .custom-font-wrap h3{
276		font-family: Montserrat;
277	}
278
279	.wb-content p, 
280    .wb-content div{
281		font-size: 16px;
282		font-weight: normal;
283		font-family: Lato;
284	}
285
286    .custom-font-wrap p, 
287    .custom-font-wrap div{
288		font-family: Lato;
289	}
290
291	.wb-content button:not(.exclude-from-custom-button-styles),
292    .custom-font-wrap button:not(.exclude-from-custom-button-styles){
293		font-family: Montserrat;
294        font-weight: bold;
295        font-size: 14px;
296        text-transform: uppercase;
297        border-radius: 3px;
298        color: #fff;
299        background-color: #1b2556;
300        border: none !important;
301        transition: filter 0.15s ease-in-out;
302	}
303
304    .wb-content button:not(.exclude-from-custom-button-styles):hover,
305    .custom-font-wrap button:not(.exclude-from-custom-button-styles):hover{
306        filter: brightness(1.2);
307        background-color: #1b2556;
308        border: none !important;
309    }
310
311	/*-- tab bars --*/
312
313	.cs-tab-bar-option.active, 
314	.cs-tab-bar-option:hover
315	{
316	    color: #fff !important;
317	    background-color: #236651;
318	}
319
320    /* -- custom code for millbrook --*/
321    
322    /*-- end custom code for millbrook --*/
323
324	/*-- color schemes --*/
325
326	.wb-content .color_scheme_white .background, 
327	.wb-content .color_scheme_white.background
328	{
329		background-color: #fff;
330		color: #181b3e;
331	}
332
333	.wb-content .color_scheme_light .background, 
334	.wb-content .color_scheme_light.background
335	{
336		background-color: #90a2bc;
337		color: #181b3e;
338	}
339
340	.wb-content .color_scheme_dark .background, 
341	.wb-content .color_scheme_dark.background
342	{
343		background-color: #181b3e;
344		color: #fff;
345	}
346
347	.wb-content .color_scheme_black .background, 
348	.wb-content .color_scheme_black.background
349	{
350		background-color: #000;
351		color: #fff;
352	}
353
354	.wb-content .color_scheme_accent_light .background, 
355	.wb-content .color_scheme_accent_light.background
356	{
357		background-color: #fff;
358		color: #236651;
359	}
360
361	.wb-content .color_scheme_accent_dark .background, 
362	.wb-content .color_scheme_accent_dark.background
363	{
364		background-color: #236651;
365		color: #fff;
366	}
367
368	/*----- color schemes for header ------ */
369
370	/* Keep gradient channels tied to the same custom palette as the solid header. */
371	.cs-header.color_scheme_light{ --cs-header-background-rgb: 144, 162, 188 !important; }
372	.cs-header.color_scheme_dark{ --cs-header-background-rgb: 24, 27, 62 !important; }
373	.cs-header.color_scheme_accent_dark{ --cs-header-background-rgb: 35, 102, 81 !important; }
374	.cs-header.cs-header-background-color-white{ --cs-header-background-rgb: 255, 255, 255 !important; }
375	.cs-header.cs-header-background-color-light{ --cs-header-background-rgb: 144, 162, 188 !important; }
376	.cs-header.cs-header-background-color-dark{ --cs-header-background-rgb: 24, 27, 62 !important; }
377	.cs-header.cs-header-background-color-black{ --cs-header-background-rgb: 0, 0, 0 !important; }
378	.cs-header.cs-header-background-color-accent_light{ --cs-header-background-rgb: 255, 255, 255 !important; }
379	.cs-header.cs-header-background-color-accent_dark{ --cs-header-background-rgb: 35, 102, 81 !important; }
380
381	.color_scheme_white .cs-header-link-wrap > p, 
382	.color_scheme_white .cs-burgee-wrap > p{
383	  color: #181b3e !important;
384	}
385
386	.color_scheme_white .cs-header-link-wrap::after{
387	  background: #181b3e;
388	}
389
390	.color_scheme_light .cs-header-background{
391	  background-color: #90a2bc;
392	}
393
394	.color_scheme_light .cs-header-link-wrap > p, 
395	.color_scheme_light .cs-burgee-wrap > p{
396	  color: #181b3e !important;
397	}
398
399	.color_scheme_light .cs-header-link-wrap::after{
400	  background: #181b3e;
401	}
402
403	.color_scheme_dark .cs-header-background{
404	  background-color: #181b3e;
405	} 
406
407	.color_scheme_dark .cs-header-link-wrap > p, 
408	.color_scheme_dark .cs-burgee-wrap > p{
409	  color: #fff;
410	}
411
412	.color_scheme_dark .cs-header-link-wrap::after{
413	  background: #fff;
414	}
415
416	.color_scheme_black .cs-header-background{
417	  background-color: #000;
418	}
419
420	.color_scheme_black .cs-header-link-wrap > p, 
421	.color_scheme_black .cs-burgee-wrap > p{
422	  color: #fff;
423	}
424
425	.color_scheme_black .cs-header-link-wrap::after{
426	  background: #fff;
427	}
428
429	.color_scheme_accent_light .cs-header-background{
430	  background-color: #fff;
431	}
432
433	.color_scheme_accent_light .cs-header-link-wrap > p, 
434	.color_scheme_accent_light .cs-burgee-wrap > p{
435	  color: #236651;
436	}
437
438	.color_scheme_accent_light .cs-header-link-wrap::after{
439	  background: #236651;
440	}
441
442	.color_scheme_accent_dark .cs-header-background{
443	  background-color: #236651;
444	}
445
446	.color_scheme_accent_dark .cs-header-link-wrap > p, 
447	.color_scheme_accent_dark .cs-burgee-wrap > p{
448	  color: #fff;
449	}
450
451	.color_scheme_accent_dark .cs-header-link-wrap::after{
452	  background: #fff;
453	}
454
455	/* Animated sub-menu text follows the same accent color as the nav links. */
456	.cs-header-submenu-style_animated_buttons.color_scheme_accent_light .cs-header-sublink-label,
457	.cs-header-submenu-style_animated_flat.color_scheme_accent_light .cs-header-sublink-label{
458	  color: #236651;
459	}
460
461	.cs-header-submenu-style_animated_buttons.color_scheme_accent_light .cs-header-sublink-label:hover,
462	.cs-header-submenu-style_animated_flat.color_scheme_accent_light .cs-header-sublink-label:hover{
463	  color: #236651;
464	  opacity: .7;
465	}
466
467	/* Edge-fade / color-token overlays follow club palette. Do not set
468	   background-color on bare .cs-image-overlay — classic opacity-only
469	   layers must keep the hardcoded #141a21 fill from hosted.css. */
470	.cs-image-overlay.cs-overlay-color-light,
471	.cs-header-section-gradient.cs-overlay-color-light{
472	  --cs-overlay-rgb: 144, 162, 188;
473	}
474	.cs-image-overlay.cs-overlay-color-dark,
475	.cs-header-section-gradient.cs-overlay-color-dark{
476	  --cs-overlay-rgb: 24, 27, 62;
477	}
478	.cs-image-overlay.cs-overlay-color-top-light{ --cs-overlay-rgb-top: 144, 162, 188; }
479	.cs-image-overlay.cs-overlay-color-top-dark{ --cs-overlay-rgb-top: 24, 27, 62; }
480	.cs-image-overlay.cs-overlay-color-bottom-light{ --cs-overlay-rgb-bottom: 144, 162, 188; }
481	.cs-image-overlay.cs-overlay-color-bottom-dark{ --cs-overlay-rgb-bottom: 24, 27, 62; }
482	.cs-image-overlay.cs-overlay-top,
483	.cs-image-overlay.cs-overlay-bottom,
484	.cs-image-overlay.cs-overlay-both,
485	.cs-image-overlay.cs-overlay-gradient_top,
486	.cs-image-overlay.cs-overlay-gradient_bottom{
487	  background-color: transparent;
488	}
489	.cs-fade-color-top-light{
490	  --cs-fade-color-top: #90a2bc;
491	}
492	.cs-fade-color-top-dark{
493	  --cs-fade-color-top: #181b3e;
494	}
495	.cs-fade-color-bottom-light{
496	  --cs-fade-color-bottom: #90a2bc;
497	}
498	.cs-fade-color-bottom-dark{
499	  --cs-fade-color-bottom: #181b3e;
500	}
501
502	/* Overlap fade-through color follows club palette when set. */
503	.cs-overlap-through-light,
504	:not(.event-page-callout):has(+ .cs-overlap-through-light),
505	.componentPreview:has(+ .componentPreview .cs-overlap-through-light),
506	.wb-diff-section:has(+ .wb-diff-section .cs-overlap-through-light){
507	  --cs-overlap-through-rgb: 144, 162, 188;
508	}
509	.cs-overlap-through-dark,
510	:not(.event-page-callout):has(+ .cs-overlap-through-dark),
511	.componentPreview:has(+ .componentPreview .cs-overlap-through-dark),
512	.wb-diff-section:has(+ .wb-diff-section .cs-overlap-through-dark){
513	  --cs-overlap-through-rgb: 24, 27, 62;
514	}
515
516
517</style>        <!-- favicon -->
518        
519            <link rel="shortcut icon" href="https://d282wvk2qi4wzk.cloudfront.net/q4qrmUsrDP_burgee_1669240759761">
520            <link rel="icon" sizes="16x16 32x32 64x64" href="https://d282wvk2qi4wzk.cloudfront.net/q4qrmUsrDP_burgee_1669240759761">
521        
522        <!-- touch icon (mobile web) -->
523        
524        <!-- web app title (mobile safari) -->
525        
526            <meta name="apple-mobile-web-app-title" content="Dillon Yacht Club">
527        
528        <!-- app_manifest (android) -->
529        
530    
531
532    
533
534    
535    
536        <title>Home - Dillon Yacht Club</title>
537    
538    
539        <link rel="canonical" href="https://dillonyachtclub.com/">
540    
541    <!-- fb open graph tags -->
542    <meta property="fb:app_id" content="314788812688609" />
543    
544        <meta property="og:image" content="https://d282wvk2qi4wzk.cloudfront.net/q4qrmUsrDP_burgee_1677808999381">
545    
546    
547
548    <!-- Google Analytics for clubs -->
549     
550    <!-- END google analytics for clubs -->
551
552    <!-- FB pixel for clubs -->
553     
554    <!-- FB pixel for clubs -->
555
556    
557
558    <!-- vue date picker -->
559    <!-- see: https://vue3datepicker.com/ -->
560    
560<script src="/public/assets/js/vue-date-picker-8.5.1.js"></script>
560
561    <link rel="stylesheet" href="/public/assets/css/vue-date-picker.css">
562
563    <!-- Member portal splash video: warm the CDN connection and start fetching the splash
564         video during initial HTML parse, before Vue mounts the <video> element. This avoids
565         the multi-second gap (especially in the native app webview) where only the poster
566         image shows while the JS bundle loads, the club loads, and Vue renders. -->
567    
568
569</head>    
570    <body 
571        class='wb-content hosted-page' 
572        data-club-id='q4qrmUsrDP' 
573        data-club-name='Dillon Yacht Club' 
574        data-timezone='America/Denver'
575    >
576        
577
578    <div class="globalMessageInsert flexNoWrap easeFast hideForPrinting">
579        <div class="global-message-icon-loading">
580            <div class="global-message-icon-spinner"></div>
581        </div>
582        <p>
583            <i class="fa-solid fa-square-check global-message-icon-good"></i>
584            <i class="fa-solid fa-square-xmark global-message-icon-bad"></i>
585            <span></span>
586        </p>
587    </div>
588
589
590        <!-- start standard content -->
591        <div id="v-content" class="hidden relative leftText relative easeFast paddingTopHeader">
592            <!-- header -->
593            
594            <custom-page-header
595                v-if="! is_react_native_member_app"
596            ></custom-page-header>
597            
598            <!-- page components -->
599            
600            
601                
602                
603                    
604                        <!-- check the config -->
605
606
607
608<!-- clean them first, to weed out null values -->
609
610
611    
612
613    
614
615    
616
617    
618
619    
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655 
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682<!-- now generate markup -->
683
684    <hero-banner-component
685        :client_id="'dfeIDjSygNL_1670867171'"
686        :formatting="'{&#34;section_height&#34;:&#34;16&#34;,&#34;section_width&#34;:&#34;70&#34;,&#34;align_items&#34;:&#34;center&#34;,&#34;background_size&#34;:&#34;cover&#34;,&#34;overlay_opacity&#34;:&#34;0&#34;}'"
687        :class_string="' color_scheme_dark orientation-left number_of_columns-3 cs-first-page-section caption_style-show_on_hover'"
688        :style_string="'min-height:16vh;align-items:center;'"
689        :element_style_string="'max-width:70%;'"
690        :component_type="'hero-banner'"
691        :elements_string="'%5B%7B%22type%22%3A%22h1-center%22%2C%22content%22%3A%22%3Ch1%20style%3D%5C%22text-align%3A%20center%3B%5C%22%3EWelcome%20to%26nbsp%3B%3C%2Fh1%3E%3Ch1%20style%3D%5C%22text-align%3A%20center%3B%5C%22%3EDillon%20Yacht%20Club%3C%2Fh1%3E%22%7D%5D'"
692        :images_string="'[]'"
693    ></hero-banner-component>
694
695                        
696                    
697                
698                    
699                        <!-- check the config -->
700
701
702
703<!-- clean them first, to weed out null values -->
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740 
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767<!-- now generate markup -->
768	
769    <div class="cs-text background flexWrap relative  color_scheme_white orientation-left number_of_columns-3 caption_style-show_on_hover" style="min-height:50vh;align-items:center;">
770        
771            <div class="element-wrapper flexNoWrap flexWrapOnMobile" class="'element-wrapper_' + text" style="max-width:100%;">
772                
773                    <text-component
774                        :number_of_columns="'3'"	
775                        :column_prop="'%5B%7B%22type%22%3A%22h2-center%22%2C%22content%22%3A%22%3Ch3%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cspan%20class%3D%5C%22cs-bold%5C%22%3E%3Ca%20href%3D%5C%22%2Fmembership%5C%22%20target%3D%5C%22_blank%5C%22%20class%3D%5C%22cs-link-ZrL3uWvPxf1_1670900503%5C%22%3EMembership%3C%2Fa%3E%3C%2Fspan%3E%3Cbr%20%2F%3E%3C%2Fh3%3E%3Cdiv%3E%3Cbr%20%2F%3E%3C%2Fdiv%3E%3Cdiv%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cimg%20src%3D%5C%22https%3A%2F%2Fd282wvk2qi4wzk.cloudfront.net%2FbHrhhPQHkbya_upload_inline_q4qrmUsrDP_zddPOfQCSU_1778108316795%5C%22%20%2F%3E%3C%2Fdiv%3E%22%2C%22client_id%22%3A%22WWsa9qbHH6FShtO0%22%2C%22column_count%22%3A1%2C%22column_gap%22%3A1%7D%2C%7B%22type%22%3A%22h3-center%22%2C%22content%22%3A%22%3Cp%20style%3D%5C%22text-align%3Acenter%5C%22%3E%C2%A0%3C%2Fp%3E%3Cp%20style%3D%5C%22text-align%3Acenter%5C%22%3EPlaying%20on%20or%20by%20the%20water%E2%80%99s%20edge%20at%20Lake%20Dillon%20rejuvenates%20the%20spirit%2C%20restores%20the%20soul%20and%20rebuilds%20families%20and%20friendships.%20Come%20explore%20Lake%20Dillon%2C%20the%20beaches%20and%20secret%20spots%20that%20can%20only%20accessed%20by%20water%2521%C2%A0%C2
775%A0%3C%2Fp%3E%3Cp%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%22%2C%22client_id%22%3A%22xPktZ76GrawtGA32%22%2C%22column_count%22%3A1%2C%22column_gap%22%3A0%7D%5D'"
776                        :column_index="'0'"
777                        :section_element_animation="''"
778                    ></text-component>	
779                
780                    <text-component
781                        :number_of_columns="'3'"	
782                        :column_prop="'%5B%7B%22type%22%3A%22h2-center%22%2C%22content%22%3A%22%3Ch3%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cspan%20class%3D%5C%22cs-bold%5C%22%3E%3Ca%20href%3D%5C%22https%3A%2F%2Fdillonopen.com%2F%5C%22%20target%3D%5C%22_blank%5C%22%20class%3D%5C%22cs-link-KYe70RGTC9Q_1670900520%5C%22%3EDillon%20Open%20Regatta%3C%2Fa%3E%3C%2Fspan%3E%3C%2Fh3%3E%3Cdiv%3E%3Cbr%20%2F%3E%3C%2Fdiv%3E%3Cdiv%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cimg%20src%3D%5C%22https%3A%2F%2Fd282wvk2qi4wzk.cloudfront.net%2FuQQAcqOugvcI_upload_inline_q4qrmUsrDP_zddPOfQCSU_1778108338596%5C%22%20%2F%3E%3C%2Fdiv%3E%3Cdiv%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cbr%20%2F%3E%3C%2Fdiv%3E%22%2C%22client_id%22%3A%22F3G45nbwb49pFiKt%22%2C%22column_count%22%3A1%2C%22column_gap%22%3A0%7D%2C%7B%22type%22%3A%22h3-center%22%2C%22content%22%3A%22%3Cp%20style%3D%5C%22text-align%3Acenter%5C%22%3E%C2%A0The%20Dillon%20Yacht%20Club%20extends%20a%20warm%20invitation%20to%20all%20sailors%20interested%20in%20participating%20in%20our%20annual%20Dillon%20Open%20Regatta%2C%20the%20world%E2%80%99s%20highest%20regatta%20at%209%2C017%20ft%20above%20sea%20level.%20Racing%20will%20be%20held%20on%20beautiful%20Lake%20Dillon%20where%20competitors%20will%20enjoy%20sunny%20days%20and%20spectacular%20views%20of%20the%20snow-capped%20Rocky%20Mountains.%C2%A0%3C%2Fp%3E%3Cp%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%22%2C%22client_id%22%3A%22r2Y1vKojEmMozeyd%22%2C%22column_count%22%3A1%2C%22column_gap%22%3A0%7D%2C%7B%22type%22%3A%22text-left%22%2C%22content%22%3A%22%3Cp%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cimg%20class%3D%5C%22cs-inline-image%5C%22%20src%3D%5C%22https%3A%2F%2Fd282wvk2qi4wzk.cloudfront.net%2FJiGOroATJtKx_upload_inline_q4qrmUsrDP_zddPOfQCSU_1778108045862%5C%22%20%2F%3E%3C%2Fp%3E%22%2C%22client_id%22%3A%22d7sPiqgRhdgOYJk9%22%2C%22column_count%22%3A1%2C%22column_gap%22%3A0%7D%2C%7B%22type%22%3A%22text-left%22%2C%22content%22%3A%22%3Cp%20style%3D%5C%22text-align%3Acenter%5C%22%3EJoin%20the%20Dillon%20Yacht%20Club%20WhatsApp%20community%20today%20for%20your%20source%20of%20racing%20announcements%2C%20fleet%20chats%2C%20and%20all%20other%20sources%20of%20member%20communication.%3C%2Fp%3E%3Cp%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%3Cp%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cspan%20class%3D%5C%22cs-bold%5C%22%20style%3D%5C%22background-color%3Argb%2528254%2C%20251%2C%20251%2529%3Bcolor%3Argb%2528234%2C%2083%2C%2083%2529%5C%22%3ETo%20join%20the%20WhatsApp%20community%2C%20select%20the%20groups%20you%20wish%20to%20access.%20Then%20send%20a%20direct%20message%20on%20WhatsApp%20to%20Cooper%20Robinson%20at%20%2528303%2529265-1855.%20Cooper%20Robinson%20can%20then%20approve%20your%20group%20join%20requests.%20We%20appreciate%20your%20patience%20in%20the%20approval%20process%20to%20help%20restrict%20potential%20spam%20accounts%20from%20joining%20our%20communication%20channels.%3C%2Fspan%3E%3C%2Fp%3E%3Cp%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cspan%20class%3D%5C%22cs-bold%5C%22%20style%3D%5C%22background-color%3Argb%2528254%2C%20251%2C%20251%2529%3Bcolor%3Argb%2528234%2C%2083%2C%2083%2529%5C%22%3E%3Cbr%20%2F%3E%3C%2Fspan%3E%3C%2Fp%3E%3Cp%20style%3D%5C%22text-align%3Acenter%5C%22%3ELink%20to%20join%20the%20WhatsApp%20community%3A%3C%2Fp%3E%3Cp%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Ca%20href%3D%5C%22https%3A%2F%2Fchat.whatsapp.com%2FH0JDPgJOkGcD8qjox21JBP%5C%22%20target%3D%5C%22_blank%5C%22%20class%3D%5C%22cs-link-9ccSWYqHIpB_1778107381%5C%22%20style%3D%5C%22font-family%3ALato%3Bbackground-color%3Argb%2528255%2C%20255%2C%20255%2529%5C%22%3Ehttps%3A%2F%2Fchat.whatsapp.com%2FH0JDPgJOkGcD8qjox21JBP%3C%2Fa%3E%3C%2Fp%3E%3Cp%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%22%2C%22client_id%22%3A%22JkeKUslFTStYfYTW%22%2C%22column_count%22%3A1%2C%22column_gap%22%3A0%7D%5D'"
783                        :column_index="'1'"
784                        :section_element_animation="''"
785                    ></text-component>	
786                
787                    <text-component
788                        :number_of_columns="'3'"	
789                        :column_prop="'%5B%7B%22type%22%3A%22h2-center%22%2C%22content%22%3A%22%3Ch3%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cspan%20class%3D%5C%22cs-bold%5C%22%3E%3Ca%20href%3D%5C%22https%3A%2F%2Fwww.dillonjuniorsailing.com%2F%5C%22%20target%3D%5C%22_blank%5C%22%20class%3D%5C%22cs-link-alxfnImHVE7_1670900537%5C%22%3EJunior%2
7890Sailing%20Program%3C%2Fa%3E%3C%2Fspan%3E%3C%2Fh3%3E%3Cdiv%3E%3Cbr%20%2F%3E%3C%2Fdiv%3E%3Cdiv%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cimg%20src%3D%5C%22https%3A%2F%2Fwww.dillonjuniorsailing.com%2Fuploads%2F1%2F7%2F3%2F8%2F17381411%2Feditor%2Flogo-gold_2.png%3F1525407852%5C%22%20alt%3D%5C%22DILLON%20JUNIOR%20SAILING%20-%20Summer%20Sailing%20Programs%5C%22%20%2F%3E%3C%2Fdiv%3E%22%2C%22client_id%22%3A%22gUzkjTHNUNHbGnHG%22%2C%22column_count%22%3A1%2C%22column_gap%22%3A0%7D%2C%7B%22type%22%3A%22text-left%22%2C%22content%22%3A%22%3Cp%20style%3D%5C%22text-align%3Acenter%5C%22%3E%C2%A0DYCJSP%20has%20created%20a%20history%20of%20excellent%20summer%20sailing%20programs%20and%20is%20a%20great%20way%20to%20spend%20a%20Colorado%20summer%20outside%20learning%20a%20lifelong%20skill%2C%20sailing.%20We%20focus%20on%203%20things%3A%20safety%2C%20fun%2C%20%26amp%3B%20sailing%20skills.%20Our%20classes%20are%20for%20kids%20ages%208%20to%2018%20of%20all%20skill%20levels.%C2%A0%3C%2Fp%3E%3Cp%20style%3D%5C%22text-align%3Acenter%5C%22%3E%3Cbr%20%2F%3E%3C%2Fp%3E%22%2C%22client_id%22%3A%225lUvt5FQTpseymIX%22%2C%22column_count%22%3A1%2C%22column_gap%22%3A0%7D%5D'"
790                        :column_index="'2'"
791                        :section_element_animation="''"
792                    ></text-component>	
793                
794            </div>
795        
796    </div>
797
798                        
799                    
800                
801                    
802                        <!-- check the config -->
803
804
805
806<!-- clean them first, to weed out null values -->
807
808
809    
810
811    
812
813    
814
815    
816
817    
818
819    
820
821    
822
823    
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859 
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886<!-- now generate markup -->
887
888    <image-grid-component
889        :client_id="'ZMrYGt2sKCR_1670867478'"
890        :class_string="' color_scheme_white orientation-left number_of_columns-3 caption_style-show_on_hover'"
891        :image_grid_layout="'grid'"
892        :images_per_row="'4'"
893        :caption_size="'24'"
894        :subtext_size="'24'"
895        :link_label_size="'24'"
896        :text_position="'center'"
897        :caption_font_weight="'400'"
898        :image_grid_opacity="'40'"
899        :image_grid_border_radius="'0'"
900        :formatting="'{&#34;caption_style&#34;:&#34;show_on_hover&#34;,&#34;overlay_opacity&#34;:&#34;40&#34;,&#34;caption_align&#34;:&#34;center&#34;}'"
901        :images_string="'[{\&#34;id\&#34;:\&#34;Nq1LolhpIx0_1670867700\&#34;,\&#34;url\&#34;:\&#34;https://d282wvk2qi4wzk.cloudfront.net/ZMrYGt2sKCR_1670867478_grid_image_1670867698545\&#34;},{\&#34;id\&#34;:\&#34;PHBISSysB2U_1670867712\&#34;,\&#34;url\&#34;:\&#34;https://d282wvk2qi4wzk.cloudfront.net/1670867711163_2649\&#34;},{\&#34;id\&#34;:\&#34;M7lSVOwa9LJ_1671591695\&#34;,\&#34;url\&#34;:\&#34;https://d282wvk2qi4wzk.cloudfront.net/1671591694648_600\&#34;},{\&#34;id\&#34;:\&#34;ElXjM2crcq2_1671594131\&#34;,\&#34;url\&#34;:\&#34;https://d282wvk2qi4wzk.cloudfront.net/1671594130801_3347\&#34;},{\&#34;id\&#34;:\&#34;XyjfnUAHBhG_1671594560\&#34;,\&#34;url\&#34;:\&#34;https://d282wvk2qi4wzk.cloudfront.net/1671594560108_6717\&#34;},{\&#34;id\&#34;:\&#34;jK08KT2SJIQ_1671594720\&#34;,\&#34;url\&#34;:\&#34;https://d282wvk2qi4wzk.cloudfront.net/1671594719366_6551\&#34;},{\&#34;id\&#34;:\&#34;0XclhyCPMHm_1671596578\&#34;,\&#34;url\&#34;:\&#34;https://d282wvk2qi4wzk.cloudfront.net/1671596578388_2795\&#34;},{\&#34;id\&#34;:\&#34;sHT9R8dX7vX_1671596726\&#34;,\&#34;url\&#34;:\&#34;https://d282wvk2qi4wzk.cloudfront.net/1671596726047_4372\&#34;}]'"
902    ></image-grid-component>
903
904                        
905                    
906                
907            
908            <!-- footer -->
909            
910                
911
912<!-- check the config -->
913
914    
915<div class="cs-footer relative color_scheme_white" style="" >
916    <div class="">
917        <div class="cs-footer-flex-wrap-row">
918            <div class="cs-footer-logo-wrap background">
919                
920                    
921                    
922                    <div class="cs-burgee-wrap relative" style="height:70px;width:120px;">
923                        <div class="cs-burgee" style="background-image:url(https://d282wvk2qi4wzk.cloudfront.net/q4qrmUsrDP_burgee_1669240759761)"></div>
924                        <a class="absoluteA" href="/"></a>
925                    </div>
926                
927            </div>
928            <div>
929                <div class="flexWrapLeftAlign">
930                    <div class="cs-footer-link-wrap flexGrowOne noPaddingBottom">
931                        
932                            
933                            
934                            
935                                
936                                    <!-- component -->
937                                    <div class="footer-link-section flexGrowOne">
938                                        
939                                        
940                                            <p class="footer-sublink-section-header smallMarginTop">MAILING ADDRESS</p>
941                                        
942                                        <!-- handle sub-components -->
943                                        
944                                        
945                                            <div class="cs-footer-sublink-outer easeFast">
946                                                
947                                                    
948                                                    
949                                                    
950                                                        
951                                                            <div class="cs-footer-sublink-wrap relative">
952                                                                
953                                                                    
954                                                                    
955                                                                        <p class="cs-footer-sublink-label">PO Box 4308, Dillon, CO 80435</p>
956                                                                    
957                                                                
958                                                            </div>
959                                                        
960                                                    
961                                                
962                                            </div>
963                                        
964                                    </div>
965                                
966                            
967                        
968                            
969                            
970                            
971                                
972                                    <!-- component -->
973                                    <div class="footer-link-section flexGrowOne">
974                                        
975                                        
976                                            <p class="footer-sublink-section-header smallMarginTop">
977                                                FACEBOOK
978                                                <a class="absoluteA" href="https://www.facebook.com/dillonyachtclub/"></a>
979                                            </p>
980                                        
981                                        <!-- handle sub-components -->
982                                        
983                                        
984                                    </div>
985                                
986                            
987                        
988                        <!-- social icons -->
989                        
990                    </div>
991                </div> 
992                <div class="cs-footer-link-wrap noPaddingTop">
993                    <div class="powered_by_wrap relative">
994                        <div class="flexNoWrap align-items-center">
995                            <p class="powered_by"><i class="fa-solid fa-shield-check" style="padding-right:3px;"></i> Powered by</p>
996                            <p class="modern pointer powered_by_text relative" style="font-weight:bold;">Clubspot <a class="absoluteA" href="https://theclubspot.com"></a></p>
997                        </div>
998                        
999                        <p style="display:flex;font-family:roboto !important;" class="cs-footer-sublink-label roboto copyright_text noPointer">© 2026 Dillon Yacht Club, All rights reserved.</p>
1000                    </div> 
1001                </div>
1002            </div>
1003        </div>
1004    </div>
1005</div>            
1006
1007            <!-- react native tab bar -->
1008            <native-tab-bar-component></native-tab-bar-component>
1009
1010        </div>
1011        <div id="v-overlays" class="hidden">
1012            <!-- common -->
1013            <template v-if="overlays.show_push_notification">
1014                <show-push-notification></show-push-notification>
1015            </template>
1016            <template v-if="overlays.overflow_overlay">
1017                <overflow-overlay></overflow-overlay>
1018            </template>
1019
1020            <template v-if="overlays.view_all_events">
1021                <view-all-events-overlay></view-all-events-overlay>
1022            </template>
1023
1024            <template v-if="overlays.view_photo_carousel">
1025                <view-photo-carousel></view-photo-carousel>
1026            </template>
1027             <template v-if="overlays.show_event_details_in_member_cal">
1028                <show-event-details-in-member-cal-overlay></show-event-details-in-member-cal-overlay>
1029            </template>
1030
1031        </div>
1032        <div id="overlay_membership-directory-popup" class="standardOverlay scrollable hugePaddingBottom">
1033    <div class="standardOverlayCard centeredBlock eightyThenNinety overflowHidden relative">
1034        <div class="xButton" onclick="fadeOut('#overlay_membership-directory-popup');"></div>
1035        <div class="profile-background overflowHidden">
1036            <div class="profile-background-inner hundop" style="height:100%;"></div>
1037        </div>
1038        <div class="profile-picture color_scheme_accent_dark background capitalize" onclick="openDirectoryProfileImage(this)"></div>
1039        <!-- enlarged image overlay -->
1040        <div id="directory-image-lightbox" class="directory-image-lightbox" onclick="closeDirectoryProfileImage()">
1041            <div class="directory-image-lightbox-inner">
1042                <img class="directory-image-lightbox-img" src="" alt="Profile photo">
1043            </div>
1044        </div>
1045        <div class="paddingAllAround smallMarginBottom">
1046
1047            <p class="insert insert_name product-card-label"></p>
1048
1049            <div class="section-wrap tinyMarginTop tinyMarginBottom">
1050                <p class="flexGrowOne inline-value" data-db-key="street"></p>
1051                <p class="flexGrowOne inline-value" data-db-key="city"></p>
1052                <p class="flexGrowOne inline-value" data-db-key="state"></p>
1053                <p class="flexGrowOne inline-value" data-db-key="zip"></p>
1054            </div>
1055
1056            <div class="section-wrap tinyMarginTop tinyMarginBottom">
1057                <h4 class="smallBlockLetters leftText smallMarginTop">Contact</h4>
1058                <p class="flexGrowOne inline-value tinyPaddingTop" data-db-key="email"></p>
1059                <p class="flexGrowOne inline-value tinyPaddingTop" data-db-key="mobile"></p>
1060            </div>
1061
1062            <div class="section-wrap tinyMarginTop tinyMarginBottom">
1063                <h4 class="smallBlockLetters leftText smallMarginTop">Secondary address</h4>
1064                <p class="flexGrowOne inline-value dark tinyPaddingTop" data-db-key="street_secondary"></p>
1065                <p class="flexGrowOne inline-value dark" data-db-key="city_secondary"></p>
1066                <p class="flexGrowOne inline-value dark" data-db-key="state_secondary"></p>
1067                <p class="flexGrowOne inline-value dark" data-db-key="zip_secondary"></p>
1068            </div>
1069
1070            <div class="section-wrap section-wrap_custom_fields tinyMarginTop tinyMarginBottom">
1071                
1072            </div>
1073
1074            <!-- boat fields -->
1075            <div class="section-wrap tinyMarginTop tinyMarginBottom">
1076                <p class="flexGrowOne inline-value" data-db-key="boatName"></p>
1077                <p class="flexGrowOne inline-value" data-db-key="sailNumber" data-prepend="Sail #:"></p>
1078                <p class="flexGrowOne inline-value" data-db-key="hullNumber" data-prepend="Hull #:"></p>
1079                <p class="flexGrowOne inline-value" data-db-key="class" data-prepend="Class: "></p>
1080                <p class="flexGrowOne inline-value" data-db-key="registration" data-prepend="Registration #:"></p>
1081            </div>
1082
1083            <div class="section-wrap section-wrap_documents hidden tinyMarginTop tinyMarginBottom">
1084                <h4 class="smallBlockLetters leftText smallMarginTop">Documents</h4>
1085                <!-- documents inserted here -->
1086
1087            </div>
1088
1089            <div class="section-wrap section-wrap_family tinyMarginTop tinyMarginBottom">
1090                <h4 class="smallBlockLetters leftText smallMarginTop version_member">Family</h4>
1091                <h4 class="smallBlockLetters leftText smallMarginTop version_boat hidden">Related Members</h4>
1092                <!-- family members inserted here -->
1093
1094            </div>
1095
1096            <div class="section-wrap section-wrap_boats hidden tinyMarginTop tinyMarginBottom">
1097                <h4 class="smallBlockLetters leftText smallMarginTop version_member">Boats</h4>
1098                <h4 class="smallBlockLetters leftText smallMarginTop version_boat hidden">Related boats</h4>
1099                <!-- boats inserted here -->
1100
1101            </div>
1102
1103        </div>
1104    </div>
1105</div>        <div id="overlay_event-description-popup" class="standardOverlay scrollable hugePaddingBottom" style="display:none;opacity:0;">
1106    <div class="standardOverlayCard centeredBlock eightyThenNinety" style="max-width:850px;">
1107        <h3></h3>
1108        <div class="standardCardBody noPadding fff">
1109            <div class="centeredBlock eightyThenNinety bigPaddingBottom marginTop">
1110                <p class="inline-label smallerMarginBottom">Date</p>
1111                <p class="metric marginBottom leftText tinyMarginTop metric_start_date" style="margin-bottom:20px;"></p>
1112                <p class="inline-label smallerMarginBottom">Time</p>
1113                <p class="metric marginBottom leftText tinyMarginTop metric_start_date_time" style="margin-bottom:20px;"></p>
1114                <p class="inline-label smallerMarginBottom">Event Details</p>
1115                <div class="marginBottom leftText tinyMarginTop metric_description" style="line-height:1.5;"></div>
1116            </div>
1117        </div>
1118        <div class="standardCardFooter flexNoWrap">
1119            <div class="flexGrowOne"></div>
1120            <button class="cardFooterButton easeFast" onclick="fadeOut('#overlay_event-description-popup');">Done</button>
1121        </div>
1122    </div>
1123</div>
1124    </body>
1125    
1125<script>
1126
1127        //add the timezone to the global store
1128        store.timezone = $('body').data('timezone');
1129
1130        //add to global store
1131        store.eventBus = ref({
1132            listeners: {},
1133            $on(event, callback) {
1134                if (!this.listeners[event]) {
1135                this.listeners[event] = [];
1136                }
1137                this.listeners[event].push(callback);
1138            },
1139            $emit(event, ...args) {
1140                if (this.listeners[event]) {
1141                for (const callback of this.listeners[event]) {
1142                    callback(...args);
1143                }
1144                }
1145            },
1146            $off(event, callback) {
1147                if (!this.listeners[event]) {
1148                    return;
1149                }
1150                if (!callback) {
1151                    delete this.listeners[event];
1152                    return;
1153                }
1154                this.listeners[event] = this.listeners[event].filter(listener => listener !== callback);
1155            },
1156        });
1157
1158        //create the app
1159        var app_content = createApp({
1160            data() {
1161                return store
1162            },
1163            mounted() {
1164                console.log("mounted vue app: main content");
1165                $('#v-content').removeClass('hidden');
1166                var clubId = $('body').data('clubId');
1167                if(clubId) {
1168                    var query = new Parse.Query('clubs');
1169                    query.include('web_settings');
1170                    return query.get(clubId).then((foundClub) => {
1171                        store.clubObject = foundClub;
1172                    })
1173                }
1174            },
1175            methods: {
1176               
1177            },
1178            computed: {
1179                is_react_native_member_app(){
1180                    return store?.is_react_native_member_app || false;
1181                }
1182            }
1183        });
1184
1185        //create the app
1186        var app_overlays = createApp({
1187            data() {
1188                return store
1189            },
1190            mounted() {
1191                console.log("mounted vue app: overlays");
1192                $('#v-overlays').removeClass('hidden');
1193            }
1194        });
1195
1196    </script>
1196
1197
1198    <!-- define/register vue components  -->
1199    
1199<script type="text/html" id="template-custom-page-header">
1200    
1201
1202    <!-- globals -->
1203    
1204    
1205
1206    <!-- check the config -->
1207    
1208    
1209    
1210    
1211    
1212    
1213    
1214
1215    <!-- color scheme -->
1216    
1217    
1218    
1219    
1220
1221    <!-- opacity -->
1222    
1223    
1224
1225    <!-- font-size -->
1226    
1227    
1228    
1229
1230    <!-- font-weight -->
1231    
1232    
1233    
1234
1235    <!-- header style (eg stacked vs inline vs centered) -->
1236    
1237    
1238    
1239
1240    <!-- header position (eg relative vs absolute) -->
1241    
1242    
1243    
1244
1245    <!-- sub-menu style (eg simple vs animated slide-down) -->
1246    
1247
1248    
1249
1250    
1251    
1252
1253    <!-- header background image, eg. for inverness yc -->
1254    
1255
1256    <!-- burgee url -->
1257    
1258
1259    <!-- ok, start header markup -->
1260    <div
1261        ref="header"
1262        class="cs-header easeFast absolute-header color_scheme_white cs-font-size-16 header-400 header-style_stacked header-position_relative"
1263        style=""
1264        
1265        
1266    >
1267        
1268            <div class="cs-header-background background" style="opacity: 100%;"></div>
1269        
1270        <div class="innerHeader align-items-middle flexNoWrap">
1271            <div class="hamburgerWrap marketingMenu">
1272                <div style="position: fixed; height: 1px; width: 1px; opacity: .01;"></div>
1273                <div class="hamburger hideOnDesktop">
1274                    
1275                        <div id="headerLineTop" class="ease"></div>
1276                        <div id="headerLineMiddle" class="ease"></div>
1277                        <div id="headerLineBottom" class="ease"></div>
1278                    
1279                </div>
1280                
1281                    <p class="burger-menu-text hideOnDesktop hideOnMobile">Menu</p>
1282                
1283            </div>
1284
1285            
1286                
1287                
1288                
1289                    <div class="cs-burgee-wrap hideOnMobile" style="height:118px;width:129px;">
1290                        <div class="cs-burgee" style="background-image:url(https://d282wvk2qi4wzk.cloudfront.net/q4qrmUsrDP_burgee_1677808999381);background-size:contain;"></div>
1291                        <a href="/" class="absoluteA"></a>
1292                    </div>
1293                
1294                <div class="cs-burgee-wrap-mobile">
1295                    <div class="cs-burgee" style="background-image:url(https://d282wvk2qi4wzk.cloudfront.net/q4qrmUsrDP_burgee_1677808999381);background-size:contain;"></div>
1296                    <a :href="burgee_link" class="absoluteA"></a>
1297                </div>
1298            
1299
1300            
1301            <div class="cs-desktop-menu-wrap flexGrowOne flexWrapRightAlign hideOnMobile  header-style_stacked">
1302                
1303                
1304                    
1305                    
1306                    
1307                    
1308                    <div class="cs-header-link-wrap  header-style_stacked">
1309                                <p class="button_link_p" style="">HOME</p>
1310                                <!-- link -->
1311                                
1312                                
1313                                    <a class="absoluteA" href="/"></a>
1314                                
1315                                <!-- handle sub-components -->
1316                                
1317                                
1318                    </div>
1319                
1320                    
1321                    
1322                    
1323                    
1324                    <div class="cs-header-link-wrap  header-style_stacked">
1325                                <p class="button_link_p" style="">OUR CLUB</p>
1326                                <!-- link -->
1327                                
1328                                
1329                                <!-- handle sub-components -->
1330                                
1331                                
1332                                    <div class="cs-header-sublink-outer easeFast">
1333                                        
1334                                            
1335                                            
1336                                            
1337                                                
1338                                                
1339                                                    <div class="cs-header-sublink-wrap relative" style="--sm-index:0;">
1340                                                        <p class="cs-header-sublink-label">Club History</p>
1341                                                        
1342                                                        
1343                                                        
1344                                                            <a class="absoluteA" href="/club-history"></a>
1345                                                        
1346                                                    </div>
1347                                                
1348                                            
1349                                        
1350                                            
1351                                            
1352                                            
1353                                                
1354                                                
1355                                                    <div class="cs-header-sublink-wrap relative" style="--sm-index:1;">
1356                                                        <p class="cs-header-sublink-label">Our Board of Directors</p>
1357                                                        
1358                                                        
1359                                                        
1360                                                            <a class="absoluteA" href="/board-of-directors"></a>
1361                                                        
1362                                                    </div>
1363                                                
1364                                            
1365                                        
1366                                            
1367                                            
1368                                            
1369                                                
1370                                                
1371                                                    <div class="cs-header-sublink-wrap relative" style="--sm-index:2;">
1372                                                        <p class="cs-header-sublink-label">Commodores</p>
1373                                                        
1374                                                        
1375                                                        
1376                                                            <a class="absoluteA" href="/commodores"></a>
1377                                                        
1378                                                    </div>
1379                                                
1380                                            
1381                                        
1382                                            
1383                                            
1384                                            
1385                                                
1386                                                
1387                                                    <div class="cs-header-sublink-wrap relative" style="--sm-index:3;">
1388                                                        <p class="cs-header-sublink-label">Membership</p>
1389                                                        
1390                                                        
1391                                                        
1392                                                            <a class="absoluteA" href="/membership"></a>
1393                                                        
1394                                                    </div>
1395                                                
1396                                            
1397                                        
1398                                            
1399                                            
1400                                            
1401                                                
1402                                                
1403                                                    <div class="cs-header-sublink-wrap relative" style="--sm-index:4;">
1404                                                        <p class="cs-header-sublink-label">Photo Gallery</p>
1405                                                        
1406                                                        
1407                                                        
1408                                                            <a class="absoluteA" href="/photo-gallery"></a>
1409                                                        
1410                                                    </div>
1411                                                
1412                                            
1413                                        
1414                                            
1415                                            
1416                                            
1417                                                
1418                                                
1419                                            
1420                                        
1421                                            
1422                                            
1423                                            
1424                                                
1425                                                
1426                                                    <div class="cs-header-sublink-wrap relative" style="--sm-index:6;">
1427                                                        <p class="cs-header-sublink-label">Map of Lake Dillon</p>
1428                                                        
1429                                                        
1430                                                        
1431                                                            <a class="absoluteA" href="/map-of-lake-dillon"></a>
1432                                                        
1433                                                    </div>
1434                                                
1435                                            
1436                                        
1437                                            
1438                                            
1439                                            
1440                                                
1441                                                
1442                                            
1443                                        
1444                                    </div>
1445                                
1446                    </div>
1447                
1448                    
1449                    
1450                    
1451                    
1452                    <div class="cs-header-link-wrap  header-style_stacked">
1453                                <p class="button_link_p" style="">RACING</p>
1454                                <!-- link -->
1455                                
1456                                
1457                                <!-- handle sub-components -->
1458                                
1459                                
1460                                    <div class="cs-header-sublink-outer easeFast">
1461                                        
1462                                            
1463                                            
1464                                            
1465                                                
1466                                                
1467                                                    <div class="cs-header-sublink-wrap relative" style="--sm-index:0;">
1468                                                        <p class="cs-header-sublink-label">Fleets</p>
1469                                                        
1470                                                        
1471                                                        
1472                                                            <a class="absoluteA" href="/fleets"></a>
1473                                                        
1474                                                    </div>
1475                                                
1476                                            
1477                                        
1478                                            
1479                                            
1480                                            
1481                                                
1482                                                
1483                                                    <div class="cs-header-sublink-wrap relative" style="--sm-index:1;">
1484                                                        <p class="cs-header-sublink-label">Racing Events</p>
1485                                                        
1486                                                        
1487                                                        
1488                                                            <a class="absoluteA" href="/racing-events"></a>
1489                                                        
1490                                                    </div>
1491                                                
1492                                            
1493                                        
1494                                            
1495                                            
1496                                            
1497                                                
1498                                                
1499                                                    <div class="cs-header-sublink-wrap relative" style="--sm-index:2;">
1500                                                        <p class="cs-header-sublink-label">Dillon Open</p>
1501                                                        
1502                                                        
1503                                                        
1504                                                            <a class="absoluteA" href="https://dillonopen.com/"></a>
1505                                                        
1506                                                    </div>
1507                                                
1508                                            
1509                                        
1510                                            
1511                                            
1512                                            
1513                                                
1514                                                
1515                                                    <div class="cs-header-sublink-wrap relative" style="--sm-index:3;">
1516                                                        <p class="cs-header-sublink-label">Race Committee</p>
1517                                                        
1518                                                        
1519                                                        
1520                                                            <a class="absoluteA" href="/race-committee"></a>
1521                                                        
1522                                                    </div>
1523                                                
1524                                            
1525                                        
1526                                            
1527                                            
1528                                            
1529                                                
1530                                                
1531                                                    <div class="cs-header-sublink-wrap relative" style="--sm-index:4;">
1532                                                        <p class="cs-header-sublink-label">Crew Match</p>
1533                                                        
1534                                                        
1535                                                        
1536                                                            <a class="absoluteA" href="/crew-match"></a>
1537                                                        
1538                                                    </div>
1539                                                
1540                                            
1541                                        
1542                                            
1543                                            
1544                                            
1545                                                
1546                                                
1547                                    
1547                <div class="cs-header-sublink-wrap relative" style="--sm-index:5;">
1548                                                        <p class="cs-header-sublink-label">DYC Dillon Marina Slip Wait List Policy</p>
1549                                                        
1550                                                        
1551                                                        
1552                                                            <a class="absoluteA" href="/dyc-dillon-marina-slip-waitlist-policy-(public)"></a>
1553                                                        
1554                                                    </div>
1555                                                
1556                                            
1557                                        
1558                                            
1559                                            
1560                                            
1561                                                
1562                                                
1563                                            
1564                                        
1565                                    </div>
1566                                
1567                    </div>
1568                
1569                    
1570                    
1571                    
1572                    
1573                    <div class="cs-header-link-wrap  header-style_stacked">
1574                                <p class="button_link_p" style="">CALENDAR</p>
1575                                <!-- link -->
1576                                
1577                                
1578                                    <a class="absoluteA" href="/calendar"></a>
1579                                
1580                                <!-- handle sub-components -->
1581                                
1582                                
1583                    </div>
1584                
1585                    
1586                    
1587                    
1588                    
1589                    <div class="cs-header-link-wrap  header-style_stacked">
1590                                <p class="button_link_p" style="">JUNIOR SAILING</p>
1591                                <!-- link -->
1592                                
1593                                
1594                                    <a class="absoluteA" href="https://www.dillonjuniorsailing.com/"></a>
1595                                
1596                                <!-- handle sub-components -->
1597                                
1598                                
1599                    </div>
1600                
1601                    
1602                    
1603                    
1604                    
1605                    <div class="cs-header-link-wrap  header-style_stacked">
1606                                <p class="button_link_p" style="">CONTACT US</p>
1607                                <!-- link -->
1608                                
1609                                
1610                                    <a class="absoluteA" href="/contact-us"></a>
1611                                
1612                                <!-- handle sub-components -->
1613                                
1614                                
1615                    </div>
1616                
1617                    
1618                    
1619                    
1620                    
1621                    <div class="cs-header-link-wrap  link-style-button header-style_stacked">
1622                                <p class="button_link_p" style="background-color:#2856a1 !important; color:#ffffff !important;">MEMBERS</p>
1623                                <!-- link -->
1624                                
1625                                
1626                                    <a class="absoluteA" href="/account/billing"></a>
1627                                
1628                                <!-- handle sub-components -->
1629                                
1630                                
1631                                    <div class="cs-header-sublink-outer easeFast">
1632                                        
1633                                            
1634                                            
1635                                            
1636                                                
1637                                                
1638                                                    <div class="cs-header-sublink-wrap relative" style="--sm-index:0;">
1639                                                        <p class="cs-header-sublink-label">Directory</p>
1640                                                        
1641                                                        
1642                                                        
1643                                                            <a class="absoluteA" href="/directory"></a>
1644                                                        
1645                                                    </div>
1646                                                
1647                                            
1648                                        
1649                                            
1650                                            
1651                                            
1652                                                
1653                                                
1654                                                    <div class="cs-header-sublink-wrap relative" style="--sm-index:1;">
1655                                                        <p class="cs-header-sublink-label">DYC By Laws</p>
1656                                                        
1657                                                        
1658                                                        
1659                                                            <a class="absoluteA" href="/dyc-by-laws-2018"></a>
1660                                                        
1661                                                    </div>
1662                                                
1663                                            
1664                                        
1665                                            
1666                                            
1667                                            
1668                                                
1669                                                
1670                                                    <div class="cs-header-sublink-wrap relative" style="--sm-index:2;">
1671                                                        <p class="cs-header-sublink-label">DYC Wait List Policy</p>
1672                                                        
1673                                                        
1674                                                        
1675                                                            <a class="absoluteA" href="/dyc-dillon-marina-wait-list-and-policy"></a>
1676                                                        
1677                                                    </div>
1678                                                
1679                                            
1680                                        
1681                                    </div>
1682                                
1683                    </div>
1684                
1685                
1686                
1687            </div>
1688        </div>
1689    </div>
1690
1691    <!-- now the mobile menu -->
1692    <div class="mobileMenuOverlay hideOnDesktop ease"></div>
1693    <div class="mobileMenu hideOnDesktop easeFast scrollable">
1694        <div class="innerMenu">
1695            <div class="mobile-link-wrap">
1696
1697
1698                
1699                    <div class="cs-burgee-wrap" style="width:175px;height:60px;float:none;margin-top:-40px;margin-bottom:15px;">
1700                        <div class="cs-burgee" style="background-position:left;background-image:url(https://d282wvk2qi4wzk.cloudfront.net/q4qrmUsrDP_burgee_1677808999381);background-size:contain;margin-bottom:15px;"></div>
1701                        <a :href="burgee_link" class="absoluteA"></a>
1702                    </div>
1703                
1704
1705
1706                <div class="mobile-sublink-outer easeFast">
1707
1708                    
1709                        <div class="header-sublink-wrap relative">
1710                            <p class="header-sublink-label">My Account</p>
1711                            <p class="header-sublink-explain">Click below to view your account</p>
1712                            <button class="wb-button-primary easeFast boxShadowBottom smallMarginTop">
1713                                View my account
1714                                <a class="absoluteA" href="/account"></a>
1715                            </button>
1716                        </div>
1717                    
1718                    
1719                        
1720                        
1721                        
1722                            
1723                            
1724                                <!-- component -->
1725                                
1726                                
1727                                    <p class="header-sublink-section-header smallMarginTop">
1728                                        HOME
1729                                        <a class="absoluteA" href="/"></a>
1730                                    </p>
1731                                
1732                                <!-- handle sub-components -->
1733                                
1734                                
1735                            
1736                        
1737                    
1738                        
1739                        
1740                        
1741                            
1742                            
1743                                <!-- component -->
1744                                
1745                                
1746                                    <p class="header-sublink-section-header smallMarginTop">OUR CLUB</p>
1747                                
1748                                <!-- handle sub-components -->
1749                                
1750                                
1751                                    
1752                                        
1753                                        
1754                                        
1755                                            
1756                                            
1757                                                <div class="cs-header-sublink-wrap relative">
1758                                                    
1759                                                    
1760                                                        <p class="cs-header-sublink-label">
1761                                                            Club History
1762                                                            <a class="absoluteA" href="/club-history"></a>
1763                                                        </p>
1764                                                    
1765                                                </div>
1766                                            
1767                                        
1768                                    
1769                                        
1770                                        
1771                                        
1772                                            
1773                                            
1774                                                <div class="cs-header-sublink-wrap relative">
1775                                                    
1776                                                    
1777                                                        <p class="cs-header-sublink-label">
1778                                                            Our Board of Directors
1779                                                            <a class="absoluteA" href="/board-of-directors"></a>
1780                                                        </p>
1781                                                    
1782                                                </div>
1783                                            
1784                                        
1785                                    
1786                                        
1787                                        
1788                                        
1789                                            
1790                                            
1791                                                <div class="cs-header-sublink-wrap relative">
1792                                                    
1793                                                    
1794                                                        <p class="cs-header-sublink-label">
1795                                                            Commodores
1796                                                            <a class="absoluteA" href="/commodores"></a>
1797                                                        </p>
1798                                                    
1799                                                </div>
1800                                            
1801                                        
1802                                    
1803                                        
1804                                        
1805                                        
1806                                            
1807                                            
1808                                                <div class="cs-header-sublink-wrap relative">
1809                                                    
1810                                                    
1811                                                        <p class="cs-header-sublink-label">
1812                                                            Membership
1813                                                            <a class="absoluteA" href="/membership"></a>
1814                                                        </p>
1815                                                    
1816                                                </div>
1817                                            
1818                                        
1819                                    
1820                                        
1821                                        
1822                                        
1823                                            
1824                                            
1825                                                <div class="cs-header-sublink-wrap relative">
1826                                                    
1827                                                    
1828                                                        <p class="cs-header-sublink-label">
1829                                                            Photo Gallery
1830                                                            <a class="absoluteA" href="/photo-gallery"></a>
1831                                                        </p>
1832                                                    
1833                                                </div>
1834                                            
1835                                        
1836                                    
1837                                        
1838                                        
1839                                        
1840                                            
1841                                            
1842                                        
1843                                    
1844                                        
1845                                        
1846                                        
1847                                            
1848                                            
1849                                                <div class="cs-header-sublink-wrap relative">
1850                                                    
1851                                                    
1852                                                        <p class="cs-header-sublink-label">
1853                                                            Map of Lake Dillon
1854                                                            <a class="absoluteA" href="/map-of-lake-dillon"></a>
1855                                                        </p>
1856                                                    
1857                                                </div>
1858                                            
1859                                        
1860                                    
1861                                        
1862                                        
1863                                        
1864                                            
1865                                            
1866                                        
1867                                    
1868                                
1869                            
1870             
1870           
1871                    
1872                        
1873                        
1874                        
1875                            
1876                            
1877                                <!-- component -->
1878                                
1879                                
1880                                    <p class="header-sublink-section-header smallMarginTop">RACING</p>
1881                                
1882                                <!-- handle sub-components -->
1883                                
1884                                
1885                                    
1886                                        
1887                                        
1888                                        
1889                                            
1890                                            
1891                                                <div class="cs-header-sublink-wrap relative">
1892                                                    
1893                                                    
1894                                                        <p class="cs-header-sublink-label">
1895                                                            Fleets
1896                                                            <a class="absoluteA" href="/fleets"></a>
1897                                                        </p>
1898                                                    
1899                                                </div>
1900                                            
1901                                        
1902                                    
1903                                        
1904                                        
1905                                        
1906                                            
1907                                            
1908                                                <div class="cs-header-sublink-wrap relative">
1909                                                    
1910                                                    
1911                                                        <p class="cs-header-sublink-label">
1912                                                            Racing Events
1913                                                            <a class="absoluteA" href="/racing-events"></a>
1914                                                        </p>
1915                                                    
1916                                                </div>
1917                                            
1918                                        
1919                                    
1920                                        
1921                                        
1922                                        
1923                                            
1924                                            
1925                                                <div class="cs-header-sublink-wrap relative">
1926                                                    
1927                                                    
1928                                                        <p class="cs-header-sublink-label">
1929                                                            Dillon Open
1930                                                            <a class="absoluteA" href="https://dillonopen.com/"></a>
1931                                                        </p>
1932                                                    
1933                                                </div>
1934                                            
1935                                        
1936                                    
1937                                        
1938                                        
1939                                        
1940                                            
1941                                            
1942                                                <div class="cs-header-sublink-wrap relative">
1943                                                    
1944                                                    
1945                                                        <p class="cs-header-sublink-label">
1946                                                            Race Committee
1947                                                            <a class="absoluteA" href="/race-committee"></a>
1948                                                        </p>
1949                                                    
1950                                                </div>
1951                                            
1952                                        
1953                                    
1954                                        
1955                                        
1956                                        
1957                                            
1958                                            
1959                                                <div class="cs-header-sublink-wrap relative">
1960                                                    
1961                                                    
1962                                                        <p class="cs-header-sublink-label">
1963                                                            Crew Match
1964                                                            <a class="absoluteA" href="/crew-match"></a>
1965                                                        </p>
1966                                                    
1967                                                </div>
1968                                            
1969                                        
1970                                    
1971                                        
1972                                        
1973                                        
1974                                            
1975                                            
1976                                                <div class="cs-header-sublink-wrap relative">
1977                                                    
1978                                                    
1979                                                        <p class="cs-header-sublink-label">
1980                                                            DYC Dillon Marina Slip Wait List Policy
1981                                                            <a class="absoluteA" href="/dyc-dillon-marina-slip-waitlist-policy-(public)"></a>
1982                                                        </p>
1983                                                    
1984                                                </div>
1985                                            
1986                                        
1987                                    
1988                                        
1989                                        
1990                                        
1991                                            
1992                                            
1993                                        
1994                                    
1995                                
1996                            
1997                        
1998                    
1999                        
2000                        
2001                        
2002                            
2003                            
2004                                <!-- component -->
2005                                
2006                                
2007                                    <p class="header-sublink-section-header smallMarginTop">
2008                                        CALENDAR
2009                                        <a class="absoluteA" href="/calendar"></a>
2010                                    </p>
2011                                
2012                                <!-- handle sub-components -->
2013                                
2014                                
2015                            
2016                        
2017                    
2018                        
2019                        
2020                        
2021                            
2022                            
2023                                <!-- component -->
2024                                
2025                                
2026                                    <p class="header-sublink-section-header smallMarginTop">
2027                                        JUNIOR SAILING
2028                                        <a class="absoluteA" href="https://www.dillonjuniorsailing.com/"></a>
2029                                    </p>
2030                                
2031                                <!-- handle sub-components -->
2032                                
2033                                
2034                            
2035                        
2036                    
2037                        
2038                        
2039                        
2040                            
2041                            
2042                                <!-- component -->
2043                                
2044                                
2045                                    <p class="header-sublink-section-header smallMarginTop">
2046                                        CONTACT US
2047                                        <a class="absoluteA" href="/contact-us"></a>
2048                                    </p>
2049                                
2050                                <!-- handle sub-components -->
2051                                
2052                                
2053                            
2054                        
2055                    
2056                        
2057                        
2058                        
2059                            
2060                            
2061                                <!-- component -->
2062                                
2063                                
2064                                    <p class="header-sublink-section-header smallMarginTop">
2065                                        MEMBERS
2066                                        <a class="absoluteA" href="/account/billing"></a>
2067                                    </p>
2068                                
2069                                <!-- handle sub-components -->
2070                                
2071                                
2072                                    
2073                                        
2074                                        
2075                                        
2076                                            
2077                                            
2078                                                <div class="cs-header-sublink-wrap relative">
2079                                                    
2080                                                    
2081                                                        <p class="cs-header-sublink-label">
2082                                                            Directory
2083                                                            <a class="absoluteA" href="/directory"></a>
2084                                                        </p>
2085                                                    
2086                                                </div>
2087                                            
2088                                        
2089                                    
2090                                        
2091                                        
2092                                        
2093                                            
2094                                            
2095                                                <div class="cs-header-sublink-wrap relative">
2096                                                    
2097                                                    
2098                                                        <p class="cs-header-sublink-label">
2099                                                            DYC By Laws
2100                                                            <a class="absoluteA" href="/dyc-by-laws-2018"></a>
2101                                                        </p>
2102                                                    
2103                                                </div>
2104                                            
2105                                        
2106                                    
2107                                        
2108                                        
2109                                        
2110                                            
2111                                            
2112                                                <div class="cs-header-sublink-wrap relative">
2113                                                    
2114                                                    
2115                                                        <p class="cs-header-sublink-label">
2116                                                            DYC Wait List Policy
2117                                                            <a class="absoluteA" href="/dyc-dillon-marina-wait-list-and-policy"></a>
2118                                                        </p>
2119                                                    
2120                                                </div>
2121                                            
2122                                        
2123                                    
2124                                
2125                            
2126                        
2127                    
2128                </div>
2129            </div>
2130        </div>
2131    </div>
2132    
2133</script>
2133
2134
2135
2136
2137<script>
2138
2139    //define the component
2140    app_content.component("custom-page-header", {
2141        template: "#template-custom-page-header",
2142        data() {
2143            return {}
2144        }, 
2145        props: {},
2146        mounted(){
2147            this.sync_submenu_edge_alignment();
2148            window.addEventListener("resize", this.schedule_submenu_edge_alignment, { passive: true });
2149            if( !this.$refs.header ){
2150                return;
2151            }
2152            // Belt-and-suspenders: re-check right before a panel opens so a
2153            // stale measurement (from before nav content settled) can't leave
2154            // it hanging off-screen.
2155            this.$refs.header.addEventListener("mouseover", this.handle_submenu_hover_check);
2156            this.$refs.header.addEventListener("focusin", this.handle_submenu_hover_check);
2157            let sticky_header = this.$refs.header.classList.contains("cs-header-behavior_always_visible");
2158            if( !sticky_header ){
2159                return;
2160            }
2161            window.addEventListener("scroll", this.handle_header_scroll, { passive: true });
2162            this.$nextTick(() => {
2163                window.requestAnimationFrame(() => {
2164                    this.handle_header_scroll();
2165                });
2166            });
2167        },
2168        beforeUnmount(){
2169            window.removeEventListener("scroll", this.handle_header_scroll);
2170            window.removeEventListener("resize", this.schedule_submenu_edge_alignment);
2171            window.clearTimeout(this._submenu_edge_alignment_timeout);
2172            if( this.$refs.header ){
2173                this.$refs.header.removeEventListener("mouseover", this.handle_submenu_hover_check);
2174                this.$refs.header.removeEventListener("focusin", this.handle_submenu_hover_check);
2175            }
2176        },
2177        methods: {
2178            // Left-aligned animated sub-menus can run past the right edge of the
2179            // viewport for nav items near the end of the menu - flip those to
2180            // hang from the right instead so they stay on screen.
2181            schedule_submenu_edge_alignment(){
2182                window.clearTimeout(this._submenu_edge_alignment_timeout);
2183                this._submenu_edge_alignment_timeout = window.setTimeout(() => {
2184                    this.sync_submenu_edge_alignment();
2185                }, 150);
2186            },
2187            handle_submenu_hover_check(event){
2188                if( event.target.closest && event.target.closest(".cs-header-link-wrap") ){
2189                    this.sync_submenu_edge_alignment();
2190                }
2191            },
2192            sync_submenu_edge_alignment(){
2193                if( !this.$refs.header ){
2194                    return;
2195                }
2196                let is_animated = this.$refs.header.classList.contains("cs-header-submenu-style_animated_buttons")
2197                    || this.$refs.header.classList.contains("cs-header-submenu-style_animated_flat");
2198                if( !is_animated ){
2199                    return;
2200                }
2201                let panels = this.$refs.header.querySelectorAll(".cs-header-sublink-outer");
2202                let viewport_width = window.innerWidth || document.documentElement.clientWidth || 0;
2203                panels.forEach((panel) => {
2204                    panel.classList.remove("cs-header-sublink-outer_align-right");
2205                    if( panel.getBoundingClientRect().right > viewport_width - 8 ){
2206                        panel.classList.add("cs-header-sublink-outer_align-right");
2207                    }
2208                });
2209            },
2210            handle_header_scroll(){
2211                if( !this.$refs.header ){
2212                    return;
2213                }
2214                let scroll_y = window.scrollY || window.pageYOffset || 0;
2215                let header_rect = this.$refs.header.getBoundingClientRect();
2216                let first_component = document.querySelector("#v-content .cs-first-page-section")
2217                    || document.querySelector("#v-content .cs-custom-code-wrap.first-page-section");
2218                let first_component_rect = first_component
2219                    ? first_component.getBoundingClientRect()
2220                    : null;
2221                let viewport_height = window.innerHeight || document.documentElement.clientHeight || 0;
2222                let transition_mode = this.$refs.header.getAttribute("data-header-transition-mode")
2223                    || 'phased';
2224                let was_past_handoff = !!this._header_past_handoff;
2225                let past_full_handoff = !!(
2226                    first_component_rect
2227                    && first_component_rect.height > 0
2228                    && first_component_rect.bottom <= (
2229                        header_rect.bottom + ( was_past_handoff ? 8 : -8 )
2230                    )
2231                ) || (
2232                    viewport_height > 0
2233                    && scroll_y >= viewport_height + ( was_past_handoff ? -8 : 8 )
2234                );
2235                let immediate_threshold = Math.max(1, Math.round(header_rect.height / 2));
2236                let header_past_handoff = transition_mode === 'immediate'
2237                    ? scroll_y >= immediate_threshold
2238                    : scroll_y > 1 && past_full_handoff;
2239                this._header_past_handoff = header_past_handoff;
2240                let early_offset = Math.max(8, Math.round(header_rect.height * 0.2));
2241                let early_default_gradient = scroll_y >= early_offset
2242                    && !header_past_handoff
2243                    && transition_mode === 'phased'
2244                    && this.$refs.header.classList.contains("cs-header-background-on-scroll");
2245                this.apply_header_background_for_scroll(header_past_handoff);
2246                if( early_default_gradient ){
2247                    this.sync_header_background_rgb();
2248                }
2249                this.$refs.header.classList.remove("cs-header-following-section-gradient");
2250                this.$refs.header.classList.toggle(
2251                    "cs-header-early-default-gradient",
2252                    early_default_gradient
2253                );
2254            },
2255            apply_header_background_for_scroll(is_scrolled){
2256                if( !this.$refs.header ){
2257                    return;
2258                }
2259                let top_raw = this.$refs.header.getAttribute("data-header-bg-top");
2260                let scrolled_raw = this.$refs.header.getAttribute("data-header-bg-scrolled");
2261                if( !top_raw ){
2262                    return;
2263                }
2264                let top = {};
2265                let scrolled = {};
2266                try {
2267                    top = JSON.parse(top_raw) || {};
2268                    scrolled = JSON.parse(scrolled_raw || top_raw) || top;
2269                } catch (error) {
2270                    return;
2271                }
2272                let active = is_scrolled ? scrolled : top;
2273                let previous_text_color = this.$refs.header.className.match(/color_scheme_[^\s]+/);
2274                if( previous_text_color ){
2275                    this.$refs.header.classList.remove(previous_text_color[0]);
2276                }
2277                this.$refs.header.classList.add(
2278                    "color_scheme_" + ( active.color || 'white' )
2279                );
2280                let previous_mode = this.$refs.header.className.match(/cs-header-background-mode-[^\s]+/);
2281                if( previous_mode ){
2282                    this.$refs.header.classList.remove(previous_mode[0]);
2283                }
2284                let previous_blur = this.$refs.header.className.match(/cs-header-blur-(light|medium|strong)/);
2285                if( previous_blur ){
2286                    this.$refs.header.classList.remove(previous_blur[0]);
2287                }
2288                let previous_color = this.$refs.header.className.match(/cs-header-background-color-[^\s]+/);
2289                if( previous_color ){
2290                    this.$refs.header.classList.remove(previous_color[0]);
2291                }
2292                this.$refs.header.classList.add("cs-header-background-mode-" + ( active.mode || 'none' ));
2293                this.$refs.header.classList.add(
2294                    "cs-header-background-color-" + ( active.color || 'white' )
2295                );
2296                if( active.mode === 'blur_tint' ){
2297                    this.$refs.header.classList.add("cs-header-blur-" + ( active.blur_amount || 'medium' ));
2298                }
2299                if( active.opacity !== undefined ){
2300                    this.$refs.header.style.setProperty(
2301                        '--cs-header-background-opacity',
2302                        String(active.opacity / 100)
2303                    );
2304                }
2305                this.$refs.header.classList.toggle(
2306                    "cs-header-top-visible",
2307                    !is_scrolled && active.mode !== 'none'
2308                );
2309                this.$refs.header.classList.toggle(
2310                    "cs-header-blur-active",
2311                    is_scrolled && active.mode !== 'none'
2312                );
2313                let header_visible = !( is_scrolled && active.mode === 'none' );
2314                this.$refs.header.style.opacity = header_visible ? '' : '0';
2315                this.$refs.header.style.visibility = header_visible ? '' : 'hidden';
2316                this.$refs.header.style.pointerEvents = header_visible ? '' : 'none';
2317            },
2318            // Sample the real sticky fill (incl. club custom CSS) so the interim
2319            // gradient never falls back to overlay navy / a mismatched default.
2320            sync_header_background_rgb(){
2321                if( !this.$refs.header ){
2322                    return;
2323                }
2324                let scrolled_raw = this.$refs.header.getAttribute("data-header-bg-scrolled");
2325                if( scrolled_raw ){
2326                    try {
2327                        let scrolled = JSON.parse(scrolled_raw) || {};
2328                        let previous_color = this.$refs.header.className.match(/cs-header-background-color-[^\s]+/);
2329                        if( previous_color ){
2330                            this.$refs.header.classList.remove(previous_color[0]);
2331                        }
2332                        this.$refs.header.classList.add(
2333                            "cs-header-background-color-" + ( scrolled.color || 'white' )
2334                        );
2335                        if( scrolled.opacity !== undefined ){
2336                            this.$refs.header.style.setProperty(
2337                                '--cs-header-background-opacity',
2338                                String(scrolled.opacity / 100)
2339                            );
2340                        }
2341                    } catch (error) {
2342                        return;
2343                    }
2344                }
2345                let bg = this.$refs.header.querySelector('.cs-header-background');
2346                if( !bg ){
2347                    return;
2348                }
2349                let color = window.getComputedStyle(bg).backgroundColor || '';
2350                let match = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([0-9.]+))?/.exec(color);
2351                if( !match ){
2352                    return;
2353                }
2354                let alpha = match[4] === undefined ? 1 : Number(match[4]);
2355                if( !( alpha > 0 ) ){
2356                    return;
2357                }
2358                this.$refs.header.style.setProperty(
2359                    '--cs-header-background-rgb',
2360                    match[1] + ', ' + match[2] + ', ' + match[3]
2361                );
2362            },
2363            sync_section_gradient_height(section_height_px){
2364                if( !this.$refs.section_gradient || !this.$refs.header ){
2365                    return;
2366                }
2367                let transfer_raw = this.$refs.header.getAttribute("data-header-top-fade-transfer");
2368                if( !transfer_raw ){
2369                    return;
2370                }
2371                let transfer = null;
2372                try {
2373                    transfer = JSON.parse(transfer_raw);
2374                } catch (error) {
2375                    return;
2376                }
2377                if( !transfer || transfer.kind === 'legacy_fade' ){
2378                    return;
2379                }
2380                if( !section_height_px || section_height_px <= 0 ){
2381                    return;
2382                }
2383                this.$refs.section_gradient.style.height =
2384                    ( section_height_px * ( transfer.height_percent / 100 ) ) + "px";
2385            },
2386        },
2387        computed: {
2388            burgee_link(){
2389                //starting june 18 2026 we don't show this custom page header component in the member app at all..
2390                /*
2391                if( this.is_react_native_member_app ){
2392                    return "/account"
2393                }
2394                */
2395                return "/"
2396            },
2397            current_user(){
2398                return Parse.User.current()
2399            },
2400        },
2401    });
2402
2403
2404</script>
2404    
2404<script type="text/html" id="template-image-grid">
2405    <template v-if="!image_grid_layout || image_grid_layout !== 'carousel'">
2406        <div 
2407            class="image-grid-wrap centeredBlock"
2408            :class="'image-grid-wrap_' + client_id + ' images_per_row_' + images_per_row + ' ' + class_string"
2409        >
2410            <!-- draggable start for image-grid-image -->
2411            <div 
2412                v-if="!show_first_row_skeleton && images?.length > 0" 
2413                class="image-grid-insert flexWrapLeftAlign" 
2414                :class=" is_admin ? 'draggable-container_image_grid_' + component.id : ''  "
2415            >
2416                <template v-for="(image, index) in images" :key="image?.id">
2417                    <div 
2418                        v-if="image && image.url" 
2419                        class="image-grid-image relative" 
2420                        :class="'image-grid-image_' + image?.id"
2421                        :id="image?.id"
2422                    >
2423                        <!-- video -->
2424                        <div 
2425                            v-if="image.is_video" 
2426                            class="image-grid-image-inner" 
2427                            :class="{ 'image-grid-fade-media': !is_admin, 'is-loaded': image_has_loaded(image) }"
2428                            style="align-items:center;display:flex;" 
2429                            :style="{'border-radius': image_grid_border_radius + 'px' }"
2430                        >
2431                            <video class="cs-background-video" autoplay playsinline loop muted :src="image.url" style="display:block;"></video>
2432                            <div v-if="is_admin" class="image-grid-drag-handle-wrap draggable-handle">
2433                                <i class="fa-regular fa-grip-dots image-grid-drag-handle"></i>
2434                            </div>
2435                        </div>
2436                        <!-- image -->
2437                        <div 
2438                            v-else
2439                            class="image-grid-image-inner" 
2440                            :class="{ 'image-grid-fade-media': !is_admin, 'is-loaded': image_has_loaded(image) }"
2441                            :style="{'background-image': 'url(' + image.url + ')', 'border-radius': image_grid_border_radius + 'px' }"
2442                        >
2443                            <div v-if="is_admin" class="image-grid-drag-handle-wrap draggable-handle">
2444                                <i class="fa-regular fa-grip-dots image-grid-drag-handle"></i>
2445                            </div>
2446                        </div>
2447                        <!-- overlay -->
2448                        <div v-if="is_admin" class="image-grid-image-overlay flexWrap easeFast" :style="{ 'background-color': returnRGBAStringForImageGridImageOverlayOpacity( image_grid_opacity ), 'border-radius': image_grid_border_radius + 'px' }" @click.stop="change_image(image)">
2449                            <div class="image-toolbar-wrapper flexNoWrap centeredBlock" @click.stop>
2450                                <div 
2451                                    class="image-toolbar-option easeFast" 
2452                                    data-option="change" 
2453                                    style="border-right:1px solid #fff;" 
2454                                    @click.stop="change_image(image)"
2455                                >Change</div>
2456                                <div 
2457                                    class="image-toolbar-option easeFast" 
2458                                    data-option="update" 
2459                                    style="border-right:1px solid #fff;" 
2460                                    @click.stop="prepThenDisplayImageSettings(image)"
2461                                >Update</div>
2462                                <div 
2463                                    class="image-toolbar-option easeFast" 
2464                                    data-option="remove" 
2465                                    @click.stop="tapped_remove_image(image)"
2466                                ><i class="fa-regular fa-trash" style="font-size:14px;"></i></div>
2467                            </div>
2468                            <div class="image-text-wrapper" :style="{ alignItems: text_position === 'start' ? 'flex-start' : text_position === 'center' ? 'center' : 'flex-end' }">
2469                                <h2 
2470                                    v-if="image.caption"
2471                                    class="image-grid-image-caption"
2472                                    :style="{ 'font-size': caption_size + 'px', 'font-weight': caption_font_weight }"
2473                                >{{ image.caption }}</h2>
2474                                <p
2475                                    v-if="image.subtext"
2476                                    class="image-grid-image-caption marginTopFour"
2477                                    :style="{ 'font-size': subtext_size + 'px', 'font-weight': caption_font_weight }"
2478                                >{{ image.subtext }}</p>
2479                                <p
2480                                    v-if="image.link_label"
2481                                    class="image-grid-image-caption marginTopFour"
2482                                    style="text-decoration: underline;"
2483                                    :style="{ 'font-size': link_label_size + 'px', 'font-weight': caption_font_weight }"
2484                                >{{ image.link_label }}</p>
2485                            </div>
2486                        </div> 
2487                        <div v-if="!is_admin" class="image-grid-image-overlay flexWrap easeFast" :style="{ 'background-color': returnRGBAStringForImageGridImageOverlayOpacity( image_grid_opacity ), 'border-radius': image_grid_border_radius + 'px' }">
2488                            <div class="image-text-wrapper" :style="{ alignItems: text_position === 'start' ? 'flex-start' : text_position === 'center' ? 'center' : 'flex-end' }">
2489                                <h2 
2490                                    v-if="image.caption"
2491                                    class="image-grid-image-caption"
2492                                    :style="{ 'font-size': caption_size + 'px', 'font-weight': caption_font_weight }"
2493                                >{{ image.caption }}</h2>
2494                                <p
2495                                    v-if="image.subtext"
2496                                    class="image-grid-image-caption marginTopFour"
2497                                    :style="{ 'font-size': subtext_size + 'px', 'font-weight': caption_font_weight }"
2498                                >{{ image.subtext }}</p>
2499                                <p
2500                                    v-if="image.link_label"
2501                                    class="image-grid-image-caption marginTopFour"
2502                                    style="text-decoration: underline;"
2503                                    :style="{ 'font-size': link_label_size + 'px', 'font-weight': caption_font_weight }"
2504                                >{{ image.link_label }}</p>
2505                                <a
2506                                    v-if="return_link_href(image.link_to)"
2507                                    class="absoluteA"
2508                                    :href="return_link_href(image.link_to)"
2509                                    rel="noopener noreferrer"
2510                                ></a>
2511                            </div>
2512                        </div> 
2513                    </div>
2514                    <div
2515                        v-else-if="is_admin && image && image.placeholder"
2516                        class="image-grid-image relative cs-image-slot-placeholder-host"
2517                        :class="'image-grid-image_' + image?.id"
2518                        :id="image?.id"
2519                        @click.stop="fill_image_placeholder(image)"
2520                    >
2521                        <div class="image-grid-image-inner cs-image-slot-placeholder" :style="{ 'border-radius': image_grid_border_radius + 'px' }">
2522                            <span class="cs-image-slot-placeholder-icon"><i class="fa-regular fa-image"></i></span>
2523                            <span class="cs-image-slot-placeholder-label">Add {{ image.label || 'image' }}</span>
2524                        </div>
2525                    </div>
2526                </template>
2527                <!-- upload image **for admin -->
2528                <template v-if="is_admin">
2529                    <div class="image-grid-image image-grid-image_upload non-draggable">
2530                        <div class="image-grid-image-inner">
2531                            <div class="uploadImageZone relative pointer">
2532                                <input 
2533                                    @change="imageUploadChange($event)"
2534                                    class="image-grid-upload-input absoluteInput" 
2535                                    type="file" accept="image/*,video/*"
2536                                >
2537                                <p class="centeredBlock" style="font-family:roboto !important;">+ Upload image/video</p>
2538                                <div class="uploadImageOverlay">
2539                                    <div class="uploadImageSpinner centeredBlock"></div>
2540                                </div>
2541                            </div>
2542                        </div>
2543                    </div>
2544                </template>
2545            </div>
2546            <!-- draggable end for image-grid-image -->
2547            <div v-if="!show_first_row_skeleton && images?.length === 0 && is_admin" class="image-grid-insert flexWrapLeftAlign">
2548                <div class="image-grid-image image-grid-image_upload">
2549                    <div class="image-grid-image-inner">
2550                        <div class="uploadImageZone relative pointer">
2551                            <input 
2552                                @change="imageUploadChange($event)"
2553                                class="image-grid-upload-input absoluteInput" 
2554                                type="file" accept="image/*,video/*"
2555                            >
2556                            <p class="centeredBlock" style="font-family:roboto !important;">+ Upload image/video</p>
2557                            <div class="uploadImageOverlay">
2558                                <div class="uploadImageSpinner centeredBlock"></div>
2559                            </div>
2560                        </div>
2561                    </div>
2562                </div>
2563            </div>
2564            <div v-if="show_first_row_skeleton" class="image-grid-insert flexWrapLeftAlign">
2565                <div
2566                    v-for="n in skeleton_tile_count"
2567                    :key="'image-grid-skeleton-' + n"
2568                    class="image-grid-image relative"
2569                >
2570                    <div
2571                        class="image-grid-image-inner image-grid-skeleton-tile"
2572                        :style="{ 'border-radius': image_grid_border_radius + 'px' }"
2573                    ></div>
2574                </div>
2575            </div>
2576        </div>
2577    </template>
2578    <template v-else>
2579
2580        <div style="box-sizing: border-box; padding: 20px;" class="event-list-wrap centeredBlock centeredText" :class="'event-list-wrap_' + client_id">
2581            <!-- <div 
2582                class="flexNoWrap align-items-center event_list_carousel_default"
2583                style="padding: 0px 0px 15px 5px;"
2584            >
2585                <p>PLACEHOLDER</p>
2586            </div> -->
2587            <div 
2588                class="image-grid-wrap centeredBlock"
2589                :class="'image-grid-wrap_' + client_id + class_string"
2590                style="padding: 20px;"
2591            >
2592                <div 
2593                    class="tableInsert event-list-card-wrapper"
2594                    style="overflow: scroll !important; scrollbar-width: 5px !important; flex-wrap: nowrap;"
2595                    :style="{ 'gap': formatting_obj.card_spacing ? Number(formatting_obj.card_spacing) + 'px' : '25px' }"
2596                >
2597                    <template v-if="show_first_row_skeleton">
2598                        <div
2599                            v-for="n in skeleton_tile_count"
2600                            :key="'image-grid-carousel-skeleton-' + n"
2601                            class="event-list-card noPadding noMargin"
2602                            :class="{
2603                                'small-card-image-grid': formatting_obj.card_size == 'small',
2604                                'medium-card-image-grid': !formatting_obj.card_size || formatting_obj.card_size == 'medium',
2605                                'large-card-image-grid': formatting_obj.card_size == 'large',
2606                            }"
2607                            :style="{'border-radius': image_grid_border_radius + 'px' ||'0px' }"
2608                        >
2609                            <div class="image-grid-image relative">
2610                                <div
2611                                    class="image-grid-event-image image-grid-skeleton-tile"
2612                                    :style="{'border-radius': image_grid_border_radius + 'px' }"
2613                                ></div>
2614                            </div>
2615                            <div
2616                                v-if="formatting_obj.caption_align === 'under'"
2617                                class="image-grid-card-content background"
2618                                :style="{'border-radius': image_grid_border_radius + 'px' ||'0px' }"
2619                            >
2620                                <div class="image-grid-skeleton-caption image-grid-skeleton-tile"></div>
2621                            </div>
2622                        </div>
2623                    </template>
2624                    <template
2625                        v-else-if="images && images.length > 0"
2626                        v-for="(image, index) in images"
2627                        :key="image?.id"
2628                    >
2629                        <div
2630                            v-if="image && image.url" 
2631                            class="event-list-card noPadding noMargin" 
2632                            :class="{
2633                                'small-card-image-grid': formatting_obj.card_size == 'small',
2634                                'medium-card-image-grid': !formatting_obj.card_size || formatting_obj.card_size == 'medium',
2635                                'large-card-image-grid': formatting_obj.card_size == 'large',
2636                                'box-shadow-light': formatting_obj.box_shadow == 'light',
2637                                'box-shadow-dark': formatting_obj.box_shadow == 'dar
2637k',
2638                                'pointer': !is_admin,
2639                            }"
2640                            :style="{'border-radius': image_grid_border_radius + 'px' ||'0px' }"
2641                            @click.stop="clicked_carousel_image(image, $event)"
2642                        >
2643                            <div 
2644                                v-if="image && image.url" 
2645                                class="image-grid-image relative" 
2646                                :class="'image-grid-image_' + image?.id"
2647                                :id="image?.id"
2648                            >
2649                                <!-- video -->
2650                                <div 
2651                                    v-if="image.is_video" 
2652                                    class="image-grid-event-image"
2653                                    :class="{ 'image-grid-fade-media': !is_admin, 'is-loaded': image_has_loaded(image) }"
2654                                    style="align-items:center;display:flex;" 
2655                                    :style="{'border-radius': image_grid_border_radius + 'px' }"
2656                                >
2657                                    <video class="cs-background-video" autoplay playsinline loop muted :src="image.url" style="display:block;"></video>
2658                                </div>
2659                                <!-- image -->
2660                                <div 
2661                                    v-else
2662                                    class="image-grid-event-image"
2663                                    :class="{ 'image-grid-fade-media': !is_admin, 'is-loaded': image_has_loaded(image) }"
2664                                    :style="{'background-image': 'url(' + image.url + ')', 'border-radius': image_grid_border_radius + 'px' }"
2665                                ></div>
2666                                <!-- overlay -->
2667                                <div 
2668                                    @click.stop="change_image(image)"
2669                                    v-if="is_admin" 
2670                                    class="image-grid-image-overlay flexWrap easeFast" 
2671                                    style="left:0px;right:0px;bottom:0px;top:0px"
2672                                    :style="{ 'background-color': returnRGBAStringForImageGridImageOverlayOpacity( image_grid_opacity ), 'border-radius': image_grid_border_radius + 'px' }"
2673                                >
2674                                    <div class="image-toolbar-wrapper flexNoWrap centeredBlock" @click.stop>
2675                                        <div 
2676                                            class="image-toolbar-option easeFast" 
2677                                            data-option="change" 
2678                                            style="border-right:1px solid #fff;" 
2679                                            @click.stop="change_image(image)"
2680                                        >Change</div>
2681                                        <div 
2682                                            class="image-toolbar-option easeFast" 
2683                                            data-option="update" 
2684                                            style="border-right:1px solid #fff;" 
2685                                            @click.stop="prepThenDisplayImageSettings(image)"
2686                                        >Update</div>
2687                                        <div 
2688                                            class="image-toolbar-option easeFast" 
2689                                            data-option="remove" 
2690                                            @click.stop="tapped_remove_image(image)"
2691                                        ><i class="fa-regular fa-trash" style="font-size:14px;"></i></div>
2692                                    </div>
2693                                    <div v-if="formatting_obj.caption_align !== 'under'" class="image-text-wrapper" :style="{ alignItems: text_position === 'start' ? 'flex-start' : text_position === 'center' ? 'center' : 'flex-end' }">
2694                                        <h2 
2695                                            v-if="image.caption"
2696                                            class="image-grid-image-caption"
2697                                            :style="{ 'font-size': caption_size + 'px', 'font-weight': caption_font_weight }"
2698                                        >{{ image.caption }}</h2>
2699                                        <p
2700                                            v-if="image.subtext"
2701                                            class="image-grid-image-caption marginTopFour"
2702                                            :style="{ 'font-size': subtext_size + 'px', 'font-weight': caption_font_weight }"
2703                                        >{{ image.subtext }}</p>
2704                                        <p
2705                                            v-if="image.link_label"
2706                                            class="image-grid-image-caption marginTopFour"
2707                                            style="text-decoration: underline;"
2708                                            :style="{ 'font-size': link_label_size + 'px', 'font-weight': caption_font_weight }"
2709                                        >{{ image.link_label }}</p>
2710                                    </div>
2711                                </div> 
2712                                <div v-if="!is_admin" class="image-grid-image-overlay image-grid-carousel-live-overlay flexWrap easeFast pointer" style="left:0px;right:0px;bottom:0px;top:0px" :style="{ 'background-color': returnRGBAStringForImageGridImageOverlayOpacity( image_grid_opacity ), 'border-radius': image_grid_border_radius + 'px' }" @click.stop="clicked_carousel_image(image, $event)">
2713                                    <div v-if="formatting_obj.caption_align !== 'under'" class="image-text-wrapper" :style="{ alignItems: text_position === 'start' ? 'flex-start' : text_position === 'center' ? 'center' : 'flex-end' }">
2714                                        <h2 
2715                                            v-if="image.caption"
2716                                            class="image-grid-image-caption"
2717                                            :style="{ 'font-size': caption_size + 'px', 'font-weight': caption_font_weight }"
2718                                        >{{ image.caption }}</h2>
2719                                        <p
2720                                            v-if="image.subtext"
2721                                            class="image-grid-image-caption marginTopFour"
2722                                            :style="{ 'font-size': subtext_size + 'px', 'font-weight': caption_font_weight }"
2723                                        >{{ image.subtext }}</p>
2724                                        <p
2725                                            v-if="image.link_label"
2726                                            class="image-grid-image-caption marginTopFour"
2727                                            style="text-decoration: underline;"
2728                                            :style="{ 'font-size': link_label_size + 'px', 'font-weight': caption_font_weight }"
2729                                        >{{ image.link_label }}</p>
2730                                    </div>
2731                                    <a
2732                                        v-if="return_link_href(image.link_to)"
2733                                        class="absoluteA"
2734                                        :href="return_link_href(image.link_to)"
2735                                        rel="noopener noreferrer"
2736                                    ></a>
2737                                </div> 
2738                            </div>
2739                            <div 
2740                                v-if="formatting_obj.caption_align === 'under'"
2741                                class="image-grid-card-content background"
2742                                :style="{'border-radius': image_grid_border_radius + 'px' ||'0px' }"
2743                            >
2744                                <div class="image-text-wrapper" :style="{ alignItems: text_position === 'start' ? 'flex-start' : text_position === 'center' ? 'center' : 'flex-end' }">
2745                                    <h2 
2746                                        v-if="image.caption"
2747                                        class="image-grid-image-caption visible"
2748                                        :style="{ 'font-size': caption_size + 'px', 'font-weight': caption_font_weight }"
2749                                    >{{ image.caption }}</h2>
2750                                    <p
2751                                        v-if="image.subtext"
2752                                        class="image-grid-image-caption visible marginTopFour"
2753                                        :style="{ 'font-size': subtext_size + 'px', 'font-weight': caption_font_weight }"
2754                                    >{{ image.subtext }}</p>
2755                                    <p
2756                                        v-if="image.link_label"
2757                                        class="image-grid-image-caption visible marginTopFour"
2758                                        style="text-decoration: underline;"
2759                                        :style="{ 'font-size': link_label_size + 'px', 'font-weight': caption_font_weight }"
2760                                    >
2761                                        <a
2762                                            v-if="return_link_href(image.link_to)"
2763                                            :href="return_link_href(image.link_to)"
2764                                            rel="noopener noreferrer"
2765                                        >{{ image.link_label }}</a>
2766                                        <template v-else>{{ image.link_label }}</template>
2767                                    </p>
2768                                </div>
2769                            </div>
2770                        </div>
2771                        <div
2772                            v-else-if="is_admin && image && image.placeholder"
2773                            class="event-list-card noPadding noMargin cs-image-slot-placeholder-host pointer"
2774                            :class="{
2775                                'small-card-image-grid': formatting_obj.card_size == 'small',
2776                                'medium-card-image-grid': !formatting_obj.card_size || formatting_obj.card_size == 'medium',
2777                                'large-card-image-grid': formatting_obj.card_size == 'large',
2778                            }"
2779                            :style="{'border-radius': image_grid_border_radius + 'px' ||'0px' }"
2780                            @click.stop="fill_image_placeholder(image)"
2781                        >
2782                            <div class="image-grid-image relative">
2783                                <div class="image-grid-event-image cs-image-slot-placeholder">
2784                                    <span class="cs-image-slot-placeholder-icon"><i class="fa-regular fa-image"></i></span>
2785                                    <span class="cs-image-slot-placeholder-label">Add {{ image.label || 'image' }}</span>
2786                                </div>
2787                            </div>
2788                        </div>
2789                    </template>
2790                    <!-- upload image **for admin -->
2791                    <template v-if="is_admin">
2792                        <div 
2793                            class="event-list-card pointer" 
2794                            :class="{
2795                                'small-card-image-grid': formatting_obj.card_size == 'small',
2796                                'medium-card-image-grid': !formatting_obj.card_size || formatting_obj.card_size == 'medium',
2797                                'large-card-image-grid': formatting_obj.card_size == 'large',
2798                            }"
2799                        >
2800                            <div class="image-grid-image relative upload-image-grid-image-zone">
2801                                <div class="image-grid-event-image"></div>
2802                                <input 
2803                                    @change="imageUploadChange($event)"
2804                                    class="image-grid-upload-input absoluteInput" 
2805                                    type="file" accept="image/*,video/*"
2806                                >
2807                                <p class="centeredBlock" style="font-family:roboto !important;">+ Upload image/video</p>
2808                                <!-- <div class="uploadImageOverlay">
2809                                    <div class="uploadImageSpinner centeredBlock"></div>
2810                                </div> -->
2811                            </div>
2812                            <div 
2813                                v-if="formatting_obj.caption_align === 'under'"
2814                                class="image-grid-card-content"
2815                                :style="{'border-radius': image_grid_border_radius + 'px' ||'0px' }"
2816                            >
2817                                <div class="image-text-wrapper" :style="{ alignItems: text_position === 'start' ? 'flex-start' : text_position === 'center' ? 'center' : 'flex-end' }">
2818                                    <!-- PLACEHOLDER -->
2819                                </div>
2820                            </div>
2821                        </div>
2822                    </template>
2823                </div>
2824            </div>
2825        </div>
2826    </template>
2827</script>
2827
2828
2829<style>
2830    .cs-image-slot-placeholder-host {
2831        cursor: pointer;
2832    }
2833    .cs-image-slot-placeholder {
2834        display: flex;
2835        flex-direction: column;
2836        align-items: center;
2837        justify-content: center;
2838        gap: 8px;
2839        width: 100%;
2840        height: 100%;
2841        min-height: 140px;
2842        background: #f1f4f8;
2843        border: 1.5px dashed #b7c1d1;
2844        color: #5f6f8d;
2845        text-align: center;
2846        box-sizing: border-box;
2847    }
2848    .cs-image-slot-placeholder-icon {
2849        font-size: 22px;
2850    }
2851    .cs-image-slot-placeholder-label {
2852        font-family: roboto !important;
2853        font-size: 14px;
2854        font-weight: 600;
2855    }
2856    .image-grid-image.dragging{
2857        overflow: hidden;
2858        border-radius: 10px;
2859    }
2860    .image-grid-image.dragging .image-grid-image-inner {
2861        transition: all 0.35s ease-in-out;
2862        border: 1.2px solid #fff;
2863        border-radius: 10px;
2864        z-index: 1000;
2865        transform: scale(1.1);
2866        cursor: grab;
2867        cursor: move;
2868    }
2869    .image-grid-image.dragging .image-grid-drag-handle-wrap {
2870        display: none;
2871        position: relative;
2872    }
2873    .event-list-card {
2874        overflow: hidden;
2875        background: white;
2876        font-family: 'Inter', sans-serif;
2877        display: flex;
2878        flex-direction: column;
2879        flex: 0 0 auto;
2880        transition: transform 0.1s ease;
2881    }
2882    .event-list-card.small-card-image-grid {
2883        width: 190px;
2884    }
2885    .event-list-card.medium-card-image-grid {
2886        width: 230px;
2887    }
2888    .event-list-card.large-card-image-grid {
2889        width: 300px;
2890    }
2891    .event-list-card.box-shadow-light {
2892        box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2);
2893    }
2894    .event-list-card.box-shadow-dark {
2895        box-shadow: 6px 6px 16px rgba(0, 0, 0, 0.2);
2896    }
2897    .event-list-card .image-grid-image {
2898        width: 100%;
2899        flex-basis: auto;
2900        aspect-ratio: 1 / 1;
2901    }
2902    .event-list-card .image-grid-image:before {
2903        display: none;
2904    }
2905    .event-list-card .image-grid-event-image {
2906        display: flex;
2907        text-align: center;
2908        align-items: center;
2909        width: 100%;
2910        height: 100%;
2911        background-size: cover;
2912        background-repeat: no-repeat;
2913        background-position: center;
2914        color: #fff;
2915        position: absolute;
2916        pointer-events: none;
2917    }
2918    .event-list-card .image-grid-event-image * {
2919        pointer-events: none;
2920    }
2921    .event-list-card .image-grid-image-overlay {
2922        z-index: 1;
2923    }
2924    .event-list-card .image-grid-carousel-live-overlay {
2925        pointer-events: auto;
2926        cursor: pointer;
2927    }
2928    .image-grid-card-content {
2929        height: 100px;
2930        padding: 12px 5px;
2931    }
2932    .image-grid-skeleton-tile {
2933        position: relative;
2934        background-color: #e8edf1;
2935        overflow: hidden;
2936        margin-right: 0;
2937    }
2938    .image-grid-skeleton-tile::after {
2939        background: linear-gradient(40deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0));
2940        display: block;
2941        content: "";
2942        position: absolute;
2943        width: 100%;
2944        height: 100%;
2945        transform: translateX(-100%);
2946        top: 0;
2947        left: 0;
2948        animation: coming_soon 0.75s infinite;
2949    }
2950    .image-grid-skeleton-caption {
2951        height: 16px;
2952        width: 70%;
2953        border-radius: 4px;
2954        margin: 8px auto 0;
2955    }
2956    .image-grid-fade-media {
2957        opacity: 0;
2958        transition: opacity 0.4s ease;
2959    }
2960    .image-grid-fade-media.is-loaded {
2961        opacity: 1;
2962    }
2963    .upload-image-grid-image-zone{
2964        color: #323d63;
2965        text-align: center;
2966        border-radius: 2px;
2967        background-color: #f1f5f9;
2968        position: relative;
2969        display: flex;
2970        align-items: center;
2971    }
2972</style>
2973
2974<script>
2975
2976    app_content.component("image-grid-component", {
2977        template: "#template-image-grid",
2978        data() {
2979            return {
2980                global: store,
2981                loading: false,
2982                first_row_ready: false,
2983                loaded_image_ids: {},
2984            };
2985        },
2986        props: {
2987            client_id: String,
2988            class_string: String,
2989            image_grid_layout: String,
2990            images_per_row: [String, Number],
2991            caption_size: String,
2992            subtext_size: String,
2993            link_label_size: String,
2994            text_position: String,
2995            caption_font_weight: String,
2996            image_grid_opacity: String,
2997            images_string: String,
2998            image_grid_border_radius: String,
2999            // carousel
3000            formatting: String,
3001            // admin
3002            is_admin: {
3003                type: Boolean,
3004                default: false,
3005            },
3006            data_source_type: String,
3007            images_arr: Array,
3008            component: Object,
3009            last_update: Number,
3010        },
3011        mounted() {
3012            this.preload_first_row();
3013            this.$nextTick(() => {
3014                if (this.is_admin && this.images_arr){
3015                    this.makeElementsSortable_image_grid();
3016                }
3017            })
3018        },
3019        methods: {
3020            preload_first_row(){
3021                if( this.is_admin ){
3022                    this.first_row_ready = true;
3023                    return;
3024                }
3025                let list = this.images || [];
3026                let count = this.first_row_slot_count;
3027                let slice = [];
3028                for( let i = 0; i < list.length && slice.length < count; i++ ){
3029                    let entry = list[i];
3030                    if( entry && entry.url && !entry.placeholder && !entry.is_video ){
3031                        slice.push(entry);
3032                    }
3033                }
3034                if( slice.length === 0 ){
3035                    this.first_row_ready = true;
3036                    this.preload_remaining_images();
3037                    return;
3038                }
3039                let remaining = slice.length;
3040                let finish = () => {
3041                    this.first_row_ready = true;
3042                    this.preload_remaining_images();
3043                };
3044                setTimeout(finish, 2500);
3045                slice.forEach((entry) => {
3046                    this.preload_image_entry(entry, () => {
3047                        remaining -= 1;
3048                        if( remaining <= 0 ){
3049                            finish();
3050                        }
3051                    });
3052                });
3053            },
3054            preload_remaining_images(){
3055                let list = this.images || [];
3056                list.forEach((entry) => {
3057                    this.preload_image_entry(entry);
3058                });
3059            },
3060            preload_image_entry( entry, on_done ){
3061                if( !entry || !entry.id ){
3062                    if( on_done ){
3063                        on_done();
3064                    }
3065                    return;
3066                }
3067                if( entry.is_video || !entry.url || entry.placeholder ){
3068                    this.mark_image_loaded(entry.id);
3069                    if( on_done ){
3070                        on_done();
3071                    }
3072                    return;
3073                }
3074                if( this.loaded_image_ids[entry.id] ){
3075                    if( on_done ){
3076                        on_done();
3077                    }
3078                    return;
3079                }
3080                let img = new Image();
3081                img.onload = () => {
3082                    this.mark_image_loaded(entry.id);
3083                    if( on_done ){
3084                        on_done();
3085                    }
3086                };
3087                img.onerror = () => {
3088                    this.mark_image_loaded(entry.id);
3089                    if( on_done ){
3090                        on_done();
3091                    }
3092                };
3093                img.src = entry.url;
3094            },
3095            mark_image_loaded( id ){
3096                if( !id ){
3097                    return;
3098                }
3099                let next = Object.assign({}, this.loaded_image_ids);
3100                next[id] = true;
3101                this.loaded_image_ids = next;
3102            },
3103            image_has_loaded( image ){
3104                if( this.is_admin ){
3105                    return true;
3106                }
3107                if( !image || !image.id ){
3108                    return false;
3109                }
3110                if( image.is_video ){
3111                    return true;
3112                }
3113                return !!this.loaded_image_ids[image.id];
3114            },
3115            makeElementsSortable_image_grid(){
3116                //now create the new instance
3117                const containerSelector = `.draggable-container_image_grid_${this.component.id}`;
3118                const containers = document.querySelectorAll(containerSelector);
3119                if (containers?.length === 0) {
3120                    return false;
3121                }
3122                const sortable = new Draggable.Sortable(containers, {
3123                    draggable: '.image-grid-image:not(.non-draggable)',
3124                    handle: '.draggable-handle',
3125                });
3126                //now register listeners
3127                sortable.on('sortable:start', event => {
3128                    var target = event.data.dragEvent.source;
3129                    target.classList.add('dragging');
3130                });
3131                sortable.on('sortable:stop', event => {
3132                    var target = event.data.dragEvent.source;
3133                    target.classList.remove('dragging');
3134                    //now handle ui updates inside a timeout due to a draggable library issue (https://github.com/Shopify/draggable/issues/92)
3135                    setTimeout(() => {  
3136                        this.handle_sortable_stop( event );
3137                    }, 0)
3138                });
3139            },
3140            handle_sortable_stop( event ){
3141                let targetId = event.data.dragEvent.source.id;
3142                let dropElement = document.getElementById(targetId);
3143                var reordered_ids = Array.from(event.newContainer.children).map(event => event.id);
3144                reordered_ids.pop(); //pop off last value which is an empty string placeholder for image upload
3145                var reordered_images = [];
3146                reordered_ids.forEach(image_id => {
3147                    //find the image 
3148                    var this_image = null;
3149                    this.images.forEach(image => {
3150                        if( image.id == image_id ){
3151                            this_image = image
3152                        }
3153                    });
3154                    if( this_image ){
3155                        reordered_images.push( this_image );
3156                    }
3157                });
3158                //save changes
3159                save_changes_for_component( this.component, 'images', reordered_images );
3160            },
3161            returnRGBAStringForImageGridImageOverlayOpacity( opacity ){
3162                if( ! opacity && opacity !== 0 ){
3163                    opacity = 40;
3164                }
3165                return "rgba(8,8,19,"+ opacity / 100 +")";
3166            },
3167            clicked_carousel_image( image, event ){
3168                if( this.is_admin ){
3169                    return;
3170                }
3171                if( event && event.target && event.target.closest && event.target.closest('a') ){
3172                    return;
3173                }
3174                // An image with a link navigates instead of opening the lightbox.
3175                let href = this.return_link_href( image && image.link_to );
3176                if( href ){
3177                    window.location.href = href;
3178                    return;
3179                }
3180                this.open_carousel_lightbox( image );
3181            },
3182            // link_to is saved raw from the image settings input, so:
3183            // - a bare domain needs a protocol or it resolves as a relative path
3184            // - anything that isn't a usable link returns '' so callers fall back
3185            //   to the lightbox instead of navigating nowhere
3186            return_link_href( link_to ){
3187                if( ! link_to ){
3188                    return '';
3189                }
3190                let trimmed = String( link_to ).trim();
3191                if( /^(https?:\/\/|mailto:|tel:)\S/i.test( trimmed ) ){
3192                    return trimmed;
3193                }
3194                if( /^\//.test( trimmed ) || /^#\S/.test( trimmed ) ){
3195                    return trimmed;
3196                }
3197                if( /^[a-z0-9][a-z0-9-]*(\.[a-z0-9-]+)+([\/?#]\S*)?$/i.test( trimmed ) ){
3198                    return 'https://' + trimmed;
3199                }
3200                return '';
3201            },
3202            lightbox_file_name( entry ){
3203                let url = (entry && entry.url) || '';
3204                let from_url = url.split('?')[0].split('/').pop();
3205                if( entry && entry.is_video ){
3206                    if( from_url && from_url.indexOf('.') !== -1 ){
3207                        return from_url;
3208                    }
3209                    return 'video.mp4';
3210                }
3211                return from_url || 'photo.jpg';
3212            },
3213            open_carousel_lightbox( image ){
3214                if( !store.components || !store.components.view_photo_carousel ){
3215                    return;
3216                }
3217                let photos = (this.images || []).filter((entry) => {
3218                    return entry && entry.url && !entry.placeholder;
3219                });
3220                if( photos.length === 0 ){
3221                    return;
3222                }
3223                let current_index = photos.findIndex((entry) => {
3224                    return image && entry.id === image.id;
3225                });
3226                if( current_index < 0 ){
3227                    current_index = 0;
3228                }
3229                store.components.view_photo_carousel.json_photos = photos.map((entry) => {
3230                    return {
3231                        id: entry.id,
3232                        url: entry.url,
3233                        caption: entry.caption,
3234                        file_name: this.lightbox_file_name(entry),
3235                    };
3236                });
3237                store.components.view_photo_carousel.current_index = current_index;
3238                store.components.view_photo_carousel.json_config = {
3239                    context: 'web_builder',
3240                    icon_only_actions: true,
3241                    allow_download: false,
3242                    allow_share: true,
3243                    show_caption: true,
3244                    caption_size: this.caption_size || 16,
3245                    caption_font_weight: this.caption_font_weight || 400,
3246                    blur_background: false,
3247                };
3248                store.overlays.view_photo_carousel = true;
3249            },
3250            confirmed_remove_image( data ){
3251                var image = data.image;
3252                var component = data.component;
3253                //find existing images
3254                var images = component.get('config_latest').images || [];
3255                //loop through to remove this one
3256                var new_images = [];
3257                for (var i = 0; i <= images?.length - 1; i++) {
3258                    if( images[i].id !== image.id ){
3259                        new_images.push( images[i] );
3260                    }
3261                }
3262                //now save and update the preview
3263                save_changes_for_component( component, 'images', new_images );
3264                store.overlays.are_you_sure = false;
3265            },
3266            // Agent-placed empty slot. Open the shared image-slot picker; on a
3267            // pick, write the chosen/imported URL into this slot and clear the
3268            // placeholder flag, so it renders like any uploaded tile.
3269            fill_image_placeholder( image ){
3270                if( typeof webbuilder_open_image_slot_picker !== 'function' ){
3271                    if( typeof displayMessage === 'function' ){
3272                        displayMessage('Image picker is not loaded. Refresh the page and try again.', true, 5000);
3273                    }
3274                    return;
3275                }
3276                let label = (image && image.label) ? image.label : 'image';
3277                let slot_id = image && image.id;
3278                webbuilder_open_image_slot_picker({
3279                    label: label,
3280                    mode: 'placeholder',
3281                    on_pick: (url) => {
3282                        if( !url || !slot_id ){
3283                            return;
3284                        }
3285                        let current = (this.component.get('config_latest') && this.component.get('config_latest').images) || [];
3286                        let next = current.map((entry) => {
3287                            if( !entry || entry.id !== slot_id ){
3288                                return entry;
3289                            }
3290                            let updated = Object.assign({}, entry, { url: url, is_video: false });
3291                            delete updated.placeholder;
3292                            return updated;
3293                        });
3294                        save_changes_for_component( this.component, 'images', next );
3295                    },
3296                    on_keep_empty: () => {
3297                        if( !slot_id ){
3298                            return;
3299                        }
3300                        let current = (this.component.get('config_latest') && this.component.get('config_latest').images) || [];
3301                        let next = current.filter((entry) => entry && entry.id !== slot_id);
3302                        save_changes_for_component( this.component, 'images', next );
3303                    },
3304                });
3305            },
3306            // Filled tile "Change" action — swap the image via the shared picker
3307            // (mirrors custom-code's attached-slot menu: pick/paste/copy-external/
3308            // remove). Caption/link editing stays on the existing Update toolbar.
3309            change_image( image ){
3310                if( typeof webbuilder_open_image_slot_picker !== 'function' ){
3311                    if( typeof displayMessage === 'function' ){
3312                        displayMessage('Image picker is not loaded. Refresh the page and try again.', true, 5000);
3313                    }
3314                    return;
3315                }
3316                let slot_id = image && image.id;
3317                if( !slot_id ){
3318                    return;
3319                }
3320                webbuilder_open_image_slot_picker({
3321                    label: 'image',
3322                    mode: 'attached',
3323                    current_url: (image && image.url) || '',
3324                    on_pick: (url) => {
3325                        if( !url ){
3326                            return;
3327                        }
3328                        let current = (this.component.get('config_latest') && this.component.get('config_latest').images) || [];
3329                        let next = current.map((entry) => {
3330                            if( !entry || entry.id !== slot_id ){
3331                                return entry;
3332                            }
3333                            return Object.assign({}, entry, { url: url, is_video: false });
3334                        });
3335                        save_changes_for_component( this.component, 'images', next );
3336                    },
3337                    on_remove: () => {
3338                        this.confirmed_remove_image({ image: image, component: this.component });
3339                    },
3340                });
3341            },
3342            tapped_remove_image( image ){
3343                //ask for confirmation
3344                var params = {};
3345                params["fn"] = this.confirmed_remove_image;                
3346                params["title"] = "Remove image";
3347                params["subtext"] = "Are you sure you want to permanently remove this tile from the image grid?";
3348                params["button_label"] = "Remove";
3349                params["data"] = {
3350                    "image": image,
3351                    "component": this.component
3352                };
3353                are_you_sure( params );
3354                return
3355            },
3356            prepThenDisplayImageSettings(imageObject){
3357                store.components.image_settings_overlay.componentObject = this.component;
3358                store.components.image_settings_overlay.imageObject = imageObject;
3359                store.components.image_settings_overlay.settings = {
3360                    link_input: imageObject.link_to  || '',
3361                    caption_input: imageObject.caption || '',
3362                    subtext_input: imageObject.subtext || '',
3363                    link_label_input: imageObject.link_label || '',
3364                }
3365                store.components.image_settings_overlay.image_grid_props = {
3366                    client_id: this.client_id,
3367                    caption_size: this.caption_size,
3368                    caption_font_weight: this.caption_font_weight,
3369                };
3370                this.global.overlays.image_settings_overlay = !this.global.overlays.image_settings_overlay;
3371                this.$nextTick(() => {
3372                    this.updatePositionForImageSettings( imageObject );
3373                });
3374            },
3375            updatePositionForImageSettings(imageObject){
3376                var image_object = imageObject;
3377                if( ! image_object ){
3378                    return
3379                }
3380                var element = $(".image-grid-image_" + image_object.id);
3381                if( ! element ){
3382                    return
3383                }
3384                var offset = element.offset();
3385                var top = offset.top;
3386                var popupHeight = $('.image-settings').outerHeight(true) || 0;
3387                var above_top = top - ( popupHeight + 10 );
3388                var viewportTop = $(window).scrollTop();
3389                var correct_top = above_top >= viewportTop + 10 ? above_top : top + 45;
3390                store.components.image_settings_overlay.position = {
3391                    top: `${correct_top}px`,
3392                    left: `${offset.left}px`
3393                };
3394            },
3395            imageUploadChange($event){
3396                var file = event.target.files[0];
3397                var componentObject = this.component;
3398                if(file && componentObject){
3399                    //update the ui
3400                    $(this).parents('.uploadImageZone').addClass('loading');
3401                    //set attributes
3402                    var type = 'image_grid';
3403                    var timestamp = Date.now();
3404                    //check the file type
3405                    var is_video = this.returnIsVideo( file['name'] ) || false;
3406                    if( is_video ){
3407                        //ok, it's a video
3408                        //.. handle the video
3409                        var file_name = componentObject.get('client_id')+"_grid_video_"+timestamp;
3410                        return uploadFileToAWSV2( file, file_name ).then(( url )=>{
3411                            this.handleUploadedImageGridImage( url, componentObject, true );
3412                        });
3413                    }else{
3414                        //assume it's an image!
3415                        var file_name = componentObject.get('client_id')+"_grid_image_"+timestamp;
3416                        //handle the image
3417                        loadCleanImage(file, this.handleCleanImage_image_grid, type, file_name, componentObject.get('client_id') );
3418                    }
3419                }
3420                //reset input value to detect change if same file is uploaded
3421                event.target.value = '';
3422            },
3423            returnIsVideo(filename) {
3424                var ext = this.getExtension(filename);
3425                switch (ext.toLowerCase()) {
3426                    case 'm4v':
3427                    case 'avi':
3428                    case 'mpg':
3429                    case 'mp4':
3430                    case 'mp3':
3431                    case 'mov':
3432                    // etc
3433                    return true;
3434                }
3435                return false;
3436            },
3437            getExtension(filename) {
3438                var parts = filename.split('.');
3439                return parts[parts?.length - 1];
3440            },
3441            completed_crop(params){
3442                const { fileType, is_video, file_name, blob, componentObject } = params;
3443                //upload it
3444                return uploadFileToAWSV2( blob, file_name ).then(( url )=>{
3445                    this.handleUploadedImageGridImage( url, componentObject, is_video );
3446                });
3447            },
3448            handleCleanImage_image_grid( canvas, type, file_name, componentID, image_index, is_video, fileType ){
3449                $('body').data('can', canvas);
3450                let componentObject = this.component;
3451                if(!fileType) {
3452                    fileType = 'image/jpeg'
3453                }
3454                var url = canvas.toDataURL(fileType);
3455                var params = {};
3456                params["url"] = url;
3457                params["fn"] = this.completed_crop;
3458                params["componentID"] = componentID;
3459                params["fileType"] = fileType;
3460                params["is_video"] = is_video || false;
3461                params["file_name"] = file_name;
3462                params["componentObject"] = componentObject;
3463                launch_image_cropper( params );
3464            },
3465            handleUploadedImageGridImage(url, componentObject, is_video){
3466                if( componentObject ){
3467                    //it's still in the dom
3468                    //.. update it
3469                    var images = componentObject.get('config_latest')?.images || [];
3470                    images.push({
3471                        "id": generateClientID(),
3472                        "url": url, 
3473                        "is_video": is_video
3474                    });
3475                    save_changes_for_component( this.component, 'images', images );
3476                    //store.eventBus.$emit('saveChangesForComponent', this.component, option, value)
3477                    //message
3478                    if( is_video ){
3479                        displayMessage("Video uploaded. It may take a moment for your video to appear.", false, 5000);
3480                    }else{
3481                        displayMessage("Image uploaded.", false, 3000);
3482                    }
3483                }
3484            },
3485        },
3486        computed: {
3487            images(){
3488                if (!this.is_admin && this.images_string){
3489                    try {
3490                        const parsedObject = JSON.parse(this.images_string);
3491                        // Drop unfilled agent placeholders on the public site —
3492                        // they are an admin-only "Add image" affordance.
3493                        return Array.isArray(parsedObject)
3494                            ? parsedObject.filter((entry) => entry && entry.url && !entry.placeholder)
3495                            : parsedObject;
3496                    } catch (error) {
3497                        console.error('Error parsing JSON: ', error)
3498                    }
3499                } else if (this.is_admin && this.images_arr){
3500                    return this.images_arr || [];
3501                }
3502            },
3503            formatting_obj(){
3504                if (this.formatting){
3505                    return JSON.parse(this.formatting);
3506                }
3507                return {};
3508            },
3509            first_row_slot_count(){
3510                if( this.image_grid_layout === 'carousel' ){
3511                    let size = this.formatting_obj.card_size;
3512                    if( size === 'small' ){
3513                        return 5;
3514                    }
3515                    if( size === 'large' ){
3516                        return 3;
3517                    }
3518                    return 4;
3519                }
3520                let per_row = Number(this.images_per_row);
3521                if( !per_row || per_row < 1 ){
3522                    return 3;
3523                }
3524                return per_row;
3525            },
3526            skeleton_tile_count(){
3527                let images = this.images;
3528                let row = this.first_row_slot_count;
3529                if( images && images.length > 0 && images.length < row ){
3530                    return images.length;
3531                }
3532                return row;
3533            },
3534            show_first_row_skeleton(){
3535                if( this.is_admin ){
3536                    return false;
3537                }
3538                return !this.first_row_ready;
3539            },
3540        },
3541    })
3542
3543</script>
3543    
3543<script type="text/html" id="template-photo-gallery">
3544    <div class="photo-gallery-wrap centeredBlock centeredText">
3545        <div v-if="loading" class="photo-gallery">
3546            <div
3547                v-for="n in skeleton_tile_count"
3548                :key="'photo-gallery-skeleton-' + n"
3549                class="photo-container"
3550                :style="calculate_container_dimensions()"
3551            >
3552                <div class="photo-cover photo-gallery-skeleton-tile"></div>
3553            </div>
3554        </div>
3555        <!-- hosted site -->
3556        <div v-if="!loading && !is_admin">
3557            <div 
3558                v-if="photo_album_id && json_photos"
3559                class="photo-gallery"
3560            >
3561                <div 
3562                    v-for="(photo, index) in json_photos" 
3563                    class="photo-container" 
3564                    :id="photo.id"
3565                    @click="clicked_gallery_photo( index )"
3566                    :style="calculate_container_dimensions()"
3567                >
3568                    <video v-if="photo && returnIsVideo(photo.file_name)" class="video-cover paddingTop" autoplay muted loop playsinline>
3569                        <source :src="photo.url" type="video/mp4">
3570                    </video>
3571                    <div 
3572                        v-else-if="photo && photo.url" 
3573                        class="photo-cover"
3574                        :class="{ 'photo-cover-fade': !is_admin, 'is-loaded': photo_has_loaded(photo) }"
3575                        :style="'background-image:url('+encodeURI(photo.url)+')'"
3576                    ></div>
3577                    <!-- overlay -->
3578                    <div class="photo-overlay"></div>
3579                </div>
3580            </div>
3581        </div>
3582        <!-- admin -->
3583        <div v-if="!loading && is_admin">
3584            <div
3585                v-if="!photo_album_id"
3586                @click="link_photo_album()"
3587                class="noObjectsMSG centeredBlock centeredText"
3588                style="padding: 5em;"
3589            >
3590                <p style="cursor: pointer; font-family: 'Roboto', 'Arial' !important;"><i class="fa-light fa-images smallMarginRight"></i>Click to link a photo album</p>
3591            </div>
3592            <div 
3593                v-else-if="photo_album_id && json_photos"
3594                class="photo-gallery"
3595            >
3596                <div 
3597                    v-for="(photo, index) in json_photos" 
3598                    class="photo-container" 
3599                    :id="photo.id"
3600                    @click="clicked_gallery_photo( index )"
3601                    :style="calculate_container_dimensions()"
3602                >
3603                    <video v-if="photo && returnIsVideo(photo.file_name)" class="video-cover paddingTop" autoplay muted loop playsinline>
3604                        <source :src="photo.url" type="video/mp4">
3605                    </video>
3606                    <div 
3607                        v-else-if="photo && photo.url" 
3608                        class="photo-cover"
3609                        :class="{ 'photo-cover-fade': !is_admin, 'is-loaded': photo_has_loaded(photo) }"
3610                        :style="'background-image:url('+encodeURI(photo.url)+')'"
3611                    ></div>
3612                    <!-- overlay -->
3613                    <div class="photo-overlay"></div>
3614                </div>
3615            </div>
3616        </div>
3617    </div>
3618</script>
3618
3619
3620<style>
3621    .photo-gallery-wrap{
3622        height: fit-content;
3623        width: 100%;
3624    }
3625    .photo-gallery{
3626        display: flex;
3627        flex-wrap: wrap;
3628        align-items: center;
3629        justify-content: flex-start; /* center for mobile */
3630    }
3631    .photo-container {
3632        position: relative;
3633        box-sizing: border-box;
3634        margin: 0.3em;
3635        text-align: center;
3636    }
3637    .photo-container .video-cover {
3638        width: 100%;
3639        object-fit: cover;
3640    }
3641    .photo-container > .photo-cover {
3642        height: 100%;
3643        width: inherit;
3644        display: block;
3645        overflow: hidden;
3646        margin: 0 auto;
3647        background-size: cover;
3648        background-position: center;
3649        background-repeat: no-repeat;
3650        opacity: 1;
3651    }
3652    .photo-container > .photo-cover.photo-cover-fade {
3653        opacity: 0;
3654        transition: opacity 0.4s ease;
3655    }
3656    .photo-container > .photo-cover.photo-cover-fade.is-loaded {
3657        opacity: 1;
3658    }
3659    .photo-overlay{
3660        position: absolute;
3661        display: flex;
3662        align-items: center;
3663        justify-content: center;
3664        top: 0;
3665        left: 0;
3666        height: 100%;
3667        width: 100%;
3668        opacity: 0;
3669        background-color: rgba(8,8,19,0.3);
3670        transition: opacity 0.2s ease-out;
3671    }
3672    .photo-container:hover .photo-overlay {
3673        cursor: pointer;
3674        opacity: 0.8 !important;
3675    }
3676    .photo-gallery-skeleton-tile {
3677        position: relative;
3678        background-color: #e8edf1;
3679        overflow: hidden;
3680        margin-right: 0;
3681        height: 100%;
3682        width: 100%;
3683        border-radius: 2px;
3684    }
3685    .photo-gallery-skeleton-tile::after {
3686        background: linear-gradient(40deg, rgba(255, 255, 255, 0), rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0));
3687        display: block;
3688        content: "";
3689        position: absolute;
3690        width: 100%;
3691        height: 100%;
3692        transform: translateX(-100%);
3693        top: 0;
3694        left: 0;
3695        animation: coming_soon 0.75s infinite;
3696    }
3697</style>
3698
3699<script>
3700
3701    app_content.component("photo-gallery-component", {
3702        template: "#template-photo-gallery",
3703        data() {
3704            return {
3705                global: store,
3706                loading: true,
3707                json_photos: [],
3708                loaded_photo_ids: {},
3709            };
3710        },
3711        props: {
3712            client_id: String,
3713            is_admin: Boolean,
3714            component: Object,
3715            last_update: Number,
3716            photo_album: [String, Object],
3717            show_caption: Boolean,
3718            caption_size: Number,
3719            caption_font_weight: Number,
3720            photos_per_row: Number,
3721        },
3722        mounted() {
3723            if( !this.photo_album_id ){
3724                this.loading = false;
3725                return;
3726            }
3727            this.load_photo_albums( this.client_id );
3728        },
3729        methods: {
3730            calculate_container_dimensions(){
3731                const percentage = 100 / this.photos_per_row;
3732                const isMobile = window.innerWidth <= 600;
3733                if ( isMobile ){
3734                    return {
3735                        flex: `1 1 calc(${percentage}% - 0.6em)`,
3736                        maxWidth: `calc(${percentage}% - 0.6em)`,
3737                        height: `calc(${100}px * (${percentage} / 100) * 2.5)`,
3738                    };
3739                }
3740                return {
3741                    flex: `1 1 calc(${percentage}% - 0.6em)`,
3742                    maxWidth: `calc(${percentage}% - 0.6em)`,
3743                    height: `calc(${500}px * (${percentage} / 100) * 2.5)`,
3744                };
3745            },
3746            load_photo_albums( client_id, overrides = {} ){
3747                this.loading = true;
3748                var promise = Parse.Promise.as();
3749                var thisPromise = promise.then(() => {
3750                    var params = {};
3751                    params["client_id"] = client_id;
3752                    params["photo_album_id"] = this.photo_album_id;
3753                    params["overrides"] = overrides;
3754                    params["request_url"] = window.location.href;
3755                    return Parse.Cloud.run('retrieve_data_for_component', params);
3756                }).then(retrieved_photos => {
3757                    this.json_photos = retrieved_photos;
3758                    this.preload_gallery_photos(retrieved_photos);
3759                    return Parse.Promise.as();
3760                }).then(() => {
3761                    this.loading = false;
3762                }, error => {
3763                    checkParseError(error);
3764                    this.loading = false;
3765                });
3766                return thisPromise;
3767            },
3768            clicked_gallery_photo( index ){
3769                store.components.view_photo_carousel.json_photos = this.json_photos;
3770                store.components.view_photo_carousel.current_index = index;
3771                store.components.view_photo_carousel.json_config = {
3772                    context: 'web_builder',
3773                    icon_only_actions: true,
3774                    allow_download: false,
3775                    allow_share: true,
3776                    show_caption: this.show_caption || false,
3777                    caption_size: this.caption_size || 16,
3778                    caption_font_weight: this.caption_font_weight || 400,
3779                    blur_background: false,
3780                };
3781                store.overlays.view_photo_carousel = true;
3782            },
3783            link_photo_album(){
3784                store.components.link_photo_album.component = this.component;
3785                store.components.link_photo_album.last_update = this.last_update;
3786                store.overlays.link_photo_album = true;
3787            },
3788            returnIsVideo(filename) {
3789                var ext = this.getExtension(filename);
3790                switch (ext.toLowerCase()) {
3791                    case 'm4v':
3792                    case 'avi':
3793                    case 'mpg':
3794                    case 'mp4':
3795                    case 'mp3':
3796                    case 'mov':
3797                    // etc
3798                    return true;
3799                }
3800                return false;
3801            },
3802            getExtension(filename) {
3803                var parts = filename.split('.');
3804                return parts[parts.length - 1];
3805            },
3806            preload_gallery_photos( photos ){
3807                if( !photos || !photos.length ){
3808                    return;
3809                }
3810                photos.forEach((photo) => {
3811                    if( !photo || !photo.id ){
3812                        return;
3813                    }
3814                    if( this.returnIsVideo(photo.file_name) || !photo.url ){
3815                        this.mark_photo_loaded(photo.id);
3816                        return;
3817                    }
3818                    let img = new Image();
3819                    img.onload = () => {
3820                        this.mark_photo_loaded(photo.id);
3821                    };
3822                    img.onerror = () => {
3823                        this.mark_photo_loaded(photo.id);
3824                    };
3825                    img.src = photo.url;
3826                });
3827            },
3828            mark_photo_loaded( id ){
3829                if( !id ){
3830                    return;
3831                }
3832                let next = Object.assign({}, this.loaded_photo_ids);
3833                next[id] = true;
3834                this.loaded_photo_ids = next;
3835            },
3836            photo_has_loaded( photo ){
3837                if( this.is_admin ){
3838                    return true;
3839                }
3840                if( !photo || !photo.id ){
3841                    return false;
3842                }
3843                if( this.returnIsVideo(photo.file_name) ){
3844                    return true;
3845                }
3846                return !!this.loaded_photo_ids[photo.id];
3847            },
3848        },
3849        computed: {
3850            photo_album_obj(){
3851                if (this.photo_album && typeof this.photo_album === 'string'){
3852                    return JSON.parse(unescape(decodeURIComponent(this.photo_album)));
3853                } else if (this.photo_album){
3854                    return this.photo_album;
3855                } 
3856                return;
3857            },
3858            photo_album_id(){
3859                let album = this.photo_album_obj;
3860                if( !album ){
3861                    return null;
3862                }
3863                return album.id || null;
3864            },
3865            skeleton_tile_count(){
3866                let per_row = Number(this.photos_per_row);
3867                if( !per_row || per_row < 1 ){
3868                    return 4;
3869                }
3870                return per_row;
3871            },
3872            photo_album_name(){
3873                return this.photo_album_obj.name || null;
3874            },
3875        },
3876        watch: {
3877            last_update(newVal, oldVal){
3878                if (newVal !== oldVal){
3879                    this.load_photo_albums( this.client_id );
3880                }
3881            }
3882        }
3883    })
3884
3885</script>
3885    
3885<script type="text/html" id="template-membership-calendar">
3886     <!-- calendar component -->
3887    <div v-if="current_date"class="lightBorderTop hundop">
3888        <calendar-booking-component
3889            ref="bookingCalendar"
3890            :is_admin="false"
3891            :rerender_key="rerender_key"
3892            :calendar_type="'web_component'"
3893            :default_calendar_tags="formatted_event_types"
3894            :selected_tags="selected_tags"
3895            :events="filtered_events"
3896            :view_preference="view_preference"
3897            :current_date="current_date"
3898            :calendar_tags="calendar_tags"
3899            :timezone="timezone"
3900            :height="'initial'"
3901            :filter_logic="filter_logic"
3902            :loading_parent="loading"
3903            :ui_tag_ids_default="ui_tag_ids_default"
3904            :ui_tag_ids_custom="ui_tag_ids_custom"
3905            :disable_filtering="!allow_user_filtering"
3906            @change_date="change_date"
3907            @change_view_preference="change_view_preference"
3908            @clicked_default_tag="clicked_default_tag"
3909            @clicked_calendar_tag="clicked_calendar_tag"
3910            @clicked_event="clicked_event"
3911            @clicked_show_more_events="clicked_show_more_events"
3912            @zoom_in_to_date="zoom_in_to_date"
3913            @add_event="clicked_day_cell"
3914            @isolate_default_tag="isolate_default_tag"
3915            @select_all_default_tags="select_all_default_tags"
3916            @clear_all_default_tags="clear_all_default_tags"
3917            @select_visible_default_tags="select_visible_default_tags"
3918            @clear_visible_default_tags="clear_visible_default_tags"
3919            @isolate_calendar_tag="isolate_calendar_tag"
3920            @select_all_custom_tags="select_all_custom_tags"
3921            @clear_all_custom_tags="clear_all_custom_tags"
3922            @select_visible_custom_tags="select_visible_custom_tags"
3923            @clear_visible_custom_tags="clear_visible_custom_tags"
3924            @unselect_all_custom_tags="unselect_all_custom_tags"
3925        />
3926    </div>
3927</script>
3927
3928
3929<script>    
3930    /* ----- STORE ----- */
3931    store.components['membership_cal'] = {
3932        clubId: null,
3933        config_obj: {},
3934        // new 
3935        rerender_key: 0,
3936        current_date: null,
3937        view_preference: "Month",
3938        events: [],
3939        loading: {
3940            calendar: false, 
3941            calendar_tags: false,
3942            ui_refresh: false,
3943        }, 
3944        ui_tag_ids_default: {}, //used to track which tags are selected in the UI for default tags
3945        ui_tag_ids_custom: {},
3946        selected_tags: {
3947            default: [],
3948            custom: [],
3949        },
3950        calendar_tags: [], 
3951    };
3952    /* ----- COMPONENT ----- */
3953    let membership_calendar_config = {
3954        template: '#template-membership-calendar',
3955        data() {
3956            return store.components['membership_cal'];
3957        },
3958        props: {
3959            config: String,
3960            client_id: String,
3961            class_string: String,
3962            style_string: String,
3963            //admin
3964            component: Object,
3965            last_update: Number,
3966            tz: String,
3967        },
3968        mounted() {
3969            if (this.config){
3970                try {
3971                    let raw_config = this.config;
3972                    try {
3973                        raw_config = decodeURIComponent(raw_config);
3974                    } catch (decode_error) {
3975                        // Already plain JSON from older markup.
3976                    }
3977                    this.config_obj = JSON.parse(raw_config);
3978                } catch (parse_error) {
3979                    this.config_obj = {};
3980                }
3981            }
3982            this.clubId = $('body').data('club-id'); 
3983            // this.load_membership_cal(this.client_id);
3984            // Use component prop to determine if admin flow
3985            this.is_admin = !!this.component;
3986            // this.checkThenHandleEventTagsForCalendar(this.client_id);
3987            // //eventBus for overflow-overlay
3988            // store.eventBus.$on('clickSchedule_membership_cal', (event) => {
3989            //     this.clickSchedule(event)
3990            // })
3991
3992            //******* NEW CALENDAR BELOW *******//
3993            if(!this.current_date){
3994                this.current_date = moment.tz( this.timezone );
3995            }
3996            this.load_calendar();
3997            this.set_tags();
3998            // this.handle_filter_options();
3999            this.load_event_tags();
4000            this.get_timezone();
4001        },
4002        methods: {
4003            get_timezone(){
4004                var promise = Parse.Promise.as().then(() => {
4005                    var query = new Parse.Query("clubs");
4006                    query.select("timezone");
4007                    return query.get(this.clubId);
4008                }).then((clubObj) => {
4009                    if(clubObj){
4010                        store.clubObject = clubObj;
4011                    }
4012                    return Parse.Promise.as();
4013                }).catch((error) => {
4014                    checkParseError(error);
4015                    displayMessage("An error occurred while loading the club timezone: " + error.message, true, 5000);
4016                });
4017            },
4018            load_calendar(){
4019                this.loading.calendar = true;
4020                var promise = Parse.Promise.as();
4021                var events = [];
4022                var camps = [];
4023                var regattas = [];
4024                var group_classes = [];
4025                var recurring_events = [];
4026                var thisPromise = promise.then(() => {
4027                    var promises = [];
4028                    //if admin configured filter_tag_ids, we need to load all event types
4029                    //so we can match events that have those tags regardless of type
4030                    var should_load_all_types = this.filter_tag_ids && this.filter_tag_ids.length > 0;
4031                    //social events
4032                    var primary_promise = promise.then(()=>{
4033                        //see if we want these event types
4034                        if( ! this.event_types.includes('socials') && ! should_load_all_types ){
4035                            //not retrieving social events
4036                            return Promise.resolve({
4037                                formatted_events: [],
4038                                recurring_events: [],
4039                            });
4040                        }
4041                        var params = {};
4042                        var { startTimestamp, endTimestamp } = this.getCalendarRange();
4043                        params["club_id"] = $('body').data('club-id');
4044                        params["start_timestamp"] = startTimestamp;
4045                        params["end_timestamp"] = endTimestamp;
4046                        params["flow"] = "member_portal";
4047                        params["is_admin"] = false;
4048                        return Parse.Cloud.run('retrieve_social_events_for_calendar_v2', params);
4049                    }).then((retrievedEvents) => {
4050                        events = retrievedEvents.formatted_events || [];
4051                        recurring_events = retrievedEvents.recurring_events || [];
4052                        return Parse.Promise.as();
4053                    });
4054                    promises.push(primary_promise);
4055                    // load camps
4056                    var camps_promise = promise.then(()=>{
4057                        //see if we want these event types
4058                        if( ! this.event_types.includes('camps') && ! should_load_all_types ){
4059                            //not retrieving camps
4060                            return Promise.resolve([]);
4061                        }
4062                        var params = {};
4063                        var { startTimestamp, endTimestamp } = this.getCalendarRange();
4064                        params["club_id"] = $('body').data('club-id');
4065                        params["start_timestamp"] = startTimestamp;
4066                        params["end_timestamp"] = endTimestamp;
4067                        params["is_admin"] = false;
4068                        return Parse.Cloud.run('retrieve_camps_for_calendar_v2', params);
4069                    }).then((retrievedCamps) => {
4070                        camps = retrievedCamps;
4071                        return Parse.Promise.as();
4072                    });
4073                    promises.push(camps_promise);
4074                    // load regattas
4075                    var regattas_promise = promise.then(()=>{
4076                        //see if we want these event types
4077                        if( this.hide_regattas ){
4078                            return Promise.resolve([]);
4079                        }
4080                        if( ! this.event_types.includes('regattas') && ! should_load_all_types ){
4081                            //not retrieving regattas
4082                            return Promise.resolve([]);
4083                        }
4084                        var params = {};
4085                        var { startTimestamp, endTimestamp } = this.getCalendarRange();
4086                        params["club_id"] = $('body').data('club-id');
4087                        params["start_timestamp"] = startTimestamp;
4088                        params["end_timestamp"] = endTimestamp;
4089                        params["flow"] = "member_portal";
4090                        return Parse.Cloud.run('retrieve_regattas_for_calendar_v2', params);
4091                    }).then((retrievedRegattas) => {
4092                        regattas = retrievedRegattas;
4093                        return Parse.Promise.as();
4094                    });
4095                    promises.push(regattas_promise);
4096                    // load group classes
4097                    var group_classes_promise = promise.then(()=>{
4098                        if( ! this.event_types.includes('group_classes') && ! should_load_all_types ){
4099                            return Promise.resolve([]);
4100                        }
4101                        var params = {};
4102                        var { startTimestamp, endTimestamp } = this.getCalendarRange();
4103                        params["club_id"] = $('body').data('club-id');
4104                        params["start_timestamp"] = startTimestamp;
4105                        params["end_timestamp"] = endTimestamp;
4106                        params["is_admin"] = false;
4107                        return Parse.Cloud.run('retrieve_group_classes_for_calendar_v2', params);
4108                    }).then((retrievedGroupClasses) => {
4109                        group_classes = retrievedGroupClasses || [];
4110                        return Parse.Promise.as();
4111                    });
4112                    promises.push(group_classes_promise);
4113                    //return them in parallel
4114                    return Promise.all(promises);
4115                }).then(() => {
4116                    this.events = [];
4117                    this.events = [ ...events, ...camps, ...regattas, ...group_classes, ...recurring_events ];
4118                    this.loading.calendar = false;
4119                }).catch((error) => {
4120                    checkParseError(error);
4121                    displayMessage("An error occurred while loading the calendar: " + error.message, true, 5000);
4122                    this.loading.calendar = false;
4123                    this.events = [];
4124                });
4125                return thisPromise;
4126            },
4127            load_event_tags(){
4128                this.loading.calendar_tags = true;
4129                var promise = Parse.Promise.as();
4130                var thisPromise = promise.then(() => {
4131                    var query = new Parse.Query("event_tags");
4132                    query.equalTo("clubObject", {
4133                        __type: "Pointer",
4134                        objectId: $('body').data('club-id'),
4135                        className: "clubs"
4136                    });
4137                    query.equalTo("archived", false);
4138                    query.notEqualTo("internal_only", true);
4139                    query.limit( 1000 );
4140                    query.ascending("name");
4141                    return query.find();
4142                }).then((retrievedTags) => {
4143                    //if admin restricted to specific tags, only expose those in the user filter UI
4144                    if( this.filter_tag_ids && this.filter_tag_ids.length > 0 ){
4145                        retrievedTags = retrievedTags.filter(tag => this.filter_tag_ids.indexOf(tag.id) !== -1);
4146                    }
4147                    this.calendar_tags = retrievedTags;
4148                    //now set all of them to selected
4149                    this.ui_tag_ids_custom = {};
4150                    this.calendar_tags.forEach((tag) => {
4151                        this.ui_tag_ids_custom[tag.id] = true;
4152                    });
4153                    this.selected_tags.custom = this.calendar_tags.map(t => t.id);
4154                    this.loading.calendar_tags = false;
4155                }).catch((error) => {
4156                    checkParseError(error);
4157                    displayMessage("An error occurred while loading calendar tags: " +error.message, true, 5000);
4158                    this.loading.calendar_tags = false;
4159                    this.calendar_tags = [];
4160                    this.loading.calendar = false;
4161                });
4162                return thisPromise;
4163            },
4164            set_tags(){
4165                //select all event types by default
4166                //options: "camp", "regatta", "social", "calendar_event"
4167                this.selected_tags.default = [];
4168                this.selected_tags.custom = []; 
4169                let selected = [];
4170                //check what event types are available
4171                let event_types = this.event_types || [];
4172                if( event_types.indexOf('camps') !== -1  ) {
4173                   selected.push('camp');
4174                }
4175                if( event_types.indexOf('regattas') !== -1 ) {
4176                    selected.push('regatta');
4177                }
4178                if( event_types.indexOf('socials') !== -1 ) {
4179                    selected.push('social');
4180                    selected.push('calendar_event');
4181                }
4182                if( event_types.indexOf('group_classes') !== -1 ) {
4183                    selected.push('group_class');
4184                }
4185                this.selected_tags.default = selected;
4186                //and update the ui tags
4187                this.ui_tag_ids_default = {};
4188                this.selected_tags.default.forEach((id) => {
4189                    this.ui_tag_ids_default[id] = true;
4190                });
4191            },
4192            getCalendarRange() {
4193                let startTimestamp, endTimestamp;
4194                if (this.view_preference === "Month") {
4195                    // Start and end of the previous and next months
4196                    startTimestamp = moment(this.current_date).startOf("month").subtract(1, "month").valueOf();
4197                    endTimestamp = moment(this.current_date).endOf("month").add(1, "month").valueOf();
4198                } else if (this.view_preference === "Week") {
4199                    // Start and end of the previous and next 2 weeks
4200                    startTimestamp = moment(this.current_date).startOf("isoWeek").subtract(2, "week").valueOf();
4201                    endTimestamp = moment(this.current_date).endOf("isoWeek").add(2, "week").valueOf();
4202                } else if (this.view_preference === "Day") {
4203                    // Start and end of the previous and next 2 weeks
4204                    //.. important to do a wide enough range so that multi-day events are included
4205                    startTimestamp = moment(this.current_date).startOf("isoWeek").subtract(2, "week").valueOf();
4206                    endTimestamp = moment(this.current_date).endOf("isoWeek").add(2, "week").valueOf();
4207                }
4208
4209                return { startTimestamp, endTimestamp };
4210            },
4211            change_date( new_date ){
4212                this.current_date = new_date;
4213                this.load_calendar();
4214            },
4215            change_view_preference( new_value ){
4216                this.view_preference = new_value;
4217                this.load_calendar();
4218            },
4219            clicked_day_cell(date_components){
4220                // if its month view, and we are on mobile (width < 720), go to day view
4221                if(this.view_preference === "Month" && window.innerWidth < 720){
4222                     //set the current date to the clicked date
4223                    var clicked_date = moment.tz({
4224                        year: date_components.year,
4225                        month: date_components.month,
4226                        date: date_components.date, 
4227                    }, this.timezone);
4228                    this.zoom_in_to_date( clicked_date );
4229                }
4230            },
4231            zoom_in_to_date( date ){
4232                this.change_view_preference( "Day" );
4233                this.current_date = date;
4234            },
4235            clicked_default_tag(tag){
4236                this.loading.ui_refresh = true;
4237                //update the ui
4238                if(this.ui_tag_ids_default[tag.id] === undefined){
4239                    //if the tag is not in the ui_tag_ids_default, add it
4240                    this.ui_tag_ids_default[tag.id] = true;
4241                }else{
4242                    this.ui_tag_ids_default[tag.id] = !this.ui_tag_ids_default[tag.id];
4243                }
4244                requestAnimationFrame(() => {
4245                    // second RAF helps ensure we’re past layout/paint on Chrome/Safari
4246                    requestAnimationFrame(() => {
4247                        const list = this.selected_tags.default;
4248                        const idx = list.indexOf(tag.id);
4249                        // create a new array to avoid deep reactive churn
4250                        this.selected_tags.default = idx !== -1
4251                            ? list.filter(t => t !== tag.id)
4252                            : [...list, tag.id];
4253                        this.loading.ui_refresh = false;
4254                    });
4255                });     
4256            },
4257            clicked_calendar_tag(tag){
4258                this.loading.ui_refresh = true;
4259                //update the ui
4260                if(this.ui_tag_ids_custom[tag.id] === undefined){
4261                    //if the tag is not in the ui_tag_ids_custom, add it
4262                    this.ui_tag_ids_custom[tag.id] = true;
4263                }else{
4264                    this.ui_tag_ids_custom[tag.id] = !this.ui_tag_ids_custom[tag.id];
4265                }
4266                requestAnimationFrame(() => {
4267                    // second RAF helps ensure we’re past layout/paint on Chrome/Safari
4268                    requestAnimationFrame(() => {
4269                        const list = this.selected_tags.custom;
4270                        const idx = list.indexOf(tag.id);
4271                        // create a new array to avoid deep reactive churn
4272                        this.selected_tags.custom = idx !== -1
4273                            ? list.filter(t => t !== tag.id)
4274                            : [...list, tag.id];
4275                        this.loading.ui_refresh = false;
4276                    });
4277                });   
4278            },
4279             isolate_default_tag(tag){
4280                //set the ui check boxes
4281                this.ui_tag_ids_default = {};
4282                this.ui_tag_ids_default[tag.id] = true;
4283                this.ui_tags_
4284                //set the selected tags to just this one
4285                this.selected_tags.default = [tag.id];   
4286            },
4287            select_all_default_tags(){
4288                //set the ui check boxes
4289                this.ui_tag_ids_default = {};
4290                var event_types = ["camp", "regatta", "social", "banquet_event", "calendar_event", "group_class"];
4291                if(store.clubObject.get('hide_regattas')){
4292                    event_types = event_types.filter(type => type !== 'regatta');
4293                }
4294                event_types.forEach((type) => {
4295                    this.ui_tag_ids_default[type] = true;
4296                });
4297                //set the selected tags to all default tags
4298                this.selected_tags.default = event_types;
4299            },
4300            clear_all_default_tags(){
4301                this.ui_tag_ids_default = {};
4302                this.selected_tags.default = [];
4303            },
4304            select_visible_default_tags(visibleIds){
4305                if (!visibleIds || !visibleIds.length) return;
4306                var current = this.selected_tags.default || [];
4307                var next = current.slice();
4308                visibleIds.forEach(function(id){ if (next.indexOf(id) === -1) next.push(id); });
4309                this.selected_tags.default = next;
4310                next.forEach(function(id){ this.ui_tag_ids_default[id] = true; }.bind(this));
4311            },
4312            clear_visible_default_tags(visibleIds){
4313                if (!visibleIds || !visibleIds.length) return;
4314                var set = {};
4315                visibleIds.forEach(function(id){ set[id] = true; });
4316                var current = this.selected_tags.default || [];
4317                this.selected_tags.default = current.filter(function(id){ return !set[id]; });
4318                this.ui_tag_ids_default = {};
4319                (this.selected_tags.default || []).forEach(function(id){ this.ui_tag_ids_default[id] = true; }.bind(this));
4320            },
4321            isolate_calendar_tag(tag){
4322                //set the ui check boxes
4323                this.ui_tag_ids_custom = {};
4324                this.ui_tag_ids_custom[tag.id] = true;
4325                //set the selected tags to just this one
4326                this.selected_tags.custom = [tag.id];         
4327            },
4328            select_all_custom_tags(){
4329                //set the ui check boxes
4330                this.ui_tag_ids_custom = {};
4331                this.calendar_tags.forEach((tag) => {
4332                    this.ui_tag_ids_custom[tag.id] = true;
4333                });
4334                //set the selected tags to all custom tags
4335                this.selected_tags.custom = this.calendar_tags.map((tag) => tag.id);
4336            },
4337            unselect_all_custom_tags(){
4338                //used for the "catch all" option
4339                //set the ui check boxes
4340                this.ui_tag_ids_custom = {};
4341                //set the selected tags to no custom tags
4342                this.selected_tags.custom = [];
4343            },
4344            clear_all_custom_tags(){
4345                this.ui_tag_ids_custom = {};
4346                this.selected_tags.custom = [];
4347            },
4348            select_visible_custom_tags(visibleIds){
4349                if (!visibleIds || !visibleIds.length) return;
4350                var tagIds = visibleIds.filter(function(id){ return id !== 'catch_all_custom'; });
4351                var current = this.selected_tags.custom || [];
4352                var next = current.slice();
4353                tagIds.forEach(function(id){ if (next.indexOf(id) === -1) next.push(id); });
4354                this.selected_tags.custom = next;
4355                next.forEach(function(id){ this.ui_tag_ids_custom[id] = true; }.bind(this));
4356            },
4357            clear_visible_custom_tags(visibleIds){
4358                if (!visibleIds || !visibleIds.length) return;
4359                var set = {};
4360                visibleIds.forEach(function(id){ set[id] = true; });
4361                var current = this.selected_tags.custom || [];
4362                this.selected_tags.custom = current.filter(function(id){ return !set[id]; });
4363                this.ui_tag_ids_custom = {};
4364                (this.selected_tags.custom || []).forEach(function(id){ this.ui_tag_ids_custom[id] = true; }.bind(this));
4365            },
4366            clicked_event(event){
4367                var is_recurring_event = event.is_recurring;
4368                // open detail view
4369                if(is_recurring_event) {
4370                    // if(event.external_event_url) {
4371                    //     //its external, just pop open the window
4372                    //     return window.open(event.external_event_url);
4373                    // }
4374                    // var params = {
4375                    //     event_json: event,
4376                    // };
4377                    show_event_details_in_member_cal({ event_json: event });
4378                } else if(event) {
4379                    if(event.regatta_subevent) { 
4380                        //go to microsite
4381                        var regatta_id = event.parent_regatta_id;
4382                        window.open(`/regatta/${ regatta_id }`);
4383                    } else if(event.type == 'regatta'){
4384                        if(event.external_regatta_url){
4385                            window.open(event.external_regatta_url);
4386                        } else {
4387                            window.open(`/regatta/${ event.id }`);
4388                        }
4389                    } else if(event.type == 'camp'){
4390                        //go to registration
4391                        window.open(`/register/camp/${ event.id }/class`);
4392                    } else if(event.type === 'group_class'){
4393                        var timestamp = event.ts;
4394                        var dateComponents = return_date_components_v2({
4395                            timezone: this.timezone,
4396                            date: new Date(timestamp)
4397                        });
4398                        var dateString = dateComponents.year + '-' + (dateComponents.month + 1) + '-' + dateComponents.date;
4399                        let event_json = {
4400                            ...event,
4401                            external_event_url: '/group-classes?date_string=' + dateString,
4402                        };
4403                        let params = {
4404                            event_json: event_json,
4405                        };
4406                        show_event_details_in_member_cal(params);
4407                    } else {
4408                        // if(event.external_event_url){
4409                        //     window.open(event.external_event_url);
4410                        // } else
4411                        if(event.type === 'social') {
4412                            //not calendar only  ... go to the event page 
4413                            window.open(`/event/${ event.id }`);
4414                        } else {
4415                            //calendar event
4416                            var params = {
4417                                event_json: event,
4418                            };
4419                            show_event_details_in_member_cal( params );
4420                        }
4421                    }
4422                }
4423            },
4424        },
4425        computed: {
4426            event_types(){
4427                if(this.is_admin) {
4428                    return this.component?.get('config_latest').event_types || [];
4429                } else {
4430                    return this.config_obj.event_types || [];
4431                }
4432            },
4433            filter_tag_ids(){
4434                if(this.is_admin) {
4435                    return this.component?.get('config_latest').filter_tag_ids || [];
4436                } else {
4437                    return this.config_obj.filter_tag_ids || [];
4438                }
4439            },
4440            allow_user_filtering(){
4441                //defaults to true for legacy components where the field is not set
4442                var raw;
4443                if(this.is_admin) {
4444                    raw = this.component?.get('config_latest').allow_user_filtering;
4445                } else {
4446                    raw = this.config_obj.allow_user_filtering;
4447                }
4448                return raw !== false;
4449            },
4450            filter_logic(){
4451                //when admin restricts the calendar to a set of event tags, an event
4452                //should be visible if it matches the type OR has a matching tag (per
4453                //admin filter rules), so we use OR logic on the user-facing filter
4454                //to avoid hiding events that admin intends to show.
4455                if( this.filter_tag_ids && this.filter_tag_ids.length > 0 ){
4456                    return 'or';
4457                }
4458                return 'and';
4459            },
4460            event_type_setting_for_event(){
4461                //map the actual event.type strings to the toggle keys used in event_types config
4462                return {
4463                    'camp': 'camps',
4464                    'regatta': 'regattas',
4465                    'social': 'socials',
4466                    'calendar_event': 'socials',
4467                    'group_class': 'group_classes',
4468                };
4469            },
4470            filtered_events(){
4471                var event_types = this.event_types || [];
4472                var filter_tag_ids = this.filter_tag_ids || [];
4473                //if neither types nor tags are configured, fall back to whatever was loaded
4474                if( event_types.length === 0 && filter_tag_ids.length === 0 ){
4475                    return this.events;
4476                }
4477                var type_map = this.event_type_setting_for_event;
4478                return this.events.filter(event => {
4479                    var setting_key = type_map[event.type];
4480                    var type_in_selected = setting_key && event_types.indexOf(setting_key) !== -1;
4481                    if( filter_tag_ids.length === 0 ){
4482                        //no tag filter configured: only show events whose type is selected
4483                        return type_in_selected;
4484                    }
4485                    //tags are configured: an event is shown if its type is selected
4486                    //(regardless of tags applied) OR it has any of the selected tags
4487                    if( type_in_selected ){
4488                        return true;
4489                    }
4490                    var event_tag_ids = event.event_tag_ids || [];
4491                    return event_tag_ids.some(id => filter_tag_ids.indexOf(id) !== -1);
4492                });
4493            },
4494            formatted_event_types(){
4495                this.rerender_key; //depend on this to force recompute when it changes
4496                var event_types = this.event_types || [];
4497                // Updated vibrant, distinct colors of similar intensity to #853fc6
4498                var options = [];
4499                var camp_option =  {
4500                    key: "camp",
4501                    label: "Camps",
4502                    id: "camp",
4503                    color: "#236651" 
4504                };
4505                var social_option = {
4506                    key: "social",
4507                    label: "Social events",
4508                    id: "social",
4509                    color: "#ffa69e"
4510                };
4511                var calendar_event_option = {
4512                    key: "calendar_event",
4513                    label: "Other",
4514                    id: "calendar_event",
4515                    color: "#e1e7ea"
4516                };
4517                if( event_types.includes('camps') ) {
4518                    options.push(camp_option);
4519                }
4520                //respect hide regattas on club object
4521                if( event_types.includes('regattas') && ! this.hide_regattas ) {
4522                    var regatta_option = {
4523                        key: "regatta",
4524                        label: "Regattas",
4525                        id: "regatta",
4526                        color: "#87d4f9"
4527                    };
4528                    options.push(regatta_option);
4529                }
4530                if( event_types.includes('socials') ) {
4531                    options.push(social_option);
4532                    options.push(calendar_event_option);
4533                }
4534                if( event_types.includes('group_classes') ) {
4535                    options.push({
4536                        key: "group_class",
4537                        label: "Group classes",
4538                        id: "group_class",
4539                        color: "#5c7c99"
4540                    });
4541                }
4542                return {
4543                    title: "Type",
4544                    options: options
4545                };
4546            },
4547            hide_regattas(){
4548                return store.clubObject && store.clubObject.get('hide_regattas');
4549            },
4550            timezone(){
4551                if(this.tz){
4552                    return this.tz;
4553                }
4554                return store.clubObject ? store.clubObject.get('timezone') : moment.tz.guess();
4555            }
4556
4557        },
4558        watch: {
4559            last_update(newVal, oldVal){
4560                if (newVal !== oldVal){
4561                    if (this.config){
4562                        try {
4563                            let raw_config = this.config;
4564                            try {
4565                                raw_config = decodeURIComponent(raw_config);
4566                            } catch (decode_error) {
4567                                // Already plain JSON from older markup.
4568                            }
4569                            this.config_obj = JSON.parse(raw_config);
4570                        } catch (parse_error) {
4571                            this.config_obj = {};
4572                        }
4573                    }
4574                    this.rerender_key = generateClientID(); //force update of event types included
4575                    this.clubId = $('body').data('club-id'); 
4576                    this.load_calendar();
4577                    //re-filter the user-facing tag list in case admin changed filter_tag_ids
4578                    this.load_event_tags();
4579                    this.is_admin = !!this.component;
4580                }
4581            }
4582        },
4583    }
4584    app_content.component('membership-calendar-component', membership_calendar_config);
4585    app_overlays.component('membership-calendar-component', membership_calendar_config);
4586</script>
4586    
4586<script type="text/html" id="template-membership-directory">
4587    <div class="cs-background-image relative" :class="class_string" :style="style_string">
4588        <template v-if="formatting && formatting.overlay_opacity">
4589            <div class="cs-image-overlay" :style="{ 'opacity': formatting.overlay_opacity.toString() }"></div>
4590        </template>
4591        <div class="element-wrapper" :class="'element-wrapper_' + component_type" :style="element_style_string">
4592            <div class="membership-directory-wrap centeredBlock centeredText" :data-client-id="client_id">
4593                <div class="ease flexNoWrap show_when_loaded">
4594                    <div class="inputWrap searchBarWrap flexGrowOne" style="min-width:100%;">
4595                        <input class="searchBar easeFast" placeholder="Search members..." v-model="search">
4596                    </div>
4597                    <div class="flexGrowOne"></div>
4598                </div> 
4599                <div v-if="!loading && member_tags?.length >= 1" class="scrollable posScrollbar lightBorderTop noPaddingBottom">
4600                    <div class="flexNoWrap insert_member_tags" style="padding:15px;">
4601                        <div :class="{ 'active' : selectedTag === 'all' }" class="cs-tab-bar-option cs-tab-bar-option_member_tags cs-tab-bar-option_member_tags_all mobile-order" data-option="all" @click="handleTagSelection( 'all' )">All members</div>
4602                        <template v-for="tag in member_tags" :key="tag.id">
4603                            <div :class="{ 'active' : selectedTag === tag.id }" class="cs-tab-bar-option cs-tab-bar-option_member_tags mobile-order" :data-option="tag.id" @click="handleTagSelection( tag.id )">{{ tag.get('name') }}</div>
4604                        </template>
4605                    </div>
4606                </div>
4607                <!-- <div class="show_when_loaded table_markup_insert"> -->
4608                <div class="lightBorderTop scrollable fff noPadding">
4609                <table v-if="!loading && filtered_members?.length >= 1 && configurable_columns.length >= 2" class="tableInsert hundop">
4610                    <tbody>
4611                        <tr class="tableHeaderRow" :class="{ 'ascending' : sortObject.order == 'ascending' }">
4612                            <template v-for="column in configurable_columns">
4613                                <th v-if="!Array.isArray(column.headerName) && !column.headerClasses.includes('sortable')" :class="column.headerClasses" :style="column.headerStyle">{{ column.headerName }}</th>
4614                                <th v-if="!Array.isArray(column.headerName) && column.headerClasses.includes('sortable')" 
4615                                    :style="column.headerStyle" 
4616                                    :data-db-key="column['data-db-key']"
4617                                    @click="clicked_sort(column['data-db-key'])"
4618                                    :class="[column.headerClasses, { 'sorted': sortObject.key === column['data-db-key'] }]"
4619                                    >
4620                                        {{ column.headerName }}
4621                                        <div class="sort-arrow"></div>
4622                                </th>
4623                                <template v-for="headerName in column.headerName">
4624                                    <th v-if="Array.isArray(column.headerName)" :class="column.headerClasses">{{ headerName }}</th>
4625                                </template>
4626                            </template>
4627                        </tr>
4628                        <!-- member rows injected here -->
4629                        <template v-for="member in filtered_members" :key="member.id">
4630                            <tr 
4631                                v-if="check_archive_status( member )"
4632                                class="tableRow pointer" 
4633                                :class="`member-row_${member.id || member.objectId}`"
4634                                @click="clicked_member_row( member )"
4635                            >
4636                                <!-- photo column -->
4637                                <td v-if="check_configurable_classes('column_photo')" class="lessPadding column_photo first-cell" style="padding-right:0px;">
4638                                    <div v-if="member && member.get('profPicURL')" class="smallIcon noMargin noBorder" :style="{ backgroundImage: 'url(' + member.get('profPicURL') + ')' }"></div>
4639                                    <div v-else 
4640                                        class="smallIcon noMargin noBorder flexWrap align-items-center" 
4641                                        style="padding-right:0px;
4642                                        background-color: #f1f5fa;
4643                                        text-align: center;
4644                                        font-size: 16px;"
4645                                    >
4646                                        <i class="fa-regular fa-user"></i>
4647                                    </div>
4648                                </td>
4649                                <!-- firstName column -->
4650                                <td v-if="check_configurable_classes('firstName')">
4651                                    <p v-if="(member && member.get('firstName')) && !member.get('nickname')" class="capitalize">{{ member.get('firstName') }}</p>
4652                                    <p v-if="(member && member.get('firstName')) && member.get('nickname')" class="capitalize">{{ member.get('firstName') }} ({{ member.get('nickname') }})</p>
4653                                    <p v-if="member && !member.get('firstName')" class="incomplete">None</p>
4654                                </td>
4655                                <!-- lastName column -->
4656                                <td v-if="check_configurable_classes('lastName')">
4657                                    <p v-if="(member && member.get('lastName'))" class="capitalize">{{ member.get('lastName') }}</p>
4658                                    <p v-else class="incomplete">None</p>
4659                                </td>
4660                                <!-- member number column -->
4661                                <td v-if="check_configurable_classes('column_member_number')" class="column_member_number">
4662                                    <p v-if="(member && member.get('membershipObject')) && member.get('membershipObject').get('memberNumber')" class="uppercase">{{ member.get('membershipObject').get('memberNumber') }}</p>
4663                                    <p v-else class="incomplete">None</p>
4664                                </td>
4665                                <!-- email column -->
4666                                <td v-if="check_configurable_classes('column_email')" class="column_email">
4667                                    <p v-if="member && member.get('email')"><a @click.stop :href="'mailto:' + member.get('email')">{{ member.get('email')}}</a></p>
4668                                    <p v-else class="incomplete">None</p>
4669                                </td>
4670                                <!-- mobile column -->
4671                                <td v-if="check_configurable_classes('column_mobile')" class="column_mobile">
4672                                    <p v-if="member && member.get('mobile')"><a @click.stop :href="'tel:' + member.get('mobile')">{{ member.get('mobile')}}</a></p>
4673                                    <p v-else class="incomplete">None</p>
4674                                </td>
4675                                <!-- home phone column -->
4676                                <td v-if="check_configurable_classes('column_phone_home')" class="column_phone_home">
4677                                    <p v-if="member && member.get('phone_home')"><a @click.stop :href="'tel:' + member.get('phone_home')">{{ member.get('phone_home')}}</a></p>
4678                                    <p v-else class="incomplete">None</p>
4679                                </td>
4680                                <!-- member tags column -->
4681                                <td v-if="check_configurable_classes('column_member_tags')" class="column_member_tags">
4682                                    <div class="flexWrapLeftAlign" v-html="return_member_tags( member ) || ''"></div>
4683                                </td>
4684                                <!-- ghin number column -->
4685                                <td v-if="check_configurable_classes('column_ghin_number')" class="column_ghin_number">
4686                                    <p v-if="member && member.get('ghin_number')">{{ member.get('ghin_number') }}</p>
4687                                    <p v-else class="incomplete">None</p>
4688                                </td>
4689                                <!-- address ( street, city, state, zip) columns -->
4690                                <template v-if="check_configurable_classes('column_address')">
4691                                    <td class="column_address">
4692                                        <p v-if="member && member.get('street')">{{ member.get('street') }}</p>
4693                                        <p v-else class="incomplete">None</p>
4694                                    </td>
4695                                    <td class="column_address">
4696                                        <p v-if="member && member.get('city')">{{ member.get('city') }}</p>
4697                                        <p v-else class="incomplete">None</p>
4698                                    </td>
4699                                    <td class="column_address">
4700                                        <p v-if="member && member.get('state')">{{ member.get('state') }}</p>
4701                                        <p v-else class="incomplete">None</p>
4702                                    </td>
4703                                    <td class="column_address">
4704                                        <p v-if="member && member.get('zip')">{{ member.get('zip') }}</p>
4705                                        <p v-else class="incomplete">None</p>
4706                                    </td>
4707                                </template>
4708                                <!-- secondary address ( street, city, state, zip ) columns -->
4709                                <template v-if="check_configurable_classes('column_address_secondary')">
4710                                    <td class="column_address_secondary">
4711                                        <p v-if="member && member.get('street_secondary')">{{ member.get('street_secondary') }}</p>
4712                                        <p v-else class="incomplete">None</p>
4713                                    </td>
4714                                    <td class="column_address_secondary">
4715                                        <p v-if="member && member.get('city_secondary')">{{ member.get('city_secondary') }}</p>
4716                                        <p v-else class="incomplete">None</p>
4717                                    </td>
4718                                    <td class="column_address_secondary">
4719                                        <p v-if="member && member.get('state_secondary')">{{ member.get('state_secondary') }}</p>
4720                                        <p v-else class="incomplete">None</p>
4721                                    </td>
4722                                    <td class="column_address_secondary">
4723                                        <p v-if="member && member.get('zip_secondary')">{{ member.get('zip_secondary') }}</p>
4724                                        <p v-else class="incomplete">None</p>
4725                                    </td>
4726                                </template>
4727                                <!-- start date column -->
4728                                <td v-if="check_configurable_classes('column_start_date')" class="column_start_date">
4729                                    <p v-if="(member && member.get('membershipObject')) && member.get('membershipObject').get('startDate')">{{ format_utc_date( member.get('membershipObject').get('startDate') ) }}</p>
4730                                    <p v-else class="incomplete">None</p>
4731                                </td>
4732                                <!-- end date column -->
4733                                <td v-if="check_configurable_classes('column_end_date')" class="column_end_date">
4734                                    <p v-if="(member && member.get('membershipObject')) && member.get('membershipObject').get('endDate')">{{ format_utc_date( member.get('membershipObject').get('endDate')) }}</p>
4735                                    <p v-else class="incomplete">None</p>
4736                                </td>
4737                                <!-- renewal column -->
4738                                <td v-if="check_configurable_classes('column_renewed')" class="column_renewed">
4739                                    <p v-if="(member && member.get('membershipObject')) && member.get('membershipObject').get('startDate')">{{ format_utc_date( member.get('membershipObject').get('renewed') || member.get('membershipObject').get('startDate') ) }}</p>
4740                                    <p v-else class="incomplete">None</p>
4741                                </td>
4742                                <!-- category column -->
4743                                <td v-if="check_configurable_classes('column_category')" class="column_category">
4744                                    <p v-if="(member && member.get('membershipObject')) && member.get('membershipObject').get('tierObject')">{{ member.get('membershipObject').get('tierObject').get('name') }}</p>
4745                                    <p v-else class="incomplete">None</p>
4746                                </td>
4747                                <!-- plan column -->
4748                                <td v-if="check_configurable_classes('column_plan')" class="column_plan">
4749                                    <p v-if="(member && member.get('membershipObject')) && member.get('membershipObject').get('planObject')">{{ member.get('membershipObject').get('planObject').get('name') }}</p>
4750                                    <p v-else class="incomplete">None</p>
4751                                </td>
4752                            </tr>
4753                        </template>
4754                    </tbody>
4755                </table>
4756                </div>
4757                <!-- </div> -->
4758                <div v-if="loading" class="cardBodySpinnerWrap relative active">
4759                    <div class="cardBodySpinner centeredBlock"></div>
4760                </div>
4761                <p v-if="!loading && filtered_members.length == 0" class="noObjectsMSG withPadding">{{ return_no_results_msg() }}</p>
4762            </div>
4763        </div>
4764    </div>
4765</script>
4765
4766
4767<script>
4768
4769    function prepThenDisplayMembershipDirectoryPopup( memberObject ){
4770        //clear stuff
4771        $('#overlay_membership-directory-popup .insert').text('');
4772        //name
4773        var name = '';
4774        //check type
4775        if( memberObject.className == "boats" ){
4776            //actually a boat - we re-use the same overlay
4777            $('#overlay_membership-directory-popup .version_boat').removeClass('hidden');
4778            $('#overlay_membership-directory-popup .version_member').addClass('hidden');
4779            name = returnNameStringForBoat( memberObject );
4780            //handle documents
4781            $('.section-wrap_documents').find('.inline-value').remove();
4782            // var documents = $('.membership-directory-wrap').data('documents');
4783            var documents = store.components.membership_directory.documents;
4784            if( documents ){
4785                for (var i = 0; i <= documents.length - 1; i++) {
4786                    if( documents[i].get('boatObject') ){
4787                        if( documents[i].get('boatObject').id == memberObject.objectId ){
4788                            var this_name = documents[i].get('name');
4789                            if( this_name  && this_name.length >= 1 ){
4790                                $('#overlay_membership-directory-popup .section-wrap_documents').append('<p class="flexGrowOne inline-value tinyPaddingTop"><a class="tappedDocument" data-document-id="'+ documents[i].id +'" target="_blank" href="'+ documents[i].get('URL') +'">'+ this_name +' ></a></p>');
4791                            }
4792                        }
4793                    }
4794                }
4795            }
4796        }else{
4797            //ok, it's a member
4798            $('#overlay_membership-directory-popup .version_boat').addClass('hidden');
4799            $('#overlay_membership-directory-popup .version_member').removeClass('hidden');
4800            //name
4801            if( memberObject.get('firstName') ){
4802                name += memberObject.get('firstName');
4803                if( memberObject.get('nickname') ){
4804                    name += ' ('+ memberObject.get('nickname') + ')';
4805                }
4806            }
4807            if( memberObject.get('lastName') ){
4808                if( name.length >= 1 ){
4809                    name += ' ';
4810                }
4811                name += memberObject.get('lastName');
4812            }
4813        }
4814        $('#overlay_membership-directory-popup .insert_name').text(name);
4815        //prof pic
4816        var profPicURL = memberObject.get('profPicURL') || memberObject.get('imageURL'); //the or is if the "memberObject" is a boat because you clicked on a boat and are seeing boat details
4817        if( profPicURL ){
4818            $('#overlay_membership-directory-popup .profile-picture').text("");
4819            $('#overlay_membership-directory-popup .profile-picture').css('background-image', 'url('+profPicURL+')' );
4820            $('#overlay_membership-directory-popup .profile-background-inner').css('background-image', 'url('+profPicURL+')' );
4821        }else{
4822            $('#overlay_membership-directory-popup .profile-picture').css('background-image', '' );
4823            $('#overlay_membership-directory-popup .profile-background-inner').css('background-image', '');
4824            if( name.length >= 1 ){
4825                var initial = name.substr(0,1).toLowerCase();
4826                $('#overlay_membership-directory-popup .profile-picture').text( initial );
4827            }
4828        }
4829        //plug in data
4830        $('#overlay_membership-directory-popup .inline-value').each(function(){
4831            var key = $(this).data('db-key');
4832            if( key ){
4833                var value = sanitize_string(memberObject.get(key));
4834                if( value ){
4835                    if( key.toLowerCase().indexOf('email') >= 0 ){
4836                        //it's an email
4837                        $(this).html( '<a href="mailto:'+ value +'">'+ value +'</a>' );
4838                        $(this).removeClass( 'hidden' );
4839                    }else if( key.toLowerCase().includes('mobile') || key.toLowerCase().includes('phone') ){
4840                        //it's a mobile number
4841                        $(this).html( '<a href="tel:'+ returnCleanMobileNumber( value ) +'">'+ formatMobileNumber( value ) +'</a>' );
4842                        $(this).removeClass( 'hidden' );
4843                    }else if( $(this).data('prepend') ){
4844                        $(this).text( $(this).data('prepend') + " " + value );
4845                        $(this).removeClass( 'hidden' );
4846                    }else{
4847                        //not an email, mobile number, or special boat field
4848                        $(this).text( value );
4849                        $(this).removeClass( 'hidden' );
4850                    }
4851                }else{
4852                    $(this).text( "" ).addClass('hidden');
4853                    $(this).data( 'date', null );
4854                }
4855            }
4856        });
4857        //custom fields
4858        $('.section-wrap_custom_fields').html('');
4859        // var custom_fields = $('.membership-directory-wrap').data('custom_fields');
4860        var custom_fields = store.components.membership_directory.custom_fields;
4861        if( custom_fields ){
4862            //plug them in
4863            for (var i = 0; i <= custom_fields.length - 1; i++) {
4864                var response = returnResponseForCustomField( memberObject, custom_fields[i].id );
4865                if( response ){
4866                    $('.section-wrap_custom_fields').append('<h4 class="smallBlockLetters leftText smallMarginTop">'+ custom_fields[i].get('name') +'</h4>');
4867                    $('.section-wrap_custom_fields').append('<p class="inline-value dark tinyPaddingTop">'+ response +'</p>');
4868                }
4869            }
4870        }
4871        //family
4872        $('.section-wrap_family').find('.inline-value').remove();
4873        var membershipObject = memberObject.get('membershipObject');
4874        var membersArray = membershipObject.get('membersArray');
4875        var family_members = [];
4876        if( membersArray ){
4877            for (var i = 0; i <= membersArray.length - 1; i++) {
4878                if( membersArray[i].id != memberObject.id ){
4879                    var this_member = returnObjectByID( membersArray[i].id, store.components.membership_directory.allMembers );
4880                    if( this_member ){
4881                        family_members.push( this_member );
4882                    }
4883                }
4884            }
4885        }
4886        for (var i = 0; i <= family_members.length - 1; i++) {
4887            var this_name = '';
4888            if( family_members[i].get('firstName') ){
4889                this_name = family_members[i].get('firstName');
4890                if( family_members[i].get('lastName') ){
4891                    this_name += ' ';
4892                    this_name += family_members[i].get('lastName');
4893                }
4894            }
4895            if( this_name.length >= 1 ){
4896                $('#overlay_membership-directory-popup .section-wrap_family').append('<p class="flexGrowOne inline-value tinyPaddingTop"><a class="tappedFamilyMember" data-member-id="'+ family_members[i].id +'">'+ this_name +' ></a></p>');
4897            }
4898        }
4899        //boats
4900        $('.section-wrap_boats').find('.inline-value').remove();
4901        var boats_map = store.components.membership_directory.boats_map;
4902        if( boats_map ){
4903            var these_boats = boats_map[ membershipObject.id ];
4904            if( these_boats ){
4905                for (var i = 0; i <= these_boats.length - 1; i++) {
4906                    var this_name = returnNameStringForBoat( these_boats[i] );
4907                    if( this_name.length >= 1 ){
4908                        $('#overlay_membership-directory-popup .section-wrap_boats').append('<p class="flexGrowOne inline-value tinyPaddingTop"><a class="tappedBoat" data-boat-id="'+ these_boats[i].id +'" data-membership-id="'+ membershipObject.id +'">'+ this_name +' ></a></p>');
4909                    }
4910                }
4911            }
4912        }
4913        //continue
4914        $('#overlay_membership-directory-popup .section-wrap').each(function(){
4915            if( $(this).find('.inline-value').not( $('.hidden') ).length >= 1 ){
4916                $(this).removeClass('hidden');
4917            }else{
4918                $(this).addClass('hidden');
4919            }
4920        });
4921        //fade it in
4922        fadeIn('#overlay_membership-directory-popup');
4923    }
4924
4925    function openDirectoryProfileImage( el ){
4926        var bgImage = $(el).css('background-image');
4927        if( !bgImage || bgImage === 'none' ) return;
4928        var url = bgImage.replace(/^url\(["']?/, '').replace(/["']?\)$/, '');
4929        if( !url ) return;
4930        $('#directory-image-lightbox .directory-image-lightbox-img').attr('src', url);
4931        $('#directory-image-lightbox').addClass('active');
4932    }
4933
4934    function closeDirectoryProfileImage(){
4935        $('#directory-image-lightbox').removeClass('active');
4936        $('#directory-image-lightbox .directory-image-lightbox-img').attr('src', '');
4937    }
4938
4939    function returnNameStringForBoat( boatObject ){
4940        var this_name = '';
4941        if( boatObject.get('boatName') ){
4942            this_name += boatObject.get('boatName');
4943        }
4944        if( boatObject.get('sailNumber') ){
4945            if( this_name.length >= 1 ){
4946                this_name += " - ";
4947            }
4948            this_name += boatObject.get('sailNumber');
4949        }
4950        if( boatObject.get('class') ){
4951            if( this_name.length >= 1 ){
4952                this_name += " - ";
4953            }
4954            this_name += boatObject.get('class');
4955        }
4956        if( boatObject.get('hullNumber') ){
4957            if( this_name.length >= 1 ){
4958                this_name += " - ";
4959            }
4960            this_name += boatObject.get('hullNumber');
4961        }
4962        return this_name;
4963    }
4964
4965    function returnResponseForCustomField( member, custom_field_id ){
4966        //find the response in custom_field_responses where .get('member') == ,ember and .get('custom_field').id matches custom_field_id
4967        var all_responses = store.components.membership_directory.custom_field_responses;
4968        if(!member || !custom_field_id){
4969            return null;
4970        }
4971        var matching_response = all_responses?.find(resp => {
4972            // Match member (Parse.Object, so check .id)
4973            const member_matches = resp.get('member')?.id === member.id;
4974
4975            // Match custom field (resp.get('custom_field') is also a Parse.Object)
4976            const field_matches = resp.get('custom_field')?.id === custom_field_id;
4977
4978            return member_matches && field_matches;
4979        });
4980        if(matching_response){
4981            if( matching_response.get('field_type') == 'text' || matching_response.get('field_type') == 'textarea' || matching_response.get('field_type') == 'number' ){
4982                return matching_response.get('response')
4983            }else if (matching_response.get('field_type') == 'select'){                           
4984                var selected_options = matching_response.get('selected_options');              
4985                var displayString = selected_options.map(opt => opt.option_name).join(', ');     
4986                return displayString;           
4987            }else if( matching_response.get('field_type') == 'radio' ){   
4988                return matching_response.get('selected_options').map(option => option.option_name)                       
4989            }else if( matching_response.get('field_type') == 'file_upload' ){
4990                return matching_response.get('field_url') || null
4991            }  
4992        }
4993
4994        return null;
4995    }
4996    
4997    /* ----- STORE ----- */
4998    store.components["membership_directory"] = {
4999        clubId: null,
5000        clubIds: ['8pB9LjQKoE', '2oWU1chg3P'], //for mantoloking and cambridge yc
5001        allMembers: [],
5002        member_tags: [],
5003        json: [],
5004        boats_map: [],
5005        custom_fields: [],
5006        custom_field_responses: [],
5007        documents: [], //for scira
5008        selectedTag: 'all',
5009        // configurable_columns: [],
5010        count: 0,
5011        search: '',
5012        loading: true,
5013        sortObject: {
5014            order: 'ascending',
5015            key: 'lastName'
5016        },
5017    };
5018
5019    /* ----- COMPONENT ----- */
5020    app_content.component("membership-directory-component", {
5021        template: "#template-membership-directory",
5022        data() {
5023            return store.components['membership_directory']
5024        },
5025        props: {
5026            formatting: Object,
5027            client_id: String,
5028            class_string: String,
5029            style_string: String,
5030            element_style_string: String,
5031            component_type: String,
5032            //admin
5033            last_update: Number,
5034        },
5035        mounted() {
5036            this.clubId = $('body').data('club-id'); 
5037            this.load_membership_directory();
5038            // this.fillColumnClasses();
5039        },
5040        methods: {
5041            sortByFirstName_ascending( a, b ){
5042                var aFirstName = '';
5043                if( a.get('firstName') ){
5044                    aFirstName += a.get('firstName').trim().toLowerCase();
5045                }
5046                var bFirstName = '';
5047                if( b.get('firstName') ){
5048                    bFirstName += b.get('firstName').trim().toLowerCase();
5049                }
5050                if(aFirstName < bFirstName) { return -1; }
5051                if(aFirstName > bFirstName) { return 1; }
5052                //they had the same name
5053                return 0;
5054            },
5055            sortByFirstName_descending( a, b ){
5056                var aFirstName = '';
5057                if( a.get('firstName') ){
5058                    aFirstName += a.get('firstName').trim().toLowerCase();
5059                }
5060                var bFirstName = '';
5061                if( b.get('firstName') ){
5062                    bFirstName += b.get('firstName').trim().toLowerCase();
5063                }
5064                if(aFirstName > bFirstName) { return -1; }
5065                if(aFirstName < bFirstName) { return 1; }
5066                return 0;
5067            },
5068            sortByLastName_ascending( a, b ){
5069                var aLastName = '';
5070                if( a.get('lastName') ){
5071                    aLastName += a.get('lastName').trim().toLowerCase();
5072                }
5073                var bLastName = '';
5074                if( b.get('lastName') ){
5075                    bLastName += b.get('lastName').trim().toLowerCase();
5076                }
5077                if(aLastName < bLastName) { return -1; }
5078                if(aLastName > bLastName) { return 1; }
5079                return 0;
5080            },
5081            sortByLastName_descending( a, b ){
5082                var aLastName = '';
5083                if( a.get('lastName') ){
5084                    aLastName += a.get('lastName').trim().toLowerCase();
5085                }
5086                var bLastName = '';
5087                if( b.get('lastName') ){
5088                    bLastName += b.get('lastName').trim().toLowerCase();
5089                }
5090                if(aLastName > bLastName) { return -1; }
5091                if(aLastName < bLastName) { return 1; }
5092                return 0;
5093            }, 
5094            sortByMemberNumber_ascending( a, b ){
5095                var aMemberNumber = '';
5096                if( a.get('membershipObject') && a.get('membershipObject').get('memberNumber') ){
5097                    aMemberNumber += a.get('membershipObject').get('memberNumber').trim().toLowerCase();
5098                }
5099                var bMemberNumber = '';
5100                if( b.get('membershipObject') && b.get('membershipObject').get('memberNumber') ){
5101                    bMemberNumber += b.get('membershipObject').get('memberNumber').trim().toLowerCase();
5102                }
5103                if(aMemberNumber < bMemberNumber) { return -1; }
5104                if(aMemberNumber > bMemberNumber) { return 1; }
5105                return 0;
5106            },
5107            sortByMemberNumber_descending( a, b ){
5108                var aMemberNumber = '';
5109                if( a.get('membershipObject') && a.get('membershipObject').get('memberNumber') ){
5110                    aMemberNumber += a.get('membershipObject').get('memberNumber').trim().toLowerCase();
5111                }
5112                var bMemberNumber = '';
5113                if( b.get('membershipObject') && b.get('membershipObject').get('memberNumber') ){
5114                    bMemberNumber += b.get('membershipObject').get('memberNumber').trim().toLowerCase();
5115                }
5116                if(aMemberNumber > bMemberNumber) { return -1; }
5117                if(aMemberNumber < bMemberNumber) { return 1; }
5118                return 0;
5119            },
5120            sortByStartDate_ascending( a, b ){
5121                var aStartDate;
5122                if( a.get('membershipObject') && a.get('membershipObject').get('startDate') ){
5123                    aStartDate = a.get('membershipObject').get('startDate');
5124                }
5125                var bStartDate;
5126                if( b.get('membershipObject') && b.get('membershipObject').get('startDate') ){
5127                    bStartDate = b.get('membershipObject').get('startDate');
5128                }
5129                if(aStartDate < bStartDate) { return -1; }
5130                if(aStartDate > bStartDate) { return 1; }
5131                return 0;
5132            },
5133            sortByStartDate_descending( a, b ){
5134                var aStartDate;
5135                if( a.get('membershipObject') && a.get('membershipObject').get('startDate') ){
5136                    aStartDate = a.get('membershipObject').get('startDate');
5137                }
5138                var bStartDate;
5139                if( b.get('membershipObject') && b.get('membershipObject').get('startDate') ){
5140                    bStartDate = b.get('membershipObject').get('startDate');
5141                }
5142                if(aStartDate > bStartDate) { return -1; }
5143                if(aStartDate < bStartDate) { return 1; }
5144                return 0;
5145            },
5146            sortByRenewed_ascending( a, b ){
5147                var aRenewed;
5148                if( a.get('membershipObject') && a.get('membershipObject').get('renewed') ){
5149                    aRenewed = a.get('membershipObject').get('renewed');
5150                }else if( a.get('membershipObject') && a.get('membershipObject').get('startDate') ){
5151                    aRenewed = a.get('membershipObject').get('startDate');
5152                }
5153                var bRenewed;
5154                if( b.get('membershipObject') && b.get('membershipObject').get('renewed') ){
5155                    bRenewed = b.get('membershipObject').get('renewed');
5156                }else if( b.get('membershipObject') && b.get('membershipObject').get('startDate') ){
5157                    bRenewed = b.get('membershipObject').get('startDate');
5158                }
5159                if(aRenewed < bRenewed) { return -1; }
5160                if(aRenewed > bRenewed) { return 1; }
5161                return 0;
5162            },
5163            sortByRenewed_descending( a, b ){
5164                var aRenewed;
5165                if( a.get('membershipObject') && a.get('membershipObject').get('renewed') ){
5166                    aRenewed = a.get('membershipObject').get('renewed');
5167                }else if( a.get('membershipObject') && a.get('membershipObject').get('startDate') ){
5168                    aRenewed = a.get('membershipObject').get('startDate');
5169                }
5170                var bRenewed;
5171                if( b.get('membershipObject') && b.get('membershipObject').get('renewed') ){
5172                    bRenewed = b.get('membershipObject').get('renewed');
5173                }else if( b.get('membershipObject') && b.get('membershipObject').get('startDate') ){
5174                    bRenewed = b.get('membershipObject').get('startDate');
5175                }
5176                if(aRenewed > bRenewed) { return -1; }
5177                if(aRenewed < bRenewed) { return 1; }
5178                return 0;
5179            },
5180            sortByEndDate_ascending( a, b ){
5181                var aEndDate;
5182                if( a.get('membershipObject') && a.get('membershipObject').get('endDate') ){
5183                    aEndDate = a.get('membershipObject').get('endDate');
5184                }
5185                var bEndDate;
5186                if( b.get('membershipObject') && b.get('membershipObject').get('endDate') ){
5187                    bEndDate = b.get('membershipObject').get('endDate');
5188                }
5189                if(aEndDate < bEndDate) { return -1; }
5190                if(aEndDate > bEndDate) { return 1; }
5191                return 0;
5192            },
5193            sortByEndDate_descending( a, b ){
5194                var aEndDate;
5195                if( a.get('membershipObject') && a.get('membershipObject').get('endDate') ){
5196                    aEndDate = a.get('membershipObject').get('endDate');
5197                }
5198                var bEndDate;
5199                if( b.get('membershipObject') && b.get('membershipObject').get('endDate') ){
5200                    bEndDate = b.get('membershipObject').get('endDate');
5201                }
5202                if(aEndDate > bEndDate) { return -1; }
5203                if(aEndDate < bEndDate) { return 1; }
5204                return 0;
5205            },
5206            load_membership_directory(){
5207                //update the ui
5208                store.components.membership_directory.loading = true;
5209                var promise = Parse.Promise.as();
5210                var thisPromise = promise.then(() => {
5211                    //get new data
5212                    var params = {};
5213                    params["client_id"] = $('.membership-directory-wrap').data('client-id');
5214                    params["request_url"] = window.location.href;
5215                    return Parse.Cloud.run('retrieve_data_for_component', params);
5216                }).then(result => {
5217                    //vue 
5218                    store.components.membership_directory.member_tags = result.member_tags;
5219                    store.components.membership_directory.loading = false;   
5220                    var json = [];
5221                    var retrieved_members = result.members;
5222                    for (var i = 0; i <= retrieved_members.length - 1; i++) {
5223                        var boats_string = "";
5224                        var number = "";
5225                        if( retrieved_members[i].get('membershipObject') ){
5226                            number = retrieved_members[i].get('membershipObject').get('memberNumber');
5227                        }
5228                        var boats_map = result.boats_map;
5229                        if( boats_map && retrieved_members[i].get('membershipObject') ){
5230                            var these_boats = boats_map[ retrieved_members[i].get('membershipObject').id ];
5231                            if( these_boats ){
5232                                for (var b = these_boats.length - 1; b >= 0; b--) {
5233                                    if( boats_string.length >= 1 ){
5234                                        boats_string += " ";
5235                                    }
5236                                    boats_string += returnNameStringForBoat( these_boats[b] );
5237                                }
5238                            }
5239                        }
5240                        retrieved_members[i].boats_string = boats_string
5241                        json.push({
5242                            "objectId": retrieved_members[i].id,
5243                            "name": retrieved_members[i].get('firstName') + " " + retrieved_members[i].get('lastName'),
5244                            "boats_string": boats_string,
5245                            "number": number
5246                        })
5247                    }
5248                    //refactor for search
5249                    store.components.membership_directory.allMembers = retrieved_members;
5250                    //vue 
5251                    store.components.membership_directory.json = json;
5252                    store.components.membership_directory.boats_map = result.boats_map;
5253                    store.components.membership_directory.custom_fields = result.custom_fields;
5254                    store.components.membership_directory.custom_field_responses = result.custom_field_responses;
5255                    store.components.membership_directory.documents = result.documents; //for scira
5256                }, function(error){
5257                    checkParseError( error );
5258                });
5259                return thisPromise;
5260            },
5261            /* ----- common methods ----- */
5262            clicked_sort( key ){
5263                if( this.sortObject.key == key ){
5264                    //already sorting by this one - flip it
5265                    if( this.sortObject.order == "descending" ){
5266                        this.sortObject.order = "ascending";
5267                    }else{
5268                        this.sortObject.order = "descending";
5269                    }
5270                }else{
5271                    //not sorting by this yet
5272                    this.sortObject.key = key;
5273                    this.sortObject.order = "descending";
5274                }
5275            },
5276            /* ----- table methods ----- */
5277            handleTagSelection( tag_id ){
5278                this.selectedTag = tag_id;
5279            },
5280            fillColumnClasses(){
5281                // let configureColumns = {
5282                //     'show_photo': {
5283                //         order: 1,
5284                //         headerClasses: 'column_photo lessPadding',
5285                //         headerStyle: 'padding-left:10px;0px',
5286                //         headerName: null,
5287                //     },
5288                //     'show_member_number': {
5289                //         order: 4,
5290                //         headerClasses: 'column_member_number sortable',
5291                //         'data-db-key': 'memberNumber',
5292                //         headerName: 'Number'
5293                //     },
5294                //     'show_email': {
5295                //         order: 5,
5296                //         headerClasses: 'column_email',
5297                //         headerName: 'Email'
5298                //     },
5299                //     'show_mobile': {
5300                //         order: 6,
5301                //         headerClasses: 'column_mobile',
5302                //         headerName: 'Mobile'
5303                //     },
5304                //     'show_phone_home': {
5305                //         order: 7,
5306                //         headerClasses: 'column_phone_home',
5307                //         headerName: 'Home Phone'
5308                //     },
5309                //     'show_member_tags': {
5310                //         order: 8,
5311                //         headerClasses: 'column_member_tags',
5312                //         headerName: 'Tags'
5313                //     },
5314                //     'show_ghin_number': {
5315                //         order: 9,
5316                //         headerClasses: 'column_ghin_number',
5317                //         headerName: 'GHIN Number'
5318                //     },
5319                //     'show_address': {
5320                //         order: 10,
5321                //         headerClasses: 'column_address',
5322                //         headerName: ['Street', 'City', 'State', 'Postal Code']
5323                //     },
5324                //     'show_address_secondary': {
5325                //         order: 11,
5326                //         headerClasses: 'column_address_secondary',
5327                //         headerName: ['Street (Secondary)', 'City (Secondary)', 'State (Secondary)', 'Postal Code (Secondary)']
5328                //     },
5329                //     'show_start_date': {
5330                //         order: 12,
5331                //         headerClasses: 'sortable column_start_date',
5332                //         'data-db-key': 'startDate',
5333                //         headerName: 'Start Date'
5334                //     },
5335                //     'show_end_date': {
5336                //         order: 13,
5337                //         headerClasses: 'sortable column_end_date',
5338                //         'data-db-key': 'endDate',
5339                //         headerName: 'End Date'
5340                //     },
5341                //     'show_renewed': {
5342                //         order: 14,
5343                //         headerClasses: 'sortable column_renewed',
5344                //         'data-db-key': 'renewed',
5345                //         headerName: 'Renewed'
5346                //     },
5347                //     'show_category': {
5348                //         order: 15,
5349                //         headerClasses: 'column_category',
5350                //         headerName: 'Category'
5351                //     },
5352                //     'show_plan': {
5353                //         order: 16,
5354                //         headerClasses: 'column_plan',
5355                //         headerName: 'Plan'
5356                //     },
5357                // }
5358                // let classStringArr = this.class_string.split(' ');
5359                // this.configurable_columns = classStringArr.reduce((acc, currentClass) => {
5360                //     if (configureColumns[currentClass] !== undefined){
5361                //         acc.push(configureColumns[currentClass])
5362                //     }
5363                //     return acc;
5364                // }, [
5365                //     {
5366                //         order: 2,
5367                //         headerClasses: 'sortable firstName',
5368                //         'data-db-key': 'firstName',
5369                //         required: true,  
5370                //         headerName: 'First Name'
5371                //     },
5372                //     {
5373                //         order: 3,
5374                //         headerClasses: 'sortable lastName',
5375                //         'data-db-key': 'lastName',
5376                //         required: true,  
5377                //         headerName: 'Last Name'
5378                //     },
5379                // ]).sort((a, b) => {
5380                //     return a.order - b.order;
5381                // })
5382            },
5383            check_archive_status( memberObject ){
5384                var membershipObject = memberObject.get('membershipObject');
5385                if(membershipObject && ! membershipObject.get('archived') && membershipObject.get('status') != 'canceled' ){
5386                    return true;
5387                }
5388                return false;
5389            },
5390            clicked_member_row( memberObject ){
5391                $('#overlay_membership-directory-popup').data('members', this.allMembers);
5392                $('#overlay_membership-directory-popup').data('boats_map', this.boats_map);
5393                prepThenDisplayMembershipDirectoryPopup( memberObject );
5394            },
5395            check_configurable_classes( className ){
5396                for (let i = 0; i < this.configurable_columns.length; i++){
5397                    if (this.configurable_columns[i].headerClasses.includes(className)){
5398                        return true;
5399                        break;
5400                    }
5401                }
5402                return false;
5403            },
5404            return_member_tags( memberObject ){
5405                var member_tags = memberObject.get('member_tags') || [];
5406                var tag_string = "";
5407                var tagsSituation = '';
5408                if( member_tags.length >= 1 ){
5409                    for (var i = 0; i <= member_tags.length - 1; i++) {
5410                        if( member_tags[i].className == "member_tags" ){
5411                            if( ! member_tags[i].get('archived') && ! member_tags[i].get('hidden') ){
5412                                if( tag_string.length >= 1 ){
5413                                    tag_string += ',';
5414                                }
5415                                tag_string += member_tags[i].id;
5416                                if( member_tags[i].get('hex_value') ){
5417                                    //custom color
5418                                    tagsSituation += '<p class="table-row-tag" style="background-color:#'+ member_tags[i].get('hex_value') +';">' + member_tags[i].get('name') + '</p>';
5419                                }else{
5420                                    //default color
5421                                    tagsSituation += '<p class="table-row-tag grey">' + member_tags[i].get('name') + '</p>';
5422                                }
5423                            }
5424                        }
5425                    }
5426                }
5427                if( tagsSituation.length >= 1 ){
5428                    return tagsSituation;
5429                }else{
5430                    return '<p class="incomplete">None</p>'
5431                }
5432            },
5433            format_utc_date( memberObject ){
5434                return formatUTCDate( memberObject, true, false, true )
5435            },
5436            return_no_results_msg(){
5437                if (this.clubIds.includes(this.clubId)){
5438                    return 'Search a member to get started.' //for mantoloking, cambridge yc, northport yc
5439                } else {
5440                    return 'No members found.';
5441                }
5442            },
5443            sortMembershipDirectory( members ){
5444                var sortObject = store.components.membership_directory.sortObject;
5445                if( ! sortObject ){
5446                    sortObject = {}
5447                }
5448                if( ! sortObject.key ){
5449                    sortObject.key = "startDate";
5450                }
5451                if( ! sortObject.order ){
5452                    sortObject.order = "descending"
5453                }
5454                if( sortObject.key == "firstName" ){
5455                    if( sortObject.order == "ascending" ){
5456                        members = members.sort( this.sortByFirstName_ascending );
5457                    }else{
5458                        members = members.sort( this.sortByFirstName_descending );
5459                    }
5460                }else if( sortObject.key == "lastName" ){
5461                    if( sortObject.order == "ascending" ){
5462                        members = members.sort( this.sortByLastName_ascending );
5463                    }else{
5464                        members = members.sort( this.sortByLastName_descending );
5465                    }
5466                }else if( sortObject.key == "memberNumber" ){
5467                    if( sortObject.order == "ascending" ){
5468                        members = members.sort( this.sortByMemberNumber_ascending );
5469                    }else{
5470                        members = members.sort( this.sortByMemberNumber_descending );
5471                    }
5472                }else if( sortObject.key == "startDate" ){
5473                    if( sortObject.order == "ascending" ){
5474                        members = members.sort( this.sortByStartDate_ascending );
5475                    }else{
5476                        members = members.sort( this.sortByStartDate_descending );
5477                    }
5478                }else if( sortObject.key == "endDate" ){
5479                    if( sortObject.order == "ascending" ){
5480                        members = members.sort( this.sortByEndDate_ascending );
5481                    }else{
5482                        members = members.sort( this.sortByEndDate_descending );
5483                    }
5484                }else if( sortObject.key == "renewed" ){
5485                    if( sortObject.order == "ascending" ){
5486                        members = members.sort( this.sortByRenewed_ascending );
5487                    }else{
5488                        members = members.sort( this.sortByRenewed_de
5488scending );
5489                    }
5490                }
5491                return members;
5492            }
5493        },
5494        computed: {
5495            members_json(){
5496                let json = [];
5497                this.allMembers.forEach(member => {
5498                    let this_search = '';
5499                    if( member.get('firstName') ){
5500                        this_search += ' ' + member.get('firstName').toLowerCase();
5501                    }
5502                    if( member.get('lastName') ){
5503                        this_search += ' ' + member.get('lastName').toLowerCase();
5504                    }
5505                    if( member['boats_string'] ){
5506                        this_search += ' ' + member['boats_string'].toLowerCase();
5507                    }
5508                    if( member?.get('membershipObject')?.get('memberNumber') ){
5509                        this_search += ' ' + member?.get('membershipObject')?.get('memberNumber').toLowerCase();
5510                    }
5511                    json.push({
5512                        "memberObject": member,
5513                        "search": this_search,
5514                    })
5515                });
5516                return json;
5517            },
5518            members_matching_search(){
5519                let val = this.search.toLowerCase();
5520                var keys = [ 'search' ];
5521                //sort them
5522                var these_matches = fuzzysort.go(val, this.members_json, {
5523                    threshold: -10000, // Don't return matches worse than this (higher is faster)
5524                    limit: 20, // Don't return more results than this (lower is faster)
5525                    allowTypo: false, // Allows a snigle transpoes (false is faster)
5526                    key: null, // For when targets are objects (see its example usage)
5527                    keys: keys, // For when targets are objects (see its example usage)
5528                    scoreFn: null, // For use with `keys` (see its example usage)
5529                });
5530                return these_matches.map(match => match.obj.memberObject);
5531            },
5532            filtered_members(){
5533                let val = this.search.toLowerCase();
5534                let filteredMembers;
5535                if (this.clubIds.includes(this.clubId)){
5536                    filteredMembers = []; //for mantoloking, cambridge yc, northport yc
5537                } else {
5538                    filteredMembers = this.allMembers;
5539                }
5540                // handle search results using all members
5541                if (this.search.length > 0){
5542                    //use the new method
5543                    filteredMembers = this.members_matching_search;
5544                }
5545                // handle member tag selection using filtered search results
5546                if (this.selectedTag !== 'all'){
5547                    filteredMembers = filteredMembers.filter(member => {
5548                        let memberTags = member.get('member_tags') || [];
5549                        for (let i = 0; i < memberTags.length; i++){
5550                            if (memberTags[i].id === this.selectedTag){
5551                                return member;
5552                                break;
5553                            }
5554                        }
5555                    })
5556                }
5557                if (this.clubIds.includes(this.clubId) && filteredMembers.length > 20){
5558                    return this.sortMembershipDirectory( filteredMembers.slice(0, 20) ); //for mantoloking, cambridge yc, northport yc
5559                } else {
5560                    return this.sortMembershipDirectory( filteredMembers );
5561                }
5562            },
5563            configurable_columns(){
5564                let configureColumns = {
5565                    'show_photo': {
5566                        order: 1,
5567                        headerClasses: 'column_photo lessPadding',
5568                        headerStyle: 'padding-left:10px;0px',
5569                        headerName: null,
5570                    },
5571                    'show_member_number': {
5572                        order: 4,
5573                        headerClasses: 'column_member_number sortable',
5574                        'data-db-key': 'memberNumber',
5575                        headerName: 'Number'
5576                    },
5577                    'show_email': {
5578                        order: 5,
5579                        headerClasses: 'column_email',
5580                        headerName: 'Email'
5581                    },
5582                    'show_mobile': {
5583                        order: 6,
5584                        headerClasses: 'column_mobile',
5585                        headerName: 'Mobile'
5586                    },
5587                    'show_phone_home': {
5588                        order: 7,
5589                        headerClasses: 'column_phone_home',
5590                        headerName: 'Home Phone'
5591                    },
5592                    'show_member_tags': {
5593                        order: 8,
5594                        headerClasses: 'column_member_tags',
5595                        headerName: 'Tags'
5596                    },
5597                    'show_ghin_number': {
5598                        order: 9,
5599                        headerClasses: 'column_ghin_number',
5600                        headerName: 'GHIN Number'
5601                    },
5602                    'show_address': {
5603                        order: 10,
5604                        headerClasses: 'column_address',
5605                        headerName: ['Street', 'City', 'State', 'Postal Code']
5606                    },
5607                    'show_address_secondary': {
5608                        order: 11,
5609                        headerClasses: 'column_address_secondary',
5610                        headerName: ['Street (Secondary)', 'City (Secondary)', 'State (Secondary)', 'Postal Code (Secondary)']
5611                    },
5612                    'show_start_date': {
5613                        order: 12,
5614                        headerClasses: 'sortable column_start_date',
5615                        'data-db-key': 'startDate',
5616                        headerName: 'Start Date'
5617                    },
5618                    'show_end_date': {
5619                        order: 13,
5620                        headerClasses: 'sortable column_end_date',
5621                        'data-db-key': 'endDate',
5622                        headerName: 'End Date'
5623                    },
5624                    'show_renewed': {
5625                        order: 14,
5626                        headerClasses: 'sortable column_renewed',
5627                        'data-db-key': 'renewed',
5628                        headerName: 'Renewed'
5629                    },
5630                    'show_category': {
5631                        order: 15,
5632                        headerClasses: 'column_category',
5633                        headerName: 'Category'
5634                    },
5635                    'show_plan': {
5636                        order: 16,
5637                        headerClasses: 'column_plan',
5638                        headerName: 'Plan'
5639                    },
5640                }
5641                let classStringArr = this.class_string.split(' ');
5642                return classStringArr.reduce((acc, currentClass) => {
5643                    if (configureColumns[currentClass] !== undefined){
5644                        acc.push(configureColumns[currentClass])
5645                    }
5646                    return acc;
5647                }, [
5648                    {
5649                        order: 2,
5650                        headerClasses: 'sortable firstName',
5651                        'data-db-key': 'firstName',
5652                        required: true,  
5653                        headerName: 'First Name'
5654                    },
5655                    {
5656                        order: 3,
5657                        headerClasses: 'sortable lastName',
5658                        'data-db-key': 'lastName',
5659                        required: true,  
5660                        headerName: 'Last Name'
5661                    },
5662                ]).sort((a, b) => {
5663                    return a.order - b.order;
5664                })
5665            },
5666        }
5667    })
5668</script>
5668    
5668<script type="text/html" id="template-event-list-card">
5669    <div
5670        @click.stop="tapped_event_list_event()"
5671        class="event-row overlay-event-row flexEventOne noBorderBottom"
5672        :class="'event-row_' + event.id"
5673        :data-event-id="event.id"
5674    > 
5675        <div
5676            class="event-list-card" 
5677            :class="{
5678                'small-card': formatting.card_size == 'small',
5679                'large-card': formatting.card_size == 'large',
5680                'box-shadow-light': formatting.box_shadow == 'light',
5681                'box-shadow-dark': formatting.box_shadow == 'dark',
5682            }"
5683            :style="{ 'border-radius': formatting.border_radius + 'px' || '0px' }"
5684        >
5685            <div 
5686                v-if="image_data.url"
5687                class="event-image" 
5688                :class="{
5689                    'small-card': formatting.card_size == 'small',
5690                    'large-card': formatting.card_size == 'large',
5691                    'use-contain': image_data.crop === 'contain'
5692                }"
5693                :style="{
5694                    '--event-image-url': `url(${image_data.url})`,
5695                    backgroundImage: 'var(--event-image-url)',
5696                    backgroundSize: image_data.crop || 'cover'
5697                }"
5698            >
5699            </div>
5700            <div 
5701                v-else 
5702                class="event-image"
5703                :class="{
5704                    'small-card': formatting.card_size == 'small',
5705                    'large-card': formatting.card_size == 'large'
5706                }"
5707            >
5708                <p class="no-background">{{ event.get('name').charAt(0).toUpperCase() }}</p>
5709            </div>
5710
5711            <div 
5712                class="card-content"
5713                :style="{ 'border-radius': formatting.border_radius + 'px' || '0px' }"
5714            >
5715                <p class="event-type">{{ event_type }}</p>
5716                <p
5717                    class="event-title"
5718                    :style="event_title_style"
5719                >{{ event_title }}</p>
5720                <div class="event-date-inline">
5721                    <i class="fa-regular fa-calendar" style="font-size: 0.8em;"></i>
5722                    <span>{{ short_month }} {{ days }}</span>
5723                </div>
5724                <div v-if="show_time" class="event-time">
5725                    <i class="fa-regular fa-clock" style="font-size: 0.8em;"></i>
5726                    <span>{{ event_time }}</span>
5727                </div>
5728                <button
5729                    @click.stop
5730                    v-if="button_label"
5731                    class="event-button exclude-from-custom-button-styles relative background"
5732                    v-html="button_label"
5733                ></button>
5734            </div>
5735        </div>
5736    </div>
5737</script>
5737
5738
5739<style>
5740    .event-list-card {
5741        width: 190px;
5742        overflow: hidden;
5743        background: white;
5744        font-family: 'Inter', sans-serif;
5745        display: flex;
5746        flex-direction: column;
5747        transition: transform 0.1s ease, background-color 0.15s ease;
5748    }
5749    .event-list-card.small-card {
5750        width: 190px;
5751    }
5752    .event-list-card.large-card {
5753        width: 230px;
5754    }
5755    .event-list-card.box-shadow-light {
5756        box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.2);
5757    }
5758    .event-list-card.box-shadow-dark {
5759        box-shadow: 6px 6px 16px rgba(0, 0, 0, 0.2);
5760    }
5761    .event-list-card .event-image {
5762        display: flex;
5763        text-align: center;
5764        align-items: center;
5765        width: 100%;
5766        height: 170px;
5767        /* background-size now controlled via inline style / CSS class */
5768        background-repeat: no-repeat;
5769        background-position: center;
5770        background-color: #77818f;
5771        color: #fff;
5772        position: relative;
5773        overflow: hidden; /* hide blurred overflow */
5774    }
5775    /* When the image uses "contain", add a blurred cover background behind it */
5776    .event-list-card .event-image.use-contain::before {
5777        content: '';
5778        position: absolute;
5779        inset: 0;
5780        background-image: var(--event-image-url);
5781        background-size: cover;
5782        background-position: center;
5783        filter: blur(12px);
5784        transform: scale(1.1); /* avoid sharp edges from blur */
5785        z-index: 0;
5786        pointer-events: none;
5787    }
5788    /* Foreground, non-blurred "contain" image layer */
5789    .event-list-card .event-image.use-contain::after {
5790        content: '';
5791        position: absolute;
5792        inset: 0;
5793        background-image: var(--event-image-url);
5794        background-size: contain;
5795        background-position: center;
5796        background-repeat: no-repeat;
5797        z-index: 1;
5798        pointer-events: none;
5799    }
5800    /* Ensure foreground content (initials) sits above the image layers */
5801    .event-list-card .event-image .no-background {
5802        position: relative;
5803        z-index: 2;
5804    }
5805    .event-list-card .event-image.small-card{
5806        height: 170px;
5807    }
5808    .event-list-card .event-image.large-card{
5809        height: 220px;
5810    }
5811    .event-list-card .event-image .no-background {
5812        width: 100%;
5813        font-weight: 500;
5814        font-size: 5em;
5815    }
5816    .card-content {
5817        height: 140px;
5818        padding: 14px 12px;
5819        background-color: #f9f9f9;
5820        display: flex;
5821        flex-direction: column;
5822        transition: background-color 0.15s ease;
5823    }
5824    .event-row:hover .card-content {
5825        background-color: #f0f0f0;
5826    }
5827    .event-date-inline {
5828        display: flex;
5829        align-items: center;
5830        font-size: 14px;
5831        color: #000;
5832        margin-bottom: 2px;
5833    }
5834    .event-date-inline i {
5835        margin-right: 6px;
5836    }
5837    .event-list-card .event-type {
5838        font-size: 12px;
5839        text-transform: uppercase;
5840        color: #000;
5841        margin-bottom: 2px;
5842    }
5843    .event-list-card .event-title {
5844        font-size: 18px;
5845        text-transform: uppercase;
5846        margin-bottom: 8px;
5847        color: #000;
5848        font-family: serif;
5849        letter-spacing: 0.2px;
5850    }
5851    .event-list-card .event-time {
5852        display: flex;
5853        align-items: center;
5854        font-size: 14px;
5855        color: #000;
5856        margin-bottom: 12px;
5857    }
5858    .event-time i {
5859        margin-right: 6px;
5860    }
5861    .event-list-card .event-button {
5862        width: 100%;
5863        display: flex;
5864        align-items: center;
5865        justify-content: center;
5866        position: relative;
5867        z-index: 2;
5868        margin-top: auto;
5869        letter-spacing: 1px;
5870        line-height: 16px;
5871        cursor: pointer;
5872        font-size: 14px;
5873        font-weight: normal;
5874        padding: 6px 12px;
5875        text-transform: uppercase;
5876        transition: all 0.2s ease;
5877        border: 1.5px solid #000;
5878        border-radius: 2px;
5879    }
5880    .event-list-card .event-button:hover{
5881        background-color: #000;
5882        color: white;
5883    }
5884    .res-callout{
5885        display: flex;
5886        align-items: center;
5887        white-space: nowrap;
5888        height: fit-content;
5889        width: fit-content;
5890        padding: 3.5px;
5891        background-color: #fff;
5892        border: 1px solid #e4ebf1;
5893        border-radius: 5px;
5894        font-size: 10px;
5895        text-transform: uppercase;
5896        color: #5c6d7a;
5897    }
5898</style>
5899
5900<script>
5901    
5902    var config = defineComponent({
5903        template: "#template-event-list-card",
5904        data() {
5905            return {}
5906        },
5907        props: {
5908            event_data: {
5909                type: Object,
5910                required: true,
5911            },
5912            event_props: {
5913                type: Object,
5914                required: true,
5915            },
5916            formatting: Object,
5917        },
5918        methods: {
5919            tapped_event_list_event(){
5920                this.$emit('tapped-event-list-event', this.event_data);
5921            },
5922        },
5923        computed: {
5924            short_month(){
5925                if( this.start_date ){
5926                    return format_utc_date_v2({
5927                        "utcDate": this.start_date,
5928                        "timezone": store.timezone,
5929                        "formatString": "MMM"
5930                    });
5931                }
5932                return null;
5933            },
5934            days(){
5935                let string = '';
5936                if( this.start_date ){
5937                    let start_components = return_date_components_v2({
5938                        "date": this.start_date,
5939                        "timezone": store.timezone
5940                    });
5941                    string += start_components.date;
5942                    if( this.end_date && ! this.event.get('all_day') ){
5943                        let end_components = return_date_components_v2({
5944                            "date": this.end_date,
5945                            "timezone": store.timezone
5946                        });
5947                        if( start_components.date !== end_components.date ){
5948                            string += ' - ' + end_components.date;
5949                        }
5950                    }
5951                }
5952                return string; 
5953            },
5954            show_register_button(){
5955                return this.event_props.show_register_button;
5956            },
5957            show_time(){
5958                return this.event_props.show_time;
5959            },
5960            image_data(){
5961                return this.event_props.image_data;
5962            },
5963            start_date(){
5964                return this.event_props.start_date;
5965            },
5966            end_date(){
5967                return this.event_props.end_date;
5968            },
5969            event_title() {
5970                return this.event.get('name') || '';
5971            },
5972            event_title_style() {
5973                let style = {};
5974                if( this.formatting.event_title_overflow === 'wrap' ){
5975                    style['white-space'] = 'break-spaces';
5976                }
5977                if( this.formatting.event_title_font_size ){
5978                    style['font-size'] = this.formatting.event_title_font_size + 'px';
5979                }
5980                return style;
5981            },
5982            event_type(){
5983                if( this.event.className == 'camps' ){
5984                    return 'camp';
5985                }else if( this.event.className == 'regattas' ){
5986                    return 'regatta';
5987                }else{
5988                    return 'event';
5989                }
5990            },
5991            button_label(){
5992                let eventObject = this.event;
5993                let href_base = '';
5994                let clubObject = store.clubObject;
5995                let web_settings = null;
5996                if( clubObject ){
5997                    web_settings = clubObject.get('web_settings');
5998                }
5999                if( web_settings ){
6000                    if( web_settings.get('event_links') == "use_clubspot_regatta_links" && ( eventObject.className == "regattas" || eventObject.className == "subevents" ) ){
6001                        //force clubspot urls for regattas
6002                        href_base += 'https://theclubspot.com';
6003                    }   
6004                }
6005                var content;
6006                var show_register_button = this.show_register_button;
6007                if( eventObject.className == "regattas" || eventObject.className == "subevents" ){
6008                    if( eventObject.className == "subevents" ){
6009                        eventObject = eventObject.get('regattaObject');
6010                    }
6011                    //check for external events
6012                    if( eventObject.get('regatta_external') ){
6013                        //it's an external regatta - only render the external button if a URL is set; never fall back to register/results
6014                        if( eventObject.get('external_regatta_url') ){
6015                            content = 'View event<a class="absoluteA" target="_blank" href="'+ eventObject.get('external_regatta_url') +'"></a>';
6016                        }
6017                    }else{
6018                        //handle button status
6019                        if( show_register_button ){
6020                            //register
6021                            content = 'Register<a class="absoluteA" href="'+href_base+'/regatta/'+ eventObject.id +'"></a>';
6022                        }else{
6023                            //results
6024                            content = 'View Results<a class="absoluteA" href="'+href_base+'/regatta/'+ eventObject.id +'/results"></a>';
6025                        }
6026                    }
6027                }else if( eventObject.className == "events" ){
6028                    var registration_link;
6029                    var details_link;
6030                    if( eventObject.get('calendarOnly') ){
6031                        if( eventObject.get('external_event') ){
6032                            details_link = this.event_props.resolved_external_url || eventObject.get('external_event_url');
6033                        }
6034                    }else{
6035                        if( eventObject.get('events_v2') ){
6036                            registration_link = '/event/' + eventObject.id;
6037                        }else if( eventObject.get('external_event') && eventObject.get('external_register_link') ){
6038                            registration_link = eventObject.get('external_register_link');
6039                        }else if( eventObject.get('event_product') ){
6040                            if( eventObject.get('event_product').get('availability') && eventObject.get('event_product').get('availability').startDate ){
6041                                registration_link = "/reserve/" + eventObject.get('event_product').id + "?date=" + eventObject.get('event_product').get('availability').startDate;
6042                            }
6043                        }
6044                    }
6045                    if( registration_link && show_register_button ){
6046                        content = 'RSVP<a class="absoluteA" href="'+href_base+''+registration_link+'"></a>';
6047                    } else if( details_link && details_link.length > 0 ){
6048                        let button_label = eventObject.get('external_event_url_type') === 'dining' ? 'Reserve' : 'View details';
6049                        content = button_label + '<a class="absoluteA" target="_blank" href="'+ details_link +'"></a>';
6050                    }
6051                }else if( eventObject.className == "camps" ){
6052                    //handle button status
6053                    if( show_register_button ){
6054                        //register
6055                        content = 'Register<a href="'+href_base+'/register/camp/'+ eventObject.id +'/class" class="absoluteA"></a>';
6056                    }
6057                }
6058                return content;
6059            },
6060            event(){
6061                return this.event_data.event;
6062            },
6063            clubObject(){
6064                return store.clubObject;
6065            },
6066            event_time() {
6067                if( this.show_time ){
6068                    return format_utc_date_v2({
6069                        "utcDate": this.start_date,
6070                        "timezone": store.timezone,
6071                        "formatString": "h:mm A"
6072                    });
6073                }
6074                return "";
6075            },
6076        }
6077    })
6078    //define the component
6079    app_overlays.component("event-list-card-component", config);
6080    app_content.component("event-list-card-component", config);
6081
6082</script>
6082    
6082<script type="text/html" id="template-event-list-event">
6083    <div 
6084        @click.stop="tapped_event_list_event()"
6085        class="event-row overlay-event-row flexEventOne"
6086        :class="'event-row_' + event.id"
6087        :data-event-id="event.id"
6088    > 
6089        <!-- date and image wrap -->
6090        <div class="cs-signups-row-left flexNoWrapCenterAlign">
6091            <div class="cs-signups-date-wrap relative" style="width: 55px;">
6092                <div class="centeredBlock relative">
6093                    <div>
6094                        <p class="cs-signups-date-month">{{ short_month }}</p>
6095                        <p class="cs-signups-date-day">{{ days }}</p>
6096                    </div>
6097                </div>
6098            </div>
6099            <div 
6100                v-if="image_data.url" 
6101                class="cs-signups-row-image flexWrap" 
6102                :class="{ 'background-contain' : image_data.crop === 'contain', 'background-cover' : image_data.crop === 'cover'}"
6103                :style="{ backgroundImage: 'url(' + image_data.url + ')' }"
6104            >
6105                <div v-if="is_different_year" class="header-callout">{{ full_year }}</div>
6106            </div>
6107            <div 
6108                v-else 
6109                class="cs-signups-row-image flexWrap" 
6110            >
6111                <div v-if="is_different_year" class="header-callout">{{ full_year }}</div>
6112                {{ event.get('name').charAt(0).toUpperCase() }}
6113            </div>
6114        </div>
6115        <!-- event info, links, and host wrap -->
6116        <div class="cs-signups-row-inner flexWrapLeftAlign ellipsis align-items-center">
6117            <!-- event info -->
6118            <div class="cs-signups-name-wrap ellipsis textLeft">
6119                <span class="flexWrap" style="display: flex; align-items: center; justify-content: flex-start; margin-bottom: 5px;">
6120                    <div class="cs-signups-name-0 textLeft" style="flex-grow: 0; margin-right: 8px;">{{ event.get('name') }}</div>
6121                </span>
6122                <div class="event-date-wrap-v2">{{ niceDate }}</div>
6123                <div class="event-date-wrap-v2 tiniestPaddingTop">
6124                    <p>{{ clubObject.get('name') }}</p>
6125                    <p>{{ return_markup( event, 'event-host-2' )}}</p>
6126                </div>
6127            </div>
6128            <!-- event links -->
6129            <div @click.stop class="cs-signups-links-wrap flexGrowOne">
6130                <div class="cs-signups-links-wrap-inner textLeft">
6131                    <div class="event-nav-1" v-html="return_markup( event, 'links' )"></div>
6132                    <div class="event-nav-2" v-html="return_markup( event, 'button' )"></div>
6133                </div>
6134            </div>
6135        </div>
6136    </div>
6137</script>
6137
6138
6139<script>
6140    
6141    var config = defineComponent({
6142        template: "#template-event-list-event",
6143        data() {
6144            return {}
6145        },
6146        props: {
6147            event_data: {
6148                type: Object,
6149                required: true,
6150            },
6151            event_props: {
6152                type: Object,
6153                required: true,
6154            },
6155        },
6156        methods: {
6157            tapped_event_list_event(){
6158                this.$emit('tapped-event-list-event', this.event_data);
6159            },
6160            return_markup( eventObject, type ){
6161                var href_base = '';
6162                var clubObject = this.clubObject;
6163                var web_settings = clubObject.get('web_settings');
6164                if( web_settings ){
6165                    if( web_settings.get('event_links') == "use_clubspot_regatta_links" && ( eventObject.className == "regattas" || eventObject.className == "subevents" ) ){
6166                        //force clubspot urls for regattas
6167                        href_base += 'https://theclubspot.com';
6168                    }   
6169                }
6170                // returns here
6171                if ( type === 'event-host-2' ){
6172                    var club_string = '';
6173                    var location_params = {
6174                        eventObject: eventObject,
6175                        hideCountry: true, 
6176                        hideZip: true,
6177                        hideStreet: true,
6178                    }
6179                    var club_location_params = {
6180                        eventObject: clubObject,
6181                        hideCountry: true, 
6182                        hideZip: true,
6183                        hideStreet: true,
6184                    }
6185                    if(formatLocation(location_params) ){
6186                        if( club_string.length >= 1 ){
6187                            club_string += ', ';
6188                        }
6189                        club_string += formatLocation(location_params);
6190                    }else if( clubObject && formatLocation(club_location_params) ){
6191                        if( club_string.length >= 1 ){
6192                            club_string += ', ';
6193                        }
6194                        club_string += formatLocation(club_location_params);
6195                    }
6196                    return club_string;
6197                }  else if ( type === 'links' ){
6198                    var content;
6199                    if( eventObject.className == "regattas" || eventObject.className == "subevents" ){
6200                        //check for external events
6201                        if( eventObject.get('regatta_external') ){
6202                            //it's an external regatta - only render the external link if a URL is set; never fall back to internal links
6203                            if( eventObject.get('external_regatta_url') ){
6204                                content = '<p class="tiniestPaddingTop"><a target="_blank" href="'+ eventObject.get('external_regatta_url') +'">Event page</a></p>';
6205                            }
6206                        }else{
6207                            if( eventObject.className == "subevents" ){
6208                                eventObject = eventObject.get('regattaObject');
6209                            }
6210                            //event page
6211                            content = '<p class="tiniestPaddingTop"><a href="'+href_base+'/regatta/'+ eventObject.id +'">Event page</a></p>';
6212                            //entries
6213                            content += '<p class="tiniestPaddingTop"><a href="'+href_base+'/regatta/'+ eventObject.id +'/#entry-list">Entries</a></p>';
6214                            //race docs
6215                            content += '<p class="tiniestPaddingTop"><a href="'+href_base+'/regatta/'+ eventObject.id +'/#notice-board">Notice board</a></p>';
6216                        }
6217                    }else if( eventObject.className == "events" ){
6218                        //check for external events
6219                        if( ! eventObject.get('calendarOnly') ){
6220                            if( eventObject.get('external_event') && eventObject.get('external_event_url') ){
6221                                //it's an external event
6222                                content = '<p class="tiniestPaddingTop"><a target="_blank" href="'+ eventObject.get('external_event_url') +'">Event page</a></p>';
6223                            }else{
6224                                //event page
6225                                content = '<p class="tiniestPaddingTop"><a href="'+href_base+'/event/'+ eventObject.id +'">Event page</a></p>';
6226                            }
6227                        }else{
6228                            //calendar only event: use resolved URL per instance (recurring dining) or stored URL
6229                            let external_url = this.event_props.resolved_external_url || eventObject.get('external_event_url');
6230                            if(
6231                                eventObject.get('external_event')
6232                                && eventObject.get('external_event_url_type') !== 'dining'
6233                                && external_url
6234                            ){
6235                                content = '<p class="tiniestPaddingTop"><a target="_blank" href="'+ external_url +'">View details</a></p>';
6236                            }
6237                        }
6238                    }else if( eventObject.className == "camps" ){
6239                        //it's a camp
6240                        content = '<p class="tiniestPaddingTop"><a href="'+href_base+'/camp-registrations/'+ eventObject.id +'">Entry list</a></p>';
6241                    }
6242                    return content;
6243                } else if ( type === 'button' ){
6244                    var content;
6245                    var show_register_button = this.show_register_button;
6246                    if( eventObject.className == "regattas" || eventObject.className == "subevents" ){
6247                        if( eventObject.className == "subevents" ){
6248                            eventObject = eventObject.get('regattaObject');
6249                        }
6250                        //check for external events
6251                        if( eventObject.get('regatta_external') ){
6252                            //it's an external regatta - only render the external button if a URL is set; never fall back to register/results
6253                            if( eventObject.get('external_regatta_url') ){
6254                                content = '<button class="relative background event-list-inline-button exclude-from-custom-button-styles">View event<a class="absoluteA" target="_blank" href="'+ eventObject.get('external_regatta_url') +'"></a></button>';
6255                            }
6256                        }else{
6257                            //handle button status
6258                            if( show_register_button ){
6259                                //register
6260                                content = '<button class="relative background event-list-inline-button exclude-from-custom-button-styles">Register<a class="absoluteA" href="'+href_base+'/register/regatta/'+ eventObject.id +'/class"></a></button>';
6261                            }else{
6262                                //results
6263                                content = '<button class="relative background event-list-inline-button exclude-from-custom-button-styles">View Results<a class="absoluteA" href="'+href_base+'/regatta/'+ eventObject.id +'/results"></a></button>';
6264                            }
6265                        }
6266                    }else if( eventObject.className == "events" ){
6267                        var registration_link;
6268                        var details_link;
6269                        if( eventObject.get('calendarOnly') ){
6270                            if( eventObject.get('external_event') ){
6271                                details_link = this.event_props.resolved_external_url || eventObject.get('external_event_url');
6272                            }
6273                        }else{
6274                            if( eventObject.get('events_v2') ){
6275                                registration_link = '/event/' + eventObject.id;
6276                            }else if( eventObject.get('external_event') && eventObject.get('external_register_link') ){
6277                                registration_link = eventObject.get('external_register_link');
6278                            }
6279                        }
6280                        if( registration_link && show_register_button ){
6281                            content = '<button class="relative background event-list-inline-button exclude-from-custom-button-styles">RSVP<a class="absoluteA" href="'+href_base+''+registration_link+'"></a></button>';
6282                        }else if( details_link ){
6283                            if( eventObject.get('external_event_url_type') === 'dining' ){
6284                                content = '<button class="relative background event-list-inline-button exclude-from-custom-button-styles">Reserve<a class="absoluteA" target="_blank" href="'+ details_link +'"></a></button>';
6285                            }else{
6286                                content = '<button class="relative background event-list-inline-button exclude-from-custom-button-styles">View details<a class="absoluteA" target="_blank" href="'+ details_link +'"></a></button>';
6287                            }
6288                        } 
6289                    }else if( eventObject.className == "camps" ){
6290                        //handle button status
6291                        if( show_register_button ){
6292                            //register
6293                            content = '<button class="relative background event-list-inline-button exclude-from-custom-button-styles">Register<a href="'+href_base+'/register/camp/'+ eventObject.id +'/class" class="absoluteA"></a></button>';
6294                        }
6295                    }
6296                    return content;
6297                }
6298            },
6299        },
6300        computed: {
6301            show_register_button(){
6302                return this.event_props.show_register_button;
6303            },
6304            show_time(){
6305                return this.event_props.show_time;
6306            },
6307            image_data(){
6308                return this.event_props.image_data;
6309            },
6310            start_date(){
6311                return this.event_props.start_date;
6312            },
6313            end_date(){
6314                return this.event_props.end_date;
6315            },
6316            niceDate(){
6317                if( this.start_date ){
6318                    if( this.show_time ){
6319                        //show the time
6320                        return format_utc_date_v2({
6321                            "utcDate": this.start_date,
6322                            "timezone": store.timezone,
6323                            "formatString": "MMM D, YYYY, h:mm A"
6324                        });
6325                    }else{
6326                        //show the date only
6327                        return format_utc_date_v2({
6328                            "utcDate": this.start_date,
6329                            "timezone": store.timezone,
6330                            "formatString": "MMM D, YYYY"
6331                        });
6332                    }
6333                }
6334                return null;
6335            },
6336            full_year(){
6337                if( this.start_date ){
6338                    return format_utc_date_v2({
6339                        "utcDate": this.start_date,
6340                        "timezone": store.timezone,
6341                        "formatString": "YYYY"
6342                    });
6343                }
6344                return null;
6345            },
6346            days(){
6347                let string = '';
6348                if( this.start_date ){
6349                    let start_components = return_date_components_v2({
6350                        "date": this.start_date,
6351                        "timezone": store.timezone
6352                    });
6353                    string += start_components.date;
6354                    if( this.end_date && ! this.event.get('all_day') ){
6355                        let end_components = return_date_components_v2({
6356                            "date": this.end_date,
6357                            "timezone": store.timezone
6358                        });
6359                        if( start_components.date !== end_components.date ){
6360                            string += ' - ' + end_components.date;
6361                        }
6362                    }
6363                }
6364                return string; 
6365            },
6366            short_month(){
6367                if( this.start_date ){
6368                    return format_utc_date_v2({
6369                        "utcDate": this.start_date,
6370                        "timezone": store.timezone,
6371                        "formatString": "MMM"
6372                    });
6373                }
6374                return null;
6375            },
6376            is_different_year(){
6377                if( this.start_date ){
6378                    let start_components = return_date_components_v2({
6379                        "date": this.start_date,
6380                        "timezone": store.timezone
6381                    });
6382                    let now_components = return_date_components_v2({
6383                        "date": new Date(),
6384                        "timezone": store.timezone
6385                    });
6386                    return start_components.year !== now_components.year;
6387                }
6388                return false;
6389            },
6390            event(){
6391                return this.event_data.event;
6392            },
6393            clubObject(){
6394                return store.clubObject;
6395            },
6396            event_time() {
6397                if( this.show_time ){
6398                    return format_utc_date_v2({
6399                        "utcDate": this.start_date,
6400                        "timezone": store.timezone,
6401                        "formatString": "h:mm A"
6402                    });
6403                }
6404                return "";
6405            },
6406        },
6407    })
6408    //define the component
6409    app_overlays.component("event-list-event-component", config);
6410    app_content.component("event-list-event-component", config);
6411
6412</script>
6412
6413
6414<style>
6415    .res-callout{
6416        display: flex; /* Add this to make the container a flexbox */
6417        align-items: center; /* Center items vertically */
6418        white-space: nowrap; /* Prevent wrapping */
6419        height: fit-content;
6420        width: fit-content;
6421        padding: 3.5px;
6422        background-color: #fff;
6423        border: 1px solid #e4ebf1;
6424        border-radius: 5px;
6425        font-size: 10px;
6426        text-transform: uppercase;
6427        color: #5c6d7a;
6428    }
6429</style>    
6429<script type="text/html" id="template-event-list">
6430    <div class="cs-background-image relative" :class="class_string" :style="style_string">
6431        <div class="element-wrapper" 
6432            :class="'element-wrapper_' + component_type" 
6433            :style="element_style_string" 
6434            style="max-width:100% !important;min-width:90%;"
6435        >
6436            <!-- list view -->
6437            <template v-if="formatting_obj.layout === 'list' || !formatting_obj.layout">
6438                <div class="event-list-wrap centeredBlock centeredText" :class="'event-list-wrap_' + client_id" :data-client_id="client_id">
6439                    <div 
6440                        class="flexNoWrap align-items-center"
6441                        style="padding-bottom:5px;"
6442                    >
6443                        <h3 class="smallMarginRight" :style="{ 'font-size': '20px !important', 'font-weight': '600 !important', 'padding': '10px' }">{{ date_filter.charAt(0).toUpperCase() + date_filter.slice(1) || 'Upcoming' }} events</h3>
6444                        <div 
6445                            class="view-all-events-wrapper tinyMarginLeft"
6446                            @click="popViewAllEventsOverlay()"
6447                        >
6448                            <div :style="{ 'font-size': '14px !important', 'font-weight': '400' }">View all</div>
6449                            <div class="smallDrilldownHeaderBackButton flip"></div>
6450                        </div>
6451                    </div>
6452                    <div class="standardCardBody noBorderBottom fff noPadding overflowHidden">
6453                        <div v-if="!loading && sorted_event_data.length >= 1" class="tableInsert hundop flexWrapEvent">
6454                            <template v-for="(event_data, index) in sorted_event_data" :key="event_key(event_data)">
6455                                <p 
6456                                    v-if="is_first_visible_event_of_month(event_data)"
6457                                    class="cs-section-header"
6458                                >{{ month_string( event_data ) }}</p>
6459                                <event-list-event-component
6460                                    :event_data="event_data"
6461                                    :event_props="event_props( event_data )"
6462                                    @tapped-event-list-event="tapped_event_list_event"
6463                                ></event-list-event-component>
6464                            </template>
6465                        </div>
6466                        <div v-if="loading" class="cardBodySpinnerWrap active relative">
6467                            <div class="cardBodySpinner centeredBlock"></div>
6468                        </div>
6469                        <no-objects-found 
6470                            v-if="!loading && sorted_event_data.length == 0"
6471                            subtext="No events found"
6472                            icon_class="fa-light fa-calendar-days"
6473                        ></no-objects-found>
6474                    </div>
6475                </div>
6476            </template>
6477
6478            <!-- carousel view -->
6479            <template v-if="formatting_obj.layout === 'carousel'">
6480                <div class="event-list-wrap centeredBlock centeredText" :class="'event-list-wrap_' + client_id" :data-client_id="client_id">
6481                    <div 
6482                        class="flexNoWrap align-items-center event_list_carousel_default"
6483                        style="padding: 0px 0px 15px 5px;"
6484                    >
6485                        <h3
6486                            class="event_list_carousel_header"
6487                        >{{ date_filter || 'upcoming' }} events</h3>
6488                    </div>
6489                    <span
6490                        @mouseenter="start_auto_scroll('left')"
6491                        @mouseleave="stop_auto_scroll"
6492                        class="event-list-auto-scroll left"
6493                    ></span>
6494                    <div class="standardCardBody noBorderTop noBorderBottom fff noPadding">
6495                        <div 
6496                            ref="event_list_card_wrapper"
6497                            v-if="!loading && sorted_event_data.length >= 1"
6498                            class="tableInsert hundop event-list-card-wrapper"
6499                            :style="{ gap: formatting_obj.card_spacing + 'px' || '25px' }"
6500                        >
6501                            <template v-for="(event_data, index) in sorted_event_data" :key="event_key(event_data)">
6502                                <event-list-card-component
6503                                    :event_data="event_data"
6504                                    :formatting="formatting_obj"
6505                                    :event_props="event_props( event_data )"
6506                                    @tapped-event-list-event="tapped_event_list_event"
6507                                ></event-list-card-component>
6508                            </template>
6509                        </div>
6510                        <div v-if="loading" class="cardBodySpinnerWrap active relative">
6511                            <div class="cardBodySpinner centeredBlock"></div>
6512                        </div>
6513                        <no-objects-found 
6514                            v-if="!loading && sorted_event_data.length == 0"
6515                            subtext="No events found"
6516                            icon_class="fa-light fa-calendar-days"
6517                        ></no-objects-found>
6518                    </div>
6519                    <span
6520                        @mouseenter="start_auto_scroll('right')"
6521                        @mouseleave="stop_auto_scroll"
6522                        class="event-list-auto-scroll right"
6523                    ></span>
6524                </div>
6525            </template>
6526        </div>
6527    </div>
6528</script>
6528
6529
6530<style>
6531    .event_list_carousel_default {
6532        --event-list-carousel-font-family: 'Cormorant Garamond';
6533        --event-list-carousel-font-size: 16px;
6534        --event-list-carousel-font-weight: 600;
6535        --event-list-carousel-text-transform: uppercase;
6536        --event-list-carousel-letter-spacing: 1.2px;
6537        --event-list-carousel-color: #000;
6538    }
6539    .event_list_carousel_header {
6540        /* font-family: var(--event-list-carousel-font-family) !important; */
6541        font-size: var(--event-list-carousel-font-size) !important;
6542        font-weight: var(--event-list-carousel-font-weight) !important;
6543        text-transform: var(--event-list-carousel-text-transform) !important;
6544        letter-spacing: var(--event-list-carousel-letter-spacing) !important;
6545        color: var(--event-list-carousel-color) !important;
6546    }
6547    .event_list_carousel_header.select_filter {
6548        width: 100px !important;
6549        text-overflow: ellipsis !important;
6550        border: none !important;
6551        border-bottom: 0.5px solid black !important;
6552        appearance: none !important;
6553        -webkit-appearance: none !important;
6554        -moz-appearance: none !important;
6555        background: none !important;
6556        padding: 0px !important;
6557        box-shadow: none !important;
6558        display: inline-block !important;
6559        background-image: none !important;
6560    }
6561    .event-list-card-wrapper {
6562        position: relative;
6563        display: flex;
6564        justify-content: flex-start;
6565        align-items: center;
6566        padding: 10px;
6567        overflow: scroll !important;
6568        scrollbar-width: none !important;
6569    }
6570    .event-list-auto-scroll {
6571        position: absolute;
6572        top: 0;
6573        height: 100%;
6574        width: 20px;
6575    }
6576    .event-list-auto-scroll.left {
6577        left: 0;
6578    }
6579    .event-list-auto-scroll.right {
6580        right: 0;
6581    }
6582</style>
6583
6584<script>    
6585    
6586    app_content.component('event-list-component', {
6587        template: '#template-event-list',
6588        data() {
6589            return {
6590                event_data: [],
6591                loading: true,
6592                sortObject: {
6593                    order: 'ascending',
6594                    key: 'startDate'
6595                },
6596                scroll_animation_frame: null,
6597            }
6598        },
6599        props: {
6600            class_string: String,
6601            style_string: String,
6602            element_style_string: String,
6603            formatting: String,
6604            filter_types: String,
6605            filter_tag_ids: String,
6606            date_filter: {
6607                type: String,
6608                default: 'upcoming',
6609            },
6610            component_type: String,
6611            client_id: String,
6612            //admin
6613            component: Object,
6614            last_update: Number,
6615        },
6616        mounted() {
6617            if( this.date_filter == "past" ){
6618                this.sortObject = {
6619                    order: "descending",
6620                    key: "startDate"
6621                };
6622            }
6623            this.loadEventList(this.client_id);
6624        },
6625        methods: {
6626            is_first_visible_event_of_month( event_data ){
6627                return this.first_visible_events.includes( this.event_key(event_data) );
6628            },
6629            event_key( event_data ){
6630                return event_data.event.id + '_' + event_data.ts;
6631            },
6632            month_string( event_data ){
6633                let start_date = new Date(event_data.ts);
6634                return format_utc_date_v2({
6635                    "utcDate": start_date,
6636                    "timezone": store.timezone,
6637                    "formatString": "MMMM YYYY"
6638                });
6639            },
6640            event_props( event_data ){
6641                return {
6642                    show_register_button: this.show_register_button( event_data ),
6643                    show_time: this.show_time( event_data.event ),
6644                    image_data: this.image_data( event_data.event ),
6645                    start_date: this.start_date( event_data ),
6646                    end_date: this.end_date( event_data ),
6647                    resolved_external_url: this.resolve_external_url_for_instance( event_data ),
6648                };
6649            },
6650            resolve_external_url_for_instance( event_data ){
6651                let event = event_data.event;
6652                if ( !event || !event.get('calendarOnly') ) {
6653                    return null;
6654                }
6655                if ( event.get('external_event_url_type') === 'dining' ) {
6656                    if ( event_data.external_event_url ) {
6657                        return event_data.external_event_url;
6658                    }
6659                    let startDate = event.get('startDate');
6660                    let components = return_date_components_v2({
6661                        "date": startDate,
6662                        "timezone": 'UTC'
6663                    });
6664                    let date_string = components.year + '-' + ( components.month + 1 ) + '-' + components.date;
6665                    let dining_url = '/dining?date_string=' + date_string;
6666                    if ( event.get('startTime') ) {
6667                        dining_url += '&time=' + event.get('startTime');
6668                    }
6669                    return dining_url;
6670                }
6671                if ( event.get('external_event_url') ) {
6672                    let url = event.get('external_event_url');
6673                    if ( url && !/^https?:\/\//i.test(url) ) {
6674                        url = 'https://' + url;
6675                    }
6676                    return url;
6677                }
6678                return null;
6679            },
6680            start_date( event_data ){
6681                if( event_data.ts ){
6682                    return new Date(event_data.ts);
6683                }
6684                return null;
6685            },
6686            end_date( event_data ){
6687                if( event_data.end_ts ){
6688                    return new Date(event_data.end_ts);
6689                }
6690                return null;
6691            },
6692            tapped_event_list_event( event_data ){
6693                let event = event_data.event;
6694                if(event && event.className == 'events') {
6695                    $('#overlay_event-description-popup h3').text(event.get('name'));
6696                    let start_date = this.start_date( event_data );
6697                    let nice_date = format_utc_date_v2({
6698                        "utcDate": start_date,
6699                        "timezone": store.timezone,
6700                        "formatString": "MMM D, YYYY"
6701                    });
6702                    $('#overlay_event-description-popup .metric_start_date').text( nice_date );
6703                    if( this.show_time( event ) ){
6704                        let event_time = format_utc_date_v2({
6705                            "utcDate": start_date,
6706                            "timezone": store.timezone,
6707                            "formatString": "h:mm A"
6708                        });
6709                        $('#overlay_event-description-popup .metric_start_date_time').text( event_time ).removeClass('incomplete');
6710                    }else{
6711                        $('#overlay_event-description-popup .metric_start_date_time').text('Not provided').addClass('incomplete');
6712                    }
6713                    if(event.get('description')) {
6714                        $('#overlay_event-description-popup .metric_description').html(event.get('description')).removeClass('incomplete');
6715                    } else {
6716                        $('#overlay_event-description-popup .metric_description').text('Not provided').addClass('incomplete');
6717                    }
6718                    fadeIn('#overlay_event-description-popup');
6719                }
6720            },
6721            show_register_button( event_data ){
6722                var now = new Date();
6723                var ended = false;
6724                let eventObject = event_data.event;
6725                var registration_closed = eventObject.get('registration_closed');
6726                var lastChanceDate = eventObject.get('lastChanceDate');
6727                var startDate = new Date(event_data.ts);
6728                if( event_data.end_ts ){
6729                    var endDate = new Date(event_data.end_ts);
6730                }
6731                if( ! endDate && startDate ){
6732                    endDate = startDate;
6733                }
6734                var endDate_plus_one;
6735                if( endDate ){
6736                    endDate_plus_one = new Date( endDate.getTime() ); 
6737                    endDate_plus_one.setDate( endDate.getDate() + 1 );
6738                }
6739                if( now > endDate_plus_one && endDate_plus_one ){ 
6740                    ended = true; 
6741                }
6742                if( now > lastChanceDate && lastChanceDate ){ 
6743                    registration_closed = true; 
6744                }
6745                if( ended || registration_closed || eventObject.get('calendarOnly') ){ 
6746                    return false;
6747                } else {
6748                    return true;
6749                }
6750            },
6751            show_time( event ){
6752                if( 
6753                    event.get('calendarOnly') && event.get('all_day') 
6754                ){
6755                    return false;
6756                }else if( 
6757                    event.className == "camps" ||
6758                    event.className == "regattas" ||
6759                    event.className == "subevents"
6760                ){
6761                    return false;
6762                }
6763                return true;
6764            },
6765            image_data( event ){
6766                if( event.className == "subevents" && event.get('regattaObject') ){
6767                    event = event.get('regattaObject');
6768                }
6769                return {
6770                    url: event.get('imageURL'),
6771                    crop: event.get('imageCrop')
6772                }
6773            },
6774            popViewAllEventsOverlay(){
6775                view_all_events({
6776                    "class_string": this.class_string,
6777                    "style_string": this.style_string,
6778                    "element_style_string": this.element_style_string,
6779                    "component_type": this.component_type,
6780                    "client_id": this.client_id,
6781                    "event_data": this.event_data,
6782                    "props_generator": this.event_props,
6783                });
6784            },
6785            start_auto_scroll(direction) {
6786                const wrapper = this.$refs.event_list_card_wrapper;
6787                const amount = direction === 'right' ? 10 : -10; //scroll speed
6788                const scrollStep = () => {
6789                    wrapper.scrollBy({ left: amount, behavior: 'auto' });
6790                    this.scroll_animation_frame = requestAnimationFrame(scrollStep);
6791                };
6792                scrollStep();
6793            },
6794            stop_auto_scroll() {
6795                cancelAnimationFrame(this.scroll_animation_frame);
6796                this.scroll_animation_frame = null;
6797            },
6798            loadEventList( client_id, overrides = {} ){
6799                var promise = Parse.Promise.as();
6800                var thisPromise = promise.then(() => {
6801                    var params = {};
6802                    params["client_id"] = client_id;
6803                    params["overrides"] = overrides;
6804                    params["request_url"] = window.location.href;
6805                    params["date_filter"] = this.date_filter;
6806                    return Parse.Cloud.run('retrieve_data_for_component', params);
6807                }).then(event_data => {
6808                    this.event_data = event_data;
6809                    return Promise.resolve();
6810                }).then(() => {
6811                    this.loading = false;
6812                }, error => {
6813                    checkParseError(error);
6814                    this.loading = false;
6815                });
6816                return thisPromise;
6817            },
6818            sortEventList( event_data, sortObject ){
6819                event_data = [... event_data];
6820                if( ! sortObject ){
6821                    sortObject = {}
6822                }
6823                if( ! sortObject.key ){
6824                    sortObject.key = "startDate";
6825                }
6826                if( ! sortObject.order ){
6827                    sortObject.order = "ascending"
6828                }
6829                if( sortObject.key == "name" ){
6830                    if( sortObject.order == "ascending" ){
6831                        return event_data.sort( this.sortByEventName_ascending );
6832                    }else{
6833                        return event_data.sort( this.sortByEventName_descending );
6834                    }
6835                }else if( sortObject.key == "startDate" ){
6836                    if( sortObject.order == "ascending" ){
6837                        return event_data.sort( this.sortByStartDate_event_ascending );
6838                    }else{
6839                        return event_data.sort( this.sortByStartDate_event_descending );
6840                    }
6841                }else if( sortObject.key == "endDate" ){
6842                    if( sortObject.order == "ascending" ){
6843                        return event_data.sort( this.sortByEndDate_event_ascending );
6844                    }else{
6845                        return event_data.sort( this.sortByEndDate_event_descending );
6846                    }
6847                }
6848                return event_data;
6849            },
6850            sortByEventName_ascending( a, b ){
6851                let event_a = a.event;
6852                let event_b = b.event;
6853                var aName = '';
6854                if( event_a.get('name') ){
6855                    aName += event_a.get('name').trim().toLowerCase();
6856                }
6857                var bName = '';
6858                if( event_b.get('name') ){
6859                    bName += event_b.get('name').trim().toLowerCase();
6860                }
6861                if(aName < bName) { return -1; }
6862                if(aName > bName) { return 1; }
6863                //they had the same name
6864                return 0;
6865            },
6866            sortByEventName_descending( a, b ){
6867                let event_a = a.event;
6868                let event_b = b.event;
6869                var aName = '';
6870                if( event_a.get('name') ){
6871                    aName += event_a.get('name').trim().toLowerCase();
6872                }
6873                var bName = '';
6874                if( event_b.get('name') ){
6875                    bName += event_b.get('name').trim().toLowerCase();
6876                }
6877                if(aName > bName) { return -1; }
6878                if(aName < bName) { return 1; }
6879                //they had the same name
6880                return 0;
6881            },
6882            sortByStartDate_event_ascending( a, b ){
6883                let a_ts = a.ts;
6884                let b_ts = b.ts;
6885                if(a_ts < b_ts) { return -1; }
6886                if(a_ts > b_ts) { return 1; }
6887                return 0;
6888            },
6889            sortByStartDate_event_descending( a, b ){
6890                let a_ts = a.ts;
6891                let b_ts = b.ts;
6892                if(a_ts > b_ts) { return -1; }
6893                if(a_ts < b_ts) { return 1; }
6894                return 0;
6895            },
6896            sortByEndDate_event_ascending( a, b ){
6897                let a_end_ts = a.end_ts;
6898                let b_end_ts = b.end_ts;
6899                if(a_end_ts < b_end_ts) { return -1; }
6900                if(a_end_ts > b_end_ts) { return 1; }
6901                return 0;
6902            },
6903            sortByEndDate_event_descending( a, b ){
6904                let a_end_ts = a.end_ts;
6905                let b_end_ts = b.end_ts;
6906                if(a_end_ts < b_end_ts) { return 1; }
6907                if(a_end_ts > b_end_ts) { return -1; }
6908                return 0;
6909            },
6910        },
6911        computed: {
6912            pre_filtered_event_data(){
6913                //filter by date first
6914                let pre_filtered_event_data = [...this.event_data];
6915                var timestamp = new Date().getTime();
6916                if ( this.date_filter !== 'all' ){
6917                    pre_filtered_event_data = pre_filtered_event_data.filter(event_data => {
6918                        let start = event_data.ts;
6919                        let end = event_data.end_ts;
6920                        if (this.date_filter === 'past'){
6921                            if( start < timestamp && ( end < timestamp || ! end || end == "undefined" ) ){
6922                                return true;
6923                            }
6924                        } else if (this.date_filter === 'upcoming'){
6925                            if ( start > timestamp || end > timestamp ){
6926                                return true;
6927                            }
6928                            // Single-day events: ts and end_ts are both midnight, so they get filtered out
6929                            // when the event is today. Include if the event's date is today or later.
6930                            if ( start === end || !end ){
6931                                let startOfEventDay = new Date(start);
6932                                startOfEventDay.setUTCHours(0, 0, 0, 0);
6933                                let startOfToday = new Date();
6934                                startOfToday.setUTCHours(0, 0, 0, 0);
6935                                if ( startOfEventDay.getTime() >= startOfToday.getTime() ){
6936                                    return true;
6937                                }
6938                            }
6939                        }
6940                        return false;
6941                    })
6942                }
6943                return pre_filtered_event_data;
6944            },
6945            first_visible_events(){
6946                let first_visible_events = [];
6947                let handled_keys = [];
6948                //loop through the event_data
6949                this.sorted_event_data.forEach(event_data => {
6950                    let start_date = new Date(event_data.ts);
6951                    let start_components = return_date_components_v2({
6952                        "date": start_date,
6953                        "timezone": store.timezone
6954                    });
6955                    let key = start_components.year + '_' + start_components.month;
6956                    if( ! handled_keys.includes(key) ){
6957                        first_visible_events.push( this.event_key(event_data) );
6958                        handled_keys.push(key);
6959                    }
6960                });
6961                return first_visible_events;
6962            },
6963            filtered_event_data(){
6964                let filtered_event_data = this.pre_filtered_event_data;
6965                //filter by selected tag IDs (configured in admin settings panel)
6966                if( this.filter_tag_ids_arr && this.filter_tag_ids_arr.length > 0 ){
6967                    filtered_event_data = filtered_event_data.filter(event_data => {
6968                        let event_tag_ids = event_data.event_tag_ids;
6969                        return event_tag_ids && this.filter_tag_ids_arr.some(id => event_tag_ids.includes(id));
6970                    });
6971                }
6972                //return them
6973                return filtered_event_data;
6974            },
6975            sorted_event_data(){
6976                let sorted = this.sortEventList( this.filtered_event_data, this.sortObject );
6977                if( this.formatting_obj.layout === 'carousel' ){
6978                    return sorted.splice(0, 20);
6979                }else{
6980                    return sorted.splice(0, 5);
6981                }
6982            },
6983            formatting_obj(){
6984                if (this.formatting){
6985                    return JSON.parse(this.formatting);
6986                }
6987                return {};
6988            },
6989            filter_tag_ids_arr(){
6990                if (this.filter_tag_ids){
6991                    return JSON.parse(this.filter_tag_ids);
6992                }
6993                return [];
6994            },
6995            filter_types_arr(){
6996                if (this.filter_types){
6997                    return JSON.parse(this.filter_types);
6998                }
6999                return [];
7000            },
7001        },
7002        watch: {
7003            last_update(newVal, oldVal){
7004                if (newVal !== oldVal){
7005                    let event_types = this.component.get('config_latest')?.event_types || [];
7006                    this.loadEventList( this.client_id, { event_types } );
7007                }
7008            }
7009        },
7010    });
7011</script>
7011    
7011<script type="text/html" id="template-iframe">
7012    <div v-if="!loading && !is_admin" class="cs-iframe-wrap" :class="class_string" :style="style_string">
7013        <iframe class="cs-iframe" :src="iframe_src"></iframe>
7014    </div>
7015    <div v-if="!loading && is_admin" class="cs-iframe-wrap cs-min-section-width" :class="class_string" :style="[style_string, { 'max-width': formatting_obj.section_width + '%' }]">
7016        <template v-if="iframe_src" >
7017            <iframe class="cs-iframe" sandbox="allow-forms allow-scripts" :src="iframe_src"></iframe>
7018        </template>
7019        <template v-if="!iframe_src">
7020            <p class="noObjectsMSG centeredBlock centeredText">Click on edit to configure your iframe.</p>
7021        </template>
7022    </div>
7023</script>
7023
7024
7025<style>
7026    /* Draft-editor only floor (the admin wrap carries cs-min-section-width;
7027       the live wrap does not). Without it an iframe with no configured
7028       section_height — or an empty/unconfigured embed — collapses shorter
7029       than the component toolbar + inline "add section" button, so those
7030       controls overlap. A configured section_height (inline min-height)
7031       still wins since it's taller. */
7032    .cs-iframe-wrap.cs-min-section-width {
7033        min-height: 130px;
7034    }
7035</style>
7036
7037<script>    
7038    /* ----- COMPONENT ----- */
7039    app_content.component('iframe-component', {
7040        template: '#template-iframe',
7041        data() {
7042            return {
7043                global: store,
7044                loading: true,
7045                is_admin: false,
7046            }
7047        },
7048        props: {
7049            class_string: String,
7050            style_string: String,
7051            iframe_src: String,
7052            //admin
7053            component: Object,
7054            formatting_string: String,
7055            last_update: Number
7056        },
7057        mounted() {
7058            this.is_admin = window.location.href.indexOf('website-builder') !== -1;
7059            this.loading = false;
7060        },
7061        computed: {
7062            formatting_obj(){
7063                if (this.formatting_string){
7064                    return JSON.parse(this.formatting_string);
7065                }
7066                return {};
7067            },
7068        },
7069    });
7070</script>
7070    
7070<script type="text/html" id="template-camera-feed">
7071    <div v-if="!loading && !is_admin" class="cs-iframe-wrap" :class="class_string" :style="style_string">
7072        <div v-if="!errored" style="position:relative;padding:8px;width:100%;box-sizing:border-box;">
7073            <div v-if="!image_loaded" style="width:100%;aspect-ratio:16 / 9;border-radius:12px;background:#e9ecef;display:flex;align-items:center;justify-content:center;color:#adb5bd;font-size:13px;">Loading live camera…</div>
7074            <img v-show="image_loaded" :src="img_src" style="width:100%;height:100%;object-fit:contain;display:block;border-radius:12px;">
7075            <span style="position:absolute;top:16px;left:16px;display:inline-flex;align-items:center;gap:5px;background:rgba(0,0,0,0.6);color:#fff;font-size:11px;font-weight:600;letter-spacing:0.5px;padding:3px 9px;border-radius:12px;">
7076                <span style="width:7px;height:7px;border-radius:50%;background:#ff3b30;display:inline-block;"></span>LIVE
7077            </span>
7078            <span v-if="last_image_time" style="position:absolute;top:16px;right:16px;background:rgba(0,0,0,0.6);color:#fff;font-size:11px;font-weight:600;padding:3px 9px;border-radius:12px;">{{ last_image_time }}</span>
7079        </div>
7080        <p v-if="errored" class="noObjectsMSG centeredBlock centeredText">The live camera is temporarily unavailable.</p>
7081    </div>
7082    <div v-if="!loading && is_admin" class="cs-iframe-wrap cs-min-section-width" :class="class_string" :style="[style_string, { 'max-width': formatting_obj.section_width + '%' }]">
7083        <template v-if="has_camera">
7084            <div style="position:relative;padding:8px;width:100%;box-sizing:border-box;">
7085                <div v-if="!image_loaded" style="width:100%;aspect-ratio:16 / 9;border-radius:12px;background:#e9ecef;display:flex;align-items:center;justify-content:center;color:#adb5bd;font-size:13px;">Loading live camera…</div>
7086                <img v-show="image_loaded" :src="img_src" style="width:100%;height:100%;object-fit:contain;display:block;border-radius:12px;">
7087                <span style="position:absolute;top:16px;left:16px;display:inline-flex;align-items:center;gap:5px;background:rgba(0,0,0,0.6);color:#fff;font-size:11px;font-weight:600;letter-spacing:0.5px;padding:3px 9px;border-radius:12px;">
7088                    <span style="width:7px;height:7px;border-radius:50%;background:#ff3b30;display:inline-block;"></span>LIVE
7089                </span>
7090                <span v-if="last_image_time" style="position:absolute;top:16px;right:16px;background:rgba(0,0,0,0.6);color:#fff;font-size:11px;font-weight:600;padding:3px 9px;border-radius:12px;">{{ last_image_time }}</span>
7091            </div>
7092        </template>
7093        <template v-if="!has_camera">
7094            <p class="noObjectsMSG centeredBlock centeredText">Click on edit to configure your camera.</p>
7095        </template>
7096    </div>
7097</script>
7097
7098
7099<script>
7100    app_content.component('camera-feed-component', {
7101        template: '#template-camera-feed',
7102        data() {
7103            return {
7104                global: store,
7105                loading: true,
7106                is_admin: false,
7107                img_src: '',
7108                errored: false,
7109                refresh_timer: null,
7110                loader: null,
7111                last_image_time: '',
7112                image_loaded: false,
7113            }
7114        },
7115        props: {
7116            class_string: String,
7117            style_string: String,
7118            client_id: String,
7119            camera_feed_id: String,
7120            refresh_interval_ms: [Number, String],
7121            //admin
7122            component: Object,
7123            formatting_string: String,
7124            last_update: Number,
7125        },
7126        mounted() {
7127            this.is_admin = window.location.href.indexOf('website-builder') !== -1;
7128            this.loading = false;
7129            if (this.has_camera) {
7130                this.start_refresh();
7131            }
7132        },
7133        beforeUnmount() {
7134            this.stop_refresh();
7135        },
7136        methods: {
7137            snapshot_base() {
7138                let base = `/camera-feed/${this.client_id}/snapshot.jpg`;
7139                //editor has no subdomain rewrite, so pass the club id explicitly
7140                if (this.is_admin && store.clubObject) {
7141                    base += `?club_id=${store.clubObject.id}`;
7142                }
7143                return base;
7144            },
7145            return_refresh_interval() {
7146                let interval = Number(this.refresh_interval_ms);
7147                if (!interval || isNaN(interval) || interval < 500) {
7148                    interval = 2000;
7149                }
7150                return interval;
7151            },
7152            return_next_snapshot_url() {
7153                let base = this.snapshot_base();
7154                let separator = base.indexOf('?') === -1 ? '?' : '&';
7155                return `${base}${separator}t=${Date.now()}`;
7156            },
7157            start_refresh() {
7158                this.stop_refresh();
7159                this.load_next_frame();
7160            },
7161            //fetch the next frame into an off-DOM Image so the visible <img>
7162            //is only swapped once bytes are fully loaded. we never reassign a
7163            //pending src, so the browser never cancels an in-flight request.
7164            load_next_frame() {
7165                let started = Date.now();
7166                let url = this.return_next_snapshot_url();
7167                let loader = new Image();
7168                loader.onload = () => {
7169                    //loaded bytes are in cache, so this swap paints instantly
7170                    this.img_src = url;
7171                    this.image_loaded = true;
7172                    this.errored = false;
7173                    this.last_image_time = this.format_time(new Date());
7174                    this.schedule_next_frame(started);
7175                };
7176                loader.onerror = () => {
7177                    this.errored = true;
7178                    this.schedule_next_frame(started);
7179                };
7180                loader.src = url;
7181                this.loader = loader;
7182            },
7183            //only advance after the current frame finishes, subtracting elapsed
7184            //so cadence tracks the target interval (never firing early)
7185            schedule_next_frame(started) {
7186                let interval = this.return_refresh_interval();
7187                let elapsed = Date.now() - started;
7188                let delay = Math.max(0, interval - elapsed);
7189                this.refresh_timer = setTimeout(() => {
7190                    this.load_next_frame();
7191                }, delay);
7192            },
7193            stop_refresh() {
7194                if (this.refresh_timer) {
7195                    clearTimeout(this.refresh_timer);
7196                    this.refresh_timer = null;
7197                }
7198                if (this.loader) {
7199                    this.loader.onload = null;
7200                    this.loader.onerror = null;
7201                    this.loader = null;
7202                }
7203            },
7204            format_time(date) {
7205                let hours = date.getHours();
7206                let minutes = date.getMinutes().toString().padStart(2, '0');
7207                let seconds = date.getSeconds().toString().padStart(2, '0');
7208                let period = hours >= 12 ? 'PM' : 'AM';
7209                hours = hours % 12;
7210                if (hours === 0) {
7211                    hours = 12;
7212                }
7213                return `${hours}:${minutes}:${seconds} ${period}`;
7214            },
7215        },
7216        computed: {
7217            has_camera() {
7218                if (!this.client_id) {
7219                    return false;
7220                }
7221                if (this.camera_feed_id) {
7222                    return true;
7223                }
7224                return false;
7225            },
7226            formatting_obj() {
7227                if (this.formatting_string) {
7228                    return JSON.parse(this.formatting_string);
7229                }
7230                return {};
7231            },
7232        },
7233        watch: {
7234            camera_feed_id() {
7235                if (this.has_camera) {
7236                    this.start_refresh();
7237                } else {
7238                    this.stop_refresh();
7239                }
7240            },
7241            refresh_interval_ms() {
7242                if (this.has_camera) {
7243                    this.start_refresh();
7244                }
7245            },
7246        },
7247    });
7248</script>
7248
7249    
7249<script type="text/html" id="template-blog-list">
7250    <div class="relative" :class="class_string" :style="style_string">
7251        <div class="element-wrapper cs-min-section-width" :class="'element-wrapper_' + component_type" :style="[element_style_string, { 'min-width': `${formatting_obj.section_width}%`, 'max-width': `${formatting_obj.section_width}%` }]">
7252            <div class="blog-list-wrap centeredBlock centeredText" :class="'blog-list-wrap_' + client_id" :data-client_id="client_id">
7253                <div class="ease flexNoWrap show_when_loaded" style="justify-content: center; align-items: center;">
7254                    <!-- template type - list -->
7255                    <template v-if="!formatting_obj.template || formatting_obj.template === 'list'">
7256                        <div class="inputWrap searchBarWrap flexGrowOne" style="min-width:100%;">
7257                            <input class="searchBar searchBar_blog-list easeFast" placeholder="Search posts..." v-model="search">
7258                        </div>
7259                        <div class="flexGrowOne"></div>
7260                    </template>
7261                    <!-- template type - bulletin -->
7262                    <template v-if="formatting_obj.template === 'bulletin'">
7263                        <div class="searchBarWrap_v2" style="width: 100%;
7264                        display: flex;
7265                        flex-direction: row;
7266                        align-items: center;
7267                        justify-content: center;
7268                        margin-right: 8px;
7269                        margin-left: 8px;
7270                        margin-bottom: 10px;">
7271                            <input id="searchBar_blog-list_v2" type="text" placeholder="Search posts..." v-model="search" />
7272                            <button id="searchBar_blog-list_submit" type="submit">
7273                                <svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="#666666" d="M9.5,3A6.5,6.5 0 0,1 16,9.5C16,11.11 15.41,12.59 14.44,13.73L14.71,14H15.5L20.5,19L19,20.5L14,15.5V14.71L13.73,14.44C12.59,15.41 11.11,16 9.5,16A6.5,6.5 0 0,1 3,9.5A6.5,6.5 0 0,1 9.5,3M9.5,5C7,5 5,7 5,9.5C5,12 7,14 9.5,14C12,14 14,12 14,9.5C14,7 12,5 9.5,5Z" /></svg>
7274                            </button>
7275                        </div>
7276                    </template>
7277                </div> 
7278                <div class="fff noPadding">
7279                    <!-- template type - list -->
7280                    <template v-if="!formatting_obj.template || formatting_obj.template === 'list'">
7281                        <div v-if="!loading && truncated_posts.length >= 1" class="tableInsert hundop">
7282                            <template v-for="(post, index) in truncated_posts" :key="post.id">
7283                                <div 
7284                                    :style="{ 'background-color': formatting_obj.background_color || '#fff', 'border-radius': '4px' }"
7285                                    class="blog-row flexWrapLeftAlign relative pointer blog-row-border"
7286                                    :class="'blog-row_' + post.id" 
7287                                    :data-post-id="post.id"
7288                                >
7289                                    <div v-if="post && post.get('thumbnail')" class="blog-post-thumbnail" :style="'background-image:url(' + post.get('thumbnail') + ')'"></div>
7290                                    <div v-if="post && !post.get('thumbnail')" class="blog-post-thumbnail default-image-icon"></div>
7291                                    <div class="blog-row-inner flexGrowOne">
7292                                        <div class="blog-name-wrap-column">
7293                                            <p 
7294                                                class="blog-title" 
7295                                                :style="{ 
7296                                                    'color': formatting_obj.title_color || '#000',
7297                                                    'font-weight': formatting_obj.title_styling && formatting_obj.title_styling.bold ? '600 !important' : '400 !important',
7298                                                    'font-style': formatting_obj.title_styling && formatting_obj.title_styling.italic ? 'italic' : 'normal',
7299                                                    'text-decoration': formatting_obj.title_styling && formatting_obj.title_styling.underline ? 'underline' : 'none',
7300                                                }"
7301                                            >{{ post.get('title') }}</p>
7302                                            <p 
7303                                                v-if="post && post.get('subtitle')"
7304                                                class="blog-subtitle" 
7305                                                :style="{
7306                                                    'color': formatting_obj.subtitle_color || '#000',
7307                                                    'font-weight': formatting_obj.subtitle_styling && formatting_obj.subtitle_styling.bold ? '600 !important' : '400 !important',
7308                                                    'font-style': formatting_obj.subtitle_styling && formatting_obj.subtitle_styling.italic ? 'italic' : 'normal',
7309                                                    'text-decoration': formatting_obj.subtitle_styling && formatting_obj.subtitle_styling.underline ? 'underline' : 'none',
7310                                                }"
7311                                            >{{ post.get('subtitle') }}</p>
7312                                            <p class="blog-date" :style="{ 'color': formatting_obj.date_color || '#000' }">{{ niceDate(post) }}</p>
7313                                            <p><a class="absoluteA" :href="'/blog-post/' + post.id" style="text-transform: none !important; border-bottom: none !important;"></a>
7314                                                <a
7315                                                :href="'/blog-post/' + post.id"
7316                                                :style="{
7317                                                    'font-variant': 'small-caps',
7318                                                    'opacity': '0.9',
7319                                                    'color': formatting_obj.date_color || '#000',
7320                                                    'border-bottom': 'none !important',
7321                                                    'text-decoration': 'underline !important',
7322                                                    'text-decoration-color': (formatting_obj.date_color || '#000') + ' !important'
7323                                                }"
7324                                                >Read more</a>
7325                                            </p>
7326                                        </div>
7327                                    </div>
7328                                    <div v-if="post && post.get('pinned') === true">
7329                                        <i class="fa-solid fa-thumbtack thumbtack-icon" style="height: 1em; transform: rotate(45deg);"></i>
7330                                    </div>
7331                                </div>
7332                            </template>
7333                            <div 
7334                                v-if="has_more_posts"
7335                                class="view_more_button flexNoWrapCenter align-items-center"
7336                                @click="view_all = true"
7337                                style="margin-bottom:0px;margin-left:0px;margin-right:0px;"
7338                            >
7339                                <div>
7340                                    <p>View more</p>
7341                                </div>
7342                                <i class="fa-regular fa-arrow-right easeFast"></i>
7343                            </div>
7344                        </div>
7345                    </template>
7346                    <!-- template type - bulletin -->
7347                    <template v-if="formatting_obj.template === 'bulletin'">
7348                        <div 
7349                            v-if="!loading && truncated_posts.length >= 1" 
7350                            :class="class_string"
7351                        >
7352                            <div class="blog-list-container">
7353                                <section 
7354                                    class="blog-list-cards-v2" 
7355                                >
7356                                    <div v-for="(post, index) in truncated_posts" :key="post.id">
7357                                        <article 
7358                                            :style="{ 'background-color': formatting_obj.background_color || '#fff' }"
7359                                            style="display: flex; flex-direction: column; align-items: center; justify-content: flex-start; position: relative;"
7360                                            class="blog_post_card_v2"
7361                                            @click="go_to_post( post )"
7362                                        >
7363                                            <div class="blog-list-image-container">
7364                                                <img v-if="post && post.get('thumbnail')" class="blog_post_thumbnail" :src="post.get('thumbnail')">
7365                                                <div v-if="post && !post.get('thumbnail')" style="display: flex; align-items: center; justify-content: center;">
7366                                                    <i class="fa-duotone fa-solid fa-newspaper" style="font-size: 10em;"></i>
7367                                                </div>
7368                                            </div>
7369                                            <i v-if="post && post.get('pinned') === true" class="fa-solid fa-thumbtack" style="margin: 3px 0 0 2px; font-size: 1em; color: #fff; transform: rotate(45deg); position: absolute; top: 0; right: 5px;"></i>
7370                                            <div class="card__content">
7371                                                <div 
7372                                                    v-if="post && post.get('title')" 
7373                                                    class="card__title_v2" 
7374                                                    :style="{ 
7375                                                        'color': formatting_obj.title_color || '#000',
7376                                                        'font-weight': formatting_obj.title_styling && formatting_obj.title_styling.bold ? '600 !important' : '400 !important',
7377                                                        'font-style': formatting_obj.title_styling && formatting_obj.title_styling.italic ? 'italic' : 'normal',
7378                                                        'text-decoration': formatting_obj.title_styling && formatting_obj.title_styling.underline ? 'underline' : 'none',
7379                                                    }"
7380                                                >
7381                                                    {{ post.get('title') }}
7382                                                </div>
7383                                                <div 
7384                                                    v-if="post && post.get('subtitle')"
7385                                                    class="card__subtitle tinyMarginTop"
7386                                                    :style="{ 
7387                                                        'color': formatting_obj.subtitle_color || '#000',
7388                                                        'font-weight': formatting_obj.subtitle_styling && formatting_obj.subtitle_styling.bold ? '600 !important' : '400 !important',
7389                                                        'font-style': formatting_obj.subtitle_styling && formatting_obj.subtitle_styling.italic ? 'italic' : 'normal',
7390                                                        'text-decoration': formatting_obj.subtitle_styling && formatting_obj.subtitle_styling.underline ? 'underline' : 'none',
7391                                                    }"
7392                                                >
7393                                                    {{ post.get('subtitle') }}
7394                                                </div>
7395                                                <div class="card__date" :style="{ 'color': formatting_obj.date_color || '#000' }">{{ niceDate(post) }}</div>
7396                                                <div class="card__excerpt">
7397                                                    <a 
7398                                                        @click.stop 
7399                                                        :href="'/blog-post/' + post.id" 
7400                                                        :style="{
7401                                                            'font-variant': 'small-caps',
7402                                                            'opacity': '0.9',
7403                                                            'color': formatting_obj.date_color || '#000',
7404                                                            'border-bottom': 'none !important',
7405                                                            'text-decoration': 'underline !important',
7406                                                            'text-decoration-color': (formatting_obj.date_color || '#000') + ' !important'
7407                                                        }"
7408                                                    >
7409                                                        read more
7410                                                    </a>
7411                                                </div>
7412                                            </div>
7413                                        </article>
7414                                    </div>
7415                                </section>
7416                            </div>
7417                            <div 
7418                                v-if="has_more_posts"
7419                                class="view_more_button flexNoWrapCenter align-items-center"
7420                                :class="{'larchmont': is_larchmont || is_larchmont === 'true'}"
7421                                @click="view_all = true"
7422                                style="margin-bottom:0px;"
7423                            >
7424                                <div>
7425                                    <p>View more</p>
7426                                </div>
7427                                <i class="fa-regular fa-arrow-right easeFast"></i>
7428                            </div>
7429                        </div>
7430                    </template>
7431                    <div v-if="!loading && filtered_posts.length == 0" class="tableInsert hundop blog-list-contain">
7432                        <p class="noObjectsMSG withPadding textLeft blog-row flexWrapLeftAlign relative pointer">No posts found.</p>
7433                    </div>
7434                </div>         
7435                <div v-if="loading" class="cardBodySpinnerWrap active relative">
7436                    <div class="cardBodySpinner centeredBlock"></div>
7437                </div>
7438            </div>
7439        </div>
7440    </div>
7441</script>
7441
7442
7443<style>
7444
7445    /*------------------------ view more buttons --------------------- */
7446
7447    .view_more_button.card-layout{
7448        margin-top:0px;
7449    }
7450    .view_more_button{
7451        color: #6a7482;
7452        margin: 8px;
7453        border: 1px solid #e5eaec;
7454        border-top: 1px solid #e5eaec;
7455        border-radius: 5px;
7456        cursor: pointer;
7457        font-size: 16px;
7458        padding: 16px;
7459        margin-top: 8px;
7460    }
7461    .view_more_button:hover{
7462        color: #081333;
7463        background-color: #f8f9fb;
7464    }
7465    .view_more_button.larchmont{
7466        color: #fff;
7467        margin: 8px;
7468        border: 1px solid #fff;
7469        border-top: 1px solid #fff;
7470        border-radius: 5px;
7471        cursor: pointer;
7472        font-size: 16px;
7473        padding: 16px;
7474        margin-top: 8px;
7475    }
7476    .view_more_button.larchmont:hover{
7477        background-color: #002D62;
7478        color: #fff;
7479    }
7480    .view_more_button i{
7481        padding-left: 8px;
7482    }
7483    .view_more_button:hover i{
7484        color: #4a56f8;
7485        transform: translateX(4px);
7486    }
7487
7488    /* -- end view more button --*/
7489
7490    section.blog-list-cards-v2 {
7491        margin: 0 auto;
7492        display: grid;
7493        grid-template-columns: repeat(3, 1fr);
7494        grid-auto-rows: auto;
7495        grid-gap: 16px;
7496        align-content: center;
7497        justify-content: center;
7498        padding: 8px;
7499        width: 100%;
7500        box-sizing: border-box;
7501    }
7502    .blog-list-image-container {
7503        width: 100%;
7504        height: 300px;
7505        display: flex;
7506        align-items: center;
7507        justify-content: center;
7508        overflow: hidden;
7509        border-top-left-radius: 6px;
7510        border-top-right-radius: 6px;
7511        border-bottom-left-radius: 0px;
7512        border-bottom-right-radius: 0px;
7513        background-color: #f2f2f2;
7514    }
7515    .blog_post_thumbnail {
7516        width: 100%;
7517        height: 100%;
7518        object-fit: cover;
7519        display: block;
7520    }
7521    .default_image_icon {
7522        background-image: url('https://s3-us-west-2.amazonaws.com/myclubspot/upload-photo-icon.jpg');
7523        background-size: contain;
7524        background-position: center;
7525        background-repeat: no-repeat;
7526        width: 100%;
7527        height: 100%;
7528    }
7529    @media (max-width: 768px) {
7530        section.blog-list-cards-v2 {
7531            grid-template-columns: repeat(1, 1fr);
7532        }
7533    }
7534</style>
7535
7536<script>
7537
7538    store.components["blog_list"] = {
7539        allPosts: [],
7540        search: '',
7541        loading: true,
7542        view_all: false,
7543        post_limit: 6 //how many to show on initial load (before clicking "see more")
7544    };
7545
7546    app_content.component("blog-list-component", {
7547        template: "#template-blog-list",
7548        data() {
7549            return store.components['blog_list']
7550        },
7551        props: {
7552            client_id: String,
7553            class_string: String,
7554            style_string: String,
7555            element_style_string: String,
7556            formatting: String,
7557            component_type: String,
7558            is_larchmont: Boolean, String,
7559            //admin
7560            component: Object,
7561            last_update: Number,
7562        },
7563        mounted() {
7564            this.loadBlogList( this.client_id )
7565        },
7566        methods: {
7567            go_to_post( post ){
7568                window.location.href = "blog-post/" + post.id;
7569            },
7570            loadBlogList( client_id, overrides = {} ){
7571                var promise = Parse.Promise.as();
7572                var thisPromise = promise.then(() => {
7573                    //get new data
7574                    var params = {};
7575                    params["client_id"] = client_id;
7576                    params["overrides"] = overrides;
7577                    params["request_url"] = window.location.href;
7578                    return Parse.Cloud.run('retrieve_data_for_component', params);
7579                }).then(retrieved_posts => {
7580                    // sort with 'pinned' posts on top
7581                    this.allPosts = retrieved_posts.sort((a, b) => (a.get('pinned') === b.get('pinned') ? 0 : a.get('pinned') ? -1 : 1));
7582                    this.loading = false;
7583                    //and return
7584                    return Parse.Promise.as();
7585                }, error => {
7586                    checkParseError( error );
7587                });
7588                return thisPromise;
7589            },
7590            niceDate( postObject ){
7591                return dayjs(postObject.get('post_date')).format('MMM DD, YYYY');
7592            },
7593            return_grid_column_css(){
7594                if (window.innerWidth <= 990) {
7595                    //for mobile
7596                    return 'repeat(1, 256px)';
7597                } else {
7598                    return 'repeat(4, 256px)';
7599                }
7600            },
7601        },
7602        computed: {
7603            has_more_posts(){
7604                if( this.view_all ){
7605                    return false;
7606                }
7607                if( this.filtered_posts.length > this.post_limit ){
7608                    return true;
7609                }
7610                return false;
7611            },
7612            truncated_posts(){
7613                if( this.view_all ){
7614                    return this.filtered_posts;
7615                }else{
7616                    return this.filtered_posts.slice(0, this.post_limit);
7617                }
7618            },
7619            filtered_posts(){
7620                let val = this.search.toLowerCase();
7621                let filteredPosts = this.allPosts;
7622                // handle search results using all posts
7623                if (this.search.length > 0){
7624                    filteredPosts = this.allPosts.filter(post => {
7625                        let title = post.get('title').toLowerCase();
7626                        if ( title.includes(val) ){
7627                            return post;
7628                        }
7629                    })
7630                }
7631                return filteredPosts;
7632            },
7633            formatting_obj(){
7634                if (this.formatting){
7635                    return JSON.parse(this.formatting);
7636                }
7637                return {};
7638            },
7639        }
7640    })
7641</script>
7641    
7641<script type="text/html" id="template-custom-code">
7642    <template v-if="preview_element">
7643        <div
7644            class="cs-custom-code-wrap"
7645            :class="[class_string, {
7646                'preview-mode': active_tab === 'preview',
7647                'first-page-section': is_first,
7648                'active-ai-thread-target': ai_panel_is_open_for_this_component,
7649                'custom-code-preview-blank': !custom_code_is_empty && custom_code_preview_is_blank,
7650            }]"
7651            :style="ai_loading_wrap_style"
7652            @mouseenter="activate_ai_builder_target_from_presence"
7653        >
7654            <!-- Tab bar -->
7655            <div class="custom-code-tab-bar">
7656                <div
7657                    class="custom-code-tab"
7658                    :class="{ active: active_tab === 'edit' }"
7659                    @click="active_tab = 'edit'"
7660                >Code</div>
7661                <div
7662                    class="custom-code-tab"
7663                    :class="{ active: active_tab === 'preview' }"
7664                    @click="active_tab = 'preview'"
7665                >Preview</div>
7666            </div>
7667            <!-- Code pane -->
7668            <div v-show="active_tab === 'edit'" class="custom-code-pane">
7669                <div ref="cm_editor" class="custom-code-editor"></div>
7670            </div>
7671            <!-- Preview pane -->
7672            <!-- Hide the welcome page during the pre-first-delta state.
7673                 The loading pane below owns that window completely so
7674                 the preview area does not show both the composer and
7675                 "thinking" UI at the same time. -->
7676            <div
7677                v-show="active_tab === 'preview' && custom_code_is_empty && !has_ai_thread_for_component && !ai_streaming && !ai_streaming_pending && !ai_welcome_generating"
7678                class="custom-code-pane custom-code-preview-pane custom-code-welcome-pane"
7679                :class="{ 'is-welcome-drop-active': welcome_drop_active }"
7680                style="display: flex; align-items: center; justify-content: center; min-height: 420px; padding: 48px 20px; background: #fbfcff;"
7681                @dragenter="handle_welcome_dragenter"
7682                @dragover.prevent="handle_welcome_dragover"
7683                @dragleave="handle_welcome_dragleave"
7684                @drop.prevent="handle_welcome_drop"
7685            >
7686                <!-- Drop overlay (welcome composer) — same pattern as
7687                     the chat panel's drop overlay; mounts only while
7688                     a file drag is hovering this pane. -->
7689                <div
7690                    v-if="welcome_file_upload_enabled && welcome_drop_active"
7691                    aria-hidden="true"
7692                    style="position: absolute; inset: 0; z-index: 5; display: flex; align-items: center; justify-content: center; background: rgba(244, 246, 255, 0.85); pointer-events: none;"
7693                >
7694                    <div style="display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 18px 22px; border-radius: 12px; background: #fff; border: 1px solid #d9dfeb; color: #4a56f8; font-size: 13px; font-weight: 600; box-shadow: 0 6px 22px rgba(74, 86, 248, 0.15);">
7695                        <i class="fa-regular fa-image" style="font-size: 22px;"></i>
7696                        <p class="noMargin">Drop images to attach</p>
7697                    </div>
7698                </div>
7699                <div style="width: min(680px, 92%); margin: 0 auto; text-align: center;">
7700                    <div style="width: 54px; height: 54px; border-radius: 18px; margin: 0 auto 18px auto; display: flex; align-items: center; justify-content: center; background: rgba(74, 86, 248, 0.08); color: #4a56f8;">
7701                        <i class="fa-regular fa-wand-magic-sparkles" style="font-size: 22px;"></i>
7702                    </div>
7703                    <h3 class="noMargin" style="font-size: 24px; line-height: 32px; color: #081333; font-weight: 700;">Build a custom section</h3>
7704                    <p class="noMargin" style="font-size: 14px; line-height: 22px; color: #748095; margin-top: 6px !important;">Start with AI, then refine the code directly whenever you need to.</p>
7705                    <div class="flexWrap" style="gap: 8px; justify-content: center; margin: 22px auto 14px auto;">
7706                        <button
7707                            v-for="example in ai_prompt_examples"
7708                            :key="example"
7709                            type="button"
7710                            class="ai-composer-pill pointer exclude-from-custom-button-styles"
7711                            @click.stop.prevent="set_ai_prompt_example(example)"
7712                        >{{ example }}</button>
7713                    </div>
7714                    <!-- Attached image thumbnails — read-only mirror of
7715                         the chat panel's mcp_attachments. Files queued
7716                         here flow into the chat panel's state via the
7717                         global ai_mcp_panel_attach_files helper, so
7718                         submit ships them alongside the prompt without
7719                         this composer needing its own prep pipeline. -->
7720                    <div
7721                        v-if="welcome_file_upload_enabled && welcome_attachments && welcome_attachments.length"
7722                        style="display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; margin-bottom: 10px;"
7723                    >
7724                        <div
7725                            v-for="(att, ai) in welcome_attachments"
7726                            :key="'welcome-att-' + att.id"
7727                            :title="att.name"
7728                            style="position: relative; width: 56px; height: 56px; border-radius: 8px; overflow: hidden; background: #eef0f4; border: 1px solid #d9dfeb; flex: 0 0 auto;"
7729                            :style="att.status === 'error' ? 'border-color: #e
772907b6f;' : ''"
7730                        >
7731                            <img
7732                                v-if="att.preview_url"
7733                                :src="att.preview_url"
7734                                :alt="att.name"
7735                                style="width: 100%; height: 100%; object-fit: cover; display: block;"
7736                                :style="att.status === 'pending' ? 'opacity: 0.55;' : ''"
7737                            />
7738                            <div
7739                                v-if="att.status === 'pending'"
7740                                style="position: absolute; top: 50%; left: 50%; width: 18px; height: 18px; margin: -9px 0 0 -9px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.45); border-top-color: #fff; animation: aiMcpLauncherSpin 1s linear infinite; pointer-events: none;"
7741                                aria-hidden="true"
7742                            ></div>
7743                            <button
7744                                type="button"
7745                                class="pointer exclude-from-custom-button-styles"
7746                                style="position: absolute; top: 2px; right: 2px; width: 18px; height: 18px; padding: 0; margin: 0; border: none; border-radius: 999px; background: rgba(8, 19, 51, 0.65); color: #fff; font-size: 9px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center;"
7747                                :title="'Remove ' + (att.name || 'image')"
7748                                @click.stop.prevent="welcome_remove_attachment(ai)"
7749                            >
7750                                <i class="fa-regular fa-xmark"></i>
7751                            </button>
7752                        </div>
7753                    </div>
7754                    <div style="position: relative; display: flex; align-items: center; gap: 10px; border: 1px solid rgba(8, 19, 51, 0.1); border-radius: 16px; background: #fff; box-shadow: 0 14px 45px rgba(8, 19, 51, 0.08); padding: 11px 12px 11px 15px; text-align: left;">
7755                        <div style="flex: 1 1 auto; min-width: 0;">
7756                            <p class="noMargin" style="font-size: 12px; line-height: 16px; color: #4a56f8; font-weight: 700; margin-bottom: 8px !important;">
7757                                <i class="fa-regular fa-sparkles" style="padding-right: 4px;"></i>Design with AI
7758                            </p>
7759                            <textarea
7760                                ref="ai_prompt_textarea"
7761                                v-model="ai_prompt"
7762                                rows="1"
7763                                class="ai-prompt-textarea"
7764                                style="display: block; width: 100%; height: 21px; min-height: 21px; max-height: 168px; overflow-y: auto; border: none; box-shadow: none; resize: none; padding: 0; font-size: 15px; line-height: 21px; color: #081333;"
7765                                placeholder="Describe the section you want to build..."
7766                                :disabled="ai_prompt_loading"
7767                                @input="auto_resize_ai_prompt"
7768                                @paste="handle_welcome_paste"
7769                                @keydown.enter.exact.prevent="submit_ai_prompt"
7770                            ></textarea>
7771                        </div>
7772                        <input
7773                            v-if="welcome_file_upload_enabled"
7774                            ref="ai_prompt_file_input"
7775                            type="file"
7776                            accept="image/*"
7777                            multiple
7778                            style="display: none;"
7779                            @change="handle_welcome_file_input_change"
7780                        />
7781                        <button
7782                            v-if="welcome_file_upload_enabled"
7783                            type="button"
7784                            class="pointer exclude-from-custom-button-styles"
7785                            :disabled="ai_prompt_loading"
7786                            title="Attach image"
7787                            style="width: 32px; min-width: 32px; height: 32px; border: 0; border-radius: 999px; background: transparent; color: #5f6f8d; display: flex; justify-content: center; align-items: center; cursor: pointer; font-size: 15px;"
7788                            @click.stop.prevent="open_welcome_file_picker"
7789                        >
7790                            <i class="fa-regular fa-paperclip"></i>
7791                        </button>
7792                        <button
7793                            type="button"
7794                            class="ai-submit-button easeFast exclude-from-custom-button-styles"
7795                            :class="{
7796                                inactive: !ai_prompt_can_submit,
7797                                loading: ai_prompt_loading
7798                            }"
7799                            :disabled="!ai_prompt_can_submit"
7800                            title="Send"
7801                            @click.stop.prevent="submit_ai_prompt"
7802                        >
7803                            <i class="fa-regular fa-paper-plane"></i>
7804                        </button>
7805                    </div>
7806                </div>
7807            </div>
7808            <!-- Welcome "Generating…" state — shown after the blank welcome
7809                 composer hands the prompt to the page agent, until the first
7810                 result lands (content arrives) or the safety timer fires. -->
7811            <div
7812                v-show="active_tab === 'preview' && custom_code_is_empty && ai_welcome_generating"
7813                class="custom-code-pane custom-code-preview-pane custom-code-welcome-generating-pane"
7814                style="display: flex; align-items: center; justify-content: center; min-height: 420px; padding: 48px 20px; background: #fbfcff;"
7815                aria-live="polite"
7816            >
7817                <div style="display: flex; flex-direction: column; align-items: center; gap: 14px; text-align: center;">
7818                    <span class="custom-code-welcome-generating-spinner" aria-hidden="true"></span>
7819                    <p class="noMargin" style="font-size: 15px; line-height: 22px; color: #4a56f8; font-weight: 600;">Generating…</p>
7820                </div>
7821            </div>
7822            <div
7823                v-show="active_tab === 'preview' && custom_code_is_empty && has_ai_thread_for_component && !ai_streaming && !ai_streaming_pending && !ai_welcome_generating"
7824                class="custom-code-pane custom-code-preview-pane custom-code-empty-state-pane"
7825            >
7826                <div class="custom-code-empty-state-copy">
7827                    <i class="fa-regular fa-code"></i>
7828                    <span>No content</span>
7829                </div>
7830            </div>
7831            <!-- Preview strips scripts / non-visual tags, so a section that
7832                 only has those would otherwise collapse to 0 height. -->
7833            <div
7834                v-show="active_tab === 'preview' && !custom_code_is_empty && custom_code_preview_is_blank && (!ai_streaming || ai_streaming_pending || !ai_generation_active)"
7835                class="custom-code-pane custom-code-preview-pane custom-code-empty-state-pane custom-code-nonvisual-pane"
7836            >
7837                <div class="custom-code-empty-state-copy">
7838                    <i class="fa-regular fa-code"></i>
7839                    <span>This section has code that isn't visible in preview</span>
7840                </div>
7841            </div>
7842            <!-- Static preview pane: HTML is mounted into an open shadow
7843                 root (see ensure_isolated_custom_code_host) so host theme
7844                 CSS cannot restyle the section — same isolation as live. -->
7845            <div
7846                v-show="active_tab === 'preview' && !custom_code_is_empty && !custom_code_preview_is_blank && (!ai_streaming || ai_streaming_pending || !ai_generation_active)"
7847                ref="custom_code_preview"
7848                class="custom-code-pane custom-code-preview-pane"
7849                :style="ai_loading_pane_style"
7850            ></div>
7851            <!-- Pre-first-delta loading. Empty sections get a full pane;
7852                 edits to existing content use the same large dots as an
7853                 overlay so the current section remains visible while the
7854                 updated HTML is being prepared. -->
7855            <div
7856                v-show="active_tab === 'preview' && ai_streaming_pending"
7857                class="custom-code-pane custom-code-preview-pane custom-code-loading-pane"
7858                :class="{ 'is-overlay': !custom_code_is_empty }"
7859                :style="ai_loading_pane_style"
7860                aria-live="polite"
7861            >
7862                <div class="custom-code-pending-dots" aria-label="Generating">
7863                    <span></span><span></span><span></span>
7864                </div>
7865            </div>
7866            <!-- Streaming preview pane: managed by flush_streaming_preview_pane
7867                 during a live SSE codegen stream. We bypass v-html so a
7868                 tiny morph diff can append-or-replace top-level children
7869                 by index — keeps already-rendered sections stable as new
7870                 ones stream in below them. Snapped back to the v-html
7871                 pane on stream `done`. -->
7872            <div
7873                v-show="active_tab === 'preview' && ai_streaming && !ai_streaming_pending && ai_generation_active"
7874                ref="custom_code_stream_preview"
7875                class="custom-code-pane custom-code-preview-pane"
7876                :style="ai_loading_pane_style"
7877            ></div>
7878
7879            <!-- AI builder launcher — bottom-right of the component.
7880                 Clicking sets this component as the active target for
7881                 the shared right-side AI panel (defined in
7882                 ai-mcp-panel.html) and expands it. The panel's webbuilder
7883                 mode then reads `current_code` server-side from
7884                 pageComponents.config_latest.custom_code and runs in a
7885                 thread pinned to this component instance. -->
7886            <button
7887                type="button"
7888                class="custom-code-ai-launcher pointer exclude-from-custom-button-styles"
7889                :class="{ 'has-active-stream': ai_generation_active }"
7890                @mousedown.stop.prevent
7891                @mouseup.stop
7892                @touchstart.stop
7893                @click.stop.prevent="open_ai_builder"
7894                :title="ai_launcher_title"
7895                v-if="ai_launcher_visible"
7896            >
7897                <i class="fa-regular fa-sparkles"></i>
7898                <span class="custom-code-ai-launcher-label">
7899                    <template v-if="effective_ai_streaming_status === 'queued'">Queued…</template>
7900                    <template v-else-if="ai_generation_active">Generating…</template>
7901                    <template v-else>Edit with AI</template>
7902                </span>
7903                <span
7904                    v-if="ai_generation_active"
7905                    class="custom-code-ai-launcher-spinner"
7906                    aria-hidden="true"
7907                ></span>
7908            </button>
7909        </div>
7910    </template>
7911    <template v-if="!preview_element">
7912        <!-- Live host: same shadow isolation as builder preview. -->
7913        <span v-if="!loading" ref="custom_code_static" class="cs-custom-code-live-host"></span>
7914    </template>
7915</script>
7915
7916
7917<script>    
7918    // Reset + inheritance for custom-code shadow roots. Host theme element
7919    // selectors (h1/a/details/…) cannot pierce the shadow; :host inherits
7920    // color/font from the light-DOM wrapper so club theme still flows in.
7921    var CS_CUSTOM_CODE_ISOLATION_CSS = [
7922        ':host { display: block; width: 100%; color: inherit; font-family: inherit; font-size: inherit; line-height: inherit; }',
7923        '.cs-cc-root { display: block; width: 100%; color: inherit; font: inherit; box-sizing: border-box; }',
7924        '.cs-cc-root *, .cs-cc-root *::before, .cs-cc-root *::after { box-sizing: border-box; }',
7925        '.cs-cc-root summary { list-style: none; cursor: pointer; }',
7926        '.cs-cc-root summary::-webkit-details-marker, .cs-cc-root summary::marker { display: none; }',
7927        '.cs-cc-root img { max-width: 100%; }',
7928    ].join('\n');
7929
7930    // Builder-only overlays live inside the shadow tree. Document CSS
7931    // cannot pierce it, so these rules must ship with the isolation sheet
7932    // or Add/Change chips render unstyled and get clipped by overflow.
7933    var CS_CUSTOM_CODE_PREVIEW_CHROME_CSS = [
7934        '.cs-asset-placeholder-host, .cs-asset-attached-host { position: relative; }',
7935        '.cs-asset-background-placeholder-host:hover { outline: 2px dashed rgba(74, 86, 248, 0.55) !important; outline-offset: -4px !important; }',
7936        '.cs-asset-background-placeholder-chip { position: absolute !important; top: 10px !important; right: 10px !important; z-index: 10000 !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; width: 34px !important; height: 34px !important; padding: 0 !important; margin: 0 !important; border: 1px solid rgba(74, 86, 248, 0.45) !important; border-radius: 999px !important; background: rgba(255, 255, 255, 0.88) !important; color: #4a56f8 !important; box-shadow: 0 6px 18px rgba(8, 19, 51, 0.14) !important; cursor: pointer !important; appearance: none !important; }',
7937        '.cs-asset-background-placeholder-chip:hover { background: #ffffff !important; border-color: rgba(74, 86, 248, 0.85) !important; }',
7938        '.cs-asset-placeholder-overlay { position: absolute !important; inset: 0 !important; z-index: 125 !important; display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; gap: 4px !important; margin: 0 !important; padding: 12px !important; border: 2px dashed rgba(74, 86, 248, 0.55) !important; border-radius: 8px !important; background: rgba(74, 86, 248, 0.08) !important; color: #2a2f5a !important; font-family: Roboto, Arial, Helvetica, sans-serif !important; font-size: 13px !important; font-style: normal !important; font-weight: 500 !important; line-height: 1.25 !important; letter-spacing: 0 !important; text-align: center !important; white-space: normal !important; cursor: pointer !important; backdrop-filter: blur(2px) !important; appearance: none !important; }',
7939        '.cs-asset-placeholder-overlay:hover { background: rgba(74, 86, 248, 0.16) !important; border-color: rgba(74, 86, 248, 0.85) !important; }',
7940        '.cs-asset-placeholder-icon { display: inline-flex !important; align-items: center !important; justify-content: center !important; width: 28px !important; height: 28px !important; flex: 0 0 28px !important; margin-bottom: 4px !important; border-radius: 50% !important; background: rgba(255, 255, 255, 0.65) !important; color: #4a56f8 !important; font-size: 14px !important; font-style: normal !important; line-height: 1 !important; }',
7941        '.cs-asset-placeholder-label { display: inline-block !important; padding: 4px 10px !important; border-radius: 999px !important; background: rgba(255, 255, 255, 0.92) !important; color: #2a2f5a !important; font-family: Roboto, Arial, Helvetica, sans-serif !important; font-size: 13px !important; font-weight: 600 !important; line-height: 1.2 !important; min-width: 118px !important; max-width: 180px !important; white-space: normal !important; }',
7942        '.cs-asset-change-button { position: absolute !important; top: 8px !important; right: 8px !important; z-index: 10000 !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; padding: 6px 10px !important; margin: 0 !important; border: none !important; border-radius: 6px !important; background: rgba(20, 24, 45, 0.82) !important; color: #fff !important; font-family: Roboto, Arial, Helvetica, sans-serif !important; font-size: 12px !important; font-weight: 600 !important; font-style: normal !important; line-height: 1 !important; letter-spacing: 0 !important; text-transform: none !important; cursor: pointer !important; opacity: 0.92 !important; pointer-events: auto !important; appearance: none !important; box-shadow: 0 4px 12px rgba(8, 19, 51, 0.22) !important; }',
7943        '.cs-asset-change-button:hover, .cs-asset-change-button:focus { opacity: 1 !important; background: rgba(20, 24, 45, 0.95) !important; }',
7944        '.cs-ai-asset-hover-highlight { outline: 2px solid rgba(74, 86, 248, 0.32) !important; outline-offset: 4px !important; box-shadow: 0 0 0 6px rgba(74, 86, 248, 0.08) !important; }',
7945        '.cs-cc-root i[class*="fa-"] { font-family: "Font Awesome 6 Pro", "Font Awesome 6 Free", "Font Awesome 5 Pro", "Font Awesome 5 Free", "FontAwesome" !important; font-style: normal !important; }',
7946    ].join('\n');
7947
7948    function createDebounce() {
7949        let timeout = null;
7950        return function (fnc, delayMs) {
7951            clearTimeout(timeout);
7952            timeout = setTimeout(() => {
7953                fnc();
7954            }, delayMs || 800);
7955        };
7956    }
7957
7958    // ─── External image detection (shared) ──────────────────────────────
7959    //
7960    // Shared by the custom-code component and section-actions overlay.
7961    var WEBBUILDER_CLUBSPOT_ASSET_HOSTS = [
7962        'd282wvk2qi4wzk.cloudfront.net',
7963        'myclubspot.s3-us-west-2.amazonaws.com',
7964        'myclubspot.s3.us-west-2.amazonaws.com',
7965    ];
7966
7967    function webbuilder_is_external_image_url(url) {
7968        if (typeof url !== 'string' || !url.trim()) {
7969            return false;
7970        }
7971        let parsed;
7972        try {
7973            parsed = new URL(url.trim());
7974        } catch (err) {
7975            return false;
7976        }
7977        if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
7978            return false;
7979        }
7980        return WEBBUILDER_CLUBSPOT_ASSET_HOSTS.indexOf((parsed.hostname || '').toLowerCase()) === -1;
7981    }
7982
7983    // Parse an HTML string and return the unique external image URLs it
7984    // references — both <img src> and inline `background-image: url(...)`.
7985    function webbuilder_collect_external_image_urls(source_html) {
7986        let source = (typeof source_html === 'string') ? source_html : '';
7987        if (!source.trim()) {
7988            return [];
7989        }
7990        let found = [];
7991        let seen = {};
7992        let push_if_external = (raw_url) => {
7993            let candidate = (typeof raw_url === 'string') ? raw_url.trim() : '';
7994            if (!candidate || seen[candidate]) {
7995                return;
7996            }
7997            if (!webbuilder_is_external_image_url(candidate)) {
7998                return;
7999            }
8000            seen[candidate] = true;
8001            found.push(candidate);
8002        };
8003        let template = document.createElement('template');
8004        template.innerHTML = source;
8005        let root = template.content;
8006        let imgs = Array.from(root.querySelectorAll('img[src]'));
8007        for (let img of imgs) {
8008            push_if_external(img.getAttribute('src'));
8009        }
8010        let styled = Array.from(root.querySelectorAll('[style*="background-image"]'));
8011        for (let el of styled) {
8012            let style = el.getAttribute('style') || '';
8013            let bg_pattern = /background-image\s*:\s*url\(\s*(['"]?)([^'"\)]+)\1\s*\)/gi;
8014            let match;
8015            while ((match = bg_pattern.exec(style)) !== null) {
8016                push_if_external(match[2]);
8017            }
8018        }
8019        return found;
8020    }
8021
8022    // ─── Text editing ───────────────────────────────────────────────────
8023    //
8024    // In PREVIEW mode we walk the rendered DOM looking for "text leaf"
8025    // elements (headings, paragraphs, buttons, links, list items, etc.)
8026    // and decorate each one with a click-to-edit affordance. Clicking
8027    // opens a small inline popover with a textarea; on save we mutate
8028    // the persisted custom_code source and persist via
8029    // save_changes_for_component, mirroring the placeholder fill flow.
8030    //
8031    // Unlike images, there's no "placeholder" concept for text — the AI
8032    // writes plausible draft copy directly into the section and admins
8033    // tweak it inline. Round-tripping (preview path → source path)
8034    // works because the preview's DOM is parsed from the same source
8035    // string we mutate, with the only structural divergence being
8036    // 
8036<script> tags removed by sanitize_custom_code_html (which we
8037    // skip on the source side when walking).
8038    //
8039    // A "text leaf" is any element whose element children are all
8040    // inline tags (or none). That treats `<h1>Welcome <em>back</em></h1>`
8041    // as a single editable string — emphasis collapses on save, by
8042    // design. Admins can use the Code tab for mixed inline formatting.
8043
8044    var TEXT_INLINE_TAGS = new Set([
8045        'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'em', 'i',
8046        'kbd', 'mark', 'q', 's', 'samp', 'small', 'span', 'strong',
8047        'sub', 'sup', 'time', 'u', 'var', 'wbr',
8048    ]);
8049
8050    var TEXT_SKIP_TAGS = new Set([
8051        'script', 'style', 'template', 'noscript', 'iframe', 'svg',
8052        'video', 'audio', 'canvas', 'img', 'picture', 'source', 'track',
8053        'input', 'textarea', 'select', 'option', 'optgroup', 'meta',
8054        'link', 'br', 'hr', 'col', 'param', 'embed', 'object',
8055    ]);
8056
8057    // Tags that never produce a layout box in the sanitized preview.
8058    var CUSTOM_CODE_PREVIEW_SKIP_TAGS = new Set([
8059        'script', 'style', 'template', 'noscript', 'meta', 'link', 'head',
8060    ]);
8061    // Replaced / form controls that take space even without text.
8062    var CUSTOM_CODE_PREVIEW_VISUAL_TAGS = new Set([
8063        'img', 'iframe', 'video', 'audio', 'canvas', 'svg', 'object', 'embed',
8064        'picture', 'input', 'textarea', 'select', 'hr', 'br',
8065    ]);
8066
8067    function custom_code_element_has_explicit_preview_box(element) {
8068        let style = element.getAttribute('style');
8069        if (!style || typeof style !== 'string') {
8070            return false;
8071        }
8072        if (/background-image\s*:\s*url\s*\(/i.test(style)) {
8073            return true;
8074        }
8075        let height_match = style.match(/(?:^|;)\s*(?:min-)?height\s*:\s*([^;]+)/i);
8076        if (!height_match) {
8077            return false;
8078        }
8079        let value = height_match[1].trim().toLowerCase();
8080        if (!value || value === '0' || value === '0px' || value === '0%' || value === 'auto') {
8081            return false;
8082        }
8083        return true;
8084    }
8085
8086    function custom_code_node_has_visible_preview_content(node) {
8087        if (!node) {
8088            return false;
8089        }
8090        if (node.nodeType === 8) {
8091            return false;
8092        }
8093        if (node.nodeType === 3) {
8094            return !!(node.nodeValue && node.nodeValue.trim());
8095        }
8096        if (node.nodeType !== 1) {
8097            let children = node.childNodes || [];
8098            for (let i = 0; i < children.length; i++) {
8099                if (custom_code_node_has_visible_preview_content(children[i])) {
8100                    return true;
8101                }
8102            }
8103            return false;
8104        }
8105        let tag = (node.tagName || '').toLowerCase();
8106        if (CUSTOM_CODE_PREVIEW_SKIP_TAGS.has(tag)) {
8107            return false;
8108        }
8109        if (CUSTOM_CODE_PREVIEW_VISUAL_TAGS.has(tag)) {
8110            return true;
8111        }
8112        if (custom_code_element_has_explicit_preview_box(node)) {
8113            return true;
8114        }
8115        for (let i = 0; i < node.childNodes.length; i++) {
8116            if (custom_code_node_has_visible_preview_content(node.childNodes[i])) {
8117                return true;
8118            }
8119        }
8120        return false;
8121    }
8122
8123    function custom_code_html_has_visible_preview_content(html) {
8124        if (!html || typeof html !== 'string' || !html.trim()) {
8125            return false;
8126        }
8127        let template = document.createElement('template');
8128        template.innerHTML = html;
8129        return custom_code_node_has_visible_preview_content(template.content);
8130    }
8131
8132    app_content.component('custom-code-component', {
8133        template: '#template-custom-code',
8134        data() {
8135            return {
8136                loading: true,
8137                debounce: createDebounce(),
8138                custom_code_value: '',
8139                active_tab: 'preview',
8140                // True while the section-actions "save external images"
8141                // batch is in flight, so duplicate overlay triggers no-op.
8142                importing_external_images: false,
8143                ai_prompt: '',
8144                ai_prompt_examples: [
8145                    'A modern hero section for our club homepage',
8146                    'A membership benefits section with three cards',
8147                    'An event promo section with image placeholders',
8148                ],
8149                ai_launcher_title: 'Edit with AI',
8150                // ─── Streaming state ───────────────────────────────
8151                // Flipped to true by begin_streaming_code() and back
8152                // to false by finish_streaming_code(). Controls the
8153                // template's preview-pane selection (v-html static vs.
8154                // morph-managed div) and the overlay visibility.
8155                ai_streaming: false,
8156                ai_streaming_status: null, // 'streaming' | 'queued' | 'aborted_local' | 'error' | 'done'
8157                // Snapshot of the editor contents at stream begin —
8158                // used by apply_generated_code as the return value
8159                // (Undo target) when the panel's done handler runs.
8160                ai_streaming_pre_code: null,
8161                // Accumulated code chunks. Always the full snapshot
8162                // of what the stream has emitted so far (chunks are
8163                // appended as they arrive).
8164                ai_streaming_buffer: '',
8165                // Local reactive tick for store.ai_streams snapshots.
8166                // Bumped by subscriber events; lets computeds that
8167                // read entry.code_buffer / entry.message_text
8168                // re-evaluate. Launcher status no longer depends on
8169                // this — that lives in store.codegen_request_state.
8170                ai_stream_event_tick: 0,
8171                ai_loading_pane_height: null,
8172                ai_loading_pane_height_release_timer: null,
8173                ai_streaming_child_min_heights: [],
8174                // Welcome composer drag-drop counter — increments on
8175                // dragenter, decrements on dragleave so the overlay
8176                // doesn't flicker on nested-element drag events. The
8177                // attachment LIST itself lives on the chat panel
8178                // (store.components.ai_mcp_panel.mcp_attachments) so
8179                // both composers stay in sync without a second prep
8180                // pipeline.
8181                welcome_drop_counter: 0,
8182                welcome_drop_active: false,
8183                // True between submitting from the blank welcome composer and
8184                // the page agent's first result landing. Swaps the welcome
8185                // composer for a "Generating…" pane. The codegen stream no
8186                // longer drives this flow (the page agent does), so this is a
8187                // local flag cleared when content arrives or on a safety timer.
8188                ai_welcome_generating: false,
8189            }
8190        },
8191        props: {
8192            custom_code: String,
8193            //admin
8194            class_string: String,
8195            preview_element: {
8196                type: Boolean,
8197                default: false,
8198            },
8199            is_first: {
8200                type: Boolean,
8201                default: false,
8202            },
8203            component: {
8204                type: Object,
8205                default: null,
8206            },
8207            first_section_top_spacer_px_override: {
8208                type: Number,
8209                default: null,
8210            },
8211            last_update: Number,
8212        },
8213        watch: {
8214            active_tab(val) {
8215                if (val !== 'preview') {
8216                    this.close_text_edit_popover();
8217                }
8218                if (val === 'edit' && this.cm_instance) {
8219                    this.$nextTick(() => this.cm_instance.refresh());
8220                }
8221                if (val === 'preview') {
8222                    this.$nextTick(() => this.initialize_custom_code_interactions());
8223                }
8224                this.publish_active_tab_to_store();
8225            },
8226            // First result from the page agent has landed — drop the welcome
8227            // "Generating…" pane so the new section renders.
8228            custom_code_value(val) {
8229                if (this.ai_welcome_generating && (val || '').trim()) {
8230                    this.clear_welcome_generating_state();
8231                }
8232            },
8233            last_update() {
8234                this.close_text_edit_popover();
8235                // Don't fight a LIVE stream — the streaming buffer is the
8236                // source of truth until the build settles. But only an actually
8237                // active stream (entry still streaming/queued) should block the
8238                // resync: a stale `ai_streaming` flag (entry terminal or gone,
8239                // e.g. an aborted/errored build) must NOT wedge the section on
8240                // old HTML, or the agent's authoritative __refresh re-pull can
8241                // never land. Otherwise sync the preview straight from the
8242                // component config — the live preview repaints off
8243                // custom_code_value even when the (hidden) editor isn't mounted.
8244                let entry = this.active_codegen_stream_entry;
8245                let stream_active = this.ai_streaming
8246                    && entry
8247                    && (entry.status === 'streaming' || entry.status === 'queued');
8248                if (!stream_active) {
8249                    if (this.ai_streaming) {
8250                        // Stale flag — release the editor lock before syncing.
8251                        this.force_clear_ai_loading_state();
8252                    }
8253                    let config_latest = this.component.get('config_latest') || {};
8254                    let new_value = config_latest.custom_code || '';
8255                    if (this.cm_instance && new_value !== this.cm_instance.getValue()) {
8256                        this.cm_instance.setValue(new_value);
8257                    }
8258                    this.custom_code_value = new_value;
8259                    this.publish_current_code_to_store();
8260                }
8261                this.$nextTick(() => this.initialize_custom_code_interactions());
8262            },
8263            // Re-measure the auto-grow welcome composer whenever the
8264            // prompt changes from outside the textarea — example pill
8265            // click, post-submit clear. Typing still routes through
8266            // @input for synchronous response.
8267            ai_prompt() {
8268                this.$nextTick(() => this.auto_resize_ai_prompt());
8269            },
8270            // Editor-lock cleanup. The launcher computed reads the
8271            // store entry directly (and the 1Hz heartbeat keeps it
8272            // fresh), so this watcher is no longer the launcher's
8273            // safety net — it just makes sure `ai_streaming` and the
8274            // editor's readOnly flag release when the entry goes
8275            // terminal, since the morph-preview / editor lock are
8276            // still driven from local flags rather than the store.
8277            ai_stream_event_tick() {
8278                let entry = this.active_codegen_stream_entry;
8279                if (!entry) {
8280                    return;
8281                }
8282                if (entry.status !== 'done' && entry.status !== 'error' && entry.status !== 'aborted_local') {
8283                    return;
8284                }
8285                // Editor-lock-only cleanup: this watcher does not
8286                // gate launcher visibility (the launcher binds to
8287                // store.codegen_request_state). It exists to release
8288                // ai_streaming / readOnly when the central store
8289                // says the request is over, in case the subscription
8290                // path missed an event.
8291                let needs_clear = this.ai_streaming
8292                    || this.ai_streaming_status === 'streaming'
8293                    || this.ai_streaming_status === 'queued';
8294                if (!needs_clear) {
8295                    return;
8296                }
8297                this.force_clear_ai_loading_state();
8298            },
8299        },
8300        computed: {
8301            safe_custom_code_value() {
8302                return this.sanitize_custom_code_html(this.custom_code_value);
8303            },
8304            safe_custom_code() {
8305                return this.sanitize_custom_code_html(this.custom_code || '');
8306            },
8307            // Source emptiness, not sanitized HTML — a 
8307<script> load is
8308            // still "has code" even though preview strips it.
8309            custom_code_is_empty() {
8310                return !(this.custom_code_value || '').trim();
8311            },
8312            custom_code_preview_is_blank() {
8313                return !custom_code_html_has_visible_preview_content(this.safe_custom_code_value);
8314            },
8315            // Unique external (non-Clubspot-CDN) image URLs referenced
8316            // by this section's persisted HTML — both <img src> and
8317            // inline `background-image: url(...)`. Drives the bulk
8318            // "save external images to my library" affordance. Recomputes
8319            // only when custom_code_value changes, so parsing cost is
8320            // bounded to actual edits.
8321            section_external_image_urls() {
8322                return this.collect_section_external_image_urls(this.custom_code_value);
8323            },
8324            section_external_image_count() {
8325                return this.section_external_image_urls.length;
8326            },
8327            ai_loading_pane_style() {
8328                if (!this.ai_loading_pane_height) {
8329                    return null;
8330                }
8331                return {
8332                    minHeight: this.ai_loading_pane_height + 'px',
8333                };
8334            },
8335            ai_loading_wrap_style() {
8336                if (!this.ai_loading_pane_height) {
8337                    return null;
8338                }
8339                return {
8340                    minHeight: this.ai_loading_pane_height + 'px',
8341                };
8342            },
8343            has_ai_thread_for_component() {
8344                if (typeof store === 'undefined') {
8345                    return false;
8346                }
8347                let component_id = this.component && this.component.id;
8348                if (!component_id) {
8349                    return false;
8350                }
8351                let config_latest = this.component && typeof this.component.get === 'function'
8352                    ? (this.component.get('config_latest') || {})
8353                    : {};
8354                if (config_latest.ai_thread_id) {
8355                    return true;
8356                }
8357                let thread_map = store.webbuilder_code_component_has_ai_thread || {};
8358                return !!thread_map[component_id];
8359            },
8360            ai_prompt_loading() {
8361                if (typeof store === 'undefined' || !store.components || !store.components.ai_mcp_panel) {
8362                    return false;
8363                }
8364                return !!store.components.ai_mcp_panel.mcp_loading;
8365            },
8366            ai_prompt_can_submit() {
8367                if (!(this.ai_prompt || '').trim().length) {
8368                    return false;
8369                }
8370                return !this.ai_prompt_loading;
8371            },
8372            // ─── Welcome composer image attachments ──────────────────
8373            //
8374            // The welcome-state composer (rendered when a custom-code
8375            // section is empty and there's no prior AI thread) gets a
8376            // paperclip + drag-drop + thumbnail row mirroring the
8377            // chat panel. To avoid duplicating the canvas-resize /
8378            // base64 prep pipeline, we delegate ALL state to the chat
8379            // panel via the global ai_mcp_panel_attach_files helper.
8380            // This computed is just a reactive read of that shared
8381            // state so the welcome composer's thumbnails update in
8382            // lockstep with the chat panel's thumbnails.
8383            welcome_file_upload_enabled() {
8384                if (typeof store === 'undefined' || !store.ai_mcp_panel_component_instance) {
8385                    return false;
8386                }
8387                return !!store.ai_mcp_panel_component_instance.mode_enable_file_upload;
8388            },
8389            welcome_attachments() {
8390                if (typeof store === 'undefined' || !store.components || !store.components.ai_mcp_panel) {
8391                    return [];
8392                }
8393                let panel = store.components.ai_mcp_panel;
8394                return Array.isArray(panel.mcp_attachments) ? panel.mcp_attachments : [];
8395            },
8396            // ─── Single source of truth: store.codegen_request_state ──
8397            //
8398            // The launcher's "Generating…" UI reads ONE reactive field:
8399            //   store.codegen_request_state[component_id]
8400            //
8401            // That field is owned by store.ai_streams (the central
8402            // streams manager in ai-mcp-panel.html) and written ONLY
8403            // from there as the request transitions through:
8404            //   'starting' → 'queued' → 'streaming' → 'done' / 'error' / 'aborted'
8405            //
8406            // Because `store` is a Vue 3 `reactive()` proxy, both the
8407            // launcher (this file) and the chat panel bind to the same
8408            // key reactively. START and STOP are visible to both
8409            // surfaces from a single write — no subscriptions, no
8410            // ticks, no heartbeats, no watchdog needed.
8411            //
8412            // Local flags (`ai_streaming`, `ai_streaming_buffer`,
8413            // `ai_streaming_status`) still drive the EDITOR LOCK /
8414            // morph preview pane via the subscription path; those
8415            // concerns are deliberately decoupled from the launcher
8416            // so a missed editor-side event can't strand the spinner.
8417            codegen_request_status() {
8418                if (typeof store === 'undefined' || !store.codegen_request_state) {
8419                    return null;
8420                }
8421                let component_id = this.component && this.component.id;
8422                if (!component_id) {
8423                    return null;
8424                }
8425                return store.codegen_request_state[component_id] || null;
8426            },
8427            active_codegen_stream_entry() {
8428                /* eslint-disable-next-line no-unused-expressions */
8429                this.ai_stream_event_tick;
8430                if (typeof store === 'undefined' || !store.ai_streams || !store.ai_streams.get) {
8431                    return null;
8432                }
8433                let component_id = this.component && this.component.id;
8434                if (!component_id) {
8435                    return null;
8436                }
8437                return store.ai_streams.get('codegen:' + component_id);
8438            },
8439            effective_ai_streaming_status() {
8440                let s = this.codegen_request_status;
8441                if (s) {
8442                    return s;
8443                }
8444                return this.ai_streaming_status;
8445            },
8446            // Launcher visibility — one rule, one field.
8447            ai_generation_active() {
8448                let s = this.codegen_request_status;
8449                return s === 'starting' || s === 'queued' || s === 'streaming';
8450            },
8451            ai_panel_is_open() {
8452                if (typeof store === 'undefined' || !store.components || !store.components.ai_mcp_panel) {
8453                    return false;
8454                }
8455                return !!store.components.ai_mcp_panel.expanded;
8456            },
8457            ai_panel_is_open_for_this_component() {
8458                if (!this.ai_panel_is_open || typeof store === 'undefined') {
8459                    return false;
8460                }
8461                store.webbuilder_code_active_component_revision;
8462                let component_id = this.component && this.component.id;
8463                if (!component_id) {
8464                    return false;
8465                }
8466                return store.webbuilder_code_active_component_id === component_id;
8467            },
8468            // Idle Edit with AI lives on the hover toolbar. This in-section
8469            // pill is only the generating/queued indicator.
8470            ai_launcher_visible() {
8471                if (this.ai_panel_is_open) {
8472                    return false;
8473                }
8474                return this.ai_generation_active;
8475            },
8476            // Dots overlay — visible while we have a stream that
8477            // hasn't started emitting code yet. Reads the central
8478            // status for the bookended states; reads the entry's
8479            // code_buffer (still owned by the streams manager) to
8480            // distinguish "streaming but no chunks yet" from
8481            // "streaming with content".
8482            ai_streaming_pending() {
8483                let s = this.codegen_request_status;
8484                if (s === 'starting' || s === 'queued') {
8485                    return true;
8486                }
8487                if (s !== 'streaming') {
8488                    return false;
8489                }
8490                let entry = this.active_codegen_stream_entry;
8491                if (this.ai_streaming_buffer && this.ai_streaming_buffer.trim()) {
8492                    return false;
8493                }
8494                return !!(entry && !(entry.code_buffer || '').trim());
8495            },
8496        },
8497        mounted() {
8498            this.loading = false;
8499            // Listen for the section-actions overlay's "Save external
8500            // images" action. The overlay only has the component Parse
8501            // object, so it broadcasts the id and the matching live
8502            // instance runs the batch (which owns the persist +
8503            // re-render path).
8504            if (typeof store !== 'undefined' && store.eventBus) {
8505                this._save_external_images_handler = (component) => {
8506                    let target_id = component && component.id ? component.id : component;
8507                    if (this.component && this.component.id === target_id) {
8508                        this.import_all_external_images_in_section();
8509                    }
8510                };
8511                store.eventBus.$on('webbuilderSaveExternalImages', this._save_external_images_handler);
8512                this._webbuilder_refresh_replay_handler = () => {
8513                    let component_id = this.component && this.component.id;
8514                    if (!component_id || !store.ai_streams || typeof store.ai_streams.get !== 'function') {
8515                        return;
8516                    }
8517                    let entry = store.ai_streams.get('codegen:' + component_id);
8518                    if (!entry) {
8519                        return;
8520                    }
8521                    this.subscribe_to_codegen_stream();
8522                };
8523                store.eventBus.$on('webbuilderPageRefreshReconciled', this._webbuilder_refresh_replay_handler);
8524            }
8525            if (this.preview_element) {
8526                let config_latest = (this.component && this.component.get('config_latest')) || {};
8527                this.custom_code_value = config_latest.custom_code || '';
8528                // Publish the live editor contents to the shared store
8529                // so the chat panel's per-message apply-state badge
8530                // can diff against it reactively without needing a
8531                // back-reference to this component.
8532                this.publish_current_code_to_store();
8533                this.$nextTick(() => {
8534                    this.cm_instance = CodeMirror(this.$refs.cm_editor, {
8535                        value: this.custom_code_value,
8536                        mode: 'htmlmixed',
8537                        lineNumbers: true,
8538                        autoCloseBrackets: true,
8539                        autoCloseTags: true,
8540                        matchBrackets: true,
8541                        lineWrapping: true,
8542                        tabSize: 2,
8543                        indentWithTabs: false,
8544                        extraKeys: {
8545                            'Tab': function(cm) {
8546                                cm.replaceSelection('  ', 'end');
8547                            }
8548                        },
8549                    });
8550                    this.cm_instance.on('change', (editor) => {
8551                        // Suppress save while we're driving the editor
8552                        // from a streaming buffer — the stream's `done`
8553                        // handler runs the canonical save once at the
8554                        // end, and persisting partial frames would
8555                        // spam the API.
8556                        if (this._suppress_change_save) {
8557                            // Still surface the live value to the
8558                            // store so the apply-state badge stays
8559                            // accurate during the typewriter — just
8560                            // don't fire the debounced save.
8561                            this.custom_code_value = editor.getValue();
8562                            this.publish_current_code_to_store();
8563                            return;
8564                        }
8565                        this.debounce(() => {
8566                            let value = editor.getValue();
8567                            this.custom_code_value = value;
8568                            this.publish_current_code_to_store();
8569                            save_changes_for_component(this.component, 'custom_code', value);
8570                            this.mark_ai_code_revision_changed();
8571                        });
8572                    });
8573                });
8574                // Subscribe to this component's codegen stream key so
8575                // the editor + preview update live, independently of
8576                // whether the chat panel is open. Re-subscribing on
8577                // every mount also covers the page-reload recovery
8578                // case — if a passive entry exists for our key, the
8579                // replay event hands us the partial buffer.
8580                this.subscribe_to_codegen_stream();
8581            }
8582            this.$nextTick(() => {
8583                this.initialize_custom_code_interactions();
8584                this.auto_resize_ai_prompt();
8585                this.setup_ai_builder_presence_observer();
8586                this.publish_active_tab_to_store();
8587                if (typeof store !== 'undefined' &&
8588                        store.webbuilder_code_active_component_id === (this.component && this.component.id)) {
8589                    store.webbuilder_code_active_component = this;
8590                    store.webbuilder_code_active_component_revision = Date.now();
8591                    this.subscribe_to_codegen_stream();
8592                }
8593            });
8594        },
8595        beforeUnmount() {
8596            this.close_text_edit_popover();
8597            this.cm_instance = null;
8598            if (typeof store !== 'undefined' && store.eventBus && this._save_external_images_handler) {
8599                if (typeof store.eventBus.$off === 'function') {
8600                    store.eventBus.$off('webbuilderSaveExternalImages', this._save_external_images_handler);
8601                } else if (store.eventBus.listeners && Array.isArray(store.eventBus.listeners.webbuilderSaveExternalImages)) {
8602                    store.eventBus.listeners.webbuilderSaveExternalImages = store.eventBus.listeners.webbuilderSaveExternalImages.filter(callback => {
8603                        return callback !== this._save_external_images_handler;
8604                    });
8605                }
8606                this._save_external_images_handler = null;
8607            }
8608            if (typeof store !== 'undefined' && store.eventBus && this._webbuilder_refresh_replay_handler) {
8609                if (typeof store.eventBus.$off === 'function') {
8610                    store.eventBus.$off('webbuilderPageRefreshReconciled', this._webbuilder_refresh_replay_handler);
8611                } else if (store.eventBus.listeners && Array.isArray(store.eventBus.listeners.webbuilderPageRefreshReconciled)) {
8612                    store.eventBus.listeners.webbuilderPageRefreshReconciled = store.eventBus.listeners.webbuilderPageRefreshReconciled.filter(callback => {
8613                        return callback !== this._webbuilder_refresh_replay_handler;
8614                    });
8615                }
8616                this._webbuilder_refresh_replay_handler = null;
8617            }
8618            // Unsubscribe — but do NOT cancel the underlying stream.
8619            // The entry stays alive in store.ai_streams so a remount
8620            // (e.g. component re-rendered after a save) can re-attach
8621            // and continue showing progress.
8622            if (typeof this._codegen_stream_unsubscribe === 'function') {
8623                try { this._codegen_stream_unsubscribe(); } catch (e) {}
8624                this._codegen_stream_unsubscribe = null;
8625            }
8626            if (this._codegen_stream_raf) {
8627                cancelAnimationFrame(this._codegen_stream_raf);
8628                this._codegen_stream_raf = null;
8629            }
8630            if (this._ai_streaming_starting_timer) {
8631                clearTimeout(this._ai_streaming_starting_timer);
8632                this._ai_streaming_starting_timer = null;
8633            }
8634            if (this._ai_welcome_generating_timer) {
8635                clearTimeout(this._ai_welcome_generating_timer);
8636                this._ai_welcome_generating_timer = null;
8637            }
8638            if (this.ai_loading_pane_height_release_timer) {
8639                clearTimeout(this.ai_loading_pane_height_release_timer);
8640                this.ai_loading_pane_height_release_timer = null;
8641            }
8642            if (this._ai_builder_presence_observer) {
8643                this._ai_builder_presence_observer.disconnect();
8644                this._ai_builder_presence_observer = null;
8645            }
8646            if (this._ai_builder_presence_timeout) {
8647                clearTimeout(this._ai_builder_presence_timeout);
8648                this._ai_builder_presence_timeout = null;
8649            }
8650            // If this component was the active target for the AI
8651            // builder, clear the pointer so the panel doesn't try to
8652            // apply code into a Vue instance that's no longer mounted.
8653            if (typeof store !== 'undefined' && store.webbuilder_code_active_component === this) {
8654                store.webbuilder_code_active_component = null;
8655                store.webbuilder_code_active_component_id = null;
8656            }
8657            // Drop this section's published tab state so a stale 'edit' value
8658            // can't keep the launcher shown after the section is gone.
8659            if (typeof store !== 'undefined'
8660                    && store.webbuilder_custom_code_tab_by_client_id
8661                    && this.component
8662                    && typeof this.component.get === 'function') {
8663                let cid = this.component.get('client_id');
8664                if (cid) {
8665                    delete store.webbuilder_custom_code_tab_by_client_id[cid];
8666                }
8667            }
8668        },
8669        methods: {
8670            page_has_multiple_custom_code_components() {
8671                if (typeof store === 'undefined' || !Array.isArray(store.page_components_latest)) {
8672                    return false;
8673                }
8674                let custom_code_count = 0;
8675                for (let i = 0; i < store.page_components_latest.length; i++) {
8676                    let page_component = store.page_components_latest[i];
8677                    if (page_component && page_component.get && page_component.get('type') === 'custom-code') {
8678                        custom_code_count++;
8679                    }
8680                    if (custom_code_count > 1) {
8681                        return true;
8682                    }
8683                }
8684                return false;
8685            },
8686            setup_ai_builder_presence_observer() {
8687                if (!this.preview_element) {
8688                    return;
8689                }
8690                if (!this.page_has_multiple_custom_code_components()) {
8691                    return;
8692                }
8693                if (typeof IntersectionObserver === 'undefined') {
8694                    return;
8695                }
8696                if (!this.$el || this.$el.nodeType !== 1) {
8697                    return;
8698                }
8699                this._ai_builder_presence_observer = new IntersectionObserver(entries => {
8700                    entries.forEach(entry => {
8701                        if (entry && entry.isIntersecting && entry.intersectionRatio >= 0.55) {
8702                            this.activate_ai_builder_target_from_presence();
8703                        }
8704                    });
8705                }, {
8706                    threshold: [0.55],
8707                });
8708                this._ai_builder_presence_observer.observe(this.$el);
8709            },
8710            activate_ai_builder_target_from_presence() {
8711                if (typeof store === 'undefined') {
8712                    return;
8713                }
8714                if (!this.page_has_multiple_custom_code_components()) {
8715                    return;
8716                }
8717                if (!store.ai_mcp_floating_panel_ui) {
8718                    return;
8719                }
8720                let panel_state = store.components && store.components.ai_mcp_panel;
8721                if (!panel_state || !panel_state.expanded) {
8722                    return;
8723                }
8724                if (panel_state.mcp_loading || (panel_state.mcp_prompt || '').trim()) {
8725                    return;
8726                }
8727                let messages = Array.isArray(panel_state.mcp_messages) ? panel_state.mcp_messages : [];
8728                let last_message = messages.length ? messages[messages.length - 1] : null;
8729                if (last_message && last_message.streaming) {
8730                    return;
8731                }
8732                let component_id = this.component && this.component.id ? this.component.id : null;
8733                if (!component_id || store.webbuilder_code_active_component_id === component_id) {
8734                    return;
8735                }
8736                if (this._ai_builder_presence_timeout) {
8737                    clearTimeout(this._ai_builder_presence_timeout);
8738                }
8739                this._ai_builder_presence_timeout = setTimeout(() => {
8740                    this.activate_ai_builder_target();
8741                    this._ai_builder_presence_timeout = null;
8742                }, 120);
8743            },
8744            activate_ai_builder_target(options) {
8745                if (typeof store === 'undefined') {
8746                    return;
8747                }
8748                // Reclaim the per-component codegen mode in case the
8749                // page-level webbuilder-agent launcher switched it.
8750                store.ai_mcp_mode = 'webbuilder-code';
8751                let opts = options || {};
8752                let previous_component_id = store.webbuilder_code_active_component_id || null;
8753                store.webbuilder_code_active_component = this;
8754                store.webbuilder_code_active_component_id = this.component && this.component.id ? this.component.id : null;
8755                store.webbuilder_code_active_component_revision = Date.now();
8756                this.subscribe_to_codegen_stream();
8757                let panel = store.ai_mcp_panel_component_instance || null;
8758                if (panel && typeof panel.switch_webbuilder_component_thread === 'function') {
8759                    panel.switch_webbuilder_component_thread(
8760                        store.webbuilder_code_active_component_id,
8761                        previous_component_id,
8762                        opts
8763                    );
8764                }
8765            },
8766            set_ai_prompt_example(example) {
8767                this.ai_prompt = example || '';
8768            },
8769            // ─── Welcome composer attachment handlers ────────────────
8770            //
8771            // All paths funnel through the global helper
8772            // ai_mcp_panel_attach_files which writes into the shared
8773            // chat panel state (canvas-resize + base64 prep happens
8774            // there). The welcome composer is a UI-only mirror — no
8775            // local Blob storage, no local prep.
8776            open_welcome_file_picker() {
8777                if (!this.welcome_file_upload_enabled) {
8778                    return;
8779                }
8780                if (this.ai_prompt_loading) {
8781                    return;
8782                }
8783                let input = this.$refs && this.$refs.ai_prompt_file_input;
8784                if (!input) {
8785                    return;
8786                }
8787                input.click();
8788            },
8789            handle_welcome_file_input_change(event) {
8790                let files = event && event.target && event.target.files
8791                    ? Array.from(event.target.files)
8792                    : [];
8793                if (event && event.target) {
8794                    event.target.value = '';
8795                }
8796                if (!files.length) {
8797                    return;
8798                }
8799                if (typeof ai_mcp_panel_attach_files === 'function') {
8800                    ai_mcp_panel_attach_files({
8801                        component: this,
8802                        files:     files,
8803                    });
8804                }
8805            },
8806            handle_welcome_dragenter(event) {
8807                if (!this.welcome_file_upload_enabled) {
8808                    return;
8809                }
8810                if (!this.welcome_event_has_image_files(event)) {
8811                    return;
8812                }
8813                event.preventDefault();
8814                this.welcome_drop_counter++;
8815                this.welcome_drop_active = this.welcome_drop_counter > 0;
8816            },
8817            handle_welcome_dragover(event) {
8818                if (!this.welcome_file_upload_enabled) {
8819                    return;
8820                }
8821                if (!this.welcome_event_has_image_files(event)) {
8822                    return;
8823                }
8824                if (event.dataTransfer) {
8825                    try {
8826                        event.dataTransfer.dropEffect = 'copy';
8827                    } catch (drop_effect_err) {}
8828                }
8829            },
8830            handle_welcome_dragleave() {
8831                if (!this.welcome_file_upload_enabled) {
8832                    return;
8833                }
8834                if (this.welcome_drop_counter > 0) {
8835                    this.welcome_drop_counter--;
8836                }
8837                if (this.welcome_drop_counter <= 0) {
8838                    this.welcome_drop_counter = 0;
8839                    this.welcome_drop_active  = false;
8840                }
8841            },
8842            handle_welcome_drop(event) {
8843                this.welcome_drop_counter = 0;
8844                this.welcome_drop_active  = false;
8845                if (!this.welcome_file_upload_enabled) {
8846                    return;
8847                }
8848                let files = event && event.dataTransfer && event.dataTransfer.files
8849                    ? Array.from(event.dataTransfer.files)
8850                    : [];
8851                if (!files.length) {
8852                    return;
8853                }
8854                let images = files.filter(function (file) {
8855                    return file && typeof file.type === 'string' && file.type.indexOf('image/') === 0;
8856                });
8857                if (!images.length) {
8858                    return;
8859                }
8860                if (typeof ai_mcp_panel_attach_files === 'function') {
8861                    ai_mcp_panel_attach_files({
8862                        component: this,
8863                        files:     images,
8864                    });
8865                }
8866            },
8867            handle_welcome_paste(event) {
8868                if (!this.welcome_file_upload_enabled) {
8869                    return;
8870                }
8871                if (!event || !event.clipboardData || !event.clipboardData.items) {
8872                    return;
8873                }
8874                let items = Array.from(event.clipboardData.items);
8875                let pasted = [];
8876                for (let i = 0; i < items.length; i++) {
8877                    let item = items[i];
8878                    if (!item || item.kind !== 'file') {
8879                        continue;
8880                    }
8881                    if (typeof item.type !== 'string' || item.type.indexOf('image/') !== 0) {
8882                        continue;
8883                    }
8884                    let file = item.getAsFile && item.getAsFile();
8885                    if (file) {
8886                        pasted.push(file);
8887                    }
8888                }
8889                if (!pasted.length) {
8890                    return;
8891                }
8892                event.preventDefault();
8893                if (typeof ai_mcp_panel_attach_files === 'function') {
8894                    ai_mcp_panel_attach_files({
8895                        component: this,
8896                        files:     pasted,
8897                    });
8898                }
8899            },
8900            welcome_event_has_image_files(event) {
8901                if (!event || !event.dataTransfer) {
8902                    return false;
8903                }
8904                let types = event.dataTransfer.types;
8905                if (!types) {
8906                    return false;
8907                }
8908                for (let i = 0; i < types.length; i++) {
8909                    if (types[i] === 'Files' || types[i] === 'application/x-moz-file') {
8910                        return true;
8911                    }
8912                }
8913                return false;
8914            },
8915            welcome_remove_attachment(index) {
8916                if (typeof store === 'undefined' || !store.ai_mcp_panel_component_instance) {
8917                    return;
8918                }
8919                let panel = store.ai_mcp_panel_component_instance;
8920                if (typeof panel.remove_attachment === 'function') {
8921                    panel.remove_attachment(index);
8922                }
8923            },
8924            // Auto-grow the welcome-page composer textarea. Starts at
8925            // a single line (height matches one line-height) and grows
8926            // up to the CSS max-height (~8 lines), after which the
8927            // textarea scrolls. We reset height to '0' before measuring
8928            // scrollHeight so the height can shrink as well as grow
8929            // (otherwise scrollHeight only ever increases).
8930            //
8931            // Called from @input (typing), the watcher on `ai_prompt`
8932            // (programmatic changes like example pill click /
8933            // post-submit clear), and once on mounted() to settle the
8934            // initial layout.
8935            auto_resize_ai_prompt() {
8936                let el = this.$refs && this.$refs.ai_prompt_textarea;
8937                if (!el) {
8938                    return;
8939                }
8940                el.style.height = '0px';
8941                let next = el.scrollHeight;
8942                if (!next) {
8943                    next = 21;
8944                }
8945                el.style.height = next + 'px';
8946            },
8947            submit_ai_prompt() {
8948                if (!this.ai_prompt_can_submit) {
8949                    return;
8950                }
8951                // The blank welcome composer now seeds the single page agent
8952                // (focused on this section) rather than the per-component
8953                // codegen chat. The page agent owns the conversation + send.
8954                if (typeof ai_mcp_panel_open_agent_with_reference !== 'function') {
8955                    this.open_ai_builder();
8956                    return;
8957                }
8958                let opened = ai_mcp_panel_open_agent_with_reference({
8959                    client_id:      this.component && this.component.get('client_id'),
8960                    component_type: 'custom-code',
8961                    seed_prompt:    this.ai_prompt,
8962                    auto_submit:    true,
8963                });
8964                if (opened) {
8965                    this.ai_prompt = '';
8966                    this.start_welcome_generating_state();
8967                } else {
8968                    this.open_ai_builder();
8969                }
8970            },
8971            // Swap the welcome composer for the "Generating…" pane while the
8972            // page agent produces the first result. Cleared when content lands
8973            // (custom_code_value watcher) or by a safety timer.
8974            start_welcome_generating_state() {
8975                this.active_tab = 'preview';
8976                this.close_text_edit_popover();
8977                this.ai_welcome_generating = true;
8978                if (this._ai_welcome_generating_timer) {
8979                    clearTimeout(this._ai_welcome_generating_timer);
8980                }
8981                this._ai_welcome_generating_timer = setTimeout(() => {
8982                    this.clear_welcome_generating_state();
8983                }, 90000);
8984            },
8985            clear_welcome_generating_state() {
8986                this.ai_welcome_generating = false;
8987                if (this._ai_welcome_generating_timer) {
8988                    clearTimeout(this._ai_welcome_generating_timer);
8989                    this._ai_welcome_generating_timer = null;
8990                }
8991            },
8992            // Writes 'starting' into the central store so the
8993            // launcher reflects intent instantly. store.ai_streams.start
8994            // overwrites this with 'queued' or 'streaming' once the
8995            // entry materializes. The 15s timer is the safety net for
8996            // the rare case where the panel never actually fires a
8997            // submit (e.g. the panel app didn't mount yet).
8998            start_ai_streaming_starting_state() {
8999                this.capture_ai_loading_pane_height();
9000                this.blur_ai_prompt_before_loading();
9001                let prior_scroll_y = window.scrollY;
9002                let component_id = this.component && this.component.id;
9003                if (typeof store !== 'undefined' && store.ai_streams && typeof store.ai_streams.mark_starting === 'function' && component_id) {
9004                    store.ai_streams.mark_starting(component_id);
9005                }
9006                this.active_tab = 'preview';
9007                this.close_text_edit_popover();
9008                this.$nextTick(() => {
9009                    if (Math.abs(window.scrollY - prior_scroll_y) > 1) {
9010                        window.scrollTo(window.scrollX, prior_scroll_y);
9011                    }
9012                });
9013                if (this._ai_streaming_starting_timer) {
9014                    clearTimeout(this._ai_streaming_starting_timer);
9015                }
9016                this._ai_streaming_starting_timer = setTimeout(() => {
9017                    this.clear_ai_streaming_starting_state();
9018                }, 15000);
9019            },
9020            // Clears the 'starting' bridge from central state if (and
9021            // only if) it's still the current value — never overwrites
9022            // a real downstream status like 'streaming' / 'done'.
9023            clear_ai_streaming_starting_state() {
9024                if (typeof store !== 'undefined' && store.codegen_request_state) {
9025                    let component_id = this.component && this.component.id;
9026                    if (component_id && store.codegen_request_state[component_id] === 'starting') {
9027                        delete store.codegen_request_state[component_id];
9028                    }
9029                }
9030                if (this._ai_streaming_starting_timer) {
9031                    clearTimeout(this._ai_streaming_starting_timer);
9032                    this._ai_streaming_starting_timer = null;
9033                }
9034            },
9035            // Idempotent hard-reset of every flag that feeds the
9036            // launcher / loading-overlay computeds. Called from the
9037            // panel's done / fail / snap paths so the launcher always
9038            // converges to idle even if a stream subscriber missed
9039            // an event, threw mid-handler, or fired its `done` while
9040            // the component was unmounted. Safe to call multiple
9041            // times. Does NOT touch the editor contents — the panel's
9042            // apply_generated_code is responsible for the canonical
9043            // code save.
9044            force_clear_ai_loading_state() {
9045                this.clear_ai_streaming_starting_state();
9046                this.ai_streaming = false;
9047                this.release_ai_loading_pane_height_after_render();
9048                if (this.ai_streaming_status === 'streaming' || this.ai_streaming_status === 'queued' || this.ai_streaming_status === null) {
9049                    this.ai_streaming_status = 'done';
9050                }
9051                this.ai_streaming_buffer = '';
9052                this._last_flushed_buffer_len = -1;
9053                this.ai_streaming_child_min_heights = [];
9054                if (this._codegen_stream_raf) {
9055                    cancelAnimationFrame(this._codegen_stream_raf);
9056                    this._codegen_stream_raf = null;
9057                }
9058                if (this.cm_instance && this.cm_instance.getOption && this.cm_instance.getOption('readOnly')) {
9059                    this.cm_instance.setOption('readOnly', false);
9060                }
9061                this.ai_stream_event_tick = (this.ai_stream_event_tick + 1) | 0;
9062            },
9063            capture_ai_loading_pane_height() {
9064                if (this.ai_loading_pane_height_release_timer) {
9065                    clearTimeout(this.ai_loading_pane_height_release_timer);
9066                    this.ai_loading_pane_height_release_timer = null;
9067                }
9068                // `$el` can be a Comment / Text node when the root
9069                // `<template v-if>` is currently false (Vue 3 treats
9070                // a falsy single-root v-if as a placeholder comment),
9071                // and it lacks querySelectorAll. Bail in that case
9072                // so the streaming subscriber doesn't throw on every
9073                // pane-height capture.
9074                let root = this.$el;
9075                if (!root || typeof root.querySelectorAll !== 'function') {
9076                    return;
9077                }
9078                let panes = Array.prototype.slice.call(root.querySelectorAll('.custom-code-preview-pane'));
9079                let visible = panes.find((pane) => {
9080                    if (!pane || pane.classList.contains('custom-code-loading-pane')) {
9081                        return false;
9082                    }
9083                    let style = window.getComputedStyle(pane);
9084                    return style.display !== 'none' && pane.getClientRects().length > 0;
9085                });
9086                if (!visible) {
9087                    let hidden_height = this.measure_hidden_custom_code_preview_height();
9088                    if (hidden_height > 0) {
9089                        this.ai_loading_pane_height = Math.max(420, hidden_height);
9090                    }
9091                    return;
9092                }
9093                let rect_height = visible.getBoundingClientRect().height || 0;
9094                let scroll_height = visible.scrollHeight || 0;
9095                let height = Math.ceil(Math.max(rect_height, scroll_height));
9096                if (height > 0) {
9097                    this.ai_loading_pane_height = Math.max(420, height);
9098                }
9099                this.capture_streaming_child_min_heights_from_node(visible);
9100            },
9101            measure_hidden_custom_code_preview_height() {
9102                let root = this.$el;
9103                if (!root || typeof root.getBoundingClientRect !== 'function') {
9104                    return 0;
9105                }
9106                let html = this.safe_custom_code_value || '';
9107                if (!html.trim()) {
9108                    return 0;
9109                }
9110                let root_rect = root.getBoundingClientRect();
9111                let width = Math.floor(root_rect.width || root.clientWidth || 0);
9112                if (width <= 0 && root.parentElement) {
9113                    let parent_rect = root.parentElement.getBoundingClientRect();
9114                    width = Math.floor(parent_rect.width || root.parentElement.clientWidth || 0);
9115                }
9116                if (width <= 0) {
9117                    return 0;
9118                }
9119                let probe = document.createElement('div');
9120                probe.className = 'custom-code-pane custom-code-preview-pane';
9121                probe.style.position = 'absolute';
9122                probe.style.visibility = 'hidden';
9123                probe.style.pointerEvents = 'none';
9124                probe.style.left = '-10000px';
9125                probe.style.top = '0';
9126                probe.style.width = width + 'px';
9127                probe.style.height = 'auto';
9128                probe.style.overflow = 'visible';
9129                probe.style.boxSizing = 'border-box';
9130                probe.innerHTML = html;
9131                document.body.appendChild(probe);
9132                let rect_height = probe.getBoundingClientRect().height || 0;
9133                let scroll_height = probe.scrollHeight || 0;
9134                let height = Math.ceil(Math.max(rect_height, scroll_height));
9135                this.capture_streaming_child_min_heights_from_node(probe);
9136                probe.remove();
9137                return height;
9138            },
9139            capture_streaming_child_min_heights_from_node(node) {
9140                if (!node || !node.childNodes) {
9141                    this.ai_streaming_child_min_heights = [];
9142                    return;
9143                }
9144                this.ai_streaming_child_min_heights = Array.prototype.slice.call(node.childNodes).map((child) => {
9145                    if (!child || child.nodeType !== 1) {
9146                        return 0;
9147                    }
9148                    let rect_height = child.getBoundingClientRect().height || 0;
9149                    let scroll_height = child.scrollHeight || 0;
9150                    return Math.ceil(Math.max(rect_height, scroll_height));
9151                });
9152            },
9153            release_ai_loading_pane_height_after_render() {
9154                if (this.ai_loading_pane_height_release_timer) {
9155                    clearTimeout(this.ai_loading_pane_height_release_timer);
9156                    this.ai_loading_pane_height_release_timer = null;
9157                }
9158                if (!this.ai_loading_pane_height) {
9159                    this.ai_streaming_child_min_heights = [];
9160                    return;
9161                }
9162                this.ai_loading_pane_height_release_timer = setTimeout(() => {
9163                    this.ai_loading_pane_height = null;
9164                    this.ai_streaming_child_min_heights = [];
9165                    this.ai_loading_pane_height_release_timer = null;
9166                }, 50);
9167            },
9168            blur_ai_prompt_before_loading() {
9169                let prompt = this.$refs && this.$refs.ai_prompt_textarea;
9170                if (!prompt || document.activeElement !== prompt) {
9171                    return;
9172                }
9173                prompt.blur();
9174            },
9175            // Launcher click — pin this component as the AI panel's
9176            // active target and expand the panel. The panel runs in
9177            // a separate Vue app (app_ai_mcp, mounted in
9178            // web-builder/primary.ejs) and watches
9179            // store.webbuilder_code_active_component_id; setting it
9180            // here triggers the panel to load this component's thread.
9181            open_ai_builder() {
9182                if (typeof store === 'undefined') {
9183                    return;
9184                }
9185                // Open the single page agent focused on this section. (The old
9186                // per-component codegen activation is left dormant below for a
9187                // future live agent->codegen stream hookup.)
9188                if (typeof ai_mcp_panel_open_agent_with_reference === 'function'
9189                    && this.component
9190                    && typeof this.component.get === 'function') {
9191                    ai_mcp_panel_open_agent_with_reference({
9192                        client_id:      this.component.get('client_id'),
9193                        component_type: 'custom-code',
9194                    });
9195                    return;
9196                }
9197                let panel_state = store.components && store.components.ai_mcp_panel;
9198                if (panel_state) {
9199                    panel_state.user_toggled = true;
9200                    panel_state.expanded = true;
9201                    if (panel_state.mcp_panel_width) {
9202                        document.body.style.setProperty('--mcp-panel-width', panel_state.mcp_panel_width + 'px');
9203                    }
9204                    document.body.classList.add('ai-mcp-expanded');
9205                }
9206                this.activate_ai_builder_target({ force: true });
9207            },
9208            mark_ai_thread_for_component() {
9209                if (typeof store === 'undefined') {
9210                    return;
9211                }
9212                let component_id = this.component && this.component.id;
9213                if (!component_id) {
9214                    return;
9215                }
9216                if (!store.webbuilder_code_component_has_ai_thread) {
9217                    store.webbuilder_code_component_has_ai_thread = {};
9218                }
9219                store.webbuilder_code_component_has_ai_thread[component_id] = true;
9220            },
9221            contains_ai_builder_target(target) {
9222                if (!target) {
9223                    return false;
9224                }
9225                if (this.$el && this.$el.contains(target)) {
9226                    return true;
9227                }
9228                if (this.$refs.cm_editor && this.$refs.cm_editor.contains(target)) {
9229                    return true;
9230                }
9231                // The text-edit popover lives at document.body level
9232                // (position: fixed) so it isn't inside $el. Treat clicks
9233                // landing on it as in-component interactions so the AI
9234                // panel's outside-click handler doesn't auto-close
9235                // while the admin is typing in the popover.
9236                if (typeof target.closest === 'function' && target.closest('.cs-text-edit-popover')) {
9237                    return true;
9238                }
9239                return false;
9240            },
9241            get_current_code() {
9242                if (this.cm_instance) {
9243                    return this.cm_instance.getValue();
9244                }
9245                if (typeof this.custom_code_value === 'string') {
9246                    return this.custom_code_value;
9247                }
9248                if (this.component) {
9249                    let config_latest = this.component.get('config_latest') || {};
9250                    return config_latest.custom_code || '';
9251                }
9252                return '';
9253            },
9254            mark_ai_code_revision_changed() {
9255                if (typeof store === 'undefined') {
9256                    return;
9257                }
9258                if (store.webbuilder_code_active_component !== this) {
9259                    return;
9260                }
9261                store.webbuilder_code_active_component_revision = Date.now();
9262            },
9263            custom_code_render_root() {
9264                let host = null;
9265                if (this.preview_element) {
9266                    host = this.$refs.custom_code_preview || null;
9267                } else {
9268                    host = this.$refs.custom_code_static || null;
9269                }
9270                if (!host) {
9271                    return null;
9272                }
9273                let html = this.preview_element
9274                    ? (this.safe_custom_code_value || '')
9275                    : (this.safe_custom_code || '');
9276                let root = this.ensure_isolated_custom_code_host(host, html);
9277                this.apply_first_section_top_spacer(root);
9278                return root;
9279            },
9280            first_section_top_spacer_px(){
9281                if( this.first_section_top_spacer_px_override !== null ){
9282                    return normalize_first_section_top_spacer_px(this.first_section_top_spacer_px_override);
9283                }
9284                if( !this.is_first || !this.component ){
9285                    return 0;
9286                }
9287                let config_latest = this.component.get('config_latest') || {};
9288                let formatting = config_latest.formatting || {};
9289                return normalize_first_section_top_spacer_px(formatting.first_section_top_spacer_px);
9290            },
9291            apply_first_section_top_spacer(root){
9292                if( typeof apply_first_section_top_spacer_to_root === 'function' ){
9293                    apply_first_section_top_spacer_to_root(root, this.first_section_top_spacer_px());
9294                    return;
9295                }
9296                if( !root ){
9297                    return;
9298                }
9299            },
9300            // Mount sanitized HTML into an open shadow root so hosted theme
9301            // CSS cannot override section styles (preview ≈ live).
9302            ensure_isolated_custom_code_host(host, html) {
9303                if (!host || typeof host.attachShadow !== 'function') {
9304                    // Very old browsers: fall back to light-DOM innerHTML.
9305                    host.innerHTML = html || '';
9306                    return host;
9307                }
9308                let shadow = host.shadowRoot;
9309                if (!shadow) {
9310                    shadow = host.attachShadow({ mode: 'open' });
9311                    // Font Awesome lives on the light DOM page; clone its
9312                    // stylesheet so icons inside custom-code still render.
9313                    let fa_link = document.querySelector(
9314                        'link[href*="font-awesome"], link[href*="fontawesome"], link[href*="FontAwesome"]'
9315                    );
9316                    if (fa_link) {
9317                        let cloned = fa_link.cloneNode(true);
9318                        cloned.setAttribute('data-cs-fa-isolation', '1');
9319                        shadow.appendChild(cloned);
9320                    }
9321                    let root = document.createElement('div');
9322                    root.className = 'cs-cc-root';
9323                    shadow.appendChild(root);
9324                }
9325                // Only direct shadow children — never a <style> inside the
9326                // section HTML (querySelector would walk into .cs-cc-root).
9327                let isolation_style = null;
9328                let shadow_children = shadow.children;
9329                for (let i = 0; i < shadow_children.length; i++) {
9330                    let node = shadow_children[i];
9331                    if (node.tagName === 'STYLE') {
9332                        if (node.getAttribute('data-cs-isolation') === '1') {
9333                            isolation_style = node;
9334                            break;
9335                        }
9336                    }
9337                }
9338                if (!isolation_style) {
9339                    isolation_style = document.createElement('style');
9340                    isolation_style.setAttribute('data-cs-isolation', '1');
9341                    shadow.insertBefore(isolation_style, shadow.firstChild);
9342                }
9343                isolation_style.textContent = CS_CUSTOM_CODE_ISOLATION_CSS;
9344                if (this.preview_element) {
9345                    isolation_style.textContent += '\n' + CS_CUSTOM_CODE_PREVIEW_CHROME_CSS;
9346                }
9347                let content_root = shadow.querySelector('.cs-cc-root');
9348                if (!content_root) {
9349                    content_root = document.createElement('div');
9350                    content_root.className = 'cs-cc-root';
9351                    shadow.appendChild(content_root);
9352                }
9353                // Compare against the source we last wrote, not live
9354                // innerHTML. Decoration injects Change/Add chips into
9355                // the tree; a live compare would see that as a mismatch
9356                // and wipe the chips on the next render_root call
9357                // (text-edit decorate runs immediately after).
9358                let next_html = html || '';
9359                if (content_root.__cs_source_html !== next_html) {
9360                    content_root.innerHTML = next_html;
9361                    content_root.__cs_source_html = next_html;
9362                }
9363                return content_root;
9364            },
9365            sanitize_custom_code_html(html) {
9366                if (!html || typeof html !== 'string') {
9367                    return '';
9368                }
9369                let template = document.createElement('template');
9370                template.innerHTML = html;
9371                template.content.querySelectorAll('script').forEach((node) => {
9372                    node.remove();
9373                });
9374                // Drop external FA kits — they override Clubspot's Pro icons document-wide.
9375                template.content.querySelectorAll('link[rel="stylesheet"], link[rel="preload"]').forEach((node) => {
9376                    let href = String(node.getAttribute('href') || '').toLowerCase();
9377                    if (
9378                        href.indexOf('font-awesome') >= 0
9379                        || href.indexOf('fontawesome') >= 0
9380                        || href.indexOf('kit.fontawesome') >= 0
9381                    ) {
9382                        node.remove();
9383                    }
9384                });
9385                template.content.querySelectorAll('*').forEach((node) => {
9386                    Array.prototype.slice.call(node.attributes || []).forEac
9386h((attr) => {
9387                        let name = String(attr.name || '').toLowerCase();
9388                        let value = String(attr.value || '').trim().toLowerCase();
9389                        if (name.indexOf('on') === 0) {
9390                            node.removeAttribute(attr.name);
9391                        }
9392                        if ((name === 'href' || name === 'src' || name === 'xlink:href') && value.indexOf('javascript:') === 0) {
9393                            node.removeAttribute(attr.name);
9394                        }
9395                    });
9396                });
9397                return template.innerHTML;
9398            },
9399            initialize_custom_code_interactions() {
9400                let root = this.custom_code_render_root();
9401                if (!root) {
9402                    return;
9403                }
9404                this.apply_first_section_top_spacer(root);
9405                // Show/hide a tab panel. The `hidden` attribute alone is
9406                // not reliable: panels usually carry an inline `display`
9407                // (e.g. `display:flex`), and an inline declaration beats
9408                // the UA `[hidden]{display:none}` rule, so inactive panels
9409                // would stay visible and every tab would render at once.
9410                // Drive `display` directly (remembering each panel's
9411                // original inline value) and keep `hidden` for semantics.
9412                let set_panel_active = (panel, active) => {
9413                    if (panel.__csPanelDisplay === undefined) {
9414                        panel.__csPanelDisplay = panel.style.display || '';
9415                    }
9416                    panel.hidden = !active;
9417                    panel.classList.toggle('is-active', active);
9418                    panel.style.display = active ? panel.__csPanelDisplay : 'none';
9419                };
9420                let find_tab_panels = (group) => {
9421                    let panels = Array.prototype.slice.call(group.querySelectorAll('[data-cs-panel]'));
9422                    if (panels.length) {
9423                        return panels;
9424                    }
9425                    let panel_scope = group.closest('section') || root;
9426                    return Array.prototype.slice.call(panel_scope.querySelectorAll('[data-cs-panel]'));
9427                };
9428                let sync_tabs = () => {
9429                    root.querySelectorAll('[data-cs-tabs]').forEach((group) => {
9430                        let tabs = Array.prototype.slice.call(group.querySelectorAll('[data-cs-tab]'));
9431                        let panels = find_tab_panels(group);
9432                        if (!tabs.length || !panels.length) {
9433                            return;
9434                        }
9435                        let active_tab = tabs.find((tab) => tab.classList.contains('is-active')) || tabs[0];
9436                        let active_name = active_tab.getAttribute('data-cs-tab');
9437                        tabs.forEach((tab) => {
9438                            let active = tab === active_tab;
9439                            tab.classList.toggle('is-active', active);
9440                            tab.setAttribute('aria-selected', active ? 'true' : 'false');
9441                        });
9442                        panels.forEach((panel) => {
9443                            set_panel_active(panel, panel.getAttribute('data-cs-panel') === active_name);
9444                        });
9445                    });
9446                };
9447                // The switcher group for a `<select data-cs-select>`: its wrapping
9448                // [data-cs-tabs] if present, else its section, else root — the same
9449                // scope rule tabs use, so a select can drive the same panels.
9450                let find_switcher_group = (el) => el.closest('[data-cs-tabs]') || el.closest('section') || root;
9451                // `<select data-cs-select>` whose `<option value="X">` reveal the
9452                // matching `[data-cs-panel="X"]`. The native control renders/opens
9453                // with no JS; only the panel switch needs the runtime. Ideal for a
9454                // many-option filter (year/category) where tabs would be unwieldy.
9455                let sync_selects = () => {
9456                    root.querySelectorAll('[data-cs-select]').forEach((select) => {
9457                        let panels = find_tab_panels(find_switcher_group(select));
9458                        if (!panels.length) {
9459                            return;
9460                        }
9461                        let active_name = (select.value !== null && select.value !== '')
9462                            ? select.value
9463                            : panels[0].getAttribute('data-cs-panel');
9464                        panels.forEach((panel) => {
9465                            set_panel_active(panel, panel.getAttribute('data-cs-panel') === active_name);
9466                        });
9467                    });
9468                };
9469                let sync_all = () => {
9470                    sync_tabs();
9471                    sync_selects();
9472                };
9473                if (root.__csCustomCodeInteractionsInitialized) {
9474                    sync_all();
9475                    if (this.preview_element) {
9476                        this.decorate_asset_placeholders();
9477                        this.decorate_editable_text_nodes();
9478                    }
9479                    return;
9480                }
9481                root.__csCustomCodeInteractionsInitialized = true;
9482                if (this.preview_element) {
9483                    this.decorate_asset_placeholders();
9484                    this.decorate_editable_text_nodes();
9485                }
9486                root.addEventListener('click', (event) => {
9487                    let trigger = event.target && event.target.closest('[data-cs-toggle], [data-cs-tab]');
9488                    if (!trigger || !root.contains(trigger)) {
9489                        return;
9490                    }
9491
9492                    let toggle_selector = trigger.getAttribute('data-cs-toggle');
9493                    if (toggle_selector) {
9494                        event.preventDefault();
9495                        let target = root.querySelector(toggle_selector);
9496                        if (target) {
9497                            let class_name = trigger.getAttribute('data-cs-toggle-class') || 'is-active';
9498                            target.classList.toggle(class_name);
9499                            let expanded = target.classList.contains(class_name);
9500                            trigger.setAttribute('aria-expanded', expanded ? 'true' : 'false');
9501                        }
9502                    }
9503
9504                    let tab_name = trigger.getAttribute('data-cs-tab');
9505                    if (tab_name) {
9506                        event.preventDefault();
9507                        let group = trigger.closest('[data-cs-tabs]') || root;
9508                        group.querySelectorAll('[data-cs-tab]').forEach((tab) => {
9509                            tab.classList.toggle('is-active', tab === trigger);
9510                            tab.setAttribute('aria-selected', tab === trigger ? 'true' : 'false');
9511                        });
9512                        find_tab_panels(group).forEach((panel) => {
9513                            set_panel_active(panel, panel.getAttribute('data-cs-panel') === tab_name);
9514                        });
9515                    }
9516                });
9517                root.addEventListener('change', (event) => {
9518                    let select = event.target && event.target.closest('[data-cs-select]');
9519                    if (!select || !root.contains(select)) {
9520                        return;
9521                    }
9522                    let active_name = select.value;
9523                    find_tab_panels(find_switcher_group(select)).forEach((panel) => {
9524                        set_panel_active(panel, panel.getAttribute('data-cs-panel') === active_name);
9525                    });
9526                });
9527                sync_all();
9528            },
9529            // ─── Asset placeholders ───────────────────────────────────
9530            //
9531            // The AI route plants inert `<div data-cs-asset-placeholder>`
9532            // (or `<a>`) elements in the section HTML when it needs an
9533            // asset it couldn't verify. The live (public) site renders
9534            // those placeholders as styled fallback wrappers as-is —
9535            // they're authored to look intentional. In PREVIEW mode
9536            // (admin builder) we overlay a click-to-fill control on top
9537            // of each placeholder so the admin can choose a club photo,
9538            // upload a new one, or paste a URL. On selection we mutate
9539            // the persisted HTML so the next page render shows the
9540            // attached asset.
9541            //
9542            // Two element states:
9543            //   - `data-cs-asset-placeholder="<id>"`  unfilled slot;
9544            //     gets the big full-bleed "Add image" overlay.
9545            //   - `data-cs-asset-attached="<id>"`     filled slot;
9546            //     gets the small top-right "Change" chip.
9547            //
9548            // We keep the wrapper element across the fill transition —
9549            // the wrapper preserves the section's layout box (size,
9550            // border-radius, positioning) and we just swap its inner
9551            // content for an `<img>`/`<a>`/`background-image`. That way
9552            // the section never reflows when an admin fills a slot, and
9553            // styled fallback "frames" the resolved asset.
9554            //
9555            // Sequencing: decorate runs after every preview re-render
9556            // (initialize_custom_code_interactions). The overlay DOM
9557            // lives ONLY in the live preview tree — we never persist
9558            // the overlay markup back to `custom_code`.
9559
9560            decorate_asset_placeholders() {
9561                if (!this.preview_element) {
9562                    return;
9563                }
9564                let root = this.custom_code_render_root();
9565                if (!root) {
9566                    return;
9567                }
9568                // Unfilled placeholders → big "Add image" overlay.
9569                let placeholders = root.querySelectorAll('[data-cs-asset-placeholder]');
9570                placeholders.forEach((placeholder) => {
9571                    if (placeholder.__cs_asset_decorated) {
9572                        return;
9573                    }
9574                    placeholder.__cs_asset_decorated = true;
9575                    let computed_position = window.getComputedStyle(placeholder).position;
9576                    if (computed_position === 'static') {
9577                        placeholder.style.position = 'relative';
9578                    }
9579                    placeholder.classList.add('cs-asset-placeholder-host');
9580                    placeholder.style.cursor = 'pointer';
9581
9582                    let id = placeholder.getAttribute('data-cs-asset-placeholder') || '';
9583                    let target = (placeholder.getAttribute('data-cs-asset-target') || '').toLowerCase();
9584                    if (target === 'background-image') {
9585                        placeholder.classList.add('cs-asset-background-placeholder-host');
9586                    }
9587                    let label_attr = (placeholder.getAttribute('data-cs-asset-label') || '').trim();
9588                    let label = label_attr || 'asset';
9589
9590                    if (target === 'background-image') {
9591                        let background_chip = document.createElement('button');
9592                        background_chip.type = 'button';
9593                        background_chip.className = 'cs-asset-background-placeholder-chip exclude-from-custom-button-styles';
9594                        background_chip.setAttribute('data-cs-asset-placeholder-id', id);
9595                        background_chip.setAttribute('aria-label', 'Add ' + label);
9596                        background_chip.setAttribute('title', 'Add ' + label);
9597                        background_chip.innerHTML = '<i class="fa-regular fa-image"></i>';
9598                        background_chip.addEventListener('mousedown', (event) => {
9599                            event.stopPropagation();
9600                        });
9601                        background_chip.addEventListener('click', (event) => {
9602                            event.preventDefault();
9603                            event.stopPropagation();
9604                            this.open_placeholder_menu(id, { mode: 'placeholder' });
9605                        });
9606                        let background_chip_host = placeholder;
9607                        if (placeholder.parentElement
9608                                && placeholder.parentElement !== root
9609                                && root.contains(placeholder.parentElement)) {
9610                            background_chip_host = placeholder.parentElement;
9611                            if (window.getComputedStyle(background_chip_host).position === 'static') {
9612                                background_chip_host.style.position = 'relative';
9613                            }
9614                        }
9615                        background_chip_host.appendChild(background_chip);
9616                    } else {
9617                        let overlay = document.createElement('button');
9618                        overlay.type = 'button';
9619                        overlay.className = 'cs-asset-placeholder-overlay exclude-from-custom-button-styles';
9620                        overlay.setAttribute('data-cs-asset-placeholder-id', id);
9621                        overlay.innerHTML =
9622                            '<span class="cs-asset-placeholder-icon"><i class="fa-regular fa-image"></i></span>' +
9623                            '<span class="cs-asset-placeholder-label">Add ' + this.escape_html_text(label) + '</span>';
9624                        overlay.addEventListener('mousedown', (event) => {
9625                            event.stopPropagation();
9626                        });
9627                        overlay.addEventListener('click', (event) => {
9628                            event.preventDefault();
9629                            event.stopPropagation();
9630                            this.open_placeholder_menu(id, { mode: 'placeholder' });
9631                        });
9632                        placeholder.appendChild(overlay);
9633                    }
9634
9635                    // Host-level click — make the entire placeholder area
9636                    // clickable so admins can tap anywhere inside the
9637                    // styled fallback wrapper to open the fill menu, not
9638                    // just the centered overlay button. The overlay's own
9639                    // listener stops propagation, so we only fire when
9640                    // the click originated outside it (e.g. on padding,
9641                    // background, or fallback child text/icons).
9642                    placeholder.addEventListener('click', (event) => {
9643                        if (event.target.closest('.cs-asset-placeholder-overlay')) {
9644                            return;
9645                        }
9646                        if (event.target.closest('.cs-asset-background-placeholder-chip')) {
9647                            return;
9648                        }
9649                        if (target === 'background-image' && event.target !== placeholder) {
9650                            return;
9651                        }
9652                        event.preventDefault();
9653                        event.stopPropagation();
9654                        this.open_placeholder_menu(id, { mode: 'placeholder' });
9655                    });
9656                });
9657
9658                // Filled placeholders → small "Change" chip.
9659                let attached = root.querySelectorAll('[data-cs-asset-attached]');
9660                attached.forEach((attached_el) => {
9661                    if (attached_el.__cs_asset_attached_decorated) {
9662                        return;
9663                    }
9664                    attached_el.__cs_asset_attached_decorated = true;
9665                    let computed_position = window.getComputedStyle(attached_el).position;
9666                    if (computed_position === 'static') {
9667                        attached_el.style.position = 'relative';
9668                    }
9669                    attached_el.classList.add('cs-asset-attached-host');
9670                    attached_el.style.cursor = 'pointer';
9671
9672                    let id = attached_el.getAttribute('data-cs-asset-attached') || '';
9673                    let change_button = document.createElement('button');
9674                    change_button.type = 'button';
9675                    change_button.className = 'cs-asset-change-button exclude-from-custom-button-styles';
9676                    change_button.setAttribute('data-cs-asset-attached-id', id);
9677                    change_button.innerHTML =
9678                        '<i class="fa-regular fa-image" style="margin-right:5px;"></i>' +
9679                        '<span>Change</span>';
9680                    change_button.addEventListener('mousedown', (event) => {
9681                        event.stopPropagation();
9682                    });
9683                    change_button.addEventListener('click', (event) => {
9684                        event.preventDefault();
9685                        event.stopPropagation();
9686                        this.open_placeholder_menu(id, { mode: 'attached' });
9687                    });
9688                    attached_el.appendChild(change_button);
9689
9690                    // Host-level click — clicking anywhere on the filled
9691                    // image / link area opens the same change menu the
9692                    // small "Change" hover chip uses. The chip's listener
9693                    // stops propagation, so this only fires for clicks
9694                    // outside it (e.g. directly on the image surface).
9695                    attached_el.addEventListener('click', (event) => {
9696                        if (event.target.closest('.cs-asset-change-button')) {
9697                            return;
9698                        }
9699                        event.preventDefault();
9700                        event.stopPropagation();
9701                        this.open_placeholder_menu(id, { mode: 'attached' });
9702                    });
9703                });
9704
9705                // Bare `<img>` tags that weren't emitted as a
9706                // `data-cs-asset-*` slot — for example, every image the
9707                // model dropped in during a `capture_web_reference` build
9708                // (`<img src="https://oakmont.../">` directly). Without
9709                // this pass those imgs had no change/import affordance,
9710                // so the only way to swap one out was to hand-edit the
9711                // section HTML.
9712                //
9713                // Implementation:
9714                //   - Wrap each bare img in a transient `<span
9715                //     class="cs-bare-image-host">` host purely in the
9716                //     LIVE preview DOM. The persisted custom_code never
9717                //     sees this wrapper — it's recreated every time
9718                //     `decorate_asset_placeholders` runs.
9719                //   - Append a "Change" chip into the host. The chip
9720                //     reuses `.cs-asset-change-button` so it matches
9721                //     filled placeholder slots.
9722                //   - On click, route to a bare-img menu that uses
9723                //     `src`-string replacement against the persisted
9724                //     html instead of the `data-cs-asset-attached`
9725                //     marker lookup the placeholder pipeline depends
9726                //     on. That lets us avoid touching the persisted
9727                //     HTML until the admin actually picks a different
9728                //     image — there's no surprise `unsaved_changes`
9729                //     bump just from opening a reference-built section.
9730                let bare_imgs = Array.from(root.querySelectorAll('img')).filter((img) => {
9731                    if (img.__cs_bare_image_decorated) {
9732                        return false;
9733                    }
9734                    if (img.closest('[data-cs-asset-placeholder], [data-cs-asset-attached]')) {
9735                        return false;
9736                    }
9737                    if (img.parentElement && img.parentElement.classList && img.parentElement.classList.contains('cs-bare-image-host')) {
9738                        return false;
9739                    }
9740                    let src = img.getAttribute('src') || '';
9741                    return src.trim().length > 0;
9742                });
9743                bare_imgs.forEach((img) => {
9744                    img.__cs_bare_image_decorated = true;
9745                    if (!img.parentNode) {
9746                        return;
9747                    }
9748                    let host = document.createElement('span');
9749                    host.className = 'cs-bare-image-host cs-asset-attached-host';
9750                    host.setAttribute('aria-hidden', 'true');
9751                    // line-height: 0 kills the descender gap an inline
9752                    // img would otherwise produce; max-width lets the
9753                    // img stay responsive inside flex/grid layouts.
9754                    host.style.cssText = 'position: relative; display: inline-block; line-height: 0; max-width: 100%;';
9755                    img.parentNode.insertBefore(host, img);
9756                    host.appendChild(img);
9757
9758                    let change_button = document.createElement('button');
9759                    change_button.type = 'button';
9760                    change_button.className = 'cs-asset-change-button cs-bare-image-change-button exclude-from-custom-button-styles';
9761                    change_button.innerHTML =
9762                        '<i class="fa-regular fa-image" style="margin-right:5px;"></i>' +
9763                        '<span>Change</span>';
9764                    change_button.addEventListener('mousedown', (event) => {
9765                        event.stopPropagation();
9766                    });
9767                    change_button.addEventListener('click', (event) => {
9768                        event.preventDefault();
9769                        event.stopPropagation();
9770                        this.open_bare_image_menu(img);
9771                    });
9772                    host.appendChild(change_button);
9773
9774                    // Host click → same menu, for clicks on the image
9775                    // surface itself (chip stops propagation so this
9776                    // only fires when the user clicks outside it).
9777                    host.addEventListener('click', (event) => {
9778                        if (event.target.closest('.cs-asset-change-button')) {
9779                            return;
9780                        }
9781                        event.preventDefault();
9782                        event.stopPropagation();
9783                        this.open_bare_image_menu(img);
9784                    });
9785                });
9786            },
9787            escape_html_text(text) {
9788                if (typeof text !== 'string') {
9789                    return '';
9790                }
9791                let div = document.createElement('div');
9792                div.appendChild(document.createTextNode(text));
9793                return div.innerHTML;
9794            },
9795            open_placeholder_menu(id, options_in) {
9796                let opts = options_in || {};
9797                let mode = opts.mode || 'placeholder';
9798                if (typeof handle_common !== 'function') {
9799                    // Fallback: jump straight to the image picker.
9800                    this.pick_existing_for_placeholder(id);
9801                    return;
9802                }
9803                let target_el = this.find_asset_element_by_id(id);
9804                if (!target_el) {
9805                    return;
9806                }
9807                let accepts = (target_el.getAttribute('data-cs-asset-accepts') || 'image').toLowerCase();
9808                let label_attr = (target_el.getAttribute('data-cs-asset-label') || '').trim();
9809                let label = label_attr || 'asset';
9810                // Sniff the currently-attached URL so we can offer
9811                // "Copy image to club photos" only when it actually
9812                // points somewhere outside the Clubspot CDN. The same
9813                // helper handles <img src>, <a href>, and inline
9814                // background-image: url(...) wrappers.
9815                let attached_url = mode === 'attached'
9816                    ? this.return_attached_url_for_wrapper(target_el)
9817                    : null;
9818                let attached_is_external = accepts === 'image' && this.is_external_image_url(attached_url);
9819                let options = [];
9820                // Photos picker — same webbuilder asset picker, filtered to photos.
9821                if (accepts === 'image') {
9822                    options.push({
9823                        text:    mode === 'attached' ? 'Pick a different photo' : 'Upload or choose from club photos',
9824                        subtext: 'Upload a new image or pick an existing one from this club',
9825                        fn:      () => this.pick_existing_for_placeholder(id),
9826                    });
9827                }
9828                // Documents/files picker — `webbuilder_asset_picker`
9829                // overlay, backed by `browse_webbuilder_assets`. Covers
9830                // PDFs and any public-access club document. Also offered
9831                // for image accepts because admins sometimes store hero
9832                // images in the documents folder.
9833                options.push({
9834                    text:    mode === 'attached'
9835                        ? 'Pick a different file from club documents'
9836                        : (accepts === 'pdf' ? 'Choose from club documents' : 'Choose from club documents'),
9837                    subtext: accepts === 'pdf'
9838                        ? 'Public-access PDFs in the club documents folder'
9839                        : 'Public-access files in the club documents folder',
9840                    fn:      () => this.pick_document_for_placeholder(id),
9841                });
9842                options.push({
9843                    text:    mode === 'attached' ? 'Paste a different URL' : 'Paste a URL',
9844                    subtext: accepts === 'pdf'
9845                        ? 'Paste a public PDF link (e.g. a public document on this club)'
9846                        : 'Paste a public image URL (Clubspot CDN or external host)',
9847                    fn:      () => this.paste_url_for_placeholder(id),
9848                });
9849                if (mode === 'attached' && attached_is_external) {
9850                    // Only surface this when the slot is currently
9851                    // pointing at a non-Clubspot host - importing a
9852                    // CDN URL into itself is a no-op and the server
9853                    // rejects it explicitly. Sits ABOVE "Remove" so
9854                    // it lines up with the constructive actions
9855                    // (pick, paste, copy) rather than the destructive
9856                    // one.
9857                    options.push({
9858                        text:    'Copy image to club photos',
9859                        subtext: 'Save a local copy in your photo library and use that instead of the external URL.',
9860                        fn:      () => this.run_after_placeholder_menu_close(() => this.import_external_image_for_attached(id)),
9861                    });
9862                }
9863                if (mode === 'attached') {
9864                    options.push({
9865                        text:    'Remove this image',
9866                        subtext: 'Revert this slot back to its empty placeholder.',
9867                        fn:      () => this.run_after_placeholder_menu_close(() => this.revert_attached_to_placeholder(id)),
9868                    });
9869                } else {
9870                    options.push({
9871                        text:    'Keep empty for now',
9872                        subtext: 'Hide this prompt — the slot stays available with a "Change" button if you want to fill it later.',
9873                        fn:      () => this.run_after_placeholder_menu_close(() => this.remove_placeholder(id)),
9874                    });
9875                }
9876                let params = {
9877                    title:             mode === 'attached' ? 'Change "' + label + '"' : 'Fill "' + label + '"',
9878                    subtitle:          mode === 'attached'
9879                        ? 'Pick a different photo, paste another URL, or remove the current asset.'
9880                        : 'Pick how you want to attach this asset.',
9881                    hide_on_selection: false,
9882                    options:           options,
9883                };
9884                handle_common(params);
9885            },
9886            run_after_placeholder_menu_close(fn) {
9887                store.overlays.handle_common = false;
9888                let run_next = () => {
9889                    setTimeout(fn, 0);
9890                };
9891                if (typeof this.$nextTick === 'function') {
9892                    this.$nextTick(run_next);
9893                    return;
9894                }
9895                run_next();
9896            },
9897            pick_existing_for_placeholder(id) {
9898                if (typeof webbuilder_asset_picker !== 'function') {
9899                    if (typeof displayMessage === 'function') {
9900                        displayMessage('Club photo picker is not loaded. Refresh the page and try again.', true, 5000);
9901                    }
9902                    return;
9903                }
9904                this.run_after_placeholder_menu_close(() => {
9905                    let params = {
9906                        title:             'Select club photo',
9907                        accepts:           'image',
9908                        source:            'photos',
9909                        hide_on_selection: true,
9910                        fn: (result) => {
9911                            let url = result && result.url ? result.url : null;
9912                            if (!url) {
9913                                return;
9914                            }
9915                            let attach_params = {
9916                                id:         id,
9917                                url:        url,
9918                                verified:   true,
9919                                source:     'club_photos',
9920                                visibility: 'public_web',
9921                            };
9922                            this.attach_url_to_placeholder(attach_params);
9923                        },
9924                    };
9925                    webbuilder_asset_picker(params);
9926                });
9927            },
9928            // Opens the documents picker overlay. Same deferred-tick
9929            // dance as `pick_existing_for_placeholder` so the chain of
9930            // handle_common → documents picker doesn't race.
9931            pick_document_for_placeholder(id) {
9932                if (typeof webbuilder_asset_picker !== 'function') {
9933                    if (typeof displayMessage === 'function') {
9934                        displayMessage('Documents picker is not loaded. Refresh the page and try again.', true, 5000);
9935                    }
9936                    return;
9937                }
9938                let placeholder = this.find_asset_element_by_id(id);
9939                if (!placeholder) {
9940                    return;
9941                }
9942                let accepts = (placeholder.getAttribute('data-cs-asset-accepts') || 'image').toLowerCase();
9943                this.run_after_placeholder_menu_close(() => {
9944                    let params = {
9945                        title:             accepts === 'pdf' ? 'Select a document' : 'Select a file',
9946                        accepts:           accepts,
9947                        source:            'documents',
9948                        hide_on_selection: true,
9949                        fn: (result) => {
9950                            let url = result && result.url ? result.url : null;
9951                            if (!url) {
9952                                return;
9953                            }
9954                            let attach_params = {
9955                                id:         id,
9956                                url:        url,
9957                                verified:   true,
9958                                source:     result.source || 'public_document',
9959                                visibility: result.visibility || 'public_link',
9960                            };
9961                            this.attach_url_to_placeholder(attach_params);
9962                        },
9963                    };
9964                    webbuilder_asset_picker(params);
9965                });
9966            },
9967            paste_url_for_placeholder(id) {
9968                let placeholder = this.find_placeholder_element(id);
9969                if (!placeholder) {
9970                    return;
9971                }
9972                let prompt_label = placeholder.getAttribute('data-cs-asset-label') || 'asset';
9973                let do_attach = (attach) => {
9974                    this.attach_url_to_placeholder({
9975                        id:         id,
9976                        url:        attach.url,
9977                        verified:   attach.verified,
9978                        source:     attach.source,
9979                        visibility: attach.visibility,
9980                    });
9981                };
9982                // Defer one tick so handle_common can finish dismissing
9983                // before we open the URL-entry modal — otherwise the
9984                // menu overlay stays painted on top.
9985                this.run_after_placeholder_menu_close(() => {
9986                    this.prompt_for_pasted_url({
9987                        slot_label: prompt_label,
9988                        alt:        prompt_label,
9989                        do_attach:  do_attach,
9990                    });
9991                });
9992            },
9993            // ─── Pasted-URL pipeline (shared) ──────────────────────────
9994            //
9995            // Both the placeholder-marker flow and the bare-<img> flow
9996            // funnel pasted URLs through these three helpers:
9997            //
9998            //   prompt_for_pasted_url
9999            //     → opens a Clubspot universal_edit_overlay modal for
10000            //       URL entry (replaces window.prompt, which blocks
10001            //       the event loop, ignores app styling, and on iOS
10002            //       leaves overlays painted on top of the OS dialog).
10003            //   process_pasted_url
10004            //     → verifies the URL server-side and dispatches to
10005            //       do_attach() for trusted hosts, or to
10006            //       confirm_external_url_action() for external ones.
10007            //   confirm_external_url_action
10008            //     → opens an are_you_sure modal that rolls what used
10009            //       to be two separate browser confirms (the
10010            //       "external URL — attach anyway?" confirm and the
10011            //       follow-up "save to library?" prompt) into a single
10012            //       decision: "Save to club photos" (primary) or
10013            //       "Attach as external URL" (secondary).
10014            //
10015            // `do_attach(attach)` is the only caller-specific bit —
10016            // placeholder flow calls attach_url_to_placeholder, bare-
10017            // img flow calls swap_bare_image_src. Both receive the
10018            // same { url, verified, source, visibility } payload.
10019            prompt_for_pasted_url({ slot_label, current_url, alt, do_attach }) {
10020                let label = (typeof slot_label === 'string' && slot_label.trim()) ? slot_label.trim() : 'asset';
10021                if (typeof universal_edit_overlay !== 'function') {
10022                    // Fallback to the browser prompt if the overlay
10023                    // isn't loaded for some reason — better than
10024                    // silently dropping the action.
10025                    let pasted = window.prompt('Paste a URL for "' + label + '":', current_url || '');
10026                    if (!pasted) {
10027                        return;
10028                    }
10029                    this.process_pasted_url({ pasted: pasted, alt: alt, do_attach: do_attach });
10030                    return;
10031                }
10032                universal_edit_overlay({
10033                    title:             'Paste a URL',
10034                    subtitle:          { label: 'Paste a public URL for "' + label + '". Clubspot 
10034CDN links attach directly; external URLs will prompt you to save a local copy.' },
10035                    button_label:      'Attach URL',
10036                    hide_on_selection: true,
10037                    fields: [{
10038                        key:      'url',
10039                        label:    'URL',
10040                        value:    current_url || '',
10041                        type:     'text',
10042                        required: true,
10043                    }],
10044                    fn: (params) => {
10045                        let field = (params && params.fields ? params.fields : []).find((f) => f.key === 'url');
10046                        let value = field && typeof field.value === 'string' ? field.value.trim() : '';
10047                        if (!value) {
10048                            return;
10049                        }
10050                        this.process_pasted_url({ pasted: value, alt: alt, do_attach: do_attach });
10051                    },
10052                });
10053            },
10054            process_pasted_url({ pasted, alt, do_attach }) {
10055                let trimmed = (typeof pasted === 'string') ? pasted.trim() : '';
10056                if (!trimmed) {
10057                    return;
10058                }
10059                let club_id = this.return_active_club_id();
10060                if (!club_id || typeof Parse === 'undefined') {
10061                    // Without a club_id we can't run the server-side
10062                    // verifier — fall back to attaching the URL with a
10063                    // warning so the admin isn't stuck.
10064                    if (typeof displayMessage === 'function') {
10065                        displayMessage('Could not verify the URL; attaching as-is. Review before publishing.', true, 5000);
10066                    }
10067                    do_attach({ url: trimmed, verified: false, source: 'pasted_url', visibility: 'unknown' });
10068                    return;
10069                }
10070                Parse.Cloud.run('verify_webbuilder_asset_url', {
10071                    club_id: club_id,
10072                    url:     trimmed,
10073                }, this.parse_session_options()).then((verdict) => {
10074                    if (verdict && verdict.allowed) {
10075                        do_attach({
10076                            url:        verdict.embed_url || trimmed,
10077                            verified:   true,
10078                            source:     verdict.source || 'verified_url',
10079                            visibility: verdict.visibility || 'public_web',
10080                        });
10081                        return;
10082                    }
10083                    let reason = verdict && verdict.reason ? verdict.reason : 'unverified';
10084                    // External URLs come back as `external_url_not_allowed`
10085                    // — surface as a Clubspot modal so the admin can
10086                    // choose between importing to the library or
10087                    // attaching as-is. The custom-code sanitizer still
10088                    // strips dangerous schemes at render time.
10089                    if (reason === 'external_url_not_allowed') {
10090                        this.confirm_external_url_action({
10091                            url: trimmed,
10092                            alt: alt,
10093                            on_import: () => {
10094                                return this.import_external_image_url(trimmed, alt).then((cdn_url) => {
10095                                    do_attach({
10096                                        url:        cdn_url,
10097                                        verified:   true,
10098                                        source:     'imported_external_url',
10099                                        visibility: 'public_web',
10100                                    });
10101                                });
10102                            },
10103                            on_attach_external: () => {
10104                                do_attach({ url: trimmed, verified: false, source: 'external', visibility: 'unknown' });
10105                            },
10106                        });
10107                        return;
10108                    }
10109                    if (typeof displayMessage === 'function') {
10110                        displayMessage('URL rejected: ' + reason.replace(/_/g, ' '), true, 5000);
10111                    }
10112                }).catch((error) => {
10113                    // The verifier cloud function ships in the same
10114                    // deploy as this UI, but local devs sometimes don't
10115                    // restart their primary server between code pulls.
10116                    // Treat "Invalid function" as a soft fallback: attach
10117                    // the URL with a warning so the admin isn't stuck,
10118                    // and surface the underlying error in the console.
10119                    let msg = error && error.message ? error.message : '';
10120                    if (msg.indexOf('Invalid function') !== -1) {
10121                        console.warn('[custom-code] verify_webbuilder_asset_url not deployed — attaching URL without verification.');
10122                        if (typeof displayMessage === 'function') {
10123                            displayMessage('Attached without verification (server hasn\'t reloaded the new function yet).', true, 5000);
10124                        }
10125                        do_attach({ url: trimmed, verified: false, source: 'pasted_url', visibility: 'unknown' });
10126                        return;
10127                    }
10128                    if (typeof displayMessage === 'function') {
10129                        displayMessage(msg || 'Could not verify URL.', true, 5000);
10130                    }
10131                });
10132            },
10133            confirm_external_url_action({ url, alt, on_import, on_attach_external }) {
10134                if (typeof are_you_sure !== 'function') {
10135                    if (window.confirm('This URL is hosted outside Clubspot. Save a copy to your club photo library?\n\nClick Cancel to attach the external URL as-is.\n\n' + url)) {
10136                        Promise.resolve().then(() => on_import()).catch(() => {});
10137                    } else {
10138                        on_attach_external();
10139                    }
10140                    return;
10141                }
10142                let host = '';
10143                try {
10144                    host = new URL(url).hostname || '';
10145                } catch (err) {
10146                    host = '';
10147                }
10148                let truncated = url.length > 90 ? (url.slice(0, 87) + '...') : url;
10149                let escaped = (typeof url === 'string') ? url.replace(/"/g, '&quot;') : '';
10150                are_you_sure({
10151                    title:           'External image URL',
10152                    subtext:         'This image lives on ' + (host || 'another site') + '. Save a copy to your club photo library so it stays available even if the source site changes.',
10153                    callout:         { html: '<a href="' + escaped + '" target="_blank" rel="noopener" style="word-break:break-all;">' + truncated + '</a>' },
10154                    button_label:    'Save to club photos',
10155                    warning_button:  false,
10156                    no_thanks_label: 'Attach as external URL',
10157                    fn: () => {
10158                        store.components.are_you_sure.loading = true;
10159                        Promise.resolve().then(() => on_import()).then(() => {
10160                            store.components.are_you_sure.loading = false;
10161                            store.overlays.are_you_sure = false;
10162                            if (typeof displayMessage === 'function') {
10163                                displayMessage('Image saved to your photo library.', false, 4000);
10164                            }
10165                        }).catch((error) => {
10166                            store.components.are_you_sure.loading = false;
10167                            if (typeof checkParseError === 'function') {
10168                                checkParseError(error);
10169                            }
10170                            let msg = error && error.message ? error.message : 'Image import failed.';
10171                            if (typeof displayMessage === 'function') {
10172                                displayMessage(msg, true, 6000);
10173                            }
10174                        });
10175                    },
10176                    secondary_fn: () => {
10177                        store.overlays.are_you_sure = false;
10178                        on_attach_external();
10179                    },
10180                });
10181            },
10182            // Run the `import_external_image_to_club_photos` Cloud
10183            // function and resolve with the canonical CDN URL.
10184            // Centralizes the embed_url fallback + "no URL returned"
10185            // diagnostics that used to be duplicated across both
10186            // `import_external_image_for_attached` and
10187            // `import_external_image_for_bare`.
10188            import_external_image_url(url, alt) {
10189                let club_id = this.return_active_club_id();
10190                if (!club_id || typeof Parse === 'undefined') {
10191                    return Promise.reject(new Error('Could not determine the active club. Refresh and try again.'));
10192                }
10193                return Parse.Cloud.run('import_external_image_to_club_photos', {
10194                    club_id: club_id,
10195                    url:     url,
10196                    alt:     alt || '',
10197                }, this.parse_session_options()).then((result) => {
10198                    let returned_cdn = result && result.cdn_url ? result.cdn_url : null;
10199                    let embed_fallback = result && result.result && result.result.embed_url ? result.result.embed_url : null;
10200                    let cdn_url = returned_cdn || embed_fallback;
10201                    if (!cdn_url) {
10202                        let detail = result ? (' (server returned: ' + JSON.stringify(result).slice(0, 160) + ')') : '';
10203                        throw new Error('Image import finished but no URL was returned.' + detail);
10204                    }
10205                    return cdn_url;
10206                });
10207            },
10208            // ─── Section-level "save external images" batch ───────────
10209            //
10210            // Pure CLIENT operation, mirroring upload_webbuilder_asset_photo:
10211            // discover every external image URL in the section, loop the
10212            // existing import_external_image_to_club_photos cloud function
10213            // per URL (with the admin's real session token / permissions),
10214            // then rewrite the persisted HTML to point at the new CDN
10215            // URLs. No AI service involvement.
10216            //
10217            // Parses the persisted source (not the live preview DOM) so
10218            // it sees the canonical URLs and isn't confused by transient
10219            // decoration wrappers. Returns a deduped array of external
10220            // http(s) image URLs found in <img src> and inline
10221            // `background-image: url(...)`.
10222            collect_section_external_image_urls(source_html) {
10223                return webbuilder_collect_external_image_urls(source_html);
10224            },
10225            import_all_external_images_in_section() {
10226                if (this.importing_external_images) {
10227                    return;
10228                }
10229                let urls = this.section_external_image_urls;
10230                if (!urls.length) {
10231                    if (typeof displayMessage === 'function') {
10232                        displayMessage('No external images to save in this section.', false, 4000);
10233                    }
10234                    return;
10235                }
10236                let club_id = this.return_active_club_id();
10237                if (!club_id || typeof Parse === 'undefined') {
10238                    if (typeof displayMessage === 'function') {
10239                        displayMessage('Could not determine the active club. Refresh and try again.', true, 5000);
10240                    }
10241                    return;
10242                }
10243                let count = urls.length;
10244                let run_batch = () => {
10245                    this.importing_external_images = true;
10246                    if (typeof displayMessage === 'function') {
10247                        displayMessage('Saving ' + count + ' image' + (count === 1 ? '' : 's') + ' to your photo library...', false, 4000);
10248                    }
10249                    this.run_external_image_batch(urls).then((summary) => {
10250                        this.importing_external_images = false;
10251                        this.report_external_image_batch_result(summary);
10252                    }).catch((error) => {
10253                        this.importing_external_images = false;
10254                        if (typeof checkParseError === 'function') {
10255                            checkParseError(error);
10256                        }
10257                        let msg = error && error.message ? error.message : 'Image import failed.';
10258                        if (typeof displayMessage === 'function') {
10259                            displayMessage(msg, true, 6000);
10260                        }
10261                    });
10262                };
10263                if (typeof are_you_sure === 'function') {
10264                    are_you_sure({
10265                        title:          'Save external images',
10266                        subtext:        'This section uses ' + count + ' image' + (count === 1 ? '' : 's') + ' hosted on other sites. Save '
10267                            + (count === 1 ? 'a copy' : 'copies') + ' to your club photo library and swap the section to use '
10268                            + (count === 1 ? 'it' : 'them') + '? This keeps the images available even if the source site changes.',
10269                        button_label:   'Save to club photos',
10270                        warning_button: false,
10271                        fn: () => {
10272                            store.overlays.are_you_sure = false;
10273                            run_batch();
10274                        },
10275                    });
10276                    return;
10277                }
10278                run_batch();
10279            },
10280            // Import each external URL sequentially (bounded to one
10281            // in-flight request — these are byte-level server fetches,
10282            // and a club reference page rarely has more than a handful
10283            // of images). Builds a url->cdn_url map, then rewrites the
10284            // persisted HTML once at the end so the editor only takes a
10285            // single save + re-render.
10286            run_external_image_batch(urls) {
10287                let url_map = {};
10288                let errors = [];
10289                let chain = Promise.resolve();
10290                urls.forEach((url) => {
10291                    chain = chain.then(() => {
10292                        return this.import_external_image_url(url, '').then((cdn_url) => {
10293                            url_map[url] = cdn_url;
10294                        }).catch((error) => {
10295                            errors.push({ url: url, message: error && error.message ? error.message : 'import failed' });
10296                        });
10297                    });
10298                });
10299                return chain.then(() => {
10300                    let swapped = this.rewrite_section_external_image_urls(url_map);
10301                    return {
10302                        requested: urls.length,
10303                        imported:  Object.keys(url_map).length,
10304                        errors:    errors,
10305                        swapped:   swapped,
10306                    };
10307                });
10308            },
10309            // Replace every occurrence of each imported external URL
10310            // with its new CDN URL across the persisted HTML — both
10311            // <img src> and inline background-image declarations — and
10312            // persist once. Returns the number of URLs actually swapped.
10313            rewrite_section_external_image_urls(url_map) {
10314                let keys = Object.keys(url_map || {});
10315                if (!keys.length) {
10316                    return 0;
10317                }
10318                let swapped = 0;
10319                let mutated = this.mutate_persisted_html((doc) => {
10320                    let changed = false;
10321                    let imgs = Array.from(doc.querySelectorAll('img[src]'));
10322                    for (let img of imgs) {
10323                        let src = img.getAttribute('src');
10324                        if (src && url_map[src]) {
10325                            img.setAttribute('src', url_map[src]);
10326                            changed = true;
10327                        }
10328                    }
10329                    let styled = Array.from(doc.querySelectorAll('[style*="background-image"]'));
10330                    for (let el of styled) {
10331                        let style = el.getAttribute('style') || '';
10332                        let next_style = style;
10333                        keys.forEach((old_url) => {
10334                            if (next_style.indexOf(old_url) !== -1) {
10335                                next_style = next_style.split(old_url).join(url_map[old_url]);
10336                            }
10337                        });
10338                        if (next_style !== style) {
10339                            el.setAttribute('style', next_style);
10340                            changed = true;
10341                        }
10342                    }
10343                    return changed;
10344                });
10345                if (mutated) {
10346                    this.persist_mutated_code(mutated);
10347                    swapped = keys.length;
10348                }
10349                return swapped;
10350            },
10351            report_external_image_batch_result(summary) {
10352                if (typeof displayMessage !== 'function') {
10353                    return;
10354                }
10355                let safe = summary || {};
10356                let imported = safe.imported || 0;
10357                let errors = Array.isArray(safe.errors) ? safe.errors : [];
10358                if (imported > 0 && errors.length === 0) {
10359                    displayMessage('Saved ' + imported + ' image' + (imported === 1 ? '' : 's') + ' to your photo library.', false, 4000);
10360                    return;
10361                }
10362                if (imported > 0 && errors.length > 0) {
10363                    displayMessage('Saved ' + imported + ', but ' + errors.length + ' failed: ' + (errors[0].message || 'unknown error') + '.', true, 6000);
10364                    return;
10365                }
10366                displayMessage('Could not save the external images: ' + (errors.length ? (errors[0].message || 'unknown error') : 'nothing imported') + '.', true, 6000);
10367            },
10368            return_active_club_id() {
10369                if (typeof store !== 'undefined' && store.clubObject && store.clubObject.id) {
10370                    return store.clubObject.id;
10371                }
10372                return null;
10373            },
10374            parse_session_options() {
10375                if (typeof Parse === 'undefined' || !Parse.User || !Parse.User.current()) {
10376                    return {};
10377                }
10378                let token = Parse.User.current().getSessionToken ? Parse.User.current().getSessionToken() : null;
10379                if (!token) {
10380                    return {};
10381                }
10382                return { sessionToken: token };
10383            },
10384            find_placeholder_element(id) {
10385                return this.find_asset_element_by_id(id);
10386            },
10387            // Match either `data-cs-asset-placeholder="<id>"` (unfilled)
10388            // or `data-cs-asset-attached="<id>"` (already filled) so the
10389            // action menu can re-open against the same slot regardless
10390            // of state.
10391            find_asset_element_by_id(id) {
10392                let root = this.custom_code_render_root();
10393                if (!root || !id) {
10394                    return null;
10395                }
10396                let escaped = (typeof CSS !== 'undefined' && CSS.escape) ? CSS.escape(id) : id.replace(/"/g, '\\"');
10397                let selector =
10398                    '[data-cs-asset-placeholder="' + escaped + '"], ' +
10399                    '[data-cs-asset-attached="' + escaped + '"]';
10400                return root.querySelector(selector);
10401            },
10402            // Hostnames that mean "the file is already in the Clubspot
10403            // library." Mirrors the server-side PUBLIC_ASSET_HOSTS set
10404            // in primary/cloud/webbuilder-assets.js. Drift in this list
10405            // is non-fatal: at worst we offer the user "Copy to club
10406            // photos" on a URL that turns out to be local, and the
10407            // server rejects the import with a clear error. The set
10408            // here matters only for the UX surface (showing/hiding the
10409            // menu option).
10410            return_clubspot_asset_hosts() {
10411                return new Set(WEBBUILDER_CLUBSPOT_ASSET_HOSTS);
10412            },
10413            is_external_image_url(url) {
10414                return webbuilder_is_external_image_url(url);
10415            },
10416            // Pull the currently-attached URL out of an asset wrapper.
10417            // Handles the three shapes attach_url_to_placeholder writes:
10418            //   - <a data-cs-asset-attached href="..."> for documents.
10419            //   - <img data-cs-asset-attached src="..."> (rare; the
10420            //     wrapper itself is the <img>).
10421            //   - <div data-cs-asset-attached style="background-image:
10422            //     url(...);"> for the background-image target.
10423            //   - A wrapper <div data-cs-asset-attached> whose first
10424            //     <img> child carries the real src.
10425            //
10426            // Returns the raw URL string or null when nothing readable
10427            // is attached.
10428            return_attached_url_for_wrapper(wrapper) {
10429                if (!wrapper || wrapper.nodeType !== 1) {
10430                    return null;
10431                }
10432                let tag_name = (wrapper.tagName || '').toLowerCase();
10433                let target = (wrapper.getAttribute('data-cs-asset-target') || '').toLowerCase();
10434                if (target === 'background-image') {
10435                    let style = wrapper.getAttribute('style') || '';
10436                    let match = style.match(/background-image\s*:\s*url\(\s*(['"]?)([^'"\)]+)\1\s*\)/i);
10437                    return match ? match[2] : null;
10438                }
10439                if (tag_name === 'a' || target === 'href') {
10440                    return wrapper.getAttribute('href') || null;
10441                }
10442                if (tag_name === 'img') {
10443                    return wrapper.getAttribute('src') || null;
10444                }
10445                let child_img = wrapper.querySelector('img[src]');
10446                return child_img ? child_img.getAttribute('src') : null;
10447            },
10448            // Single-image "Copy image to club photos" flow for an
10449            // already-attached external image. Reads the wrapper's
10450            // current URL, runs the import via the shared
10451            // `import_external_image_url` helper, and swaps the
10452            // returned CDN URL into the same wrapper. The wrapper
10453            // stays put across the swap, so layout doesn't reflow.
10454            import_external_image_for_attached(id) {
10455                let wrapper = this.find_asset_element_by_id(id);
10456                if (!wrapper) {
10457                    return;
10458                }
10459                let original_url = this.return_attached_url_for_wrapper(wrapper);
10460                if (!this.is_external_image_url(original_url)) {
10461                    if (typeof displayMessage === 'function') {
10462                        displayMessage('This image is already in your library.', false, 4000);
10463                    }
10464                    return;
10465                }
10466                let alt = wrapper.getAttribute('data-cs-asset-label')
10467                    || wrapper.getAttribute('alt')
10468                    || '';
10469                if (typeof displayMessage === 'function') {
10470                    displayMessage('Importing image to your photo library...', false, 4000);
10471                }
10472                this.import_external_image_url(original_url, alt).then((cdn_url) => {
10473                    this.attach_url_to_placeholder({
10474                        id:         id,
10475                        url:        cdn_url,
10476                        verified:   true,
10477                        source:     'imported_external_url',
10478                        visibility: 'public_web',
10479                    });
10480                    if (typeof displayMessage === 'function') {
10481                        displayMessage('Image saved to your photo library.', false, 4000);
10482                    }
10483                }).catch((error) => {
10484                    if (typeof checkParseError === 'function') {
10485                        checkParseError(error);
10486                    }
10487                    let msg = error && error.message ? error.message : 'Image import failed.';
10488                    if (typeof displayMessage === 'function') {
10489                        displayMessage(msg, true, 6000);
10490                    }
10491                });
10492            },
10493
10494            // ─── Bare <img> change flow ───────────────────────────────
10495            //
10496            // Parallel to the placeholder-marker flow above, but keyed
10497            // on the image's current `src` instead of a synthetic id.
10498            // Used by the third pass of `decorate_asset_placeholders`
10499            // that catches bare `<img>` tags from URL-reference builds.
10500            //
10501            // The same handle_common menu shape powers both — the
10502            // bare-img menu just routes its callbacks through src-based
10503            // mutation so the persisted HTML stays bare (no surprise
10504            // wrappers, no surprise unsaved_changes) until the admin
10505            // actually swaps the image.
10506
10507            open_bare_image_menu(img_el) {
10508                if (typeof handle_common !== 'function') {
10509                    return;
10510                }
10511                if (!img_el || typeof img_el.getAttribute !== 'function') {
10512                    return;
10513                }
10514                let current_src = img_el.getAttribute('src') || '';
10515                let current_alt = img_el.getAttribute('alt') || '';
10516                if (!current_src.trim()) {
10517                    return;
10518                }
10519                let is_external = this.is_external_image_url(current_src);
10520                let options = [];
10521                options.push({
10522                    text:    'Pick a different photo',
10523                    subtext: 'Upload a new image or pick an existing one from this club',
10524                    fn:      () => this.run_after_placeholder_menu_close(() => this.pick_existing_for_bare_image(img_el)),
10525                });
10526                options.push({
10527                    text:    'Choose from club documents',
10528                    subtext: 'Public-access files in the club documents folder',
10529                    fn:      () => this.run_after_placeholder_menu_close(() => this.pick_document_for_bare_image(img_el)),
10530                });
10531                options.push({
10532                    text:    'Paste a different URL',
10533                    subtext: 'Paste a public image URL (Clubspot 
10533CDN or external host)',
10534                    fn:      () => this.run_after_placeholder_menu_close(() => this.paste_url_for_bare_image(img_el)),
10535                });
10536                if (is_external) {
10537                    options.push({
10538                        text:    'Copy image to club photos',
10539                        subtext: 'Save a local copy in your photo library and use that instead of the external URL.',
10540                        fn:      () => this.run_after_placeholder_menu_close(() => this.import_external_image_for_bare(img_el)),
10541                    });
10542                }
10543                let label = (current_alt || '').trim() || 'image';
10544                handle_common({
10545                    title:             'Change "' + label + '"',
10546                    subtitle:          is_external
10547                        ? 'Pick a different photo, paste another URL, or save this external image into your library.'
10548                        : 'Pick a different photo or paste another URL.',
10549                    hide_on_selection: false,
10550                    options:           options,
10551                });
10552            },
10553            // Persist a swap from one img src to another. Matches by
10554            // exact `src` attribute value, which covers the model's
10555            // output and any direct admin edits (the codegen pipeline
10556            // doesn't normalize URL strings between turns). Also lifts
10557            // a missing `alt` from the new label so SEO/accessibility
10558            // copy survives the swap.
10559            swap_bare_image_src(old_src, new_url, new_alt) {
10560                if (typeof old_src !== 'string' || !old_src.trim()) {
10561                    return false;
10562                }
10563                if (typeof new_url !== 'string' || !new_url.trim()) {
10564                    return false;
10565                }
10566                let mutated = this.mutate_persisted_html((doc) => {
10567                    let imgs = Array.from(doc.querySelectorAll('img[src]'));
10568                    let changed = false;
10569                    for (let img of imgs) {
10570                        if (img.getAttribute('src') !== old_src) {
10571                            continue;
10572                        }
10573                        img.setAttribute('src', new_url);
10574                        if (new_alt && !(img.getAttribute('alt') || '').trim()) {
10575                            img.setAttribute('alt', new_alt);
10576                        }
10577                        changed = true;
10578                    }
10579                    return changed;
10580                });
10581                if (mutated) {
10582                    this.persist_mutated_code(mutated);
10583                    return true;
10584                }
10585                return false;
10586            },
10587            pick_existing_for_bare_image(img_el) {
10588                if (typeof webbuilder_asset_picker !== 'function') {
10589                    if (typeof displayMessage === 'function') {
10590                        displayMessage('Club photo picker is not loaded. Refresh the page and try again.', true, 5000);
10591                    }
10592                    return;
10593                }
10594                let old_src = img_el.getAttribute('src') || '';
10595                if (!old_src) {
10596                    return;
10597                }
10598                webbuilder_asset_picker({
10599                    title:             'Select club photo',
10600                    accepts:           'image',
10601                    source:            'photos',
10602                    hide_on_selection: true,
10603                    fn: (result) => {
10604                        let url = result && result.url ? result.url : null;
10605                        if (!url) {
10606                            return;
10607                        }
10608                        this.swap_bare_image_src(old_src, url, result.label || '');
10609                    },
10610                });
10611            },
10612            pick_document_for_bare_image(img_el) {
10613                if (typeof webbuilder_asset_picker !== 'function') {
10614                    if (typeof displayMessage === 'function') {
10615                        displayMessage('Documents picker is not loaded. Refresh the page and try again.', true, 5000);
10616                    }
10617                    return;
10618                }
10619                let old_src = img_el.getAttribute('src') || '';
10620                if (!old_src) {
10621                    return;
10622                }
10623                webbuilder_asset_picker({
10624                    title:             'Select a file',
10625                    accepts:           'image',
10626                    source:            'documents',
10627                    hide_on_selection: true,
10628                    fn: (result) => {
10629                        let url = result && result.url ? result.url : null;
10630                        if (!url) {
10631                            return;
10632                        }
10633                        this.swap_bare_image_src(old_src, url, result.label || '');
10634                    },
10635                });
10636            },
10637            paste_url_for_bare_image(img_el) {
10638                let old_src = img_el.getAttribute('src') || '';
10639                let label = img_el.getAttribute('alt') || 'image';
10640                if (!old_src) {
10641                    return;
10642                }
10643                let do_attach = (attach) => {
10644                    this.swap_bare_image_src(old_src, attach.url, label);
10645                };
10646                this.prompt_for_pasted_url({
10647                    slot_label: label,
10648                    alt:        label,
10649                    do_attach:  do_attach,
10650                });
10651            },
10652            // Single-image import path for bare imgs. Reads the img's
10653            // current src, runs the same Cloud function the attached-
10654            // wrapper menu uses, and swaps the src for the CDN URL on
10655            // success. The img stays exactly where it is — no wrapper
10656            // upgrade, no layout change.
10657            import_external_image_for_bare(img_el) {
10658                let original_url = img_el.getAttribute('src') || '';
10659                let alt = img_el.getAttribute('alt') || '';
10660                if (!this.is_external_image_url(original_url)) {
10661                    if (typeof displayMessage === 'function') {
10662                        displayMessage('This image is already in your library.', false, 4000);
10663                    }
10664                    return;
10665                }
10666                if (typeof displayMessage === 'function') {
10667                    displayMessage('Importing image to your photo library...', false, 4000);
10668                }
10669                this.import_external_image_url(original_url, alt).then((cdn_url) => {
10670                    this.swap_bare_image_src(original_url, cdn_url, alt);
10671                    if (typeof displayMessage === 'function') {
10672                        displayMessage('Image saved to your photo library.', false, 4000);
10673                    }
10674                }).catch((error) => {
10675                    if (typeof checkParseError === 'function') {
10676                        checkParseError(error);
10677                    }
10678                    let msg = error && error.message ? error.message : 'Image import failed.';
10679                    if (typeof displayMessage === 'function') {
10680                        displayMessage(msg, true, 6000);
10681                    }
10682                });
10683            },
10684
10685            // Rewrite the persisted custom-code string so the matching
10686            // placeholder element gains a real `src`/`href`/`background-
10687            // image`. We keep the wrapper element across the transition
10688            // — it carries the section's layout box (dimensions, border-
10689            // radius, positioning, etc) — and just rotate its children
10690            // for `target="src"`. The wrapper's `data-cs-asset-
10691            // placeholder` attribute is renamed to `data-cs-asset-
10692            // attached` so:
10693            //
10694            //   1. Subsequent decorate passes show the small "Change"
10695            //      overlay instead of the big "Add" overlay.
10696            //   2. The AI model can tell on follow-up turns that the
10697            //      slot is filled (and must not be re-placeholderized).
10698            //
10699            // We also accept being called against an already-attached
10700            // element — that's the "Change" flow swapping one URL for
10701            // another.
10702            attach_url_to_placeholder(params) {
10703                let { id, url } = params;
10704                if (!id || typeof url !== 'string' || !url.trim()) {
10705                    return;
10706                }
10707                let mutated = this.mutate_persisted_html((doc) => {
10708                    let escaped = (typeof CSS !== 'undefined' && CSS.escape) ? CSS.escape(id) : id.replace(/"/g, '\\"');
10709                    let wrapper = doc.querySelector(
10710                        '[data-cs-asset-placeholder="' + escaped + '"], ' +
10711                        '[data-cs-asset-attached="' + escaped + '"]'
10712                    );
10713                    if (!wrapper) {
10714                        return false;
10715                    }
10716                    let tag_name = (wrapper.tagName || '').toLowerCase();
10717                    let target = (wrapper.getAttribute('data-cs-asset-target') || '').toLowerCase();
10718                    if (!target) {
10719                        if (tag_name === 'a') {
10720                            target = 'href';
10721                        } else if (tag_name === 'img') {
10722                            target = 'src';
10723                        } else {
10724                            target = 'src';
10725                        }
10726                    }
10727                    let alt = wrapper.getAttribute('data-cs-asset-label') || wrapper.getAttribute('alt') || '';
10728
10729                    if (target === 'background-image') {
10730                        let existing_style = wrapper.getAttribute('style') || '';
10731                        // Strip any existing background-image declaration
10732                        // so the new url() wins without doubling up. Also
10733                        // wipe any inner content the placeholder used as
10734                        // a fallback label — once we've got a real image,
10735                        // the gradient/icon "Add a photo" copy would just
10736                        // clutter the live site.
10737                        let stripped = existing_style.replace(/background-image\s*:[^;]*;?/gi, '').trim();
10738                        let suffix = stripped && !/;\s*$/.test(stripped) ? '; ' : (stripped ? ' ' : '');
10739                        let bg_value = 'background-image: url("' + this.escape_css_url_segment(url) + '"); background-size: cover; background-position: center;';
10740                        wrapper.setAttribute('style', (stripped + suffix + bg_value).trim());
10741                    } else if (target === 'href' || tag_name === 'a') {
10742                        wrapper.setAttribute('href', url);
10743                        wrapper.setAttribute('target', '_blank');
10744                        wrapper.setAttribute('rel', 'noopener noreferrer');
10745                    } else if (tag_name === 'img') {
10746                        wrapper.setAttribute('src', url);
10747                        if (alt && !wrapper.getAttribute('alt')) {
10748                            wrapper.setAttribute('alt', alt);
10749                        }
10750                    } else {
10751                        // `target=src` on a wrapper element: drop any
10752                        // fallback children (icon, text label) and insert
10753                        // an <img> sized to fill the wrapper. The wrapper
10754                        // continues to carry the layout box, so the live
10755                        // site never reflows between empty/filled states.
10756                        this.empty_placeholder_inner(wrapper);
10757                        let owner = wrapper.ownerDocument || document;
10758                        let img = owner.createElement('img');
10759                        img.setAttribute('src', url);
10760                        if (alt) {
10761                            img.setAttribute('alt', alt);
10762                        }
10763                        img.setAttribute('style', 'width:100%;height:100%;object-fit:cover;display:block;border-radius:inherit;');
10764                        wrapper.appendChild(img);
10765                    }
10766                    // Rename the placeholder marker → attached marker
10767                    // (same id). Preserve everything else so the model
10768                    // still has the role/label/target context next turn.
10769                    let placeholder_id = wrapper.getAttribute('data-cs-asset-placeholder');
10770                    if (placeholder_id !== null) {
10771                        wrapper.removeAttribute('data-cs-asset-placeholder');
10772                        wrapper.setAttribute('data-cs-asset-attached', placeholder_id);
10773                    }
10774                    return true;
10775                });
10776                if (mutated) {
10777                    this.persist_mutated_code(mutated);
10778                }
10779            },
10780            // Drop any inert fallback content the placeholder shipped
10781            // with (e.g. "Add a hero photo" label, decorative icon spans).
10782            // Leaves the wrapper element itself untouched.
10783            empty_placeholder_inner(wrapper) {
10784                if (!wrapper) {
10785                    return;
10786                }
10787                while (wrapper.firstChild) {
10788                    wrapper.removeChild(wrapper.firstChild);
10789                }
10790            },
10791            // "Keep empty for now" — dismiss the prominent "Add image"
10792            // overlay but keep the wrapper marked so the admin can
10793            // still attach an asset later via the small Change button.
10794            // We rename `data-cs-asset-placeholder` → `data-cs-asset-
10795            // attached` (same id, no URL set) which:
10796            //   - swaps the decorator from big overlay → Change chip,
10797            //   - tells the AI to preserve the slot as-is on follow-up
10798            //     rewrites (don't re-placeholderize).
10799            // Permanent deletion is a code-editor operation.
10800            remove_placeholder(id) {
10801                if (!id) {
10802                    return;
10803                }
10804                let mutated = this.mutate_persisted_html((doc) => {
10805                    let escaped = (typeof CSS !== 'undefined' && CSS.escape) ? CSS.escape(id) : id.replace(/"/g, '\\"');
10806                    let placeholder = doc.querySelector('[data-cs-asset-placeholder="' + escaped + '"]');
10807                    if (!placeholder) {
10808                        return false;
10809                    }
10810                    placeholder.removeAttribute('data-cs-asset-placeholder');
10811                    placeholder.setAttribute('data-cs-asset-attached', id);
10812                    return true;
10813                });
10814                if (mutated) {
10815                    this.persist_mutated_code(mutated);
10816                }
10817            },
10818            // "Remove this image" on an attached slot — wipe the
10819            // filled URL/contents and put the slot back into placeholder
10820            // state so the admin can re-fill from scratch. For `src`
10821            // wrappers we also need to drop the inner <img>.
10822            revert_attached_to_placeholder(id) {
10823                if (!id) {
10824                    return;
10825                }
10826                let mutated = this.mutate_persisted_html((doc) => {
10827                    let escaped = (typeof CSS !== 'undefined' && CSS.escape) ? CSS.escape(id) : id.replace(/"/g, '\\"');
10828                    let wrapper = doc.querySelector('[data-cs-asset-attached="' + escaped + '"]');
10829                    if (!wrapper) {
10830                        return false;
10831                    }
10832                    let tag_name = (wrapper.tagName || '').toLowerCase();
10833                    let target = (wrapper.getAttribute('data-cs-asset-target') || '').toLowerCase();
10834                    if (!target) {
10835                        if (tag_name === 'a') {
10836                            target = 'href';
10837                        } else if (tag_name === 'img') {
10838                            target = 'src';
10839                        } else {
10840                            target = 'src';
10841                        }
10842                    }
10843                    if (target === 'background-image') {
10844                        let style = wrapper.getAttribute('style') || '';
10845                        let stripped = style.replace(/background-image\s*:[^;]*;?/gi, '').trim();
10846                        if (stripped) {
10847                            wrapper.setAttribute('style', stripped);
10848                        } else {
10849                            wrapper.removeAttribute('style');
10850                        }
10851                    } else if (target === 'href' || tag_name === 'a') {
10852                        wrapper.removeAttribute('href');
10853                        wrapper.removeAttribute('target');
10854                        wrapper.removeAttribute('rel');
10855                    } else {
10856                        this.empty_placeholder_inner(wrapper);
10857                    }
10858                    let attached_id = wrapper.getAttribute('data-cs-asset-attached');
10859                    if (attached_id !== null) {
10860                        wrapper.removeAttribute('data-cs-asset-attached');
10861                        wrapper.setAttribute('data-cs-asset-placeholder', attached_id);
10862                    }
10863                    return true;
10864                });
10865                if (mutated) {
10866                    this.persist_mutated_code(mutated);
10867                }
10868            },
10869            strip_all_asset_attributes(element) {
10870                if (!element) {
10871                    return;
10872                }
10873                let attrs_to_remove = [];
10874                Array.prototype.slice.call(element.attributes || []).forEach((attr) => {
10875                    let name = String(attr.name || '').toLowerCase();
10876                    if (name.indexOf('data-cs-asset-') === 0) {
10877                        attrs_to_remove.push(attr.name);
10878                    }
10879                });
10880                attrs_to_remove.forEach((name) => element.removeAttribute(name));
10881            },
10882            escape_css_url_segment(url) {
10883                if (typeof url !== 'string') {
10884                    return '';
10885                }
10886                return url.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
10887            },
10888            // Parse the persisted custom-code string into a scratch
10889            // <template>, hand it to `mutator` for in-place changes,
10890            // and serialize back. Returns the new HTML string or null
10891            // if the mutator reported no change.
10892            mutate_persisted_html(mutator) {
10893                let source = typeof this.custom_code_value === 'string' ? this.custom_code_value : '';
10894                let template = document.createElement('template');
10895                template.innerHTML = source;
10896                let changed = mutator(template.content);
10897                if (!changed) {
10898                    return null;
10899                }
10900                return template.innerHTML;
10901            },
10902            persist_mutated_code(new_html) {
10903                if (typeof new_html !== 'string') {
10904                    return;
10905                }
10906                this.custom_code_value = new_html;
10907                if (this.cm_instance) {
10908                    this.cm_instance.setValue(new_html);
10909                }
10910                if (this.component && typeof save_changes_for_component === 'function') {
10911                    save_changes_for_component(this.component, 'custom_code', new_html);
10912                }
10913                this.mark_ai_code_revision_changed();
10914                this.$nextTick(() => this.initialize_custom_code_interactions());
10915            },
10916
10917            // ─── Editable text leaves ─────────────────────────────────
10918            //
10919            // Walk the rendered preview, find every "text leaf" element
10920            // (any element whose own children are inline-only, plus
10921            // direct text), and attach a click handler that opens an
10922            // inline edit popover. Skip:
10923            //   - asset placeholder/attached wrappers (their inner text
10924            //     is fallback content the placeholder fill flow wipes),
10925            //   - data-cs-toggle / data-cs-tab interactive triggers
10926            //     (admin uses those to test interactions in preview),
10927            //   - the AI launcher button.
10928            //
10929            // Each decorated element gets a `__cs_text_decorated` flag
10930            // so we don't double-bind across re-decorate passes within
10931            // one render, and a `cs-text-editable-host` class so CSS can
10932            // give it a hover outline + cursor:pointer affordance.
10933            decorate_editable_text_nodes() {
10934                if (!this.preview_element) {
10935                    return;
10936                }
10937                let root = this.custom_code_render_root();
10938                if (!root) {
10939                    return;
10940                }
10941                let candidates = this.collect_editable_text_leaves(root);
10942                candidates.forEach((el) => {
10943                    if (el.__cs_text_decorated) {
10944                        return;
10945                    }
10946                    el.__cs_text_decorated = true;
10947                    el.classList.add('cs-text-editable-host');
10948                    let handler = (event) => {
10949                        if (this.should_allow_text_click_through(event)) {
10950                            return;
10951                        }
10952                        event.preventDefault();
10953                        event.stopPropagation();
10954                        this.open_text_edit_popover(el);
10955                    };
10956                    el.addEventListener('click', handler);
10957                });
10958            },
10959
10960            // DFS-collect text leaves. When an element qualifies, we do
10961            // NOT recurse into it — we want the OUTERMOST text leaf so
10962            // `<h1>Hello <em>world</em></h1>` is edited as one string,
10963            // not two. We also stop recursing into media/script/svg
10964            // wrappers and into asset placeholder/attached wrappers
10965            // (those manage their own UI), since nothing inside should
10966            // be a text leaf anyway.
10967            collect_editable_text_leaves(root) {
10968                let results = [];
10969                let visit = (el) => {
10970                    if (!el || el.nodeType !== 1) {
10971                        return;
10972                    }
10973                    let tag = (el.tagName || '').toLowerCase();
10974                    if (TEXT_SKIP_TAGS.has(tag)) {
10975                        return;
10976                    }
10977                    if (this.is_preview_injected_node(el)) {
10978                        return;
10979                    }
10980                    if (el.hasAttribute && (
10981                        el.hasAttribute('data-cs-asset-placeholder') ||
10982                        el.hasAttribute('data-cs-asset-attached')
10983                    )) {
10984                        return;
10985                    }
10986                    if (this.is_editable_text_leaf(el, root)) {
10987                        results.push(el);
10988                        return;
10989                    }
10990                    let child_elements = el.children || [];
10991                    for (let child of child_elements) {
10992                        visit(child);
10993                    }
10994                };
10995                let top = root.children || [];
10996                for (let child of top) {
10997                    visit(child);
10998                }
10999                return results;
11000            },
11001
11002            is_editable_text_leaf(el, root) {
11003                if (!el || el.nodeType !== 1) {
11004                    return false;
11005                }
11006                let tag = (el.tagName || '').toLowerCase();
11007                if (TEXT_SKIP_TAGS.has(tag)) {
11008                    return false;
11009                }
11010                if (el.classList && (
11011                    el.classList.contains('cs-asset-placeholder-overlay') ||
11012                    el.classList.contains('cs-asset-change-button') ||
11013                    el.classList.contains('cs-asset-background-placeholder-chip') ||
11014                    el.classList.contains('cs-bare-image-preview-host') ||
11015                    el.classList.contains('custom-code-ai-launcher') ||
11016                    el.classList.contains('cs-text-edit-popover')
11017                )) {
11018                    return false;
11019                }
11020                // Skip declarative-interaction triggers — admins want to
11021                // click those to test the toggle/tab behavior in preview.
11022                if (el.hasAttribute && (
11023                    el.hasAttribute('data-cs-toggle') ||
11024                    el.hasAttribute('data-cs-tab')
11025                )) {
11026                    return false;
11027                }
11028                // Skip if self or ancestor is an asset placeholder/attached
11029                // wrapper — those wrappers own their fallback content.
11030                let ancestor = el;
11031                while (ancestor && ancestor !== root) {
11032                    if (ancestor.nodeType === 1 && ancestor.hasAttribute && (
11033                        ancestor.hasAttribute('data-cs-asset-placeholder') ||
11034                        ancestor.hasAttribute('data-cs-asset-attached')
11035                    )) {
11036                        return false;
11037                    }
11038                    if (this.is_preview_injected_node(ancestor)) {
11039                        return false;
11040                    }
11041                    ancestor = ancestor.parentNode;
11042                }
11043                let has_text = false;
11044                for (let child of el.childNodes) {
11045                    if (child.nodeType === 3 && child.nodeValue && child.nodeValue.trim()) {
11046                        has_text = true;
11047                        break;
11048                    }
11049                }
11050                if (!has_text) {
11051                    return false;
11052                }
11053                let element_children = el.children || [];
11054                for (let child of element_children) {
11055                    let child_tag = (child.tagName || '').toLowerCase();
11056                    if (!TEXT_INLINE_TAGS.has(child_tag)) {
11057                        return false;
11058                    }
11059                }
11060                return true;
11061            },
11062
11063            // Walk up from `element` to `root`, recording the index of
11064            // each step among its parent's child NODES (text + element).
11065            // Skip preview-only injected nodes (asset overlays) since
11066            // they don't exist in the persisted source. The same path
11067            // can then be replayed on the parsed source (skipping
11068            // 
11068<script>, which sanitize_custom_code_html removes from
11069            // the rendered preview).
11070            compute_text_element_path(element, root) {
11071                if (!element || !root || element === root) {
11072                    return null;
11073                }
11074                let path = [];
11075                let current = element;
11076                while (current && current !== root) {
11077                    let parent = current.parentNode;
11078                    if (!parent) {
11079                        return null;
11080                    }
11081                    let visible_index = -1;
11082                    let count = 0;
11083                    let found = false;
11084                    for (let child of parent.childNodes) {
11085                        if (this.is_preview_injected_node(child)) {
11086                            continue;
11087                        }
11088                        if (child === current) {
11089                            visible_index = count;
11090                            found = true;
11091                            break;
11092                        }
11093                        count++;
11094                    }
11095                    if (!found || visible_index === -1) {
11096                        return null;
11097                    }
11098                    path.unshift(visible_index);
11099                    current = parent;
11100                }
11101                if (current !== root) {
11102                    return null;
11103                }
11104                return path;
11105            },
11106
11107            is_preview_injected_node(node) {
11108                if (!node || node.nodeType !== 1) {
11109                    return false;
11110                }
11111                if (!node.classList) {
11112                    return false;
11113                }
11114                return (
11115                    node.classList.contains('cs-asset-placeholder-overlay') ||
11116                    node.classList.contains('cs-asset-change-button') ||
11117                    node.classList.contains('cs-asset-background-placeholder-chip') ||
11118                    node.classList.contains('cs-bare-image-host') ||
11119                    node.classList.contains('cs-bare-image-preview-host')
11120                );
11121            },
11122            should_allow_text_click_through(event) {
11123                if (!this._text_click_through_until || Date.now() > this._text_click_through_until) {
11124                    return false;
11125                }
11126                let target = event && event.target ? event.target : null;
11127                if (!target || typeof target.closest !== 'function') {
11128                    return false;
11129                }
11130                return !!target.closest('a, button, [role="button"], [data-cs-toggle], [data-cs-tab]');
11131            },
11132
11133            // Walk a parsed-source tree using a path captured from the
11134            // preview. Skip 
11134<script> nodes on the source side because
11135            // sanitize_custom_code_html strips them at render time —
11136            // without skipping, source/preview indices diverge any
11137            // time a section embeds a script tag.
11138            find_text_element_by_path(root, path) {
11139                if (!root || !path) {
11140                    return null;
11141                }
11142                let current = root;
11143                for (let target_index of path) {
11144                    if (!current || !current.childNodes) {
11145                        return null;
11146                    }
11147                    let visible_index = 0;
11148                    let next = null;
11149                    for (let child of current.childNodes) {
11150                        if (
11151                            child.nodeType === 1 &&
11152                            (child.tagName || '').toLowerCase() === 'script'
11153                        ) {
11154                            continue;
11155                        }
11156                        if (visible_index === target_index) {
11157                            next = child;
11158                            break;
11159                        }
11160                        visible_index++;
11161                    }
11162                    if (!next) {
11163                        return null;
11164                    }
11165                    current = next;
11166                }
11167                return current;
11168            },
11169
11170            // Inline popover: textarea + Save/Cancel, positioned near
11171            // the clicked element via fixed coords. We close on outside
11172            // click, ESC, or scroll of the preview pane (re-positioning
11173            // on scroll added complexity for little gain — admins will
11174            // re-click after scrolling).
11175            open_text_edit_popover(target_el) {
11176                this.close_text_edit_popover();
11177                if (!target_el) {
11178                    return;
11179                }
11180                let root = this.custom_code_render_root();
11181                if (!root) {
11182                    return;
11183                }
11184                let path = this.compute_text_element_path(target_el, root);
11185                if (!path) {
11186                    return;
11187                }
11188                let initial_value = this.read_element_text(target_el);
11189                let link_target = this.find_text_edit_link_target(target_el, root);
11190                let link_path = link_target ? this.compute_text_element_path(link_target, root) : null;
11191                let initial_url = link_target ? this.read_text_edit_url(link_target) : '';
11192
11193                let popover = document.createElement('div');
11194                popover.className = 'cs-text-edit-popover';
11195                popover.setAttribute('data-cs-text-popover', '1');
11196
11197                let header = document.createElement('div');
11198                header.className = 'cs-text-edit-popover-header';
11199                header.textContent = 'Edit text';
11200                popover.appendChild(header);
11201
11202                let textarea = document.createElement('textarea');
11203                textarea.className = 'cs-text-edit-textarea';
11204                textarea.value = initial_value;
11205                textarea.rows = Math.min(8, Math.max(2, initial_value.split('\n').length + 1));
11206                popover.appendChild(textarea);
11207
11208                let url_input = null;
11209                if (link_target && link_path) {
11210                    let url_wrap = document.createElement('label');
11211                    url_wrap.className = 'cs-text-edit-url-field';
11212
11213                    let url_label = document.createElement('span');
11214                    url_label.className = 'cs-text-edit-url-label';
11215                    url_label.textContent = 'URL or path';
11216                    url_wrap.appendChild(url_label);
11217
11218                    url_input = document.createElement('input');
11219                    url_input.type = 'text';
11220                    url_input.className = 'cs-text-edit-url-input';
11221                    url_input.value = initial_url;
11222                    url_input.placeholder = '';
11223                    url_wrap.appendChild(url_input);
11224
11225                    popover.appendChild(url_wrap);
11226                }
11227
11228                let footer = document.createElement('div');
11229                footer.className = 'cs-text-edit-popover-footer';
11230                let ask_btn = document.createElement('button');
11231                ask_btn.type = 'button';
11232                ask_btn.className = 'cs-text-edit-ask';
11233                ask_btn.title = 'Ask AI about this text — adds it as a reference in the chat';
11234                ask_btn.innerHTML =
11235                    '<i class="fa-regular fa-sparkles"></i>' +
11236                    '<span>Ask</span>';
11237                let footer_spacer = document.createElement('div');
11238                footer_spacer.className = 'cs-text-edit-popover-footer-spacer';
11239                let cancel_btn = document.createElement('button');
11240                cancel_btn.type = 'button';
11241                cancel_btn.className = 'cs-text-edit-cancel';
11242                cancel_btn.textContent = 'Cancel';
11243                let save_btn = document.createElement('button');
11244                save_btn.type = 'button';
11245                save_btn.className = 'cs-text-edit-save';
11246                save_btn.textContent = 'Save';
11247                footer.appendChild(ask_btn);
11248                footer.appendChild(footer_spacer);
11249                footer.appendChild(cancel_btn);
11250                footer.appendChild(save_btn);
11251                popover.appendChild(footer);
11252
11253                document.body.appendChild(popover);
11254                this.position_text_edit_popover(popover, target_el);
11255
11256                let do_save = () => {
11257                    let new_value = textarea.value;
11258                    let new_url = url_input ? url_input.value : null;
11259                    this.commit_text_edit(path, new_value, link_path, new_url);
11260                    this.close_text_edit_popover();
11261                };
11262                let do_cancel = () => {
11263                    this.close_text_edit_popover();
11264                };
11265                let do_ask = () => {
11266                    // Snapshot the textarea value (admin may have
11267                    // tweaked the text before clicking Ask) and add
11268                    // it as a chat reference. We don't persist the
11269                    // edit — the chip carries the current draft so
11270                    // the AI sees what the admin is looking at.
11271                    let snippet = textarea.value;
11272                    this.send_text_edit_to_ai_chat(target_el, snippet);
11273                    this.close_text_edit_popover();
11274                };
11275                save_btn.addEventListener('click', do_save);
11276                cancel_btn.addEventListener('click', do_cancel);
11277                ask_btn.addEventListener('click', do_ask);
11278                textarea.addEventListener('keydown', (event) => {
11279                    if (event.key === 'Escape') {
11280                        event.preventDefault();
11281                        do_cancel();
11282                        return;
11283                    }
11284                    if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
11285                        event.preventDefault();
11286                        do_save();
11287                    }
11288                });
11289                if (url_input) {
11290                    url_input.addEventListener('keydown', (event) => {
11291                        if (event.key === 'Escape') {
11292                            event.preventDefault();
11293                            do_cancel();
11294                            return;
11295                        }
11296                        if (event.key === 'Enter') {
11297                            event.preventDefault();
11298                            do_save();
11299                        }
11300                    });
11301                }
11302                // Stop propagation so our outside-click handler doesn't
11303                // see clicks landing inside the popover and dismiss it.
11304                popover.addEventListener('mousedown', (event) => {
11305                    event.stopPropagation();
11306                });
11307                popover.addEventListener('click', (event) => {
11308                    event.stopPropagation();
11309                });
11310
11311                let outside_handler = (event) => {
11312                    if (popover.contains(event.target)) {
11313                        return;
11314                    }
11315                    let target = event && event.target ? event.target : null;
11316                    if (target && root.contains(target)) {
11317                        this._text_click_through_until = Date.now() + 250;
11318                    }
11319                    do_cancel();
11320                };
11321                // Pin the popover to the target element on scroll +
11322                // resize. Capture-phase scroll listener on window
11323                // catches scroll events from any nested scroll
11324                // container (scroll doesn't bubble, but capture fires
11325                // for every ancestor). If the target gets detached
11326                // (e.g. AI re-renders the section, admin switches
11327                // tabs) we close instead of leaving an orphan popover
11328                // pointing at a stale rect.
11329                let reposition = () => {
11330                    if (!this._text_popover || this._text_popover.el !== popover) {
11331                        return;
11332                    }
11333                    if (!target_el || !target_el.isConnected) {
11334                        do_cancel();
11335                        return;
11336                    }
11337                    this.position_text_edit_popover(popover, target_el);
11338                };
11339                // Defer attach so the click that opened the popover
11340                // doesn't immediately close it. Also listen for
11341                // touchstart so taps on a touch device dismiss the
11342                // popover the same way mouse clicks do.
11343                setTimeout(() => {
11344                    document.addEventListener('mousedown',  outside_handler, true);
11345                    document.addEventListener('touchstart', outside_handler, true);
11346                    window.addEventListener('scroll', reposition, true);
11347                    window.addEventListener('resize', reposition);
11348                }, 0);
11349
11350                this._text_popover = {
11351                    el:               popover,
11352                    target_el:        target_el,
11353                    outside_handler:  outside_handler,
11354                    reposition:       reposition,
11355                };
11356
11357                // Highlight the element being edited so it's obvious
11358                // which text the popover targets.
11359                target_el.classList.add('cs-text-editable-active');
11360
11361                this.$nextTick(() => {
11362                    textarea.focus();
11363                    textarea.select();
11364                });
11365            },
11366
11367            close_text_edit_popover() {
11368                if (!this._text_popover) {
11369                    return;
11370                }
11371                let popover_state = this._text_popover;
11372                this._text_popover = null;
11373                if (popover_state.outside_handler) {
11374                    document.removeEventListener('mousedown',  popover_state.outside_handler, true);
11375                    document.removeEventListener('touchstart', popover_state.outside_handler, true);
11376                }
11377                if (popover_state.reposition) {
11378                    window.removeEventListener('scroll', popover_state.reposition, true);
11379                    window.removeEventListener('resize', popover_state.reposition);
11380                }
11381                if (popover_state.target_el && popover_state.target_el.classList) {
11382                    popover_state.target_el.classList.remove('cs-text-editable-active');
11383                }
11384                if (popover_state.el && popover_state.el.parentNode) {
11385                    popover_state.el.parentNode.removeChild(popover_state.el);
11386                }
11387            },
11388
11389            // Position the popover below the element when there's room,
11390            // otherwise above. Use viewport (fixed) coordinates so the
11391            // popover doesn't get clipped by overflow:hidden ancestors
11392            // (the preview pane itself scrolls).
11393            position_text_edit_popover(popover, target_el) {
11394                if (!popover || !target_el) {
11395                    return;
11396                }
11397                let rect = target_el.getBoundingClientRect();
11398                // Width: clamp between 240 and 420 px, max 92vw.
11399                let max_width = Math.min(window.innerWidth - 24, 420);
11400                let width = Math.max(240, Math.min(max_width, Math.round(rect.width)));
11401                popover.style.width = width + 'px';
11402
11403                // Render once so we can measure height for vertical fit.
11404                popover.style.left = '0px';
11405                popover.style.top = '0px';
11406                popover.style.visibility = 'hidden';
11407                popover.style.display = 'block';
11408
11409                let popover_rect = popover.getBoundingClientRect();
11410                let popover_height = popover_rect.height;
11411                let gap = 8;
11412                let viewport_h = window.innerHeight;
11413                let viewport_w = window.innerWidth;
11414
11415                let left = Math.round(rect.left);
11416                if (left + width + 8 > viewport_w) {
11417                    left = Math.max(8, viewport_w - width - 8);
11418                }
11419                if (left < 8) {
11420                    left = 8;
11421                }
11422
11423                let top;
11424                let space_below = viewport_h - rect.bottom;
11425                let space_above = rect.top;
11426                if (space_below >= popover_height + gap || space_below >= space_above) {
11427                    top = Math.round(rect.bottom + gap);
11428                    if (top + popover_height + 8 > viewport_h) {
11429                        top = Math.max(8, viewport_h - popover_height - 8);
11430                    }
11431                } else {
11432                    top = Math.round(rect.top - popover_height - gap);
11433                    if (top < 8) {
11434                        top = 8;
11435                    }
11436                }
11437
11438                popover.style.left = left + 'px';
11439                popover.style.top = top + 'px';
11440                popover.style.visibility = '';
11441            },
11442
11443            // Concatenate all text nodes inside the element, preserving
11444            // line breaks for `<br>` so multi-line copy round-trips
11445            // sensibly. Inline emphasis tags collapse to plain text on
11446            // save by design — admins use the Edit tab for fancy
11447            // inline markup.
11448            read_element_text(el) {
11449                if (!el) {
11450                    return '';
11451                }
11452                let parts = [];
11453                let walk = (node) => {
11454                    if (!node) {
11455                        return;
11456                    }
11457                    if (node.nodeType === 3) {
11458                        parts.push(node.nodeValue || '');
11459                        return;
11460                    }
11461                    if (node.nodeType !== 1) {
11462                        return;
11463                    }
11464                    let tag = (node.tagName || '').toLowerCase();
11465                    if (tag === 'br') {
11466                        parts.push('\n');
11467                        return;
11468                    }
11469                    if (TEXT_SKIP_TAGS.has(tag)) {
11470                        return;
11471                    }
11472                    for (let child of node.childNodes) {
11473                        walk(child);
11474                    }
11475                };
11476                for (let child of el.childNodes) {
11477                    walk(child);
11478                }
11479                return parts.join('').replace(/[ \t]+/g, ' ').replace(/\s+\n/g, '\n').replace(/\n\s+/g, '\n').trim();
11480            },
11481
11482            // "Ask" button on the text-edit popover — push a chat
11483            // composer chip referencing this element and pin the panel
11484            // open. The shared AI panel handles attachment +
11485            // formatting; we just describe the element.
11486            send_text_edit_to_ai_chat(target_el, snippet) {
11487                if (typeof ai_mcp_panel_attach_context !== 'function') {
11488                    if (typeof displayMessage === 'function') {
11489                        displayMessage('AI panel is not available right now.', true, 4000);
11490                    }
11491                    return;
11492                }
11493                if (!target_el) {
11494                    return;
11495                }
11496                let tag = (target_el.tagName || 'element').toLowerCase();
11497                let raw_text = typeof snippet === 'string' && snippet.length
11498                    ? snippet
11499                    : this.read_element_text(target_el);
11500                let display = (raw_text || '').replace(/\s+/g, ' ').trim();
11501                let label = display.length > 60 ? display.slice(0, 60).trim() + '…' : display;
11502                if (!label) {
11503                    label = '(empty)';
11504                }
11505                let id = 'ctx_' + Math.random().toString(36).slice(2, 10) + '_' + Date.now().toString(36);
11506                ai_mcp_panel_attach_context({
11507                    component: this,
11508                    context: {
11509                        id:      id,
11510                        kind:    'preview-text',
11511                        tag:     tag,
11512                        text:    raw_text || '',
11513                        label:   label,
11514                        preview: display,
11515                    },
11516                });
11517            },
11518
11519            find_text_edit_link_target(target_el, root) {
11520                if (!target_el || !root || typeof target_el.closest !== 'function') {
11521                    return null;
11522                }
11523                let link_target = target_el.closest('a, button');
11524                if (!link_target || !root.contains(link_target)) {
11525                    return null;
11526                }
11527                if (link_target.hasAttribute && (
11528                    link_target.hasAttribute('data-cs-toggle') ||
11529                    link_target.hasAttribute('data-cs-tab')
11530                )) {
11531                    return null;
11532                }
11533                return link_target;
11534            },
11535            read_text_edit_url(link_target) {
11536                if (!link_target || !link_target.getAttribute) {
11537                    return '';
11538                }
11539                let tag = (link_target.tagName || '').toLowerCase();
11540                if (tag === 'a') {
11541                    return link_target.getAttribute('href') || '';
11542                }
11543                return link_target.getAttribute('data-cs-href') || '';
11544            },
11545            normalize_text_edit_url(value) {
11546                let url = typeof value === 'string' ? value.trim() : '';
11547                if (!url) {
11548                    return '';
11549                }
11550                let lowered = url.toLowerCase();
11551                if (lowered.indexOf('javascript:') === 0) {
11552                    return '';
11553                }
11554                return url;
11555            },
11556            apply_text_edit_url(doc, link_path, url_value) {
11557                if (!doc || !link_path) {
11558                    return false;
11559                }
11560                let target = this.find_text_element_by_path(doc, link_path);
11561                if (!target || target.nodeType !== 1) {
11562                    return false;
11563                }
11564                let url = this.normalize_text_edit_url(url_value);
11565                let tag = (target.tagName || '').toLowerCase();
11566                if (tag === 'a') {
11567                    if (url) {
11568                        target.setAttribute('href', url);
11569                    } else {
11570                        target.removeAttribute('href');
11571                    }
11572                    return true;
11573                }
11574                if (tag !== 'button') {
11575                    return false;
11576                }
11577                if (!url) {
11578                    target.removeAttribute('data-cs-href');
11579                    return true;
11580                }
11581                // A plain <button> cannot navigate without JavaScript,
11582                // and inline JS is stripped by sanitize_custom_code_html.
11583                // For CTA-like buttons, convert to an anchor while
11584                // preserving classes, children, and safe generic attrs.
11585                let owner = doc.ownerDocument || document;
11586                let anchor = owner.createElement('a');
11587                Array.prototype.slice.call(target.attributes || []).forEach((attr) => {
11588                    let name = String(attr.name || '').toLowerCase();
11589                    if (name === 'type' || name === 'disabled' || name.indexOf('form') === 0) {
11590                        return;
11591                    }
11592                    if (name.indexOf('on') === 0 || name === 'data-cs-href') {
11593                        return;
11594                    }
11595                    anchor.setAttribute(attr.name, attr.value);
11596                });
11597                anchor.setAttribute('href', url);
11598                while (target.firstChild) {
11599                    anchor.appendChild(target.firstChild);
11600                }
11601                if (target.parentNode) {
11602                    target.parentNode.replaceChild(anchor, target);
11603                }
11604                return true;
11605            },
11606
11607            // Apply the edit against the parsed source and persist.
11608            // Multi-line input becomes textNode + <br> + textNode + ...
11609            // so it round-trips to the same lines on next read.
11610            commit_text_edit(path, new_value, link_path, url_value) {
11611                if (!path) {
11612                    return;
11613                }
11614                let safe_value = typeof new_value === 'string' ? new_value : '';
11615                let mutated = this.mutate_persisted_html((doc) => {
11616                    let target = this.find_text_element_by_path(doc, path);
11617                    if (!target || target.nodeType !== 1) {
11618                        return false;
11619                    }
11620                    while (target.firstChild) {
11621                        target.removeChild(target.firstChild);
11622                    }
11623                    let lines = safe_value.split('\n');
11624                    let owner = doc.ownerDocument || document;
11625                    lines.forEach((line, index) => {
11626                        if (index > 0) {
11627                            target.appendChild(owner.createElement('br'));
11628                        }
11629                        if (line.length) {
11630                            target.appendChild(owner.createTextNode(line));
11631                        }
11632                    });
11633                    if (link_path) {
11634                        this.apply_text_edit_url(doc, link_path, url_value);
11635                    }
11636                    return true;
11637                });
11638                if (mutated !== null) {
11639                    this.persist_mutated_code(mutated);
11640                }
11641            },
11642
11643            // Called from ai-mcp-panel.html. Pushes AI-generated HTML
11644            // into CodeMirror, persists via save_changes_for_component,
11645            // and returns the prior editor value so the panel can offer
11646            // Undo for auto-applied responses.
11647            //
11648            // Streaming-aware: when we've been driving the editor from
11649            // a live stream, the canonical "previous_code" the panel
11650            // wants is the snapshot we took at begin_streaming_code,
11651            // NOT the streamed-in code currently in the buffer.
11652            apply_generated_code(code, options) {
11653                if (typeof code !== 'string') {
11654                    return null;
11655                }
11656                this.close_text_edit_popover();
11657                let opts = options || {};
11658                if (opts.manual_apply || opts.auto) {
11659                    this.clear_ai_streaming_starting_state();
11660                    this.ai_streaming = false;
11661                    this.release_ai_loading_pane_height_after_render();
11662                    if (opts.manual_apply || this.ai_streaming_status === 'streaming' || this.ai_streaming_status === 'queued') {
11663                        this.ai_streaming_status = 'done';
11664                    }
11665                    this.ai_streaming_buffer = '';
11666                    this._last_flushed_buffer_len = -1;
11667                    this.ai_streaming_child_min_heights = [];
11668                    if (this._codegen_stream_raf) {
11669                        cancelAnimationFrame(this._codegen_stream_raf);
11670                        this._codegen_stream_raf = null;
11671                    }
11672                    this.ai_stream_event_tick = (this.ai_stream_event_tick + 1) | 0;
11673                }
11674                let previous_code;
11675                if (this.ai_streaming_pre_code !== null && this.ai_streaming_pre_code !== undefined) {
11676                    previous_code = this.ai_streaming_pre_code;
11677                    this.ai_streaming_pre_code = null;
11678                } else {
11679                    previous_code = this.get_current_code();
11680                }
11681                let safe_code = this.sanitize_custom_code_html(code);
11682                if (this.cm_instance) {
11683                    this._suppress_change_save = true;
11684                    this.cm_instance.setValue(safe_code);
11685                    this._suppress_change_save = false;
11686                }
11687                this.custom_code_value = safe_code;
11688                this.publish_current_code_to_store();
11689                this.save_ai_applied_custom_code(safe_code);
11690                this.mark_ai_code_revision_changed();
11691                if (opts.switch_to_preview !== false) {
11692                    this.active_tab = 'preview';
11693                }
11694                this.$nextTick(() => this.initialize_custom_code_interactions());
11695                return previous_code;
11696            },
11697            save_ai_applied_custom_code(code) {
11698                if (!this.component) {
11699                    return;
11700                }
11701                let config_latest = this.component.get('config_latest') || this.component.get('config') || {};
11702                let next_config_latest = Object.assign({}, config_latest, { custom_code: code });
11703                let ai_description = this.extract_ai_description_from_code(code);
11704                if (ai_description) {
11705                    next_config_latest.ai_description = ai_description;
11706                }
11707                this.component.set('config_latest', next_config_latest);
11708                if (typeof save_changes_for_component === 'function') {
11709                    save_changes_for_component(this.component, 'custom_code', code);
11710                    return;
11711                }
11712                this.component.set('unsaved_changes', true);
11713                this.component.save().catch((error) => {
11714                    console.warn('[custom-code] AI custom code save failed:', error && error.message);
11715                });
11716            },
11717
11718            // ─── Codegen stream subscription ──────────────────────
11719            //
11720            // Subscribes to store.ai_streams for this component's key.
11721            // The component owns the editor + preview, so it's the
11722            // right place to handle code-side events directly — the
11723            // panel only cares about message text + tool calls.
11724            subscribe_to_codegen_stream() {
11725                if (typeof store === 'undefined' || !store.ai_streams) {
11726                    return;
11727                }
11728                let component_id = this.component && this.component.id;
11729                if (!component_id) {
11730                    return;
11731                }
11732                if (typeof this._codegen_stream_unsubscribe === 'function') {
11733                    try {
11734                        this._codegen_stream_unsubscribe();
11735                    } catch (e) {}
11736                    this._codegen_stream_unsubscribe = null;
11737                }
11738                let key = 'codegen:' + component_id;
11739                let me = this;
11740                this._codegen_stream_unsubscribe = store.ai_streams.subscribe(key, function (event_type, payload) {
11741                    me.handle_codegen_stream_event(event_type, payload);
11742                });
11743                // If we're (re-)subscribing into an already-terminal
11744                // entry, the synchronous `replay` event above won't
11745                // unwind any stuck loading flags on its own (replay
11746                // for status='done' is intentionally lightweight to
11747                // avoid re-touching the editor). Hard-converge here
11748                // so a stale "Generating…" label clears on the next
11749                // mount / panel-open.
11750                let entry = (store.ai_streams.get && store.ai_streams.get(key)) || null;
11751                if (entry && (entry.status === 'done' || entry.status === 'error' || entry.status === 'aborted_local')) {
11752                    let needs_clear = this.ai_streaming
11753                        || this.ai_streaming_status === 'streaming'
11754                        || this.ai_streaming_status === 'queued';
11755                    if (needs_clear) {
11756                        this.force_clear_ai_loading_state();
11757                    }
11758                }
11759            },
11760            handle_codegen_stream_event(event_type, payload) {
11761                let safe = payload || {};
11762                this.ai_stream_event_tick = (this.ai_stream_event_tick + 1) | 0;
11763                switch (event_type) {
11764                    case 'replay':
11765                        // Late subscriber catch-up. If a stream is
11766                        // already in flight we lock the editor + flush
11767                        // the accumulated buffer in one shot so the
11768                        // preview catches up before the next delta.
11769                        if (safe.status === 'streaming' || safe.status === 'queued') {
11770                            this.capture_ai_loading_pane_height();
11771                            this.clear_ai_streaming_starting_state();
11772                            if (safe.response_json_buffer) {
11773                                this._response_json_buffer = '';
11774                                this._response_json_html = '';
11775                                this._append_split_ready = false;
11776                                this._append_head = '';
11777                                this._append_tail = '';
11778                                this.handle_codegen_response_json_delta({ text: safe.response_json_buffer });
11779                            } else if (!this.ai_streaming && safe.code_buffer) {
11780                                this.begin_streaming_code(safe.code_buffer || '');
11781                            } else if (safe.code_buffer) {
11782                                this.ai_streaming_buffer = safe.code_buffer;
11783                                this.schedule_streaming_flush();
11784                            }
11785                            this.ai_streaming_status = safe.status;
11786                        } else if (safe.status === 'done' && safe.code_buffer) {
11787                            // Parent-agent loopback streams are passive: a new
11788                            // section may mount only after the child stream has
11789                            // already settled. Replay the final buffered HTML so
11790                            // the preview catches up even before the page refresh
11791                            // reconciles the persisted component.
11792                            this.clear_ai_streaming_starting_state();
11793                            this.force_clear_ai_loading_state();
11794                            let replay_code = safe.code_buffer || '';
11795                            if (this.cm_instance && replay_code !== this.cm_instance.getValue()) {
11796                                this._suppress_change_save = true;
11797                                this.cm_instance.setValue(replay_code);
11798                                this._suppress_change_save = false;
11799                            }
11800                            this.custom_code_value = replay_code;
11801                            this.publish_current_code_to_store();
11802                            this.$nextTick(() => this.initialize_custom_code_interactions());
11803                        } else if (safe.final && safe.status === 'done') {
11804                            // We mounted right after the standalone stream
11805                            // finished — make sure chrome is clear without
11806                            // touching the canonical editor value.
11807                            this.clear_ai_streaming_starting_state();
11808                        }
11809                        break;
11810                    case 'status':
11811                        this.clear_ai_streaming_starting_state();
11812                        if (safe.status === 'streaming' || safe.status === 'queued') {
11813                            this.capture_ai_loading_pane_height();
11814                        }
11815                        if (safe.status === 'queued') {
11816                            // Show queued chrome without engaging the
11817                            // editor lock — nothing is actually being
11818                            // generated yet.
11819                            this.ai_streaming_status = 'queued';
11820                        }
11821                        if (safe.status === 'aborted_local' && this.ai_streaming) {
11822                            this.cancel_streaming({ initiated_remotely: true });
11823                        }
11824                        break;
11825                    case 'code_delta':
11826                        if (safe.component_id && this.component && safe.component_id !== this.component.id) {
11827                            break;
11828                        }
11829                        this.clear_ai_streaming_starting_state();
11830                        if (typeof safe.text !== 'string' || !safe.text.length) {
11831                            break;
11832                        }
11833                        if (safe.atomic) {
11834                            if (!this.ai_streaming) {
11835                                this.begin_streaming_code(safe.text);
11836                            } else {
11837                                this.ai_streaming_buffer = safe.text;
11838                                this.schedule_streaming_flush();
11839                            }
11840                            break;
11841                        }
11842                        if (!this.ai_streaming) {
11843                            this.begin_streaming_code('');
11844                        }
11845                        this.append_streaming_code(safe.text);
11846                        break;
11847                    case 'response_json_delta':
11848                        this.handle_codegen_response_json_delta(safe);
11849                        break;
11850                    case 'new_section_created':
11851                        if (safe.new_component && this.component && safe.new_component.id !== this.component.id) {
11852                            this.force_clear_ai_loading_state();
11853                        }
11854                        break;
11855                    case 'agent_settle':
11856                        // Parent webbuilder-agent build finished/failed. The
11857                        // live preview already morphed in the full document via
11858                        // the forwarded deltas, so just clear the loading
11859                        // overlay WITHOUT a client save — the agent's
11860                        // authoritative refresh re-pull reconciles the canonical
11861                        // HTML (incl. post-build image-import URL swaps).
11862                        this.clear_ai_streaming_starting_state();
11863                        this.force_clear_ai_loading_state();
11864                        if (safe.error) {
11865                            this.ai_streaming_status = 'error';
11866                        }
11867                        break;
11868                    case 'stream_reset':
11869                        this.clear_ai_streaming_starting_state();
11870                        this.ai_streaming_buffer = '';
11871                        this._response_json_buffer = '';
11872                        this._response_json_html = '';
11873                        this._append_split_ready = false;
11874                        this._append_head = '';
11875                        this._append_tail = '';
11876                        if (this.ai_streaming) {
11877                            let restore_code = (this.ai_streaming_pre_code !== null && this.ai_streaming_pre_code !== undefined)
11878                                ? this.ai_streaming_pre_code
11879                                : this.get_current_code();
11880                            if (this.cm_instance) {
11881                                if (this.cm_instance.getValue() !== restore_code) {
11882                                    this._suppress_change_save = true;
11883                                    this.cm_instance.setValue(restore_code);
11884                                    this._suppress_change_save = false;
11885                                }
11886                                this.cm_instance.setOption('readOnly', false);
11887                            }
11888                            this.custom_code_value = restore_code;
11889                            this.publish_current_code_to_store();
11890                            this.ai_streaming = false;
11891                            this.ai_streaming_status = 'streaming';
11892                        }
11893                        // Wipe the morph preview too — next deltas
11894                        // re-build it from a clean slate.
11895                        let stream_root = this.$refs.custom_code_stream_preview;
11896                        if (stream_root) {
11897                            while (stream_root.firstChild) {
11898                                stream_root.removeChild(stream_root.firstChild);
11899                            }
11900                        }
11901                        break;
11902                    case 'done':
11903                        this.clear_ai_streaming_starting_state();
11904                        if (safe.edit_mode === 'new_section' || safe.edit_mode === 'new_sections') {
11905                            this.finish_streaming_code(null);
11906                            break;
11907                        }
11908                        let final_code = null;
11909                        if (typeof safe.code === 'string') {
11910                            final_code = safe.code;
11911                        } else if (safe.result && typeof safe.result.code === 'string') {
11912                            final_code = safe.result.code;
11913                        }
11914                        this.finish_streaming_code(final_code);
11915                        break;
11916                    case 'error':
11917                        this.clear_ai_streaming_starting_state();
11918                        // Leave the partial buffer in place but flip
11919                        // the overlay so the admin can dismiss.
11920                        this.ai_streaming_status = 'error';
11921                        if (this.ai_streaming) {
11922                            this.snap_streaming_done({ error: true });
11923                        }
11924                        break;
11925                    default:
11926                        break;
11927                }
11928            },
11929            handle_codegen_response_json_delta(payload) {
11930                if (!payload || typeof payload.text !== 'string') {
11931                    return;
11932                }
11933                this._response_json_buffer = (this._response_json_buffer || '') + payload.text;
11934                let tool_name = this.detect_streamed_response_action_name(this._response_json_buffer);
11935                if (tool_name === 'set_html') {
11936                    this.stream_set_html_from_buffer();
11937                    return;
11938                }
11939                if (tool_name === 'append_html') {
11940                    this.stream_append_html_from_buffer();
11941                    return;
11942                }
11943            },
11944            // Live set_html: the streamed `html` field IS the whole section, so
11945            // forward each new tail slice as a code delta.
11946            stream_set_html_from_buffer() {
11947                let extracted = this.extract_streamed_json_string_field(this._response_json_buffer, 'html');
11948                if (!extracted || !extracted.value) {
11949                    return;
11950                }
11951                let previous = this._response_json_html || '';
11952                if (extracted.value.length <= previous.length) {
11953                    return;
11954                }
11955                let delta = extracted.value.slice(previous.length);
11956                this._response_json_html = extracted.value;
11957                this.clear_ai_streaming_starting_state();
11958                if (!this.ai_streaming) {
11959                    this.begin_streaming_code('');
11960                }
11961                this.append_streaming_code(delta);
11962            },
11963            // Live append_html: the streamed `html` field is only the NEW items
11964            // (a fragment). Reconstruct the whole document each delta as
11965            // head + fragment + tail, split at the <!--cs-append-anchor--> so
11966            // the new items render in place while existing items stay stable.
11967            stream_append_html_from_buffer() {
11968                let extracted = this.extract_streamed_json_string_field(this._response_json_buffer, 'html');
11969                if (!extracted) {
11970                    return;
11971                }
11972                let fragment = extracted.value || '';
11973                if (!this._append_split_ready) {
11974                    let code = this.get_current_code() || '';
11975                    let anchor_index = code.indexOf('<!--cs-append-anchor-->');
11976                    if (anchor_index >= 0) {
11977                        this._append_head = code.slice(0, anchor_index);
11978                        this._append_tail = code.slice(anchor_index);
11979                    } else {
11980                        this._append_head = code;
11981                        this._append_tail = '';
11982                    }
11983                    this._append_split_ready = true;
11984                }
11985                this.clear_ai_streaming_starting_state();
11986                if (!this.ai_streaming) {
11987                    this.begin_streaming_code('');
11988                }
11989                // Fragment grows in the MIDDLE, so set the whole buffer (not an
11990                // append) and let the preview morph reconcile against it.
11991                this.ai_streaming_buffer = this._append_head + fragment + this._append_tail;
11992                this.schedule_streaming_flush();
11993            },
11994            detect_streamed_response_action_name(buffer) {
11995                let match = /"name"\s*:\s*"(set_html|patch_html|append_html|create_section|propose_sections)"/.exec(buffer || '');
11996                if (!match) {
11997                    return null;
11998                }
11999                return match[1];
12000            },
12001            extract_streamed_json_string_field(buffer, field_name) {
12002                if (!buffer || !field_name) {
12003                    return null;
12004                }
12005                let target = '"' + field_name + '"';
12006                let key_index = buffer.indexOf(target);
12007                if (key_index === -1) {
12008                    return null;
12009                }
12010                let colon_index = buffer.indexOf(':', key_index + target.length);
12011                if (colon_index === -1) {
12012                    return null;
12013                }
12014                let quote_index = buffer.indexOf('"', colon_index + 1);
12015                if (quote_index === -1) {
12016                    return null;
12017                }
12018                let value = '';
12019                let escaping = false;
12020                let unicode_remaining = 0;
12021                let unicode_buffer = '';
12022                for (let i = quote_index + 1; i < buffer.length; i++) {
12023                    let character = buffer.charAt(i);
12024                    if (unicode_remaining > 0) {
12025                        unicode_buffer += character;
12026                        unicode_remaining--;
12027                        if (unicode_remaining === 0) {
12028                            let code = parseInt(unicode_buffer, 16);
12029                            if (Number.isFinite(code)) {
12030                                value += String.fromCharCode(code);
12031                            }
12032                            unicode_buffer = '';
12033                        }
12034                        continue;
12035                    }
12036                    if (escaping) {
12037                        escaping = false;
12038                        if (character === 'u') {
12039                            unicode_remaining = 4;
12040                            unicode_buffer = '';
12041                        } else {
12042                            value += this.decode_streamed_json_escape(character);
12043                        }
12044                        continue;
12045                    }
12046                    if (character === '\\') {
12047                        escaping = true;
12048                        continue;
12049                    }
12050                    if (character === '"') {
12051                        return {
12052                            value:  value,
12053                            closed: true,
12054                        };
12055                    }
12056                    value += character;
12057                }
12058                return {
12059                    value:  value,
12060                    closed: false,
12061                };
12062            },
12063            decode_streamed_json_escape(character) {
12064                if (character === 'n') {
12065                    return '\n';
12066                }
12067                if (character === 'r') {
12068                    return '\r';
12069                }
12070                if (character === 't') {
12071                    return '\t';
12072                }
12073                if (character === 'b') {
12074                    return '\b';
12075                }
12076                if (character === 'f') {
12077                    return '\f';
12078                }
12079                return character;
12080            },
12081            // Lock the editor, snapshot prior code for Undo, switch to
12082            // preview tab. `initial_buffer` is the accumulated code we
12083            // missed if subscribing mid-stream — it seeds the buffer
12084            // and triggers an immediate flush.
12085            begin_streaming_code(initial_buffer) {
12086                if (this.ai_streaming) {
12087                    return;
12088                }
12089                this.capture_ai_loading_pane_height();
12090                this.clear_ai_streaming_starting_state();
12091                this.ai_streaming = true;
12092                this.ai_streaming_status = 'streaming';
12093                this.ai_streaming_pre_code = this.get_current_code();
12094                this.ai_streaming_buffer = (typeof initial_buffer === 'string') ? initial_buffer : '';
12095                if (this.cm_instance) {
12096                    this.cm_instance.setOption('readOnly', 'nocursor');
12097                }
12098                // Switch to preview so the admin sees the morph render
12099                // by default — most builds are done from preview anyway.
12100                this.active_tab = 'preview';
12101                this.close_text_edit_popover();
12102                if (this.ai_streaming_buffer) {
12103                    this.schedule_streaming_flush();
12104                }
12105            },
12106            append_streaming_code(chunk) {
12107                this.ai_streaming_buffer += chunk;
12108                this.schedule_streaming_flush();
12109            },
12110            // Coalesce many code_delta events into one DOM update per
12111            // animation frame. cheap and keeps streaming responsive
12112            // even on chatty 100-chunk-per-second responses.
12113            schedule_streaming_flush() {
12114                if (this._codegen_stream_raf) {
12115                    return;
12116                }
12117                let me = this;
12118                this._codegen_stream_raf = requestAnimationFrame(function () {
12119                    me._codegen_stream_raf = null;
12120                    me.flush_streaming_panes();
12121                });
12122            },
12123            flush_streaming_panes() {
12124                if (!this.ai_streaming) {
12125                    return;
12126                }
12127                let buffer = this.ai_streaming_buffer || '';
12128                // Cheap stable check — avoids reflushing when the buffer length hasn't changed.
12129                if (this._last_flushed_buffer_len === buffer.length) {
12130                    return;
12131                }
12132                this._last_flushed_buffer_len = buffer.length;
12133                this.flush_streaming_edit_pane(buffer);
12134                this.flush_streaming_preview_pane(buffer);
12135            },
12136            flush_streaming_edit_pane(buffer) {
12137                if (!this.cm_instance) {
12138                    return;
12139                }
12140                // setValue replaces the doc atomically. That's fine
12141                // for streaming because the doc is not user-editable
12142                // (readOnly: 'nocursor') and the buffer is always
12143                // strictly-grown — selection / scroll resets are
12144                // unavoidable but visually acceptable.
12145                let current = this.cm_instance.getValue();
12146                if (current === buffer) {
12147                    return;
12148                }
12149                this._suppress_change_save = true;
12150                this.cm_instance.setValue(buffer);
12151                this._suppress_change_save = false;
12152                this.custom_code_value = buffer;
12153                this.publish_current_code_to_store();
12154            },
12155            // Sequential morph: parse only the stable-prefix (chars up
12156            // through the last '>') of the buffer into a detached
12157            // tree, then walk top-level children in parallel against
12158            // the live preview root. Append-or-replace by index so
12159            // already-rendered sections stay stable while new ones
12160            // stream in below them.
12161            flush_streaming_preview_pane(buffer) {
12162                let root = this.$refs.custom_code_stream_preview;
12163                if (!root) {
12164                    return;
12165                }
12166                let last_gt = buffer.lastIndexOf('>');
12167                if (last_gt < 0) {
12168                    // Nothing parseable yet (we haven't even seen one
12169                    // closing bracket) — defer the flush.
12170                    return;
12171                }
12172                let stable_prefix = buffer.slice(0, last_gt + 1);
12173                // Sanitize first so the morph never plants scripts /
12174                // inline handlers into the live DOM. Same function
12175                // safe_custom_code_value uses for the static pane.
12176                let safe_html = this.sanitize_custom_code_html(stable_prefix);
12177                let template_el = document.createElement('template');
12178                template_el.innerHTML = safe_html;
12179                let new_nodes = template_el.content.childNodes;
12180                let live_nodes = root.childNodes;
12181                let i = 0;
12182                let max = new_nodes.length;
12183                // Track whether this flush appended brand-new top-level
12184                // nodes. If so the section is growing (new HTML in a
12185                // new_section / full build) and the page should follow
12186                // the bottom. If only existing nodes were morphed in
12187                // place (edit / edits flows replacing content) the
12188                // scroll position should stay put.
12189                let appended_new_node = false;
12190                let last_appended_node = null;
12191                while (i < max) {
12192                    let new_node = new_nodes[i];
12193                    let live_node = live_nodes[i];
12194                    let next_node = new_node.cloneNode(true);
12195                    this.apply_streaming_child_min_height(next_node, i);
12196                    if (!live_node) {
12197                        // Append-only path — most common during forward
12198                        // streaming. next_node is cloned because new_node lives
12199                        // in a <template> document fragment we don't
12200                        // want to consume.
12201                        root.appendChild(next_node);
12202                        appended_new_node = true;
12203                        last_appended_node = next_node;
12204                    } else {
12205                        let new_html = (next_node.outerHTML !== undefined)
12206                            ? next_node.outerHTML
12207                            : (next_node.textContent || '');
12208                        let live_html = (live_node.outerHTML !== undefined)
12209                            ? live_node.outerHTML
12210                            : (live_node.textContent || '');
12211                        if (new_html !== live_html) {
12212                            root.replaceChild(next_node, live_node);
12213                        }
12214                    }
12215                    i++;
12216                }
12217                // Trim any leftover live nodes — happens on stream_reset
12218                // (we wipe in the handler so this is rare) and on the
12219                // edge case where the model deletes trailing content.
12220                while (live_nodes.length > max) {
12221                    root.removeChild(live_nodes[live_nodes.length - 1]);
12222                }
12223                this.$nextTick(() => this.initialize_custom_code_interactions());
12224                // Forward-streaming auto-follow: new top-level nodes
12225                // were appended this frame, so the user is watching new
12226                // content land. Ask the panel to smooth-scroll the
12227                // window so the bottom of the newly-appended chunk
12228                // stays in view. Edits/replacements skip this branch
12229                // because their scroll position should be preserved.
12230                if (appended_new_node && last_appended_node) {
12231                    let panel = (typeof store !== 'undefined') ? store.ai_mcp_panel_component_instance : null;
12232                    if (panel && typeof panel.schedule_smooth_scroll_to_streaming_bottom === 'function') {
12233                        let scroll_target = (last_appended_node.nodeType === 1)
12234                            ? last_appended_node
12235                            : root;
12236                        panel.schedule_smooth_scroll_to_streaming_bottom(scroll_target);
12237                    }
12238                }
12239            },
12240            apply_streaming_child_min_height(node, index) {
12241                if (!node || node.nodeType !== 1) {
12242                    return;
12243                }
12244                let min_height = this.ai_streaming_child_min_heights[index] || 0;
12245                if (min_height <= 0) {
12246                    return;
12247                }
12248                node.style.minHeight = min_height + 'px';
12249            },
12250            // Snap streaming closed and run the canonical save. Called
12251            // from the SSE `done` event handler. Idempotent — a
12252            // duplicate `done` (e.g. from a cancel race) is safe.
12253            finish_streaming_code(final_code) {
12254                if (typeof final_code !== 'string' && !(this.ai_streaming_buffer || '').trim()) {
12255                    this.clear_ai_streaming_starting_state();
12256                    this.ai_streaming = false;
12257                    this.release_ai_loading_pane_height_after_render();
12258                    this.ai_streaming_status = 'done';
12259                    this.ai_streaming_buffer = '';
12260                    this._last_flushed_buffer_len = -1;
12261                    this.ai_streaming_child_min_heights = [];
12262                    if (this._codegen_stream_raf) {
12263                        cancelAnimationFrame(this._codegen_stream_raf);
12264                        this._codegen_stream_raf = null;
12265                    }
12266                    if (this.cm_instance) {
12267                        this.cm_instance.setOption('readOnly', false);
12268                    }
12269                    this.ai_stream_event_tick = (this.ai_stream_event_tick + 1) | 0;
12270                    return;
12271                }
12272                if (!this.ai_streaming && (this.ai_streaming_status !== 'done')) {
12273                    // Late `done` for a stream we never engaged with —
12274                    // usually because the panel auto-applied via the
12275                    // separate apply_generated_code path. Still settle
12276                    // local loading UI so the launcher / pending overlay
12277                    // don't get stranded in "Generating…".
12278                    this.clear_ai_streaming_starting_state();
12279                    this.ai_streaming = false;
12280                    this.release_ai_loading_pane_height_after_render();
12281                    this.ai_streaming_status = 'done';
12282                    this.ai_streaming_buffer = '';
12283                    this._last_flushed_buffer_len = -1;
12284                    this.ai_streaming_child_min_heights = [];
12285                    if (this._codegen_stream_raf) {
12286                        cancelAnimationFrame(this._codegen_stream_raf);
12287                        this._codegen_stream_raf = null;
12288                    }
12289                    return;
12290                }
12291                this.snap_streaming_done({ final_code: final_code });
12292            },
12293            snap_streaming_done(options) {
12294                let opts = options || {};
12295                let canonical = (typeof opts.final_code === 'string')
12296                    ? this.sanitize_custom_code_html(opts.final_code)
12297                    : (this.ai_streaming_buffer || '');
12298                // Editor + persistence writes can throw (CodeMirror
12299                // teardown race, save failures, etc). Wrap so the
12300                // loading-state convergence in `finally` still runs
12301                // and the launcher never gets stranded in
12302                // "Generating…".
12303                try {
12304                    if (this.cm_instance) {
12305                        if (this.cm_instance.getValue() !== canonical) {
12306                            this._suppress_change_save = true;
12307                            this.cm_instance.setValue(canonical);
12308                            this._suppress_change_save = false;
12309                        }
12310                        this.cm_instance.setOption('readOnly', false);
12311                    }
12312                    this.custom_code_value = canonical;
12313                    this.publish_current_code_to_store();
12314                    this.save_ai_applied_custom_code(canonical);
12315                    this.mark_ai_code_revision_changed();
12316                } finally {
12317                    this.ai_streaming = false;
12318                    this.release_ai_loading_pane_height_after_render();
12319                    this.ai_streaming_status = opts.canceled ? 'aborted_local' : (opts.error ? 'error' : 'done');
12320                    this.ai_streaming_buffer = '';
12321                    this._last_flushed_buffer_len = -1;
12322                    this.ai_streaming_child_min_heights = [];
12323                    if (this._codegen_stream_raf) {
12324                        cancelAnimationFrame(this._codegen_stream_raf);
12325                        this._codegen_stream_raf = null;
12326                    }
12327                    this.ai_stream_event_tick = (this.ai_stream_event_tick + 1) | 0;
12328                }
12329                // Defer the snapshot teardown — the chat panel's `done`
12330                // subscriber will consume it via apply_generated_code in
12331                // the same JS task. Clearing after 5s guards against
12332                // a stale-message Apply click later misreading it as
12333                // the live editor's previous_code.
12334                if (this._clear_pre_code_timer) {
12335                    clearTimeout(this._clear_pre_code_timer);
12336                }
12337                this._clear_pre_code_timer = setTimeout(() => {
12338                    this.ai_streaming_pre_code = null;
12339                    this._clear_pre_code_timer = null;
12340                }, 5000);
12341                this.$nextTick(() => this.initialize_custom_code_interactions());
12342            },
12343            // Local soft-cancel. Tells the streams manager to stop
12344            // emitting deltas to subscribers (the upstream LLM keeps
12345            // running for billing + persistence), then snaps our
12346            // local view of the editor to whatever we have so far.
12347            cancel_streaming(options) {
12348                let opts = options || {};
12349                if (!opts.initiated_remotely && typeof store !== 'undefined' && store.ai_streams) {
12350                    let component_id = this.component && this.component.id;
12351                    if (component_id) {
12352                        store.ai_streams.cancel_local('codegen:' + component_id);
12353                    }
12354                }
12355                this.snap_streaming_done({ canceled: true });
12356            },
12357            // Echo the current editor code into the shared store so
12358            // the chat panel's apply-state badge can diff against it
12359            // reactively. Setting on `store` directly is safe — the
12360            // panel uses a manual tick for reactivity anyway, and the
12361            // value is also surfaced via a computed in the panel.
12362            publish_current_code_to_store() {
12363                if (typeof store === 'undefined') {
12364                    return;
12365                }
12366                if (this.component) {
12367                    let config_latest = this.component.get('config_latest') || {};
12368                    let next_config_latest = Object.assign({}, config_latest, {
12369                        custom_code: this.custom_code_value || '',
12370                    });
12371                    let ai_description = this.extract_ai_description_from_code(this.custom_code_value || '');
12372                    if (ai_description) {
12373                        next_config_latest.ai_description = ai_description;
12374                    }
12375                    this.component.set('config_latest', next_config_latest);
12376                    // Do NOT bump store.component_updates here. That field
12377                    // drives the `last_update` prop, whose watcher calls
12378                    // back into this method — bumping it would re-trigger
12379                    // the watcher and spin a reactive feedback loop on
12380                    // every edit (runaway re-render → tab hang). The
12381                    // canonical save path (save_changes_for_component)
12382                    // owns the re-render signal; the live preview here
12383                    // re-renders off custom_code_value directly.
12384                    if (this.component.id === store.webbuilder_code_active_component_id) {
12385                        store.webbuilder_code_active_component_revision = Date.now();
12386                    }
12387                }
12388                if (typeof store.webbuilder_code_active_component_id !== 'undefined'
12389                    && this.component
12390                    && this.component.id === store.webbuilder_code_active_component_id) {
12391                    store.webbuilder_code_current_code = this.custom_code_value || '';
12392                }
12393            },
12394            // Mirror this section's active tab (edit/preview) into the shared
12395            // store so the per-section AI launcher (toolbar-option-component)
12396            // can show "Edit with AI" only while the code editor is open.
12397            publish_active_tab_to_store() {
12398                if (typeof store === 'undefined' || !this.preview_element) {
12399                    return;
12400                }
12401                let cid = (this.component && typeof this.component.get === 'function')
12402                    ? this.component.get('client_id')
12403                    : null;
12404                if (!cid) {
12405                    return;
12406                }
12407                if (!store.webbuilder_custom_code_tab_by_client_id) {
12408                    store.webbuilder_custom_code_tab_by_client_id = {};
12409                }
12410                store.webbuilder_custom_code_tab_by_client_id[cid] = this.active_tab;
12411            },
12412            extract_ai_description_from_code(code) {
12413                if (typeof code !== 'string') {
12414                    return '';
12415                }
12416                let description_regex = new RegExp("^\\s*<!--\\s*cs-ai-description:\\s*([\\s\\S]*?)-->", "i");
12417                let description_match = code.match(description_regex);
12418                if (!description_match || !description_match[1]) {
12419                    return '';
12420                }
12421                return description_match[1].replace(/\s+/g, ' ').trim().slice(0, 160);
12422            },
12423        },
12424    });
12425</script>
12425
12426
12427<style>
12428    .cs-custom-code-wrap{
12429        position: relative !important;
12430        isolation: isolate;
12431        z-index: 1;
12432        overflow: visible;
12433    }
12434    /* Overlap masks clip short/non-visual sections (script pixels, etc.). */
12435    .componentPreview > .cs-custom-code-wrap.custom-code-preview-blank{
12436        -webkit-mask-image: none !important;
12437        mask-image: none !important;
12438    }
12439    .cs-custom-code-live-host{
12440        display: block;
12441        width: 100%;
12442    }
12443    .cs-custom-code-wrap.first-page-section{
12444        isolation: auto;
12445        z-index: auto;
12446    }
12447    .cs-custom-code-wrap.active-ai-thread-target{
12448        z-index: 3;
12449    }
12450    .cs-custom-code-wrap.active-ai-thread-target::after{
12451        content: "";
12452        position: absolute;
12453        inset: 0;
12454        z-index: 120;
12455        pointer-events: none;
12456        border: 3px solid rgba(74, 86, 248, 0.62);
12457        box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.75), 0 0 0 4px rgba(74, 86, 248, 0.12);
12458    }
12459
12460    .custom-code-ai-launcher{
12461        position: absolute !important;
12462        left: auto !important;
12463        right: 125px !important;
12464        bottom: calc(20px + var(--cs-overlap-chrome-lift, 0px)) !important;
12465        z-index: 99 !important;
12466        display: inline-flex !important;
12467        align-items: center !important;
12468        justify-content: center !important;
12469        gap: 8px !important;
12470        pointer-events: auto !important;
12471        touch-action: manipulation !important;
12472        width: auto !important;
12473        min-width: 0 !important;
12474        height: auto !important;
12475        min-height: 45px !important;
12476        max-height: none !important;
12477        padding: 0 15px !important;
12478        border-radius: 6px !important;
12479        background: linear-gradient(135deg, #4a56f8 0%, #7b58ff 100%) !important;
12480        color: #fff !important;
12481        border: none !important;
12482        font-family: Roboto, Arial, Helvetica, sans-serif !important;
12483        font-size: 14px !important;
12484        font-weight: 600 !important;
12485        font-style: normal !important;
12486        line-height: 45px !important;
12487        letter-spacing: 0 !important;
12488        text-transform: none !important;
12489        white-space: nowrap !important;
12490        box-shadow: 0 12px 40px rgba(40, 40, 41, 0.05), 0 0 0 1px rgba(0, 0, 0, 0.01) !important;
12491        transition: transform 0.12s ease, box-shadow 0.12s ease !important;
12492    }
12493    .custom-code-ai-launcher.has-active-stream{
12494        box-shadow: 0 12px 40px rgba(74, 86, 248, 0.35), 0 0 0 1px rgba(74, 86, 248, 0.25) !important;
12495    }
12496    .custom-code-ai-launcher-spinner{
12497        display: inline-block;
12498        width: 12px;
12499        height: 12px;
12500        border-radius: 999px;
12501        border: 2px solid rgba(255,255,255,0.4);
12502        border-top-color: #fff;
12503        animation: csCustomCodeStreamSpin 0.9s linear infinite;
12504    }
12505    .custom-code-welcome-generating-spinner{
12506        display: inline-block;
12507        width: 30px;
12508        height: 30px;
12509        border-radius: 999px;
12510        border: 3px solid rgba(74, 86, 248, 0.2);
12511        border-top-color: #4a56f8;
12512        animation: csCustomCodeStreamSpin 0.9s linear infinite;
12513    }
12514    @keyframes csCustomCodeStreamSpin{
12515        from { transform: rotate(0deg); }
12516        to   { transform: rotate(360deg); }
12517    }
12518
12519    .custom-code-tab-bar{
12520        position: relative;
12521        z-index: 101;
12522    }
12523    .cs-custom-code-wrap.preview-mode > .custom-code-preview-pane:not(.custom-code-welcome-pane):not(.custom-code-empty-state-pane):not(.custom-code-loading-pane){
12524        padding: 0;
12525    }
12526
12527    /* Welcome composer (Build a custom section) is builder-chrome,
12528       not user-rendered hosted-site content. Hosted sites can apply
12529       a custom theme font that would otherwise cascade in here and
12530       make the heading / textarea look unbranded. Pin everything
12531       inside this pane to the Clubspot dashboard font so the
12532       builder UI is always consistent across sites. !important
12533       because some host themes set font-family with high
12534       specificity on `body *` selectors. */
12535    .custom-code-welcome-pane,
12536    .custom-code-welcome-pane *:not(i):not([class*="fa-"]),
12537    .custom-code-welcome-pane input,
12538    .custom-code-welcome-pane textarea,
12539    .custom-code-welcome-pane button:not([class*="fa-"]){
12540        font-family: Roboto, Arial, Helvetica, sans-serif !important;
12541    }
12542    /* Re-assert the Font Awesome stack on the icon elements inside
12543       the welcome pane. The wildcard rule above used `!important`
12544       to beat hosted-site themes that set `font-family` with high
12545       specificity on `body *`, which had the side-effect of also
12546       beating Font Awesome's own selectors (.fa-regular, .fa-solid,
12547       etc.) on the icon `<i>` tags — the icon glyphs are unicode
12548       private-use codepoints, so without the FA font-family they
12549       render as empty tofu boxes. Excluding `i` / `[class*="fa-"]`
12550       from the wildcard above lets FA's own font-family win again,
12551       and this explicit fallback covers any host theme that ALSO
12552       pinned font-family on `i` or `.fa-*` with high specificity. */
12553    .custom-code-welcome-pane i[class*="fa-"]{
12554        font-family: "Font Awesome 6 Pro", "Font Awesome 6 Free", "Font Awesome 5 Pro", "Font Awesome 5 Free", "FontAwesome" !important;
12555    }
12556
12557    /* Example-prompt suggestions on the welcome composer. The
12558       ai-mcp-panel.html sheet defines a heavier .ai-composer-pill
12559       look intended for the dashboard chat composer, but that
12560       file doesn't load on hosted-sites pages. Style here is
12561       intentionally low-key — dotted border + normal-weight
12562       muted text — so the suggestions read as inline hints
12563       rather than competing call-to-action buttons. Hover
12564       firms up the border + tints the surface to confirm
12565       they're clickable. */
12566    .cs-custom-code-wrap .ai-composer-pill{
12567        display: inline-flex;
12568        align-items: center;
12569        max-width: 100%;
12570        padding: 5px 11px;
12571        border: 1px dotted rgba(8, 19, 51, 0.18);
12572        border-radius: 8px;
12573        background: transparent;
12574        color: #7280a0;
12575        font-size: 12.5px;
12576        line-height: 18px;
12577        font-weight: 400;
12578        letter-spacing: 0;
12579        text-align: left;
12580        white-space: normal;
12581        transition: border-color 0.12s ease, background 0.12s ease, color 0.12s ease;
12582    }
12583    .cs-custom-code-wrap .ai-composer-pill:hover{
12584        border-color: rgba(74, 86, 248, 0.45);
12585        background: rgba(74, 86, 248, 0.04);
12586        color: #4a56f8;
12587    }
12588    .cs-custom-code-wrap .ai-composer-pill:focus-visible{
12589        outline: 2px solid rgba(74, 86, 248, 0.35);
12590        outline-offset: 1px;
12591    }
12592
12593    /* Send button on the welcome composer. The ai-mcp-panel.html
12594       sheet defines an .ai-submit-button rule too, but that file
12595       isn't loaded on hosted-sites pages — so we redeclare it
12596       locally to guarantee the blue pill renders here.
12597       - flex centering keeps the paper-plane glyph on the
12598         button's optical center
12599       - paper-plane has visual weight on the right tip, so a
12600         1px left nudge looks balanced */
12601    .cs-custom-code-wrap .ai-submit-button{
12602        flex: 0 0 auto;
12603        width: 36px;
12604        min-width: 36px;
12605        height: 36px;
12606        padding: 0;
12607        margin: 0;
12608        border: 0;
12609        border-radius: 999px;
12610        background: #4a56f8;
12611        color: #fff;
12612        display: inline-flex;
12613        align-items: center;
12614        justify-content: center;
12615        line-height: 1;
12616        box-shadow: 0 2px 6px rgba(74, 86, 248, 0.30);
12617        cursor: pointer;
12618        transition: background 0.12s ease, box-shadow 0.12s ease, transform 0.12s ease;
12619    }
12620    .cs-custom-code-wrap .ai-submit-button:hover{
12621        background: #3a45e0;
12622        box-shadow: 0 4px 12px rgba(74, 86, 248, 0.40);
12623        transform: translateY(-1px);
12624    }
12625    .cs-custom-code-wrap .ai-submit-button .fa-paper-plane{
12626        font-size: 14px;
12627        line-height: 1;
12628        transform: translateX(-1px);
12629    }
12630    .cs-custom-code-wrap .ai-submit-button.inactive,
12631    .cs-custom-code-wrap .ai-submit-button[disabled]{
12632        background: #c4cbe2;
12633        color: #fff;
12634        box-shadow: none;
12635        cursor: not-allowed;
12636        transform: none;
12637    }
12638    .cs-custom-code-wrap .ai-submit-button.loading{
12639        background: #4a56f8;
12640        color: #fff;
12641        cursor: progress;
12642    }
12643
12644    /* Pre-first-delta loading pane. This is intentionally a real
12645       preview pane, not an overlay: while waiting for the first code
12646       chunk, the preview window shows only the dots. The chat panel is
12647       already open for context/progress, so avoid duplicating labels or
12648       exposing the stale welcome/current preview underneath. */
12649    .custom-code-loading-pane{
12650        display: flex;
12651        align-items: center;
12652        justify-content: center;
12653        min-height: 420px;
12654        padding: 48px 20px;
12655        background: #fbfcff;
12656    }
12657    .custom-code-loading-pane.is-overlay{
12658        position: absolute !important;
12659        inset: 0 !important;
12660        z-index: 80 !important;
12661        min-height: 0 !important;
12662        padding: 0 !important;
12663        background: rgba(251, 252, 255, 0.62) !important;
12664        backdrop-filter: blur(1px);
12665        pointer-events: none;
12666    }
12667    .custom-code-empty-state-pane{
12668        display: flex;
12669        align-items: center;
12670        justify-content: center;
12671        min-height: 420px;
12672        padding: 48px 20px;
12673        background: #fbfcff;
12674    }
12675    .custom-code-nonvisual-pane{
12676        min-height: 88px;
12677        padding: 20px 16px;
12678    }
12679    .custom-code-empty-state-copy{
12680        display: inline-flex;
12681        align-items: center;
12682        justify-content: center;
12683        flex-wrap: wrap;
12684        gap: 8px;
12685        max-width: 100%;
12686        text-align: center;
12687        color: #9aa5ba;
12688        font-family: Roboto, Arial, Helvetica, sans-serif !important;
12689        font-size: 14px;
12690        font-style: normal;
12691        line-height: 20px;
12692        font-weight: 500;
12693    }
12694    .custom-code-empty-state-copy i{
12695        font-size: 14px;
12696        color: #b0bad0;
12697    }
12698    .custom-code-pending-dots{
12699        display: inline-flex;
12700        align-items: center;
12701        gap: 8px;
12702    }
12703    .custom-code-pending-dots span{
12704        display: inline-block;
12705        width: 10px;
12706        height: 10px;
12707        border-radius: 50%;
12708        background: #4a56f8;
12709        opacity: 0.35;
12710        animation: csCustomCodePendingPulse 1.2s ease-in-out infinite both;
12711    }
12712    .custom-code-pending-dots span:nth-child(2){ animation-delay: 0.15s; }
12713    .custom-code-pending-dots span:nth-child(3){ animation-delay: 0.30s; }
12714    @keyframes csCustomCodePendingPulse{
12715        0%, 80%, 100% { transform: scale(0.6); opacity: 0.35; }
12716        40%           { transform: scale(1.0); opacity: 1.0;  }
12717    }
12718
12719    .custom-code-ai-launcher:hover{
12720        transform: translateY(-1px) !important;
12721        box-shadow: 0 3px 8px rgba(74, 86, 248, 0.34) !important;
12722    }
12723
12724    .custom-code-ai-launcher .fa-sparkles{
12725        display: inline-block !important;
12726        flex: 0 0 auto !important;
12727        font-family: "Font Awesome 6 Pro", "Font Awesome 6 Free", "Font Awesome 5 Pro", "Font Awesome 5 Free", "FontAwesome" !important;
12728        font-size: 14px !important;
12729        line-height: 1 !important;
12730        margin: 0 7px 0 0 !important;
12731    }
12732
12733    .custom-code-ai-launcher-label{
12734        display: inline-block !important;
12735        flex: 0 0 auto !important;
12736        font-family: Roboto, Arial, Helvetica, sans-serif !important;
12737        font-style: normal !important;
12738        letter-spacing: 0 !important;
12739        text-transform: none !important;
12740    }
12741
12742    /*
12743       Placeholder pick/upload/paste overlay. Only rendered in PREVIEW
12744       mode by `decorate_asset_placeholders` — the live (public) site
12745       never sees this class because the overlay element is injected
12746       into the rendered preview DOM and is never persisted back to
12747       the `custom_code` source.
12748    */
12749    .cs-asset-placeholder-host{
12750        position: relative;
12751    }
12752    .custom-code-preview-pane .cs-asset-background-placeholder-host:hover{
12753        outline: 2px dashed rgba(74, 86, 248, 0.55) !important;
12754        outline-offset: -4px !important;
12755    }
12756    .custom-code-preview-pane .cs-asset-background-placeholder-chip{
12757        position: absolute !important;
12758        top: 10px !important;
12759        right: 10px !important;
12760        z-index: 10000 !important;
12761        display: inline-flex !important;
12762        align-items: center !important;
12763        justify-content: center !important;
12764        width: 34px !important;
12765        height: 34px !important;
12766        padding: 0 !important;
12767        margin: 0 !important;
12768        border: 1px solid rgba(74, 86, 248, 0.45) !important;
12769        border-radius: 999px !important;
12770        background: rgba(255, 255, 255, 0.88) !important;
12771        color: #4a56f8 !important;
12772        box-shadow: 0 6px 18px rgba(8, 19, 51, 0.14) !important;
12773        cursor: pointer !important;
12774    }
12775    .custom-code-preview-pane .cs-asset-background-placeholder-chip:hover{
12776        background: #ffffff !important;
12777        border-color: rgba(74, 86, 248, 0.85) !important;
12778    }
12779    .custom-code-preview-pane .cs-asset-placeholder-overlay{
12780        position: absolute !important;
12781        inset: 0 !important;
12782        z-index: 125 !important;
12783        display: flex !important;
12784        flex-direction: column !important;
12785        align-items: center !important;
12786        justify-content: center !important;
12787        gap: 4px !important;
12788        margin: 0 !important;
12789        padding: 12px !important;
12790        border: 2px dashed rgba(74, 86, 248, 0.55) !important;
12791        border-radius: 8px !important;
12792        background: rgba(74, 86, 248, 0.08) !important;
12793        color: #2a2f5a !important;
12794        font-family: Roboto, Arial, Helvetica, sans-serif !important;
12795        font-size: 13px !important;
12796        font-style: normal !important;
12797        font-weight: 500 !important;
12798        line-height: 1.25 !important;
12799        letter-spacing: 0 !important;
12800        text-align: center !important;
12801        white-space: normal !important;
12802        cursor: pointer !important;
12803        backdrop-filter: blur(2px) !important;
12804        transition: background 0.12s ease, border-color 0.12s ease !important;
12805    }
12806    .custom-code-preview-pane .cs-asset-placeholder-overlay:hover{
12807        background: rgba(74, 86, 248, 0.16) !important;
12808        border-color: rgba(74, 86, 248, 0.85) !important;
12809    }
12810    .custom-code-preview-pane .cs-asset-placeholder-icon{
12811        display: inline-flex !important;
12812        align-items: center !important;
12813        justify-content: center !important;
12814        width: 28px !important;
12815        height: 28px !important;
12816        flex: 0 0 28px !important;
12817        margin-bottom: 4px !important;
12818        border-radius: 50% !important;
12819        background: rgba(255, 255, 255, 0.65) !important;
12820        color: #4a56f8 !important;
12821        font-size: 14px !important;
12822        font-style: normal !important;
12823        line-height: 1 !important;
12824        letter-spacing: 0 !important;
12825    }
12826    .custom-code-preview-pane .cs-asset-placeholder-label{
12827        display: inline-block !important;
12828        padding: 4px 10px !important;
12829        border-radius: 999px !important;
12830        background: rgba(255, 255, 255, 0.92) !important;
12831        color: #2a2f5a !important;
12832        font-family: Roboto, Arial, Helvetica, sans-serif !important;
12833        font-size: 13px !important;
12834        font-style: normal !important;
12835        font-weight: 600 !important;
12836        line-height: 1.2 !important;
12837        letter-spacing: 0 !important;
12838        min-width: 118px !important;
12839        max-width: 180px !important;
12840        white-space: normal !important;
12841    }
12842
12843    /*
12844       "Change" overlay for filled placeholder slots
12845       (`[data-cs-asset-attached]`). Light-DOM fallback only — preview
12846       chrome for the isolated shadow tree is in
12847       CS_CUSTOM_CODE_PREVIEW_CHROME_CSS. Always visible, matching
12848       image-with-text / image-grid slot chips.
12849    */
12850    .cs-asset-attached-host{
12851        position: relative;
12852    }
12853    .custom-code-preview-pane .cs-asset-change-button{
12854        position: absolute !important;
12855        top: 8px !important;
12856        right: 8px !important;
12857        z-index: 10000 !important;
12858        display: inline-flex !important;
12859        align-items: center !important;
12860        justify-content: center !important;
12861        padding: 6px 10px !important;
12862        margin: 0 !important;
12863        border: none !important;
12864        border-radius: 6px !important;
12865        background: rgba(20, 24, 45, 0.82) !important;
12866        color: #fff !important;
12867        font-family: Roboto, Arial, Helvetica, sans-serif !important;
12868        font-size: 12px !important;
12869        font-weight: 600 !important;
12870        line-height: 1 !important;
12871        cursor: pointer !important;
12872        opacity: 0.92 !important;
12873        pointer-events: auto !important;
12874        box-shadow: 0 4px 12px rgba(8, 19, 51, 0.22) !important;
12875        transition: opacity 0.12s ease, background 0.12s ease !important;
12876    }
12877    .custom-code-preview-pane .cs-asset-change-button:hover,
12878    .custom-code-preview-pane .cs-asset-change-button:focus{
12879        opacity: 1 !important;
12880        background: rgba(20, 24, 45, 0.95) !important;
12881    }
12882    .custom-code-preview-pane .cs-ai-asset-hover-highlight{
12883        outline: 2px solid rgba(74, 86, 248, 0.32) !important;
12884        outline-offset: 4px !important;
12885        box-shadow: 0 0 0 6px rgba(74, 86, 248, 0.08) !important;
12886        transition: outline-color 0.12s ease, box-shadow 0.12s ease !important;
12887    }
12888
12889    /*
12890       Click-to-edit text leaves. Only active in PREVIEW mode — the
12891       host class is added by `decorate_editable_text_nodes` after
12892       every preview re-render and never persisted to source. The
12893       hover affordance is a soft dashed outline with cursor:pointer
12894       so admins can tell the text is editable without the section
12895       feeling like a form.
12896    */
12897    .custom-code-preview-pane .cs-text-editable-host{
12898        cursor: text !important;
12899        position: relative !important;
12900        transition: outline-color 0.12s ease, background-color 0.12s ease !important;
12901        outline: 1px dashed transparent !important;
12902        outline-offset: 2px !important;
12903        border-radius: 3px !important;
12904    }
12905    .custom-code-preview-pane .cs-text-editable-host:hover{
12906        outline-color: rgba(74, 86, 248, 0.55) !important;
12907        background-color: rgba(74, 86, 248, 0.06) !important;
12908    }
12909    .custom-code-preview-pane .cs-text-editable-host.cs-text-editable-active{
12910        outline: 1px solid rgba(74, 86, 248, 0.85) !important;
12911        background-color: rgba(74, 86, 248, 0.12) !important;
12912    }
12913
12914    /*
12915       Inline text-edit popover. Lives at document.body level (fixed
12916       positioning) so it isn't clipped by overflow:hidden ancestors
12917       in the preview pane. Closed on outside click, ESC, scroll, or
12918       a Vue re-render of the preview.
12919    */
12920    .cs-text-edit-popover{
12921        position: fixed !important;
12922        z-index: 10000 !important;
12923        box-sizing: border-box !important;
12924        max-width: 92vw !important;
12925        padding: 10px !important;
12926        background: #fff !important;
12927        border: 1px solid rgba(20, 24, 45, 0.12) !important;
12928        border-radius: 8px !important;
12929        box-shadow: 0 18px 48px rgba(20, 24, 45, 0.18), 0 2px 6px rgba(20, 24, 45, 0.08) !important;
12930        font-family: inherit !important;
12931        font-size: 13px !important;
12932        color: #2a2f5a !important;
12933    }
12934    .cs-text-edit-popover-header{
12935        display: block !important;
12936        margin-bottom: 6px !important;
12937        font-size: 11px !important;
12938        font-weight: 600 !important;
12939        text-transform: uppercase !important;
12940        letter-spacing: 0.04em !important;
12941        color: #4a4f6a !important;
12942    }
12943    .cs-text-edit-popover .cs-text-edit-textarea{
12944        display: block !important;
12945        box-sizing: border-box !important;
12946        width: 100% !important;
12947        min-height: 60px !important;
12948        max-height: 240px !important;
12949        padding: 8px 10px !important;
12950        margin: 0 !important;
12951        border: 1px solid rgba(20, 24, 45, 0.18) !important;
12952        border-radius: 6px !important;
12953        background: #fff !important;
12954        color: #14182d !important;
12955        font-family: inherit !important;
12956        font-size: 13px !important;
12957        line-height: 1.4 !important;
12958        resize: vertical !important;
12959        outline: none !important;
12960        box-shadow: none !important;
12961    }
12962    .cs-text-edit-popover .cs-text-edit-textarea:focus{
12963        border-color: rgba(74, 86, 248, 0.7) !important;
12964        box-shadow: 0 0 0 3px rgba(74, 86, 248, 0.15) !important;
12965    }
12966    .cs-text-edit-popover .cs-text-edit-url-field{
12967        display: block !important;
12968        margin: 8px 0 0 0 !important;
12969    }
12970    .cs-text-edit-popover .cs-text-edit-url-label{
12971        display: block !important;
12972        margin: 0 0 4px 0 !important;
12973        color: #4a4f6a !important;
12974        font-size: 11px !important;
12975        font-weight: 600 !important;
12976        line-height: 14px !important;
12977    }
12978    .cs-text-edit-popover .cs-text-edit-url-input{
12979        display: block !important;
12980        box-sizing: border-box !important;
12981        width: 100% !important;
12982        height: 34px !important;
12983        padding: 7px 10px !important;
12984        margin: 0 !important;
12985        border: 1px solid rgba(20, 24, 45, 0.18) !important;
12986        border-radius: 6px !important;
12987        background: #fff !important;
12988        color: #14182d !important;
12989        font-family: inherit !important;
12990        font-size: 13px !important;
12991        line-height: 18px !important;
12992        outline: none !important;
12993        box-shadow: none !important;
12994    }
12995    .cs-text-edit-popover .cs-text-edit-url-input:focus{
12996        border-color: rgba(74, 86, 248, 0.7) !important;
12997        box-shadow: 0 0 0 3px rgba(74, 86, 248, 0.15) !important;
12998    }
12999    .cs-text-edit-popover-footer{
13000        display: flex !important;
13001        align-items: center !important;
13002        gap: 6px !important;
13003        margin-top: 8px !important;
13004    }
13005    .cs-text-edit-popover-footer-spacer{
13006        flex: 1 1 auto !important;
13007    }
13008    .cs-text-edit-popover-footer button{
13009        appearance: none !important;
13010        -webkit-appearance: none !important;
13011        display: inline-flex !important;
13012        align-items: center !important;
13013        justify-content: center !important;
13014        padding: 6px 12px !important;
13015        margin: 0 !important;
13016        border-radius: 6px !important;
13017        border: 1px solid transparent !important;
13018        font-family: inherit !important;
13019        font-size: 12px !important;
13020        font-weight: 600 !important;
13021        line-height: 1 !important;
13022        cursor: pointer !important;
13023        transition: background 0.12s ease, transform 0.12s ease, border-color 0.12s ease, color 0.12s ease !important;
13024    }
13025    .cs-text-edit-popover .cs-text-edit-ask{
13026        padding: 6px 8px !important;
13027        background: transparent !important;
13028        border-color: transparent !important;
13029        color: #4a56f8 !important;
13030        gap: 6px !important;
13031    }
13032    .cs-text-edit-popover .cs-text-edit-ask:hover{
13033        background: rgba(74, 86, 248, 0.08) !important;
13034        color: #3b46d6 !important;
13035    }
13036    .cs-text-edit-popover .cs-text-edit-ask .fa-sparkles{
13037        font-size: 12px !important;
13038    }
13039    .cs-text-edit-popover .cs-text-edit-cancel{
13040        background: #fff !important;
13041        border-radius: 999px !important;
13042        border-color: rgba(20, 24, 45, 0.18) !important;
13043        color: #4a4f6a !important;
13044    }
13045    .cs-text-edit-popover .cs-text-edit-cancel:hover{
13046        background: rgba(20, 24, 45, 0.04) !important;
13047    }
13048    .cs-text-edit-popover .cs-text-edit-save{
13049        background: linear-gradient(135deg, #4a56f8 0%, #7b58ff 100%) !important;
13050        border-radius: 999px !important;
13051        color: #fff !important;
13052        border-color: transparent !important;
13053    }
13054    .cs-text-edit-popover .cs-text-edit-save:hover{
13055        transform: translateY(-1px) !important;
13056    }
13057</style>
13058    
13058<script type="text/html" id="template-text">
13059    <div 
13060        v-memo="[last_update, last_element_update]"
13061        :class="[{'hidden': (column_index + 1) > number_of_columns }, 'text-column_' + column_index]"
13062        class="cs-text_text-wrap-column cs-element-stack" 
13063        :data-column="column_index"
13064    >
13065        <template
13066            v-for="(element, element_index) in nice_elements"
13067            :key="element.client_id || element_index"
13068        >
13069            <template v-if="preview_element">
13070                <preview-element-component
13071                    :element="element"
13072                    :element_type="element.type"
13073                    :element_content="element.content"
13074                    :element_column_count="element.column_count"
13075                    :element_column_gap="element.column_gap"
13076                    :column_index="column_index"
13077                    :element_index="element_index"
13078                    :componentObject="component"
13079                ></preview-element-component>
13080            </template>
13081            <template v-if="!preview_element">
13082                <element-component
13083                    :element_type="element.type"
13084                    :element_content="element.content"
13085                    :element_column_count="element.column_count"
13086                    :element_column_gap="element.column_gap"
13087                    :diff_changed="element._wb_diff_changed === true"
13088                    :element_index="element_index"
13089                    :section_element_animation="section_element_animation"
13090                ></element-component>
13091            </template>
13092        </template>
13093        <!-- handle case where there are no elements (admin only) -->
13094        <template v-if="preview_element && elements && elements.length == 0">
13095            <div class="no-elements-wrap">
13096                <div 
13097                    class="tapped-insert-new-element-relative" 
13098                    @click="tapped_add_element( $event )"
13099                >+</div>
13100            </div>
13101        </template>
13102    </div>
13103</script>
13103
13104
13105<script>
13106    app_content.component('text-component', {
13107        template: '#template-text',
13108        data() {
13109            return {}
13110        },
13111        props: {
13112            number_of_columns: [String, Number],
13113            preview_element: Boolean,
13114            component: Object,
13115            last_update: Number, //last component update
13116            last_element_update: Number, //last element update
13117            elements: Array,
13118            column_prop: String,
13119            column_index: Number,
13120            section_element_animation: {
13121                type: String,
13122                default: '',
13123            },
13124        },
13125        methods:{
13126            tapped_add_element(){
13127                var params = {};
13128                params["component"] = this.component;
13129                params["insert_after"] = 0;
13130                params["column_index"] = this.column_index;
13131                handle_add_element( params );
13132            }
13133        },
13134        computed: {
13135            nice_elements(){
13136                var nice_elements = [];
13137
13138                if (this.elements){
13139                    nice_elements = this.elements;
13140                } else if (this.column_prop && typeof this.column_prop === 'string'){
13141                    nice_elements = JSON.parse(unescape(decodeURIComponent(this.column_prop)));
13142                } 
13143                return nice_elements;
13144            }
13145        },
13146    });
13147</script>
13147    
13147<script type="text/html" id="template-image-with-text">
13148    <div ref="animationTarget" class="cs-image-with-text flexWrap relative delay5s" :class="[class_string, { 'scroll-fade': !is_admin && !tab_element }, { 'view_hidden': ! is_admin && ( !formatting_obj.animation || formatting_obj.animation !== 'none' ) }, { 'background': image_mat === 'hide' }]" :style="style_string">
13149        <template v-if="!loading && left_right_orientation === 'right'">
13150            <!-- text first (image on the right) -->
13151            <div class="cs-image-with-text_text-wrap flexGrowOne">
13152                <div :style="element_style_string">
13153                    <div v-memo="[last_update, last_element_update]" class="element-wrapper cs-element-stack">
13154                        <template
13155                            v-for="(element, element_index) in elements_obj"
13156                            :key="element.client_id || element_index"
13157                        >
13158                            <template v-if="preview_element">
13159                                <preview-element-component
13160                                    :element="element"
13161                                    :element_type="element.type"
13162                                    :element_content="element.content"
13163                                    :element_column_count="element.column_count"
13164                                    :element_column_gap="element.column_gap"
13165                                    :element_index="element_index"
13166                                    :componentObject="component"
13167                                ></preview-element-component>
13168                            </template>
13169                            <template v-if="!preview_element">
13170                                <element-component
13171                                    :element_type="element.type"
13172                                    :element_content="element.content"
13173                                    :element_column_count="element.column_count"
13174                                    :element_column_gap="element.column_gap"
13175                                    :diff_changed="element._wb_diff_changed === true"
13176                                    :element_index="element_index"
13177                                    :section_element_animation="section_element_animation"
13178                                ></element-component>
13179                            </template>
13180                        </template>
13181                        <!-- handle case where there are no elements (admin only) -->
13182                        <template v-if="preview_element && elements_obj && elements_obj.length == 0">
13183                            <div class="no-elements-wrap">
13184                                <div 
13185                                    class="tapped-insert-new-element-relative" 
13186                                    @click="tapped_add_element( $event )"
13187                                >+</div>
13188                            </div>
13189                        </template>
13190                    </div>
13191                </div>
13192            </div>
13193            <div class="cs-image-with-text_image-wrap flexGrowOne">
13194                <div class="cs-image-with-text_image-wrap-inner">
13195                    <div :style="check_full_background()" class="cs-image-with-text_image-slider-outer">
13196                        <div 
13197                            v-if="formatting_obj.background_size !== 'full'" 
13198                            :class="['cs-image-with-text-image-mat', { background: image_mat === 'show' }]"
13199                        ></div>
13200                        <image-slider-component
13201                            :client_id="client_id"
13202                            :formatting="formatting"
13203                            :images_string="images_string"
13204                            :preview_element="preview_element"
13205                            :component="component"
13206                            :last_update="last_update"
13207                        ></image-slider-component>
13208                    </div>
13209                </div>
13210            </div>
13211        </template>
13212        <template v-if="!loading && left_right_orientation === 'left'">
13213            <!-- image first (text on the right) -->
13214            <div class="cs-image-with-text_image-wrap flexGrowOne">
13215                <div class="cs-image-with-text_image-wrap-inner">
13216                    <div :style="check_full_background()" class="cs-image-with-text_image-slider-outer">
13217                        <div 
13218                            v-if="formatting_obj.background_size !== 'full'" 
13219                            :class="['cs-image-with-text-image-mat', { background: image_mat === 'show' }]"
13220                        ></div>
13221                        <image-slider-component
13222                            :client_id="client_id"
13223                            :formatting="formatting"
13224                            :images_string="images_string"
13225                            :preview_element="preview_element"
13226                            :component="component"
13227                            :last_update="last_update"
13228                        ></image-slider-component>
13229                    </div>
13230                </div>
13231            </div>
13232            <div class="cs-image-with-text_text-wrap flexGrowOne">
13233                <div :style="element_style_string">
13234                    <div 
13235                        v-memo="[last_update, last_element_update]" 
13236                        class="element-wrapper cs-element-stack"
13237                    >
13238                        <template
13239                            v-for="(element, element_index) in elements_obj"
13240                            :key="element.client_id || element_index"
13241                        >
13242                            <template v-if="preview_element">
13243                                <preview-element-component
13244                                    :element="element"
13245                                    :element_type="element.type"
13246                                    :element_content="element.content"
13247                                    :element_column_count="element.column_count"
13248                                    :element_column_gap="element.column_gap"
13249                                    :element_index="element_index"
13250                                    :componentObject="component"
13251                                ></preview-element-component>
13252                            </template>
13253                            <template v-if="!preview_element">
13254                                <element-component
13255                                    :element_type="element.type"
13256                                    :element_content="element.content"
13257                                    :element_column_count="element.column_count"
13258                                    :element_column_gap="element.column_gap"
13259                                    :diff_changed="element._wb_diff_changed === true"
13260                                    :element_index="element_index"
13261                                    :section_element_animation="section_element_animation"
13262                                ></element-component>
13263                            </template>
13264                        </template>
13265                        <!-- handle case where there are no elements (admin only) -->
13266                        <template v-if="preview_element && elements_obj && elements_obj.length == 0">
13267                            <div class="no-elements-wrap">
13268                                <div 
13269                                    class="tapped-insert-new-element-relative" 
13270                                    @click="tapped_add_element( $event )"
13271                                >+</div>
13272                            </div>
13273                        </template>
13274                    </div>
13275                </div>
13276            </div>
13277        </template>
13278    </div>
13279</script>
13279
13280
13281<style>
13282    .delay5s{
13283        animation-delay: 0.5s;
13284    }
13285    
13286    .view_hidden{
13287        opacity: 0;
13288    }
13289
13290    @keyframes fade-left-right {
13291        0% {
13292            opacity: 0;
13293            transform: translateX(-100%) scaleX(0.5);
13294        }
13295        100% {
13296            opacity: 1;
13297            transform: translateX(0) scaleX(1);
13298        }
13299    }
13300
13301    @keyframes fade-right-left {
13302        0% {
13303            opacity: 0;
13304            transform: translateX(100%) scaleX(0.5);
13305        }
13306        100% {
13307            opacity: 1;
13308            transform: translateX(0) scaleX(1);
13309        }
13310    }
13311
13312    .fade-left-right {
13313        animation: fade-left-right 1s ease-in-out forwards;
13314    }
13315
13316    .fade-right-left {
13317        animation: fade-right-left 1s ease-in-out backwards;
13318    }
13319</style>
13320
13321<script>
13322    app_content.component('image-with-text-component', {
13323        template: '#template-image-with-text',
13324        data() {
13325            return {
13326                is_admin: false,
13327                loading: true,
13328            }
13329        },
13330        props: {
13331            class_string: String,
13332            style_string: String,
13333            element_style_string: String,
13334            elements_string: [String, Object],
13335            left_right_orientation: String,
13336            image_mat: String,
13337            //image slider
13338            client_id: String,
13339            formatting: String,
13340            images_string: String,
13341            //admin
13342            preview_element: Boolean,
13343            component: Object,
13344            last_update: Number, //last component update
13345            last_element_update: Number, //last element update
13346            tab_element: Boolean,
13347        },
13348        mounted() {
13349            this.loading = false;
13350            this.is_admin = window.location.href.indexOf('website-builder') !== -1;
13351        },
13352        updated(){
13353            this.createIntersectionObserver();
13354        },
13355        methods: {
13356            check_full_background(){
13357                if ( this.formatting_obj && this.formatting_obj.background_size === 'full' ){
13358                    return {
13359                        top: 0,
13360                        bottom: 0,
13361                        left: 0,
13362                        right: 0,
13363                    }
13364                }
13365            },
13366            tapped_add_element(){
13367                var params = {};
13368                params["component"] = this.component;
13369                params["insert_after"] = 0;
13370                params["column_index"] = 0;
13371                handle_add_element( params );
13372            },
13373            createIntersectionObserver(){
13374                const options = {
13375                    root: null, 
13376                    rootMargin: '0px',
13377                    threshold: 0.5 //trigger animation when at least 50% of the target is visible
13378                };
13379                const observer = new IntersectionObserver(this.handleIntersection, options);
13380                //observe the target element
13381                observer.observe(this.$refs.animationTarget);
13382            },
13383            handleIntersection(entries, observer) {
13384                entries.forEach(entry => {
13385                    //check if target is intersecting
13386                    if (entry.isIntersecting) {
13387                        //apply animation class
13388                        entry.target.classList.add(this.formatting_obj.animation);
13389                        entry.target.classList.remove('view_hidden');
13390                        //stop observing to prevent re-triggering
13391                        observer.unobserve(entry.target);
13392                    }
13393                });
13394            },
13395        },
13396        computed: {
13397            elements_obj(){
13398                if (this.elements_string && typeof this.elements_string === 'string'){
13399                    return JSON.parse(unescape(decodeURIComponent(this.elements_string)));
13400                } else if (this.elements_string){
13401                    return this.elements_string;
13402                } else {
13403                    return {};
13404                }
13405            },
13406            formatting_obj(){
13407                if (this.formatting){
13408                    return JSON.parse(this.formatting);
13409                } else {
13410                    return {};
13411                }
13412            },
13413            section_element_animation(){
13414                let component_animation = resolve_web_builder_component_animation(this.formatting_obj);
13415                return section_element_animation_token(component_animation);
13416            },
13417        },
13418    });
13419</script>
13419    
13419<script type="text/html" id="template-image-slider">
13420    <div class="cs-image-slider-wrap background">
13421        <div 
13422            class="cs-image-slider"
13423            :class="[ 'cs-image-slider-' + filtered_images_length, 'cs-image-slider_' + client_id ]"
13424            :data-client-id="client_id" 
13425            data-interval="8000" 
13426            data-slider-index="0"
13427        >
13428            <!-- now add the images -->
13429            <template v-if="!loading && filtered_images_length > 0" v-for="(image, index) in filtered_images">
13430                <template v-if="image && typeof image !== 'object'">
13431                    <template v-if="image && image.indexOf('video') >= 1">
13432                        <div
13433                            class="cs-image-slider-card"
13434                            :class="[
13435                                'cs-image-slider-card-' + index,
13436                                { 'active': index === display_index },
13437                                { 'cs-image-slot-attached-host': preview_element && !is_hero_banner }
13438                            ]"
13439                            style="display:flex;align-items:center;justify-content:center;"
13440                            @click.stop="preview_element && !is_hero_banner && change_image_slot(index)"
13441                        >
13442                            <video class="cs-background-video" autoplay playsinline loop muted :src="image" style="display:initial;"></video>
13443                            <button
13444                                v-if="preview_element && !is_hero_banner"
13445                                type="button"
13446                                class="cs-image-slot-change-chip exclude-from-custom-button-styles"
13447                                @click.stop="change_image_slot(index)"
13448                            ><i class="fa-regular fa-image"></i><span>Change</span></button>
13449                        </div>
13450                    </template>
13451                    <template v-if="image && typeof image === 'string'">
13452                        <div
13453                            class="cs-image-slider-card"
13454                            :class="[
13455                                'cs-image-slider-card-' + index,
13456                                { 'active': index === display_index },
13457                                { 'cs-image-slot-attached-host': preview_element && !is_hero_banner }
13458                            ]"
13459                            @click.stop="preview_element && !is_hero_banner && change_image_slot(index)"
13460                        >
13461                            <div class="cs-background-image-photo" :style="`background-image: url(${image})`"></div>
13462                            <button
13463                                v-if="preview_element && !is_hero_banner"
13464                                type="button"
13465                                class="cs-image-slot-change-chip exclude-from-custom-button-styles"
13466                                @click.stop="change_image_slot(index)"
13467                            ><i class="fa-regular fa-image"></i><span>Change</span></button>
13468                        </div>
13469                    </template>
13470                </template>
13471                <template v-if="is_image_with_text && !image">
13472                    <div 
13473                        class="cs-image-slider-card"
13474                        :class="[
13475                            'cs-image-slider-card-' + index,
13476                            { 'active': index === display_index },
13477                            { 'cs-image-slot-placeholder-host': preview_element }
13478                        ]"
13479                        @click.stop="fill_image_slot(index)"
13480                    >
13481                        <div v-if="preview_element" class="cs-image-slot-placeholder">
13482                            <span class="cs-image-slot-placeholder-icon"><i class="fa-regular fa-image"></i></span>
13483                            <span class="cs-image-slot-placeholder-label">Add image</span>
13484                        </div>
13485                        <div v-else class="cs-background-image-photo default-image-icon" style="background-size:30%!important;"></div>
13486                    </div>
13487                </template>
13488            </template>
13489            <template v-for="(layer, layer_index) in overlay_layers" :key="'overlay-layer-' + layer_index + '-' + (layer.class_string || layer.kind || '')">
13490                <div
13491                    class="cs-image-overlay"
13492                    :class="(layer.class_string || '').trim().split(/\s+/).filter(
13492Boolean)"
13493                    :style="layer.style_obj"
13494                ></div>
13495            </template>
13496        </div>
13497    </div>
13498</script>
13498
13499
13500<style>
13501    /* Preview-only "Change" chip for hero-banner / image-with-text slots. */
13502    .cs-image-slot-attached-host {
13503        cursor: pointer;
13504    }
13505    .cs-image-slot-change-chip {
13506        position: absolute;
13507        top: 12px;
13508        right: 12px;
13509        z-index: 2000;
13510        display: inline-flex;
13511        align-items: center;
13512        gap: 5px;
13513        padding: 6px 10px;
13514        border: none;
13515        border-radius: 6px;
13516        background: rgba(20, 24, 45, 0.82);
13517        color: #fff;
13518        font-family: 'Roboto', Arial, Helvetica, sans-serif;
13519        font-size: 12px;
13520        font-weight: 600;
13521        font-style: normal;
13522        line-height: 1;
13523        letter-spacing: 0;
13524        text-transform: none;
13525        cursor: pointer;
13526        opacity: 0.92;
13527        pointer-events: auto;
13528        box-shadow: 0 4px 12px rgba(8, 19, 51, 0.22);
13529        transition: opacity 0.12s ease, background 0.12s ease;
13530    }
13531    .cs-image-slot-attached-host:hover .cs-image-slot-change-chip,
13532    .cs-image-slot-change-chip:hover,
13533    .cs-image-slot-change-chip:focus {
13534        opacity: 1;
13535        background: rgba(20, 24, 45, 0.95);
13536    }
13537</style>
13538
13539<script>    
13540    /* ----- COMPONENT ----- */
13541    app_content.component('image-slider-component', {
13542        template: '#template-image-slider',
13543        data() {
13544            return {
13545                loading: true,
13546                display_index: 0,
13547            }
13548        },
13549        props: {
13550            client_id: String,
13551            formatting: String,
13552            images_string: String,
13553            //for preview element
13554            preview_element: {
13555                type: Boolean,
13556                default: false,
13557            },
13558            component: {
13559                type: Object,
13560                default: null,
13561            },
13562            last_update: {
13563                type: Number,
13564                default: null,
13565            },
13566        },
13567        mounted() {
13568            this.loading = false;
13569            this.sync_display_index_with_config_and_images();
13570        },
13571        methods: {
13572            parsed_images(){
13573                if( !this.images_string ){
13574                    return [];
13575                }
13576                try {
13577                    let parsed = JSON.parse(this.images_string);
13578                    return Array.isArray(parsed) ? parsed : [];
13579                } catch (error) {
13580                    console.error('Error parsing image slider images: ', error);
13581                    return [];
13582                }
13583            },
13584            // image-with-text keeps an empty slot in preview for the Add-image card.
13585            // Hero has no image-layer fill — empty state is the section color_scheme.
13586            preview_images_with_slots(){
13587                let parsed = this.parsed_images();
13588                if( !this.preview_element || (!this.is_hero_banner && !this.is_image_with_text) ){
13589                    return parsed;
13590                }
13591                if( this.is_hero_banner ){
13592                    return parsed.filter((image) => {
13593                        if( image ){
13594                            return true;
13595                        }
13596                        return false;
13597                    });
13598                }
13599                if( !parsed.length ){
13600                    return [null];
13601                }
13602                return parsed;
13603            },
13604            return_first_filled_index( images ){
13605                if( !Array.isArray(images) || images.length === 0 ){
13606                    return 0;
13607                }
13608                for( let i = 0; i < images.length; i++ ){
13609                    if( images[i] ){
13610                        return i;
13611                    }
13612                }
13613                return 0;
13614            },
13615            sync_display_index_with_config_and_images(){
13616                let config_latest = this.component ? (this.component.get('config_latest') || {}) : {};
13617                let saved_display_index = Number(config_latest.display_index);
13618                let has_saved_display_index = config_latest.display_index === 0 || !isNaN(saved_display_index);
13619                let fallback_index = this.display_index;
13620                if( this.preview_element && this.is_image_with_text ){
13621                    fallback_index = this.return_first_filled_index(this.parsed_images());
13622                }
13623                let target_index = has_saved_display_index ? saved_display_index : fallback_index;
13624                let max_index = this.filtered_images_length > 0 ? this.filtered_images_length - 1 : 0;
13625                this.display_index = Math.min(Math.max(target_index, 0), max_index);
13626            },
13627            // Write `value` (a URL string or null) into slot `index`, preserving
13628            // the fixed 5-slot string|null shape the settings panel expects.
13629            write_image_slot( index, value ){
13630                let component = this.component;
13631                if( !component ){
13632                    return;
13633                }
13634                let current = (component.get('config_latest') && component.get('config_latest').images) || [];
13635                let next = [];
13636                for( let i = 0; i <= 4; i++ ){
13637                    if( i === index ){
13638                        next.push( value );
13639                    } else if( current[i] ){
13640                        next.push( current[i] );
13641                    } else {
13642                        next.push( null );
13643                    }
13644                }
13645                this.display_index = index;
13646                save_changes_for_component( component, 'images', next );
13647                save_changes_for_component( component, 'display_index', index );
13648            },
13649            // Empty-slot picker for image-with-text / hero-banner.
13650            fill_image_slot( index ){
13651                if( !this.preview_element ){
13652                    return;
13653                }
13654                if( typeof webbuilder_open_image_slot_picker !== 'function' ){
13655                    if( typeof displayMessage === 'function' ){
13656                        displayMessage('Image picker is not loaded. Refresh the page and try again.', true, 5000);
13657                    }
13658                    return;
13659                }
13660                if( !this.component ){
13661                    return;
13662                }
13663                webbuilder_open_image_slot_picker({
13664                    label: 'image',
13665                    mode: 'placeholder',
13666                    on_pick: ( url ) => {
13667                        if( !url ){
13668                            return;
13669                        }
13670                        this.write_image_slot( index, url );
13671                    },
13672                });
13673            },
13674            // Filled-slot "Change" action — swap or remove the image in slot
13675            // `index` via the shared picker (pick/paste/copy-external/remove),
13676            // matching custom-code's attached-asset menu.
13677            change_image_slot( index ){
13678                if( !this.preview_element ){
13679                    return;
13680                }
13681                if( typeof webbuilder_open_image_slot_picker !== 'function' ){
13682                    if( typeof displayMessage === 'function' ){
13683                        displayMessage('Image picker is not loaded. Refresh the page and try again.', true, 5000);
13684                    }
13685                    return;
13686                }
13687                let component = this.component;
13688                if( !component ){
13689                    return;
13690                }
13691                let current = (component.get('config_latest') && component.get('config_latest').images) || [];
13692                let current_url = typeof current[index] === 'string' ? current[index] : '';
13693                webbuilder_open_image_slot_picker({
13694                    label: 'image',
13695                    mode: 'attached',
13696                    current_url: current_url,
13697                    on_pick: ( url ) => {
13698                        if( !url ){
13699                            return;
13700                        }
13701                        this.write_image_slot( index, url );
13702                    },
13703                    on_remove: () => {
13704                        this.write_image_slot( index, null );
13705                    },
13706                });
13707            },
13708        },
13709        created(){
13710            setInterval(() => {
13711                if (!this.preview_element){
13712                    if (this.display_index < this.filtered_images_length - 1){
13713                        this.display_index++;
13714                    } else {
13715                        this.display_index = 0;
13716                    }
13717                }
13718            }, 8000);
13719        },
13720        computed: {
13721            component_type(){
13722                if( this.component && typeof this.component.get === 'function' ){
13723                    return this.component.get('type');
13724                }
13725                return null;
13726            },
13727            is_hero_banner(){
13728                return this.component_type === 'hero-banner';
13729            },
13730            is_image_with_text(){
13731                return this.component_type === 'image-with-text';
13732            },
13733            formatting_obj(){
13734                if (this.formatting){
13735                    return JSON.parse(this.formatting);
13736                }
13737            },
13738            overlay_layers(){
13739                let formatting = this.formatting_obj || {};
13740                if( typeof build_web_builder_overlay_layers === 'function' ){
13741                    return build_web_builder_overlay_layers(formatting).layers || [];
13742                }
13743                if( typeof build_web_builder_overlay_attrs === 'function' ){
13744                    let attrs = build_web_builder_overlay_attrs(formatting);
13745                    return attrs.has_overlay ? [{ class_string: attrs.class_string, style_obj: attrs.style_obj }] : [];
13746                }
13747                let opacity = Number(formatting.overlay_opacity) || 0;
13748                if( opacity > 0 ){
13749                    return [{ class_string: '', style_obj: { 'opacity': opacity / 100 } }];
13750                }
13751                return [];
13752            },
13753            filtered_images(){
13754                if (!this.preview_element){
13755                    return this.parsed_images().filter(image => {
13756                        if ( image !== null ){
13757                            return image;
13758                        }
13759                    });
13760                } else {
13761                    return this.preview_images_with_slots();
13762                }
13763                return [];
13764            },
13765            filtered_images_length(){
13766                if (!this.preview_element){
13767                    return this.parsed_images().filter(image => {
13768                        if ( image !== null ){
13769                            return image;
13770                        }
13771                    }).length;
13772                } else {
13773                    return this.preview_images_with_slots().length;
13774                }
13775                return 0;
13776            },
13777        },
13778        watch: {
13779            last_update(newVal, oldVal){
13780                if (newVal !== oldVal){
13781                    this.sync_display_index_with_config_and_images();
13782                }
13783            },
13784        }
13785    });
13786</script>
13786    
13786<script type="text/html" id="template-hero-banner">
13787    <div class="cs-background-image background relative" :class="[class_string, { 'scroll-fade': !is_admin && !tab_element }]" :style="style_string">
13788        <image-slider-component
13789            :client_id="client_id"
13790            :formatting="formatting"
13791            :images_string="images_string"
13792            :preview_element="preview_element"
13793            :component="component"
13794            :last_update="last_update"
13795        ></image-slider-component>
13796        <div 
13797            class="element-wrapper" 
13798            :class="'element-wrapper_' + component_type" 
13799            :style="element_style_string"
13800        >
13801            <div v-memo="[last_update, last_element_update]" class="cs-element-stack">
13802                <!-- loop through elements -->
13803                <template
13804                    v-for="(element, element_index) in elements_obj"
13805                    :key="element.client_id || element_index"
13806                >
13807                    <template v-if="preview_element">
13808                        <preview-element-component
13809                            :element="element"
13810                            :element_type="element.type"
13811                            :element_content="element.content"
13812                            :element_column_count="element.column_count"
13813                            :element_column_gap="element.column_gap"
13814                            :element_index="element_index"
13815                            :componentObject="component"
13816                        ></preview-element-component>
13817                    </template>
13818                    <template v-if="!preview_element">
13819                        <element-component
13820                            :element_type="element.type"
13821                            :element_content="element.content"
13822                            :element_column_count="element.column_count"
13823                            :element_column_gap="element.column_gap"
13824                            :diff_changed="element._wb_diff_changed === true"
13825                            :element_index="element_index"
13826                            :section_element_animation="section_element_animation"
13827                        ></element-component>
13828                    </template>
13829                </template>
13830                <!-- handle case where there are no elements (admin only) -->
13831                <template v-if="preview_element && elements_obj.length == 0">
13832                    <div class="no-elements-wrap">
13833                        <div 
13834                            class="tapped-insert-new-element-relative" 
13835                            @click="tapped_add_element( $event )"
13836                        >+</div>
13837                    </div>
13838                </template>
13839            </div>
13840        </div>
13841    </div>
13842</script>
13842
13843
13844<script>
13845    app_content.component('hero-banner-component', {
13846        template: '#template-hero-banner',
13847        data() {
13848            return {
13849                is_admin: false,
13850                loading: true,
13851            }
13852        },
13853        props: {
13854            class_string: String,
13855            style_string: String,
13856            element_style_string: String,
13857            component_type: String,
13858            elements_string: [String, Object],
13859            //image slider
13860            client_id: String,
13861            formatting: String,
13862            images_string: String,
13863            //admin
13864            preview_element: Boolean,
13865            component: Object,
13866            last_update: Number, //last component update
13867            last_element_update: Number, //last element update
13868            tab_element: Boolean,
13869        },
13870        mounted() {
13871            this.loading = false;
13872            this.is_admin = window.location.href.indexOf('website-builder') !== -1;
13873        },
13874        methods:{
13875            tapped_add_element(){
13876                var params = {};
13877                params["component"] = this.component;
13878                params["insert_after"] = 0;
13879                params["column_index"] = 0;
13880                handle_add_element( params );
13881            },
13882            parsed_hero_images(){
13883                if( !this.images_string ){
13884                    return [];
13885                }
13886                try {
13887                    let parsed = JSON.parse(this.images_string);
13888                    return Array.isArray(parsed) ? parsed : [];
13889                } catch (error) {
13890                    console.error('Error parsing hero banner images: ', error);
13891                    return [];
13892                }
13893            },
13894            // Prefer the first filled slot so multi-image heroes edit the visible one.
13895            hero_image_slot_index(){
13896                let images = this.parsed_hero_images();
13897                for( let i = 0; i < images.length; i++ ){
13898                    if( images[i] ){
13899                        return i;
13900                    }
13901                }
13902                return 0;
13903            },
13904            write_hero_image_slot( index, value ){
13905                let component = this.component;
13906                if( !component ){
13907                    return;
13908                }
13909                let current = (component.get('config_latest') && component.get('config_latest').images) || [];
13910                let next = [];
13911                for( let i = 0; i <= 4; i++ ){
13912                    if( i === index ){
13913                        next.push( value );
13914                    } else if( current[i] ){
13915                        next.push( current[i] );
13916                    } else {
13917                        next.push( null );
13918                    }
13919                }
13920                save_changes_for_component( component, 'images', next );
13921            },
13922            tapped_change_hero_image(){
13923                if( !this.preview_element ){
13924                    return;
13925                }
13926                if( typeof webbuilder_open_image_slot_picker !== 'function' ){
13927                    if( typeof displayMessage === 'function' ){
13928                        displayMessage('Image picker is not loaded. Refresh the page and try again.', true, 5000);
13929                    }
13930                    return;
13931                }
13932                if( !this.component ){
13933                    return;
13934                }
13935                let index = this.hero_image_slot_index();
13936                let current = (this.component.get('config_latest') && this.component.get('config_latest').images) || [];
13937                let current_url = typeof current[index] === 'string' ? current[index] : '';
13938                if( current_url ){
13939                    webbuilder_open_image_slot_picker({
13940                        label: 'image',
13941                        mode: 'attached',
13942                        current_url: current_url,
13943                        on_pick: ( url ) => {
13944                            if( !url ){
13945                                return;
13946                            }
13947                            this.write_hero_image_slot( index, url );
13948                        },
13949                        on_remove: () => {
13950                            this.write_hero_image_slot( index, null );
13951                        },
13952                    });
13953                    return;
13954                }
13955                webbuilder_open_image_slot_picker({
13956                    label: 'image',
13957                    mode: 'placeholder',
13958                    on_pick: ( url ) => {
13959                        if( !url ){
13960                            return;
13961                        }
13962                        this.write_hero_image_slot( index, url );
13963                    },
13964                });
13965            },
13966        },
13967        computed: {
13968            elements_obj(){
13969                if (this.elements_string && typeof this.elements_string === 'string'){
13970                    return JSON.parse(unescape(decodeURIComponent(this.elements_string)))
13971                } else {
13972                    return this.elements_string
13973                }
13974                return [];
13975            },
13976            formatting_obj(){
13977                if( this.formatting && typeof this.formatting === 'string' ){
13978                    try {
13979                        return JSON.parse(this.formatting);
13980                    } catch (error) {
13981                        return {};
13982                    }
13983                }
13984                if( this.formatting && typeof this.formatting === 'object' ){
13985                    return this.formatting;
13986                }
13987                return {};
13988            },
13989            section_element_animation(){
13990                let component_animation = resolve_web_builder_component_animation(this.formatting_obj);
13991                return section_element_animation_token(component_animation);
13992            },
13993        },
13994    });
13995</script>
13995    
13995<script type="text/html" id="template-element">
13996    <template v-if="element_type === 'accordion_header'">
13997        <div
13998            class="cs-accordion-header"
13999            :class="[element_anim_class, { 'wb-diff-element-changed': diff_changed }]"
14000            :style="element_anim_inline_style"
14001        >
14002            <div class="cs-accordion-header-toggle-wrap flexWrap">
14003                <div class="cs-accordion-header-toggle">+</div>
14004            </div>
14005            <span v-html="element_content"></span>
14006        </div>
14007    </template>
14008    <template v-if="element_type === 'accordion_body'">
14009        <div 
14010            class="cs-accordion-body"
14011            :class="[element_anim_class, { 'wb-diff-element-changed': diff_changed }]"
14012            v-html="element_content"
14013            :style="element_body_inline_style"
14014        ></div>
14015    </template>
14016    <template v-else-if="element_type !== 'accordion_header' && element_type !== 'accordion_body'">
14017        <div
14018            :class="[element_anim_class, { 'wb-diff-element-changed': diff_changed }]"
14019            :style="element_body_inline_style"
14020        >
14021            <span v-html="element_content"></span>
14022        </div>
14023    </template>
14024</script>
14024
14025
14026<script>
14027    app_content.component('element-component', {
14028        template: '#template-element',
14029        data() {
14030            return {
14031                loading: true,
14032            }
14033        },
14034        props: {
14035            element_type: String,
14036            element_content: String,
14037            element_column_count: Number,
14038            element_column_gap: Number,
14039            diff_changed: Boolean,
14040            element_index: {
14041                type: [Number, String],
14042                default: 0,
14043            },
14044            section_element_animation: {
14045                type: String,
14046                default: '',
14047            },
14048        },
14049        mounted() {
14050            this.loading = false;
14051        },
14052        computed: {
14053            uses_section_element_animation(){
14054                return this.section_element_animation === 'up'
14055                    || this.section_element_animation === 'fade_up'
14056                    || this.section_element_animation === 'fade_in';
14057            },
14058            element_anim_class(){
14059                if( this.uses_section_element_animation ){
14060                    return 'cs-el-anim-target';
14061                }
14062                return '';
14063            },
14064            el_index_style_fragment(){
14065                if( this.uses_section_element_animation ){
14066                    return '--el-index:' + ( Number(this.element_index) || 0 ) + ';';
14067                }
14068                return '';
14069            },
14070            element_anim_inline_style(){
14071                return this.el_index_style_fragment;
14072            },
14073            element_body_inline_style(){
14074                return 'column-count: ' + this.element_column_count + ' !important; column-gap: ' + this.element_column_gap + 'em;' + this.el_index_style_fragment;
14075            },
14076        },
14077    });
14078</script>
14078
14079    
14079<script type="text/html" id="template-unlayer-component">    
14080    <template v-if="preview_element">
14081        <div class="cs-unlayer-component-wrap unlayer-content" :class="class_string">
14082            <!-- hover button to open unlayer builder -->
14083            <div class="darken-overlay easeFast">
14084               <div class="hover-toolbar">          
14085                   <div class="hover-button flexNoWrap centeredBlock pointer" @click="openUnlayerBuilder()">
14086                       <div class="hover-button-text">
14087                           Update <i class="fa-regular fa-pen-to-square" style="padding-left:3px;"></i>
14088                       </div>
14089                   </div>
14090               </div>
14091           </div>
14092            <!-- if there is content -->
14093            <div v-if="!loading && unlayerHtml" class="relative" >
14094                <div 
14095                    :id="'freeform_id_' + component.id"  
14096                    v-html="return_html_with_css()"
14097                ></div>
14098            </div>
14099            <!-- if there is no content -->
14100            <div v-else-if="!loading">
14101                <div class="addFreeformContentZone" @click="openUnlayerBuilder()">
14102                    <div 
14103                        class="quick-action-wrap noMaxWidth centeredBlock"
14104                    >
14105                        <div class="quick-action-icon-wrap">
14106                            <i class="fa-light fa-scribble"></i>
14107                        </div>
14108                        <p class="quick-action-title" style="font-family:roboto;">Freeform section</p>
14109                    </div>
14110                </div>
14111            </div>
14112        </div>
14113    </template>
14114    <template v-if="!preview_element">
14115        <div class="cs-unlayer-component-wrap">
14116            <div class="relative"></div>
14117            <div v-if="!loading" v-html="return_html_with_css()"></div>
14118        </div>
14119    </template>
14120</script>
14120
14121
14122<script>
14123
14124    function createDebounce() {
14125        let timeout = null;
14126        return function (fnc, delayMs) {
14127            clearTimeout(timeout);
14128            timeout = setTimeout(() => {
14129                fnc();
14130            }, delayMs || 800);
14131        };
14132    }
14133    app_content.component('unlayer-component', {
14134        template: '#template-unlayer-component',
14135        data() {
14136            return {
14137                unlayerHtml: null,
14138                loading: true,
14139                debounce: createDebounce(),
14140                template: null,
14141                unlayer: '',
14142            }
14143        },
14144        props: {
14145            unlayer_chunks: Object,
14146            class_string: String,
14147            preview_element: {
14148                type: Boolean,
14149                default: false,
14150            },
14151            component: {
14152                type: Object,
14153                default: null,
14154            },
14155        },
14156        mounted() {
14157            if(!this.unlayer_chunks){       
14158                // find template for component id
14159                this.return_unlayer_data()
14160            }
14161            this.loading = false;
14162        },
14163        methods: {
14164            return_unlayer_data(){
14165                let config_latest = this.component.get('config_latest') || {};
14166                if(!config_latest.unlayer_component || !config_latest.unlayer_component.chunks){
14167                    return;
14168                }
14169                this.unlayerHtml = config_latest.unlayer_component.chunks.body || '';
14170                this.unlayerHtml = this.unlayerHtml.replace('style="min-height: 100vh;', 'style="min-height: auto;');
14171            },
14172            openUnlayerBuilder(){
14173                let jsonContent;
14174                let config_latest = this.component.get('config_latest') || {};
14175                if(config_latest.unlayer_component){
14176                    jsonContent = config_latest.unlayer_component.design;
14177                }
14178                let params = {
14179                    jsonContent: jsonContent,
14180                    fn: this.saveUnlayerContent
14181                }
14182                launch_unlayer_builder(params)
14183            },
14184            saveUnlayerContent(content){
14185                let promise = Promise.resolve().then(() => {
14186                    let htmlContent = content && content.chunks ? (content.chunks.body || '') : '';
14187                    htmlContent = htmlContent.replace('style="min-height: 100vh;', 'style="min-height: auto;');
14188                    this.unlayerHtml = htmlContent;
14189                    save_changes_for_component( this.component, 'unlayer_component', content );
14190                })
14191                return promise;
14192            }, 
14193            return_html_with_css(){
14194                let data;
14195                if(this.preview_element){
14196                    let config_latest = this.component.get('config_latest') || {};
14197                    if(!config_latest.unlayer_component || !config_latest.unlayer_component.chunks){
14198                        return '';
14199                    }
14200                    data = config_latest.unlayer_component.chunks;
14201                }else{
14202                    data = this.unlayer_chunks;
14203                }
14204                if(!data){
14205                    return '';
14206                }
14207                let css = data.css || '';
14208                let body = data.body || '';
14209                let html_with_css = juice.inlineContent(body, css);
14210                html_with_css = html_with_css.replace('100vh', 'auto')
14211                return html_with_css
14212            }        
14213        },
14214        computed: {
14215              
14216        }
14217    });
14218</script>
14218
14219
14220<style scoped>
14221
14222    @media screen and (max-width: 768px) {
14223        .u-row:not(.no-stack) .u-col:not(:only-child) {
14224            flex: 0 0 50% !important;
14225            max-width: 50% !important;
14226        }
14227    }
14228    @media screen and (max-width: 480px) {
14229        .u-col {
14230            flex: 0 0 100% !important; /* One column per row */
14231            max-width: 100% !important;
14232        }
14233    }
14234
14235</style>    
14235<script type="text/html" id="template-form">
14236    <div class="form-wrapper centeredBlock centeredText">
14237        <template v-if="!loading && is_admin">
14238            <div
14239                v-if="!form_id"
14240                @click="link_form()"
14241                class="noObjectsMSG centeredBlock centeredText"
14242                style="padding: 5em;"
14243            >
14244                <p style="cursor: pointer;"><i class="fa-light fa-list-dropdown smallMarginRight"></i>Click to link a form</p>
14245            </div>
14246        </template>
14247        <template v-if="!loading">
14248            <div 
14249                v-if="form_id && json_form" 
14250                id="form-template"
14251                class="cs-min-section-width"
14252                :style="{ 'max-width': formatting_obj.section_width + '%' }"
14253            >
14254                <div
14255                    class="form-color-theme"
14256                    :style="{ 'background-color': color_theme }">
14257                </div>
14258                <div class="form-header-wrapper">
14259                    <!-- name -->
14260                    <div v-if="json_form && json_form.name" class="form-name flex-row-start">
14261                        <h3 class="default-font-css" style="font-size: 32px; padding: 5px; text-align: left;">{{ json_form.name }}</h3>
14262                    </div>
14263                    <!-- description -->
14264                    <div v-if="json_form && json_form.description" class="form-description flex-row-start tinyMarginTop">
14265                        <p class="default-font-css" style="padding: 5px; text-align: left;">{{ json_form.description }}</p>
14266                    </div>
14267                    <!-- disabled notice -->
14268                    <div v-if="form_disabled" class="form-description flex-row-start tinyMarginTop">
14269                        <p class="extra-copy-v2" style="padding: 5px; text-align: left;">This form is currently disabled and not accepting responses.</p>
14270                    </div>
14271                    <template v-if="!valid_membership">
14272                        <div
14273                            @click="tapped_log_in()"
14274                            class="noObjectsMSG leftText"
14275                            style="padding: 5px; min-width: fit-content;"
14276                        >
14277                            <p style="cursor: pointer;"><i class="fa-light fa-circle-exclamation smallMarginRight"></i>This form is restricted to members only. Please <a :href="login_url">click here to log in</a> and view this form.</p>
14278                        </div>
14279                    </template>
14280                </div>
14281
14282                <template v-if="valid_membership && !pending_redirect_url">
14283
14284                    <!-- default fields -->
14285                    <template v-if="collect_default_fields && selected_fields.length > 0">
14286                        <default-field-component-wb
14287                            :form="json_form"
14288                            :is_admin="is_admin"
14289                            :default_fields="selected_fields"
14290                            :rerender_key="rerender_key"
14291                        ></default-field-component-wb>
14292                    </template>
14293                    <!-- custom fields -->
14294                    <template v-if="json_form.custom_fields" v-for="(custom_field, index) in custom_fields">
14295                        <custom-field-component-wb
14296                            :id="custom_field.id"
14297                            :form="json_form"
14298                            :is_admin="is_admin"
14299                            :custom_field="custom_field"
14300                            :last_update="last_update"
14301                            ref="custom_field_components"
14302                            @field-updated="handle_field_update"
14303                        ></custom-field-component-wb>
14304                    </template>
14305                    <!-- submit -->
14306                    <div style="width: 100%; display: flex; justify-content: space-between; align-items: center;" class="tinyMarginTop">
14307                        <button 
14308                            v-if="!loading_submission"
14309                            @click="tapped_submit_form()"
14310                            :style="{ 'background-color': button_disabled ? 'var(--custom-field-grey-2)' : color_theme }"
14311                            :class="[ 'default-font-css', 'submit-button', { 'inactive': button_disabled } ]"
14312                            :disabled="is_admin"
14313                        >Submit</button>
14314                        <i 
14315                            v-if="loading_submission"
14316                            class="fa-sharp-duotone fa-solid fa-spinner-third fa-spin smallMarginLeft"
14317                            :style="{ 'color': color_theme, 'font-size': '1.2em', 'animation-duration': '1.2s' }"
14318                        ></i>
14319                        <p 
14320                            @click="!loading_submission && tapped_clear_form()"
14321                            :style="{ 'color': loading_submission ?  'var(--custom-field-grey-2)': color_theme }"
14322                            style="font-size: 14px; font-weight: 600; cursor: pointer;"
14323                        >Clear form</p>
14324                    </div>
14325                </template>
14326                <template v-if="valid_membership && pending_redirect_url">
14327                    <div class="centeredText" style="padding: 2em 5px;">
14328                        <i
14329                            class="fa-solid fa-circle-check solid-circle-check"
14330                            style="font-size: 48px;"
14331                        ></i>
14332                        <h3 class="default-font-css smallMarginTop">Response submitted</h3>
14333                        <p class="extra-copy-v2 tinyMarginTop">Redirecting...</p>
14334                    </div>
14335                </template>
14336            </div>
14337            <div
14338                v-if="form_id && !json_form" 
14339                class="noObjectsMSG centeredBlock centeredText"
14340                style="padding: 5em;"
14341            >
14342                <p style="cursor: pointer;"><i class="fa-light fa-list-dropdown smallMarginRight"></i>No form found.</p>
14343            </div>
14344        </template>
14345    </div>
14346</script>
14346
14347
14348<style>
14349   
14350    #form-template{
14351        display: flex;
14352        flex-direction: column;
14353        align-items: center;
14354        justify-content: flex-start;
14355        margin: 0.5em 0.5em 1em 0.5em;
14356        padding: 0 0 1.5em 0;
14357        width: 100%;
14358        max-width: 700px; 
14359        height: 100%;
14360    }
14361    #form-template .form-color-theme{
14362        color: #fff;
14363        height: 10px;
14364        border-top-left-radius: 8px;
14365        border-top-right-radius: 8px;
14366        width: 100%;
14367        padding: 0 1.5em 0 1.5em;
14368        box-sizing: border-box;
14369    }
14370    #form-template .default-font-css{
14371        font-family: Roboto, Arial, Helvetica, sans-serif;
14372        font-size: 16px;
14373        font-weight: 400;
14374    }
14375    #form-template .flex-row-start{
14376        display: flex;
14377        flex-direction: row;
14378        align-items: center;
14379        justify-content: flex-start;
14380    }
14381    #form-template .submit-button{
14382        color: #fff; 
14383        border-radius: 6px; 
14384        font-size: 14px; 
14385        padding: 8px 24px; 
14386        cursor: pointer;
14387        border: none;
14388    }
14389    #form-template .submit-button.inactive{
14390        color: #fff; 
14391        border-radius: 6px; 
14392        font-size: 14px; 
14393        padding: 8px 24px; 
14394        cursor: auto;
14395        border: none;
14396    }
14397    .form-wrapper{
14398        display: flex;
14399        align-items: center;
14400        justify-content: center;
14401        height: fit-content;
14402        width: 100%;
14403    }    
14404    .form-header-wrapper{
14405        border-width: 0px 1px 1px 1px;
14406        border-color: rgba(195, 195, 195, 0.7);
14407        border-style: solid;
14408        width: 100%;
14409        border-bottom-left-radius: 8px;
14410        border-bottom-right-radius: 8px;
14411        padding: 1.45em;
14412        box-sizing: border-box;
14413    }
14414    .form-name{
14415        color: rgb(32, 33, 36);
14416        box-sizing: border-box;
14417        font-weight: 400;
14418        font-size: 32px;
14419        width: 100%;
14420        min-height: 21px;
14421        border-bottom: 1.5px solid transparent;
14422        transition: border-bottom 0.1s;
14423    }
14424    .form-description{
14425        color: rgb(32, 33, 36);
14426        box-sizing: border-box;
14427        font-weight: 400;
14428        font-size: 14px;
14429        width: 100%;
14430        line-height: 1.5;
14431        letter-spacing: 0;
14432        min-height: 21px;
14433        border-bottom: 1.5px solid transparent;
14434        transition: border-bottom 0.1s;
14435    }
14436    .form-name.input-is-focused, .form-description.input-is-focused{
14437        border-bottom: 1.5px solid rgba(195, 195, 195, 0.7);
14438    }
14439</style>
14440
14441<script>
14442    app_content.component("form-component", {
14443        template: "#template-form",
14444        data() {
14445            return {
14446                global: store,
14447                loading: false,
14448                loading_submission: false,
14449                json_form: null,
14450                invalid_membership: false,
14451                selected_fields: [
14452                    {
14453                        label: 'First name',
14454                        value: 'firstName',
14455                        selected: false,
14456                        input_value: null,
14457                    },
14458                    {
14459                        label: 'Last name',
14460                        value: 'lastName',
14461                        selected: false,
14462                        input_value: null,
14463                    },
14464                    {
14465                        label: 'Email',
14466                        value: 'email',
14467                        selected: false,
14468                        input_value: null,
14469                    },
14470                    {
14471                        label: 'Mobile',
14472                        value: 'mobile',
14473                        selected: false,
14474                        input_value: null,
14475                    },
14476                ], 
14477                login_url: null,
14478                pending_redirect_url: null,
14479                redirect_timeout: null,
14480            };
14481
14482        },
14483        props: {
14484            client_id: String,
14485            is_admin: Boolean,
14486            component: Object,
14487            form: Object,
14488            formatting: String,
14489            last_update: String,
14490        },
14491        mounted() {
14492            this.load_forms( this.client_id );
14493        },
14494        beforeUnmount() {
14495            if (this.redirect_timeout) {
14496                clearTimeout(this.redirect_timeout);
14497            }
14498        },
14499        methods: {
14500            load_forms( client_id, overrides = {} ){
14501                this.loading = true;
14502                var promise = Parse.Promise.as();
14503                var thisPromise = promise.then(() => {
14504                    var params = {};
14505                    params["client_id"] = client_id;
14506                    params["form_id"] = this.form_id;
14507                    params["overrides"] = overrides;
14508                    params["request_url"] = window.location.href;
14509                    return Parse.Cloud.run('retrieve_data_for_component', params);
14510                }).then(retrieved_form => {
14511                    this.json_form = retrieved_form;
14512                    if(this.json_form.members_only){
14513                        //check if theres a logged in member and redirect them if needed
14514                        return this.check_valid_membership().then(is_member => {
14515                            if(is_member){
14516                                this.invalid_membership = false;
14517                                this.set_default_fields();
14518                                if ( this.is_admin ){
14519                                    return Parse.Promise.as();
14520                                }
14521                                return this.prefill_member_fields();
14522                            }
14523                        }).catch(error => {
14524                            if(error === "invalid_membership"){
14525                                this.invalid_membership = true;
14526                                this.loading = false;
14527                                this.set_login_url();
14528                                // return the error
14529                                return Parse.Promise.error("invalid_membership");
14530                            }
14531                        });
14532                    }else{
14533                        this.set_default_fields();
14534                        return Parse.Promise.as();
14535                    }
14536                    
14537                }).then(() => {
14538                    this.loading = false;
14539                }, error => {
14540                    checkParseError(error);
14541                    this.loading = false;
14542                });
14543                return thisPromise;
14544            },
14545            prefill_member_fields(){
14546                let query = new Parse.Query(Parse.User);
14547                query.include('memberObject');
14548                return query.get(Parse.User.current().id).then(fullUser => {
14549                    let memberObject = fullUser.get('memberObject');
14550                    if ( !memberObject ){
14551                        return Promise.resolve();
14552                    }
14553                    let member_data = {
14554                        firstName: memberObject.get('firstName') || null,
14555                        lastName: memberObject.get('lastName') || null,
14556                        email: memberObject.get('email') || null,
14557                        mobile: memberObject.get('mobile') || null,
14558                    };
14559                    this.selected_fields.flat().forEach(field => {
14560                        if ( member_data[field.value] ){
14561                            field.input_value = member_data[field.value];
14562                        }
14563                    });
14564                    return Promise.resolve();
14565                });
14566            },
14567            check_valid_membership(){
14568                var club_id = $('body').data('clubId');
14569                var user = Parse.User.current();
14570
14571                var memberObject;
14572                var promise = Parse.Promise.as();
14573                var thisPromise = promise.then(() => {
14574                    if( user && user.get('memberObject') ){
14575                        //ok, get the member and membership
14576                        var query = new Parse.Query("members");
14577                        query.select('membershipObject.status');
14578                        query.select('membershipObject.memberNumber');
14579                        query.select('membershipObject.tierObject');
14580                        query.select('clubObject.web_settings.custom_domain');
14581                        query.include('membershipObject');
14582                        query.include('clubObject.web_settings');
14583                        return query.get( user.get('memberObject').id );
14584                    }else{
14585                        //redirect to login
14586                        return Promise.reject("invalid_membership");
14587                    }
14588                }).then(( retrievedMember )=>{
14589                    if( retrievedMember && retrievedMember.get('clubObject')?.id == club_id ){
14590                        //ok, they're legit
14591                        memberObject = retrievedMember;
14592                    }
14593
14594                    if( ! memberObject ){
14595                        //redirect to login
14596                        return Promise.reject("invalid_membership");
14597                    }
14598                    //check membership
14599                    let membershipObject = memberObject.get('membershipObject');
14600                    if( ! membershipObject ){
14601                        return Promise.reject("invalid_membership");
14602                    }
14603                    if( membershipObject.get('status') == "canceled" ){
14604                        return Promise.reject("invalid_membership");
14605                    }   
14606                    var valid_membership = true;
14607                    //ok, they're a legit member
14608                    return Parse.Promise.as(valid_membership);
14609                });
14610                return thisPromise;
14611            },
14612            set_login_url(){
14613                const clubId = $('body').data('clubId'); // or wherever you're storing it
14614                const query = new Parse.Query("clubs");
14615                query.include("web_settings");
14616                query.get(clubId).then(club => {
14617                    const webSettings = club.get('web_settings');
14618                    const customDomain = webSettings?.get('custom_domain');
14619                    const currentUrl = window.location.href;
14620
14621                    let redirectBase = customDomain
14622                        ? `https://${customDomain}`
14623                        : "https://theclubspot.com";
14624
14625                    const loginUrl = `${redirectBase}/login?next=${encodeURIComponent(currentUrl)}&msg=${encodeURIComponent("Please log in to view and complete this form.")}`;
14626                    this.login_url = loginUrl;
14627                });
14628
14629
14630            },
14631            tapped_clear_form(){
14632                if ( this.collect_default_fields && this.selected_fields.length > 0 ){
14633                    this.selected_fields.flat().forEach(default_field => {
14634                        default_field.input_value = null;
14635                    })
14636                }
14637                if ( this.custom_fields && this.$refs.custom_field_components ){
14638                    this.$refs.custom_field_components.forEach(ref => {
14639                        ref.clear_field();
14640                    });
14641                }
14642            },
14643            maybe_redirect_after_submit(redirect_url){
14644                if (!redirect_url) {
14645                    return false;
14646                }
14647                this.pending_redirect_url = redirect_url;
14648                this.redirect_timeout = setTimeout(() => {
14649                    window.location.href = redirect_url;
14650                }, 2000);
14651                return true;
14652            },
14653            tapped_submit_form(){
14654                if ( this.form_disabled ){
14655                    return displayMessage("This form is no longer accepting submissions.", true, 4000);
14656                }
14657                if ( this.button_disabled ){
14658                    return displayMessage("Required fields are missing.", true, 4000);
14659                }
14660                let club_id = $('body').data('clubId');
14661                if ( !club_id || !this.form_id ){
14662                    return displayMessage("There was an issue submitting your form. Please try again, or contact support if the problem persists.", true, 4000);
14663                }
14664                this.loading_submission = true;
14665                if ( this.custom_fields && this.custom_fields.length > 0 ){
14666                    for ( const custom_field of this.custom_fields ){
14667                        if ( custom_field.type === 'number' ){
14668                            if ( custom_field.input_value !== null && custom_field.input_value !== undefined && custom_field.input_value !== '' ){
14669                                if ( isNaN(Number(custom_field.input_value)) ){
14670                                    this.loading_submission = false;
14671                                    displayMessage("Please enter a valid number.", true, 4000);
14672                                    return;
14673                                }
14674                            }
14675                        }
14676                    }
14677                }
14678                let club_timezone = store.clubObject && store.clubObject.get ? (store.clubObject.get('timezone') || 'UTC') : 'UTC';
14679                return this.prepare_custom_fields_for_submit(this.custom_fields || []).then(preparedCustomFields => {
14680                    let params = {
14681                        club_id: club_id,
14682                        form_id: this.form_id,
14683                        selected_fields: this.selected_fields,
14684                        collect_default_fields: this.collect_default_fields,
14685                        custom_fields: preparedCustomFields,
14686                        club_timezone: club_timezone
14687                    };
14688                    return Parse.Cloud.run('submit_form_response', params);
14689                }).then((result) => {
14690                    this.loading_submission = false;
14691                    if (this.maybe_redirect_after_submit(result && result.redirect_url)) {
14692                        return;
14693                    }
14694                    displayMessage('Your response has been successfully submitted', false, 6000);
14695                    this.tapped_clear_form();
14696                }).catch(error => {
14697                    this.loading_submission = false;
14698                    checkParseError(error);
14699                    displayMessage(error.message || error, true, 4000);
14700                });
14701            },
14702            prepare_custom_fields_for_submit(custom_fields){
14703                let promise = Promise.resolve();
14704                let thisPromise = promise.then(() => {
14705                    if( !custom_fields || custom_fields.length === 0 ){
14706                        return Promise.resolve([]);
14707                    }
14708                    let preparedCustomFields = [];
14709                    let promises = [];
14710                    for (let index = 0; index < custom_fields.length; index++) {
14711                        let methodToRun = (customField, index) => {
14712                            let innerPromise = promise.then(() => {
14713                                let preparedCustomField = {
14714                                    id: customField.id,
14715                                    name: customField.name,
14716                                    type: customField.type,
14717                                    required: customField.required,
14718                                    input_value: customField.input_value,
14719                                    custom_field_options: customField.custom_field_options || [],
14720                                    order: index,
14721                                };
14722                                if( customField.type !== 'file_upload' ){
14723                                    preparedCustomFields.push(preparedCustomField);
14724                                    return Promise.resolve();
14725                                }
14726                                preparedCustomField.input_value = '';
14727                                if( customField.document_id ){
14728                                    preparedCustomField.document_id = customField.document_id;
14729                                    preparedCustomFields.push(preparedCustomField);
14730                                    return Promise.resolve();
14731                                }
14732                                let uploadFile = this.return_upload_file_from_custom_field(customField);
14733                                if( !uploadFile || !uploadFile.name ){
14734                                    preparedCustomFields.push(preparedCustomField);
14735                                    return Promise.resolve();
14736                                }
14737                                let uploadParams = {
14738                                    file: uploadFile,
14739                                    fileName: uploadFile.name
14740                                };
14741                                return upload_private_file_v2(uploadParams).then(uploadResult => {
14742                                    preparedCustomField.uploaded_file_url = uploadResult.url;
14743                                    preparedCustomField.uploaded_file_key = uploadResult.file_key;
14744                                    preparedCustomField.uploaded_file_name = uploadFile.name;
14745                                    preparedCustomFields.push(preparedCustomField);
14746                                    return Promise.resolve();
14747                                });
14748                            });
14749                            promises.push(innerPromise);
14750                        };
14751                        methodToRun(custom_fields[index], index);
14752                    }
14753                    return Promise.all(promises).then(() => {
14754                        return Promise.resolve(preparedCustomFields);
14755                    });
14756                });
14757                return thisPromise;
14758            },
14759            return_upload_file_from_custom_field(customField){
14760                if( !customField ){
14761                    return null;
14762                }
14763                if( customField.input_value && customField.input_value.name ){
14764                    return customField.input_value;
14765                }
14766                if( Array.isArray(customField.input_value) && customField.input_value.length > 0 ){
14767                    let firstFile = customField.input_value[0];
14768                    if( firstFile && firstFile.name ){
14769                        return firstFile;
14770                    }
14771                }
14772                if( customField.input_value && customField.input_value.file && customField.input_value.file.name ){
14773                    return customField.input_value.file;
14774                }
14775                if( customField.input_value && customField.input_value.value && customField.input_value.value.name ){
14776                    return customField.input_value.value;
14777                }
14778                return null;
14779            },
14780            returnIsVideo(filename) {
14781                var ext = this.getExtension(filename);
14782                switch (ext.toLowerCase()) {
14783                    case 'm4v':
14784                    case 'avi':
14785                    case 'mpg':
14786                    case 'mp4':
14787                    case 'mp3':
14788                    case 'mov':
14789                    // etc
14790                    return true;
14791                }
14792                return false;
14793            },
14794            getExtension(filename) {
14795                var parts = filename.split('.');
14796                return parts[parts.length - 1];
14797            },
14798            load_clean_image( file, type, newFileName ){
14799                return new Promise((resolve, reject) => {
14800                    setTimeout(() => {
14801                        loadImage(file, canvas => {
14802                            if (canvas) {
14803                                this.clean_image_callback(canvas, type, newFileName, file)
14804                                .then(url => resolve(url))
14805                                .catch(err => reject(err));
14806                            } else {
14807                                reject(new Error('Failed to load image'));
14808                            }
14809                        },
14810                        { orientation: 1, canvas: true }
14811                        );
14812                    }, 500);
14813                });
14814            },
14815            clean_image_callback( canvas, type, newFileName, file ){
14816                return new Promise((resolve, reject) => {
14817                    if (canvas) {
14818                        var url = canvas.toDataURL(type);
14819                        canvas.toBlob(blob => {
14820                            uploadFileToAWSV2( blob, newFileName )
14821                            .then(uploadUrl => resolve(uploadUrl))
14822                            .catch(err => reject(err));
14823                        }, type);
14824                    } else {
14825                        reject(new Error('canvas is null'));
14826                    }
14827                });
14828            },
14829            link_form(){
14830                store.components.link_form.component = this.component;
14831                store.overlays.link_form = true;
14832            },
14833            set_default_fields(){
14834                if ( this.json_form && this.default_fields.length > 0 ){
14835                    this.selected_fields = this.selected_fields.map(default_field => {
14836                        if ( this.default_fields.indexOf(default_field.value ) !== -1 ){
14837                            return {
14838                                ...default_field,
14839                                selected: true,
14840                            }
14841                        } else {
14842                            return {
14843                                ...default_field,
14844                                selected: false,
14845                            }
14846                        }
14847                    });
14848                }
14849                if ( this.default_configuration === 'list' ){
14850                    //list
14851                    this.selected_fields = this.selected_fields.filter(field => field.selected);
14852                } else if ( this.default_configuration === 'grid' ){
14853                    //grid
14854                    this.selected_fields = this.selected_fields.filter(field => field.selected).reduce((acc, field) => {
14855                        if ( field.value === 'firstName' || field.value === 'lastName' ){
14856                            acc[0].push(field);
14857                        } else {
14858                            acc[1].push(field);
14859                        }
14860                        return acc;
14861                    }, [[], []])
14862                }
14863            },
14864            handle_field_update({ id, value }){
14865                const field = this.custom_fields.find(f => f.id === id);
14866                if (field) {
14867                    field.input_value = value;
14868                }
14869            },
14870        },
14871        computed: {
14872            form_id(){
14873                if ( typeof( this.form ) === 'string' ){
14874                    return JSON.parse(this.form).id;
14875                }
14876                return this.form.id || null;
14877            },
14878            color_theme(){
14879                if ( this.json_form && this.json_form.color_theme ){
14880                    return this.json_form.color_theme;
14881                }
14882                return '#5280ff';
14883            },
14884            custom_fields(){
14885                if ( this.json_form && this.json_form.custom_fields ){
14886                    return this.json_form.custom_fields;
14887                }
14888                return [];
14889            },
14890            default_fields(){
14891                if ( this.json_form && this.json_form.default_fields  ){
14892                    return this.json_form.default_fields;
14893                }
14894                return [];
14895            },
14896            collect_default_fields(){
14897                if ( this.json_form && this.json_form.collect_default_fields  ){
14898                    return this.json_form.collect_default_fields;
14899                }
14900                return false;
14901            },
14902            default_configuration(){
14903                if ( this.json_form && this.json_form.default_configuration  ){
14904                    return this.json_form.default_configuration;
14905                }
14906                return 'list';
14907            },
14908            missing_default_field(){
14909                if ( this.json_form && this.collect_default_fields && this.selected_fields.length > 0 ){
14910                    let default_values = this.default_configuration === 'list' 
14911                        ? this.selected_fields.map(field => field.input_value) 
14912                        : this.selected_fields.flatMap(arr => arr.map(field => field.input_value));
14913                    return default_values.includes(null) || default_values.includes('');
14914                }
14915                return false;
14916            },
14917            missing_custom_field(){
14918                if ( this.json_form && this.custom_fields ){
14919                    for ( let i = 0; i < this.custom_fields.length; i++ ){
14920                        let custom_field = this.custom_fields[i];
14921                        let option_values = custom_field.custom_field_options && custom_field.custom_field_options.length > 0 
14922                            ? custom_field.custom_field_options.map(option => option.selected)
14923                            : [];
14924                        if ( custom_field.required && ( !custom_field.input_value && option_values.indexOf(true) === -1 ) ){
14925                            return true;
14926                        }
14927                    }
14928                    return false;
14929                }
14930            },
14931            form_disabled(){
14932                if ( this.json_form && this.json_form.disabled ){
14933                    return true;
14934                }
14935                return false;
14936            },
14937            button_disabled(){
14938                if ( this.is_admin ){
14939                    return true;
14940                }else{
14941                    return this.form_disabled || this.missing_custom_field || this.missing_default_field;
14942                }
14943            },
14944            formatting_obj(){
14945                if ( this.formatting ){
14946                    return JSON.parse(this.formatting);
14947                }
14948                return;
14949            },
14950            valid_membership(){
14951                if(this.json_form && this.json_form.members_only){
14952                    return !this.invalid_membership;
14953                }
14954                //not members only, doesn't matter
14955                return true;
14956            },
14957        },
14958        watch: {
14959            form_id(newVal, oldVal){
14960                if (newVal !== oldVal){
14961                    this.load_forms( this.client_id );
14962                }
14963            }
14964        }
14965    })
14966</script>
14966    
14966<script type="text/html" id="template-tab-bar">
14967    <template v-if="is_web_builder()">
14968        <template v-if="config_latest()['tabs']?.length > 0">
14969            <div
14970                class="tab-bar-wrapper"
14971                :style="{
14972                    gap: (config_latest().formatting.section_width || 50) + 'px',
14973                    height: (config_latest().formatting.section_height || 50) + 'px',
14974                }"
14975            >
14976                <div
14977                    v-for="tab in config_latest()['tabs']"
14978                    :key="tab.id"
14979                    style="width: 200px; display: flex; align-items: center; justify-content: center;"
14980                >
14981                    <div
14982                        @click="selected_tab_id = tab.id"
14983                        style="width: 100%; text-align: center;"
14984                        class="tab-bar-label"
14985                        :class="{ 'active': selected_tab_id === tab.id }"
14986                    >
14987                        <span>
14988                            <preview-element-component
14989                                v-if="tab.elements"
14990                                :element="tab.elements"
14991                                :element_type="tab.elements.type"
14992                                :element_content="tab.elements.content"
14993                                :componentObject="component"
14994                                :disable_remove="true"
14995                                :disable_column="true"
14996                                :disable_new_element="true"
14997                            ></preview-element-component>
14998                        </span>
14999                    </div>
15000                </div>
15001            </div>
15002            <web-builder-components
15003                v-if="selected_tab_id"
15004                :component="return_page_component(selected_tab_id)"
15005                :component_index="0"
15006                :preview_element="true"
15007            ></web-builder-components>
15008        </template>
15009        <template v-else>
15010            <div
15011                class="noObjectsMSG centeredBlock centeredText"
15012                style="padding: 5em;"
15013            >
15014                <p style="cursor: pointer;"><i class="fa-light fa-table-columns smallMarginRight"></i>Use the left side menu to start adding tabs</p>
15015            </div>
15016        </template>
15017    </template>
15018    <template v-if="!is_web_builder() && !loading">
15019        <template v-if="hosted_config()['tabs']?.length > 0">
15020            <div
15021                class="tab-bar-wrapper"
15022                :style="{
15023                    gap: (hosted_config().formatting.section_width || 50) + 'px',
15024                    height: (hosted_config().formatting.section_height || 50) + 'px',
15025                }"
15026            >
15027                <div
15028                    v-for="tab in hosted_config()['tabs']"
15029                    :key="tab.id"
15030                    style="width: 200px; display: flex; align-items: center; justify-content: center;"
15031                >
15032                    <div
15033                        @click="selected_tab_id = tab.id"
15034                        style="width: 100%; text-align: center;"
15035                        class="tab-bar-label"
15036                        :class="{ 'active': selected_tab_id === tab.id }"
15037                    >
15038                        <element-component
15039                            v-if="tab.elements"
15040                            :element="tab.elements"
15041                            :element_type="tab.elements.type"
15042                            :element_content="tab.elements.content"
15043                            :componentObject="component"
15044                            :disable_remove="true"
15045                            :disable_column="true"
15046                            :disable_new_element="true"
15047                        ></element-component>
15048                    </div>
15049                </div>
15050            </div>
15051            <web-builder-components
15052                v-if="selected_tab_id"
15053                :component="return_hosted_page_component(selected_tab_id)"
15054                :component_index="0"
15055                :tab_element="true"
15056                :preview_element="false"
15057            >
15057</web-builder-components>
15058        </template>
15059    </template>
15060</script>
15060
15061
15062<style>
15063    .tab-bar-wrapper{
15064        display: flex;
15065        flex-direction: row;
15066        align-items: center;
15067        justify-content: center;
15068        border-top: 1.5px solid rgb(204, 203, 203);
15069        border-bottom: 1.5px solid rgb(204, 203, 203);
15070        box-sizing: border-box;
15071    }
15072    .tab-bar-label span {
15073        position: relative;
15074        display: inline-block;
15075        padding-bottom: 4px;
15076        border-bottom: none !important;
15077        text-decoration: none !important;
15078    }
15079    .tab-bar-label span::after {
15080        content: "";
15081        position: absolute;
15082        bottom: 0;
15083        left: 50%;
15084        width: 0;
15085        height: 1px;
15086        background-color: #000;
15087        transform: translateX(-50%);
15088        transition: width 0.3s ease;
15089    }
15090    .tab-bar-label.active span::after {
15091        width: 100%;
15092    }
15093</style>
15094
15095<script>    
15096    app_content.component('tab-bar-component', {
15097        template: '#template-tab-bar',
15098        data() {
15099            return {
15100                loading: true,
15101                selected_tab_id: null,
15102                hosted_tab_component: null,
15103                hosted_page_components: [],
15104            }
15105        },
15106        props: {
15107            client_id: String,
15108            component: {
15109                type: Object,
15110                default: null,
15111            },
15112            last_update: Number,
15113        },
15114        mounted() {
15115            if(this.is_web_builder()){
15116                this.selected_tab_id = this.config_latest()?.tabs?.[0]?.id || null;
15117            }else{
15118                this.load_tab_bar( this.client_id );
15119            }
15120        },
15121        methods: {
15122            load_tab_bar( client_id, overrides = {} ){
15123                this.loading = true;
15124                var promise = Parse.Promise.as();
15125                var thisPromise = promise.then(() => {
15126                    var params = {};
15127                    params["client_id"] = client_id;
15128                    params["overrides"] = overrides;
15129                    params["request_url"] = window.location.href;
15130                    return Parse.Cloud.run('retrieve_data_for_component', params);
15131                }).then(response => {
15132                    this.hosted_tab_component = response.tab_component || null;
15133                    this.hosted_page_components = response.page_components || [];
15134                    this.selected_tab_id = this.hosted_config()?.tabs?.[0]?.id || null;
15135                }).catch(error => {
15136                    checkParseError(error);
15137                }).finally(() => {
15138                    this.loading = false;
15139                });
15140                return thisPromise;
15141            },
15142            return_page_component(id){
15143                return store.page_components_latest.find(c => c.id === id)
15144            },
15145            return_hosted_page_component(id){
15146                return this.hosted_page_components.find(c => c.id === id)
15147            },
15148            config_latest(){
15149                if(this.component && this.component.get('config_latest')){
15150                    return this.component.get('config_latest');
15151                }
15152            },
15153            hosted_config(){
15154                if(this.hosted_tab_component && this.hosted_tab_component.get('config')){
15155                    return this.hosted_tab_component.get('config');
15156                }
15157            },
15158            is_web_builder(){
15159                return window.location.href.includes('website-builder');
15160            },
15161        },
15162        watch: {
15163            last_update(newVal) {
15164                if (this.is_web_builder()) {
15165                    this.selected_tab_id = this.config_latest()?.tabs?.[0]?.id || null;
15166                }
15167            }
15168        }
15169    });
15170</script>
15170    
15170<script type="text/html" id="template-web-builder-components">
15171    <div
15172        :id="'componentPreview_' + component.get('client_id')"
15173        class="componentPreview"
15174        :class="{'hidePreview': tab_element}"
15175        :data-client-id="component.get('client_id')"
15176        :style="return_preview_style(component)"
15177    >
15178        <div
15179            v-if="component.get('visibility') === 'members_only'"
15180            class="event-page-callout"
15181            :class="{ 'no-hover': !can_open_visibility_settings }"
15182            style="width:fit-content;font-size:12px;position: absolute;z-index:100;display:flex;align-items:center;justify-content:center;"
15183            :title="can_open_visibility_settings ? 'Edit visibility' : null"
15184            @click.stop="open_members_only_visibility_settings"
15185        >
15186            <i class="fa-solid fa-user-shield"></i>
15187            <p class="tinyMarginLeft" style="font-family:'Roboto';font-size:12px;">{{ (component.get('allowed_categories') && component.get('allowed_categories').length > 0) ? 'Members only (categories)' : 'Members only' }}</p>
15188        </div>
15189        <template v-if="component.get('type') === 'iframe'">
15190            <iframe-component
15191                :class_string="return_attributes(component, 'classString')"
15192                :style_string="return_attributes(component, 'style_string')"
15193                :iframe_src="return_attributes(component, 'config_latest').iframe_src"
15194                :component="component"
15195                :formatting_string="return_attributes(component, 'formatting')"
15196                :last_update="return_last_update(component)"
15197            ></iframe-component>
15198            <toolbar-option-component
15199                @click.stop
15200                :component="component"
15201                :is_first="component_index === 0"
15202                :disable_options="disable_options(component)"
15203            ></toolbar-option-component>
15204        </template>
15205        <template v-if="component.get('type') === 'camera-feed'">
15206            <camera-feed-component
15207                :class_string="return_attributes(component, 'classString')"
15208                :style_string="return_attributes(component, 'style_string')"
15209                :client_id="return_attributes(component, 'client_id')"
15210                :camera_feed_id="return_attributes(component, 'config_latest').camera_feed_id"
15211                :refresh_interval_ms="return_attributes(component, 'config_latest').refresh_interval_ms"
15212                :component="component"
15213                :formatting_string="return_attributes(component, 'formatting')"
15214                :last_update="return_last_update(component)"
15215            ></camera-feed-component>
15216            <toolbar-option-component
15217                @click.stop
15218                :component="component"
15219                :is_first="component_index === 0"
15220                :disable_options="disable_options(component)"
15221            ></toolbar-option-component>
15222        </template>
15223        <template v-if="component.get('type') === 'blog-list'">
15224            <blog-list-component
15225                :client_id="return_attributes(component, 'client_id')"
15226                :class_string="return_attributes(component, 'classString')"
15227                :style_string="return_attributes(component, 'style_string')"
15228                :element_style_string="return_attributes(component, 'element_style')"
15229                :formatting="return_attributes(component, 'formatting')"
15230                :component_type="return_attributes(component, 'component_type')"
15231                :is_larchmont="global.is_larchmont"
15232                :component="component"
15233                :last_update="return_last_update(component)"
15234            ></blog-list-component>
15235            <toolbar-option-component
15236                @click.stop
15237                :component="component"
15238                :is_first="component_index === 0"
15239                :disable_options="disable_options(component)"
15240            ></toolbar-option-component>
15241        </template> 
15242        <template v-if="component.get('type') === 'calendar'">
15243            <membership-calendar-component
15244                :class_string="return_attributes(component, 'classString')"
15245                :client_id="return_attributes(component, 'client_id')"
15246                :style_string="return_attributes(component, 'style_string')"
15247                :component="component" 
15248                :last_update="return_last_update(component)"
15249            ></membership-calendar-component>
15250            <toolbar-option-component
15251                @click.stop
15252                :component="component"
15253                :is_first="component_index === 0"
15254                :disable_options="disable_options(component)"
15255            ></toolbar-option-component>
15256        </template>
15257        <template v-if="component.get('type') === 'membership-directory'">
15258            <membership-directory-component
15259                :formatting="JSON.parse(return_attributes(component, 'formatting'))"
15260                :class_string="return_attributes(component, 'classString')"
15261                :client_id="return_attributes(component, 'client_id')"
15262                :style_string="return_attributes(component, 'style_string')"
15263                :element_style_string="return_attributes(component, 'element_style')"
15264                :component_type="return_attributes(component, 'component_type')"
15265                :last_update="return_last_update(component)"
15266            ></membership-directory-component>
15267            <toolbar-option-component
15268                @click.stop
15269                :component="component"
15270                :is_first="component_index === 0"
15271                :disable_options="disable_options(component)"
15272            ></toolbar-option-component>
15273        </template>
15274        <template v-if="component.get('type') === 'event-list'">
15275            <event-list-component
15276                :class_string="return_attributes(component, 'classString')"
15277                :style_string="return_attributes(component, 'style_string')"
15278                :element_style_string="return_attributes(component, 'element_style')"
15279                :formatting="return_attributes(component, 'formatting')"
15280                :filter_types="return_attributes(component, 'filter_types')"
15281                :filter_tag_ids="return_attributes(component, 'filter_tag_ids')"
15282                :event_dates="return_attributes(component, 'event_dates')"
15283                :component_type="return_attributes(component, 'component_type')"
15284                :client_id="return_attributes(component, 'client_id')"
15285                :component="component"
15286                :last_update="return_last_update(component)"
15287            ></event-list-component>
15288            <toolbar-option-component
15289                @click.stop
15290                :component="component"
15291                :is_first="component_index === 0"
15292                :disable_options="disable_options(component)"
15293            ></toolbar-option-component>
15294        </template>
15295        <template v-if="component.get('type') === 'image-grid'">
15296            <image-grid-component
15297                :client_id="return_attributes(component, 'client_id')"
15298                :class_string="return_attributes(component, 'classString')"
15299                :image_grid_layout="return_attributes(component, 'image_grid_layout')"
15300                :images_per_row="return_attributes(component, 'images_per_row')"
15301                :caption_size="return_attributes(component, 'caption_size')"
15302                :subtext_size="return_attributes(component, 'subtext_size')"
15303                :link_label_size="return_attributes(component, 'link_label_size')"
15304                :text_position="return_attributes(component, 'text_position')"
15305                :caption_font_weight="return_attributes(component, 'caption_font_weight')"
15306                :image_grid_opacity="return_attributes(component, 'image_grid_overlay_opacity')"
15307                :image_grid_border_radius="return_attributes(component, 'image_grid_border_radius')"
15308                :formatting="return_attributes(component, 'formatting')"
15309                :is_admin="preview_element"
15310                :data_source_type="return_attributes(component, 'data_source')['type']"
15311                :images_arr="return_attributes(component, 'images')"
15312                :images_string="JSON.stringify(return_attributes(component, 'images'))"
15313                :component="component"
15314                :last_update="return_last_update(component)"
15315            ></image-grid-component>
15316            <toolbar-option-component
15317                @click.stop
15318                :component="component"
15319                :is_first="component_index === 0"
15320                :disable_options="disable_options(component)"
15321            ></toolbar-option-component>
15322        </template>
15323        <template v-if="component.get('type') === 'photo-gallery'">
15324            <photo-gallery-component
15325                :client_id="return_attributes(component, 'client_id')"
15326                :component="component"
15327                :photo_album="return_attributes(component, 'photo_album')"
15328                :show_caption="return_attributes(component, 'show_caption')"
15329                :caption_size="return_attributes(component, 'caption_size')"
15330                :caption_font_weight="return_attributes(component, 'caption_font_weight')"
15331                :photos_per_row="return_attributes(component, 'photos_per_row')"
15332                :is_admin="preview_element"
15333                :last_update="return_last_update(component)"
15334            ></photo-gallery-component>
15335            <toolbar-option-component
15336                @click.stop
15337                :component="component"
15338                :is_first="component_index === 0"
15339                :disable_options="disable_options(component)"
15340            ></toolbar-option-component>
15341        </template>
15342        <template v-if="component.get('type') === 'form'">
15343            <form-component
15344                :client_id="return_attributes(component, 'client_id')"
15345                :component="component"
15346                :form="return_attributes(component, 'form')"
15347                :formatting="return_attributes(component, 'formatting')"
15348                :is_admin="preview_element"
15349                :last_update="return_last_update(component)"
15350            ></form-component>
15351            <toolbar-option-component
15352                @click.stop
15353                :component="component"
15354                :is_first="component_index === 0"
15355                :disable_options="disable_options(component)"
15356            ></toolbar-option-component>
15357        </template>
15358        <template v-if="component.get('type') === 'image-with-text'">
15359            <image-with-text-component
15360                :client_id="return_attributes(component, 'client_id')"
15361                :formatting="return_attributes(component, 'formatting')"
15362                :images_string="JSON.stringify(return_attributes(component, 'images'))"
15363                :class_string="return_attributes(component, 'classString')"
15364                :style_string="return_attributes(component, 'style_string')"
15365                :element_style_string="return_attributes(component, 'element_style')"	
15366                :image_mat="return_attributes(component, 'image_mat')"
15367                :elements_string="return_attributes(component, 'elements')"
15368                :left_right_orientation="return_attributes(component, 'left_right_orientation')" 
15369                :preview_element="preview_element"
15370                :component="component"
15371                :last_update="return_last_update(component)"
15372                :last_element_update="return_last_element_update( component )"
15373                :tab_element="tab_element"
15374            ></image-with-text-component>
15375            <toolbar-option-component
15376                @click.stop
15377                :component="component"
15378                :is_first="component_index === 0"
15379                :disable_options="disable_options(component)"
15380            ></toolbar-option-component>
15381        </template>
15382        <template v-if="component.get('type') === 'text' || component.get('type') === 'table'">
15383            <div 
15384                class="cs-text background flexWrap relative" 
15385                :class="return_attributes(component, 'classString')" 
15386                :style="return_attributes(component, 'style_string')"
15387            >
15388                <div 
15389                    v-if="return_attributes(component, 'columns').length > 0" 
15390                    class="element-wrapper flexNoWrap flexWrapOnMobile" 
15391                    :class="'element-wrapper_' + return_attributes(component, 'component_type')"
15392                    :style="return_attributes(component, 'element_style')"
15393                >
15394                    <template
15395                        v-for="(elements, column_index) in return_attributes(component, 'columns')"
15396                        :key="column_index"
15397                    >
15398                        <text-component
15399                            :number_of_columns="return_attributes(component, 'number_of_columns')"	
15400                            :preview_element="preview_element"
15401                            :component="component"
15402                            :last_update="return_last_update(component)"
15403                            :last_element_update="return_last_element_update( component )"
15404                            :elements="elements"
15405                            :column_index="column_index"
15406                            :section_element_animation="return_attributes(component, 'section_element_animation')"
15407                        ></text-component>	
15408                    </template>
15409                </div>
15410            </div>
15411            <toolbar-option-component
15412                @click.stop
15413                :component="component"
15414                :is_first="component_index === 0"
15415                :disable_options="disable_options(component)"
15416            ></toolbar-option-component>
15417        </template>
15418        <template v-if="component.get('type') === 'hero-banner'">
15419            <hero-banner-component
15420                ref="heroBanner"
15421                :client_id="return_attributes(component, 'client_id')"
15422                :formatting="return_attributes(component, 'formatting')"
15423                :class_string="return_attributes(component, 'classString')"
15424                :style_string="return_attributes(component, 'style_string')"
15425                :element_style_string="return_attributes(component, 'element_style')"
15426                :component_type="return_attributes(component, 'component_type')"
15427                :elements_string="return_attributes(component, 'elements')"
15428                :images_string="JSON.stringify(return_attributes(component, 'images'))"
15429                :preview_element="preview_element"
15430                :component="component"
15431                :last_update="return_last_update(component)"
15432                :last_element_update="return_last_element_update( component )"
15433                :tab_element="tab_element"
15434            ></hero-banner-component>
15435            <button
15436                v-if="preview_element"
15437                type="button"
15438                class="cs-image-slot-change-chip cs-hero-image-change-chip exclude-from-custom-button-styles"
15439                @click.stop="tapped_change_hero_image()"
15440            ><i class="fa-regular fa-image"></i><span>Change</span></button>
15441            <toolbar-option-component
15442                @click.stop
15443                :component="component"
15444                :is_first="component_index === 0"
15445                :disable_options="disable_options(component)"
15446            ></toolbar-option-component>
15447        </template>
15448        <template v-if="component.get('type') === 'custom-code'">
15449            <custom-code-component	
15450                :class_string="return_attributes(component, 'classString')"
15451                :preview_element="preview_element"
15452                :is_first="is_first_visible_component(component)"
15453                :component="component"
15454                :last_update="return_last_update(component)"
15455            ></custom-code-component>
15456            <toolbar-option-component
15457                @click.stop
15458                :component="component"
15459                :is_first="component_index === 0"
15460                :disable_options="disable_options(component)"
15461            ></toolbar-option-component>
15462        </template>
15463        <template v-if="component.get('type') === 'unlayer-component'">
15464            <unlayer-component
15465                :class_string="return_attributes(component, 'classString')"
15466                :preview_element="preview_element"
15467                :component="component"
15468                :last_update="return_last_update(component)"
15469            ></unlayer-component>
15470            <toolbar-option-component
15471                @click.stop
15472                :component="component"
15473                :is_first="component_index === 0"
15474                :disable_options="disable_options(component)"
15475            ></toolbar-option-component>
15476        </template>
15477        <!-- <template v-if="component.get('type') === 'tab-bar'">
15478            <tab-bar-component
15479                :client_id="return_attributes(component, 'client_id')"
15480                :preview_element="preview_element"
15481                :component="component"
15482                :last_update="return_last_update(component)"
15483            ></tab-bar-component>
15484            <toolbar-option-component
15485                @click.stop
15486                :component="component"
15487                :is_first="component_index === 0"
15488                :disable_options="disable_options(component)"
15489            ></toolbar-option-component>
15490        </template> -->
15491    </div>
15492</script>
15492
15493
15494<style>
15495    /* Hero Change chip: sibling of the section under .componentPreview so
15496       overlap masks / fades can't cover it. */
15497    .componentPreview > .cs-hero-image-change-chip {
15498        position: absolute;
15499        top: auto;
15500        right: auto;
15501        bottom: calc(12px + var(--cs-overlap-chrome-lift, 0px));
15502        left: 50%;
15503        transform: translateX(-50%);
15504        z-index: 3000;
15505        display: inline-flex;
15506        align-items: center;
15507        gap: 5px;
15508        padding: 6px 10px;
15509        border: none;
15510        border-radius: 6px;
15511        background: rgba(20, 24, 45, 0.82);
15512        color: #fff;
15513        font-family: 'Roboto', Arial, Helvetica, sans-serif;
15514        font-size: 12px;
15515        font-weight: 600;
15516        font-style: normal;
15517        line-height: 1;
15518        letter-spacing: 0;
15519        text-transform: none;
15520        cursor: pointer;
15521        opacity: 0;
15522        pointer-events: none;
15523        box-shadow: 0 4px 12px rgba(8, 19, 51, 0.22);
15524        transition: opacity 0.12s ease, background 0.12s ease;
15525    }
15526    .componentPreview:hover > .cs-hero-image-change-chip,
15527    .componentPreview > .cs-hero-image-change-chip:focus {
15528        opacity: 0.92;
15529        pointer-events: auto;
15530    }
15531    .componentPreview:hover > .cs-hero-image-change-chip:hover,
15532    .componentPreview > .cs-hero-image-change-chip:focus {
15533        opacity: 1;
15534        background: rgba(20, 24, 45, 0.95);
15535    }
15536    .componentPreview.settings-active > .cs-hero-image-change-chip {
15537        opacity: 0 !important;
15538        visibility: hidden !important;
15539        pointer-events: none !important;
15540    }
15541</style>
15542
15543<script>    
15544    app_content.component('web-builder-components', {
15545        template: '#template-web-builder-components',
15546        data() {
15547            return {
15548                global: store,
15549                is_admin: false,
15550            }
15551        },
15552        props: {
15553            component: Object,
15554            component_index: Number,
15555            tab_element: Boolean,
15556            preview_element: Boolean,
15557            last_update: Number,
15558        },
15559        methods: {
15560            component_is_linked_to_tab_bar( componentObject ){
15561                if (!componentObject || !Array.isArray(store.page_components_latest)) {
15562                    return false;
15563                }
15564                return store.page_components_latest.some(c => {
15565                    if (!c || !c.get || c.get('type') !== 'tab-bar') {
15566                        return false;
15567                    }
15568                    let tabs = (c.get('config_latest') && c.get('config_latest').tabs) || [];
15569                    return tabs.some(tab => {
15570                        return tab && tab.id === componentObject.id;
15571                    });
15572                });
15573            },
15574            is_first_visible_component( componentObject ){
15575                if (!componentObject || !Array.isArray(store.page_components_latest)) {
15576                    return this.component_index === 0;
15577                }
15578                let first_visible = store.page_components_latest.find(c => {
15579                    return c && !this.component_is_linked_to_tab_bar(c);
15580                });
15581                return !!(first_visible && first_visible.id === componentObject.id);
15582            },
15583            disable_options( componentObject ){
15584                return this.component_is_linked_to_tab_bar(componentObject);
15585            },
15586            open_members_only_visibility_settings(){
15587                if( typeof window.wb_open_component_visibility_settings !== 'function' ){
15588                    return;
15589                }
15590                window.wb_open_component_visibility_settings(this.component);
15591            },
15592            tapped_change_hero_image(){
15593                let hero = this.$refs.heroBanner;
15594                if( !hero || typeof hero.tapped_change_hero_image !== 'function' ){
15595                    return;
15596                }
15597                hero.tapped_change_hero_image();
15598            },
15599            return_last_element_update( componentObject ){
15600                return store.element_updates ? store.element_updates[ componentObject.id ] : 0;
15601            },
15602            return_last_update(componentObject){
15603                return store.component_updates ? store.component_updates[ componentObject.id ] : 0;
15604            },
15605            return_preview_style(componentObject){
15606                let type = componentObject.get('type');
15607                if ( type === 'hero-banner' || type === 'image-with-text' ){
15608                    return this.return_attributes(componentObject, 'background_image_style');
15609                }
15610            },
15611            return_attributes(componentObject, attribute){
15612                let attributes = {};
15613                if(componentObject){
15614                    var type = componentObject.get('type');
15615                    var config_latest = componentObject.get('config_latest') || returnDefaultConfigForComponent( type );
15616                    var images = config_latest.images;
15617                    if(!images){
15618                        // images = [];
15619                        images = [null, null, null, null, null]
15620                    }
15621                    var formatting = config_latest.formatting;
15622                    if( ! formatting ){
15623                        formatting = returnDefaultFormattingForComponent( type );
15624                    }
15625                    var color_scheme = config_latest.color_scheme;
15626                    if( !color_scheme ){
15627                        let default_config = returnDefaultConfigForComponent( type );
15628                        color_scheme = (default_config && default_config.color_scheme) ? default_config.color_scheme : 'white';
15629                    }
15630                    if (store.is_larchmont && store.pageObject.id === 'QAcpSXHP5G'){
15631                        color_scheme = 'larchmont_secondary';
15632                    }
15633                    //start the class string up here
15634                    var classString = '';
15635                    classString += ' color_scheme_' + color_scheme;
15636                    //now generate markup
15637                    var background_image_style = "";
15638                    var style_string = "";
15639                    if( formatting.section_height ){
15640                        // Browser vh is the outer window; the canvas is #previewZoneHeight.
15641                        // Use cqh (preview container) so heights match iframe/live viewport,
15642                        // including absolute/sticky header overlay. Keep vh as fallback.
15643                        let section_vh = ( formatting.section_height ).toString();
15644                        style_string += "min-height:" + section_vh + "vh;";
15645                        style_string += "min-height:calc(var(--wb-preview-vh, 1vh) * " + section_vh + ");";
15646                        style_string += "min-height:" + section_vh + "cqh;";
15647                    }
15648                    if( formatting.align_items
15649                        && ( typeof is_web_builder_element_stack_component !== 'function'
15650                            || is_web_builder_element_stack_component( type ) ) ){
15651                        style_string += "align-items:" + formatting.align_items + ";" ;
15652                    }
15653                    if ( formatting.background_size === 'inset' ){
15654                        background_image_style += "padding: 30px;" ;
15655                    } else if ( formatting.background_size === 'cover' ){
15656                        background_image_style += "padding: 0px;" ;
15657                    }
15658                    //element wrapper
15659                    var element_style = "";
15660                    //extra_columns
15661                    var extra_columns = config_latest.extra_columns;
15662                    if( extra_columns ){
15663                        for (var c = extra_columns.length - 1; c >= 0; c--) {
15664                            classString += ' show_' + extra_columns[c];
15665                        }   
15666                    }
15667                    //images per row (image grid)
15668                    var images_per_row = config_latest.images_per_row;
15669                    if( ! images_per_row ){
15670                        images_per_row = 3;
15671                    }
15672                    //photos per row (photo gallery)
15673                    var photos_per_row = config_latest.photos_per_row;
15674                    if( ! photos_per_row ){
15675                        photos_per_row = 4;
15676                    }
15677                    //left/right orientation (image with text)
15678                    var left_right_orientation = config_latest.left_r
15678ight_orientation;
15679                    if( ! left_right_orientation ){
15680                        left_right_orientation = "left";
15681                    }
15682                    classString += " orientation-" + left_right_orientation;
15683                    //number of columns (text)
15684                    var number_of_columns = config_latest.number_of_columns;
15685                    if( ! number_of_columns ){
15686                        number_of_columns = 3;
15687                    }
15688                    classString += " number_of_columns-" + number_of_columns;
15689
15690                    // Opt-in refinement tokens (padding, fade edges, scroll/element
15691                    // anim, max-width cap). Absent keys leave classString/styles unchanged.
15692                    let refinement_result = apply_web_builder_refinement_formatting({
15693                        formatting: formatting,
15694                        class_string: classString,
15695                        element_style: element_style,
15696                        activate_immediately: ( typeof window !== 'undefined' && window.location.href.indexOf('website-builder') !== -1 ),
15697                        is_first_section: this.is_first_visible_component(componentObject),
15698                    });
15699                    classString = refinement_result.class_string;
15700                    element_style = refinement_result.element_style;
15701
15702                    //overlay opacity
15703                    var opacity = 0;
15704                    if( formatting.overlay_opacity ){
15705                        opacity = formatting.overlay_opacity;
15706                    }
15707                    //photo album id
15708                    var photo_album = config_latest.photo_album;
15709                    if( ! photo_album ){
15710                        photo_album = {
15711                            id: null,
15712                            name: null,
15713                        };
15714                    }
15715                    //form id
15716                    var form = config_latest.form;
15717                    if( ! form ){
15718                        form = {
15719                            id: null,
15720                            name: null,
15721                        };
15722                    }
15723                    //image with text
15724                    var image_mat = config_latest.image_mat ? config_latest.image_mat : 'show';
15725                    //filter types (event list)
15726                    var filter_types = config_latest.filter_types ? JSON.stringify(config_latest.filter_types) : null;
15727                    //filter tag IDs (event list)
15728                    var filter_tag_ids = config_latest.filter_tag_ids ? JSON.stringify(config_latest.filter_tag_ids) : null;
15729                    //event_dates (event list)
15730                    var event_dates = config_latest.event_dates ? config_latest.event_dates : 'upcoming';
15731                    //caption style
15732                    if( formatting.caption_style ){
15733                        classString += " caption_style-" + formatting.caption_style;
15734                    }
15735                    //caption alignment
15736                    if( formatting.caption_align && ( formatting.caption_align == "top" || formatting.caption_align == "bottom" ||  formatting.caption_align == "under" ) ){
15737                        classString += ' ' + formatting.caption_align + '-align-image-caption';
15738                    }
15739                    var show_caption = formatting.show_caption;
15740                    if( ! show_caption ){
15741                        show_caption = false;
15742                    }
15743                    var caption_size = formatting.caption_size;
15744                    if( ! caption_size ){
15745                        caption_size = "24";
15746                    }
15747                    var subtext_size = formatting.subtext_size;
15748                    if( ! subtext_size ){
15749                        subtext_size = "24";
15750                    }
15751                    var link_label_size = formatting.link_label_size;
15752                    if( ! link_label_size ){
15753                        link_label_size = "24";
15754                    }
15755                    var text_position = formatting.text_position;
15756                    if( ! text_position ){
15757                        text_position = "center";
15758                    }
15759                    var caption_font_weight = formatting.caption_font_weight;
15760                    if( ! caption_font_weight ){
15761                        caption_font_weight = "400";
15762                    }
15763                    var image_grid_overlay_opacity = formatting.image_grid_overlay_opacity;
15764                    if( ! image_grid_overlay_opacity ){
15765                        image_grid_overlay_opacity = "40";
15766                    }
15767                    var image_grid_border_radius = formatting.image_grid_border_radius;
15768                    if ( ! image_grid_border_radius ){
15769                        image_grid_border_radius = "0"
15770                    }
15771                    var image_grid_layout = formatting.layout;
15772                    if ( ! image_grid_layout ){
15773                        image_grid_layout = "grid"
15774                    }
15775                    //add all variables to attribute object
15776                    attributes['client_id'] = componentObject.get('client_id');
15777                    attributes['component_type'] = componentObject.get('type');
15778                    if( componentObject.get('config') && componentObject.get('config').event_types ){
15779                        attributes['event_types'] = componentObject.get('config').event_types;
15780                    }
15781                    // attributes['images'] = componentObject.get('images');
15782                    attributes['type'] = type;
15783                    attributes['config_latest'] = config_latest;
15784                    attributes['data_source'] = config_latest['data_source'];
15785                    attributes['images'] = images;
15786                    attributes['formatting'] = JSON.stringify(formatting);
15787                    attributes['color_scheme'] = color_scheme;
15788                    attributes['classString'] = classString;
15789                    attributes['style_string'] = style_string;
15790                    attributes['elements'] = config_latest['elements'];
15791                    attributes['element_style'] = element_style;
15792                    attributes['opacity'] = opacity;
15793                    attributes['extra_columns'] = extra_columns;
15794                    attributes['images_per_row'] = images_per_row;
15795                    attributes['photos_per_row'] = photos_per_row;
15796                    attributes['left_right_orientation'] = left_right_orientation;
15797                    attributes['columns'] = config_latest.columns;
15798                    attributes['number_of_columns'] = number_of_columns;
15799                    attributes['show_caption'] = show_caption;
15800                    attributes['caption_size'] = caption_size;
15801                    attributes['subtext_size'] = subtext_size;
15802                    attributes['link_label_size'] = link_label_size;
15803                    attributes['text_position'] = text_position;
15804                    attributes['caption_font_weight'] = caption_font_weight;
15805                    attributes['image_grid_overlay_opacity'] = image_grid_overlay_opacity;
15806                    attributes['image_grid_border_radius'] = image_grid_border_radius;
15807                    attributes['image_grid_layout'] = image_grid_layout;
15808                    attributes['background_image_style'] = background_image_style;
15809                    attributes['photo_album'] = photo_album;
15810                    attributes['form'] = form;
15811                    attributes['filter_types'] = filter_types;
15812                    attributes['filter_tag_ids'] = filter_tag_ids;
15813                    attributes['event_dates'] = event_dates;
15814                    attributes['image_mat'] = image_mat;
15815                    let section_element_animation = refinement_result.section_element_animation || '';
15816                    attributes['section_element_animation'] = section_element_animation;
15817                    //return requested attribute
15818                    return attributes[attribute];
15819                }
15820            },
15821        },
15822        computed: {
15823            can_open_visibility_settings(){
15824                return typeof window.wb_open_component_visibility_settings === 'function';
15825            },
15826        },
15827    });
15828</script>
15828
15829    <!-- overlays  -->
15830    
15830<script type="text/html" id="template-view-all-events">
15831    <bottom-sheet-overlay
15832        id="overlay_view_all_events" 
15833        @dismiss_overlay="dismiss_overlay()"
15834    >
15835        <template #header>
15836            <div class="sticky-wrap overflowHidden">
15837                <div class="posScrollbar table-tab-wrap noPaddingBottom">
15838                    <div class="flexNoWrap table-tab-inner">
15839                        <div class="table-tab-option" :class="{'active': date_filter === 'all'}" @click="handleDatesFilter('all')" style="margin-left:22px;">All</div>
15840                        <div class="table-tab-option noMarginLeft" :class="{'active': date_filter === 'upcoming'}" @click="handleDatesFilter('upcoming')">Upcoming</div>
15841                        <div class="table-tab-option noMarginLeft" :class="{'active': date_filter === 'past'}" @click="handleDatesFilter('past')">Past</div>
15842                    </div>
15843                </div>
15844            </div>
15845            <div class="searchBarWrap inline-search relative noMargin overflowHidden">
15846                <div class="inputWrap searchBar searchBar_event-list flexGrowOne boxShadowBottom maxFourHundo">
15847                    <input 
15848                        class="easeFast" 
15849                        placeholder="Search events..."
15850                        v-model="search"
15851                    >
15852                </div>
15853            </div>
15854        </template>
15855
15856        <!-- body content -->
15857        <template #body>
15858            <div 
15859                class="event-list-outer-contain lightBorderTop scrollable" 
15860                :class="class_string" 
15861                :style="style_string"
15862            >
15863                <div 
15864                    class="event-list-wrap centeredBlock centeredText event-list-inner-contain" 
15865                >
15866                    <div class="fff noPadding overflowHidden">
15867                        <div v-if="!loading && sorted_event_data.length >= 1" class="tableInsert hundop flexWrapEvent">
15868                            <template 
15869                                v-for="(event_data, index) in sorted_event_data" 
15870                                :key="event_key(event_data)"
15871                            >
15872                                <p 
15873                                    v-if="is_first_visible_event_of_month(event_data)"
15874                                    class="cs-section-header"
15875                                >{{ month_string( event_data ) }}</p>
15876                                <event-list-event-component
15877                                    :event_data="event_data"
15878                                    :event_props="return_event_props( event_data )"
15879                                    @tapped-event-list-event="tapped_event_list_event"
15880                                ></event-list-event-component>
15881                            </template>
15882                        </div>
15883                        <no-objects-found 
15884                            v-if="!loading && sorted_event_data.length == 0"
15885                            subtext="No events found"
15886                            icon_class="fa-light fa-calendar-days"
15887                        ></no-objects-found>
15888                    </div>
15889                    <div v-if="loading" class="cardBodySpinnerWrap active relative">
15890                        <div class="cardBodySpinner centeredBlock"></div>
15891                    </div>
15892                </div>
15893            </div>
15894        </template>
15895    
15896    </bottom-sheet-overlay>
15897</script>
vendor: 1 bytes, line 15897
15897
15898<script>
15899
15900    function view_all_events( params ){
15901        store.components.view_all_events.event_data = params.event_data;
15902        store.components.view_all_events.class_string = params.class_string;
15903        store.components.view_all_events.style_string = params.style_string;
15904        store.components.view_all_events.element_style_string = params.element_style_string;
15905        store.components.view_all_events.props_generator = params.props_generator;
15906        store.overlays.view_all_events = true;
15907    }
15908    //global store
15909    store.components["view_all_events"] = {
15910        event_data: [],
15911        date_filter: 'upcoming',
15912        search: '',
15913        loading: false,
15914        sortObject: {
15915            order: 'ascending',
15916            key: 'startDate'
15917        },
15918        //passed props
15919        class_string: '',
15920        style_string: '',
15921        element_style_string: '',
15922        props_generator: null, //function to generate props for the individual events
15923    };
15924
15925    //define the component
15926    app_overlays.component("view-all-events-overlay", {
15927        template: "#template-view-all-events",
15928        data() {
15929            return store.components.view_all_events
15930        },
15931        mounted(){
15932            
15933        },
15934        methods: {
15935            month_string( event_data ){
15936                let start_date = new Date(event_data.ts);
15937                return format_utc_date_v2({
15938                    "utcDate": start_date,
15939                    "timezone": store.timezone,
15940                    "formatString": "MMMM YYYY"
15941                });
15942            },
15943            event_key( event_data ){
15944                return event_data.event.id + '_' + event_data.ts;
15945            },
15946            is_first_visible_event_of_month( event_data ){
15947                return this.first_visible_events.includes( this.event_key(event_data) );
15948            },
15949            return_event_props( event_data ){
15950                return this.props_generator( event_data );
15951            },
15952            sortEventList( event_data, sortObject ){
15953                event_data = [... event_data];
15954                if( ! sortObject ){
15955                    sortObject = {}
15956                }
15957                if( ! sortObject.key ){
15958                    sortObject.key = "startDate";
15959                }
15960                if( ! sortObject.order ){
15961                    sortObject.order = "ascending"
15962                }
15963                if( sortObject.key == "name" ){
15964                    if( sortObject.order == "ascending" ){
15965                        return event_data.sort( this.sortByEventName_ascending );
15966                    }else{
15967                        return event_data.sort( this.sortByEventName_descending );
15968                    }
15969                }else if( sortObject.key == "startDate" ){
15970                    if( sortObject.order == "ascending" ){
15971                        return event_data.sort( this.sortByStartDate_event_ascending );
15972                    }else{
15973                        return event_data.sort( this.sortByStartDate_event_descending );
15974                    }
15975                }else if( sortObject.key == "endDate" ){
15976                    if( sortObject.order == "ascending" ){
15977                        return event_data.sort( this.sortByEndDate_event_ascending );
15978                    }else{
15979                        return event_data.sort( this.sortByEndDate_event_descending );
15980                    }
15981                }
15982                return event_data;
15983            },
15984            sortByEventName_ascending( a, b ){
15985                let event_a = a.event;
15986                let event_b = b.event;
15987                var aName = '';
15988                if( event_a.get('name') ){
15989                    aName += event_a.get('name').trim().toLowerCase();
15990                }
15991                var bName = '';
15992                if( event_b.get('name') ){
15993                    bName += event_b.get('name').trim().toLowerCase();
15994                }
15995                if(aName < bName) { return -1; }
15996                if(aName > bName) { return 1; }
15997                //they had the same name
15998                return 0;
15999            },
16000            sortByEventName_descending( a, b ){
16001                let event_a = a.event;
16002                let event_b = b.event;
16003                var aName = '';
16004                if( event_a.get('name') ){
16005                    aName += event_a.get('name').trim().toLowerCase();
16006                }
16007                var bName = '';
16008                if( event_b.get('name') ){
16009                    bName += event_b.get('name').trim().toLowerCase();
16010                }
16011                if(aName > bName) { return -1; }
16012                if(aName < bName) { return 1; }
16013                //they had the same name
16014                return 0;
16015            },
16016            sortByStartDate_event_ascending( a, b ){
16017                let a_ts = a.ts;
16018                let b_ts = b.ts;
16019                if(a_ts < b_ts) { return -1; }
16020                if(a_ts > b_ts) { return 1; }
16021                return 0;
16022            },
16023            sortByStartDate_event_descending( a, b ){
16024                let a_ts = a.ts;
16025                let b_ts = b.ts;
16026                if(a_ts > b_ts) { return -1; }
16027                if(a_ts < b_ts) { return 1; }
16028                return 0;
16029            },
16030            sortByEndDate_event_ascending( a, b ){
16031                let a_end_ts = a.end_ts;
16032                let b_end_ts = b.end_ts;
16033                if(a_end_ts < b_end_ts) { return -1; }
16034                if(a_end_ts > b_end_ts) { return 1; }
16035                return 0;
16036            },
16037            sortByEndDate_event_descending( a, b ){
16038                let a_end_ts = a.end_ts;
16039                let b_end_ts = b.end_ts;
16040                if(a_end_ts < b_end_ts) { return 1; }
16041                if(a_end_ts > b_end_ts) { return -1; }
16042                return 0;
16043            },
16044            tapped_event_list_event( event_data ){
16045                let event = event_data.event;
16046                if(event && event.className == 'events') {
16047                    $('#overlay_event-description-popup h3').text(event.get('name'));
16048                    let start_date = this.start_date( event_data );
16049                    let nice_date = format_utc_date_v2({
16050                        "utcDate": start_date,
16051                        "timezone": store.timezone,
16052                        "formatString": "MMM D, YYYY"
16053                    });
16054                    $('#overlay_event-description-popup .metric_start_date').text( nice_date );
16055                    if( this.show_time( event ) ){
16056                        let event_time = format_utc_date_v2({
16057                            "utcDate": start_date,
16058                            "timezone": store.timezone,
16059                            "formatString": "h:mm A"
16060                        });
16061                        $('#overlay_event-description-popup .metric_start_date_time').text( event_time ).removeClass('incomplete');
16062                    }else{
16063                        $('#overlay_event-description-popup .metric_start_date_time').text('Not provided').addClass('incomplete');
16064                    }
16065                    if(event.get('description')) {
16066                        $('#overlay_event-description-popup .metric_description').html(event.get('description')).removeClass('incomplete');
16067                    } else {
16068                        $('#overlay_event-description-popup .metric_description').text('Not provided').addClass('incomplete');
16069                    }
16070                    fadeIn('#overlay_event-description-popup');
16071                }
16072            },
16073            start_date( event_data ){
16074                if( event_data.ts ){
16075                    return new Date(event_data.ts);
16076                }
16077                return null;
16078            },
16079            show_time( event ){
16080                if(
16081                    event.get('calendarOnly') && event.get('all_day')
16082                ){
16083                    return false;
16084                }else if(
16085                    event.className == "camps" ||
16086                    event.className == "regattas" ||
16087                    event.className == "subevents"
16088                ){
16089                    return false;
16090                }
16091                return true;
16092            },
16093            handleDatesFilter( option ){
16094                this.date_filter = option;
16095                // handle sortObject["order"] here instead of in loadEventList post-retrieval for better UX
16096                if ( option === 'upcoming' ){
16097                    this.sortObject["order"] = "ascending";
16098                } else {
16099                    this.sortObject["order"] = "descending";
16100                }
16101            },
16102            dismiss_overlay(){
16103                store.overlays.view_all_events = false;
16104            }
16105        },
16106        computed: {
16107            first_visible_events(){
16108                let first_visible_events = [];
16109                let handled_keys = [];
16110                //loop through the event_data
16111                this.sorted_event_data.forEach(event_data => {
16112                    let start_date = new Date(event_data.ts);
16113                    let start_components = return_date_components_v2({
16114                        "date": start_date,
16115                        "timezone": store.timezone
16116                    });
16117                    let key = start_components.year + '_' + start_components.month;
16118                    if( ! handled_keys.includes(key) ){
16119                        first_visible_events.push( this.event_key(event_data) );
16120                        handled_keys.push(key);
16121                    }
16122                });
16123                return first_visible_events;
16124            },
16125            pre_filtered_event_data(){
16126                //filter by date first
16127                let pre_filtered_event_data = [...this.event_data];
16128                var timestamp = new Date().getTime();
16129                if ( this.date_filter !== 'all' ){
16130                    pre_filtered_event_data = pre_filtered_event_data.filter(event_data => {
16131                        let start = event_data.ts;
16132                        let end = event_data.end_ts;
16133                        if (this.date_filter === 'past'){
16134                            if( start < timestamp && ( end < timestamp || ! end || end == "undefined" ) ){
16135                                return true;
16136                            }
16137                        } else if (this.date_filter === 'upcoming'){
16138                            if ( start > timestamp || end > timestamp ){
16139                                return true;
16140                            }
16141                            // Single-day events: ts and end_ts are both midnight, so they get filtered out
16142                            // when the event is today. Include if the event's date is today or later.
16143                            if ( start === end || !end ){
16144                                let startOfEventDay = new Date(start);
16145                                startOfEventDay.setUTCHours(0, 0, 0, 0);
16146                                let startOfToday = new Date();
16147                                startOfToday.setUTCHours(0, 0, 0, 0);
16148                                if ( startOfEventDay.getTime() >= startOfToday.getTime() ){
16149                                    return true;
16150                                }
16151                            }
16152                        }
16153                        return false;
16154                    })
16155                }
16156                return pre_filtered_event_data;
16157            },
16158            filtered_event_data(){
16159                let filtered_event_data = this.pre_filtered_event_data;
16160                //filter by search
16161                if (this.search.length > 0){
16162                    filtered_event_data = filtered_event_data.filter(event_data => {
16163                        let name = event_data.event.get('name').toLowerCase();
16164                        if ( name.includes(this.search.toLowerCase()) ){
16165                            return event_data;
16166                        }
16167                    })
16168                }
16169                // handle tab selection using filteredEvents 
16170                var yesterday = new Date();
16171                yesterday.setDate( yesterday.getDate() - 1 );
16172                var timestamp = yesterday.getTime();
16173
16174                //filter by tag
16175                if( this.selected_event_tag_id ){
16176                    filtered_event_data = filtered_event_data.filter(event_data => {
16177                        let event_tag_ids = event_data.event_tag_ids;
16178                        return event_tag_ids && event_tag_ids.includes(this.selected_event_tag_id);
16179                    })
16180                }
16181                //return them
16182                return filtered_event_data;
16183            },
16184            sorted_event_data(){
16185                return this.sortEventList( this.filtered_event_data, this.sortObject );
16186            },
16187        }
16188    });
16189
16190</script>
16190    
16190<script type="text/html" id="template-view-photo-carousel">
16191    <div 
16192        id="overlay_view-photo-carousel"
16193        class="carousel-v-overlay"
16194        :class="{ 'carousel-v-overlay-blur': should_blur_background }"
16195        @touchstart="touched_overlay()"
16196        @mousedown="touched_overlay()"
16197        @click.self="clicked_overlay()"
16198    >
16199        <div 
16200            class="carousel-container"
16201            @click.self="clicked_overlay()"
16202        >
16203            <div class="actions-container" @click.stop>
16204                <div
16205                    class="event-page-callout close-action-container"
16206                    :class="{ 'carousel-action-icon-only': use_icon_only_actions }"
16207                    style="color: #fff;"
16208                    @click.stop="dismiss_overlay()"
16209                >
16210                    <i class="fa-regular fa-xmark" style="color: #fff;"></i>
16211                    <template v-if="!use_icon_only_actions">Close</template>
16212                </div>
16213                <div
16214                    v-if="can_show_download"
16215                    class="event-page-callout"
16216                    :class="{ 'carousel-action-icon-only': use_icon_only_actions }"
16217                    style="color: #fff;"
16218                    @click.stop="tapped_download()"
16219                >
16220                    <template v-if="!downloading">
16221                        <i class="fa-regular fa-arrow-down-to-line" style="color: #fff;"></i>
16222                        <template v-if="!use_icon_only_actions">Download</template>
16223                    </template>
16224                    <template v-else>
16225                        <i class="fa-regular fa-check" style="color: #fff;"></i>
16226                        <template v-if="!use_icon_only_actions">Downloaded</template>
16227                    </template>
16228                </div>
16229                <div
16230                    v-if="can_show_share"
16231                    class="event-page-callout"
16232                    :class="{ 'carousel-action-icon-only': use_icon_only_actions }"
16233                    style="color: #fff;"
16234                    @click.stop="tapped_share()"
16235                >
16236                    <template v-if="!sharing">
16237                        <i class="fa-regular fa-arrow-up-from-bracket" style="color: #fff;"></i>
16238                        <template v-if="!use_icon_only_actions">Share</template>
16239                    </template>
16240                    <template v-else>
16241                        <i class="fa-regular fa-check" style="color: #fff;"></i>
16242                        <template v-if="!use_icon_only_actions">Link copied</template>
16243                    </template>
16244                </div>
16245            </div>
16246            <i v-if="!hide_nav_arrows" class="fa-light fa-chevron-left nav-arrow" @click.stop="prev()"></i>
16247            <div v-if="current_photo" class="carousel-photo-container" @click="clicked_overlay()">
16248                <div class="carousel-photo-frame" @click.stop>
16249                    <video 
16250                        v-if="returnIsVideo(current_photo.file_name) && current_photo.url"
16251                        class="carousel-photo" 
16252                        :class="{ 'is-loaded': detail_photo_loaded }"
16253                        autoplay muted loop playsinline
16254                        :key="'video-' + current_index"
16255                        @loadeddata="detail_photo_loaded = true"
16256                    >
16257                        <source :src="current_photo.url" type="video/mp4">
16258                    </video>
16259                    <img
16260                        v-else-if="current_photo.url"
16261                        class="carousel-photo"
16262                        :class="{ 'is-loaded': detail_photo_loaded }"
16263                        :src="current_photo.url"
16264                        :key="'photo-' + current_index"
16265                        alt=""
16266                        ref="detail_photo"
16267                        @load="detail_photo_loaded = true"
16268                    >
16269                    <template v-if="show_caption">
16270                        <p class="carousel-caption" v-if="current_photo.caption" :style="{ 'font-size': caption_size + 'px', 'font-weight': caption_font_weight }">{{ current_photo.caption }}</p>
16271                    </template>
16272                </div>
16273            </div>
16274            <div v-else class="carousel-photo-container" @click="clicked_overlay()">
16275                <p class="carousel-caption">No media found</p>
16276            </div>
16277            <i v-if="!hide_nav_arrows" class="fa-light fa-chevron-right nav-arrow" @click.stop="next()"></i>
16278        </div>
16279    </div>
16280</script>
16280
16281
16282<style>
16283    .carousel-v-overlay{
16284        display: flex;
16285        align-items: center;
16286        justify-content: center;
16287        cursor: auto;
16288        background-color: transparent;
16289        position: fixed;
16290        z-index: 1000 !important;
16291        left: 0px;
16292        right: 0px;
16293        top: 0px;
16294        bottom: 0px;
16295        width: 100vw;
16296        height: 100vh;
16297    }
16298    .carousel-v-overlay::before{
16299        content: '';
16300        position: absolute;
16301        inset: 0;
16302        z-index: 0;
16303        pointer-events: none;
16304        background-color: rgba(8,8,19,0.64);
16305        opacity: 0;
16306        animation: carousel-overlay-fade 0.3s ease forwards;
16307    }
16308    .carousel-v-overlay-blur::before{
16309        backdrop-filter: blur(6px);
16310    }
16311    @keyframes carousel-overlay-fade {
16312        0% {
16313            opacity: 0;
16314        }
16315        100% {
16316            opacity: 1;
16317        }
16318    }
16319    @keyframes carousel-chrome-fade {
16320        0% {
16321            opacity: 0;
16322        }
16323        100% {
16324            opacity: 1;
16325        }
16326    }
16327    @keyframes carousel-photo-scale {
16328        0% {
16329            transform: scale(0.8);
16330        }
16331        100% {
16332            transform: scale(1);
16333        }
16334    }
16335    .carousel-container{
16336        display: flex;
16337        justify-content: space-between;
16338        align-items: center;
16339        height: 100vh;
16340        width: 98vw;
16341        position: relative;
16342        z-index: 1;
16343        overflow: hidden;
16344        cursor: auto;
16345        background-color: rgba(0, 0, 0, 0);
16346    }
16347    .carousel-container > .nav-arrow {
16348        font-size: 28px;
16349        padding: 1em;
16350        color: #fff;
16351        z-index: 1;
16352        opacity: 0;
16353        animation: carousel-nav-fade 0.3s ease forwards;
16354        transition: opacity 0.3s ease;
16355        cursor: pointer;
16356    }
16357    @keyframes carousel-nav-fade {
16358        0% {
16359            opacity: 0;
16360        }
16361        100% {
16362            opacity: 0.6;
16363        }
16364    }
16365    .carousel-container > .nav-arrow:hover {
16366        opacity: 1;
16367    }
16368    .carousel-photo-container {
16369        display: flex;
16370        flex-direction: column;
16371        justify-content: center;
16372        align-items: center;
16373        flex: 1 1 auto;
16374        width: auto;
16375        height: 70%;
16376        min-width: 0;
16377        min-height: 200px;
16378        cursor: pointer;
16379    }
16380    .carousel-photo-frame {
16381        display: flex;
16382        flex-direction: column;
16383        justify-content: center;
16384        align-items: center;
16385        max-width: 100%;
16386        max-height: 100%;
16387        cursor: auto;
16388        animation: carousel-photo-scale 0.3s ease forwards;
16389    }
16390    .carousel-photo {
16391        width: auto;
16392        height: auto;
16393        max-width: 100%;
16394        max-height: 70vh;
16395        object-fit: contain;
16396        background-size: contain;
16397        background-repeat: no-repeat;
16398        background-position: center;
16399        opacity: 0;
16400        transition: opacity 0.35s ease;
16401    }
16402    .carousel-photo.is-loaded {
16403        opacity: 1;
16404    }
16405    .carousel-caption{
16406        margin: 10px 0 0 0;
16407        text-transform: uppercase;
16408        letter-spacing: 2px;
16409        color: #fff;
16410        padding: 5px;
16411        border-radius: 2px;
16412        background-color: rgba(0, 0 ,0, 0.1);
16413    }
16414    .actions-container{
16415        position: fixed;
16416        top: 9px;
16417        right: 9px;
16418        display: flex;
16419        align-items: center;
16420        justify-content: center;
16421        opacity: 0;
16422        animation: carousel-chrome-fade 0.3s ease forwards;
16423    }
16424    .actions-container > .event-page-callout.close-action-container{
16425        position: fixed;
16426        top: 9px;
16427        left: 9px;
16428    }
16429    .actions-container > .event-page-callout{
16430        padding-left: 10px;
16431        padding-right: 10px;
16432        border-radius: 5px;
16433        border: 1px solid #e4ebf1;
16434        font-size: 13px;
16435        margin: 6px;
16436        color: #6a7482;
16437        padding-bottom: 8px;
16438        padding-top: 8px;
16439        cursor: pointer;
16440    }
16441    .actions-container > .event-page-callout i{
16442        padding-right:3px;
16443    }
16444    .actions-container > .event-page-callout:hover{
16445        color: #081333;
16446    }
16447    .actions-container > .event-page-callout.carousel-action-icon-only{
16448        border: none;
16449        background: transparent;
16450        padding: 6px;
16451        margin: 4px;
16452        min-width: auto;
16453    }
16454    .actions-container > .event-page-callout.carousel-action-icon-only i{
16455        padding-right: 0;
16456        font-size: 28px;
16457    }
16458</style>
16459
16460<script>
16461
16462    function downloadImage( url, filename ) {
16463        return fetch(url, { mode: 'cors' })
16464            .then(response => {
16465                if (!response.ok) {
16466                    throw new Error(`Network response was not ok: ${response.statusText}`);
16467                }
16468                return response.blob();
16469            })
16470            .then(blob => {
16471                const url = window.URL.createObjectURL(blob);
16472                const a = document.createElement('a');
16473                a.style.display = 'none';
16474                a.href = url;
16475                a.download = filename;
16476                // Append the anchor to the body
16477                document.body.appendChild(a);
16478                a.click();
16479                // Remove the anchor from the body
16480                document.body.removeChild(a);
16481                window.URL.revokeObjectURL(url);
16482            })
16483            .catch(err => console.error('Error downloading the image:', err));
16484    }
16485
16486    store.components.view_photo_carousel = {
16487        json_photos: [],
16488        current_index: 0,
16489        downloading: false,
16490        sharing: false,
16491        detail_photo_loaded: false,
16492        json_config: {
16493            allow_share: false, 
16494            allow_download: false, 
16495            context: '',
16496        },
16497    }
16498    
16499    app_overlays.component("view-photo-carousel", {
16500        template: "#template-view-photo-carousel",
16501        data() {
16502            return store.components.view_photo_carousel;
16503        },
16504        mounted() {
16505            this.bound_keydown = (event) => {
16506                this.handle_detail_keydown(event);
16507            };
16508            window.addEventListener('keydown', this.bound_keydown);
16509            this.$nextTick(() => {
16510                this.check_detail_photo_cached();
16511            });
16512        },
16513        unmounted() {
16514            if( this.bound_keydown ){
16515                window.removeEventListener('keydown', this.bound_keydown);
16516            }
16517        },
16518        watch: {
16519            current_index(){
16520                this.detail_photo_loaded = false;
16521                this.$nextTick(() => {
16522                    this.check_detail_photo_cached();
16523                });
16524            },
16525        },
16526        methods: {
16527            return_config_value( key, default_value ){
16528                if ( this.json_config && this.json_config[key] !== undefined && this.json_config[key] !== null ){
16529                    return this.json_config[key];
16530                }
16531                return default_value;
16532            },
16533            handle_detail_keydown( event ){
16534                if( !event ){
16535                    return;
16536                }
16537                let tag = event.target && event.target.tagName;
16538                if( tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' ){
16539                    return;
16540                }
16541                if( event.key === 'ArrowLeft' ){
16542                    event.preventDefault();
16543                    this.prev();
16544                    return;
16545                }
16546                if( event.key === 'ArrowRight' ){
16547                    event.preventDefault();
16548                    this.next();
16549                }
16550            },
16551            check_detail_photo_cached(){
16552                if( this.current_photo && this.returnIsVideo(this.current_photo.file_name) ){
16553                    this.detail_photo_loaded = true;
16554                    return;
16555                }
16556                let el = this.$refs.detail_photo;
16557                if( el && el.complete && el.naturalWidth ){
16558                    this.detail_photo_loaded = true;
16559                }
16560            },
16561            async tapped_download(){
16562                if ( !this.current_photo || !this.current_photo.url ){
16563                    return;
16564                }
16565                let url = this.current_photo.url;
16566                let file_name = this.current_photo.file_name || ('photo-' + (this.current_index + 1));
16567                this.downloading = true;
16568                try {
16569                    downloadImage( url, file_name ).then(() => {
16570                        displayMessage("File downloaded.", false, 6000);
16571                        this.downloading = false;
16572                    }).catch(() => {
16573                        displayMessage("Sorry, something went wrong with your download. Please try again, or contact support if the problem persists.", true, 8000);
16574                        this.downloading = false;
16575                    })
16576                } catch (error) {
16577                    displayMessage("Sorry, something went wrong with your download. Please try again, or contact support if the problem persists.", true, 8000);
16578                }
16579            },
16580            returnIsVideo(filename) {
16581                var ext = this.getExtension(filename);
16582                switch (ext.toLowerCase()) {
16583                    case 'm4v':
16584                    case 'avi':
16585                    case 'mpg':
16586                    case 'mp4':
16587                    case 'mp3':
16588                    case 'mov':
16589                    // etc
16590                    return true;
16591                }
16592                return false;
16593            },
16594            getExtension(filename) {
16595                var parts = filename.split('.');
16596                return parts[parts.length - 1];
16597            },
16598            tapped_share(){
16599                copyToClipboard( window.location.href );
16600                displayMessage("Link copied.", false, 6000);
16601                this.sharing = true;
16602                setTimeout(() => {
16603                    this.sharing = false;
16604                }, 3000);
16605            },
16606            prev(){
16607                if ( this.current_index === 0 ){
16608                    this.current_index = this.json_photos.length - 1;
16609                } else {
16610                    this.current_index -= 1;
16611                }
16612            },
16613            next(){
16614                if ( this.current_index === this.json_photos.length - 1 ){
16615                    this.current_index = 0;
16616                } else {
16617                    this.current_index += 1;
16618                }
16619            },
16620            dismiss_overlay(){
16621                store.overlays.view_photo_carousel = false;
16622            },
16623            clicked_overlay(){
16624                if( ! should_close_overlay( this.last_touch ) ){
16625                    return
16626                }
16627                this.dismiss_overlay();
16628            },
16629            touched_overlay(){
16630                this.last_touch = new Date().getTime();
16631            },
16632        },
16633        computed: {
16634            current_photo(){
16635                if ( this.json_photos && this.json_photos.length > 0 && this.json_photos[this.current_index] ){
16636                    return this.json_photos[this.current_index];
16637                }
16638                return null;
16639            },
16640            can_show_download(){
16641                var default_value = false;
16642                return this.return_config_value('allow_download', default_value) === true;
16643            },
16644            can_show_share(){
16645                var default_value = true;
16646                return this.return_config_value('allow_share', default_value) === true;
16647            },
16648            show_caption(){
16649                var default_value = false;
16650                return this.return_config_value('show_caption', default_value) === true;
16651            },
16652            caption_size(){
16653                var default_value = 16;
16654                return this.return_config_value('caption_size', default_value);
16655            },
16656            caption_font_weight(){
16657                var default_value = 400;
16658                return this.return_config_value('caption_font_weight', default_value);
16659            },
16660            should_blur_background(){
16661                var default_value = false;
16662                return this.return_config_value('blur_background', default_value) === true;
16663            },
16664            use_icon_only_actions(){
16665                var default_value = false;
16666                return this.return_config_value('icon_only_actions', default_value) === true;
16667            },
16668            hide_nav_arrows(){
16669                let default_value = false;
16670                return this.return_config_value('hide_nav_arrows', default_value) === true;
16671            },
16672        }
16673    });
16674    
16675</script>
16675    
16675<script type="text/html" id="template-bottom-sheet-overlay">
16676
16677    <div 
16678        class="v-overlay scrollable hugePaddingBottom active" 
16679        @touchstart.self="touched_overlay()"
16680        @mousedown.self="touched_overlay()"
16681        @click.self="clicked_overlay()"
16682        :class="[{'noPointer': prevent_close}, {'bottom-sheet-blur': blur_background}]"
16683    >
16684        <div 
16685            class="standardOverlayCard centeredBlock bottom-sheet"
16686            :class="[
16687                {'short': height == 'short' || is_short}, 
16688                {'narrow': narrow},
16689                {'noMaxWidth': full_width}
16690            ]"
16691        >
16692
16693            <!-- generic header content -->
16694            <div class="bottom-sheet-overlay-header">
16695
16696                <div 
16697                    v-if="! prevent_close"
16698                    class="relative"
16699                >
16700                    <div 
16701                        class="overlay-header-handle-wrap centeredBlock"
16702                        @click.stop="toggle_height()" 
16703                    >
16704                        <div class="overlay-header-handle"></div>
16705                    </div>
16706                    <i 
16707                        class="overlay-x-button fa-regular fa-xmark pointer"
16708                        @click.stop="dismiss_overlay()"
16709                    ></i>
16710                </div>
16711
16712                <!-- header content injected here -->
16713                <slot name="header"></slot>
16714
16715            </div>
16716
16717            <!-- body content injected here -->
16718            <slot name="body"></slot>
16719
16720            <!-- footer content injected here -->
16721            <slot name="footer"></slot>
16722
16723        </div>
16724    </div>
16725
16726</script>
16726
16727
16728<style>
16729
16730    .standardOverlayCard.bottom-sheet.narrow{
16731        max-width: 600px;
16732    }
16733
16734    .standardOverlayCard.bottom-sheet.medium{
16735        max-width: 800px;
16736    }
16737
16738    .standardOverlayCard.bottom-sheet{
16739        position: fixed;
16740        bottom: 0px;
16741        top: 50px;
16742        left: 0px;
16743        right: 0px;
16744        z-index: 1001;
16745        background-color: #fff;
16746        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
16747        transition: bottom 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
16748        border-top-left-radius: 10px;
16749        border-top-right-radius: 10px;
16750        padding-bottom: 10px;
16751        padding-top: 10px;
16752        max-width: 1100px;
16753        margin-top: 0px;
16754        border-bottom-left-radius: 0px;
16755        border-bottom-right-radius: 0px;
16756    }
16757
16758    .standardOverlayCard.bottom-sheet.short{
16759        top: 50%;
16760    }
16761
16762    @media (max-height: 600px) {
16763
16764        .standardOverlayCard.bottom-sheet.short{
16765            top: 150px;
16766        }
16767
16768    }
16769
16770    .standardOverlayCard.bottom-sheet h3{
16771        padding-top:0px;
16772    }
16773
16774    .standardOverlayCard.bottom-sheet{
16775        display: flex;
16776        flex-direction: column;
16777        padding-bottom:0px;
16778        padding-top: 0px;
16779    }
16780
16781    .standardOverlayCard.bottom-sheet .bottom-sheet-overlay-card-header
16782    {
16783        flex: 0 0 auto; /* Fixed height, doesn't grow or shrink */
16784    }
16785
16786    .standardOverlayCard.bottom-sheet .standardCardFooter{
16787        position: absolute;
16788        bottom: 0px;
16789        padding-bottom: var(--cs-safe-area-bottom, 10px);
16790        left: 0px;
16791        right: 0px;
16792        background-color: #fff;
16793        border-top: 1px solid #e4ebf1;
16794        z-index: 10;
16795    }
16796
16797    .standardOverlayCard.bottom-sheet .standardCardBody{
16798        flex: 1 1 auto; /* Flexible height, can grow and shrink */
16799        padding-bottom: 90px !important;
16800    }
16801
16802    .overlay-header-handle-wrap{
16803        padding: 10px;
16804        cursor: pointer;
16805        max-width: 120px;
16806    }
16807    .overlay-header-handle{
16808        width: 70px;
16809        height: 3px;
16810        border-radius: 10px;
16811        background-color: #acb3ba;
16812        margin-left: auto;
16813        margin-right: auto;
16814        margin-top: 8px;
16815        margin-bottom: 8px;
16816    }
16817    .overlay-x-button{
16818        position: absolute;
16819        right: 14px;
16820        color: #535e6c;
16821        font-size: 20px;
16822        top: 12px;
16823    }
16824</style>
16825
16826<script>
16827
16828    //define the component
16829    app_overlays.component("bottom-sheet-overlay", {
16830        template: "#template-bottom-sheet-overlay",
16831        data() {
16832            return {
16833                height: "tall"
16834            }
16835        },
16836        props: {
16837            blur_background: {
16838                type: Boolean,
16839                default: false
16840            },
16841            narrow: {
16842                type: Boolean,
16843                default: false
16844            },
16845            prevent_close: {
16846                type: Boolean,
16847                default: false
16848            },
16849            full_width: {
16850                type: Boolean,
16851                default: false
16852            },
16853            is_short: {
16854                type: Boolean,
16855                default: false
16856            }
16857        },
16858        mounted(){
16859            const universalEditOverlay = document.getElementById('overlay_universal-edit');
16860            if (universalEditOverlay) {
16861                universalEditOverlay.style.zIndex = "1000000";
16862            }
16863            const membershipValidationOverlay = document.getElementById('overlay_verifyMembership');
16864            if (membershipValidationOverlay) {
16865                membershipValidationOverlay.style.zIndex = "1000000";
16866            }
16867        },
16868        beforeUnmount() {
16869            //reset the z-index
16870            const universalEditOverlay = document.getElementById('overlay_universal-edit');
16871            if (universalEditOverlay) {
16872                universalEditOverlay.style.zIndex = ""; // Revert to default
16873            }
16874            const membershipValidationOverlay = document.getElementById('overlay_verifyMembership');
16875            if (membershipValidationOverlay) {
16876                membershipValidationOverlay.style.zIndex = ""; // Revert to default
16877            }
16878        },
16879        methods: {
16880            dismiss_overlay(){
16881                this.$emit('dismiss_overlay');
16882            },
16883            toggle_height(){
16884                if( this.height === "tall" ){
16885                    this.height = "short";
16886                }else{
16887                    this.height = "tall";
16888                }
16889            },
16890            clicked_overlay(){
16891                if( this.prevent_close ){
16892                    return;
16893                }
16894                if (should_close_overlay(this.last_touch)) {
16895                    this.dismiss_overlay();
16896                }
16897            },
16898            touched_overlay(){
16899                this.last_touch = new Date().getTime();
16900            },
16901        },
16902    });
16903
16904    
16905</script>
16905
16906
16907<style>
16908    .bottom-sheet-blur{
16909        backdrop-filter: blur(6px);
16910    }
16911
16912    @keyframes bottom-sheet-fade-in {
16913        from { opacity: 0; }
16914        to   { opacity: 1; }
16915    }
16916</style>    
16916<script type="text/html" id="template-show-event-details-in-member-cal">
16917    <bottom-sheet-overlay
16918        @dismiss_overlay="dismiss_overlay()"
16919        :narrow="true"
16920    >
16921        <!-- header content -->
16922        <template #header></template>
16923
16924        <!-- body content -->
16925        <template #body>
16926            <div 
16927                class="standardCardBody fff scrollable "
16928            >
16929                <h2 v-if="event_json.title" class="event-page-name centeredText marginTop">{{ event_json.title }}</h2>           
16930                <div class="flexWrap smallMarginBottom tinyMarginTop">
16931                    <p v-if="date_string" class="hundop flexNoWrapLeftAlign">
16932                        <i class="fa-light fa-calendar-week" style="font-size:20px;"></i>
16933                        <span class="leftText tinyMarginLeft">{{ date_string }}</span>
16934                    </p>
16935                    <p v-if="time_string" class="hundop flexNoWrapLeftAlign" style="margin-top: 12px;">
16936                        <i class="fa-light fa-clock" style="font-size:20px;"></i>
16937                        <span class="leftText tinyMarginLeft">{{ time_string }}</span>
16938                    </p>
16939                    <!-- in-portal booking links (eg. group classes surfaced in the member
16940                         portal booking hub) navigate within the SPA instead of opening the
16941                         standalone page in a new tab -->
16942                    <p v-if="event_json.in_portal_path" class="hundop flexNoWrapLeftAlign" style="margin-top: 12px;">
16943                        <i class="fa-light fa-link" style="font-size:20px;"></i>
16944                        <span class="leftText tinyMarginLeft">
16945                            <a class="pointer" @click="tapped_in_portal_link()">Follow link</a>
16946                        </span>
16947                    </p>
16948                    <p v-else-if="event_json.external_event_url" class="hundop flexNoWrapLeftAlign" style="margin-top: 12px;">
16949                        <i class="fa-light fa-link" style="font-size:20px;"></i>
16950                        <span class="leftText tinyMarginLeft">
16951                            <a target="_blank" :href="event_json.external_event_url">Follow link</a>
16952                        </span>
16953                    </p>
16954                </div>
16955
16956                <div
16957                    v-if="event_image_url"
16958                    style="display:flex; width:100%; justify-content:center;"
16959                >
16960                    <div
16961                        v-if="image_crop_style == 'cover'"
16962                        style="width:100%; max-width:580px; height:400px; position:relative; overflow:hidden; border:1px solid #e4ebf1; border-radius:5px; display:flex; align-items:center; justify-content:center;"
16963                    >
16964                        <img
16965                            :src="event_image_url"
16966                            style="width:100%; height:100%; display:block; object-fit:cover;"
16967                            @error="console.log('event image failed to load:', event_image_url)"
16968                        />
16969                    </div>
16970
16971                    <div
16972                        v-else
16973                        style="width:100%; max-width:580px; height:400px; position:relative; overflow:hidden; border:1px solid #e4ebf1; border-radius:5px;"
16974                    >
16975                        <div
16976                            :style="{ backgroundImage: 'url(' + event_image_url + ')', position: 'absolute', top: '0', left: '0', right: '0', bottom: '0', backgroundRepeat: 'no-repeat', backgroundPosition: 'center', backgroundSize: 'cover', filter: 'blur(6px)' }"
16977                        >
16978                            <div
16979                                style="position:absolute; top:0; right:0; left:0; bottom:0; background-color: rgba(253,253,253,0.8);"
16980                            ></div>
16981                        </div>
16982                        <div
16983                            :style="{ backgroundImage: 'url(' + event_image_url + ')', position: 'absolute', top: '0', left: '0', right: '0', bottom: '0', backgroundRepeat: 'no-repeat', backgroundPosition: 'center', backgroundSize: 'contain' }"
16984                        ></div>
16985                    </div>
16986                </div>
16987                <div
16988                    class="event-page-details flex-col"
16989                >
16990                    <div class="event-page-details-wrapper maxSixFifty flex-col centeredText">
16991                        <p 
16992                            v-if="event_json.description" 
16993                            class="smallMarginTop tiptap-container marginBottom" 
16994                            style="line-height: 1.5;" 
16995                            v-html="event_json.description">
16996                        </p>
16997                    </div>
16998                </div>
16999            </div>
17000        </template>
17001    
17002
17003    </bottom-sheet-overlay>
17004</script>
17004
17005
17006
17007<script>
17008
17009    function follow_member_cal_event_link(event_json) {
17010        if (event_json.in_portal_path) {
17011            updateURL(event_json.in_portal_path);
17012            displayViewForUrlPath();
17013            return true;
17014        }
17015        if (event_json.external_event_url) {
17016            window.open(event_json.external_event_url, '_blank');
17017            return true;
17018        }
17019        return false;
17020    }
17021
17022    function show_event_details_in_member_cal(params) {
17023        store.components.show_event_details_in_member_cal.event_json = params.event_json;
17024        store.overlays.show_event_details_in_member_cal = true;
17025    }
17026
17027    store.components["show_event_details_in_member_cal"] = {
17028        event_json: null,
17029    }
17030
17031    app_overlays.component("show-event-details-in-member-cal-overlay", {
17032        template: "#template-show-event-details-in-member-cal",
17033        data() {
17034            return store.components["show_event_details_in_member_cal"];
17035        },
17036        methods: {
17037            dismiss_overlay(){
17038                store.overlays.show_event_details_in_member_cal = false;
17039            },
17040            tapped_in_portal_link(){
17041                this.dismiss_overlay();
17042                follow_member_cal_event_link(this.event_json);
17043            },
17044        }, 
17045        computed: {
17046            event_image_url(){
17047                if(this.event_json && this.event_json.image && this.event_json.image.url){
17048                    return this.event_json.image.url;
17049                }
17050                return null;
17051            },
17052            image_crop_style(){
17053                if(this.event_json && this.event_json.image && this.event_json.image.crop){
17054                    return this.event_json.image.crop;
17055                }
17056                return null;
17057            },
17058            date_string(){
17059                var start_date_string = format_utc_date_v2({
17060                    utcDate: this.event_json.ts,
17061                    formatString: 'MMMM D, YYYY', 
17062                    timezone: store.clubObject.get('timezone')
17063                });
17064                if(this.event_json.isAllDay){
17065                    //it could span multiple days, check if we should show the end date
17066                    var endDate = this.event_json.end_ts || null;
17067                    if(endDate){
17068                        //compare the date/month/year only
17069                        var startDateObj = new Date(this.event_json.ts);
17070                        var endDateObj = new Date(endDate);
17071                        if(startDateObj.getUTCFullYear() !== endDateObj.getUTCFullYear() ||startDateObj.getUTCMonth() !== endDateObj.getUTCMonth() || startDateObj.getUTCDate() !== endDateObj.getUTCDate()){
17072                            //different dates, show the end date
17073                            start_date_string += ` - ${ format_utc_date_v2({
17074                                utcDate: endDate,
17075                                formatString: 'MMMM D, YYYY', 
17076                                timezone: store.clubObject.get('timezone')
17077                            })}`;
17078                        }
17079                    }
17080                }
17081                return start_date_string;
17082            },
17083            time_string(){
17084                if(this.event_json.isAllDay){
17085                    return "";
17086                }
17087                var startDate = this.event_json.ts;
17088                var endDate = this.event_json.end_ts || null;
17089                var time_string = format_utc_date_v2({
17090                    utcDate: this.event_json.ts,
17091                    formatString: 'h:mm A',
17092                    timezone: store.clubObject.get('timezone')
17093                });
17094                if(endDate && endDate !== startDate) {
17095                    time_string += ` - ${ format_utc_date_v2({
17096                        utcDate: endDate,
17097                        formatString: 'h:mm A',
17098                        timezone: store.clubObject.get('timezone')
17099                    })}`;
17100                }
17101                return time_string;
17102            }
17103        },
17104    });
17105</script>
17105
17106
17107
17108    <!-- define/register shared vue components  -->
17109    
17110    
17110<script type="text/html" id="template-calendar-date-picker">
17111    <vue-date-picker
17112        v-model="selected_date" 
17113        :mode="mode"
17114        :modes="['year', 'month']" 
17115        :monthPicker="true" 
17116        :yearPicker="true"
17117        @update:modelValue="date_changed()"
17118        :clearable="false"
17119        format="MMM-d-yyyy"
17120        auto-apply
17121        :teleport="true"
17122        :month-change-on-scroll="false"
17123        menu-class-name="cs-calendar-date-picker-menu"
17124        input-class-name="cs-calendar-date-picker-input"
17125    >
17126        <template #input-icon>
17127            <i class="fa-regular fa-calendar-arrow-down"></i>
17128        </template>
17129        <template #calendar-icon>
17130            <i class="fa-regular fa-arrow-turn-down-left"></i>
17131        </template>
17132        <template #arrow-left>
17133            <i class="fa-regular fa-chevron-left"></i>
17134        </template>
17135        <template #arrow-right>
17136            <i class="fa-regular fa-chevron-right"></i>
17137        </template>
17138    </vue-date-picker>
17139 </script>
17139
17140 
17141 <style>
17142
17143    .cs-calendar-date-picker-menu .dp__btn{
17144        color: #000000 !important;
17145        background: none !important; 
17146        font: inherit !important;
17147        line-height: normal !important;
17148     }
17149
17150     .cs-calendar-date-picker-input{
17151        width: 0px;
17152        padding-left: 26px;
17153        padding-right: 22px;
17154        text-align: center;
17155        margin-right: 8px;
17156        overflow: hidden;
17157        padding-top: 7px;
17158        padding-bottom: 7px;
17159        border: 1px solid #e4ebf1;
17160     }
17161
17162     .cs-calendar-menu-wrap .dp__input_icon i{
17163        font-size: 16px !important;
17164        transform: none;
17165     }
17166
17167 </style>
17168 
17169 
17169<script>
17170 
17171     app_content.component("calendar-date-picker-component", {
17172         template: "#template-calendar-date-picker",
17173         data() {
17174            return {
17175                selected_date: new Date(),
17176                mode: 'month',
17177            }
17178         },
17179         props: {
17180            handle_button_click: Function,
17181         },
17182         methods: {
17183            date_changed(){
17184                this.handle_button_click( 'date-picker', this.selected_date );
17185            },
17186         },
17187     });
17188 
17189 </script>
17189
17190 
17191 
17191<script>
17192     app_content.component('VueDatePicker', VueDatePicker);
17193 </script>
17193
17194     
17194<script type="text/html" id="template-calendar-grid">
17195    <div class="centeredOnMobile smallMarginTop" :class="{'ninetyPercent': !is_membership_cal, 'standardCard': !is_membership_cal}">
17196        <!-- main options header // stays consistent across all views -->
17197        <div class="standardCardBody noBorderTop calendar noPadding fff" style="overflow: none;">
17198            <!-- options for non-membership cal -->
17199            <div v-if="filterOptions?.length >= 1 && !is_membership_cal" class="cal-filter-option-wrap scrollable paddingLeftRight tinyPaddingTop smallPaddingBottom">
17200                <div class="flexNoWrap">
17201                    <template v-for="option in filterOptions">
17202                        <div
17203                            class="cs-cal-filter-option" 
17204                            :class="[
17205                                'cs-cal-filter-option_' + option.data_option,
17206                                { 'active': selectedFilterOptions.indexOf(option.data_option) !== -1 }
17207                            ]"
17208                            :data-option="option.data_option"
17209                            @click="$emit('handleFilterSelection', option)"
17210                        >
17211                            {{ option.name }}
17212                        </div>
17213                    </template>
17214                </div>
17215            </div>
17216            <!-- main calendar header // stays consistent across all views -->
17217            <!-- options for membership cal -->
17218            <div class="cs-calendar-wrap overflowHidden" :class="[{ 'wrapBorderBottom': is_membership_cal }, 'cs-calendar-wrap_' + client_id, class_string]" :style="generateWrapStyle('cs-calendar-wrap')" :data-client-id="client_id">
17219                <div v-if="filterOptions?.length >= 1 && is_membership_cal" class="scrollable posScrollbar lightBorderTop noPaddingBottom">
17220                    <div class="flexNoWrap" :style="generateWrapStyle('options')">
17221                        <template v-for="option in filterOptions">
17222                            <div
17223                                class="cs-cal-filter-option" 
17224                                :class="{
17225                                    ['cs-cal-filter-option_' + option.data_option]: true,
17226                                    'active': selectedFilterOptions.indexOf(option.data_option) !== -1
17227                                  }"
17228                                :data-option="option.data_option"
17229                                @click="$emit('handleFilterSelection', option)"
17230                            >
17231                                {{ option.name }}
17232                            </div>
17233                        </template>
17234                    </div>
17235                </div>
17236                <div class="cs-calendar-menu-wrap flexNoWrap" :style="generateWrapStyle('cs-calendar-menu-wrap')">
17237                    <div class="flexWrapLeftAlign">
17238                        <div class="flexNoWrap">
17239                            <div class="cs-calendar-button roboto hideOnMobile" data-option="today" @click="handleButtonClick('today')">Today</div>
17240                            <div class="cs-calendar-button cs-calendar-button-previous roboto" data-option="previous" @click="handleButtonClick('previous')"></div>
17241                            <div class="cs-calendar-button cs-calendar-button-next roboto" data-option="next" @click="handleButtonClick('next')"></div>
17242                        </div>
17243                        <p v-if="calendarTitle" class="cs-calendar-date-range-title roboto">{{ calendarTitle }}</p>
17244                    </div>
17245                    <div class="flexGrowOne roboto"></div>
17246                    <!-- calendar date picker -->
17247                    <calendar-date-picker-component
17248                        :handle_button_click="handleButtonClick"
17249                    ></calendar-date-picker-component>
17250                    <div class="vue-cs-calendar-select-wrap onWhite roboto">
17251                        <select 
17252                            class="vue-cs-calendar-view-select roboto" 
17253                            v-model="view_preference" 
17254                            @change="changeViewPreference"
17255                        >
17256                            <option value="Month">Month</option>
17257                            <option value="Week">Week</option>
17258                            <option value="Day">Day</option>
17259                        </select>
17260                    </div>
17261                </div>
17262                <!-- inject month view -->
17263                <div v-if="view_preference === 'Month' && !loading" class="flexGrid">
17264                    <!-- header -->
17265                    <div class="daysOfWeek-grid">
17266                        <template v-for="day in daysOfWeek">
17267                            <calendar-cell-component
17268                                class="grid-cell dayOfWeek"
17269                                :class="{ 'borderClose': day === 'Sat' }"
17270                                :day="day.toUpperCase()"
17271                                cellType="dayOfWeek"
17272                                :view_preference="view_preference"
17273                            ></calendar-cell-component>
17274                        </template>
17275                    </div>
17276                    <!-- events -->
17277                    <div class="calendar-grid">
17278                        <template v-for="(calendarObj, i) in calendar">
17279                            <calendar-cell-component
17280                                class="grid-cell"
17281                                :client_id="client_id"
17282                                :date="calendarObj.date"
17283                                :class="{
17284                                    'notCurrentMonth': calendarObj.class === 'prevMonthDate' || calendarObj.class === 'nextMonthDate',
17285                                    'currentMonth': calendarObj.class === 'currentMonthDate',
17286                                    'borderClose': calendarObj.date.day() === 6,
17287                                    'borderTopRightBottomClose': i >= 35 && i <= 40,
17288                                    'borderTopBottomClose': i === calendar.length - 1
17289                                }"
17290                                :month_class="calendarObj.class"
17291                                :day="calendarObj.day"
17292                                :events="calendarObj.events"
17293                                cellType="calendarObj"
17294                                :view_preference="view_preference"
17295                                :defaultDate="calendarObj.defaultDate"
17296                                :monthIndex="i"
17297                                @createSchedule_emitted="createSchedule_emitted"
17298                                @clickSchedule_emitted="clickSchedule_emitted"
17299                                :is_membership_cal="is_membership_cal"
17300                            ></calendar-cell-component>
17301                        </template>
17302                    </div>
17303                </div>
17304                <!-- inject week view -->
17305                <div v-if="view_preference === 'Week' && !loading" class="flexGrid">
17306                    <!-- header -->
17307                    <div class="placeholder-daysOfWeek-grid">
17308                        <template v-for="day in placeholder_daysOfWeek">
17309                            <calendar-cell-component v-if="day.dayOfWeek === null"
17310                                class="grid-cell-week dayOfWeek hide"
17311                                :day="day"
17312                                cellType="dayOfWeek"
17313                                :view_preference="view_preference"
17314                            ></calendar-cell-component> 
17315                            <calendar-cell-component v-if="day.dayOfWeek !== null"
17316                                class="grid-cell-week dayOfWeek"
17317                                :day="day"
17318                                cellType="dayOfWeek"
17319                                :view_preference="view_preference"
17320                                :defaultDate="day.defaultDate"
17321                            ></calendar-cell-component>
17322                        </template>
17323                        <!-- all day events -->
17324                        <template v-for="allDay in placeholder_daysOfWeek">
17325                            <calendar-cell-component v-if="allDay.dayOfWeek === null"
17326                                class="grid-cell-week allDay"
17327                                :allDay="allDay"
17328                                cellType="allDay_events"
17329                                :view_preference="view_preference"
17330                                :is_membership_cal="is_membership_cal"
17331                            ></calendar-cell-component> 
17332                            <calendar-cell-component v-if="allDay.dayOfWeek !== null"
17333                                class="grid-cell-week allDay"
17334                                :allDay="allDay"
17335                                cellType="allDay_events"
17336                                :view_preference="view_preference"
17337                                @createSchedule_emitted="createSchedule_emitted"
17338                                @clickSchedule_emitted="clickSchedule_emitted"
17339                                :is_membership_cal="is_membership_cal"
17340                            ></calendar-cell-component>
17341                        </template>
17342                    </div>
17343                    <!-- timestamps and events -->
17344                    <div class="placeholder-calendar-grid">
17345                        <template v-for="(timestamp, i) in timestamps">
17346                            <template v-for="(calendarObj, j) in calendar">
17347                                <calendar-cell-component 
17348                                    v-if="j === 0"
17349                                    class="grid-cell-week timestamp"
17350                                    :date="calendarObj.date"
17351                                    :timestamp="i % 4 === 0 ? formatShortTime( timestamp.customFormat ) : null"
17352                                    :events="[]"
17353                                    cellType="timestamp"
17354                                    :view_preference="view_preference"
17355                                    :is_membership_cal="is_membership_cal"
17356                                    :class="{ 'borderBottomClose': i === timestamps.length - 1 }"
17357                                ></calendar-cell-component>
17358                                <calendar-cell-component 
17359                                    v-else
17360                                    class="grid-cell-week"
17361                                    :style="calendarObj.defaultDate ? { 'background-color': 'rgba( 175, 213, 240, 0.1)' } : null"
17362                                    :class="{
17363                                        'showBorder': i % 4 === 0,
17364                                        'hideBorder': i === 0 || i % 4 !== 0
17365                                    }"
17366                                    :date="calendarObj.date"
17367                                    :events="generateEventForTimestamp( timestamp.customFormat, calendarObj.events, calendarObj.date )"
17368                                    cellType="calendarObj"
17369                                    :view_preference="view_preference"
17370                                    @createSchedule_emitted="createSchedule_emitted"
17371                                    @clickSchedule_emitted="clickSchedule_emitted"
17372                                ></calendar-cell-component>
17373                            </template>
17374                        </template>
17375                    </div>
17376                </div>
17377                <!-- inject day view -->
17378                <div v-if="view_preference === 'Day' && !loading" class="flexGrid">
17379                    <!-- header -->
17380                    <div class="day-daysOfWeek-grid">
17381                        <template v-for="day in today_daysOfWeek">
17382                            <calendar-cell-component v-if="day.dayOfWeek === null"
17383                                class="grid-cell-week dayOfWeek hide"
17384                                :day="day"
17385                                cellType="dayOfWeek"
17386                                :view_preference="view_preference"
17387                            ></calendar-cell-component> 
17388                            <calendar-cell-component v-if="day.dayOfWeek !== null"
17389                                class="grid-cell-week day"
17390                                :day="day"
17391                                cellType="dayOfWeek"
17392                                :view_preference="view_preference"
17393                                :defaultDate="day.defaultDate"
17394                            ></calendar-cell-component>
17395                        </template>
17396                        <!-- all day events -->
17397                        <template v-for="allDay in today_daysOfWeek">
17398                            <calendar-cell-component v-if="allDay.dayOfWeek === null"
17399                                class="grid-cell-week allDay"
17400                                :allDay="allDay"
17401                                cellType="allDay_events"
17402                                :view_preference="view_preference"
17403                                :is_membership_cal="is_membership_cal"
17404                            ></calendar-cell-component> 
17405                            <calendar-cell-component v-if="allDay.dayOfWeek !== null"
17406                                class="grid-cell-week allDay"
17407                                :allDay="allDay"
17408                                cellType="allDay_events"
17409                                :view_preference="view_preference"
17410                                @createSchedule_emitted="createSchedule_emitted"
17411                                @clickSchedule_emitted="clickSchedule_emitted"
17412                                :is_membership_cal="is_membership_cal"
17413                            ></calendar-cell-component>
17414                        </template>
17415                    </div>
17416                    <!-- timestamps and events -->
17417                    <div class="day-calendar-grid">
17418                        <template v-for="(timestamp, i) in timestamps">
17419                            <template v-for="(calendarObj, j) in calendar">
17420                                <calendar-cell-component v-if="j === 0"
17421                                    class="grid-cell-week timestamp"
17422                                    :date="calendarObj.date"
17423                                    :timestamp="i % 4 === 0 ? formatShortTime( timestamp.customFormat ) : null"
17424                                    :events="[]"
17425                                    cellType="timestamp"
17426                                    :view_preference="view_preference"
17427                                    :is_membership_cal="is_membership_cal"
17428                                    :class="{ 'borderBottomClose': i === timestamps.length - 1 }"
17429                                ></calendar-cell-component>
17430                                <calendar-cell-component v-if="j !== 0"
17431                                    class="grid-cell-week"
17432                                    :style="calendarObj.defaultDate ? { 'background-color': 'rgba( 175, 213, 240, 0.1)' } : null"
17433                                    :class="{
17434                                        'showBorder': i % 4 === 0,
17435                                        'hideBorder': i === 0 || i % 4 !== 0
17436                                    }"
17437                                    :date="calendarObj.date"
17438                                    :events="generateEventForTimestamp( timestamp.customFormat, calendarObj.events, calendarObj.date )"
17439                                    cellType="calendarObj"
17440                                    :view_preference="view_preference"
17441                                    @createSchedule_emitted="createSchedule_emitted"
17442                                    @clickSchedule_emitted="clickSchedule_emitted"
17443                                ></calendar-cell-component>
17444                            </template>
17445                        </template>
17446                    </div>
17447                </div>
17448                <!-- spinner while loading -->
17449                <div v-if="loading" class="cardBodySpinnerWrap relative active">
17450                    <div class="cardBodySpinner centeredBlock"></div>
17451                </div>
17452            </div>
17453        </div>
17454    </div>
17455</script>
vendor: 1 bytes, line 17455
17455
17456<script>
17457    /* ----- STORE ----- */
17458    store.components["calendar_grid"] = {
17459        calendar: [],
17460        loading: true,
17461        view_preference: 'Month',
17462        dropdownOpen: false,
17463        daysOfWeek: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
17464        placeholder_daysOfWeek: [],
17465        today_daysOfWeek: [],
17466        view_options: [ 'Month', 'Week', 'Day' ],
17467        calendarTitle: null,
17468        //dates and times
17469        currentDate: dayjs() || new Date(),
17470        defaultDate: dayjs() || new Date(),
17471        currentMonth: dayjs().format('MMMM'),
17472        startOfWeek: dayjs().startOf('week'), 
17473        endOfWeek: dayjs().endOf('week'),
17474        currentYear: dayjs().year(),
17475        timestamps: [],
17476        currentTime: dayjs().format('HH:mm:ss'),
17477    };
17478
17479    /* ----- COMPONENT ----- */
17480    app_content.component("calendar-grid-component", {
17481        template: "#template-calendar-grid",
17482        data() {
17483            return store.components['calendar_grid']
17484        },
17485        created(){
17486            if (this.timestamps.length === 0){
17487                this.generateTimestamps();  
17488            }
17489        },
17490        props: {
17491            client_id: String,
17492            class_string: String,
17493            style_string: String,
17494            filterOptions: Array,
17495            selectedFilterOptions: Array,
17496            schedules: Array,
17497            is_membership_cal: Boolean,
17498            load_cal: Function,
17499            handleFilterSelection: Function,
17500            createSchedule: Function,
17501            clickSchedule: Function,
17502        },
17503        methods: {
17504            createSchedule_emitted( date ){
17505                this.$emit('createSchedule', date)
17506            },
17507            clickSchedule_emitted( event ){
17508                this.$emit('clickSchedule', event )
17509            },
17510            formatShortTime( timeString ){
17511                if (!timeString){
17512                    return '' // placeholder for 11:45-12:00 block
17513                }
17514                // if (timeString === '12:00 AM'){
17515                //     return Intl.DateTimeFormat().resolvedOptions().timeZone;
17516                // }
17517                const [hours, minutes] = timeString.split(':');
17518                const AM_PM = minutes.split(' ')[1]
17519                return `${hours} ${AM_PM}`;
17520            },
17521            generateWrapStyle( type ){
17522                if (type === 'cs-calendar-wrap'){
17523                    return this.style_string === undefined ? {'width': '100%'} : this.style_string;
17524                } else if (type === 'options' && this.is_membership_cal){
17525                    return { 
17526                        'padding-top': '15px', 
17527                        'padding-right': '15px', 
17528                        'padding-left': '15px', 
17529                        'padding-bottom': '5px', 
17530                        'border-width': '0 1.5px 0 1.5px', 
17531                        'border-color': '#e2e8e8', 
17532                        'border-style': 'solid',
17533                        'overflow': 'auto',
17534                    }
17535                } else if (type === 'cs-calendar-menu-wrap' && this.is_membership_cal){
17536                    return { 
17537                        'border-width': '0 1.5px 0 1.5px', 
17538                        'border-color': '#e2e8e8', 
17539                        'border-style': 'solid' 
17540                    }
17541                }
17542            },
17543            generateEventForTimestamp( timestamp, events, date ){
17544                // do not want to display all day or multiple day events -- being handled separately
17545                let eventsArr = [];
17546                if (events.length > 0){
17547                    for (let i = 0; i < events.length; i++){
17548                        let event = events[i];
17549                        // normal event
17550                        if ((event.hours_mins_start && event.hours_mins_end) && !event.isAllDay && event.calendarOnly) {
17551                            let startTime = dayjs().set('hour', event.hours_mins_start.hours).set('minute', event.hours_mins_start.minutes);
17552                            let endTime = dayjs().set('hour', event.hours_mins_end.hours).set('minute', event.hours_mins_end.minutes).subtract(15, 'minutes');
17553
17554                            let formattedStartTime = startTime.format('h:mm A');
17555                            let formattedEndTime = endTime.format('h:mm A');
17556
17557                            let timesArray = [formattedStartTime];
17558                            while (startTime.isBefore(endTime)) {
17559                                startTime = startTime.add(15, 'minutes');
17560                                timesArray.push(startTime.format('h:mm A'));
17561                            }
17562                            if ( formattedStartTime === timestamp ){    
17563                                eventsArr.push({ event, numOfTimeBlocks: timesArray.length });
17564                            }
17565                        // recurring event
17566                        }
17566 else if ( (event.start && event.end) && (event.start !== event.end) && !event.isAllDay && !event.isMultipleDay && event.calendarOnly ){
17567                            let startTime = dayjs(event.start);
17568                            let endTime = dayjs(event.end).subtract(15, 'minutes');
17569
17570                            let formattedStartTime = startTime.format('h:mm A');
17571                            let formattedEndTime = endTime.format('h:mm A');
17572
17573                            let timesArray = [formattedStartTime];
17574
17575                            while (startTime.isBefore(endTime)) {
17576                                startTime = startTime.add(15, 'minutes');
17577                                timesArray.push(startTime.format('h:mm A'));
17578                            }
17579                            if ( formattedStartTime === timestamp ){    
17580                                eventsArr.push({ event, numOfTimeBlocks: timesArray.length });
17581                            }
17582                        // non-calendarOnly events
17583                        } else if ( (event.start && event.end) && (event.start !== event.end) && !event.calendarOnly ){
17584                            let startTime;
17585                            let endTime;
17586
17587                            if (dayjs(event.start).startOf('day').$d.toString() === dayjs(date).startOf('day').$d.toString()){
17588                                startTime = dayjs(event.start);
17589                                endTime = dayjs(event.end).subtract(15, 'minutes');
17590                            } else if (dayjs(event.end).startOf('day').$d.toString() === dayjs(date).startOf('day').$d.toString()) {
17591                                startTime = dayjs(event.end).startOf('day');
17592                                endTime = dayjs(event.end).subtract(15, 'minutes');
17593                            } else {
17594                                startTime = dayjs().startOf('day');
17595                                endTime = dayjs().endOf('day');
17596                            }
17597
17598                            let formattedStartTime = startTime.format('h:mm A');
17599                            let formattedEndTime = endTime.format('h:mm A');
17600
17601                            let timesArray = [formattedStartTime];
17602
17603                            while (startTime.isBefore(endTime)) {
17604                                startTime = startTime.add(15, 'minutes');
17605                                timesArray.push(startTime.format('h:mm A'));
17606                            }
17607                            if ( formattedStartTime === timestamp ){    
17608                                eventsArr.push({ event, numOfTimeBlocks: timesArray.length });
17609                            }
17610                        }
17611                    } 
17612                }
17613                if (eventsArr.length > 0){
17614                    return eventsArr;
17615                } else {
17616                    return [];
17617                }
17618            },
17619            changeViewPreference(){
17620                this.loading = true;
17621                //and re-load data
17622                this.$emit('load_cal', this.client_id);
17623            },
17624            calculateTitleRange(){
17625                if ( this.view_preference === 'Month' ){
17626                    this.calendarTitle = `${this.currentMonth} ${this.currentYear.toString()}`;
17627                } else if ( this.view_preference === 'Week' ){
17628                    this.calendarTitle = `${this.startOfWeek.format('MMMM')} ${this.startOfWeek.$D} - ${this.endOfWeek.format('MMMM')} ${this.endOfWeek.$D}`;
17629                } else if ( this.view_preference === 'Day' ){
17630                    this.calendarTitle = `${this.currentMonth} ${this.currentDate.$D}, ${this.currentYear}`;
17631                }
17632            },
17633            handleButtonClick( option, selectedDate ){
17634                this.loading = true;
17635                if (this.view_preference === 'Month'){
17636                    if( option === "today" ){
17637                        this.currentDate = dayjs();
17638                        this.currentMonth = this.currentDate.format('MMMM')
17639                        this.currentYear = this.currentDate.$y
17640                        this.startOfWeek = this.currentDate.startOf('week');
17641                        this.endOfWeek = this.currentDate.endOf('week');
17642                    }else if( option === "next" ){
17643                        this.currentDate = this.currentDate.add(1, 'month').startOf('month');
17644                        this.currentMonth = this.currentDate.format('MMMM')
17645                        this.currentYear = this.currentDate.$y
17646                        this.startOfWeek = this.currentDate.startOf('week');
17647                        this.endOfWeek = this.currentDate.endOf('week');
17648
17649                    }else if( option === "previous" ){
17650                        this.currentDate = this.currentDate.subtract(1, 'month').startOf('month');
17651                        this.currentMonth = this.currentDate.format('MMMM')
17652                        this.currentYear = this.currentDate.$y
17653                        this.startOfWeek = this.currentDate.startOf('week');
17654                        this.endOfWeek = this.currentDate.endOf('week');
17655                    }else if ( option === "date-picker" ){
17656                        let { month, year } = selectedDate;
17657                        this.currentDate = dayjs(new Date(year, month, 1));
17658                        this.currentMonth = this.currentDate.format('MMMM')
17659                        this.currentYear = this.currentDate.$y
17660                        this.startOfWeek = this.currentDate.startOf('week');
17661                        this.endOfWeek = this.currentDate.endOf('week');
17662                    }
17663                } else if (this.view_preference === 'Week'){
17664                    if( option === "today" ){
17665                        this.currentDate = dayjs();
17666                        this.currentMonth = this.currentDate.format('MMMM')
17667                        this.currentYear = this.currentDate.$y
17668                        this.startOfWeek = this.currentDate.startOf('week');
17669                        this.endOfWeek = this.currentDate.endOf('week');
17670                    }else if( option === "next" ){
17671                        this.currentDate = this.currentDate.add(1, 'week');
17672                        this.currentMonth = this.currentDate.format('MMMM')
17673                        this.currentYear = this.currentDate.$y
17674                        this.startOfWeek = this.currentDate.startOf('week');
17675                        this.endOfWeek = this.currentDate.endOf('week');
17676                    }else if( option === "previous" ){
17677                        this.currentDate = this.currentDate.subtract(1, 'week');
17678                        this.currentMonth = this.currentDate.format('MMMM')
17679                        this.currentYear = this.currentDate.$y
17680                        this.startOfWeek = this.currentDate.startOf('week');
17681                        this.endOfWeek = this.currentDate.endOf('week');
17682                    }else if ( option === "date-picker" ){
17683                        let { month, year } = selectedDate;
17684                        this.currentDate = dayjs(new Date(year, month, 1));
17685                        this.currentMonth = this.currentDate.format('MMMM')
17686                        this.currentYear = this.currentDate.$y
17687                        this.startOfWeek = this.currentDate.startOf('week');
17688                        this.endOfWeek = this.currentDate.endOf('week');
17689                    }
17690                } else if (this.view_preference === 'Day'){
17691                    if( option === "today" ){
17692                        this.currentDate = dayjs();
17693                        this.currentMonth = this.currentDate.format('MMMM')
17694                        this.currentYear = this.currentDate.$y
17695                        this.startOfWeek = this.currentDate.startOf('week');
17696                        this.endOfWeek = this.currentDate.endOf('week');
17697                    }else if( option === "next" ){
17698                        this.currentDate = this.currentDate.add(1, 'day');
17699                        this.currentMonth = this.currentDate.format('MMMM')
17700                        this.currentYear = this.currentDate.$y
17701                        this.startOfWeek = this.currentDate.startOf('week');
17702                        this.endOfWeek = this.currentDate.endOf('week');
17703                    }else if( option === "previous" ){
17704                        this.currentDate = this.currentDate.subtract(1, 'day');
17705                        this.currentMonth = this.currentDate.format('MMMM')
17706                        this.currentYear = this.currentDate.$y
17707                        this.startOfWeek = this.currentDate.startOf('week');
17708                        this.endOfWeek = this.currentDate.endOf('week');
17709                    }else if ( option === "date-picker" ){
17710                        let { month, year } = selectedDate;
17711                        this.currentDate = dayjs(new Date(year, month, 1));
17712                        this.currentMonth = this.currentDate.format('MMMM')
17713                        this.currentYear = this.currentDate.$y
17714                        this.startOfWeek = this.currentDate.startOf('week');
17715                        this.endOfWeek = this.currentDate.endOf('week');
17716                    }
17717                }
17718                //and re-load data
17719                this.$emit('load_cal', this.client_id);
17720            },
17721            /* ----- date methods ----- */
17722            generateTimestamps(){
17723                const startTime = dayjs().startOf('day');
17724                const endTime = dayjs().endOf('day');
17725                let currentTime = startTime;
17726                while (currentTime.isBefore(endTime)) {
17727                    this.timestamps.push({
17728                        customFormat: currentTime.format('h:mm A'),
17729                        militaryFormat: currentTime.format('HH:mm:ss'),
17730                    });
17731                    currentTime = currentTime.add(15, 'minutes');
17732                }
17733                this.timestamps.push({
17734                    customFormat: null,
17735                    militaryFormat: null,
17736                })
17737            },
17738            generateCalendar( ){
17739                console.log("generate calendar")
17740                this.loading = true;
17741                if ( this.view_preference === 'Month'){
17742                    // current month start date, end date, total days calculation
17743                    const start = this.currentDate.startOf('month');
17744                    const end = start.endOf('month');
17745                    const totalDays = end.diff(start, 'day') + 1;
17746                    // number of calendar days to add before and after current month days
17747                    const spaceBefore = start.day();
17748                    const spaceAfter = 42 - (spaceBefore + totalDays);
17749                    // generate number of days for grid cell of 42
17750                    this.calendar = [...Array(spaceBefore)].map((_, i) => {
17751                        const prevMonthDate = start.subtract(spaceBefore - i, 'day');
17752                        return {
17753                            date: prevMonthDate,
17754                            day: prevMonthDate.$D,
17755                            class: 'prevMonthDate',
17756                            events: [],
17757                            defaultDate: false,
17758                        }
17759                    }).concat([...Array(totalDays)].map((_, i) => {
17760                        const currentMonthDate = start.add(i, 'day');
17761                        if (this.defaultDate.startOf('day').$d.toString() === currentMonthDate.$d.toString()){
17762                            return {
17763                                date: currentMonthDate,
17764                                day: currentMonthDate.$D === 1 ? `${currentMonthDate.format('MMM')} ${currentMonthDate.$D}` : currentMonthDate.$D,
17765                                class: 'currentMonthDate',
17766                                events: [],
17767                                defaultDate: true,
17768                            }
17769                        }else {
17770                            return {
17771                                date: currentMonthDate,
17772                                day: currentMonthDate.$D === 1 ? `${currentMonthDate.format('MMM')} ${currentMonthDate.$D}` : currentMonthDate.$D,
17773                                class: 'currentMonthDate',
17774                                events: [],
17775                                defaultDate: false,
17776                            }
17777                        }
17778                    })).concat([...Array(spaceAfter)].map((_, i) => {
17779                        const nextMonthDate = end.add(i + 1, 'day');
17780                        return {
17781                            date: nextMonthDate,
17782                            day: nextMonthDate.$D === 1 ? `${nextMonthDate.format('MMM')} ${nextMonthDate.$D}` : nextMonthDate.$D,
17783                            class: 'nextMonthDate',
17784                            events: [],
17785                            defaultDate: false,
17786                        }
17787                    }))
17788                } else if ( this.view_preference === 'Week' ){
17789                    console.log("generate week A")
17790                    // start and end of current week
17791                    const startOfWeek = this.currentDate.startOf('week');
17792                    const endOfWeek = this.currentDate.endOf('week');
17793
17794                    const weekDays = [{ date: null, day: null, events: [] }];
17795                    let currentDay = startOfWeek;
17796                    while (currentDay.isBefore(endOfWeek) || currentDay.isSame(endOfWeek, 'day')) {
17797                        weekDays.push(currentDay);
17798                        currentDay = currentDay.add(1, 'day');
17799                    }
17800                    this.placeholder_daysOfWeek = [null, 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
17801                    this.placeholder_daysOfWeek = this.placeholder_daysOfWeek.map((dayOfTheWeek, i) => {
17802                        if (JSON.stringify(this.defaultDate.startOf('day').$d) === JSON.stringify(weekDays[i].$d)){
17803                            return {
17804                                dayOfWeek: dayOfTheWeek,
17805                                day: weekDays[i].$D,
17806                                date: weekDays[i],
17807                                events: [],
17808                                defaultDate: true,
17809                            }
17810                        } else {
17811                            return {
17812                                dayOfWeek: dayOfTheWeek,
17813                                day: weekDays[i].$D,
17814                                date: weekDays[i],
17815                                events: [],
17816                                defaultDate: false,
17817                            }
17818                        }
17819                    })
17820                    console.log("generate week B")
17821                    this.mapAllDayEvents();
17822                    this.calendar = weekDays.map(date => {
17823                        if(JSON.stringify(this.defaultDate.startOf('day').$d) === JSON.stringify(date.$d)){
17824                            return {
17825                                date: date,
17826                                events: [],
17827                                defaultDate: true,
17828                            }
17829                        } else {
17830                            return {
17831                                date: date,
17832                                events: [],
17833                                defaultDate: false,
17834                            }   
17835                        }
17836                    })
17837                    console.log("generate week C")
17838                } else if ( this.view_preference === 'Day' ){
17839                    this.today_daysOfWeek = [null, this.daysOfWeek[this.currentDate.day()]]
17840                    this.today_daysOfWeek = this.today_daysOfWeek.map((dayOfTheWeek, i) => {
17841                        if (i === 1 && this.defaultDate.startOf('day').$d.toString() === this.currentDate.startOf('day').$d.toString()){
17842                            return {
17843                                dayOfWeek: dayOfTheWeek,
17844                                day: dayOfTheWeek !== null ? this.currentDate.$D : null,
17845                                date: dayOfTheWeek !== null ? this.currentDate : null,
17846                                events: [],
17847                                defaultDate: true,
17848                            }
17849                        } else {
17850                            return {
17851                                dayOfWeek: dayOfTheWeek,
17851
17852                                day: dayOfTheWeek !== null ? this.currentDate.$D : null,
17853                                date: dayOfTheWeek !== null ? this.currentDate : null,
17854                                events: [],
17855                                defaultDate: false,
17856                            }
17857                        }
17858                    })
17859                    this.mapAllDayEvents();
17860                    this.calendar = [{ date: null, day: null, events: [] }, this.currentDate ].map((date, i) => {
17861                        if(i === 1 && this.defaultDate.startOf('day').$d.toString() === date.startOf('day').$d.toString()){
17862                            return {
17863                                date: date,
17864                                events: [],
17865                                defaultDate: true,
17866                            }
17867                        } else {
17868                            return {
17869                                date: date,
17870                                events: [],
17871                                defaultDate: false,
17872                            }
17873                        }
17874                    })
17875                }
17876                // map events onto calendar days
17877                this.mapCalendarEvents_v2();
17878            },
17879            // mapCalendarEvents(){
17880            //     let multipleDaySchedules = [];
17881            //     this.calendar.map(day => {
17882            //         let calendarDay;
17883            //         if ( day.date !== null ){
17884            //             calendarDay = dayjs(day.date.$d).format('MMMM DD, YYYY');
17885            //         }
17886            //         this.schedules.forEach(schedule => {
17887            //             let scheduleStart = dayjs( schedule.start ).format('MMMM DD, YYYY');
17888            //             let scheduleEnd = dayjs( schedule.end ).format('MMMM DD, YYYY');
17889            //             let scheduleOnDay = scheduleStart === calendarDay && scheduleStart === scheduleEnd;
17890            //             let scheduleOnDay_isAllDay = scheduleStart === calendarDay && schedule.isAllDay;
17891            //             // check to see if there are any multiple event days
17892            //             if ( multipleDaySchedules.length > 0 ){
17893            //                 multipleDaySchedules.forEach((multipleDaySchedule, i) => {
17894            //                     if ( multipleDaySchedule.daysRemaining > 0 && calendarDay === multipleDaySchedule.scheduleStart.format('MMMM DD, YYYY')){
17895            //                         day.events.push(multipleDaySchedule.schedule)
17896            //                         multipleDaySchedule.scheduleStart = multipleDaySchedule.scheduleStart.add(1, 'day')
17897            //                         multipleDaySchedule.daysRemaining--;
17898            //                     } else if ( multipleDaySchedule.daysRemaining === 0 ){
17899            //                         multipleDaySchedules.splice(i, 1)
17900            //                     }
17901            //                 })
17902            //             }
17903            //             // there is an event or all day event on this day
17904            //             if ( scheduleOnDay || scheduleOnDay_isAllDay ){
17905            //                 day.events.push(schedule)
17906            //             // there is a multiple day event that starts on this day
17907            //             } else if ( scheduleStart === calendarDay && scheduleStart !== scheduleEnd && !schedule.isAllDay ){
17908            //                 multipleDaySchedules.push({
17909            //                     schedule: schedule,
17910            //                     scheduleStart: dayjs(scheduleStart, { format: 'MMMM DD, YYYY' }).add(1, 'day'),
17911            //                     daysRemaining: dayjs(scheduleEnd, { format: 'MMMM DD, YYYY' }).diff(dayjs(scheduleStart, { format: 'MMMM DD, YYYY' }), 'day') - 1
17912            //                 })
17913            //                 schedule.isMultipleDay = true;
17914            //                 day.events.push(schedule);
17915            //             }
17916            //         })
17917            //         return day;
17918            //     })
17919            //     console.log('this is final mapping: ', this.calendar)
17920            //     this.loading = false;
17921            // },
17922            mapCalendarEvents_v2(){
17923                console.log("map calendar events v2");
17924                let previousDayEventOrder = [];
17925                this.calendar.map((day, dayIndex) => {
17926                    let calendarDay;
17927                    if ( day.date !== null ){
17928                        calendarDay = dayjs(day.date.$d)
17929                    }
17930                    this.schedules.forEach(schedule => {
17931                        let scheduleStart = dayjs( schedule.start )
17932                        let scheduleEnd = dayjs( schedule.end )
17933                        let scheduleOnDay = scheduleStart.format('MMMM DD, YYYY') === calendarDay.format('MMMM DD, YYYY') && scheduleStart.format('MMMM DD, YYYY') === scheduleEnd.format('MMMM DD, YYYY');
17934                        let scheduleOnDay_isAllDay = scheduleStart.format('MMMM DD, YYYY') === calendarDay.format('MMMM DD, YYYY') && schedule.isAllDay;
17935                        // event falls on current day OR it's an all day event
17936                        // added scheduleOnDay && scheduleOnDay_isAllDay 1/25/24 for regatta subevents start start and end on the same day
17937                        if ( scheduleOnDay && !scheduleOnDay_isAllDay || scheduleOnDay && scheduleOnDay_isAllDay ){
17938                            day.events.push(schedule)
17939                        } 
17940                        // it's the first day of a multiple day event
17941                        // these checks are not thorough enough
17942                        else if (scheduleStart.format('MMMM DD, YYYY') === calendarDay.format('MMMM DD, YYYY') && scheduleStart.format('MMMM DD, YYYY') !== scheduleEnd.format('MMMM DD, YYYY')){
17943                            schedule.isMultipleDay = true;
17944                            day.events.push(schedule)
17945                        }
17946                        // it's a multiple day event and the current date falls between the start and end date
17947                        else if ( scheduleStart.format('MMMM DD, YYYY') !== calendarDay.format('MMMM DD, YYYY') && (calendarDay.isAfter(scheduleStart) && calendarDay.isBefore(scheduleEnd)) ){
17948                            schedule.isMultipleDay = true;
17949                            day.events.push(schedule)
17950                        }  
17951                    })
17952
17953                    /* ui stacking logic for month view */
17954                    if (this.view_preference === 'Month'){
17955                        if ( dayIndex !== 0 ){
17956                            let currentDayEventOrder;
17957                            let currentDayIndexOrder = {};
17958                            let notMatchingEvents = [];
17959                            let matchingEvents = [];
17960                            for (let i = 0; i < day.events.length; i++){
17961                                let currentDayEventId = day.events[i].id;
17962                                let previousDayIndex = previousDayEventOrder?.indexOf(currentDayEventId);
17963                                if (previousDayIndex === -1){
17964                                    notMatchingEvents.push(day.events[i])
17965                                } else if (previousDayIndex !== -1) {
17966                                    currentDayIndexOrder[previousDayIndex] = day.events[i];
17967                                    matchingEvents.push(day.events[i]);
17968                                }
17969                            }
17970
17971                            let missingIndexes = [];
17972                            for (let j = 0; j < 3; j++ ){
17973                                if (Object.keys(currentDayIndexOrder).indexOf(j.toString()) === -1 ){
17974                                    missingIndexes.push(j)
17975                                }
17976                            }
17977                   
17978                            let placeholder = { 
17979                                bgColor: '',
17980                                calendarId: '',
17981                                calendarOnly: '',
17982                                category: '',
17983                                className: '',
17984                                color: '',
17985                                end: '',
17986                                id: 'placeholder',
17987                                raw: {dot_color: ''},
17988                                start: '',
17989                                title: 'placeholder',
17990                            };
17991
17992                            for (let k = 0; k < missingIndexes.length; k++){
17993                                let missingIndexValue = missingIndexes[k];
17994                                currentDayIndexOrder[missingIndexValue] = notMatchingEvents[k] || placeholder;
17995                            }
17996
17997                            let remainingIndexes = day.events.length - 3;
17998                            let calculateRemainingEvents = matchingEvents.length > 3 ? matchingEvents.concat(notMatchingEvents).splice(3) : [...notMatchingEvents].splice(missingIndexes.length);
17999
18000                            if (remainingIndexes > 0){
18001                                for (let l = 3; l < 3 + remainingIndexes; l++){
18002                                    let targetIndex = matchingEvents.length > 3 ? l - 3 : l - 3;
18003                                    //added placeholder for white lake yacht club bugs
18004                                    currentDayIndexOrder[l] = calculateRemainingEvents[targetIndex] || placeholder;
18005                                }
18006                            }
18007
18008                            const sortedObj = Object.fromEntries(
18009                                Object.entries(currentDayIndexOrder).sort(([keyA], [keyB]) => parseInt(keyA) - parseInt(keyB))
18010                            );
18011                            currentDayEventOrder = Object.values(sortedObj);
18012                            //set order of previous day events using event ids
18013                            previousDayEventOrder = currentDayEventOrder.map(event =>
18013 { return event.id });
18014                            //set current day events to newly sorted order
18015                            day.events = currentDayEventOrder.filter((event, i, self) => event.id === 'placeholder' || (event.id !== 'placeholder' && i === self.findIndex(e => e.id === event.id)));
18016                        }
18017                    }
18018
18019                    return day;
18020                })
18021                this.loading = false;
18022            },
18023            mapAllDayEvents(){
18024                let daysOfWeekArr;
18025                if (this.view_preference === 'Week'){
18026                    daysOfWeekArr = this.placeholder_daysOfWeek;
18027                } else if (this.view_preference === 'Day'){
18028                    daysOfWeekArr = this.today_daysOfWeek;
18029                }
18030                let multipleDaySchedules = [];
18031                daysOfWeekArr = daysOfWeekArr.map((day, i) => {
18032                    this.schedules.forEach((schedule, i) => {
18033                        if ( (i !== 0 && this.view_preference === 'Day' || i !== 0 && this.view_preference === 'Week') && ( schedule.calendarOnly || schedule.category === 'allday') ){
18034                            let calendarDay = dayjs(day.date)
18035                            let scheduleStart = dayjs( schedule.start )
18036                            let scheduleEnd = dayjs( schedule.end )
18037                            let scheduleOnDay_isAllDay = scheduleStart.format('MMMM DD, YYYY') === calendarDay.format('MMMM DD, YYYY') && schedule.isAllDay;
18038                            // it's an all day event
18039                            if ( scheduleOnDay_isAllDay ){
18040                                day.events.push(schedule)
18041                            } 
18042                            // it's the first day of a multiple day event
18043                            else if (scheduleStart.format('MMMM DD, YYYY') === calendarDay.format('MMMM DD, YYYY') && scheduleStart.format('MMMM DD, YYYY') !== scheduleEnd.format('MMMM DD, YYYY')){
18044                                schedule.isMultipleDay = true;
18045                                day.events.push(schedule)
18046                            }
18047                            // it's a multiple day event and the current date falls between the start and end date
18048                            else if ( scheduleStart.format('MMMM DD, YYYY') !== calendarDay.format('MMMM DD, YYYY') && (calendarDay.isAfter(scheduleStart) && calendarDay.isBefore(scheduleEnd)) ){
18049                                schedule.isMultipleDay = true;
18050                                day.events.push(schedule)
18051                            }
18052                        } 
18053                    })
18054                    return day;
18055                })
18056                if (this.view_preference === 'Week'){
18057                    this.placeholder_daysOfWeek = daysOfWeekArr;
18058                } else if (this.view_preference === 'Day'){
18059                    this.today_daysOfWeek = daysOfWeekArr;
18060                }
18061            },
18062        },
18063    })
18064</script>
18064    
18064<script type="text/html" id="template-calendar-cell">
18065    <div @click="$emit('createSchedule_emitted', date)">
18066        <!-- cell for month view -->
18067        <template v-if="view_preference === 'Month'">
18068            <div class="cell-content" :class="cellType">
18069                <div v-if="day" class="cell-day" :class="cellType" :style="checkDefaultDate( day, defaultDate, 'month' )">{{ day }}</div>
18070                <!-- all events -->
18071                <div v-if="cellType === 'calendarObj' && events.length > 0" class="cell-events-month" ref="cell" @click.stop>
18072                    <template v-for="(event, index) in events">
18073                        <span
18074                            v-if="index <= 2"
18075                            :class="{
18076                                'cell-event-month': event.id !== 'placeholder',
18077                                'transparent-event': event.id === 'placeholder'
18078                            }"
18079                            :style="generateEventStyleForMonth( event, 'event_body', date, monthIndex )"
18080                            @click="handleScheduleClickForMonth( event )"
18081                        ><div :style="generateEventStyleForMonth( event, 'status_dot', '', '', month_class )"></div><div class="event-title" :style="generateFontSize('eventTitle', event)">{{ generateEventTitle( event ) }}</div></span>
18082                        <span v-if="index >= 3 && index === events.length - 1" class="overflow-cell-events" ref="overflow-cell" @click="toggleOverflowView( date, events, $event )">
18083                            <div :style="[ { display: 'flex' }, generateFontSize('eventTitle') ]" class="event-title">{{ index - 2 }} more</div>
18084                        </span>
18085                    </template>
18086                </div>
18087            </div>
18088        </template>
18089        <!-- cell for week view -->
18090        <template v-if="view_preference === 'Week'">
18091            <div class="cell-content-week" :class="cellType">
18092                <div v-if="day" :class="{ 'week-cellDay-container' : day.dayOfWeek !== null }">
18093                    <div class="cell-day numberHeader" :class="cellType" :style="[{ 'font-size': '25px', 'font-weight': '400' }, checkDefaultDate(day, defaultDate, 'week_header')]">{{ day.day }}</div>
18094                    <div class="cell-day dayHeader" :class="cellType">{{ day.dayOfWeek !== null ? day.dayOfWeek.toUpperCase() : null }}</div>
18095                </div>
18096                <div v-if="timestamp" class="cell-day" :class="cellType">
18097                    <div class="timestamp"  :style="generateFontSize('timestamp')">{{ timestamp }}</div>
18098                </div>
18099                <!-- all day events -->
18100                <div v-if="allDay">
18101                    <div v-if="allDay.dayOfWeek === null" class="allDay_events" :class="cellType" :style="generateFontSize('allDay')">All Day</div>
18102                    <div class="cell-events-month">
18103                        <template v-if="allDay.dayOfWeek !== null" v-for="event in allDay.events">
18104                            <span
18105                                :style="generateEventStyleForWeek( event, 'allDay_event_body' )"
18106                                class="allDay-cell-event"
18107                                @click="$emit('clickSchedule_emitted', event)"
18108                            >{{ generateEventTitle( event ) }}</span>
18109                        </template>
18110                    </div>
18111                </div>
18112                 <!-- all events -->
18113                <div v-if="cellType === 'calendarObj' && events.length > 0" class="cell-events-week" @click.stop>
18114                    <template v-for="(event, index) in events">
18115                        <span
18116                            class="cell-event-week"
18117                            :style="generateEventStyleForWeek( event, 'event_body' )"
18118                            @click="$emit('clickSchedule_emitted', event.event)"
18119                        ><div></div>{{ generateEventTitle( event.event ) }}</span>
18120                    </template>
18121                </div>
18122            </div>
18123        </template>
18124        <!-- cell for day view -->
18125        <template v-if="view_preference === 'Day'">
18126            <div class="cell-content-week" :class="cellType">
18127                <div v-if="day" :class="{ 'day-cellDay-container' : day.dayOfWeek !== null }">
18128                    <div class="cell-day numberHeader" :class="cellType" :style="[{ 'font-size': '25px', 'font-weight': '400' }, checkDefaultDate(day, defaultDate, 'day_header')]">{{ day.day }}</div>
18129                    <div class="cell-day dayHeader" :class="cellType">{{ day.dayOfWeek !== null ? day.dayOfWeek.toUpperCase() : null }}</div>
18130                </div>
18131                <div v-if="timestamp" class="cell-day" :class="cellType">
18132                    <div class="timestamp" :style="generateFontSize('timestamp')">{{ timestamp }}</div>
18133                </div>
18134                <!-- all day events -->
18135                <div v-if="allDay">
18136                    <div v-if="allDay.dayOfWeek === null" class="allDay_events" :class="cellType" :style="generateFontSize('allDay')">All Day</div>
18137                    <div class="cell-events-month">
18138                        <template v-if="allDay.dayOfWeek !== null" v-for="event in allDay.events">
18139                            <span
18140                                :style="generateEventStyleForWeek( event, 'allDay_event_body' )"
18141                                class="allDay-cell-event"
18142                                @click="$emit('clickSchedule_emitted', event)"
18143                            >{{ generateEventTitle( event ) }}</span>
18144                        </template>
18145                    </div>
18146                </div>
18147                <!-- all events -->
18148                <div v-if="cellType === 'calendarObj' && events.length > 0" class="cell-events-week" @click.stop>
18149                    <template v-for="(event, index) in events">
18150                        <span
18151                            class="cell-event-week"
18152                            :style="generateEventStyleForWeek( event, 'event_body' )"
18153                            @click="$emit('clickSchedule_emitted', event.event )"
18154                        ><div></div>{{ generateEventTitle( event.event ) }}</span>
18155                    </template>
18156                </div>
18157            </div>
18158        </template>
18159    </div>
18160</script>
18160
18161
18162<script>
18163    /* ----- STORE ----- */
18164    store.components["calendar_cell"] = {
18165        cellWidth: this.getCellWidth,
18166        foo: null,
18167    };
18168    /* ----- COMPONENT ----- */
18169    app_content.component("calendar-cell-component", {
18170        template: "#template-calendar-cell",
18171        data() {
18172            return store.components['calendar_cell']
18173        },
18174        mounted(){
18175            window.addEventListener('resize', this.handleResize);
18176            this.$nextTick(() => {
18177                this.handleResize();
18178            });
18179        },
18180        beforeDestroy(){
18181            window.removeEventListener('resize', this.handleResize);
18182        },
18183        props: {
18184            client_id: String,
18185            date: Object,
18186            defaultDate: Boolean,
18187            day: {
18188                type: [String, Number, Object],
18189                required: false,
18190            },
18191            allDay: Object,
18192            timestamp: String,
18193            currentTime: String,
18194            events: Array,
18195            cellType: String,
18196            view_preference: String,
18197            monthIndex: Number,
18198            month_class: String,
18199            is_membership_cal: Boolean,
18200            cell_emitted: Function,
18201            createSchedule_emitted: Function,
18202            clickSchedule_emitted: Function,
18203        },
18204        methods: {
18205            handleResize(){
18206                this.cellWidth = this.getCellWidth();
18207            },
18208            getCellWidth(){
18209                const cell = this.$refs.cell;
18210                // Get the computed width of the cell
18211                if (cell){
18212                    this.cellWidth = cell.getBoundingClientRect().width;
18213                    return cell.getBoundingClientRect().width;
18214                }
18215            },
18216            checkDefaultDate( day, defaultDate, type ){
18217                if( defaultDate ){
18218                    if ( type === 'month' ){
18219                        if ( day.length > 2){
18220                            return {
18221                                'padding': '2.5px',
18222                                'background-color': '#4a56f8',
18223                                'color': '#fff',
18224                                'min-width': 'fit-content',
18225                                'max-width': 'fit-content',
18226                                'min-height': 'fit-content',
18227                                'max-height': 'fit-content',
18228                            }
18229                        } else {
18230                            return {
18231                                'background-color': '#4a56f8',
18232                                'color': '#fff',
18233                                'min-width': '22px',
18234                                'max-width': '22px',
18235                                'min-height': '22px',
18236                                'max-height': '22px',
18237                            }
18238                        }
18239                    } else if ( type === 'week_header' || type === 'day_header' ){
18240                        return {
18241                            'font-weight': '575',
18242                            // 'color': '#4a56f8',
18243                        }
18244                    }
18245                } else if ( !defaultDate ){
18246                    if ( type === 'month' ){
18247                        if ( day.length > 2){
18248                            return {
18249                                'padding': '2.5px',
18250                                'min-width': 'fit-content',
18251                                'max-width': 'fit-content',
18252                                'min-height': 'fit-content',
18253                                'max-height': 'fit-content',
18254                            }
18255                        } else {
18256                            return {
18257                                'min-width': '22px',
18258                                'max-width': '22px',
18259                                'min-height': '22px',
18260                                'max-height': '22px',
18261                            }
18262                        }
18263                    }
18264                }
18265            },
18266            generateEventTitle( eventObject ){
18267                if ( !eventObject.isAllDay && !eventObject.isMultipleDay ) {
18268                    return eventObject.raw.nice_time !== undefined ? `${eventObject.raw.nice_time} ${eventObject.title}` : `${eventObject.title}`;
18269                } else if ( eventObject.isAllDay || eventObject.isMultipleDay ) {
18270                    return eventObject.title || 'Not Provided';
18271                }
18272            },
18273            generateFontSize( type, eventObject ){
18274                if (this.is_membership_cal){
18275                    if (type === 'timestamp') {
18276                        return {
18277                            'font-size': '12px', 
18278                            'font-weight': '300',
18279                        }
18280                    } else if (type === 'allDay'){
18281                        return {
18282                            'font-size': '13px',
18283                        }
18284                    } else if (type === 'eventTitle'){
18285                        if (eventObject?.id === 'placeholder'){
18286                            return {
18287                                'color': 'transparent',
18288                                'font-size': '13px',
18289                            }
18290                        } 
18291                        return {
18292                            'font-size': '13px',
18293                        }
18294                    }
18295                } else {
18296                    if (type === 'timestamp') {
18297                        return {
18298                            'font-size': '10px', 
18299                            'font-weight': '300',
18300                        }
18301                    } else if (type === 'allDay'){
18302                        return {
18303                            'font-size': '11px',
18304                        }
18305                    }else if (type === 'eventTitle'){
18306                        if (eventObject?.id === 'placeholder'){
18307                            return {
18308                                'color': 'transparent',
18309                                'font-size': '12.5px',
18310                            }
18311                        } 
18312                        return {
18313                            'font-size': '12.5px',
18314                        }
18315                    }
18316                }
18317            },
18318            generateEventStyleForMonth( eventObject, type, date = null, monthIndex = null, month_class = null ){
18319                let styleObject = {};
18320                const dotColor = eventObject.raw.dot_color;
18321                const bgColor = eventObject.bgColor || '#e8edf1';
18322                const color = /* eventObject.color || */ this.get_contrast_color( bgColor ) || 'inherit';
18323                if ( type === 'status_dot' ){
18324                    if ( eventObject.id === 'placeholder' ){
18325                        styleObject = {
18326                            'color': 'trasparent',
18327                        }
18328                    } else if ( !eventObject.isAllDay && !eventObject.isMultipleDay ) {
18329                        if ( month_class === 'currentMonthDate' ){
18330                            styleObject = {
18331                                'min-width': '2.2px',
18332                                'max-width': '2.2px',
18333                                'min-height': '2.2px',
18334                                'max-height': '2.2px',
18335                                'border': `2.2px solid ${dotColor || '#333'}`,
18336                                'border-radius': '50%',
18337                                'background-color': `${dotColor || '#333'}`,
18338                                'margin': '0 5px 0 5px',
18339                            }
18340                        } else if ( month_class === 'prevMonthDate' || month_class === 'nextMonthDate' ){
18341                            styleObject = {
18342                                'min-width': '2.2px',
18343                                'max-width': '2.2px',
18344                                'min-height': '2.2px',
18345                                'max-height': '2.2px',
18346                                'border': `2.2px solid rgb(232, 237, 241)`,
18347                                'border-radius': '50%',
18348                                'background-color': `rgb(232, 237, 241)`,
18349                                'margin': '0 5px 0 5px',
18350                            }
18351                        }
18352                    } else if ( eventObject.isAllDay || eventObject.isMultipleDay ){
18353                        styleObject = {
18354                            'margin-right': '3.5px',
18355                        }
18356                    }
18357                } else if ( type === 'event_body' ){
18358                    if ( eventObject.isAllDay && !eventObject.isMultipleDay ) {
18359                        styleObject = {
18360                            'background-color': bgColor,
18361                            'color': color,
18362                        };
18363                    } else if ( eventObject.isMultipleDay ){
18364                        let calendarDay = dayjs(date.$d).format('MMMM DD, YYYY');
18365                        let scheduleStart = dayjs( eventObject.start ).format('MMMM DD, YYYY');
18366                        let scheduleEnd = dayjs( eventObject.end ).format('MMMM DD, YYYY');
18367                        let isOneDay = ((dayjs( eventObject.start ).format('HH:mm:ss') === dayjs( eventObject.end ).format('HH:mm:ss')) && (dayjs( eventObject.start ).format('HH:mm:ss') === dayjs( eventObject.start ).startOf('day').format('HH:mm:ss'))) && dayjs( eventObject.start ).add(1, 'day').format('MMMM DD, YYYY') === scheduleEnd;
18368                        let endsAtMidnight = dayjs( eventObject.end ).format('HH:mm:ss') === dayjs().startOf('day').format('HH:mm:ss');
18369                        if (calendarDay === scheduleStart || monthIndex % 7 === 0 ){
18370                            let remainingDays = dayjs(scheduleEnd, { format: 'MMMM DD, YYYY' }).diff(dayjs(calendarDay, { format: 'MMMM DD, YYYY' }), 'day');
18371                            //handle non-calendarOnly events since end time is different than calendarOnly events
18372                            if (!eventObject.calendarOnly && !isOneDay && !endsAtMidnight ){
18373                                remainingDays += 1;
18374                            }
18375                            if (this.cellWidth === undefined){
18376                                styleObject = {
18377                                    'background-color': bgColor,
18378                                    'color': color,
18379                                    'z-index': '5',
18380                                    'width' : `${remainingDays*this.getCellWidth()}px`
18381                                };
18382                            } else {
18383                                styleObject = {
18384                                    'background-color': bgColor,
18385                                    'color': color,
18386                                    'z-index': '5',
18387                                    'width': `${remainingDays*this.cellWidth}px`
18388                                };
18389                            }
18390                        }
18391                    }
18392                }
18393                return styleObject
18394            },
18395            generateEventStyleForWeek( eventObject, type ){
18396                const event = eventObject.event || eventObject;
18397                const numOfTimeBlocks = eventObject.numOfTimeBlocks;
18398
18399                let styleObject = {};
18400                const dotColor = event?.raw.dot_color;
18401                const bgColor = event?.bgColor || '#e8edf1';
18402                const color = /* event?.color */ this.get_contrast_color( bgColor ) || 'inherit';
18403
18404                if ( type === 'event_body' ){
18405                    if ( event ) {
18406                        styleObject = {
18407                            'background-color': dotColor || bgColor,
18408                            'color': color,
18409                            'width': '50%',
18410                            'border-width': '1px 1px 1px 1.5px',
18411                            'border-left-color': 'black',
18412                            'border-top-color':'white',
18413                            'border-right-color':'transparent',
18414                            'border-bottom-color': 'white',
18415                            'border-style': 'solid',
18416                            'border-radius': '2px',
18417                            'opacity': '85%',
18418                            'z-index': '5',
18419                            'height': `${(numOfTimeBlocks*12) - 5}px`, /* (number of 15 min increments * .placeholder-calendar-grid > grid-auto-rows) - .cell-event-week > padding(top) */
18420                        };
18421                    }
18422                } else if ( type === 'allDay_event_body' ){
18423                    if ( event ) {
18424                        styleObject = {
18425                            'background-color': dotColor || bgColor,
18426                            'color': color,
18427                        };
18428                    }
18429                }
18430                return styleObject
18431            },
18432            toggleOverflowView( date, eventsArr, event ){
18433                if (store.overlays.overflow_overlay){
18434                    store.overlays.overflow_overlay = false;
18435                } else {
18436                    store.components.overflow_overlay.cellPosition = {
18437                        top: (event.clientY + window.scrollY)-(230/2), //230 overlay height
18438                        left: (event.clientX + window.scrollX)-(230/2), //230 overlay width
18439                    }
18440                    if (this.is_membership_cal){
18441                        store.components.overflow_overlay.is_membership_cal = true;
18442                    }
18443                    store.components.overflow_overlay.client_id = this.client_id;
18444                    store.components.overflow_overlay.date = date;
18445                    store.components.overflow_overlay.events = eventsArr;
18446                    store.overlays.overflow_overlay = true;
18447                }
18448            },
18449            handleScheduleClickForMonth( event ){
18450                if (event.id !== 'placeholder') {
18451                    this.$emit('clickSchedule_emitted', event);
18452                } else {
18453                    this.$emit('createSchedule_emitted', this.date);
18454                }
18455            },
18456            //methods to determine font color in contrast to bg color
18457            calculate_luminance(color) {
18458                const hex = color.substring(1); //remove #
18459                const rgb = parseInt(hex, 16); //convert to RGB
18460                const r = (rgb >> 16) & 0xff; //extract the red channel
18461                const g = (rgb >>  8) & 0xff; //extract the green channel
18462                const b = (rgb >>  0) & 0xff; //extract the blue channel
18463                //convert RGB to linear values
18464                const sR = r / 255;
18465                const sG = g / 255;
18466                const sB = b / 255;
18467                //calculate luminance
18468                const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
18469                return luminance;
18470            },
18471            get_contrast_color(background) {
18472                const backgroundLuminance = this.calculate_luminance(background);
18473                const fontColor = backgroundLuminance > 0.5 ? '#000000' : '#ffffff';
18474                return fontColor;
18475            }
18476        },
18477    })
18478</script>
18478    
18478<script type="text/html" id="template-overflow-overlay">
18479    <div v-if="global.overlays.overflow_overlay" id="overlay_overflow_overlay" :style="generateStyle()" class="calendar-overlay active" @click.self="global.overlays.overflow_overlay=false;">
18480        <div class="calendarOverlayCard">
18481            <div class="overlay-events-contain">
18482                <div v-if="me.date" class="overflow-date-contain">
18483                    <span class="overflow-dateOfWeek">{{ me.date.format('ddd') }}</span>
18484                    <span class="overflow-day">{{ me.date.$D }}</span>
18485                </div>
18486                </div>
18487                <template v-for="event in me.events">
18488                    <template v-if="event.id !== 'placeholder'">
18489                        <span 
18490                            class="cell-event-overlay" 
18491                            :style="generateEventStyleForMonth( event, 'event_body' )"
18492                            @click="clickSchedule( event )"
18493                        ><div :style="generateEventStyleForMonth( event, 'status_dot' )"></div><div class="event-title">{{ generateEventTitle( event ) }}</div></span>
18494                    </template>
18495                </template>
18496            </div>
18497            <div class="xButton-calendarOverlay" @click.self="global.overlays.overflow_overlay=false;"></div>
18498        </div>
18499    </div>
18500</script>
18500
18501
18502<script>
18503    //global store
18504    store.components["overflow_overlay"] = {
18505        client_id: null,
18506        date: null,
18507        events: [],
18508        cellPosition: {},
18509        loading: false,
18510        is_membership_cal: false,
18511    };
18512    //define the component
18513    app_overlays.component("overflow-overlay", {
18514        template: "#template-overflow-overlay",
18515        data() {
18516            return {
18517            	global: store,
18518                me: store.components.overflow_overlay
18519            }
18520        },
18521        methods: {
18522            generateStyle(){
18523                const cellPosition = this.me.cellPosition;
18524                const viewportHeight = window.innerHeight;
18525                const viewportWidth = window.innerWidth;
18526                const overlayHeight = 230;
18527                const overlayWidth = 230;
18528
18529                let top = cellPosition.top;
18530                let left = cellPosition.left;
18531
18532                // Adjust top position if overlay goes off-screen
18533                // if ( top + overlayHeight > viewportHeight) {
18534                // }
18535
18536                // Adjust left position if overlay goes off-screen
18537                if ( left + overlayWidth > viewportWidth) {
18538                    let difference = left + overlayWidth;
18539                    left -= difference - viewportWidth; // 20px margin from right
18540                }
18541                return {
18542                    'top': `${top}px`,
18543                    'left': `${left}px`
18544                };
18545            },
18546            generateEventTitle( eventObject ){
18547                if ( !eventObject.isAllDay && !eventObject.isMultipleDay ) {
18548                    return eventObject.raw.nice_time !== undefined ? `${eventObject.raw.nice_time} ${eventObject.title}` : `${eventObject.title}`;
18549                } else if ( eventObject.isAllDay || eventObject.isMultipleDay ) {
18550                    return eventObject.title || 'Not Provided';
18551                }
18552            },
18553            generateEventStyleForMonth( eventObject, type ){
18554                let styleObject = {};                
18555                const dotColor = eventObject.raw.dot_color;
18556                const bgColor = eventObject.bgColor || '#e8edf1';
18557                const color = /* eventObject.color */ this.get_contrast_color( bgColor ) || 'inherit';
18558                if ( type === 'status_dot' ){
18559                    if ( !eventObject.isAllDay && !eventObject.isMultipleDay ) {
18560                        styleObject = {
18561                            'min-width': '2.2px',
18562                            'max-width': '2.2px',
18563                            'min-height': '2.2px',
18564                            'max-height': '2.2px',
18565                            'border': `2.2px solid ${dotColor || '#333'}`,
18566                            'border-radius': '50%',
18567                            'background-color': `${dotColor || '#333'}`,
18568                            'margin': '0 5px 0 5px',
18569                        }
18570                    } else if ( eventObject.isAllDay || eventObject.isMultipleDay ){
18571                        styleObject = {
18572                            'margin-right': '3.5px',
18573                        }
18574                    }
18575                } else if ( type === 'event_body' ){
18576                    if ( eventObject.isAllDay || eventObject.isMultipleDay ) {
18577                        styleObject = {
18578                            'background-color': bgColor,
18579                            'color': color,
18580                            'height': '22px',
18581                        };  
18582                    } 
18583                }
18584                return styleObject
18585            },
18586            clickSchedule(event){
18587                if ( this.me.client_id === 'primary' ){
18588                    store.eventBus.$emit('clickSchedule_events_cal', event)
18589                    store.overlays.overflow_overlay = false;
18590                } else if ( this.me.client_id === 'banquets' ){
18591                    store.eventBus.$emit('clickSchedule_banquets_cal', event)
18592                    store.overlays.overflow_overlay = false;
18593                } else if ( this.me.client_id === 'bookings' ){
18594                    store.eventBus.$emit('clickSchedule_reservations_cal', event)
18595                    store.overlays.overflow_overlay = false;
18596                } else if ( this.me.client_id === 'time_blocks' ){
18597                    store.eventBus.$emit('clickSchedule_time_blocks_cal', event)
18598                    store.overlays.overflow_overlay = false;
18599                } else if (this.me.is_membership_cal ){
18600                    store.eventBus.$emit('clickSchedule_membership_cal', event)
18601                    store.overlays.overflow_overlay = false;
18602                }
18603            },
18604            //methods to determine font color in contrast to bg color
18605            calculate_luminance(color) {
18606                const hex = color.substring(1); //remove #
18607                const rgb = parseInt(hex, 16); //convert to RGB
18608                const r = (rgb >> 16) & 0xff; //extract the red channel
18609                const g = (rgb >>  8) & 0xff; //extract the green channel
18610                const b = (rgb >>  0) & 0xff; //extract the blue channel
18611                //convert RGB to linear values
18612                const sR = r / 255;
18613                const sG = g / 255;
18614                const sB = b / 255;
18615                //calculate luminance
18616                const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
18617                return luminance;
18618            },
18619            get_contrast_color(background) {
18620                const backgroundLuminance = this.calculate_luminance(background);
18621                const fontColor = backgroundLuminance > 0.5 ? '#000000' : '#ffffff';
18622                return fontColor;
18623            }
18624        },
18625    });
18626
18627    
18628</script>
18628    
18628<script type="text/html" id="template-custom-field-wb">
18629    <div class="custom-field-wrapper tinyMarginTop" :id="custom_field.id">
18630        <div class="flex-row-start" style="width: 100%;">
18631            <!-- question -->
18632            <div class="flex-column-start tinyMarginRight" style="width: 100%;">
18633                <div class="custom-field flex-row-start">
18634                    <p v-if="custom_field && custom_field.name" class="default-font-body">{{ custom_field.name }}</p>
18635                    <span v-if="custom_field && custom_field.required" class="flex-row-start">
18636                        <p style="color: rgba(255, 0, 0, 0.885); margin: 0 5.5px 0 3px;">*</p>
18637                        <p class="default-font-body" style="font-size: 14px; color: rgb(150, 152, 154); font-style: italic;">(required)</p>
18638                    </span>
18639                </div>
18640            </div>
18641        </div>
18642        <!-- response options -->
18643        <div style="width: 100%;">
18644            <!-- short response -->
18645            <template v-if="custom_field && custom_field.type === 'text'">
18646                <span class="flex-row-start tinyMarginBottom">
18647                    <input
18648                        type="text" 
18649                        placeholder="Your answer"
18650                        class="border-bottom-grey-2 default-font-body"
18651                        style="width: 50%; line-height: 24px;"
18652                        v-model="input_value"
18653                        :disabled="is_admin"
18654                    >
18655                </span>
18656            </template>
18657            <!-- paragraph -->
18658            <template v-if="custom_field && custom_field.type === 'textarea'">
18659                <span class="flex-row-start tinyMarginBottom">
18660                    <input
18661                        type="text" 
18662                        placeholder="Your answer"
18663                        class="border-bottom-grey-2 default-font-body"
18664                        style="width: 100%; line-height: 24px;"
18665                        v-model="input_value"
18666                        :disabled="is_admin"
18667                    >
18668                </span>
18669            </template>
18670            <!-- multiple choice, checkbox -->
18671            <div v-if="custom_field.type === 'radio' || custom_field.type === 'checkbox'" class="flex-column-start">
18672                <template v-for="(option, index) in custom_field_options" :id="index">
18673                    <span class="flex-row-start" style="position: relative; width: 100%; left: -5px;">
18674                        <!-- multiple choice -->
18675                        <template v-if="custom_field.type === 'radio'">
18676                            <span v-if="!option.selected" @click="select_mc_option( true, option )" class="flex-row-start">
18677                                <i class="fa-light fa-circle options-icon"></i>
18678                                <p v-if="option && option.name" class="default-font-body">{{ option.name }}</p>
18679                            </span>
18680                            <span v-if="option.selected" @click="select_mc_option( false, option )" class="flex-row-start">
18681                                <i class="fa-solid fa-circle-dot options-icon"></i>
18682                                <p v-if="option && option.name" class="default-font-body">{{ option.name }}</p>
18683                            </span>
18684                        </template>
18685                        <!-- checkbox -->
18686                        <template  v-if="custom_field.type === 'checkbox'">
18687                            <span v-if="!option.selected" @click="option.selected = true;" class="flex-row-start">
18688                                <i class="fa-light fa-square options-icon"></i>
18689                                <p v-if="option && option.name" class="default-font-body">{{ option.name }}</p>
18690                            </span>
18691                            <span v-if="option.selected" @click="option.selected = false;" class="flex-row-start">
18692                                <i class="fa-solid fa-square-check options-icon"></i>
18693                                <p v-if="option && option.name" class="default-font-body">{{ option.name }}</p>
18694                            </span>
18695                        </template>
18696                    </span>
18697                </template>
18698            </div>
18699            <!-- dropdown -->
18700            <template  v-if="custom_field.type === 'select' && custom_field_options">
18701                <div id="response-dropdown" :style="{ 
18702                    borderBottomLeftRadius: dropdown_open ? '0' : '4px', 
18703                    borderBottomRightRadius: dropdown_open ? '0' : '4px' 
18704                }">
18705                    <!-- dropdown closed -->
18706                    <div id="selected-response" @click="dropdown_open = !dropdown_open;">
18707                        <p v-if="selected_dropdown_index < 0" class="response-option default-font-body">Choose</p>
18708                        <p v-if="selected_dropdown_index >= 0 && custom_field_options && custom_field_options[selected_dropdown_index]" class="response-option default-font-body">
18709                            {{ custom_field_options[selected_dropdown_index].name }}
18710                        </p>
18711                        <i v-if="!dropdown_open" class="fa-solid fa-sort-down" style="color: rgb(95, 95, 95)"></i>
18712                        <i v-if="dropdown_open" class="fa-solid fa-sort-up" style="color: rgba(95, 95, 95)"></i>
18713                    </div>
18714                    <!-- dropdown open -->
18715                    <div v-if="dropdown_open" id="response-options">
18716                        <div 
18717                            class="response-option border-bottom-grey-2"
18718                            @click="select_dropdown_option( null, -1 )"
18719                            :style="[ return_selected_style( -1 ) ]"
18720                        >
18721                            <p class="default-font-body" style="color: rgb(150, 152, 154);">Choose</p>
18722                        </div>
18723                        <div 
18724                            v-for="(option, index) in custom_field_options" 
18725                            class="response-option"
18726                            :key="option.value" 
18727                            @click="select_dropdown_option( option, index )"
18728                            :style="[ return_selected_style( index ) ]"
18729                        >
18730                            <p class="default-font-body">{{ option.name }}</p>
18731                        </div>
18732                    </div>
18733                </div>
18734            </template>
18735            <!-- searchable dropdown (same option model as select) -->
18736            <template v-if="custom_field && custom_field.type === 'searchable_select' && custom_field_options">
18737                <div class="tinyMarginBottom" style="width: 100%;">
18738                    <rich-select-dropdown
18739                        :value="get_searchable_select_value()"
18740                        :options="rich_wb_dropdown_options()"
18741                        :placeholder="custom_field.name || 'Select...'"
18742                        @input="set_searchable_select_from_rich($event)"
18743                        :showFieldLabelWhenSelected="true"
18744                        :searchable="true"
18745                        :searchSubtext="false"
18746                    ></rich-select-dropdown>
18747                </div>
18748            </template>
18749            <!-- numeric value -->
18750            <template v-if="custom_field && custom_field.type === 'number'">
18751                <span class="flex-row-start tinyMarginBottom">
18752                    <input
18753                        type="text" 
18754                        placeholder="Number"
18755                        class="border-bottom-grey-2 inherit-font-css"
18756                        style="width: 20%; line-height: 24px;"
18757                        v-model="input_value"
18758                        @input="check_input_is_num()"
18759                        :disabled="is_admin"
18760                    >
18761                    <span v-if="display_num_warning" class="flex-row-start" @mouseover="display_popup = true;" @mouseleave="display_popup = false;">
18762                        <i class="fa-solid fa-triangle-exclamation" style="color: rgba(255, 0, 0, 0.885); font-size: 1em; cursor: pointer;"></i>
18763                    </span>
18764                    <div v-if="display_popup" class="display-popup" style="font-size: 12px;">This value must be a number.</div>
18765                </span>
18766            </template>
18767            <!-- file upload -->
18768            <template v-if="custom_field && custom_field.type === 'file_upload'">              
18769                <file-upload-component
18770                    @file_selected="file_selected"
18771                    :valid_file_types="valid_file_types"
18772                ></file-upload-component>
18773            </template>
18774            <!-- date value -->
18775            <template v-if="custom_field && custom_field.type === 'date'">
18776                <span class="flex-row-start tinyMarginBottom">
18777                    <input
18778                        type="date" 
18779                        placeholder="mm/dd/yyyy"
18780                        class="border-bottom-grey-2 default-font-body"
18781                        style="width: 20%; line-height: 24px;"
18782                        v-model="input_value"
18783                        :disabled="is_admin"
18784                    >
18785                </span>
18786            </template>
18787            <!-- rating -->
18788            <template v-if="custom_field && custom_field.type === 'rating'">
18789                <div class="flex-row-start tinyMarginBottom" style="gap: 12px; padding-top: 4px;">
18790                    <i
18791                        v-for="star in 5"
18792                        :key="star"
18793                        :class="star <= input_value ? 'fa-solid fa-star' : 'fa-regular fa-star'"
18794                        style="font-size: 28px; color: #4a56f8; cursor: pointer;"
18795                        @click="set_rating( star )"
18796                    ></i>
18797                </div>
18798            </template>
18799        </div>
18800    </div>
18801</script>
vendor: 1 bytes, line 18801
18801
18802<script>
18803    app_content.component("custom-field-component-wb", {
18804        template: "#template-custom-field-wb",
18805        data() {
18806            return {
18807                selected_dropdown_index: -1, //default
18808                dropdown_open: false,
18809                custom_field_name: '',
18810                custom_field_required: false,
18811                display_num_warning: false,
18812                display_popup: false,
18813                input_value: '',
18814            }
18815        },
18816        props: {
18817            form: Object,
18818            is_admin: Boolean,
18819            custom_field: Object,
18820            last_update: Number,
18821        },
18822        methods: {
18823            return_selected_style( index ){
18824                if ( index === this.selected_dropdown_index ){
18825                    return {
18826                        'background-color': 'rgba(26, 115, 232, .09)',
18827                    }
18828                }
18829            },
18830            clear_dropdown_index(){
18831                this.selected_dropdown_index = -1;
18832            },
18833            clear_field(){
18834                this.input_value = '';
18835                this.selected_dropdown_index = -1;
18836                this.display_num_warning = false;
18837                if ( this.custom_field_options ){
18838                    this.custom_field_options.forEach(option => {
18839                        option.selected = false;
18840                    });
18841                }
18842            },
18843            select_dropdown_option( option, index ){
18844                this.selected_dropdown_index = index;
18845                this.dropdown_open = false;
18846                if ( option ){
18847                    option.selected = true;
18848                    this.unselect_other_options( option, false );
18849                } else if ( !option ){
18850                    this.unselect_other_options( option, true );
18851                }
18852            },
18853            select_mc_option( select, option ){
18854                if ( select ){
18855                    option.selected = true;
18856                    this.unselect_other_options( option, false );
18857                } else if ( !select ){
18858                    option.selected = false;
18859                }
18860            },
18861            get_searchable_select_value(){
18862                if ( !this.custom_field_options || !this.custom_field_options.length ) return null;
18863                const selected = this.custom_field_options.find(o => o.selected);
18864                return selected ? selected.id : null;
18865            },
18866            rich_wb_dropdown_options(){
18867                if ( !this.custom_field_options ) return [];
18868                return this.custom_field_options.map(o => ({ id: o.id, name: o.name || '' }));
18869            },
18870            set_searchable_select_from_rich(val){
18871                let id = null;
18872                if ( val != null && typeof val === 'object' && val.id != null ){
18873                    id = val.id;
18874                } else {
18875                    id = val;
18876                }
18877                if ( !id ){
18878                    this.select_dropdown_option(null, -1);
18879                    return;
18880                }
18881                const idx = this.custom_field_options.findIndex(o => o.id === id);
18882                if ( idx === -1 ){
18883                    this.select_dropdown_option(null, -1);
18884                    return;
18885                }
18886                this.select_dropdown_option(this.custom_field_options[idx], idx);
18887            },
18888            unselect_other_options( option, clear_all = false ){
18889                if ( clear_all ){
18890                    this.custom_field_options.forEach( custom_field_option => {
18891                        custom_field_option.selected = false;
18892                    })
18893                } else if ( !clear_all ){
18894                    this.custom_field_options.forEach( custom_field_option => {
18895                        if ( custom_field_option.id !== option.id ){
18896                            custom_field_option.selected = false;
18897                        }
18898                    })
18899                }
18900            },
18901            check_input_is_num(){
18902                let number = Number(this.input_value);
18903                if ( isNaN(number) || !this.input_value ){
18904                    this.display_num_warning = true;
18905                } else {
18906                    this.input_value = number;
18907                    this.display_num_warning = false;
18908                }
18909            },
18910            file_selected( data ){
18911                let file = data.value;
18912                if (file) {
18913                    this.input_value = file;
18914                }
18915            },
18916            set_rating( star ){
18917                if ( this.is_admin ){
18918                    return;
18919                }
18920                this.input_value = star;
18921            },
18922        },
18923        computed: {
18924            custom_field_options(){
18925                if ( this.custom_field && this.custom_field.custom_field_options ){
18926                    return this.custom_field.custom_field_options;
18927                }
18928                return [];
18929            },
18930            valid_file_types(){
18931                return [ ];
18932            },
18933        },
18934        watch: {
18935            input_value( newValue ){
18936                this.$emit('field-updated', {
18937                    id: this.custom_field.id,
18938                    value: newValue
18939                });
18940            }
18941        }
18942    })
18943</script>
18943
18944
18945
18946<style scoped>
18947    :root {
18948        --custom-field-grey-1: rgba(180, 180, 180, 0.8);
18949        --custom-field-grey-2: rgba(195, 195, 195, 0.7);
18950        --custom-field-grey-3: rgb(154, 160, 166);
18951        --custom-field-primary: #202124;
18952    }
18953    .custom-field-wrapper{
18954        width: 100%;
18955        border: 1px solid rgba(195, 195, 195, 0.7);
18956        border-radius: 8px;
18957        padding: 1.45em;
18958        padding-top: 10px;
18959        background-color: #fff;
18960        box-sizing: border-box;
18961    }
18962    .custom-field-wrapper .options-icon{
18963        color: var(--custom-field-grey-3);
18964        margin-right: 8px;
18965        padding: 7.5px;
18966        cursor: pointer;
18967        border-radius: 50%;
18968        font-size: 1.1em;
18969    }
18970    .custom-field-wrapper .options-icon:hover{
18971        background-color: rgba(150, 150, 150, 0.1);
18972    }
18973    .custom-field-wrapper .default-font-body{
18974        font-family: Roboto, Arial, Helvetica, sans-serif;
18975        font-size: 15px; 
18976        font-weight: 400;
18977        text-align: left;
18978    }
18979    .custom-field-wrapper .file-upload-wrapper{
18980        height: 90px;
18981        width: 50%;
18982        border: 1px dashed var(--custom-field-grey-1);
18983    }
18984    .custom-field-wrapper  .inherit-font-css{
18985        font-family: inherit;
18986        font-size: inherit;
18987        font-weight: inherit;
18988    }
18989    .custom-field-wrapper .flex-row-center{
18990        display: flex;
18991        flex-direction: row;
18992        align-items: center;
18993        justify-content: center;
18994    }
18995    .custom-field-wrapper .flex-column-center{
18996        display: flex;
18997        flex-direction: column;
18998        align-items: center;
18999        justify-content: center;
19000    }
19001    .custom-field-wrapper .flex-row-start{
19002        display: flex;
19003        flex-direction: row;
19004        align-items: center;
19005        justify-content: flex-start;
19006    }
19007    .custom-field-wrapper .flex-column-start{
19008        display: flex;
19009        flex-direction: column;
19010        align-items: flex-start;
19011        justify-content: center;
19012    }
19013    .custom-field-wrapper .custom-field{
19014        color: rgb(32, 33, 36);
19015        box-sizing: border-box;
19016        font-weight: 400;
19017        font-size: 14px;
19018        width: 100%;
19019        line-height: 2.5;
19020        letter-spacing: 0;
19021    }
19022    .custom-field-wrapper .custom-field > .custom-field-input{
19023        font-family: inherit;
19024        font-size: inherit;
19025        font-weight: inherit;
19026        width: inherit;
19027        line-height: inherit;
19028        background-color: rgb(248, 249, 250);
19029        padding: 5px 0 5px 15px;
19030    }
19031    .custom-field-wrapper .border-bottom-grey-2, .custom-field-wrapper .input-is-focused{
19032        border-bottom: 1.25px solid var(--custom-field-grey-2);
19033    }
19034    .custom-field-wrapper input {
19035        transition: border-color 0.2s;
19036    }
19037    #response-dropdown{
19038        position: relative;
19039        display: flex;
19040        flex-direction: column;
19041        align-items: flex-start;
19042        justify-content: center;
19043        font-size: 14px; 
19044        color: var(--custom-field-primary);
19045        background-color: #fff;
19046        width: 33%;
19047        padding: 12px;
19048        border: 1px solid var(--custom-field-grey-1);
19049        border-top-left-radius: 4px;
19050        border-top-right-radius: 4px;
19051    }
19052    #response-dropdown > #selected-response{
19053        display: flex;
19054        flex-direction: row;
19055        align-items: center;
19056        justify-content: space-between;
19057        width: 100%;
19058        cursor: pointer;
19059    }
19060    #response-options{
19061        position: absolute;
19062        top: 100%;
19063        left: -1px;
19064        z-index: 10;
19065        background-color: #fff;
19066        border: 1px solid var(--custom-field-grey-1);
19067        border-bottom-left-radius: 4px;
19068        border-bottom-right-radius: 4px;
19069        width: 100%;
19070    }
19071    #response-options > .response-option{
19072        display: flex;
19073        flex-direction: row;
19074        align-items: center;
19075        justify-content: flex-start;
19076        padding: 12px;
19077        font-family: Roboto;
19078        cursor: pointer;
19079    }
19080    #response-options > .response-option:hover{
19081        background-color: rgba(150, 150, 150, 0.2);
19082    }
19083    .display-popup{
19084        background-color: rgb(245, 245, 245);
19085        padding: 5px;
19086        border-radius: 6px;
19087        position: relative;
19088        left: -100px;
19089        bottom: 30px;
19090        z-index: 10;
19091        width: fit-content;
19092    }
19093</style>
19094
19095    
19095<script type="text/html" id="template-rich-select-dropdown">
19096    <div class="relative">
19097        <div v-if="showFieldLabelWhenSelected && has_selected_value" class="rich-select-floating-label">
19098            <span class="rich-select-trigger-title">{{ label || placeholder }}</span>
19099        </div>
19100        <div 
19101            class="rich-select-dropdown relative" ref="container"
19102            style="width: 100%;"
19103        >    <!-- Closed state: box showing selected option(s) -->
19104            <div
19105                class="rich-select-trigger flexNoWrap align-items-center pointer"
19106                :class="[triggerClass, { 'rich-select-trigger-open': isOpen, 'rich-select-trigger-borderless': borderless, 'rich-select-trigger-disabled': disabled }]"
19107                @click="toggle()"
19108            >
19109                <slot name="indicator"></slot>
19110                <template v-if="multiple">
19111                    <template v-if="selectedOptionsForDisplay.length === 0">
19112                        <span class="rich-select-placeholder">{{ label || placeholder }}</span>
19113                    </template>
19114                    <template v-else-if="selectedOptionsForDisplay.length === 1">
19115                        <div v-if="optionImage(selectedOptionsForDisplay[0])" class="rich-select-option-icon-wrap">
19116                            <img v-if="optionImageIsUrl(selectedOptionsForDisplay[0])" :src="optionImage(selectedOptionsForDisplay[0])" class="rich-select-option-icon" alt="">
19117                            <i v-else :class="optionImage(selectedOptionsForDisplay[0])" class="rich-select-option-icon fa"></i>
19118                        </div>
19119                        <div class="rich-select-trigger-text">
19120                            <span class="rich-select-trigger-title">{{ optionTitle(selectedOptionsForDisplay[0]) }}</span>
19121                            <span v-if="optionSubtext(selectedOptionsForDisplay[0])" class="rich-select-trigger-subtext">{{ optionSubtext(selectedOptionsForDisplay[0]) }}</span>
19122                        </div>
19123                    </template>
19124                    <template v-else>
19125                        <span class="rich-select-trigger-title">{{ selectedOptionsForDisplay.length }} selected</span>
19126                    </template>
19127                </template>
19128                <template v-else>
19129                    <template v-if="selectedOptionSingle">
19130                        <div v-if="optionImage(selectedOptionSingle)" class="rich-select-option-icon-wrap">
19131                            <img v-if="optionImageIsUrl(selectedOptionSingle)" :src="optionImage(selectedOptionSingle)" class="rich-select-option-icon" alt="">
19132                            <i v-else :class="optionImage(selectedOptionSingle)" class="rich-select-option-icon fa"></i>
19133                        </div>
19134                        <div class="rich-select-trigger-text">
19135                            <span class="rich-select-trigger-title" :class="{ 'rich-select-trigger-title-no-subtext': !optionSubtext(selectedOptionSingle) }">{{ optionTitle(selectedOptionSingle) }}</span>
19136                            <span v-if="optionSubtext(selectedOptionSingle)" class="rich-select-trigger-subtext">{{ optionSubtext(selectedOptionSingle) }}</span>
19137                        </div>
19138                    </template>
19139                    <template v-else-if="disabled && disabledSubtext">
19140                        <div class="rich-select-trigger-text">
19141                            <span class="rich-select-placeholder">{{ label || placeholder }}</span>
19142                            <span class="rich-select-trigger-subtext">{{ disabledSubtext }}</span>
19143                        </div>
19144                    </template>
19145                    <template v-else>
19146                        <span class="rich-select-placeholder">{{ label || placeholder }}</span>
19147                    </template>
19148                </template>
19149                <div class="flexGrowOne"></div>
19150                <i class="fa-light fa-chevron-down rich-select-chevron" :class="{ 'rich-select-chevron-open': isOpen }" style="z-index: 10;"></i>
19151            </div>
19152
19153            <!-- Open state: dropdown panel (hovering above content) -->
19154            <div
19155                v-show="isOpen"
19156                class="rich-select-dropdown-panel boxShadowBottom moreBlur"
19157                :class="{ 'rich-select-dropdown-panel--fixed': uses_fixed_dropdown_panel && isOpen }"
19158                :style="fixedDropdownPanelInlineStyle"
19159                ref="dropdownPanel"
19160                @mouseover="isMouseOver = true"
19161                @mouseout="isMouseOver = false"
19162                tabindex="-1"
19163                @focusout="scheduleClose"
19164            >
19165                <div v-if="searchable" class="rich-select-search-wrap relative">
19166                    <input
19167                        ref="searchInput"
19168                        v-model="searchQuery"
19169                        class="rich-select-search-input"
19170                        :aria-label="search_placeholder || 'Search...'"
19171                        @click.stop
19172                    >
19173                    <span
19174                        v-if="!searchQuery"
19175                        class="unclickable"
19176                        style="position:absolute; left:18px; right:18px; top:50%; transform:translateY(-50%); color:#9ca4b7; font-size:14px; font-family:roboto, sans-serif; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; pointer-events:none;"
19177                    >{{ search_placeholder || 'Search...' }}</span>
19178                </div>
19179                <div class="rich-select-dropdown-list scrollable" ref="dropdownList">
19180                    <div v-if="loading" class="rich-select-option flexNoWrap align-items-center unclickable">
19181                        <div class="rich-select-option-text">
19182                            <span class="rich-select-option-subtext">Searching...</span>
19183                        </div>
19184                    </div>
19185                    <div
19186                        v-if="showNoneOption"
19187                        class="rich-select-option flexNoWrap align-items-center"
19188                        :class="{ 'rich-select-option-selected': value == null }"
19189                        @click="selectOption(null)"
19190                    >
19191                        <div class="rich-select-option-text">
19192                            <span class="rich-select-option-title">None</span>
19193                        </div>
19194                    </div>
19195                    <!-- Suggested section: only renders when the parent passes
19196                            lookup-derived candidates AND the user hasn't typed a
19197                            search query. Header + divider use inline styles per
19198                            the no-new-classes rule for HTML files. -->
19199                    <template v-if="visibleSuggestedOptions.length > 0">
19200                        <p style="margin: 0; padding: 8px 14px 4px 14px; font-size: 10px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: #7a8298;">{{ suggested_label }}</p>
19201                        <div
19202                            v-for="option in visibleSuggestedOptions"
19203                            :key="'suggested-' + optionId(option)"
19204                            class="rich-select-option flexNoWrap align-items-center"
19205                            :class="{
19206                                'rich-select-option-selected': isSelected(option),
19207                                'unclickable': option && (option.disabled || option.visually_disabled)
19208                            }"
19209                            @click="selectOption(option)"
19210                        >
19211                            <template v-if="multiple">
19212                                <i v-if="isSelected(option)" class="fa-solid fa-square-check rich-select-option-check"></i>
19213                                <i v-else class="fa-light fa-square rich-select-option-check"></i>
19214                            </template>
19215                            <div v-if="optionImage(option)" class="rich-select-option-icon-wrap">
19216                                <img v-if="optionImageIsUrl(option)" :src="optionImage(option)" class="rich-select-option-icon" alt="">
19217                                <i v-else :class="optionImage(option)" class="rich-select-option-icon fa"></i>
19218                            </div>
19219                            <div class="rich-select-option-text">
19220                                <span class="rich-select-option-title" :class="optionTitleClass(option)">{{ optionTitle(option) }}</span>
19221                                <span v-if="optionSubtext(option)" class="rich-select-option-subtext">{{ optionSubtext(option) }}</span>
19222                            </div>
19223                            <slot name="option-action" :option="option"></slot>
19224                        </div>
19225                        <div style="height: 1px; background-color: #e4ebf1; margin: 4px 0;"></div>
19226                    </template>
19227                    <div
19228                        v-for="option in nonSuggestedFilteredOptions"
19229                        :key="optionId(option)"
19230                        class="rich-select-option flexNoWrap align-items-center"
19231                        :class="{
19232                            'rich-select-option-selected': isSelected(option),
19233                            'unclickable': option && (option.disabled || option.visually_disabled)
19234                        }"
19235                        @click="selectOption(option)"
19236                    >
19237                        <template v-if="multiple">
19238                            <i v-if="isSelected(option)" class="fa-solid fa-square-check rich-select-option-check"></i>
19239                            <i v-else class="fa-light fa-square rich-select-option-check"></i>
19240                        </template>
19241                        <div v-if="optionImage(option)" class="rich-select-option-icon-wrap">
19242                            <img v-if="optionImageIsUrl(option)" :src="optionImage(option)" class="rich-select-option-icon" alt="">
19243                            <i v-else :class="optionImage(option)" class="rich-select-option-icon fa"></i>
19244                        </div>
19245                        <div class="rich-select-option-text">
19246                            <span class="rich-select-option-title" :class="optionTitleClass(option)">{{ optionTitle(option) }}</span>
19247                            <span v-if="optionSubtext(option)" class="rich-select-option-subtext">{{ optionSubtext(option) }}</span>
19248                        </div>
19249                        <slot name="option-action" :option="option"></slot>
19250                    </div>
19251                    <div v-if="!loading && nonSuggestedFilteredOptions.length == 0" class="rich-select-option flexNoWrap align-items-center unclickable">
19252                        <div class="rich-select-option-text">
19253                            <span class="rich-select-option-subtext">{{ empty_text || "No options found" }}</span>
19254                        </div>
19255                    </div>
19256                </div>
19257                <slot name="panel-footer"></slot>
19258            </div>
19259        </div>
19260    </div>
19261</script>
19261
19262
19263<script>
19264(function() {
19265    function optionId(option) {
19266        if (!option) return '';
19267        return option.id != null ? option.id : (option.get ? option.get('id') : option);
19268    }
19269    function optionTitle(option) {
19270        if (!option) return '';
19271        return option.title != null ? option.title : (option.name != null ? option.name : (option.get ? (option.get('title') || option.get('name')) : ''));
19272    }
19273    function optionSubtext(option) {
19274        if (!option) return '';
19275        return option.subtext != null ? option.subtext : (option.get ? option.get('subtext') : '');
19276    }
19277    function optionTitleClass(option) {
19278        if (!option) return '';
19279        return option.title_class != null ? option.title_class : (option.get ? option.get('title_class') : '');
19280    }
19281    function optionImage(option) {
19282        if (!option) return null;
19283        return option.image != null ? option.image : (option.icon != null ? option.icon : (option.get ? (option.get('image') || option.get('icon')) : null));
19284    }
19285    function optionImageIsUrl(option) {
19286        var img = optionImage(option);
19287        return img && (typeof img === 'string' && (img.indexOf('http') === 0 || img.indexOf('/') === 0 || img.indexOf('data:') === 0));
19288    }
19289
19290    var richSelectDropdownConfig = {
19291        template: "#template-rich-select-dropdown",
19292        emits: ['input', 'update:value', 'change', 'search'],
19293        data() {
19294            return {
19295                isOpen: false,
19296                isMouseOver: false,
19297                closeTimeout: null,
19298                searchQuery: '',
19299                fixedPanelPositionStyle: null,
19300                _fixedPanelScrollCleanup: null,
19301                auto_fix_dropdown_panel: false,
19302            };
19303        },
19304        mounted() {
19305            this.auto_fix_dropdown_panel = !!this.$refs.container.closest('.wb-inline-controls');
19306        },
19307        watch: {
19308            isOpen: function(is_open) {
19309                if (!this.uses_fixed_dropdown_panel) {
19310                    return;
19311                }
19312                if (is_open) {
19313                    this.scroll_trigger_into_safe_view_if_needed();
19314                    this.sync_fixed_dropdown_panel();
19315                    this.$nextTick(() => {
19316                        this.sync_fixed_dropdown_panel();
19317                        this.teardown_fixed_dropdown_panel_listeners();
19318                        this._fixedPanelScrollCleanup = this.setup_fixed_dropdown_panel_listeners();
19319                    });
19320                } else {
19321                    this.fixedPanelPositionStyle = null;
19322                    this.teardown_fixed_dropdown_panel_listeners();
19323                }
19324            },
19325            searchQuery: function(updated_query) {
19326                if (!this.searchable) {
19327                    return;
19328                }
19329                this.$emit('search', updated_query);
19330            }
19331        },
19332        props: {
19333            options: {
19334                type: Array,
19335                default: function() { return []; }
19336            },
19337            value: {
19338                type: [Object, Array, String, Number],
19339                default: null
19340            },
19341            multiple: {
19342                type: Boolean,
19343                default: false
19344            },
19345            placeholder: {
19346                type: String,
19347                default: "Select..."
19348            },
19349            label: {
19350                type: String,
19351                default: ""
19352            },
19353            triggerClass: {
19354                type: String,
19355                default: ""
19356            },
19357            /** If true, options may be Backbone models with .get('key') */
19358            parse_objects: {
19359                type: Boolean,
19360                default: false
19361            },
19362            showFieldLabelWhenSelected: {
19363                type: Boolean,
19364                default: false
19365            },
19366            searchable: {
19367                type: Boolean,
19368                default: false
19369            },
19370            search_placeholder: {
19371                type: String,
19372                default: ""
19373            },
19374            remote_search: {
19375                type: Boolean,
19376                default: false
19377            },
19378            loading: {
19379                type: Boolean,
19380                default: false
19381            },
19382            empty_text: {
19383                type: String,
19384                default: ''
19385            },
19386            /** When true, the closed trigger renders without its border/box so it can sit flush inside a table cell. */
19387            borderless: {
19388                type: Boolean,
19389                default: false
19390            },
19391            searchSubtext: {
19392                type: Boolean,
19393                default: true
19394            },
19395            /** When true, the open panel uses position:fixed so it escapes overflow:auto 
19395scroll parents (e.g. lodging step). */
19396            dropdownPanelFixed: {
19397                type: Boolean,
19398                default: false
19399            },
19400            /** When true, a "None" option is shown at the top of the list to clear the selection. */
19401            showNoneOption: {
19402                type: Boolean,
19403                default: false
19404            },
19405            /**
19406             * Optional, ranked list of options to surface at the top of the
19407             * dropdown panel under a "Suggested" header. Items must use the
19408             * same shape as `options` (id + title/name). The component
19409             * doesn't decide what's "suggested" — that's the parent's job
19410             * (e.g. the add-product modal feeds in the top N candidates from
19411             * its similar-products lookup). Hidden once the user starts
19412             * typing in the search input so search results aren't fragmented
19413             * across two sections.
19414             */
19415            suggested_options: {
19416                type: Array,
19417                default: function() { return []; }
19418            },
19419            /** Header label rendered above suggested_options. */
19420            suggested_label: {
19421                type: String,
19422                default: 'Suggested'
19423            },
19424            /** When true, the trigger can't be opened and renders in a muted state. */
19425            disabled: {
19426                type: Boolean,
19427                default: false
19428            },
19429            /** Explanatory line rendered under the placeholder when disabled (e.g. "No products found"). */
19430            disabledSubtext: {
19431                type: String,
19432                default: null
19433            }
19434        },
19435        computed: {
19436            uses_fixed_dropdown_panel() {
19437                return this.dropdownPanelFixed || this.auto_fix_dropdown_panel;
19438            },
19439            fixedDropdownPanelInlineStyle() {
19440                if (!this.uses_fixed_dropdown_panel || !this.isOpen || !this.fixedPanelPositionStyle) {
19441                    return null;
19442                }
19443                return this.fixedPanelPositionStyle;
19444            },
19445            displayOptions() {
19446                return this.options || [];
19447            },
19448            filteredOptions() {
19449                if (this.remote_search) {
19450                    return this.displayOptions;
19451                }
19452                if (!this.searchable || !this.searchQuery.trim()) {
19453                    return this.displayOptions;
19454                }
19455                var normalized_search_query = this.searchQuery.trim().toLowerCase();
19456                var searchSubtext = this.searchSubtext;
19457                return this.displayOptions.filter(function(option) {
19458                    var title = (optionTitle(option) || '').toLowerCase();
19459                    if (title.indexOf(normalized_search_query) !== -1) {
19460                        return true;
19461                    }
19462                    if (searchSubtext) {
19463                        var option_subtext = (optionSubtext(option) || '').toLowerCase();
19464                        if (option_subtext.indexOf(normalized_search_query) !== -1) {
19465                            return true;
19466                        }
19467                    }
19468                    return false;
19469                });
19470            },
19471            // Suggested-section visibility. Hidden once the user types so the
19472            // search hit list doesn't get split across two sections.
19473            visibleSuggestedOptions() {
19474                if (!Array.isArray(this.suggested_options) || this.suggested_options.length === 0) {
19475                    return [];
19476                }
19477                if (this.searchable && this.searchQuery && this.searchQuery.trim()) {
19478                    return [];
19479                }
19480                return this.suggested_options;
19481            },
19482            // Filtered options minus anything already rendered in the
19483            // suggested section, so suggested items don't appear twice in
19484            // the panel.
19485            nonSuggestedFilteredOptions() {
19486                var suggested = this.visibleSuggestedOptions;
19487                if (!suggested.length) {
19488                    return this.filteredOptions;
19489                }
19490                var suggestedIds = {};
19491                suggested.forEach(function(option) {
19492                    suggestedIds[optionId(option)] = true;
19493                });
19494                return this.filteredOptions.filter(function(option) {
19495                    return !suggestedIds[optionId(option)];
19496                });
19497            },
19498            selectedOptionSingle() {
19499                if (this.multiple || !this.options.length) return null;
19500                var v = this.value;
19501                if (v == null) return null;
19502                var id = typeof v === 'object' && v !== null && (v.id != null || (v.get && v.get('id') != null))
19503                    ? (v.id != null ? v.id : v.get('id'))
19504                    : v;
19505                return this.options.find(function(o) { return optionId(o) == id; }) || null;
19506            },
19507            selectedOptionsForDisplay() {
19508                if (!this.multiple || !Array.isArray(this.value)) return [];
19509                var ids = this.value;
19510                return this.options.filter(function(o) { return ids.some(function(id) { return id == optionId(o); }); });
19511            },
19512            has_selected_value() {
19513                if (this.multiple) {
19514                    return this.selectedOptionsForDisplay.length > 0;
19515                }
19516                return !!this.selectedOptionSingle;
19517            }
19518        },
19519        methods: {
19520            optionId: function(o) { return optionId(o); },
19521            optionTitle: function(o) { return optionTitle(o); },
19522            optionSubtext: function(o) { return optionSubtext(o); },
19523            optionTitleClass: function(option) { return optionTitleClass(option); },
19524            optionImage: function(o) { return optionImage(o); },
19525            optionImageIsUrl: function(o) { return optionImageIsUrl(o); },
19526            isSelected(option) {
19527                var id = optionId(option);
19528                if (this.multiple && Array.isArray(this.value))
19529                    return this.value.some(function(v) { return v == id; });
19530                var single = this.value;
19531                if (single == null) return false;
19532                var currentId = typeof single === 'object' && single !== null
19533                    ? (single.id != null ? single.id : (single.get ? single.get('id') : null))
19534                    : single;
19535                return currentId == id;
19536            },
19537            toggle() {
19538                if (this.disabled) {
19539                    return;
19540                }
19541                if (this.closeTimeout) {
19542                    clearTimeout(this.closeTimeout);
19543                    this.closeTimeout = null;
19544                }
19545                this.isOpen = !this.isOpen;
19546                if (this.isOpen) {
19547                    this.searchQuery = '';
19548                    this.$nextTick(() => {
19549                        if (this.$refs.dropdownList) {
19550                            this.$refs.dropdownList.scrollTop = 0;
19551                        }
19552                        if (this.uses_fixed_dropdown_panel && this.searchable) {
19553                            requestAnimationFrame(() => {
19554                                requestAnimationFrame(() => {
19555                                    this.focus_open_panel();
19556                                });
19557                            });
19558                        } else {
19559                            this.focus_open_panel();
19560                        }
19561                    });
19562                    document.addEventListener('click', this.handleClickOutside);
19563                } else {
19564                    this.searchQuery = '';
19565                    document.removeEventListener('click', this.handleClickOutside);
19566                }
19567            },
19568            focus_open_panel() {
19569                if (!this.isOpen) {
19570                    return;
19571                }
19572                if (this.searchable && this.$refs.searchInput) {
19573                    this.$refs.searchInput.focus();
19574                } else if (this.$refs.dropdownPanel) {
19575                    this.$refs.dropdownPanel.focus();
19576                }
19577            },
19578            scheduleClose() {
19579                var self = this;
19580                this.closeTimeout = setTimeout(function() {
19581                    if (!self.isMouseOver) {
19582                        self.isOpen = false;
19583                        document.removeEventListener('click', self.handleClickOutside);
19584                    }
19585                }, 150);
19586            },
19587            handleClickOutside(e) {
19588                var panel = this.$refs.dropdownPanel;
19589                var inPanel = panel && panel.contains(e.target);
19590                var inContainer = this.$refs.container && this.$refs.container.contains(e.target);
19591                if (!inContainer && !inPanel) {
19592                    this.isOpen = false;
19593                    document.removeEventListener('click', this.handleClickOutside);
19594                }
19595            },
19596            return_visual_viewport() {
19597                let view = window.visualViewport;
19598                if (view) {
19599                    return {
19600                        height: view.height,
19601                        width: view.width,
19602                        offset_top: view.offsetTop || 0,
19603                        offset_left: view.offsetLeft || 0,
19604                    };
19605                }
19606                return {
19607                    height: window.innerHeight,
19608                    width: window.innerWidth,
19609                    offset_top: 0,
19610                    offset_left: 0,
19611                };
19612            },
19613            scroll_trigger_into_safe_view_if_needed() {
19614                if (!this.searchable || !this.$refs.container) {
19615                    return;
19616                }
19617                let view = this.return_visual_viewport();
19618                let trigger_rect = this.$refs.container.getBoundingClientRect();
19619                if (view.height - trigger_rect.bottom >= 200) {
19620                    return;
19621                }
19622                let node = this.$refs.container.parentElement;
19623                while (node && node !== document.documentElement) {
19624                    let style = window.getComputedStyle(node);
19625                    let overflow_y = style.overflowY;
19626                    let overflow = style.overflow;
19627                    if (overflow_y === 'auto' || overflow_y === 'scroll' || overflow === 'auto' || overflow === 'scroll') {
19628                        let parent_rect = node.getBoundingClientRect();
19629                        let delta = trigger_rect.top - parent_rect.top - 8;
19630                        if (delta > 0) {
19631                            node.scrollTop = node.scrollTop + delta;
19632                        }
19633                        return;
19634                    }
19635                    node = node.parentElement;
19636                }
19637            },
19638            sync_fixed_dropdown_panel() {
19639                if (!this.uses_fixed_dropdown_panel || !this.isOpen || !this.$refs.container) {
19640                    return;
19641                }
19642                let trigger_rect = this.$refs.container.getBoundingClientRect();
19643                let view = this.return_visual_viewport();
19644                let edge = 8;
19645                let gap = 4;
19646                let desired_height = 280;
19647                let min_usable = 160;
19648                let space_below = view.height - trigger_rect.bottom - edge;
19649                let space_above = trigger_rect.top - edge;
19650                let visual_top = trigger_rect.bottom + gap;
19651                let max_height = Math.min(desired_height, space_below);
19652
19653                if (space_below < min_usable && space_above > space_below) {
19654                    max_height = Math.min(desired_height, space_above);
19655                    visual_top = trigger_rect.top - gap - max_height;
19656                } else if (space_below < 80 && space_above >= 80) {
19657                    max_height = Math.min(desired_height, space_above);
19658                    visual_top = trigger_rect.top - gap - max_height;
19659                } else if (space_below < 80 && space_above < 80) {
19660                    visual_top = edge;
19661                    max_height = view.height - edge * 2;
19662                } else if (space_below < min_usable) {
19663                    max_height = Math.max(space_below, 0);
19664                    visual_top = trigger_rect.bottom + gap;
19665                }
19666
19667                if (visual_top < edge) {
19668                    max_height = max_height - (edge - visual_top);
19669                    visual_top = edge;
19670                }
19671                if (visual_top + max_height > view.height - edge) {
19672                    max_height = view.height - edge - visual_top;
19673                }
19674                if (max_height < 80) {
19675                    max_height = Math.min(80, Math.max(0, view.height - edge * 2));
19676                    visual_top = Math.min(visual_top, view.height - edge - max_height);
19677                    visual_top = Math.max(edge, visual_top);
19678                }
19679
19680                //keyboard pan: getBoundingClientRect is visual, position:fixed is layout
19681                this.fixedPanelPositionStyle = {
19682                    position: 'fixed',
19683                    top: (visual_top + view.offset_top) + 'px',
19684                    left: (trigger_rect.left + view.offset_left) + 'px',
19685                    width: trigger_rect.width + 'px',
19686                    minWidth: trigger_rect.width + 'px',
19687                    maxHeight: max_height + 'px',
19688                    zIndex: 10050,
19689                };
19690            },
19691            setup_fixed_dropdown_panel_listeners() {
19692                let sync_panel = () => {
19693                    this.sync_fixed_dropdown_panel();
19694                };
19695                let scroll_nodes = [];
19696                let node = this.$refs.container;
19697                while (node && node !== document.documentElement) {
19698                    let style = window.getComputedStyle(node);
19699                    let overflow_y = style.overflowY;
19700                    let overflow_x = style.overflowX;
19701                    let overflow = style.overflow;
19702                    if (overflow_y === 'auto' || overflow_y === 'scroll' || overflow_x === 'auto' || overflow_x === 'scroll' || overflow === 'auto' || overflow === 'scroll') {
19703                        node.addEventListener('scroll', sync_panel, { passive: true });
19704                        scroll_nodes.push(node);
19705                    }
19706                    node = node.parentElement;
19707                }
19708                window.addEventListener('resize', sync_panel);
19709                window.addEventListener('scroll', sync_panel, true);
19710                let visual_viewport = window.visualViewport;
19711                if (visual_viewport) {
19712                    visual_viewport.addEventListener('resize', sync_panel);
19713                    visual_viewport.addEventListener('scroll', sync_panel);
19714                }
19715                return () => {
19716                    scroll_nodes.forEach((scroll_node) => {
19717                        scroll_node.removeEventListener('scroll', sync_panel);
19718                    });
19719                    window.removeEventListener('resize', sync_panel);
19720                    window.removeEventListener('scroll', sync_panel, true);
19721                    if (visual_viewport) {
19722                        visual_viewport.removeEventListener('resize', sync_panel);
19723                        visual_viewport.removeEventListener('scroll', sync_panel);
19724                    }
19725                };
19726            },
19727            teardown_fixed_dropdown_panel_listeners() {
19728                if (this._fixedPanelScrollCleanup) {
19729                    this._fixedPanelScrollCleanup();
19730                    this._fixedPanelScrollCleanup = null;
19731                }
19732            },
19733            selectOption(option) {
19734                if (option && option.disabled) {
19735                    return;
19736                }
19737                var id = optionId(option);
19738                if (this.multiple) {
19739                    var arr = Array.isArray(this.value) ? this.value.slice() : [];
19740                    var idx = arr.indexOf(id);
19741                    if (idx >= 0) {
19742                        arr.splice(idx, 1);
19743                    } else {
19744                        arr.push(id);
19745                    }
19746                    this.$emit('input', arr);
19747                    this.$emit('update:value', arr);
19748                    this.$emit('change', arr);
19749                } else {
19750                    this.$emit('input', option);
19751                    this.$emit('update:value', option);
19752                    this.$emit('change', option);
19753                    this.isOpen = false;
19754                    document.removeEventListener('click', this.handleClickOutside);
19755                }
19756            }
19757        },
19758        beforeDestroy() {
19759            document.removeEventListener('click', this.handleClickOutside);
19760            if (this.closeTimeout) {
19761                clearTimeout(this.closeTimeout);
19762            }
19763            this.teardown_fixed_dropdown_panel_listeners();
19764        }
19765    };
19766    app_content.component("rich-select-dropdown", richSelectDropdownConfig);
19767    app_overlays.component("rich-select-dropdown", richSelectDropdownConfig);
19768})();
19769</script>
19769
19770
19771<style>
19772.rich-select-floating-label {
19773    position: absolute;
19774    left: 10px;
19775    top: -4px;
19776    z-index: 10;
19777    font-size: 11px;
19778    padding-left: 8px;
19779    padding-right: 8px;
19780    background-color: #fff;
19781    color: #8493a0;
19782    text-transform: uppercase;
19783    letter-spacing: 1px;
19784}
19785.rich-select-floating-label .rich-select-trigger-title {
19786    font-size: 11px;
19787    color: #8493a0;
19788}
19789
19790.rich-select-dropdown {
19791    display: inline-block;
19792    min-width: 180px;
19793}
19794
19795.rich-select-trigger {
19796    border: 1px solid #e4ebf1;
19797    border-radius: 8px;
19798    padding: 10px 12px;
19799    min-height: 32px;
19800    -webkit-user-select: none;
19801    user-select: none;
19802    transition: border-color 0.15s ease, box-shadow 0.15s ease;
19803}
19804
19805.rich-select-trigger:hover {
19806    border-color: #c5cdd6;
19807}
19808
19809.rich-select-trigger.rich-select-trigger-borderless,
19810.rich-select-trigger.rich-select-trigger-borderless:hover,
19811.rich-select-trigger.rich-select-trigger-borderless.rich-select-trigger-open {
19812    border-color: transparent;
19813    box-shadow: none;
19814    padding-left: 2px;
19815    padding-right: 2px;
19816}
19817
19818.rich-select-trigger.rich-select-trigger-disabled,
19819.rich-select-trigger.rich-select-trigger-disabled:hover {
19820    background-color: #fbfcfe;
19821    border-color: #e4ebf1;
19822    cursor: default;
19823}
19824
19825.rich-select-trigger.rich-select-trigger-disabled .rich-select-chevron {
19826    opacity: 0.4;
19827}
19828
19829.rich-select-trigger-open,
19830.rich-select-trigger.rich-select-trigger-open {
19831    /* border-color: #2e59d6; */
19832    /* box-shadow: 0 0 0 1px #2e59d6; */
19833    border-color: #e4ebf1;
19834    box-shadow: none;
19835    outline: none;
19836}
19837
19838.rich-select-dropdown:focus-within .rich-select-trigger {
19839    border-color: #e4ebf1;
19840    box-shadow: none;
19841    outline: none;
19842}
19843
19844.rich-select-trigger:focus,
19845.rich-select-trigger:focus-visible,
19846.rich-select-dropdown-panel:focus,
19847.rich-select-dropdown-panel:focus-visible {
19848    border-color: #e4ebf1;
19849    box-shadow: none;
19850    outline: none;
19851}
19852
19853.rich-select-trigger-text {
19854    /* flex: 1; */
19855    min-width: 0;
19856    display: flex;
19857    flex-direction: column;
19858    gap: 2px;
19859}
19860
19861.rich-select-trigger-title {
19862    font-size: 15px;
19863    color: #081333;
19864    font-family: 'roboto', sans-serif;
19865}
19866
19867.rich-select-trigger-title-no-subtext {
19868    font-size: 18px;
19869    font-weight: 100;
19870    color: #081333;
19871    font-family: 'roboto', sans-serif;
19872}
19873
19874.rich-select-trigger-subtext {
19875    font-size: 13px;
19876    color: #83909d;
19877}
19878
19879.rich-select-placeholder {
19880    font-size: 15px;
19881    color: #83909d;
19882}
19883
19884.rich-select-option-icon-wrap {
19885    width: 32px;
19886    height: 32px;
19887    min-width: 32px;
19888    border-radius: 50%;
19889    overflow: hidden;
19890    background-color: #e8eaf1;
19891    display: flex;
19892    align-items: center;
19893    justify-content: center;
19894    margin-right: 10px;
19895}
19896
19897.rich-select-option-icon {
19898    width: 100%;
19899    height: 100%;
19900    object-fit: cover;
19901}
19902
19903.rich-select-option-icon.fa {
19904    width: auto;
19905    height: auto;
19906    font-size: 16px;
19907    color: #52596f;
19908    background: none !important;
19909}
19910.rich-select-option-icon.fa::before {
19911    background: none !important;
19912}
19913
19914.rich-select-chevron {
19915    margin-left: 8px;
19916    font-size: 12px;
19917    color: #83909d;
19918    transition: transform 0.2s ease;
19919}
19920
19921.rich-select-chevron.rich-select-chevron-open {
19922    transform: rotate(180deg);
19923}
19924
19925/* Dropdown panel: floats below trigger, above surrounding content */
19926.rich-select-dropdown-panel {
19927    position: absolute;
19928    top: 100%;
19929    left: 0;
19930    margin-top: 4px;
19931    z-index: 1001;
19932    background-color: #fff;
19933    border: 1px solid #e4ebf1;
19934    border-radius: 5px;
19935    overflow: hidden;
19936    padding: 6px 0;
19937    max-height: 280px;
19938    min-width: 100%;
19939    display: flex;
19940    flex-direction: column;
19941    transform-origin: top center;
19942    animation: rich-select-dropdown-panel-open 0.11s ease-out;
19943}
19944
19945/* v-show flips display, which restarts the animation on every open */
19946@keyframes rich-select-dropdown-panel-open {
19947    from {
19948        opacity: 0;
19949        transform: translateY(-5px) scaleY(0.96);
19950    }
19951    to {
19952        opacity: 1;
19953        transform: translateY(0) scaleY(1);
19954    }
19955}
19956
19957/* Fixed panel: escapes overflow:auto ancestors (position + size from inline style) */
19958.rich-select-dropdown-panel--fixed {
19959    position: fixed;
19960    top: auto;
19961    left: auto;
19962    margin-top: 0;
19963    min-width: 0;
19964    max-height: none;
19965}
19966
19967.rich-select-search-wrap {
19968    padding: 6px 8px;
19969    border-bottom: 1px solid #e4ebf1;
19970    flex-shrink: 0;
19971}
19972
19973.rich-select-search-input {
19974    width: 100%;
19975    border: 1px solid #e4ebf1;
19976    border-radius: 5px;
19977    padding: 8px 10px;
19978    font-size: 14px;
19979    font-family: 'roboto', sans-serif;
19980    outline: none;
19981    box-sizing: border-box;
19982}
19983
19984.rich-select-search-input:focus {
19985    /* border-color: #2e59d6; */
19986    /* box-shadow: 0 0 0 1px #2e59d6; */
19987    border-color: #e4ebf1;
19988    box-shadow: none;
19989    outline: none;
19990}
19991
19992.rich-select-dropdown-list {
19993    flex: 1 1 auto;
19994    overflow-y: auto;
19995    overflow-x: hidden;
19996}
19997
19998.rich-select-option {
19999    padding: 10px 14px;
20000    cursor: pointer;
20001    transition: background-color 0.1s ease;
20002}
20003
20004.rich-select-option:hover {
20005    background-color: #f6f8f9;
20006}
20007
20008.rich-select-option-selected {
20009    background-color: #f0f4ff;
20010}
20011
20012.rich-select-option-check {
20013    margin-right: 10px;
20014    font-size: 18px;
20015    color: #52596f;
20016}
20017
20018.rich-select-option-selected .rich-select-option-check {
20019    color: var(--custom-mp-accent, #2e59d6);
20020}
20021
20022.rich-select-option-text {
20023    flex: 1;
20024    min-width: 0;
20025    display: flex;
20026    flex-direction: column;
20027    gap: 2px;
20028}
20029
20030.rich-select-option-title {
20031    font-size: 15px;
20032    color: #081333;
20033    font-family: 'roboto', sans-serif;
20034}
20035
20036.rich-select-option-subtext {
20037    font-size: 13px;
20038    color: #83909d;
20039}
20040</style>
20041    
20041<script type="text/html" id="template-default-field-wb">
20042    <div class="default-field-wrapper tinyMarginTop">
20043        <span class="flex-row-start">
20044            <p style="color: rgba(255, 0, 0, 0.885); margin: 0 5.5px 0 3px;">*</p>
20045            <p class="default-font-body" style="font-size: 14px; color: rgb(150, 152, 154); font-style: italic;">(required)</p>
20046        </span>
20047        <!-- default fields -->
20048        <div class="flex-row-start" style="width: 100%;">
20049            <div class="flex-column-start" style="width: 100%;">
20050                <template v-if="default_configuration === 'list'" v-for="(field, index) in default_fields" :id="index">
20051                    <div class="flex-row-start smallMarginTop" style="width: 100%;">
20052                        <input
20053                            type="text"
20054                            :placeholder="field.label"
20055                            class="default-field-input border-bottom-grey-2 default-font-body"
20056                            style="width: 100%;"
20057                            v-model="field.input_value"
20058                            :disabled="is_admin"
20059                        >
20060                    </div>
20061                </template>
20062                <template v-if="default_configuration === 'grid'" v-for="(row, i) in default_fields" :id="i">
20063                    <div class="flex-row-start smallMarginTop" style="width: 100%;">
20064                        <template v-for="(field, j) in row" :id="j">
20065                            <input
20066                                type="text"
20067                                :placeholder="field.label"
20068                                class="default-field-input border-bottom-grey-2  default-font-body marginRight"
20069                                style="width: 100%;"
20070                                v-model="field.input_value"
20071                                :disabled="is_admin"
20072                            >
20073                        </template>
20074                    </div>
20075                </template>
20076            </div>
20077        </div>
20078    </div>
20079</script>
20079
20080
20081<style scoped>
20082    :root {
20083        --default-field-grey-1: rgba(180, 180, 180, 0.8);
20084        --default-field-grey-2: rgba(195, 195, 195, 0.7);
20085        --default-field-grey-3: rgb(154, 160, 166);
20086        --default-field-primary: #202124;
20087    }
20088    .default-field-wrapper{
20089        width: 100%;
20090        border: 1px solid rgba(195, 195, 195, 0.7);
20091        border-radius: 8px;
20092        padding: 1.45em;
20093        background-color: #fff;
20094        box-sizing: border-box;
20095    }
20096    .default-field-wrapper .default-font-body{
20097        font-family: Roboto, Arial, Helvetica, sans-serif;
20098        font-size: 15px; 
20099        font-weight: 400;
20100    }
20101    .default-field-wrapper .configuration-icon{
20102        font-size: 1em;
20103        cursor: pointer;
20104        padding: 5px;
20105        border-radius: 50%;
20106        color: rgba(95, 95, 95);
20107        background-color: #fff;
20108        transition: color 0.2s, background-color 0.2s;
20109    }
20110    .default-field-wrapper .configuration-icon:hover{
20111        color: var(--default-field-primary);
20112        background-color: rgba(180, 180, 180, 0.15);
20113    }
20114    .default-field-wrapper .file-upload-wrapper{
20115        height: 90px;
20116        width: 50%;
20117        border: 1px dashed var(--default-field-grey-1);
20118    }
20119    .default-field-wrapper  .inherit-font-css{
20120        font-family: inherit;
20121        font-size: inherit;
20122        font-weight: inherit;
20123    }
20124    .default-field-wrapper .flex-row-center{
20125        display: flex;
20126        flex-direction: row;
20127        align-items: center;
20128        justify-content: center;
20129    }
20130    .default-field-wrapper .flex-column-center{
20131        display: flex;
20132        flex-direction: column;
20133        align-items: center;
20134        justify-content: center;
20135    }
20136    .default-field-wrapper .flex-row-start{
20137        display: flex;
20138        flex-direction: row;
20139        align-items: center;
20140        justify-content: flex-start;
20141    }
20142    .default-field-wrapper .flex-row-end{
20143        display: flex;
20144        flex-direction: row;
20145        align-items: center;
20146        justify-content: flex-end;
20147    }
20148    .default-field-wrapper .flex-column-start{
20149        display: flex;
20150        flex-direction: column;
20151        align-items: flex-start;
20152        justify-content: center;
20153    }
20154    .default-field-wrapper .default-field{
20155        color: rgb(32, 33, 36);
20156        box-sizing: border-box;
20157        font-weight: 400;
20158        font-size: 14px;
20159        width: 100%;
20160        line-height: 2.5;
20161        letter-spacing: 0;
20162    }
20163    .default-field-wrapper .default-field > .default-field-input{
20164        font-family: inherit;
20165        font-size: inherit;
20166        font-weight: inherit;
20167        width: inherit;
20168        line-height: inherit;
20169        background-color: rgb(248, 249, 250);
20170        padding: 5px 0 5px 15px;
20171    }
20172    .default-field-wrapper .border-bottom-grey-2, .default-field-wrapper .input-is-focused{
20173        border-bottom: 1.25px solid var(--default-field-grey-2);
20174    }
20175    .default-field-wrapper input {
20176        transition: border-color 0.2s;
20177    }
20178</style>
20179
20180<script>
20181    app_content.component("default-field-component-wb", {
20182        template: "#template-default-field-wb",
20183        data() {
20184            return {
20185
20186            }
20187        },
20188        props: {
20189            form: Object, //json
20190            is_admin: Boolean,
20191            default_fields: Array,
20192            rerender_key: String,
20193        },
20194        mounted() {
20195
20196        },
20197        methods: {
20198
20199        },
20200        computed: {
20201            default_configuration(){
20202                if ( this.form && this.form.default_configuration ){
20203                    return this.form.default_configuration;
20204                }
20205                return 'list';
20206            },
20207        }
20208    })
20209</script>
20209
20210    
20210<script type="text/html" id="template-file-upload-component">
20211    <div>
20212        <p v-if="label" class="tinyPaddingBottom subtext evenSmaller">{{ label }}</p>
20213        <div 
20214            class="file-upload-container lightBorderAllAround overflowHidden" 
20215            :class="{ 'file-selected': hasFiles, 'upload-loading': loading }"
20216            @dragover.prevent="!loading && (isDragging = true)" 
20217            @dragleave.prevent="!loading && (isDragging = false)" 
20218            @drop.prevent="handle_drop"
20219        >
20220            <div 
20221                class="file-upload-inner relative flexWrap align-items-center flexGrowOne"
20222            >
20223                <input 
20224                    type="file" 
20225                    id="fileInput" 
20226                    ref="fileInput" 
20227                    class="absoluteInput" 
20228                    @change="handle_file_upload"
20229                    :multiple="multiple"
20230                    :accept="acceptAttribute"
20231                    :disabled="loading"
20232                />
20233                <div v-if="loading" class="file-upload-loading-overlay">
20234                    <div class="cardBodySpinner small-spinner centeredBlock active"></div>
20235                </div>
20236                <div 
20237                    class="upload-placeholder flexNoWrapCenter align-items-center"
20238                    :style="{ opacity: loading ? 0.55 : 1 }"
20239                >
20240                    <div>
20241                        <i :class="icon ? icon : 'fa-light fa-file-arrow-up'" class="upload-icon tinyMarginBottom"></i>
20242                        <p class="upload-text">Drag and drop here or click to upload</p>
20243                        <p class="upload-subtext">{{ fileTypeMessage }}</p>
20244                    </div>
20245                </div>
20246            </div>
20247            
20248            <div 
20249                class="selected-file-wrap flexNoWrap lightBorderTop align-items-center"
20250                v-if="hasFiles && !loading"
20251            >
20252                <template v-if="multiple">
20253                    <div class="flexColumn flexGrowOne selected-file-list">
20254                        <div 
20255                            v-for="(file, index) in selectedFiles" 
20256                            :key="file.name + index" 
20257                            class="flexNoWrap align-items-center"
20258                            :class="{ 'smallMarginBottom': index !== selectedFiles.length - 1 }"
20259                        >
20260                            <img
20261                                v-if="imagePreviewURLs[return_file_key(file, index)]"
20262                                :src="imagePreviewURLs[return_file_key(file, index)]"
20263                                class="file-preview-thumb"
20264                                alt="Image preview"
20265                            >
20266                            <p class="selected-file-name flexGrowOne">
20267                                <i class="fa-solid fa-square-check" style="padding-right:3px;"></i> {{ file.name }}
20268                            </p>
20269                            <div 
20270                                class="remove-file-wrap pointer"
20271                                :class="{ unclickable: loading }"
20272                                @click="remove_file(index)" 
20273                            >
20274                                <i class="fa-light fa-trash-can"></i>
20275                            </div>
20276                        </div>
20277                    </div>
20278                </template>
20279                <template v-else>
20280                    <div class="flexNoWrap align-items-center flexGrowOne">
20281                        <img
20282                            v-if="selectedFiles[0] && imagePreviewURLs[return_file_key(selectedFiles[0], 0)]"
20283                            :src="imagePreviewURLs[return_file_key(selectedFiles[0], 0)]"
20284                            class="file-preview-thumb"
20285                            alt="Image preview"
20286                        >
20287                        <p class="selected-file-name flexGrowOne"><i class="fa-solid fa-square-check" style="padding-right:3px;"></i> {{ selectedFiles[0].name }}</p>
20288                    </div>
20289                    <div 
20290                        class="remove-file-wrap pointer"
20291                        :class="{ unclickable: loading }"
20292                        @click="remove_file" 
20293                    >
20294                        <i 
20295                            class="fa-light fa-trash-can" 
20296                        ></i>
20297                    </div>
20298                </template>
20299            </div>
20300
20301        </div>
20302    </div>
20303    
20304</script>
20304
20305
20306<script>
20307
20308    var config = {
20309        template: "#template-file-upload-component",
20310        data() {
20311            return {
20312                selectedFiles: [],
20313                imagePreviewURLs: {},
20314                isDragging: false,
20315                //file type mappings
20316                fileTypeMappings: {
20317                    csv: [".csv", "text/csv", "application/csv", "application/vnd.ms-excel"],
20318                    pdf: [".pdf", "application/pdf"],
20319                    image: [".jpg", ".jpeg", ".png", "image/jpeg", "image/png"],
20320                    docx: [".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
20321                    video: [
20322                        ".mp4", ".webm", ".avi", ".mov", ".mkv", ".flv", ".wmv",
20323                        "video/mp4", "video/webm", "video/x-msvideo", "video/quicktime",
20324                        "video/x-matroska", "video/x-flv", "video/x-ms-wmv"
20325                    ]
20326                }
20327            }
20328        }, 
20329        props: {
20330            icon: String,
20331            label: String,
20332            valid_file_types: {
20333                type: Array,
20334                default: () => []
20335            },
20336            multiple: {
20337                type: Boolean,
20338                default: false
20339            },
20340            loading: {
20341                type: Boolean,
20342                default: false,
20343            },
20344        },
20345        mounted(){
20346            
20347        },
20348        beforeUnmount(){
20349            this.revoke_image_previews();
20350        },
20351        beforeDestroy(){
20352            this.revoke_image_previews();
20353        },
20354        methods: {
20355            open_file_picker() {
20356                if( this.loading ) {
20357                    return;
20358                }
20359                this.$refs.fileInput.click();
20360            },
20361            handle_file_upload(event) {
20362                if( this.loading ) return;
20363                const files = Array.from(event.target.files || []);
20364                this.set_files_from_input(files);
20365            },
20366            handle_drop(event) {
20367                if( this.loading ) return;
20368                event.preventDefault();
20369                const files = Array.from(event.dataTransfer.files || []);
20370                this.set_files_from_input(files);
20371                this.isDragging = false;
20372            },
20373            set_files_from_input(files) {
20374                if (!files.length) {
20375                    this.selectedFiles = [];
20376                    return;
20377                }
20378                const validFiles = files.filter(file => this.is_valid_file_type(file));
20379                if (!validFiles.length) {
20380                    alert("Invalid file type. Please upload a valid file.");
20381                    this.selectedFiles = [];
20382                    this.$refs.fileInput.value = "";
20383                    return;
20384                }
20385                if (validFiles.length !== files.length) {
20386                    alert("Some files were skipped because they are not allowed.");
20387                }
20388                if (this.multiple) {
20389                    // Append rather than replace so re-dropping or "add
20390                    // another" grows the batch. Dedupe on name/size/mtime so
20391                    // picking the same file twice doesn't create a duplicate.
20392                    const existingKeys = new Set(
20393                        this.selectedFiles.map(existingFile => `${existingFile.name}_${existingFile.size}_${existingFile.lastModified}`)
20394                    );
20395                    const filesToAdd = validFiles.filter(file => {
20396                        return !existingKeys.has(`${file.name}_${file.size}_${file.lastModified}`);
20397                    });
20398                    this.selectedFiles = this.selectedFiles.concat(filesToAdd);
20399                } else {
20400                    this.selectedFiles = [validFiles[0]];
20401                }
20402            },
20403            is_valid_file_type(file) {
20404                const fileExtension = `.${file.name.split('.').pop().toLowerCase()}`;
20405                const fileType = (file.type || "").toLowerCase();
20406                if (!this.valid_file_types.length){
20407                    // If no restrictions, allow all , but limit to standard types for security
20408                    const defaultAllowedTypes = this.default_file_types;
20409                    return defaultAllowedTypes.includes(fileExtension) || defaultAllowedTypes.includes(fileType);                    
20410                }
20411                
20412                return this.resolvedFileTypes.includes(fileExtension) || this.resolvedFileTypes.includes(fileType);
20413            },
20414            clear_files() {
20415                this.revoke_image_previews();
20416                this.selectedFiles = [];
20417                this.$refs.fileInput.value = "";
20418            },
20419            return_file_key(file, index = 0){
20420                if(!file){
20421                    return '';
20422                }
20423                return `${file.name}_${file.size}_${file.lastModified}_${index}`;
20424            },
20425            is_image_file(file){
20426                if(!file || !file.name){
20427                    return false;
20428                }
20429                const fileType = (file.type || "").toLowerCase();
20430                if(fileType.startsWith("image/")){
20431                    return true;
20432                }
20433                const fileExtension = `.${file.name.split('.').pop().toLowerCase()}`;
20434                return [".jpg", ".jpeg", ".png", ".gif", ".webp"].includes(fileExtension);
20435            },
20436            refresh_image_previews(files){
20437                this.revoke_image_previews();
20438                const nextPreviewURLs = {};
20439                (files || []).forEach((file, index) => {
20440                    if(this.is_image_file(file)){
20441                        const key = this.return_file_key(file, index);
20442                        nextPreviewURLs[key] = URL.createObjectURL(file);
20443                    }
20444                });
20445                this.imagePreviewURLs = nextPreviewURLs;
20446            },
20447            revoke_image_previews(){
20448                Object.values(this.imagePreviewURLs || {}).forEach((url) => {
20449                    try {
20450                        URL.revokeObjectURL(url);
20451                    } catch (e) {
20452                        // no-op
20453                    }
20454                });
20455                this.imagePreviewURLs = {};
20456            },
20457            remove_file(index) {
20458                if( this.loading ) return;
20459                if (this.multiple && typeof index === "number") {
20460                    this.selectedFiles.splice(index, 1);
20461                    if (!this.selectedFiles.length) {
20462                        this.$refs.fileInput.value = "";
20463                    }
20464                    this.$emit("file_selected", {
20465                        value: this.multiple ? this.selectedFiles : this.selectedFiles[0]
20466                    });
20467                } else {
20468                    this.clear_files();
20469                }
20470            }
20471        },
20472        computed: {
20473            hasFiles() {
20474                return this.selectedFiles && this.selectedFiles.length > 0;
20475            },
20476            resolvedFileTypes() {
20477                if(this.valid_file_types && this.valid_file_types.length > 0){
20478                    return this.valid_file_types.flatMap(type => this.fileTypeMappings[type] || [type]);
20479                }else{
20480                    return this.default_file_types;
20481                }
20482            },
20483            acceptAttribute() {
20484                return this.resolvedFileTypes.join(",");
20485            },
20486            fileTypeMessage() {
20487                if (!this.valid_file_types.length) {
20488                    // No restrictions
20489                    return "";
20490                }
20491
20492                // Get only extensions and remove dots for display
20493                const displayTypes = this.valid_file_types
20494                    .flatMap(type => this.fileTypeMappings[type] || [type])
20495                    .filter(type => type.startsWith(".")) // Show only file extensions
20496                    .map(type => type.replace(".", "").toUpperCase()) // Remove dot and uppercase
20497
20498                return `You may upload ${displayTypes.join(", ")} files`;
20499            }, 
20500            default_file_types(){
20501                return [
20502                    // Documents
20503                    ".pdf", "application/pdf",
20504                    ".doc", ".docx",
20505                    "application/msword",
20506                    "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
20507                    ".rtf", "application/rtf",
20508                    ".txt", "text/plain",
20509
20510                    // Spreadsheets
20511                    ".xls", ".xlsx", ".csv",
20512                    "application/vnd.ms-excel",
20513                    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
20514                    "text/csv", "application/csv", "application/vnd.ms-excel",
20515
20516                    // Presentations
20517                    ".ppt", ".pptx",
20518                    "application/vnd.ms-powerpoint",
20519                    "application/vnd.openxmlformats-officedocument.presentationml.presentation",
20520
20521                    // Images
20522                    ".jpg", ".jpeg", ".png", ".gif", ".webp",
20523                    "image/jpeg", "image/png", "image/gif", "image/webp",
20524                    // Videos
20525                    ".mp4", ".webm", ".avi", ".mov", ".mkv", ".flv", ".wmv",
20526                    "video/mp4", "video/webm",
20527                ];
20528            }
20529        },
20530        watch: {
20531            selectedFiles(newVal, oldVal){
20532                this.refresh_image_previews(newVal);
20533                this.$emit("file_selected", {
20534                    value: this.multiple ? newVal : newVal[0]
20535                });            
20536            }
20537        }
20538    }
20539
20540    //define the component
20541    app_content.component("file-upload-component", config);
20542    app_overlays.component("file-upload-component", config);
20543
20544
20545</script>
20545
20546
20547<style scoped>
20548
20549    .remove-file-wrap{
20550        padding-left: 4px;
20551        padding-right: 4px;
20552    }
20553
20554    .selected-file-wrap{
20555        padding: 12px;
20556        background-color: #fff;
20557    }
20558
20559    /* Ensure both children take full width but account for padding */
20560    .file-upload-inner, .selected-file-wrap {
20561        width: 100%;
20562        flex: 1; /* Make sure they stretch */
20563        box-sizing: border-box; /* Includes padding in width calculation */
20564        padding: 12px; /* Add your desired padding */
20565    }
20566
20567    .selected-file-name{
20568        color: #292e4a;
20569        font-size: 16px;
20570        line-height: 20px;
20571    }
20572    .file-preview-thumb{
20573        width: 42px;
20574        height: 42px;
20575        object-fit: cover;
20576        border-radius: 6px;
20577        border: 1px solid #e4ebf1;
20578        margin-right: 10px;
20579        flex-shrink: 0;
20580    }
20581
20582    .file-upload-inner{
20583        min-height: 160px;
20584    }
20585    .file-upload-container {
20586        display: flex;
20587        flex-direction: column;
20588        align-items: center;
20589        justify-content: center;
20590        width: 100%;
20591        border-radius: 8px;
20592        background-color: #f8f9fb;
20593        cursor: pointer;
20594        transition: all 0.3s ease;
20595    }
20596    .file-upload-container.upload-loading {
20597        cursor: not-allowed;
20598    }
20599    .file-upload-loading-overlay {
20600        position: absolute;
20601        top: 0;
20602        right: 0;
20603        bottom: 0;
20604        left: 0;
20605        z-index: 3;
20606        display: flex;
20607        justify-content: center;
20608        align-items: center;
20609        background: rgba(255, 255, 255, 0.6);
20610    }
20611    .file-upload-container.upload-loading .upload-text,
20612    .file-upload-container.upload-loading .upload-subtext,
20613    .file-upload-container.upload-loading .selected-file-name {
20614        color: transparent !important;
20615    }
20616    .file-upload-container.upload-loading .upload-icon {
20617        color: transparent !important;
20618    }
20619    .file-upload-container:hover:not(:has(.selected-file-wrap:hover)), 
20620    .file-upload-container.dragging:not(:has(.selected-file-wrap:hover)) {
20621        border-color: var(--custom-mp-accent, #4a56f8);
20622    }
20623    .file-upload-container:hover .upload-icon:not(:has(.selected-file-wrap:hover)), 
20624    .file-upload-container.dragging .upload-icon:not(:has(.selected-file-wrap:hover)){
20625        color: var(--custom-mp-accent, #4a56f8);
20626    }
20627
20628    .upload-placeholder {
20629        text-align: center;
20630        padding: 20px;
20631    }
20632
20633    .upload-icon{
20634        color: #788097;
20635        font-size: 28px;
20636        padding-right: 16px;
20637    }
20638    .upload-text {
20639        font-size: 16px;
20640        font-weight: 500;
20641        color: #081333;
20642    }
20643    .upload-subtext {
20644        font-size: 14px;
20645        color: #8890a3;
20646        padding-top: 4px;
20647        line-height: 18px;
20648    }
20649</style>
20650    
20650<script type="text/html" id="template-no-objects-found">
20651    
20652    <div 
20653        class="marginTopBottom"
20654        style="display: flex; flex-direction: column; justify-content: center; align-items: center;"
20655    >
20656        <i 
20657            :class="icon_class" 
20658            class="no-objects-icon"
20659        ></i>
20660        <p class="no-objects-subtext">{{ subtext }}</p>
20661        <p v-if="call_to_action" class="no-objects-subtext"><a @click="call_to_action.fn">{{ call_to_action.text }}</a></p>
20662    </div>
20663
20664</script>
20664
20665
20666<script>
20667
20668/* ----- COMPONENT ----- */
20669
20670let no_objects_config = {
20671    template: "#template-no-objects-found",
20672    data() {
20673        return {}
20674    },
20675    props: {
20676        icon_class: {
20677            type: String,
20678            required: true,
20679            default: "fa-regular fa-magnifying-glass",
20680        },
20681        subtext: {
20682            type: String,
20683            required: true,
20684            default: "No items found.",
20685        },
20686        call_to_action: {
20687            type: Object,
20688            required: false,
20689            default: null,
20690        }
20691    },
20692    mounted() {
20693        
20694    
20695    },
20696    methods: {
20697        
20698
20699    }
20700
20701}
20702app_content.component("no-objects-found", no_objects_config);
20703app_overlays.component("no-objects-found", no_objects_config);
20704if (typeof app_ai_mcp !== 'undefined') {
20705    app_ai_mcp.component("no-objects-found", no_objects_config);
20706}
20707
20708</script>
20708
20709
20710<style>
20711    .no-objects-icon{
20712        font-size: 18px;
20713        padding: 15px;
20714        background-color: rgba(230, 232, 237, 0.667);
20715        border-radius: 50%;
20716        color: #6a7482;
20717        height: 20px;
20718        width: 20px;
20719        text-align: center;
20720    }
20721    .no-objects-subtext{
20722        font-size: 16px;
20723        padding: 2px;
20724        margin-top: 5px;
20725        color: #9ca4b7;
20726        text-align: center;
20727    }
20728</style>
20729
20730
20731
20732    
20732<script type="text/html" id="template-icon-button">
20733     <button    
20734        class="card-header-button-v3 easeFast flexNoWrap align-items-center relative icon-button centeredText flexGrowOne"
20735        :class="[
20736            {'unclickable': disabled || loading}, 
20737            {'full-width': full_width}, 
20738            {'warning': warning},
20739            {'text-hides-on-mobile': hide_text_on_mobile}
20740        ]"
20741        :disabled="disabled || loading"
20742        @click="tapped_button()"
20743        style="width: auto; max-width: fit-content;"
20744        :style="height ? {'height': height + 'px', 'line-height': height + 'px', 'padding': '0 8px'} : {}"
20745        :style="no_margin_right ? {'margin-right': '0'} : {}"
20746    >
20747        <i 
20748            v-if="corrected_icon_position == 'left' && ! image_url"
20749            :class="icon_class" 
20750            :style="{ opacity: loading ? 0 : 1 }"
20751        ></i>
20752        <div
20753            v-else-if="corrected_icon_position == 'left' && image_url"
20754            class="icon-button-avatar"
20755            :style="{ backgroundImage: 'url(' + image_url + ')', opacity: loading ? 0 : 1 }"
20756        ></div>
20757        <span :style="{ opacity: loading ? 0 : 1 }" :class="{ 'hideOnMobile': hide_text_on_mobile }">{{ label }}</span>
20758        <i 
20759            v-if="corrected_icon_position == 'right' && ! image_url"
20760            :class="icon_class" 
20761            style="padding-left: 8px;"
20762            :style="{ opacity: loading ? 0 : 1 }"
20763        ></i>
20764        <div
20765            v-else-if="corrected_icon_position == 'right' && image_url"
20766            class="icon-button-avatar icon-button-avatar-right"
20767            :style="{ backgroundImage: 'url(' + image_url + ')', opacity: loading ? 0 : 1 }"
20768        ></div>
20769        <div v-if="loading" class="icon-button-spinner-wrap">
20770            <div class="card-header-button-v3-spinner easeFast"></div>
20771        </div>
20772        <a v-if="href" :href="href" :target="target" class="absoluteA"></a>
20773    </button>
20774</script>
vendor: 1 bytes, line 20774
20774
20775<script>
20776    var config = {
20777        template: "#template-icon-button",       
20778        props: {
20779            disabled: {
20780                type: Boolean,
20781                default: false,
20782            },
20783            label: {
20784                type: String,
20785                default: 'Done',
20786            },
20787            icon_class: String,
20788            image_url: {
20789                type: String,
20790                default: null,
20791            },
20792            icon_position: {
20793                type: String,
20794                default: 'left',
20795            },
20796            loading:  {
20797                type: Boolean,
20798                default: false,
20799            },
20800            href: {
20801                type: String,
20802                default: null,
20803            },
20804            target: {
20805                type: String,
20806                default: '',
20807            },
20808            full_width: {
20809                type: Boolean,
20810                default: false,
20811            },
20812            warning: {
20813                type: Boolean,
20814                default: false,
20815            },
20816            no_margin_right: {
20817                type: Boolean,
20818                default: false,
20819            },
20820            hide_text_on_mobile: {
20821                type: Boolean,
20822                default: false,
20823            }
20824        },
20825        methods: {
20826            tapped_button() {
20827                if (this.loading || this.disabled) return;
20828                this.$emit('clicked');
20829            },
20830        },
20831        computed: {
20832            corrected_icon_position(){
20833                return this.icon_position || 'left';
20834            },
20835        },
20836    }
20837    app_content.component("icon-button", config);
20838    if (typeof app_overlays !== "undefined") {
20839        app_overlays.component("icon-button", config);
20840    }
20841
20842</script>
20842
20843
20844<style>
20845    .icon-button.full-width{
20846        width: 100% !important;
20847        max-width: unset !important;
20848        justify-content: center !important;
20849        text-align: center !important;
20850        line-height: 30px;
20851        font-size: 15px;
20852    }
20853    .icon-button.taller {
20854        padding-top: 12px;
20855        padding-bottom: 12px;
20856    }
20857    .icon-button.warning {
20858        color: #9b1818 !important;
20859    }
20860    .icon-button-spinner-wrap {
20861        position: absolute;
20862        left: 0;
20863        right: 0;
20864        top: 0;
20865        bottom: 0;
20866        display: flex;
20867        justify-content: center;
20868        align-items: center;
20869    }
20870    .icon-button-avatar {
20871        width: 18px;
20872        height: 18px;
20873        border-radius: 50%;
20874        background-size: cover;
20875        background-position: center;
20876        background-repeat: no-repeat;
20877        flex-shrink: 0;
20878        margin-right: 4px;
20879    }
20880    .icon-button-avatar-right {
20881        margin-right: 0;
20882        margin-left: 4px;
20883    }
20884</style>
20885    <!-- new calendar -->
20886    <template id="template-booking-calendar">
20887    <div
20888        class="centeredOnMobile smallMarginTop"
20889        :class="{'standardCard ninetyPercent': calendar_type !== 'web_component'}"
20890    >
20891        <div
20892            class="standardCardBody noBorderTop calendar noPadding fff native-tab-bar-padding-bottom"
20893        >
20894           <div class="flexNoWrapLeftAlign hundop" style="display: flex; width: 100%;" :style="[{ height: height || '100vh' }]">
20895                <div
20896                    style="flex: 1; min-width: 0;"
20897                    :class="{ scrollable: calendar_type !== 'web_component' }"
20898                >
20899                  <div
20900                      class="cs-calendar-wrap noBorder"
20901                      :style="generateWrapStyle('cs-calendar-wrap')"
20902                  >
20903                    <!-- NAVIGATION MENU -->
20904                    <div
20905                        class="cs-calendar-menu-wrap flexNoWrap lightBorderBottom"
20906                        :style="generateWrapStyle('cs-calendar-menu-wrap')"
20907                    >
20908                        <div class="flexWrapLeftAlign flexGrowOne smallMarginRight align-items-center">
20909                            <div class="flexNoWrap">
20910                                <div class="card-header-button-v3 roboto hideOnMobile" data-option="today" @click="handleButtonClick('today')">Today</div>
20911                                <div class="card-header-button-v3 cs-calendar-button-previous roboto" data-option="previous" @click="handleButtonClick('previous')"></div>
20912                                <div class="card-header-button-v3 cs-calendar-button-next roboto" data-option="next" @click="handleButtonClick('next')"></div>
20913                            </div>
20914                            <p
20915                                v-if="calendarTitle"
20916                                class="cs-calendar-date-range-title roboto textLeft noMarginLeft hideOnMobile"
20917                            >{{ calendarTitle }}</p>
20918                            <p
20919                                v-if="calendarTitle_noYear"
20920                                class="cs-calendar-date-range-title roboto textLeft noMarginLeft showOnMobile"
20921                            >{{ calendarTitle_noYear }}</p>
20922                            <div class="flexGrowOne hideOnMobile" style="max-width: fit-content;">
20923                                <refresh-component
20924                                    @refresh-table="refresh_calendar"
20925                                    style="margin-left: 2px;"
20926                                ></refresh-component>
20927                            </div>
20928                        </div>
20929
20930                        <!-- nav butttons -->
20931                        <div class="flexWrapLeftAlign align-items-center">
20932                            <div class="flexGrowOne"></div>
20933                             <div class="flexWrap align-items-center calendar-menu-v2">
20934                                <calendar-date-picker-component
20935                                    :handle_button_click="handleButtonClick"
20936                                ></calendar-date-picker-component>
20937                            </div>
20938
20939                            <div
20940                                class="hideOnMobile card-header-button-v3 pointer noWrap centeredText relative"
20941                            >
20942                                {{ view_preference }}
20943                                <i class="fa-light fa-chevron-down" style="padding-left: 3px;"></i>
20944                                <select
20945                                    v-model="v_model_view_preference"
20946                                    @change="changeViewPreference"
20947                                    class="pointer"
20948                                    style="position:absolute;
20948left:0px;top:0px;width:100%;height:100%;opacity:0;"
20949                                >
20950                                    <option value="Month">Month</option>
20951                                    <option value="Week">Week</option>
20952                                    <option value="Day">Day</option>
20953                                </select>
20954                            </div>
20955                            <div 
20956                                v-if="!show_calendar_tags && !disable_filtering"
20957                                class="flexWrap align-items-center calendar-menu-v2 tinyMarginTop"
20958                            >
20959                                <h3 
20960                                    class="calendar-list menu-icon"
20961                                >
20962                                    <i 
20963                                        class="fa-regular fa-bars pointer"
20964                                        @click="show_calendar_tags = !show_calendar_tags"
20965                                    ></i>
20966                                </h3>
20967                            </div>
20968                        </div>                      
20969                     
20970                    </div>
20971
20972                    <div
20973                        class="relative"
20974                    >
20975                        <!-- Progress Bar -->
20976                        <div v-if="loading_parent.calendar || loading_parent.ui_refresh || loading_parent.calendar_tags || loading_parent.filter" class="progress-bar"></div>
20977                    </div>
20978
20979                    <!-- CALENDAR VIEWS -->
20980
20981                    <!-- month view -->
20982                    <template v-if="view_preference == 'Month'">
20983                        <!-- days of week header -->
20984                        <div
20985                            class="daysOfWeek-grid relative"
20986                        >
20987                            <div v-for="(day, index) in daysOfWeek" :key="index" class="grid-cell dayOfWeek noBorderTop">
20988                                {{ day }}
20989                            </div>
20990                        </div>
20991
20992                        <!-- calendar grid -->
20993                        <div
20994                            class="calendar-grid month-view"
20995                            :style="{ '--calendar-row-height': calendarRowHeight() }"
20996                        >
20997                            <template v-for="(day, index) in days_array" :key="index">
20998                                <booking-calendar-cell
20999                                    :date="day.date"
21000                                    :day="day.day"
21001                                    :date_components="day.date_components"
21002                                    :day_of_week="day.day_of_week"
21003                                    :events="day.events"
21004                                    :slots="day.slots"
21005                                    :timezone="timezone"
21006                                    :days_array="days_array"
21007                                    :is_current_month="day.date.month() === current_date.month()"
21008                                    @clicked_event="clicked_event"
21009                                    @clicked_show_more_events="clicked_show_more_events"
21010                                    @add_event="add_event"
21011                                    @zoom_in_to_date="zoom_in_to_date"
21012                                />
21013                            </template>
21014                        </div>
21015
21016                    </template>
21017
21018                    <!-- week view -->
21019                    <template v-else-if="view_preference === 'Week'" >
21020
21021                        <!-- multi/all day events section -->
21022                        <div class="flexNoWrap">
21023                            <div style="width:60px;" class="flexWrap align-items-center">
21024                                <div class="time-label">All day</div>
21025                            </div>
21026                            <div class="flexGrowOne">
21027                                <!-- days of week header -->
21028                                <div
21029                                    class="daysOfWeek-grid relative"
21030                                >
21031                                    <div v-for="(day, index) in daysOfWeek" :key="index" class="grid-cell dayOfWeek noBorderTop">
21032                                        {{ day }}
21033                                        <br>
21034                                        <span class="day-date-label">{{ current_date.clone().startOf("week").add(index, "day").date() }}</span>
21035                                    </div>
21036                                </div>
21037                                <div
21038                                    class="all-day-week-wrap"
21039                                >
21040                                    <div v-for="(day, index) in days_array" :key="index" class="grid-cell noBorderTop">
21041                                        <div class="events">
21042                                            <!-- handle all day events -->
21043                                            <!-- render content for each slot -->
21044                                            <template
21045                                                v-for="(slot, slot_index) in day.slots"
21046                                                :key="slot_index"
21047                                            >
21048                                                <!-- ok, we have an event for this slot-->
21049                                                <template v-if="slot.event">
21050                                                    <div
21051                                                        class="event"
21052                                                        :class="[{'all_day': slot.event.isAllDay}, {'partial-day': ! slot.event.isAllDay}]"
21053                                                        @click.stop="clicked_event( slot.event )"
21054                                                        :style="return_multi_day_event_style( slot.event, day.date )"
21055                                                        style="position:relative;z-index:1;"
21056                                                    >
21057                                                        <div
21058                                                            class="event-title-v2 flexNoWrap align-items-center"
21059                                                            style="font-size:12.5px;"
21060                                                        >
21061                                                            <!-- event title -->
21062                                                            {{ slot.event.title }}
21063                                                        </div>
21064                                                    </div>
21065                                                </template>
21066                                                <template v-else-if="should_hold_slot(slot, slot_index, day)">
21067                                                    <div
21068                                                        class="event"
21069                                                    >
21070                                                        <!-- add a placeholder if there's an event in this slot but it doesn't start on this day -->
21071                                                        <div
21072                                                            class="event-title-v2 flexNoWrap align-items-center easeSuperFast"
21073                                                            style="font-size:12.5px; visibility: hidden;"
21074                                                        >&nbsp;</div><!-- &nbsp; adds a hidden, non-breaking space -->
21075                                                    </div>
21076                                                </template>
21077
21078                                                <!-- no event for this slot -->
21079                                                <template v-else>
21080                                                    <div
21081                                                        class="event partial-day"
21082                                                    ></div>
21083                                                </template>
21084
21085                                            </template>
21086
21087                                        </div>
21088                                    </div>
21089                                </div>
21090                            </div>
21091                        </div>
21092
21093                        <!-- main content -->
21094                        <div
21095                            class="flexNoWrap"
21096                        >
21097                            <!-- times -->
21098                            <div
21099                                class="time-label-wrap"
21100                                style="width: 60px;"
21101                            >
21102                                <div v-for="hour in 24" :key="hour" class="time-slot">
21103                                    <div
21104                                        class="time-label"
21105                                    >{{ formatHour( hour - 1 ) }}</div>
21106                                </div>
21107                            </div>
21108
21109                            <!-- main content -->
21110                            <div class="flexGrowOne">
21111
21112                                <!-- day columns -->
21113                                <div class="flexNoWrap">
21114                                    <template
21115                                        v-for="(day, index) in days_array"
21116                                        :key="index"
21117                                    >
21118                                        <booking-calendar-day-column
21119                                            :events="day.events"
21120                                            :date_components="day.date_components"
21121                                            :timezone="timezone"
21122                                            :view_preference="view_preference"
21123                                            @clicked_event="clicked_event"
21124                                            @add_event="add_event"
21125                                        ></booking-calendar-day-column>
21126                                    </template>
21127                                </div>
21128
21129                            </div>
21130                        </div>
21131                    </template>
21132
21133                    <!-- day view -->
21134                    <template v-else-if="view_preference === 'Day'">
21135                        <div class="showOnMobile">
21136                            <icon-button
21137                                :label="'Back to month'"
21138                                icon_class="fa-regular fa-chevron-left"
21139                                @clicked="back_to_month"
21140                                :style="[{ marginLeft: '8px' }, calendar_type !== 'web_component' ? { maxHeight: '28px' } : {}]"
21141                            ></icon-button>
21142                        </div>
21143                        <div
21144                            class="flexNoWrap tinyPaddingTop"
21145                        >
21146                            <booking-calendar-day-column
21147                                :events="days_array[0].events"
21148                                :date_components="days_array[0].date_components"
21149                                :view_preference="view_preference"
21150                                :timezone="timezone"
21151                                @clicked_event="clicked_event"
21152                                @add_event="add_event"
21153                            />
21154                        </div>
21155                    </template>
21156                  </div>
21157                </div>
21158                <!-- start of new component -->
21159                <booking-calendar-filters
21160                    v-if="!disable_filtering"
21161                    :show_calendar_tags="show_calendar_tags"
21162                    :default_calendar_tags="default_calendar_tags"
21163                    :default_calendar_tags_secondary="default_calendar_tags_secondary"
21164                    :show_catch_all_custom_tags_option="show_catch_all_custom_tags_option"
21165                    :catch_all_custom_tag_selected="catch_all_custom_tag_selected"
21166                    :ui_catch_all_custom_tag_selected="ui_catch_all_custom_tag_selected"
21167                    :ui_tag_ids_default_secondary="ui_tag_ids_default_secondary"
21168                    :view_preference="view_preference"
21169                    :calendar_tags="calendar_tags"
21170                    :selected_tags="selected_tags"
21171                    :ui_tag_ids_default="ui_tag_ids_default"
21172                    :ui_tag_ids_custom="ui_tag_ids_custom"
21173                    :collapsible_menus_open="collapsible_menus_open"
21174                    :loading_parent="loading_parent"
21175                    :calendar_type="calendar_type"
21176                    :view_only="view_only"
21177                    @clicked_default_tag="clicked_default_tag"
21178                    @clicked_default_tag_secondary="clicked_default_tag_secondary"
21179                    @clicked_calendar_tag="clicked_calendar_tag"
21180                    @clicked_catch_all_custom_tag="clicked_catch_all_custom_tag"
21181                    @clicked_add_calendar_tag="clicked_add_calendar_tag"
21182                    @clicked_remove_tag="clicked_remove_tag"
21183                    @isolate_default_tag="isolate_default_tag"
21184                    @isolate_default_tag_secondary="isolate_default_tag_secondary"
21185                    @select_all_default_tags="select_all_default_tags"
21186                    @select_all_default_tags_secondary="select_all_default_tags_secondary"
21187                    @select_visible_default_tags_secondary="select_visible_default_tags_secondary"
21188                    @clear_all_default_tags="clear_all_default_tags"
21189                    @clear_all_default_tags_secondary="clear_all_default_tags_secondary"
21190                    @clear_visible_default_tags_secondary="clear_visible_default_tags_secondary"
21191                    @select_visible_default_tags="select_visible_default_tags"
21192                    @clear_visible_default_tags="clear_visible_default_tags"
21193                    @isolate_calendar_tag="isolate_calendar_tag"
21194                    @isolate_catch_all_custom="isolate_catch_all_custom"
21195                    @select_all_custom_tags="select_all_custom_tags"
21196                    @clear_all_custom_tags="clear_all_custom_tags"
21197                    @select_visible_custom_tags="select_visible_custom_tags"
21198                    @clear_visible_custom_tags="clear_visible_custom_tags"
21199                    @add_event="add_event"
21200                    @change_view_preference="changeViewPreference"
21201                    @change_show_calendar_tags="change_show_calendar_tags"
21202                    @close="show_calendar_tags = false"
21203                ></booking-calendar-filters>
21204            </div>
21205        </div>
21206    </div>
21207  </template>
21208
21209  <style scoped>
21210    .cs-calendar-menu-wrap{
21211        position: sticky;
21212        top: 0px;
21213        background-color: #fff;
21214        z-index: 3;
21215    }
21216    .calendar-list.menu-icon{
21217        min-width: 38px !important;
21218        max-width: 38px !important;
21219        width: 38px !important;
21220        min-height: 38px !important;
21221        max-height: 38px !important;
21222        height: 38px !important;
21223        padding: 0;
21224        display: flex;
21225        justify-content: center;
21226        align-items: center;   
21227        cursor: pointer;
21228    }
21229
21230    .day-date-label{
21231        font-size: 20px;
21232        color: #081333;
21233    }
21234
21235    .cs-calendar-date-range-title {
21236        font-size: 18px !important;
21237    }
21238
21239    .calendar-menu-v2 .cs-calendar-date-picker-input{
21240        border-radius: 30px;
21241        overflow: hidden !important;
21242        padding-bottom: 8px;
21243        padding-top: 8px;
21244    }
21245
21246    .cs-calendar-button-next,
21247    .cs-calendar-button-previous{
21248        min-width: 38px !important;
21249        max-width: 38px !important;
21250        width: 38px !important;
21251        min-height: 38px !important;
21252        max-height: 38px !important;
21253        height: 38px !important;
21254    }
21255
21256    .daysOfWeek-grid{
21257        grid-auto-rows: initial;
21258    }
21259
21260    .all-day-week-wrap{
21261        display: grid;
21262        grid-template-columns: repeat(7, minmax(10%, 1fr));
21263        border-bottom: 1px solid #e7ecf0;
21264    }
21265
21266    .progress-bar {
21267        position: absolute;
21268        top: 0;
21269        left: 0;
21270        height: 2px; /* Thin progress bar */
21271        background-color: #5774f5; /* Primary color */
21272        z-index: 10; /* Ensure it appears above the calendar */
21273        animation: progress-bar-animation 0.5s linear infinite; /* Animation */
21274    }
21275
21276    /* Animation Keyframes */
21277    @keyframes progress-bar-animation {
21278        0% {
21279            left: -100%; /* Start outside of the viewport */
21280            width: 30%; /* Start with a small bar */
21281        }
21282        50% {
21283            left: 25%; /* Move towards the center */
21284            width: 50%; /* Expand the bar */
21285        }
21286        100% {
21287            left: 100%; /* End outside of the viewport */
21288            width: 30%; /* Shrink the bar */
21289        }
21290    }
21291
21292    .calendar-grid.month-view{
21293        grid-auto-rows: var(--calendar-row-height, 130px);
21294    }
21295
21296    .grid-cell.dayOfWeek{
21297        border-radius: 5px;
21298    }
21299
21300    .time-content {
21301        position: relative;
21302        height: 100%; /* Full height of the hour */
21303        flex-grow: 1;
21304        border-left: 1px solid #e7ecf0;
21305    }
21306
21307    /* Day Header */
21308    .day-header {
21309        background-color: white;
21310        padding: 16px;
21311        border-bottom: 1px solid #e7ecf0;
21312    }
21313
21314    .day-title {
21315        margin: 0;
21316        font-size: 24px;
21317        font-weight: bold;
21318        color: #202124;
21319    }
21320
21321    /* All Day Section */
21322    .all-day-section {
21323        padding: 12px;
21324        border-bottom: 1px solid #e7ecf0;
21325        width: 100%;
21326    }
21327
21328    .all-day-section h3 {
21329        font-size: 16px;
21330        font-weight: bold;
21331        color: #5f6368;
21332        margin-bottom: 8px;
21333    }
21334
21335    .all-day-events {
21336        display: flex;
21337        flex-wrap: wrap;
21338        gap: 8px;
21339        padding-left: 12px;
21340        padding-right: 12px;
21341    }
21342
21343    .no-events {
21344        color: #9e9e9e;
21345        font-size: 12px;
21346    }
21347
21348    /* Time Slots */
21349    .time-slots {
21350        flex-grow: 1;
21351        overflow-y: auto;
21352        background-color: white;
21353        width:100%;
21354    }
21355
21356    .time-slot {
21357        display: flex;
21358        border-bottom: 1px solid #e7ecf0;
21359        height: 60px; /* One slot per hour */
21360    }
21361
21362    .time-label {
21363        width: 60px;
21364        text-align: center;
21365        font-size: 12px;
21366        padding-top: 8px;
21367        color: #5f6368;
21368    }
21369
21370  </style>
21371
21372  
21372<script>
21373    app_content.component("calendar-booking-component", {
21374        template: "#template-booking-calendar",
21375        data() {
21376            return {
21377                show_calendar_tags: true,
21378                v_model_view_preference: this.view_preference,
21379                daysOfWeek: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
21380                loading: {
21381                    calendar_tags: true,
21382                },
21383                catch_all_custom_tag_selected: true,
21384                ui_catch_all_custom_tag_selected: true,
21385                collapsible_menus_open: {
21386                    default_primary: true,
21387                    default_secondary: true,
21388                    custom: true,
21389                },
21390            };
21391        },
21392        mounted(){
21393            this.check_if_mobile();
21394        },
21395        props: {
21396            filter_logic: {
21397                type: String,
21398                default: "or" //and vs. or
21399            },
21400            height: String, //optional
21401            timezone: {
21402                type: String,
21403                default: moment.tz.guess(),
21404            },
21405            loading_parent: Object,  //object with several loading attributes
21406            view_preference: String,
21407            current_date: Date,
21408            events: Array,
21409            is_membership_cal: Boolean,
21410            calendar_type: String, 
21411            default_calendar_tags: Object,
21412            default_calendar_tags_secondary: Object,
21413            calendar_tags: Array,
21414            selected_tags: Object,
21415            ui_tag_ids_default: Object,
21416            ui_tag_ids_default_secondary: Object,
21417            ui_tag_ids_custom: Array,
21418            is_admin: Boolean, 
21419            view_only: {
21420                type: Boolean,
21421                default: false
21422            },
21423            disable_filtering: {
21424                type: Boolean,
21425                default: false
21426            }
21427        },
21428        methods: {
21429            back_to_month(){
21430                this.$emit("change_view_preference", "Month");
21431            },
21432            check_if_mobile(){
21433                if(this.disable_filtering){
21434                    this.show_calendar_tags = false;
21435                    return;
21436                }
21437                var is_mobile = window.innerWidth < 720;
21438                if(is_mobile || this.calendar_type === 'web_component'){
21439                    this.show_calendar_tags = false;
21440                }else{
21441                    this.show_calendar_tags = true;
21442                }
21443            },
21444            clicked_default_tag(tag) {
21445                this.$emit('clicked_default_tag', tag);
21446            },
21447            clicked_default_tag_secondary(tag) {
21448                this.$emit('clicked_default_tag_secondary', tag);
21449            },
21450            isolate_default_tag_secondary(tag) {
21451                this.$emit('isolate_default_tag_secondary', tag);
21452            },
21453            select_all_default_tags_secondary() {
21454                this.$emit('select_all_default_tags_secondary');
21455            },
21456            select_visible_default_tags_secondary(visibleIds) {
21457                this.$emit('select_visible_default_tags_secondary', visibleIds);
21458            },
21459            clear_all_default_tags_secondary() {
21460                this.$emit('clear_all_default_tags_secondary');
21461            },
21462            clear_visible_default_tags_secondary(visibleIds) {
21463                this.$emit('clear_visible_default_tags_secondary', visibleIds);
21464            },
21465            clicked_calendar_tag(tag) {
21466                this.$emit('clicked_calendar_tag', tag);
21467            }, 
21468            clicked_catch_all_custom_tag(){
21469                this.ui_catch_all_custom_tag_selected = !this.ui_catch_all_custom_tag_selected;
21470                requestAnimationFrame(() => {
21471                    // second RAF helps ensure we’re past layout/paint on Chrome/Safari
21472                    requestAnimationFrame(() => {
21473                        this.catch_all_custom_tag_selected = !this.catch_all_custom_tag_selected;
21474                    });
21475                });   
21476            },
21477            remaining_days_in_event(event, date) {
21478                const cellDate = moment.tz(date, this.timezone).startOf("day");
21479                const eventEnd = moment.tz(event.end_ts, this.timezone).startOf("day");
21480                if (cellDate.isAfter(eventEnd)) {
21481                    //already ended
21482                    return 0;
21483                }
21484                // Calculate the remaining days, including the current day
21485                return eventEnd.diff(cellDate, "days") + 1;
21486            },
21487            remaining_visible_days_in_event(event, date) {
21488                const cellDate = moment.tz(date, this.timezone).startOf("day");
21489                const eventEnd = moment.tz(event.end_ts, this.timezone).endOf("day");
21490                const eventStart = moment.tz(event.ts, this.timezone).startOf("day");
21491                const startOfWeek = this.current_date.clone().startOf("week");
21492                const endOfWeek = this.current_date.clone().endOf("week");
21493
21494                // Determine the latest starting point between cellDate and eventStart
21495                const visibleStart = moment.max(cellDate, eventStart, startOfWeek);
21496
21497                // Determine the earliest end point between eventEnd and endOfWeek
21498                const visibleEnd = moment.min(eventEnd, endOfWeek);
21499
21500                // If the visible start is after the visible end, the event isn't visible
21501                if (visibleStart.isAfter(visibleEnd)) {
21502                    return 0;
21503                }
21504
21505                // Calculate number of visible days the event should span, inclusive
21506                return visibleEnd.diff(visibleStart, "days") + 1;
21507            },
21508            return_multi_day_event_style( event, date ){
21509                if( ! this.is_multi_day_event( event ) ){
21510                    return {
21511                        "background": event.bgColors && event.bgColors.length > 0
21512                            ? `repeating-linear-gradient(
21513                                    -45deg,
21514                                    ${event.bgColors.map((color, i) => `${color} ${i * 20}px, ${color} ${(i + 1) * 20}px`).join(",")}
21515                                )`
21516                            : event.bgColor || "",
21517                    };
21518                }
21519                let day_of_week = moment.tz(date, this.timezone).day();
21520                let days_until_end_of_week = 7 - day_of_week;
21521                let number_of_days = this.remaining_visible_days_in_event(event, date);
21522                let is_first_day = this.is_first_day_of_event( event, date );
21523                if( is_first_day ){
21524                    //figure out how many days it spans
21525                    let days_to_span = number_of_days;
21526                    if( days_to_span > days_until_end_of_week ){
21527                        days_to_span = days_until_end_of_week;
21528                    }
21529                    return {
21530                        "width": `calc(${days_to_span * 100}% - 4px)`,
21531                        "zIndex": 1,
21532                        "background": event.bgColors && event.bgColors.length > 0
21533                            ? `repeating-linear-gradient(
21534                                    -45deg,
21535                                    ${event.bgColors.map((color, i) => `${color} ${i * 20}px, ${color} ${(i + 1) * 20}px`).join(",")}
21536                                )`
21537                            : event.bgColor || "",
21538                    }
21539                }else if( day_of_week == 0 ){
21540                    //not the first day of the event
21541                    //.. but see it's the first day of a new week
21542                    //... figure out which day of the event this is, and how many days are left (including this day)
21543                    let remaining_days_in_event = this.remaining_days_in_event( event, date );
21544                    let days_to_span = remaining_days_in_event;
21545                    if( days_to_span > days_until_end_of_week ){
21546                        days_to_span = days_until_end_of_week;
21547                    }
21548                    return {
21549                        "width": `calc(${days_to_span * 100}% - 4px)`,
21550                        "zIndex": 1,
21551                        "background": event.bgColors && event.bgColors.length > 0
21552                            ? `repeating-linear-gradient(
21553                                    -45deg,
21554                                    ${event.bgColors.map((color, i) => `${color} ${i * 20}px, ${color} ${(i + 1) * 20}px`).join(",")}
21555                                )`
21556                            : event.bgColor || "",
21557                    }
21558                }else{
21559                    return {
21560                        "visibility":"hidden"
21561                    }
21562                }
21563            },
21564            zoom_in_to_date( date ){
21565                this.$emit("zoom_in_to_date", date);
21566            },
21567            clicked_event( event ){
21568                this.$emit('clicked_event', event);
21569            },
21570            clicked_show_more_events( date, day_of_week, events ){
21571                this.$emit('clicked_show_more_events', date, day_of_week, events);
21572            },
21573            add_event( date_components ){
21574                this.$emit('add_event', date_components);
21575            },
21576            formatHour(hour) {
21577                const baseDate = moment().startOf('day').hour(hour);
21578                return baseDate.format("h:mm A");
21579            },
21580            changeViewPreference( event ){
21581                this.$emit("change_view_preference", event.target.value);
21582            },
21583            return_number_of_days_for_event( event ){
21584                let start = moment.tz(event.ts, this.timezone);
21585                let end = moment.tz(event.end_ts, this.timezone);
21586                //continue
21587                return end.diff(start, 'days') + 1;
21588            },
21589            is_first_day_of_event( event, date ) {
21590                // Compare the event start date with the cell's date
21591                const eventStart = moment.tz(event.ts, this.timezone).startOf("day");
21592                const cellDate = moment.tz(date, this.timezone).startOf("day");
21593                return eventStart.isSame(cellDate, "day");
21594            },
21595           sort_events_for_day(events){
21596                //multi day and all day events first, then by start time
21597                return events.sort((a, b) => {
21598                    const aAllDay = a.isAllDay;
21599                    const bAllDay = b.isAllDay;
21600
21601                    const aMulti = this.is_multi_day_event(a);
21602                    const bMulti = this.is_multi_day_event(b);
21603
21604                    // Sort all-day events first
21605                    if (aAllDay && !bAllDay) return -1;
21606                    if (!aAllDay && bAllDay) return 1;
21607
21608                    // Then multi-day events
21609                    if (aMulti && !bMulti) return -1;
21610                    if (!aMulti && bMulti) return 1;
21611
21612                    // Otherwise, by start time
21613                    return moment.tz(a.ts, this.timezone).isBefore(moment.tz(b.ts, this.timezone)) ? -1 : 1;
21614                });
21615            },
21616            is_multi_day_event( event ){
21617                return moment.tz(event.ts, this.timezone).isBefore(moment.tz(event.end_ts, this.timezone), 'day');
21618            },
21619            handleButtonClick(option, new_date) {
21620                if (option === "today") {
21621                    //vReplace with a new moment object
21622                    var new_date = moment.tz( this.timezone ).startOf("day");
21623                } else if (option === "next") {
21624                    // Add time and replace with a new object
21625                    var new_date = moment(this.current_date).add(1, this.view_preference.toLowerCase());
21626                } else if (option === "previous") {
21627                    // Subtract time and replace with a new object
21628                    var new_date = moment(this.current_date).subtract(1, this.view_preference.toLowerCase());
21629                }else if( option === 'date-picker' ){
21630                    var new_date = moment.tz( new_date, this.timezone ).startOf("day");
21631                }
21632                if( new_date ){
21633                    this.$emit("change_date", new_date);
21634                }
21635            },
21636            generateWrapStyle( type ){
21637                if (type === 'cs-calendar-wrap'){
21638                    return {'width': '100%'}
21639                } else if (type === 'options' && this.is_membership_cal){
21640                    return {
21641                        'padding-top': '15px',
21642                        'padding-right': '15px',
21643                        'padding-left': '15px',
21644                        'padding-bottom': '5px',
21645                        'border-width': '0 1.5px 0 1.5px',
21646                        'border-color': '#e2e8e8',
21647                        'border-style': 'solid',
21648                        'overflow': 'auto',
21649                    }
21650                } else if (type === 'cs-calendar-menu-wrap' && this.is_membership_cal){
21651                    return {
21652                        'border-width': '0 1.5px 0 1.5px',
21653                        'border-color': '#e2e8e8',
21654                        'border-style': 'solid'
21655                    }
21656                }
21657            },
21658            // tapped_add_to_cal(){
21659            //     var params = {};
21660            //     params["title"] = "Create calendar item";
21661            //     params["hide_on_selection"] = true;
21662            //     params["retrieve_sections_fn"] = this.return_sections_for_handle_common_grid;
21663            //     handle_common_grid( params );
21664            // }, 
21665            // return_sections_for_handle_common_grid(){
21666            //     var sections = [];
21667            //     var options = [
21668            //         {
21669            //             text: "Event",
21670            //             fn: this.add_event,
21671            //             icon_class: "fa-regular fa-calendar-plus",
21672            //         }, 
21673            //         {
21674            //             text: "Calendar tag",
21675            //             fn: this.clicked_add_calendar_tag,
21676            //             icon_class: "fa-regular fa-tags",
21677            //         }, 
21678            //     ]
21679            //     sections.push({
21680            //         options: options
21681            //     });
21682            //     return sections;
21683            // }, 
21684            clicked_add_calendar_tag(){
21685                store.components.tag_event.type_text = "this calendar";
21686                store.components.tag_event.handle_selected_event_tag = this.handle_selected_event_tag;
21687                store.components.tag_event.event_tag_input = '';
21688                store.components.tag_event.event_source = this.calendar_type;
21689                store.overlays.tag_event = true;
21690            }, 
21691            handle_selected_event_tag(tag_data, event_tags){
21692                //emit
21693                this.$emit('added_calendar_tag', tag_data, event_tags);
21694            },
21695            refresh_calendar(){
21696                this.$emit('refresh_calendar');
21697            }, 
21698            clicked_remove_tag(tag){
21699                this.$emit('remove_calendar_tag', tag);
21700            }, 
21701            isolate_calendar_tag(tag){
21702                this.ui_catch_all_custom_tag_selected = false;
21703                this.$emit('isolate_calendar_tag', tag);
21704            },
21705            isolate_default_tag(tag){
21706                this.$emit('isolate_default_tag', tag);
21707            },
21708            select_all_custom_tags(){
21709                this.ui_catch_all_custom_tag_selected = true;
21710                this.$emit('select_all_custom_tags');
21711            },
21712            select_all_default_tags(){
21713                this.$emit('select_all_default_tags');
21714            },
21715            clear_all_default_tags(){
21716                this.$emit('clear_all_default_tags');
21717            },
21718            select_visible_default_tags(visibleIds){
21719                this.$emit('select_visible_default_tags', visibleIds);
21720            },
21721            clear_visible_default_tags(visibleIds){
21722                this.$emit('clear_visible_default_tags', visibleIds);
21723            },
21724            clear_all_custom_tags(){
21725                this.$emit('clear_all_custom_tags');
21726            },
21727            select_visible_custom_tags(visibleIds){
21728                this.$emit('select_visible_custom_tags', visibleIds);
21729            },
21730            clear_visible_custom_tags(visibleIds){
21731                this.$emit('clear_visible_custom_tags', visibleIds);
21732            },
21733            isolate_catch_all_custom(){
21734                this.$emit('unselect_all_custom_tags');
21735                this.ui_catch_all_custom_tag_selected = true;
21736            },
21737            calendarRowHeight() {
21738                let maxSlots = 0;
21739                this.days_array.forEach((d) => {
21740                    var used_slots = 0;
21741                    if (Array.isArray(d.slots)) {
21742                        used_slots = d.slots.filter(slot => slot.event).length;
21743                    }
21744                    maxSlots = Math.max(maxSlots, used_slots);
21745                });
21746                const baseHeight = 100; // Base height for the calendar row
21747                var slotHeight = 10; // Height for each slot
21748                if(maxSlots >= 6){
21749                    slotHeight = 20;  //extra height for more slots
21750                }
21751                return `${baseHeight + (maxSlots * slotHeight)}px`;
21752            },
21753            should_hold_slot(slot, index, day) {
21754                var date = day.date;
21755                const usedSlots = this.return_slots_in_use_for_week(slot, index, date);
21756                return usedSlots.includes(index);
21757            },
21758            return_slots_in_use_for_week(slot, index, date) {
21759                const startOfWeek = moment.tz(date, this.timezone).startOf("week");
21760                const endOfWeek = moment.tz(date, this.timezone).endOf("week");
21761
21762                const slotsUsed = new Set();
21763
21764                this.days_array.forEach(day => {
21765                    const dayMoment = moment.tz(day.date, this.timezone).startOf("day");
21766
21767                    // Only consider days in the current week
21768                    if (dayMoment.isBetween(startOfWeek, endOfWeek, null, '[]')) {
21769                        if (Array.isArray(day.slots)) {
21770                            day.slots.forEach(slotObj => {
21771                                if (slotObj.event) {
21772                                    slotsUsed.add(slotObj.slot);
21773                                }
21774                            });
21775                        }
21776                    }
21777                });
21778
21779                return Array.from(slotsUsed);
21780            },
21781            change_show_calendar_tags(  ){
21782                this.show_calendar_tags = !this.show_calendar_tags;
21783            },
21784            // default_tag_selected(tag) {
21785            //     return this.ui_tag_ids_default && this.ui_tag_ids_default.includes(tag.id);
21786            // },
21787        },
21788        computed: {
21789            show_catch_all_custom_tags_option() {
21790                //if we have AND logic, we want to show an option that's a catch-all for all custom tags
21791                return this.filter_logic == 'and';
21792            },
21793            selected_tags_set() {
21794                let selected_tags_set = new Set( this.selected_tags.custom );
21795                return selected_tags_set;
21796            },
21797            selected_filter_set() {
21798                let selected_filter_set = new Set( this.selected_tags.default );
21799                return selected_filter_set;
21800            },
21801            selected_filter_set_secondary() {
21802                return new Set( this.selected_tags.default_secondary || [] );
21803            },
21804            dates_for_days_array(){
21805                //create a basic structure of dates that does not change so that when filters change, we can just update events and slots instead of recreating every date object
21806               //create an object with the following structure for every day in the given date range 
21807                /** 
21808                {
21809                    "date": "2025-07-27T07:00:00.000Z",
21810                    "date_components": {
21811                        "year": 2025,
21812                        "month": 6,
21813                        "day": 27
21814                    },
21815                    "day": 27,
21816                    "day_of_week": "Sunday",
21817                    "slots": [
21818                        {
21819                            "slot": 0,
21820                            "event": null
21821                        },
21822                        {
21823                            "slot": 1,
21824                            "event": null
21825                        },
21826                        {
21827                            "slot": 2,
21828                            "event": null
21829                        },
21830                        {
21831                            "slot": 3,
21832                            "event": null
21833                        },
21834                        {
21835                            "slot": 4,
21836                            "event": null
21837                        },
21838                        {
21839                            "slot": 5,
21840                            "event": null
21841                        },
21842                        {
21843                            "slot": 6,
21844                            "event": null
21845                        },
21846                        {
21847                            "slot": 7,
21848                            "event": null
21849                        },
21850                        {
21851                            "slot": 8,
21852                            "event": null
21853                        },
21854                        {
21855                            "slot": 9,
21856                            "event": null
21857                        }
21858                    ],
21859                    "events": []
21860                } **/
21861
21862                const start = this.current_date.clone().startOf("month").startOf("week");
21863                const end = this.current_date.clone().endOf("month").endOf("week");
21864                let current = start.clone();
21865                let days_array = [];
21866                while (current.isBefore(end) || current.isSame(end, "day")) {
21867                    //create the day object
21868                    let day = {
21869                        date: current.clone(),
21870                        date_components: {
21871                            year: current.year(),
21872                            month: current.month(),
21873                            day: current.date(),
21874                        },
21875                        day: current.date(),
21876                        day_of_week: current.format("dddd"),
21877                        slots: Array.from({ length: 10 }, (_, i) => ({
21878                            slot: i,
21879                            event: null, // Initially no event
21880                        })),
21881                        events: []
21882                    };
21883                    //add the day object to the array
21884                    days_array.push(day);
21885                    current.add(1, "day");
21886                }
21887                return days_array;
21888            },
21889            days_array() {
21890                //leverage days_array_dates 
21891                //make sure previous things get addressed first and start by filling up the slots with any allday/multi day events that started before the calendar start date but take place and end within the current month/week
21892                if (this.view_preference === "Month" || this.view_preference == "Week") {
21893                    //always start at the beginning/end of the month, EVEN IN WEEK VIEW
21894                    //.. so that we can generate slots effectively
21895                    //... Then, we only push the day to the array in week view if it's in the current week
21896                   
21897                    const start = this.current_date.clone().startOf("month").startOf("week");
21898                    const end = this.current_date.clone().endOf("month").endOf("week");
21899                    
21900                    
21901                    let days_array = [];
21902                    let visible_slots = 10;
21903                    if( this.view_preference == "Week" ){
21904                        visible_slots = 100;
21905                    }
21906                    let reserved_slots = {};
21907                    let highest_reserved_slot_in_week = 0;
21908                    /*
21909                        //example of reserved_slots
21910                        {
21911                            0: {
21912                                event: Object,
21913                                days_remaining: 3
21914                            },
21915                            1: {
21916                                event: Object,
21917                                days_remaining: 1
21918                            },
21919                            2: {
21920                                event: Object,
21921                                days_remaining: 4
21922                            },
21923                        }
21924                    */
21925
21926                    var dates = this.dates_for_days_array;
21927                    dates.forEach(day => {
21928                        let current = day.date.clone();
21929                        let day_is_in_week = current.isSame( this.current_date, "week" );
21930                        let events_for_day = this.visible_events_by_day[current.format("YYYY-MM-DD")] || [];
21931                        let handled_events = []; //events we have already managed to slot in
21932                        //generate slots for the day
21933                        let slots = [];
21934                        for (let i = 0; i <= visible_slots - 1; i++) {
21935                            //check for a reserved slot already
21936                            if( reserved_slots[ i ] ){
21937                                //slot is reserved
21938                                slots.push({
21939                                    slot: i,
21940                                    event: reserved_slots[ i ].event
21941                                })
21942                                //update it
21943                                reserved_slots[ i ].days_remaining -= 1;
21944                            }else{
21945                                //slot is open
21946                                //.. check for an event
21947                                //.. if there is an event, reserve the slot
21948                                let event_to_use = null;
21949                                for (let e = 0; e < events_for_day.length; e++) {
21950                                    const this_event = events_for_day[ e ];
21951                                    if( this.view_preference == "Week" && ! this_event.isAllDay ){
21952                                        //we only slot all day events for the week view
21953                                        continue;
21954                                    }
21955                                    //keep going
21956                                    if( ! handled_events.includes( this_event.id ) ){
21957                                        //haven't used it yet
21958                                        if( ! this.is_multi_day_event( this_event ) ){
21959                                            //it's a single-day event
21960                                            //.. slot it right in!
21961                                            event_to_use = this_event;
21962                                            break;
21963                                        }else{
21964                                            //check if it's the first visible day of the month calendar 
21965                                            var is_first_of_month = current.isSame(this.current_date.clone().startOf("month").startOf("week"), 'day');
21966                                            //ok, it's a multi-day event
21967                                            //.. see if it's the first day of a new event
21968                                            var remaining_days = this.remaining_days_in_event( this_event, current );
21969                                            if(is_first_of_month){
21970                                                //check for events that started before the month started but are still ongoing
21971                                                //.. and slot them 
21972                                                if( remaining_days > 0 ){
21973                                                    //it's an ongoing event that started before the month started
21974                                                    event_to_use = this_event;
21975                                                    reserved_slots[ i ] = {
21976                                                        event: this_event,
21977                                                        days_remaining: Math.max(0, this.remaining_days_in_event(this_event, current) - 1)
21978                                                    }
21979                                                    break;
21980                                                }
21981                                            }else if( this.is_first_day_of_event( this_event, current ) ){
21982                                                //it's the first day of a multi-day event
21983                                                //.. reserve the slot
21984                                                event_to_use = this_event;
21985                                                reserved_slots[ i ] = {
21986                                                    event: this_event,
21987                                                    days_remaining: Math.max(0, this.remaining_days_in_event(this_event, current) - 1)
21988                                                }
21989                                                break;
21990                                            } 
21991                                        }
21992                                    }
21993                                }
21994                                if( event_to_use ){
21995                                    slots.push({
21996                                        slot: i,
21997                                        event: event_to_use
21998                                    })
21999                                    handled_events.push( event_to_use.id );
22000                                    if( day_is_in_week && i > highest_reserved_slot_in_week ){
22001                                        highest_reserved_slot_in_week = i;
22002                                    }
22003                                }else{
22004                                    //push it, even if we don't have an event
22005                                    slots.push({
22006                                        slot: i,
22007                                        event: null
22008                                    })
22009                                }
22010                            }
22011                            if( reserved_slots[ i ] && reserved_slots[ i ].days_remaining === 0 ){
22012                                //remove the slot
22013                                delete reserved_slots[ i ];
22014                            }
22015                        }
22016                        //end slots
22017                        day.slots = slots; //visible events for the day
22018                        day.events = events_for_day; //all events for the day
22019                        if( this.view_preference == "Month" || day_is_in_week){
22020                            days_array.push(day);
22021                        }
22022                    });
22023
22024                    /**
22025                    let current = start.clone();
22026                    while (current.isBefore(end) || current.isSame(end, "day")) {
22027                        let day_is_in_week = current.isSame( this.current_date, "week" );
22028                        //find all events for this day
22029                        let events_for_day = this.visible_events_by_day[current.format("YYYY-MM-DD")] || [];
22030                        let handled_events = []; //events we have already managed to slot in
22031                        //generate slots for the day
22032                        let slots = [];
22033                        for (let i = 0; i <= visible_slots - 1; i++) {
22034                            //check for a reserved slot already
22035                            if( reserved_slots[ i ] ){
22036                                //slot is reserved
22037                                slots.push({
22038                                    slot: i,
22039                                    event: reserved_slots[ i ].event
22040                                })
22041                                //update it
22042                                reserved_slots[ i ].days_remaining -= 1;
22043                            }else{
22044                                //slot is open
22045                                //.. check for an event
22046                                //.. if there is an event, reserve the slot
22047                                let event_to_use = null;
22048                                for (let e = 0; e < events_for_day.length; e++) {
22049                                    const this_event = events_for_day[ e ];
22050                                    if( this.view_preference == "Week" && ! this_event.isAllDay ){
22051                                        //we only slot all day events for the week view
22052                                        continue;
22053                                    }
22054                                    //keep going
22055                                    if( ! handled_events.includes( this_event.id ) ){
22056                                        //haven't used it yet
22057                                        if( ! this.is_multi_day_event( this_event ) ){
22058                                            //it's a single-day event
22059                                            //.. slot it right in!
22060                                            event_to_use = this_event;
22061                                            break;
22062                                        }else{
22063                                            //check if it's the first visible day of the month calendar 
22064                                            var is_first_of_month = current.isSame(this.current_date.clone().startOf("month").startOf("week"), 'day');
22065                                            //ok, it's a multi-day event
22066                                            //.. see if it's the first day of a new event
22067                                            var remaining_days = this.remaining_days_in_event( this_event, current );
22068                                            if(is_first_of_month){
22069                                                //check for events that started before the month 
22069started but are still ongoing
22070                                                //.. and slot them 
22071                                                if( remaining_days > 0 ){
22072                                                    //it's an ongoing event that started before the month started
22073                                                    event_to_use = this_event;
22074                                                    reserved_slots[ i ] = {
22075                                                        event: this_event,
22076                                                        days_remaining: remaining_days
22077                                                    }
22078                                                    break;
22079                                                }
22080                                            }else if( this.is_first_day_of_event( this_event, current ) ){
22081                                                //it's the first day of a multi-day event
22082                                                //.. reserve the slot
22083                                                event_to_use = this_event;
22084                                                reserved_slots[ i ] = {
22085                                                    event: this_event,
22086                                                    days_remaining: this.return_number_of_days_for_event( this_event )
22087                                                }
22088                                                break;
22089                                            } 
22090                                        }
22091                                    }
22092                                }
22093                                if( event_to_use ){
22094                                    slots.push({
22095                                        slot: i,
22096                                        event: event_to_use
22097                                    })
22098                                    handled_events.push( event_to_use.id );
22099                                    if( day_is_in_week && i > highest_reserved_slot_in_week ){
22100                                        highest_reserved_slot_in_week = i;
22101                                    }
22102                                }else{
22103                                    //push it, even if we don't have an event
22104                                    slots.push({
22105                                        slot: i,
22106                                        event: null
22107                                    })
22108                                }
22109                            }
22110                            if( reserved_slots[ i ] && reserved_slots[ i ].days_remaining === 0 ){
22111                                //remove the slot
22112                                delete reserved_slots[ i ];
22113                            }
22114                        }
22115                        //end slots
22116                        //see if we should push to the days array
22117                        if( this.view_preference == "Month" || day_is_in_week){
22118                            days_array.push({
22119                                date: current.clone(),
22120                                date_components: {
22121                                    year: current.year(),
22122                                    month: current.month(),
22123                                    day: current.date(),
22124                                },
22125                                day: current.date(),
22126                                day_of_week: current.format("dddd"),
22127                                slots: slots, //visible events for the day
22128                                events: events_for_day, //all events for the day
22129                            });
22130                        }
22131                        //increment it
22132                        current.add(1, "day");
22133                    }
22134
22135                    */
22136                    //remove slots that are greater than highest actually used slot
22137                    if (this.view_preference === "Week") {
22138                        let highest_used_slot_index = 0;
22139                        days_array.forEach(day => {
22140                            day.slots.forEach(slot => {
22141                                if (slot.event && slot.slot > highest_used_slot_index) {
22142                                    highest_used_slot_index = slot.slot;
22143                                }
22144                            });
22145                        });
22146                        days_array.forEach(day => {
22147                            day.slots = day.slots.filter(slot => {
22148                                return slot.slot <= highest_used_slot_index;
22149                            });
22150                        });
22151                    }
22152                    return days_array;
22153                } else if (this.view_preference === "Day") {
22154                    // Generate only the current day
22155                    return [
22156                        {
22157                            date: this.current_date.clone(),
22158                            date_components: {
22159                                year: this.current_date.year(),
22160                                month: this.current_date.month(),
22161                                day: this.current_date.date(),
22162                            },
22163                            day: this.current_date.date(),
22164                            day_of_week: this.current_date.format("dddd"),
22165                            events: this.visible_events_by_day[this.current_date.format("YYYY-MM-DD")] || [],
22166                        },
22167                    ];
22168                }
22169                return [];
22170            },
22171            calendarTitle() {
22172                if (this.view_preference === "Month") {
22173                    return this.current_date.format("MMMM YYYY");
22174                } else if (this.view_preference === "Week") {
22175                    // Set the title to the range of the week
22176                    const start = this.current_date.clone().startOf("week");
22177                    const end = this.current_date.clone().endOf("week");
22178                    return `${start.format("MMM D")} - ${end.format("MMM D")}`;
22179                } else if (this.view_preference === "Day") {
22180                    return this.current_date.format("MMMM D, YYYY");
22181                }
22182                return null;
22183            },
22184            calendarTitle_noYear(){
22185                  if (this.view_preference === "Month") {
22186                    return this.current_date.format("MMMM");
22187                } else if (this.view_preference === "Week") {
22188                    // Set the title to the range of the week
22189                    const start = this.current_date.clone().startOf("week");
22190                    const end = this.current_date.clone().endOf("week");
22191                    return `${start.format("MMM D")} - ${end.format("MMM D")}`;
22192                } else if (this.view_preference === "Day") {
22193                    return this.current_date.format("MMMM D");
22194                }
22195                return null;
22196            },
22197            events_by_day(){
22198                //create an object with keys as days and values as arrays of events
22199                let events_by_day = {};
22200                this.events.forEach(event => {
22201                    //get the start and end dates of the event
22202                    const start = moment.tz(event.ts, this.timezone).startOf("day");
22203                    const end = moment.tz(event.end_ts, this.timezone).endOf("day");
22204                    //iterate through the days from start to end
22205                    let current = start.clone();
22206                    while (current.isSameOrBefore(end)) {
22207                        const date_key = current.format("YYYY-MM-DD");
22208                        if (!events_by_day[date_key]) {
22209                            events_by_day[date_key] = [];
22210                        }
22211                        events_by_day[date_key].push(event);
22212                        current.add(1, "day");
22213                    }
22214                });
22215                //sort array for each day
22216                for (let date in events_by_day) {
22217                    if(date && events_by_day[date] && events_by_day[date].length > 0){
22218                        //sort the events for the day
22219                        events_by_day[date] = this.sort_events_for_day(events_by_day[date]);
22220                    }
22221                }
22222                return events_by_day;
22223            },
22224            visible_events_by_day() {
22225                const visible_events_by_day = {};
22226                const defaultFilterIDs = this.selected_tags?.default || [];
22227                const selectedCalendarTagIDs = this.selected_tags?.custom || [];
22228                
22229                for (const [dayKey, events] of Object.entries(this.events_by_day)) {
22230                    visible_events_by_day[dayKey] = events.filter(event => {
22231                        if(event.type !== 'booking'){ 
22232
22233                            var hasMatchingDefault = false;
22234                            var hasMatchingDefaultSecondary = true;
22235                            // Two default groups (e.g. lodging requests: room type + room with AND)
22236                            if (this.default_calendar_tags_secondary) {
22237                                var room_type_ids = event.room_type_ids || [];
22238                                var room_ids = event.room_ids || [];
22239                                if (room_type_ids.length || room_ids.length) {
22240                                    var is_day_trip_request_filter = this.calendar_type === 'lodging_requests' && room_type_ids.indexOf('day_trip') !== -1;
22241                                    if (this.default_calendar_tags && room_type_ids.length) {
22242                                        for (var ri = 0; ri < room_type_ids.length; ri++) {
22243                                            if (this.selected_filter_set.has(room_type_ids[ri])) {
22244                                                hasMatchingDefault = true;
22245                                                break;
22246                                            }
22247                                        }
22248                                    } else if (!this.default_calendar_tags || (this.selected_filter_set && this.selected_filter_set.size === 0)) {
22249                                        hasMatchingDefault = true;
22250                                    }
22251                                    if (is_day_trip_request_filter) {
22252                                        hasMatchingDefaultSecondary = true;
22253                                    } else {
22254                                        var secSet = this.selected_filter_set_secondary;
22255                                        if (secSet && secSet.size > 0) {
22256                                            hasMatchingDefaultSecondary = false;
22257                                            if (secSet.has('no_room') && room_ids.length === 0) {
22258                                                hasMatchingDefaultSecondary = true;
22259                                            }
22260                                            for (var rj = 0; rj < room_ids.length; rj++) {
22261                                                if (secSet.has(room_ids[rj])) {
22262                                                    hasMatchingDefaultSecondary = true;
22263                                                    break;
22264                                                }
22265                                            }
22266                                        }
22267                                    }
22268                                } else {
22269                                    hasMatchingDefault = true;
22270                                    hasMatchingDefaultSecondary = true;
22271                                }
22272                            } else if (this.default_calendar_tags) {
22273                                // Lodging special-case: if grouped, match ANY of the group's room ids (have to handle case where we group mult. rooms for one member)
22274                                if (
22275                                    (this.calendar_type === 'lodging' ||  (this.calendar_type === 'reservations' && event.type === "lodging_reservation")) &&
22276                                    event.filter_ids &&
22277                                    event.filter_ids.length > 0
22278                                ) {
22279                                    const filter_ids = event.filter_ids || [];
22280                                    for (const filter of filter_ids) {
22281                                        if (this.selected_filter_set.has(filter)) {
22282                                            hasMatchingDefault = true;
22283                                            if( this.filter_logic == "or" && hasMatchingDefault ){
22284                                                return true;
22285                                            }
22286                                        }
22287                                    }
22288                                } else {
22289                                    const filter_ids = [event.filter_id];
22290                                    for (const filter of filter_ids) {
22291                                        if (this.selected_filter_set.has(filter)) {
22292                                            hasMatchingDefault = true;
22293                                            if( this.filter_logic == "or" && hasMatchingDefault ){
22294                                                return true;
22295                                            }
22296                                        }
22297                                    }
22298                                }
22299                            }
22300                            if (this.default_calendar_tags_secondary && (!hasMatchingDefault || !hasMatchingDefaultSecondary)) {
22301                                return false;
22302                            }
22303                            //see if we should exit early, based on default
22304                            if( this.filter_logic == "or" && hasMatchingDefault ){
22305                                return true;
22306                            }
22307                            //now check if one of the events tags are selected
22308                            var hasMatchingCalendar = false;
22309                            const event_tag_ids = event.event_tag_ids || [];
22310                            if( 
22311                                this.show_catch_all_custom_tags_option && 
22312                                this.catch_all_custom_tag_selected && 
22313                                event_tag_ids.length === 0 
22314                            ){
22315                                //ok, they have the default (no tags) option selected
22316                                //.. AND this event isn't tagged
22317                                //... it's a match 
22318                                hasMatchingCalendar = true;
22319                            }else{
22320                                //ok, do a full check on the tags
22321                                if( this.calendar_tags && this.calendar_tags.length >= 1 ){
22322                                    //ok, we have tags
22323                                    //.. it actually needs to match one of the tags
22324                                    if( selectedCalendarTagIDs.length > 0 && selectedCalendarTagIDs.some(tagId => event_tag_ids.includes(tagId)) ){
22325                                        hasMatchingCalendar = true;
22326                                    }
22327                                }else if( this.filter_logic == "and" ){
22328                                    //if no tags, it's effectively a match
22329                                    hasMatchingCalendar = true;
22330                                }
22331                            }
22332                            //check logic
22333                            if( this.filter_logic == "and" ){
22334                                //AND logic
22335                                return hasMatchingDefault && hasMatchingCalendar;
22336                            }else{
22337                                //OR logic
22338                                return hasMatchingDefault || hasMatchingCalendar;
22339                            }
22340                        } else if (this.calendar_tags && this.calendar_tags.length > 0) {
22341
22342                            const event_tag_ids = event.event_tag_ids || [];
22343                            for (const tag_id of event_tag_ids) {
22344                                if (this.selected_tags_set.has(tag_id)) {
22345                                    return true; // exit early
22346                                }
22347                            }
22348                            return false;
22349
22350                        }
22351                        return false;
22352                    });
22353                }
22354                return visible_events_by_day;
22355            },
22356
22357        },
22358        watch: {
22359            view_preference(newVal) {
22360                this.v_model_view_preference = newVal;
22361            }, 
22362        }
22363    });
22364  </script>
22364    
22364<script type="text/html" id="template-booking-calendar-filters">
22365    <!-- DESKTOP VERSION -->
22366    <div 
22367        v-if="show_calendar_tags" 
22368        class="hideOnMobile calendar-tag-picker lightBorderLeft scrollable native-tab-bar-padding-bottom" 
22369        style="width: 240px;"
22370        :class="{'flexible-height-version scrollable': calendar_type === 'web_component'}"
22371    >
22372        <div class="relative flexNoWrapLeftAlign" style="align-items: baseline;">
22373               
22374            <div style="position: sticky;top: 0;background-color: #fff;z-index: 10;" >
22375                <div v-if="can_edit_tags"  class="flexWrapLeftAlign align-items-center  tinyPaddingBottom" style="padding-top: 10px;padding-bottom: 6px; margin-right: 10px;">
22376                        
22377                    <div class="smallPaddingLeft">
22378                        <icon-button
22379                            label="Add event"
22380                            icon_class="fa-regular fa-plus"
22381                            @clicked="add_event()"
22382                            style="max-height: fit-content;"
22383                        ></icon-button>
22384                    </div>
22385                    <div  class="flexGrowOne"></div>
22386                    <div class="flexWrap align-items-center calendar-menu-v2 tinyMarginTop">
22387                        <h3 
22388                            class="calendar-list menu-icon pointer"
22389                            @click="change_show_calendar_tags()"
22390                        >
22391                            <i 
22392                                class="fa-regular fa-bars pointer"
22393                            ></i>
22394                        </h3>
22395                    </div>
22396                </div>
22397                <h3 
22398                    class="flexWrapLeftAlign align-items-center"
22399                    :class="{'lightBorderBottom': calendar_type !== 'web_component'}"
22400                    style="padding-top: 10px;">
22401                    Calendars
22402                    <template v-if="!can_edit_tags">
22403                        <div  class="flexGrowOne"></div>
22404                        <div class="flexWrap align-items-center calendar-menu-v2 tinyMarginTop">
22405                            <h3 
22406                                class="calendar-list menu-icon pointer"
22407                                style="margin-right: 10px;"
22408                                @click="change_show_calendar_tags()"
22409                            >
22410                                <i 
22411                                    class="fa-regular fa-bars pointer"
22412                                ></i>
22413                            </h3>
22414                        </div>
22415                    </template>
22416                </h3>
22417            </div>
22418
22419            <div class="calendar-tag-picker-input bigPaddingLeftRight scrollable paddingBottom">
22420                <div v-if="default_calendar_tags && default_calendar_tags.title" class="smallMarginTop">
22421                    <h4 class="smallBlockLetters leftText tinyMarginBottom pointer" @click="collapsible_menus_open.default_primary = !collapsible_menus_open.default_primary">
22422                        {{ default_calendar_tags.title }}
22423                        <i 
22424                            class="fa fa-chevron-down rotate-chevron"
22425                            :class="{ down: collapsible_menus_open.default_primary }"
22426                            style="margin-left: 6px;"
22427                        ></i>
22428                    </h4>
22429                    <div v-if="collapsible_menus_open.default_primary && loading_parent.filter" class="coming-soon">xxx</div>
22430                    <div v-if="collapsible_menus_open.default_primary && !loading_parent.filter" class="calendar-filter-search-wrap tinyMarginBottom">
22431                        <div 
22432                            class="calendar-filter-search-container relative" 
22433                            :class="{ 'active': filter_search_default_focused }"
22434                        >
22435                            <i class="fa-light fa-magnifying-glass calendar-filter-search-icon" style="z-index: 8;"></i>
22436                            <input 
22437                                class="calendar-filter-search-input" 
22438                                :placeholder="default_search_placeholder" 
22439                                v-model="filter_search_default" 
22440                                @focus="filter_search_default_focused = true" 
22441                                @blur="filter_search_default_focused = false"
22442                                autocomplete="off" 
22443                                autocorrect="off" 
22444                                autocapitalize="none" 
22445                                spellcheck="false"
22446                            >
22447                        </div>
22448                    </div>
22449                    <div class="flexWrapLeftAlign align-items-center">
22450                        <p 
22451                            v-if="collapsible_menus_open.default_primary && !loading_parent.filter" 
22452                            @click="select_default_tags()" 
22453                            class="smallBlockLetters tinyMarginBottom"
22454                            style="margin-right: 12px; font-size: 12px;"
22455                        >
22456                            <a>
22457                                <i class="fa-regular fa-check" style="margin-right: 4px;"></i>{{ default_select_all_label }}
22458                            </a>
22459                        </p>
22460                        <p 
22461                            v-if="collapsible_menus_open.default_primary && (has_default_search ? true : has_any_default_filters_selected)"
22462                            @click="clear_default_tags()" 
22463                            class="smallBlockLetters tinyMarginBottom"
22464                            style="margin-right: 12px; font-size: 12px;"
22465                        >
22466                            <a>
22467                                <i class="fa-regular fa-xmark" style="margin-right: 4px;"></i>{{ default_clear_label }}
22468                            </a>
22469                        </p>
22470                    </div>
22471                    <template
22472                        v-if="collapsible_menus_open.default_primary && filtered_default_options?.length > 0" 
22473                        v-for="tag in filtered_default_options" 
22474                        :key="tag.id"
22475                    >
22476                        <div 
22477                            @click="clicked_default_tag(tag)" 
22478                            class="calendar-tag-item flexWrapLeftAlign align-items-center tinyPaddingTop relative pointer" 
22479                            style="display: flex;"
22480                        >
22481                            <label class="calendar-tag-item flexWrapLeftAlign align-items-center" style="display: flex; align-items: center; flex: 1;">
22482                                <div 
22483                                    class="custom-checkbox" 
22484                                    :style="{
22485                                        backgroundColor: ui_tag_ids_default && ui_tag_ids_default[tag.id]
22486                                            ? (tag.color || '#5774f5')
22487                                            : '',
22488                                        border: '2px solid ' + (tag.color || '#5774f5')
22489                                    }"
22490                                >
22491                                    <i class="fa-regular fa-check" 
22492                                        style="color: white; position: absolute; top: 2px; left: 2px; font-size: 12px;"
22493                                    ></i>
22494                                </div>
22495                                <span class="calendar-tag-name" style="flex: 1 1 auto; word-break: break-word;">
22496                                    {{ tag.label }}
22497                                </span>
22498                            </label>
22499                            <!-- Default tag context menu trigger -->
22500                            <div 
22501                                class="calendar-tag-remove-icon" 
22502                                style="margin-left: 4px; flex-shrink: 0; padding-left: 4px; padding-right: 4px; z-index: 3;"
22503                                @click.stop="toggleTagMenu(tag, $event)"
22504                            >
22505                                <i class="fa-regular fa-ellipsis-vertical"></i>
22506                            </div>
22507                            <!-- Default tag context menu dropdown (only isolate option) -->
22508                            <div
22509                                v-if="open_menu_for_tag_id === tag.id"
22510                                class="tag-context-menu"
22511                                @click.stop
22512                            >
22513                                <button
22514                                    type="button"
22515                                    class="tag-context-menu-item"
22516                                    @click.stop="closeTagMenu(); isolate_default_tag(tag)"
22517                                >
22518                                    Display this only
22519                                </button>
22520                            </div>
22521                        </div>
22522                    </template>
22523                </div>
22524                <!-- Secondary default group (e.g. Room for lodging requests: room type + room with AND) -->
22525                <div v-if="default_calendar_tags_secondary && default_calendar_tags_secondary.title" class="smallMarginTop">
22526                    <h4 class="smallBlockLetters leftText tinyMarginBottom pointer" @click="collapsible_menus_open.default_secondary = !collapsible_menus_open.default_secondary">
22527                        {{ default_calendar_tags_secondary.title }}
22528                        <i class="fa fa-chevron-down rotate-chevron" :class="{ down: collapsible_menus_open.default_secondary }" style="margin-left: 6px;"></i>
22529                    </h4>
22530                    <div v-if="collapsible_menus_open.default_secondary" class="calendar-filter-search-wrap tinyMarginBottom">
22531                        <div class="calendar-filter-search-container relative" :class="{ 'active': filter_search_default_secondary_focused }">
22532                            <i class="fa-light fa-magnifying-glass calendar-filter-search-icon" style="z-index: 8;"></i>
22533                            <input class="calendar-filter-search-input" :placeholder="'Search ' + (default_calendar_tags_secondary.title || '').toLowerCase()" v-model="filter_search_default_secondary" @focus="filter_search_default_secondary_focused = true" @blur="filter_search_default_secondary_focused = false" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false">
22534                        </div>
22535                    </div>
22536                    <div class="flexWrapLeftAlign align-items-center">
22537                        <p v-if="collapsible_menus_open.default_secondary" @click="select_default_tags_secondary()" class="smallBlockLetters tinyMarginBottom" style="margin-right: 12px; font-size: 12px;"><a><i class="fa-regular fa-check" style="margin-right: 4px;"></i>Select all</a></p>
22538                        <p v-if="collapsible_menus_open.default_secondary && (filter_search_default_secondary || '').trim() && filtered_default_secondary_options?.length" @click="clear_visible_default_tags_secondary()" class="smallBlockLetters tinyMarginBottom" style="margin-right: 12px; font-size: 12px;"><a><i class="fa-regular fa-xmark" style="margin-right: 4px;"></i>Clear</a></p>
22539                        <p v-if="collapsible_menus_open.default_secondary && !(filter_search_default_secondary || '').trim() && has_any_default_secondary_filters_selected" @click="clear_default_tags_secondary()" class="smallBlockLetters tinyMarginBottom" style="margin-right: 12px; font-size: 12px;"><a><i class="fa-regular fa-xmark" style="margin-right: 4px;"></i>Clear</a></p>
22540                    </div>
22541                    <template v-if="collapsible_menus_open.default_secondary && filtered_default_secondary_options?.length > 0" v-for="tag in filtered_default_secondary_options" :key="tag.id">
22542                        <div @click="clicked_default_tag_secondary(tag)" class="calendar-tag-item flexWrapLeftAlign align-items-center tinyPaddingTop relative pointer" style="display: flex;">
22543                            <label class="calendar-tag-item flexWrapLeftAlign align-items-center" style="display: flex; align-items: center; flex: 1;">
22544                                <div class="custom-checkbox" :style="{ backgroundColor: ui_tag_ids_default_secondary && ui_tag_ids_default_secondary[tag.id] ? (tag.color || '#5774f5') : '', border: '2px solid ' + (tag.color || '#5774f5') }">
22545                                    <i class="fa-regular fa-check" style="color: white; position: absolute; top: 2px; left: 2px; font-size: 12px;"></i>
22546                                </div>
22547                                <span class="calendar-tag-name" style="flex: 1 1 auto; word-break: break-word;">{{ tag.label }}</span>
22548                            </label>
22549                            <div class="calendar-tag-remove-icon" style="margin-left: 4px; flex-shrink: 0; padding-left: 4px; padding-right: 4px; z-index: 3;" @click.stop="toggleTagMenu(tag, $event)">
22550                                <i class="fa-regular fa-ellipsis-vertical"></i>
22551                            </div>
22552                            <div v-if="open_menu_for_tag_id === tag.id" class="tag-context-menu" @click.stop>
22553                                <button type="button" class="tag-context-menu-item" @click.stop="closeTagMenu(); isolate_default_tag_secondary(tag)">Display this only</button>
22554                            </div>
22555                        </div>
22556                    </template>
22557                </div>
22558                <!-- Other custom calendars (event tags) -->
22559                <div v-if="show_other_filters" class="marginTop">
22560                    <h4 class="smallBlockLetters leftText tinyMarginBottom pointer noWrap" @click="collapsible_menus_open.custom = !collapsible_menus_open.custom">
22561                        {{ calendar_tag_label }}
22562                        <i 
22563                            v-if="can_edit_tags"
22564                            class="fa-regular fa-plus pointer"
22565                            style="margin-left: 6px; margin-right: 3px;"
22566                            @click.stop="clicked_add_calendar_tag()"
22567                        ></i>
22568                        <i 
22569                            class="fa fa-chevron-down rotate-chevron"
22570                            :class="{ down: collapsible_menus_open.custom }"
22571                            style="margin-left: 6px;"
22572                        ></i>
22573                    </h4>
22574                    <div v-if="collapsible_menus_open.custom && loading_parent.calendar_tags" class="coming-soon">xxx</div>
22575                    <div v-if="collapsible_menus_open.custom && !loading_parent.calendar_tags" class="calendar-filter-search-wrap tinyMarginBottom">
22576                        <div 
22577                            class="calendar-filter-search-container relative" 
22578                            :class="{ 'active': filter_search_custom_focused }"
22579                        >
22580                            <i class="fa-light fa-magnifying-glass calendar-filter-search-icon" style="z-index: 8;"></i>
22581                            <input 
22582                                class="calendar-filter-search-input" 
22583                                :placeholder="'Search tags'"
22584                                v-model="filter_search_custom" 
22585                                @focus="filter_search_custom_focused = true" 
22586                                @blur="filter_search_custom_focused = false"
22587                                autocomplete="off" 
22588                                autocorrect="off" 
22589                                autocapitalize="none" 
22590                                spellcheck="false"
22591                            >
22592                        </div>
22593                    </div>
22594                    <div v-if="collapsible_menus_open.custom && !loading_parent.calendar_tags" class="flexWrapLeftAlign align-items-center tinyMarginBottom">
22595                        <p @click="select_custom_tags()" class="smallBlockLetters tinyMarginBottom" style="margin-right: 12px; font-size: 12px;">
22596                            <a><i class="fa-regular fa-check" style="margin-right: 4px;"></i>{{ custom_select_all_label }}</a>
22597                        </p>
22598                        <p 
22599                            v-if="has_custom_search || has_any_custom_filters_selected"
22600                            @click="clear_custom_tags()" 
22601                            class="smallBlockLetters tinyMarginBottom"
22602                            style="margin-right: 12px; font-size: 12px;"
22603                        >
22604                            <a><i class="fa-regular fa-xmark" style="margin-right: 4px;"></i>{{ custom_clear_label }}</a>
22605                        </p>
22606                    </div>
22607
22608                    <!-- default (no tags) option -->
22609                    <div 
22610                        v-if="show_catch_all_custom_tags_option && collapsible_menus_open.custom && show_no_tags_in_custom" 
22611                        @click="clicked_catch_all_custom_tag()" 
22612                        class="calendar-tag-item flexWrapLeftAlign align-items-center tinyPaddingTop relative pointer" 
22613                        style="display: flex;"
22614                    >
22615                        <label class="calendar-tag-item flexWrapLeftAlign align-items-center" style="display: flex; align-items: center; flex: 1;">
22616                            <div 
22617                                class="custom-checkbox" 
22618                                :style="{
22619                                    backgroundColor: ui_catch_all_custom_tag_selected
22620                                    ? '#5774f5'
22621                                    : '',
22622                                    border: '2px solid #5774f5'
22623                                }"
22624                            >
22625                                <i class="fa-regular fa-check" 
22626                                    v-if="ui_catch_all_custom_tag_selected"
22627                                    style="color: white; position: absolute; top: 2px; left: 2px; font-size: 12px;"
22628                                ></i>
22629                            </div>
22630                            <span class="calendar-tag-name" style="flex: 1 1 auto; word-break: break-word;">
22631                                No tags
22632                            </span>
22633                        </label>
22634                         <!-- Tag context menu trigger -->
22635                        <div 
22636                            class="calendar-tag-remove-icon" 
22637                            style="margin-left: 4px; flex-shrink: 0; padding-left: 4px; padding-right: 4px; z-index: 3;"
22638                            @click.stop="toggleTagMenu({id: 'catch_all_custom'}, $event)"
22639                        >
22640                            <i class="fa-regular fa-ellipsis-vertical"></i>
22641                        </div>
22642                        <!-- Tag context menu dropdown -->
22643                        <div
22644                            v-if="open_menu_for_tag_id === 'catch_all_custom'"
22645                            class="tag-context-menu"
22646                            @click.stop
22647                        >
22648                            <button
22649                                type="button"
22650                                class="tag-context-menu-item"
22651                                @click.stop="closeTagMenu(); isolate_catch_all_custom()"
22652                            >
22653                                Display this only
22654                            </button>
22655                        </div>
22656                    </div>
22657                    <!-- end default (no tags) option -->
22658
22659                    <template 
22660                        v-if="collapsible_menus_open.custom && !loading_parent.calendar_tags && filtered_custom_tags && filtered_custom_tags.length > 0" 
22661                        v-for="tag in filtered_custom_tags" 
22662                        :key="tag.id"
22663                    >
22664                        <div 
22665                            v-if="tag" 
22666                            @click="clicked_calendar_tag(tag)" 
22667                            class="calendar-tag-item flexWrapLeftAlign align-items-center tinyPaddingTop relative pointer" 
22668                            style="display: flex;"
22669                        >
22670                            <label class="calendar-tag-item flexWrapLeftAlign align-items-center" style="display: flex; align-items: center; flex: 1;">
22671                                <div 
22672                                    class="custom-checkbox" 
22673                                    :style="{
22674                                        backgroundColor:  ui_tag_ids_custom && ui_tag_ids_custom[tag.id]
22675                                            ? (tag.get('hex_value') || '#5774f5')
22676                                            : '',
22677                                        border: '2px solid ' + (tag.get('hex_value') || '#5774f5')
22678                                    }"
22679                                >
22680                                    <i class="fa-regular fa-check" 
22681                                        v-if="ui_tag_ids_custom && ui_tag_ids_custom[tag.id]"
22682                                        style="color: white; position: absolute; top: 2px; left: 2px; font-size: 12px;"
22683                                    ></i>
22684                                </div>
22685                                <span class="calendar-tag-name" style="flex: 1 1 auto; word-break: break-word;">
22686                                    {{ tag.get('name') }}
22687                                </span>
22688                            </label>
22689                            <!-- Tag context menu trigger -->
22690                            <div 
22691                                class="calendar-tag-remove-icon" 
22692                                style="margin-left: 4px; flex-shrink: 0; padding-left: 4px; padding-right: 4px; z-index: 3;"
22693                                @click.stop="toggleTagMenu(tag, $event)"
22694                            >
22695                                <i class="fa-regular fa-ellipsis-vertical"></i>
22696                            </div>
22697                            <!-- Tag context menu dropdown -->
22698                            <div
22699                                v-if="open_menu_for_tag_id === tag.id"
22700                                class="tag-context-menu"
22701                                :class="{'tag-context-menu--above': isLastTag(tag)}"
22702                                @click.stop
22703                            >
22704                                <button
22705                                    type="button"
22706                                    class="tag-context-menu-item"
22707                                    @click.stop="closeTagMenu(); isolate_calendar_tag(tag)"
22708                                >
22709                                    Display this only
22710                                </button>
22711                                <button
22712                                    v-if="can_edit_tags"
22713                                    type="button"
22714                                    class="tag-context-menu-item tag-context-menu-item-danger"
22715                                    @click.stop="closeTagMenu(); clicked_remove_tag(tag)"
22716                                >
22717                                    Remove tag
22718                                </button>
22719                            </div>
22720                        </div>
22721                    </template>
22722                </div>                        
22723            </div>
22724        </div>
22725    </div>
22726    <!-- MOBILE VERSION -->
22727     <!-- Teleport the modal to body -->
22728     <teleport to="#v-overlays">
22729        <div v-if="show_calendar_tags" class="showOnMobile">
22730                <transition name="slide-in-right">
22731                    <aside class="mobile-filter-overlay scrollable" role="dialog" aria-modal="true" aria-labelledby="calendar-filters-title">
22732                        <div class="calendar-tag-picker marginBottom">
22733                            <div class="relative flexNoWrapLeftAlign" style="align-items: baseline;">
22734                                <div 
22735                                    style="position: sticky;top: 0;background-color: #fff;z-index: 10;"
22736                                    class="lightBorderBottom smallPaddingTop smallPaddingBottom bigPaddingLeftRight"
22737                                >
22738                                    <div 
22739                                        v-if="can_edit_tags"
22740                                        class="tinyPaddingBottom"
22741                                    >
22742                                        <icon-button
22743                                            label="Add event"
22744                                            icon_class="fa-regular fa-plus"
22745                                            @clicked="add_event()"
22746                                            style="max-height: fit-content;"
22747                                        ></icon-button>
22748                                    </div>
22749                                    <div
22750                                        class="card-header-button-v3 pointer noWrap centeredText relative"
22751                                    >
22752                                        {{ v_model_view_preference }}
22753                                        <i class="fa-light fa-chevron-down" style="padding-left: 3px;"></i>
22754                                        <select
22755                                            v-model="v_model_view_preference"
22756                                            @change="changeViewPreference($event)"
22757                                            class="pointer"
22758                                            style="position:absolute;left:0px;top:0px;width:100%;height:100%;opacity:0;"
22759                                        >
22760                                            <option value="Month">Month</option>
22761                                            <option value="Week">Week</option>
22762                                            <option value="Day">Day</option>
22763                                        </select>
22764                                    </div>
22765                                    <h3 id="calendar-filters-title" class="flexWrapLeftAlign align-items-center smallMarginTop" style="padding-top: 0;">
22766                                        Calendars
22767                                        <div class="flexGrowOne"></div>
22768                                        <!-- <i class="fa-regular fa-xmark pointer" @click="$emit('close')" style="margin-right: 10px;"></i> -->
22769                                    </h3>
22770                                </div>
22771                                <div class="calendar-tag-picker-input bigPaddingLeftRight scrollable paddingBottom">
22772                                    <div v-if="default_calendar_tags && default_calendar_tags.title" class="smallMarginTop">
22773                                        <h4 class="smallBlockLetters leftText tinyMarginBottom pointer" @click="collapsible_menus_open.default_primary = !collapsible_menus_open.default_primary">
22774                                        {{ default_calendar_tags.title }}
22775                                            <i 
22776                                                class="fa fa-chevron-down rotate-chevron"
22777                                                :class="{ down: collapsible_menus_open.default_primary }"
22778                                                style="margin-left: 6px;"
22779                                            ></i>
22780                                        </h4>
22781                                        <div v-if="collapsible_menus_open.default_primary && loading_parent.filter" class="coming-soon">xxx</div>
22782                                        <div v-if="collapsible_menus_open.default_primary && !loading_parent.filter" class="calendar-filter-search-wrap tinyMarginBottom">
22783                                            <div 
22784                                                class="calendar-filter-search-container relative" 
22785                                                :class="{ 'active': filter_search_default_focused }"
22786                                            >
22787                                                <i class="fa-light fa-magnifying-glass calendar-filter-search-icon" style="z-index: 10;"></i>
22788                                                <input 
22789                                                    class="calendar-filter-search-input" 
22790                                                    :placeholder="default_search_placeholder" 
22791                                                    v-model="filter_search_default" 
22792                                                    @focus="filter_search_default_focused = true" 
22793                                                    @blur="filter_search_default_focused = false"
22794                                                    autocomplete="off" 
22795                                                    autocorrect="off" 
22796                                                    autocapitalize="none" 
22797                                                    spellcheck="false"
22798                                                >
22799                                            </div>
22800                                        </div>
22801                                        <div class="flexWrapLeftAlign align-items-center">
22802                                            <p 
22803                                                v-if="collapsible_menus_open.default_primary" 
22804                                                @click="select_default_tags()" 
22805                                                class="smallBlockLetters tinyMarginBottom"
22806                                                style="margin-right: 12px; font-size: 12px;"
22807                                            >
22808                                                <a>
22809                                                    <i class="fa-regular fa-check" style="margin-right: 4px;"></i>{{ default_select_all_label }}
22810                                                </a>
22811                                            </p>
22812                                            <p 
22813                                                v-if="collapsible_menus_open.default_primary && (has_default_search ? true : has_any_default_filters_selected)" 
22814                                                @click="clear_default_tags()" 
22815                                                class="smallBlockLetters tinyMarginBottom"
22816                                                style="margin-right: 12px; font-size: 12px;"
22817                                            >
22818                                                <a>
22819                                                    <i class="fa-regular fa-xmark" style="margin-right: 4px;"></i>{{ default_clear_label }}
22820                                                </a>
22821                                            </p>
22822                                        </div>
22823                                        <template 
22824                                            v-if="collapsible_menus_open.default_primary && filtered_default_options?.length > 0" 
22825                                            v-for="tag in filtered_default_options" 
22826                                            :key="tag.id"
22827                                        >
22828                                            <div 
22829                                                @click="clicked_default_tag(tag)" 
22830                                                class="calendar-tag-item flexWrapLeftAlign align-items-center relative pointer" 
22831                                                style="display: flex; padding-top: 16px;"
22832                                            >
22833                                                <label class="calendar-tag-item flexWrapLeftAlign align-items-center" style="display: flex; align-items: center; flex: 1;">
22834                                                    <div 
22835                                                        class="custom-checkbox" 
22836                                                        :style="{
22837                                                            backgroundColor: ui_tag_ids_default && ui_tag_ids_default[tag.id]
22838                                                                ? (tag.color || '#5774f5')
22839                                                                : '',
22840                                                            border: '2px solid ' + (tag.color || '#5774f5')
22841                                                        }"
22842                                                    >
22843                                                        <i class="fa-regular fa-check" 
22844                                                            style="color: white; position: absolute; top: 2px; left: 2px; font-size: 12px;"
22845                                                        ></i>
22846                                                    </div>
22847                                                    <span class="calendar-tag-name" style="flex: 1 1 auto; word-break: break-word;">
22848                                                        {{ tag.label }}
22849                                                    </span>
22850                                                </label>
22851                                                <!-- Default tag context menu trigger (mobile) -->
22852                                                <div 
22853                                                    class="calendar-tag-more mobile-version" 
22854                                                    style="margin-left: 4px; flex-shrink: 0; padding-left: 4px; padding-right: 4px; z-index: 3;"
22855                                                    @click.stop="toggleTagMenu(tag, $event)"
22856                                                >
22857                                                    <i class="fa-regular fa-ellipsis-vertical"></i>
22858                                                </div>
22859                                                <!-- Default tag context menu dropdown (only isolate option) -->
22860                                                <div
22861                                                    v-if="open_menu_for_tag_id === tag.id"
22862                                                    class="tag-context-menu"
22863                                                    @click.stop
22864                                                >
22865                                                    <button
22866                                                        type="button"
22867                                                        class="tag-context-menu-item"
22868                                                        @click.stop="closeTagMenu(); isolate_default_tag(tag)"
22869                                                    >
22870                                                        Display this only
22871                                                    </button>
22872                                                </div>
22873                                            </div>
22874                                        </template>
22875                                    </div>
22876                                    <!-- Secondary default group (mobile) -->
22877                                    <div v-if="default_calendar_tags_secondary && default_calendar_tags_secondary.title" class="marginTop">
22878                                        <h4 class="smallBlockLetters leftText tinyMarginBottom pointer" @click="collapsible_menus_open.default_secondary = !collapsible_menus_open.default_secondary">
22879                                            {{ default_calendar_tags_secondary.title }}
22880                                            <i class="fa fa-chevron-down rotate-chevron" :class="{ down: collapsible_menus_open.default_secondary }" style="margin-left: 6px;"></i>
22881                                        </h4>
22882                                        <div v-if="collapsible_menus_open.default_secondary" class="flexWrapLeftAlign align-items-center tinyMarginBottom">
22883                                            <p @click="select_default_tags_secondary()" class="smallBlockLetters tinyMarginBottom" style="margin-right: 12px; font-size: 12px;"><a><i class="fa-regular fa-check" style="margin-right: 4px;"></i>Select all</a></p>
22884                                            <p v-if="has_any_default_secondary_filters_selected" @click="clear_default_tags_secondary()" class="smallBlockLetters tinyMarginBottom" style="margin-right: 12px; font-size: 12px;"><a><i class="fa-regular fa-xmark" style="margin-right: 4px;"></i>Clear</a></p>
22885                                        </div>
22886                                        <template v-if="collapsible_menus_open.default_secondary && filtered_default_secondary_options?.length > 0" v-for="tag in filtered_default_secondary_options" :key="tag.id">
22887                                            <div @click="clicked_default_tag_secondary(tag)" class="calendar-tag-item flexWrapLeftAlign align-items-center relative pointer" style="display: flex; padding-top: 16px;">
22888                                                <label class="calendar-tag-item flexWrapLeftAlign align-items-center" style="display: flex; align-items: center; flex: 1;">
22889                                                    <div class="custom-checkbox" :style="{ backgroundColor: ui_tag_ids_default_secondary && ui_tag_ids_default_secondary[tag.id] ? (tag.color || '#5774f5') : '', border: '2px solid ' + (tag.color || '#5774f5') }">
22890                                                        <i class="fa-regular fa-check" style="color: white; position: absolute; top: 2px; left: 2px; font-size: 12px;"></i>
22891                                                    </div>
22892                                                    <span class="calendar-tag-name" style="flex: 1 1 auto; word-break: break-word;">{{ tag.label }}</span>
22893                                                </label>
22894                                                <div class="calendar-tag-more mobile-version" style="margin-left: 4px; flex-shrink: 0; padding-left: 4px; padding-right: 4px; z-index: 3;" @click.stop="toggleTagMenu(tag, $event)">
22895                                                    <i class="fa-regular fa-ellipsis-vertical"></i>
22896                                                </div>
22897                                                <div v-if="open_menu_for_tag_id === tag.id" class="tag-context-menu" @click.stop>
22898                                                    <button type="button" class="tag-context-menu-item" @click.stop="closeTagMenu(); isolate_default_tag_secondary(tag)">Display this only</button>
22899                                                </div>
22900                                            </div>
22901                                        </template>
22902                                    </div>
22903                                    <!-- Other custom calendars (event tags) -->
22904                                    <div v-if="has_other_calendar_filters" class="marginTop">
22905                                        <h4 class="smallBlockLetters leftText tinyMarginBottom pointer noWrap" @click="collapsible_menus_open.custom = !collapsible_menus_open.custom">
22906                                        {{ calendar_tag_label }}
22907                                        <i 
22908                                            v-if="can_edit_tags"
22909                                            class="fa-regular fa-plus pointer"
22910                                            style="margin-left: 6px; margin-right: 3px;"
22911                                            @click.stop="clicked_add_calendar_tag()"
22912                                        ></i>
22913                                        <i 
22914                                            class="fa fa-chevron-down rotate-chevron"
22915                                            :class="{ down: collapsible_menus_open.custom }"
22916                                            style="margin-left: 6px;"
22917                                        ></i>
22918                                        </h4>
22919                                        <div v-if="collapsible_menus_open.custom && loading_parent.calendar_tags" class="coming-soon">xxx</div>
22920                                        <div v-if="collapsible_menus_open.custom && !loading_parent.calendar_tags" class="calendar-filter-search-wrap tinyMarginBottom">
22921                                            <div 
22922                                                class="calendar-filter-search-container relative" 
22923                                                :class="{ 'active': filter_search_custom_focused }"
22924                                            >
22925                                                <i class="fa-light fa-magnifying-glass calendar-filter-search-icon" style="z-index: 10;"></i>
22926                                                <input 
22927                                                    class="calendar-filter-search-input" 
22928                                                    :placeholder="'Search tags'"
22929                                                    v-model="filter_search_custom" 
22930                                                    @focus="filter_search_custom_focused = true" 
22931                                                    @blur="filter_search_custom_focused = false"
22932                                                    autocomplete="off" 
22933                                                    autocorrect="off" 
22934                                                    autocapitalize="none" 
22935                                                    spellcheck="false"
22936                                                >
22937                                            </div>
22938                                        </div>
22939                                        <div v-if="collapsible_menus_open.custom && !loading_parent.calendar_tags" class="flexWrapLeftAlign align-items-center tinyMarginBottom">
22940                                            <p @click="select_custom_tags()" class="smallBlockLetters tinyMarginBottom" style="margin-right: 12px; font-size: 12px;">
22941                                                <a><i class="fa-regular fa-check" style="margin-right: 4px;"></i>{{ custom_select_all_label }}</a>
22942                                            </p>
22943                                            <p 
22944                                                v-if="has_custom_search || has_any_custom_filters_selected"
22945                                                @click="clear_custom_tags()" 
22946                                                class="smallBlockLetters tinyMarginBottom"
22947                                                style="margin-right: 12px; font-size: 12px;"
22948                                            >
22949                                                <a><i class="fa-regular fa-xmark" style="margin-right: 4px;"></i>{{ custom_clear_label }}</a>
22950                                            </p>
22951                                        </div>
22952                                        
22953                                        <!-- default (no tags) option -->
22954                                        <div 
22955                                            v-if="show_catch_all_custom_tags_option && collapsible_menus_open.custom && show_no_tags_in_custom" 
22956                                            @click="clicked_catch_all_custom_tag()" 
22957                                            class="calendar-tag-item flexWrapLeftAlign align-items-center tinyPaddingTop relative pointer" 
22958                                            style="display: flex;"
22959                                        >
22960                                            <label class="calendar-tag-item flexWrapLeftAlign align-items-center" style="display: flex; align-items: center; flex: 1;">
22961                                                <div 
22962                                                    class="custom-checkbox" 
22963                                                    :style="{
22964                                                        backgroundColor: ui_catch_all_custom_tag_selected
22965                                                        ? '#5774f5'
22966                                                        : '',
22967                                                        border: '2px solid #5774f5'
22968                                                    }"
22969                                                >
22970                                                    <i class="fa-regular fa-check" 
22971                                                        v-if="ui_catch_all_custom_tag_selected"
22972                                                        style="color: white; position: absolute; top: 2px; left: 2px; font-size: 12px;"
22973                                                    ></i>
22974                                                </div>
22975                                                <span class="calendar-tag-name" style="flex: 1 1 auto; word-break: break-word;">
22976                                                    No tags
22977                                                </span>
22978                                            </label>
22979                                            <!-- Tag context menu trigger -->
22980                                            <div 
22981                                                class="calendar-tag-catch-all mobile-version" 
22982                                                style="margin-left: 4px; flex-shrink: 0; padding-left: 4px; padding-right: 4px; z-index: 3;"
22983                                                @click.stop="toggleTagMenu({id: 'catch_all_custom'}, $event)"
22984                                            >
22985                                                <i class="fa-regular fa-ellipsis-vertical"></i>
22986                                            </div>
22987                                            <!-- Tag context menu dropdown -->
22988                                            <div
22989                                                v-if="open_menu_for_tag_id === 'catch_all_custom'"
22990                                                class="tag-context-menu"
22991                                                @click.stop
22992                                            >
22993                                                <button
22994                                                    type="button"
22995                                                    class="tag-context-menu-item"
22996                                                    @click.stop="closeTagMenu(); isolate_catch_all_custom()"
22997                                                >
22998                                                    Display this only
22999                                                </button>
23000                                            </div>
23001                                        </div>
23002                                        <!-- end default (no tags) option -->
23003                                        
23004                                        <template 
23005                                            v-if="collapsible_menus_open.custom && !loading_parent.calendar_tags && filtered_custom_tags && filtered_custom_tags.length > 0" 
23006                                            v-for="tag in filtered_custom_tags" 
23007                                            :key="tag.id"
23008                                            >
23009                                            <div v-if="tag" @click="clicked_calendar_tag(tag)" class="calendar-tag-item flexWrapLeftAlign align-items-center relative pointer" style="display: flex; padding-top: 16px">
23010                                                <label class="calendar-tag-item flexWrapLeftAlign align-items-center" style="display: flex; align-items: center; flex: 1;">
23011                                                    <div 
23012                                                        class="custom-checkbox" 
23013                                                        :style="{
23014                                                        backgroundColor:  ui_tag_ids_custom && ui_tag_ids_custom[tag.id]
23015                                                            ? (tag.get('hex_value') || '#5774f5')
23016                                                            : '',
23017                                                        border: '2px solid ' + (tag.get('hex_value') || '#5774f5')
23018                                                        }"
23019                                                    >
23020                                                        <i class="fa-regular fa-check" 
23021                                                            v-if="ui_tag_ids_custom && ui_tag_ids_custom[tag.id]"
23022                                                            style="color: white; position: absolute; top: 2px; left: 2px; font-size: 12px;"
23023                                                        ></i>
23024                                                    </div>
23025                                                    <span class="calendar-tag-name" style="flex: 1 1 auto; word-break: break-word;">
23026                                                        {{ tag.get('name') }}
23027                                                    </span>
23028                                                </label>
23029                                                <!-- Tag context menu trigger (mobile)-->
23030                                                <div 
23031                                                    class="calendar-tag-more mobile-version" 
23032                                                    style="margin-left: 4px; flex-shrink: 0; padding-left: 4px; padding-right: 4px; z-index: 3;"
23033                                                    @click.stop="toggleTagMenu(tag, $event)"
23034                                                >
23035                                                    <i class="fa-regular fa-ellipsis-vertical"></i>
23036                                                </div>
23037                                                <!-- Tag context menu dropdown -->
23038                                                <div
23039                                                    v-if="open_menu_for_tag_id === tag.id"
23040                                                    class="tag-context-menu"
23041                                                    :class="{'tag-context-menu--above': isLastTag(tag)}"
23042                                                    @click.stop
23043                                                >
23044                                                    <button
23045                                                        type="button"
23046                                                        class="tag-context-menu-item"
23047                                                        @click.stop="closeTagMenu(); isolate_calendar_tag(tag)"
23048                                                    >
23049                                                        Display this only
23050                                                    </button>
23051                                                    <button
23052                                                        v-if="can_edit_tags"
23053                                                        type="button"
23054                                                        class="tag-context-menu-item tag-context-menu-item-danger"
23055                                                        @click.stop="closeTagMenu(); clicked_remove_tag(tag)"
23056                                                    >
23057                                                        Remove tag
23058                                                    </button>
23059                                                </div>
23060                                            </div>
23061                                        </template>
23062
23063                                    </div>                        
23064                                </div>
23065                            </div>
23066                        </div>
23067                    </aside>
23068                </transition>
23069
23070                <div v-if="show_calendar_tags" class="overlay-backdrop showOnMobile" @click="$emit('close')"></div>
23071        </div>
23072    </teleport>
23073</script>
23073
23074
23075<script>
23076    app_content.component("booking-calendar-filters", {
23077        template: "#template-booking-calendar-filters",
23078        data(){
23079            return {
23080                collapsible_menus_open: {
23081                    default_primary: true,
23082                    default_secondary: true,
23083                    custom: true,
23084                },
23085                v_model_view_preference: this.view_preference || 'Month',
23086                open_menu_for_tag_id: null,
23087                filter_search_default: '',
23088                filter_search_custom: '',
23089                filter_search_default_secondary: '',
23090                filter_search_default_focused: false,
23091                filter_search_custom_focused: false,
23092                filter_search_default_secondary_focused: false,
23093            }
23094        }, 
23095        props: {
23096            show_calendar_tags: Boolean,
23097            default_calendar_tags: Object,
23098            calendar_tags: Array,
23099            selected_tags: Object,
23100            catch_all_custom_tag_selected: Boolean,
23101            ui_tag_ids_default: Object,
23102            ui_tag_ids_custom: Array,
23103            ui_catch_all_custom_tag_selected: Boolean,
23104            show_catch_all_custom_tags_option: {
23105                type: Boolean,
23106                default: false,
23107            },
23108            default_calendar_tags_secondary: Object,
23109            ui_tag_ids_default_secondary: Object,
23110            collapsible_menus_open: Object, 
23111            loading_parent: Object,
23112            calendar_type: String, 
23113            view_only: Boolean,
23114            view_preference: String,
23115        }, 
23116        mounted(){
23117            this.v_model_view_preference = this.view_preference || 'Month';
23118            document.addEventListener('click', this.handleClickOutside);
23119        },
23120        beforeDestroy(){
23121            document.removeEventListener('click', this.handleClickOutside);
23122        },
23123        methods: {
23124            add_event(){
23125                this.$emit('add_event');
23126            }, 
23127            isLastTag(tag){
23128                if (!this.calendar_tags || this.calendar_tags.length === 0 || !tag) {
23129                    return false;
23130                }
23131                const len = this.calendar_tags.length;
23132                const last = this.calendar_tags[len - 1];
23133                const secondLast = this.calendar_tags[len - 2];
23134                return (last && last.id === tag.id) || (secondLast && secondLast.id === tag.id);
23135            },
23136            toggleTagMenu(tag, event){
23137                if (event && event.stopPropagation) {
23138                    event.stopPropagation();
23139                }
23140                // Toggle the menu for this tag; close if it's already open
23141                this.open_menu_for_tag_id = this.open_menu_for_tag_id === tag.id ? null : tag.id;
23142            },
23143            closeTagMenu(){
23144                this.open_menu_for_tag_id = null;
23145            },
23146            handleClickOutside(){
23147                // Close any open menu when clicking outside
23148                if (this.open_menu_for_tag_id !== null) {
23149                    this.open_menu_for_tag_id = null;
23150                }
23151            },
23152            clicked_default_tag(tag) {
23153                this.$emit('clicked_default_tag', tag);
23154            },   
23155            clicked_catch_all_custom_tag(){
23156                this.$emit('clicked_catch_all_custom_tag');
23157            },
23158            clicked_calendar_tag(tag) {
23159                this.$emit('clicked_calendar_tag', tag);
23160            }, 
23161            clicked_add_calendar_tag(){
23162                this.$emit('clicked_add_calendar_tag');
23163            }, 
23164            clicked_remove_tag(tag) {
23165                this.$emit('clicked_remove_tag', tag);
23166            }, 
23167            changeViewPreference(event){
23168                this.$emit('change_view_preference', event );
23169            }, 
23170            change_show_calendar_tags(){
23171                this.$emit('change_show_calendar_tags');
23172            }, 
23173            isolate_default_tag(tag){
23174                this.$emit('isolate_default_tag', tag);
23175            },
23176            clicked_default_tag_secondary(tag) {
23177                this.$emit('clicked_default_tag_secondary', tag);
23178            },
23179            select_default_tags_secondary() {
23180                var q = (this.filter_search_default_secondary || '').trim();
23181                if (q && this.filtered_default_secondary_options && this.filtered_default_secondary_options.length) {
23182                    this.$emit('select_visible_default_tags_secondary', this.filtered_default_secondary_options.map((t)=>{ return t.id; }));
23183                } else {
23184                    this.$emit('select_all_default_tags_secondary');
23185                }
23186            },
23187            clear_default_tags_secondary() {
23188                var q = (this.filter_search_default_secondary || '').trim();
23189                if (q && this.filtered_default_secondary_options && this.filtered_default_secondary_options.length) {
23190                    this.$emit('clear_visible_default_tags_secondary', this.filtered_default_secondary_options.map((t)=>{ return t.id; }));
23191                } else {
23192                    this.$emit('clear_all_default_tags_secondary');
23193                }
23194            },
23195            clear_visible_default_tags_secondary() {
23196                var ids = (this.filtered_default_secondary_options || []).map((t)=>{ return t.id; });
23197                this.$emit('clear_visible_default_tags_secondary', ids);
23198            },
23199            isolate_default_tag_secondary(tag) {
23200                this.$emit('isolate_default_tag_secondary', tag);
23201            },
23202            isolate_calendar_tag(tag){
23203                this.$emit('isolate_calendar_tag', tag);
23204            }, 
23205            select_all_custom_tags(){
23206                this.$emit('select_all_custom_tags');
23207            },
23208            select_default_tags(){
23209                var q = (this.filter_search_default || '').trim();
23210                if (q && this.filtered_default_options && this.filtered_default_options.length) {
23211                    var visibleIds = this.filtered_default_options.map((t)=>{ return t.id; });
23212                    this.$emit('select_visible_default_tags', visibleIds);
23213                } else {
23214                    this.$emit('select_all_default_tags');
23215                }
23216            },
23217            clear_default_tags(){
23218                var q = (this.filter_search_default || '').trim();
23219                if (q && this.filtered_default_options && this.filtered_default_options.length) {
23220                    var visibleIds = this.filtered_default_options.map((t)=>{ return t.id; });
23221                    this.$emit('clear_visible_default_tags', visibleIds);
23222                } else {
23223                    this.$emit('clear_all_default_tags');
23224                }
23225            },
23226            select_custom_tags(){
23227                var q = (this.filter_search_custom || '').trim();
23228                if (q) {
23229                    var visibleIds = (this.filtered_custom_tags || []).map((t)=>{ return t.id; });
23230                    if (this.show_no_tags_in_custom) { visibleIds.push('catch_all_custom'); }
23231                    this.$emit('select_visible_custom_tags', visibleIds);
23232                } else {
23233                    this.$emit('select_all_custom_tags');
23234                }
23235            },
23236            clear_custom_tags(){
23237                var q = (this.filter_search_custom || '').trim();
23238                if (q) {
23239                    var visibleIds = (this.filtered_custom_tags || []).map((t)=>{ return t.id; });
23240                    if (this.show_no_tags_in_custom) { visibleIds.push('catch_all_custom'); }
23241                    this.$emit('clear_visible_custom_tags', visibleIds);
23242                } else {
23243                    this.$emit('clear_all_custom_tags');
23244                }
23245            },
23246            select_all_default_tags(){
23247                this.$emit('select_all_default_tags');
23248            },
23249            clear_all_default_tags(){
23250                this.$emit('clear_all_default_tags');
23251            },
23252            isolate_catch_all_custom(){
23253                this.$emit('isolate_catch_all_custom');
23254            }
23255        }, 
23256        computed: {
23257            show_other_filters(){
23258                // member facing calendars only show if we have tags 
23259                if(this.calendar_type && (this.calendar_type == 'member_portal' || this.calendar_type == 'web_component' )){
23260                    //only show if we have tags
23261                    return this.has_other_calendar_filters;
23262                } 
23263                //admin calendars always show
23264                //...since they need to be able to add tags
23265                return true;
23266            },
23267            has_other_calendar_filters(){
23268                if(!this.calendar_tags || this.calendar_tags.length == 0){
23269                    return false;
23270                }
23271                return true;
23272            }, 
23273            can_edit_tags(){
23274                if(this.view_only){
23275                    return false;
23276                }
23277                if(this.calendar_type && ( this.calendar_type == 'member_portal' || this.calendar_type == 'web_component' || this.calendar_type == 'events_primary' )){
23278                    //member facing calendars
23279                    // also the primary events calendar since it shows ALL event tags
23280                    return false;
23281                }
23282                return true;
23283            }, 
23284            calendar_tag_label(){
23285                if(this.calendar_type && (this.calendar_type == 'member_portal' || this.calendar_type == 'web_component' || this.calendar_type == 'events_primary' )){
23286                    return "Event tags";
23287                }
23288                return "Other calendars";
23289            },
23290            default_search_placeholder(){
23291                var title = this.default_calendar_tags?.title || 'event types';
23292                if(title.toLowerCase().trim() === 'type'){
23293                    return 'Search event types';
23294                }else{
23295                    return 'Search ' + title.toLowerCase();
23296                }
23297            },
23298            has_any_default_filters_selected(){
23299                var defaultSelected = (this.ui_tag_ids_default && typeof this.ui_tag_ids_default === 'object') ? Object.keys(this.ui_tag_ids_default).length : 0;
23300                return defaultSelected > 0;
23301            },
23302            has_any_custom_filters_selected(){
23303                var customSelected = (this.ui_tag_ids_custom && typeof this.ui_tag_ids_custom === 'object') ? Object.keys(this.ui_tag_ids_custom).length : 0;
23304                if (customSelected > 0) { return true; }
23305                if (this.ui_catch_all_custom_tag_selected) { return true; }
23306                return false;
23307            },
23308            has_default_search(){
23309                return (this.filter_search_default || '').trim().length > 0;
23310            },
23311            has_custom_search(){
23312                return (this.filter_search_custom || '').trim().length > 0;
23313            },
23314            filtered_default_options(){
23315                var options = this.default_calendar_tags?.options || [];
23316                var q = (this.filter_search_default || '').trim().toLowerCase();
23317                if (!q) { return options; }
23318                if (typeof fuzzysort !== 'undefined') {
23319                    var matches = fuzzysort.go(q, options, { keys: ['label'], threshold: -1000, limit: 100, allowTypo: false });
23320                    return matches ? matches.map((m)=>{ return m.obj; }) : [];
23321                }
23322                return options.filter((opt)=>{ return (opt.label || '').toLowerCase().indexOf(q) !== -1; });
23323            },
23324            filtered_default_secondary_options(){
23325                var options = this.default_calendar_tags_secondary?.options || [];
23326                var q = (this.filter_search_default_secondary || '').trim().toLowerCase();
23327                if (!q) { return options; }
23328                if (typeof fuzzysort !== 'undefined') {
23329                    var matches = fuzzysort.go(q, options, { keys: ['label'], threshold: -1000, limit: 100, allowTypo: false });
23330                    return matches ? matches.map((m)=>{ return m.obj; }) : [];
23331                }
23332                return options.filter((opt)=>{ return (opt.label || '').toLowerCase().indexOf(q) !== -1; });
23333            },
23334            has_any_default_secondary_filters_selected(){
23335                var o = this.ui_tag_ids_default_secondary && typeof this.ui_tag_ids_default_secondary === 'object' ? this.ui_tag_ids_default_secondary : {};
23336                return Object.keys(o).filter((k)=>{ return o[k]; }).length > 0;
23337            },
23338            default_select_all_label(){
23339                if (!this.has_default_search || !this.filtered_default_options) { return 'Select all'; }
23340                var n = this.filtered_default_options.length;
23341                return n ? 'Select all (' + n + ')' : 'Select all';
23342            },
23343            default_clear_label(){
23344                if (!this.has_default_search || !this.filtered_default_options) { return 'Clear'; }
23345                var n = this.filtered_default_options.length;
23346                return n ? 'Clear (' + n + ')' : 'Clear';
23347            },
23348            _custom_searchables(){
23349                var list = [];
23350                if (this.calendar_tags && this.calendar_tags.length) {
23351                    this.calendar_tags.forEach((tag)=>{
23352                        list.push({ name: (tag.get ? tag.get('name') : (tag.name || '') || ''), tag: tag });
23353                    });
23354                }
23355                if (this.show_catch_all_custom_tags_option) {
23356                    list.push({ name: 'No tags', tag: null });
23357                }
23358                return list;
23359            },
23360            filtered_custom_tags(){
23361                var searchables = this._custom_searchables;
23362                var q = (this.filter_search_custom || '').trim().toLowerCase();
23363                if (!q) {
23364                    return this.calendar_tags || [];
23365                }
23366                if (typeof fuzzysort !== 'undefined') {
23367                    var matches = fuzzysort.go(q, searchables, { keys: ['name'], threshold: -1000, limit: 100, allowTypo: false });
23368                    if (!matches || !matches.length) { return []; }
23369                    return matches.map((m)=>{ return m.obj.tag; }).filter(Boolean);
23370                }
23371                return (this.calendar_tags || []).filter((tag)=>{
23372                    var name = (tag.get ? tag.get('name') : (tag.name || '')) || '' || '';
23373                    return name.toLowerCase().indexOf(q) !== -1;
23374                });
23375            },
23376            filtered_custom_display_count(){
23377                var n = (this.filtered_custom_tags || []).length;
23378                if (this.show_no_tags_in_custom) { n += 1; }
23379                return n;
23380            },
23381            custom_select_all_label(){
23382                if (!this.has_custom_search) { return 'Select all'; }
23383                var n = this.filtered_custom_display_count;
23384                return n ? 'Select all (' + n + ')' : 'Select all';
23385            },
23386            custom_clear_label(){
23387                if (!this.has_custom_search) { return 'Clear'; }
23388                var n = this.filtered_custom_display_count;
23389                return n ? 'Clear (' + n + ')' : 'Clear';
23390            },
23391            show_no_tags_in_custom(){
23392                if (!this.show_catch_all_custom_tags_option) { return false; }
23393                var q = (this.filter_search_custom || '').trim().toLowerCase();
23394                if (!q) { return true; }
23395                if (typeof fuzzysort !== 'undefined') {
23396                    var matches = fuzzysort.go(q, [{ name: 'No tags', tag: null }], { keys: ['name'], threshold: -1000, allowTypo: false });
23397                    return matches && matches.length > 0;
23398                }
23399                return 'no tags'.indexOf(q) !== -1 || q === 'no';
23400            }, 
23401        }, 
23402        watch: {
23403            view_preference(newVal) {
23404                this.v_model_view_preference = newVal;
23405
23406            }
23407        }
23408
23409
23410    });
23411</script>
23411
23412
23413<style scoped>
23414 /* Rotating chevrons */
23415    .rotate-chevron.down {
23416        transform: rotate(180deg);
23417    }
23418
23419    .calendar-filter-search-wrap {
23420        margin-top: 6px;
23421    }
23422    .calendar-filter-search-container {
23423        display: flex;
23424        align-items: center;
23425        border-radius: 5px;
23426        border: 1px solid #e5eaec;
23427        overflow: hidden;
23428        margin-bottom: 2px;
23429    }
23430    .calendar-filter-search-container.active {
23431        border: 1px solid #4a8be6;
23432    }
23433    .calendar-filter-search-input {
23434        flex: 1;
23435        padding: 8px 8px 8px 30px;
23436        border: none;
23437        outline: none;
23438    }
23439    .calendar-filter-search-icon {
23440        font-size: 12px;
23441        position: absolute;
23442        left: 10px;
23443        top: 50%;
23444        transform: translateY(-50%);
23445        color: #52596f;
23446    }
23447
23448    .calendar-tag-picker.flexible-height-version{
23449        max-height: 100vh;
23450    }
23451
23452    .custom-checkbox {
23453        width: 18px;
23454        height: 18px;
23455        display: inline-block;
23456        position: relative;
23457        align-items: center;
23458        border-radius: 4px;
23459        margin-right: 8px;
23460        box-sizing: border-box;
23461        vertical-align: middle;
23462        cursor: pointer;
23463    }
23464
23465    /* Improved calendar tag item for wrapping and left alignment */
23466    .calendar-tag-item {
23467        /* align-items: flex-start !important; */
23468        flex-direction: row;
23469        flex-wrap: wrap;
23470        white-space: normal;
23471        text-align: left;
23472        line-height: 1.4;
23473    }
23474
23475    .calendar-tag-name {
23476        display: inline-block;
23477        word-break: break-word;
23478        max-width: 80%;
23479    }
23480
23481    .calendar-tag-item:hover .calendar-tag-remove-icon {
23482        visibility: visible;
23483    }
23484
23485    .calendar-tag-remove-icon {
23486        visibility: hidden;
23487        cursor: pointer;
23488        color:#788097;
23489    }
23490
23491    /* MOBILE */
23492    .mobile-filter-overlay{
23493        position: fixed;
23494        top: 0;
23495        right: 0;
23496        height: 100vh;
23497        width: 65vw;
23498        min-width: 250px;
23499        max-width: 480px;
23500        background: #fff;
23501        z-index: 1002;
23502        box-shadow: 0 0 20px rgba(0,0,0,0.2);
23503        display: flex;
23504        flex-direction: column;
23505        border-radius: 5px 0 0 5px;
23506    }
23507    .overlay-backdrop{
23508        position: fixed;
23509        inset: 0;
23510        background: rgba(0,0,0,0.4);
23511        z-index: 1001;
23512    }
23513        /* Slide-in-from-right transition */
23514    .slide-in-right-enter-active, .slide-in-right-leave-active{
23515        transition: transform 200ms ease-out;
23516    }
23517    .slide-in-right-enter-from, .slide-in-right-leave-to{
23518        transform: translateX(100%);
23519    }
23520
23521      .tag-context-menu{
23522        position: absolute;
23523        top: 100%;
23524        right: 0;
23525        margin-top: 4px;
23526        background: #fff;
23527        border: 1px solid #e0e4ee;
23528        border-radius: 6px;
23529        box-shadow: 0 4px 12px rgba(0,0,0,0.08);
23530        min-width: 160px;
23531        z-index: 10;
23532        padding: 4px 0;
23533    }
23534
23535    .tag-context-menu--above{
23536        top: auto;
23537        bottom: 100%;
23538        margin-top: 0;
23539        margin-bottom: -5px;
23540    }
23541
23542    .tag-context-menu-item{
23543        width: 100%;
23544        padding: 6px 10px;
23545        background: transparent;
23546        border: none;
23547        text-align: left;
23548        font-size: 16px;
23549        cursor: pointer;
23550        color: #333;
23551        white-space: nowrap;
23552    }
23553
23554    .tag-context-menu-item:hover{
23555        background: #f3f5fb;
23556    }
23557
23558    .tag-context-menu-item-danger{
23559        color: #c0392b;
23560    }
23561</style>
23562      
23562<script type="text/html" id="template-booking-calendar-cell">
23563    <div
23564        class="calendar-cell grid-cell pointer booking-calendar"
23565        :class="{'notCurrentMonth': ! is_current_month, 'today': is_today}"
23566        @click="add_event"
23567    >
23568        <div class="day-info flexWrap">
23569            <div @click.stop="zoom_in_to_date">
23570                <div class="cell-day calendarObj">{{ day }}</div>
23571                <div class="day-name">{{ dayOfWeek }}</div>
23572            </div>
23573        </div>
23574        <div class="events">
23575            <!-- render content for each slot -->
23576            <template
23577                v-for="(slot, index) in slots"
23578                :key="index"
23579            >
23580                <!-- ok, we have an event for this slot-->
23581                <template v-if="slot.event">
23582                    <div
23583                        class="event"
23584                        :class="[{'all_day': slot.event.isAllDay}, {'partial-day': ! slot.event.isAllDay}]"
23585                        @click.stop="clicked_event( slot.event )"
23586                        :style="return_multi_day_event_style( slot.event )"
23587                    >
23588                        <div
23589                            class="event-title-v2 flexNoWrap align-items-center easeSuperFast"
23590                            style="font-size:12.5px;"
23591                        >
23592                            <template
23593                                v-if="! slot.event.isAllDay"
23594                            >
23595                                <!-- dot -->
23596                                <i
23597                                    style="padding-right:6px;padding-left:2px;font-size:8px;"
23598                                    class="fa-solid fa-circle hideOnMobile"
23599                                    :style="{color: slot.event.color}"
23600                                ></i>
23601                                <!-- start time -->
23602                                <span v-if="! slot.event.isAllDay" class="event-time-v2 hideOnMobile">{{ formatEventTime( slot.event.ts ) }}</span>
23603                            </template>
23604                            <!-- event title -->
23605                            {{ slot.event.title }}
23606                        </div>
23607                    </div>
23608                </template>
23609                <template v-else-if="should_hold_slot(slot, index)">
23610                    <!-- Leave it blank if this slot is used later in the week  -->
23611                    <div
23612                        class="event placeholder"
23613                    >
23614                        <!-- add a placeholder if there's an event in this slot but it doesn't start on this day -->
23615                        <div
23616                            class="event-title-v2 flexNoWrap align-items-center easeSuperFast"
23617                            style="font-size:12.5px; visibility: hidden;"
23618                        >&nbsp;</div>
23619                        <!-- &nbsp; adds a hidden, non-breaking space -->
23620                    </div>
23621                </template>
23622
23623                <!-- no event for this slot -->
23624                <template v-else>
23625                    <div
23626                        class="event partial-day"
23627                    ></div>
23628                </template>
23629
23630            </template>
23631
23632            <!-- "+ x more" Label for Overflow -->
23633            <div
23634                v-if="overflowCount > 0"
23635                class="more-events-label"
23636                @click.stop="clicked_show_more_events"
23637            >
23638                + {{ overflowCount }} more
23639            </div>
23640        </div>
23641    </div>
23642</script>
23642
23643
23644<style scoped>
23645
23646    .event-time-v2{
23647        font-size: 11px;
23648        padding-right: 4px;;
23649    }
23650
23651    .more-events-label {
23652        font-size: 12px;
23653        cursor: pointer;
23654        padding-left: 6px;
23655        padding-right: 6px;
23656        text-align: left;
23657        padding-top: 2px;
23658        padding-bottom: 2px;
23659        cursor: pointer;
23660        border-radius: 5px;
23661    }
23662
23663    .event-title-v2{
23664        font-size: 12.5px;
23665        white-space: nowrap;
23666        padding-top: 2px;
23667        padding-bottom: 2px;;
23668        padding-left: 6px;
23669        padding-right: 6px;
23670        cursor: pointer;
23671        border-radius: 5px;
23672    }
23673
23674    .more-events-label:hover,
23675    .partial-day .event-title-v2:hover{
23676        background-color: #e4ebf1;
23677    }
23678    .booking-calendar .event.partial-day {
23679        /* color: #fff; */
23680        border-radius: 8px;
23681    }
23682
23683    .cell-day.calendarObj{
23684        min-width: 22px;
23685        max-width: 22px;
23686        min-height: 22px;
23687        max-height: 22px;
23688        margin: 6px !important;
23689        font-size: 12px;
23690    }
23691    .grid-cell.today .cell-day{
23692        background-color: var(--custom-mp-accent, #4a56f8);
23693        color: rgb(255, 255, 255);
23694    }
23695
23696
23697    /* Event List Styling */
23698    .events {
23699        display: flex;
23700        flex-direction: column;
23701        gap: 4px; /* Space between events */
23702    }
23703
23704    .event{
23705        font-size: 12px;
23706        font-weight: 400;
23707        overflow: hidden;
23708        white-space: nowrap;
23709        text-align: left;
23710    }
23711
23712    /* Individual Event Styling */
23713    .event.all_day {
23714        background-color: var(--custom-mp-accent, #4a56f8);
23715        color: #fff;
23716        border-radius: 8px;
23717        width: calc(100% - 4px);
23718    }
23719
23720</style>
23721
23722<script>
23723
23724    /* ----- COMPONENT ----- */
23725    app_content.component("booking-calendar-cell", {
23726        template: "#template-booking-calendar-cell",
23727        data() {
23728            return {}
23729        },
23730        props: {
23731            date: Object,
23732            day: Number,
23733            day_of_week: String,
23734            date_components: Object,
23735            events: Array,
23736            slots: Array,
23737            timezone: String,
23738            is_current_month: Boolean,
23739            days_array: Array,
23740            calendar_type: String
23741        },
23742        mounted(){
23743        },
23744        methods: {
23745            add_event(){
23746                var date_components = return_date_components_v2({
23747                    date: this.date,
23748                    timezone: this.timezone
23749                })
23750                this.$emit("add_event", date_components);
23751            },
23752            zoom_in_to_date(){
23753                this.$emit("zoom_in_to_date", this.date);
23754            },
23755            formatEventTime(timestamp) {
23756                const time = moment.tz(timestamp, this.timezone);
23757                if (time.minute() === 0) {
23758                    return time.format("ha");
23759                }
23760                return time.format("h:mma");
23761            },
23762            is_multi_day_event( event ){
23763                return moment.tz(event.ts, this.timezone).isBefore(moment.tz(event.end_ts, this.timezone), 'day');
23764            },
23765            // Updated to fix last day rendering & timezone issues for multi-day events
23766            return_multi_day_event_style(event) {
23767                const cellDate = moment.tz(this.date, this.timezone).startOf('day');
23768                const eventStart = moment.tz(event.ts, this.timezone).startOf('day');
23769                const eventEnd = moment.tz(event.end_ts, this.timezone).startOf('day');
23770
23771                // Skip styling if the event doesn't span this day
23772                if (cellDate.isBefore(eventStart) || cellDate.isAfter(eventEnd)) {
23773                    return { display: 'none' };
23774                }
23775
23776                const isFirstDay = cellDate.isSame(eventStart, 'day');
23777                const isStartOfWeek = cellDate.day() === 0;
23778                const textColor = this.adaptiveTextColorFromColors(event.bgColors);
23779
23780                if (isFirstDay || isStartOfWeek) {
23781                    const remainingDays = eventEnd.diff(cellDate, 'days') + 1;
23782                    const span = Math.min(remainingDays, 7 - cellDate.day());
23783
23784                    return {
23785                        width: `calc(${span * 100}% - 4px)`,
23786                        zIndex: 1,
23787                        color: textColor,
23788                        background: event.bgColors?.length
23789                            ? `repeating-linear-gradient(
23790                                -45deg,
23791                                ${event.bgColors.map((color, i) => `${color} ${i * 20}px, ${color} ${(i + 1) * 20}px`).join(",")}
23792                              )`
23793                            : event.bgColor || "#4a56f8"
23794                    };
23795                }
23796
23797                return {
23798                    width: 'calc(100% - 4px)',
23799                    background: 'transparent',
23800                    visibility: 'hidden'
23801                };
23802            },
23803            remaining_days_in_event(event) {
23804                const cellDate = moment.tz(this.date, this.timezone).startOf("day");
23805                const eventEnd = moment.tz(event.end_ts, this.timezone).startOf("day");
23806                if (cellDate.isAfter(eventEnd)) {
23807                    return 0;
23808                }
23809                return eventEnd.diff(cellDate, "days") + 1;
23810            },
23811            return_number_of_days_for_event( event ){
23812                let start = moment.tz(event.ts, this.timezone);
23813                let end = moment.tz(event.end_ts, this.timezone);
23814                //continue
23815                return end.diff(start, 'days') + 1;
23816            },
23817            is_first_day_of_event(event) {
23818                // Compare the event start date with the cell's date
23819                const eventStart = moment.tz(event.ts, this.timezone).startOf("day");
23820                const cellDate = moment.tz(this.date, this.timezone).startOf("day");
23821                return eventStart.isSame(cellDate, "day");
23822            },
23823            clicked_event(event) {
23824                // Handle event click
23825                this.$emit("clicked_event", event);
23826            },
23827            clicked_show_more_events() {
23828                // Handle "+ x more" click (e.g., show a modal or expand view
23828)
23829                this.$emit("clicked_show_more_events", this.date, this.day_of_week, this.events);
23830            },
23831            should_hold_slot(slot, index) {
23832                const usedSlots = this.return_slots_in_use_for_week(slot, index);
23833                return usedSlots.includes(index);
23834            },
23835            return_slots_in_use_for_week(slot, index) {
23836                const startOfWeek = moment.tz(this.date, this.timezone).startOf("week");
23837                const endOfWeek = moment.tz(this.date, this.timezone).endOf("week");
23838
23839                const slotsUsed = new Set();
23840
23841                this.days_array.forEach(day => {
23842                    const dayMoment = moment.tz(day.date, this.timezone).startOf("day");
23843
23844                    // Only consider days in the current week
23845                    if (dayMoment.isBetween(startOfWeek, endOfWeek, null, '[]')) {
23846                        if (Array.isArray(day.slots)) {
23847                            day.slots.forEach(slotObj => {
23848                                if (slotObj.event) {
23849                                    slotsUsed.add(slotObj.slot);
23850                                }
23851                            });
23852                        }
23853                    }
23854                });
23855                return Array.from(slotsUsed);
23856            },
23857            // --- text color helpers ---
23858            computeLuminance(hex) {
23859                if (!hex) return 1; // treat as light
23860                const m = String(hex).trim();
23861                let r, g, b;
23862                if (/^#?[0-9a-fA-F]{3}$/.test(m)) {
23863                    const s = m.replace('#','');
23864                    r = parseInt(s[0] + s[0], 16);
23865                    g = parseInt(s[1] + s[1], 16);
23866                    b = parseInt(s[2] + s[2], 16);
23867                } else {
23868                    const s = m.replace('#','').slice(0,6);
23869                    r = parseInt(s.substr(0,2), 16);
23870                    g = parseInt(s.substr(2,2), 16);
23871                    b = parseInt(s.substr(4,2), 16);
23872                }
23873                const toLin = (c) => {
23874                    c /= 255;
23875                    return c <= 0.03928 ? c/12.92 : Math.pow((c+0.055)/1.055, 2.4);
23876                };
23877                return 0.2126*toLin(r) + 0.7152*toLin(g) + 0.0722*toLin(b); // 0=dark, 1=light
23878            },
23879            adaptiveTextColorFromColors(colors, lightText = '#ffffff', darkText = '#2c2e30') {
23880                const arr = (colors && colors.length) ? colors : [];
23881                if (!arr.length) {
23882                    const defaultBg = '#4a56f8';
23883                    return this.computeLuminance(defaultBg) < 0.5 ? lightText : darkText;
23884                }
23885                let darkCount = 0;
23886                for (let i = 0; i < arr.length; i++) {
23887                    const L = this.computeLuminance(arr[i]);
23888                    if (!isNaN(L) && L < 0.5) darkCount++;
23889                }
23890                const mostlyDark = darkCount >= Math.ceil(arr.length / 2);
23891                return mostlyDark ? lightText : darkText;
23892            },
23893        },        
23894        computed:{
23895            is_today(){
23896                return moment.tz(new Date(), this.timezone).isSame(moment.tz(this.date, this.timezone), 'day');
23897            },
23898            overflowCount() {
23899                // Calculate the number of hidden events
23900                let visible_events = 0;
23901                this.slots.forEach(slot => {
23902                    if( slot.event ){
23903                        visible_events ++;
23904                    }
23905                });
23906                return this.events.length - visible_events;
23907            },
23908            days_until_end_of_week(){
23909                let start = moment.tz(this.date, this.timezone);
23910                let days_until_end_of_week = 7 - start.day();
23911                return days_until_end_of_week;
23912            },
23913            is_first_day_of_week(){
23914                return moment.tz(this.date, this.timezone).day() === 0;
23915            },
23916        },
23917    })
23918</script>
23918    
23918<script type="text/html" id="template-booking-calendar-day-column">
23919    
23920    <div class="flexGrowOne">
23921        <!-- Time Slots Section -->
23922        <div class="time-slots">
23923            <!-- all day -->
23924            <div 
23925                v-if="view_preference == 'Day'"
23926                class="time-slot"
23927                style="height:auto;"
23928            >
23929                <div class="time-label">All day</div>
23930                <div 
23931                    class="time-content"
23932                    style="display: flex;flex-direction: column;gap: 4px; padding: 8px;"
23933                >
23934                    <!-- Filter and position events for this hour -->
23935                    
23936                    <div v-for="(event, index) in all_day_events" :key="index" class="event all_day">
23937                        <div 
23938                            class="event-title-v2 flexNoWrap align-items-center easeSuperFast"
23939                            :style="all_day_event_style(event)"
23940                            @click="clicked_event(event)"
23941                        >
23942                            {{ event.title }}
23943                        </div>
23944                    </div>
23945
23946                </div>
23947            </div>
23948            <!-- hours -->
23949            <div 
23950                v-for="hour in 24" 
23951                :key="hour" 
23952                class="time-slot pointer"
23953                @click="add_event( hour - 1 )"
23954            >
23955                <div 
23956                    class="time-label"
23957                    v-if="view_preference == 'Day'"
23958                >{{ formatHour( hour - 1 ) }}</div>
23959                <div class="time-content">
23960                    <!-- Filter and position events for this hour -->
23961                    <div
23962                        v-for="(event, index) in filterEventsForHour(hour - 1)"
23963                        :key="index"
23964                        class="event time-slot-event"
23965                        :style="calculateEventStyle(event, index)"
23966                        @click.stop="clicked_event( event )"
23967                    >
23968                        <div class="event-title-v2" style="white-space: initial;">{{ event.title }}</div>
23969                    </div>
23970                </div>
23971            </div>
23972        </div>
23973    </div>
23974    
23975</script>
23975
23976
23977<style scoped>
23978
23979    .event.time-slot-event {
23980        position: absolute;
23981        left: 5px; /* Align slightly inside the slot */
23982        right: 5px;
23983        color: white;
23984        border-radius: 4px;
23985        font-size: 12px;
23986        font-weight: 500;
23987        overflow: hidden;
23988        white-space: nowrap;
23989        text-overflow: ellipsis;
23990        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
23991        z-index: 1;
23992        padding-top: 0px;
23993        padding-bottom: 0px;
23994        padding-left: 8px;
23995        padding-right: 8px;
23996        cursor: pointer;
23997        background-color: #5774f5; /* Primary color */
23998    }
23999
24000</style>
24001
24002<script>
24003
24004  /* ----- COMPONENT ----- */
24005  app_content.component("booking-calendar-day-column", {
24006        template: "#template-booking-calendar-day-column",
24007        data() {
24008            return {}
24009        },
24010        props: {
24011            date_components: Object,
24012            events: Array,
24013            timezone: String,
24014            view_preference: String
24015        },
24016        mounted(){
24017        },
24018        methods: {
24019            add_event( hour ){
24020                let date_components = {... this.date_components};
24021                date_components.hour = hour;
24022                date_components.date = this.date_components.day;
24023                this.$emit('add_event', date_components);
24024            },
24025            clicked_event( event ){
24026                this.$emit('clicked_event', event);
24027            },
24028            formatHour(hour) {
24029                const baseDate = moment().startOf('day').hour(hour);
24030                return baseDate.format("h:mm A");
24031            },
24032            filterEventsForHour(hour) {
24033                const filtered = this.events.filter((event) => {
24034                    if (event.isAllDay) return false;
24035                    const eventStart = moment(event.ts).tz(this.timezone);
24036                    return eventStart.hour() === hour;
24037                });
24038                const count = filtered.length;
24039                return filtered.map(event => ({
24040                    ...event,
24041                    countForHour: count
24042                }));
24043            },
24044            all_day_event_style(event){
24045                return {
24046                    "background": event.bgColors && event.bgColors.length > 0
24047                            ? `repeating-linear-gradient(
24048                                    -45deg,
24049                                    ${event.bgColors.map((color, i) => `${color} ${i * 20}px, ${color} ${(i + 1) * 20}px`).join(",")}
24050                                )`
24051                            : event.bgColor || "",
24052                };
24053            },
24054            calculateEventStyle(event, i) {
24055                const eventStart = moment( event.ts ).tz(this.timezone);
24056                const eventEnd = moment(event.end_ts).tz(this.timezone);
24057                const count = event.countForHour;
24058
24059                // Calculate start and end as a percentage of the hour
24060                const startMinutes = eventStart.minutes();
24061                const endMinutes = eventEnd.minutes();
24062                const duration = eventEnd.diff(eventStart, "minutes");
24063
24064                return {
24065                    top: `${(startMinutes / 60) * 100}%`, // Start position
24066                    height: `calc(${(duration / 60) * 100}% - 3px)`, // Subtracts 3px for padding
24067                    width: `calc(${(1/count) * 100}% - 3px)`, // Subtracts 3px for padding
24068                    'margin-left': `calc(${(i/count) * 100}%)`,
24069                    'padding-left': '0px',
24070                    'padding-right': '0px',
24071                    "background": event.bgColors && event.bgColors.length > 0
24072                            ? `repeating-linear-gradient(
24073                                    -45deg,
24074                                    ${event.bgColors.map((color, i) => `${color} ${i * 20}px, ${color} ${(i + 1) * 20}px`).join(",")}
24075                                )`
24076                            : event.bgColor || "",
24077                };
24078            },
24079        },
24080        computed:{
24081            all_day_events(){
24082                let all_day =  this.events.filter( event => event.isAllDay === true );
24083                return all_day;
24084            },
24085        },
24086  })
24087</script>
24087
24088
24089
24090
24091
24092
24093
24094    <!-- native tab bar -->
24095    <!DOCTYPE html>
24096<html lang="en">
24097    
24097<script type="text/html" id="template-bottom-sheet-overlay">
24098
24099    <div 
24100        class="v-overlay scrollable hugePaddingBottom active" 
24101        @touchstart.self="touched_overlay()"
24102        @mousedown.self="touched_overlay()"
24103        @click.self="clicked_overlay()"
24104        :class="[{'noPointer': prevent_close}, {'bottom-sheet-blur': blur_background}]"
24105    >
24106        <div 
24107            class="standardOverlayCard centeredBlock bottom-sheet"
24108            :class="[
24109                {'short': height == 'short' || is_short}, 
24110                {'narrow': narrow},
24111                {'noMaxWidth': full_width}
24112            ]"
24113        >
24114
24115            <!-- generic header content -->
24116            <div class="bottom-sheet-overlay-header">
24117
24118                <div 
24119                    v-if="! prevent_close"
24120                    class="relative"
24121                >
24122                    <div 
24123                        class="overlay-header-handle-wrap centeredBlock"
24124                        @click.stop="toggle_height()" 
24125                    >
24126                        <div class="overlay-header-handle"></div>
24127                    </div>
24128                    <i 
24129                        class="overlay-x-button fa-regular fa-xmark pointer"
24130                        @click.stop="dismiss_overlay()"
24131                    ></i>
24132                </div>
24133
24134                <!-- header content injected here -->
24135                <slot name="header"></slot>
24136
24137            </div>
24138
24139            <!-- body content injected here -->
24140            <slot name="body"></slot>
24141
24142            <!-- footer content injected here -->
24143            <slot name="footer"></slot>
24144
24145        </div>
24146    </div>
24147
24148</script>
24148
24149
24150<style>
24151
24152    .standardOverlayCard.bottom-sheet.narrow{
24153        max-width: 600px;
24154    }
24155
24156    .standardOverlayCard.bottom-sheet.medium{
24157        max-width: 800px;
24158    }
24159
24160    .standardOverlayCard.bottom-sheet{
24161        position: fixed;
24162        bottom: 0px;
24163        top: 50px;
24164        left: 0px;
24165        right: 0px;
24166        z-index: 1001;
24167        background-color: #fff;
24168        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
24169        transition: bottom 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
24170        border-top-left-radius: 10px;
24171        border-top-right-radius: 10px;
24172        padding-bottom: 10px;
24173        padding-top: 10px;
24174        max-width: 1100px;
24175        margin-top: 0px;
24176        border-bottom-left-radius: 0px;
24177        border-bottom-right-radius: 0px;
24178    }
24179
24180    .standardOverlayCard.bottom-sheet.short{
24181        top: 50%;
24182    }
24183
24184    @media (max-height: 600px) {
24185
24186        .standardOverlayCard.bottom-sheet.short{
24187            top: 150px;
24188        }
24189
24190    }
24191
24192    .standardOverlayCard.bottom-sheet h3{
24193        padding-top:0px;
24194    }
24195
24196    .standardOverlayCard.bottom-sheet{
24197        display: flex;
24198        flex-direction: column;
24199        padding-bottom:0px;
24200        padding-top: 0px;
24201    }
24202
24203    .standardOverlayCard.bottom-sheet .bottom-sheet-overlay-card-header
24204    {
24205        flex: 0 0 auto; /* Fixed height, doesn't grow or shrink */
24206    }
24207
24208    .standardOverlayCard.bottom-sheet .standardCardFooter{
24209        position: absolute;
24210        bottom: 0px;
24211        padding-bottom: var(--cs-safe-area-bottom, 10px);
24212        left: 0px;
24213        right: 0px;
24214        background-color: #fff;
24215        border-top: 1px solid #e4ebf1;
24216        z-index: 10;
24217    }
24218
24219    .standardOverlayCard.bottom-sheet .standardCardBody{
24220        flex: 1 1 auto; /* Flexible height, can grow and shrink */
24221        padding-bottom: 90px !important;
24222    }
24223
24224    .overlay-header-handle-wrap{
24225        padding: 10px;
24226        cursor: pointer;
24227        max-width: 120px;
24228    }
24229    .overlay-header-handle{
24230        width: 70px;
24231        height: 3px;
24232        border-radius: 10px;
24233        background-color: #acb3ba;
24234        margin-left: auto;
24235        margin-right: auto;
24236        margin-top: 8px;
24237        margin-bottom: 8px;
24238    }
24239    .overlay-x-button{
24240        position: absolute;
24241        right: 14px;
24242        color: #535e6c;
24243        font-size: 20px;
24244        top: 12px;
24245    }
24246</style>
24247
24248<script>
24249
24250    //define the component
24251    app_overlays.component("bottom-sheet-overlay", {
24252        template: "#template-bottom-sheet-overlay",
24253        data() {
24254            return {
24255                height: "tall"
24256            }
24257        },
24258        props: {
24259            blur_background: {
24260                type: Boolean,
24261                default: false
24262            },
24263            narrow: {
24264                type: Boolean,
24265                default: false
24266            },
24267            prevent_close: {
24268                type: Boolean,
24269                default: false
24270            },
24271            full_width: {
24272                type: Boolean,
24273                default: false
24274            },
24275            is_short: {
24276                type: Boolean,
24277                default: false
24278            }
24279        },
24280        mounted(){
24281            const universalEditOverlay = document.getElementById('overlay_universal-edit');
24282            if (universalEditOverlay) {
24283                universalEditOverlay.style.zIndex = "1000000";
24284            }
24285            const membershipValidationOverlay = document.getElementById('overlay_verifyMembership');
24286            if (membershipValidationOverlay) {
24287                membershipValidationOverlay.style.zIndex = "1000000";
24288            }
24289        },
24290        beforeUnmount() {
24291            //reset the z-index
24292            const universalEditOverlay = document.getElementById('overlay_universal-edit');
24293            if (universalEditOverlay) {
24294                universalEditOverlay.style.zIndex = ""; // Revert to default
24295            }
24296            const membershipValidationOverlay = document.getElementById('overlay_verifyMembership');
24297            if (membershipValidationOverlay) {
24298                membershipValidationOverlay.style.zIndex = ""; // Revert to default
24299            }
24300        },
24301        methods: {
24302            dismiss_overlay(){
24303                this.$emit('dismiss_overlay');
24304            },
24305            toggle_height(){
24306                if( this.height === "tall" ){
24307                    this.height = "short";
24308                }else{
24309                    this.height = "tall";
24310                }
24311            },
24312            clicked_overlay(){
24313                if( this.prevent_close ){
24314                    return;
24315                }
24316                if (should_close_overlay(this.last_touch)) {
24317                    this.dismiss_overlay();
24318                }
24319            },
24320            touched_overlay(){
24321                this.last_touch = new Date().getTime();
24322            },
24323        },
24324    });
24325
24326    
24327</script>
24327
24328
24329<style>
24330    .bottom-sheet-blur{
24331        backdrop-filter: blur(6px);
24332    }
24333
24334    @keyframes bottom-sheet-fade-in {
24335        from { opacity: 0; }
24336        to   { opacity: 1; }
24337    }
24338</style>    
24338<script type="text/html" id="template-show-push-notification">
24339	
24340    <bottom-sheet-overlay
24341        @dismiss_overlay="dismiss_overlay()"
24342    >
24343
24344        <!-- header content -->
24345        <template #header>
24346            <div class="flexNoWrap">
24347                <h3 
24348                    class="flexGrowOne" 
24349                    style="padding-bottom:0px;"
24350                >
24351                    <p>{{ title }}</p>
24352                    <template v-if="has_date">
24353                        <p 
24354                            class="card-header-subtext pointer"
24355                        >{{ date_string }}</p>
24356                    </template>
24357                </h3>
24358            </div>
24359        </template>
24360
24361        <!-- body content -->
24362        <template #body>
24363            <div 
24364                class="standardCardBody fff scrollable"
24365            >
24366                <p class="p-1" style="margin: 0px 8px;">{{ notification.get('data').alert }}</p>
24367            </div>
24368        </template>
24369
24370        <!-- footer content -->
24371        <template 
24372            #footer
24373        >
24374            <div 
24375                class="standardCardFooter flexNoWrap"
24376            >
24377                <div class="flexGrowOne"></div>
24378                <button class="marketing-header-button blue easeSuperFast flexWrapLeftAlign align-items-center" @click="dismiss_overlay()" >Done</button>
24379            </div>
24380        </template>
24381
24382    </bottom-sheet-overlay>
24383
24384</script>
24384
24385
24386<script>
24387
24388    function show_push_notification( params ){
24389        store.components.show_push_notification.notification = params.notification;
24390        store.overlays.show_push_notification = true;
24391    }
24392
24393    //add a value to our global store, specific to this component
24394    store.components["show_push_notification"] = {
24395        notification: null
24396    }
24397
24398    //define the component
24399    app_overlays.component("show-push-notification", {
24400        template: "#template-show-push-notification",
24401        data() {
24402            return store.components["show_push_notification"]
24403        },        
24404        methods: {
24405            dismiss_overlay(){
24406                store.overlays.show_push_notification = false;
24407            } 
24408        },
24409        computed: {
24410            title(){               
24411                let title = this.notification.get('data').title;
24412                if (title && title.length > 0){
24413                    return title;
24414                }
24415                return "Notification";
24416            },
24417            has_date(){
24418                return this.notification.createdAt !== undefined;
24419            },
24420            date_string(){
24421                const createdAt = this.notification.createdAt;
24422
24423                if (! createdAt){
24424                    return createdAt;
24425                }
24426
24427                if ( is_utc_date_today( createdAt ) ){
24428                    return format_utc_date_v2({
24429                        "utcDate": createdAt,
24430                        "formatString": "[Today at] h:mm A"
24431                    });
24432                } else {
24433                    return format_utc_date_v2({
24434                        "utcDate": createdAt,
24435                        "formatString": "ddd M/D h:mm A"
24436                    });
24437                }
24438            }
24439        }
24440    });
24441
24442</script>
24442    
24442<script type="text/html" id="template-native-tab-bar">
24443
24444    <!-- first, make sure we're in the react native member app -->
24445    <template v-if="is_member_portal || is_react_native_member_app || is_admin_preview">
24446
24447        <!-- ── Floating glass tab bar ─────────── -->
24448        <div
24449            v-if="show_tab_bar && use_floating_tab_bar"
24450            class="mp-float-tab-bar"
24451            :class="{
24452                'mp-float-tab-bar--preview': is_admin_preview,
24453                'mp-float-tab-bar--settings-preview': settings_preview_mode
24454            }"
24455        >
24456            <!-- Single sliding pill — moves between tabs via CSS transition -->
24457            <div class="mp-float-tab-pill" :style="float_pill_style"></div>
24458
24459            <template v-if="visible_tabs">
24460                <div
24461                    v-for="tab in visible_tabs"
24462                    :key="tab.key"
24463                    class="mp-float-tab-item"
24464                    :class="{ 'mp-float-tab-item--active': tab_is_active(tab) }"
24465                    @touchstart.prevent="tapped_tab_bar_item(tab)"
24466                    @click="tapped_tab_bar_item(tab)"
24467                >
24468                    <template v-if="show_member_details_avatar(tab)">
24469                        <span
24470                            class="mp-float-tab-avatar"
24471                            :class="{ 'mp-float-tab-avatar--active': tab_is_active(tab) }"
24472                            :style="{ backgroundImage: 'url(' + member_tab_avatar_url + ')' }"
24473                        ></span>
24474                    </template>
24475                    <template v-else>
24476                        <i class="mp-float-tab-icon" :class="float_icon_class(tab, tab_is_active(tab))"></i>
24477                    </template>
24478                </div>
24479                <!-- More item -->
24480                <div
24481                    class="mp-float-tab-item"
24482                    :class="{ 'mp-float-tab-item--active': more_is_active }"
24483                    @touchstart.prevent="tapped_more()"
24484                    @click="tapped_more()"
24485                >
24486                    <i :class="(more_is_active ? 'fa-solid' : 'fa-regular') + ' fa-ellipsis mp-float-tab-icon'"></i>
24487                </div>
24488            </template>
24489        </div>
24490
24491        <!-- Classic tab bar ─────────────── -->
24492        <div 
24493            v-else-if="show_tab_bar"
24494            class="native-tab-bar-outer flexNoWrap"
24495            :style="[{ 
24496                backgroundColor: tab_bar_colors.background || 'rgba(249, 251, 251, 1)' 
24497            }, {
24498                backdropFilter: tab_bar_colors.blur ? 'blur(' + tab_bar_colors.blur + ')' : 'none'
24499            }]"
24500            :class="{ 'noBorder': tab_bar_colors.background, 'preview': is_admin_preview }"
24501        >
24502            <template v-if="visible_tabs">
24503                <div 
24504                    v-for="tab in visible_tabs"
24505                    :key="tab.key"
24506                    class="native-tab-bar-item"
24507                    :class="{ active : tab_is_active( tab ) }"
24508                    :style=" tab_is_active( tab ) ? { color: tab_bar_colors.active || '#4a56f8' } : { color: tab_bar_colors.text || '#697684' } "
24509                    @touchstart.prevent="tapped_tab_bar_item( tab )"
24510                    @click="tapped_tab_bar_item( tab )"
24511                >
24512                    <div class="centeredText centeredBlock">
24513                        <p>
24514                            <template v-if="show_member_details_avatar(tab)">
24515                                <span
24516                                    class="native-tab-bar-avatar"
24517                                    :style="{ backgroundImage: 'url(' + member_tab_avatar_url + ')' }"
24518                                ></span>
24519                            </template>
24520                            <template v-else>
24521                                <i class="native-tab-bar-icon" :class="tab.icon_class"></i>
24522                            </template>
24523                        </p>
24524                        <p class="native-tab-bar-label">{{ tab.label }}</p>
24525                    </div>
24526                </div>
24527                <div 
24528                    class="native-tab-bar-item"
24529                    @touchstart.prevent="tapped_more()"
24530                    @click="tapped_more()"
24531                    :class="{ 'active': more_is_active }"
24532                    :style=" more_is_active ? { color: tab_bar_colors.active || '#4a56f8' } : { color: tab_bar_colors.text || '#697684' } "
24533                >
24534                    <div class="centeredText centeredBlock">
24535                        <p><i class="fa-regular fa-ellipsis native-tab-bar-icon"></i></p>
24536                        <p class="native-tab-bar-label">More</p>
24537                    </div>
24538                </div>
24539            </template>
24540        </div>
24541
24542        <!-- compact more-section tab bar (floating) -->
24543        <div
24544            v-else-if="show_more_section_tab_bar && use_floating_tab_bar"
24545            class="mp-float-tab-bar"
24546        >
24547            <div class="mp-float-tab-pill" :style="compact_float_pill_style"></div>
24548            <div
24549                class="mp-float-tab-item"
24550                @touchstart.prevent="tapped_back_immediate()"
24551                @click="tapped_back_immediate()"
24552            >
24553                <i class="fa-regular fa-chevron-left mp-float-tab-icon"></i>
24554            </div>
24555            <div
24556                class="mp-float-tab-item"
24557                @touchstart.prevent="tapped_home()"
24558                @click="tapped_home()"
24559            >
24560                <i class="mp-float-tab-icon" :class="compact_home_icon_class"></i>
24561            </div>
24562            <div
24563                class="mp-float-tab-item mp-float-tab-item--active"
24564                @touchstart.prevent="tapped_tab_bar_item(compact_selected_tab)"
24565                @click="tapped_tab_bar_item(compact_selected_tab)"
24566            >
24567                <template v-if="show_member_details_avatar(compact_selected_tab)">
24568                    <span
24569                        class="mp-float-tab-avatar mp-float-tab-avatar--active"
24570                        :style="{ backgroundImage: 'url(' + member_tab_avatar_url + ')' }"
24571                    ></span>
24572                </template>
24573                <template v-else>
24574                    <i class="mp-float-tab-icon" :class="compact_selected_tab_icon_class"></i>
24575                </template>
24576            </div>
24577            <div
24578                class="mp-float-tab-item"
24579                @touchstart.prevent="tapped_more()"
24580                @click="tapped_more()"
24581            >
24582                <i class="fa-regular fa-ellipsis mp-float-tab-icon"></i>
24583            </div>
24584        </div>
24585
24586        <!-- compact more-section tab bar (classic) -->
24587        <div
24588            v-else-if="show_more_section_tab_bar"
24589            class="native-tab-bar-outer flexNoWrap"
24590            :style="[{ 
24591                backgroundColor: tab_bar_colors.background || 'rgba(249, 251, 251, 1)' 
24592            }, {
24593                backdropFilter: tab_bar_colors.blur ? 'blur(' + tab_bar_colors.blur + ')' : 'none'
24594            }]"
24595            :class="{ 'noBorder': tab_bar_colors.background, 'preview': is_admin_preview }"
24596        >
24597            <div
24598                class="native-tab-bar-item"
24599                :style="{ color: tab_bar_colors.text || '#697684' }"
24600                @touchstart.prevent="tapped_back_immediate()"
24601                @click="tapped_back_immediate()"
24602            >
24603                <div class="centeredText centeredBlock">
24604                    <p><i class="fa-regular fa-chevron-left native-tab-bar-icon"></i></p>
24605                    <p class="native-tab-bar-label">Back</p>
24606                </div>
24607            </div>
24608            <div
24609                class="native-tab-bar-item"
24610                :style="{ color: tab_bar_colors.text || '#697684' }"
24611                @touchstart.prevent="tapped_home()"
24612                @click="tapped_home()"
24613            >
24614                <div class="centeredText centeredBlock">
24615                    <p><i class="native-tab-bar-icon" :class="compact_home_icon_class"></i></p>
24616                    <p class="native-tab-bar-label">{{ compact_home_tab_label }}</p>
24617                </div>
24618            </div>
24619            <div
24620                class="native-tab-bar-item active"
24621                :style="{ color: tab_bar_colors.active || '#4a56f8' }"
24622                @touchstart.prevent="tapped_tab_bar_item(compact_selected_tab)"
24623                @click="tapped_tab_bar_item(compact_selected_tab)"
24624            >
24625                <div class="centeredText centeredBlock">
24626                    <p>
24627                        <template v-if="show_member_details_avatar(compact_selected_tab)">
24628                            <span
24629                                class="native-tab-bar-avatar"
24630                                :style="{ backgroundImage: 'url(' + member_tab_avatar_url + ')' }"
24631                            ></span>
24632                        </template>
24633                        <template v-else>
24634                            <i class="native-tab-bar-icon" :class="compact_selected_tab_icon_class"></i>
24635                        </template>
24636                    </p>
24637                    <p class="native-tab-bar-label">{{ compact_selected_tab.label }}</p>
24638                </div>
24639            </div>
24640            <div
24641                class="native-tab-bar-item"
24642                :style="{ color: tab_bar_colors.text || '#697684' }"
24643                @touchstart.prevent="tapped_more()"
24644                @click="tapped_more()"
24645            >
24646                <div class="centeredText centeredBlock">
24647                    <p><i class="fa-regular fa-ellipsis native-tab-bar-icon"></i></p>
24648                    <p class="native-tab-bar-label">More</p>
24649                </div>
24650            </div>
24651        </div>
24652
24653        <!-- back button (global) -->
24654        <div v-else-if="show_back_button">
24655
24656            <!-- overlay -->
24657            <div 
24658                class="native-tab-bar-back-button-overlay pointer"
24659                @touchstart.self.prevent="back_button_expanded = false"
24660                @click.self="back_button_expanded = false"
24661                :class="{ 'active': back_button_expanded }"
24662            ></div>
24663
24664            <!-- home button -->
24665            <div 
24666                class="native-tab-bar-floating-home-button boxShadowBottom moreBlur pointer easeFast"
24667                @click="tapped_home()"
24668                :class="{ 'active': back_button_expanded }"
24669            >
24670                <div class="centeredText centeredBlock">
24671                    <p><i class="fa-solid fa-house native-tab-bar-icon" style="font-size:20px;"></i><span v-if="back_button_expanded" style="padding-left:6px;font-size:18px;">Home</span></p>
24672                </div>
24673            </div>
24674
24675            <!-- back button -->
24676            <div 
24677                class="native-tab-bar-floating-back-button boxShadowBottom moreBlur pointer easeFast"
24678                @click="tapped_back()"
24679                :class="{ 'expanded': back_button_expanded }"
24680            >
24681                <div class="centeredText centeredBlock">
24682                    <p><i class="fa-regular fa-chevron-left native-tab-bar-icon" style="font-size:20px;"></i><span v-if="back_button_expanded" style="padding-left:6px;font-size:18px;">Back</span></p>
24683                </div>
24684            </div>
24685
24686        </div>
24687
24688    </template>
24689    
24690</script>
24690
24691
24692
24693<style>
24694
24695    .native-tab-bar-outer{
24696        position: fixed;
24697        bottom: 0px;   
24698        padding-bottom: max(0px, calc(var(--cs-safe-area-bottom, 0px) - 10px));
24699        left: 0px;
24700        right: 0px;
24701        background-color: rgba(249, 251, 251, 1);
24702        border-top: 1px solid #e4ebf1;
24703        z-index: 1000;
24704        transition: 0.15s cubic-bezier(0.25, 0.1, 0.25, 1);
24705    }
24706
24707    .native-tab-bar-padding-bottom {
24708        padding-bottom: var(--native-tab-bar-padding-bottom, 0px);
24709    }
24710
24711    /* In admin preview mode this is just a sample, so keep z-index low and let real UI layers sit above it. */
24712    .native-tab-bar-outer.preview{
24713        position: relative;
24714        z-index: 1;
24715    }
24716
24717    .native-tab-bar-item{
24718        padding: 16px 11px;
24719        display: flex;
24720        flex: 1 1 0; /* flex-grow: 1; flex-shrink: 1; flex-basis: 0; */
24721        color: #697684;
24722        cursor: pointer;
24723    }
24724    .native-tab-bar-item.active{
24725        color: #4a56f8;
24726    }
24727    .native-tab-bar-label{
24728        font-size: 12px;
24729        padding-top: 4px;
24730        white-space: nowrap;
24731    }
24732    .native-tab-bar-icon{
24733        font-size: 22px;
24734    }
24735    .native-tab-bar-avatar{
24736        width: 23px;
24737        height: 23px;
24738        border-radius: 100%;
24739        background-size: cover;
24740        background-position: center center;
24741        background-repeat: no-repeat;
24742        display: inline-block;
24743        vertical-align: middle;
24744    }
24745
24746    /*------  back button  -------*/
24747
24748    .native-tab-bar-back-button-overlay{
24749        position: fixed;
24750        top: 0;
24751        left: 0;
24752        width: 100%;
24753        height: 100%;
24754        /*
24755        background-color: rgba(111,117,122,0.5);
24756        */
24757        background-color: rgba(249, 251, 251, 0.8);
24758        opacity: 0;
24759        visibility: hidden;
24760        transition: opacity 0.15s ease;
24761        z-index: 999;
24762    }
24763
24764    .native-tab-bar-back-button-overlay.active{
24765        opacity: 1;
24766        visibility: visible;
24767    }
24768
24769    .native-tab-bar-floating-back-button{
24770        position: fixed;
24771        bottom: 22px;
24772        left: 22px;
24773        right: auto;
24774        z-index: 1000;
24775        transition: 0.15s cubic-bezier(0.25, 0.1, 0.25, 1);
24776        border-radius: 100%;
24777        width: 50px;
24778        height: 50px;
24779        background-color: rgba(255, 255, 255, 0.28);
24780        backdrop-filter: blur(15px);
24781        -webkit-backdrop-filter: blur(15px);
24782        line-height: 50px;
24783        font-size: 16px;
24784        white-space: nowrap;
24785        min-width: 50px;
24786    }
24787
24788    .native-tab-bar-floating-back-button.expanded{
24789        border-radius: 25px;
24790        padding-left: 16px;
24791        padding-right: 16px;
24792        width: auto;
24793    }
24794
24795    /*------  home button  -------*/
24796
24797    .native-tab-bar-floating-home-button.active{
24798        border-radius: 25px;
24799        padding-left: 16px;
24800        padding-right: 16px;
24801        width: auto;
24802        visibility: visible;
24803        opacity: 1;
24804        animation: expand 0.15s cubic-bezier(0.25, 0.1, 0.25, 1) forwards;
24805        background-color: #4a56f8;
24806        color: #fff;
24807    }
24808
24809    .native-tab-bar-floating-home-button{
24810        position: fixed;
24811        bottom: 82px;
24812        left: 22px;
24813        right: auto;
24814        visibility: hidden;
24815        opacity: 0;
24816        background-color: rgba(249, 251, 251, 1);
24817        /* border-top: 1px solid #e4ebf1; */
24818        z-index: 1000;
24819        transition: 0.15s cubic-bezier(0.25, 0.1, 0.25, 1);
24820        border-radius: 20px;
24821        width: auto;
24822        height: 50px;
24823        background-color: #fff;
24824        line-height: 50px;
24825        font-size: 16px;
24826        white-space: nowrap;
24827
24828        /* Initial state for animation */
24829        transform: scale(0);
24830        animation: none;
24831    }
24832
24833    /* Keyframes for the expansion effect */
24834    @keyframes expand {
24835        from {
24836            transform: scale(0);
24837            opacity: 0;
24838        }
24839        to {
24840            transform: scale(1);
24841            opacity: 1;
24842        }
24843    }
24844
24845    /* ── Floating glass tab bar (club XVgOrNASDY only) ─────────────────── */
24846
24847    .mp-float-tab-bar {
24848        position: fixed;
24849        /* sit above the safe-area bottom edge with a small gap */
24850        bottom: max(12px, var(--cs-safe-area-bottom, 12px));
24851        /* center the pill horizontally; max-width keeps it compact on tablets */
24852        left: 0;
24853        right: 0;
24854        margin-left: auto;
24855        margin-right: auto;
24856        max-width: 360px;
24857        width: calc(100% - 48px);
24858        /* light frosted glass pill */
24859        border-radius: 30px;
24860        background: rgba(255, 255, 255, 0.28);
24861        backdrop-filter: blur(15px);
24862        -webkit-backdrop-filter: blur(15px);
24863        border: 1px solid rgba(0, 0, 0, 0.06);
24864        box-shadow: 0 4px 28px rgba(0, 0, 0, 0.12);
24865        /* layout */
24866        display: flex;
24867        align-items: center;
24868        z-index: 1000;
24869        overflow: hidden;
24870    }
24871
24872    .mp-float-tab-bar--preview {
24873        position: relative;
24874        bottom: auto;
24875        margin: 12px auto;
24876        /* In preview mode we do not want the production-level z-index to overlap the left menu peek. */
24877        z-index: 1;
24878    }
24879
24880    .mp-float-tab-bar--settings-preview {
24881        margin: 0;
24882    }
24883
24884    .mp-float-tab-item {
24885        flex: 1 1 0;
24886        display: flex;
24887        align-items: center;
24888        justify-content: center;
24889        position: relative;
24890        padding: 18px 0;
24891        cursor: pointer;
24892        /* tap highlight off */
24893        -webkit-tap-highlight-color: transparent;
24894    }
24895
24896    /* Single sliding active pill — left and width are driven by float_pill_style computed */
24897    .mp-float-tab-pill {
24898        position: absolute;
24899        top: 7px;
24900        bottom: 7px;
24901        /* left + width set inline; transition slides the pill between tabs */
24902        border-radius: 22px;
24903        background: rgba(0, 0, 0, 0.08);
24904        pointer-events: none;
24905        transition: left 0.28s cubic-bezier(0.25, 0.1, 0.25, 1),
24906                    width 0.28s cubic-bezier(0.25, 0.1, 0.25, 1);
24907    }
24908
24909    .mp-float-tab-icon {
24910        font-size: 22px;
24911        color: rgba(0, 0, 0, 0.7);
24912        position: relative; /* above the pill */
24913        z-index: 1;
24914    }
24915
24916    .mp-float-tab-avatar {
24917        width: 26px;
24918        height: 26px;
24919        border-radius: 50%;
24920        background-size: cover;
24921        background-position: center;
24922        background-repeat: no-repeat;
24923        display: inline-block;
24924        position: relative;
24925        z-index: 1;
24926        opacity: 1;
24927        box-shadow: 0 0 0 2px transparent;
24928        transition: box-shadow 0.2s ease;
24929    }
24930
24931    .mp-float-tab-avatar--active {
24932        box-shadow: 0 0 0 2px #fff;
24933    }
24934
24935</style>
24936
24937<script>
24938
24939    let native_tab_bar_config = {
24940        template: "#template-native-tab-bar",
24941        data() {
24942            return {
24943                back_button_expanded: false
24944            }
24945        },
24946        props: {
24947            primary_view: String,
24948            tabs: Array, 
24949            hide_back_button: Boolean,
24950            is_admin_preview: {
24951                type: Boolean,
24952                default: false
24953            },
24954            preview_colors: {
24955                type: Object,
24956                default: null
24957            },
24958            preview_tab_bar_style: {
24959                type: String,
24960                default: null
24961            },
24962            member_portal_config: {
24963                type: Object,
24964                default: null
24965            },
24966            settings_preview_mode: {
24967                type: Boolean,
24968                default: false
24969            },
24970            is_member_portal: {
24971                type: Boolean,
24972                default: false
24973            }
24974        },
24975        mounted(){
24976            console.log("Mounted native tab bar");
24977        },
24978        methods: {
24979            tab_is_active( tab ){
24980                if( this.primary_view ){
24981                    return tab.key === this.primary_view;
24982                }
24983                return false;
24984            },
24985            tapped_home(){
24986                if(this.is_admin_preview){
24987                    return;
24988                }
24989                if( this.is_member_portal ){
24990                    this.back_button_expanded = false;
24991                    updateURL('home');
24992                    displayViewForUrlPath();
24993                }else{
24994                    window.location.href = '/account/home';
24995                }
24996            },
24997            tapped_more(){
24998                if(this.is_admin_preview){
24999                    return;
25000                }
25001                toggle_left_menu();
25002            },
25003            tapped_tab_bar_item( tab ){
25004                if(this.is_admin_preview){
25005                    return;
25006                }
25007                if( tab && tab.type === 'custom' && tab.link ){
25008                    window.location.href = tab.link;
25009                    return;
25010                }
25011                updateURL( tab.key );
25012                displayViewForUrlPath();
25013            },
25014            tapped_back(){
25015                if(this.is_admin_preview){
25016                    return;
25017                }
25018                if( this.back_button_expanded ){
25019                    this.back_button_expanded = false;
25020                    window.history.back();
25021                } else {
25022                    this.back_button_expanded = true;
25023                }
25024            },
25025            tapped_back_immediate(){
25026                if(this.is_admin_preview){
25027                    return;
25028                }
25029                window.history.back();
25030            },
25031            float_icon_class( tab, is_active ){
25032                if (!tab || !tab.icon_class) {
25033                    return '';
25034                }
25035                if (is_active) {
25036                    return tab.icon_class.replace('fa-regular', 'fa-solid');
25037                }
25038                return tab.icon_class;
25039            },
25040            show_member_details_avatar( tab ){
25041                if( ! tab ){
25042                    return false;
25043                }
25044                //only show if its the account tab AND we have an avatar url
25045                return tab.key === "settings" && !!this.member_tab_avatar_url;
25046            },
25047        },
25048        computed: {
25049            member_tab_avatar_url(){
25050                const current_user = Parse.User.current();
25051                if( ! current_user ){
25052                    return null;
25053                }
25054                const member_object = current_user.get('memberObject');
25055                if( member_object && member_object.get('profPicURL') ){
25056                    return member_object.get('profPicURL');
25057                }
25058                if( current_user.get('profPicURL') ){
25059                    return current_user.get('profPicURL');
25060                }
25061                return null;
25062            },
25063            tab_bar_colors(){
25064                if( this.preview_colors ){
25065                    return this.preview_colors;
25066                }
25067                //this is  only supported in newer versions of the app (where we have the native_app_props set so we can calculate padding-bottom on the safe area)
25068                if( ! store.native_app_props || ! store.native_app_props.safeAreaInsets ){
25069                    if(!this.is_admin_preview){
25070                        return {};
25071                    }
25072                }
25073                //ok, it's a newer version of the app!
25074                if( store.clubObject && store.clubObject.get('web_settings').get('tab_bar_colors') ){
25075                    //validate the config
25076                    let tab_bar_colors = store.clubObject.get('web_settings').get('tab_bar_colors');
25077                    if( 
25078                        tab_bar_colors.background && 
25079                        tab_bar_colors.text && 
25080                        tab_bar_colors.active 
25081                    ){
25082                        return tab_bar_colors;
25083                    }
25084                }
25085                return {};
25086            },
25087            //we only show it when we're on one of the relevant primary views (contained in the tab bar)
25088            //.. otherwise we should display the back button bar
25089            tab_bar_keys(){
25090
25091                /*
25092                var keys = [];
25093                if( this.visible_tabs ){
25094                    this.visible_tabs.forEach(tab => {
25095                        keys.push( tab.key );
25096                    });
25097                }
25098                return keys;
25099                */
25100                
25101                let keys = [];
25102                if( this.tabs ){
25103                    this.tabs.forEach(tab => {
25104                        keys.push( tab.key );
25105                    });
25106                }
25107                return keys;
25108            
25109            },
25110            visible_tab_keys(){
25111                let keys = [];
25112                if( this.visible_tabs ){
25113                    this.visible_tabs.forEach(tab => {
25114                        keys.push( tab.key );
25115                    });
25116                }
25117                return keys;
25118            },
25119            more_is_active(){
25120                if( ! this.tab_bar_keys.includes( this.primary_view ) ){
25121                    return false;
25122                }
25123                //it's active if not in the visible tabs
25124                if( this.visible_tab_keys.includes( this.primary_view ) ){
25125                    return false;
25126                }
25127                return true;
25128            },
25129            show_more_section_tab_bar(){
25130                if( ! this.is_member_portal ){
25131                    return false;
25132                }
25133                if( this.primary_view === 'booking' && this.is_inside_booking_flow ){
25134                    return false;
25135                }
25136                if( ! this.tab_bar_keys.includes( this.primary_view ) ){
25137                    return false;
25138                }
25139                if( this.visible_tab_keys.includes( this.primary_view ) ){
25140                    return false;
25141                }
25142                return true;
25143            },
25144            show_tab_bar(){
25145                //1) hide the tab bar if a member has drilled into one ofthe in-portal booking flows (golf, courts, simulators,
25146                if( this.primary_view === 'booking' && this.is_inside_booking_flow ){
25147                    return false;
25148                }
25149                //2) see if it's a visible tab, else don't show
25150                if( this.visible_tab_keys.includes( this.primary_view ) ){
25151                    return true;
25152                }
25153                return false;
25154
25155
25156                /*
25157                // hide the tab bar once a member has drilled into one of
25158                // the in-portal booking flows (golf, courts, simulators,
25159                // group-classes, dining, staff-services, lodging). The hub
25160                // landing page itself has secondary_view == 'default', so
25161                // we keep the tab bar visible there.
25162                if( this.primary_view === 'booking' && this.is_inside_booking_flow ){
25163                    return false;
25164                }
25165                for (let t = 0; t < this.tab_bar_keys.length; t++) {
25166                    if( this.tab_bar_keys[t] === this.primary_view ){
25167                        return true
25168                    }   
25169                }
25170                return false;
25171                */
25172            },
25173            is_inside_booking_flow(){
25174                let secondary = store.secondary_view;
25175                if( ! secondary || secondary === 'default' ){
25176                    return false;
25177                }
25178                return true;
25179            },
25180            visible_tabs(){
25181                if( this.tabs ){
25182                    return this.tabs.slice(0,4)
25183                }
25184                return [];
25185            },
25186            show_back_button(){
25187                if( this.hide_back_button ){
25188                    return false;
25189                }
25190                if( this.show_more_section_tab_bar ){
25191                    return false;
25192                }
25193                //if we're on the home screen, show the back button
25194                if( ! this.show_tab_bar && window.history.length >= 1 ){
25195                    return true;
25196                }
25197                return false;
25198            },
25199            is_react_native_member_app(){
25200                return store.is_react_native_member_app;
25201            },
25202            use_floating_tab_bar(){
25203                // In the admin preview, respect the dropdown selection immediately.
25204                if( this.is_admin_preview ){
25205                    return this.preview_tab_bar_style === 'modern';
25206                }
25207                //end preview case
25208                if( this.member_portal_config ){
25209                    return this.member_portal_config.tab_bar_style === 'modern';
25210                }
25211                return false;
25212            },
25213            float_active_tab_index(){
25214                if (!this.visible_tabs) {
25215                    return 0;
25216                }
25217                for (let idx = 0; idx < this.visible_tabs.length; idx++) {
25218                    if (this.tab_is_active(this.visible_tabs[idx])) {
25219                        return idx;
25220                    }
25221                }
25222                // "More" is the last slot
25223                return this.visible_tabs.length;
25224            },
25225            float_pill_style(){
25226                let total = this.visible_tabs ? this.visible_tabs.length + 1 : 5;
25227                let idx = this.float_active_tab_index;
25228                // Each slot is (100% / total) wide. The pill is inset 8px on each side.
25229                return {
25230                    left: 'calc(' + idx + ' * (100% / ' + total + ') + 8px)',
25231                    width: 'calc(100% / ' + total + ' - 16px)',
25232                };
25233            },
25234            compact_float_pill_style(){
25235                // compact more-section bar is always 4 slots and selected tab is slot index 2
25236                let total = 4;
25237                let selected_tab_index = 2;
25238                return {
25239                    left: 'calc(' + selected_tab_index + ' * (100% / ' + total + ') + 8px)',
25240                    width: 'calc(100% / ' + total + ' - 16px)',
25241                };
25242            },
25243            compact_home_icon_class(){
25244                let home_tab = this.tabs.find(tab => tab.key === 'home');
25245                if( home_tab && home_tab.icon_class ){
25246                    return home_tab.icon_class;
25247                }
25248                return 'fa-regular fa-house';
25249            },
25250            compact_home_tab_label(){
25251                let home_tab = this.tabs.find(tab => tab.key === 'home');
25252                if( home_tab && home_tab.label ){
25253                    return home_tab.label;
25254                }
25255                return 'Home';
25256            },
25257            compact_selected_tab(){
25258                let current_tab = this.tabs.find(tab => tab.key === this.primary_view);
25259                if( current_tab ){
25260                    return current_tab;
25261                }
25262                return {
25263                    icon_class: 'fa-regular fa-circle',
25264                    label: 'Tab',
25265                };
25266            },
25267            compact_selected_tab_icon_class(){
25268                if( this.use_floating_tab_bar ){
25269                    return this.float_icon_class(this.compact_selected_tab, true);
25270                }
25271                if( this.compact_selected_tab && this.compact_selected_tab.icon_class ){
25272                    return this.compact_selected_tab.icon_class;
25273                }
25274                return 'fa-regular fa-circle';
25275            },
25276        },
25277    }
25278    app_content.component("native-tab-bar-component", native_tab_bar_config );
25279    app_overlays.component("native-tab-bar-component", native_tab_bar_config );
25280
25281</script>
25281</html>    
25282    
25282<script>
25283        //after we've included all the relevant views, mount the vue apps
25284        app_content.mount('#v-content');
25285        app_overlays.mount('#v-overlays');
25286
25287    </script>
25287
25288
25289    
25290
25291
25292</html>

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.