PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.3
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.3
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-add-redirection.js

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

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