PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.24
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.24
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 2.5.23 All 138 releases
metasync / media-optimization / assets / js / media-optimization-library.js

media-optimization-library.js in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.24, at media-optimization/assets/js/media-optimization-library.js

576 lines 22.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Media Optimization Image Library Scripts
3 *
4 * @package Search Atlas SEO
5 * @copyright Copyright (C) 2021-2025, Search Atlas Group - support@searchatlas.com
6 * @since 2.6.0
7 *
8 * Localized data expected via metasyncMediaLib:
9 * - ajaxUrl: string
10 * - nonce: string
11 * - batchRunning: bool
12 * - i18n.optimizing: string
13 * - i18n.optimize: string
14 * - i18n.revert: string
15 * - i18n.revertConfirm: string
16 * - i18n.optimizeFailed:string
17 * - i18n.revertFailed: string
18 * - i18n.startBatch: string
19 * - i18n.batchFailed: string
20 * - i18n.batchComplete: string
21 * - i18n.imagesProcessed: string
22 * - i18n.failed: string
23 * - i18n.optimizingOf: string (e.g. "Optimizing")
24 * - i18n.of: string
25 * - i18n.images: string (e.g. "images...")
26 * - i18n.unoptimized: string
27 * - i18n.selectImages: string
28 * - i18n.bulkFailed: string
29 * - i18n.apply: string
30 * - i18n.unoptimizing: string
31 * - i18n.unoptimizeConfirm: string
32 * - i18n.bulkUnoptimizeFailed: string
33 * - i18n.alreadyUnoptimized: string
34 */
35 (function() {
36 'use strict';
37
38 var config = window.metasyncMediaLib || {};
39 var nonce = config.nonce || '';
40 var ajaxUrl = config.ajaxUrl || window.ajaxurl;
41 var i18n = config.i18n || {};
42 var batchActive = false;
43 var fallbackPoll = null;
44 var batchErrorCount = 0;
45 var BATCH_MAX_ERRORS = 5;
46
47 function sanitizeHtml(str) {
48 var div = document.createElement('div');
49 div.textContent = str;
50 return div.innerHTML;
51 }
52
53 function sanitizeTrustedHtml(html) {
54 var parser = new DOMParser();
55 var doc = parser.parseFromString(html, 'text/html');
56 doc.querySelectorAll('script,iframe,object,embed,form,link[rel="import"]').forEach(function(el) { el.remove(); });
57 return doc.body.innerHTML;
58 }
59
60 // ── Single Optimize ──
61 document.addEventListener('click', function(e) {
62 var btn = e.target.closest('.metasync-optimize-btn');
63 if (!btn) return;
64
65 var id = btn.dataset.id;
66 btn.classList.add('loading');
67 btn.disabled = true;
68 btn.innerHTML = '<span class="metasync-batch-spinner" style="width:14px;height:14px;display:inline-block;"></span> ' + sanitizeHtml(i18n.optimizing || 'Optimizing...');
69
70 fetch(ajaxUrl, {
71 method: 'POST',
72 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
73 body: 'action=metasync_optimize_single_image&nonce=' + nonce + '&attachment_id=' + encodeURIComponent(id)
74 })
75 .then(function(r) { return r.json(); })
76 .then(function(data) {
77 if (data.success) {
78 var row = btn.closest('tr');
79 var statusCell = row.querySelector('.column-status');
80 var parser = new DOMParser();
81 var statusDoc = parser.parseFromString(data.data.status_html || '', 'text/html');
82 statusDoc.querySelectorAll('script,iframe,object,embed,form').forEach(function(el) { el.remove(); });
83 while (statusCell.firstChild) { statusCell.removeChild(statusCell.firstChild); }
84 Array.prototype.forEach.call(statusDoc.body.childNodes, function(node) {
85 statusCell.appendChild(node.cloneNode(true));
86 });
87
88 var actionsCell = row.querySelector('.column-actions');
89 if (data.data.can_revert === false) {
90 actionsCell.innerHTML = '<button type="button" class="button button-small metasync-revert-btn" data-id="' + sanitizeHtml(String(id)) + '" disabled title="' + sanitizeHtml(i18n.revertDisabled || 'Original image unavailable — revert is disabled') + '">' +
91 '<span class="dashicons dashicons-undo" style="margin-top:3px;"></span> ' + sanitizeHtml(i18n.revert || 'Revert') + '</button>';
92 } else {
93 actionsCell.innerHTML = '<button type="button" class="button button-small metasync-revert-btn" data-id="' + sanitizeHtml(String(id)) + '">' +
94 '<span class="dashicons dashicons-undo" style="margin-top:3px;"></span> ' + sanitizeHtml(i18n.revert || 'Revert') + '</button>';
95 }
96
97 updateStats();
98 } else {
99 btn.classList.remove('loading');
100 btn.disabled = false;
101 btn.innerHTML = '<span class="dashicons dashicons-performance" style="margin-top:3px;"></span> ' + sanitizeHtml(i18n.optimize || 'Optimize');
102 alert(data.data || (i18n.optimizeFailed || 'Optimization failed.'));
103 }
104 })
105 .catch(function() {
106 btn.classList.remove('loading');
107 btn.disabled = false;
108 btn.innerHTML = '<span class="dashicons dashicons-performance" style="margin-top:3px;"></span> ' + sanitizeHtml(i18n.optimize || 'Optimize');
109 });
110 });
111
112 // ── Single Revert ──
113 document.addEventListener('click', function(e) {
114 var btn = e.target.closest('.metasync-revert-btn');
115 if (!btn || btn.disabled) return;
116
117 if (!confirm(i18n.revertConfirm || 'Revert this image to its original format?')) {
118 return;
119 }
120
121 var id = btn.dataset.id;
122 btn.classList.add('loading');
123 btn.disabled = true;
124
125 fetch(ajaxUrl, {
126 method: 'POST',
127 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
128 body: 'action=metasync_revert_single_image&nonce=' + nonce + '&attachment_id=' + id
129 })
130 .then(function(r) { return r.json(); })
131 .then(function(data) {
132 if (data.success) {
133 var row = btn.closest('tr');
134 var statusCell = row.querySelector('.column-status');
135 statusCell.innerHTML = '<span class="metasync-status-badge metasync-status-unoptimized">' + (i18n.unoptimized || 'Unoptimized') + '</span>';
136
137 var actionsCell = row.querySelector('.column-actions');
138 actionsCell.innerHTML = '<button type="button" class="button button-small button-primary metasync-optimize-btn" data-id="' + id + '">' +
139 '<span class="dashicons dashicons-performance" style="margin-top:3px;"></span> ' + (i18n.optimize || 'Optimize') + '</button>';
140
141 updateStats();
142 } else {
143 btn.classList.remove('loading');
144 btn.disabled = false;
145 alert(data.data || (i18n.revertFailed || 'Revert failed.'));
146 }
147 })
148 .catch(function() {
149 btn.classList.remove('loading');
150 btn.disabled = false;
151 });
152 });
153
154 // ── Delete Orphaned Record (missing file) ──
155 document.addEventListener('click', function(e) {
156 var btn = e.target.closest('.metasync-delete-orphan-btn');
157 if (!btn || btn.disabled) return;
158
159 if (!confirm(i18n.deleteOrphanConfirm || 'The file for this image is missing on disk. Delete this orphaned media record? This cannot be undone.')) {
160 return;
161 }
162
163 var id = btn.dataset.id;
164 btn.classList.add('loading');
165 btn.disabled = true;
166
167 fetch(ajaxUrl, {
168 method: 'POST',
169 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
170 body: 'action=metasync_delete_orphaned_image&nonce=' + nonce + '&attachment_id=' + encodeURIComponent(id)
171 })
172 .then(function(r) { return r.json(); })
173 .then(function(data) {
174 if (data.success) {
175 var row = btn.closest('tr');
176 if (row) { row.parentNode.removeChild(row); }
177 if (data.data && data.data.stats) {
178 applyStats(data.data.stats);
179 } else {
180 updateStats();
181 }
182 } else {
183 btn.classList.remove('loading');
184 btn.disabled = false;
185 alert(data.data || (i18n.deleteOrphanFailed || 'Failed to delete the orphaned media record.'));
186 }
187 })
188 .catch(function() {
189 btn.classList.remove('loading');
190 btn.disabled = false;
191 });
192 });
193
194 // ── Optimize All (Start Batch) ──
195 var optimizeAllBtn = document.getElementById('metasync-optimize-all');
196 if (optimizeAllBtn) {
197 optimizeAllBtn.addEventListener('click', function() {
198 if (!confirm(i18n.startBatch || 'Start optimizing all unoptimized images?')) {
199 return;
200 }
201
202 optimizeAllBtn.disabled = true;
203
204 fetch(ajaxUrl, {
205 method: 'POST',
206 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
207 body: 'action=metasync_start_batch_optimize&nonce=' + nonce
208 })
209 .then(function(r) { return r.json(); })
210 .then(function(data) {
211 if (data.success) {
212 showBatchProgress(data.data);
213 startBatchProcessing();
214 } else {
215 optimizeAllBtn.disabled = false;
216 alert(data.data || (i18n.batchFailed || 'Failed to start batch optimization.'));
217 }
218 })
219 .catch(function() {
220 optimizeAllBtn.disabled = false;
221 });
222 });
223 }
224
225 // ── Cancel Batch ──
226 var cancelBtn = document.getElementById('metasync-cancel-batch');
227 if (cancelBtn) {
228 cancelBtn.addEventListener('click', function() {
229 batchActive = false;
230 stopFallbackPoll();
231
232 fetch(ajaxUrl, {
233 method: 'POST',
234 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
235 body: 'action=metasync_cancel_batch_optimize&nonce=' + nonce
236 })
237 .then(function(r) { return r.json(); })
238 .then(function() {
239 document.getElementById('metasync-batch-progress').style.display = 'none';
240 if (optimizeAllBtn) optimizeAllBtn.disabled = false;
241 location.reload();
242 })
243 .catch(function() {
244 // The cancel request failed (expired nonce, network drop) —
245 // the server may still be running the batch, so resync the
246 // UI from a fresh page load instead of leaving it stuck.
247 location.reload();
248 });
249 });
250 }
251
252 // ── Copy URL ──
253 document.addEventListener('click', function(e) {
254 var btn = e.target.closest('.metasync-copy-btn');
255 if (!btn) return;
256
257 var url = btn.dataset.url;
258 if (!url) return;
259
260 navigator.clipboard.writeText(url).then(function() {
261 btn.classList.add('copied');
262 var icon = btn.querySelector('.dashicons');
263 if (icon) {
264 icon.className = 'dashicons dashicons-yes';
265 setTimeout(function() {
266 icon.className = 'dashicons dashicons-clipboard';
267 btn.classList.remove('copied');
268 }, 1500);
269 }
270 });
271 });
272
273 // ── Dismiss Complete Notice ──
274 var dismissBtn = document.getElementById('metasync-dismiss-complete');
275 if (dismissBtn) {
276 dismissBtn.addEventListener('click', function() {
277 document.getElementById('metasync-batch-complete').style.display = 'none';
278 });
279 }
280
281 // ── AJAX-Driven Batch Processing ──
282
283 function startBatchProcessing() {
284 batchActive = true;
285 startFallbackPoll();
286 processNextBatch();
287 }
288
289 function processNextBatch() {
290 if (!batchActive) return;
291
292 fetch(ajaxUrl, {
293 method: 'POST',
294 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
295 body: 'action=metasync_process_batch_tick&nonce=' + nonce
296 })
297 .then(function(r) { return r.json(); })
298 .then(function(data) {
299 if (!batchActive) return;
300
301 // A tick rejection (expired nonce, lost capabilities) used to
302 // stall the loop silently with the progress bar frozen.
303 if (!data.success) {
304 onBatchError();
305 return;
306 }
307
308 batchErrorCount = 0;
309
310 var progress = data.data;
311 showBatchProgress(progress);
312 applyStats(progress.stats);
313
314 if (progress.status === 'running') {
315 setTimeout(processNextBatch, 500);
316 } else {
317 onBatchFinished(progress);
318 }
319 })
320 .catch(function() {
321 // Transient network errors retry with a backoff, but only a
322 // bounded number of times — retrying forever on an expired
323 // nonce kept the batch UI "running" indefinitely with no error.
324 if (!batchActive) return;
325
326 batchErrorCount++;
327 if (batchErrorCount >= BATCH_MAX_ERRORS) {
328 onBatchError();
329 return;
330 }
331
332 setTimeout(processNextBatch, 5000);
333 });
334 }
335
336 /**
337 * Surface a stalled/failed batch and stop all polling. The server-side
338 * batch itself keeps its own state; reloading shows the real status.
339 */
340 function onBatchError() {
341 batchActive = false;
342 stopFallbackPoll();
343 document.getElementById('metasync-batch-progress').style.display = 'none';
344 if (optimizeAllBtn) optimizeAllBtn.disabled = false;
345 alert(i18n.batchStalled || 'Batch optimization stopped after repeated errors. Please reload the page and try again.');
346 }
347
348 function onBatchFinished(progress) {
349 batchActive = false;
350 stopFallbackPoll();
351 document.getElementById('metasync-batch-progress').style.display = 'none';
352
353 if (progress.status === 'completed') {
354 var completeEl = document.getElementById('metasync-batch-complete');
355 var textEl = document.getElementById('metasync-batch-complete-text');
356 textEl.textContent = (i18n.batchComplete || 'Batch optimization complete!') + ' ' +
357 progress.processed + ' ' + (i18n.imagesProcessed || 'images processed') +
358 (progress.failed > 0 ? ', ' + progress.failed + ' ' + (i18n.failed || 'failed') : '') + '.';
359 completeEl.style.display = 'flex';
360 }
361
362 if (optimizeAllBtn) optimizeAllBtn.disabled = false;
363 setTimeout(function() { location.reload(); }, 1500);
364 }
365
366 function startFallbackPoll() {
367 if (fallbackPoll) return;
368 fallbackPoll = setInterval(function() {
369 fetch(ajaxUrl, {
370 method: 'POST',
371 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
372 body: 'action=metasync_batch_progress&nonce=' + nonce
373 })
374 .then(function(r) { return r.json(); })
375 .then(function(data) {
376 if (!data.success) return;
377 showBatchProgress(data.data);
378 applyStats(data.data.stats);
379 if (data.data.status !== 'running') {
380 onBatchFinished(data.data);
381 }
382 });
383 }, 10000);
384 }
385
386 function stopFallbackPoll() {
387 if (fallbackPoll) {
388 clearInterval(fallbackPoll);
389 fallbackPoll = null;
390 }
391 }
392
393 function showBatchProgress(progress) {
394 var el = document.getElementById('metasync-batch-progress');
395 el.style.display = '';
396
397 var pct = progress.total > 0 ? Math.round((progress.processed / progress.total) * 100) : 0;
398 document.getElementById('metasync-batch-fill').style.width = pct + '%';
399 document.getElementById('metasync-batch-text').textContent =
400 (i18n.optimizingOf || 'Optimizing') + ' ' + progress.processed + ' ' + (i18n.of || 'of') + ' ' + progress.total + ' ' + (i18n.images || 'images...');
401 document.getElementById('metasync-batch-processed').textContent = progress.processed;
402 document.getElementById('metasync-batch-total').textContent = progress.total;
403 }
404
405 function applyStats(stats) {
406 if (!stats) return;
407 var statNumbers = document.querySelectorAll('.metasync-stat-number');
408 if (statNumbers.length >= 3) {
409 statNumbers[0].textContent = stats.total;
410 statNumbers[1].textContent = stats.optimized;
411 statNumbers[2].textContent = stats.unoptimized;
412 }
413 var pctEl = document.querySelector('.metasync-stat-percentage');
414 if (pctEl) pctEl.textContent = stats.percentage + '%';
415 var fillEl = document.querySelector('.metasync-stat-progress-fill');
416 if (fillEl) fillEl.style.width = stats.percentage + '%';
417
418 // Update "Optimize All" button state based on unoptimized count
419 if (optimizeAllBtn && !batchActive) {
420 var unoptimized = parseInt(stats.unoptimized, 10) || 0;
421 optimizeAllBtn.disabled = unoptimized === 0;
422
423 var badge = optimizeAllBtn.querySelector('.metasync-count-badge');
424 if (unoptimized > 0) {
425 if (badge) {
426 badge.textContent = stats.unoptimized;
427 } else {
428 badge = document.createElement('span');
429 badge.className = 'metasync-count-badge';
430 badge.textContent = stats.unoptimized;
431 optimizeAllBtn.appendChild(badge);
432 }
433 } else if (badge) {
434 badge.remove();
435 }
436 }
437 }
438
439 function updateStats() {
440 fetch(ajaxUrl, {
441 method: 'POST',
442 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
443 body: 'action=metasync_batch_progress&nonce=' + nonce
444 })
445 .then(function(r) { return r.json(); })
446 .then(function(data) {
447 if (data.success) applyStats(data.data.stats);
448 });
449 }
450
451 // Auto-resume AJAX chain if batch is already running (e.g. page reload)
452 if (config.batchRunning) {
453 startBatchProcessing();
454 }
455
456 // ── Bulk Optimize Selected ──
457 var bulkForm = document.getElementById('metasync-image-library-form');
458 if (bulkForm) {
459 bulkForm.querySelectorAll('input[type="submit"], button[type="submit"]').forEach(function(btn) {
460 btn.addEventListener('click', function() {
461 bulkForm._lastSubmitter = btn;
462 });
463 });
464
465 bulkForm.addEventListener('submit', function(e) {
466 var submitter = e.submitter || bulkForm._lastSubmitter;
467 var applyBtnTop = bulkForm.querySelector('#doaction');
468 var applyBtnBottom = bulkForm.querySelector('#doaction2');
469
470 var isTopApply = submitter && submitter === applyBtnTop;
471 var isBottomApply = submitter && submitter === applyBtnBottom;
472 if (!isTopApply && !isBottomApply) return;
473
474 var actionName = isTopApply ? 'action' : 'action2';
475 var action = bulkForm.querySelector('[name="' + actionName + '"]');
476 if (!action || (action.value !== 'bulk_optimize' && action.value !== 'bulk_unoptimize')) return;
477
478 e.preventDefault();
479
480 var checked = bulkForm.querySelectorAll('input[name="image_ids[]"]:checked');
481 if (checked.length === 0) {
482 alert(i18n.selectImages || 'Please select at least one image.');
483 return;
484 }
485
486 var ids = [];
487 checked.forEach(function(cb) { ids.push(cb.value); });
488
489 var isBulkUnoptimize = action.value === 'bulk_unoptimize';
490
491 if (isBulkUnoptimize && !confirm(i18n.unoptimizeConfirm || 'Revert selected images to their original format?')) {
492 return;
493 }
494
495 var clickedBtn = isTopApply ? applyBtnTop : applyBtnBottom;
496 clickedBtn.disabled = true;
497
498 var ajaxAction = isBulkUnoptimize ? 'metasync_bulk_unoptimize_selected' : 'metasync_bulk_optimize_selected';
499 clickedBtn.value = isBulkUnoptimize
500 ? (i18n.unoptimizing || 'Unoptimizing...')
501 : (i18n.optimizing || 'Optimizing...');
502
503 fetch(ajaxUrl, {
504 method: 'POST',
505 headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
506 body: 'action=' + ajaxAction + '&nonce=' + nonce + '&ids=' + ids.join(',')
507 })
508 .then(function(r) { return r.json(); })
509 .then(function(data) {
510 if (data.success) {
511 var result = data.data;
512 if (isBulkUnoptimize && result.errors && result.errors.length > 0) {
513 alert(result.errors.join('\n'));
514 }
515 if (isBulkUnoptimize && result.success === 0 && result.skipped > 0) {
516 clickedBtn.disabled = false;
517 clickedBtn.value = i18n.apply || 'Apply';
518 alert(i18n.alreadyUnoptimized || 'All selected images are already unoptimized.');
519 return;
520 }
521 location.reload();
522 } else {
523 clickedBtn.disabled = false;
524 clickedBtn.value = i18n.apply || 'Apply';
525 var failMsg = isBulkUnoptimize
526 ? (i18n.bulkUnoptimizeFailed || 'Bulk unoptimize failed.')
527 : (i18n.bulkFailed || 'Bulk optimization failed.');
528 alert(data.data || failMsg);
529 }
530 })
531 .catch(function() {
532 clickedBtn.disabled = false;
533 clickedBtn.value = i18n.apply || 'Apply';
534 });
535 });
536 }
537
538 // ── Search Clear Button ──
539 function initSearchClearBtn() {
540 var searchInput = document.querySelector('.metasync-toolbar-right .search-box input[type="search"]');
541 if (!searchInput) return;
542
543 var wrapper = document.createElement('div');
544 wrapper.className = 'metasync-search-input-wrap';
545 searchInput.parentNode.insertBefore(wrapper, searchInput);
546 wrapper.appendChild(searchInput);
547
548 var clearBtn = document.createElement('button');
549 clearBtn.type = 'button';
550 clearBtn.className = 'metasync-clear-search';
551 clearBtn.innerHTML = '&times;';
552 clearBtn.setAttribute('aria-label', 'Clear search');
553 wrapper.appendChild(clearBtn);
554
555 function updateVisibility() {
556 if (searchInput.value.length > 0) {
557 clearBtn.classList.add('visible');
558 } else {
559 clearBtn.classList.remove('visible');
560 }
561 }
562
563 searchInput.addEventListener('input', updateVisibility);
564 updateVisibility();
565
566 clearBtn.addEventListener('click', function() {
567 searchInput.value = '';
568 updateVisibility();
569 var form = searchInput.closest('form');
570 if (form) { form.submit(); }
571 });
572 }
573
574 initSearchClearBtn();
575 })();
576