PageSourceSearch

https://westeaston.gov/wp-content/plugins/modern-events-calendar-l…/events.js?ver=7.37.0.1791559373.1791387757

js westeaston.gov collected 2026-10-10 01:27:49 UTC 126,314 bytes, 3,102 lines download raw bytes

1// Set datepicker default value.
2var datepicker_format = 'yy-mm-dd';
3
4function mecEnhanceDatepickerAccessibility($inputs)
5{
6    if(!$inputs || !$inputs.length) return;
7
8    $inputs.attr('aria-haspopup', 'dialog');
9    $inputs.off('focus.mecDatepickerA11y').on('focus.mecDatepickerA11y', function()
10    {
11        window.setTimeout(function()
12        {
13            var $datepicker = jQuery('#ui-datepicker-div');
14            if(!$datepicker.length) return;
15
16            $datepicker.attr({
17                role: 'dialog',
18                'aria-live': 'polite',
19                'aria-label': (typeof mecdata !== 'undefined' && mecdata.a11y_calendar_dialog) ? mecdata.a11y_calendar_dialog : 'Calendar date picker'
20            });
21        }, 0);
22    });
23}
24
25jQuery(document).ready(function($)
26{
27    // Image picker on terms menu
28    // Image picker on terms menu
29    // Several buttons share this handler with different data attributes, so the
30    // select callback must close over the CURRENT click's IDs — dispose the old
31    // frame and rebuild on each click (reusing one frame would keep stale IDs).
32    var mecMediaFrame;
33    $('.mec_upload_image_button').click(function(event)
34    {
35        event.preventDefault();
36
37        if(wp.ajax.settings.url.indexOf('mec_fes=1') === -1) wp.ajax.settings.url = wp.ajax.settings.url + '?mec_fes=1';
38
39        var post_id = $(this).data('post-id');
40        if(post_id && post_id !== -1) wp.media.model.settings.post.id = post_id;
41        if(post_id === -1) wp.media.model.settings.post.id = null;
42
43        var preview_id = 'mec_thumbnail_img';
44        var input_id = 'mec_thumbnail';
45
46        if($(this).data('preview-id')) preview_id = $(this).data('preview-id');
47        if($(this).data('input-id')) input_id = $(this).data('input-id');
48
49        if(mecMediaFrame) mecMediaFrame.dispose();
50
51        mecMediaFrame = wp.media();
52        mecMediaFrame.on('select', function()
53        {
54            // Grab the selected attachment.
55            var attachment = mecMediaFrame.state().get('selection').first();
56
57            $('#'+preview_id).html('<img src="'+attachment.attributes.url+'" alt="Featured image preview" />');
58            $('#'+input_id).val(attachment.attributes.url);
59
60            // Toggle only this pair's remove button (several upload/remove pairs can coexist on one page)
61            $('.mec_remove_image_button').filter(function()
62            {
63                return (($(this).data('preview-id') || 'mec_thumbnail_img') === preview_id);
64            }).removeClass('mec-util-hidden');
65
66            // Banner row: hide the "Choose image" button while an image is set
67            if(preview_id === 'mec_banner_thumbnail_img') mecSyncThumbnailButtons('banner');
68
69            mecMediaFrame.close();
70        });
71
72        mecMediaFrame.open();
73    });
74
75    // Image remover on terms menu
76    $('.mec_remove_image_button').click(function(event)
77    {
78        event.preventDefault();
79
80        var preview_id = 'mec_thumbnail_img';
81        var input_id = 'mec_thumbnail';
82
83        if($(this).data('preview-id')) preview_id = $(this).data('preview-id');
84        if($(this).data('input-id')) input_id = $(this).data('input-id');
85
86        $('#'+preview_id).html('');
87        $('#'+input_id).val('');
88
89        $('.mec_remove_image_button').filter(function()
90        {
91            return (($(this).data('preview-id') || 'mec_thumbnail_img') === preview_id);
92        }).addClass('mec-util-hidden');
93
94        // Banner row: bring back the "Choose image" button
95        if(preview_id === 'mec_banner_thumbnail_img') mecSyncThumbnailButtons('banner');
96    });
97
98    // Keep thumbnail "Choose image" / "Remove image" buttons in sync with the current image state
99    function mecSyncThumbnailButtons(which)
100    {
101        var hasImage = $.trim($('#mec_' + which + '_thumbnail').val()) !== '' || $('#mec_' + which + '_thumbnail_img').children('img').length > 0;
102        // Dedicated remove button (.mec_{which}_remove_image_button) or the generic
103        // pair (.mec_remove_image_button[data-preview-id]) — the popup has neither.
104        var $remove = $('.mec_' + which + '_remove_image_button');
105        if(!$remove.length) $remove = $('.mec_remove_image_button').filter(function()
106        {
107            return (($(this).data('preview-id') || 'mec_thumbnail_img') === 'mec_' + which + '_thumbnail_img');
108        });
109
110        $remove.toggleClass('mec-util-hidden', !hasImage);
111        $('#mec_' + which + '_thumbnail_button').toggleClass('mec-util-hidden', hasImage && $remove.length > 0);
112    }
113
114    // Image picker on add event menu for location
115    var mecLocationMediaFrame;
116    $('.mec_location_upload_image_button').click(function(event)
117    {
118        event.preventDefault();
119
120        // Reuse the open frame across clicks (IDs are fixed for this pair)
121        if(mecLocationMediaFrame)
122        {
123            mecLocationMediaFrame.open();
124            return;
125        }
126
127        mecLocationMediaFrame = wp.media();
128        mecLocationMediaFrame.on('select', function()
129        {
130            // Grab the selected attachment.
131            var attachment = mecLocationMediaFrame.state().get('selection').first();
132
133            $('#mec_location_thumbnail_img').html('<img src="'+attachment.attributes.url+'" />');
134            $('#mec_location_thumbnail').val(attachment.attributes.url);
135
136            mecSyncThumbnailButtons('location');
137
138            mecLocationMediaFrame.close();
139        });
140
141        mecLocationMediaFrame.open();
142    });
143
144    // Image remover on add event menu for location
145    $('.mec_location_remove_image_button').click(function(event)
146    {
147        event.preventDefault();
148
149        $('#mec_location_thumbnail_img').html('');
150        $('#mec_location_thumbnail').val('');
151
152        mecSyncThumbnailButtons('location');
153    });
154
155    // Image picker on add event menu for organizer
156    var mecOrganizerMediaFrame;
157    $('.mec_organizer_upload_image_button').click(function(event)
158    {
159        event.preventDefault();
160
161        // Reuse the open frame across clicks (IDs are fixed for this pair)
162        if(mecOrganizerMediaFrame)
163        {
164            mecOrganizerMediaFrame.open();
165            return;
166        }
167
168        mecOrganizerMediaFrame = wp.media();
169        mecOrganizerMediaFrame.on('select', function()
170        {
171            // Grab the selected attachment.
172            var attachment = mecOrganizerMediaFrame.state().get('selection').first();
173
174            $('#mec_organizer_thumbnail_img').html('<img src="'+attachment.attributes.url+'" />');
175            $('#mec_organizer_thumbnail').val(attachment.attributes.url);
176
177            mecSyncThumbnailButtons('organizer');
178
179            mecOrganizerMediaFrame.close();
180        });
181
182        mecOrganizerMediaFrame.open();
183    });
184
185    // Image remover on add event menu for organizer
186    $('.mec_organizer_remove_image_button').click(function(event)
187    {
188        event.preventDefault();
189
190        $('#mec_organizer_thumbnail_img').html('');
191        $('#mec_organizer_thumbnail').val('');
192
193        mecSyncThumbnailButtons('organizer');
194    });
195
196    // Initial state sync (e.g. edit forms pre-populated by other scripts)
197    mecSyncThumbnailButtons('location');
198    mecSyncThumbnailButtons('organizer');
199    mecSyncThumbnailButtons('banner');
200
201    // Image remover on frontend event submission menu
202    $('#mec_fes_remove_image_button').click(function(event)
203    {
204        event.preventDefault();
205
206        $('#mec_fes_thumbnail_img').html('');
207        $('#mec_fes_thumbnail').val('');
208        $('#mec_featured_image_file').val('');
209
210        $('#mec_fes_remove_image_button').addClass('mec-util-hidden');
211    });
212
213    // Location Image remover on frontend event submission menu
214    $('#mec_fes_location_remove_image_button').click(function(event)
215    {
216        event.preventDefault();
217
218        $('#mec_fes_location_thumbnail_img').html('');
219        $('#mec_fes_location_thumbnail').val('');
220        $('#mec_fes_location_thumbnail_file').val('');
221
222        $('#mec_fes_location_remove_image_button').addClass('mec-util-hidden');
223    });
224
225    // Organizer Image remover on frontend event submission menu
226    $('#mec_fes_organizer_remove_image_button').click(function(event)
227    {
228        event.preventDefault();
229
230        $('#mec_fes_organizer_thumbnail_img').html('');
231        $('#mec_fes_organizer_thumbnail').val('');
232        $('#mec_fes_organizer_thumbnail_file').val('');
233
234        $('#mec_fes_organizer_remove_image_button').addClass('mec-util-hidden');
235    });
236
237    // Sponsor Image remover on frontend event submission menu
238    $('#mec_fes_sponsor_remove_image_button').click(function(event)
239    {
240        event.preventDefault();
241
242        $('#mec_fes_sponsor_thumbnail_img').html('');
243        $('#mec_fes_sponsor_thumbnail').val('');
244        $('#mec_fes_sponsor_thumbnail_file').val('');
245
246        $('#mec_fes_sponsor_remove_image_button').addClass('mec-util-hidden');
247    });
248
249    // Speaker Image remover on frontend event submission menu
250    $('#mec_fes_speaker_remove_image_button').click(function(event)
251    {
252        event.preventDefault();
253
254        $('#mec_fes_speaker_thumbnail_img').html('');
255        $('#mec_fes_speaker_thumbnail').val('');
256        $('#mec_fes_speaker_thumbnail_file').val('');
257
258        $('#mec_fes_speaker_remove_image_button').addClass('mec-util-hidden');
259    });
260
261    var date_splite;
262    if(typeof mec_admin_localize !== 'undefined')
263    {
264        date_splite = mec_admin_localize.datepicker_format.split('&');
265        if(date_splite[0] !== undefined && date_splite.length == 2) datepicker_format = date_splite[0];
266    }
267    else if(typeof mecdata !== 'undefined')
268    {
269        date_splite = mecdata.datepicker_format.split( '&' );
270        if(date_splite[0] !== undefined && date_splite.length == 2) datepicker_format = date_splite[0];
271    }
272
273    $('[id^="mec-book-form-btn-step"]').on('click',function()
274    {
275        setTimeout(function()
276        {
277            if($.fn.datepicker)
278            {
279                if('date' !== $('.mec-date-picker').prop('type'))
280                {
281                    $('.mec-date-picker').datepicker(
282                    {
283                        changeYear: true,
284                        changeMonth: true,
285                        dateFormat: datepicker_format,
286                        gotoCurrent: true,
287                        yearRange: 'c-3:c+5',
288                    });
289                }
290
291            }
292        }, 1000);
293    });
294
295    if($.fn.datepicker)
296    {
297        mecEnhanceDatepickerAccessibility($('.mec-date-picker, .mec_date_picker_dynamic_format, .mec_date_picker, .mec_date_picker_dynamic_format_start, .mec_date_picker_dynamic_format_end, #mec_start_date, #mec_end_date, #mec_date_repeat_end_at_date'));
298
299        $('.mec-date-picker').datepicker(
300        {
301            changeYear: true,
302            changeMonth: true,
303            dateFormat: datepicker_format,
304            gotoCurrent: true,
305            yearRange: 'c-3:c+5',
306        });
307
308        $('#mec_start_date').datepicker(
309        {
310            changeYear: true,
311            changeMonth: true,
312            dateFormat: datepicker_format,
313            gotoCurrent: true,
314            yearRange: 'c-3:c+5',
315            onSelect: function(value, i)
316            {
317                const end = $("#mec_end_date");
318                if(value !== i.lastVal)
319                {
320                    end.datepicker("option", "minDate", value);
321                }
322
323                if(end.val() === '')
324                {
325                    end.val(value);
326                }
327
328                // Datepicker doesn't natively fire `change` on the input, so
329                // trigger it to clear inline validation errors (audit issue #1).
330                $(this).trigger('change');
331            }
332        });
333
334        $('#mec_end_date').datepicker(
335        {
336            changeYear: true,
337            changeMonth: true,
338            dateFormat: datepicker_format,
339            gotoCurrent: true,
340            yearRange: 'c-3:c+5',
341            onSelect: function(value, i)
342            {
343                const start = $("#mec_start_date");
344                if(value !== i.lastVal)
345                {
346                    start.datepicker("option", "maxDate", value);
347                }
348
349                if(start.val() === '')
350                {
351                    start.val(value);
352                }
353
354                // Datepicker doesn't natively fire `change` on the input, so
355                // trigger it to clear inline validation errors (audit issue #1).
356                $(this).trigger('change');
357            }
358        });
359
360        // ===== Combobox time control (audit R3) =====
361        // A typeable field; typing filters a dropdown of every 5-minute slot.
362        // The hidden [hour][minutes][ampm] fields stay in sync (looked up LOCALLY
363        // inside the wrapper, so cloned rows — tickets — keep working). Save is
364        // unchanged.
365
366        function mecTimeLabel(h24, min, is12)
367        {
368            var pad = function(n){ return (n < 10 ? '0' : '') + n; };
369            if(is12)
370            {
371                var h12 = h24 % 12; if(h12 === 0) h12 = 12;
372                return pad(h12) + ':' + pad(min) + ' ' + (h24 < 12 ? 'AM' : 'PM');
373            }
374            return pad(h24) + ':' + pad(min);
375        }
376
377        // Write total-minutes-from-midnight into the hidden fields + sync the field.
378        // Used by the duration auto-advance (start/end event time, which have ids).
379        function mecWriteTime(prefix, totalMinutes)
380        {
381            totalMinutes = Math.max(0, Math.min(1439, totalMinutes));
382            var hour24 = Math.floor(totalMinutes / 60);
383            var min = Math.floor((totalMinutes % 60) / 5) * 5;
384            var $h = $('#' + prefix + 'hour');
385            if(!$h.length) return;
386            var $ampm = $('#' + prefix + 'ampm');
387            if($ampm.length)
388            {
389                var h12 = hour24 % 12; if(h12 === 0) h12 = 12;
390                $h.val(h12);
391                $ampm.val(hour24 < 12 ? 'AM' : 'PM');
392            }
393            else $h.val(hour24);
394            $('#' + prefix + 'minutes').val(min);
395            var $w = $('#' + prefix + 'time');
396            if($w.length) $w.val(mecTimeLabel(hour24, min, $ampm.length > 0));
397        }
398
399        function mecTimeInitOne($in)
400            {
401                // Use jQuery .data() (not a class) so that elements cloned via
402                // .html() + .append() (MEC's ticket-add flow) are re-initialized.
403                if(!$in.length || $in.data('mec-time-init')) return;
404                $in.data('mec-time-init', true);
405
406            var $wrap = $in.closest('.mec-time-combo-wrap');
407            if(!$wrap.length) return;
408            var is12 = parseInt($in.data('format'), 10) !== 24;
409            var $dd = $('<div class="mec-time-dd"><ul></ul></div>').appendTo('body');
410            var $list = $dd.find('ul');
411            var $clear = $wrap.find('.mec-time-clear');
412            var hi = -1;
413
414            // Hidden fields are siblings inside the wrapper (local lookup).
415            var $h = $wrap.find('.mec-time-hour');
416            var $m = $wrap.find('.mec-time-minutes');
417            var $ampm = $wrap.find('.mec-time-ampm');
418            if(!$h.length) return; // not a combobox instance
419
420            var options = [];
421            for(var i = 0; i < 288; i++)
422            {
423                var total = i * 5;
424                options.push({total: total, label: mecTimeLabel(Math.floor(total / 60), total % 60, is12)});
425            }
426
427            function toggleClear(){ $clear.toggle(!!$in.val()); }
428
429            function matches(term, label)
430            {
431                term = (term || '').toLowerCase().trim();
432                if(!term) return true;
433                if(label.toLowerCase().indexOf(term) > -1) return true;
434                var td = term.replace(/\D/g, '');
435                return td.length > 0 && label.replace(/\D/g, '').indexOf(td) > -1;
436            }
437
438            function render(term)
439            {
440                $list.empty();
441                options.forEach(function(o){ if(matches(term, o.label)) $('<li/>').text(o.label).data('total', o.total).appendTo($list); });
442                hi = $list.children().length ? 0 : -1;
443                highlight();
444            }
445
446            function highlight()
447            {
448                $list.children().removeClass('mec-time-dd-active');
449                if(hi >= 0) $list.children().eq(hi).addClass('mec-time-dd-active');
450            }
451
452            function move(delta)
453            {
454                var n = $list.children().length;
455                if(!n) return;
456                hi = (hi + delta + n) % n;
457                highlight();
458                var $el = $list.children().eq(hi);
459                if($el.length) $dd.scrollTop($el.position().top + $dd.scrollTop() - 60);
460            }
461
462            function open()
463            {
464                render('');
465                var pos = $in.offset();
466                $dd.css({ left: pos.left + 'px', top: (pos.top + $in.outerHeight() + 2) + 'px', width: $in.outerWidth() + 'px' }).show();
467            }
468            function close(){ $dd.hide(); hi = -1; }
469
470            function writeLocal(total)
471            {
472                var hour24 = Math.floor(total / 60);
473                var min = Math.floor((total % 60) / 5) * 5;
474                if($ampm.length)
475                {
476                    var h12 = hour24 % 12; if(h12 === 0) h12 = 12;
477                    $h.val(h12);
478                    $ampm.val(hour24 < 12 ? 'AM' : 'PM');
479                }
480                else $h.val(hour24);
481                $m.val(min);
482                $in.val(mecTimeLabel(hour24, min, is12));
483            }
484
485            function readLocalH24()
486            {
487                var h24 = parseInt($h.val(), 10); if(isNaN(h24)) h24 = 0;
488                if($ampm.length){ if($ampm.val() === 'PM' && h24 !== 12) h24 += 12; else if($ampm.val() === 'AM' && h24 === 12) h24 = 0; }
489                return h24;
490            }
491
492            function choose(total)
493            {
494                writeLocal(total);
495                $in.trigger('change'); // duration auto-advance (#8)
496                toggleClear();
497                close();
498            }
499
500            toggleClear();
501
502            // "Clear" (x) button: empty the text for retyping (hidden value preserved).
503            $wrap.on('mousedown', '.mec-time-clear', function(e){ e.preventDefault(); $in.val(''); open(); $in.focus(); toggleClear(); });
504
505            $in.on('focus', function(){ open(); });
506            $in.on('click', function(){ if(!$dd.is(':visible')) open(); });
507            $in.on('input', function(){ if(!$dd.is(':visible')) $dd.show(); render($in.val()); toggleClear(); });
508            $in.on('keydown', function(e)
509            {
510                if(e.key === 'ArrowDown'){ e.preventDefault(); if(!$dd.is(':visible')) open(); else move(1); }
511                else if(e.key === 'ArrowUp'){ e.preventDefault(); move(-1); }
512                else if(e.key === 'Enter'){ if(hi >= 0){ e.preventDefault(); var t = $list.children().eq(hi).data('total'); if(t !== undefined) choose(t); } }
513                else if(e.key === 'Escape'){ close(); }
514                else if(e.key === 'Tab'){ close(); }
515            });
516            $list.on('mousedown', 'li', function(e){ e.preventDefault(); choose($(this).data('total')); });
517            $in.on('blur', function()
518            {
519                setTimeout(function()
520                {
521                    close();
522                    var val = ($in.val() || '').trim();
523                    var found = null;
524                    for(var k = 0; k < options.length; k++){ if(options[k].label.toLowerCase() === val.toLowerCase()){ found = options[k]; break; } }
525                    if(found) writeLocal(found.total);
526                    else
527                    {
528                        var h24 = readLocalH24();
529                        var mm = parseInt($m.val(), 10) || 0;
530                        $in.val(mecTimeLabel(h24, mm, is12));
531                    }
532                    toggleClear();
533                }, 150);
534            });
535        }
536
537        function mecInitTimeCombos(){ $('.mec-time-combo').each(function(){ mecTimeInitOne($(this)); }); }
538
539        mecInitTimeCombos();
540
541        // Re-init comboboxes added dynamically (in_days rows, tickets, appointments, popup modal).
542        if(typeof MutationObserver !== 'undefined')
543        {
544            var mecTimeMO = new MutationObserver(function(mutations)
545            {
546                for(var i = 0; i < mutations.length; i++)
547                {
548                    var added = mutations[i].addedNodes;
549                    for(var j = 0; j < added.length; j++)
550                    {
551                        var n = added[j];
552                        if(n.nodeType === 1 && ((n.matches && (n.matches('.mec-time-combo') || n.matches('.mec-hourly-time'))) || (n.querySelector && (n.querySelector('.mec-time-combo') || n.querySelector('.mec-hourly-time')))))
553                        {
554                            setTimeout(function(){ mecInitTimeCombos(); mecInitHourlyTimes(); }, 30);
555                            return;
556                        }
557                    }
558                }
559            });
560            mecTimeMO.observe(document.body, {childList: true, subtree: true});
561        }
562
563        // Close dropdowns when clicking outside (dropdown is in <body>, not the wrap).
564        var $activeDDs = $(); // track dropdowns created by comboboxes
565        $(document).on('mousedown', function(e)
566        {
567            $('.mec-time-dd').each(function()
568            {
569                if(this !== e.target && !this.contains(e.target))
570                {
571                    var $w = $(this);
572                    var $combo = $w.prevAll('.mec-time-combo-wrap').first().find('.mec-time-combo');
573                    // Simple check: if click is not in this dropdown and not in any combo wrap
574                    var inCombo = false;
575                    $('.mec-time-combo-wrap').each(function(){ if(this.contains(e.target)) inCombo = true; });
576                    $('.mec-hourly-time-wrap').each(function(){ if(this.contains(e.target)) inCombo = true; });
577                    if(!inCombo) $w.hide();
578                }
579            });
580        });
581
582        // ===== Hourly schedule string time combobox (audit #5) =====
583        // Same UX as the R3 combobox, but the value lives in a single text
584        // field (the hourly schedule stores "from"/"to" as raw strings echoed
585        // on the front-end), so we read/write the input directly — no hidden
586        // fields, no storage change.
587
588        function mecParseTimeString(str, is12)
589        {
590            str = (str || '').trim().toUpperCase();
591            if(!str) return null;
592            var m = str.match(/^(\d{1,2})[:.\s](\d{1,2})\s*(AM|PM)?$/);
593            if(!m) return null;
594            var h = parseInt(m[1], 10);
595            var min = parseInt(m[2], 10);
596            if(isNaN(h) || isNaN(min) || h < 0 || h > 23 || min < 0 || min > 59) return null;
597            var ap = m[3];
598            if(ap === 'PM' && h !== 12) h += 12;
599            else if(ap === 'AM' && h === 12) h = 0;
600            return h * 60 + min;
601        }
602
603        function mecHourlyTimeInitOne($in)
604        {
605            // Use jQuery .data() (not a class) so dynamically added rows are re-initialized.
606            if(!$in.length || $in.data('mec-time-init')) return;
607            $in.data('mec-time-init', true);
608
609            var $wrap = $in.closest('.mec-hourly-time-wrap');
610            if(!$wrap.length) return;
611            var is12 = parseInt($in.data('format'), 10) !== 24;
612            var $dd = $('<div class="mec-time-dd"><ul></ul></div>').appendTo('body');
613            var $list = $dd.find('ul');
614            var $clear = $wrap.find('.mec-time-clear');
615            var hi = -1;
616
617            var options = [];
618            for(var i = 0; i < 288; i++)
619            {
620                var total = i * 5;
621                options.push({total: total, label: mecTimeLabel(Math.floor(total / 60), total % 60, is12)});
622            }
623
624            function toggleClear(){ $clear.toggle(!!$in.val()); }
625
626            function matches(term, label)
627            {
628                term = (term || '').toLowerCase().trim();
629                if(!term) return true;
630                if(label.toLowerCase().indexOf(term) > -1) return true;
631                var td = term.replace(/\D/g, '');
632                return td.length > 0 && label.replace(/\D/g, '').indexOf(td) > -1;
633            }
634
635            function highlight()
636            {
637                $list.children().removeClass('mec-time-dd-active');
638                if(hi >= 0) $list.children().eq(hi).addClass('mec-time-dd-active');
639            }
640
641            function render(term)
642            {
643                $list.empty();
644                options.forEach(function(o){ if(matches(term, o.label)) $('<li/>').text(o.label).data('total', o.total).appendTo($list); });
645                hi = $list.children().length ? 0 : -1;
646                highlight();
647            }
648
649            function move(delta)
650            {
651                var n = $list.children().length;
652                if(!n) return;
653                hi = (hi + delta + n) % n;
654                highlight();
655                var $el = $list.children().eq(hi);
656                if($el.length) $dd.scrollTop($el.position().top + $dd.scrollTop() - 60);
657            }
658
659            function open()
660            {
661                render('');
662                // Highlight the current value if parseable.
663                var cur = mecParseTimeString($in.val(), is12);
664                if(cur !== null)
665                {
666                    var snapped = Math.floor(cur / 5) * 5;
667                    $list.children().each(function(){ if($(this).data('total') === snapped){ hi = $(this).index(); return false; } });
668                    highlight();
669                }
670                var pos = $in.offset();
671                $dd.css({ left: pos.left + 'px', top: (pos.top + $in.outerHeight() + 2) + 'px', width: $in.outerWidth() + 'px' }).show();
672            }
673            function close(){ $dd.hide(); hi = -1; }
674
675            function choose(total)
676            {
677                var hour24 = Math.floor(total / 60);
678                var min = total % 60;
679                $in.val(mecTimeLabel(hour24, min, is12)).trigger('change');
680                toggleClear();
681                close();
682            }
683
684            toggleClear();
685
686            // "Clear" (x) button: empty the text for retyping.
687            $wrap.on('mousedown', '.mec-time-clear', function(e){ e.preventDefault(); $in.val(''); open(); $in.focus(); toggleClear(); });
688
689            $in.on('focus', function(){ open(); });
690            $in.on('click', function(){ if(!$dd.is(':visible')) open(); });
691            $in.on('input', function(){ if(!$dd.is(':visible')) $dd.show(); render($in.val()); toggleClear(); });
692            $in.on('keydown', function(e)
693            {
694                if(e.key === 'ArrowDown'){ e.preventDefault(); if(!$dd.is(':visible')) open(); else move(1); }
695                else if(e.key === 'ArrowUp'){ e.preventDefault(); move(-1); }
696                else if(e.key === 'Enter'){ if(hi >= 0){ e.preventDefault(); var t = $list.children().eq(hi).data('total'); if(t !== undefined) choose(t); } }
697                else if(e.key === 'Escape'){ close(); }
698                else if(e.key === 'Tab'){ close(); }
699            });
700            $list.on('mousedown', 'li', function(e){ e.preventDefault(); choose($(this).data('total')); });
701            $in.on('blur', function()
702            {
703                setTimeout(function()
704                {
705                    close();
706                    var val = ($in.val() || '').trim();
707                    if(!val){ toggleClear(); return; }
708                    // Snap typed text to the nearest 5-minute slot when parseable.
709                    var total = mecParseTimeString(val, is12);
710                    if(total !== null)
711                    {
712                        var snapped = Math.floor(total / 5) * 5;
713                        $in.val(mecTimeLabel(Math.floor(snapped / 60), snapped % 60, is12));
714                    }
715                    toggleClear();
716                }, 150);
717            });
718        }
719
720        function mecInitHourlyTimes(){ $('.mec-hourly-time').each(function(){ mecHourlyTimeInitOne($(this)); }); }
721
722        mecInitHourlyTimes();
723
724        // ===== Admin event editor: required-field + End >= Start validation =====
725        // Audit issue #1: no required-field markers/validation in the admin editor.
726        (function()
727        {
728            var $postForm = $('#post');
729            var $startDate = $('#mec_start_date');
730            var $endDate = $('#mec_end_date');
731            // Only relevant on the admin event editor (#post form with MEC date fields).
732            if(!$postForm.length || !$startDate.length) return;
733
734            var mecVL = (typeof mec_admin_localize !== 'undefined') ? mec_admin_localize : {};
735            var warningsAcknowledged = false;
736
737            // MEC reusable notice system — modern card with accent bar, type-aware,
738            // dismissible. Supports title, message/items, primary + dismiss buttons.
739            function mecNotice(opts)
740            {
741                var o = $.extend({
742                    type: 'info',          // error | warning | info | success
743                    title: '',
744                    message: '',
745                    items: [],
746                    primaryText: '',
747                    primaryAction: null,
748                    dismissText: '',
749                    container: '#poststuff',
750                    id: 'mec-notice-' + Date.now()
751                }, opts);
752
753                $('.mec-notice-temp').remove();
754
755                var body = '';
756                if(o.title) body += '<div class="mec-notice-title">' + o.title + '</div>';
757                if(o.items && o.items.length) body += '<ul class="mec-notice-list">' + o.items.map(function(i){ return '<li>' + i + '</li>'; }).join('') + '</ul>';
758                else if(o.message) body += '<p class="mec-notice-text">' + o.message + '</p>';
759
760                if(o.primaryText || o.dismissText)
761                {
762                    body += '<div class="mec-notice-actions">';
763                    if(o.primaryText) body += '<button type="button" class="mec-notice-btn mec-notice-btn--primary mec-notice-primary">' + o.primaryText + '</button>';
764                    if(o.dismissText) body += '<button type="button" class="mec-notice-btn mec-notice-btn--ghost mec-notice-dismiss">' + o.dismissText + '</button>';
765                    body += '</div>';
766                }
767
768                var $n = $('<div/>', {
769                    id: o.id,
770                    'class': 'mec-notice mec-notice--' + o.type + ' mec-notice-temp',
771                    html: '<div class="mec-notice-accent"></div><div class="mec-notice-body">' + body + '</div><div class="mec-notice-close" aria-label="Close" role="button" tabindex="0">&times;</div>'
772                });
773
774                $(o.container).prepend($n);
775                $('html, body').animate({scrollTop: $n.offset().top - 40}, 200);
776
777                $n.find('.mec-notice-dismiss, .mec-notice-close').on('click', function(){ $n.remove(); });
778                if(o.primaryText)
779                {
780                    $n.find('.mec-notice-primary').on('click', function()
781                    {
782                        if(typeof o.primaryAction === 'function') o.primaryAction();
783                        $n.remove();
784                    });
785                }
786                return $n;
787            }
788
789            function mecVLTab(tabId)
790            {
791                $('#mec_metabox_details .mec-add-event-tabs-link[data-href="' + tabId + '"]').trigger('click');
792            }
793
794            function mecVLTime24(prefix)
795            {
796                var $h = $('#' + prefix + 'hour');
797                if(!$h.length) return {hour: 0, min: 0};
798                var hour = parseInt($h.val(), 10) || 0;
799                var min = ($('#' + prefix + 'minutes').length) ? (parseInt($('#' + prefix + 'minutes').val(), 10) || 0) : 0;
800                var $ampm = $('#' + prefix + 'ampm');
801                if($ampm.length)
802                {
803                    if($ampm.val() === 'PM' && hour !== 12) hour += 12;
804                    else if($ampm.val() === 'AM' && hour === 12) hour = 0;
805                }
806                return {hour: hour, min: min};
807            }
808
809            // Write a 24h time (in total minutes since midnight) back into the
810            // hour/minutes/ampm selects, respecting the active time format.
811            function mecVLSetTime24(prefix, totalMinutes)
812            {
813                if(typeof mecWriteTime === 'function') mecWriteTime(prefix, totalMinutes);
814            }
815
816            function mecVLParseDate($field)
817            {
818                var raw = $.trim($field.val() || '');
819                if(!raw) return null;
820                try
821                {
822                    return $.datepicker.parseDate(datepicker_format, raw);
823                }
824                catch(e)
825                {
826                    var fb = new Date(raw);
827                    return isNaN(fb.getTime()) ? null : fb;
828                }
829            }
830
831            function mecVLShowError($field, message)
832            {
833                $field.addClass('mec-field-invalid').attr('aria-invalid', 'true');
834                var $err = $field.next('.mec-field-error');
835                if(!$err.length)
836                {
837                    $err = $('<span class="mec-field-error" role="alert"></span>');
838                    $field.after($err);
839                }
840                $err.text(message);
841            }
842
843            function mecVLClearError($field)
844            {
845                $field.removeClass('mec-field-invalid').removeAttr('aria-invalid');
846                $field.next('.mec-field-error').remove();
847            }
848
849            // Clear the inline error as soon as the user edits the field.
850            $startDate.add($endDate).on('input change', function() { mecVLClearError($(this)); });
851
852            // Smart end time: when the start time changes, shift the end time to
853            // preserve the event duration (Google Calendar behavior). Audit issue #8.
854            var $startWidget = $('#mec_start_time');
855            if($startWidget.length)
856            {
857                // Combobox mode (R3): the visible field is a text input; capture on
858                // focus, shift on the 'change' the combobox dispatches on 
858selection.
859                var startBefore = null;
860                $startWidget.on('focus', function()
861                {
862                    if($('#mec_allday').is(':checked')) return;
863                    var t = mecVLTime24('mec_start_');
864                    startBefore = t.hour * 60 + t.min;
865                }).on('change', function()
866                {
867                    if(startBefore === null) return;
868                    var t = mecVLTime24('mec_start_');
869                    var now = t.hour * 60 + t.min;
870                    var delta = now - startBefore;
871                    startBefore = now;
872                    if(!delta) return;
873                    var e = mecVLTime24('mec_end_');
874                    mecWriteTime('mec_end_', (e.hour * 60 + e.min) + delta);
875                });
876            }
877            else
878            {
879                // Legacy 3-select mode: focus/change on the selects.
880                var $startSelects = $('#mec_start_hour, #mec_start_minutes, #mec_start_ampm');
881                if($startSelects.length)
882                {
883                    var legacyBefore = null;
884                    $startSelects.on('focus', function()
885                    {
886                        if($('#mec_allday').is(':checked')) return;
887                        var t = mecVLTime24('mec_start_');
888                        legacyBefore = t.hour * 60 + t.min;
889                    }).on('change', function()
890                    {
891                        if($('#mec_allday').is(':checked')) { legacyBefore = null; return; }
892                        if(legacyBefore === null) return;
893                        var t = mecVLTime24('mec_start_');
894                        var now = t.hour * 60 + t.min;
895                        var delta = now - legacyBefore;
896                        legacyBefore = now;
897                        if(delta === 0) return;
898                        var e = mecVLTime24('mec_end_');
899                        mecVLSetTime24('mec_end_', (e.hour * 60 + e.min) + delta);
900                    });
901                }
902            }
903
904            $postForm.on('submit', function(e)
905            {
906                // Clear previous errors before re-validating.
907                mecVLClearError($startDate);
908                mecVLClearError($endDate);
909
910                // Publish-intent detection: enforce on every submit EXCEPT
911                // explicit draft saves and previews. This covers the publish
912                // button, Enter-key submits and any programmatic path — the
913                // old "publishClicked" flag missed keyboard submits.
914                var submitter = (e.originalEvent && e.originalEvent.submitter) ? $(e.originalEvent.submitter) : null;
915                if(submitter && submitter.attr('id') === 'save-post') return;
916                if($('#wp-preview').val() === 'dopreview') return;
917
918                // Undo WordPress core's URL rewrite: post.js appends
919                // "wp-post-new-reload=true" to the address bar on every publish
920                // click of an auto-draft, even when the submit ends up blocked.
921                // Strip it so the misleading state doesn't linger.
922                if(window.history && window.history.replaceState && window.location.href.indexOf('wp-post-new-reload') > -1)
923                {
924                    window.history.replaceState(null, null, window.location.href.replace(/([?&])wp-post-new-reload=true&?/, '$1').replace(/[?&]$/, ''));
925                }
926
927                var startRaw = $.trim($startDate.val() || '');
928                var endRaw = $.trim($endDate.val() || '');
929
930                // 1. Start Date is required.
931                if(!startRaw)
932                {
933                    e.preventDefault();
934                    mecVLTab('mec_meta_box_date_form');
935                    mecVLShowError($startDate, mecVL.start_date_required || 'Please enter a start date for the event.');
936                    $startDate.trigger('focus');
937                    mecNotice({ type:'error', title:'Start date required', message: mecVL.start_date_required || 'Please enter a start date for the event.', dismissText:'Got it' });
938                    return;
939                }
940
941                // 2. End must be on or after Start.
942                if(endRaw)
943                {
944                    var sDate = mecVLParseDate($startDate);
945                    var eDate = mecVLParseDate($endDate);
946                    if(sDate && eDate)
947                    {
948                        var allday = $('#mec_allday').is(':checked');
949                        var sT = allday ? {hour: 0, min: 0} : mecVLTime24('mec_start_');
950                        var eT = allday ? {hour: 0, min: 0} : mecVLTime24('mec_end_');
951                        var start = new Date(sDate.getFullYear(), sDate.getMonth(), sDate.getDate(), sT.hour, sT.min, 0, 0);
952                        var finish = new Date(eDate.getFullYear(), eDate.getMonth(), eDate.getDate(), eT.hour, eT.min, 0, 0);
953
954                        if(finish.getTime() < start.getTime())
955                        {
956                            e.preventDefault();
957                            mecVLTab('mec_meta_box_date_form');
958                            mecVLShowError($endDate, mecVL.end_after_start || 'The end date and time must be on or after the start date and time.');
959                            $endDate.trigger('focus');
960                            mecNotice({ type:'error', title:'End is before start', message: mecVL.end_after_start || 'The end date and time must be on or after the start date and time.', dismissText:'Got it' });
961                            return;
962                        }
963                    }
964                }
965
966                // ===== R5: Extended pre-publish checks =====
967                var warnings = [];
968                var $bookingBox = $('#mec_metabox_booking');
969                $('.mec-notice-temp').remove();
970
971                // Only treat as "bookable" if there are real tickets (with names).
972                // A blank default ticket row is NOT a real ticket.
973                var $realTickets = $('#mec-tickets input[name*="[name]"]').filter(function(){ return $.trim($(this).val()) !== ''; });
974                var hasTickets = $realTickets.length > 0;
975
976                // --- BLOCKS (prevent publish) ---
977                // B3: certain_weekdays + none checked
978                if($('#mec_repeat').is(':checked') && $('#mec_repeat_type').val() === 'certain_weekdays' && $('input[name="mec[date][repeat][certain_weekdays][]"]:checked').length === 0)
979                {
980                    e.preventDefault(); mecVLTab('mec_meta_box_repeat_form');
981                    mecNotice({ type:'error', title:'No weekdays selected', message:'Repeating is set to "Certain Weekdays" but no weekdays are checked.', dismissText:'Got it' });
982                    return;
983                }
984                // B4: occurrences = 0
985                var $endRadio = $('#mec_repeat_end_type').length ? $('#mec_repeat_end_type') : $('input[name="mec[date][repeat][end]"]:checked');
986                if($endRadio.val() === 'occurrences')
987                {
988                    var occ = parseInt($('#mec_date_repeat_end_at_occurrences').val(), 10);
989                    if(isNaN(occ) || occ <= 0)
990                    {
991                        e.preventDefault(); mecVLTab('mec_meta_box_repeat_form');
992                        mecNotice({ type:'error', title:'Invalid occurrence count', message:'The number of occurrences must be at least 1.', dismissText:'Got it' });
993                        return;
994                    }
995                }
996
997                if(parseInt(mecVL.publish_warnings_status, 10) === 0) return;
998
999                // --- WARNINGS (allow publish with confirmation) ---
1000                // Booking-specific warnings only if there are real tickets
1001                if(hasTickets)
1002                {
1003                    // W2: ticket with name but no price
1004                    $realTickets.each(function(){
1005                        var $name = $(this);
1006                        var $row = $name.closest('.mec-ticket-start-time, .wn-ticket-time').parent();
1007                        var $price = $row.find('input[name*="price"]');
1008                        if(!$price.length) $price = $name.closest('[class*="ticket"]').find('input[name*="price"]');
1009                        if($price.length && $.trim($price.val()) === '') warnings.push('A ticket has a name but no price set.');
1010                    });
1011                    // W3: cost empty on bookable event with tickets
1012                    if($.trim($('#mec_cost').val()) === '' && !$('#mec_cost_is_free').is(':checked') && !$('#mec_cost_auto_calculate').is(':checked')) warnings.push('This event has tickets but no cost is displayed.');
1013                }
1014                // W4: no featured image
1015                if(!$('#set-post-thumbnail img').length && !$('#postimagediv img').length) warnings.push('No featured image set.');
1016                // W5: no content
1017                var c = (typeof tinymce !== 'undefined' && tinymce.get('content')) ? tinymce.get('content').getContent() : ($('#content').val() || '');
1018                if($.trim(c.replace(/<[^>]*>/g, '')) === '') warnings.push('No event description/content.');
1019                // W6: no location
1020                var lv = $('#mec_location_id').val(); if(!lv || lv === '0' || lv === '1') warnings.push('No location set.');
1021                // W7: no organizer
1022                var ov = $('#mec_organizer_id').val(); if(!ov || ov === '0' || ov === '1') warnings.push('No organizer set.');
1023                // W8: occurrences <= 1
1024                if($endRadio.val() === 'occurrences'){ var ow = parseInt($('#mec_date_repeat_end_at_occurrences').val(), 10); if(!isNaN(ow) && ow <= 1) warnings.push('Repeat ends after only ' + ow + ' occurrence(s).'); }
1025                // W10: zero duration
1026                if(!$('#mec_allday').is(':checked')){ var sT = mecVLTime24('mec_start_'), eT = mecVLTime24('mec_end_'); if(sT && eT && sT.hour === eT.hour && sT.min === eT.min) warnings.push('Start and end times are identical (zero duration).'); }
1027
1028                if(!warningsAcknowledged && warnings.length > 0)
1029                {
1030                    e.preventDefault();
1031                    mecNotice({
1032                        type:'warning',
1033                        title:'Publishing with warnings',
1034                        items: warnings,
1035                        primaryText:'Publish anyway',
1036                        primaryAction: function(){ warningsAcknowledged = true; $('#publish').trigger('click'); },
1037                        dismissText:'Go back and fix'
1038                    });
1039                    return;
1040                }
1041            });
1042        })();
1043
1044        // ===== Unsaved-changes protection for MEC meta boxes =====
1045        // Audit issue #10. Warn before navigating away with unsaved MEC edits.
1046        (function()
1047        {
1048            var mecDirty = false;
1049            var $scope = $('#mec_metabox_details, #mec_metabox_booking, #mec_metabox_visibility, #mec_metabox_gallery');
1050
1051            // Ignore spurious change/input events fired by 3rd-party init scripts
1052            // (nice-select, select2, color picker, etc.) before the user interacts.
1053            var mecUserInteracted = false;
1054            $('#post').one('mousedown keydown', function() { mecUserInteracted = true; });
1055
1056            $scope.on('change input', 'input, select, textarea', function() {
1057                if(mecUserInteracted) mecDirty = true;
1058            });
1059
1060            // Clear the dirty flag when the post actually submits. Must run AFTER
1061            // the validation handler so preventDefault (invalid form) keeps the flag.
1062            $('#post').on('submit', function(e)
1063            {
1064                if(!e.isDefaultPrevented()) mecDirty = false;
1065            });
1066
1067            $(window).on('beforeunload', function(e)
1068            {
1069                if(mecDirty)
1070                {
1071                    e.preventDefault();
1072                    e.returnValue = '';
1073                    return '';
1074                }
1075            });
1076        })();
1077
1078        $('#mec_date_repeat_end_at_date').datepicker(
1079        {
1080            changeYear: true,
1081            changeMonth: true,
1082            dateFormat: datepicker_format,
1083            gotoCurrent: true,
1084            yearRange: 'c-3:c+5',
1085        });
1086
1087        $('.mec_date_picker_dynamic_format').datepicker(
1088        {
1089            changeYear: true,
1090            changeMonth: true,
1091            dateFormat: datepicker_format,
1092            gotoCurrent: true,
1093            yearRange: 'c-3:c+5',
1094        });
1095
1096        // End Repeat: conditional fields driven by the type <select> (was radios).
1097        // Date field shows only for "date", occurrences field only for "occurrences".
1098        $('#mec_repeat_end_type').on('change', function()
1099        {
1100            var v = $(this).val();
1101            $('.mec-end-repeat-date-wrap').toggleClass('mec-is-hidden', v !== 'date');
1102            $('.mec-end-repeat-occurrences-wrap').toggleClass('mec-is-hidden', v !== 'occurrences');
1103        });
1104
1105        $('.mec_date_picker').datepicker(
1106        {
1107            changeYear: true,
1108            changeMonth: true,
1109            dateFormat: 'yy-mm-dd',
1110            gotoCurrent: true,
1111            yearRange: 'c-3:c+5',
1112        });
1113
1114        $('.mec_date_picker_dynamic_format_start').datepicker(
1115        {
1116            changeYear: true,
1117            changeMonth: true,
1118            dateFormat: datepicker_format,
1119            gotoCurrent: true,
1120            yearRange: 'c-1:c+5',
1121            onSelect: function(date)
1122            {
1123                var selectedDate;
1124
1125                try
1126                {
1127                    selectedDate = $.datepicker.parseDate(datepicker_format, date);
1128                }
1129                catch(e)
1130                {
1131                    selectedDate = new Date(date);
1132                }
1133
1134                if(!(selectedDate instanceof Date) || isNaN(selectedDate.getTime())) return;
1135
1136                var endDate = new Date(selectedDate.getTime());
1137
1138                var $end_picker = $(this).next();
1139                $end_picker.datepicker("option", "minDate", endDate);
1140                $end_picker.datepicker("option", "maxDate", '+5y');
1141
1142                $(this).trigger('change');
1143            }
1144        });
1145
1146        $('.mec_date_picker_dynamic_format_end').datepicker(
1147        {
1148            changeYear: true,
1149            changeMonth: true,
1150            dateFormat: datepicker_format,
1151            gotoCurrent: true,
1152            yearRange: 'c-1:c+5',
1153        });
1154
1155        trigger_period_picker();
1156    }
1157
1158    // Initialize WP Color Picker
1159    if($.fn.wpColorPicker) jQuery('.mec-color-picker').wpColorPicker();
1160
1161    $('#mec_location_id').on('change', function()
1162    {
1163        mec_location_toggle();
1164
1165        // Mirror of the Other Organizers behavior: "Other Locations" is only
1166        // available once a real main location is selected (not "Hide location" = 1).
1167        var mec_location_val = parseInt($(this).val());
1168        if(mec_location_val != 1) $('#mec-additional-location-wrap').show();
1169        else $('#mec-additional-location-wrap').hide();
1170    });
1171
1172    $('#mec_organizer_id').on('change', function()
1173    {
1174        mec_organizer_toggle();
1175        var mec_organizer_val = parseInt($(this).val());
1176
1177        // Direct ID lookup — the wrap is a sibling of the select's row, so
1178        // the old ".parent().parent().find(...)" traversal never reached it.
1179        if(mec_organizer_val != 1) $('#mec-additional-organizer-wrap').show();
1180        else $('#mec-additional-organizer-wrap').hide();
1181    });
1182
1183    mec_location_toggle();
1184    mec_organizer_toggle();
1185
1186    $('#mec_repeat').on('change', function()
1187    {
1188        mec_repeat_toggle();
1189    });
1190
1191    mec_repeat_toggle();
1192
1193    $('#mec_repeat_type').on('change', function()
1194    {
1195        mec_repeat_type_toggle();
1196    });
1197
1198    mec_repeat_type_toggle();
1199
1200    // "This event is free" toggle: sets the cost field to 0 (which the frontend
1201    // renders as "Free") and locks it, preserving the previous value to restore.
1202    // Audit issue #15.
1203    (function()
1204    {
1205        var $toggle = $('#mec_cost_is_free');
1206        var $cost = $('#mec_cost');
1207        if(!$toggle.length || !$cost.length) return;
1208
1209        function applyFree()
1210        {
1211            if($toggle.is(':checked'))
1212            {
1213                $cost.data('prev-cost', $cost.val());
1214                $cost.val('0').prop('readonly', true).addClass('mec-cost-locked');
1215            }
1216            else
1217            {
1218                $cost.prop('readonly', false).removeClass('mec-cost-locked');
1219                var prev = $cost.data('prev-cost');
1220                if(typeof prev !== 'undefined') $cost.val(prev);
1221            }
1222        }
1223
1224        $toggle.on('change', applyFree);
1225        applyFree();
1226    })();
1227
1228    // ===== Accessibility: ARIA tabs + keyboard nav + focusable tooltips =====
1229    // Audit issue #3. Enhances the existing tab markup in place (no PHP changes).
1230    function mecEnhanceTabs(cfg)
1231    {
1232        var $ctx = $(cfg.context);
1233        if(!$ctx.length) return;
1234        var $tablist = $ctx.find(cfg.tablist);
1235        var $tabs = $ctx.find(cfg.tab);
1236        var $panels = $ctx.find(cfg.panel);
1237        if(!$tabs.length) return;
1238
1239        $tablist.attr('role', 'tablist');
1240
1241        $tabs.each(function() {
1242            var $t = $(this);
1243            var panelId = $t.attr('data-href');
1244            var tabId = $t.attr('id') || (panelId + '-tab');
1245            var active = $t.hasClass('mec-tab-active');
1246            $t.attr({
1247                'id': tabId,
1248                'role': 'tab',
1249                'aria-controls': panelId,
1250                'aria-selected': active ? 'true' : 'false',
1251                'tabindex': active ? '0' : '-1'
1252            });
1253        });
1254
1255        $panels.each(function() {
1256            var $p = $(this);
1257            $p.attr({
1258                'role': 'tabpanel',
1259                'aria-labelledby': $p.attr('id') + '-tab',
1260                'aria-hidden': $p.hasClass('mec-tab-active') ? 'false' : 'true'
1261            });
1262        });
1263
1264        function syncTabs()
1265        {
1266            $tabs.each(function() {
1267                var $t = $(this);
1268                var active = $t.hasClass('mec-tab-active');
1269                $t.attr('aria-selected', active ? 'true' : 'false');
1270                $t.attr('tabindex', active ? '0' : '-1');
1271            });
1272            $panels.each(function() {
1273                $(this).attr('aria-hidden', $(this).hasClass('mec-tab-active') ? 'false' : 'true');
1274            });
1275        }
1276
1277        // Keep ARIA state in sync after the existing inline click handlers switch tabs.
1278        $tablist.on('click', cfg.tab, function() { syncTabs(); });
1279
1280        // Keyboard navigation (automatic activation): arrows + Home/End.
1281        $tablist.on('keydown', cfg.tab, function(e) {
1282            var idx = $tabs.index(this);
1283            var $target = null;
1284            switch(e.key) {
1285                case 'ArrowRight': case 'ArrowDown': $target = $tabs.eq(idx + 1); break;
1286                case 'ArrowLeft': case 'ArrowUp': $target = $tabs.eq(idx - 1); break;
1287                case 'Home': $target = $tabs.first(); break;
1288                case 'End': $target = $tabs.last(); break;
1289                case 'Enter': case ' ':
1290                    e.preventDefault();
1291                    $(this).trigger('click');
1292                    this.focus();
1293                    return;
1294                default: return;
1295            }
1296            if($target && $target.length) {
1297                e.preventDefault();
1298                $target.trigger('click'); // activate (existing handler runs, then syncTabs)
1299                $target[0].focus();
1300            }
1301        });
1302    }
1303
1304    mecEnhanceTabs({
1305        context: '#mec_metabox_details',
1306        tablist: '.mec-add-event-tabs-left',
1307        tab: '.mec-add-event-tabs-link',
1308        panel: '.mec-event-tab-content'
1309    });
1310    mecEnhanceTabs({
1311        context: '#mec_metabox_booking',
1312        tablist: '.mec-add-booking-tabs-left',
1313        tab: '.mec-add-booking-tabs-link',
1314        panel: '.mec-booking-tab-content'
1315    });
1316
1317    // Make hover-only help tooltips keyboard accessible (audit issue #3).
1318    $('.mec-tooltip').each(function(idx) {
1319        var $tip = $(this);
1320        var $box = $tip.find('.box').first();
1321        var $icon = $tip.find('i').first();
1322        if(!$box.length || !$icon.length) return;
1323        var boxId = $box.attr('id') || ('mec-tip-' + idx);
1324        $box.attr('id', boxId);
1325        $icon.attr({
1326            'tabindex': '0',
1327            'role': 'button',
1328            'aria-describedby': boxId
1329        });
1330        if(!$icon.attr('aria-label') && !$icon.attr('title')) {
1331            var title = $tip.find('.title').first().text();
1332            $icon.attr('aria-label', title || 'More information');
1333        }
1334    });
1335
1336    // ===== Recurrence summary + next-5 preview =====
1337    // Audit issue #6. Live, human-readable rule summary + next occurrences,
1338    // computed from the event start date (not "today").
1339    (function()
1340    {
1341        var $box = $('#mec-repeat-preview');
1342        var $body = $box.find('.mec-repeat-preview-body');
1343        var $form = $('#mec_meta_box_repeat_form');
1344        if(!$box.length || !$body.length || !$form.length) return;
1345
1346        var loc = (typeof mec_admin_localize !== 'undefined') ? mec_admin_localize : ((typeof mecdata !== 'undefined') ? mecdata : {});
1347        var units = loc.interval_units || {};
1348        var PREVIEW_MAX = 5;
1349
1350        function parseDate($field)
1351        {
1352            var raw = $.trim($field.val() || '');
1353            if(!raw) return null;
1354            try { return $.datepicker.parseDate(datepicker_format, raw); }
1355            catch(e) { var d = new Date(raw); return isNaN(d.getTime()) ? null : d; }
1356        }
1357
1358        function fmt(date)
1359        {
1360            try { return $.datepicker.formatDate(datepicker_format, date); }
1361            catch(e) { return date.toDateString(); }
1362        }
1363
1364        function addDays(d, n){ var x = new Date(d.getTime()); x.setDate(x.getDate() + n); return x; }
1365        function addMonths(d, n){ var x = new Date(d.getTime()); x.setMonth(x.getMonth() + n); return x; }
1366        function addYears(d, n){ var x = new Date(d.getTime()); x.setFullYear(x.getFullYear() + n); return x; }
1367
1368        // MEC weekday number (1=Mon..7=Sun) -> JS getDay() (0=Sun..6=Sat)
1369        function mecDayToJs(n){ n = parseInt(n, 10); return n === 7 ? 0 : n; }
1370
1371        function readEnd()
1372        {
1373            var end = ($('#mec_repeat_end_type').length ? $('#mec_repeat_end_type').val() : $('input[name="mec[date][repeat][end]"]:checked').val());
1374            var endDate = null, maxOcc = null;
1375            if(end === 'date') endDate = parseDate($('#mec_date_repeat_end_at_date'));
1376            else if(end === 'occurrences')
1377            {
1378                var n = parseInt($('#mec_date_repeat_end_at_occurrences').val(), 10);
1379                if(n > 0) maxOcc = n;
1380            }
1381            return {type: end || 'never', endDate: endDate, maxOcc: maxOcc};
1382        }
1383
1384        function generate(type, interval, start, weekdays, end)
1385        {
1386            var dates = [];
1387            var guard = 0;
1388            // Generate one extra beyond the display cap so the UI can detect
1389            // truncation and show a "+N more" chip. Only PREVIEW_MAX are shown.
1390            var max = end.maxOcc ? Math.min(end.maxOcc, PREVIEW_MAX + 1) : (PREVIEW_MAX + 1);
1391            var endDateMs = end.endDate ? end.endDate.getTime() : null;
1392
1393            function pushIfValid(d)
1394            {
1395                if(endDateMs !== null && d.getTime() > endDateMs) return false;
1396                dates.push(d);
1397                return dates.length < max;
1398            }
1399
1400            if(type === 'daily' || type === 'weekly')
1401            {
1402                var stepDays = (type === 'weekly' ? Math.max(1, interval) * 7 : Math.max(1, interval));
1403                var d = new Date(start.getTime());
1404                while(dates.length < max && guard++ < 10000){ if(!pushIfValid(d)) break; d = addDays(d, stepDays); }
1405            }
1406            else if(type === 'monthly')
1407            {
1408                var n = Math.max(1, interval);
1409                var d = new Date(start.getTime());
1410                while(dates.length < max && guard++ < 10000){ if(!pushIfValid(d)) break; d = addMonths(d, n); }
1411            }
1412            else if(type === 'yearly')
1413            {
1414                var n = Math.max(1, interval);
1415                var d = new Date(start.getTime());
1416                while(dates.length < max && guard++ < 10000){ if(!pushIfValid(d)) break; d = addYears(d, n); }
1417            }
1418            else if(type === 'weekday' || type === 'weekend' || type === 'certain_weekdays')
1419            {
1420                var target;
1421                if(type === 'weekday') target = [1, 2, 3, 4, 5];
1422                else if(type === 'weekend') target = [0, 6];
1423                else target = weekdays.map(mecDayToJs);
1424                var d = new Date(start.getTime());
1425                while(dates.length < max && guard++ < 40000)
1426                {
1427                    if(target.indexOf(d.getDay()) !== -1){ if(!pushIfValid(d)) break; }
1428                    if(endDateMs !== null && d.getTime() > endDateMs) break;
1429                    d = addDays(d, 1);
1430                }
1431            }
1432            return dates;
1433        }
1434
1435        function buildSummary(type, interval, start, weekdays, end)
1436        {
1437            var s = '';
1438            var u = {daily: units.daily || 'days', weekly: units.weekly || 'weeks', monthly: units.monthly || 'months', yearly: units.yearly || 'years'};
1439            if(type === 'daily' || type === 'weekly' || type === 'monthly' || type === 'yearly')
1440            {
1441                var n = Math.max(1, interval);
1442                var unit = u[type];
1443                s = (n === 1) ? ('Every ' + unit.replace(/s$/, '')) : ('Every ' + n + ' ' + unit);
1444            }
1445            else if(type === 'weekday') s = 'Every weekday (Mon-Fri)';
1446            else if(type === 'weekend') s = 'Every weekend (Sat-Sun)';
1447            else if(type === 'certain_weekdays') s = weekdays.length ? ('Weekly on ' + weekdays.length + ' day(s)') : 'No weekdays selected';
1448            else if(type === 'custom_days') s = 'On the custom dates listed above';
1449            else if(type === 'advanced') s = 'Advanced repeating pattern';
1450            else s = type;
1451
1452            if(start) s += ', starting ' + fmt(start);
1453            if(end.type === 'date' && end.endDate) s += ', until ' + fmt(end.endDate);
1454            else if(end.type === 'occurrences' && end.maxOcc) s += ', for ' + end.maxOcc + ' occurrence' + (end.maxOcc === 1 ? '' : 's');
1455            else s += ', no end';
1456            return s;
1457        }
1458
1459        function update()
1460        {
1461            if(!$('#mec_repeat').is(':checked'))
1462            {
1463                $box.hide();
1464                return;
1465            }
1466            $box.show();
1467
1468            var type = $('#mec_repeat_type').val();
1469            var interval = parseInt($('#mec_repeat_interval').val(), 10);
1470            if(isNaN(interval) || interval < 1) interval = 1;
1471            var start = parseDate($('#mec_start_date'));
1472            var weekdays = [];
1473            $('input[name="mec[date][repeat][certain_weekdays][]"]:checked').each(function(){ weekdays.push($(this).val()); });
1474            var end = readEnd();
1475
1476            var html = '<p class="mec-repeat-preview-summary">' + buildSummary(type, interval, start, weekdays, end) + '</p>';
1477
1478            var previewable = ['daily', 'weekly', 'monthly', 'yearly', 'weekday', 'weekend', 'certain_weekdays'];
1479            if(start && previewable.indexOf(type) !== -1)
1480            {
1481                if(type === 'certain_weekdays' && !weekdays.length)
1482                {
1483                    html += '<p class="mec-repeat-preview-empty">' + (loc.repeat_preview_no_weekdays || 'Select weekdays to preview dates.') + '</p>';
1484                }
1485                else
1486                {
1487                    var dates = generate(type, interval, start, weekdays, end);
1488                    var total = '';
1489                    if(end.type === 'occurrences' && end.maxOcc) total = end.maxOcc;
1490                    else if(end.type === 'date' && dates.length < PREVIEW_MAX) total = dates.length;
1491
1492                    if(dates.length)
1493                    {
1494                        var shown = Math.min(dates.length, PREVIEW_MAX);
1495                        var items = dates.slice(0, PREVIEW_MAX).map(function(d){ return '<li>' + fmt(d) + '</li>'; }).join('');
1496                        // Truncation indicator: when more occurrences exist than
1497                        // shown, append a muted chip so the user understands the 5
1498                        // dates are samples (resolves "5 shown vs. N total" confusion).
1499                        if(dates.length > PREVIEW_MAX)
1500                        {
1501                            var moreText = '…';
1502                            if(total !== '' && parseInt(total, 10) > shown)
1503                            {
1504                                moreText = (loc.repeat_preview_more || '+ %d more').replace('%d', parseInt(total, 10) - shown);
1505                            }
1506                            items += '<li class="mec-repeat-preview-more">' + moreText + '</li>';
1507                        }
1508                        html += '<div class="mec-repeat-preview-dates"><span class="mec-repeat-preview-label">' + (loc.repeat_preview_next || 'Next occurrences (from start date)') + ':</span><ul>' + items + '</ul></div>';
1509                    }
1510                    if(total !== '') html += '<span class="mec-repeat-preview-count">' + (loc.repeat_preview_total || 'This event will occur %d times in total.').replace('%d', total) + '</span>';
1511                }
1512            }
1513            else if(!start)
1514            {
1515                html += '<p class="mec-repeat-preview-empty">' + (loc.repeat_preview_need_start || 'Enter a start date to preview dates.') + '</p>';
1516            }
1517
1518            $body.html(html);
1519        }
1520
1521        $form.on('change input', 'input, select', update);
1522        $('#mec_start_date').on('change input', update);
1523        update();
1524    })();
1525
1526    // ===== Currency options live preview (audit #11) =====
1527    // Mirrors main::render_price() so the sample matches the real front-end.
1528    (function()
1529    {
1530        var $preview = $('#mec_currency_preview');
1531        if(!$preview.length) return;
1532
1533        var sample = parseFloat($preview.data('sample'));
1534        if(isNaN(sample)) sample = 1234.5;
1535        var defaultSign = $preview.data('default-sign') || '$';
1536
1537        // Equivalent of PHP number_format(amount, decs, decChar, thouChar).
1538        function format(amount, decs, decChar, thouChar)
1539        {
1540            decs = Math.max(0, parseInt(decs, 10) || 0);
1541            var s = amount.toFixed(decs);
1542            var parts = s.split('.');
1543            parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thouChar);
1544            return parts.length > 1 ? parts[0] + decChar + parts[1] : parts[0];
1545        }
1546
1547        function val($el, fallback)
1548        {
1549            var v = $el.val();
1550            return (v === null || v === '') ? fallback : v;
1551        }
1552
1553        function update()
1554        {
1555            var sign = $.trim($('#mec_currency_currency_symptom').val()) || defaultSign;
1556            var pos = val($('#mec_currency_currency_sign'), 'before');
1557            var thou = val($('#mec_currency_thousand_separator'), ',');
1558            var decChar = val($('#mec_currency_decimal_separator'), '.');
1559            // "No decimal" checkbox is checked => decimal_separator_status = 0 => no decimals.
1560            var noDec = $('#mec_currency_decimal_separator_status').is(':checked');
1561            var decs = noDec ? 0 : Math.max(0, parseInt($('#mec_currency_decimals').val(), 10) || 0);
1562
1563            var num = format(sample, decs, (noDec ? '' : decChar), thou);
1564            var result;
1565            if(pos === 'after') result = num + sign;
1566            else if(pos === 'after_space') result = num + ' ' + sign;
1567            else if(pos === 'before_space') result = sign + ' ' + num;
1568            else result = sign + num;
1569
1570            $preview.text(result);
1571        }
1572
1573        $('#mec_currency_currency_symptom, #mec_currency_currency_sign, #mec_currency_thousand_separator, #mec_currency_decimal_separator, #mec_currency_decimals, #mec_currency_decimal_separator_status').on('input change', update);
1574        update();
1575    })();
1576
1577    // ===== Location duplicate-name guard =====
1578    // Audit issue #14. Warn when a newly typed location name matches an existing one.
1579    (function()
1580    {
1581        var $name = $('#mec_location_name');
1582        var $select = $('#mec_location_id');
1583        if(!$name.length || !$select.length) return;
1584
1585        var loc = (typeof mec_admin_localize !== 'undefined') ? mec_admin_localize : ((typeof mecdata !== 'undefined') ? mecdata : {});
1586
1587        // Build {lowercaseName: termId} from existing location options (exclude the special 0 / 1).
1588        var existing = {};
1589        $select.find('option').each(function()
1590        {
1591            var v = $(this).attr('value');
1592            if(v === '0' || v === '1') return;
1593            existing[$.trim($(this).text()).toLowerCase()] = v;
1594        });
1595
1596        var $warn = null;
1597        function clearWarn(){ if($warn){ $warn.remove(); $warn = null; } }
1598
1599        function showWarn(existingId)
1600        {
1601            $warn = $('<div class="mec-location-duplicate-warn" role="alert"></div>');
1602            $('<span class="mec-location-duplicate-text"></span>')
1603                .text(loc.location_duplicate_warn || 'A location with this name already exists.')
1604                .appendTo($warn);
1605            var $btn = $('<button type="button" class="button mec-location-use-existing"></button>')
1606                .text(loc.location_use_existing || 'Use existing');
1607            $btn.on('click', function()
1608            {
1609                // Switch the select to the existing location; mec_location_toggle() hides the new form.
1610                $select.val(existingId).trigger('change');
1611                clearWarn();
1612            });
1613            $warn.append(' ').append($btn);
1614            $name.closest('.mec-form-row').after($warn);
1615        }
1616
1617        $name.on('input change', function()
1618        {
1619            clearWarn();
1620            var val = $.trim($name.val() || '').toLowerCase();
1621            if(!val) return;
1622            if(existing.hasOwnProperty(val)) showWarn(existing[val]);
1623        });
1624    })();
1625
1626    $('#mec_bookings_limit_unlimited').on('change', function()
1627    {
1628        mec_bookings_unlimited_toggle();
1629    });
1630
1631    $('#mec_add_in_days').on('click', function()
1632    {
1633        var allday = $(this).data('allday');
1634
1635        var start = $('#mec_exceptions_in_days_start_date').val();
1636        if(start === '') return false;
1637
1638        var end = $('#mec_exceptions_in_days_end_date').val();
1639        if(end === '') return false;
1640
1641        var start_hour = $('#mec_exceptions_in_days_start_hour').val();
1642        if(start_hour.length === 1) start_hour = '0'+start_hour;
1643
1644        var start_minutes = $('#mec_exceptions_in_days_start_minutes').val();
1645        if(start_minutes.length === 1) start_minutes = '0'+start_minutes;
1646
1647        var start_ampm = $('#mec_exceptions_in_days_start_ampm').val();
1648        if(typeof start_ampm === 'undefined') start_ampm = '';
1649
1650        var end_hour = $('#mec_exceptions_in_days_end_hour').val();
1651        if(end_hour.length === 1) end_hour = '0'+end_hour;
1652
1653        var end_minutes = $('#mec_exceptions_in_days_end_minutes').val();
1654        if(end_minutes.length === 1) end_minutes = '0'+end_minutes;
1655
1656        var end_ampm = $('#mec_exceptions_in_days_end_ampm').val();
1657        if(typeof end_ampm === 'undefined') end_ampm = '';
1658
1659        var value = start + ':' + end + ':' + start_hour + '-' + start_minutes + '-' + start_ampm + ':' + end_hour + '-' + end_minutes + '-' + end_ampm;
1660        var label = start + ' <span class="mec-time-picker-label '+(allday ? 'mec-util-hidden' : '')+'">' + start_hour + ':' + start_minutes + ' ' + start_ampm + '</span> - ' + end + ' <span class="mec-time-picker-label '+(allday ? 'mec-util-hidden' : '')+'">' + end_hour + ':' + end_minutes + ' ' + end_ampm + '</span>';
1661
1662        // Don't add exactly same occurrences
1663        if($('#mec_in_days input[value="'+value+'"]').length > 0) return false;
1664
1665        var $key = $('#mec_new_in_days_key');
1666
1667        var key = $key.val();
1668        var html = $('#mec_new_in_days_raw').html().replace(/:i:/g, key).replace(/:val:
1668/g, value).replace(/:label:/g, label);
1669
1670        $('#mec_in_days').append(html);
1671        $key.val(parseInt(key)+1);
1672    });
1673
1674    $('#mec_edit_in_days').on('click', function()
1675    {
1676        // Form
1677        const $form = $('#mec-in-days-form');
1678
1679        const modify_id = $form.data('modify');
1680        const $row = $('#mec_in_days_row'+modify_id);
1681
1682        var allday = $(this).data('allday');
1683
1684        var start = $('#mec_exceptions_in_days_start_date').val();
1685        if(start === '') return false;
1686
1687        var end = $('#mec_exceptions_in_days_end_date').val();
1688        if(end === '') return false;
1689
1690        var start_hour = $('#mec_exceptions_in_days_start_hour').val();
1691        if(start_hour.length === 1) start_hour = '0'+start_hour;
1692
1693        var start_minutes = $('#mec_exceptions_in_days_start_minutes').val();
1694        if(start_minutes.length === 1) start_minutes = '0'+start_minutes;
1695
1696        var start_ampm = $('#mec_exceptions_in_days_start_ampm').val();
1697        if(typeof start_ampm === 'undefined') start_ampm = '';
1698
1699        var end_hour = $('#mec_exceptions_in_days_end_hour').val();
1700        if(end_hour.length === 1) end_hour = '0'+end_hour;
1701
1702        var end_minutes = $('#mec_exceptions_in_days_end_minutes').val();
1703        if(end_minutes.length === 1) end_minutes = '0'+end_minutes;
1704
1705        var end_ampm = $('#mec_exceptions_in_days_end_ampm').val();
1706        if(typeof end_ampm === 'undefined') end_ampm = '';
1707
1708        var value = start + ':' + end + ':' + start_hour + '-' + start_minutes + '-' + start_ampm + ':' + end_hour + '-' + end_minutes + '-' + end_ampm;
1709        var label = start + ' <span class="mec-time-picker-label '+(allday ? 'mec-util-hidden' : '')+'">' + start_hour + ':' + start_minutes + ' ' + start_ampm + '</span> - ' + end + ' <span class="mec-time-picker-label '+(allday ? 'mec-util-hidden' : '')+'">' + end_hour + ':' + end_minutes + ' ' + end_ampm + '</span>';
1710
1711        $row.find($('input[type=hidden]')).val(value);
1712        $row.find($('.mec-in-days-day')).html(label);
1713
1714        // Reset Dates
1715        $form.parent().find($('input[type=text]')).val('');
1716
1717        // Modification Mode
1718        $form.removeClass('mec-in-days-edit-mode').addClass('mec-in-days-add-mode').removeData('modify');
1719    });
1720
1721    $('#mec_cancel_in_days').on('click', function()
1722    {
1723        // Form
1724        let $form = $('#mec-in-days-form');
1725
1726        // Reset Dates
1727        $form.parent().find($('input[type=text]')).val('');
1728
1729        // Modification Mode
1730        $form.removeClass('mec-in-days-edit-mode').addClass('mec-in-days-add-mode').removeData('modify');
1731    });
1732
1733    // Keyboard support for Custom Days card action buttons (audit #6).
1734    $('#mec_in_days').on('keydown', '[role="button"]', function(e)
1735    {
1736        if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); this.click(); }
1737    });
1738
1739    $('#mec_add_not_in_days').on('click', function()
1740    {
1741        let date = $('#mec_exceptions_not_in_days_date').val();
1742        if(date === '') return false;
1743
1744        let d = date.replaceAll('-', '');
1745        d = d.replaceAll('/', '');
1746        d = d.replaceAll('.', '');
1747
1748
1749        let $wrapper = $('#mec_not_in_days');
1750        let $key = $('#mec_new_not_in_days_key');
1751
1752        let c = 'mec-date-'+d;
1753        if($wrapper.find($('.'+c)).length) return;
1754
1755        let key = $key.val();
1756        let html = $('#mec_new_not_in_days_raw').html().replace(/:i:/g, key).replace(/:d:/g, d).replace(/:val:/g, date);
1757
1758        $wrapper.append(html);
1759        $key.val(parseInt(key)+1);
1760    });
1761
1762    $('#mec_add_ticket_button').on('click', function()
1763    {
1764        let $key = $('#mec_new_ticket_key');
1765        let key = $key.val();
1766        let html = $('#mec_new_ticket_raw').html().replace(/:i:/g, key);
1767
1768        $('#mec_tickets').append(html);
1769        $key.val(parseInt(key)+1);
1770
1771        mec_init_sortable_sections();
1772
1773        $('.mec_add_price_date_button').off('click').on('click', function()
1774        {
1775            mec_handle_add_price_date_button(this);
1776        });
1777
1778        $.each($(".mec-select2"), function(i,v)
1779        {
1780            if($(v).attr('name').search(":i:") > 0)
1781            {
1782                return;
1783            }
1784
1785            if(typeof $(v).data('select2-id') == 'undefined')
1786            {
1787                $(v).select2();
1788            }
1789        });
1790
1791        trigger_period_picker();
1792    });
1793
1794    $('.mec_add_price_date_button').off('click').on('click', function()
1795    {
1796        mec_handle_add_price_date_button(this);
1797    });
1798
1799    mec_hourly_schedule_add_day_listener();
1800    mec_init_sortable_sections();
1801
1802    $('#mec_add_fee_button').on('click', function()
1803    {
1804        var key = $('#mec_new_fee_key').val();
1805        var html = $('#mec_new_fee_raw').html().replace(/:i:/g, key);
1806
1807        $('#mec_fees_list').append(html);
1808        $('#mec_new_fee_key').val(parseInt(key)+1);
1809
1810        mec_init_sortable_sections();
1811    });
1812
1813    $('#mec_add_ticket_variation_button').on('click', function()
1814    {
1815        var key = $('#mec_new_ticket_variation_key').val();
1816        var html = $('#mec_new_ticket_variation_raw').html().replace(/:i:/g, key);
1817
1818        $('#mec_ticket_variations_list').append(html);
1819        $('#mec_new_ticket_variation_key').val(parseInt(key)+1);
1820
1821        mec_init_sortable_sections();
1822    });
1823
1824    $('.mec-form-row.mec-available-color-row span').on('click', function()
1825    {
1826        $('.mec-form-row.mec-available-color-row span').removeClass('color-selected');
1827        $(this).addClass('color-selected');
1828    });
1829
1830    $('#mec_reg_form_field_types button').on('click', function()
1831    {
1832        var type = $(this).data('type');
1833
1834        if (type == 'mec_email') {
1835            if ($('#mec_reg_form_fields').find('input[value="mec_email"][type="hidden"]').length) {
1836                return false;
1837            }
1838        }
1839
1840        if (type == 'name') {
1841            if ($('#mec_reg_form_fields').find('input[value="name"][type="hidden"]').length) {
1842                return false;
1843            }
1844        }
1845
1846        var key  = $('#mec_new_reg_field_key').val();
1847        var html = $('#mec_reg_field_'+type).html().replace(/:i:/g, key);
1848
1849        $('#mec_reg_form_fields').append(html);
1850        $('#mec_new_reg_field_key').val(parseInt(key)+1);
1851
1852        // Set onclick listener for add option fields
1853        mec_reg_fields_option_listeners();
1854        mec_refresh_booking_condition_editors('reg');
1855    });
1856
1857    // Set onclick listener for add option fields
1858    mec_reg_fields_option_listeners();
1859    mec_refresh_booking_condition_editors('reg');
1860
1861    // Advanced Repeating
1862    $('#mec-advanced-wraper ul > ul > li').click(function()
1863    {
1864        if($(this).attr('class') == '') $(this).attr('class', 'mec-active');
1865        else $(this).attr('class', '');
1866
1867        $('#mec_date_repeat_advanced').val($('#mec-advanced-wraper div:first-child > ul').find('.mec-active').find('span').text().slice(0, -1));
1868    });
1869
1870    $('#mec_event_form_field_types button').on('click', function()
1871    {
1872        var type = $(this).data('type');
1873
1874        var key  = $('#mec_new_event_field_key').val();
1875        var html = $('#mec_event_field_'+type).html().replace(/:i:/g, key);
1876
1877        $('#mec_event_form_fields').append(html);
1878        $('#mec_new_event_field_key').val(parseInt(key)+1);
1879
1880        // Set onclick listener for add option fields
1881        mec_event_fields_option_listeners();
1882    });
1883
1884    // Set onclick listener for add option fields
1885    mec_event_fields_option_listeners();
1886
1887    $('#mec_bfixed_form_field_types button').on('click', function()
1888    {
1889        var type = $(this).data('type');
1890
1891        var key  = $('#mec_new_bfixed_field_key').val();
1892        var html = $('#mec_bfixed_field_'+type).html().replace(/:i:/g, key);
1893
1894        $('#mec_bfixed_form_fields').append(html);
1895        $('#mec_new_bfixed_field_key').val(parseInt(key)+1);
1896
1897        // Set onclick listener for add option fields
1898        mec_bfixed_fields_option_listeners();
1899        mec_refresh_booking_condition_editors('bfixed');
1900    });
1901
1902    // Set onclick listener for add option fields
1903    mec_bfixed_fields_option_listeners();
1904    mec_refresh_booking_condition_editors('bfixed');
1905
1906    jQuery(document).on('change', '.mec-booking-condition-enabled, .mec-booking-condition-source, .mec-booking-condition-option', function()
1907    {
1908        const $trigger = jQuery(this);
1909        const $conditionBox = jQuery(this).closest('.mec-booking-condition-box');
1910        const prefix = String($conditionBox.data('conditionPrefix') || '');
1911
1912        if($trigger.hasClass('mec-booking-condition-source'))
1913        {
1914            $conditionBox.data('currentSourceFieldId', String($trigger.val() || ''));
1915            $conditionBox.data('currentOptionKey', '');
1916            $conditionBox.find('.mec-booking-condition-option').val('');
1917        }
1918        else if($trigger.hasClass('mec-booking-condition-option'))
1919        {
1920            $conditionBox.data('currentOptionKey', String($trigger.val() || ''));
1921        }
1922
1923        mec_refresh_booking_condition_editor($conditionBox);
1924        if(prefix) mec_refresh_booking_condition_editors(prefix);
1925    });
1926
1927    jQuery(document).on('input change', '#mec_reg_form_fields input[name*="[label]"], #mec_bfixed_form_fields input[name*="[label]"]', function()
1928    {
1929        const $field = jQuery(this).closest('li[id^="mec_reg_fields_"], li[id^="mec_bfixed_fields_"]');
1930        if($field.attr('id') && $field.attr('id').indexOf('mec_reg_fields_') === 0) mec_refresh_booking_condition_editors('reg');
1931        if($field.attr('id') && $field.attr('id').indexOf('mec_bfixed_fields_') === 0) mec_refresh_booking_condition_editors('bfixed');
1932    });
1933
1934    // Additional Organizers
1935    mec_additional_organizers_listeners();
1936
1937    // Additional Locations
1938    mec_additional_locations_listeners();
1939
1940    // Show / Hide Password
1941    $('.mec-show-hide-password').on('click', function()
1942    {
1943        var $input = $(this).siblings("input");
1944        var current = $input.attr('type');
1945
1946        if(current === 'password') $input.attr('type', 'text');
1947        else $input.attr('type', 'password');
1948    });
1949
1950    // FAQ
1951    $('#mec_add_faq_button').on('click', function()
1952    {
1953        const $key = $('#mec_new_faq_key');
1954        const key = $key.val();
1955        const html = $('#mec_new_faq_raw').html().replace(/:i:/g, key);
1956
1957        $('#mec_faq_list').append(html);
1958        $key.val(parseInt(key)+1);
1959
1960        mec_init_sortable_sections();
1961    });
1962
1963    // Appointments
1964    // Move the selector to the metabox header
1965    var $header = $('#mec_metabox_details .hndle');
1966    // If .hndle is not found, try .postbox-header (newer WP versions)
1967    if ($header.length === 0) {
1968        $header = $('#mec_metabox_details .postbox-header');
1969    }
1970    $('.mec-event-appointment-type-wrap').appendTo($header).removeClass('mec-util-hidden').on('click', function(e) {
1971        e.stopPropagation();
1972    });
1973
1974    $('#mec_entity_type_select').on('change', function()
1975    {
1976        const entity_type = $(this).val();
1977        const $fes_form = $('#mec_fes_form');
1978        const $metabox_details = $('#mec_metabox_details');
1979        const $appointment_form_wrapper = $('.mec-appointment-form-wrap');
1980
1981        if (entity_type === 'appointment')
1982        {
1983            $fes_form.removeClass('mec-entity-type-event').addClass('mec-entity-type-appointment');
1984            $metabox_details.removeClass('mec-entity-type-event').addClass('mec-entity-type-appointment');
1985
1986            $appointment_form_wrapper.removeClass('mec-util-hidden');
1987        }
1988        else
1989        {
1990            $fes_form.removeClass('mec-entity-type-appointment').addClass('mec-entity-type-event');
1991            $metabox_details.removeClass('mec-entity-type-appointment').addClass('mec-entity-type-event');
1992
1993            $appointment_form_wrapper.addClass('mec-util-hidden');
1994        }
1995    });
1996
1997    $('#mec_entity_type_select').trigger('change');
1998
1999    const $repeatType = $('#mec_appointments_availability_repeat_type');
2000    if($.fn.datepicker)
2001    {
2002        $('#mec_appointments_start_date').datepicker({
2003            changeYear: true,
2004            changeMonth: true,
2005            dateFormat: datepicker_format,
2006            gotoCurrent: true,
2007            yearRange: 'c-3:c+5',
2008        });
2009    }
2010    function mec_toggle_repeat_type()
2011    {
2012        const val = $repeatType.val();
2013        if (val === 'no_repeat')
2014        {
2015            $('.lsd-apt-days-wrapper').addClass('mec-util-hidden');
2016            $('.lsd-apt-adjusted-title').addClass('mec-util-hidden');
2017            $('.lsd-apt-start-date-wrapper').addClass('mec-util-hidden');
2018        }
2019        else
2020        {
2021            $('.lsd-apt-days-wrapper').removeClass('mec-util-hidden');
2022            $('.lsd-apt-adjusted-title').removeClass('mec-util-hidden');
2023            $('.lsd-apt-start-date-wrapper').removeClass('mec-util-hidden');
2024        }
2025    }
2026    $repeatType.on('change', mec_toggle_repeat_type);
2027    mec_toggle_repeat_type();
2028
2029    $(document).on('click', '.lsd-apt-day-icon-remove', function()
2030    {
2031        const $button = $(this);
2032        const $timeslot = $button.parent();
2033        const $timeslots = $button.parents('.lsd-apt-day-timeslots');
2034
2035        $timeslot.remove();
2036        if ($timeslots.find('.lsd-apt-day-timeslot-wrapper').length === 0)
2037        {
2038            $timeslots.find('.lsd-apt-day-timeslots-unavailable').removeClass('mec-util-hidden');
2039        }
2040    });
2041
2042    $(document).on('click', '.lsd-apt-day-icon-plus', function()
2043    {
2044        const $button = $(this);
2045        let key = $button.data('key');
2046
2047        const $day = $button.parents('.lsd-apt-day-wrapper');
2048        const day = $day.data('key');
2049
2050        const $timeslots = $day.find('.lsd-apt-day-timeslots-wrapper');
2051        const $template = $('#lsd-apt-day-templates-'+day+'-timeslot');
2052
2053        key = key + 1;
2054        const html = $template.html().replace(/:t:/g, key);
2055        $timeslots.append(html);
2056
2057        $button.data('key', key);
2058        $day.find('.lsd-apt-day-timeslots-unavailable').addClass('mec-util-hidden');
2059    });
2060
2061    $(document).on('click', '.lsd-apt-day-icon-copy', function ()
2062    {
2063        const $button = $(this);
2064        const $sourceDay = $button.closest('.lsd-apt-day-wrapper');
2065        const sourceDayKey = $sourceDay.data('key');
2066
2067        const $weekWrapper = $sourceDay.parent();
2068        const $sourceTimeslots = $sourceDay.find('.lsd-apt-day-timeslots-wrapper .lsd-apt-day-timeslot-wrapper');
2069
2070        $weekWrapper.find('.lsd-apt-day-wrapper').each(function ()
2071        {
2072            const $targetDay = $(this);
2073
2074            const targetDayKey = $targetDay.data('key');
2075            if (targetDayKey === sourceDayKey) return;
2076
2077            const isActive = $targetDay.find('.lsd-apt-day-timeslots-wrapper .lsd-apt-day-timeslot-wrapper').length > 0;
2078            if (!isActive) return;
2079
2080            const $targetWrapper = $targetDay.find('.lsd-apt-day-timeslots-wrapper');
2081            $targetWrapper.empty();
2082
2083            $sourceTimeslots.each(function ()
2084            {
2085                const $sourceSlot = $(this);
2086                const $clone = $sourceSlot.clone(true);
2087
2088                $sourceSlot.find('select').each(function (i) {
2089                    const value = $(this).val();
2090                    $clone.find('select').eq(i).val(value);
2091                });
2092
2093                $sourceSlot.find('input').each(function (i) {
2094                    const value = $(this).val();
2095                    $clone.find('input').eq(i).val(value);
2096                });
2097
2098                $clone.find('[name], [id], [for]').each(function ()
2099                {
2100                    $.each(this.attributes, function () {
2101                        if (this.name === 'name' || this.name === 'id' || this.name === 'for') {
2102                            this.value = this.value
2103                            .replace(`[availability][${sourceDayKey}]`, `[availability][${targetDayKey}]`)
2104                            .replace(`availability_${sourceDayKey}_`, `availability_${targetDayKey}_`);
2105                        }
2106                    });
2107                });
2108
2109                $targetWrapper.append($clone);
2110            });
2111        });
2112    });
2113
2114    $(document).on('click', '.lsd-apt-adjusted-day-add', function()
2115    {
2116        const $wrapper = $('.lsd-apt-adjusted-days-wrapper');
2117        let key = $wrapper.data('key') || 0;
2118
2119        key = key + 1;
2120        let html = $('#lsd-apt-adjusted-template-day').html();
2121        html = html.replace(/:i:/g, key);
2122
2123        $wrapper.append(html);
2124        $wrapper.data('key', key);
2125
2126        if($.fn.datepicker)
2127        {
2128            $wrapper.find('.mec-apt-date-picker').last().datepicker({
2129                changeYear: true,
2130                changeMonth: true,
2131                dateFormat: datepicker_format,
2132                gotoCurrent: true,
2133                yearRange: 'c-3:c+5',
2134            });
2135        }
2136    });
2137
2138    $(document).on('click', '.lsd-apt-adjusted-day-remove', function()
2139    {
2140        $(this).closest('.lsd-apt-day-wrapper').remove();
2141    });
2142
2143    $(document).on('click', '.lsd-apt-adj-day-icon-plus', function()
2144    {
2145        const $button = $(this);
2146        let key = $button.data('key');
2147        const $day = $button.closest('.lsd-apt-day-wrapper');
2148        const day = $day.data('day');
2149        const $timeslots = $day.find('.lsd-apt-day-timeslots-wrapper');
2150
2151        key = key + 1;
2152        let html = $('#lsd-apt-adjusted-template-timeslot').html();
2153
2154        html = html.replace(/:i:/g, day).replace(/:t:/g, key);
2155        $timeslots.append(html);
2156
2157        $button.data('key', key);
2158        $day.find('.lsd-apt-day-timeslots-unavailable').addClass('mec-util-hidden');
2159    });
2160});
2161
2162function trigger_period_picker()
2163{
2164    jQuery('.mec-date-picker-start').datepicker(
2165    {
2166        changeYear: true,
2167        changeMonth: true,
2168        dateFormat: 'yy-mm-dd',
2169        gotoCurrent: true,
2170        yearRange: 'c-1:c+5',
2171        onSelect: function(date)
2172        {
2173            const selectedDate = new Date(date);
2174            const endDate = new Date(selectedDate.getTime());
2175
2176            const $end_picker = jQuery(this).next();
2177            $end_picker.datepicker("option", "minDate", endDate);
2178            $end_picker.datepicker("option", "maxDate", '+5y');
2179            jQuery(this).trigger('change');
2180        }
2181    });
2182
2183    jQuery('.mec-date-picker-end').datepicker(
2184    {
2185        changeYear: true,
2186        changeMonth: true,
2187        dateFormat: 'yy-mm-dd',
2188        gotoCurrent: true,
2189        yearRange: 'c-1:c+5',
2190    });
2191}
2192
2193function mec_location_toggle()
2194{
2195    if(jQuery('#mec_location_id').val() != '0') jQuery('#mec_location_new_container').hide();
2196    else jQuery('#mec_location_new_container').show();
2197}
2198
2199function mec_organizer_toggle()
2200{
2201    if(jQuery('#mec_organizer_id').val() != '0') jQuery('#mec_organizer_new_container').hide();
2202    else jQuery('#mec_organizer_new_container').show();
2203}
2204
2205function mec_repeat_toggle()
2206{
2207    if(jQuery('#mec_repeat').is(':checked')) jQuery('.mec-form-repeating-event-row').show();
2208    else jQuery('.mec-form-repeating-event-row').hide();
2209}
2210
2211// Update the "Repeat Interval" unit suffix (days/weeks/months/years) based on
2212// the selected repeat type. Audit issue #17.
2213function mec_update_interval_unit()
2214{
2215    var $unit = jQuery('#mec_repeat_interval').siblings('.mec-interval-unit');
2216    if(!$unit.length) return;
2217    var loc = (typeof mec_admin_localize !== 'undefined') ? mec_admin_localize : ((typeof mecdata !== 'undefined') ? mecdata : {});
2218    var units = loc.interval_units || {};
2219    var type = jQuery('#mec_repeat_type').val();
2220    $unit.text(units[type] || '');
2221}
2222
2223function mec_repeat_type_toggle()
2224{
2225    var repeat_type = jQuery('#mec_repeat_type').val();
2226
2227    if(repeat_type == 'certain_weekdays')
2228    {
2229        jQuery('#mec_repeat_interval_container').hide();
2230        jQuery('#mec_repeat_certain_weekdays_container').show();
2231        jQuery('#mec_exceptions_in_days_container').hide();
2232        jQuery('#mec_end_wrapper').show();
2233        jQuery('#mec-advanced-wraper').hide();
2234    }
2235    else if(repeat_type == 'custom_days')
2236    {
2237        jQuery('#mec_repeat_interval_container').hide();
2238        jQuery('#mec_repeat_certain_weekdays_container').hide();
2239        jQuery('#mec_exceptions_in_days_container').show();
2240        jQuery('#mec_end_wrapper').hide();
2241        jQuery('#mec-advanced-wraper').hide();
2242    }
2243    else if(repeat_type == 'advanced')
2244    {
2245        jQuery('#mec_repeat_interval_container').hide();
2246        jQuery('#mec_repeat_certain_weekdays_container').hide();
2247        jQuery('#mec_exceptions_in_days_container').hide();
2248        jQuery('#mec_end_wrapper').show();
2249        jQuery('#mec-advanced-wraper').show();
2250    }
2251    else if(repeat_type != 'daily' && repeat_type != 'weekly' && repeat_type != 'monthly')
2252    {
2253        jQuery('#mec_repeat_interval_container').hide();
2254        jQuery('#mec_repeat_certain_weekdays_container').hide();
2255        jQuery('#mec_exceptions_in_days_container').hide();
2256        jQuery('#mec_end_wrapper').show();
2257        jQuery('#mec-advanced-wraper').hide();
2258    }
2259    else
2260    {
2261        jQuery('#mec_repeat_interval_container').show();
2262        jQuery('#mec_repeat_certain_weekdays_container').hide();
2263        jQuery('#mec_exceptions_in_days_container').hide();
2264        jQuery('#mec_end_wrapper').show();
2265        jQuery('#mec-advanced-wraper').hide();
2266    }
2267
2268    mec_update_interval_unit();
2269}
2270
2271function mec_in_days_remove(i)
2272{
2273    var $row = jQuery('#mec_in_days_row'+i);
2274    // Fade-out animation before removal (audit #6).
2275    if($row.length)
2276    {
2277        $row.addClass('mec-in-days-removing');
2278        setTimeout(function(){ jQuery('#mec_in_days_row'+i).remove(); }, 180);
2279    }
2280}
2281
2282function mec_in_days_edit(i)
2283{
2284    // Date
2285    let $row = jQuery('#mec_in_days_row'+i);
2286    let value = $row.find(jQuery('input[type=hidden]')).val();
2287
2288    const values = value.split(':');
2289    const start_times = values[2].split('-')
2290    const end_times = values[3].split('-')
2291
2292    // Form
2293    let $form = jQuery('#mec-in-days-form');
2294
2295    // Set Dates
2296    jQuery('#mec_exceptions_in_days_start_date').val(values[0]);
2297    jQuery('#mec_exceptions_in_days_end_date').val(values[1]);
2298
2299    // Set Times
2300    jQuery('#mec_exceptions_in_days_start_hour').val(parseInt(start_times[0]));
2301    jQuery('#mec_exceptions_in_days_start_minutes').val(parseInt(start_times[1]));
2302    jQuery('#mec_exceptions_in_days_start_ampm').val(start_times[2]);
2303
2304    jQuery('#mec_exceptions_in_days_end_hour').val(parseInt(end_times[0]));
2305    jQuery('#mec_exceptions_in_days_end_minutes').val(parseInt(end_times[1]));
2306    jQuery('#mec_exceptions_in_days_end_ampm').val(end_times[2]);
2307
2308    // Modification Mode
2309    $form.removeClass('mec-in-days-add-mode').addClass('mec-in-days-edit-mode').data('modify', i);
2310}
2311
2312function mec_not_in_days_remove(i)
2313{
2314    jQuery('#mec_not_in_days_row'+i).remove();
2315}
2316
2317function mec_bookings_unlimited_toggle()
2318{
2319    jQuery('#mec_bookings_limit').toggleClass('mec-util-hidden');
2320}
2321
2322function mec_hourly_schedule_add_day_listener()
2323{
2324    jQuery('.mec-add-hourly-schedule-day-button').each(function()
2325    {
2326        jQuery(this).off('click').on('click', function()
2327        {
2328            var k = jQuery(this).data('key');
2329            var raw = jQuery(this).data('raw');
2330            var append = jQuery(this).data('append');
2331
2332            var key = jQuery(k).val();
2333            var html = jQuery(raw).html().replace(/:d:/g, key).replace(/:dd:/g, parseInt(key)+1);
2334
2335            jQuery(append).append(html);
2336            jQuery(k).val(parseInt(key)+1);
2337
2338            mec_hourly_schedule_listeners();
2339        });
2340
2341        mec_hourly_schedule_listeners();
2342    });
2343}
2344
2345function mec_bookings_after_occurrence_cancel_listener()
2346{
2347    jQuery('.mec-occurrences-bookings-after-occurrences-cancel').off('change').on('change', function()
2348    {
2349        const $dropdown = jQuery(this);
2350        const value = $dropdown.val();
2351        const $moveWrapper = $dropdown.next();
2352
2353        if(value === 'move' || value === 'move_notify')
2354        {
2355            $moveWrapper.removeClass('w-hidden');
2356        }
2357        else
2358        {
2359            $moveWrapper.addClass('w-hidden');
2360        }
2361    });
2362}
2363
2364function mec_hourly_schedule_listeners()
2365{
2366    jQuery('.mec-add-hourly-schedule-button').off('click').on('click', function()
2367    {
2368        var prefix = jQuery(this).data('prefix');
2369        var day = jQuery(this).data('day');
2370        var $key = jQuery('#'+prefix+'mec_new_hourly_schedule_key'+day);
2371
2372        var key = $key.val();
2373        var html = jQuery('#'+prefix+'mec_new_hourly_schedule_raw'+day).html().replace(/:i:/g, key).replace(/:d:/g, day).replace();
2374        var g_field_id = prefix+"-hourly_schedules-"+day+"-schedules-:i:-description";
2375        var field_id = prefix+"-hourly_schedules-"+day+"-schedules-"+key+"-description";
2376        html = html.replace(g_field_id,field_id);
2377        html = html.replace(':k:',key);
2378        jQuery('#'+prefix+'mec_hourly_schedules'+day).append(html);
2379        $key.val(parseInt(key)+1);
2380
2381        wp.editor.initialize("mec"+field_id,{
2382            tinymce: {
2383                wpautop: true,
2384                plugins : 'charmap colorpicker compat3x directionality fullscreen hr image lists media paste tabfocus textcolor wordpress wpautoresize wpdialogs wpeditimage wpemoji wpgallery wplink wptextpattern wpview',
2385                toolbar1: 'bold italic underline strikethrough | bullist numlist | blockquote hr wp_more | alignleft aligncenter alignright | link unlink | fullscreen | wp_adv',
2386                toolbar2: 'formatselect alignjustify forecolor | pastetext removeformat charmap | outdent indent | undo redo | wp_help'
2387            },
2388            quicktags: true,
2389            mediaButtons: false,
2390        });
2391    });
2392
2393    jQuery(".mec-hourly-schedule-schedules").sortable({
2394        handle: '.mec_field_sort'
2395    });
2396}
2397
2398function mec_hourly_schedule_remove(day, i, prefix)
2399{
2400    jQuery("#"+prefix+"mec_hourly_schedule_row"+day+'_'+i).remove();
2401}
2402
2403function mec_hourly_schedule_day_remove(day, prefix)
2404{
2405    jQuery("#"+prefix+"mec_meta_box_hourly_schedule_day_"+day).remove();
2406}
2407
2408function mec_ticket_remove(i)
2409{
2410    jQuery("#mec_ticket_row"+i).remove();
2411}
2412
2413function mec_set_event_color(color)
2414{
2415    try
2416    {
2417        jQuery("#mec_event_color").wpColorPicker('color', '#'+color);
2418    }
2419    catch(e)
2420    {
2421        jQuery("#mec_event_color").val(color);
2422    }
2423}
2424
2425function mec_remove_fee(key)
2426{
2427    jQuery("#mec_fee_row"+key).remove();
2428}
2429
2430function mec_remove_ticket_variation(key, id_prefix)
2431{
2432    jQuery("#mec_"+id_prefix+"_row"+key).remove();
2433}
2434
2435function add_variation_per_ticket(ticket_id)
2436{
2437    var $input = jQuery('#mec_new_variation_per_ticket_key');
2438
2439    var key = $input.val();
2440    var html = jQuery('#mec_new_variation_per_ticket_raw'+ticket_id).html().replace(/:v:/g, key);
2441
2442    jQuery('#mec_ticket_variations_list'+ticket_id).append(html);
2443    $input.val(parseInt(key)+1);
2444
2445    mec_init_sortable_sections();
2446}
2447
2448function mec_init_sortable_instance(selector, options)
2449{
2450    jQuery(selector).each(function()
2451    {
2452        var $element = jQuery(this);
2453
2454        if(!$element.length) return;
2455
2456        if(typeof $element.data('ui-sortable') === 'undefined')
2457        {
2458            $element.sortable(options);
2459        }
2460        else
2461        {
2462            $element.sortable('refresh');
2463        }
2464    });
2465}
2466
2467function mec_init_sortable_sections()
2468{
2469    if(typeof jQuery.fn.sortable === 'undefined') return;
2470
2471    mec_init_sortable_instance('#mec_tickets', { handle: '.mec_field_sort', items: '> .mec_ticket_row' });
2472    mec_init_sortable_instance('#mec_fees_list', { handle: '.mec_field_sort', items: '> .mec-box' });
2473    mec_init_sortable_instance('[id^="mec_ticket_variations_list"]', { handle: '.mec_field_sort', items: '> .mec_ticket_variation_row' });
2474    mec_init_sortable_instance('#mec_faq_list', { handle: '.mec_field_sort', items: '> .mec_faq_row' });
2475}
2476
2477function mec_get_booking_condition_form_fields(prefix)
2478{
2479    return jQuery('#mec_' + prefix + '_form_fields > li');
2480}
2481
2482function mec_get_booking_condition_field_id($field, prefix)
2483{
2484    return String(($field.attr('id') || '').replace('mec_' + prefix + '_fields_', ''));
2485}
2486
2487function mec_get_booking_condition_field_type($field, prefix)
2488{
2489    const fieldId = mec_get_booking_condition_field_id($field, prefix);
2490    return String($field.find('input[name="mec[' + prefix + '_fields][' + fieldId + '][type]"]').val() || '');
2491}
2492
2493function mec_get_booking_condition_field_label($field, prefix)
2494{
2495    const fieldId = mec_get_booking_condition_field_id($field, prefix);
2496    const $labelInput = $field.find('input[name="mec[' + prefix + '_fields][' + fieldId + '][label]"]').first();
2497    const fallback = jQuery.trim($field.find('.mec_' + prefix + '_field_type').first().text() || '');
2498
2499    return jQuery.trim(($labelInput.val() || fallback || ''));
2500}
2501
2502function mec_get_booking_condition_field_option_list($field, prefix)
2503{
2504    const fieldId = mec_get_booking_condition_field_id($field, prefix);
2505    const options = [];
2506
2507    $field.find('input[name^="mec[' + prefix + '_fields][' + fieldId + '][options]"][name$="[label]"]').each(function()
2508    {
2509        const name = jQuery(this).attr('name') || '';
2510        const match = name.match(/\[options\]\[(.*?)\]\[label\]$/);
2511        if(!match) return;
2512
2513        options.push({
2514            key: String(match[1]),
2515            label: jQuery.trim(jQuery(this).val() || '')
2516        });
2517    });
2518
2519    return options;
2520}
2521
2522function mec_booking_condition_field_has_duplicate_options($field, prefix)
2523{
2524    const seen = {};
2525    const options = mec_get_booking_condition_field_option_list($field, prefix);
2526
2527    for(let i = 0; i < options.length; i++)
2528    {
2529        const label = options[i].label;
2530        if(!label) continue;
2531        if(seen[label]) return true;
2532
2533        seen[label] = true;
2534    }
2535
2536    return false;
2537}
2538
2539function mec_refresh_booking_condition_editor($conditionBox)
2540{
2541    if(!$conditionBox || !$conditionBox.length) return;
2542
2543    const prefix = String($conditionBox.data('conditionPrefix') || '');
2544    const currentFieldId = String($conditionBox.data('conditionFieldId') || '');
2545    const $enabled = $conditionBox.find('.mec-booking-condition-enabled');
2546    const $controls = $conditionBox.find('.mec-booking-condition-controls');
2547    const $source = $conditionBox.find('.mec-booking-condition-source');
2548    const $option = $conditionBox.find('.mec-booking-condition-option');
2549    const $matchType = $conditionBox.find('.mec-booking-condition-match-type');
2550    const $message = $conditionBox.find('.mec-booking-condition-message');
2551    const enabled = $enabled.is(':checked');
2552    const strings = (typeof mec_admin_localize !== 'undefined' && mec_admin_localize.booking_conditions) ? mec_admin_localize.booking_conditions : {};
2553
2554    $controls.toggleClass('mec-util-hidden', !enabled);
2555    if(!enabled)
2556    {
2557        $message.text('');
2558        return;
2559    }
2560
2561    const currentSourceId = String($source.val() || $conditionBox.data('currentSourceFieldId') || '');
2562    const currentOptionKey = String($option.val() || $conditionBox.data('currentOptionKey') || '');
2563    const sourceFields = [];
2564    const sourceMap = {};
2565
2566    mec_get_booking_condition_form_fields(prefix).each(function()
2567    {
2568        const $field = jQuery(this);
2569        const fieldId = mec_get_booking_condition_field_id($field, prefix);
2570        const fieldType = mec_get_booking_condition_field_type($field, prefix);
2571
2572        if(!fieldId) return;
2573        if(fieldId === currentFieldId) return false;
2574        if(jQuery.inArray(fieldType, ['checkbox', 'radio', 'select', 'agreement']) === -1) return;
2575        if(jQuery.inArray(fieldType, ['checkbox', 'radio', 'select']) !== -1 && mec_booking_condition_field_has_duplicate_options($field, prefix)) return;
2576
2577        const sourceField = {
2578            id: fieldId,
2579            type: fieldType,
2580            label: mec_get_booking_condition_field_label($field, prefix)
2581        };
2582
2583        sourceFields.push(sourceField);
2584        sourceMap[fieldId] = sourceField;
2585    });
2586
2587    $source.empty().append('<option value="">' + (strings.select_field || 'Select a field') + '</option>');
2588    jQuery.each(sourceFields, function(index, sourceField)
2589    {
2590        $source.append('<option value="' + sourceField.id + '">' + sourceField.label + '</option>');
2591    });
2592
2593    if(currentSourceId && sourceMap[currentSourceId]) $source.val(currentSourceId);
2594
2595    const selectedSourceId = String($source.val() || '');
2596    const selectedSource = sourceMap[selectedSourceId] || null;
2597
2598    $option.empty();
2599
2600    if(!selectedSource)
2601    {
2602        $matchType.val('');
2603        $option.prop('disabled', true).append('<option value="">' + (strings.select_option || 'Select an option') + '</option>');
2604        $message.text(sourceFields.length ? (strings.select_controller || 'Choose a controller field.') : (strings.no_sources || 'No eligible controller fields are available.'));
2605        $conditionBox.data('currentSourceFieldId', '');
2606        $conditionBox.data('currentOptionKey', '');
2607        return;
2608    }
2609
2610    if(selectedSource.type === 'agreement')
2611    {
2612        $matchType.val('checked');
2613        $option.prop('disabled', true).append('<option value="">' + (strings.checked_state || 'Checked') + '</option>');
2614        $message.text((strings.checked_message || 'This field will show when "%s" is checked.').replace('%s', selectedSource.label));
2615        $conditionBox.data('currentSourceFieldId', selectedSourceId);
2616        $conditionBox.data('currentOptionKey', '');
2617        return;
2618    }
2619
2620    const $sourceField = jQuery('#mec_' + prefix + '_fields_' + selectedSource.id);
2621    const options = mec_get_booking_condition_field_option_list($sourceField, prefix);
2622    const matchType = selectedSource.type === 'checkbox' ? 'contains_option' : 'equals_option';
2623
2624    $matchType.val(matchType);
2625    $option.prop('disabled', false).append('<option value="">' + (strings.select_option || 'Select an option') + '</option>');
2626
2627    jQuery.each(options, function(index, option)
2628    {
2629        $option.append('<option value="' + option.key + '">' + option.label + '</option>');
2630    });
2631
2632    if(currentOptionKey) $option.val(currentOptionKey);
2633
2634    if(!$option.val())
2635    {
2636        $message.text((strings.select_value || 'Choose the value that should reveal this field.').replace('%s', selectedSource.label));
2637    }
2638    else
2639    {
2640        $message.text('');
2641    }
2642
2643    $conditionBox.data('currentSourceFieldId', selectedSourceId);
2644    $conditionBox.data('currentOptionKey', String($option.val() || ''));
2645}
2646
2647function mec_refresh_booking_condition_editors(prefix)
2648{
2649    jQuery('.mec-booking-condition-box[data-condition-prefix="' + prefix + '"]').each(function()
2650    {
2651        mec_refresh_booking_condition_editor(jQuery(this));
2652    });
2653}
2654
2655function mec_reg_fields_option_listeners()
2656{
2657    jQuery('button.mec-reg-field-add-option').off('click').on('click', function()
2658    {
2659        var field_id = jQuery(this).data('field-id');
2660        var key = jQuery('#mec_new_reg_field_option_key_'+field_id).val();
2661        var html = jQuery('#mec_reg_field_option').html().replace(/:i:/g, key).replace(/:fi:/g, field_id);
2662
2663        jQuery('#mec_reg_fields_'+field_id+'_options_container').append(html);
2664        jQuery('#mec_new_reg_field_option_key_'+field_id).val(parseInt(key)+1);
2665        mec_refresh_booking_condition_editors('reg');
2666    });
2667
2668    if(typeof jQuery.fn.sortable !== 'undefined')
2669    {
2670        jQuery("#mec_reg_form_fields").sortable(
2671        {
2672            handle: '.mec_reg_field_sort',
2673            stop: function()
2674            {
2675                mec_refresh_booking_condition_editors('reg');
2676            }
2677        });
2678
2679        jQuery(".mec_reg_fields_options_container").sortable(
2680        {
2681            handle: '.mec_reg_field_option_sort'
2682        });
2683
2684        jQuery(".mec-hourly-schedule-days").sortable(
2685        {
2686            handle: 'h4'
2687        });
2688
2689        jQuery(".mec-hourly-schedule-schedules").sortable({
2690            handle: '.mec_field_sort'
2691        });
2692    }
2693}
2694
2695function mec_reg_fields_option_remove(field_key, key)
2696{
2697    jQuery("#mec_reg_fields_option_"+field_key+"_"+key).remove();
2698    mec_refresh_booking_condition_editors('reg');
2699}
2700
2701function mec_reg_fields_remove(key)
2702{
2703    jQuery("#mec_reg_fields_"+key).remove();
2704    mec_refresh_booking_condition_editors('reg');
2705}
2706
2707function mec_handle_add_price_date_button(e)
2708{
2709    var key = jQuery(e).data('key');
2710    var p = jQuery('#mec_new_ticket_price_key_'+key).val();
2711    var html = jQuery('#mec_new_ticket_price_raw_'+key).html().replace(/:i:/g, key).replace(/:j:/g, p);
2712
2713    jQuery('#mec-ticket-price-dates-'+key).append(html);
2714    jQuery('#mec_new_ticket_price_key_'+key).val(parseInt(p)+1);
2715    jQuery('#mec-ticket-price-dates-'+key+' .new_added').datepicker(
2716    {
2717        changeYear: true,
2718        changeMonth: true,
2719        dateFormat: datepicker_format,
2720        gotoCurrent: true,
2721        yearRange: 'c-3:c+5',
2722    });
2723}
2724
2725function mec_ticket_price_remove(ticket_key, price_key)
2726{
2727    jQuery("#mec_ticket_price_raw_"+ticket_key+"_"+price_key).remove();
2728}
2729
2730function mec_event_fields_option_listeners()
2731{
2732    jQuery('button.mec-event-field-add-option').off('click').on('click', function()
2733    {
2734        var field_id = jQuery(this).data('field-id');
2735        var key = jQuery('#mec_new_event_field_option_key_'+field_id).val();
2736        var html = jQuery('#mec_event_field_option').html().replace(/:i:/g, key).replace(/:fi:/g, field_id);
2737
2738        jQuery('#mec_event_fields_'+field_id+'_options_container').append(html);
2739        jQuery('#mec_new_event_field_option_key_'+field_id).val(parseInt(key)+1);
2740    });
2741
2742    if(typeof jQuery.fn.sortable !== 'undefined')
2743    {
2744        jQuery("#mec_event_form_fields").sortable(
2745        {
2746            handle: '.mec_event_field_sort'
2747        });
2748
2749        jQuery(".mec_event_fields_options_container").sortable(
2750        {
2751            handle: '.mec_event_field_option_sort'
2752        });
2753    }
2754}
2755
2756function mec_event_fields_option_remove(field_key, key)
2757{
2758    jQuery("#mec_event_fields_option_"+field_key+"_"+key).remove();
2759}
2760
2761function mec_event_fields_remove(key)
2762{
2763    jQuery("#mec_event_fields_"+key).remove();
2764}
2765
2766function mec_bfixed_fields_option_listeners()
2767{
2768    jQuery('button.mec-bfixed-field-add-option').off('click').on('click', function()
2769    {
2770        var field_id = jQuery(this).data('field-id');
2771        var key = jQuery('#mec_new_bfixed_field_option_key_'+field_id).val();
2772        var html = jQuery('#mec_bfixed_field_option').html().replace(/:i:/g, key).replace(/:fi:/g, field_id);
2773
2774        jQuery('#mec_bfixed_fields_'+field_id+'_options_container').append(html);
2775        jQuery('#mec_new_bfixed_field_option_key_'+field_id).val(parseInt(key)+1);
2776        mec_refresh_booking_condition_editors('bfixed');
2777    });
2778
2779    if(typeof jQuery.fn.sortable !== 'undefined')
2780    {
2781        jQuery("#mec_bfixed_form_fields").sortable(
2782        {
2783            handle: '.mec_bfixed_field_sort',
2784            stop: function()
2785            {
2786                mec_refresh_booking_condition_editors('bfixed');
2787            }
2788        });
2789
2790        jQuery(".mec_bfixed_fields_options_container").sortable(
2791        {
2792            handle: '.mec_bfixed_field_option_sort'
2793        });
2794    }
2795}
2796
2797function mec_bfixed_fields_option_remove(field_key, key)
2798{
2799    jQuery("#mec_bfixed_fields_option_"+field_key+"_"+key).remove();
2800    mec_refresh_booking_condition_editors('bfixed');
2801}
2802
2803function mec_bfixed_fields_remove(key)
2804{
2805    jQuery("#mec_bfixed_fields_"+key).remove();
2806    mec_refresh_booking_condition_editors('bfixed');
2807}
2808
2809function mec_additional_organizers_listeners()
2810{
2811    jQuery('#mec_additional_organizers_add').off('click').on('click', function()
2812    {
2813        var value = jQuery('.mec-additional-organizers select').val();
2814        if (!value) return;
2815
2816        // Each organizer can be added only once.
2817        if (jQuery('.mec-additional-organizers-list input[name="mec[additional_organizer_ids][]"][value="' + value + '"]').length) return;
2818
2819        var text = jQuery('.mec-additional-organizers select option:selected').text();
2820
2821        var sortLabel = jQuery(this).data('sort-label');
2822        var removeLabel = jQuery(this).data('remove-label');
2823
2824        jQuery('.mec-additional-organizers-list').append('<li><span class="mec-additional-organizer-sort">'+sortLabel+'</span> <span onclick="mec_additional_organizers_remove(this);" class="mec-additional-organizer-remove">'+removeLabel+'</span><input type="hidden" name="mec[additional_organizer_ids][]" value="'+value+'"><span class="mec_orgz_item_name">'+text+'</span></li>');
2825
2826        mec_additional_organizers_listeners();
2827    });
2828
2829    if(typeof jQuery.fn.sortable !== 'undefined')
2830    {
2831        jQuery(".mec-additional-organizers-list").sortable(
2832        {
2833            handle: '.mec-additional-organizer-sort'
2834        });
2835    }
2836}
2837
2838function mec_additional_organizers_remove(element)
2839{
2840    jQuery(element).parent().remove();
2841}
2842
2843function mec_additional_locations_listeners()
2844{
2845    jQuery('#mec_additional_locations_add').off('click').on('click', function()
2846    {
2847        var value = jQuery('.mec-additional-locations select').val();
2848        if (!value) return;
2849
2850        // Each location can be added only once.
2851        if (jQuery('.mec-additional-locations-list input[name="mec[additional_location_ids][]"][value="' + value + '"]').length) return;
2852
2853        var text = jQuery('.mec-additional-locations select option:selected').text();
2854
2855        var sortLabel = jQuery(this).data('sort-label');
2856        var removeLabel = jQuery(this).data('remove-label');
2857
2858        jQuery('.mec-additional-locations-list').append('<li><span class="mec-additional-location-sort">'+sortLabel+'</span> <span onclick="mec_additional_locations_remove(this);" class="mec-additional-location-remove">'+removeLabel+'</span><input type="hidden" name="mec[additional_location_ids][]" value="'+value+'"><span class="mec_loc_item_name">'+text+'</span></li>');
2859
2860        mec_additional_locations_listeners();
2861    });
2862
2863    if(typeof jQuery.fn.sortable !== 'undefined')
2864    {
2865        jQuery(".mec-additional-locations-list").sortable(
2866        {
2867            handle: '.mec-additional-location-sort'
2868        });
2869    }
2870}
2871
2872function mec_additional_locations_remove(element)
2873{
2874    jQuery(element).parent().remove();
2875}
2876
2877function mec_faq_remove(key)
2878{
2879    jQuery("#mec_faq_row"+key).remove();
2880}
2881
2882jQuery(document).on('focus', '.mec_date_picker', function ()
2883{
2884    if (!jQuery(this).hasClass('hasDatepicker'))
2885    {
2886        jQuery(this).datepicker({
2887            changeYear: true,
2888            changeMonth: true,
2889            dateFormat: 'yy-mm-dd',
2890            gotoCurrent: true,
2891            yearRange: 'c-3:c+5',
2892        });
2893    }
2894});
2895
2896// Ticket availability start/end: live-init + open for rows that missed (or
2897// were added after) trigger_period_picker(). Two quirks to handle:
2898// 1) jQuery UI only opens a picker from its own focus handler — one bound
2899//    BEFORE the current focus event — so we must explicitly show() here.
2900// 2) The hidden "#mec_new_ticket_raw" template gets its pickers initialized
2901//    on page load; that serializes a stale "hasDatepicker" class into rows
2902//    cloned from it, and jQuery UI refuses to attach to such elements
2903//    (_connectDatepicker bails on the class). So test for the actual
2904//    instance and strip the stale class before initializing.
2905jQuery(document).on('focus', '.mec-date-picker-start, .mec-date-picker-end', function ()
2906{
2907    var $input = jQuery(this);
2908    if(!jQuery.fn.datepicker) return;
2909
2910    if(!$input.data('datepicker'))
2911    {
2912        $input.removeClass('hasDatepicker');
2913
2914        var isStart = $input.hasClass('mec-date-picker-start');
2915        $input.datepicker({
2916            changeYear: true,
2917            changeMonth: true,
2918            dateFormat: 'yy-mm-dd',
2919            gotoCurrent: true,
2920            yearRange: 'c-1:c+5',
2921            onSelect: isStart ? function(date)
2922            {
2923                var endDate = new Date(date);
2924                var $end_picker = $input.next();
2925                $end_picker.datepicker("option", "minDate", endDate);
2926                $end_picker.datepicker("option", "maxDate", '+5y');
2927                $input.trigger('change');
2928            } : null,
2929        });
2930
2931        // Initialize the sibling end picker eagerly so the start picker's
2932        // onSelect targets a live instance even before end is ever focused.
2933        if(isStart)
2934        {
2935            var $end = $input.next('.mec-date-picker-end');
2936            if($end.length && !$end.data('datepicker'))
2937            {
2938                $end.removeClass('hasDatepicker');
2939                $end.datepicker({
2940                    changeYear: true,
2941                    changeMonth: true,
2942                    dateFormat: 'yy-mm-dd',
2943                    gotoCurrent: true,
2944                    yearRange: 'c-1:c+5',
2945                });
2946            }
2947        }
2948    }
2949
2950    $input.datepicker('show');
2951});
2952
2953// Appointment date pickers (.mec-apt-date-picker): only the "Add a date"
2954// button initializes them, so server-rendered inputs (start date, existing
2955// adjusted days) never get a picker — and day-copy clones carry a stale
2956// "hasDatepicker" class without an instance. Live-init + open on focus,
2957// same pattern as the availability handlers above.
2958jQuery(document).on('focus', '.mec-apt-date-picker', function ()
2959{
2960    var $input = jQuery(this);
2961    if(!jQuery.fn.datepicker) return;
2962
2963    if(!$input.data('datepicker'))
2964    {
2965        $input.removeClass('hasDatepicker');
2966        $input.datepicker({
2967            changeYear: true,
2968            changeMonth: true,
2969            dateFormat: datepicker_format,
2970            gotoCurrent: true,
2971            yearRange: 'c-3:c+5',
2972        });
2973    }
2974
2975    $input.datepicker('show');
2976});
2977
2978// ===== Basic / Advanced toggle (mec-basvanced-toggle) =====
2979// Delegated so it works wherever events.js loads: the admin editor AND the
2980// FES front-end form (backend.js, which used to bind this directly, is not
2981// loaded on the front-end). Replaces the old direct binding in backend.js.
2982jQuery(document).on('click', '.mec-basvanced-toggle .mec-backend-tab-item', function (e)
2983{
2984    e.preventDefault();
2985
2986    var $item = jQuery(this);
2987    if($item.hasClass('mec-b-active-tab')) return;
2988
2989    var $toggle = $item.closest('.mec-basvanced-toggle');
2990    var wrapper = $toggle.data('for');
2991    var method = $toggle.data('method');
2992
2993    $item.parent().find('.mec-b-active-tab').removeClass('mec-b-active-tab');
2994    $item.addClass('mec-b-active-tab');
2995
2996    var $wrapper = jQuery(wrapper);
2997    if(method === 'addition') $wrapper.find('.mec-basvanced-advanced').toggleClass('w-hidden');
2998    else
2999    {
3000        $wrapper.find('.mec-basvanced-basic').toggleClass('w-hidden');
3001        $wrapper.find('.mec-basvanced-advanced').toggleClass('w-hidden');
3002    }
3003});
3004
3005// ===== Publish-validation fallback (crash-isolated) =====
3006// The main validation module lives INSIDE jQuery(document).ready and shares
3007// that callback with a dozen initializers — a runtime error anywhere above
3008// it would silently prevent its submit handler from ever being bound (no
3009// error surfaces at click time; the form simply submits). This top-level,
3010// document-delegated copy guarantees the critical guard — "cannot publish
3011// without a start date" + the wp-post-new-reload URL cleanup — survives any
3012// such failure. It is bound on document, so it runs AFTER the main
3013// form-level handler; if that one already handled the submit it stays
3014// silent (e.isDefaultPrevented()).
3015jQuery(document).on('submit', '#post', function (e)
3016{
3017    if(e.isDefaultPrevented()) return; // the main validation already handled it
3018
3019    // Draft saves and previews are exempt from publish validation.
3020    var submitter = (e.originalEvent && e.originalEvent.submitter) ? jQuery(e.originalEvent.submitter) : null;
3021    if(submitter && submitter.attr('id') === 'save-post') return;
3022    if(jQuery('#wp-preview').val() === 'dopreview') return;
3023
3024    var $startDate = jQuery('#mec_start_date');
3025    if(!$startDate.length) return;
3026
3027    // Undo WP core's address-bar rewrite: wp-admin/js/post.js appends
3028    // "wp-post-new-reload=true" on publish-click of an auto-draft even when
3029    // the submit ends up blocked, leaving a misleading URL behind.
3030    if(window.history && window.history.replaceState && window.location.href.indexOf('wp-post-new-reload') > -1)
3031    {
3032        window.history.replaceState(null, null, window.location.href.replace(/([?&])wp-post-new-reload=true&?/, '$1').replace(/[?&]$/, ''));
3033    }
3034
3035    if(jQuery.trim($startDate.val() || '') === '')
3036    {
3037        e.preventDefault();
3038
3039        // Jump to the Date & Time tab so the field is visible.
3040        jQuery('#mec_metabox_details .mec-add-event-tabs-link[data-href="mec_meta_box_date_form"]').trigger('click');
3041
3042        // Inline error on the field.
3043        $startDate.addClass('mec-field-invalid').attr('aria-invalid', 'true');
3044        var $err = $startDate.next('.mec-field-error');
3045        if(!$err.length) $err = jQuery('<span class="mec-field-error" role="alert"></span>').insertAfter($startDate);
3046        var msg = (typeof mec_admin_localize !== 'undefined' && mec_admin_localize.start_date_required) ? mec_admin_localize.start_date_required : 'Please enter a start date for the event.';
3047        $err.text(msg);
3048
3049        // Notice card — self-contained so it renders even if the main module died.
3050        jQuery('.mec-notice-temp').remove();
3051        var $n = jQuery('<div/>', {
3052            'class': 'mec-notice mec-notice--error mec-notice-temp',
3053            html: '<div class="mec-notice-accent"></div><div class="mec-notice-body"><div class="mec-notice-title">Start date required</div><p class="mec-notice-text">' + msg + '</p><div class="mec-notice-actions"><button type="button" class="mec-notice-btn mec-notice-btn--ghost mec-notice-dismiss">Got it</button></div></div><div class="mec-notice-close" aria-label="Close" role="button" tabindex="0">&times;</div>'
3054        });
3055        jQuery('#poststuff').prepend($n);
3056        jQuery('html, body').animate({scrollTop: $n.offset().top - 40}, 200);
3057        $n.find('.mec-notice-dismiss, .mec-notice-close').on('click', function(){ $n.remove(); });
3058
3059        $startDate.trigger('focus');
3060    }
3061});
3062
3063// ===== Native-required feedback (e.g. required Event Data fields) =====
3064// When a field carries the HTML5 "required" attribute (custom Event Data
3065// fields can), the browser blocks submission BEFORE the submit event fires:
3066// none of the submit handlers above run, so no MEC notice appears and WP
3067// core's publish-click URL rewrite ("wp-post-new-reload=true") lingers in
3068// the address bar. The only signal is the non-bubbling "invalid" event —
3069// captured here on the document so the user still gets MEC-style feedback.
3070document.addEventListener('invalid', function (ev)
3071{
3072    var field = ev.target;
3073    if(!field || !field.form || field.form.id !== 'post') return;
3074
3075    // Undo WP core's address-bar rewrite (same cleanup as the fallback).
3076    if(window.history && window.history.replaceState && window.location.href.indexOf('wp-post-new-reload') > -1)
3077    {
3078        window.history.replaceState(null, null, window.location.href.replace(/([?&])wp-post-new-reload=true&?/, '$1').replace(/[?&]$/, ''));
3079    }
3080
3081    // Friendly field name: its label, aria-label, placeholder or title.
3082    var name = '';
3083    if(field.id)
3084    {
3085        var $l = jQuery('label[for="' + field.id + '"]').first();
3086        if($l.length) name = jQuery.trim($l.text());
3087    }
3088    if(!name) name = field.getAttribute('aria-label') || field.placeholder || field.title || '';
3089
3090    jQuery('.mec-notice-temp').remove();
3091    var msg = name
3092        ? '"' + jQuery('<div/>').text(name).html() + '" ' + (field.validationMessage || 'is required.')
3093        : (field.validationMessage || 'Please fill in the required fields.');
3094
3095    var $n = jQuery('<div/>', {
3096        'class': 'mec-notice mec-notice--error mec-notice-temp',
3097        html: '<div class="mec-notice-accent"></div><div class="mec-notice-body"><div class="mec-notice-title">Required field missing</div><p class="mec-notice-text">' + msg + '</p><div class="mec-notice-actions"><button type="button" class="mec-notice-btn mec-notice-btn--ghost mec-notice-dismiss">Got it</button></div></div><div class="mec-notice-close" aria-label="Close" role="button" tabindex="0">&times;</div>'
3098    });
3099    jQuery('#poststuff').prepend($n);
3100    jQuery('html, body').animate({scrollTop: $n.offset().top - 40}, 200);
3101    $n.find('.mec-notice-dismiss, .mec-notice-close').on('click', function(){ $n.remove(); });
3102}, true);

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.