PageSourceSearch

https://www.brainproducts.com/wp-content/plugins/modern-events-cal…/assets/js/events.js?ver=7.36.4.1789107395

js brainproducts.com collected 2026-09-24 08:58:16 UTC 126,233 bytes, 3,100 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                // --- WARNINGS (allow publish with confirmation) ---
998                // Booking-specific warnings only if there are real tickets
999                if(hasTickets)
1000                {
1001                    // W2: ticket with name but no price
1002                    $realTickets.each(function(){
1003                        var $name = $(this);
1004                        var $row = $name.closest('.mec-ticket-start-time, .wn-ticket-time').parent();
1005                        var $price = $row.find('input[name*="price"]');
1006                        if(!$price.length) $price = $name.closest('[class*="ticket"]').find('input[name*="price"]');
1007                        if($price.length && $.trim($price.val()) === '') warnings.push('A ticket has a name but no price set.');
1008                    });
1009                    // W3: cost empty on bookable event with tickets
1010                    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.');
1011                }
1012                // W4: no featured image
1013                if(!$('#set-post-thumbnail img').length && !$('#postimagediv img').length) warnings.push('No featured image set.');
1014                // W5: no content
1015                var c = (typeof tinymce !== 'undefined' && tinymce.get('content')) ? tinymce.get('content').getContent() : ($('#content').val() || '');
1016                if($.trim(c.replace(/<[^>]*>/g, '')) === '') warnings.push('No event description/content.');
1017                // W6: no location
1018                var lv = $('#mec_location_id').val(); if(!lv || lv === '0' || lv === '1') warnings.push('No location set.');
1019                // W7: no organizer
1020                var ov = $('#mec_organizer_id').val(); if(!ov || ov === '0' || ov === '1') warnings.push('No organizer set.');
1021                // W8: occurrences <= 1
1022                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).'); }
1023                // W10: zero duration
1024                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).'); }
1025
1026                if(!warningsAcknowledged && warnings.length > 0)
1027                {
1028                    e.preventDefault();
1029                    mecNotice({
1030                        type:'warning',
1031                        title:'Publishing with warnings',
1032                        items: warnings,
1033                        primaryText:'Publish anyway',
1034                        primaryAction: function(){ warningsAcknowledged = true; $('#publish').trigger('click'); },
1035                        dismissText:'Go back and fix'
1036                    });
1037                    return;
1038                }
1039            });
1040        })();
1041
1042        // ===== Unsaved-changes protection for MEC meta boxes =====
1043        // Audit issue #10. Warn before navigating away with unsaved MEC edits.
1044        (function()
1045        {
1046            var mecDirty = false;
1047            var $scope = $('#mec_metabox_details, #mec_metabox_booking, #mec_metabox_visibility, #mec_metabox_gallery');
1048
1049            // Ignore spurious change/input events fired by 3rd-party init scripts
1050            // (nice-select, select2, color picker, etc.) before the user interacts.
1051            var mecUserInteracted = false;
1052            $('#post').one('mousedown keydown', function() { mecUserInteracted = true; });
1053
1054            $scope.on('change input', 'input, select, textarea', function() {
1055                if(mecUserInteracted) mecDirty = true;
1056            });
1057
1058            // Clear the dirty flag when the post actually submits. Must run AFTER
1059            // the validation handler so preventDefault (invalid form) keeps the flag.
1060            $('#post').on('submit', function(e)
1061            {
1062                if(!e.isDefaultPrevented()) mecDirty = false;
1063            });
1064
1065            $(window).on('beforeunload', function(e)
1066            {
1067                if(mecDirty)
1068                {
1069                    e.preventDefault();
1070                    e.returnValue = '';
1071                    return '';
1072                }
1073            });
1074        })();
1075
1076        $('#mec_date_repeat_end_at_date').datepicker(
1077        {
1078            changeYear: true,
1079            changeMonth: true,
1080            dateFormat: datepicker_format,
1081            gotoCurrent: true,
1082            yearRange: 'c-3:c+5',
1083        });
1084
1085        $('.mec_date_picker_dynamic_format').datepicker(
1086        {
1087            changeYear: true,
1088            changeMonth: true,
1089            dateFormat: datepicker_format,
1090            gotoCurrent: true,
1091            yearRange: 'c-3:c+5',
1092        });
1093
1094        // End Repeat: conditional fields driven by the type <select> (was radios).
1095        // Date field shows only for "date", occurrences field only for "occurrences".
1096        $('#mec_repeat_end_type').on('change', function()
1097        {
1098            var v = $(this).val();
1099            $('.mec-end-repeat-date-wrap').toggleClass('mec-is-hidden', v !== 'date');
1100            $('.mec-end-repeat-occurrences-wrap').toggleClass('mec-is-hidden', v !== 'occurrences');
1101        });
1102
1103        $('.mec_date_picker').datepicker(
1104        {
1105            changeYear: true,
1106            changeMonth: true,
1107            dateFormat: 'yy-mm-dd',
1108            gotoCurrent: true,
1109            yearRange: 'c-3:c+5',
1110        });
1111
1112        $('.mec_date_picker_dynamic_format_start').datepicker(
1113        {
1114            changeYear: true,
1115            changeMonth: true,
1116            dateFormat: datepicker_format,
1117            gotoCurrent: true,
1118            yearRange: 'c-1:c+5',
1119            onSelect: function(date)
1120            {
1121                var selectedDate;
1122
1123                try
1124                {
1125                    selectedDate = $.datepicker.parseDate(datepicker_format, date);
1126                }
1127                catch(e)
1128                {
1129                    selectedDate = new Date(date);
1130                }
1131
1132                if(!(selectedDate instanceof Date) || isNaN(selectedDate.getTime())) return;
1133
1134                var endDate = new Date(selectedDate.getTime());
1135
1136                var $end_picker = $(this).next();
1137                $end_picker.datepicker("option", "minDate", endDate);
1138                $end_picker.datepicker("option", "maxDate", '+5y');
1139
1140                $(this).trigger('change');
1141            }
1142        });
1143
1144        $('.mec_date_picker_dynamic_format_end').datepicker(
1145        {
1146            changeYear: true,
1147            changeMonth: true,
1148            dateFormat: datepicker_format,
1149            gotoCurrent: true,
1150            yearRange: 'c-1:c+5',
1151        });
1152
1153        trigger_period_picker();
1154    }
1155
1156    // Initialize WP Color Picker
1157    if($.fn.wpColorPicker) jQuery('.mec-color-picker').wpColorPicker();
1158
1159    $('#mec_location_id').on('change', function()
1160    {
1161        mec_location_toggle();
1162
1163        // Mirror of the Other Organizers behavior: "Other Locations" is only
1164        // available once a real main location is selected (not "Hide location" = 1).
1165        var mec_location_val = parseInt($(this).val());
1166        if(mec_location_val != 1) $('#mec-additional-location-wrap').show();
1167        else $('#mec-additional-location-wrap').hide();
1168    });
1169
1170    $('#mec_organizer_id').on('change', function()
1171    {
1172        mec_organizer_toggle();
1173        var mec_organizer_val = parseInt($(this).val());
1174
1175        // Direct ID lookup — the wrap is a sibling of the select's row, so
1176        // the old ".parent().parent().find(...)" traversal never reached it.
1177        if(mec_organizer_val != 1) $('#mec-additional-organizer-wrap').show();
1178        else $('#mec-additional-organizer-wrap').hide();
1179    });
1180
1181    mec_location_toggle();
1182    mec_organizer_toggle();
1183
1184    $('#mec_repeat').on('change', function()
1185    {
1186        mec_repeat_toggle();
1187    });
1188
1189    mec_repeat_toggle();
1190
1191    $('#mec_repeat_type').on('change', function()
1192    {
1193        mec_repeat_type_toggle();
1194    });
1195
1196    mec_repeat_type_toggle();
1197
1198    // "This event is free" toggle: sets the cost field to 0 (which the frontend
1199    // renders as "Free") and locks it, preserving the previous value to restore.
1200    // Audit issue #15.
1201    (function()
1202    {
1203        var $toggle = $('#mec_cost_is_free');
1204        var $cost = $('#mec_cost');
1205        if(!$toggle.length || !$cost.length) return;
1206
1207        function applyFree()
1208        {
1209            if($toggle.is(':checked'))
1210            {
1211                $cost.data('prev-cost', $cost.val());
1212                $cost.val('0').prop('readonly', true).addClass('mec-cost-locked');
1213            }
1214            else
1215            {
1216                $cost.prop('readonly', false).removeClass('mec-cost-locked');
1217                var prev = $cost.data('prev-cost');
1218                if(typeof prev !== 'undefined') $cost.val(prev);
1219            }
1220        }
1221
1222        $toggle.on('change', applyFree);
1223        applyFree();
1224    })();
1225
1226    // ===== Accessibility: ARIA tabs + keyboard nav + focusable tooltips =====
1227    // Audit issue #3. Enhances the existing tab markup in place (no PHP changes).
1228    function mecEnhanceTabs(cfg)
1229    {
1230        var $ctx = $(cfg.context);
1231        if(!$ctx.length) return;
1232        var $tablist = $ctx.find(cfg.tablist);
1233        var $tabs = $ctx.find(cfg.tab);
1234        var $panels = $ctx.find(cfg.panel);
1235        if(!$tabs.length) return;
1236
1237        $tablist.attr('role', 'tablist');
1238
1239        $tabs.each(function() {
1240            var $t = $(this);
1241            var panelId = $t.attr('data-href');
1242            var tabId = $t.attr('id') || (panelId + '-tab');
1243            var active = $t.hasClass('mec-tab-active');
1244            $t.attr({
1245                'id': tabId,
1246                'role': 'tab',
1247                'aria-controls': panelId,
1248                'aria-selected': active ? 'true' : 'false',
1249                'tabindex': active ? '0' : '-1'
1250            });
1251        });
1252
1253        $panels.each(function() {
1254            var $p = $(this);
1255            $p.attr({
1256                'role': 'tabpanel',
1257                'aria-labelledby': $p.attr('id') + '-tab',
1258                'aria-hidden': $p.hasClass('mec-tab-active') ? 'false' : 'true'
1259            });
1260        });
1261
1262        function syncTabs()
1263        {
1264            $tabs.each(function() {
1265                var $t = $(this);
1266                var active = $t.hasClass('mec-tab-active');
1267                $t.attr('aria-selected', active ? 'true' : 'false');
1268                $t.attr('tabindex', active ? '0' : '-1');
1269            });
1270            $panels.each(function() {
1271                $(this).attr('aria-hidden', $(this).hasClass('mec-tab-active') ? 'false' : 'true');
1272            });
1273        }
1274
1275        // Keep ARIA state in sync after the existing inline click handlers switch tabs.
1276        $tablist.on('click', cfg.tab, function() { syncTabs(); });
1277
1278        // Keyboard navigation (automatic activation): arrows + Home/End.
1279        $tablist.on('keydown', cfg.tab, function(e) {
1280            var idx = $tabs.index(this);
1281            var $target = null;
1282            switch(e.key) {
1283                case 'ArrowRight': case 'ArrowDown': $target = $tabs.eq(idx + 1); break;
1284                case 'ArrowLeft': case 'ArrowUp': $target = $tabs.eq(idx - 1); break;
1285                case 'Home': $target = $tabs.first(); break;
1286                case 'End': $target = $tabs.last(); break;
1287                case 'Enter': case ' ':
1288                    e.preventDefault();
1289                    $(this).trigger('click');
1290                    this.focus();
1291                    return;
1292                default: return;
1293            }
1294            if($target && $target.length) {
1295                e.preventDefault();
1296                $target.trigger('click'); // activate (existing handler runs, then syncTabs)
1297                $target[0].focus();
1298            }
1299        });
1300    }
1301
1302    mecEnhanceTabs({
1303        context: '#mec_metabox_details',
1304        tablist: '.mec-add-event-tabs-left',
1305        tab: '.mec-add-event-tabs-link',
1306        panel: '.mec-event-tab-content'
1307    });
1308    mecEnhanceTabs({
1309        context: '#mec_metabox_booking',
1310        tablist: '.mec-add-booking-tabs-left',
1311        tab: '.mec-add-booking-tabs-link',
1312        panel: '.mec-booking-tab-content'
1313    });
1314
1315    // Make hover-only help tooltips keyboard accessible (audit issue #3).
1316    $('.mec-tooltip').each(function(idx) {
1317        var $tip = $(this);
1318        var $box = $tip.find('.box').first();
1319        var $icon = $tip.find('i').first();
1320        if(!$box.length || !$icon.length) return;
1321        var boxId = $box.attr('id') || ('mec-tip-' + idx);
1322        $box.attr('id', boxId);
1323        $icon.attr({
1324            'tabindex': '0',
1325            'role': 'button',
1326            'aria-describedby': boxId
1327        });
1328        if(!$icon.attr('aria-label') && !$icon.attr('title')) {
1329            var title = $tip.find('.title').first().text();
1330            $icon.attr('aria-label', title || 'More information');
1331        }
1332    });
1333
1334    // ===== Recurrence summary + next-5 preview =====
1335    // Audit issue #6. Live, human-readable rule summary + next occurrences,
1336    // computed from the event start date (not "today").
1337    (function()
1338    {
1339        var $box = $('#mec-repeat-preview');
1340        var $body = $box.find('.mec-repeat-preview-body');
1341        var $form = $('#mec_meta_box_repeat_form');
1342        if(!$box.length || !$body.length || !$form.length) return;
1343
1344        var loc = (typeof mec_admin_localize !== 'undefined') ? mec_admin_localize : ((typeof mecdata !== 'undefined') ? mecdata : {});
1345        var units = loc.interval_units || {};
1346        var PREVIEW_MAX = 5;
1347
1348        function parseDate($field)
1349        {
1350            var raw = $.trim($field.val() || '');
1351            if(!raw) return null;
1352            try { return $.datepicker.parseDate(datepicker_format, raw); }
1353            catch(e) { var d = new Date(raw); return isNaN(d.getTime()) ? null : d; }
1354        }
1355
1356        function fmt(date)
1357        {
1358            try { return $.datepicker.formatDate(datepicker_format, date); }
1359            catch(e) { return date.toDateString(); }
1360        }
1361
1362        function addDays(d, n){ var x = new Date(d.getTime()); x.setDate(x.getDate() + n); return x; }
1363        function addMonths(d, n){ var x = new Date(d.getTime()); x.setMonth(x.getMonth() + n); return x; }
1364        function addYears(d, n){ var x = new Date(d.getTime()); x.setFullYear(x.getFullYear() + n); return x; }
1365
1366        // MEC weekday number (1=Mon..7=Sun) -> JS getDay() (0=Sun..6=Sat)
1367        function mecDayToJs(n){ n = parseInt(n, 10); return n === 7 ? 0 : n; }
1368
1369        function readEnd()
1370        {
1371            var end = ($('#mec_repeat_end_type').length ? $('#mec_repeat_end_type').val() : $('input[name="mec[date][repeat][end]"]:checked').val());
1372            var endDate = null, maxOcc = null;
1373            if(end === 'date') endDate = parseDate($('#mec_date_repeat_end_at_date'));
1374            else if(end === 'occurrences')
1375            {
1376                var n = parseInt($('#mec_date_repeat_end_at_occurrences').val(), 10);
1377                if(n > 0) maxOcc = n;
1378            }
1379            return {type: end || 'never', endDate: endDate, maxOcc: maxOcc};
1380        }
1381
1382        function generate(type, interval, start, weekdays, end)
1383        {
1384            var dates = [];
1385            var guard = 0;
1386            // Generate one extra beyond the display cap so the UI can detect
1387            // truncation and show a "+N more" chip. Only PREVIEW_MAX are shown.
1388            var max = end.maxOcc ? Math.min(end.maxOcc, PREVIEW_MAX + 1) : (PREVIEW_MAX + 1);
1389            var endDateMs = end.endDate ? end.endDate.getTime() : null;
1390
1391            function pushIfValid(d)
1392            {
1393                if(endDateMs !== null && d.getTime() > endDateMs) return false;
1394                dates.push(d);
1395                return dates.length < max;
1396            }
1397
1398            if(type === 'daily' || type === 'weekly')
1399            {
1400                var stepDays = (type === 'weekly' ? Math.max(1, interval) * 7 : Math.max(1, interval));
1401                var d = new Date(start.getTime());
1402                while(dates.length < max && guard++ < 10000){ if(!pushIfValid(d)) break; d = addDays(d, stepDays); }
1403            }
1404            else if(type === 'monthly')
1405            {
1406                var n = Math.max(1, interval);
1407                var d = new Date(start.getTime());
1408                while(dates.length < max && guard++ < 10000){ if(!pushIfValid(d)) break; d = addMonths(d, n); }
1409            }
1410            else if(type === 'yearly')
1411            {
1412                var n = Math.max(1, interval);
1413                var d = new Date(start.getTime());
1414                while(dates.length < max && guard++ < 10000){ if(!pushIfValid(d)) break; d = addYears(d, n); }
1415            }
1416            else if(type === 'weekday' || type === 'weekend' || type === 'certain_weekdays')
1417            {
1418                var target;
1419                if(type === 'weekday') target = [1, 2, 3, 4, 5];
1420                else if(type === 'weekend') target = [0, 6];
1421                else target = weekdays.map(mecDayToJs);
1422                var d = new Date(start.getTime());
1423                while(dates.length < max && guard++ < 40000)
1424                {
1425                    if(target.indexOf(d.getDay()) !== -1){ if(!pushIfValid(d)) break; }
1426                    if(endDateMs !== null && d.getTime() > endDateMs) break;
1427                    d = addDays(d, 1);
1428                }
1429            }
1430            return dates;
1431        }
1432
1433        function buildSummary(type, interval, start, weekdays, end)
1434        {
1435            var s = '';
1436            var u = {daily: units.daily || 'days', weekly: units.weekly || 'weeks', monthly: units.monthly || 'months', yearly: units.yearly || 'years'};
1437            if(type === 'daily' || type === 'weekly' || type === 'monthly' || type === 'yearly')
1438            {
1439                var n = Math.max(1, interval);
1440                var unit = u[type];
1441                s = (n === 1) ? ('Every ' + unit.replace(/s$/, '')) : ('Every ' + n + ' ' + unit);
1442            }
1443            else if(type === 'weekday') s = 'Every weekday (Mon-Fri)';
1444            else if(type === 'weekend') s = 'Every weekend (Sat-Sun)';
1445            else if(type === 'certain_weekdays') s = weekdays.length ? ('Weekly on ' + weekdays.length + ' day(s)') : 'No weekdays selected';
1446            else if(type === 'custom_days') s = 'On the custom dates listed above';
1447            else if(type === 'advanced') s = 'Advanced repeating pattern';
1448            else s = type;
1449
1450            if(start) s += ', starting ' + fmt(start);
1451            if(end.type === 'date' && end.endDate) s += ', until ' + fmt(end.endDate);
1452            else if(end.type === 'occurrences' && end.maxOcc) s += ', for ' + end.maxOcc + ' occurrence' + (end.maxOcc === 1 ? '' : 's');
1453            else s += ', no end';
1454            return s;
1455        }
1456
1457        function update()
1458        {
1459            if(!$('#mec_repeat').is(':checked'))
1460            {
1461                $box.hide();
1462                return;
1463            }
1464            $box.show();
1465
1466            var type = $('#mec_repeat_type').val();
1467            var interval = parseInt($('#mec_repeat_interval').val(), 10);
1468            if(isNaN(interval) || interval < 1) interval = 1;
1469            var start = parseDate($('#mec_start_date'));
1470            var weekdays = [];
1471            $('input[name="mec[date][repeat][certain_weekdays][]"]:checked').each(function(){ weekdays.push($(this).val()); });
1472            var end = readEnd();
1473
1474            var html = '<p class="mec-repeat-preview-summary">' + buildSummary(type, interval, start, weekdays, end) + '</p>';
1475
1476            var previewable = ['daily', 'weekly', 'monthly', 'yearly', 'weekday', 'weekend', 'certain_weekdays'];
1477            if(start && previewable.indexOf(type) !== -1)
1478            {
1479                if(type === 'certain_weekdays' && !weekdays.length)
1480                {
1481                    html += '<p class="mec-repeat-preview-empty">' + (loc.repeat_preview_no_weekdays || 'Select weekdays to preview dates.') + '</p>';
1482                }
1483                else
1484                {
1485                    var dates = generate(type, interval, start, weekdays, end);
1486                    var total = '';
1487                    if(end.type === 'occurrences' && end.maxOcc) total = end.maxOcc;
1488                    else if(end.type === 'date' && dates.length < PREVIEW_MAX) total = dates.length;
1489
1490                    if(dates.length)
1491                    {
1492                        var shown = Math.min(dates.length, PREVIEW_MAX);
1493                        var items = dates.slice(0, PREVIEW_MAX).map(function(d){ return '<li>' + fmt(d) + '</li>'; }).join('');
1494                        // Truncation indicator: when more occurrences exist than
1495                        // shown, append a muted chip so the user understands the 5
1496                        // dates are samples (resolves "5 shown vs. N total" confusion).
1497                        if(dates.length > PREVIEW_MAX)
1498                        {
1499                            var moreText = '…';
1500                            if(total !== '' && parseInt(total, 10) > shown)
1501                            {
1502                                moreText = (loc.repeat_preview_more || '+ %d more').replace('%d', parseInt(total, 10) - shown);
1503                            }
1504                            items += '<li class="mec-repeat-preview-more">' + moreText + '</li>';
1505                        }
1506                        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>';
1507                    }
1508                    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>';
1509                }
1510            }
1511            else if(!start)
1512            {
1513                html += '<p class="mec-repeat-preview-empty">' + (loc.repeat_preview_need_start || 'Enter a start date to preview dates.') + '</p>';
1514            }
1515
1516            $body.html(html);
1517        }
1518
1519        $form.on('change input', 'input, select', update);
1520        $('#mec_start_date').on('change input', update);
1521        update();
1522    })();
1523
1524    // ===== Currency options live preview (audit #11) =====
1525    // Mirrors main::render_price() so the sample matches the real front-end.
1526    (function()
1527    {
1528        var $preview = $('#mec_currency_preview');
1529        if(!$preview.length) return;
1530
1531        var sample = parseFloat($preview.data('sample'));
1532        if(isNaN(sample)) sample = 1234.5;
1533        var defaultSign = $preview.data('default-sign') || '$';
1534
1535        // Equivalent of PHP number_format(amount, decs, decChar, thouChar).
1536        function format(amount, decs, decChar, thouChar)
1537        {
1538            decs = Math.max(0, parseInt(decs, 10) || 0);
1539            var s = amount.toFixed(decs);
1540            var parts = s.split('.');
1541            parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thouChar);
1542            return parts.length > 1 ? parts[0] + decChar + parts[1] : parts[0];
1543        }
1544
1545        function val($el, fallback)
1546        {
1547            var v = $el.val();
1548            return (v === null || v === '') ? fallback : v;
1549        }
1550
1551        function update()
1552        {
1553            var sign = $.trim($('#mec_currency_currency_symptom').val()) || defaultSign;
1554            var pos = val($('#mec_currency_currency_sign'), 'before');
1555            var thou = val($('#mec_currency_thousand_separator'), ',');
1556            var decChar = val($('#mec_currency_decimal_separator'), '.');
1557            // "No decimal" checkbox is checked => decimal_separator_status = 0 => no decimals.
1558            var noDec = $('#mec_currency_decimal_separator_status').is(':checked');
1559            var decs = noDec ? 0 : Math.max(0, parseInt($('#mec_currency_decimals').val(), 10) || 0);
1560
1561            var num = format(sample, decs, (noDec ? '' : decChar), thou);
1562            var result;
1563            if(pos === 'after') result = num + sign;
1564            else if(pos === 'after_space') result = num + ' ' + sign;
1565            else if(pos === 'before_space') result = sign + ' ' + num;
1566            else result = sign + num;
1567
1568            $preview.text(result);
1569        }
1570
1571        $('#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);
1572        update();
1573    })();
1574
1575    // ===== Location duplicate-name guard =====
1576    // Audit issue #14. Warn when a newly typed location name matches an existing one.
1577    (function()
1578    {
1579        var $name = $('#mec_location_name');
1580        var $select = $('#mec_location_id');
1581        if(!$name.length || !$select.length) return;
1582
1583        var loc = (typeof mec_admin_localize !== 'undefined') ? mec_admin_localize : ((typeof mecdata !== 'undefined') ? mecdata : {});
1584
1585        // Build {lowercaseName: termId} from existing location options (exclude the special 0 / 1).
1586        var existing = {};
1587        $select.find('option').each(function()
1588        {
1589            var v = $(this).attr('value');
1590            if(v === '0' || v === '1') return;
1591            existing[$.trim($(this).text()).toLowerCase()] = v;
1592        });
1593
1594        var $warn = null;
1595        function clearWarn(){ if($warn){ $warn.remove(); $warn = null; } }
1596
1597        function showWarn(existingId)
1598        {
1599            $warn = $('<div class="mec-location-duplicate-warn" role="alert"></div>');
1600            $('<span class="mec-location-duplicate-text"></span>')
1601                .text(loc.location_duplicate_warn || 'A location with this name already exists.')
1602                .appendTo($warn);
1603            var $btn = $('<button type="button" class="button mec-location-use-existing"></button>')
1604                .text(loc.location_use_existing || 'Use existing');
1605            $btn.on('click', function()
1606            {
1607                // Switch the select to the existing location; mec_location_toggle() hides the new form.
1608                $select.val(existingId).trigger('change');
1609                clearWarn();
1610            });
1611            $warn.append(' ').append($btn);
1612            $name.closest('.mec-form-row').after($warn);
1613        }
1614
1615        $name.on('input change', function()
1616        {
1617            clearWarn();
1618            var val = $.trim($name.val() || '').toLowerCase();
1619            if(!val) return;
1620            if(existing.hasOwnProperty(val)) showWarn(existing[val]);
1621        });
1622    })();
1623
1624    $('#mec_bookings_limit_unlimited').on('change', function()
1625    {
1626        mec_bookings_unlimited_toggle();
1627    });
1628
1629    $('#mec_add_in_days').on('click', function()
1630    {
1631        var allday = $(this).data('allday');
1632
1633        var start = $('#mec_exceptions_in_days_start_date').val();
1634        if(start === '') return false;
1635
1636        var end = $('#mec_exceptions_in_days_end_date').val();
1637        if(end === '') return false;
1638
1639        var start_hour = $('#mec_exceptions_in_days_start_hour').val();
1640        if(start_hour.length === 1) start_hour = '0'+start_hour;
1641
1642        var start_minutes = $('#mec_exceptions_in_days_start_minutes').val();
1643        if(start_minutes.length === 1) start_minutes = '0'+start_minutes;
1644
1645        var start_ampm = $('#mec_exceptions_in_days_start_ampm').val();
1646        if(typeof start_ampm === 'undefined') start_ampm = '';
1647
1648        var end_hour = $('#mec_exceptions_in_days_end_hour').val();
1649        if(end_hour.length === 1) end_hour = '0'+end_hour;
1650
1651        var end_minutes = $('#mec_exceptions_in_days_end_minutes').val();
1652        if(end_minutes.length === 1) end_minutes = '0'+end_minutes;
1653
1654        var end_ampm = $('#mec_exceptions_in_days_end_ampm').val();
1655        if(typeof end_ampm === 'undefined') end_ampm = '';
1656
1657        var value = start + ':' + end + ':' + start_hour + '-' + start_minutes + '-' + start_ampm + ':' + end_hour + '-' + end_minutes + '-' + end_ampm;
1658        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>';
1659
1660        // Don't add exactly same occurrences
1661        if($('#mec_in_days input[value="'+value+'"]').length > 0) return false;
1662
1663        var $key = $('#mec_new_in_days_key');
1664
1665        var key = $key.val();
1666        var html = $('#mec_new_in_days_raw').html().replace(/:i:/g, key).replace(/:val:
1666/g, value).replace(/:label:/g, label);
1667
1668        $('#mec_in_days').append(html);
1669        $key.val(parseInt(key)+1);
1670    });
1671
1672    $('#mec_edit_in_days').on('click', function()
1673    {
1674        // Form
1675        const $form = $('#mec-in-days-form');
1676
1677        const modify_id = $form.data('modify');
1678        const $row = $('#mec_in_days_row'+modify_id);
1679
1680        var allday = $(this).data('allday');
1681
1682        var start = $('#mec_exceptions_in_days_start_date').val();
1683        if(start === '') return false;
1684
1685        var end = $('#mec_exceptions_in_days_end_date').val();
1686        if(end === '') return false;
1687
1688        var start_hour = $('#mec_exceptions_in_days_start_hour').val();
1689        if(start_hour.length === 1) start_hour = '0'+start_hour;
1690
1691        var start_minutes = $('#mec_exceptions_in_days_start_minutes').val();
1692        if(start_minutes.length === 1) start_minutes = '0'+start_minutes;
1693
1694        var start_ampm = $('#mec_exceptions_in_days_start_ampm').val();
1695        if(typeof start_ampm === 'undefined') start_ampm = '';
1696
1697        var end_hour = $('#mec_exceptions_in_days_end_hour').val();
1698        if(end_hour.length === 1) end_hour = '0'+end_hour;
1699
1700        var end_minutes = $('#mec_exceptions_in_days_end_minutes').val();
1701        if(end_minutes.length === 1) end_minutes = '0'+end_minutes;
1702
1703        var end_ampm = $('#mec_exceptions_in_days_end_ampm').val();
1704        if(typeof end_ampm === 'undefined') end_ampm = '';
1705
1706        var value = start + ':' + end + ':' + start_hour + '-' + start_minutes + '-' + start_ampm + ':' + end_hour + '-' + end_minutes + '-' + end_ampm;
1707        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>';
1708
1709        $row.find($('input[type=hidden]')).val(value);
1710        $row.find($('.mec-in-days-day')).html(label);
1711
1712        // Reset Dates
1713        $form.parent().find($('input[type=text]')).val('');
1714
1715        // Modification Mode
1716        $form.removeClass('mec-in-days-edit-mode').addClass('mec-in-days-add-mode').removeData('modify');
1717    });
1718
1719    $('#mec_cancel_in_days').on('click', function()
1720    {
1721        // Form
1722        let $form = $('#mec-in-days-form');
1723
1724        // Reset Dates
1725        $form.parent().find($('input[type=text]')).val('');
1726
1727        // Modification Mode
1728        $form.removeClass('mec-in-days-edit-mode').addClass('mec-in-days-add-mode').removeData('modify');
1729    });
1730
1731    // Keyboard support for Custom Days card action buttons (audit #6).
1732    $('#mec_in_days').on('keydown', '[role="button"]', function(e)
1733    {
1734        if(e.key === 'Enter' || e.key === ' '){ e.preventDefault(); this.click(); }
1735    });
1736
1737    $('#mec_add_not_in_days').on('click', function()
1738    {
1739        let date = $('#mec_exceptions_not_in_days_date').val();
1740        if(date === '') return false;
1741
1742        let d = date.replaceAll('-', '');
1743        d = d.replaceAll('/', '');
1744        d = d.replaceAll('.', '');
1745
1746
1747        let $wrapper = $('#mec_not_in_days');
1748        let $key = $('#mec_new_not_in_days_key');
1749
1750        let c = 'mec-date-'+d;
1751        if($wrapper.find($('.'+c)).length) return;
1752
1753        let key = $key.val();
1754        let html = $('#mec_new_not_in_days_raw').html().replace(/:i:/g, key).replace(/:d:/g, d).replace(/:val:/g, date);
1755
1756        $wrapper.append(html);
1757        $key.val(parseInt(key)+1);
1758    });
1759
1760    $('#mec_add_ticket_button').on('click', function()
1761    {
1762        let $key = $('#mec_new_ticket_key');
1763        let key = $key.val();
1764        let html = $('#mec_new_ticket_raw').html().replace(/:i:/g, key);
1765
1766        $('#mec_tickets').append(html);
1767        $key.val(parseInt(key)+1);
1768
1769        mec_init_sortable_sections();
1770
1771        $('.mec_add_price_date_button').off('click').on('click', function()
1772        {
1773            mec_handle_add_price_date_button(this);
1774        });
1775
1776        $.each($(".mec-select2"), function(i,v)
1777        {
1778            if($(v).attr('name').search(":i:") > 0)
1779            {
1780                return;
1781            }
1782
1783            if(typeof $(v).data('select2-id') == 'undefined')
1784            {
1785                $(v).select2();
1786            }
1787        });
1788
1789        trigger_period_picker();
1790    });
1791
1792    $('.mec_add_price_date_button').off('click').on('click', function()
1793    {
1794        mec_handle_add_price_date_button(this);
1795    });
1796
1797    mec_hourly_schedule_add_day_listener();
1798    mec_init_sortable_sections();
1799
1800    $('#mec_add_fee_button').on('click', function()
1801    {
1802        var key = $('#mec_new_fee_key').val();
1803        var html = $('#mec_new_fee_raw').html().replace(/:i:/g, key);
1804
1805        $('#mec_fees_list').append(html);
1806        $('#mec_new_fee_key').val(parseInt(key)+1);
1807
1808        mec_init_sortable_sections();
1809    });
1810
1811    $('#mec_add_ticket_variation_button').on('click', function()
1812    {
1813        var key = $('#mec_new_ticket_variation_key').val();
1814        var html = $('#mec_new_ticket_variation_raw').html().replace(/:i:/g, key);
1815
1816        $('#mec_ticket_variations_list').append(html);
1817        $('#mec_new_ticket_variation_key').val(parseInt(key)+1);
1818
1819        mec_init_sortable_sections();
1820    });
1821
1822    $('.mec-form-row.mec-available-color-row span').on('click', function()
1823    {
1824        $('.mec-form-row.mec-available-color-row span').removeClass('color-selected');
1825        $(this).addClass('color-selected');
1826    });
1827
1828    $('#mec_reg_form_field_types button').on('click', function()
1829    {
1830        var type = $(this).data('type');
1831
1832        if (type == 'mec_email') {
1833            if ($('#mec_reg_form_fields').find('input[value="mec_email"][type="hidden"]').length) {
1834                return false;
1835            }
1836        }
1837
1838        if (type == 'name') {
1839            if ($('#mec_reg_form_fields').find('input[value="name"][type="hidden"]').length) {
1840                return false;
1841            }
1842        }
1843
1844        var key  = $('#mec_new_reg_field_key').val();
1845        var html = $('#mec_reg_field_'+type).html().replace(/:i:/g, key);
1846
1847        $('#mec_reg_form_fields').append(html);
1848        $('#mec_new_reg_field_key').val(parseInt(key)+1);
1849
1850        // Set onclick listener for add option fields
1851        mec_reg_fields_option_listeners();
1852        mec_refresh_booking_condition_editors('reg');
1853    });
1854
1855    // Set onclick listener for add option fields
1856    mec_reg_fields_option_listeners();
1857    mec_refresh_booking_condition_editors('reg');
1858
1859    // Advanced Repeating
1860    $('#mec-advanced-wraper ul > ul > li').click(function()
1861    {
1862        if($(this).attr('class') == '') $(this).attr('class', 'mec-active');
1863        else $(this).attr('class', '');
1864
1865        $('#mec_date_repeat_advanced').val($('#mec-advanced-wraper div:first-child > ul').find('.mec-active').find('span').text().slice(0, -1));
1866    });
1867
1868    $('#mec_event_form_field_types button').on('click', function()
1869    {
1870        var type = $(this).data('type');
1871
1872        var key  = $('#mec_new_event_field_key').val();
1873        var html = $('#mec_event_field_'+type).html().replace(/:i:/g, key);
1874
1875        $('#mec_event_form_fields').append(html);
1876        $('#mec_new_event_field_key').val(parseInt(key)+1);
1877
1878        // Set onclick listener for add option fields
1879        mec_event_fields_option_listeners();
1880    });
1881
1882    // Set onclick listener for add option fields
1883    mec_event_fields_option_listeners();
1884
1885    $('#mec_bfixed_form_field_types button').on('click', function()
1886    {
1887        var type = $(this).data('type');
1888
1889        var key  = $('#mec_new_bfixed_field_key').val();
1890        var html = $('#mec_bfixed_field_'+type).html().replace(/:i:/g, key);
1891
1892        $('#mec_bfixed_form_fields').append(html);
1893        $('#mec_new_bfixed_field_key').val(parseInt(key)+1);
1894
1895        // Set onclick listener for add option fields
1896        mec_bfixed_fields_option_listeners();
1897        mec_refresh_booking_condition_editors('bfixed');
1898    });
1899
1900    // Set onclick listener for add option fields
1901    mec_bfixed_fields_option_listeners();
1902    mec_refresh_booking_condition_editors('bfixed');
1903
1904    jQuery(document).on('change', '.mec-booking-condition-enabled, .mec-booking-condition-source, .mec-booking-condition-option', function()
1905    {
1906        const $trigger = jQuery(this);
1907        const $conditionBox = jQuery(this).closest('.mec-booking-condition-box');
1908        const prefix = String($conditionBox.data('conditionPrefix') || '');
1909
1910        if($trigger.hasClass('mec-booking-condition-source'))
1911        {
1912            $conditionBox.data('currentSourceFieldId', String($trigger.val() || ''));
1913            $conditionBox.data('currentOptionKey', '');
1914            $conditionBox.find('.mec-booking-condition-option').val('');
1915        }
1916        else if($trigger.hasClass('mec-booking-condition-option'))
1917        {
1918            $conditionBox.data('currentOptionKey', String($trigger.val() || ''));
1919        }
1920
1921        mec_refresh_booking_condition_editor($conditionBox);
1922        if(prefix) mec_refresh_booking_condition_editors(prefix);
1923    });
1924
1925    jQuery(document).on('input change', '#mec_reg_form_fields input[name*="[label]"], #mec_bfixed_form_fields input[name*="[label]"]', function()
1926    {
1927        const $field = jQuery(this).closest('li[id^="mec_reg_fields_"], li[id^="mec_bfixed_fields_"]');
1928        if($field.attr('id') && $field.attr('id').indexOf('mec_reg_fields_') === 0) mec_refresh_booking_condition_editors('reg');
1929        if($field.attr('id') && $field.attr('id').indexOf('mec_bfixed_fields_') === 0) mec_refresh_booking_condition_editors('bfixed');
1930    });
1931
1932    // Additional Organizers
1933    mec_additional_organizers_listeners();
1934
1935    // Additional Locations
1936    mec_additional_locations_listeners();
1937
1938    // Show / Hide Password
1939    $('.mec-show-hide-password').on('click', function()
1940    {
1941        var $input = $(this).siblings("input");
1942        var current = $input.attr('type');
1943
1944        if(current === 'password') $input.attr('type', 'text');
1945        else $input.attr('type', 'password');
1946    });
1947
1948    // FAQ
1949    $('#mec_add_faq_button').on('click', function()
1950    {
1951        const $key = $('#mec_new_faq_key');
1952        const key = $key.val();
1953        const html = $('#mec_new_faq_raw').html().replace(/:i:/g, key);
1954
1955        $('#mec_faq_list').append(html);
1956        $key.val(parseInt(key)+1);
1957
1958        mec_init_sortable_sections();
1959    });
1960
1961    // Appointments
1962    // Move the selector to the metabox header
1963    var $header = $('#mec_metabox_details .hndle');
1964    // If .hndle is not found, try .postbox-header (newer WP versions)
1965    if ($header.length === 0) {
1966        $header = $('#mec_metabox_details .postbox-header');
1967    }
1968    $('.mec-event-appointment-type-wrap').appendTo($header).removeClass('mec-util-hidden').on('click', function(e) {
1969        e.stopPropagation();
1970    });
1971
1972    $('#mec_entity_type_select').on('change', function()
1973    {
1974        const entity_type = $(this).val();
1975        const $fes_form = $('#mec_fes_form');
1976        const $metabox_details = $('#mec_metabox_details');
1977        const $appointment_form_wrapper = $('.mec-appointment-form-wrap');
1978
1979        if (entity_type === 'appointment')
1980        {
1981            $fes_form.removeClass('mec-entity-type-event').addClass('mec-entity-type-appointment');
1982            $metabox_details.removeClass('mec-entity-type-event').addClass('mec-entity-type-appointment');
1983
1984            $appointment_form_wrapper.removeClass('mec-util-hidden');
1985        }
1986        else
1987        {
1988            $fes_form.removeClass('mec-entity-type-appointment').addClass('mec-entity-type-event');
1989            $metabox_details.removeClass('mec-entity-type-appointment').addClass('mec-entity-type-event');
1990
1991            $appointment_form_wrapper.addClass('mec-util-hidden');
1992        }
1993    });
1994
1995    $('#mec_entity_type_select').trigger('change');
1996
1997    const $repeatType = $('#mec_appointments_availability_repeat_type');
1998    if($.fn.datepicker)
1999    {
2000        $('#mec_appointments_start_date').datepicker({
2001            changeYear: true,
2002            changeMonth: true,
2003            dateFormat: datepicker_format,
2004            gotoCurrent: true,
2005            yearRange: 'c-3:c+5',
2006        });
2007    }
2008    function mec_toggle_repeat_type()
2009    {
2010        const val = $repeatType.val();
2011        if (val === 'no_repeat')
2012        {
2013            $('.lsd-apt-days-wrapper').addClass('mec-util-hidden');
2014            $('.lsd-apt-adjusted-title').addClass('mec-util-hidden');
2015            $('.lsd-apt-start-date-wrapper').addClass('mec-util-hidden');
2016        }
2017        else
2018        {
2019            $('.lsd-apt-days-wrapper').removeClass('mec-util-hidden');
2020            $('.lsd-apt-adjusted-title').removeClass('mec-util-hidden');
2021            $('.lsd-apt-start-date-wrapper').removeClass('mec-util-hidden');
2022        }
2023    }
2024    $repeatType.on('change', mec_toggle_repeat_type);
2025    mec_toggle_repeat_type();
2026
2027    $(document).on('click', '.lsd-apt-day-icon-remove', function()
2028    {
2029        const $button = $(this);
2030        const $timeslot = $button.parent();
2031        const $timeslots = $button.parents('.lsd-apt-day-timeslots');
2032
2033        $timeslot.remove();
2034        if ($timeslots.find('.lsd-apt-day-timeslot-wrapper').length === 0)
2035        {
2036            $timeslots.find('.lsd-apt-day-timeslots-unavailable').removeClass('mec-util-hidden');
2037        }
2038    });
2039
2040    $(document).on('click', '.lsd-apt-day-icon-plus', function()
2041    {
2042        const $button = $(this);
2043        let key = $button.data('key');
2044
2045        const $day = $button.parents('.lsd-apt-day-wrapper');
2046        const day = $day.data('key');
2047
2048        const $timeslots = $day.find('.lsd-apt-day-timeslots-wrapper');
2049        const $template = $('#lsd-apt-day-templates-'+day+'-timeslot');
2050
2051        key = key + 1;
2052        const html = $template.html().replace(/:t:/g, key);
2053        $timeslots.append(html);
2054
2055        $button.data('key', key);
2056        $day.find('.lsd-apt-day-timeslots-unavailable').addClass('mec-util-hidden');
2057    });
2058
2059    $(document).on('click', '.lsd-apt-day-icon-copy', function ()
2060    {
2061        const $button = $(this);
2062        const $sourceDay = $button.closest('.lsd-apt-day-wrapper');
2063        const sourceDayKey = $sourceDay.data('key');
2064
2065        const $weekWrapper = $sourceDay.parent();
2066        const $sourceTimeslots = $sourceDay.find('.lsd-apt-day-timeslots-wrapper .lsd-apt-day-timeslot-wrapper');
2067
2068        $weekWrapper.find('.lsd-apt-day-wrapper').each(function ()
2069        {
2070            const $targetDay = $(this);
2071
2072            const targetDayKey = $targetDay.data('key');
2073            if (targetDayKey === sourceDayKey) return;
2074
2075            const isActive = $targetDay.find('.lsd-apt-day-timeslots-wrapper .lsd-apt-day-timeslot-wrapper').length > 0;
2076            if (!isActive) return;
2077
2078            const $targetWrapper = $targetDay.find('.lsd-apt-day-timeslots-wrapper');
2079            $targetWrapper.empty();
2080
2081            $sourceTimeslots.each(function ()
2082            {
2083                const $sourceSlot = $(this);
2084                const $clone = $sourceSlot.clone(true);
2085
2086                $sourceSlot.find('select').each(function (i) {
2087                    const value = $(this).val();
2088                    $clone.find('select').eq(i).val(value);
2089                });
2090
2091                $sourceSlot.find('input').each(function (i) {
2092                    const value = $(this).val();
2093                    $clone.find('input').eq(i).val(value);
2094                });
2095
2096                $clone.find('[name], [id], [for]').each(function ()
2097                {
2098                    $.each(this.attributes, function () {
2099                        if (this.name === 'name' || this.name === 'id' || this.name === 'for') {
2100                            this.value = this.value
2101                            .replace(`[availability][${sourceDayKey}]`, `[availability][${targetDayKey}]`)
2102                            .replace(`availability_${sourceDayKey}_`, `availability_${targetDayKey}_`);
2103                        }
2104                    });
2105                });
2106
2107                $targetWrapper.append($clone);
2108            });
2109        });
2110    });
2111
2112    $(document).on('click', '.lsd-apt-adjusted-day-add', function()
2113    {
2114        const $wrapper = $('.lsd-apt-adjusted-days-wrapper');
2115        let key = $wrapper.data('key') || 0;
2116
2117        key = key + 1;
2118        let html = $('#lsd-apt-adjusted-template-day').html();
2119        html = html.replace(/:i:/g, key);
2120
2121        $wrapper.append(html);
2122        $wrapper.data('key', key);
2123
2124        if($.fn.datepicker)
2125        {
2126            $wrapper.find('.mec-apt-date-picker').last().datepicker({
2127                changeYear: true,
2128                changeMonth: true,
2129                dateFormat: datepicker_format,
2130                gotoCurrent: true,
2131                yearRange: 'c-3:c+5',
2132            });
2133        }
2134    });
2135
2136    $(document).on('click', '.lsd-apt-adjusted-day-remove', function()
2137    {
2138        $(this).closest('.lsd-apt-day-wrapper').remove();
2139    });
2140
2141    $(document).on('click', '.lsd-apt-adj-day-icon-plus', function()
2142    {
2143        const $button = $(this);
2144        let key = $button.data('key');
2145        const $day = $button.closest('.lsd-apt-day-wrapper');
2146        const day = $day.data('day');
2147        const $timeslots = $day.find('.lsd-apt-day-timeslots-wrapper');
2148
2149        key = key + 1;
2150        let html = $('#lsd-apt-adjusted-template-timeslot').html();
2151
2152        html = html.replace(/:i:/g, day).replace(/:t:/g, key);
2153        $timeslots.append(html);
2154
2155        $button.data('key', key);
2156        $day.find('.lsd-apt-day-timeslots-unavailable').addClass('mec-util-hidden');
2157    });
2158});
2159
2160function trigger_period_picker()
2161{
2162    jQuery('.mec-date-picker-start').datepicker(
2163    {
2164        changeYear: true,
2165        changeMonth: true,
2166        dateFormat: 'yy-mm-dd',
2167        gotoCurrent: true,
2168        yearRange: 'c-1:c+5',
2169        onSelect: function(date)
2170        {
2171            const selectedDate = new Date(date);
2172            const endDate = new Date(selectedDate.getTime());
2173
2174            const $end_picker = jQuery(this).next();
2175            $end_picker.datepicker("option", "minDate", endDate);
2176            $end_picker.datepicker("option", "maxDate", '+5y');
2177            jQuery(this).trigger('change');
2178        }
2179    });
2180
2181    jQuery('.mec-date-picker-end').datepicker(
2182    {
2183        changeYear: true,
2184        changeMonth: true,
2185        dateFormat: 'yy-mm-dd',
2186        gotoCurrent: true,
2187        yearRange: 'c-1:c+5',
2188    });
2189}
2190
2191function mec_location_toggle()
2192{
2193    if(jQuery('#mec_location_id').val() != '0') jQuery('#mec_location_new_container').hide();
2194    else jQuery('#mec_location_new_container').show();
2195}
2196
2197function mec_organizer_toggle()
2198{
2199    if(jQuery('#mec_organizer_id').val() != '0') jQuery('#mec_organizer_new_container').hide();
2200    else jQuery('#mec_organizer_new_container').show();
2201}
2202
2203function mec_repeat_toggle()
2204{
2205    if(jQuery('#mec_repeat').is(':checked')) jQuery('.mec-form-repeating-event-row').show();
2206    else jQuery('.mec-form-repeating-event-row').hide();
2207}
2208
2209// Update the "Repeat Interval" unit suffix (days/weeks/months/years) based on
2210// the selected repeat type. Audit issue #17.
2211function mec_update_interval_unit()
2212{
2213    var $unit = jQuery('#mec_repeat_interval').siblings('.mec-interval-unit');
2214    if(!$unit.length) return;
2215    var loc = (typeof mec_admin_localize !== 'undefined') ? mec_admin_localize : ((typeof mecdata !== 'undefined') ? mecdata : {});
2216    var units = loc.interval_units || {};
2217    var type = jQuery('#mec_repeat_type').val();
2218    $unit.text(units[type] || '');
2219}
2220
2221function mec_repeat_type_toggle()
2222{
2223    var repeat_type = jQuery('#mec_repeat_type').val();
2224
2225    if(repeat_type == 'certain_weekdays')
2226    {
2227        jQuery('#mec_repeat_interval_container').hide();
2228        jQuery('#mec_repeat_certain_weekdays_container').show();
2229        jQuery('#mec_exceptions_in_days_container').hide();
2230        jQuery('#mec_end_wrapper').show();
2231        jQuery('#mec-advanced-wraper').hide();
2232    }
2233    else if(repeat_type == 'custom_days')
2234    {
2235        jQuery('#mec_repeat_interval_container').hide();
2236        jQuery('#mec_repeat_certain_weekdays_container').hide();
2237        jQuery('#mec_exceptions_in_days_container').show();
2238        jQuery('#mec_end_wrapper').hide();
2239        jQuery('#mec-advanced-wraper').hide();
2240    }
2241    else if(repeat_type == 'advanced')
2242    {
2243        jQuery('#mec_repeat_interval_container').hide();
2244        jQuery('#mec_repeat_certain_weekdays_container').hide();
2245        jQuery('#mec_exceptions_in_days_container').hide();
2246        jQuery('#mec_end_wrapper').show();
2247        jQuery('#mec-advanced-wraper').show();
2248    }
2249    else if(repeat_type != 'daily' && repeat_type != 'weekly' && repeat_type != 'monthly')
2250    {
2251        jQuery('#mec_repeat_interval_container').hide();
2252        jQuery('#mec_repeat_certain_weekdays_container').hide();
2253        jQuery('#mec_exceptions_in_days_container').hide();
2254        jQuery('#mec_end_wrapper').show();
2255        jQuery('#mec-advanced-wraper').hide();
2256    }
2257    else
2258    {
2259        jQuery('#mec_repeat_interval_container').show();
2260        jQuery('#mec_repeat_certain_weekdays_container').hide();
2261        jQuery('#mec_exceptions_in_days_container').hide();
2262        jQuery('#mec_end_wrapper').show();
2263        jQuery('#mec-advanced-wraper').hide();
2264    }
2265
2266    mec_update_interval_unit();
2267}
2268
2269function mec_in_days_remove(i)
2270{
2271    var $row = jQuery('#mec_in_days_row'+i);
2272    // Fade-out animation before removal (audit #6).
2273    if($row.length)
2274    {
2275        $row.addClass('mec-in-days-removing');
2276        setTimeout(function(){ jQuery('#mec_in_days_row'+i).remove(); }, 180);
2277    }
2278}
2279
2280function mec_in_days_edit(i)
2281{
2282    // Date
2283    let $row = jQuery('#mec_in_days_row'+i);
2284    let value = $row.find(jQuery('input[type=hidden]')).val();
2285
2286    const values = value.split(':');
2287    const start_times = values[2].split('-')
2288    const end_times = values[3].split('-')
2289
2290    // Form
2291    let $form = jQuery('#mec-in-days-form');
2292
2293    // Set Dates
2294    jQuery('#mec_exceptions_in_days_start_date').val(values[0]);
2295    jQuery('#mec_exceptions_in_days_end_date').val(values[1]);
2296
2297    // Set Times
2298    jQuery('#mec_exceptions_in_days_start_hour').val(parseInt(start_times[0]));
2299    jQuery('#mec_exceptions_in_days_start_minutes').val(parseInt(start_times[1]));
2300    jQuery('#mec_exceptions_in_days_start_ampm').val(start_times[2]);
2301
2302    jQuery('#mec_exceptions_in_days_end_hour').val(parseInt(end_times[0]));
2303    jQuery('#mec_exceptions_in_days_end_minutes').val(parseInt(end_times[1]));
2304    jQuery('#mec_exceptions_in_days_end_ampm').val(end_times[2]);
2305
2306    // Modification Mode
2307    $form.removeClass('mec-in-days-add-mode').addClass('mec-in-days-edit-mode').data('modify', i);
2308}
2309
2310function mec_not_in_days_remove(i)
2311{
2312    jQuery('#mec_not_in_days_row'+i).remove();
2313}
2314
2315function mec_bookings_unlimited_toggle()
2316{
2317    jQuery('#mec_bookings_limit').toggleClass('mec-util-hidden');
2318}
2319
2320function mec_hourly_schedule_add_day_listener()
2321{
2322    jQuery('.mec-add-hourly-schedule-day-button').each(function()
2323    {
2324        jQuery(this).off('click').on('click', function()
2325        {
2326            var k = jQuery(this).data('key');
2327            var raw = jQuery(this).data('raw');
2328            var append = jQuery(this).data('append');
2329
2330            var key = jQuery(k).val();
2331            var html = jQuery(raw).html().replace(/:d:/g, key).replace(/:dd:/g, parseInt(key)+1);
2332
2333            jQuery(append).append(html);
2334            jQuery(k).val(parseInt(key)+1);
2335
2336            mec_hourly_schedule_listeners();
2337        });
2338
2339        mec_hourly_schedule_listeners();
2340    });
2341}
2342
2343function mec_bookings_after_occurrence_cancel_listener()
2344{
2345    jQuery('.mec-occurrences-bookings-after-occurrences-cancel').off('change').on('change', function()
2346    {
2347        const $dropdown = jQuery(this);
2348        const value = $dropdown.val();
2349        const $moveWrapper = $dropdown.next();
2350
2351        if(value === 'move' || value === 'move_notify')
2352        {
2353            $moveWrapper.removeClass('w-hidden');
2354        }
2355        else
2356        {
2357            $moveWrapper.addClass('w-hidden');
2358        }
2359    });
2360}
2361
2362function mec_hourly_schedule_listeners()
2363{
2364    jQuery('.mec-add-hourly-schedule-button').off('click').on('click', function()
2365    {
2366        var prefix = jQuery(this).data('prefix');
2367        var day = jQuery(this).data('day');
2368        var $key = jQuery('#'+prefix+'mec_new_hourly_schedule_key'+day);
2369
2370        var key = $key.val();
2371        var html = jQuery('#'+prefix+'mec_new_hourly_schedule_raw'+day).html().replace(/:i:/g, key).replace(/:d:/g, day).replace();
2372        var g_field_id = prefix+"-hourly_schedules-"+day+"-schedules-:i:-description";
2373        var field_id = prefix+"-hourly_schedules-"+day+"-schedules-"+key+"-description";
2374        html = html.replace(g_field_id,field_id);
2375        html = html.replace(':k:',key);
2376        jQuery('#'+prefix+'mec_hourly_schedules'+day).append(html);
2377        $key.val(parseInt(key)+1);
2378
2379        wp.editor.initialize("mec"+field_id,{
2380            tinymce: {
2381                wpautop: true,
2382                plugins : 'charmap colorpicker compat3x directionality fullscreen hr image lists media paste tabfocus textcolor wordpress wpautoresize wpdialogs wpeditimage wpemoji wpgallery wplink wptextpattern wpview',
2383                toolbar1: 'bold italic underline strikethrough | bullist numlist | blockquote hr wp_more | alignleft aligncenter alignright | link unlink | fullscreen | wp_adv',
2384                toolbar2: 'formatselect alignjustify forecolor | pastetext removeformat charmap | outdent indent | undo redo | wp_help'
2385            },
2386            quicktags: true,
2387            mediaButtons: false,
2388        });
2389    });
2390
2391    jQuery(".mec-hourly-schedule-schedules").sortable({
2392        handle: '.mec_field_sort'
2393    });
2394}
2395
2396function mec_hourly_schedule_remove(day, i, prefix)
2397{
2398    jQuery("#"+prefix+"mec_hourly_schedule_row"+day+'_'+i).remove();
2399}
2400
2401function mec_hourly_schedule_day_remove(day, prefix)
2402{
2403    jQuery("#"+prefix+"mec_meta_box_hourly_schedule_day_"+day).remove();
2404}
2405
2406function mec_ticket_remove(i)
2407{
2408    jQuery("#mec_ticket_row"+i).remove();
2409}
2410
2411function mec_set_event_color(color)
2412{
2413    try
2414    {
2415        jQuery("#mec_event_color").wpColorPicker('color', '#'+color);
2416    }
2417    catch(e)
2418    {
2419        jQuery("#mec_event_color").val(color);
2420    }
2421}
2422
2423function mec_remove_fee(key)
2424{
2425    jQuery("#mec_fee_row"+key).remove();
2426}
2427
2428function mec_remove_ticket_variation(key, id_prefix)
2429{
2430    jQuery("#mec_"+id_prefix+"_row"+key).remove();
2431}
2432
2433function add_variation_per_ticket(ticket_id)
2434{
2435    var $input = jQuery('#mec_new_variation_per_ticket_key');
2436
2437    var key = $input.val();
2438    var html = jQuery('#mec_new_variation_per_ticket_raw'+ticket_id).html().replace(/:v:/g, key);
2439
2440    jQuery('#mec_ticket_variations_list'+ticket_id).append(html);
2441    $input.val(parseInt(key)+1);
2442
2443    mec_init_sortable_sections();
2444}
2445
2446function mec_init_sortable_instance(selector, options)
2447{
2448    jQuery(selector).each(function()
2449    {
2450        var $element = jQuery(this);
2451
2452        if(!$element.length) return;
2453
2454        if(typeof $element.data('ui-sortable') === 'undefined')
2455        {
2456            $element.sortable(options);
2457        }
2458        else
2459        {
2460            $element.sortable('refresh');
2461        }
2462    });
2463}
2464
2465function mec_init_sortable_sections()
2466{
2467    if(typeof jQuery.fn.sortable === 'undefined') return;
2468
2469    mec_init_sortable_instance('#mec_tickets', { handle: '.mec_field_sort', items: '> .mec_ticket_row' });
2470    mec_init_sortable_instance('#mec_fees_list', { handle: '.mec_field_sort', items: '> .mec-box' });
2471    mec_init_sortable_instance('[id^="mec_ticket_variations_list"]', { handle: '.mec_field_sort', items: '> .mec_ticket_variation_row' });
2472    mec_init_sortable_instance('#mec_faq_list', { handle: '.mec_field_sort', items: '> .mec_faq_row' });
2473}
2474
2475function mec_get_booking_condition_form_fields(prefix)
2476{
2477    return jQuery('#mec_' + prefix + '_form_fields > li');
2478}
2479
2480function mec_get_booking_condition_field_id($field, prefix)
2481{
2482    return String(($field.attr('id') || '').replace('mec_' + prefix + '_fields_', ''));
2483}
2484
2485function mec_get_booking_condition_field_type($field, prefix)
2486{
2487    const fieldId = mec_get_booking_condition_field_id($field, prefix);
2488    return String($field.find('input[name="mec[' + prefix + '_fields][' + fieldId + '][type]"]').val() || '');
2489}
2490
2491function mec_get_booking_condition_field_label($field, prefix)
2492{
2493    const fieldId = mec_get_booking_condition_field_id($field, prefix);
2494    const $labelInput = $field.find('input[name="mec[' + prefix + '_fields][' + fieldId + '][label]"]').first();
2495    const fallback = jQuery.trim($field.find('.mec_' + prefix + '_field_type').first().text() || '');
2496
2497    return jQuery.trim(($labelInput.val() || fallback || ''));
2498}
2499
2500function mec_get_booking_condition_field_option_list($field, prefix)
2501{
2502    const fieldId = mec_get_booking_condition_field_id($field, prefix);
2503    const options = [];
2504
2505    $field.find('input[name^="mec[' + prefix + '_fields][' + fieldId + '][options]"][name$="[label]"]').each(function()
2506    {
2507        const name = jQuery(this).attr('name') || '';
2508        const match = name.match(/\[options\]\[(.*?)\]\[label\]$/);
2509        if(!match) return;
2510
2511        options.push({
2512            key: String(match[1]),
2513            label: jQuery.trim(jQuery(this).val() || '')
2514        });
2515    });
2516
2517    return options;
2518}
2519
2520function mec_booking_condition_field_has_duplicate_options($field, prefix)
2521{
2522    const seen = {};
2523    const options = mec_get_booking_condition_field_option_list($field, prefix);
2524
2525    for(let i = 0; i < options.length; i++)
2526    {
2527        const label = options[i].label;
2528        if(!label) continue;
2529        if(seen[label]) return true;
2530
2531        seen[label] = true;
2532    }
2533
2534    return false;
2535}
2536
2537function mec_refresh_booking_condition_editor($conditionBox)
2538{
2539    if(!$conditionBox || !$conditionBox.length) return;
2540
2541    const prefix = String($conditionBox.data('conditionPrefix') || '');
2542    const currentFieldId = String($conditionBox.data('conditionFieldId') || '');
2543    const $enabled = $conditionBox.find('.mec-booking-condition-enabled');
2544    const $controls = $conditionBox.find('.mec-booking-condition-controls');
2545    const $source = $conditionBox.find('.mec-booking-condition-source');
2546    const $option = $conditionBox.find('.mec-booking-condition-option');
2547    const $matchType = $conditionBox.find('.mec-booking-condition-match-type');
2548    const $message = $conditionBox.find('.mec-booking-condition-message');
2549    const enabled = $enabled.is(':checked');
2550    const strings = (typeof mec_admin_localize !== 'undefined' && mec_admin_localize.booking_conditions) ? mec_admin_localize.booking_conditions : {};
2551
2552    $controls.toggleClass('mec-util-hidden', !enabled);
2553    if(!enabled)
2554    {
2555        $message.text('');
2556        return;
2557    }
2558
2559    const currentSourceId = String($source.val() || $conditionBox.data('currentSourceFieldId') || '');
2560    const currentOptionKey = String($option.val() || $conditionBox.data('currentOptionKey') || '');
2561    const sourceFields = [];
2562    const sourceMap = {};
2563
2564    mec_get_booking_condition_form_fields(prefix).each(function()
2565    {
2566        const $field = jQuery(this);
2567        const fieldId = mec_get_booking_condition_field_id($field, prefix);
2568        const fieldType = mec_get_booking_condition_field_type($field, prefix);
2569
2570        if(!fieldId) return;
2571        if(fieldId === currentFieldId) return false;
2572        if(jQuery.inArray(fieldType, ['checkbox', 'radio', 'select', 'agreement']) === -1) return;
2573        if(jQuery.inArray(fieldType, ['checkbox', 'radio', 'select']) !== -1 && mec_booking_condition_field_has_duplicate_options($field, prefix)) return;
2574
2575        const sourceField = {
2576            id: fieldId,
2577            type: fieldType,
2578            label: mec_get_booking_condition_field_label($field, prefix)
2579        };
2580
2581        sourceFields.push(sourceField);
2582        sourceMap[fieldId] = sourceField;
2583    });
2584
2585    $source.empty().append('<option value="">' + (strings.select_field || 'Select a field') + '</option>');
2586    jQuery.each(sourceFields, function(index, sourceField)
2587    {
2588        $source.append('<option value="' + sourceField.id + '">' + sourceField.label + '</option>');
2589    });
2590
2591    if(currentSourceId && sourceMap[currentSourceId]) $source.val(currentSourceId);
2592
2593    const selectedSourceId = String($source.val() || '');
2594    const selectedSource = sourceMap[selectedSourceId] || null;
2595
2596    $option.empty();
2597
2598    if(!selectedSource)
2599    {
2600        $matchType.val('');
2601        $option.prop('disabled', true).append('<option value="">' + (strings.select_option || 'Select an option') + '</option>');
2602        $message.text(sourceFields.length ? (strings.select_controller || 'Choose a controller field.') : (strings.no_sources || 'No eligible controller fields are available.'));
2603        $conditionBox.data('currentSourceFieldId', '');
2604        $conditionBox.data('currentOptionKey', '');
2605        return;
2606    }
2607
2608    if(selectedSource.type === 'agreement')
2609    {
2610        $matchType.val('checked');
2611        $option.prop('disabled', true).append('<option value="">' + (strings.checked_state || 'Checked') + '</option>');
2612        $message.text((strings.checked_message || 'This field will show when "%s" is checked.').replace('%s', selectedSource.label));
2613        $conditionBox.data('currentSourceFieldId', selectedSourceId);
2614        $conditionBox.data('currentOptionKey', '');
2615        return;
2616    }
2617
2618    const $sourceField = jQuery('#mec_' + prefix + '_fields_' + selectedSource.id);
2619    const options = mec_get_booking_condition_field_option_list($sourceField, prefix);
2620    const matchType = selectedSource.type === 'checkbox' ? 'contains_option' : 'equals_option';
2621
2622    $matchType.val(matchType);
2623    $option.prop('disabled', false).append('<option value="">' + (strings.select_option || 'Select an option') + '</option>');
2624
2625    jQuery.each(options, function(index, option)
2626    {
2627        $option.append('<option value="' + option.key + '">' + option.label + '</option>');
2628    });
2629
2630    if(currentOptionKey) $option.val(currentOptionKey);
2631
2632    if(!$option.val())
2633    {
2634        $message.text((strings.select_value || 'Choose the value that should reveal this field.').replace('%s', selectedSource.label));
2635    }
2636    else
2637    {
2638        $message.text('');
2639    }
2640
2641    $conditionBox.data('currentSourceFieldId', selectedSourceId);
2642    $conditionBox.data('currentOptionKey', String($option.val() || ''));
2643}
2644
2645function mec_refresh_booking_condition_editors(prefix)
2646{
2647    jQuery('.mec-booking-condition-box[data-condition-prefix="' + prefix + '"]').each(function()
2648    {
2649        mec_refresh_booking_condition_editor(jQuery(this));
2650    });
2651}
2652
2653function mec_reg_fields_option_listeners()
2654{
2655    jQuery('button.mec-reg-field-add-option').off('click').on('click', function()
2656    {
2657        var field_id = jQuery(this).data('field-id');
2658        var key = jQuery('#mec_new_reg_field_option_key_'+field_id).val();
2659        var html = jQuery('#mec_reg_field_option').html().replace(/:i:/g, key).replace(/:fi:/g, field_id);
2660
2661        jQuery('#mec_reg_fields_'+field_id+'_options_container').append(html);
2662        jQuery('#mec_new_reg_field_option_key_'+field_id).val(parseInt(key)+1);
2663        mec_refresh_booking_condition_editors('reg');
2664    });
2665
2666    if(typeof jQuery.fn.sortable !== 'undefined')
2667    {
2668        jQuery("#mec_reg_form_fields").sortable(
2669        {
2670            handle: '.mec_reg_field_sort',
2671            stop: function()
2672            {
2673                mec_refresh_booking_condition_editors('reg');
2674            }
2675        });
2676
2677        jQuery(".mec_reg_fields_options_container").sortable(
2678        {
2679            handle: '.mec_reg_field_option_sort'
2680        });
2681
2682        jQuery(".mec-hourly-schedule-days").sortable(
2683        {
2684            handle: 'h4'
2685        });
2686
2687        jQuery(".mec-hourly-schedule-schedules").sortable({
2688            handle: '.mec_field_sort'
2689        });
2690    }
2691}
2692
2693function mec_reg_fields_option_remove(field_key, key)
2694{
2695    jQuery("#mec_reg_fields_option_"+field_key+"_"+key).remove();
2696    mec_refresh_booking_condition_editors('reg');
2697}
2698
2699function mec_reg_fields_remove(key)
2700{
2701    jQuery("#mec_reg_fields_"+key).remove();
2702    mec_refresh_booking_condition_editors('reg');
2703}
2704
2705function mec_handle_add_price_date_button(e)
2706{
2707    var key = jQuery(e).data('key');
2708    var p = jQuery('#mec_new_ticket_price_key_'+key).val();
2709    var html = jQuery('#mec_new_ticket_price_raw_'+key).html().replace(/:i:/g, key).replace(/:j:/g, p);
2710
2711    jQuery('#mec-ticket-price-dates-'+key).append(html);
2712    jQuery('#mec_new_ticket_price_key_'+key).val(parseInt(p)+1);
2713    jQuery('#mec-ticket-price-dates-'+key+' .new_added').datepicker(
2714    {
2715        changeYear: true,
2716        changeMonth: true,
2717        dateFormat: datepicker_format,
2718        gotoCurrent: true,
2719        yearRange: 'c-3:c+5',
2720    });
2721}
2722
2723function mec_ticket_price_remove(ticket_key, price_key)
2724{
2725    jQuery("#mec_ticket_price_raw_"+ticket_key+"_"+price_key).remove();
2726}
2727
2728function mec_event_fields_option_listeners()
2729{
2730    jQuery('button.mec-event-field-add-option').off('click').on('click', function()
2731    {
2732        var field_id = jQuery(this).data('field-id');
2733        var key = jQuery('#mec_new_event_field_option_key_'+field_id).val();
2734        var html = jQuery('#mec_event_field_option').html().replace(/:i:/g, key).replace(/:fi:/g, field_id);
2735
2736        jQuery('#mec_event_fields_'+field_id+'_options_container').append(html);
2737        jQuery('#mec_new_event_field_option_key_'+field_id).val(parseInt(key)+1);
2738    });
2739
2740    if(typeof jQuery.fn.sortable !== 'undefined')
2741    {
2742        jQuery("#mec_event_form_fields").sortable(
2743        {
2744            handle: '.mec_event_field_sort'
2745        });
2746
2747        jQuery(".mec_event_fields_options_container").sortable(
2748        {
2749            handle: '.mec_event_field_option_sort'
2750        });
2751    }
2752}
2753
2754function mec_event_fields_option_remove(field_key, key)
2755{
2756    jQuery("#mec_event_fields_option_"+field_key+"_"+key).remove();
2757}
2758
2759function mec_event_fields_remove(key)
2760{
2761    jQuery("#mec_event_fields_"+key).remove();
2762}
2763
2764function mec_bfixed_fields_option_listeners()
2765{
2766    jQuery('button.mec-bfixed-field-add-option').off('click').on('click', function()
2767    {
2768        var field_id = jQuery(this).data('field-id');
2769        var key = jQuery('#mec_new_bfixed_field_option_key_'+field_id).val();
2770        var html = jQuery('#mec_bfixed_field_option').html().replace(/:i:/g, key).replace(/:fi:/g, field_id);
2771
2772        jQuery('#mec_bfixed_fields_'+field_id+'_options_container').append(html);
2773        jQuery('#mec_new_bfixed_field_option_key_'+field_id).val(parseInt(key)+1);
2774        mec_refresh_booking_condition_editors('bfixed');
2775    });
2776
2777    if(typeof jQuery.fn.sortable !== 'undefined')
2778    {
2779        jQuery("#mec_bfixed_form_fields").sortable(
2780        {
2781            handle: '.mec_bfixed_field_sort',
2782            stop: function()
2783            {
2784                mec_refresh_booking_condition_editors('bfixed');
2785            }
2786        });
2787
2788        jQuery(".mec_bfixed_fields_options_container").sortable(
2789        {
2790            handle: '.mec_bfixed_field_option_sort'
2791        });
2792    }
2793}
2794
2795function mec_bfixed_fields_option_remove(field_key, key)
2796{
2797    jQuery("#mec_bfixed_fields_option_"+field_key+"_"+key).remove();
2798    mec_refresh_booking_condition_editors('bfixed');
2799}
2800
2801function mec_bfixed_fields_remove(key)
2802{
2803    jQuery("#mec_bfixed_fields_"+key).remove();
2804    mec_refresh_booking_condition_editors('bfixed');
2805}
2806
2807function mec_additional_organizers_listeners()
2808{
2809    jQuery('#mec_additional_organizers_add').off('click').on('click', function()
2810    {
2811        var value = jQuery('.mec-additional-organizers select').val();
2812        if (!value) return;
2813
2814        // Each organizer can be added only once.
2815        if (jQuery('.mec-additional-organizers-list input[name="mec[additional_organizer_ids][]"][value="' + value + '"]').length) return;
2816
2817        var text = jQuery('.mec-additional-organizers select option:selected').text();
2818
2819        var sortLabel = jQuery(this).data('sort-label');
2820        var removeLabel = jQuery(this).data('remove-label');
2821
2822        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>');
2823
2824        mec_additional_organizers_listeners();
2825    });
2826
2827    if(typeof jQuery.fn.sortable !== 'undefined')
2828    {
2829        jQuery(".mec-additional-organizers-list").sortable(
2830        {
2831            handle: '.mec-additional-organizer-sort'
2832        });
2833    }
2834}
2835
2836function mec_additional_organizers_remove(element)
2837{
2838    jQuery(element).parent().remove();
2839}
2840
2841function mec_additional_locations_listeners()
2842{
2843    jQuery('#mec_additional_locations_add').off('click').on('click', function()
2844    {
2845        var value = jQuery('.mec-additional-locations select').val();
2846        if (!value) return;
2847
2848        // Each location can be added only once.
2849        if (jQuery('.mec-additional-locations-list input[name="mec[additional_location_ids][]"][value="' + value + '"]').length) return;
2850
2851        var text = jQuery('.mec-additional-locations select option:selected').text();
2852
2853        var sortLabel = jQuery(this).data('sort-label');
2854        var removeLabel = jQuery(this).data('remove-label');
2855
2856        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>');
2857
2858        mec_additional_locations_listeners();
2859    });
2860
2861    if(typeof jQuery.fn.sortable !== 'undefined')
2862    {
2863        jQuery(".mec-additional-locations-list").sortable(
2864        {
2865            handle: '.mec-additional-location-sort'
2866        });
2867    }
2868}
2869
2870function mec_additional_locations_remove(element)
2871{
2872    jQuery(element).parent().remove();
2873}
2874
2875function mec_faq_remove(key)
2876{
2877    jQuery("#mec_faq_row"+key).remove();
2878}
2879
2880jQuery(document).on('focus', '.mec_date_picker', function ()
2881{
2882    if (!jQuery(this).hasClass('hasDatepicker'))
2883    {
2884        jQuery(this).datepicker({
2885            changeYear: true,
2886            changeMonth: true,
2887            dateFormat: 'yy-mm-dd',
2888            gotoCurrent: true,
2889            yearRange: 'c-3:c+5',
2890        });
2891    }
2892});
2893
2894// Ticket availability start/end: live-init + open for rows that missed (or
2895// were added after) trigger_period_picker(). Two quirks to handle:
2896// 1) jQuery UI only opens a picker from its own focus handler — one bound
2897//    BEFORE the current focus event — so we must explicitly show() here.
2898// 2) The hidden "#mec_new_ticket_raw" template gets its pickers initialized
2899//    on page load; that serializes a stale "hasDatepicker" class into rows
2900//    cloned from it, and jQuery UI refuses to attach to such elements
2901//    (_connectDatepicker bails on the class). So test for the actual
2902//    instance and strip the stale class before initializing.
2903jQuery(document).on('focus', '.mec-date-picker-start, .mec-date-picker-end', function ()
2904{
2905    var $input = jQuery(this);
2906    if(!jQuery.fn.datepicker) return;
2907
2908    if(!$input.data('datepicker'))
2909    {
2910        $input.removeClass('hasDatepicker');
2911
2912        var isStart = $input.hasClass('mec-date-picker-start');
2913        $input.datepicker({
2914            changeYear: true,
2915            changeMonth: true,
2916            dateFormat: 'yy-mm-dd',
2917            gotoCurrent: true,
2918            yearRange: 'c-1:c+5',
2919            onSelect: isStart ? function(date)
2920            {
2921                var endDate = new Date(date);
2922                var $end_picker = $input.next();
2923                $end_picker.datepicker("option", "minDate", endDate);
2924                $end_picker.datepicker("option", "maxDate", '+5y');
2925                $input.trigger('change');
2926            } : null,
2927        });
2928
2929        // Initialize the sibling end picker eagerly so the start picker's
2930        // onSelect targets a live instance even before end is ever focused.
2931        if(isStart)
2932        {
2933            var $end = $input.next('.mec-date-picker-end');
2934            if($end.length && !$end.data('datepicker'))
2935            {
2936                $end.removeClass('hasDatepicker');
2937                $end.datepicker({
2938                    changeYear: true,
2939                    changeMonth: true,
2940                    dateFormat: 'yy-mm-dd',
2941                    gotoCurrent: true,
2942                    yearRange: 'c-1:c+5',
2943                });
2944            }
2945        }
2946    }
2947
2948    $input.datepicker('show');
2949});
2950
2951// Appointment date pickers (.mec-apt-date-picker): only the "Add a date"
2952// button initializes them, so server-rendered inputs (start date, existing
2953// adjusted days) never get a picker — and day-copy clones carry a stale
2954// "hasDatepicker" class without an instance. Live-init + open on focus,
2955// same pattern as the availability handlers above.
2956jQuery(document).on('focus', '.mec-apt-date-picker', function ()
2957{
2958    var $input = jQuery(this);
2959    if(!jQuery.fn.datepicker) return;
2960
2961    if(!$input.data('datepicker'))
2962    {
2963        $input.removeClass('hasDatepicker');
2964        $input.datepicker({
2965            changeYear: true,
2966            changeMonth: true,
2967            dateFormat: datepicker_format,
2968            gotoCurrent: true,
2969            yearRange: 'c-3:c+5',
2970        });
2971    }
2972
2973    $input.datepicker('show');
2974});
2975
2976// ===== Basic / Advanced toggle (mec-basvanced-toggle) =====
2977// Delegated so it works wherever events.js loads: the admin editor AND the
2978// FES front-end form (backend.js, which used to bind this directly, is not
2979// loaded on the front-end). Replaces the old direct binding in backend.js.
2980jQuery(document).on('click', '.mec-basvanced-toggle .mec-backend-tab-item', function (e)
2981{
2982    e.preventDefault();
2983
2984    var $item = jQuery(this);
2985    if($item.hasClass('mec-b-active-tab')) return;
2986
2987    var $toggle = $item.closest('.mec-basvanced-toggle');
2988    var wrapper = $toggle.data('for');
2989    var method = $toggle.data('method');
2990
2991    $item.parent().find('.mec-b-active-tab').removeClass('mec-b-active-tab');
2992    $item.addClass('mec-b-active-tab');
2993
2994    var $wrapper = jQuery(wrapper);
2995    if(method === 'addition') $wrapper.find('.mec-basvanced-advanced').toggleClass('w-hidden');
2996    else
2997    {
2998        $wrapper.find('.mec-basvanced-basic').toggleClass('w-hidden');
2999        $wrapper.find('.mec-basvanced-advanced').toggleClass('w-hidden');
3000    }
3001});
3002
3003// ===== Publish-validation fallback (crash-isolated) =====
3004// The main validation module lives INSIDE jQuery(document).ready and shares
3005// that callback with a dozen initializers — a runtime error anywhere above
3006// it would silently prevent its submit handler from ever being bound (no
3007// error surfaces at click time; the form simply submits). This top-level,
3008// document-delegated copy guarantees the critical guard — "cannot publish
3009// without a start date" + the wp-post-new-reload URL cleanup — survives any
3010// such failure. It is bound on document, so it runs AFTER the main
3011// form-level handler; if that one already handled the submit it stays
3012// silent (e.isDefaultPrevented()).
3013jQuery(document).on('submit', '#post', function (e)
3014{
3015    if(e.isDefaultPrevented()) return; // the main validation already handled it
3016
3017    // Draft saves and previews are exempt from publish validation.
3018    var submitter = (e.originalEvent && e.originalEvent.submitter) ? jQuery(e.originalEvent.submitter) : null;
3019    if(submitter && submitter.attr('id') === 'save-post') return;
3020    if(jQuery('#wp-preview').val() === 'dopreview') return;
3021
3022    var $startDate = jQuery('#mec_start_date');
3023    if(!$startDate.length) return;
3024
3025    // Undo WP core's address-bar rewrite: wp-admin/js/post.js appends
3026    // "wp-post-new-reload=true" on publish-click of an auto-draft even when
3027    // the submit ends up blocked, leaving a misleading URL behind.
3028    if(window.history && window.history.replaceState && window.location.href.indexOf('wp-post-new-reload') > -1)
3029    {
3030        window.history.replaceState(null, null, window.location.href.replace(/([?&])wp-post-new-reload=true&?/, '$1').replace(/[?&]$/, ''));
3031    }
3032
3033    if(jQuery.trim($startDate.val() || '') === '')
3034    {
3035        e.preventDefault();
3036
3037        // Jump to the Date & Time tab so the field is visible.
3038        jQuery('#mec_metabox_details .mec-add-event-tabs-link[data-href="mec_meta_box_date_form"]').trigger('click');
3039
3040        // Inline error on the field.
3041        $startDate.addClass('mec-field-invalid').attr('aria-invalid', 'true');
3042        var $err = $startDate.next('.mec-field-error');
3043        if(!$err.length) $err = jQuery('<span class="mec-field-error" role="alert"></span>').insertAfter($startDate);
3044        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.';
3045        $err.text(msg);
3046
3047        // Notice card — self-contained so it renders even if the main module died.
3048        jQuery('.mec-notice-temp').remove();
3049        var $n = jQuery('<div/>', {
3050            'class': 'mec-notice mec-notice--error mec-notice-temp',
3051            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>'
3052        });
3053        jQuery('#poststuff').prepend($n);
3054        jQuery('html, body').animate({scrollTop: $n.offset().top - 40}, 200);
3055        $n.find('.mec-notice-dismiss, .mec-notice-close').on('click', function(){ $n.remove(); });
3056
3057        $startDate.trigger('focus');
3058    }
3059});
3060
3061// ===== Native-required feedback (e.g. required Event Data fields) =====
3062// When a field carries the HTML5 "required" attribute (custom Event Data
3063// fields can), the browser blocks submission BEFORE the submit event fires:
3064// none of the submit handlers above run, so no MEC notice appears and WP
3065// core's publish-click URL rewrite ("wp-post-new-reload=true") lingers in
3066// the address bar. The only signal is the non-bubbling "invalid" event —
3067// captured here on the document so the user still gets MEC-style feedback.
3068document.addEventListener('invalid', function (ev)
3069{
3070    var field = ev.target;
3071    if(!field || !field.form || field.form.id !== 'post') return;
3072
3073    // Undo WP core's address-bar rewrite (same cleanup as the fallback).
3074    if(window.history && window.history.replaceState && window.location.href.indexOf('wp-post-new-reload') > -1)
3075    {
3076        window.history.replaceState(null, null, window.location.href.replace(/([?&])wp-post-new-reload=true&?/, '$1').replace(/[?&]$/, ''));
3077    }
3078
3079    // Friendly field name: its label, aria-label, placeholder or title.
3080    var name = '';
3081    if(field.id)
3082    {
3083        var $l = jQuery('label[for="' + field.id + '"]').first();
3084        if($l.length) name = jQuery.trim($l.text());
3085    }
3086    if(!name) name = field.getAttribute('aria-label') || field.placeholder || field.title || '';
3087
3088    jQuery('.mec-notice-temp').remove();
3089    var msg = name
3090        ? '"' + jQuery('<div/>').text(name).html() + '" ' + (field.validationMessage || 'is required.')
3091        : (field.validationMessage || 'Please fill in the required fields.');
3092
3093    var $n = jQuery('<div/>', {
3094        'class': 'mec-notice mec-notice--error mec-notice-temp',
3095        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>'
3096    });
3097    jQuery('#poststuff').prepend($n);
3098    jQuery('html, body').animate({scrollTop: $n.offset().top - 40}, 200);
3099    $n.find('.mec-notice-dismiss, .mec-notice-close').on('click', function(){ $n.remove(); });
3100}, 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.