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.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 / admin / js / metasync-navigation.js

metasync-navigation.js in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.13, at admin/js/metasync-navigation.js

342 lines 12.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* global metasyncNavData */
2 /**
3 * MetaSync Navigation Portal Scripts
4 *
5 * Extracted for Phase 5, #887 — Part B JS extraction.
6 * Combines Block 1 (top-bar portal menus) and Block 10 (settings inner-page
7 * navigation dropdown) into a single file.
8 *
9 * Localized data object: metasyncNavData
10 * - hideAdvanced (bool) — whether the Advanced tab is hidden
11 * - showGeneral (bool) — whether the user can see the General tab
12 * - pageSlug (string) — admin page slug for link hrefs
13 *
14 * @since Phase 5
15 * @see wp_localize_script() call in class-metasync-admin.php
16 */
17
18 /* ==========================================================================
19 Block 1 — Top-bar SEO & Settings portal menus
20 ========================================================================== */
21
22 function toggleSeoMenuPortal(event) {
23 event.preventDefault();
24 event.stopPropagation();
25 var button = event.currentTarget;
26 var existingMenu = document.getElementById('metasync-seo-portal-menu');
27 if (existingMenu) {
28 existingMenu.remove();
29 button.classList.remove('active');
30 return;
31 }
32 var menu = document.createElement('div');
33 menu.id = 'metasync-seo-portal-menu';
34 menu.className = 'metasync-portal-menu';
35
36 var rect = button.getBoundingClientRect();
37 menu.style.position = 'fixed';
38 menu.style.top = (rect.bottom + 8) + 'px';
39 menu.style.right = (window.innerWidth - rect.right) + 'px';
40 menu.style.zIndex = '999999999';
41 document.body.appendChild(menu);
42 button.classList.add('active');
43 }
44
45 function toggleSettingsMenuPortal(event) {
46 event.preventDefault();
47 event.stopPropagation();
48 var button = event.currentTarget;
49 var existingMenu = document.getElementById('metasync-portal-menu');
50 if (existingMenu) {
51 existingMenu.remove();
52 button.classList.remove('active');
53 return;
54 }
55 var menu = document.createElement('div');
56 menu.id = 'metasync-portal-menu';
57 menu.className = 'metasync-portal-menu';
58
59 var hideAdvanced = metasyncNavData.hideAdvanced;
60 var showGeneral = metasyncNavData.showGeneral;
61
62 if (showGeneral) {
63 var generalLink = document.createElement('a');
64 generalLink.href = '?page=' + metasyncNavData.pageSlug + '&tab=general';
65 generalLink.className = 'metasync-portal-item';
66 generalLink.textContent = 'General';
67 menu.appendChild(generalLink);
68 }
69
70 var whitelabelLink = document.createElement('a');
71 whitelabelLink.href = '?page=' + metasyncNavData.pageSlug + '&tab=whitelabel';
72 whitelabelLink.className = 'metasync-portal-item';
73 whitelabelLink.textContent = 'White Label';
74 menu.appendChild(whitelabelLink);
75
76 if (!hideAdvanced) {
77 var advancedLink = document.createElement('a');
78 advancedLink.href = '?page=' + metasyncNavData.pageSlug + '&tab=advanced';
79 advancedLink.className = 'metasync-portal-item';
80 advancedLink.textContent = 'Advanced';
81 menu.appendChild(advancedLink);
82 }
83
84 var rect = button.getBoundingClientRect();
85 menu.style.position = 'fixed';
86 menu.style.top = (rect.bottom + 8) + 'px';
87 menu.style.right = (window.innerWidth - rect.right) + 'px';
88 menu.style.zIndex = '999999999';
89 document.body.appendChild(menu);
90 button.classList.add('active');
91 }
92
93 document.addEventListener('click', function(event) {
94 var seoButton = document.getElementById('metasync-seo-btn');
95 var seoMenu = document.getElementById('metasync-seo-portal-menu');
96 if (seoMenu && seoButton && !seoButton.contains(event.target) && !seoMenu.contains(event.target)) {
97 seoMenu.remove();
98 seoButton.classList.remove('active');
99 }
100
101 var button = document.getElementById('metasync-settings-btn');
102 var menu = document.getElementById('metasync-portal-menu');
103 if (menu && button && !button.contains(event.target) && !menu.contains(event.target)) {
104 menu.remove();
105 button.classList.remove('active');
106 }
107 });
108
109 /* ==========================================================================
110 Block 10 — Settings inner-page navigation dropdown (portal pattern)
111 ========================================================================== */
112
113 (function() {
114 var dropdowns = document.querySelectorAll('.metasync-nav-dropdown');
115 var activePortal = null;
116 var activeButton = null;
117 var activeDropdown = null;
118 var scrollHandler = null;
119 var resizeHandler = null;
120
121 // Position the portal menu relative to its trigger button
122 function positionPortalMenu(button, portalMenu) {
123 var rect = button.getBoundingClientRect();
124 var menuRect = portalMenu.getBoundingClientRect();
125 var viewportWidth = window.innerWidth;
126 var viewportHeight = window.innerHeight;
127
128 // Calculate left position, ensuring menu stays within viewport
129 var left = rect.left;
130 if (left + menuRect.width > viewportWidth - 10) {
131 left = Math.max(10, viewportWidth - menuRect.width - 10);
132 }
133
134 // Calculate top position, prefer below button but flip above if needed
135 var top = rect.bottom + 8;
136 if (top + menuRect.height > viewportHeight - 10 && rect.top > menuRect.height + 10) {
137 top = rect.top - menuRect.height - 8;
138 }
139
140 portalMenu.style.position = 'fixed';
141 portalMenu.style.top = top + 'px';
142 portalMenu.style.left = left + 'px';
143 portalMenu.style.zIndex = '999999999';
144 }
145
146 // Close the active portal menu
147 function closeActivePortal() {
148 if (activePortal && activePortal.parentNode) {
149 activePortal.parentNode.removeChild(activePortal);
150 }
151 if (activeButton) {
152 activeButton.setAttribute('aria-expanded', 'false');
153 }
154 if (activeDropdown) {
155 activeDropdown.classList.remove('active');
156 }
157 if (scrollHandler) {
158 window.removeEventListener('scroll', scrollHandler, true);
159 scrollHandler = null;
160 }
161 if (resizeHandler) {
162 window.removeEventListener('resize', resizeHandler);
163 resizeHandler = null;
164 }
165 activePortal = null;
166 activeButton = null;
167 activeDropdown = null;
168 }
169
170 // Open a dropdown as a portal
171 function openAsPortal(dropdown, button, menu) {
172 // Close any existing portal first
173 closeActivePortal();
174
175 // Clone the menu content and create portal
176 var portalMenu = menu.cloneNode(true);
177 portalMenu.id = 'metasync-nav-portal-menu';
178 portalMenu.style.opacity = '1';
179 portalMenu.style.visibility = 'visible';
180 portalMenu.style.transform = 'none';
181
182 // Append to body (portal pattern)
183 document.body.appendChild(portalMenu);
184
185 // Position after adding to DOM so we can measure
186 positionPortalMenu(button, portalMenu);
187
188 // Update state
189 activePortal = portalMenu;
190 activeButton = button;
191 activeDropdown = dropdown;
192 dropdown.classList.add('active');
193 button.setAttribute('aria-expanded', 'true');
194
195 // Create bound handlers for repositioning
196 scrollHandler = function() {
197 if (activePortal && activeButton) {
198 positionPortalMenu(activeButton, activePortal);
199 }
200 };
201 resizeHandler = scrollHandler;
202
203 // Reposition on scroll and resize
204 window.addEventListener('scroll', scrollHandler, true);
205 window.addEventListener('resize', resizeHandler);
206
207 // Handle clicks within portal menu (for navigation)
208 portalMenu.addEventListener('click', function(e) {
209 var link = e.target.closest('a');
210 if (link) {
211 // Let the link navigate naturally, then close
212 setTimeout(closeActivePortal, 0);
213 }
214 });
215 }
216
217 dropdowns.forEach(function(dropdown) {
218 var button = dropdown.querySelector('.metasync-nav-dropdown-btn');
219 var menu = dropdown.querySelector('.metasync-nav-dropdown-menu');
220
221 if (!button || !menu) return;
222
223 button.addEventListener('click', function(e) {
224 e.preventDefault();
225 e.stopPropagation();
226
227 var isExpanded = button.getAttribute('aria-expanded') === 'true';
228
229 if (isExpanded) {
230 closeActivePortal();
231 } else {
232 openAsPortal(dropdown, button, menu);
233 }
234 });
235 });
236
237 // Close portal when clicking outside
238 document.addEventListener('click', function(e) {
239 if (activePortal && activeButton) {
240 // Check if click is outside both the portal and the trigger button
241 if (!activePortal.contains(e.target) && !activeButton.contains(e.target)) {
242 closeActivePortal();
243 }
244 }
245 });
246
247 // Close portal on escape key
248 document.addEventListener('keydown', function(e) {
249 if (e.key === 'Escape' && activePortal) {
250 closeActivePortal();
251 if (activeButton) {
252 activeButton.focus();
253 }
254 }
255 });
256 })();
257
258 // Portal-style dropdown that bypasses stacking contexts (settings inner page)
259 function toggleSettingsMenuPortalInner(event) {
260 event.preventDefault();
261 event.stopPropagation();
262
263 var button = event.currentTarget;
264 var existingMenu = document.getElementById('metasync-portal-menu');
265
266 // If menu exists, close it
267 if (existingMenu) {
268 existingMenu.remove();
269 button.classList.remove('active');
270 button.setAttribute('aria-expanded', 'false');
271 return;
272 }
273
274 // Create menu outside form context
275 var menu = document.createElement('div');
276 menu.id = 'metasync-portal-menu';
277 menu.className = 'metasync-portal-menu';
278
279 // Get current page context for active states
280 var currentUrl = window.location.href;
281 var isGeneralActive = currentUrl.indexOf('tab=general') > -1 || currentUrl.indexOf('tab=') === -1;
282 var isAdvancedActive = currentUrl.indexOf('tab=advanced') > -1;
283 var isWhitelabelActive = currentUrl.indexOf('tab=whitelabel') > -1;
284
285 // Fixed: Use safer DOM manipulation instead of innerHTML to prevent XSS
286 menu.textContent = '';
287
288 var hideAdvanced = metasyncNavData.hideAdvanced;
289 var showGeneral = metasyncNavData.showGeneral;
290
291 // Only add General link when user has access to Settings
292 if (showGeneral) {
293 var generalLink = document.createElement('a');
294 generalLink.href = '?page=' + metasyncNavData.pageSlug + '&tab=general';
295 generalLink.className = 'metasync-portal-item' + (isGeneralActive ? ' active' : '');
296 generalLink.textContent = 'General';
297 menu.appendChild(generalLink);
298 }
299
300 // White Label tab
301 var whitelabelLink = document.createElement('a');
302 whitelabelLink.href = '?page=' + metasyncNavData.pageSlug + '&tab=whitelabel';
303 whitelabelLink.className = 'metasync-portal-item' + (isWhitelabelActive ? ' active' : '');
304 whitelabelLink.textContent = 'White label';
305 menu.appendChild(whitelabelLink);
306
307 // Only add Advanced tab if not hidden
308 if (!hideAdvanced) {
309 var advancedLink = document.createElement('a');
310 advancedLink.href = '?page=' + metasyncNavData.pageSlug + '&tab=advanced';
311 advancedLink.className = 'metasync-portal-item' + (isAdvancedActive ? ' active' : '');
312 advancedLink.textContent = 'Advanced';
313 menu.appendChild(advancedLink);
314 }
315
316 // Position menu relative to button
317 var rect = button.getBoundingClientRect();
318 menu.style.position = 'fixed';
319 menu.style.top = (rect.bottom + 8) + 'px';
320 menu.style.right = (window.innerWidth - rect.right) + 'px';
321 menu.style.zIndex = '999999999';
322
323 // Append to body to escape form context
324 document.body.appendChild(menu);
325
326 // Update button state
327 button.classList.add('active');
328 button.setAttribute('aria-expanded', 'true');
329 }
330
331 // Close dropdown when clicking outside (settings inner page)
332 document.addEventListener('click', function(event) {
333 var button = document.getElementById('metasync-settings-btn');
334 var menu = document.getElementById('metasync-portal-menu');
335
336 if (menu && button && !button.contains(event.target) && !menu.contains(event.target)) {
337 menu.remove();
338 button.classList.remove('active');
339 button.setAttribute('aria-expanded', 'false');
340 }
341 });
342