PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 1.10.2
Subscriptions for WooCommerce with Stripe Recurring Payments v1.10.2
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 1.4.1 1.4.2 1.5.0 1.5.1 All 61 releases
subscription / assets / js / admin-components.js

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

412 lines 12.5 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