PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.1.6
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.1.6
1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.2.0 All 28 releases
xspeed / assets / deactivate.js

deactivate.js in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.1.6, at assets/deactivate.js

233 lines 6.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * xSpeed — deactivation feedback modal.
3 *
4 * Intercepts the Deactivate link for xSpeed Cache on the Plugins screen and
5 * shows a short, optional survey. On "Submit & Deactivate" the selected
6 * reason(s) are POSTed to admin-ajax (which stores them for Usage_Tracker to
7 * transmit); skipping / closing sends nothing. In every case the browser then
8 * follows WordPress's real deactivate URL (which carries its own nonce), so
9 * deactivation itself is never blocked or altered.
10 *
11 * Reasons are multi-select (checkboxes) — the admin may pick one or many.
12 *
13 * Vanilla JS, no dependencies. All user-facing copy is rendered server-side.
14 *
15 * NOTE: this file is enqueued in the footer, but WordPress prints the modal
16 * markup (admin_footer-plugins.php) AFTER the enqueued footer scripts. So we
17 * MUST defer wiring until the DOM is fully parsed — running at parse time,
18 * getElementById('xspeed-deactivate-modal') returns null and the survey never
19 * binds. See the DOMContentLoaded guard at the bottom.
20 */
21 (function () {
22 'use strict';
23
24 function init() {
25 var cfg = window.XSpeedDeactivate;
26 if (!cfg) {
27 return;
28 }
29
30 var modal = document.getElementById('xspeed-deactivate-modal');
31 var link = findDeactivateLink(cfg);
32 if (!modal || !link) {
33 return;
34 }
35
36 var deactivateUrl = link.getAttribute('href');
37 var lastFocus = null;
38 var pending = false;
39 var navigated = false;
40
41 /* --- wiring --------------------------------------------------------- */
42
43 link.addEventListener('click', function (e) {
44 if (!deactivateUrl) {
45 return;
46 }
47 e.preventDefault();
48 openModal();
49 });
50
51 each(reasonInputs(), function (input) {
52 input.addEventListener('change', function () {
53 syncReason(input);
54 });
55 });
56
57 each(modal.querySelectorAll('[data-xspeed-close]'), function (el) {
58 el.addEventListener('click', function (e) {
59 e.preventDefault();
60 closeModal();
61 });
62 });
63
64 on(modal.querySelector('[data-xspeed-skip]'), 'click', function (e) {
65 e.preventDefault();
66 go(); // deactivate without sending anything
67 });
68
69 on(modal.querySelector('[data-xspeed-submit]'), 'click', function (e) {
70 e.preventDefault();
71 submit();
72 });
73
74 document.addEventListener('keydown', function (e) {
75 if (e.key === 'Escape' && isOpen()) {
76 closeModal();
77 }
78 });
79
80 /* --- behavior ------------------------------------------------------- */
81
82 function submit() {
83 if (pending) {
84 return;
85 }
86 pending = true;
87 setLoading(true);
88
89 var payload = new URLSearchParams();
90 payload.append('action', cfg.action);
91 payload.append('nonce', cfg.nonce);
92
93 each(reasonInputs(), function (input) {
94 if (!input.checked) {
95 return;
96 }
97 var id = input.value;
98 payload.append('reason[]', id);
99 var box = detailFor(id);
100 if (box && !box.hidden && box.value) {
101 payload.append('detail_' + id, box.value);
102 }
103 });
104
105 fetch(cfg.ajaxUrl, {
106 method: 'POST',
107 credentials: 'same-origin',
108 headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
109 body: payload.toString()
110 }).then(go, go);
111
112 // Safety net: never trap the user if the request hangs.
113 window.setTimeout(go, 4000);
114 }
115
116 // Show/hide a single reason's follow-up textarea + support callout based
117 // on its own checked state (multi-select: each is independent).
118 function syncReason(input) {
119 var id = input.value;
120 var box = detailFor(id);
121 if (box) {
122 box.hidden = !input.checked;
123 if (input.checked) {
124 box.focus();
125 }
126 }
127 var help = helpFor(id);
128 if (help) {
129 help.hidden = !input.checked;
130 }
131 }
132
133 /* --- modal open/close ---------------------------------------------- */
134
135 function openModal() {
136 lastFocus = document.activeElement;
137 modal.classList.add('is-open');
138 modal.setAttribute('aria-hidden', 'false');
139 document.body.classList.add('xspeed-deactivate-lock');
140 var first = reasonInputs()[0];
141 if (first) {
142 first.focus();
143 }
144 }
145
146 function closeModal() {
147 modal.classList.remove('is-open');
148 modal.setAttribute('aria-hidden', 'true');
149 document.body.classList.remove('xspeed-deactivate-lock');
150 if (lastFocus && typeof lastFocus.focus === 'function') {
151 lastFocus.focus();
152 }
153 }
154
155 function go() {
156 if (navigated) {
157 return;
158 }
159 navigated = true;
160 window.location.href = deactivateUrl;
161 }
162
163 /* --- helpers -------------------------------------------------------- */
164
165 function reasonInputs() {
166 return modal.querySelectorAll('input[name="xspeed-deactivate-reason"]');
167 }
168
169 function detailFor(id) {
170 return modal.querySelector('.xspeed-deactivate__detail[data-for="' + cssAttr(id) + '"]');
171 }
172
173 function helpFor(id) {
174 return modal.querySelector('.xspeed-deactivate__help[data-for="' + cssAttr(id) + '"]');
175 }
176
177 function isOpen() {
178 return modal.classList.contains('is-open');
179 }
180
181 function setLoading(loading) {
182 var btn = modal.querySelector('[data-xspeed-submit]');
183 if (!btn) {
184 return;
185 }
186 btn.disabled = loading;
187 btn.classList.toggle('is-loading', loading);
188 }
189 }
190
191 /**
192 * Locate xSpeed's Deactivate link on the Plugins table. Primary match is the
193 * row's data-plugin attribute; falls back to scanning deactivate links for
194 * our plugin file in the query string (covers markup variations).
195 */
196 function findDeactivateLink(cfg) {
197 var byRow = document.querySelector('tr[data-plugin="' + cssAttr(cfg.plugin) + '"] .deactivate a');
198 if (byRow) {
199 return byRow;
200 }
201 var encoded = encodeURIComponent(cfg.plugin);
202 var candidates = document.querySelectorAll('a[href*="action=deactivate"]');
203 for (var i = 0; i < candidates.length; i++) {
204 var href = candidates[i].getAttribute('href') || '';
205 if (href.indexOf('plugin=' + encoded) !== -1 || href.indexOf('plugin=' + cfg.plugin) !== -1) {
206 return candidates[i];
207 }
208 }
209 return null;
210 }
211
212 function cssAttr(value) {
213 return String(value == null ? '' : value).replace(/"/g, '\\"');
214 }
215
216 function each(list, fn) {
217 Array.prototype.forEach.call(list || [], fn);
218 }
219
220 function on(el, evt, fn) {
221 if (el) {
222 el.addEventListener(evt, fn);
223 }
224 }
225
226 // The modal markup is printed AFTER this script, so wait for the DOM.
227 if (document.readyState === 'loading') {
228 document.addEventListener('DOMContentLoaded', init);
229 } else {
230 init();
231 }
232 })();
233