PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 1.10.6
Subscriptions for WooCommerce with Stripe Recurring Payments v1.10.6
2.4.0 2.3.0 2.2.0 2.1.0 2.0.0 1.11.2 1.11.1 1.11.0 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 1.10.1 1.10.0 1.9.6 1.9.5 trunk 1.3.0 1.3.1 1.3.2 1.4.0 All 65 releases
subscription / assets / js / admin-components.js

admin-components.js in Subscriptions for WooCommerce with Stripe Recurring Payments 1.10.6, at assets/js/admin-components.js

756 lines 24.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * WPSubscription admin UI components.
3 *
4 * WPSubsAdvSelect — styled dropdown replacing native <select>.
5 *
6 * Usage:
7 * PHP: wpsubs_render_adv_select( $args ) — renders the HTML
8 * JS: WPSubsAdvSelect.init() — auto-inits all .wpsubs-adv-select elements
9 *
10 * Events fired on the root element (bubbles):
11 * wpsubs:select — { value, label } when user picks an item
12 */
13 (function () {
14 "use strict";
15
16 var instances = [];
17
18 /**
19 * @param {HTMLElement} el Root .wpsubs-adv-select element.
20 */
21 function WPSubsAdvSelect(el) {
22 this.el = el;
23 this.trigger = el.querySelector(".wpsubs-adv-select__trigger");
24 this.label = el.querySelector(".wpsubs-adv-select__label");
25 this.menu = el.querySelector(".wpsubs-adv-select__menu");
26 this.input = el.querySelector('input[type="hidden"]');
27 this._bind();
28 instances.push(this);
29 }
30
31 WPSubsAdvSelect.prototype.open = function () {
32 closeAll(this);
33 this.el.classList.add("wpsubs-adv-select--open");
34 if (this.trigger) this.trigger.setAttribute("aria-expanded", "true");
35 };
36
37 WPSubsAdvSelect.prototype.close = function () {
38 this.el.classList.remove("wpsubs-adv-select--open");
39 if (this.trigger) this.trigger.setAttribute("aria-expanded", "false");
40 };
41
42 WPSubsAdvSelect.prototype.isOpen = function () {
43 return this.el.classList.contains("wpsubs-adv-select--open");
44 };
45
46 /**
47 * Programmatically select a value.
48 *
49 * @param {string} value
50 * @param {string} label Display text shown in trigger. Defaults to value.
51 */
52 WPSubsAdvSelect.prototype.select = function (value, label) {
53 if (this.input) this.input.value = value;
54 if (this.label) this.label.textContent = label || value;
55 this.el.dispatchEvent(
56 new CustomEvent("wpsubs:select", {
57 bubbles: true,
58 detail: { value: value, label: label || value },
59 }),
60 );
61 };
62
63 /** Reset trigger label back to placeholder. */
64 WPSubsAdvSelect.prototype.reset = function () {
65 var placeholder = this.el.dataset.placeholder || "";
66 if (this.input) this.input.value = this.el.dataset.defaultValue || "";
67 if (this.label && placeholder) this.label.textContent = placeholder;
68 };
69
70 WPSubsAdvSelect.prototype._bind = function () {
71 var self = this;
72
73 if (self.trigger) {
74 self.trigger.addEventListener("click", function (e) {
75 e.stopPropagation();
76 self.isOpen() ? self.close() : self.open();
77 });
78 }
79
80 if (self.menu) {
81 self.menu.addEventListener("click", function (e) {
82 var item = e.target.closest(".wpsubs-adv-select__item");
83 if (!item || item.hasAttribute("data-disabled")) return;
84
85 var value = item.dataset.value !== undefined ? item.dataset.value : "";
86 var labelEl = item.querySelector(".wpsubs-adv-select__item-label");
87 var label = labelEl ? labelEl.textContent.trim() : item.textContent.trim();
88 var confirmMsg = item.dataset.confirm || "";
89
90 if (confirmMsg && !window.confirm(confirmMsg)) return;
91
92 self.close();
93 self.select(value, label);
94 });
95 }
96 };
97
98 function closeAll(except) {
99 instances.forEach(function (inst) {
100 if (inst !== except) inst.close();
101 });
102 }
103
104 /**
105 * Initialise all un-initialised .wpsubs-adv-select elements under root.
106 *
107 * @param {Document|HTMLElement} [root]
108 */
109 function init(root) {
110 (root || document).querySelectorAll(".wpsubs-adv-select:not([data-adv-init])").forEach(function (el) {
111 el.setAttribute("data-adv-init", "1");
112 new WPSubsAdvSelect(el);
113 });
114 }
115
116 // Global: outside click and Escape close all
117 document.addEventListener("click", function () {
118 closeAll();
119 });
120 document.addEventListener("keydown", function (e) {
121 if (e.key === "Escape") closeAll();
122 });
123
124 // Auto-init on DOM ready
125 if (document.readyState === "loading") {
126 document.addEventListener("DOMContentLoaded", function () {
127 init();
128 });
129 } else {
130 init();
131 }
132
133 // Public API
134 window.WPSubsAdvSelect = { init: init };
135 })();
136
137 /**
138 * WPSubsTagSelect — pill/tag input with inline filter and filterable dropdown.
139 *
140 * Usage:
141 * PHP: wpsubs_render_tag_select( $args ) — renders the HTML
142 * JS: WPSubsTagSelect.init() — auto-inits all .wpsubs-tag-select elements
143 *
144 * Events fired on the root element (bubbles):
145 * wpsubs:select — { value, label, selected } when a pill is added or removed
146 */
147 (function () {
148 "use strict";
149
150 var instances = [];
151
152 /**
153 * @param {HTMLElement} el Root .wpsubs-tag-select element.
154 */
155 function WPSubsTagSelect(el) {
156 this.el = el;
157 this.multiple = !!el.dataset.multiple;
158 this.fieldName = el.dataset.name || "";
159 this.field = el.querySelector(".wpsubs-tag-select__field");
160 this.input = el.querySelector(".wpsubs-tag-select__input");
161 this.dropdown = el.querySelector(".wpsubs-tag-select__dropdown");
162 this.list = el.querySelector(".wpsubs-tag-select__list");
163 this.emptyEl = el.querySelector(".wpsubs-tag-select__empty");
164 this._bind();
165 instances.push(this);
166 }
167
168 WPSubsTagSelect.prototype.open = function () {
169 closeAll(this);
170 this.el.classList.add("wpsubs-tag-select--open");
171 this._filterItems("");
172 if (this.input) this.input.focus();
173 };
174
175 WPSubsTagSelect.prototype.close = function () {
176 this.el.classList.remove("wpsubs-tag-select--open");
177 if (this.input) {
178 this.input.value = "";
179 this._filterItems("");
180 }
181 };
182
183 WPSubsTagSelect.prototype.isOpen = function () {
184 return this.el.classList.contains("wpsubs-tag-select--open");
185 };
186
187 /** Show/hide dropdown items based on query, always hiding selected ones. */
188 WPSubsTagSelect.prototype._filterItems = function (query) {
189 var q = query.trim().toLowerCase();
190 var items = this.list ? this.list.querySelectorAll(".wpsubs-tag-select__item") : [];
191 var visible = 0;
192 items.forEach(function (item) {
193 if (item.hasAttribute("data-selected")) {
194 item.style.display = "none";
195 return;
196 }
197 var text = item.textContent.toLowerCase();
198 var match = !q || text.indexOf(q) !== -1;
199 item.style.display = match ? "" : "none";
200 if (match) visible++;
201 });
202 if (this.emptyEl) {
203 this.emptyEl.style.display = visible === 0 ? "" : "none";
204 }
205 };
206
207 /** Add a pill for the given dropdown item. */
208 WPSubsTagSelect.prototype._addPill = function (item) {
209 var value = item.dataset.value !== undefined ? item.dataset.value : "";
210 var label = item.textContent.trim();
211
212 // For single-select, remove the existing pill first.
213 if (!this.multiple) {
214 var existing = this.el.querySelectorAll(".wpsubs-tag-select__pill");
215 var self = this;
216 existing.forEach(function (p) {
217 self._removePillEl(p, false);
218 });
219 }
220
221 // Build the pill.
222 var pill = document.createElement("span");
223 pill.className = "wpsubs-tag-select__pill";
224 pill.dataset.value = value;
225
226 var pillLabel = document.createElement("span");
227 pillLabel.className = "wpsubs-tag-select__pill-label";
228 pillLabel.textContent = label;
229
230 var removeBtn = document.createElement("button");
231 removeBtn.type = "button";
232 removeBtn.className = "wpsubs-tag-select__pill-remove";
233 removeBtn.setAttribute("aria-label", "Remove " + label);
234 removeBtn.innerHTML = "&#x2715;"; // ×
235
236 pill.appendChild(pillLabel);
237 pill.appendChild(removeBtn);
238
239 // Insert the pill before the text input.
240 if (this.input) {
241 this.field.insertBefore(pill, this.input);
242 } else {
243 this.field.appendChild(pill);
244 }
245
246 // Mark dropdown item as selected so it stays hidden.
247 item.setAttribute("data-selected", "");
248 item.style.display = "none";
249
250 this._syncHiddenInputs();
251 this._updateInputPlaceholder();
252
253 if (this.input) this.input.value = "";
254 this._filterItems("");
255
256 this.el.dispatchEvent(
257 new CustomEvent("wpsubs:select", {
258 bubbles: true,
259 detail: { value: value, label: label, selected: true },
260 }),
261 );
262 };
263
264 /** Remove a pill element. Pass sync=true to update hidden inputs (default). */
265 WPSubsTagSelect.prototype._removePillEl = function (pill, sync) {
266 var value = pill.dataset.value !== undefined ? pill.dataset.value : "";
267 var label = pill.querySelector(".wpsubs-tag-select__pill-label");
268 var labelText = label ? label.textContent.trim() : value;
269
270 pill.parentNode.removeChild(pill);
271
272 // Un-mark the corresponding dropdown item.
273 var item = this.list
274 ? this.list.querySelector('.wpsubs-tag-select__item[data-value="' + CSS.escape(value) + '"]')
275 : null;
276 if (item) {
277 item.removeAttribute("data-selected");
278 }
279
280 if (sync !== false) {
281 this._syncHiddenInputs();
282 this._updateInputPlaceholder();
283 this._filterItems(this.input ? this.input.value : "");
284
285 this.el.dispatchEvent(
286 new CustomEvent("wpsubs:select", {
287 bubbles: true,
288 detail: { value: value, label: labelText, selected: false },
289 }),
290 );
291 }
292 };
293
294 /** Rebuild hidden inputs to match current pills. */
295 WPSubsTagSelect.prototype._syncHiddenInputs = function () {
296 var existing = this.el.querySelectorAll("input[data-ts-val]");
297 var fieldName = existing.length > 0 ? existing[0].name : this.fieldName + (this.multiple ? "[]" : "");
298
299 existing.forEach(function (inp) {
300 inp.parentNode.removeChild(inp);
301 });
302
303 var pills = this.el.querySelectorAll(".wpsubs-tag-select__pill");
304 var self = this;
305
306 if (this.multiple) {
307 if (pills.length === 0) {
308 // Empty sentinel so the form field is always present on submit.
309 var sentinel = document.createElement("input");
310 sentinel.type = "hidden";
311 sentinel.name = fieldName;
312 sentinel.value = "";
313 sentinel.setAttribute("data-ts-val", "");
314 self.el.appendChild(sentinel);
315 } else {
316 pills.forEach(function (pill) {
317 var inp = document.createElement("input");
318 inp.type = "hidden";
319 inp.name = fieldName;
320 inp.value = pill.dataset.value !== undefined ? pill.dataset.value : "";
321 inp.setAttribute("data-ts-val", "");
322 self.el.appendChild(inp);
323 });
324 }
325 } else {
326 var inp = document.createElement("input");
327 inp.type = "hidden";
328 inp.name = fieldName;
329 inp.value = pills.length > 0 && pills[0].dataset.value !== undefined ? pills[0].dataset.value : "";
330 inp.setAttribute("data-ts-val", "");
331 self.el.appendChild(inp);
332 }
333 };
334
335 /** Show placeholder only when there are no pills. */
336 WPSubsTagSelect.prototype._updateInputPlaceholder = function () {
337 if (!this.input) return;
338 var pills = this.el.querySelectorAll(".wpsubs-tag-select__pill");
339 this.input.placeholder = pills.length === 0 ? this.el.dataset.placeholder || "" : "";
340 };
341
342 WPSubsTagSelect.prototype._bind = function () {
343 var self = this;
344
345 if (self.field) {
346 self.field.addEventListener("click", function (e) {
347 e.stopPropagation();
348 var removeBtn = e.target.closest(".wpsubs-tag-select__pill-remove");
349 if (removeBtn) {
350 var pill = removeBtn.closest(".wpsubs-tag-select__pill");
351 if (pill) self._removePillEl(pill);
352 return;
353 }
354 self.open();
355 });
356 }
357
358 if (self.input) {
359 self.input.addEventListener("input", function () {
360 if (!self.isOpen()) self.open();
361 self._filterItems(self.input.value);
362 });
363 }
364
365 if (self.dropdown) {
366 self.dropdown.addEventListener("click", function (e) {
367 e.stopPropagation();
368 var item = e.target.closest(".wpsubs-tag-select__item");
369 if (!item || item.hasAttribute("data-disabled")) return;
370 self._addPill(item);
371 if (!self.multiple) self.close();
372 });
373 }
374 };
375
376 function closeAll(except) {
377 instances.forEach(function (inst) {
378 if (inst !== except) inst.close();
379 });
380 }
381
382 /**
383 * Initialise all un-initialised .wpsubs-tag-select elements under root.
384 *
385 * @param {Document|HTMLElement} [root]
386 */
387 function init(root) {
388 (root || document).querySelectorAll(".wpsubs-tag-select:not([data-ts-init])").forEach(function (el) {
389 el.setAttribute("data-ts-init", "1");
390 new WPSubsTagSelect(el);
391 });
392 }
393
394 document.addEventListener("click", function () {
395 closeAll();
396 });
397 document.addEventListener("keydown", function (e) {
398 if (e.key === "Escape") closeAll();
399 });
400
401 if (document.readyState === "loading") {
402 document.addEventListener("DOMContentLoaded", function () {
403 init();
404 });
405 } else {
406 init();
407 }
408
409 // Public API
410 window.WPSubsTagSelect = { init: init };
411 })();
412
413 /**
414 * WPSubsEditList — editable ordered list of text items.
415 *
416 * A reusable admin component: renders items in a reorderable list with per-row
417 * remove/move controls, plus an inline input + add button to append new items.
418 * The ordered list is serialized as JSON ([{ key, label }]) into a hidden input
419 * so it submits with the surrounding form. `key` is a slug derived from the label.
420 *
421 * Usage:
422 * PHP: SettingsHelper::render_editlist() / any markup with the classes below
423 * JS: WPSubsEditList.init() — auto-inits all .wpsubs-editlist elements
424 *
425 * Expected structure inside .wpsubs-editlist:
426 * input[type=hidden] — JSON store
427 * .wpsubs-editlist__items > .wpsubs-editlist__item[data-key]
428 * .wpsubs-editlist__label
429 * [data-editlist-up] [data-editlist-down] [data-editlist-remove]
430 * .wpsubs-editlist__empty — shown only when empty
431 * .wpsubs-editlist__input — inline text input
432 * [data-editlist-add] — add/confirm button
433 *
434 * Events fired on the root element (bubbles):
435 * wpsubs:change — { items } after any add/remove/reorder
436 */
437 (function () {
438 "use strict";
439
440 /**
441 * Derive a slug key from a label.
442 *
443 * @param {string} label
444 * @return {string}
445 */
446 function slugify(label) {
447 return String(label)
448 .toLowerCase()
449 .replace(/[^a-z0-9]+/g, "_")
450 .replace(/^_+|_+$/g, "");
451 }
452
453 var SVG_UP =
454 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="18 15 12 9 6 15"/></svg>';
455 var SVG_DOWN =
456 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>';
457 var SVG_TRASH =
458 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>';
459
460 /**
461 * @param {HTMLElement} el Root .wpsubs-editlist element.
462 */
463 function WPSubsEditList(el) {
464 this.el = el;
465 this.hidden = el.querySelector('input[type="hidden"]');
466 this.list = el.querySelector(".wpsubs-editlist__items");
467 this.emptyEl = el.querySelector(".wpsubs-editlist__empty");
468 this.input = el.querySelector(".wpsubs-editlist__input");
469 // Optional live count badge (e.g. when the list lives inside a modal trigger).
470 this.countEl = el.querySelector(".wpsubs-editlist__count");
471 this._bind();
472 this._serialize();
473 }
474
475 WPSubsEditList.prototype._rows = function () {
476 return this.list ? this.list.querySelectorAll(".wpsubs-editlist__item") : [];
477 };
478
479 /** Serialize the current rows into the hidden input as JSON. */
480 WPSubsEditList.prototype._serialize = function () {
481 var items = [];
482 this._rows().forEach(function (row) {
483 var labelEl = row.querySelector(".wpsubs-editlist__label");
484 items.push({
485 key: row.getAttribute("data-key") || "",
486 label: labelEl ? labelEl.textContent : "",
487 });
488 });
489 if (this.hidden) this.hidden.value = JSON.stringify(items);
490 if (this.emptyEl) this.emptyEl.hidden = items.length > 0;
491 if (this.countEl) this.countEl.textContent = String(items.length);
492 this.el.dispatchEvent(new CustomEvent("wpsubs:change", { bubbles: true, detail: { items: items } }));
493 };
494
495 /**
496 * Append a new item row.
497 *
498 * @param {string} label
499 */
500 WPSubsEditList.prototype._addItem = function (label) {
501 var text = String(label).trim();
502 if (!text || !this.list) return;
503
504 var row = document.createElement("li");
505 row.className = "wpsubs-editlist__item";
506 row.setAttribute("data-key", slugify(text));
507
508 var handle = document.createElement("span");
509 handle.className = "wpsubs-editlist__handle";
510 handle.setAttribute("aria-hidden", "true");
511 handle.innerHTML = "&#8942;&#8942;";
512
513 var lab = document.createElement("span");
514 lab.className = "wpsubs-editlist__label";
515 lab.textContent = text;
516
517 var actions = document.createElement("span");
518 actions.className = "wpsubs-editlist__actions";
519 actions.innerHTML =
520 '<button type="button" class="wpsubs-editlist__btn" data-editlist-up>' +
521 SVG_UP +
522 "</button>" +
523 '<button type="button" class="wpsubs-editlist__btn" data-editlist-down>' +
524 SVG_DOWN +
525 "</button>" +
526 '<button type="button" class="wpsubs-editlist__btn wpsubs-editlist__btn--danger" data-editlist-remove>' +
527 SVG_TRASH +
528 "</button>";
529
530 row.appendChild(handle);
531 row.appendChild(lab);
532 row.appendChild(actions);
533 this.list.appendChild(row);
534 this._serialize();
535 };
536
537 /** Add the item currently typed in the inline input, then reset it. */
538 WPSubsEditList.prototype._commitInput = function () {
539 if (!this.input) return;
540 var val = this.input.value.trim();
541 if (!val) return;
542 this._addItem(val);
543 this.input.value = "";
544 this.input.focus();
545 };
546
547 WPSubsEditList.prototype._bind = function () {
548 var self = this;
549
550 this.el.addEventListener("click", function (e) {
551 if (e.target.closest("[data-editlist-add]")) {
552 self._commitInput();
553 return;
554 }
555 var row = e.target.closest(".wpsubs-editlist__item");
556 if (!row) return;
557
558 if (e.target.closest("[data-editlist-remove]")) {
559 row.parentNode.removeChild(row);
560 self._serialize();
561 } else if (e.target.closest("[data-editlist-up]")) {
562 if (row.previousElementSibling) {
563 row.parentNode.insertBefore(row, row.previousElementSibling);
564 self._serialize();
565 }
566 } else if (e.target.closest("[data-editlist-down]")) {
567 if (row.nextElementSibling) {
568 row.parentNode.insertBefore(row.nextElementSibling, row);
569 self._serialize();
570 }
571 }
572 });
573
574 if (this.input) {
575 this.input.addEventListener("keydown", function (e) {
576 if (e.key === "Enter") {
577 e.preventDefault();
578 self._commitInput();
579 }
580 });
581 }
582
583 // Drag to reorder — only starts from the handle.
584 this._dragRow = null;
585
586 this.el.addEventListener("mousedown", function (e) {
587 var handle = e.target.closest(".wpsubs-editlist__handle");
588 if (!handle) return;
589 var row = handle.closest(".wpsubs-editlist__item");
590 if (row) row.setAttribute("draggable", "true");
591 });
592
593 this.el.addEventListener("dragstart", function (e) {
594 var row = e.target.closest(".wpsubs-editlist__item");
595 if (!row || row.getAttribute("draggable") !== "true") return;
596 self._dragRow = row;
597 row.classList.add("wpsubs-editlist__item--dragging");
598 if (e.dataTransfer) {
599 e.dataTransfer.effectAllowed = "move";
600 try {
601 e.dataTransfer.setData("text/plain", "");
602 } catch (err) {
603 /* IE guard */
604 }
605 }
606 });
607
608 this.el.addEventListener("dragover", function (e) {
609 if (!self._dragRow || !self.list) return;
610 e.preventDefault();
611 var over = e.target.closest(".wpsubs-editlist__item");
612 if (!over || over === self._dragRow) return;
613 var rect = over.getBoundingClientRect();
614 var after = (e.clientY - rect.top) / rect.height > 0.5;
615 self.list.insertBefore(self._dragRow, after ? over.nextSibling : over);
616 });
617
618 this.el.addEventListener("drop", function (e) {
619 if (self._dragRow) e.preventDefault();
620 });
621
622 this.el.addEventListener("dragend", function () {
623 if (!self._dragRow) return;
624 self._dragRow.classList.remove("wpsubs-editlist__item--dragging");
625 self._dragRow.removeAttribute("draggable");
626 self._dragRow = null;
627 self._serialize();
628 });
629 };
630
631 /**
632 * Initialise all un-initialised .wpsubs-editlist elements under root.
633 *
634 * @param {Document|HTMLElement} [root]
635 */
636 function init(root) {
637 (root || document).querySelectorAll(".wpsubs-editlist:not([data-editlist-init])").forEach(function (el) {
638 el.setAttribute("data-editlist-init", "1");
639 new WPSubsEditList(el);
640 });
641 }
642
643 if (document.readyState === "loading") {
644 document.addEventListener("DOMContentLoaded", function () {
645 init();
646 });
647 } else {
648 init();
649 }
650
651 // Public API
652 window.WPSubsEditList = { init: init };
653 })();
654
655 /**
656 * WPSubsModal — reusable centered dialog over a dimmed backdrop.
657 *
658 * Behaviour only; the visual comes from the .wpsubs-modal CSS. A modal is a
659 * `.wpsubs-modal[hidden]` element with an id. Any control opens it via
660 * `data-wpsubs-modal-open="<modal-id>"`; any control inside closes it via
661 * `data-wpsubs-modal-close` (the backdrop and the header/footer buttons use it).
662 * Escape also closes. No per-page wiring needed.
663 *
664 * A modal with `data-wpsubs-modal-autoopen` opens automatically on load (e.g. the
665 * Pro-upgrade preview modals). Opening locks body scroll; it's restored when the
666 * last open modal closes.
667 *
668 * Usage:
669 * PHP: wpsubs_render_modal( $args ) — renders the markup
670 * JS: auto-inits; WPSubsModal.open(id) / .close(id) available programmatically
671 *
672 * Events fired on the modal element (bubbles):
673 * wpsubs:modal:open / wpsubs:modal:close
674 */
675 (function () {
676 "use strict";
677
678 /**
679 * @param {string|HTMLElement} target Modal id or element.
680 * @return {HTMLElement|null}
681 */
682 function resolve(target) {
683 if (target instanceof HTMLElement) return target;
684 return document.getElementById(String(target));
685 }
686
687 function open(target) {
688 var modal = resolve(target);
689 if (!modal) return;
690 modal.hidden = false;
691 document.body.style.overflow = "hidden";
692 modal.dispatchEvent(new CustomEvent("wpsubs:modal:open", { bubbles: true }));
693 var focusable = modal.querySelector("input, textarea, select, button:not([data-wpsubs-modal-close])");
694 if (focusable) focusable.focus();
695 }
696
697 function close(target) {
698 var modal = resolve(target);
699 if (!modal) return;
700 modal.hidden = true;
701 // Restore body scroll only when no modal remains open.
702 if (!document.querySelector(".wpsubs-modal:not([hidden])")) {
703 document.body.style.overflow = "";
704 }
705 modal.dispatchEvent(new CustomEvent("wpsubs:modal:close", { bubbles: true }));
706 }
707
708 function closeAll() {
709 document.querySelectorAll(".wpsubs-modal:not([hidden])").forEach(function (m) {
710 close(m);
711 });
712 }
713
714 // Delegated open/close — works for markup added after load too.
715 document.addEventListener("click", function (e) {
716 var opener = e.target.closest("[data-wpsubs-modal-open]");
717 if (opener) {
718 e.preventDefault();
719 open(opener.getAttribute("data-wpsubs-modal-open"));
720 return;
721 }
722 var closer = e.target.closest("[data-wpsubs-modal-close]");
723 if (closer) {
724 e.preventDefault();
725 var modal = closer.closest(".wpsubs-modal");
726 if (modal) close(modal);
727 }
728 });
729
730 document.addEventListener("keydown", function (e) {
731 if (e.key === "Escape") closeAll();
732 });
733
734 /**
735 * Open any modal flagged to auto-open on load.
736 *
737 * @param {Document|HTMLElement} [root]
738 */
739 function init(root) {
740 (root || document).querySelectorAll(".wpsubs-modal[data-wpsubs-modal-autoopen]").forEach(function (modal) {
741 open(modal);
742 });
743 }
744
745 if (document.readyState === "loading") {
746 document.addEventListener("DOMContentLoaded", function () {
747 init();
748 });
749 } else {
750 init();
751 }
752
753 // Public API
754 window.WPSubsModal = { open: open, close: close, init: init };
755 })();
756