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('⟷'); 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.