PageSourceSearch

https://www.selbstverstaendlich-vielfalt.de/wp-content/plugins/events-made-easy/js/eme.js?ver=3.2.15

js selbstverstaendlich-vielfalt.de collected 2026-10-02 18:33:17 UTC 39,832 bytes, 988 lines download raw bytes

1// namespace EME variables
2if (typeof window.EME === 'undefined') {
3    window.EME = {
4        $: (selector, context = document) => context.querySelector(selector),
5        $$: (selector, context = document) => Array.from(context.querySelectorAll(selector))
6    };
7}
8
9function eme_debounce(func, wait = 300) {
10    let timeout;
11    return function(...args) {
12        const context = this;
13        clearTimeout(timeout);
14        timeout = setTimeout(() => func.apply(context, args), wait);
15    };
16}
17
18function eme_htmlDecode(inputStr) {
19    const textarea = document.createElement('textarea');
20    textarea.innerHTML = inputStr;
21    return textarea.value;
22}
23
24function eme_getValue(element) {
25    if (!element) return null;
26    if (!element.multiple) return element.value;
27    return Array.from(element.selectedOptions).map(option => option.value);
28}
29
30function eme_getQueryParams(qs) {
31    qs = qs.split('+').join(' ');
32    let params = {}, tokens, re = /[?&]?([^=]+)=([^&]*)/g;
33    while ((tokens = re.exec(qs))) {
34        params[decodeURIComponent(tokens[1])] = decodeURIComponent(tokens[2]);
35    }
36    return params;
37}
38const $_GET = eme_getQueryParams(document.location.search);
39
40function eme_isFalsey(paramValue) {
41    return paramValue === undefined || paramValue === null || paramValue === '0' || paramValue === '';
42}
43
44function eme_toggle(el, show) {
45    if (el) el.classList.toggle('eme-hidden', !show);
46    //if (el) el.style.display = show ? '' : 'none';
47}
48
49function initSnapSelect(selector, options = {}) {
50    // Convert selector to elements array
51    const elements = typeof selector === 'string'
52        ? EME.$$(selector)
53        : [selector];
54
55    if (!elements.length) return [];
56
57    return Array.from(elements).map(el => {
58        // Guard: skip if already initialised
59        if (el.snapselectInstance) return el.snapselectInstance;
60
61        if (!el.id) {
62            el.id = 'snap-select-' + Math.random().toString(36).slice(2, 11);
63        }
64
65        const config = {
66            placeholder:    options.placeholder    || undefined,
67            defaultText:    options.defaultText    || emebasic.translate_optionselect,
68            liveSearch:     options.liveSearch     !== undefined ? options.liveSearch     : el.multiple ? true : undefined,
69            closeOnSelect:  options.closeOnSelect  !== undefined ? options.closeOnSelect  : undefined,
70            showClearButton:     options.showClearButton     !== undefined ? options.showClearButton     : undefined,
71        };
72
73        const instance = new SnapSelectClass(el, config);
74        el.snapselectInstance = instance;
75        return instance;
76    });
77}
78
79function initSnapSelectRemote(selector, options = {}) {
80    // Convert selector to elements array
81    const elements = typeof selector === 'string'
82        ? EME.$$(selector)
83        : [selector];
84
85    if (!elements.length) return [];
86
87    return Array.from(elements).map(el => {
88        // Guard: skip if already initialised
89        if (el.snapselectInstance) return el.snapselectInstance;
90
91        if (!el.id) {
92            el.id = 'snap-select-' + Math.random().toString(36).slice(2, 11);
93        }
94
95        const pagesize = options.pagesize || 30;
96        // Build the ajax.url function, incorporating any extra data
97        const config = {
98            placeholder:    options.placeholder    || undefined,
99            defaultText:    options.defaultText    || emebasic.translate_optionselect,
100            closeOnSelect:  !el.multiple,
101            showClearButton:     options.showClearButton !== undefined ? options.showClearButton : undefined,
102            onItemAdd:      typeof options.onItemAdd    === 'function' ? options.onItemAdd    : undefined,
103            onItemDelete:   typeof options.onItemDelete === 'function' ? options.onItemDelete : undefined,
104            ajax: {
105                // Allow caller to pass a custom url function (e.g. for state→country cascade)
106                url: options.url || emebasic.translate_ajax_url,
107                cache: options.cache !== undefined ? options.cache : undefined,
108                pagesize: pagesize,
109                data: options.data || {},
110                processResults: function(data, search, page) {
111                    // EME backend returns { TotalRecordCount, Records:[{id,text},...] }
112                    // or a plain array
113                    const records = Array.isArray(data)
114                        ? data
115                        : (data.Records || []);
116                    let hasMore;
117                    if (data.hasMore !== undefined) {
118                        hasMore = data.hasMore;
119                    } else {
120                        const total   = data.TotalRecordCount !== undefined
121                            ? data.TotalRecordCount
122                            : records.length;
123                        hasMore = total > page * pagesize;
124                        // the next if covers the case where all rows are returned in 1 go, not respecting paging
125                        if (data.TotalRecordCount !== undefined && records.length >= data.TotalRecordCount)
126                            hasMore = false;
127                    }
128                    return { results: records, hasMore };
129                },
130            }
131        };
132        const instance = new SnapSelectClass(el, config);
133
134        // Expose a .snapselect property on the element
135        el.snapselectInstance = instance;
136
137        return instance;
138    });
139}
140
141// Helper functions for class manipulation
142function eme_addClass(el, className) {
143    if (el.classList) {
144        el.classList.add(className);
145    } else {
146        el.className += ' ' + className;
147    }
148}
149
150function eme_removeClass(el, className) {
151    if (el.classList) {
152        el.classList.remove(className);
153    } else {
154        el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
155    }
156}
157
158function eme_hasClass(el, className) {
159    return el.classList ? el.classList.contains(className) : new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className);
160}
161
162function eme_lastname_clearable() {
163    const ln = EME.$('input[name=lastname]');
164    if (!ln) return;
165    
166    const fields = ['firstname', 'address1', 'address2', 'city', 'state', 'zip', 'country', 'email', 'phone','birthdate'];
167    if (ln.value == '') {
168        ln.readOnly = false;
169        eme_removeClass(ln, 'clearable');
170        eme_removeClass(ln, 'x');
171        fields.forEach(f => {
172            const field = EME.$(`input[name=${f}]`);
173            if (field) {
174                field.value = '';
175                field.readOnly = false;
176            }
177        });
178        const birthfield = EME.$('input#birthdate');
179        if (birthfield) {
180            birthfield.value = '';
181        }
182        const wpIdField = EME.$('input[name=wp_id]');
183        const personIdField = EME.$('input[name=person_id]');
184        if (wpIdField) wpIdField.value = '';
185        if (personIdField) personIdField.value = '';
186    }
187    if (ln.value != '') {
188        eme_addClass(ln, 'clearable');
189        eme_addClass(ln, 'x');
190    }
191}
192
193// --- Widget Initialization ---
194function eme_init_widgets(dynamicOnly = false) {
195    const dynamicSelector = dynamicOnly ? '.dynamicfield' : '';
196
197    // Initialize fdatepicker for datetime fields
198    EME.$$('.eme_formfield_fdatetime' + dynamicSelector).forEach(el => {
199        if (typeof FDatepicker !== 'undefined') {
200            new FDatepicker(el, {
201                todayButton: true,
202                clearButton: true,
203                closeButton: true,
204                timepicker: true,
205                minutesStep: parseInt(emebasic.translate_minutesStep),
206                firstDayOfWeek: parseInt(emebasic.translate_firstDayOfWeek),
207                altFormat: 'Y-m-d H:i:00',
208                multipleSeparator: ", ",
209                format: emebasic.translate_fdatetimeformat,
210            });
211        } else {
212            el.removeAttribute('readonly');
213            el.type = 'datetime-local';
214        }
215    });
216
217    // Initialize fdatepicker for date fields
218    EME.$$('.eme_formfield_fdate' + dynamicSelector).forEach(el => {
219        if (typeof FDatepicker !== 'undefined') {
220            new FDatepicker(el, {
221                todayButton: true,
222                clearButton: true,
223                closeButton: true,
224                autoClose: true,
225                firstDayOfWeek: parseInt(emebasic.translate_firstDayOfWeek),
226                altFormat: 'Y-m-d',
227                multipleSeparator: ", ",
228                format: emebasic.translate_fdateformat
229            });
230        } else {
231            el.removeAttribute('readonly');
232            el.type = 'date';
233        }
234    });
235
236    // Initialize fdatepicker for time fields
237    EME.$$('.eme_formfield_ftime' + dynamicSelector).forEach(el => {
238        if (typeof FDatepicker !== 'undefined') {
239            new FDatepicker(el, {
240                timepicker: true,
241                timeOnly: true,
242                clearButton: true,
243                closeButton: true,
244                minutesStep: parseInt(emebasic.translate_minutesStep),
245                altFormat: 'H:i:00',
246                format: emebasic.translate_ftimeformat
247            });
248        } else {
249            el.removeAttribute('readonly');
250            el.type = 'time';
251        }
252    });
253
254    initSnapSelect('select.eme_snapselect' + dynamicSelector);
255    initSnapSelect('select.eme_snapselect_allow_empty' + dynamicSelector, {
256        showClearButton: true
257    });
258    initSnapSelectRemote('select.eme_snapselect_country_class' + dynamicSelector, {
259        placeholder: emebasic.translate_selectcountry,
260        data: {
261            action: 'eme_select_country',
262            eme_frontend_nonce: emebasic.translate_frontendnonce
263        },
264        cache: true,
265        showClearButton: true,
266        // When country changes, reset the state field in the same form
267        onItemAdd: function(value, text) {
268            const form       = this.closest('form');
269            const stateField = form?.querySelector('.eme_snapselect_state_class');
270            if (stateField && stateField.snapselectInstance) {
271                stateField.snapselectInstance.clear();
272                stateField.snapselectInstance.clearCache();
273            }
274        },
275        onItemDelete: function(value, text) {
276            const form       = this.closest('form');
277            const stateField = form?.querySelector('.eme_snapselect_state_class');
278            if (stateField && stateField.snapselectInstance) {
279                stateField.snapselectInstance.clear();
280                stateField.snapselectInstance.clearCache();
281            }
282        }
283    });
284
285    initSnapSelectRemote('select.eme_snapselect_state_class' + dynamicSelector, {
286        placeholder: emebasic.translate_selectstate,
287        showClearButton: true,
288        // Dynamically include the currently selected country_code in every request
289        data: function(search, page) { // since this is a function, no caching happens
290            const stateEl     = document.querySelector('select.eme_snapselect_state_class');
291            const form        = stateEl?.closest('form');
292            const countryCode = form?.querySelector('[name=country_code]')?.value || '';
293            return {
294                action: 'eme_select_state',
295                eme_frontend_nonce: emebasic.translate_frontendnonce,
296                country_code: countryCode
297            };
298        },
299        cache: true // we set the cache true and clear it in the country select if needed
300    });
301}
302
303// function to execute the injected javascript via ajax to force payment button click
304function eme_executeScriptsInElement(element) {
305    const scripts = element.querySelectorAll('script');
306    scripts.forEach(oldScript => {
307        const newScript = document.createElement('script');
308
309        // Copy all attributes from old script to new script
310        Array.from(oldScript.attributes).forEach(attr => {
311            newScript.setAttribute(attr.name, attr.value);
312        });
313
314        // Copy the script content
315        newScript.textContent = oldScript.textContent;
316
317        // Replace the old script with the new one to trigger execution
318        oldScript.parentNode.replaceChild(newScript, oldScript);
319    });
320}
321
322// --- Unified AJAX Handler for Booking/Member/Generic forms ---
323function eme_ajax_form(form_id, action, okSel, errSel, submitBtn, extraParams = {}) {
324    const form = document.getElementById(form_id);
325    if (!form) return;
326    const okEl = EME.$(okSel);
327    const errEl = EME.$(errSel);
328
329    const btn = submitBtn || form.querySelector('[type="submit"]');
330    const spinner = document.createElement('span');
331    spinner.className = 'spinner';
332    spinner.innerHTML = '⟷';
333    if (btn) {
334        btn.dataset.emeOrigText = btn.value || btn.textContent;
335        btn.disabled = true;
336        if (btn.tagName === 'INPUT') {
337            btn.value = emebasic.translate_pleasewait;
338        } else {
339            btn.textContent = emebasic.translate_pleasewait;
340        }
341        btn.parentNode.insertBefore(spinner, btn.nextSibling);
342    }
343    const restoreBtn = () => {
344        if (btn) {
345            btn.disabled = false;
346            if (btn.tagName === 'INPUT') {
347                btn.value = btn.dataset.emeOrigText;
348            } else {
349                btn.textContent = btn.dataset.emeOrigText;
350            }
351        }
352        if (spinner && spinner.parentNode) spinner.parentNode.removeChild(spinner);
353    };
354
355    let alldata = new FormData(form);
356    alldata.append('action', action);
357    Object.entries(extraParams).forEach(([k, v]) => alldata.append(k, v));
358
359    fetch(emebasic.translate_ajax_url, {
360        method: 'POST',
361        body: alldata
362    })
363        .then(response => response.json())
364        .then(data => {
365            restoreBtn();
366
367            if (data.Result === "OK") {
368                if (errSel) {
369                    if (errEl) eme_toggle(errEl, false);
370                }
371                if (okSel) {
372                    if (okEl && data.htmlmessage && (!data.redirect || ( data.redirect && parseInt(data.waitperiod)>0 ) ) ) {
373                        okEl.innerHTML = data.htmlmessage;
374                        eme_toggle(okEl, true);
375                    }
376                }
377                if (parseInt(data.keep_form) == 1) {
378                    form.reset();
379                    eme_refresh_captcha(form_id);
380                } else {
381                    const parentDiv = form.closest('[id^=div_eme-]');
382                    if (parentDiv) eme_toggle(parentDiv, false);
383                }
384                if (data.paymentform) {
385                    const paymentDiv = EME.$(`#div_eme-payment-form-${form_id}`);
386                    if (paymentDiv && data.paymentform) {
387                        paymentDiv.innerHTML = data.paymentform;
388                        eme_toggle(paymentDiv, true);
389                        eme_executeScriptsInElement(paymentDiv);
390                    }
391                }
392                if (data.redirect) {
393                    setTimeout(() => window.location.href = data.redirect, parseInt(data.waitperiod));
394                }
395                eme_scrollToEl(okEl);
396            } else {
397                if (errSel) {
398                    if (errEl && data.htmlmessage) {
399                        errEl.innerHTML = data.htmlmessage;
400                        eme_toggle(errEl, true);
401                    }
402                }
403                if (okSel) {
404                    if (okEl) eme_toggle(okEl, false);
405                }
406                eme_scrollToEl(errEl);
407            }
408        })
409        .catch(error => {
410            restoreBtn();
411            if (errSel) {
412                if (errEl) {
413                    errEl.innerHTML = emebasic.translate_error + (error?.message ? '<br>' + error.message : '');
414                    eme_toggle(errEl, true);
415                }
416            }
417            if (okSel) {
418                if (okEl) eme_toggle(okEl, false);
419            }
420            eme_scrollToEl(errEl);
421        });
422}
423
424function eme_refresh_captcha(form_id) {
425    const form = document.getElementById(form_id);
426    if (!form) return;
427
428    const captcha = form.querySelector('.eme-captcha-img');
429    if (captcha) {
430        const src = captcha.src.replace(/&ts=[^&]*/, '');
431        captcha.src = `${src}&ts=${Date.now()}`;
432    }
433}
434
435function eme_scrollToEl(sel) {
436    if (sel) {
437        sel.scrollIntoView({ behavior: 'smooth', block: 'center' });
438        //const rect = sel.getBoundingClientRect();
439        //const scrollTop = window.scrollY + rect.top - (window.innerHeight / 2) + (rect.height / 2);
440        //window.scrollTo({ top: scrollTop, behavior: 'smooth' });
441    }
442}
443
444// --- Unified Dynamic Data/Price AJAX ---
445function eme_dynamic_price_json(form_id, isBooking = true) {
446    const form = document.getElementById(form_id);
447    if (!form) return;
448
449    form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, false));
450    let alldata = new FormData(form);
451
452    const priceSpans = isBooking
453        ? [{ sel: 'span#eme_calc_bookingprice', action: 'eme_calc_bookingprice' }, { sel: 'span#eme_calc_bookingprice_detail', action: 'eme_calc_bookingprice_detail' }]
454        : [{ sel: 'span#eme_calc_memberprice', action: 'eme_calc_memberprice' }, { sel: 'span#eme_calc_memberprice_detail', action: 'eme_calc_memberprice_detail' }];
455
456    let found = false;
457    priceSpans.forEach(({ sel, action }) => {
458        const span = form.querySelector(sel);
459        if (span) {
460            found = true;
461            span.innerHTML = '<span class="spinner">⟳</span>';
462            alldata.set('action', action);
463            alldata.set('eme_frontend_nonce', emebasic.translate_frontendnonce);
464
465            fetch(emebasic.translate_ajax_url, {
466                method: 'POST',
467                body: alldata
468            })
469                .then(response => response.json())
470                .then(data => {
471                    form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
472                    span.innerHTML = data.total;
473                })
474                .catch(() => {
475                    form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
476                    span.innerHTML = 'Invalid reply';
477                });
478        }
479    });
480
481    if (!found) {
482        form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
483    }
484}
485
486function eme_dynamic_data_json(form_id, isBooking = true) {
487    const form = document.getElementById(form_id);
488    if (!form) return;
489
490    form.querySelectorAll('input[type="submit"], button[type="submit"]').forEach(btn => eme_toggle(btn, false));
491    let alldata = new FormData(form);
492
493    const dataDivSel = 'div#eme_dyndata';
494    const action = isBooking ? 'eme_dyndata_rsvp' : 'eme_dyndata_member';
495    const dataDiv = form.querySelector(dataDivSel);
496
497    if (dataDiv) {
498        dataDiv.innerHTML = '<span class="spinner">⟳</span>';
499        alldata.set('action', action);
500        alldata.set('eme_frontend_nonce', emebasic.translate_frontendnonce);
501
502        fetch(emebasic.translate_ajax_url, {
503            method: 'POST',
504            body: alldata
505        })
506            .then(response => response.json())
507            .then(data => {
508                form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
509                dataDiv.innerHTML = data.Result;
510                eme_init_widgets(true);
511                eme_dynamic_price_json(form_id, isBooking);
512            })
513            .catch(() => {
514                form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
515            });
516    } else {
517        eme_dynamic_price_json(form_id, isBooking);
518    }
519}
520
521function eme_dynamic_familymemberdata_json(form_id) {
522    const form = document.getElementById(form_id);
523    if (!form) return;
524
525    form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, false));
526    let alldata = new FormData(form);
527    const dataDiv = form.querySelector('div#eme_dyndata_family');
528
529    if (dataDiv) {
530        dataDiv.innerHTML = '<span class="spinner">⟳</span>';
531        alldata.set('action', 'eme_dyndata_familymember');
532        alldata.set('eme_frontend_nonce', emebasic.translate_frontendnonce);
533
534        fetch(emebasic.translate_ajax_url, {
535            method: 'POST',
536            body: alldata
537        })
538            .then(response => response.json())
539            .then(data => {
540                form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
541                dataDiv.innerHTML = data.Result;
542                eme_init_widgets(true);
543            })
544            .catch(() => {
545                form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
546            });
547    } else {
548        form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
549    }
550}
551
552// Calendar navigation
553function loadCalendar(
554    tableDiv, fullcalendar = 0, htmltable, htmldiv, showlong_events = 0, month = 0,
555    year = 0, cat_chosen = '', author_chosen = '', contact_person_chosen = '',
556    location_chosen = '', not_cat_chosen = '', template_chosen = 0, holiday_chosen = 0,
557    weekdays = ''
558) {
559    const formData = new FormData();
560    formData.append('eme_frontend_nonce', emebasic.translate_frontendnonce);
561    formData.append('action', 'eme_calendar');
562    formData.append('calmonth', parseInt(month, 10));
563    formData.append('calyear', parseInt(year, 10));
564    formData.append('full', fullcalendar);
565    formData.append('long_events', showlong_events);
566    formData.append('htmltable', htmltable);
567    formData.append('htmldiv', htmldiv);
568    formData.append('category', cat_chosen);
569    formData.append('notcategory', not_cat_chosen);
570    formData.append('author', author_chosen);
571    formData.append('contact_person', contact_person_chosen);
572    formData.append('location_id', location_chosen);
573    formData.append('template_id', template_chosen);
574    formData.append('holiday_id', holiday_chosen);
575    formData.append('weekdays', weekdays);
576
577    fetch(emebasic.translate_ajax_url, {
578        method: 'POST',
579        body: formData
580    })
581        .then(response => response.text())
582        .then(data => {
583            const tableEl = document.getElementById(tableDiv);
584            if (tableEl) {
585                tableEl.outerHTML = data;
586                // Re-attach event handlers after DOM replacement
587                attachCalendarHandlers();
588            }
589        });
590}
591
592function attachCalendarHandlers() {
593    EME.$$('a.eme-cal-prev-month, a.eme-cal-next-month').forEach(link => {
594        link.addEventListener('click', function(e) {
595            e.preventDefault();
596            this.innerHTML = '<span class="spinner">⟳</span>';
597            loadCalendar(
598                this.dataset.calendar_divid,
599                this.dataset.full,
600                this.dataset.htmltable,
601                this.dataset.htmldiv,
602                this.dataset.long_events,
603                this.dataset.month,
604                this.dataset.year,
605                this.dataset.category,
606                this.dataset.author,
607                this.dataset.contact_person,
608                this.dataset.location_id,
609                this.dataset.notcategory,
610                this.dataset.template_id,
611                this.dataset.holiday_id,
612                this.dataset.weekdays
613            );
614        });
615    });
616}
617
618// --- Booking/Member form handlers with confirmation ---
619function eme_handle_massmail(form_id, callback) {
620    const form = document.getElementById(form_id);
621    const massmailField = form.querySelector('#massmail');
622    const massmailDialog = EME.$('#MassMailDialog');
623
624    if (massmailField && massmailField.value != 1 && massmailDialog) {
625        massmailDialog.showModal();
626
627        const confirmBtn = EME.$('#dialog-confirm');
628        const cancelBtn = EME.$('#dialog-cancel');
629
630        if (confirmBtn) {
631            confirmBtn.addEventListener('click', function(e) {
632                e.preventDefault();
633                massmailDialog.close();
634                callback(form_id);
635            }, { once: true });
636        }
637
638        if (cancelBtn) {
639            cancelBtn.addEventListener('click', function(e) {
640                e.preventDefault();
641                massmailDialog.close();
642            }, { once: true });
643        }
644    } else {
645        callback(form_id);
646    }
647}
648
649// --- Dynamic fields AJAX debounce handlers ---
650function eme_attach_dynamic_handlers(selector, isBooking) {
651    EME.$$(selector).forEach(form => {
652        const form_id = form.id;
653        const debounced_data = eme_debounce(() => eme_dynamic_data_json(form_id, isBooking), 500);
654        const debounced_price = eme_debounce(() => eme_dynamic_price_json(form_id, isBooking), 500);
655        const debounced_family = !isBooking ? eme_debounce(() => eme_dynamic_familymemberdata_json(form_id), 500) : null;
656
657        form.addEventListener('input', function(event) {
658            // Ignore events from hidden fields (e.g. FDatepicker auto-created altField);
659            // those are internal implementation details and should not trigger a dynamic refresh.
660            if (event.target.type === 'hidden') {
661                return;
662            }
663            if (debounced_family && event.target.id === 'familycount') {
664                debounced_family();
665            }
666            if (eme_hasClass(event.target, 'nodynamicupdates') || event.target.closest('fieldset.nodynamicupdates')) {
667                if (eme_hasClass(event.target, 'dynamicprice')) {
668                    form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, false));
669                    debounced_price();
670                }
671                return;
672            }
673            form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, false));
674            debounced_data();
675        });
676
677        if (debounced_family) debounced_family();
678        debounced_data();
679    });
680}
681    
682function eme_scrollToInvalidInput(el) { 
683    // open details if closed
684    const detailsParent = el.closest('.eme_accordion');
685    if (detailsParent && !detailsParent.hasAttribute('open')) {
686        // Open it
687        detailsParent.setAttribute('open', 'true');
688        // Find the content div after summary
689        const summary = detailsParent.querySelector('summary');
690        if (summary) {
691            const contentDiv = detailsParent.querySelector('summary + div');
692            if (contentDiv) {
693                eme_toggle(contentDiv, true);
694                contentDiv.style.height = ''; // reset height
695            }
696        }
697    }
698
699    // First check if field is in a tab
700    const tabPane = el.closest('.eme-tab-content');
701    if (tabPane) {
702        const tabId = tabPane.id;
703        const isVisible = tabPane.classList.contains('active') ||
704            window.getComputedStyle(tabPane).display !== 'none';
705
706        if (!isVisible && typeof eme_activateTab === 'function') {
707            // Activate the tab containing the invalid field
708            eme_activateTab(tabId);
709            // Small delay to ensure tab is visible before scrolling
710            setTimeout(() => {
711                if (typeof el.reportValidity === 'function') {
712                    el.reportValidity();
713                } else {
714                    eme_addClass(el, 'eme_required');
715                    eme_scrollToEl(el);
716                }
717            }, 300);
718            return; // Exit, scrolling happens after timeout
719        }
720    }
721
722    // If not in tab or tab is already active, scroll normally
723    if (typeof el.reportValidity === 'function') {
724        el.reportValidity();
725    } else {
726        eme_addClass(el, 'eme_required');
727        eme_scrollToEl(el);
728    }
729}
730
731document.addEventListener('DOMContentLoaded', function() {
732    // Show elements that should be visible with JS
733    EME.$$('.eme-showifjs').forEach(el => {
734        eme_toggle(el, true);
735    });
736
737    
738    // Initial calendar handler attachment
739    attachCalendarHandlers();
740
741    // Clearable input with 'x'
742    document.addEventListener('input', function(e) {
743        if (eme_hasClass(e.target, 'clearable')) {
744            if (e.target.value) {
745                eme_addClass(e.target, 'x');
746            } else {
747                eme_removeClass(e.target, 'x');
748            }
749        }
750    });
751
752    document.addEventListener('mousemove', function(e) {
753        if (eme_hasClass(e.target, 'x')) {
754            const rect = e.target.getBoundingClientRect();
755            if (e.target.offsetWidth - 18 < e.clientX - rect.left) {
756                eme_addClass(e.target, 'onX');
757            } else {
758                eme_removeClass(e.target, 'onX');
759            }
760        }
761    });
762
763    document.addEventListener('click', function(e) {
764        if (eme_hasClass(e.target, 'onX')) {
765            e.preventDefault();
766            eme_removeClass(e.target, 'x');
767            eme_removeClass(e.target, 'onX');
768            e.target.value = '';
769            e.target.dispatchEvent(new Event('change'));
770        }
771    });
772
773    document.addEventListener('touchstart', function(e) {
774        if (eme_hasClass(e.target, 'onX')) {
775            e.preventDefault();
776            eme_removeClass(e.target, 'x');
777            eme_removeClass(e.target, 'onX');
778            e.target.value = '';
779            e.target.dispatchEvent(new Event('change'));
780        }
781    });
782
783    // Lastname clearable
784    const lastnameField = EME.$("input[name=lastname]");
785    if (lastnameField && lastnameField.dataset.clearable) {
786        lastnameField.addEventListener('change', eme_lastname_clearable);
787        eme_lastname_clearable();
788    }
789
790    // --- Generic forms ---
791    const genericForms = [
792        { sel: '[name=eme-cancel-payment-form]', action: 'eme_cancel_payment', ok: 'div#eme-cancel-payment-message-ok-', err: 'div#eme-cancel-payment-message-error-' },
793        { sel: '[name=eme-cancel-bookings-form]', action: 'eme_cancel_bookings', ok: 'div#eme-cancel-bookings-message-ok-', err: 'div#eme-cancel-bookings-message-error-' },
794        { sel: '[name=eme-subscribe-form]', action: 'eme_subscribe', ok: 'div#eme-subscribe-message-ok-', err: 'div#eme-subscribe-message-error-' },
795        { sel: '[name=eme-unsubscribe-form]', action: 'eme_unsubscribe', ok: 'div#eme-unsubscribe-message-ok-', err: 'div#eme-unsubscribe-message-error-' },
796        { sel: '[name=eme-rpi-form]', action: 'eme_rpi', ok: 'div#eme-rpi-message-ok-', err: 'div#eme-rpi-message-error-' },
797        { sel: '[name=eme-gdpr-approve-form]', action: 'eme_gdpr_approve', ok: 'div#eme-gdpr-approve-message-ok-', err: 'div#eme-gdpr-approve-message-error-' },
798        { sel: '[name=eme-cpi-request-form]', action: 'eme_cpi_request', ok: 'div#eme-cpi-request-message-ok-', err: 'div#eme-cpi-request-message-error-' },
799        { sel: '[name=eme-cpi-form]', action: 'eme_cpi', ok: 'div#eme-cpi-message-ok-', err: 'div#eme-cpi-message-error-' },
800        { sel: '[name=eme-tasks-form]', action: 'eme_tasks', ok: 'div#eme-tasks-message-ok-', err: 'div#eme-tasks-message-error-' },
801        { sel: '[name=eme-fs-form]', action: 'eme_frontend_submit', ok: 'div#eme-fs-message-ok-', err: 'div#eme-fs-message-error-', isFS: true }
802    ];
803    
804    genericForms.forEach(({ sel, action, ok, err, isFS }) => {
805        EME.$$(sel).forEach(form => {
806            form.addEventListener('submit', function(event) {
807                event.preventDefault();
808                const form_id = this.id;
809                if (isFS && emebasic.translate_htmleditor === "tinemce" && emebasic.translate_fs_wysiwyg === "true") {
810                    if (typeof tinymce !== "undefined") {
811                        const eventNotesEditor = tinymce.get('event_notes');
812                        const locationDescEditor = tinymce.get('location_description');
813                        if (eventNotesEditor) eventNotesEditor.save();
814                        if (locationDescEditor) locationDescEditor.save();
815                    }
816                }
817                eme_ajax_form(form_id, action, ok + form_id, err + form_id, event.submitter);
818            });
819        });
820    });
821
822    EME.$$('[name=eme-rsvp-form]').forEach(form => {
823        form.addEventListener('submit', function(event) {
824            event.preventDefault();
825            eme_handle_massmail(this.id, function(form_id) {
826                let extra = {};
827                ['eme_invite', 'eme_email', 'eme_ln', 'eme_fn'].forEach(k => { 
828                    if ($_GET[k]) extra[k] = $_GET[k]; 
829                });
830                eme_ajax_form(form_id, 'eme_add_bookings', 'div#eme-rsvp-addmessage-ok-' + form_id, 'div#eme-rsvp-addmessage-error-' + form_id, event.submitter, extra);
831                eme_dynamic_data_json(form_id, true);
832            });
833        });
834    });
835
836    EME.$$('[name=eme-member-form]').forEach(form => {
837        form.addEventListener('submit', function(event) {
838            event.preventDefault();
839            eme_handle_massmail(this.id, function(form_id) {
840                eme_ajax_form(form_id, 'eme_add_member', 'div#eme-member-addmessage-ok-' + form_id, 'div#eme-member-addmessage-error-' + form_id, event.submitter);
841                eme_dynamic_data_json(form_id, false);
842            });
843        });
844    });
845
846    // --- Validation for submit buttons ---
847    EME.$$('.eme_submit_button').forEach(btn => {
848        btn.addEventListener('click', function(event) {
849            let valid = true;
850            const parent_form_id = this.form.id;
851            
852            // Check required text inputs and date fields
853            EME.$$('input[type="text"]:not(:disabled)[required], input:not([type]):not(:disabled)[required], .eme_formfield_fdatetime:not(:disabled)[required], .eme_formfield_fdate:not(:disabled)[required]').forEach(input => {
854                if (valid && input.closest("form").id === parent_form_id) {
855                    const val = input.value;
856                    if (val.match(/^\s*$/)) {
857                        eme_scrollToInvalidInput(input);
858                        valid = false;
859                    } else {
860                        eme_removeClass(input, 'eme_required');
861                    }
862                }
863            });
864            
865            // Check required checkbox groups
866            EME.$$('.eme-checkbox-group-required').forEach(group => {
867                if (valid && group.closest("form").id === parent_form_id) {
868                    const checked = group.querySelectorAll('input[type="checkbox"]:checked').length;
869                    if (!checked) {
870                        eme_scrollToInvalidInput(group);
871                        valid = false;
872                    } else {
873                        eme_removeClass(group, 'eme_required');
874                    }
875                }
876            });
877            
878            if (!valid) {
879                event.preventDefault();
880                event.stopPropagation(); // optional, prevent fire of other click-listeners
881            }
882
883        });
884    });
885
886    eme_attach_dynamic_handlers('[name=eme-rsvp-form]', true);
887    eme_attach_dynamic_handlers('#eme-rsvp-adminform', true);
888    eme_attach_dynamic_handlers('[name=eme-member-form]', false);
889    eme_attach_dynamic_handlers('#eme-member-adminform', false);
890
891    // Remember-me functionality: only active if a form contains the #_REMEMBERME checkbox
892    const rememberMeForms = EME.$$('form').filter(form => form.querySelector('input#eme_rememberme'));
893    if (rememberMeForms.length) {
894        rememberMeForms.forEach(form => {
895            if (localStorage.getItem('eme_rememberme') == 1) {
896                const checkbox = form.querySelector('input#eme_rememberme');
897                if (checkbox) {
898                    checkbox.checked = true;
899                }
900            }
901            ['lastname', 'firstname', 'email', 'phone', 'task_lastname', 'task_firstname', 'task_email', 'task_phone'].forEach(fieldName => {
902                const field = form.querySelector(`input[name=${fieldName}]`);
903                if (field && field.value === '') {
904                    field.value = localStorage.getItem('eme_' + fieldName.replace('task_', '')) || '';
905                }
906            });
907            form.addEventListener('submit', function() {
908                const rememberCheckbox = this.querySelector('input#eme_rememberme');
909                if (rememberCheckbox && rememberCheckbox.checked) {
910                    localStorage.setItem('eme_rememberme', 1);
911                    ['lastname', 'firstname', 'email', 'phone', 'task_lastname', 'task_firstname', 'task_email', 'task_phone'].forEach(fieldName => {
912                        const field = this.querySelector(`input[name=${fieldName}]`);
913                        if (field) {
914                            localStorage.setItem('eme_' + fieldName.replace('task_', ''), field.value);
915                        }
916                    });
917                } else {
918                    ['eme_lastname', 'eme_firstname', 'eme_email', 'eme_phone', 'eme_rememberme'].forEach(storageKey => {
919                        localStorage.removeItem(storageKey);
920                    });
921                }
922            });
923        });
924    }
925
926    // Person image upload widget
927    const personImageButton = document.getElementById('eme_person_image_button');
928    if (personImageButton) {
929        const removeButton = document.getElementById('eme_person_remove_old_image');
930        const imageIdField = document.getElementById('eme_person_image_id');
931        const imageExample = document.getElementById('eme_person_image_example');
932        const currentImageDiv = document.getElementById('eme_person_current_image');
933        const noImageDiv = document.getElementById('eme_person_no_image');
934        
935        if (removeButton) {
936            removeButton.addEventListener('click', function() {
937                imageIdField.value = '';
938                imageExample.src = '';
939                eme_toggle(currentImageDiv, false);
940                eme_toggle(noImageDiv, true);
941                eme_toggle(removeButton, false);
942                personImageButton.value = emebasic.translate_chooseimg;
943            });
944        }
945        
946        personImageButton.addEventListener('click', function(e) {
947            e.preventDefault();
948            if (typeof wp !== 'undefined' && wp.media) {
949                const custom_uploader = wp.media({
950                    title: emebasic.translate_selectimg,
951                    button: { text: emebasic.translate_setimg },
952                    library: { type: 'image' },
953                    multiple: false
954                }).on('select', function() {
955                    const attachment = custom_uploader.state().get('selection').first().toJSON();
956                    imageIdField.value = attachment.id;
957                    imageExample.src = attachment.url;
958                    eme_toggle(currentImageDiv, true);
959                    eme_toggle(noImageDiv, false);
960                    eme_toggle(removeButton, true);
961                    personImageButton.value = emebasic.translate_replaceimg;
962                }).open();
963            }
964        });
965        
966        // Initialize display state
967        if (parseInt(imageIdField.value) > 0) {
968            eme_toggle(noImageDiv, false);
969            eme_toggle(currentImageDiv, true);
970            eme_toggle(removeButton, true);
971            personImageButton.value = emebasic.translate_replaceimg;
972        } else {
973            eme_toggle(noImageDiv, true);
974            eme_toggle(currentImageDiv, false);
975            eme_toggle(removeButton, false);
976            personImageButton.value = emebasic.translate_chooseimg;
977        }
978    }
979    
980    // Scroll to payment form if present
981    const paymentForm = document.getElementById('eme-payment-form');
982    if (paymentForm) {
983        eme_scrollToEl(paymentForm);
984    }
985
986    // Initialize widgets
987    eme_init_widgets();
988});

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.