PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.10
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.10
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 2.6.4 2.6.3 All 139 releases
metasync / views / js / metasync-add-redirection.js

metasync-add-redirection.js in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.10, at views/js/metasync-add-redirection.js

440 lines 13.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * MetaSync Add Redirection Form
3 *
4 * Extracted for Phase 5, #887.
5 * Pure JS — form display, tips toggle, source URL management,
6 * unsaved-changes warning, and client-side validation.
7 *
8 * No localized data object required (pure JS, no PHP values).
9 *
10 * @since Phase 5
11 */
12 (function () {
13 // Show the add redirection form when action=add, edit, or redirect (from 404 monitor)
14 function showForm() {
15 var params = new URLSearchParams(window.location.search);
16 var action = params.get('action');
17 if (action !== 'add' && action !== 'edit' && action !== 'redirect') {
18 return;
19 }
20
21 var element = document.getElementById('add-redirection-form');
22 if (element) {
23 element.style.display = 'block';
24 }
25 }
26
27 // Try immediately
28 showForm();
29
30 // Also try on DOMContentLoaded as backup
31 if (document.readyState === 'loading') {
32 document.addEventListener('DOMContentLoaded', showForm);
33 }
34 })();
35
36 // Handle tips toggle
37 document.addEventListener('DOMContentLoaded', function () {
38 var toggleTipsBtn = document.getElementById('toggle-redirection-tips');
39 var tipsContent = document.getElementById('redirection-tips-content');
40
41 if (toggleTipsBtn && tipsContent) {
42 toggleTipsBtn.addEventListener('click', function () {
43 if (tipsContent.style.display === 'none') {
44 tipsContent.style.display = 'block';
45 toggleTipsBtn.innerHTML = '<span class="dashicons dashicons-info" style="margin-top: 3px;"></span> Hide Redirection Tips & Examples';
46 } else {
47 tipsContent.style.display = 'none';
48 toggleTipsBtn.innerHTML = '<span class="dashicons dashicons-info" style="margin-top: 3px;"></span> Show Redirection Tips & Examples';
49 }
50 });
51 }
52
53 // Handle regex pattern field visibility
54 var searchTypeSelects = document.querySelectorAll('select[name="search_type[]"]');
55 var regexRow = document.getElementById('regex_pattern_row');
56
57 function toggleRegexField() {
58 var hasRegex = false;
59 searchTypeSelects.forEach(function (select) {
60 if (select.value === 'regex') {
61 hasRegex = true;
62 }
63 });
64
65 if (regexRow) {
66 regexRow.style.display = hasRegex ? 'table-row' : 'none';
67 }
68 }
69
70 // Initial check
71 toggleRegexField();
72
73 // Listen for changes
74 searchTypeSelects.forEach(function (select) {
75 select.addEventListener('change', toggleRegexField);
76 });
77
78 // Handle adding new source URLs
79 var addButton = document.getElementById('addNewSourceUrl');
80 if (addButton) {
81 addButton.addEventListener('click', function () {
82 var sourceUrlsList = document.getElementById('source_urls');
83 var newItem = document.createElement('li');
84 newItem.innerHTML =
85 '<input type="text" class="regular-text" name="source_url[]" value="">' +
86 '<select name="search_type[]">' +
87 '<option value="exact">Exact Match</option>' +
88 '<option value="start">Starts With</option>' +
89 '<option value="end">Ends With</option>' +
90 '<option value="wildcard">Wildcard (*)</option>' +
91 '<option value="regex">Regex Pattern</option>' +
92 '</select>' +
93 '<button type="button" class="source_url_delete">Remove</button>';
94 sourceUrlsList.appendChild(newItem);
95
96 // Add event listener to new select
97 var newSelect = newItem.querySelector('select[name="search_type[]"]');
98 newSelect.addEventListener('change', toggleRegexField);
99
100 // Add event listener to remove button
101 var removeButton = newItem.querySelector('.source_url_delete');
102 removeButton.addEventListener('click', function () {
103 newItem.remove();
104 toggleRegexField();
105 });
106 });
107 }
108
109 // Handle remove buttons
110 document.addEventListener('click', function (e) {
111 if (e.target.classList.contains('source_url_delete')) {
112 e.target.closest('li').remove();
113 toggleRegexField();
114 }
115 });
116
117 // Track form changes for unsaved changes warning
118 var formModified = false;
119 var formInputs = document.querySelectorAll('#add-redirection-form input, #add-redirection-form select, #add-redirection-form textarea');
120
121 formInputs.forEach(function (input) {
122 // Skip hidden inputs and the cancel button itself
123 if (input.type !== 'hidden' && input.id !== 'cancel-redirection') {
124 input.addEventListener('change', function () {
125 formModified = true;
126 });
127 input.addEventListener('input', function () {
128 formModified = true;
129 });
130 }
131 });
132
133 // Handle cancel button
134 var cancelButton = document.getElementById('cancel-redirection');
135 if (cancelButton) {
136 cancelButton.addEventListener('click', function (e) {
137 e.preventDefault();
138
139 // Check if form has been modified
140 if (formModified) {
141 var confirmCancel = confirm('You have unsaved changes. Are you sure you want to cancel?');
142 if (!confirmCancel) {
143 return;
144 }
145 }
146
147 // Redirect back to redirections list
148 // The cancel URL is set as a data attribute on the button by the server
149 var cancelUrl = cancelButton.getAttribute('data-cancel-url');
150 if (cancelUrl) {
151 window.location.href = cancelUrl;
152 } else {
153 window.history.back();
154 }
155 });
156 }
157
158 // Warn user about unsaved changes when leaving page
159 window.addEventListener('beforeunload', function (e) {
160 if (formModified) {
161 e.preventDefault();
162 e.returnValue = ''; // Modern browsers require this
163 return ''; // Some older browsers show this message
164 }
165 });
166
167
168 // Validation helper functions
169 function showError(element, message) {
170 // Remove any existing error
171 hideError(element);
172
173 // Create error message
174 var errorDiv = document.createElement('div');
175 errorDiv.className = 'validation-error';
176 errorDiv.style.cssText = 'color: var(--dashboard-error); font-size: 13px; margin-top: 5px; font-weight: 500; width: 100%;';
177 errorDiv.textContent = message;
178
179 // Add error styling to input
180 element.style.borderColor = 'var(--dashboard-error)';
181 element.style.boxShadow = '0 0 0 3px rgba(239, 68, 68, 0.1)';
182
183 // Insert error below the parent li (or after element if no li)
184 var li = element.closest('li');
185 if (li) {
186 li.style.flexWrap = 'wrap';
187 li.appendChild(errorDiv);
188 } else {
189 element.parentNode.insertBefore(errorDiv, element.nextSibling);
190 }
191 }
192
193 function hideError(element) {
194 // Remove error styling
195 element.style.borderColor = '';
196 element.style.boxShadow = '';
197
198 // Remove error message from parent li or sibling
199 var li = element.closest('li');
200 if (li) {
201 var err = li.querySelector('.validation-error');
202 if (err) {
203 err.remove();
204 }
205 li.style.flexWrap = '';
206 } else {
207 var nextEl = element.nextElementSibling;
208 if (nextEl && nextEl.classList.contains('validation-error')) {
209 nextEl.remove();
210 }
211 }
212 }
213
214 function validateURL(url) {
215 // Allow relative paths starting with /
216 if (url.startsWith('/')) {
217 return true;
218 }
219
220 // Allow full URLs
221 try {
222 new URL(url);
223 return true;
224 } catch (e) {
225 return false;
226 }
227 }
228
229 function isValidRegex(pattern) {
230 try {
231 new RegExp(pattern);
232 return true;
233 } catch (e) {
234 return false;
235 }
236 }
237
238 // Real-time validation for inputs
239 function setupRealtimeValidation() {
240 // Validate source URLs on blur
241 document.addEventListener('blur', function (e) {
242 if (e.target.matches('input[name="source_url[]"]')) {
243 var value = e.target.value.trim();
244 if (value && !validateURL(value)) {
245 showError(e.target, 'Please enter a valid URL (e.g., /path or https://example.com)');
246 } else {
247 hideError(e.target);
248 }
249 }
250 }, true);
251
252 // Validate destination URL on blur
253 var destinationUrl = document.getElementById('destination_url');
254 if (destinationUrl) {
255 destinationUrl.addEventListener('blur', function () {
256 var value = this.value.trim();
257 var redirectType = document.querySelector('input[name="redirect_type"]:checked');
258
259 if (redirectType && redirectType.value !== '410' && redirectType.value !== '451') {
260 if (value && !validateURL(value)) {
261 showError(this, 'Please enter a valid URL (e.g., /path or https://example.com)');
262 } else {
263 hideError(this);
264 }
265 }
266 });
267 }
268
269 // Validate regex pattern on blur
270 var regexPattern = document.getElementById('regex_pattern');
271 if (regexPattern) {
272 regexPattern.addEventListener('blur', function () {
273 var value = this.value.trim();
274 if (value && !isValidRegex(value)) {
275 showError(this, 'Invalid regex pattern. Example: /^\\/old-path\\/.*$/');
276 } else {
277 hideError(this);
278 }
279 });
280 }
281 }
282
283 setupRealtimeValidation();
284
285 // Form validation on submit
286 var form = document.querySelector('#redirection-form');
287 if (form) {
288 form.addEventListener('submit', function (e) {
289 // Skip validation for filter/search submits — only validate the Save button.
290 var submitter = e.submitter;
291 if (submitter && submitter.name !== 'submit') {
292 return true;
293 }
294
295 var isValid = true;
296 var errors = [];
297
298 // Clear all previous errors
299 document.querySelectorAll('.validation-error').forEach(function (el) {
300 el.remove();
301 });
302 document.querySelectorAll('input[type="text"], input[type="url"], select').forEach(function (el) {
303 el.style.borderColor = '';
304 el.style.boxShadow = '';
305 });
306
307 // 1. Validate source URLs
308 var sourceInputs = document.querySelectorAll('input[name="source_url[]"]');
309 var sourceUrls = [];
310 var hasEmptySource = false;
311 var hasInvalidSource = false;
312
313 sourceInputs.forEach(function (input) {
314 var value = input.value.trim();
315
316 if (!value) {
317 hasEmptySource = true;
318 showError(input, 'Source URL is required');
319 isValid = false;
320 } else if (!validateURL(value)) {
321 hasInvalidSource = true;
322 showError(input, 'Please enter a valid URL (e.g., /path or https://example.com)');
323 isValid = false;
324 } else {
325 // Check for duplicates
326 if (sourceUrls.includes(value)) {
327 showError(input, 'Duplicate source URL detected');
328 isValid = false;
329 } else {
330 sourceUrls.push(value);
331 }
332 }
333 });
334
335 if (hasEmptySource) {
336 errors.push('All source URL fields must be filled in.');
337 }
338 if (hasInvalidSource) {
339 errors.push('Please enter valid URLs for all source fields.');
340 }
341
342 // 2. Validate redirection type
343 var redirectType = document.querySelector('input[name="redirect_type"]:checked');
344 if (!redirectType) {
345 errors.push('Please select a redirection type.');
346 isValid = false;
347 }
348
349 // 3. Validate destination URL (if required)
350 var destinationUrlEl = document.getElementById('destination_url');
351 if (redirectType && redirectType.value !== '410' && redirectType.value !== '451') {
352 var destValue = destinationUrlEl.value.trim();
353 if (!destValue) {
354 showError(destinationUrlEl, 'Destination URL is required for this redirect type');
355 errors.push('Please enter a destination URL.');
356 isValid = false;
357 } else if (!validateURL(destValue)) {
358 showError(destinationUrlEl, 'Please enter a valid URL (e.g., /path or https://example.com)');
359 errors.push('Please enter a valid destination URL.');
360 isValid = false;
361 }
362 }
363
364 // 4. Validate regex pattern (if regex is selected)
365 var hasRegexPattern = false;
366 document.querySelectorAll('select[name="search_type[]"]').forEach(function (select) {
367 if (select.value === 'regex') {
368 hasRegexPattern = true;
369 }
370 });
371
372 if (hasRegexPattern) {
373 var regexPatternEl = document.getElementById('regex_pattern');
374 var regexValue = regexPatternEl ? regexPatternEl.value.trim() : '';
375
376 if (!regexValue) {
377 if (regexPatternEl) {
378 showError(regexPatternEl, 'Regex pattern is required when using "Regex Pattern" type');
379 }
380 errors.push('Please enter a regex pattern when using "Regex Pattern" as the pattern type.');
381 isValid = false;
382 } else if (!isValidRegex(regexValue)) {
383 if (regexPatternEl) {
384 showError(regexPatternEl, 'Invalid regex pattern. Example: /^\\/old-path\\/.*$/');
385 }
386 errors.push('Invalid regex pattern. Please fix the regex pattern.');
387 isValid = false;
388 }
389 }
390
391 // 5. Validate status is selected
392 var status = document.querySelector('input[name="status"]:checked');
393 if (!status) {
394 errors.push('Please select a status (Active or Inactive).');
395 isValid = false;
396 }
397
398 // Show consolidated error message if validation fails
399 if (!isValid) {
400 e.preventDefault();
401
402 // Create or update error summary at the top of the form
403 var errorSummary = document.getElementById('validation-error-summary');
404 if (!errorSummary) {
405 errorSummary = document.createElement('div');
406 errorSummary.id = 'validation-error-summary';
407 errorSummary.style.cssText = 'background: rgba(239, 68, 68, 0.1); border: 1px solid var(--dashboard-error); border-radius: 8px; padding: 15px; margin-bottom: 20px; color: var(--dashboard-error);';
408
409 var formDiv = document.getElementById('add-redirection-form');
410 var firstTable = formDiv.querySelector('.form-table');
411 formDiv.insertBefore(errorSummary, firstTable);
412 }
413
414 var uniqueErrors = errors.filter(function (err, idx, arr) {
415 return arr.indexOf(err) === idx;
416 });
417 errorSummary.innerHTML = '<strong>Please fix the following errors:</strong><ul style="margin: 10px 0 0 20px; padding: 0;">' +
418 uniqueErrors.map(function (err) {
419 return '<li>' + err + '</li>';
420 }).join('') +
421 '</ul>';
422
423 // Scroll to error summary
424 errorSummary.scrollIntoView({ behavior: 'smooth', block: 'center' });
425
426 return false;
427 } else {
428 // Remove error summary if it exists
429 var existingSummary = document.getElementById('validation-error-summary');
430 if (existingSummary) {
431 existingSummary.remove();
432 }
433 }
434
435 // If validation passes, clear the modified flag to prevent beforeunload warning
436 formModified = false;
437 });
438 }
439 });
440