PluginProbe
Subscriptions for WooCommerce with Stripe Recurring Payments / 2.1.0
Subscriptions for WooCommerce with Stripe Recurring Payments v2.1.0
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 / tag-select.js

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

276 lines 8.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * WPSubsTagSelect — pill/tag input with inline filter and filterable dropdown.
3 *
4 * Usage:
5 * PHP: wpsubs_render_tag_select( $args ) — renders the HTML
6 * JS: WPSubsTagSelect.init() — auto-inits all .wpsubs-tag-select elements
7 *
8 * Events fired on the root element (bubbles):
9 * wpsubs:select — { value, label, selected } when a pill is added or removed
10 */
11 (function () {
12 "use strict";
13
14 var instances = [];
15
16 /**
17 * @param {HTMLElement} el Root .wpsubs-tag-select element.
18 */
19 function WPSubsTagSelect(el) {
20 this.el = el;
21 this.multiple = !!el.dataset.multiple;
22 this.fieldName = el.dataset.name || "";
23 this.field = el.querySelector(".wpsubs-tag-select__field");
24 this.input = el.querySelector(".wpsubs-tag-select__input");
25 this.dropdown = el.querySelector(".wpsubs-tag-select__dropdown");
26 this.list = el.querySelector(".wpsubs-tag-select__list");
27 this.emptyEl = el.querySelector(".wpsubs-tag-select__empty");
28 this._bind();
29 instances.push(this);
30 }
31
32 WPSubsTagSelect.prototype.open = function () {
33 closeAll(this);
34 this.el.classList.add("wpsubs-tag-select--open");
35 this._filterItems("");
36 if (this.input) this.input.focus();
37 };
38
39 WPSubsTagSelect.prototype.close = function () {
40 this.el.classList.remove("wpsubs-tag-select--open");
41 if (this.input) {
42 this.input.value = "";
43 this._filterItems("");
44 }
45 };
46
47 WPSubsTagSelect.prototype.isOpen = function () {
48 return this.el.classList.contains("wpsubs-tag-select--open");
49 };
50
51 /** Show/hide dropdown items based on query, always hiding selected ones. */
52 WPSubsTagSelect.prototype._filterItems = function (query) {
53 var q = query.trim().toLowerCase();
54 var items = this.list ? this.list.querySelectorAll(".wpsubs-tag-select__item") : [];
55 var visible = 0;
56 items.forEach(function (item) {
57 if (item.hasAttribute("data-selected")) {
58 item.style.display = "none";
59 return;
60 }
61 var text = item.textContent.toLowerCase();
62 var match = !q || text.indexOf(q) !== -1;
63 item.style.display = match ? "" : "none";
64 if (match) visible++;
65 });
66 if (this.emptyEl) {
67 this.emptyEl.style.display = visible === 0 ? "" : "none";
68 }
69 };
70
71 /** Add a pill for the given dropdown item. */
72 WPSubsTagSelect.prototype._addPill = function (item) {
73 var value = item.dataset.value !== undefined ? item.dataset.value : "";
74 var label = item.textContent.trim();
75
76 // For single-select, remove the existing pill first.
77 if (!this.multiple) {
78 var existing = this.el.querySelectorAll(".wpsubs-tag-select__pill");
79 var self = this;
80 existing.forEach(function (p) {
81 self._removePillEl(p, false);
82 });
83 }
84
85 // Build the pill.
86 var pill = document.createElement("span");
87 pill.className = "wpsubs-tag-select__pill";
88 pill.dataset.value = value;
89
90 var pillLabel = document.createElement("span");
91 pillLabel.className = "wpsubs-tag-select__pill-label";
92 pillLabel.textContent = label;
93
94 var removeBtn = document.createElement("button");
95 removeBtn.type = "button";
96 removeBtn.className = "wpsubs-tag-select__pill-remove";
97 removeBtn.setAttribute("aria-label", "Remove " + label);
98 removeBtn.innerHTML = "✕"; // ×
99
100 pill.appendChild(pillLabel);
101 pill.appendChild(removeBtn);
102
103 // Insert the pill before the text input.
104 if (this.input) {
105 this.field.insertBefore(pill, this.input);
106 } else {
107 this.field.appendChild(pill);
108 }
109
110 // Mark dropdown item as selected so it stays hidden.
111 item.setAttribute("data-selected", "");
112 item.style.display = "none";
113
114 this._syncHiddenInputs();
115 this._updateInputPlaceholder();
116
117 if (this.input) this.input.value = "";
118 this._filterItems("");
119
120 this.el.dispatchEvent(
121 new CustomEvent("wpsubs:select", {
122 bubbles: true,
123 detail: { value: value, label: label, selected: true },
124 }),
125 );
126 };
127
128 /** Remove a pill element. Pass sync=true to update hidden inputs (default). */
129 WPSubsTagSelect.prototype._removePillEl = function (pill, sync) {
130 var value = pill.dataset.value !== undefined ? pill.dataset.value : "";
131 var label = pill.querySelector(".wpsubs-tag-select__pill-label");
132 var labelText = label ? label.textContent.trim() : value;
133
134 pill.parentNode.removeChild(pill);
135
136 // Un-mark the corresponding dropdown item.
137 var item = this.list
138 ? this.list.querySelector('.wpsubs-tag-select__item[data-value="' + CSS.escape(value) + '"]')
139 : null;
140 if (item) {
141 item.removeAttribute("data-selected");
142 }
143
144 if (sync !== false) {
145 this._syncHiddenInputs();
146 this._updateInputPlaceholder();
147 this._filterItems(this.input ? this.input.value : "");
148
149 this.el.dispatchEvent(
150 new CustomEvent("wpsubs:select", {
151 bubbles: true,
152 detail: { value: value, label: labelText, selected: false },
153 }),
154 );
155 }
156 };
157
158 /** Rebuild hidden inputs to match current pills. */
159 WPSubsTagSelect.prototype._syncHiddenInputs = function () {
160 var existing = this.el.querySelectorAll("input[data-ts-val]");
161 var fieldName = existing.length > 0 ? existing[0].name : this.fieldName + (this.multiple ? "[]" : "");
162
163 existing.forEach(function (inp) {
164 inp.parentNode.removeChild(inp);
165 });
166
167 var pills = this.el.querySelectorAll(".wpsubs-tag-select__pill");
168 var self = this;
169
170 if (this.multiple) {
171 if (pills.length === 0) {
172 // Empty sentinel so the form field is always present on submit.
173 var sentinel = document.createElement("input");
174 sentinel.type = "hidden";
175 sentinel.name = fieldName;
176 sentinel.value = "";
177 sentinel.setAttribute("data-ts-val", "");
178 self.el.appendChild(sentinel);
179 } else {
180 pills.forEach(function (pill) {
181 var inp = document.createElement("input");
182 inp.type = "hidden";
183 inp.name = fieldName;
184 inp.value = pill.dataset.value !== undefined ? pill.dataset.value : "";
185 inp.setAttribute("data-ts-val", "");
186 self.el.appendChild(inp);
187 });
188 }
189 } else {
190 var inp = document.createElement("input");
191 inp.type = "hidden";
192 inp.name = fieldName;
193 inp.value = pills.length > 0 && pills[0].dataset.value !== undefined ? pills[0].dataset.value : "";
194 inp.setAttribute("data-ts-val", "");
195 self.el.appendChild(inp);
196 }
197 };
198
199 /** Show placeholder only when there are no pills. */
200 WPSubsTagSelect.prototype._updateInputPlaceholder = function () {
201 if (!this.input) return;
202 var pills = this.el.querySelectorAll(".wpsubs-tag-select__pill");
203 this.input.placeholder = pills.length === 0 ? this.el.dataset.placeholder || "" : "";
204 };
205
206 WPSubsTagSelect.prototype._bind = function () {
207 var self = this;
208
209 if (self.field) {
210 self.field.addEventListener("click", function (e) {
211 e.stopPropagation();
212 var removeBtn = e.target.closest(".wpsubs-tag-select__pill-remove");
213 if (removeBtn) {
214 var pill = removeBtn.closest(".wpsubs-tag-select__pill");
215 if (pill) self._removePillEl(pill);
216 return;
217 }
218 self.open();
219 });
220 }
221
222 if (self.input) {
223 self.input.addEventListener("input", function () {
224 if (!self.isOpen()) self.open();
225 self._filterItems(self.input.value);
226 });
227 }
228
229 if (self.dropdown) {
230 self.dropdown.addEventListener("click", function (e) {
231 e.stopPropagation();
232 var item = e.target.closest(".wpsubs-tag-select__item");
233 if (!item || item.hasAttribute("data-disabled")) return;
234 self._addPill(item);
235 if (!self.multiple) self.close();
236 });
237 }
238 };
239
240 function closeAll(except) {
241 instances.forEach(function (inst) {
242 if (inst !== except) inst.close();
243 });
244 }
245
246 /**
247 * Initialise all un-initialised .wpsubs-tag-select elements under root.
248 *
249 * @param {Document|HTMLElement} [root]
250 */
251 function init(root) {
252 (root || document).querySelectorAll(".wpsubs-tag-select:not([data-ts-init])").forEach(function (el) {
253 el.setAttribute("data-ts-init", "1");
254 new WPSubsTagSelect(el);
255 });
256 }
257
258 document.addEventListener("click", function () {
259 closeAll();
260 });
261 document.addEventListener("keydown", function (e) {
262 if (e.key === "Escape") closeAll();
263 });
264
265 if (document.readyState === "loading") {
266 document.addEventListener("DOMContentLoaded", function () {
267 init();
268 });
269 } else {
270 init();
271 }
272
273 // Public API
274 window.WPSubsTagSelect = { init: init };
275 })();
276