PageSourceSearch

https://www.gottfrieding.de/wp-content/plugins/events-made-easy/js/eme.js?ver=3.2.21

js gottfrieding.de collected 2026-10-02 22:25:12 UTC 51,009 bytes, 1,260 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
49// --- generic, type aware form field helpers ---
50// setting field.value is wrong for checkboxes/radios (they use checked) and for
51// multi selects, so all EME code that fills or empties fields goes through here
52const EME_CHECKABLE_TYPES = ['checkbox', 'radio'];
53
54function eme_isCheckable(field) {
55    return !!field && EME_CHECKABLE_TYPES.includes(field.type);
56}
57
58function eme_setFieldValue(field, value) {
59    if (!field) return;
60    if (eme_isCheckable(field)) {
61        field.checked = (value === true || value === 1 || value === '1' || value === 'true');
62    } else if (field.multiple && field.options) {
63        const values = (Array.isArray(value) ? value : [value])
64            .map(v => (v === undefined || v === null) ? '' : String(v));
65        Array.from(field.options).forEach(option => {
66            option.selected = values.includes(option.value);
67        });
68    } else {
69        field.value = (value === undefined || value === null) ? '' : eme_htmlDecode(String(value));
70    }
71    eme_updateClearableIcon(field);
72}
73
74function eme_clearFieldValue(field) {
75    eme_setFieldValue(field, '');
76}
77
78// --- clearable inputs, showing an x to empty them ---
79// data-clearable marks the field. Its value picks the look: 'icon' only adds the x, the
80// default also gets the border and padding of .clearable
81function eme_clearableClassFor(field) {
82    if (!field || !field.classList) return null;
83    if (field.classList.contains('clearable')) return 'clearable';
84    if (field.classList.contains('eme-clearable')) return 'eme-clearable';
85    if (!field.dataset || !field.dataset.clearable) return null;
86    return field.dataset.clearable === 'icon' ? 'eme-clearable' : 'clearable';
87}
88
89function eme_isClearableField(field) {
90    return !!eme_clearableClassFor(field);
91}
92
93// filling a field from code (an autocomplete result for instance) fires no input event,
94// so the x has to be updated there too, else a locked field has no way to be emptied
95function eme_updateClearableIcon(field) {
96    const clearableClass = eme_clearableClassFor(field);
97    if (!clearableClass) return;
98    field.classList.toggle('x', !!field.value);
99}
100
101function eme_initClearableInputs() {
102    EME.$$('[data-clearable]').forEach(field => {
103        const clearableClass = eme_clearableClassFor(field);
104        if (clearableClass) field.classList.add(clearableClass);
105        eme_updateClearableIcon(field);
106    });
107}
108
109// The x only reacts after a mousemove over it, but a locked field can not be focused and a
110// touch has no mousemove at all, so also accept a click that lands on the icon itself.
111function eme_clearableHit(field, clientX) {
112    if (!field || !eme_hasClass(field, 'x') || !field.value) return false;
113    if (eme_hasClass(field, 'onX')) return true;
114    if (typeof clientX !== 'number' || clientX === 0) return false;
115    const rect = field.getBoundingClientRect();
116    return field.offsetWidth - 18 < clientX - rect.left;
117}
118
119// emptying the field fires change, so whatever listens to that can clean up (all the other
120// location fields for the location name for instance)
121function eme_clearFieldByIcon(field) {
122    if (!field || !field.value) return;
123    field.value = '';
124    eme_updateClearableIcon(field);
125    eme_removeClass(field, 'onX');
126    field.dispatchEvent(new Event('change'));
127}
128
129// Lock a field so the user can't change it, while the value is still submitted.
130// readOnly does nothing on checkboxes and selects, and disabled would drop the
131// value on submit, so checkables stay enabled and we just block the interaction
132function eme_lockField(field, locked = true) {
133    if (!field) return;
134    field.classList.toggle('eme-readonly', !!locked);
135    if (eme_isCheckable(field)) {
136        field.classList.toggle('eme-locked', !!locked);
137        if (locked && !field._emeLockGuard) {
138            field._emeLockGuard = (e) => {
139                e.preventDefault();
140                e.stopPropagation();
141            };
142            field.addEventListener('click', field._emeLockGuard);
143            field.addEventListener('keydown', field._emeLockGuard);
144        } else if (!locked && field._emeLockGuard) {
145            field.removeEventListener('click', field._emeLockGuard);
146            field.removeEventListener('keydown', field._emeLockGuard);
147            delete field._emeLockGuard;
148        }
149    } else {
150        field.readOnly = !!locked;
151    }
152}
153
154// the same, but for a list of element ids
155function eme_lockFields(ids, locked = true) {
156    ids.forEach(id => eme_lockField(EME.$('#' + id), locked));
157}
158
159function eme_clearFields(ids) {
160    ids.forEach(id => eme_clearFieldValue(EME.$('#' + id)));
161}
162
163function eme_fillFields(map) {
164    Object.entries(map).forEach(([id, value]) => eme_setFieldValue(EME.$('#' + id), value));
165}
166
167// --- location fields ---
168// map of the location field element id => the key as returned by the
169// eme_autocomplete_locations ajax call. Elements that are not present in the
170// current form (a frontend form has no location properties for instance) are skipped
171// these live on the EME object and not in a const, so every file can reach them
172EME.locationFields = {
173    location_id: 'location_id',
174    location_name: 'name',
175    location_address1: 'address1',
176    location_address2: 'address2',
177    location_city: 'city',
178    location_state: 'state',
179    location_zip: 'zip',
180    location_country: 'country',
181    location_latitude: 'latitude',
182    location_longitude: 'longitude',
183    location_url: 'location_url',
184    eme_loc_prop_map_icon: 'map_icon',
185    eme_loc_prop_max_capacity: 'max_capacity',
186    eme_loc_prop_online_only: 'online_only'
187};
188EME.locationFieldIds = Object.keys(EME.locationFields);
189// the plain address parts, changing one of them asks the map to geocode again
190EME.locationAddressFields = [
191    'location_address1', 'location_address2', 'location_city', 'location_state',
192    'location_zip', 'location_country'
193];
194EME.locationCoordinateFields = ['location_latitude', 'location_longitude'];
195// for an online-only location these make no sense and the map is not shown
196EME.locationNoGeoFields = [
197    ...EME.locationAddressFields,
198    ...EME.locationCoordinateFields,
199    'eme_loc_prop_map_icon'
200];
201// typing or a browser autofill burst should end in one geocode request, not one per keystroke
202EME.locationGeocodeDelay = 500;
203
204function eme_fillLocationFields(item) {
205    eme_fillFields(Object.fromEntries(
206        Object.entries(EME.locationFields).map(([id, key]) => [id, item[key]])
207    ));
208}
209
210function eme_clearLocationFields() {
211    eme_clearFields(EME.locationFieldIds);
212}
213
214function eme_lockLocationFields(locked) {
215    eme_lockFields(EME.locationFieldIds, locked);
216}
217
218// Single place that decides which location fields are locked, so every caller
219// agrees. lockExisting is for the event form, where the fields of an existing
220// location are locked once one is picked; the location form itself keeps them
221// editable, there only the online-only rule applies
222function eme_updateLocationFieldState(opts = {}) {
223    const lockExisting = !!opts.lockExisting;
224    const idField = EME.$('#location_id');
225    const onlineField = EME.$('#eme_loc_prop_online_only');
226    const overrideField = EME.$('#eme_loc_prop_override_loc');
227    const hasLocation = !!(idField && idField.value && idField.value !== '0');
228    const onlineOnly = !!(onlineField && onlineField.checked);
229    const overrideLoc = !!(overrideField && overrideField.checked);
230
231    EME.locationFieldIds.forEach(id => {
232        // an online-only location has no address and no coordinates
233        const onlineLocked = onlineOnly && EME.locationNoGeoFields.includes(id);
234        const pickedLocked = lockExisting && hasLocation;
235        // the override coordinates checkbox wins from the lock of a picked
236        // location, but not from the online-only one
237        const overrideWins = lockExisting && overrideLoc && !onlineLocked && EME.locationCoordinateFields.includes(id);
238        eme_lockField(EME.$('#' + id), (pickedLocked || onlineLocked) && !overrideWins);
239    });
240
241    return { hasLocation, onlineOnly, overrideLoc };
242}
243
244// Autocomplete on the location name field, used by the admin event form and the
245// frontend submission form. Only present if the form has a location name field,
246// the admin event form can have a location dropdown instead
247function eme_initLocationAutocomplete(opts = {}) {
248    const input = EME.$('input#location_name');
249    if (!input) return false;
250
251    const fetchFn = opts.fetchFn || ((formData, callback) => fetch(opts.url, { method: 'POST', body: formData })
252        .then(response => response.json())
253        .then(callback)
254        .catch(() => console.warn('Location autocomplete request failed')));
255
256    const removeSuggestions = () => EME.$$('.eme-autocomplete-suggestions').forEach(el => el.remove());
257
258    let timeout;
259    document.addEventListener('click', (e) => {
260        if (!e.target.closest('.eme-autocomplete-suggestions') && !e.target.closest('input#location_name')) {
261            removeSuggestions();
262        }
263    });
264
265    input.addEventListener('input', () => {
266        clearTimeout(timeout);
267        removeSuggestions();
268
269        if (input.value.length < 2) return;
270        timeout = setTimeout(() => {
271            const formData = new FormData();
272            if (opts.nonceField) formData.append(opts.nonceField, opts.nonceValue);
273            formData.append('name', input.value);
274            formData.append('action', 'eme_autocomplete_locations');
275
276            fetchFn(formData, (data) => {
277                if (!data) return;
278                const suggestions = document.createElement('div');
279                suggestions.className = 'eme-autocomplete-suggestions';
280
281                if (!data.length) {
282                    const noMatch = document.createElement('div');
283                    noMatch.className = 'eme-autocomplete-suggestion';
284                    noMatch.setHTML(`<strong>${opts.nomatchText || ''}</strong>`);
285                    suggestions.appendChild(noMatch);
286                }
287
288                data.forEach(item => {
289                    const suggestion = document.createElement('div');
290                    suggestion.className = 'eme-autocomplete-suggestion';
291                    suggestion.setHTML(`<strong>${item.name}</strong><br><small>${item.address1} - ${item.city}</small>`);
292
293                    suggestion.addEventListener('click', (e) => {
294                        e.preventDefault();
295                        e.stopPropagation();
296                        eme_fillLocationFields(item);
297                        eme_updateLocationFieldState({ lockExisting: true });
298                        removeSuggestions();
299                        if (typeof opts.afterSelect === 'function') opts.afterSelect(item);
300                    });
301
302                    suggestions.appendChild(suggestion);
303                });
304
305                removeSuggestions();
306                input.insertAdjacentElement('afterend', suggestions);
307            });
308        }, 500);
309    });
310
311    input.addEventListener('change', () => {
312        if (input.value === '') {
313            eme_clearLocationFields();
314            eme_updateLocationFieldState({ lockExisting: true });
315            removeSuggestions();
316            if (typeof opts.afterClear === 'function') opts.afterClear();
317        }
318    });
319
320    eme_updateLocationFieldState({ lockExisting: true });
321
322    // a prefilled name (a frontend form for an existing event) shows the suggestions
323    if (opts.prefillSuggestions && input.value.length >= 2) {
324        input.dispatchEvent(new Event('input'));
325    }
326    return true;
327}
328
329function initSnapSelect(selector, options = {}) {
330    // Convert selector to elements array
331    const elements = typeof selector === 'string'
332        ? EME.$$(selector)
333        : [selector];
334
335    if (!elements.length) return [];
336
337    return Array.from(elements).map(el => {
338        // Guard: skip if already initialised
339        if (el.snapselectInstance) return el.snapselectInstance;
340
341        if (!el.id) {
342            el.id = 'snap-select-' + Math.random().toString(36).slice(2, 11);
343        }
344
345        const config = {
346            placeholder:    options.placeholder    || undefined,
347            defaultText:    options.defaultText    || emebasic.translate_optionselect,
348            liveSearch:     options.liveSearch     !== undefined ? options.liveSearch     : el.multiple ? true : undefined,
349            closeOnSelect:  options.closeOnSelect  !== undefined ? options.closeOnSelect  : undefined,
350            showClearButton:     options.showClearButton     !== undefined ? options.showClearButton     : undefined,
351        };
352
353        const instance = new SnapSelectClass(el, config);
354        el.snapselectInstance = instance;
355        return instance;
356    });
357}
358
359function initSnapSelectRemote(selector, options = {}) {
360    // Convert selector to elements array
361    const elements = typeof selector === 'string'
362        ? EME.$$(selector)
363        : [selector];
364
365    if (!elements.length) return [];
366
367    return Array.from(elements).map(el => {
368        // Guard: skip if already initialised
369        if (el.snapselectInstance) return el.snapselectInstance;
370
371        if (!el.id) {
372            el.id = 'snap-select-' + Math.random().toString(36).slice(2, 11);
373        }
374
375        const pagesize = options.pagesize || 30;
376        // Build the ajax.url function, incorporating any extra data
377        const config = {
378            placeholder:    options.placeholder    || undefined,
379            defaultText:    options.defaultText    || emebasic.translate_optionselect,
380            closeOnSelect:  !el.multiple,
381            showClearButton:     options.showClearButton !== undefined ? options.showClearButton : undefined,
382            onItemAdd:      typeof options.onItemAdd    === 'function' ? options.onItemAdd    : undefined,
383            onItemDelete:   typeof options.onItemDelete === 'function' ? options.onItemDelete : undefined,
384            ajax: {
385                // Allow caller to pass a custom url function (e.g. for state→country cascade)
386                url: options.url || emebasic.translate_ajax_url,
387                cache: options.cache !== undefined ? options.cache : undefined,
388                pagesize: pagesize,
389                data: options.data || {},
390                processResults: function(data, search, page) {
391                    // EME backend returns { TotalRecordCount, Records:[{id,text},...] }
392                    // or a plain array
393                    const records = Array.isArray(data)
394                        ? data
395                        : (data.Records || []);
396                    let hasMore;
397                    if (data.hasMore !== undefined) {
398                        hasMore = data.hasMore;
399                    } else {
400                        const total   = data.TotalRecordCount !== undefined
401                            ? data.TotalRecordCount
402                            : records.length;
403                        hasMore = total > page * pagesize;
404                        // the next if covers the case where all rows are returned in 1 go, not respecting paging
405                        if (data.TotalRecordCount !== undefined && records.length >= data.TotalRecordCount)
406                            hasMore = false;
407                    }
408                    return { results: records, hasMore };
409                },
410            }
411        };
412        const instance = new SnapSelectClass(el, config);
413
414        // Expose a .snapselect property on the element
415        el.snapselectInstance = instance;
416
417        return instance;
418    });
419}
420
421// Helper functions for class manipulation
422function eme_addClass(el, className) {
423    if (el.classList) {
424        el.classList.add(className);
425    } else {
426        el.className += ' ' + className;
427    }
428}
429
430function eme_removeClass(el, className) {
431    if (el.classList) {
432        el.classList.remove(className);
433    } else {
434        el.className = el.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' ');
435    }
436}
437
438function eme_hasClass(el, className) {
439    return el.classList ? el.classList.contains(className) : new RegExp('(^| )' + className + '( |$)', 'gi').test(el.className);
440}
441
442function eme_lastname_clearable() {
443    const ln = EME.$('input[name=lastname]');
444    if (!ln) return;
445    
446    const fields = ['firstname', 'address1', 'address2', 'city', 'state', 'zip', 'country', 'email', 'phone','birthdate'];
447    if (ln.value == '') {
448        ln.readOnly = false;
449        eme_removeClass(ln, 'clearable');
450        eme_removeClass(ln, 'x');
451        fields.forEach(f => {
452            const field = EME.$(`input[name=${f}]`);
453            if (field) {
454                field.value = '';
455                field.readOnly = false;
456            }
457        });
458        const birthfield = EME.$('input#birthdate');
459        if (birthfield) {
460            birthfield.value = '';
461        }
462        const wpIdField = EME.$('input[name=wp_id]');
463        const personIdField = EME.$('input[name=person_id]');
464        if (wpIdField) wpIdField.value = '';
465        if (personIdField) personIdField.value = '';
466    }
467    if (ln.value != '') {
468        eme_addClass(ln, 'clearable');
469        eme_addClass(ln, 'x');
470    }
471}
472
473// --- Widget Initialization ---
474function eme_init_widgets(dynamicOnly = false) {
475    const dynamicSelector = dynamicOnly ? '.dynamicfield' : '';
476
477    // Initialize fdatepicker for datetime fields
478    EME.$$('.eme_formfield_fdatetime' + dynamicSelector).forEach(el => {
479        if (typeof FDatepicker !== 'undefined') {
480            new FDatepicker(el, {
481                todayButton: true,
482                clearButton: true,
483                closeButton: true,
484                timepicker: true,
485                minutesStep: parseInt(emebasic.translate_minutesStep),
486                firstDayOfWeek: parseInt(emebasic.translate_firstDayOfWeek),
487                altFormat: 'Y-m-d H:i:00',
488                multipleSeparator: ", ",
489                format: emebasic.translate_fdatetimeformat,
490            });
491        } else {
492            el.removeAttribute('readonly');
493            el.type = 'datetime-local';
494        }
495    });
496
497    // Initialize fdatepicker for date fields
498    EME.$$('.eme_formfield_fdate' + dynamicSelector).forEach(el => {
499        if (typeof FDatepicker !== 'undefined') {
500            new FDatepicker(el, {
501                todayButton: true,
502                clearButton: true,
503                closeButton: true,
504                autoClose: true,
505                firstDayOfWeek: parseInt(emebasic.translate_firstDayOfWeek),
506                altFormat: 'Y-m-d',
507                multipleSeparator: ", ",
508                format: emebasic.translate_fdateformat
509            });
510        } else {
511            el.removeAttribute('readonly');
512            el.type = 'date';
513        }
514    });
515
516    // Initialize fdatepicker for time fields
517    EME.$$('.eme_formfield_ftime' + dynamicSelector).forEach(el => {
518        if (typeof FDatepicker !== 'undefined') {
519            new FDatepicker(el, {
520                timepicker: true,
521                timeOnly: true,
522                clearButton: true,
523                closeButton: true,
524                minutesStep: parseInt(emebasic.translate_minutesStep),
525                altFormat: 'H:i:00',
526                format: emebasic.translate_ftimeformat
527            });
528        } else {
529            el.removeAttribute('readonly');
530            el.type = 'time';
531        }
532    });
533
534    initSnapSelect('select.eme_snapselect' + dynamicSelector);
535    initSnapSelect('select.eme_snapselect_allow_empty' + dynamicSelector, {
536        showClearButton: true
537    });
538    initSnapSelectRemote('select.eme_snapselect_country_class' + dynamicSelector, {
539        placeholder: emebasic.translate_selectcountry,
540        data: {
541            action: 'eme_select_country',
542            eme_frontend_nonce: emebasic.translate_frontendnonce
543        },
544        cache: true,
545        showClearButton: true,
546        // When country changes, reset the state field in the same form
547        onItemAdd: function(value, text) {
548            const form       = this.closest('form');
549            const stateField = form?.querySelector('.eme_snapselect_state_class');
550            if (stateField && stateField.snapselectInstance) {
551                stateField.snapselectInstance.clear();
552                stateField.snapselectInstance.clearCache();
553            }
554        },
555        onItemDelete: function(value, text) {
556            const form       = this.closest('form');
557            const stateField = form?.querySelector('.eme_snapselect_state_class');
558            if (stateField && stateField.snapselectInstance) {
559                stateField.snapselectInstance.clear();
560                stateField.snapselectInstance.clearCache();
561            }
562        }
563    });
564
565    initSnapSelectRemote('select.eme_snapselect_state_class' + dynamicSelector, {
566        placeholder: emebasic.translate_selectstate,
567        showClearButton: true,
568        // Dynamically include the currently selected country_code in every request
569        data: function(search, page) { // since this is a function, no caching happens
570            const stateEl     = document.querySelector('select.eme_snapselect_state_class');
571            const form        = stateEl?.closest('form');
572            const countryCode = form?.querySelector('[name=country_code]')?.value || '';
573            return {
574                action: 'eme_select_state',
575                eme_frontend_nonce: emebasic.translate_frontendnonce,
576                country_code: countryCode
577            };
578        },
579        cache: true // we set the cache true and clear it in the country select if needed
580    });
581}
582
583// function to execute the injected javascript via ajax to force payment button click
584function eme_executeScriptsInElement(element) {
585    const scripts = element.querySelectorAll('script');
586    scripts.forEach(oldScript => {
587        const newScript = document.createElement('script');
588
589        // Copy all attributes from old script to new script
590        Array.from(oldScript.attributes).forEach(attr => {
591            newScript.setAttribute(attr.name, attr.value);
592        });
593
594        // Copy the script content
595        newScript.textContent = oldScript.textContent;
596
597        // Replace the old script with the new one to trigger execution
598        oldScript.parentNode.replaceChild(newScript, oldScript);
599    });
600}
601
602// --- Unified AJAX Handler for Booking/Member/Generic forms ---
603function eme_ajax_form(form_id, action, okSel, errSel, submitBtn, extraParams = {}) {
604    const form = document.getElementById(form_id);
605    if (!form) return;
606    const okEl = EME.$(okSel);
607    const errEl = EME.$(errSel);
608
609    const btn = submitBtn || form.querySelector('[type="submit"]');
610    const spinner = document.createElement('span');
611    spinner.className = 'spinner';
612    spinner.setHTML('&#10231;');
613    if (btn) {
614        btn.dataset.emeOrigText = btn.value || btn.textContent;
615        btn.disabled = true;
616        if (btn.tagName === 'INPUT') {
617            btn.value = emebasic.translate_pleasewait;
618        } else {
619            btn.textContent = emebasic.translate_pleasewait;
620        }
621        btn.parentNode.insertBefore(spinner, btn.nextSibling);
622    }
623    const restoreBtn = () => {
624        if (btn) {
625            btn.disabled = false;
626            if (btn.tagName === 'INPUT') {
627                btn.value = btn.dataset.emeOrigText;
628            } else {
629                btn.textContent = btn.dataset.emeOrigText;
630            }
631        }
632        if (spinner && spinner.parentNode) spinner.parentNode.removeChild(spinner);
633    };
634
635    let alldata = new FormData(form);
636    alldata.append('action', action);
637    Object.entries(extraParams).forEach(([k, v]) => alldata.append(k, v));
638
639    fetch(emebasic.translate_ajax_url, {
640        method: 'POST',
641        body: alldata
642    })
643        .then(response => response.json())
644        .then(data => {
645            restoreBtn();
646
647            if (data.Result === "OK") {
648                if (errSel) {
649                    if (errEl) eme_toggle(errEl, false);
650                }
651                if (okSel) {
652                    if (okEl && data.htmlmessage && (!data.redirect || ( data.redirect && parseInt(data.waitperiod)>0 ) ) ) {
653                        okEl.innerHTML = data.htmlmessage;
654                        eme_toggle(okEl, true);
655                    }
656                }
657                if (parseInt(data.keep_form) == 1) {
658                    form.reset();
659                    eme_refresh_captcha(form_id);
660                } else {
661                    const parentDiv = form.closest('[id^=div_eme-]');
662                    if (parentDiv) eme_toggle(parentDiv, false);
663                }
664                if (data.paymentform) {
665                    const paymentDiv = EME.$(`#div_eme-payment-form-${form_id}`);
666                    if (paymentDiv && data.paymentform) {
667                        paymentDiv.innerHTML = data.paymentform;
668                        eme_toggle(paymentDiv, true);
669                        eme_executeScriptsInElement(paymentDiv);
670                    }
671                }
672                if (data.redirect) {
673                    setTimeout(() => window.location.href = data.redirect, parseInt(data.waitperiod));
674                }
675                eme_scrollToEl(okEl);
676            } else {
677                if (errSel) {
678                    if (errEl && data.htmlmessage) {
679                        errEl.innerHTML = data.htmlmessage;
680                        eme_toggle(errEl, true);
681                    }
682                }
683                if (okSel) {
684                    if (okEl) eme_toggle(okEl, false);
685                }
686                eme_scrollToEl(errEl);
687            }
688        })
689        .catch(error => {
690            restoreBtn();
691            if (errSel) {
692                if (errEl) {
693                    errEl.setHTML(emebasic.translate_error + (error?.message ? '<br>' + error.message : ''));
694                    eme_toggle(errEl, true);
695                }
696            }
697            if (okSel) {
698                if (okEl) eme_toggle(okEl, false);
699            }
700            eme_scrollToEl(errEl);
701        });
702}
703
704function eme_refresh_captcha(form_id) {
705    const form = document.getElementById(form_id);
706    if (!form) return;
707
708    const captcha = form.querySelector('.eme-captcha-img');
709    if (captcha) {
710        const src = captcha.src.replace(/&ts=[^&]*/, '');
711        captcha.src = `${src}&ts=${Date.now()}`;
712    }
713}
714
715function eme_scrollToEl(sel) {
716    if (sel) {
717        sel.scrollIntoView({ behavior: 'smooth', block: 'center' });
718        //const rect = sel.getBoundingClientRect();
719        //const scrollTop = window.scrollY + rect.top - (window.innerHeight / 2) + (rect.height / 2);
720        //window.scrollTo({ top: scrollTop, behavior: 'smooth' });
721    }
722}
723
724// --- Unified Dynamic Data/Price AJAX ---
725function eme_dynamic_price_json(form_id, isBooking = true) {
726    const form = document.getElementById(form_id);
727    if (!form) return;
728
729    form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, false));
730    let alldata = new FormData(form);
731
732    const priceSpans = isBooking
733        ? [{ sel: 'span#eme_calc_bookingprice', action: 'eme_calc_bookingprice' }, { sel: 'span#eme_calc_bookingprice_detail', action: 'eme_calc_bookingprice_detail' }]
734        : [{ sel: 'span#eme_calc_memberprice', action: 'eme_calc_memberprice' }, { sel: 'span#eme_calc_memberprice_detail', action: 'eme_calc_memberprice_detail' }];
735
736    let found = false;
737    priceSpans.forEach(({ sel, action }) => {
738        const span = form.querySelector(sel);
739        if (span) {
740            found = true;
741            span.setHTML('<span class="spinner">⟳</span>');
742            alldata.set('action', action);
743            alldata.set('eme_frontend_nonce', emebasic.translate_frontendnonce);
744
745            fetch(emebasic.translate_ajax_url, {
746                method: 'POST',
747                body: alldata
748            })
749                .then(response => response.json())
750                .then(data => {
751                    form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
752                    span.innerHTML = data.total;
753                })
754                .catch(() => {
755                    form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
756                    span.setHTML('Invalid reply');
757                });
758        }
759    });
760
761    if (!found) {
762        form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
763    }
764}
765
766function eme_dynamic_data_json(form_id, isBooking = true) {
767    const form = document.getElementById(form_id);
768    if (!form) return;
769
770    form.querySelectorAll('input[type="submit"], button[type="submit"]').forEach(btn => eme_toggle(btn, false));
771    let alldata = new FormData(form);
772
773    const dataDivSel = 'div#eme_dyndata';
774    const action = isBooking ? 'eme_dyndata_rsvp' : 'eme_dyndata_member';
775    const dataDiv = form.querySelector(dataDivSel);
776
777    if (dataDiv) {
778        dataDiv.setHTML('<span class="spinner">⟳</span>');
779        alldata.set('action', action);
780        alldata.set('eme_frontend_nonce', emebasic.translate_frontendnonce);
781
782        fetch(emebasic.translate_ajax_url, {
783            method: 'POST',
784            body: alldata
785        })
786            .then(response => response.json())
787            .then(data => {
788                form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
789                dataDiv.innerHTML = data.Result;
790                eme_init_widgets(true);
791                eme_dynamic_price_json(form_id, isBooking);
792            })
793            .catch(() => {
794                form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
795            });
796    } else {
797        eme_dynamic_price_json(form_id, isBooking);
798    }
799}
800
801function eme_dynamic_familymemberdata_json(form_id) {
802    const form = document.getElementById(form_id);
803    if (!form) return;
804
805    form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, false));
806    let alldata = new FormData(form);
807    const dataDiv = form.querySelector('div#eme_dyndata_family');
808
809    if (dataDiv) {
810        dataDiv.setHTML('<span class="spinner">⟳</span>');
811        alldata.set('action', 'eme_dyndata_familymember');
812        alldata.set('eme_frontend_nonce', emebasic.translate_frontendnonce);
813
814        fetch(emebasic.translate_ajax_url, {
815            method: 'POST',
816            body: alldata
817        })
818            .then(response => response.json())
819            .then(data => {
820                form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
821                dataDiv.innerHTML = data.Result;
822                eme_init_widgets(true);
823            })
824            .catch(() => {
825                form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
826            });
827    } else {
828        form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, true));
829    }
830}
831
832// Calendar navigation
833function loadCalendar(
834    tableDiv, fullcalendar = 0, htmltable, htmldiv, showlong_events = 0, month = 0,
835    year = 0, cat_chosen = '', author_chosen = '', contact_person_chosen = '',
836    location_chosen = '', not_cat_chosen = '', template_chosen = 0, holiday_chosen = 0,
837    weekdays = ''
838) {
839    const formData = new FormData();
840    formData.append('eme_frontend_nonce', emebasic.translate_frontendnonce);
841    formData.append('action', 'eme_calendar');
842    formData.append('calmonth', parseInt(month, 10));
843    formData.append('calyear', parseInt(year, 10));
844    formData.append('full', fullcalendar);
845    formData.append('long_events', showlong_events);
846    formData.append('htmltable', htmltable);
847    formData.append('htmldiv', htmldiv);
848    formData.append('category', cat_chosen);
849    formData.append('notcategory', not_cat_chosen);
850    formData.append('author', author_chosen);
851    formData.append('contact_person', contact_person_chosen);
852    formData.append('location_id', location_chosen);
853    formData.append('template_id', template_chosen);
854    formData.append('holiday_id', holiday_chosen);
855    formData.append('weekdays', weekdays);
856
857    fetch(emebasic.translate_ajax_url, {
858        method: 'POST',
859        body: formData
860    })
861        .then(response => response.text())
862        .then(data => {
863            const tableEl = document.getElementById(tableDiv);
864            if (tableEl) {
865                tableEl.outerHTML = data;
866                // Re-attach event handlers after DOM replacement
867                attachCalendarHandlers();
868            }
869        });
870}
871
872function attachCalendarHandlers() {
873    EME.$$('a.eme-cal-prev-month, a.eme-cal-next-month').forEach(link => {
874        link.addEventListener('click', function(e) {
875            e.preventDefault();
876            this.setHTML('<span class="spinner">⟳</span>');
877            loadCalendar(
878                this.dataset.calendar_divid,
879                this.dataset.full,
880                this.dataset.htmltable,
881                this.dataset.htmldiv,
882                this.dataset.long_events,
883                this.dataset.month,
884                this.dataset.year,
885                this.dataset.category,
886                this.dataset.author,
887                this.dataset.contact_person,
888                this.dataset.location_id,
889                this.dataset.notcategory,
890                this.dataset.template_id,
891                this.dataset.holiday_id,
892                this.dataset.weekdays
893            );
894        });
895    });
896}
897
898// --- Booking/Member form handlers with confirmation ---
899function eme_handle_massmail(form_id, callback) {
900    const form = document.getElementById(form_id);
901    const massmailField = form.querySelector('#massmail');
902    const massmailDialog = EME.$('#MassMailDialog');
903
904    if (massmailField && massmailField.value != 1 && massmailDialog) {
905        massmailDialog.showModal();
906
907        const confirmBtn = EME.$('#dialog-confirm');
908        const cancelBtn = EME.$('#dialog-cancel');
909
910        if (confirmBtn) {
911            confirmBtn.addEventListener('click', function(e) {
912                e.preventDefault();
913                massmailDialog.close();
914                callback(form_id);
915            }, { once: true });
916        }
917
918        if (cancelBtn) {
919            cancelBtn.addEventListener('click', function(e) {
920                e.preventDefault();
921                massmailDialog.close();
922            }, { once: true });
923        }
924    } else {
925        callback(form_id);
926    }
927}
928
929// --- Dynamic fields AJAX debounce handlers ---
930function eme_attach_dynamic_handlers(selector, isBooking) {
931    EME.$$(selector).forEach(form => {
932        const form_id = form.id;
933        const debounced_data = eme_debounce(() => eme_dynamic_data_json(form_id, isBooking), 500);
934        const debounced_price = eme_debounce(() => eme_dynamic_price_json(form_id, isBooking), 500);
935        const debounced_family = !isBooking ? eme_debounce(() => eme_dynamic_familymemberdata_json(form_id), 500) : null;
936
937        form.addEventListener('input', function(event) {
938            // Ignore events from hidden fields (e.g. FDatepicker auto-created altField);
939            // those are internal implementation details and should not trigger a dynamic refresh.
940            if (event.target.type === 'hidden') {
941                return;
942            }
943            if (debounced_family && event.target.id === 'familycount') {
944                debounced_family();
945            }
946            if (eme_hasClass(event.target, 'nodynamicupdates') || event.target.closest('fieldset.nodynamicupdates')) {
947                if (eme_hasClass(event.target, 'dynamicprice')) {
948                    form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, false));
949                    debounced_price();
950                }
951                return;
952            }
953            form.querySelectorAll('[type="submit"]').forEach(btn => eme_toggle(btn, false));
954            debounced_data();
955        });
956
957        if (debounced_family) debounced_family();
958        debounced_data();
959    });
960}
961    
962function eme_scrollToInvalidInput(el) { 
963    // open details if closed
964    const detailsParent = el.closest('.eme_accordion');
965    if (detailsParent && !detailsParent.hasAttribute('open')) {
966        // Open it
967        detailsParent.setAttribute('open', 'true');
968        // Find the content div after summary
969        const summary = detailsParent.querySelector('summary');
970        if (summary) {
971            const contentDiv = detailsParent.querySelector('summary + div');
972            if (contentDiv) {
973                eme_toggle(contentDiv, true);
974                contentDiv.style.height = ''; // reset height
975            }
976        }
977    }
978
979    // First check if field is in a tab
980    const tabPane = el.closest('.eme-tab-content');
981    if (tabPane) {
982        const tabId = tabPane.id;
983        const isVisible = tabPane.classList.contains('active') ||
984            window.getComputedStyle(tabPane).display !== 'none';
985
986        if (!isVisible && typeof eme_activateTab === 'function') {
987            // Activate the tab containing the invalid field
988            eme_activateTab(tabId);
989            // Small delay to ensure tab is visible before scrolling
990            setTimeout(() => {
991                if (typeof el.reportValidity === 'function') {
992                    el.reportValidity();
993                } else {
994                    eme_addClass(el, 'eme_required');
995                    eme_scrollToEl(el);
996                }
997            }, 300);
998            return; // Exit, scrolling happens after timeout
999        }
1000    }
1001
1002    // If not in tab or tab is already active, scroll normally
1003    if (typeof el.reportValidity === 'function') {
1004        el.reportValidity();
1005    } else {
1006        eme_addClass(el, 'eme_required');
1007        eme_scrollToEl(el);
1008    }
1009}
1010
1011document.addEventListener('DOMContentLoaded', function() {
1012    // Show elements that should be visible with JS
1013    EME.$$('.eme-showifjs').forEach(el => {
1014        eme_toggle(el, true);
1015    });
1016
1017    
1018    // Initial calendar handler attachment
1019    attachCalendarHandlers();
1020
1021    // Clearable input with 'x'
1022    eme_initClearableInputs();
1023
1024    document.addEventListener('input', function(e) {
1025        if (eme_isClearableField(e.target)) {
1026            eme_updateClearableIcon(e.target);
1027        }
1028    });
1029
1030    document.addEventListener('mousemove', function(e) {
1031        if (eme_hasClass(e.target, 'x')) {
1032            const rect = e.target.getBoundingClientRect();
1033            if (e.target.offsetWidth - 18 < e.clientX - rect.left) {
1034                eme_addClass(e.target, 'onX');
1035            } else {
1036                eme_removeClass(e.target, 'onX');
1037            }
1038        }
1039    });
1040
1041    document.addEventListener('click', function(e) {
1042        if (eme_clearableHit(e.target, e.clientX)) {
1043            e.preventDefault();
1044            eme_clearFieldByIcon(e.target);
1045        }
1046    });
1047
1048    document.addEventListener('touchstart', function(e) {
1049        if (eme_clearableHit(e.target, e.clientX)) {
1050            e.preventDefault();
1051            eme_clearFieldByIcon(e.target);
1052        }
1053    });
1054
1055    // Lastname clearable
1056    const lastnameField = EME.$("input[name=lastname]");
1057    if (lastnameField && lastnameField.dataset.clearable) {
1058        lastnameField.addEventListener('change', eme_lastname_clearable);
1059        eme_lastname_clearable();
1060    }
1061
1062    // --- Generic forms ---
1063    const genericForms = [
1064        { sel: '[name=eme-cancel-payment-form]', action: 'eme_cancel_payment', ok: 'div#eme-cancel-payment-message-ok-', err: 'div#eme-cancel-payment-message-error-' },
1065        { sel: '[name=eme-cancel-bookings-form]', action: 'eme_cancel_bookings', ok: 'div#eme-cancel-bookings-message-ok-', err: 'div#eme-cancel-bookings-message-error-' },
1066        { sel: '[name=eme-subscribe-form]', action: 'eme_subscribe', ok: 'div#eme-subscribe-message-ok-', err: 'div#eme-subscribe-message-error-' },
1067        { sel: '[name=eme-unsubscribe-form]', action: 'eme_unsubscribe', ok: 'div#eme-unsubscribe-message-ok-', err: 'div#eme-unsubscribe-message-error-' },
1068        { sel: '[name=eme-rpi-form]', action: 'eme_rpi', ok: 'div#eme-rpi-message-ok-', err: 'div#eme-rpi-message-error-' },
1069        { sel: '[name=eme-gdpr-approve-form]', action: 'eme_gdpr_approve', ok: 'div#eme-gdpr-approve-message-ok-', err: 'div#eme-gdpr-approve-message-error-' },
1070        { sel: '[name=eme-cpi-request-form]', action: 'eme_cpi_request', ok: 'div#eme-cpi-request-message-ok-', err: 'div#eme-cpi-request-message-error-' },
1071        { sel: '[name=eme-cpi-form]', action: 'eme_cpi', ok: 'div#eme-cpi-message-ok-', err: 'div#eme-cpi-message-error-' },
1072        { sel: '[name=eme-tasks-form]', action: 'eme_tasks', ok: 'div#eme-tasks-message-ok-', err: 'div#eme-tasks-message-error-' },
1073        { sel: '[name=eme-fs-form]', action: 'eme_frontend_submit', ok: 'div#eme-fs-message-ok-', err: 'div#eme-fs-message-error-', isFS: true }
1074    ];
1075    
1076    genericForms.forEach(({ sel, action, ok, err, isFS }) => {
1077        EME.$$(sel).forEach(form => {
1078            form.addEventListener('submit', function(event) {
1079                event.preventDefault();
1080                const form_id = this.id;
1081                if (isFS && emebasic.translate_htmleditor === "tinemce" && emebasic.translate_fs_wysiwyg === "true") {
1082                    if (typeof tinymce !== "undefined") {
1083                        const eventNotesEditor = tinymce.get('event_notes');
1084                        const locationDescEditor = tinymce.get('location_description');
1085                        if (eventNotesEditor) eventNotesEditor.save();
1086                        if (locationDescEditor) locationDescEditor.save();
1087                    }
1088                }
1089                eme_ajax_form(form_id, action, ok + form_id, err + form_id, event.submitter);
1090            });
1091        });
1092    });
1093
1094    EME.$$('[name=eme-rsvp-form]').forEach(form => {
1095        form.addEventListener('submit', function(event) {
1096            event.preventDefault();
1097            eme_handle_massmail(this.id, function(form_id) {
1098                let extra = {};
1099                ['eme_invite', 'eme_email', 'eme_ln', 'eme_fn'].forEach(k => { 
1100                    if ($_GET[k]) extra[k] = $_GET[k]; 
1101                });
1102                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);
1103                eme_dynamic_data_json(form_id, true);
1104            });
1105        });
1106    });
1107
1108    EME.$$('[name=eme-member-form]').forEach(form => {
1109        form.addEventListener('submit', function(event) {
1110            event.preventDefault();
1111            eme_handle_massmail(this.id, function(form_id) {
1112                eme_ajax_form(form_id, 'eme_add_member', 'div#eme-member-addmessage-ok-' + form_id, 'div#eme-member-addmessage-error-' + form_id, event.submitter);
1113                eme_dynamic_data_json(form_id, false);
1114            });
1115        });
1116    });
1117
1118    // --- Validation for submit buttons ---
1119    EME.$$('.eme_submit_button').forEach(btn => {
1120        btn.addEventListener('click', function(event) {
1121            let valid = true;
1122            const parent_form_id = this.form.id;
1123            
1124            // Check required text inputs and date fields
1125            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 => {
1126                if (valid && input.closest("form").id === parent_form_id) {
1127                    const val = input.value;
1128                    if (val.match(/^\s*$/)) {
1129                        eme_scrollToInvalidInput(input);
1130                        valid = false;
1131                    } else {
1132                        eme_removeClass(input, 'eme_required');
1133                    }
1134                }
1135            });
1136            
1137            // Check required checkbox groups
1138            EME.$$('.eme-checkbox-group-required').forEach(group => {
1139                if (valid && group.closest("form").id === parent_form_id) {
1140                    const checked = group.querySelectorAll('input[type="checkbox"]:checked').length;
1141                    if (!checked) {
1142                        eme_scrollToInvalidInput(group);
1143                        valid = false;
1144                    } else {
1145                        eme_removeClass(group, 'eme_required');
1146                    }
1147                }
1148            });
1149            
1150            if (!valid) {
1151                event.preventDefault();
1152                event.stopPropagation(); // optional, prevent fire of other click-listeners
1153            }
1154
1155        });
1156    });
1157
1158    eme_attach_dynamic_handlers('[name=eme-rsvp-form]', true);
1159    eme_attach_dynamic_handlers('#eme-rsvp-adminform', true);
1160    eme_attach_dynamic_handlers('[name=eme-member-form]', false);
1161    eme_attach_dynamic_handlers('#eme-member-adminform', false);
1162
1163    // Remember-me functionality: only active if a form contains the #_REMEMBERME checkbox
1164    const rememberMeForms = EME.$$('form').filter(form => form.querySelector('input#eme_rememberme'));
1165    if (rememberMeForms.length) {
1166        rememberMeForms.forEach(form => {
1167            if (localStorage.getItem('eme_rememberme') == 1) {
1168                const checkbox = form.querySelector('input#eme_rememberme');
1169                if (checkbox) {
1170                    checkbox.checked = true;
1171                }
1172            }
1173            ['lastname', 'firstname', 'email', 'phone', 'task_lastname', 'task_firstname', 'task_email', 'task_phone'].forEach(fieldName => {
1174                const field = form.querySelector(`input[name=${fieldName}]`);
1175                if (field && field.value === '') {
1176                    field.value = localStorage.getItem('eme_' + fieldName.replace('task_', '')) || '';
1177                }
1178            });
1179            form.addEventListener('submit', function() {
1180                const rememberCheckbox = this.querySelector('input#eme_rememberme');
1181                if (rememberCheckbox && rememberCheckbox.checked) {
1182                    localStorage.setItem('eme_rememberme', 1);
1183                    ['lastname', 'firstname', 'email', 'phone', 'task_lastname', 'task_firstname', 'task_email', 'task_phone'].forEach(fieldName => {
1184                        const field = this.querySelector(`input[name=${fieldName}]`);
1185                        if (field) {
1186                            localStorage.setItem('eme_' + fieldName.replace('task_', ''), field.value);
1187                        }
1188                    });
1189                } else {
1190                    ['eme_lastname', 'eme_firstname', 'eme_email', 'eme_phone', 'eme_rememberme'].forEach(storageKey => {
1191                        localStorage.removeItem(storageKey);
1192                    });
1193                }
1194            });
1195        });
1196    }
1197
1198    // Person image upload widget
1199    const personImageButton = document.getElementById('eme_person_image_button');
1200    if (personImageButton) {
1201        const removeButton = document.getElementById('eme_person_remove_old_image');
1202        const imageIdField = document.getElementById('eme_person_image_id');
1203        const imageExample = document.getElementById('eme_person_image_example');
1204        const currentImageDiv = document.getElementById('eme_person_current_image');
1205        const noImageDiv = document.getElementById('eme_person_no_image');
1206        
1207        if (removeButton) {
1208            removeButton.addEventListener('click', function() {
1209                imageIdField.value = '';
1210                imageExample.src = '';
1211                eme_toggle(currentImageDiv, false);
1212                eme_toggle(noImageDiv, true);
1213                eme_toggle(removeButton, false);
1214                personImageButton.value = emebasic.translate_chooseimg;
1215            });
1216        }
1217        
1218        personImageButton.addEventListener('click', function(e) {
1219            e.preventDefault();
1220            if (typeof wp !== 'undefined' && wp.media) {
1221                const custom_uploader = wp.media({
1222                    title: emebasic.translate_selectimg,
1223                    button: { text: emebasic.translate_setimg },
1224                    library: { type: 'image' },
1225                    multiple: false
1226                }).on('select', function() {
1227                    const attachment = custom_uploader.state().get('selection').first().toJSON();
1228                    imageIdField.value = attachment.id;
1229                    imageExample.src = attachment.url;
1230                    eme_toggle(currentImageDiv, true);
1231                    eme_toggle(noImageDiv, false);
1232                    eme_toggle(removeButton, true);
1233                    personImageButton.value = emebasic.translate_replaceimg;
1234                }).open();
1235            }
1236        });
1237        
1238        // Initialize display state
1239        if (parseInt(imageIdField.value) > 0) {
1240            eme_toggle(noImageDiv, false);
1241            eme_toggle(currentImageDiv, true);
1242            eme_toggle(removeButton, true);
1243            personImageButton.value = emebasic.translate_replaceimg;
1244        } else {
1245            eme_toggle(noImageDiv, true);
1246            eme_toggle(currentImageDiv, false);
1247            eme_toggle(removeButton, false);
1248            personImageButton.value = emebasic.translate_chooseimg;
1249        }
1250    }
1251    
1252    // Scroll to payment form if present
1253    const paymentForm = document.getElementById('eme-payment-form');
1254    if (paymentForm) {
1255        eme_scrollToEl(paymentForm);
1256    }
1257
1258    // Initialize widgets
1259    eme_init_widgets();
1260});

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.