PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.0
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.0
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / views / js / metasync-redirections.js

metasync-redirections.js in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.0, at views/js/metasync-redirections.js

261 lines 9.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * MetaSync redirections — filter auto-submit and pagination tab fix.
3 *
4 * Extracted from views/metasync-redirection.php (Phase 5, #887).
5 */
6 document.addEventListener('DOMContentLoaded', function () {
7 // Auto-submit filters when changed
8 var filterSelects = document.querySelectorAll('#status-filter, #pattern-filter, #http-code-filter');
9 var form = document.getElementById('redirection-form');
10 filterSelects.forEach(function (select) {
11 select.addEventListener('change', function () {
12 if (window.metasyncNavigateListState && window.metasyncNavigateListState(form, 'redirections')) {
13 return;
14 }
15
16 var formAction = form.getAttribute('action') || window.location.href;
17 var url = new URL(formAction, window.location.origin);
18 url.searchParams.delete('paged_redir');
19 form.setAttribute('action', url.pathname + url.search);
20
21 // Also add hidden field to ensure pagination resets
22 var pagedInput = form.querySelector('input[name="paged_redir"]');
23 if (!pagedInput) {
24 pagedInput = document.createElement('input');
25 pagedInput.type = 'hidden';
26 pagedInput.name = 'paged_redir';
27 form.appendChild(pagedInput);
28 }
29 pagedInput.value = '1';
30
31 form.submit();
32 });
33 });
34
35 // Confirm destructive bulk actions.
36 //
37 // Mirrors the 404 monitor's guard. Reads BOTH dropdowns because the top one
38 // posts `action` and the bottom one posts `action2`. Only Delete is
39 // confirmed - Activate / Deactivate are reversible and do not warrant a
40 // prompt. Skipped when Filter is the submitter, since the server ignores
41 // bulk actions on a filter_action submit.
42 var redirectionForm = document.getElementById('redirection-form');
43 if (redirectionForm) {
44 redirectionForm.addEventListener('submit', function (e) {
45 var submitterId = e.submitter ? e.submitter.id : '';
46 if (submitterId === 'redirections-filter-submit' || submitterId === 'redirections-search-submit') {
47 if (window.metasyncNavigateListState) {
48 e.preventDefault();
49 window.metasyncNavigateListState(redirectionForm, 'redirections');
50 }
51 return;
52 }
53
54 var topSelect = document.getElementById('bulk-action-selector-top');
55 var bottomSelect = document.getElementById('bulk-action-selector-bottom');
56 var action = '-1';
57
58 if (topSelect && topSelect.value !== '-1' && topSelect.value !== '') {
59 action = topSelect.value;
60 } else if (bottomSelect && bottomSelect.value !== '-1' && bottomSelect.value !== '') {
61 action = bottomSelect.value;
62 }
63
64 if (action !== 'delete_bulk') {
65 return;
66 }
67
68 var checked = document.querySelectorAll('#redirection-form input[name="items[]"]:checked');
69 if (checked.length === 0) {
70 return;
71 }
72
73 var message = checked.length > 1
74 ? 'Are you sure you want to delete the ' + checked.length + ' selected redirects? This cannot be undone.'
75 : 'Are you sure you want to delete the selected redirect? This cannot be undone.';
76
77 if (!confirm(message)) {
78 e.preventDefault();
79 }
80 });
81 }
82
83 // Submit the search when Enter is pressed in the search field.
84 //
85 // #redirection-form also contains the (hidden until action=add/edit) "Save"
86 // button from the add/edit-redirection panel, and that button is first in
87 // tree order. Per the HTML spec, pressing Enter in a text field submits the
88 // form via its *default* button (the first submit button in tree order),
89 // even if that button is hidden — so Enter here would otherwise submit as
90 // the "Save" button instead of "Search", which fails the add/edit form's
91 // validation and gets silently cancelled. Explicitly clicking the actual
92 // Search button reproduces exactly what a mouse click does, sidestepping
93 // the browser's implicit-submission button choice.
94 var searchInput = document.getElementById('redirections-search-input');
95 var searchSubmit = document.getElementById('redirections-search-submit');
96 if (searchInput && searchSubmit) {
97 searchInput.addEventListener('keydown', function (e) {
98 if (e.key === 'Enter') {
99 e.preventDefault();
100 searchSubmit.click();
101 }
102 });
103 }
104
105 // Add tab parameter to all pagination links in redirections tab
106 function addTabToPaginationLinks() {
107 // Find all pagination links within redirections-content
108 var redirectionsContent = document.getElementById('redirections-content');
109 if (redirectionsContent) {
110 var paginationLinks = redirectionsContent.querySelectorAll('.tablenav-pages a');
111 paginationLinks.forEach(function (link) {
112 var url = new URL(link.href);
113 url.searchParams.set('tab', 'redirections');
114 link.href = url.toString();
115 });
116 }
117 }
118
119 // Run immediately and after a short delay
120 addTabToPaginationLinks();
121 setTimeout(addTabToPaginationLinks, 100);
122 setTimeout(addTabToPaginationLinks, 500);
123
124 // --- Health Check ---
125 var healthBtn = document.getElementById('metasync-check-health-btn');
126 if (!healthBtn || typeof metasyncHealthCheck === 'undefined') {
127 return;
128 }
129
130 var healthBtnLabel = healthBtn.textContent;
131
132 // Single reusable tooltip appended to body — escapes all stacking contexts
133 var floatingTip = document.createElement('div');
134 floatingTip.className = 'health-tooltip-floating';
135 floatingTip.style.display = 'none';
136 document.body.appendChild(floatingTip);
137
138 // Hide tooltip on scroll to prevent detachment from icon
139 window.addEventListener('scroll', function () {
140 floatingTip.style.display = 'none';
141 }, true);
142
143 // Delegated hover handlers — work for any health cells added now or later
144 document.addEventListener('mouseover', function (e) {
145 var cell = e.target.closest('.health-status-cell[data-health-tip]');
146 if (!cell) {
147 return;
148 }
149 var rect = cell.getBoundingClientRect();
150 floatingTip.textContent = cell.getAttribute('data-health-tip');
151 floatingTip.style.display = 'block';
152 var tipRect = floatingTip.getBoundingClientRect();
153 floatingTip.style.left = (rect.left + rect.width / 2 - tipRect.width / 2) + 'px';
154 floatingTip.style.top = (rect.top - tipRect.height - 6) + 'px';
155 });
156
157 document.addEventListener('mouseout', function (e) {
158 var cell = e.target.closest('.health-status-cell[data-health-tip]');
159 if (!cell) {
160 return;
161 }
162 floatingTip.style.display = 'none';
163 });
164
165 healthBtn.addEventListener('click', function () {
166 healthBtn.disabled = true;
167 healthBtn.innerHTML = healthBtnLabel + ' <span class="spinner is-active" style="float:none;margin:0 0 0 6px;vertical-align:middle;"></span>';
168
169 var data = new FormData();
170 data.append('action', 'metasync_check_redirects_health');
171 data.append('nonce', metasyncHealthCheck.healthNonce);
172
173 fetch(metasyncHealthCheck.ajaxUrl, { method: 'POST', body: data, credentials: 'same-origin' })
174 .then(function (r) {
175 return r.json();
176 })
177 .then(function (resp) {
178 healthBtn.disabled = false;
179 healthBtn.textContent = healthBtnLabel;
180
181 if (!resp.success || !resp.data || !resp.data.results) {
182 return;
183 }
184
185 var results = resp.data.results;
186 var map = {};
187 results.forEach(function (r) {
188 map[r.id] = r;
189 });
190
191 var statusIcons = { ok: '�
192 ', loop: '🔴', chain_too_long: '⚠️', dead_end: '💀' };
193 var statusLabels = { ok: 'OK', loop: 'Loop detected', chain_too_long: 'Chain too long', dead_end: 'Dead end' };
194
195 // Insert header column
196 var table = document.querySelector('.wp-list-table');
197 if (!table) {
198 return;
199 }
200
201 // Remove existing health columns (re-run support)
202 table.querySelectorAll('.column-health_status').forEach(function (el) {
203 el.remove();
204 });
205
206 var headerRows = table.querySelectorAll('thead tr, tfoot tr');
207 headerRows.forEach(function (tr) {
208 var statusTh = tr.querySelector('.column-status');
209 if (!statusTh) {
210 // Fallback: insert at end
211 statusTh = tr.lastElementChild;
212 }
213 var th = document.createElement('th');
214 th.className = 'manage-column column-health_status';
215 th.textContent = 'Health';
216 statusTh.parentNode.insertBefore(th, statusTh.nextSibling);
217 });
218
219 // Insert cell per row
220 var bodyRows = table.querySelectorAll('tbody tr');
221 bodyRows.forEach(function (tr) {
222 var cb = tr.querySelector('input[type="checkbox"]');
223 var id = cb ? parseInt(cb.value, 10) : 0;
224 var info = map[id];
225
226 var td = document.createElement('td');
227 td.className = 'column-health_status';
228
229 if (info) {
230 var icon = statusIcons[info.status] || '❓';
231 var label = statusLabels[info.status] || info.status;
232 var tipText = label;
233 if (info.status !== 'ok' && info.chain && info.chain.length > 0) {
234 tipText += ': ' + info.chain.join(' \u2192 ');
235 }
236 td.innerHTML = '<span class="health-status-cell" data-health-tip="' +
237 tipText.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;') +
238 '">' + icon + '</span>';
239 } else {
240 td.textContent = '\u2014';
241 }
242
243 var statusTd = tr.querySelector('.column-status');
244 if (statusTd) {
245 statusTd.parentNode.insertBefore(td, statusTd.nextSibling);
246 } else {
247 tr.appendChild(td);
248 }
249 });
250
251
252 // Scroll to the table so results are visible
253 table.scrollIntoView({ behavior: 'smooth', block: 'start' });
254 })
255 .catch(function () {
256 healthBtn.disabled = false;
257 healthBtn.textContent = healthBtnLabel;
258 });
259 });
260 });
261