PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.13
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.13
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 / otto-frontend-toolbar / js / otto-toolbar.js

otto-toolbar.js in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.13, at otto-frontend-toolbar/js/otto-toolbar.js

624 lines 19.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * OTTO Frontend Toolbar JavaScript
3 *
4 * Handles preview iframe functionality for OTTO control toolbar
5 *
6 * @package Metasync
7 * @subpackage Metasync/otto-frontend-toolbar/js
8 * @since 1.0.0
9 */
10
11 (function($) {
12 'use strict';
13
14 /**
15 * OTTO Toolbar Manager
16 */
17 window.metasyncOttoToolbar = {
18
19 /**
20 * Initialize the toolbar
21 */
22 init: function() {
23 console.log('OTTO Toolbar initialized');
24
25 // Bind close button
26 $(document).on('click', '#otto-close-btn', function(e) {
27 e.preventDefault();
28 window.metasyncOttoToolbar.closeToolbar();
29 });
30
31 // Bind preview button
32 $(document).on('click', '#otto-preview-btn', function(e) {
33 e.preventDefault();
34 window.metasyncOttoToolbar.openPreview();
35 });
36
37 // Bind close preview button
38 $(document).on('click', '#otto-preview-close', function(e) {
39 e.preventDefault();
40 window.metasyncOttoToolbar.closePreview();
41 });
42
43 // Bind debug button
44 $(document).on('click', '#otto-debug-btn', function(e) {
45 e.preventDefault();
46 window.metasyncOttoToolbar.openDebugTray();
47 });
48
49 // Bind close debug tray button
50 $(document).on('click', '#otto-debug-tray-close', function(e) {
51 e.preventDefault();
52 window.metasyncOttoToolbar.closeDebugTray();
53 });
54
55 // Bind OTTO toggle switch in debug tray
56 $(document).on('change', '#otto-debug-toggle', function(e) {
57 window.metasyncOttoToolbar.handleOttoToggle(this);
58 });
59
60 // Close preview on ESC key
61 $(document).on('keydown', function(e) {
62 if (e.key === 'Escape') {
63 if ($('#metasync-otto-preview-overlay').is(':visible')) {
64 window.metasyncOttoToolbar.closePreview();
65 }
66 if ($('#metasync-otto-debug-tray').hasClass('active')) {
67 window.metasyncOttoToolbar.closeDebugTray();
68 }
69 }
70 });
71
72 // Optional: Add loading indicator when clicking toggle links
73 $('.metasync-otto-toggle').on('click', function() {
74 $(this).css('opacity', '0.6');
75 });
76 },
77
78 /**
79 * Open preview iframe with otto_preview parameter
80 */
81 openPreview: function() {
82 // Get current page URL
83 let currentUrl = window.location.href;
84
85 // Add otto_preview parameter
86 let previewUrl = this.addParameterToUrl(currentUrl, 'otto_preview', '1');
87
88 let $overlay = $('#metasync-otto-preview-overlay');
89 let $iframe = $('#metasync-otto-preview-iframe');
90 let $loading = $('#otto-preview-loading');
91
92 // Show loading spinner
93 $loading.removeClass('hidden');
94
95 // Reset iframe opacity
96 $iframe.css('opacity', '0');
97
98 // Add active class to show overlay (display: flex)
99 $overlay.addClass('active').css('opacity', '1');
100
101 // Add body class
102 $('body').addClass('otto-preview-active');
103
104 // Prevent body scroll
105 $('body').css('overflow', 'hidden');
106
107 // Set iframe src and wait for load
108 $iframe.attr('src', previewUrl);
109
110 // Listen for iframe load event
111 $iframe.off('load').on('load', function() {
112 // Hide loading spinner
113 $loading.addClass('hidden');
114
115 // Show iframe with fade in
116 $iframe.animate({ opacity: 1 }, 400);
117
118 console.log('OTTO Preview loaded successfully');
119 });
120
121 // Fallback timeout in case load event doesn't fire
122 setTimeout(function() {
123 if ($loading.is(':visible')) {
124 $loading.addClass('hidden');
125 $iframe.animate({ opacity: 1 }, 400);
126 }
127 }, 3000);
128
129 console.log('OTTO Preview opened:', previewUrl);
130 },
131
132 /**
133 * Close preview iframe
134 */
135 closePreview: function() {
136 let $overlay = $('#metasync-otto-preview-overlay');
137 let $iframe = $('#metasync-otto-preview-iframe');
138 let $loading = $('#otto-preview-loading');
139
140 // Fade out
141 $overlay.animate({ opacity: 0 }, 300, function() {
142 // Remove active class
143 $overlay.removeClass('active');
144
145 // Clear iframe src after animation
146 $iframe.attr('src', '').css('opacity', '0');
147
148 // Reset loading state
149 $loading.removeClass('hidden');
150 });
151
152 // Remove body class
153 $('body').removeClass('otto-preview-active');
154
155 // Restore body scroll
156 $('body').css('overflow', '');
157
158 console.log('OTTO Preview closed');
159 },
160
161 /**
162 * Open debug tray and load comparison data
163 */
164 openDebugTray: function() {
165 let $tray = $('#metasync-otto-debug-tray');
166 let $content = $('#otto-debug-tray-content');
167
168 // Show tray
169 $tray.addClass('active');
170
171 // Reset content to loading state
172 $content.html('<div class="otto-debug-loading"><div class="otto-loading-spinner"></div><p>Loading comparison data...</p></div>');
173
174 // Build API URL
175 let apiUrl = metasyncOttoDebug.apiUrl +
176 '?url=' + encodeURIComponent(metasyncOttoDebug.currentUrl) +
177 '&uuid=' + metasyncOttoDebug.ottoUuid;
178
179 console.log('Fetching OTTO debug data from:', apiUrl);
180
181 // Fetch data from API
182 $.ajax({
183 url: apiUrl,
184 type: 'GET',
185 dataType: 'json',
186 success: function(response) {
187 window.metasyncOttoToolbar.renderComparisonData(response);
188 },
189 error: function(xhr, status, error) {
190 console.error('OTTO Debug API Error:', error);
191 let ottoName = metasyncOttoDebug.ottoName || 'OTTO';
192 $content.html(
193 '<div class="otto-debug-error">' +
194 '<strong>Error loading comparison data</strong>' +
195 '<p>Unable to fetch data from the API. Please check your ' + ottoName + ' UUID settings and try again.</p>' +
196 '<p><small>Error: ' + error + '</small></p>' +
197 '</div>'
198 );
199 }
200 });
201 },
202
203 /**
204 * Close debug tray
205 */
206 closeDebugTray: function() {
207 $('#metasync-otto-debug-tray').removeClass('active');
208 },
209
210 /**
211 * Dismiss the toolbar for the current session.
212 * The bar will reappear on the next page load.
213 */
214 closeToolbar: function() {
215 $('#metasync-otto-debug-bar').fadeOut(200);
216 },
217
218 /**
219 * Handle OTTO toggle switch change
220 */
221 handleOttoToggle: function(toggleElement) {
222 let $toggle = $(toggleElement);
223 let postId = $toggle.data('post-id');
224 let isChecked = $toggle.is(':checked');
225 let action = isChecked ? 'enable' : 'disable';
226
227 // Disable toggle during AJAX request
228 $toggle.prop('disabled', true);
229
230 // Update status text immediately for better UX
231 $('.otto-toggle-status-text').text(isChecked ? 'Enabling...' : 'Disabling...');
232
233 console.log('OTTO Toggle - Action:', action, 'Post ID:', postId);
234
235 // Make AJAX request
236 $.ajax({
237 url: metasyncOttoDebug.ajaxUrl,
238 type: 'POST',
239 data: {
240 action: 'metasync_otto_toggle',
241 post_id: postId,
242 otto_action: action,
243 nonce: metasyncOttoDebug.nonce
244 },
245 success: function(response) {
246 console.log('OTTO Toggle Response:', response);
247
248 if (response.success) {
249 // Update status text in tray
250 $('.otto-toggle-status-text').text(isChecked ? 'Enabled' : 'Disabled');
251
252 // Update debug bar status
253 let $debugBar = $('#metasync-otto-debug-bar');
254 let ottoName = metasyncOttoDebug.ottoName || 'OTTO';
255 if (isChecked) {
256 $debugBar.removeClass('otto-disabled').addClass('otto-enabled');
257 $debugBar.find('.otto-status-text').text(ottoName + ' Enabled');
258
259 // Show Preview Original button when enabled
260 if ($('#otto-preview-btn').length === 0) {
261 let previewBtn = '<button type="button" class="otto-preview-btn" id="otto-preview-btn">' +
262 '<span class="dashicons dashicons-visibility"></span>' +
263 'Preview Original' +
264 '</button>';
265 $('.otto-debug-status').after(previewBtn);
266 }
267 } else {
268 $debugBar.removeClass('otto-enabled').addClass('otto-disabled');
269 $debugBar.find('.otto-status-text').text(ottoName + ' Disabled');
270
271 // Hide Preview Original button when disabled
272 $('#otto-preview-btn').remove();
273 }
274
275 // Keep debug tray open - no page reload
276 console.log('OTTO status updated successfully. Debug tray remains open.');
277 } else {
278 // Revert toggle on error
279 $toggle.prop('checked', !isChecked);
280 $('.otto-toggle-status-text').text(isChecked ? 'Disabled' : 'Enabled');
281 let ottoName = metasyncOttoDebug.ottoName || 'OTTO';
282 alert(response.data.message || 'Failed to update ' + ottoName + ' status.');
283 }
284 },
285 error: function(xhr, status, error) {
286 console.error('OTTO Toggle Error:', error);
287 // Revert toggle on error
288 $toggle.prop('checked', !isChecked);
289 $('.otto-toggle-status-text').text(isChecked ? 'Disabled' : 'Enabled');
290 let ottoName = metasyncOttoDebug.ottoName || 'OTTO';
291 alert('An error occurred while updating ' + ottoName + ' status.');
292 },
293 complete: function() {
294 // Re-enable toggle
295 $toggle.prop('disabled', false);
296 }
297 });
298 },
299
300 /**
301 * Render comparison data in the tray
302 */
303 renderComparisonData: function(data) {
304 let $content = $('#otto-debug-tray-content');
305
306 // Parse header_replacements array
307 let titleData = null;
308 let descriptionData = null;
309 let ogTitleData = null;
310 let ogDescriptionData = null;
311
312 if (data.header_replacements && Array.isArray(data.header_replacements)) {
313 for (let i = 0; i < data.header_replacements.length; i++) {
314 let item = data.header_replacements[i];
315
316 if (item.type === 'title') {
317 titleData = item;
318 } else if (item.type === 'meta' && item.name === 'description') {
319 descriptionData = item;
320 } else if (item.type === 'meta' && item.property === 'og:title') {
321 ogTitleData = item;
322 } else if (item.type === 'meta' && item.property === 'og:description') {
323 ogDescriptionData = item;
324 }
325 }
326 }
327
328 // Build comparison table HTML
329 let html = '<table class="otto-comparison-table">';
330 html += '<thead><tr>';
331 html += '<th class="original-col">Original</th>';
332 html += '<th class="otto-col">Otto Suggested</th>';
333 html += '</tr></thead>';
334 html += '<tbody>';
335
336 // Title - only show if present
337 if (titleData) {
338 html += '<tr>';
339 html += '<td><span class="field-label">Title:</span><div class="field-value">' +
340 this.escapeHtml(titleData.current_value) + '</div></td>';
341 html += '<td><span class="field-label">Title:</span><div class="field-value">' +
342 this.escapeHtml(titleData.recommended_value) + '</div></td>';
343 html += '</tr>';
344 }
345
346 // Meta Description - only show if present
347 if (descriptionData) {
348 html += '<tr>';
349 html += '<td><span class="field-label">Meta Description</span><div class="field-value">' +
350 this.escapeHtml(descriptionData.current_value) + '</div></td>';
351 html += '<td><span class="field-label">Meta Description</span><div class="field-value">' +
352 this.escapeHtml(descriptionData.recommended_value) + '</div></td>';
353 html += '</tr>';
354 }
355
356 // OG Title - only show if present
357 if (ogTitleData) {
358 html += '<tr>';
359 html += '<td><span class="field-label">OG Title</span><div class="field-value">' +
360 this.escapeHtml(ogTitleData.current_value) + '</div></td>';
361 html += '<td><span class="field-label">OG Title</span><div class="field-value">' +
362 this.escapeHtml(ogTitleData.recommended_value) + '</div></td>';
363 html += '</tr>';
364 }
365
366 // OG Description - only show if present
367 if (ogDescriptionData) {
368 html += '<tr>';
369 html += '<td><span class="field-label">OG Description</span><div class="field-value">' +
370 this.escapeHtml(ogDescriptionData.current_value) + '</div></td>';
371 html += '<td><span class="field-label">OG Description</span><div class="field-value">' +
372 this.escapeHtml(ogDescriptionData.recommended_value) + '</div></td>';
373 html += '</tr>';
374 }
375
376 // Body Substitutions - Headings
377 if (data.body_substitutions && data.body_substitutions.headings &&
378 Array.isArray(data.body_substitutions.headings) &&
379 data.body_substitutions.headings.length > 0) {
380 for (let i = 0; i < data.body_substitutions.headings.length; i++) {
381 let heading = data.body_substitutions.headings[i];
382 let headingLabel = 'Heading';
383
384 // Add heading type if available (H1, H2, etc.)
385 if (heading.type) {
386 headingLabel = heading.type.toUpperCase() + ' Heading';
387 }
388
389 html += '<tr>';
390 html += '<td><span class="field-label">' + this.escapeHtml(headingLabel) + '</span><div class="field-value">' +
391 this.escapeHtml(heading.current_value || '') + '</div></td>';
392 html += '<td><span class="field-label">' + this.escapeHtml(headingLabel) + '</span><div class="field-value">' +
393 this.escapeHtml(heading.recommended_value || '') + '</div></td>';
394 html += '</tr>';
395 }
396 }
397
398 // Parse Header HTML Insertion for Keywords and Schema
399 if (data.header_html_insertion) {
400 let parsedInsertions = this.parseHeaderInsertions(data.header_html_insertion);
401
402 // Keywords
403 if (parsedInsertions.keywords) {
404 html += '<tr>';
405 html += '<td><span class="field-label">Keywords</span><div class="field-value"><em>Not present</em></div></td>';
406 html += '<td><span class="field-label">Keywords</span><div class="field-value">' +
407 this.escapeHtml(parsedInsertions.keywords) + '</div></td>';
408 html += '</tr>';
409 }
410
411 // Schema (JSON-LD)
412 if (parsedInsertions.schema) {
413 html += '<tr>';
414 html += '<td><span class="field-label">Schema</span><div class="field-value"><em>Existing schema</em></div></td>';
415 html += '<td><span class="field-label">Schema</span><div class="field-value"><pre>' +
416 this.escapeHtml(parsedInsertions.schema) + '</pre></div></td>';
417 html += '</tr>';
418 }
419
420 // Twitter Meta Tags
421 if (parsedInsertions.twitterTitle) {
422 html += '<tr>';
423 html += '<td><span class="field-label">Twitter Title</span><div class="field-value"><em>Not present</em></div></td>';
424 html += '<td><span class="field-label">Twitter Title</span><div class="field-value">' +
425 this.escapeHtml(parsedInsertions.twitterTitle) + '</div></td>';
426 html += '</tr>';
427 }
428
429 if (parsedInsertions.twitterDescription) {
430 html += '<tr>';
431 html += '<td><span class="field-label">Twitter Description</span><div class="field-value"><em>Not present</em></div></td>';
432 html += '<td><span class="field-label">Twitter Description</span><div class="field-value">' +
433 this.escapeHtml(parsedInsertions.twitterDescription) + '</div></td>';
434 html += '</tr>';
435 }
436 }
437
438 // Body Substitutions (Links)
439 if (data.body_substitutions && data.body_substitutions.links &&
440 Object.keys(data.body_substitutions.links).length > 0) {
441 html += '<tr>';
442 html += '<td colspan="2"><span class="field-label">Link Updates</span>';
443 html += '<div class="field-value">';
444 html += '<ul style="margin: 5px 0; padding-left: 20px;">';
445 for (let oldLink in data.body_substitutions.links) {
446 if (data.body_substitutions.links.hasOwnProperty(oldLink)) {
447 let newLink = data.body_substitutions.links[oldLink];
448 html += '<li><strong>From:</strong> <a href="' + this.escapeHtml(oldLink) + '" target="_blank" rel="noopener noreferrer">' +
449 this.escapeHtml(oldLink) + '</a>' +
450 '<br><strong>To:</strong> <a href="' + this.escapeHtml(newLink) + '" target="_blank" rel="noopener noreferrer">' +
451 this.escapeHtml(newLink) + '</a></li>';
452 }
453 }
454 html += '</ul></div></td>';
455 html += '</tr>';
456 }
457
458 // Body Substitutions (Images - Alt Tags)
459 if (data.body_substitutions && data.body_substitutions.images &&
460 Object.keys(data.body_substitutions.images).length > 0) {
461 html += '<tr>';
462 html += '<td colspan="2"><span class="field-label">Image Alt Tag Updates</span>';
463 html += '<div class="field-value">';
464 html += '<ul style="margin: 5px 0; padding-left: 20px;">';
465 for (let imageUrl in data.body_substitutions.images) {
466 if (data.body_substitutions.images.hasOwnProperty(imageUrl)) {
467
468 let altText = data.body_substitutions.images[imageUrl] || 'N/A';
469 let shortenedUrl = this.shortenUrl(imageUrl);
470
471 html += '<li>';
472 html += '<strong>Image:</strong> <a href="' + this.escapeHtml(imageUrl) + '" target="_blank" rel="noopener noreferrer"><code>' +
473 this.escapeHtml(shortenedUrl) + '</code></a><br>';
474 html += '<strong>New Alt:</strong> ' + this.escapeHtml(altText);
475 html += '</li>';
476 }
477 }
478 html += '</ul></div></td>';
479 html += '</tr>';
480 }
481
482 html += '</tbody></table>';
483
484 $content.html(html);
485 },
486
487 /**
488 * Parse header HTML insertions to extract keywords, schema, and other meta tags
489 */
490 parseHeaderInsertions: function(htmlString) {
491 let result = {
492 keywords: null,
493 schema: null,
494 twitterTitle: null,
495 twitterDescription: null
496 };
497
498 if (!htmlString) {
499 return result;
500 }
501
502 // Create a temporary div to parse the HTML
503 let tempDiv = document.createElement('div');
504 tempDiv.innerHTML = htmlString;
505
506 // Extract keywords meta tag
507 let keywordsMeta = tempDiv.querySelector('meta[name="keywords"]');
508 if (keywordsMeta) {
509 result.keywords = keywordsMeta.getAttribute('content');
510 }
511
512 // Extract schema (JSON-LD)
513 let schemaScript = tempDiv.querySelector('script[type="application/ld+json"]');
514 if (schemaScript) {
515 result.schema = schemaScript.textContent || schemaScript.innerHTML;
516 }
517
518 // Extract Twitter meta tags
519 let twitterTitleMeta = tempDiv.querySelector('meta[name="twitter:title"], meta[property="twitter:title"]');
520 if (twitterTitleMeta) {
521 result.twitterTitle = twitterTitleMeta.getAttribute('content');
522 }
523
524 let twitterDescMeta = tempDiv.querySelector('meta[name="twitter:description"], meta[property="twitter:description"]');
525 if (twitterDescMeta) {
526 result.twitterDescription = twitterDescMeta.getAttribute('content');
527 }
528
529 return result;
530 },
531
532 /**
533 * Format headings for display
534 */
535 formatHeadings: function(headings) {
536 if (!headings || headings.length === 0) {
537 return '<em>No headings</em>';
538 }
539
540 if (typeof headings === 'string') {
541 return this.escapeHtml(headings);
542 }
543
544 let html = '<ul style="margin: 0; padding-left: 20px;">';
545 for (let i = 0; i < headings.length; i++) {
546 html += '<li>' + this.escapeHtml(headings[i]) + '</li>';
547 }
548 html += '</ul>';
549 return html;
550 },
551
552 /**
553 * Shorten long URLs for display
554 */
555 shortenUrl: function(url) {
556 if (!url || url.length <= 60) {
557 return url;
558 }
559
560 // Extract protocol and domain
561 let urlParts = url.match(/^(https?:\/\/[^\/]+)(.*)$/);
562 if (!urlParts) {
563 return url;
564 }
565
566 let domain = urlParts[1];
567 let path = urlParts[2];
568
569 // If path is too long, shorten it
570 if (path.length > 40) {
571 let pathParts = path.split('/').filter(function(part) { return part.length > 0; });
572 if (pathParts.length > 2) {
573 // Show first part ... last part
574 let firstPart = pathParts[0];
575 let lastPart = pathParts[pathParts.length - 1];
576 return domain + '/' + firstPart + '/.../' + lastPart;
577 }
578 }
579
580 return url;
581 },
582
583 /**
584 * Escape HTML to prevent XSS
585 */
586 escapeHtml: function(text) {
587 if (!text) return '';
588 let div = document.createElement('div');
589 div.textContent = text;
590 return div.innerHTML;
591 },
592
593 /**
594 * Add or update URL parameter
595 *
596 * @param {string} url - The URL to modify
597 * @param {string} param - Parameter name
598 * @param {string} value - Parameter value
599 * @return {string} Modified URL
600 */
601 addParameterToUrl: function(url, param, value) {
602 // Parse URL
603 let urlObj;
604 try {
605 urlObj = new URL(url);
606 } catch (e) {
607 // Fallback for relative URLs
608 urlObj = new URL(url, window.location.origin);
609 }
610
611 // Add or update parameter
612 urlObj.searchParams.set(param, value);
613
614 return urlObj.toString();
615 }
616 };
617
618 // Initialize on document ready
619 $(document).ready(function() {
620 metasyncOttoToolbar.init();
621 });
622
623 })(jQuery);
624