1<!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 <meta name="hydra-site-id" content="q7u17cy4"> 7 8 <!-- Primary Meta Tags --> 9 <title>Book | Eirini Vourloumis</title> 10 <meta name="description" content="In Waiting 11 12Published by Hatje Cantz 13Release Date,October 2017 14Text(s) by James Estrin, Panos Kokkinias, Eirini Voulourmis 15100 pg, 42 ills. 16hardcover 179.00â¦"> 18 <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"> 19 <meta name="author" content="eiriniphoto.com"> 20 <meta name="language" content="en"> 21 22 <!-- Canonical URL --> 23 <link rel="canonical" href="https://eiriniphoto.com/book"> 24 25 <!-- Favicon --> 26 27 28 29 30 31 <!-- Open Graph / Facebook / iMessage / Discord --> 32 <meta property="og:type" content="article"> 33 <meta property="og:url" content="https://eiriniphoto.com/book"> 34 <meta property="og:title" content="Book | Eirini Vourloumis"> 35 <meta property="og:description" content="In Waiting 36 37Published by Hatje Cantz 38Release Date,October 2017 39Text(s) by James Estrin, Panos Kokkinias, Eirini Voulourmis 40100 pg, 42 ills. 41hardcover 429.00â¦"> 43 <meta property="og:site_name" content="eiriniphoto.com"> 44 <meta property="og:locale" content="en_US"> 45 <meta property="og:image" content="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW1.jpg"> 46 <meta property="og:image:secure_url" content="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW1.jpg"> 47 <meta property="og:image:width" content="1200"> 48 <meta property="og:image:height" content="630"> 49 <meta property="og:image:type" content="image/jpeg"> 50 <meta property="og:image:alt" content="Book | Eirini Vourloumis"> 51 52 <!-- Twitter / X Card --> 53 <meta name="twitter:card" content="summary_large_image"> 54 <meta name="twitter:site" content="@eiriniphoto.com"> 55 <meta name="twitter:url" content="https://eiriniphoto.com/book"> 56 <meta name="twitter:title" content="Book | Eirini Vourloumis"> 57 <meta name="twitter:description" content="In Waiting 58 59Published by Hatje Cantz 60Release Date,October 2017 61Text(s) by James Estrin, Panos Kokkinias, Eirini Voulourmis 62100 pg, 42 ills. 63hardcover 649.00â¦"> 65 <meta name="twitter:image" content="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW1.jpg"> 66 <meta name="twitter:image:alt" content="Book | Eirini Vourloumis"> 67 68 <!-- Additional SEO Meta Tags --> 69 <meta name="theme-color" content="#000000"> 70 <meta name="msapplication-TileColor" content="#000000"> 71 <meta name="apple-mobile-web-app-capable" content="yes"> 72 <meta name="apple-mobile-web-app-status-bar-style" content="default"> 73 <meta name="apple-mobile-web-app-title" content="Book | Eirini Vourloumis"> 74 75 <!-- Security Headers --> 76 <meta http-equiv="X-Content-Type-Options" content="nosniff"> 77 <meta http-equiv="X-XSS-Protection" content="1; mode=block"> 78 <style> 79 /* FOUC gate: opacity on <html> cannot be pierced by child visibility:!important (submenu CSS) */ 80 html:not(.hydra-ready) { opacity: 0 !important; } 81 /* Critical: hide nested menus before menu-styles.css arrives */ 82 li:not(.expanded) > ol.nested-sortable { display: none !important; } 83 .hydra-seo-nav { position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0; } 84 .hydra-seo-nav ul { list-style:none;margin:0;padding:0; } 85 .hydra-skip-link { position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden; } 86 </style> 87 <noscript><style>html { opacity: 1 !important; }</style></noscript> 88
88<script type="application/ld+json"> 89 { 90 "@context": "https://schema.org", 91 "@type": [ 92 "WebPage", 93 "ImageGallery" 94 ], 95 "name": "Book | Eirini Vourloumis", 96 "description": "In Waiting\\n\\nPublished by Hatje Cantz\\nRelease Date,October 2017\\nText(s) by James Estrin, Panos Kokkinias, Eirini Voulourmis\\n100 pg, 42 ills.\\nhardcover\\n9.00â¦", 97 "url": "https://eiriniphoto.com/book", 98 "datePublished": "2026-10-06T21:33:58.466Z", 99 "dateModified": "2026-10-06T21:33:58.466Z", 100 "inLanguage": "en-US", 101 "isPartOf": { 102 "@type": "WebSite", 103 "@id": "https://eiriniphoto.com/#website", 104 "name": "eiriniphoto.com", 105 "url": "https://eiriniphoto.com/", 106 "description": "In Waiting\\n\\nPublished by Hatje Cantz\\nRelease Date,October 2017\\nText(s) by James Estrin, Panos Kokkinias, Eirini Voulourmis\\n100 pg, 42 ills.\\nhardcover\\n9.00â¦", 107 "publisher": { 108 "@type": "Organization", 109 "name": "eiriniphoto.com", 110 "url": "https://eiriniphoto.com/" 111 } 112 }, 113 "breadcrumb": { 114 "@type": "BreadcrumbList", 115 "itemListElement": [ 116 { 117 "@type": "ListItem", 118 "position": 1, 119 "name": "Home", 120 "item": "https://eiriniphoto.com/" 121 }, 122 { 123 "@type": "ListItem", 124 "position": 2, 125 "name": "Book | Eirini Vourloumis", 126 "item": "https://eiriniphoto.com/book" 127 } 128 ] 129 }, 130 "associatedMedia": [ 131 { 132 "@type": "ImageObject", 133 "name": "IW1", 134 "description": "", 135 "caption": "IW1", 136 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW1.jpg" 137 }, 138 { 139 "@type": "ImageObject", 140 "name": "IW2", 141 "description": "", 142 "caption": "IW2", 143 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW2.jpg" 144 }, 145 { 146 "@type": "ImageObject", 147 "name": "IW3", 148 "description": "", 149 "caption": "IW3", 150 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW3.jpg" 151 }, 152 { 153 "@type": "ImageObject", 154 "name": "IW4", 155 "description": "", 156 "caption": "IW4", 157 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW4.jpg" 158 }, 159 { 160 "@type": "ImageObject", 161 "name": "IW5", 162 "description": "", 163 "caption": "IW5", 164 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW5.jpg" 165 }, 166 { 167 "@type": "ImageObject", 168 "name": "IW6", 169 "description": "", 170 "caption": "IW6", 171 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW6.jpg" 172 }, 173 { 174 "@type": "ImageObject", 175 "name": "IW7", 176 "description": "", 177 "caption": "IW7", 178 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW7.jpg" 179 }, 180 { 181 "@type": "ImageObject", 182 "name": "IW8", 183 "description": "", 184 "caption": "IW8", 185 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW8.jpg" 186 }, 187 { 188 "@type": "ImageObject", 189 "name": "IW9", 190 "description": "", 191 "caption": "IW9", 192 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW9.jpg" 193 }, 194 { 195 "@type": "ImageObject", 196 "name": "IW10", 197 "description": "", 198 "caption": "IW10", 199 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW10.jpg" 200 }, 201 { 202 "@type": "ImageObject", 203 "name": "IW11", 204 "description": "", 205 "caption": "IW11", 206 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW11.jpg" 207 }, 208 { 209 "@type": "ImageObject", 210 "name": "IW12", 211 "description": "", 212 "caption": "IW12", 213 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW12.jpg" 214 }, 215 { 216 "@type": "ImageObject", 217 "name": "IW13", 218 "description": "", 219 "caption": "IW13", 220 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW13.jpg" 221 }, 222 { 223 "@type": "ImageObject", 224 "name": "IW14", 225 "description": "", 226 "caption": "IW14", 227 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW14.jpg" 228 }, 229 { 230 "@type": "ImageObject", 231 "name": "IW15", 232 "description": "", 233 "caption": "IW15", 234 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW15.jpg" 235 }, 236 { 237 "@type": "ImageObject", 238 "name": "IW16", 239 "description": "", 240 "caption": "IW16", 241 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW16.jpg" 242 }, 243 { 244 "@type": "ImageObject", 245 "name": "IW17", 246 "description": "", 247 "caption": "IW17", 248 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW17.jpg" 249 }, 250 { 251 "@type": "ImageObject", 252 "name": "IW18", 253 "description": "", 254 "caption": "IW18", 255 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW18.jpg" 256 }, 257 { 258 "@type": "ImageObject", 259 "name": "IW19w", 260 "description": "", 261 "caption": "IW19w", 262 "contentUrl": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW19w.jpg" 263 } 264 ], 265 "numberOfItems": 19, 266 "primaryImageOfPage": { 267 "@type": "ImageObject", 268 "url": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW1.jpg", 269 "width": 1200, 270 "height": 630, 271 "caption": "Book | Eirini Vourloumis" 272 }, 273 "image": { 274 "@type": "ImageObject", 275 "url": "https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW1.jpg", 276 "width": 1200, 277 "height": 630, 278 "caption": "Book | Eirini Vourloumis" 279 } 280} 281 </script>
281 282 283 284 <!-- Preview mode helper script --> 285
285<script> 286 // Immediately execute to patch request methods before any other script runs 287 (function() { 288 // Detect preview mode by checking hostname 289 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 290 291 if (isPreviewMode) { 292 console.log("[Preview Helper] Preview mode detected"); 293 294 // Extract GUID from URL path (first segment) 295 const pathParts = window.location.pathname.split('/').filter(Boolean); 296 const previewGuid = pathParts.length > 0 ? pathParts[0] : ''; 297 298 if (!previewGuid) { 299 console.warn("[Preview Helper] No GUID found in path:", window.location.pathname); 300 return; // Exit if no GUID found 301 } 302 303 console.log("[Preview Helper] Using GUID:", previewGuid); 304 305 // Store these in window for other scripts 306 window.isPreviewMode = true; 307 window.previewGuid = previewGuid; 308 window.isHydraEnvironment = true; 309 window.Parameters = window.Parameters || {}; 310 window.Parameters.isHydra = true; 311 312 // Helper function that adds the GUID to API URLs and relative links 313 window.getApiUrl = function(endpoint) { 314 // For relative paths 315 if (endpoint.startsWith('/api/')) { 316 const newUrl = '/' + previewGuid + endpoint; 317 console.log("[Preview Helper] Transformed API URL:", endpoint, "â", newUrl); 318 return newUrl; 319 } 320 return endpoint; 321 }; 322 323 // Helper function that adds the GUID to any relative link 324 window.getPreviewUrl = function(url) { 325 // For relative paths (starts with /) that aren't already prefixed with GUID 326 if (url.startsWith('/') && !url.startsWith('/' + previewGuid + '/')) { 327 const newUrl = '/' + previewGuid + url; 328 console.log("[Preview Helper] Transformed relative link:", url, "â", newUrl); 329 return newUrl; 330 } 331 return url; 332 }; 333 334 // PATCH FETCH API with enhanced URL handling 335 const originalFetch = window.fetch; 336 window.fetch = function(resource, init) { 337 // Only transform string URLs 338 if (typeof resource === 'string') { 339 // Case 1: Handle relative paths starting with /api/ 340 if (resource.startsWith('/api/')) { 341 const newUrl = window.getApiUrl(resource); 342 console.log("[Preview Helper] Patched fetch call (API):", resource, "â", newUrl); 343 return originalFetch(newUrl, init); 344 } 345 346 // Case 2: Handle other relative paths (for content/gallery links) 347 else if (resource.startsWith('/') && !resource.startsWith('/' + previewGuid + '/')) { 348 const newUrl = window.getPreviewUrl(resource); 349 console.log("[Preview Helper] Patched fetch call (relative):", resource, "â", newUrl); 350 return originalFetch(newUrl, init); 351 } 352 353 // Case 3: Handle absolute URLs to the same domain containing /api/ 354 else if (resource.includes(window.location.host) && resource.includes('/api/')) { 355 try { 356 const urlObj = new URL(resource); 357 if (urlObj.hostname === window.location.hostname) { 358 const path = urlObj.pathname; 359 // Already has /{guid}/api/... (e.g. from new URL(getApiUrl(...), origin)) â do not double-prefix 360 if (path.startsWith('/' + previewGuid + '/')) { 361 return originalFetch(resource, init); 362 } 363 const newPath = '/' + previewGuid + path; 364 urlObj.pathname = newPath; 365 const newUrl = urlObj.toString(); 366 console.log("[Preview Helper] Patched fetch call (absolute API):", resource, "â", newUrl); 367 return originalFetch(newUrl, init); 368 } 369 } catch (e) { 370 console.error("[Preview Helper] Error processing URL:", e); 371 } 372 } 373 374 // Case 4: Handle absolute URLs to the same domain (non-API) 375 else if (resource.includes(window.location.host)) { 376 try { 377 const urlObj = new URL(resource); 378 if (urlObj.hostname === window.location.hostname && 379 !urlObj.pathname.startsWith('/' + previewGuid + '/')) { 380 const path = urlObj.pathname; 381 const newPath = '/' + previewGuid + path; 382 urlObj.pathname = newPath; 383 const newUrl = urlObj.toString(); 384 console.log("[Preview Helper] Patched fetch call (absolute):", resource, "â", newUrl); 385 return originalFetch(newUrl, init); 386 } 387 } catch (e) { 388 console.error("[Preview Helper] Error processing URL:", e); 389 } 390 } 391 } 392 return originalFetch(resource, init); 393 }; 394 console.log("[Preview Helper] Patched fetch API"); 395 396 // PATCH XMLHttpRequest with enhanced URL handling 397 const originalOpen = XMLHttpRequest.prototype.open; 398 XMLHttpRequest.prototype.open = function(method, url, ...rest) { 399 // Only transform string URLs 400 if (typeof url === 'string') { 401 // Case 1: Handle relative paths starting with /api/ 402 if (url.startsWith('/api/')) { 403 const newUrl = window.getApiUrl(url); 404 console.log("[Preview Helper] Patched XMLHttpRequest (API):", url, "â", newUrl); 405 return originalOpen.call(this, method, newUrl, ...rest); 406 } 407 408 // Case 2: Handle other relative paths 409 else if (url.startsWith('/') && !url.startsWith('/' + previewGuid + '/')) { 410 const newUrl = window.getPreviewUrl(url); 411 console.log("[Preview Helper] Patched XMLHttpRequest (relative):", url, "â", newUrl); 412 return originalOpen.call(this, method, newUrl, ...rest); 413 } 414 415 // Case 3: Handle absolute URLs to the same domain containing /api/ 416 else if (url.includes(window.location.host) && url.includes('/api/')) { 417 try { 418 const urlObj = new URL(url); 419 if (urlObj.hostname === window.location.hostname) { 420 const path = urlObj.pathname; 421 if (path.startsWith('/' + previewGuid + '/')) { 422 return originalOpen.call(this, method, url, ...rest); 423 } 424 const newPath = '/' + previewGuid + path; 425 urlObj.pathname = newPath; 426 const newUrl = urlObj.toString(); 427 console.log("[Preview Helper] Patched XMLHttpRequest (absolute API):", url, "â", newUrl); 428 return originalOpen.call(this, method, newUrl, ...rest); 429 } 430 } catch (e) { 431 console.error("[Preview Helper] Error processing URL:", e); 432 } 433 } 434 435 // Case 4: Handle absolute URLs to the same domain (non-API) 436 else if (url.includes(window.location.host)) { 437 try { 438 const urlObj = new URL(url); 439 if (urlObj.hostname === window.location.hostname && 440 !urlObj.pathname.startsWith('/' + previewGuid + '/')) { 441 const path = urlObj.pathname; 442 const newPath = '/' + previewGuid + path; 443 urlObj.pathname = newPath; 444 const newUrl = urlObj.toString(); 445 console.log("[Preview Helper] Patched XMLHttpRequest (absolute):", url, "â", newUrl); 446 return originalOpen.call(this, method, newUrl, ...rest); 447 } 448 } catch (e) { 449 console.error("[Preview Helper] Error processing URL:", e); 450 } 451 } 452 } 453 return originalOpen.call(this, method, url, ...rest); 454 }; 455 console.log("[Preview Helper] Patched XMLHttpRequest"); 456 457 // Note: Link click handling is done by the existing gallery link interceptor 458 // The fetch and XMLHttpRequest patches above handle programmatic requests 459 460 // Add debugging aid 461 window.debugPreviewUrls = function() { 462 return { 463 hostname: window.location.hostname, 464 pathname: window.location.pathname, 465 guid: window.previewGuid, 466 isPreviewMode: window.isPreviewMode, 467 sampleTransformations: { 468 relativeApi: window.getApiUrl('/api/menu-styles'), 469 relativeLink: window.getPreviewUrl('/gallery'), 470 absolute: new URL('/api/save-config', window.location.origin).toString().replace('/api/', '/' + previewGuid + '/api/') 471 } 472 }; 473 }; 474 475 // Test the transformation immediately 476 console.log("[Preview Helper] Test API transformation:", window.getApiUrl('/api/menu-styles')); 477 console.log("[Preview Helper] Test relative link transformation:", window.getPreviewUrl('/gallery')); 478 console.log("[Preview Helper] Test absolute URL transformation:", 479 new URL('/api/save-config', window.location.origin).toString().replace('/api/', '/' + previewGuid + '/api/')); 480 } 481 })(); 482 </script>
482 483 484 485 486 487<!-- Early CDN retry helpers: MUST run before CDN script/link tags. 488 Changing script.src after a load error is unreliable (esp. Firefox defer); 489 always insert a new element pointing at /cdn-proxy/. -->
490<script> 491(function() { 492 window.useCdnProxy = window.useCdnProxy || false; 493 window.__hydraCdnRetryLog = window.__hydraCdnRetryLog || []; 494 function toProxy(url) { 495 if (!url || url.indexOf('cdn.neonsky.app') === -1) return url; 496 return url.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 497 } 498 function noteRetry(kind, fromUrl, toUrl) { 499 try { 500 window.useCdnProxy = true; 501 var entry = { kind: kind, from: fromUrl, to: toUrl, t: Date.now() }; 502 window.__hydraCdnRetryLog.push(entry); 503 console.warn('ð CDN failed, retrying via new element:', kind, fromUrl, '->', toUrl); 504 } catch (e) {} 505 } 506 window.__hydraRetryCdnScript = function(el) { 507 try { 508 if (!el || el.getAttribute('data-retried') === 'true') return; 509 var original = el.src || ''; 510 if (original.indexOf('cdn.neonsky.app') === -1) return; 511 el.setAttribute('data-retried', 'true'); 512 el.onerror = null; 513 var proxyUrl = toProxy(original); 514 noteRetry('script', original, proxyUrl); 515 var s = document.createElement('script'); 516 s.src = proxyUrl; 517 s.setAttribute('data-retried', 'true'); 518 if (el.id) s.id = el.id; 519 // Dynamic scripts ignore defer; omit it so the proxy copy executes on load. 520 if (el.parentNode) { 521 el.parentNode.insertBefore(s, el.nextSibling); 522 } else { 523 (document.head || document.documentElement).appendChild(s); 524 } 525 } catch (e) {} 526 }; 527 window.__hydraRetryCdnLink = function(el) { 528 try { 529 if (!el || el.getAttribute('data-retried') === 'true') return; 530 var original = el.href || ''; 531 if (original.indexOf('cdn.neonsky.app') === -1) return; 532 el.setAttribute('data-retried', 'true'); 533 el.onerror = null; 534 var proxyUrl = toProxy(original); 535 noteRetry('link', original, proxyUrl); 536 var link = document.createElement('link'); 537 link.rel = el.rel || 'stylesheet'; 538 if (el.id) link.id = el.id; 539 link.href = proxyUrl; 540 link.setAttribute('data-retried', 'true'); 541 if (el.parentNode) { 542 el.parentNode.insertBefore(link, el.nextSibling); 543 } else { 544 (document.head || document.documentElement).appendChild(link); 545 } 546 } catch (e) {} 547 }; 548})(); 549</script>
549 550<link rel="stylesheet" href="https://cdn.neonsky.app/menu-styles-v260923-002.css?t=1791322438466" onerror="window.__hydraRetryCdnLink(this)">
551<script src="https://assets.lemonsqueezy.com/lemon.js" defer></script>
vendor: 1 bytes, line 551
551
552<script src="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js"></script>
vendor: 1 bytes, line 552
552
553<script src="https://cdn.jsdelivr.net/npm/magic-sdk"></script>
vendor: 1 bytes, line 553
553
554<script src="https://cdn.jsdelivr.net/npm/@magic-ext/auth"></script>
vendor: 1 bytes, line 554
554
555<script src="https://cdn.neonsky.app/style-customizer-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
555 556<link rel="stylesheet" href="https://cdn.neonsky.app/palette-explorer-v260923-002.css?t=1791322438466" onerror="window.__hydraRetryCdnLink(this)">
557<script src="https://cdn.neonsky.app/palette-explorer-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 557
557
558<script src="https://cdn.neonsky.app/sidebar-manager-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 558
558
559<script src="https://cdn.neonsky.app/image-processor-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 559
559
560<script src="https://cdn.neonsky.app/image-uploader-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 560
560
561<script src="https://cdn.neonsky.app/page-layout-presets-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 561
561
562<script src="https://cdn.neonsky.app/site-graph-presets-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 562
562
563<script src="https://cdn.neonsky.app/site-graph-sync-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 563
563
564<script src="https://cdn.neonsky.app/page-manager-v260923-002.js?t=1791322438466" defer onerror="window.__hydraRetryCdnScript(this)"></script>
564 565<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css"> 566<link rel="stylesheet" href="https://cdn.neonsky.app/page-styles-v260923-002.css?t=1791322438466" onerror="window.__hydraRetryCdnLink(this)"> 567 568 569 570<!-- CDN Fallback Handler for CSS and JS files -->
571<script> 572(function() { 573 // Global flag: Set to true when any CDN resource fails and uses proxy 574 window.useCdnProxy = window.useCdnProxy || false; 575 576 // Global base URL function: Returns proxy base if flag is set, otherwise CDN base 577 window.getCdnBaseUrl = function() { 578 return window.useCdnProxy 579 ? window.location.origin + '/cdn-proxy/' 580 : 'https://cdn.neonsky.app/'; 581 }; 582 583 // Utility function to convert CDN URLs to proxy URLs 584 window.convertCdnToProxyUrl = function(cdnUrl) { 585 if (!cdnUrl || !cdnUrl.includes('cdn.neonsky.app')) return cdnUrl; 586 return cdnUrl.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 587 }; 588 589 // Utility function to construct CDN URLs (uses proxy if flag is set) 590 window.buildCdnUrl = function(path) { 591 return window.getCdnBaseUrl() + path; 592 }; 593 594 // Resource tracking for debugging 595 window._resourceTracking = window._resourceTracking || { 596 resources: [], 597 proxyFlagChanges: [], 598 imageLoads: [], 599 networkErrors: [] 600 }; 601 602 // Track when proxy flag changes - use a wrapper function 603 let proxyFlagChangeCount = 0; 604 let _useCdnProxyValue = window.useCdnProxy || false; 605 606 // Create a setter function that tracks changes 607 window._setUseCdnProxy = function(newValue, reason) { 608 const oldValue = _useCdnProxyValue; 609 if (oldValue !== newValue) { 610 proxyFlagChangeCount++; 611 const changeInfo = { 612 timestamp: new Date().toISOString(), 613 changeNumber: proxyFlagChangeCount, 614 oldValue: oldValue, 615 newValue: newValue, 616 reason: reason || 'Unknown', 617 stack: new Error().stack?.split('\n').slice(1, 6).join('\n') || 'No stack' 618 }; 619 window._resourceTracking.proxyFlagChanges.push(changeInfo); 620 console.error('ð PROXY_FLAG_CHANGE:', JSON.stringify(changeInfo, null, 2)); 621 // Keep only last 50 changes 622 if (window._resourceTracking.proxyFlagChanges.length > 50) { 623 window._resourceTracking.proxyFlagChanges.shift(); 624 } 625 _useCdnProxyValue = newValue; 626 window.useCdnProxy = newValue; 627 } 628 }; 629 630 // Track resource loading 631 function trackResource(url, type, status, details) { 632 const entry = { 633 timestamp: new Date().toISOString(), 634 url: url, 635 type: type, // 'script', 'link', 'image', etc. 636 status: status, // 'loading', 'success', 'failed', 'proxy', 'storage' 637 details: details || {}, 638 useCdnProxy: window.useCdnProxy 639 }; 640 window._resourceTracking.resources.push(entry); 641 console.error('ð RESOURCE_TRACK:', JSON.stringify(entry, null, 2)); 642 // Keep only last 200 resources 643 if (window._resourceTracking.resources.length > 200) { 644 window._resourceTracking.resources.shift(); 645 } 646 } 647 648 // Track image loads 649 function trackImageLoad(url, status, source) { 650 const entry = { 651 timestamp: new Date().toISOString(), 652 url: url, 653 status: status, // 'loading', 'success', 'failed' 654 source: source, // 'cdn', 'proxy', 'storage' 655 useCdnProxy: window.useCdnProxy 656 }; 657 window._resourceTracking.imageLoads.push(entry); 658 console.error('ð¼ï¸ IMAGE_LOAD:', JSON.stringify(entry, null, 2)); 659 // Keep only last 100 image loads 660 if (window._resourceTracking.imageLoads.length > 100) { 661 window._resourceTracking.imageLoads.shift(); 662 } 663 } 664 665 // Track network errors 666 function trackNetworkError(url, error, context) { 667 const entry = { 668 timestamp: new Date().toISOString(), 669 url: url, 670 error: { 671 message: error?.message || 'Unknown error', 672 name: error?.name || 'Unknown', 673 type: error?.type || 'unknown', 674 toString: String(error) 675 }, 676 context: context || {}, 677 useCdnProxy: window.useCdnProxy 678 }; 679 window._resourceTracking.networkErrors.push(entry); 680 console.error('â NETWORK_ERROR:', JSON.stringify(entry, null, 2)); 681 // Keep only last 100 errors 682 if (window._resourceTracking.networkErrors.length > 100) { 683 window._resourceTracking.networkErrors.shift(); 684 } 685 } 686 687 // Expose tracking functions globally 688 window.trackResource = trackResource; 689 window.trackImageLoad = trackImageLoad; 690 window.trackNetworkError = trackNetworkError; 691 692 // Function to retry failed resources via proxy 693 // For ISP-affected clients, we assume CDN failures are network/SSL errors and set the proxy flag 694 // The proxy can handle both network errors and 404s, so it's safer to err on the side of using it 695 function retryViaProxy(element, originalSrc, errorEvent) { 696 if (!originalSrc) originalSrc = element.href || element.src; 697 if (!originalSrc || !originalSrc.includes('cdn.neonsky.app')) return; 698 699 // Prevent multiple retries 700 if (element.getAttribute('data-retried')) { 701 return; 702 } 703 element.setAttribute('data-retried', 'true'); 704 705 // Check if element still exists in DOM 706 if (!element.parentNode) { 707 console.warn('â ï¸ Element no longer in DOM, skipping proxy retry:', originalSrc); 708 return; 709 } 710 711 // For ISP-affected clients, CDN failures are almost always network/SSL errors, not 404s 712 // We'll be aggressive and set the flag for any CDN failure to ensure images also use proxy 713 // Check error message if available, but default to assuming network error 714 const hasExplicitNetworkError = errorEvent && errorEvent.message && ( 715 errorEvent.message.includes('Failed to fetch') || 716 errorEvent.message.includes('SSL') || 717 errorEvent.message.includes('secure connection') || 718 errorEvent.message.includes('network') || 719 errorEvent.message.includes('ERR_') ||
720 errorEvent.message.includes('SSL_ERROR') 721 ); 722 723 // Track the error 724 trackNetworkError(originalSrc, errorEvent, { 725 elementType: element.tagName, 726 hasExplicitNetworkError: hasExplicitNetworkError, 727 errorMessage: errorEvent?.message || 'No error message', 728 errorName: errorEvent?.name || 'No error name' 729 }); 730 731 // Set flag if we have explicit network error, or if errorEvent is missing/null (browser silent failure) 732 // This covers ISP-affected clients where SSL handshake fails before HTTP request 733 const reason = hasExplicitNetworkError 734 ? 'Explicit network/SSL error detected' 735 : (!errorEvent || !errorEvent.message) 736 ? 'Browser silent failure (likely SSL/network)' 737 : 'CDN resource failed (using proxy as fallback)'; 738 739 if (hasExplicitNetworkError || !errorEvent || !errorEvent.message) { 740 window._setUseCdnProxy(true, reason); 741 console.error('ð© CDN_PROXY_FLAG SET: CDN failure detected, using proxy for all subsequent requests'); 742 console.error(' Error details:', errorEvent ? (errorEvent.message || 'No error message (likely SSL/network failure)') : 'No error event'); 743 } else { 744 // Even for other errors, set the flag to be safe - proxy can handle 404s too 745 window._setUseCdnProxy(true, reason); 746 console.error('ð© CDN_PROXY_FLAG SET: CDN resource failed, using proxy as fallback'); 747 } 748 749 const proxyUrl = originalSrc.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 750 console.error('ð CDN failed, retrying via proxy:', originalSrc, '->', proxyUrl); 751 752 // Track the retry 753 trackResource(originalSrc, element.tagName.toLowerCase(), 'failed', { 754 retryUrl: proxyUrl, 755 error: errorEvent?.message || 'Unknown' 756 }); 757 758 if (element.tagName === 'SCRIPT') { 759 const newScript = document.createElement('script'); 760 newScript.src = proxyUrl; 761 // Do not copy defer/async onto dynamically inserted scripts. After a CDN 762 // failure, DOMContentLoaded may already have fired; deferred dynamic 763 // scripts can fail to execute (seen on Firefox ISP/CDN-fail clients). 764 newScript.setAttribute('data-retried', 'true'); 765 newScript.onload = function() { 766 trackResource(proxyUrl, 'script', 'proxy-success', { originalUrl: originalSrc }); 767 }; 768 newScript.onerror = function() { 769 trackResource(proxyUrl, 'script', 'proxy-failed', { originalUrl: originalSrc }); 770 console.error('â Proxy also failed, trying storage:', proxyUrl); 771 const storageUrl = originalSrc.replace('cdn.neonsky.app', 'storage.neonsky.app'); 772 trackResource(storageUrl, 'script', 'storage-fallback', { originalUrl: originalSrc, proxyUrl: proxyUrl }); 773 newScript.src = storageUrl; 774 newScript.onload = function() { 775 trackResource(storageUrl, 'script', 'storage-success', { originalUrl: originalSrc }); 776 }; 777 }; 778 element.parentNode.replaceChild(newScript, element); 779 } else if (element.tagName === 'LINK') { 780 const newLink = document.createElement('link'); 781 newLink.rel = element.rel; 782 newLink.href = proxyUrl; 783 newLink.setAttribute('data-retried', 'true'); 784 newLink.onload = function() { 785 trackResource(proxyUrl, 'link', 'proxy-success', { originalUrl: originalSrc }); 786 }; 787 newLink.onerror = function() { 788 trackResource(proxyUrl, 'link', 'proxy-failed', { originalUrl: originalSrc }); 789 console.error('â Proxy also failed, trying storage:', proxyUrl); 790 const storageUrl = originalSrc.replace('cdn.neonsky.app', 'storage.neonsky.app'); 791 trackResource(storageUrl, 'link', 'storage-fallback', { originalUrl: originalSrc, proxyUrl: proxyUrl }); 792 newLink.href = storageUrl; 793 newLink.onload = function() { 794 trackResource(storageUrl, 'link', 'storage-success', { originalUrl: originalSrc }); 795 }; 796 }; 797 element.parentNode.replaceChild(newLink, element); 798 } 799 } 800 801 // Monitor script and link tags for errors 802 document.addEventListener('error', function(e) { 803 const target = e.target; 804 if ((target.tagName === 'SCRIPT' || target.tagName === 'LINK') && 805 target.src && target.src.includes('cdn.neonsky.app')) { 806 retryViaProxy(target, target.src, e); 807 } else if (target.tagName === 'LINK' && target.href && target.href.includes('cdn.neonsky.app')) { 808 retryViaProxy(target, target.href, e); 809 } 810 }, true); 811 812 // Also check existing scripts/links that may have failed before this script loaded 813 // For ISP-affected clients, we assume CDN failures are network/SSL errors 814 setTimeout(function() { 815 document.querySelectorAll('script[src*="cdn.neonsky.app"], link[href*="cdn.neonsky.app"]').forEach(function(el) { 816 if (el.tagName === 'SCRIPT' && !el.getAttribute('data-loaded') && !el.getAttribute('data-retried')) { 817 el.addEventListener('error', function(errorEvent) { 818 // For ISP-affected clients, assume any CDN failure is a network/SSL error 819 // The proxy can handle both network errors and 404s 820 retryViaProxy(el, el.src, errorEvent); 821 }); 822 } else if (el.tagName === 'LINK' && !el.sheet && !el.getAttribute('data-loaded') && !el.getAttribute('data-retried')) { 823 // CSS link failed if it has no stylesheet - check after a delay 824 setTimeout(function() { 825 if (!el.sheet && !el.getAttribute('data-retried')) { 826 // For ISP-affected clients, assume any CDN failure is a network/SSL error 827 retryViaProxy(el, el.href, null); 828 } 829 }, 2000); 830 } 831 }); 832 }, 100); 833})(); 834</script>
834 835 836<!-- Log Capture System - S+J to download logs (works even if gallery doesn't load) -->
837<script> 838(function() { 839 'use strict'; 840 841 // Only initialize if not already initialized 842 if (window._indexLogCaptureInitialized) { 843 return; 844 } 845 window._indexLogCaptureInitialized = true; 846 847 // Wait for DOM to be ready 848 function initLogCapture() { 849 // Store original console methods 850 const originalConsole = { 851 log: console.log, 852 warn: console.warn, 853 error: console.error, 854 info: console.info 855 }; 856 857 // Array to store all console messages 858 window._capturedLogs = window._capturedLogs || []; 859 860 // Override console methods to capture logs 861 ['log', 'warn', 'error', 'info'].forEach(method => { 862 console[method] = function(...args) { 863 // Call original method 864 originalConsole[method].apply(console, args); 865 866 // Capture the log with timestamp 867 window._capturedLogs.push({ 868 timestamp: new Date().toISOString(), 869 level: method.toUpperCase(), 870 message: args.map(function(arg) { 871 if (arg !== null && arg !== undefined && Object(arg) === arg) { 872 try { 873 return JSON.stringify(arg, null, 2); 874 } catch (e) { 875 return String(arg); 876 } 877 } 878 return String(arg); 879 }).join(' ') 880 }); 881 882 // Keep only last 2000 logs to prevent memory issues 883 if (window._capturedLogs.length > 2000) { 884 window._capturedLogs.shift(); 885 } 886 }; 887 }); 888 889 // Add keyboard shortcut to download logs (S+J) 890 let pressedKeys = new Set(); 891 892 document.addEventListener('keydown', function(event) { 893 const key = (event.key || '').toLowerCase(); 894 895 if (key === 's' || key === 'j') { 896 pressedKeys.add(key); 897 898 // Check if both S and J are pressed 899 if (pressedKeys.has('s') && pressedKeys.has('j')) { 900 event.preventDefault(); 901 console.log('S+J detected - downloading logs...'); 902 903 // Show a brief visual indicator 904 const indicator = document.createElement('div'); 905 indicator.style.cssText = 906 'position: fixed; top: 20px; right: 20px; z-index: 10000; ' + 907 'background: #4CAF50; color: white; padding: 10px 20px; ' + 908 'border-radius: 5px; font-family: Arial, sans-serif; ' + 909 'font-size: 14px; font-weight: bold; ' + 910 'box-shadow: 0 2px 10px rgba(0,0,0,0.3);'; 911 indicator.textContent = 'ð¥ Downloading logs...'; 912 document.body.appendChild(indicator); 913 914 setTimeout(() => { 915 if (indicator.parentNode) { 916 indicator.parentNode.removeChild(indicator); 917 } 918 }, 2000); 919 920 downloadLogs(); 921 // Clear the set after triggering 922 pressedKeys.clear(); 923 } 924 } 925 }); 926 927 document.addEventListener('keyup', function(event) { 928 const key = (event.key || '').toLowerCase(); 929 if (key === 's' || key === 'j') { 930 pressedKeys.delete(key); 931 } 932 }); 933 934 console.log('ð Log capture system initialized. Press S+J simultaneously to download logs.'); 935 } 936 937 // Download captured logs as a text file 938 function downloadLogs() { 939 try { 940 // Get all scripts and links, categorized 941 const allScripts = Array.from(document.querySelectorAll('script[src]')).map(s => s.src); 942 const allLinks = Array.from(document.querySelectorAll('link[href]')).map(l => l.href); 943 944 // Get additional debug info 945 const debugInfo = { 946 url: window.location.href, 947 userAgent: navigator.userAgent, 948 timestamp: new Date().toISOString(), 949 useCdnProxy: window.useCdnProxy || false, 950 windowLocation: window.location.toString(), 951 documentReadyState: document.readyState, 952 // All scripts/links (not just CDN) 953 allScriptsLoaded: allScripts, 954 allLinksLoaded: allLinks, 955 // CDN-specific (for backward compatibility and CDN debugging) 956 scriptsLoaded: allScripts.filter(src => src.includes('cdn.neonsky.app')), 957 linksLoaded: allLinks.filter(href => href.includes('cdn.neonsky.app')), 958 // Gallery scripts (loaded from Worker) 959 galleryScriptsLoaded: allScripts.filter(src => src.includes('/gallery-scripts/')), 960 galleryLinksLoaded: allLinks.filter(href => href.includes('/gallery-scripts/')), 961 // Failed resources 962 failedScripts: Array.from(document.querySelectorAll('script[src*="cdn.neonsky.app"][data-retried]')).map(s => s.src), 963 failedLinks: Array.from(document.querySelectorAll('link[href*="cdn.neonsky.app"][data-retried]')).map(l => l.href), 964 // Proxy resources 965 proxyScriptsLoaded: allScripts.filter(src => src.includes('/cdn-proxy/')), 966 proxyLinksLoaded: allLinks.filter(href => href.includes('/cdn-proxy/')), 967 // Resource tracking data 968 resourceTracking: window._resourceTracking || { 969 resources: [], 970 proxyFlagChanges: [], 971 imageLoads: [], 972 networkErrors: [] 973 }, 974 // Performance timing 975 performanceTiming: performance.timing ? { 976 navigationStart: performance.timing.navigationStart, 977 domContentLoaded: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart, 978 loadComplete: performance.timing.loadEventEnd - performance.timing.navigationStart, 979 domInteractive: performance.timing.domInteractive - performance.timing.navigationStart 980 } : null, 981 // Style / logo diagnostics (layout/color bug triage) 982 styleDiagnostics: (function() { 983 try { 984 var rootStyle = getComputedStyle(document.documentElement); 985 var logoNodes = Array.from(document.querySelectorAll('.sidebar img, .sidebar-element img, img[src*="robert-vizzini"], .horizontal-header-logo img')); 986 return {
987 siteBackgroundColor: rootStyle.getPropertyValue('--site-background-color').trim(), 988 menuBackgroundColor: rootStyle.getPropertyValue('--menu-background-color').trim(), 989 menuColor: rootStyle.getPropertyValue('--menu-color').trim(), 990 inlineSiteBg: document.documentElement.style.getPropertyValue('--site-background-color'), 991 inlineMenuBg: document.documentElement.style.getPropertyValue('--menu-background-color'), 992 hasMenuStyleCustomizer: !!window.MenuStyleCustomizer, 993 menuStyleCustomizerInitialized: !!(window.MenuStyleCustomizer && window.MenuStyleCustomizer._initialized), 994 menuStyleSettingsBg: window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings 995 ? { 996 siteBackgroundColor: window.MenuStyleCustomizer.settings.siteBackgroundColor, 997 menuBackgroundColor: window.MenuStyleCustomizer.settings.menuBackgroundColor, 998 color: window.MenuStyleCustomizer.settings.color 999 } 1000 : null, 1001 hasSidebarManager: !!window.SidebarManager, 1002 sidebarElementCount: window.SidebarManager && window.SidebarManager.elements 1003 ? window.SidebarManager.elements.length 1004 : 0, 1005 hasPageManager: !!window.PageManager, 1006 pageManagerHasLoadPage: !!(window.PageManager && typeof window.PageManager.loadPage === 'function'), 1007 pageElementCount: (function() { 1008 try { 1009 return document.querySelectorAll('.gallery-container .page-element, .page-container .page-element').length; 1010 } catch (e) { return -1; } 1011 })(), 1012 pageContainerHtmlLength: (function() { 1013 try { 1014 var pc = document.querySelector('.gallery-container .page-container, .page-container'); 1015 return pc ? (pc.innerHTML || '').length : 0; 1016 } catch (e) { return -1; } 1017 })(), 1018 activeContentType: window._activeContentType || null, 1019 logoImageCount: logoNodes.length, 1020 logoImages: logoNodes.slice(0, 5).map(function(img) { 1021 return { 1022 src: img.currentSrc || img.src || '', 1023 naturalWidth: img.naturalWidth || 0, 1024 display: getComputedStyle(img).display, 1025 complete: !!img.complete 1026 }; 1027 }), 1028 earlyCdnRetries: window.__hydraCdnRetryLog || [] 1029 }; 1030 } catch (e) { 1031 return { error: String(e && e.message ? e.message : e) }; 1032 } 1033 })() 1034 }; 1035 1036 // Create log content 1037 const logContent = [ 1038 '=== INDEX.TS DEBUG LOGS ===', 1039 'Generated: ' + debugInfo.timestamp, 1040 'URL: ' + debugInfo.url, 1041 'User Agent: ' + debugInfo.userAgent, 1042 'useCdnProxy Flag: ' + debugInfo.useCdnProxy, 1043 'Document Ready State: ' + debugInfo.documentReadyState, 1044 '================================', 1045 'TIP: Search for [AUTH_SAVE] in CONSOLE LOGS below for authentication/save failure details.', 1046 'TIP: Search STYLE DIAGNOSTICS for layout/color/logo triage (backgrounds, MenuStyleCustomizer, SidebarManager, logo).', 1047 '================================', 1048 '', 1049 '=== DEBUG INFO ===', 1050 JSON.stringify(debugInfo, null, 2), 1051 '', 1052 '=== STYLE DIAGNOSTICS ===', 1053 JSON.stringify(debugInfo.styleDiagnostics, null, 2), 1054 '', 1055 '=== RESOURCE TRACKING SUMMARY ===', 1056 'Total Resources Tracked: ' + (debugInfo.resourceTracking.resources.length || 0), 1057 'Proxy Flag Changes: ' + (debugInfo.resourceTracking.proxyFlagChanges.length || 0), 1058 'Image Loads Tracked: ' + (debugInfo.resourceTracking.imageLoads.length || 0), 1059 'Network Errors Tracked: ' + (debugInfo.resourceTracking.networkErrors.length || 0), 1060 '', 1061 '=== PROXY FLAG CHANGES ===', 1062 JSON.stringify(debugInfo.resourceTracking.proxyFlagChanges, null, 2), 1063 '', 1064 '=== RESOURCE LOADING TRACKING ===', 1065 JSON.stringify(debugInfo.resourceTracking.resources, null, 2), 1066 '', 1067 '=== IMAGE LOADING TRACKING ===', 1068 JSON.stringify(debugInfo.resourceTracking.imageLoads, null, 2), 1069 '', 1070 '=== NETWORK ERRORS ===', 1071 JSON.stringify(debugInfo.resourceTracking.networkErrors, null, 2), 1072 '', 1073 '=== CONSOLE LOGS ===', 1074 ...(window._capturedLogs || []).map(log => 1075 '[' + log.timestamp + '] ' + log.level + ': ' + log.message 1076 ) 1077 ].join('\n'); 1078 1079 // Create and download file 1080 const blob = new Blob([logContent], { type: 'text/plain' }); 1081 const url = URL.createObjectURL(blob); 1082 const a = document.createElement('a'); 1083 a.href = url; 1084 a.download = 'index-debug-logs-' + new Date().toISOString().replace(/[:.]/g, '-') + '.txt';
1085 document.body.appendChild(a); 1086 a.click(); 1087 document.body.removeChild(a); 1088 URL.revokeObjectURL(url); 1089 1090 console.log('â Debug logs downloaded successfully!'); 1091 } catch (error) { 1092 console.error('â Failed to download logs:', error); 1093 alert('Failed to download logs. Error: ' + (error.message || 'Unknown error')); 1094 } 1095 } 1096 1097 // Initialize when DOM is ready 1098 if (document.readyState === 'loading') { 1099 document.addEventListener('DOMContentLoaded', initLogCapture); 1100 } else { 1101 // DOM already ready, initialize immediately 1102 setTimeout(initLogCapture, 100); 1103 } 1104})(); 1105</script>
1105 1106 1107<!-- Add auth token if available --> 1108 1109 1110<style> 1111/* Override Pico.css body padding to prevent jumping between pages */ 1112body { 1113 padding: 0 !important; 1114 margin: 0 !important; 1115 display: block !important; 1116} 1117 1118/* Scope Pico.css to only affect elements in edit mode */ 1119:root:not([data-theme="pico"]) { 1120--pico-applied: dark; 1121} 1122 1123/* Add data-theme="pico" to body when in edit mode */ 1124body.edit-mode-active { 1125data-theme: dark; 1126} 1127 1128/* Custom styles to ensure Pico only affects editor UI, not content */ 1129.sidebar.editing button, 1130.edit-controls button, 1131.element-header button, 1132.text-editor button, 1133.image-uploader button { 1134--pico-applied: dark; 1135} 1136 1137/* Hide default browser-styling when Pico is active */ 1138[data-theme="pico"] button.btn { 1139appearance: dark; 1140-webkit-appearance: dark; 1141} 1142 1143/* Ensure content isn't affected by Pico styles */ 1144.gallery-container, 1145.page-text, 1146.sidebar-text { 1147--pico-applied: none !important; 1148} 1149 1150/* Conflict Indicator Styles */ 1151#conflictIndicator { 1152 background: #d32f2f !important; 1153 border: 1px solid #d32f2f !important; 1154 border-radius: 4px; 1155 padding: 8px; 1156 cursor: pointer; 1157 transition: opacity 0.2s; 1158 margin-left: 8px; 1159 display: flex; /* Use flex for centering, but hidden by default via inline style */ 1160 align-items: center; 1161 justify-content: center; 1162} 1163 1164#conflictIndicator:hover { 1165 background: #e53935 !important; 1166 border-color: #e53935 !important; 1167 opacity: 0.9; 1168} 1169 1170#conflictIndicator svg { 1171 display: block; 1172 width: 24px; 1173 height: 24px; 1174 margin: 0 auto; 1175 color: white !important; 1176} 1177 1178/* Tooltip styles for conflict indicator - allow wrapping */ 1179#tooltip-container .tooltip[data-tooltip-for="conflictIndicator"], 1180#tooltip-container .tooltip { 1181 max-width: 200px; 1182 word-wrap: break-word; 1183 white-space: normal; 1184 line-height: 1.4; 1185} 1186 1187.global-header-controls { 1188 display: flex; 1189 align-items: center; 1190 gap: 8px; 1191} 1192 1193.sidebar-header { 1194 display: flex; 1195 align-items: center; 1196 gap: 8px; 1197} 1198 1199/* Cookie Notice â paper UI (public-safe; does not depend on edit-mode pe-* scopes) */ 1200.cookie-notice { 1201 --cn-paper: #f6f5f1; 1202 --cn-paper-elevated: #fdfcfa; 1203 --cn-ink: #2c2a27; 1204 --cn-ink-muted: #6b6760; 1205 --cn-ink-faint: #9b968d; 1206 --cn-rule: rgba(44, 42, 39, 0.1); 1207 --cn-rule-strong: rgba(44, 42, 39, 0.16); 1208 --cn-shadow: 0 -8px 28px rgba(44, 42, 39, 0.1); 1209 --cn-radius: 3px; 1210 --cn-font: "DM Sans", system-ui, -apple-system, sans-serif; 1211 1212 position: fixed; 1213 bottom: 0; 1214 left: 0; 1215 right: 0; 1216 z-index: 10000; 1217 display: flex; 1218 align-items: center; 1219 justify-content: space-between; 1220 flex-wrap: wrap; 1221 gap: 0.85rem 1.25rem; 1222 padding: 1.1rem 1.35rem 1.2rem; 1223 font-family: var(--cn-font); 1224 font-size: 14px; 1225 line-height: 1.45; 1226 color: var(--cn-ink); 1227 background: var(--cn-paper-elevated); 1228 border-top: 1px solid var(--cn-rule); 1229 box-shadow: var(--cn-shadow); 1230} 1231 1232.cookie-notice.hidden { 1233 display: none; 1234} 1235 1236.cookie-notice-text { 1237 flex: 1 1 220px; 1238 min-width: 0; 1239 margin: 0; 1240 color: var(--cn-ink-muted); 1241 font-size: 13px; 1242} 1243 1244.cookie-notice-text a.cookie-notice-policy { 1245 color: var(--cn-ink); 1246 font-weight: 600; 1247 text-decoration: underline; 1248 text-underline-offset: 2px; 1249} 1250 1251.cookie-notice-text a.cookie-notice-policy:hover { 1252 color: var(--cn-ink-muted); 1253} 1254 1255.cookie-notice-buttons { 1256 display: flex; 1257 flex-wrap: wrap; 1258 align-items: center; 1259 justify-content: flex-end; 1260 gap: 0.5rem; 1261} 1262 1263.cookie-notice .pe-btn { 1264 display: inline-flex; 1265 align-items: center; 1266 justify-content: center; 1267 gap: 0.4rem; 1268 margin: 0; 1269 padding: 0.45rem 0.85rem; 1270 font-family: var(--cn-font); 1271 font-size: 11px; 1272 font-weight: 600; 1273 letter-spacing: 0.1em; 1274 text-transform: uppercase; 1275 color: var(--cn-ink-muted); 1276 background: var(--cn-paper); 1277 border: 1px solid var(--cn-rule-strong); 1278 border-radius: var(--cn-radius); 1279 box-shadow: none; 1280 cursor: pointer; 1281 transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease; 1282} 1283 1284.cookie-notice .pe-btn:hover { 1285 color: var(--cn-ink); 1286 background: var(--cn-paper-elevated); 1287 border-color: rgba(44, 42, 39, 0.22); 1288} 1289 1290.cookie-notice .pe-btn--primary { 1291 color: var(--cn-ink); 1292 background: var(--cn-paper); 1293 border-color: var(--cn-rule-strong); 1294} 1295 1296.cookie-notice .pe-btn--primary:hover {
1297 color: var(--cn-ink); 1298 background: var(--cn-paper-elevated); 1299 border-color: rgba(44, 42, 39, 0.22); 1300} 1301 1302#hydraDoNotSellLink { 1303 --cn-paper: #f6f5f1; 1304 --cn-paper-elevated: #fdfcfa; 1305 --cn-ink: #2c2a27; 1306 --cn-ink-muted: #6b6760; 1307 --cn-rule-strong: rgba(44, 42, 39, 0.16); 1308 --cn-radius: 3px; 1309 --cn-font: "DM Sans", system-ui, -apple-system, sans-serif; 1310 1311 position: fixed; 1312 bottom: 10px; 1313 left: 12px; 1314 z-index: 9999; 1315 display: inline-flex; 1316 align-items: center; 1317 justify-content: center; 1318 margin: 0; 1319 padding: 0.45rem 0.85rem; 1320 font-family: var(--cn-font); 1321 font-size: 11px; 1322 font-weight: 600; 1323 letter-spacing: 0.1em; 1324 text-transform: uppercase; 1325 color: var(--cn-ink-muted); 1326 background: var(--cn-paper-elevated); 1327 border: 1px solid var(--cn-rule-strong); 1328 border-radius: var(--cn-radius); 1329 box-shadow: 0 2px 8px rgba(44, 42, 39, 0.06); 1330 cursor: pointer; 1331} 1332 1333#hydraDoNotSellLink.hidden { 1334 display: none; 1335} 1336 1337#hydraDoNotSellLink:hover { 1338 color: var(--cn-ink); 1339 background: var(--cn-paper); 1340 border-color: rgba(44, 42, 39, 0.22); 1341} 1342 1343/* FOUC reveal (paired with early <head> html:not(.hydra-ready) { opacity: 0 }) */ 1344html.hydra-ready { 1345 opacity: 1; 1346 transition: opacity 0.2s ease; 1347} 1348/* Keep body class for any legacy selectors; visibility no longer used (submenu CSS pierces it) */ 1349body.hydra-ready { 1350 visibility: visible; 1351} 1352 1353/* Admin panel typography */ 1354.sidebar-element-form, 1355.sidebar-element-form *, 1356.add-form, 1357.add-form *, 1358.metadata-editor, 1359.metadata-editor * { 1360 font-family: "Hanken Grotesk", sans-serif; 1361 font-weight: 200; 1362 letter-spacing: 0.02em; 1363} 1364 1365.sidebar-element-form h3, 1366.sidebar-element-form label, 1367.add-form label, 1368.metadata-editor h3, 1369.metadata-editor label { 1370 text-transform: uppercase; 1371 letter-spacing: 0.04em; 1372 font-weight: 300; 1373} 1374 1375.sidebar-element-form .form-actions .btn, 1376.sidebar-element-form button, 1377.add-form .form-actions .btn, 1378.add-form button, 1379.metadata-editor .form-actions .btn, 1380.metadata-editor button { 1381 text-transform: uppercase; 1382 letter-spacing: 0.08em; 1383 font-weight: 300; 1384} 1385 1386.sidebar-element-form input, 1387.sidebar-element-form select, 1388.sidebar-element-form textarea, 1389.sidebar-element-form button, 1390.add-form input, 1391.add-form select, 1392.add-form textarea, 1393.add-form button, 1394.metadata-editor input, 1395.metadata-editor select, 1396.metadata-editor textarea, 1397.metadata-editor button { 1398 font-family: "Hanken Grotesk", sans-serif; 1399 font-weight: 200; 1400 letter-spacing: 0.02em; 1401 border-radius: 0; 1402} 1403</style> 1404
1405<script> 1406// Flag to prevent duplicate initialization 1407window.hydraInitialized = false; 1408 1409function ensureAdminPanelFont() { 1410 const head = document.head || document.getElementsByTagName('head')[0]; 1411 if (!head) { 1412 return; 1413 } 1414 1415 const linkDefinitions = [ 1416 { 1417 id: 'admin-panels-font-preconnect-googleapis', 1418 rel: 'preconnect', 1419 href: 'https://fonts.googleapis.com' 1420 }, 1421 { 1422 id: 'admin-panels-font-preconnect-gstatic', 1423 rel: 'preconnect', 1424 href: 'https://fonts.gstatic.com', 1425 crossOrigin: 'anonymous' 1426 }, 1427 { 1428 id: 'admin-panels-font-stylesheet', 1429 rel: 'stylesheet', 1430 href: 'https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,200;1,200&display=swap' 1431 }, 1432 { 1433 id: 'admin-panels-font-barlow', 1434 rel: 'stylesheet', 1435 href: 'https://fonts.googleapis.com/css2?family=Barlow:ital,wght@0,200;0,400;1,200;1,400&family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap' 1436 } 1437 ]; 1438 1439 for (const definition of linkDefinitions) { 1440 if (!document.getElementById(definition.id)) { 1441 const link = document.createElement('link'); 1442 link.id = definition.id; 1443 link.rel = definition.rel; 1444 link.href = definition.href; 1445 1446 if (definition.crossOrigin) { 1447 link.crossOrigin = definition.crossOrigin; 1448 } 1449 1450 head.appendChild(link); 1451 } 1452 } 1453} 1454 1455ensureAdminPanelFont(); 1456 1457 window.Parameters = window.Parameters || {}; 1458 window.Parameters.siteId = "q7u17cy4"; 1459 window.Parameters.SiteId = "q7u17cy4"; // Also set capitalized version 1460 window.siteId = "q7u17cy4"; 1461 window.TURNSTILE_SITE_KEY = "0x4AAAAAADyHXlv6RTUIaDOp"; 1462 window.NEON_SKY_FILES = window.NEON_SKY_FILES || {}; 1463 window.NEON_SKY_FILES.QUILL_INTEGRATION = 'quill-integration-v260923-002.js'; 1464 window.NEON_SKY_FILES.SITE_THEMES = 'site-themes-v260923-002.css'; 1465 window.NEON_SKY_FILES.SITE_THEMES_MANIFEST = 'site-themes-v260923-002.json'; 1466 1467// Initialize menu styles from server 1468document.addEventListener('DOMContentLoaded', function() { 1469// Apply initial menu styles from server 1470const initialMenuStyles = {"fontFamily":"Outfit, sans-serif","fontSize":"14px","menuTextUppercase":false,"color":"#595959","activeColor":"#000000","hoverColor":"#000000","verticalSpacing":"2px","menuBackgroundColor":"#ffffff","menuBackgroundHidden":false,"siteBackgroundColor":"#ffffff","siteBackgroundUseGradient":false,"siteGradientRandomizeOnLoad":false,"artPaletteSelection":null,"menuBackgroundImageUrl":"","siteBackgroundImageUrl":"","mobileHeaderHeight":"60px","sidebarWidth":"19vw","sidebarPaddingTop":"20px","sidebarPaddingRight":"0px","sidebarPaddingBottom":"0px","sidebarPaddingLeft":"0px","globalPagePaddingTopEnabled":true,"globalPagePaddingLeftEnabled":true,"globalPagePaddingRightEnabled":true,"globalPagePaddingBottomEnabled":false,"globalPagePaddingTop":"20vh","globalPagePaddingLeft":"10vw","globalPagePaddingRight":"10vw","globalPagePaddingBottom":"","globalGalleryPaddingTopEnabled":false,"globalGalleryPaddingLeftEnabled":false,"globalGalleryPaddingRightEnabled":false,"globalGalleryPaddingBottomEnabled":false,"globalGalleryPaddingTop":"20vh","globalGalleryPaddingLeft":"10vw","globalGalleryPaddingRight":"10vw","globalGalleryPaddingBottom":"","menuLayout":"sidebar","horizontalMenuAlignment":"left","horizontalHeaderLogoLayout":"inline","horizontalHeaderShadow":true,"disableRightClick":false,"showCookieNotice":false,"cookieNoticeText":"We use cookies and similar technologies (including analytics and optional scripts) to understand how you use this site. These run only if you select Accept. Select Decline to keep them off. You can also choose Do Not Sell or Share My Personal Information.","privacyPolicyUrl":"","alignElementsAtBottom":false,"customCSS":"","customJS":"","siteThemeId":null,"siteThemeOptions":{},"fontName":"Outfit"}; 1471window.__hydraStyleFromQuery = null; 1472const initialSiteThemeRuntimeVars = null; 1473 1474// Set CSS custom properties 1475// ?hydra-style= preview: apply merged menuStyles (skip site-theme runtime vars that would override). 1476if (initialSiteThemeRuntimeVars && !window.__hydraStyleFromQuery) {
1477 Object.keys(initialSiteThemeRuntimeVars).forEach(function(key) { 1478 document.documentElement.style.setProperty(key, initialSiteThemeRuntimeVars[key]); 1479 }); 1480 if (initialMenuStyles && initialMenuStyles.verticalSpacing) { 1481 document.documentElement.style.setProperty('--menu-vertical-spacing', initialMenuStyles.verticalSpacing); 1482 } 1483 // Match menu chrome / Gallery Options overview preview (light weight) 1484 document.documentElement.style.setProperty('--menu-font-weight', '200'); 1485 if (initialMenuStyles && typeof initialMenuStyles.secondaryFontSize === 'string' && initialMenuStyles.secondaryFontSize.trim() !== '') { 1486 document.documentElement.style.setProperty('--gallery-description-font-size', initialMenuStyles.secondaryFontSize.trim()); 1487 } 1488} else if (initialMenuStyles) { 1489 if (initialMenuStyles.fontFamily) { 1490 document.documentElement.style.setProperty('--menu-font-family', initialMenuStyles.fontFamily); 1491 } 1492 if (initialMenuStyles.fontSize) { 1493 document.documentElement.style.setProperty('--menu-font-size', initialMenuStyles.fontSize); 1494 } 1495 // Only set when Secondary was explicitly saved; otherwise descriptions follow --menu-font-size 1496 if (typeof initialMenuStyles.secondaryFontSize === 'string' && initialMenuStyles.secondaryFontSize.trim() !== '') { 1497 document.documentElement.style.setProperty('--gallery-description-font-size', initialMenuStyles.secondaryFontSize.trim()); 1498 } 1499 document.documentElement.style.setProperty('--menu-font-weight', '200'); 1500 if (initialMenuStyles.color) { 1501 document.documentElement.style.setProperty('--menu-color', initialMenuStyles.color); 1502 } 1503 if (initialMenuStyles.activeColor) { 1504 document.documentElement.style.setProperty('--menu-active-color', initialMenuStyles.activeColor); 1505 } 1506 if (initialMenuStyles.hoverColor) { 1507 document.documentElement.style.setProperty('--menu-hover-color', initialMenuStyles.hoverColor); 1508 } 1509 // Background colors must be applied here too: MenuStyleCustomizer may load late/fail 1510 // after CDNâproxy retry, and CSS defaults are white/#fafafa. 1511 if (initialMenuStyles.siteBackgroundColor) { 1512 document.documentElement.style.setProperty('--site-background-color', initialMenuStyles.siteBackgroundColor); 1513 } 1514 if (initialMenuStyles.menuBackgroundColor && !initialMenuStyles.menuBackgroundHidden) { 1515 document.documentElement.style.setProperty('--menu-background-color', initialMenuStyles.menuBackgroundColor); 1516 } 1517 if (initialMenuStyles.verticalSpacing) { 1518 document.documentElement.style.setProperty('--menu-vertical-spacing', initialMenuStyles.verticalSpacing); 1519 } 1520 if (initialMenuStyles.menuBackgroundImageUrl && String(initialMenuStyles.menuBackgroundImageUrl).trim() !== '') { 1521 var menuBgImg = String(initialMenuStyles.menuBackgroundImageUrl).trim(); 1522 document.documentElement.style.setProperty('--menu-background-image', 'url(' + JSON.stringify(menuBgImg) + ')'); 1523 } 1524 var resolvedSiteBg = ''; 1525 if (window.SiteGradientGenerator && typeof window.SiteGradientGenerator.resolveSiteBackgroundImage === 'function') { 1526 resolvedSiteBg = window.SiteGradientGenerator.resolveSiteBackgroundImage(initialMenuStyles, { allowRandomize: true }); 1527 } else if (initialMenuStyles.siteBackgroundImageUrl) { 1528 resolvedSiteBg = String(initialMenuStyles.siteBackgroundImageUrl).trim(); 1529 } 1530 if (resolvedSiteBg) { 1531 var peIsCssGradient = String(resolvedSiteBg).toLowerCase().indexOf('gradient(') !== -1; 1532 var siteBgImgCss = peIsCssGradient 1533 ? resolvedSiteBg 1534 : 'url(' + JSON.stringify(resolvedSiteBg) + ')'; 1535 document.documentElement.style.setProperty('--site-background-image', siteBgImgCss); 1536 var siteBgSize = 'cover'; 1537 if (peIsCssGradient) { 1538 siteBgSize = window.SiteGradientGenerator && window.SiteGradientGenerator.gradientBackgroundSize 1539 ? window.SiteGradientGenerator.gradientBackgroundSize(resolvedSiteBg) 1540 : '100% 100%'; 1541 document.body.classList.add('has-site-background-gradient'); 1542 } 1543 document.documentElement.style.setProperty('--site-background-size', siteBgSize); 1544 } 1545 if (initialMenuStyles.mobileHeaderHeight && String(initialMenuStyles.mobileHeaderHeight).trim() !== '') { 1546 var mhh0 = String(initialMenuStyles.mobileHeaderHeight).trim(); 1547 /* 0 / 0vh / 0px collapse logo max-height calcs; use the same floor as getUserHorizontalHeaderHeightPx */ 1548 var mhhParsed = parseInt(mhh0, 10); 1549 if (Number.isNaN(mhhParsed) || mhhParsed < 1) { 1550 mhh0 = '60px'; 1551 } 1552 document.documentElement.style.setProperty('--mobile-header-height', mh
1552h0); 1553 document.documentElement.style.setProperty('--content-margin-top', mhh0); 1554 document.documentElement.style.setProperty('--top-bar-height-setting', mhh0); 1555 } 1556} 1557 1558if (initialMenuStyles && initialMenuStyles.menuBackgroundHidden) { 1559 document.body.classList.add('menu-background-hidden'); 1560 document.documentElement.style.setProperty('--menu-background-color', 'transparent'); 1561 document.documentElement.style.setProperty('--menu-background-image', 'none'); 1562} 1563 1564if (initialMenuStyles && initialMenuStyles.menuTextUppercase) { 1565 document.body.classList.add('menu-text-uppercase'); 1566 if (!document.getElementById('menu-text-uppercase-style')) { 1567 var menuUpperStyle = document.createElement('style'); 1568 menuUpperStyle.id = 'menu-text-uppercase-style'; 1569 menuUpperStyle.textContent = '.menu-item,.horizontal-menu-label,a.horizontal-menu-label{text-transform:uppercase!important;font-weight:200!important;}'; 1570 document.head.appendChild(menuUpperStyle); 1571 } 1572} 1573 1574// Store initial styles in MenuStyleCustomizer if it exists 1575if (window.MenuStyleCustomizer) { 1576 window.MenuStyleCustomizer.settings = { 1577 ...window.MenuStyleCustomizer.defaults, 1578 ...initialMenuStyles 1579 }; 1580} 1581}); 1582</script>
vendor: 1 bytes, line 1582
1582
1583<script> 1584let magic = null; 1585let userMetadata = null; 1586let isAuthenticated = false; 1587let isAdmin = false; 1588let didToken = null; 1589let siteId = "q7u17cy4"; 1590 1591/** Clear Hydra auth tokens/flags from this browser (used by logout button and ?logout=1 links). */ 1592function clearHydraClientAuthStorage() { 1593 try { 1594 localStorage.removeItem('hydra_is_admin'); 1595 localStorage.removeItem('hydra_auth_token'); 1596 localStorage.removeItem('hydra_auth_email'); 1597 localStorage.removeItem('hydra_did_token'); 1598 localStorage.removeItem('hydra_auth_site'); 1599 localStorage.removeItem('hydra_auth_last_refresh'); 1600 localStorage.removeItem('hydra_auth_auto_refresh'); 1601 } catch (e) {} 1602 if (window.TokenManager && typeof window.TokenManager.clearToken === 'function') { 1603 try { window.TokenManager.clearToken(); } catch (e) {} 1604 } 1605 isAuthenticated = false; 1606 isAdmin = false; 1607 didToken = null; 1608 userMetadata = null; 1609 window.isAuthenticated = false; 1610 window.isAdmin = false; 1611 window.didToken = null; 1612} 1613 1614function hydraUrlRequestsLogout() { 1615 try { 1616 var params = new URLSearchParams(window.location.search); 1617 var path = (window.location.pathname || '/').replace(/\/+$/, '') || '/'; 1618 if (params.get('logout') === '1' || params.get('logout') === 'true') return true; 1619 if (params.get('hydra_logout') === '1' || params.get('hydra_logout') === 'true') return true; 1620 return path === '/_manage/logout'; 1621 } catch (e) { 1622 return false; 1623 } 1624} 1625 1626function hydraCleanLogoutFromUrl() { 1627 try { 1628 var params = new URLSearchParams(window.location.search); 1629 var path = (window.location.pathname || '/').replace(/\/+$/, '') || '/'; 1630 var hadQuery = params.has('logout') || params.has('hydra_logout'); 1631 params.delete('logout'); 1632 params.delete('hydra_logout'); 1633 var nextPath = path === '/_manage/logout' ? '/_manage' : window.location.pathname; 1634 var qs = params.toString(); 1635 var next = nextPath + (qs ? '?' + qs : '') + (window.location.hash || ''); 1636 if (hadQuery || path === '/_manage/logout') { 1637 history.replaceState({}, '', next); 1638 } 1639 } catch (e) {} 1640} 1641 1642(function hydraLogoutFromUrlEarly() { 1643 if (!hydraUrlRequestsLogout()) return; 1644 clearHydraClientAuthStorage(); 1645 try { sessionStorage.setItem('hydra_logout_cleared', '1'); } catch (e) {} 1646 hydraCleanLogoutFromUrl(); 1647 console.log('[Hydra] Auth cleared via logout link/query.'); 1648})(); 1649 1650function hydraUrlRequestsRefresh() { 1651 try { 1652 var params = new URLSearchParams(window.location.search); 1653 var v = params.get('refresh'); 1654 return v === '1' || v === 'true'; 1655 } catch (e) { 1656 return false; 1657 } 1658} 1659 1660function hydraClearGalleryClientCache() { 1661 try { 1662 Object.keys(localStorage).filter(function (key) { 1663 return key.includes('gallery') || key.includes('neon') || key.includes('lightbox'); 1664 }
1664).forEach(function (key) { 1665 localStorage.removeItem(key); 1666 }); 1667 } catch (e) {} 1668 try { 1669 delete window.__neonGalleryMainBootstrapWipeDone; 1670 } catch (e) { 1671 window.__neonGalleryMainBootstrapWipeDone = false; 1672 } 1673 window._forceGalleryReload = true; 1674} 1675 1676function hydraCleanRefreshFromUrl() { 1677 try { 1678 var params = new URLSearchParams(window.location.search); 1679 if (!params.has('refresh')) return; 1680 params.delete('refresh'); 1681 var qs = params.toString(); 1682 var next = window.location.pathname + (qs ? '?' + qs : '') + (window.location.hash || ''); 1683 history.replaceState({}, '', next); 1684 } catch (e) {} 1685} 1686 1687(function hydraRefreshFromUrlEarly() { 1688 if (!hydraUrlRequestsRefresh()) return; 1689 clearHydraClientAuthStorage(); 1690 hydraClearGalleryClientCache(); 1691 try { sessionStorage.setItem('hydra_refresh_cleared', '1'); } catch (e) {} 1692 hydraCleanRefreshFromUrl(); 1693 console.log('[Hydra] Gallery cache/auth cleared via ?refresh=1 â forcing fresh load.'); 1694})(); 1695 1696// State Management â site-wide KV metadata for the editor (not per-page SEO) 1697window.siteMetadata = {"title":"Eirini Vourloumis ","description":"Eirini Vourloumis is available for both photo assignments and freelance editing.","googleAnalytics":"","noIndex":false,"favicon":"","ogImageUrl":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","customPublicSiteEnabled":false,"customPublicSiteHtmlR2Key":"","customPublicSiteHtmlFilename":"","schemaJobTitle":"","schemaCity":"","schemaEmail":""}; 1698console.log('Client Initialized site metadata:', window.siteMetadata); 1699 1700/** Match SSR title format: "{Gallery Title} | {Site Title}" */ 1701function formatGalleryDocumentTitle(galleryTitle, win) { 1702 win = win || window; 1703 var meta = win.siteMetadata; 1704 var siteTitle = (meta && meta.title && String(meta.title).trim()) || win.location.hostname; 1705 return (galleryTitle || 'Gallery') + ' | ' + siteTitle; 1706} 1707 1708console.log('Client Google Analytics from metadataToUse:', ''); 1709console.log('Client Google Analytics in window.siteMetadata:', window.siteMetadata.googleAnalytics); 1710console.log('Client Content Language:', window.siteMetadata.contentLanguage); 1711 1712// State Management 1713let galleries = [{"id":1786071153451,"title":"home","parentId":null,"visible":false,"isIntegrated":true,"pageId":"1it0jlwb","slug":"home","url":"/home","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","isIntegratedGallery":true,"pageId":"1it0jlwb","siteId":"q7u17cy4","siteAlias":"preview.neonsky.app","initialPageUuid":"1it0jlwb","initialPageAlias":"home","timestamp":1786071202336,"horizontalScrollerText":"<p><br></p>","startInSingles":true,"layoutSelection":"singles","useRowsLayout":false,"layoutType":"grid","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":2,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":true,"autoplayRandomStart":false,"autoplayDuration":"40","autoplayTransition":"1","showDescription":false,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":false,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":false,"layout":{"type":"grid","selection":"singles","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":true,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":0}},"isPerma":false,"flyIoSaveTimestamp":1786071205387,"timestampsMatch":false,"allTimestampsMatch":false,"cdnNdjsonId":"8e6660c092d541a7b59b12ee21f580d1","cdnNdjsonUrl":"https://cdn.neonsky.app/8e6660c092d541a7b59b12ee21f580d1.ndjson","cdnNdjsonStableUrl":"https://embed.neonsky.app/ndjson/q7u17cy4_1it0jlwb.json","cdnSaveTimestamp":1786071208016},"isHomePage":true,"siteId":"q7u17cy4","siteAlias":null,"isPage":false},{"id":1786071216950,"title":"Spacer","parentId":null,"visible":true,"isSpacer":true},{"id":1786071223113,"title":"Spacer-copy","parentId":null,"visible":true,"isSpacer":true,"slug":"spacer-copy","url":"/spacer-copy","isHomePage":false,"position":2},{"id":1786070400574,"title":"In the Same Space","url":"/in-the-same-space","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"q7u17cy4","pageId":"oh3tkrlx","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"in-the-same-space","normalizedName":"in-the-same-space","galleryOptions":{"manualCollectionName":"GUID=4bd5ec0aa36d9","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"oh3tkrlx","horizontalScrollerText":"<p>In the Same Space, is a photographic dialogue with my grandfatherâs Andreas Vourloumis (1910-199) paintings of Athens and the island of Aegina. My grandfather was part of the dynamic 1930âs generation of Greek artists. He was self-taught and his art was driven by capturing everyday life in the city. On his daily walks he worked very much like a documentary photographer, sketching buildings, pedestrians and daily interactions in the city. I used my mobile phone for this project as a way to mirror his own use of the sketch book in his daily routine. At the time I began this project I was working as a photojournalist covering the economic crisis for international media. I was motivated to begin this dialogue with my grandfather's paintings as a way to celebrate my hometown and it's people in a more positive way. The pairings are influenced by color, form, light and narrative context but mostly I was intent on reflecting the emotive state of my grandfatherâs paintings. This series is a meditation on urban change, on spaces or places that have been lost, but also a tribute to my grandfather who never knew I became a photographer. In that sense, it is highly personal and emotional, and at the same time a social and anthropological take on a city that has undergone rapid transformation in the last fifty years.</p>","jsonCollections":{"GUID=4bd5ec0aa36d9":{"isClassicCollection":true,"guid":"GUID=4bd5ec0aa36d9","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"oh3tkrlx","initialPageAlias":"in-the-same-space","timestamp":1786082655213,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter"
1713:false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":false,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"isPerma":false},"isHomePage":false,"position":18,"siteAlias":null},{"id":1786070383350,"title":"In Waiting","isFolder":true,"isCollapsed":false,"visible":true,"parentId":null,"siteId":"q7u17cy4","slug":"in-waiting","importSource":"folder","isSubmenu":true,"isHomePage":false,"position":3},{"id":1786070383351,"title":"Book","url":"/book","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070383350,"siteId":"q7u17cy4","pageId":"cwd7vhoc","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"book","normalizedName":"book","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"cwd7vhoc","horizontalScrollerText":"<p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>In Waiting</b></span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"margin-bottom: 0.5em; line-height: 0.5;\"><strong style=\"font-size: 14px;\">Published by Hatje Cantz</strong></p><p style=\"margin-bottom: 0.5em; line-height: 0.5;\"><strong style=\"font-size: 14px;\">Release Date,October 2017</strong></p><p style=\"margin-bottom: 0.5em; line-height: 0.5;\"><span style=\"font-size: 14px;\">Text(s) by James Estrin, Panos Kokkinias, Eirini Voulourmis</span></p><p style=\"margin-bottom: 0.5em; line-height: 0.5;\"><span style=\"font-size: 14px;\">100 pg, 42 ills.</span></p><p style=\"margin-bottom: 0.5em; line-height: 0.5;\"><span style=\"font-size: 14px;\">hardcover</span></p><p style=\"margin-bottom: 0.5em; line-height: 0.5;\"><span style=\"font-size: 14px;\">9.00 x 24.00 cm</span></p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">SBN 978-3-7757-4340-2</span></p><p style=\"line-height: 1;\">\n</p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">âIn Waiting,â is an examination of Athens and its role as a physical stage for the economic crisis. Primarily, I am exploring the interiors of government buildings, institutions, and schools to question how these spaces reflect modern Greek culture and character. The locations I chose are familiar microcosms for Athenians that have now become the public stage for a forced re-negotiation of Greek identity. I am attempting to personally rationalize how the deceptively tedious components of urban life factor into the economic crisis. I am specifically attracted to the particular aesthetic and surreal atmosphere I encounter in these spaces. These interiors also reveal an almost antiquated and bare aesthetic quality; they evoke in their deÌcor a feeling of dependency on and idolization of the past. Spaces, which once seemed banal or unimportant, now reveal nuances of Greek reality and have social and political impl
1713ications. Like the Greek people, these places exist in anticipation of their future.</span></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":true,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"cwd7vhoc","isHydra":true,"flyIoSaveTimestamp":1786070385097,"initialPageAlias":"book","timestamp":1786082268831,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":false,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"isPerma":false},"isHomePage":false,"position":5,"siteAlias":null},{"id":1786070385200,"title":"Selected images","url":"/selected-images","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070383350,"siteId":"q7u17cy4","pageId":"h0aj7770","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"selected-images","normalizedName":"selected-images","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"h0aj7770","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'In Waiting,â is an examination of Athens and its role as a physical stage for the economic crisis. Primarily, I am exploring the interiors of government buildings, institutions, and schools to question how these spaces reflect modern Greek culture and character. The locations I chose are familiar microcosms for Athenians that have now become the public stage for a forced re-negotiation of Greek identity. I am attempting to personally rationalize how the deceptively tedious components of urban life factor into the economic crisis. I am specifically attracted to the particular aesthetic and surreal atmosphere I encounter in these spaces. These interiors also reveal an almost antiquated and bare aesthetic quality; they evoke in their deÌcor a feeling of dependency on and idolization of the past. Spaces, which once seemed banal or unimportant, now reveal nuances of Greek reality and have social and political impl
1713ications. Like the Greek people, these places exist in anticipation of their future.</span></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":21}},"siteAlias":"preview.neonsky.app","initialPageUuid":"h0aj7770","isHydra":true,"flyIoSaveTimestamp":1786070387009,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":6},{"id":1786070387113,"title":"News","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1786070387113","visible":true,"parentId":1786070383350,"siteId":"q7u17cy4","slug":"news","url":"/news","pageElements":[{"id":1786070388013,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">March 25th, 2018</span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"https://www.berlinerfestspiele.de/en/aktuell/festivals/gropiusbau/programm_mgb/gropius_bau_veranstaltungen/detail_gropius_bau_veranstaltungen_253977.php\" target=\"_blank\"><b>Martin Gropius Bau Museum, Berlin</b></a></span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Solo exhibit, In Waiting</span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">In House with Wu Tsang</span></p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><br></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">January 2018</span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"https://bookshop.thephotographersgallery.org.uk/blogs/events/eirini-vourloumis-in-waiting-talk-booksigning-thu-18-jan-18-30-20-00\" target=\"_blank\"><b>The Photographerâs Gallery, London</b></a></span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">âIn Waitingâ, screening and book launch'</span></p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><span class=\"ql-cursor\"></span></span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">May 2nd-27th May 2018</span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>Benaki Art Museum</b>, Athens</span></p><p style=\"line-height: 0.5;\">\n</p><p style=\"line-height: 0.5; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">âIn Waitingâ, Book exhibition</span></p>","textWidth":100,"fadeInOnScroll":false},{"id":1786082290513,"type":"metadata","visible":true,"position":1,"metaTitle":"Untitled","metaDescription":"","metaKeywords":""}],"classicGuid":"4bd5ec0aa36d9","importSource":"classic-page","isHomePage":false,"position":7},{"id":1786070387114,"title":"Muslims In America","isFolder":true,"isCollapsed":false,"visible":true,"parentId":null,"siteId":"q7u17cy4","slug":"muslims-in-america","importSource":"folder","isSubmenu":true,"isHomePage":false,"position":8},{"id":1786070387116,"title":"Latino Muslims","url":"/latino-muslims","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070387114,"siteId":"q7u17cy4","pageId":"zymuxtc7","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"latino-muslims","normalizedName":"latino-muslims","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","
1713importedFromClassic":true,"siteId":"q7u17cy4","pageId":"zymuxtc7","horizontalScrollerText":"<p style=\"line-height: 1.5;\">There are a an estimated 200,000 Latinos who have converted to Islam in the United States the majority of whom live in New Jersey and New York City. A 2008 study by the Pew Forum on Religious and Public Life put the number of Latino Muslim U.S. residents at four percent of all Muslim U.S.residents. Even though this demographic of Latino Muslims is very small it introduces a new unique religious and cultural identity. The 44 million Latinos who live in the United States have always been closely associated with the Roman Catholic Church and more recently the evangelical Christian traditions.</p><p style=\"line-height: 1.5;\"><br></p><p style=\"line-height: 1.5;\">The most prominent Latino Muslim community in the East Coast is based out of the North Hudson Islamic Educational center in Union City, New Jersey (a predominantly Latino neighborhood), which counts 35 per cent of its congregation as Latino Muslims. The mosque has a very active Latino Dawah committee (volunteers who actively preach Islam) who conduct classes in Spanish teaching the Qu'ran and Islamic principals and who also organize an annual Latino Muslim day. Many conversions take place at this mosque where individuals recite the Shahadah, the Muslim declaration of belief in the oneness of Allah and acceptance of Muhammad as his prophet.</p><p style=\"line-height: 1.5;\"><br></p><p style=\"line-height: 1.5;\">Latinos convert to Islam for a variety of reasons, including disenchantment with the practices of Catholicism and the church establishment. These Latinos are lured by Islam's simplicity and the Muslim's independence of a mediating clergy in his or her relationship with God. Others believe they are reverting back to their true cultural traditions referring to the Moor influence (Spain's Islamic period) in Spain. Many converts today refer to themselves as 'reverts'. Others feel estranged from mainstream Christian America and feel that Muslims identify more closely to the Latino struggle for justice and equality.</p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":true,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"zymuxtc7","isHydra":true,"flyIoSaveTimestamp":1786070388847,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":false,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"initialPageAlias":"latino-muslims","timestamp":1786082464268,"filterMenuCollectionNames":"","filterMenuTitles":"","rolloverCollectionNames":"","isPerma":false},"isHomePage":false,"position":9,"siteAlias":null},{"id":1786070388954,"title":"Selected Assignments","isFolder":true,"isCollapsed":true,"visible":true,"parentId":null,"siteId":"q7u17cy4","slug":"selected-assignments","importSource":"folder","isSubmenu":true,"isHomePage":false,"position":10},{"id":1786070388955,"title":"Refugee Camps Crisis Greece-NYTimes","url":"/refugee-camps-crisis-greece-nytimes","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070388954,"siteId":"q7u17cy4","pageId":"ekwdjn33","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"refugee-camps-crisis-greece-nytimes","normalizedName":"refugee-camps-crisis-greece-nytimes","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","
1713importedFromClassic":true,"siteId":"q7u17cy4","pageId":"ekwdjn33","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":15}},"siteAlias":"preview.neonsky.app","initialPageUuid":"ekwdjn33","isHydra":true,"flyIoSaveTimestamp":1786070390685,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":11},{"id":1786070390794,"title":"Refugee Crisis Greece-Wall Street Journal","url":"/refugee-crisis-greece-wall-street-journal","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070388954,"siteId":"q7u17cy4","pageId":"scimz0hf","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"refugee-crisis-greece-wall-street-journal","normalizedName":"refugee-crisis-greece-wall-street-journal","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"scimz0hf","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":15}},"siteAlias":"preview.neonsky.app","initialPageUuid":"scimz0hf","isHydra":true,"flyIoSaveTimestamp":1786070392617,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"
1713center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":12},{"id":1786070392727,"title":"Soul Train-NYTimes","url":"/soul-train-nytimes","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070388954,"siteId":"q7u17cy4","pageId":"wyp3dqbg","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"soul-train-nytimes","normalizedName":"soul-train-nytimes","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"wyp3dqbg","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">These images are part of an assignment for the New York Tmes, accompaning author Roxana Robinson's piece,Soul Train. Her story explores her experiences of listening to subway musicians and the way in which the music brings an ambient and surreal experience to the regular routine of a subway commute.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">The NYTimes slideshow can be viewed at this link.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"http://www.nytimes.com/slideshow/2009/05/17/nyregion/20090517-SUBWAY_index.html?scp=5&sq=eirini%20vourloumis&st=cse\" target=\"_blank\">http://www.nytimes.com/slideshow/2009/05/17/nyregion/20090517-SUBWAY_index.html?scp=5&sq=eirini%20vourloumis&st=cse</a></span></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":11}},"siteAlias":"preview.neonsky.app","initialPageUuid":"wyp3dqbg","isHydra":true,"flyIoSaveTimestamp":1786070394417,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":13},{"id":1786070394528,"title":"Philly Fight Night-NYTimes","url":"/philly-fight-night-nytimes","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070388954,"siteId":"q7u17cy4","pageId":"lezuyzwg","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"philly-fight-night-nytimes","normalizedName":"philly-fight-night-nytimes","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","
1713importedFromClassic":true,"siteId":"q7u17cy4","pageId":"lezuyzwg","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">The 2009 Philly Fight Night is organized by students of The Wharton School and Penn Law School of the University of Pennsylvania. This is a charity driven event to raise money for the Philadelphia Boys and Girls club. Students from both schools compete against eachother accompanied by entourages in WWF style costumes.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Part of this story was published as a photo essay in the Sports section of the New York Times Sunday paper. Enter gallery below to see full story. NYTimes slideshow can be viewed at this link:</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"http://www.nytimes.com/slideshow/2009/03/06/sports/20090306-boxing-slideshow_index.html?ref=sports\" target=\"_blank\">http://www.nytimes.com/slideshow/2009/03/06/sports/20090306-boxing-slideshow_index.html?ref=sports</a></span></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":25}},"siteAlias":"preview.neonsky.app","initialPageUuid":"lezuyzwg","isHydra":true,"flyIoSaveTimestamp":1786070396772,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":14},{"id":1786070396884,"title":"The Decline of Perama Shipping Yards- The Financial Times","url":"/the-decline-of-perama-shipping-yards--the-financial-times","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070388954,"siteId":"q7u17cy4","pageId":"u8im8s66","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"the-decline-of-perama-shipping-yards--the-financial-times","normalizedName":"the-decline-of-perama-shipping-yards--the-financial-times","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"u8im8s66","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Perama repair docks known locally as \"the zone\" is one of the first victims of the ongoing economic crisis in Greece.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Perama was the most active repair zone for international shipping companies once called the Wall Street of Greeceâs ship repair industry.Most repair companies have now left the area due to the decline of the number of ships coming in for repair due to continuous striking and union monopolizing. Shipping companies are sending their boats to repair yards in Asia, Romania, Italy and former Yugoslavia were repair costs are also substantially lower. Over recent years hundreds of</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">steel workers and mechanics have lost their jobs. Since the start of the economic crisis Perama has suffered further losses and is in danger of closing entirely leaving hundreds of workers jobless.</span></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":20}},"siteAlias":"preview.neonsky.app","initialPageUuid":"u8im8s66","isHydra":true,"flyIoSaveTimestamp":1786070398432,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter"
1713:false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":15},{"id":1786070398546,"title":"Centenarians in Ikaria-The Guardian","url":"/centenarians-in-ikaria-the-guardian","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070388954,"siteId":"q7u17cy4","pageId":"hye6o2kv","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"centenarians-in-ikaria-the-guardian","normalizedName":"centenarians-in-ikaria-the-guardian","galleryOptions":{"manualCollectionName":"GUID=4bd5ec0aa36d9","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"hye6o2kv","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=4bd5ec0aa36d9":{"isClassicCollection":true,"guid":"GUID=4bd5ec0aa36d9","metadata":[],"totalItems":0}},"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":16},{"id":1786070398547,"title":"Syrian Refugees in Bulgaria-The New Statesman","url":"/syrian-refugees-in-bulgaria-the-new-statesman","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1786070388954,"siteId":"q7u17cy4","pageId":"979tezg7","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"syrian-refugees-in-bulgaria-the-new-statesman","normalizedName":"syrian-refugees-in-bulgaria-the-new-statesman","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","
1713importedFromClassic":true,"siteId":"q7u17cy4","pageId":"979tezg7","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":20}},"siteAlias":"preview.neonsky.app","initialPageUuid":"979tezg7","isHydra":true,"flyIoSaveTimestamp":1786070400459,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":17},{"id":1786070400575,"title":"The Mermaid Madonna","url":"/the-mermaid-madonna","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"q7u17cy4","pageId":"9t7wfroe","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"the-mermaid-madonna","normalizedName":"the-mermaid-madonna","galleryOptions":{"manualCollectionName":"GUID=4bd5ec0aa36d9","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"9t7wfroe","horizontalScrollerText":"<p>The Mermaid Madonna</p><p><br></p><p>Since February 2015, more than 850,000-recorded refugees have crossed over from Turkey onto the 15km stretch of coast on the northern side of the island of Lesbos, in the northeastern Aegean Sea in Greece. This project s titled after the book, The Mermaid Madonna (1955) by the acclaimed Lesbos born writer, Stratis Myrivilis.</p><p><br></p><p>The story takes place in the fishing village of Skala Sikaminias, one of the main entry points for refugees today. It is set in the 1920âs when this region witnessed its first wave of refugees during the forced population exchange between Greece with Turkey on the exact same coastline. At that time over 50,000 Greeks settled on Lesbos, many of which have remained on the north side of the island.</p><p><br></p><p>A majority of the village population today descend directly from refugees from the above mentioned population exchange. Due to the lack of imminent action from the Greek government and the European Union in 2015, the locals were the first to help refugees land safely and provide food and shelter. Their lives were severely disrupted as they were also forced to face the memories of their own family histories. This drove me to start a project driven stylistically by the magical realistic narrative of the text while examining the historical repetition of mass transitory movement on this coastline, and what role social memory plays in the permanence of landscape, questioning how physical environments may be continuous stages for different states of displacement.</p>","jsonCollections":{"GUID=4bd5ec0aa36d9":{"isClassicCollection":true,"guid":"GUID=4bd5ec0aa36d9","metadata":{},"totalItems":0}},"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter"
1713:false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":false,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"siteAlias":"preview.neonsky.app","initialPageUuid":"9t7wfroe","initialPageAlias":"the-mermaid-madonna","timestamp":1786082756750,"filterMenuCollectionNames":"","filterMenuTitles":"","rolloverCollectionNames":"","isPerma":false},"isHomePage":false,"position":19},{"id":1786070400576,"title":"Party Like There is No Tomorrow","url":"/party-like-there-is-no-tomorrow","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"q7u17cy4","pageId":"b0q8nu8h","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"party-like-there-is-no-tomorrow","normalizedName":"party-like-there-is-no-tomorrow","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","importedFromClassic":true,"siteId":"q7u17cy4","pageId":"b0q8nu8h","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">For my own personal coverage of the 2015 elections, my project Party Like there is no Tomorrow, I used the Fuji Intax camera and went to all the political rallies intending to reflect on the surreal atmosphere by producing a puzzle of triptychs. From the hotdog stands to the enamored supporters of both left and right I tried to present a collage of a classic political circus. The election was for me an embodiment of a combination of the tragic and absurd which echoes our historic ancient Greek plays.</span></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":false,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":8}},"siteAlias":"preview.neonsky.app","initialPageUuid":"b0q8nu8h","isHydra":true,"flyIoSaveTimestamp":1786070402767,"startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":true,"layout":{"type":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"}},"isHomePage":false,"position":20},{"id":1786070402885,"title":"Selected Tearsheets","url":"/selected-tearsheets","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"q7u17cy4","pageId":"bmbthak2","classicGuid":"4bd5ec0aa36d9","importSource":"classic-gallery","slug":"selected-tearsheets","normalizedName":"selected-tearsheets","galleryOptions":{"manualCollectionName":"GUID=q7u17cy4","
1713importedFromClassic":true,"siteId":"q7u17cy4","pageId":"bmbthak2","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Tearsheets from International Heralad Tribune, Financial Times Magazine and The New York Times.</span></p>","jsonCollections":{"GUID=q7u17cy4":{"isClassicCollection":true,"guid":"GUID=q7u17cy4","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"bmbthak2","isHydra":true,"flyIoSaveTimestamp":1786070404871,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","itemsPerRow":2,"spacing":0.6,"showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplaySingles":false,"autoplayRandomStart":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"infoTextPlacement":"below","descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","applyOverviewTextStyles":false,"showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionGalleryPicker":"__rollover_pick_blank__","openMultipleLightboxes":false,"batchSize":20,"applyToAllGalleriesCheckbox":false,"layout":{"type":"grid","selection":"grid-columns","columns":3,"spacing":0.6,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"initialPageAlias":"selected-tearsheets","timestamp":1786082797768,"filterMenuCollectionNames":"","filterMenuTitles":"","rolloverCollectionNames":"","isPerma":false},"isHomePage":false,"position":21},{"id":1786070404990,"title":"Biography-Awards","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1786070404990","visible":true,"parentId":null,"siteId":"q7u17cy4","slug":"biography-awards","url":"/biography-awards","pageElements":[{"id":1786070405891,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Eirini Vourloumis (b.1979) is a Greek- Indonesian visual artist, photographer and filmmaker based in Athens, Greece. Vourloumis is a graduate of Parsons School of Design and of the Columbia Graduate School of Journalism in New York. As a visual artist her photographic work has been exhibited in solo shows at EMST, The National Museum of Contemporary Art, the Benaki Museum and the Onassis Cultural Center in Athens. She has also participated in group shows at the Bozar Museum in Brussels, the Sharjah Biennial 19 in Sharjah and the Gropius Bau Museum in Berlin.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Vourloumis has also worked extensively as a photojournalist mostly covering the economic and refugee crises in Greece. Her reportage clients include The New York Times, The Wall Street Journal, The New Yorker, Time, Le Monde and The Guardian, among others. Vourloumis is currently directing her first feature length documentary, funded by ERT, Greek National Broadcaster, and the Greek Film Center.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Her recognitions include Ones to Watch, from the British Journal of Photography, and The New Yorkerâs Emerging Photographers of the Year series. Her photographs have been acquired by EMST, and the Dakis Joannou, DESTE, Foundation for Contemporary Art collection. Her ï¬rst book, In Waiting, was released by the German publisher Hatje Cantz. Vourloumis is a member of the Greek Depression Era collective.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>SOLO EXHIBIITIONS</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2023</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">In the Same Space, The Municipal Art Gallery of Patras</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">In collaboration with the National Contemporary Museum of Art Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">A photographic dialogue with the paintings of Andreas Vourloumis curated by Stamatis Schizakis</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2022</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'In the Same Space',National Contemporary Museum of Art, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">A photographic dilaogue with the paintings of Andreas Vourloumis curated by Stamatis Schizakis</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2019 </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Onassis Cultural Center, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Flash Forward Festival</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">
1713'The Mermaid Madonna'</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">In colloboration with Wu Tsang</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Screening: One Emerging From a Point of VIew</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2018 </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Martin Gropius Bau Museum, Berlin</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'In Waiting'</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">In House with Wu Tsang</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Benaki Art Museum, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'In Waitingâ, Book exhibition</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">The Photographerâs Gallery, London,</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">âIn Waitingâ, screening and book launch</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>GROUP EXHIBITIONS </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2022 </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">National Contemporary Museum of Art, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'Statecraft' curated by Katerina Gregos</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2019</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Benaki Museum, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">The DESTE Foundation and New Museum, New York</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'The Same River Twice' : Contemporary Art in Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Photo Espana Festival, Madrid</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Offland</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'The Mermaid Madonna'</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Sharjah Bienniel 14</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Exhibit: 'The Mermaid Madonna'</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">In colloboration with Wu Tsang</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Screening: One Emerging From a Point of VIew</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'Photography in Crisis', Depression Era, Slought Institute, Philadelphia</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2018</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'Photo-graphé', Benaki Museum, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'The Tourists', Depression Era, Unseen Amsterdam</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2017 </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">âThe Decline of Heroes', Depression Era, Antikenmuseum, Basel,</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2016 </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'On Elite Now', Kypseli Municipal Marketplace, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2015 </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">âDepression Era', Athens Biennale, Bagkeion Hotel, Athens,</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'Greek Stories Revisited', China Lishui Photography Festival, China</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">âDepression Era', Thessaloniki Biennale of Contemporary Art, Greece</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2014</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">âDepression Era', Hungarian Month of Photography, Budapest</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">'Depression Era', Le Mois de la Photo, Paris</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">âDepression Era', Benaki Museum, Athens</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">âLens Culture Emerging Talentsâ, Barcelona</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">âTokyo International Photography Festival Competition 2014â,Tokyo,</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">New York</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">âGreek contemporary Art In Times of Crisisâ</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Bozar Museum of Fine Art- Brussels, Belgium</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">1%: Privilege in a Time of Global Inequality</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Collection of photographers (one image)</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Published by Hatje Cantz edited by Myles Little</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Travelling show</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2018 </b> Samek Art Museum, Lewisburg, USA</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2017 </b> University of VirginiaâCharlottesville Charlottesville, USA</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">fosfeniLAB Padua, Italy</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Médiathèque du Pays de Sierentz Sierentz, France</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Photival Photographic Festival Wellington, New Zealand</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2016 </b> Addis Foto Fest, Addis Ababa, Ethiopia</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Just Another Photo Festival, Varanasi, India</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Chavonnes Battery Museum, Cape Town, South Africa</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">The Eye â International Photography Festival, Aberystwyth, Wales</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Organ Vida Festival Zagreb, Croatia</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Brown University Watson Institute for International and Public Affairs</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Providence, USA</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">AMURAL Festival Brasov, Romania</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">OBSCURA Festival George Town, Malaysia</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Uri-Eichen Gallery, Chicago, USA</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Voies Off :: Arles, Arles, France</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">LOOK3 Festival of the Photograph( projection) Charlottesville,,VA</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Athens Photo Festival Athens, Greece</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Transphotographiques Lilles, France</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Head On Photo Festival Sydney, Australia</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Magazzini Fotografici Naples, Italy</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Gage Gallery at Roosevelt University Chicago, USA</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Balkan Photo Festival Sarajevo, Bosnia and Herzegovina</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Rendez-vous Image Strasbourg, France</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2015 </b> International Photography Festival GuatePhoto, Guatemala City, Guatemal</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Lishui International Photography Festival, Lishui, China</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Lagos Photo (Satellite Exhibit) Lagos, Nigeria</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Fotodoks Festival (Satellite Exhibit), Munich, Germany</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Browse Gallery, Berlin, Germany</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">East Wing Gallery, Dubai, UAE</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Pingyao International Photography Festival, Pingyao, China</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>BOOKS </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">2017 In Waiting, Published by Hatje Cantz ( November)</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">42 illus. 100 pages. Essays by James Estrin and Panos Kokinnias</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">2015 1%: Privilege in a Time of Global Inequality</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Collection of photographers (one image)</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Published by Hatje Cantz edited by Myles Little</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>AWARDS RECOGNITION</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2015 </b><b>British Journal of Photography </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Ones to Watch Issue - 25 Emerging Photographers</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>Tokyo International Photography Festival Competition </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Travelling exhibit of 8 winners Tokyo, NY, Berlin</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2014 </b><b>Duke Documentary Studies Competition</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Finalist</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>Lens Culture </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Emerging Talent</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b> PDN Annual </b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Best imagery of 2014</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b> 2013 </b><b> Emerging Photographer series</b></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Photo booth-New Yorker</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b>2012 </b> American Photography- American Illustration</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Annual Photography Book</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><b> 2006 </b> American Photography - American Illustration</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Annual Photography Book 2006</span></p>","textWidth":100}],"classicGuid":"4bd5ec0aa36d9","importSource":"classic-page","isHomePage":false,"position":22},{"id":1786070404992,"title":"NYTimes Lens Blog Articles","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1786070404992","visible":true,"parentId":null,"siteId":"q7u17cy4","slug":"nytimes-lens-blog-articles","url":"/nytimes-lens-blog-articles","pageElements":[{"id":1786070405892,"type":"text","t
1713itle":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Lens, the New York Times photojournalism blog showcases the finest work in photojournalism and documentary photography from established and emerging photographers. Below are the articles I have written for Lens presenting work from a variety of photographers.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Boogie:</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"mailto:Paolo Woods:http://lens.blogs.nytimes.com/2009/12/11/showcase-91/http://lens.blogs.nytimes.com/2009/11/30/showcase-84/\" target=\"_blank\">http://lens.blogs.nytimes.com/2009/09/15/showcase-51/</a></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Paolo Woods:</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"http://lens.blogs.nytimes.com/2009/12/11/showcase-91/\" target=\"_blank\">http://lens.blogs.nytimes.com/2009/12/11/showcase-91/</a></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"http://lens.blogs.nytimes.com/2009/11/30/showcase-84/\" target=\"_blank\">http://lens.blogs.nytimes.com/2009/11/30/showcase-84/</a></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Alfred Yaghobzadeh:</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"http://lens.blogs.nytimes.com/2009/08/03/behind-9/\" target=\"_blank\">http://lens.blogs.nytimes.com/2009/08/03/behind-9/</a></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Adam Ferguson:</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"http://lens.blogs.nytimes.com/2010/09/02/watching-a-teenage-girl-die-by-mortar/\" target=\"_blank\">http://lens.blogs.nytimes.com/2010/09/02/watching-a-teenage-girl-die-by-mortar/</a></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Maciek Nabrdalik/VII Agency Mentor series:</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"http://lens.blogs.nytimes.com/2010/04/01/showcase-146/\" target=\"_blank\">http://lens.blogs.nytimes.com/2010/04/01/showcase-146/</a></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Agnes Dherbeys/VII Agency Mentor series:</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\"><a href=\"mailto:http://lens.blogs.nytimes.com/2010/04/01/showcase-146/\" target=\"_blank\">http://lens</a><a href=\"http://lens.blogs.nytimes.com/2010/02/24/showcase-129/\" target=\"_blank\">.blogs.nytimes.com/2010/02/24/showcase-129/</a></span></p>","textWidth":100},{"id":1786071407160,"type":"metadata","visible":true,"position":1,"metaTitle":"Untitled","metaDescription":"","metaKeywords":""}],"classicGuid":"4bd5ec0aa36d9","importSource":"classic-page","isHomePage":false,"position":23},{"id":1786070404993,"title":"Contact and current location","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1786070404993","visible":true,"parentId":null,"siteId":"q7u17cy4","slug":"contact-and-current-location","url":"/contact-and-current-location","pageElements":[{"id":1786070405893,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Eirini Vourloumis is available for both photo assignments and freelance editing.</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">Greece mobile: +(30) 6989-8080-80</span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">e-mail: <a href=\"mailto:[email protected]\" target=\"_blank\">[email protected]</a></span></p>\n<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">current location: Athens, Greece</span></p>","textWidth":100},{"id":1786071268641,"type":"metadata","visible":true,"position":1,"metaTitle":"Untitled","metaDescription":"","metaKeywords":""}],"classicGuid":"4bd5ec0aa36d9","importSource":"classic-page","isHomePage":false,"position":24}]; 1714window.galleries = galleries; 1715 1716function getSiteGalleriesArray() { 1717 if (typeof galleries !== 'undefined' && Array.isArray(galleries)) { 1718 return galleries; 1719 } 1720 if (typeof window.galleries !== 'undefined' && Array.isArray(window.galleries)) { 1721 return window.galleries; 1722 } 1723 if (window.GALLERY_DATA_CACHE && 1724 window.GALLERY_DATA_CACHE.initialized && 1725 Array.isArray(window.GALLERY_DATA_CACHE.galleries)) { 1726 return window.GALLERY_DATA_CACHE.galleries; 1727 } 1728 return []; 1729} 1730window.getSiteGalleriesArray = getSiteGalleriesArray; 1731 1732let activeGalleryId = null; 1733let sortableInstance = null; 1734let isEditing = false;
1735let initialPageInfo = {"id":1786070383351,"type":"gallery","pageId":"cwd7vhoc","slug":"book","url":"/book"}; 1736 1737 1738// Store the current menu layout type (must be JSON-encoded â bare sidebar/horizontal is invalid JS) 1739let currentMenuLayout = null; 1740 1741/** True when this document is loaded in the chromeless iframe (?neon_embed=1 or body.neon-page-embed). */ 1742function isNeonPageEmbedShell() { 1743 try { 1744 if (typeof document !== 'undefined' && document.body && document.body.classList.contains('neon-page-embed')) 1745 return true; 1746 var _spEmbed = new URLSearchParams(window.location.search); 1747 return _spEmbed.get('neon_embed') === '1'; 1748 } catch (_eEmbed) { return false; } 1749} 1750 1751/** 1752 * Mirrors "hide menu on this page": in embed shells always hide menus (unless editing), 1753 * otherwise use each gallery/page flag. 1754 */ 1755function shouldApplyMenuHiddenForEmbedOrGallery(gallery) { 1756 var editing = typeof window.isInEditMode === 'function' && window.isInEditMode(); 1757 if (editing) 1758 return !!(gallery && gallery.hideMenuOnPage); 1759 return !!(gallery && gallery.hideMenuOnPage) || isNeonPageEmbedShell(); 1760} 1761 1762function shouldApplyMenuOverlayOnPage(gallery) { 1763 if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) 1764 return false; 1765 return !!(gallery && gallery.overlayMenuOnPage); 1766} 1767 1768function isOverlayMenuChromeActive() { 1769 return document.body.classList.contains('menu-overlay-on-page') 1770 && !document.body.classList.contains('edit-mode-active'); 1771} 1772 1773/** Style Customizer paints sidebar with inline background-color !important; beat that only while overlay is active. */ 1774function syncOverlayMenuInlineChrome() { 1775 var sidebar = document.querySelector('.sidebar'); 1776 var header = document.querySelector('.mobile-header'); 1777 if (isOverlayMenuChromeActive()) { 1778 if (sidebar) { 1779 sidebar.style.setProperty('background-color', 'transparent', 'important'); 1780 sidebar.style.setProperty('background-image', 'none', 'important'); 1781 } 1782 if (header) { 1783 header.style.setProperty('background-color', 'transparent', 'important'); 1784 header.style.setProperty('background-image', 'none', 'important'); 1785 } 1786 return; 1787 } 1788 if (sidebar) { 1789 var menuBg = (getComputedStyle(document.documentElement).getPropertyValue('--menu-background-color') || '').trim() || '#fafafa'; 1790 var hidden = document.body.classList.contains('menu-background-hidden'); 1791 sidebar.style.setProperty('background-color', hidden ? 'transparent' : menuBg, 'important'); 1792 if (hidden) { 1793 sidebar.style.setProperty('background-image', 'none', 'important'); 1794 } else { 1795 sidebar.style.removeProperty('background-image'); 1796 } 1797 } 1798 if (header) { 1799 header.style.removeProperty('background-color'); 1800 header.style.removeProperty('background-image'); 1801 } 1802} 1803 1804function applyMenuOverlayClassForGallery(gallery) { 1805 if (shouldApplyMenuOverlayOnPage(gallery)) { 1806 document.body.classList.add('menu-overlay-on-page'); 1807 } else { 1808 document.body.classList.remove('menu-overlay-on-page'); 1809 } 1810 syncOverlayMenuInlineChrome(); 1811} 1812 1813function hookMenuStyleCustomizerForOverlay() { 1814 var customizer = window.MenuStyleCustomizer; 1815 if (!customizer || customizer.__overlayBgHooked || typeof customizer.applyStyles !== 'function') 1816 return; 1817 var original = customizer.applyStyles.bind(customizer); 1818 customizer.applyStyles = function() { 1819 original.apply(this, arguments); 1820 if (isOverlayMenuChromeActive()) 1821 syncOverlayMenuInlineChrome(); 1822 }; 1823 customizer.__overlayBgHooked = true; 1824} 1825window.applyMenuOverlayClassForGallery = applyMenuOverlayClassForGallery; 1826 1827document.addEventListener('DOMContentLoaded', () => { 1828 if (window.hydraInitialized) { 1829 console.log("Hydra already initialized, skipping"); 1830 return; 1831 } 1832 window.hydraInitialized = true; 1833 document.body.classList.add('hydra-initialized'); 1834 try { 1835 if (sessionStorage.getItem('hydra_refresh_cleared') === '1') { 1836 sessionStorage.removeItem('hydra_refresh_cleared'); 1837 if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.resetInitializationState === 'function') { 1838 window.EnhancedGallerySystem.resetInitializationState(); 1839 } 1840 console.log('[Hydra] Applied ?refresh=1 â gallery framework reset for clean load.'); 1841 } 1842 } catch (e) {} 1843 try { 1844 var neonEmbedParams = new URLSearchParams(window.location.search); 1845 if (neonEmbedParams.get('neon_embed') === '1') { 1846 document.body.classList.add('neon-page-embed', 'menu-hidden-on-page'); 1847 } 1848 } catch (_neonEmbedErr) {} 1849 hookMenuStyleCustomizerForOverlay(); 1850 setTimeout(hookMenuStyleCustomizerForOverlay, 0); 1851 setTimeout(hookMenuStyleCustomizerForOverlay, 500); 1852 if (isOverlayMenuChromeActive()) 1853 syncOverlayMenuInlineChrome(); 1854 window.Parameters = window.Parameters || {}; 1855 if (localStorage.getItem('hydra_is_admin') === 'true') { 1856 document.body.classList.add('hydra-admin', 'hydra-authenticated'); 1857 isAdmin = true; 1858 isAuthenticated = true; 1859 console.log('Restored admin status from localStorage'); 1860 1861 // CRITICAL FIX: Clear all caches except auth tokens for logged-in admins 1862 // This ensures admins always see fresh data when opening new tabs 1863 try { 1864 console.log('Admin detected - clearing all caches except auth tokens...'); 1865 1866 // Save auth tokens first 1867 const savedAuthToken = localStorage.getItem('hydra_auth_token'); 1868 const savedAuthEmail = localStorage.getItem('hydra_auth_email'); 1869 const savedIsAdmin = localStorage.getItem('hydra_is_admin'); 1870 const savedDidToken = localStorage.getItem('hydra_did_token'); 1871 1872 // Clear ALL localStorage 1873 localStorage.clear(); 1874 1875 // Restore auth tokens 1876 if (savedAuthToken) localStorage.setItem('hydra_auth_token', savedAuthToken); 1877 if (savedAuthEmail) localStorage.setItem('hydra_auth_email', savedAuthEmail); 1878 if (savedIsAdmin) localStorage.setItem('hydra_is_admin', savedIsAdmin); 1879 if (savedDidToken) localStorage.setItem('hydra_did_token', savedDidToken); 1880 1881 // Clear ALL sessionStorage (auth tokens are not stored here) 1882 sessionStorage.clear(); 1883 1884 console.log('Cleared all caches except auth tokens for admin user'); 1885 } catch (cacheError) { 1886 console.warn('Error clearing caches on admin page load:', cacheError); 1887 // Don't fail page load if cache clearing fails 1888 } 1889 } 1890 1891 function ensureSidebarElementsAndRender() { 1892 console.log('Ensuring sidebar elements are available before rendering...'); 1893 1894 // Restore from siteConfig when hydrate missed or init invented a default menu 1895 if (window.SidebarManager) { 1896 var current = window.SidebarManager.elements; 1897 var missingOrDefault = !current || current.length === 0 || window.SidebarManager._createdDefaultElements; 1898 if (missingOrDefault && typeof siteConfig !== 'undefined' && siteConfig.sidebarElements && siteConfig.sidebarElements.length > 0) { 1899 console.log('Initializing SidebarManager elements from siteConfig'); 1900 window.SidebarManager.elements = siteConfig.sidebarElements; 1901 window.SidebarManager._elementsReady = true; 1902 window.SidebarManager._createdDefaultElements = false;
1903 if (typeof window.SidebarManager.renderElements === 'function') { 1904 window.SidebarManager.renderElements(); 1905 } 1906 } 1907 } 1908 1909 // Determine layout and render 1910 const currentMenuLayout = getEffectiveMenuLayout(); 1911 1912 if (currentMenuLayout === 'horizontal') { 1913 console.log('Rendering horizontal menu with ensured sidebar elements'); 1914 renderHorizontalMenu(); 1915 } else { 1916 renderGalleries(); 1917 } 1918} 1919 1920 setTimeout(() => { 1921 console.log("Initializing galleries:", galleries.length); 1922 // Collapse folders before first paint so nested items never flash as expanded plain text 1923 alignFolderStates(); 1924 1925 // CHANGED: Use the new function instead of direct rendering 1926 setTimeout(ensureSidebarElementsAndRender, 100); // Small delay to ensure sidebar init 1927 1928 initMobileMenu(); 1929 1930 /** If true, deep-link timeouts below must not call handleURLNavigation again (duplicate loadPage / blink). */ 1931 let bootstrapRouteAttempted = false; 1932 1933 // Handle direct URL navigation 1934 if (initialPageInfo) { 1935 console.log("Loading initial page from direct URL:", initialPageInfo); 1936 1937 // Find the gallery by ID 1938 const gallery = galleries.find(g => g.id === initialPageInfo.id); 1939 1940 if (gallery) { 1941 // Set active gallery ID 1942 activeGalleryId = gallery.id; 1943 1944 if (initialPageInfo.type === 'page') { 1945 console.log("Loading page directly:", gallery.title); 1946 loadPage(gallery.id); 1947 } else { 1948 console.log("Loading gallery directly:", gallery.title); 1949 loadGallery(gallery.id); 1950 } 1951 bootstrapRouteAttempted = true; 1952 } 1953 } else if (window.location.pathname === '/') { 1954 // No specific path, try to load home page 1955 console.log("No specific path, checking for home page"); 1956 if (!loadHomePage() && galleries.length > 0) { 1957 // Default to first gallery if no home page is set 1958 loadGallery(galleries[0].id); 1959 } 1960 bootstrapRouteAttempted = true; 1961 } else if (galleries.length > 0) { 1962 // Path exists but no matching initialPageInfo, try to handle URL path 1963 handleURLNavigation(); 1964 bootstrapRouteAttempted = true; 1965 } 1966 window.__hydraBootstrapRouteAttempted = bootstrapRouteAttempted; 1967 1968 // Log what was rendered 1969 console.log("Menu content:", document.getElementById('galleryTree')?.innerHTML || "Gallery tree not found"); 1970 1971 // Start with edit controls properly set 1972 toggleEditControlsVisibility(isAdmin && document.body.classList.contains('edit-mode-active')); 1973 1974 // Explicitly hide the import classic form too 1975 const importForm = document.getElementById('importClassicForm'); 1976 if (importForm) { 1977 importForm.style.display = 'none'; 1978 importForm.classList.remove('visible'); 1979 } 1980 1981 // Try to restore token from localStorage 1982 if (!didToken) { 1983 didToken = localStorage.getItem('hydra_auth_token'); 1984 if (didToken) { 1985 console.log('Restored auth token from localStorage'); 1986 } 1987 } 1988 1989 // Initialize Magic and check authentication 1990 1991 1992 // Initialize sidebar elements if available 1993 if (window.SidebarManager) { 1994 // Pass the initial elements from server config if available 1995 if (true) { 1996 window.SidebarManager.elements = [{"id":1786066235717,"type":"menu","title":"Menu","visible":true,"position":1},{"id":1786070996551,"type":"image","title":"Image","visible":true,"position":0,"imageUrl":"https://storage.neonsky.app/q7u17cy4/1786071007089_eirini.png","imageKey":"q7u17cy4/1786071007089_eirini.png","imageFilename":"eirini.png","imageType":"image/png","imageWidth":"68%","imageAlignment":"left","altText":"preview.neonsky.app","linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","linkAutoAdvance":false,"hideFromMobileMenu":true,"colorizeWithSiteColors":false}]; 1997 if (window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 1998 window.SidebarManager._elementsReady = true; 1999 window.SidebarManager._createdDefaultElements = false; 2000 } 2001 } 2002 window.SidebarManager.init(); 2003 } 2004 2005 2006 // Initialize metadata from server config (site-wide only â not current page SEO) 2007window.siteMetadata = {"title":"Eirini Vourloumis ","description":"Eirini Vourloumis is available for both photo assignments and freelance editing.","googleAnalytics":"","noIndex":false,"favicon":"","ogImageUrl":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","customPublicSiteEnabled":false,"customPublicSiteHtmlR2Key":"","customPublicSiteHtmlFilename":"","schemaJobTitle":"","schemaCity":"","schemaEmail":""}; 2008console.log('Client Initialized site metadata (second init):', window.siteMetadata); 2009console.log('Client Google Analytics from metadataToUse (second init):', ''); 2010console.log('Client Google Analytics in window.siteMetadata (second init):', window.siteMetadata.googleAnalytics); 2011console.log('Client Content Language (second init):', window.siteMetadata.contentLanguage); 2012
2013 // Update copyright footer after metadata is set (important for main domain) 2014 if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') { 2015 window.SidebarManager.updateMetadataFooter(); 2016 } 2017 2018 2019 2020 const submenuCheck = document.getElementById('createSubmenu'); 2021 const submenuTitleField = document.getElementById('submenuTitle'); 2022 const submenuTitleGroup = submenuTitleField?.parentElement; 2023 2024 if (submenuCheck && submenuTitleGroup) { 2025 submenuCheck.addEventListener('change', function() { 2026 submenuTitleGroup.style.display = this.checked ? 'block' : 'none'; 2027 }); 2028 } 2029 2030 // Check authentication state after initialization 2031 if (isAuthenticated && isAdmin) { 2032 console.log('Already authenticated and admin, showing edit UI'); 2033 forceShowEditUI(); 2034 } else if (didToken) { 2035 console.log('Have token, checking admin status'); 2036 checkAuth(); 2037 } 2038 }, 100); // Small delay to ensure everything is loaded 2039 2040 setTimeout(() => { 2041 if (window.location.pathname === '/' || 2042 window.location.pathname === '/_manage' || window.location.pathname === '/_manage/' || 2043 (window.location.hostname === 'preview.neonsky.app' && 2044 window.location.pathname.split('/').length <= 2)) { 2045 // Root URL or preview URL with only GUID - check for home page 2046 // Skip if bootstrap already routed (themed sites boot slower and would hit a duplicate load) 2047 if (!initialPageInfo && !window.__hydraBootstrapRouteAttempted) { 2048 loadHomePage(); 2049 } 2050 } 2051 }, 500); 2052 2053 // Dedicated editor URL: open edit tools or OTP login without requiring + key 2054 setTimeout(function hydraManageEditorEntry() { 2055 var p = window.location.pathname; 2056 if (p !== '/_manage' && p !== '/_manage/') return; 2057 try { 2058 if (sessionStorage.getItem('hydra_logout_cleared') === '1') { 2059 sessionStorage.removeItem('hydra_logout_cleared'); 2060 console.log('[Hydra] Logout link cleared auth; skipping auto-login on /_manage.'); 2061 return; 2062 } 2063 } catch (e) {} 2064 if (document.body.classList.contains('edit-mode-active')) return; 2065 try { 2066 var token = window.didToken || localStorage.getItem('hydra_auth_token'); 2067 var admin = window.isAdmin === true || localStorage.getItem('hydra_is_admin') === 'true'; 2068 if (token && admin) { 2069 if (typeof window.toggleEditMode === 'function') window.toggleEditMode(); 2070 return; 2071 } 2072 if (token && typeof checkAdminStatus === 'function') { 2073 checkAdminStatus(token).then(function() { 2074 if (window.isAdmin && typeof window.toggleEditMode === 'function' && !document.body.classList.contains('edit-mode-active')) { 2075 window.toggleEditMode(); 2076 } 2077 }).catch(function() {}); 2078 return; 2079 } 2080 if (typeof startOTPLogin === 'function') { 2081 var pr = startOTPLogin(); 2082 if (pr && typeof pr.catch === 'function') pr.catch(function() {}); 2083 } 2084 } catch (e) { 2085 console.error('hydraManageEditorEntry:', e); 2086 } 2087 }, 2200); 2088}); 2089 2090/** 2091 * Accessible name for header/sidebar brand images (no visual change). 2092 * Prefers element title or site metadata over generic "Image" / filenames. 2093 */ 2094function resolveBrandLogoAlt(element) { 2095 if (window.SidebarManager && typeof window.SidebarManager._resolveBrandImageAlt === 'function') { 2096 return window.SidebarManager._resolveBrandImageAlt(element || {}); 2097 } 2098 const siteTitle = 2099 (window.siteMetadata && String(window.siteMetadata.title || '').trim()) || 2100 (document.title && document.title.includes('|') ? document.title.split('|').pop().trim() : '') || 2101 String(location.hostname || '').replace(/^www\./i, ''); 2102 const title = element && element.title ? String(element.title).trim() : ''; 2103 if (title && !/^image(\s*\d*)?$/i.test(title)) return title; 2104 return siteTitle || 'Site logo'; 2105} 2106 2107/** 2108 * Gets the first visible sidebar element (image or text) to be used as a logo or title. 2109 * @returns {object|null} The first suitable sidebar element or null. 2110 */ 2111 2112function getFirstSidebarElementForHeader() { 2113 if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 2114 const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible 2115 if (visibleElements.length > 0) { 2116 const sortedElements = visibleElements.sort((a, b) => a.position - b.position); 2117 2118 // Debug: Log all element types found 2119 const elementTypes = sortedElements.map(el => ({ type: el.type, id: el.id, title: el.title })); 2120 console.log('[getFirstSidebarElementForHeader] Visible elements found:', elementTypes); 2121 2122 for (let el of sortedElements) { 2123 if (el.type === 'image' || el.type === 'text') { // Filter 2: type 2124 console.log('[getFirstSidebarElementForHeader] Found matching element:', { type: el.type, id: el.id, title: el.title, linkType: el.linkType, linkUrl: el.linkUrl, linkPageId: el.linkPageId }); 2125 return el; 2126 } 2127 } 2128 // If loop finishes, no visible image or text found among visible elements 2129 console.log('[getFirstSidebarElementForHeader] Found visible elements, but none were type image/text. Element types found:', elementTypes.map(e => e.type)); 2130 return null; 2131 } else { 2132 console.log('[getFirstSidebarElementForHeader] No elements with visible !== false found in SidebarManager.elements. Total elements:', window.SidebarManager.elements.length); 2133 return null; 2134 } 2135 } 2136 console.log('[getFirstSidebarElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.'); 2137 return null; 2138} 2139 2140/** 2141 * Gets the first visible social icon element from sidebar elements. 2142 * @returns {object|null} The first suitable social element or null. 2143 */ 2144function getFirstSocialElementForHeader() { 2145 if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 2146 const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible 2147 if (visibleElements.length > 0) { 2148 const sortedElements = visibleElements.sort((a, b) => a.position - b.position); 2149 for (let el of sortedElements) { 2150 if (el.type === 'social') { // Filter for social type 2151 return el; 2152 } 2153 } 2154 // If loop finishes, no visible social element found 2155 console.log('[getFirstSocialElementForHeader] Found visible elements, but none were type social.'); 2156 return null; 2157 } else { 2158 console.log('[getFirstSocialElementForHeader] No elements with visible !== false found in SidebarManager.elements.'); 2159 return null; 2160 } 2161 } 2162 console.log('[getFirstSocialElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.'); 2163 return null; 2164} 2165 2166function isSpaceColonySiteTheme() { 2167 const themeId = document.body && document.body.dataset.siteTheme; 2168 return themeId === 'space-colony'; 2169} 2170 2171/** 2172 * Apply horizontal bar logo placement (inline vs stacked) from Menu Style > Bar logo & menu. 2173 * Sets body.horizontal-header-logo-stacked and flex on .logo-menu-container; updates --mobile-header-height when stacked. 2174 */ 2175function applyHorizontalLogoMenuLayoutStyles() { 2176 const sc = window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings; 2177 const mode = (sc && sc.horizontalHeaderLogoLayout) || 'inline'; 2178 const isSpaceColonyTheme = isSpaceColonySiteTheme(); 2179 document.body.classList.toggle('horizontal-header-logo-stacked', mode === 'stacked'); 2180 const w = document.querySelector('.logo-menu-container'); 2181 if (w) { 2182 const isMobile = window.innerWidth <= 768; 2183 w.style.display = 'flex'; 2184 w.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : 'center'; 2185 if (mode === 'stacked') { 2186 w.style.flexDirection = 'column'; 2187 w.style.gap = isMobile ? '8px' : '12px'; 2188 w.style.width = '100%'; 2189 w.style.boxSizing = 'border-box';
2190 w.style.flexGrow = isMobile ? '0' : '1'; 2191 w.style.minWidth = '0'; 2192 w.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : 'flex-start'; 2193 } else { 2194 w.style.flexDirection = 'row'; 2195 w.style.gap = '20px'; 2196 w.style.width = ''; 2197 w.style.boxSizing = ''; 2198 w.style.minWidth = ''; 2199 if (isMobile) { 2200 w.style.flexGrow = '0'; 2201 w.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center'; 2202 } else { 2203 w.style.flexGrow = ''; 2204 w.style.justifyContent = ''; 2205 } 2206 } 2207 } 2208 const mhc = document.querySelector('.mobile-header-content'); 2209 if (mhc) { 2210 const isMobile = window.innerWidth <= 768; 2211 if (isSpaceColonyTheme && !isMobile) { 2212 mhc.style.alignItems = 'flex-end'; 2213 mhc.style.flexGrow = '0'; 2214 mhc.style.width = ''; 2215 } else if (isSpaceColonyTheme && isMobile) { 2216 mhc.style.alignItems = 'flex-start'; 2217 mhc.style.justifyContent = 'flex-start'; 2218 mhc.style.flexGrow = ''; 2219 mhc.style.width = '100%'; 2220 } else if (!isMobile && mode === 'stacked') { 2221 mhc.style.alignItems = 'flex-start'; 2222 mhc.style.flexGrow = ''; 2223 mhc.style.width = ''; 2224 } else { 2225 mhc.style.alignItems = ''; 2226 mhc.style.flexGrow = ''; 2227 mhc.style.width = ''; 2228 } 2229 } 2230 if (typeof window.syncContentMarginToHorizontalHeader === 'function') { 2231 window.syncContentMarginToHorizontalHeader(); 2232 } 2233} 2234window.applyHorizontalLogoMenuLayoutStyles = applyHorizontalLogoMenuLayoutStyles; 2235 2236function parseHorizontalHeaderHeightPx(value, fallback) { 2237 if (value === null || value === undefined) { 2238 return fallback; 2239 } 2240 const parsed = parseInt(String(value).trim(), 10); 2241 if (Number.isNaN(parsed) || parsed < 1) { 2242 return fallback; 2243 } 2244 return parsed; 2245} 2246 2247function getUserHorizontalHeaderHeightPx() { 2248 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 2249 const fromSetting = parseHorizontalHeaderHeightPx(window.MenuStyleCustomizer.settings.mobileHeaderHeight, 0); 2250 if (fromSetting > 0) { 2251 return fromSetting; 2252 } 2253 } 2254 const fromVar = getComputedStyle(document.documentElement).getPropertyValue('--top-bar-height-setting').trim(); 2255 const parsedVar = parseHorizontalHeaderHeightPx(fromVar, 0); 2256 if (parsedVar > 0) { 2257 return parsedVar; 2258 } 2259 const themeMin = getComputedStyle(document.documentElement).getPropertyValue('--st-header-height').trim(); 2260 const parsedTheme = parseHorizontalHeaderHeightPx(themeMin, 0); 2261 if (parsedTheme > 0) { 2262 return parsedTheme; 2263 } 2264 return 60; 2265} 2266 2267function setHorizontalHeaderHeightVars(headerPx, contentMarginPx) { 2268 const headerVal = headerPx + 'px'; 2269 const marginVal = contentMarginPx + 'px'; 2270 const currentHeader = getComputedStyle(document.documentElement).getPropertyValue('--mobile-header-height').trim(); 2271 const currentMargin = getComputedStyle(document.documentElement).getPropertyValue('--content-margin-top').trim(); 2272 if (currentHeader === headerVal && currentMargin === marginVal) { 2273 return; 2274 } 2275 document.documentElement.style.setProperty('--mobile-header-height', headerVal); 2276 document.documentElement.style.setProperty('--content-margin-top', marginVal); 2277} 2278 2279function measureHorizontalHeaderNaturalHeight(header) { 2280 const el = header; 2281 const prevHeight = el.style.getPropertyValue('height'); 2282 const prevMinHeight = el.style.getPropertyValue('min-height'); 2283 /* Beat CSS min-height: var(--mobile-header-height) !important so a prior wrap 2284 doesnât floor the measurement when the viewport widens back to one row. */ 2285 el.style.setProperty('height', 'auto', 'important'); 2286 el.style.setProperty('min-height', '0', 'important'); 2287 const measured = Math.ceil(el.getBoundingClientRect().height); 2288 if (prevHeight) { 2289 el.style.setProperty('height', prevHeight); 2290 } else { 2291 el.style.removeProperty('height'); 2292 } 2293 if (prevMinHeight) { 2294 el.style.setProperty('min-height', prevMinHeight); 2295 } else { 2296 el.style.removeProperty('min-height'); 2297 } 2298 return measured; 2299} 2300 2301function applyHorizontalHeaderContentInset() { 2302 if (!document.body.classList.contains('menu-layout-horizontal')) { 2303 return; 2304 } 2305 if (window._horizontalHeaderInsetApplying) { 2306 return; 2307 } 2308 const header = document.querySelector('.mobile-header'); 2309 if (!header) { 2310 return; 2311 } 2312 2313 window._horizontalHeaderInsetApplying = true; 2314 try { 2315 const userH = getUserHorizontalHeaderHeightPx();
2316 /* Keep logo-cap var as a real px height. Sites can save mobileHeaderHeight as 0vh; 2317 leaving that on --top-bar-height-setting makes calc(var(...) - 8px) â 0 and hides the logo. */ 2318 document.documentElement.style.setProperty('--top-bar-height-setting', userH + 'px'); 2319 const hasExpandedMenu = header.querySelector('.horizontal-menu-item.expanded, .horizontal-menu-item.submenu-open'); 2320 const isStackedLogo = document.body.classList.contains('horizontal-header-logo-stacked'); 2321 const isPhone = window.innerWidth <= 768; 2322 2323 /* 2324 * Phone hamburger chrome: fixed Style Customizer height (link row is hidden). 2325 * Desktop inline may wrap beside the logo â measure like stacked so the bar can grow. 2326 * Stacked / open submenu: always measure. 2327 */ 2328 if (!hasExpandedMenu && !isStackedLogo && isPhone) { 2329 setHorizontalHeaderHeightVars(userH, userH); 2330 return; 2331 } 2332 2333 /* Desktop inline (wrap-capable), stacked logo, or open submenu */ 2334 const measured = measureHorizontalHeaderNaturalHeight(header); 2335 if (!measured || measured < 1) { 2336 setHorizontalHeaderHeightVars(userH, userH); 2337 return; 2338 } 2339 2340 let maxH = Math.max(200, Math.round(window.innerHeight * 0.5)); 2341 const clamped = Math.min(Math.max(measured, userH), maxH); 2342 /* Extra content margin only when bar grew past the user setting (wrapped / stacked / open menu) */ 2343 const contentMargin = clamped > userH ? clamped + 8 : clamped; 2344 setHorizontalHeaderHeightVars(clamped, contentMargin); 2345 } finally { 2346 window._horizontalHeaderInsetApplying = false; 2347 } 2348} 2349 2350function ensureHorizontalHeaderHeightObserver() { 2351 if (typeof ResizeObserver === 'undefined') { 2352 return; 2353 } 2354 const header = document.querySelector('.mobile-header'); 2355 if (!header) { 2356 return; 2357 } 2358 if (window._horizontalHeaderResizeObserver) { 2359 window._horizontalHeaderResizeObserver.disconnect(); 2360 } 2361 window._horizontalHeaderResizeObserver = new ResizeObserver(function() { 2362 applyHorizontalHeaderContentInset(); 2363 }); 2364 window._horizontalHeaderResizeObserver.observe(header); 2365} 2366 2367function syncContentMarginToHorizontalHeader() { 2368 if (!document.body.classList.contains('menu-layout-horizontal')) { 2369 return; 2370 } 2371 requestAnimationFrame(function() { 2372 requestAnimationFrame(function() { 2373 applyHorizontalHeaderContentInset(); 2374 ensureHorizontalHeaderHeightObserver(); 2375 }); 2376 }); 2377} 2378window.syncContentMarginToHorizontalHeader = syncContentMarginToHorizontalHeader; 2379window.applyHorizontalHeaderContentInset = applyHorizontalHeaderContentInset; 2380 2381function getEffectiveMenuLayout() { 2382 if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.getEffectiveMenuLayout === 'function') { 2383 return window.MenuStyleCustomizer.getEffectiveMenuLayout(); 2384 } 2385 if (document.body.classList.contains('menu-layout-horizontal')) return 'horizontal'; 2386 if (document.body.classList.contains('menu-layout-top')) return 'top'; 2387 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 2388 return window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; 2389 } 2390 return 'sidebar'; 2391} 2392 2393function ensureCorrectLayoutApplied() { 2394 // Check if MenuStyleCustomizer and its necessary parts exist 2395 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') { 2396 // Prefer in-editor settings (e.g. Style History preview) over theme/body heuristics 2397 const fromSettings = window.MenuStyleCustomizer.settings.menuLayout; 2398 const currentLayout = fromSettings || getEffectiveMenuLayout(); 2399 console.log(`Ensuring layout is applied: ${currentLayout}`); 2400 2401 // Apply the layout styles (CSS classes, display properties) 2402 window.MenuStyleCustomizer._applyMenuLayout(currentLayout); 2403 2404 // --- Force Header Structure Rebuild for Horizontal Layout --- 2405 if (currentLayout === 'horizontal') { 2406 const mobileHeader = document.querySelector('.mobile-header'); 2407 if (mobileHeader) { 2408 let mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content'); 2409 // Ensure the main content container exists 2410 if (!mobileHeaderContent) { 2411 mobileHeaderContent = document.createElement('div'); 2412 mobileHeaderContent.className = 'mobile-header-content'; 2413 const hamburgerBtn = mobileHeader.querySelector('.hamburger-btn'); 2414 if (hamburgerBtn) { 2415 mobileHeader.insertBefore(mobileHeaderContent, hamburgerBtn); 2416 } else { 2417 mobileHeader.appendChild(mobileHeaderContent); 2418 } 2419 } 2420 // Ensure specific sub-containers exist, creating them if necessary 2421 // This guarantees the structure is present before renderHorizontalMenu fills them. 2422 if (!mobileHeaderContent.querySelector('.horizontal-header-logo')) { 2423 const logoContainer = document.createElement('div'); 2424 logoContainer.className = 'horizontal-header-logo'; 2425 mobileHeaderContent.insertBefore(logoContainer, mobileHeaderContent.firstChild); 2426 } 2427 if (!mobileHeaderContent.querySelector('.horizontal-menu
2427-container')) { 2428 const menuContainer = document.createElement('div'); 2429 menuContainer.className = 'horizontal-menu-container'; 2430 const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo'); 2431 if (logoContainer) { 2432 logoContainer.insertAdjacentElement('afterend', menuContainer); 2433 } else { // Fallback if logo container wasn't found/created 2434 mobileHeaderContent.insertBefore(menuContainer, mobileHeaderContent.firstChild); 2435 } 2436 } 2437 // Apply flexbox styling to position social container at far right 2438 mobileHeaderContent.style.display = 'flex'; 2439 const isSpaceColonyTheme = isSpaceColonySiteTheme(); 2440 const isMobile = window.innerWidth <= 768; 2441 mobileHeaderContent.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : (isSpaceColonyTheme && isMobile ? 'flex-start' : 'center'); 2442 mobileHeaderContent.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : (isSpaceColonyTheme ? 'flex-start' : 'space-between'); 2443 if (isSpaceColonyTheme && !isMobile) { 2444 mobileHeaderContent.style.flexGrow = '0'; 2445 mobileHeaderContent.style.width = ''; 2446 } else { 2447 mobileHeaderContent.style.flexGrow = ''; 2448 mobileHeaderContent.style.width = '100%'; 2449 } 2450 2451 // Create a flex container for logo and menu items 2452 const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container'); 2453 if (!logoMenuContainer) { 2454 console.log('[ensureCorrectLayoutApplied] Creating logo-menu wrapper...'); 2455 const logoMenuWrapper = document.createElement('div'); 2456 logoMenuWrapper.className = 'logo-menu-container'; 2457 // Move logo and menu into the wrapper 2458 const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo'); 2459 const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container'); 2460 2461 console.log('[ensureCorrectLayoutApplied] Found logo container:', !!logoContainer); 2462 console.log('[ensureCorrectLayoutApplied] Found menu container:', !!menuContainer); 2463 2464 if (logoContainer && menuContainer) { 2465 // Insert wrapper before the logo 2466 mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer); 2467 // Move both containers into the wrapper 2468 logoMenuWrapper.appendChild(logoContainer); 2469 logoMenuWrapper.appendChild(menuContainer); 2470 console.log('[ensureCorrectLayoutApplied] Successfully moved logo and menu into wrapper'); 2471 } else { 2472 console.warn('[ensureCorrectLayoutApplied] Missing logo or menu container, cannot create wrapper'); 2473 } 2474 } else { 2475 console.log('[ensureCorrectLayoutApplied] Logo-menu wrapper already exists'); 2476 } 2477 if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') { 2478 applyHorizontalLogoMenuLayoutStyles(); 2479 } 2480 2481 // Add social icons container for horizontal layout (AFTER logo-menu wrapper is created) 2482 if (!mobileHeaderContent.querySelector('.horizontal-social-container')) { 2483 const socialContainer = document.createElement('div'); 2484 socialContainer.className = 'horizontal-social-container'; 2485 // Insert at the end (far right) of the mobile header content 2486 mobileHeaderContent.appendChild(socialContainer); 2487 } 2488 } 2489 } 2490 // --- End Header Structure Rebuild --- 2491 2492 2493 // Re-render the correct menu structure based on the applied layout 2494 if (currentLayout === 'horizontal') { 2495 // If horizontal layout, render the horizontal menu 2496 if (typeof renderHorizontalMenu === 'function') { 2497 renderHorizontalMenu(); 2498 } else { 2499 console.warn("renderHorizontalMenu function not found."); 2500 } 2501 2502 // After rendering horizontal menu, update mobile header content 2503 // This ensures mobile header shows logo instead of menu items on mobile 2504 setTimeout(() => { 2505 if (typeof updateMobileHeaderContent === 'function') { 2506 updateMobileHeaderContent(); 2507 } 2508 }, 100); 2509 } else { 2510 // For sidebar or top layouts, render the standard gallery tree 2511 if (typeof renderGalleries === 'function') { 2512 renderGalleries(); 2513 } else { 2514 console.warn("renderGalleries function not found."); 2515 } 2516 2517 // Update mobile header content for other layouts too 2518 setTimeout(() => { 2519 if (typeof updateMobileHeaderContent === 'function') { 2520 updateMobileHeaderContent(); 2521 } 2522 }, 100); 2523 } 2524 } else { 2525 // Log a warning if the necessary components aren't available 2526 console.warn("MenuStyleCustomizer or its methods not available to re-apply layout."); 2527 } 2528} 2529 2530function applyLayoutStylesOnly() { 2531 // Check if MenuStyleCustomizer and its necessary parts exist 2532 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') { 2533 // Get the current layout setting, default to 'sidebar' if not set 2534 const currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; 2535 console.log(`APPLY STYLES: Applying styles for layout: ${currentLayout}`); 2536 // Apply the layout styles (CSS classes, display properties) 2537 window.MenuStyleCustomizer._applyMenuLayout(currentLayout); // This function should ONLY set body classes/styles 2538 } else { 2539 console.warn("APPLY STYLES: MenuStyleCustomizer or its methods not available."); 2540 } 2541} 2542 2543// Add this function to ensure sidebar content for mobile 2544function ensureSidebarContentForMobile() { 2545 const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal'); 2546 const isMobileView = window.innerWidth <= 768; 2547 2548 if (isHorizontalLayout && isMobileView) { 2549 console.log('Ensuring sidebar content for mobile horizontal layout'); 2550 2551 // Make sure the sidebar tree is populated 2552 const galleryTreeContainer = document.getElementById('galleryTree'); 2553 if (galleryTreeContainer && (!galleryTreeContainer.innerHTML || galleryTreeContainer.innerHTML.trim() === '')) { 2554 console.log('Sidebar tree is empty, populating with galleries'); 2555 2556 // Render the galleries in the sidebar tree for mobile navigation 2557 if (typeof renderGalleries === 'function') { 2558 renderGalleries(); 2559 } 2560 } 2561 2562 // Also ensure sidebar elements are rendered 2563 if (window.SidebarManager && typeof window.SidebarManager.renderElements === 'function') { 2564 console.log('Re-rendering sidebar elements for mobile'); 2565 window.SidebarManager.renderElements(); 2566 } 2567 } 2568} 2569 2570function renderTextLogo(container, htmlContentFromSidebarElement) { 2571 if (!container) { 2572 console.error("[RenderTextLogo] Error: Logo container not found."); 2573 return; 2574 } 2575 2576 container.innerHTML = ''; 2577 2578 const raw = (htmlContentFromSidebarElement || '').trim(); 2579 if (!raw) { 2580 container.innerHTML = '<span class="site-title-text">Menu</span>'; 2581 } else if (!/<[a-z][sS]*>/i.test(raw)) { 2582 // Plain text â wrap so theme CSS can target .site-title-text 2583 container.innerHTML = '<span class="site-title-text">' + raw + '</span>'; 2584 } else { 2585 container.innerHTML = raw; 2586 const titleEl = container.querySelector('.site-title-text, h1, h2, p:last-of-type, p:only-of-type'); 2587 if (titleEl && !titleEl.classList.contains('site-title-text')) { 2588 titleEl.classList.add('site-title-text'); 2589 } 2590 } 2591
2592 // Do not force heading/paragraph margins to 0 â menu-styles.css matches 2593 // .sidebar-text rhythm (h1 margin-bottom 0.5em). Zeroing made the bar tighter. 2594 2595 console.log(`[RenderTextLogo] Text logo HTML rendered directly into container.`); 2596} 2597 2598function renderHorizontalMenu() { 2599 console.log("[RenderHorizontalMenu] Function CALLED. Current edit mode:", typeof isEditing !== 'undefined' ? isEditing : 'isEditing_undefined'); 2600 2601 // CRITICAL: Check if we're actually in horizontal layout (theme overlay counts) 2602 const currentLayout = getEffectiveMenuLayout(); 2603 2604 const isHorizontalLayout = currentLayout === 'horizontal'; 2605 console.log("[RenderHorizontalMenu] Current layout:", currentLayout, "isHorizontalLayout:", isHorizontalLayout); 2606 2607 // If not horizontal layout, only handle logo/alignment, then return early 2608 if (!isHorizontalLayout) { 2609 console.log("[RenderHorizontalMenu] Not in horizontal layout, only handling logo/alignment, skipping menu item creation"); 2610 // Still handle logo/alignment for sidebar layout 2611 const mobileHeader = document.querySelector('.mobile-header'); 2612 if (mobileHeader) { 2613 const logoContainer = mobileHeader.querySelector('.horizontal-header-logo'); 2614 if (logoContainer) { 2615 // Handle logo rendering only (code below will handle this) 2616 // But we'll return before creating menu items 2617 } 2618 } 2619 // Return early - don't create horizontal menu items in sidebar layout 2620 return; 2621 } 2622 2623 // Wait for sidebar elements to be available before rendering 2624 if (!window.SidebarManager || !window.SidebarManager.elements || window.SidebarManager.elements.length === 0) { 2625 console.log("[RenderHorizontalMenu] Sidebar elements not yet available, waiting..."); 2626 const waitForSidebarElements = (attempts = 0) => { 2627 const maxAttempts = 20; // Increased attempts for slower loads 2628 if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 2629 console.log("[RenderHorizontalMenu] Sidebar elements now available, proceeding with render."); 2630 renderHorizontalMenu(); 2631 } else if (attempts < maxAttempts) { 2632 const attemptNum = attempts + 1; 2633 setTimeout(() => waitForSidebarElements(attempts + 1), 100); 2634 } else { 2635 console.warn("[RenderHorizontalMenu] Sidebar elements not available after waiting, rendering anyway."); 2636 // Continue with render even if sidebar elements aren't loaded 2637 } 2638 }; 2639 waitForSidebarElements(); 2640 return; 2641 } 2642 2643 const mobileHeader = document.querySelector('.mobile-header'); 2644 if (!mobileHeader) { 2645 console.error("[RenderHorizontalMenu] CRITICAL: .mobile-header element NOT FOUND."); 2646 return; 2647 } 2648 2649 // Apply flexbox styling to position social container at far right 2650 const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content'); 2651 if (mobileHeaderContent) { 2652 mobileHeaderContent.style.display = 'flex'; 2653 const isSpaceColonyTheme = isSpaceColonySiteTheme(); 2654 const isMobile = window.innerWidth <= 768; 2655 mobileHeaderContent.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : (isSpaceColonyTheme && isMobile ? 'flex-start' : 'center'); 2656 mobileHeaderContent.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : (isSpaceColonyTheme ? 'flex-start' : 'space-between'); 2657 if (isSpaceColonyTheme && !isMobile) { 2658 mobileHeaderContent.style.flexGrow = '0'; 2659 mobileHeaderContent.style.width = ''; 2660 } else { 2661 mobileHeaderContent.style.flexGrow = ''; 2662 mobileHeaderContent.style.width = '100%'; 2663 } 2664 2665 // Create a flex container for logo and menu items 2666 const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container'); 2667 if (!logoMenuContainer) { 2668 console.log('[RenderHorizontalMenu] Creating logo-menu wrapper...'); 2669 const logoMenuWrapper = document.createElement('div'); 2670 logoMenuWrapper.className = 'logo-menu-container'; 2671 // Move logo and menu into the wrapper 2672 const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo'); 2673 const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu
2673-container'); 2674 2675 console.log('[RenderHorizontalMenu] Found logo container:', !!logoContainer); 2676 console.log('[RenderHorizontalMenu] Found menu container:', !!menuContainer); 2677 2678 if (logoContainer && menuContainer) { 2679 // Insert wrapper before the logo 2680 mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer); 2681 // Move both containers into the wrapper 2682 logoMenuWrapper.appendChild(logoContainer); 2683 logoMenuWrapper.appendChild(menuContainer); 2684 console.log('[RenderHorizontalMenu] Successfully moved logo and menu into wrapper'); 2685 } else { 2686 console.warn('[RenderHorizontalMenu] Missing logo or menu container, cannot create wrapper'); 2687 } 2688 } else { 2689 console.log('[RenderHorizontalMenu] Logo-menu wrapper already exists'); 2690 } 2691 if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') { 2692 applyHorizontalLogoMenuLayoutStyles(); 2693 } 2694 } 2695 2696 const logoContainer = mobileHeader.querySelector('.horizontal-header-logo'); 2697 if (!logoContainer) { 2698 console.error("[RenderHorizontalMenu] CRITICAL: .horizontal-header-logo container NOT FOUND within .mobile-header."); 2699 return; 2700 } 2701 2702 console.log("[RenderHorizontalMenu] Found .horizontal-header-logo container"); 2703 logoContainer.innerHTML = ''; // Clear existing logo content first 2704 2705 // FIXED: Check if we're on mobile - if so, don't render the horizontal logo 2706 const isMobileView = window.innerWidth <= 768; // Adjust breakpoint as needed 2707 2708 if (isMobileView) { 2709 console.log("[RenderHorizontalMenu] Mobile view detected - skipping horizontal logo rendering"); 2710 // On mobile, the logo will be handled by updateMobileHeaderContent() 2711 } else { 2712 console.log("[RenderHorizontalMenu] Desktop view - rendering horizontal logo"); 2713 2714 // Get the logo element data 2715 let logoElementData = null; 2716 try { 2717 logoElementData = getFirstSidebarElementForHeader(); 2718 console.log('[RenderHorizontalMenu] getFirstSidebarElementForHeader() returned:', JSON.stringify(logoElementData)); 2719 } catch (error) { 2720 console.error('[RenderHorizontalMenu] Error getting sidebar element:', error); 2721 logoElementData = getFirstSidebarElementFallback(); 2722 } 2723 2724 if (logoElementData) { 2725 if (logoElementData.type === 'image' && (logoElementData.imageData || logoElementData.imageUrl)) { 2726 console.log('[RenderHorizontalMenu] Attempting to render IMAGE logo with src:', logoElementData.imageData || logoElementData.imageUrl); 2727 2728 const img = document.createElement('img'); 2729 img.src = logoElementData.imageUrl || logoElementData.imageData; 2730 img.alt = resolveBrandLogoAlt(logoElementData); 2731 img.className = 'sidebar-image'; 2732 2733 // Apply comprehensive styling to ensure visibility 2734 img.style.display = 'block'; 2735 2736 // --- FIX --- 2737 // The line below was removed. It was applying a fixed max-height of 50px, 2738 // overriding your CSS. Removing it allows your stylesheet to take control. 2739 // img.style.maxHeight = '50px'; 2740 2741 img.style.objectFit = 'contain'; 2742 img.style.minWidth = '1px'; 2743 img.style.minHeight = '0'; 2744 2745 let logoDisplayNode = img; 2746 if (logoElementData.colorizeWithSiteColors && 2747 window.SidebarManager && 2748 typeof window.SidebarManager._buildSidebarImageDisplayNode === 'function') { 2749 const buildData = Object.assign({}, logoElementData); 2750 if (!buildData.imageWidth) { 2751 buildData.imageWidth = 'auto'; 2752 } 2753 logoDisplayNode = window.SidebarManager._buildSidebarImageDisplayNode(img, buildData); 2754 } else { 2755 img.style.width = 'auto'; 2756 img.style.visibility = 'visible'; 2757 img.style.opacity = '1'; 2758 } 2759 2760 // Opt-in per-image padding wrapper (no-op when unset; does not alter .horizontal-header-logo CSS) 2761 if (window.SidebarManager && 2762 typeof window.SidebarManager._wrapWithImagePadding === 'function') { 2763 logoDisplayNode = window.SidebarManager._wrapWithImagePadding(logoDisplayNode, logoElementData); 2764 } 2765 2766 // Enhanced event listeners for debugging 2767 img.onload = () => { 2768 console.log('[RenderHorizontalMenu] Image successfully LOADED'); 2769 }; 2770 2771 img.onerror = (error) => { 2772 console.error('[RenderHorizontalMenu] Image FAILED to load:', error); 2773 renderTextLogo(logoContainer, logoElementData.title || 'Logo'); 2774 }; 2775 2776 try { 2777 // Handle linking (similar to sidebar-manager.js) 2778 // Only add links in view mode, not edit mode 2779 // Explicit edit toggle only â Parameters.isInEditor is also true for logged-in admins 2780 const isEditing = document.body.classList.contains('edit-mode-active') || 2781 (typeof window.isInEditMode === 'function' && window.isInEditMode()); 2782 2783 // Debug: Log link properties and element data 2784 console.log('[RenderHorizontalMenu] Full logoElementData:', logoElementData); 2785 console.log('[RenderHorizontalMenu] Image link properties:', { 2786 linkType: logoElementData.linkType, 2787 linkUrl: logoElementData.linkUrl, 2788 linkPageId: logoElementData.linkPageId, 2789 linkTarget: logoElementData.linkTarget, 2790 isEditing: isEditing, 2791 hasLinkType: !!logoElementData.linkType, 2792 linkTypeValue: logoElementData.linkType, 2793 linkTypeNotNone: logoElementData.linkType && logoElementData.linkType !== 'none', 2794 notEditing: !isEditing, 2795 bodyHasEditClass: document.body.classList.contains('edit-mode-active'), 2796 isInEditMode: typeof window.isInEditMode === 'function' ? window.isInEditMode() : 'N/A', 2797 ParametersIsInEditor: window.Parameters ? window.Parameters.isInEditor : 'N/A' 2798 }); 2799 2800 // Check for link - handle undefined, null, empty string, or 'none' 2801 const hasLink = logoElementData.linkType && 2802 logoElementData.linkType !== 'none' && 2803 logoElementData.linkType !== '' && 2804 !isEditing; 2805 2806 console.log('[RenderHorizontalMenu] Has link?', hasLink, 'linkType:', logoElementData.linkType); 2807 2808 if (hasLink) { 2809 const link = document.createElement('a'); 2810 link.target = logoElementData.linkTarget || '_self'; 2811 let finalLinkHref = '#'; 2812 let isValidLink = false;
2813 2814 if (logoElementData.linkType === 'external' && logoElementData.linkUrl) { 2815 finalLinkHref = logoElementData.linkUrl; 2816 if (link.target === '_blank') { 2817 link.rel = 'noopener noreferrer'; 2818 } 2819 isValidLink = true; 2820 } else if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) { 2821 const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []); 2822 const linkPageIdNum = parseInt(logoElementData.linkPageId); 2823 console.log('[RenderHorizontalMenu] Looking for internal page:', { 2824 linkPageId: logoElementData.linkPageId, 2825 linkPageIdNum: linkPageIdNum, 2826 allGalleriesLength: allGalleries ? allGalleries.length : 0, 2827 galleryIds: allGalleries ? allGalleries.map(g => ({ id: g.id, title: g.title })).slice(0, 5) : [] 2828 }); 2829 2830 const targetPage = allGalleries.find(g => g.id === linkPageIdNum); 2831 2832 console.log('[RenderHorizontalMenu] Target page found:', targetPage ? { id: targetPage.id, title: targetPage.title, slug: targetPage.slug, url: targetPage.url } : null); 2833 2834 if (targetPage) { 2835 // Handle preview mode URLs 2836 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 2837 console.log('[RenderHorizontalMenu] Preview mode:', isPreviewMode); 2838 2839 if (isPreviewMode) { 2840 const pathParts = window.location.pathname.split('/').filter(Boolean); 2841 const siteGuid = pathParts[0]; 2842 console.log('[RenderHorizontalMenu] Preview mode path parts:', pathParts, 'siteGuid:', siteGuid, 'targetPage.slug:', targetPage.slug); 2843 2844 if (siteGuid && targetPage.slug) { 2845 finalLinkHref = '/' + siteGuid + '/' + targetPage.slug; 2846 isValidLink = true; 2847 console.log('[RenderHorizontalMenu] Set preview href:', finalLinkHref); 2848 } else if (targetPage.url && targetPage.url.startsWith('/')) { 2849 finalLinkHref = targetPage.url; 2850 isValidLink = true; 2851 console.log('[RenderHorizontalMenu] Set preview href from url:', finalLinkHref); 2852 } 2853 } else { 2854 if (targetPage.slug) { 2855 finalLinkHref = '/' + targetPage.slug; 2856 isValidLink = true; 2857 console.log('[RenderHorizontalMenu] Set non-preview href:', finalLinkHref); 2858 } else if (targetPage.url && targetPage.url.startsWith('/')) { 2859 finalLinkHref = targetPage.url; 2860 isValidLink = true; 2861 console.log('[RenderHorizontalMenu] Set non-preview href from url:', finalLinkHref); 2862 } 2863 } 2864 } else { 2865 console.warn('[RenderHorizontalMenu] Target page not found for linkPageId:', logoElementData.linkPageId); 2866 } 2867 } 2868 2869 if (isValidLink) { 2870 link.href = finalLinkHref; 2871 // Ensure link is clickable 2872 link.style.pointerEvents = 'auto'; 2873 link.style.cursor = 'pointer'; 2874 link.style.textDecoration = 'none'; 2875 link.style.display = 'flex'; 2876 link.style.alignItems = 'center'; 2877 link.style.height = 'auto'; 2878 link.style.maxHeight = '100%'; 2879 link.style.minHeight = '0'; 2880 link.style.lineHeight = '0'; 2881 console.log('[RenderHorizontalMenu] Link created with href:', finalLinkHref, 'target:', link.target, 'styles applied'); 2882 2883 // For internal links, add click handler to use site navigation 2884 if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) { 2885 // Store linkPageId in a variable that won't be affected by closure 2886 const linkPageId = logoElementData.linkPageId; 2887 const linkHref = finalLinkHref; 2888 2889 // Add click handler with both capture and bubble phases to ensure it fires 2890 const clickHandler = (e) => { 2891 console.log('[RenderHorizontalMenu] Link clicked! linkPageId:', linkPageId, 'event:', e); 2892 e.preventDefault(); 2893 e.stopPropagation(); 2894 e.stopImmediatePropagation(); 2895 2896 const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []); 2897 const linkPageIdNum = parseInt(linkPageId); 2898 const targetPage = allGalleries.find(g => g.id === linkPageIdNum); 2899 2900 console.log('[RenderHorizontalMenu] Click handler - targetPage found:', targetPage ? { id: targetPage.id, title: targetPage.title, isPage: targetPage.isPage } : null); 2901 console.log('[RenderHorizontalMenu] Click handler - loadPage function:', typeof window.loadPage, 'loadGallery function:', typeof window.loadGallery); 2902 2903 if (targetPage) { 2904 if (isPageMenuItem(targetPage) && typeof window.loadPage === 'function') { 2905 console.log('[RenderHorizontalMenu] Calling loadPage with id:', targetPage.id); 2906 window.loadPage(targetPage.id); 2907 } else if (typeof window.loadGallery === 'function') { 2908 console.log('[RenderHorizontalMenu] Calling loadGallery with id:', targetPage.id); 2909 window.loadGallery(targetPage.id); 2910 } else { 2911 // Fallback to standard navigation 2912 console.log('[RenderHorizontalMenu] Fallback to window.location.href:', linkHref); 2913 window.location.href = linkHref; 2914 } 2915 } else { 2916 // Fallback to standard navigation if page not found 2917 console.warn('[RenderHorizontalMenu] Target page not found in click handler, using fallback href:', linkHref); 2918 window.location.href = linkHref; 2919 } 2920 2921 return false;
2922 }; 2923 2924 // Add handler in capture phase to catch event early 2925 link.addEventListener('click', clickHandler, true); 2926 // Also add in bubble phase as backup 2927 link.addEventListener('click', clickHandler, false); 2928 2929 // Also add mousedown as backup in case click is being prevented 2930 link.addEventListener('mousedown', (e) => { 2931 if (e.button === 0) { // Left click only 2932 console.log('[RenderHorizontalMenu] MouseDown detected on link, triggering click handler'); 2933 clickHandler(e); 2934 } 2935 }); 2936 2937 console.log('[RenderHorizontalMenu] Click handlers attached to link'); 2938 } 2939 2940 link.appendChild(logoDisplayNode); 2941 logoContainer.appendChild(link); 2942 console.log('[RenderHorizontalMenu] SUCCESSFULLY appended linked image to logoContainer. Link element:', link, 'href:', link.href, 'parent:', logoContainer); 2943 2944 // Verify link is in DOM and clickable 2945 setTimeout(() => { 2946 const verifyLink = logoContainer.querySelector('a'); 2947 if (verifyLink) { 2948 console.log('[RenderHorizontalMenu] Link verification - Found in DOM:', { 2949 href: verifyLink.href, 2950 pointerEvents: window.getComputedStyle(verifyLink).pointerEvents, 2951 cursor: window.getComputedStyle(verifyLink).cursor, 2952 zIndex: window.getComputedStyle(verifyLink).zIndex, 2953 position: window.getComputedStyle(verifyLink).position 2954 }); 2955 } else { 2956 console.warn('[RenderHorizontalMenu] Link verification - Link NOT found in DOM!'); 2957 } 2958 }, 100); 2959 } else { 2960 console.warn('[RenderHorizontalMenu] Link is not valid, appending image without link. isValidLink:', isValidLink); 2961 logoContainer.appendChild(logoDisplayNode); 2962 console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.'); 2963 } 2964 } else { 2965 logoContainer.appendChild(logoDisplayNode); 2966 console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.'); 2967 } 2968 } catch (e) { 2969 console.error('[RenderHorizontalMenu] Error during logoContainer.appendChild(img):', e); 2970 renderTextLogo(logoContainer, logoElementData.title || 'Logo'); 2971 } 2972 2973 } else if (logoElementData.type === 'text' && logoElementData.textContent) { 2974 console.log('[RenderHorizontalMenu] Attempting to render TEXT logo:', logoElementData.textContent); 2975 renderTextLogo(logoContainer, logoElementData.textContent); 2976 2977 } else { 2978 console.log('[RenderHorizontalMenu] Using title as fallback logo'); 2979 renderTextLogo(logoContainer, logoElementData.title || 'Menu'); 2980 } 2981 } else { 2982 console.log("[RenderHorizontalMenu] No logoElementData available. Using default text."); 2983 renderTextLogo(logoContainer, 'Menu'); 2984 } 2985 } 2986 2987 // Render menu items (only on desktop, not on mobile) 2988 const menuContainer = mobileHeader.querySelector('.horizontal-menu-container'); 2989 if (!menuContainer) { 2990 console.error("[RenderHorizontalMenu] .horizontal-menu-container NOT FOUND within .mobile-header."); 2991 } else { 2992 menuContainer.innerHTML = ''; 2993 2994 // Apply alignment class from settings 2995 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 2996 const rawAlign = window.MenuStyleCustomizer.settings.horizontalMenuAlignment || 'left'; 2997 const alignment = ['left', 'center', 'right'].indexOf(rawAlign) >= 0 ? rawAlign : 'left'; 2998 menuContainer.classList.remove('menu-align-left', 'menu-align-right', 'menu-align-center'); 2999 menuContainer.classList.add('menu-align-' + alignment); 3000 console.log('[RenderHorizontalMenu] Applied alignment class: menu-align-'
3000+ alignment); 3001 } 3002 3003 // Only populate menu items on desktop, not on mobile 3004 if (!isMobileView) { 3005 let galleryTree = []; 3006 if (typeof createGalleryTree === 'function' && typeof galleries !== 'undefined') { 3007 galleryTree = createGalleryTree(galleries); 3008 } else { 3009 console.warn("[RenderHorizontalMenu] createGalleryTree function or galleries array is undefined."); 3010 } 3011 3012 galleryTree.forEach(item => { 3013 const menuItemElement = createHorizontalMenuItem(item); 3014 if (menuItemElement) { 3015 menuContainer.appendChild(menuItemElement); 3016 } 3017 }); 3018 if (typeof syncSeoNavA11y === 'function') syncSeoNavA11y(); 3019 } else { 3020 console.log("[RenderHorizontalMenu] Mobile view - skipping horizontal menu item population"); 3021 } 3022 } 3023 3024 // Render social icons (only on desktop, not on mobile) 3025 let socialContainer = mobileHeader.querySelector('.horizontal-social-container'); 3026 if (!socialContainer) { 3027 // Fallback: create the social container if it doesn't exist 3028 console.log("[RenderHorizontalMenu] .horizontal-social-container not found, creating it..."); 3029 const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content'); 3030 if (mobileHeaderContent) { 3031 socialContainer = document.createElement('div'); 3032 socialContainer.className = 'horizontal-social-container'; 3033 mobileHeaderContent.appendChild(socialContainer); 3034 console.log("[RenderHorizontalMenu] Created .horizontal-social-container"); 3035 } else { 3036 console.error("[RenderHorizontalMenu] .mobile-header-content not found, cannot create social container"); 3037 } 3038 } 3039 3040 if (socialContainer) { 3041 socialContainer.innerHTML = ''; 3042 3043 // Only populate social icons on desktop, not on mobile 3044 if (!isMobileView) { 3045 const socialElement = getFirstSocialElementForHeader(); 3046 if (socialElement && socialElement.type === 'social') { 3047 console.log('[RenderHorizontalMenu] Found social element, rendering social icons'); 3048 renderSocialIcons(socialContainer, socialElement); 3049 } else { 3050 console.log('[RenderHorizontalMenu] No social element found or element is not social type'); 3051 // Container remains empty - no impact on layout 3052 } 3053 } else { 3054 console.log("[RenderHorizontalMenu] Mobile view - skipping social icons population"); 3055 } 3056 } 3057 3058 3059 3060 if (typeof updateActiveStatesHorizontal === 'function') { 3061 updateActiveStatesHorizontal(); 3062 } 3063 3064 if (!isMobileView && mobileHeaderContent) { 3065 mobileHeaderContent.querySelectorAll('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title').forEach(function(el) { 3066 if (!el.closest('.horizontal-header-logo')) { 3067 el.remove(); 3068 } 3069 }); 3070 } 3071 3072setTimeout(() => { 3073 ensureSidebarContentForMobile(); 3074}, 100); 3075 3076 if (typeof syncContentMarginToHorizontalHeader === 'function') { 3077 setTimeout(function() { syncContentMarginToHorizontalHeader(); }, 200); 3078 } 3079 3080 console.log("[RenderHorizontalMenu] Function COMPLETED."); 3081} 3082 3083/** 3084 * Renders social icons in the horizontal header container. 3085 * @param {HTMLElement} container - The container to render social icons in 3086 * @param {object} socialElement - The social element data from sidebar 3087 */
3088function renderSocialIcons(container, socialElement) { 3089 console.log('[RenderSocialIcons] Rendering social icons:', socialElement); 3090 3091 try { 3092 // Safety check: ensure we have a valid social element 3093 if (!socialElement || socialElement.type !== 'social') { 3094 console.log('[RenderSocialIcons] Invalid social element - skipping rendering'); 3095 return; 3096 } 3097 3098 /* Placeholders without URLs still need menu color (CSS also sets this). */ 3099 container.style.color = 'var(--menu-color, #333)'; 3100 3101 // Get icon size with proper fallbacks 3102 let iconSize = 30; 3103 if (typeof socialElement.socialIconSize === 'number' && !isNaN(socialElement.socialIconSize)) { 3104 iconSize = socialElement.socialIconSize; 3105 } else if (typeof socialElement.socialIconSize === 'string' && !isNaN(parseInt(socialElement.socialIconSize))) { 3106 iconSize = parseInt(socialElement.socialIconSize); 3107 } 3108 3109 // Render icons directly from element data 3110 if (socialElement.socialIcons && Array.isArray(socialElement.socialIcons)) { 3111 socialElement.socialIcons.forEach(icon => { 3112 // Create icon wrapper (color via CSS --menu-color so placeholders without URLs stay visible) 3113 const iconWrapper = document.createElement('div'); 3114 iconWrapper.className = 'social-icon-wrapper'; 3115 iconWrapper.style.cssText = 'width: ' + iconSize + 'px !important; height: ' + iconSize + 'px !important; display: flex !important; align-items: center !important; justify-content: center !important; color: inherit !important;'; 3116 3117 const applySvgSizing = (host) => { 3118 const svg = host.querySelector('svg'); 3119 if (!svg) return; 3120 svg.style.width = '100%'; 3121 svg.style.height = '100%'; 3122 svg.setAttribute('width', '100%'); 3123 svg.setAttribute('height', '100%'); 3124 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); 3125 svg.setAttribute('aria-hidden', 'true'); 3126 svg.setAttribute('focusable', 'false'); 3127 svg.style.color = 'inherit'; 3128 svg.style.fill = 'currentColor'; 3129 svg.style.stroke = 'currentColor'; 3130 }; 3131 3132 const hasUrl = !!(icon.url && String(icon.url).trim()); 3133 3134 // Add link if URL exists; otherwise still render the glyph (placeholder icons) 3135 if (hasUrl) { 3136 const link = document.createElement('a'); 3137 link.href = icon.url; 3138 link.target = '_blank'; 3139 link.rel = 'noopener noreferrer'; 3140 link.classList.add('horizontal-social-link'); 3141 link.style.cssText = 'display: flex !important; align-items: center !important; justify-content: center !important; width: 100% !important; height: 100% !important; color: inherit !important; text-decoration: none !important;'; 3142 const socialLabel = (icon.alt && String(icon.alt).trim()) 3143 || (window.SidebarManager && typeof window.SidebarManager._socialIconTypeLabel === 'function' 3144 ? window.SidebarManager._socialIconTypeLabel(icon.type) 3145 : (icon.type && icon.type !== 'custom' ? String(icon.type).replace(/_/g, ' ') : '')) 3146 || 'Social link'; 3147 const socialLabelPretty = socialLabel.charAt(0).toUpperCase() + socialLabel.slice(1); 3148 link.setAttribute('aria-label', socialLabelPretty + ' (opens in new tab)'); 3149 3150 if (icon.type === 'custom' && icon.iconUrl) { 3151 // Render uploaded image 3152 const img = document.createElement('img'); 3153 img.src = icon.iconUrl; 3154 img.alt = ''; 3155 img.setAttribute('aria-hidden', 'true'); 3156 img.style.width = '100%'; 3157 img.style.height = '100%'; 3158 img.style.objectFit = 'contain'; 3159 link.appendChild(img); 3160 } else { 3161 // Add SVG - use SidebarManager's method if available, otherwise create basic SVG 3162 if (window.SidebarManager && typeof window.SidebarManager._getSocialIconSvg === 'function') { 3163 link.innerHTML = window.SidebarManager._getSocialIconSvg(icon.type); 3164 } else { 3165 // Fallback: create a simple SVG placeholder 3166 link.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>'; 3167 } 3168 applySvgSizing(link); 3169 } 3170 iconWrapper.appendChild(link); 3171 } else {
3172 // Placeholder: no URL â still paint with menu color via container inheritance 3173 iconWrapper.setAttribute('aria-hidden', 'true'); 3174 if (icon.type === 'custom' && icon.iconUrl) { 3175 const img = document.createElement('img'); 3176 img.src = icon.iconUrl; 3177 img.alt = ''; 3178 img.style.width = '100%'; 3179 img.style.height = '100%'; 3180 img.style.objectFit = 'contain'; 3181 iconWrapper.appendChild(img); 3182 } else { 3183 if (window.SidebarManager && typeof window.SidebarManager._getSocialIconSvg === 'function') { 3184 iconWrapper.innerHTML = window.SidebarManager._getSocialIconSvg(icon.type); 3185 } else { 3186 iconWrapper.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>'; 3187 } 3188 applySvgSizing(iconWrapper); 3189 } 3190 } 3191 3192 container.appendChild(iconWrapper); 3193 console.log('[RenderSocialIcons] Rendered social icon:', icon.type, icon.url); 3194 }); 3195 } else { 3196 console.log('[RenderSocialIcons] No social icons found in element'); 3197 } 3198 } catch (error) { 3199 console.error('[RenderSocialIcons] Error rendering social icons:', error); 3200 } 3201} 3202 3203/** 3204 * Gets the appropriate icon text or class for a social platform. 3205 * @param {string} platform - The social platform name 3206 * @returns {string} The icon text or class 3207 */ 3208function getSocialIconText(platform) { 3209 const iconMap = { 3210 'facebook': 'f', 3211 'twitter': 't', 3212 'instagram': 'i', 3213 'linkedin': 'in', 3214 'youtube': 'yt', 3215 'pinterest': 'p', 3216 'tiktok': 'tt', 3217 'snapchat': 'sc', 3218 'whatsapp': 'wa', 3219 'telegram': 'tg', 3220 'discord': 'd', 3221 'github': 'gh', 3222 'email': '@', 3223 'phone': 'ð', 3224 'website': 'ð' 3225 }; 3226 3227 return iconMap[platform.toLowerCase()] || 'â¢'; 3228} 3229 3230 3231 3232 3233/** 3234 * Creates a single top-level horizontal menu item and its submenu if it has children. 3235 * (UPDATED to handle async loadGallery and pass clickedItemId to a delayed updateActiveStatesHorizontal) 3236 * @param {object} galleryItem - The gallery item data. 3237 * @returns {HTMLElement | null} The created menu item element, or null if not rendered. 3238 */ 3239function createHorizontalMenuItem(galleryItem) { 3240 if (galleryItem.visible === false || galleryItem.isSpacer) { 3241 return null; 3242 } 3243 3244 const menuItem = document.createElement('div'); 3245 menuItem.className = 'horizontal-menu-item'; 3246 menuItem.dataset.id = String(galleryItem.id); 3247 menuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false'); 3248 3249 const hasVisibleChildren = !!(galleryItem.children && galleryItem.children.filter(c => c.visible !== false && !c.isSpacer).length); 3250 const isNavigableParent = isNavigableMenuParent(galleryItem); 3251 let titleEl; 3252 if (hasVisibleChildren && !isNavigableParent) { 3253 titleEl = document.createElement('span'); 3254 titleEl.className = 'horizontal-menu-label'; 3255 titleEl.textContent = galleryItem.title; 3256 menuItem.setAttribute('aria-haspopup', 'true'); 3257 menuItem.setAttribute('aria-expanded', 'false'); 3258 } else { 3259 titleEl = document.createElement('a'); 3260 titleEl.className = 'horizontal-menu-label'; 3261 titleEl.href = typeof getMenuItemHref === 'function' ? getMenuItemHref(galleryItem) : '#'; 3262 titleEl.textContent = galleryItem.title; 3263 if (galleryItem.isExternal) { 3264 titleEl.target = '_blank'; 3265 titleEl.rel = 'noopener noreferrer'; 3266 } else { 3267 titleEl.addEventListener('click', (e) => { e.preventDefault(); }); 3268 } 3269 } 3270 menuItem.appendChild(titleEl); 3271 3272 // Ensure activeGalleryId is treated as a number for comparison during initial render 3273 const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId)); 3274 if (galleryItem.id === currentGlobalActiveId) { 3275 menuItem.classList.add('active'); 3276 } 3277 3278 let leaveTimer; // For mouseleave timeout to close dropdown 3279 3280 if (galleryItem.children && galleryItem.children.length > 0) { 3281 const visibleChildren = galleryItem.children.filter(child => child.visible !== false && !child.isSpacer); 3282 3283 if (visibleChildren.length > 0) { 3284 menuItem.classList.add('has-children'); 3285 if (isNavigableParent) { 3286 menuItem.setAttribute('aria-haspopup', 'true'); 3287 menuItem.setAttribute('aria-expanded', 'false'); 3288 } 3289 3290 const toggleIconContainer = document.createElement(isNavigableParent ? 'button' : 'span'); 3291 toggleIconContainer.className = 'submenu-toggle-horizontal'; 3292 if (isNavigableParent) { 3293 toggleIconContainer.type = 'button'; 3294 toggleIconContainer.setAttribute('aria-label', 'Show submenu for ' + galleryItem.title); 3295 toggleIconContainer.setAttribute('aria-expanded', 'false'); 3296 toggleIconContainer.addEventListener('click', function(event) { 3297 event.preventDefault(); 3298 event.stopPropagation(); 3299 const willExpand = !menuItem.classList.contains('expanded'); 3300 menuItem.classList.toggle('expanded', willExpand); 3301 menuItem.setAttribute('aria-expanded', willExpand ? 'true' : 'false'); 3302 toggleIconContainer.setAttribute('aria-expanded', willExpand ? 'true' : 'false'); 3303 const icon = toggleIconContainer.querySelector('.icon.toggle-icon'); 3304 if (icon) icon.classList.toggle('rotated', willExpand); 3305 }); 3306 } 3307 const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 3308 svgIcon.setAttribute("class", "icon toggle-icon"); 3309 svgIcon.setAttribute("viewBox", "0 0 24 24"); 3310 svgIcon.setAttribute("fill", "none"); 3311 svgIcon.setAttribute("stroke", "currentColor"); 3312 svgIcon.setAttribute("stroke-width", "2"); 3313 const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); 3314 polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow
3315 svgIcon.appendChild(polyline); 3316 toggleIconContainer.appendChild(svgIcon); 3317 menuItem.appendChild(toggleIconContainer); 3318 3319 const subMenu = document.createElement('div'); 3320 subMenu.className = 'horizontal-dropdown'; 3321 const subMenuList = document.createElement('ul'); 3322 subMenuList.className = 'horizontal-dropdown-list'; 3323 3324 visibleChildren.forEach((childItem) => { 3325 const subMenuItemElement = createHorizontalSubMenuItem(childItem); // Recursive call 3326 if (subMenuItemElement) { 3327 subMenuList.appendChild(subMenuItemElement); 3328 } 3329 }); 3330 3331 if (subMenuList.hasChildNodes()) { 3332 subMenu.appendChild(subMenuList); 3333 menuItem.appendChild(subMenu); 3334 3335 menuItem.addEventListener('mouseenter', function() { 3336 clearTimeout(leaveTimer); 3337 document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => { 3338 if (openItem !== this) { 3339 openItem.classList.remove('expanded'); 3340 openItem.setAttribute('aria-expanded', 'false'); 3341 const otherIcon = openItem.querySelector('.icon.toggle-icon'); 3342 if (otherIcon) otherIcon.classList.remove('rotated'); 3343 } 3344 }); 3345 this.classList.add('expanded'); 3346 this.setAttribute('aria-expanded', 'true'); 3347 if (isNavigableParent) toggleIconContainer.setAttribute('aria-expanded', 'true'); 3348 const currentIcon = this.querySelector('.icon.toggle-icon'); 3349 if (currentIcon) currentIcon.classList.add('rotated'); 3350 }); 3351 3352 menuItem.addEventListener('mouseleave', function(event) { 3353 // Check if we're moving to the dropdown or a flyout 3354 const relatedTarget = event.relatedTarget; 3355 const isMovingToDropdown = relatedTarget && ( 3356 relatedTarget.closest('.horizontal-dropdown') || 3357 relatedTarget.classList.contains('horizontal-submenu-item') || 3358 relatedTarget.closest('.horizontal-submenu-item') 3359 ); 3360 const isMovingToFlyout = relatedTarget && ( 3361 relatedTarget.classList.contains('horizontal-nested-dropdown') || 3362 relatedTarget.closest('.horizontal-nested-dropdown') 3363 ); 3364 3365 if (!isMovingToDropdown && !isMovingToFlyout) { 3366 console.log('Main menu item mouseleave - starting timer'); 3367 leaveTimer = setTimeout(() => { 3368 // Check if there's an active flyout - if so, keep parent open 3369 // Use the same multiple detection methods as the submenu handler 3370 const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') || 3371 document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') || 3372 document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])'); 3373 console.log('Main menu timer fired - active flyout:', activeFlyout); 3374 if (!activeFlyout) { 3375 console.log('Main menu closing dropdown - no active flyout'); 3376 this.classList.remove('expanded'); 3377 this.setAttribute('aria-expanded', 'false'); 3378 if (isNavigableParent) toggleIconContainer.setAttribute('aria-expanded', 'false'); 3379 const currentIcon = this.querySelector('.icon.toggle-icon'); 3380 if (currentIcon) currentIcon.classList.remove('rotated'); 3381 } else { 3382 console.log('Main menu keeping dropdown open - flyout is active'); 3383 } 3384 }, 200); 3385 } else { 3386 console.log('Main menu item mouseleave - not starting timer (moving to dropdown or flyout)'); 3387 } 3388 }); 3389 3390 subMenu.addEventListener('mouseenter', function() { 3391 const parentMenuItem = this.closest('.horizontal-menu
3391-item'); 3392 if (parentMenuItem && parentMenuItem.leaveTimer) { 3393 clearTimeout(parentMenuItem.leaveTimer); 3394 parentMenuItem.leaveTimer = null; 3395 } 3396 }); 3397 3398 subMenu.addEventListener('mouseleave', function(event) { 3399 const parentMenuItem = this.closest('.horizontal-menu-item'); 3400 if (parentMenuItem) { 3401 // Check if we're moving to another element within the same dropdown 3402 const relatedTarget = event.relatedTarget; 3403 console.log('SubMenu mouseleave - relatedTarget:', relatedTarget); 3404 console.log('SubMenu mouseleave - relatedTarget classes:', relatedTarget ? relatedTarget.className : 'null'); 3405 3406 // Check if we're moving to another element within the same dropdown 3407 const currentDropdown = this.closest('.horizontal-dropdown') || this.closest('.horizontal-submenu')?.closest('.horizontal-dropdown'); 3408 console.log('Current dropdown found:', !!currentDropdown); 3409 console.log('Current dropdown element:', currentDropdown); 3410 console.log('RelatedTarget:', relatedTarget); 3411 console.log('RelatedTarget closest dropdown:', relatedTarget ? relatedTarget.closest('.horizontal-dropdown') : 'null'); 3412 3413 // Check if relatedTarget is within the same dropdown container 3414 const isMovingWithinDropdown = relatedTarget && currentDropdown && ( 3415 relatedTarget.closest('.horizontal-dropdown') === currentDropdown || 3416 relatedTarget.classList.contains('horizontal-submenu-item') || 3417 relatedTarget.closest('.horizontal-submenu-item') || 3418 currentDropdown.contains(relatedTarget) 3419 ); 3420 3421 // Alternative approach: check if relatedTarget is within the same parent menu item 3422 const parentMenuContainer = this.closest('.horizontal-menu-item'); 3423 const isMovingWithinParent = relatedTarget && parentMenuContainer && parentMenuContainer.contains(relatedTarget); 3424 console.log('Is moving within parent menu item:', isMovingWithinParent); 3425 console.log('Current dropdown contains relatedTarget:', currentDropdown ? currentDropdown.contains(relatedTarget) : 'no dropdown'); 3426 console.log('RelatedTarget closest dropdown === currentDropdown:', relatedTarget && currentDropdown ? relatedTarget.closest('.horizontal-dropdown') === currentDropdown : 'null'); 3427 3428 // Also check if mouse is still within the dropdown bounds (in case relatedTarget is null or outside) 3429 // Get the parent menu item's dropdown container 3430 const parentMenuItem = this.closest('.horizontal-menu-item'); 3431 console.log('Parent menu item found:', !!parentMenuItem); 3432 const dropdownContainer = parentMenuItem ? parentMenuItem.querySelector('.horizontal-dropdown') : null; 3433 console.log('Dropdown container found:', !!dropdownContainer); 3434 if (dropdownContainer) { 3435 console.log('Dropdown container classes:', dropdownContainer.className); 3436 console.log('Dropdown container display:', window.getComputedStyle(dropdownContainer).display); 3437 console.log('Dropdown container position:', window.getComputedStyle(dropdownContainer).position); 3438 } 3439 3440 // Get bounds from the visible dropdown (not the hidden one) 3441 let dropdownRect = { left: 0, right: 0, top: 0, bottom: 0 }; 3442 if (dropdownContainer) { 3443 const computedStyle = window.getComputedStyle(dropdownContainer); 3444 if (computedStyle.display !== 'none') { 3445 dropdownRect = dropdownContainer.getBoundingClientRect(); 3446 } else { 3447 // If dropdown is hidden, try to get bounds from the parent menu item 3448 const parentRect = parentMenuItem.getBoundingClientRect(); 3449 dropdownRect = parentRect; 3450 } 3451 } 3452 const mouseX = event.clientX; 3453 const mouseY = event.clientY; 3454 const isMouseInDropdownBounds = mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && 3455 mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom; 3456
3457 console.log('Dropdown bounds:', { 3458 left: dropdownRect.left, 3459 right: dropdownRect.right, 3460 top: dropdownRect.top, 3461 bottom: dropdownRect.bottom, 3462 mouseX, 3463 mouseY, 3464 mouseInBounds: mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom 3465 }); 3466 3467 // If there's an active flyout, always keep the parent dropdown open 3468 // Check multiple ways to detect active flyouts 3469 const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') || 3470 document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') || 3471 document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])'); 3472 console.log('Active flyout found:', !!activeFlyout); 3473 if (activeFlyout) { 3474 console.log('Active flyout display style:', activeFlyout.style.display); 3475 console.log('Active flyout computed display:', window.getComputedStyle(activeFlyout).display); 3476 console.log('Active flyout visibility style:', activeFlyout.style.visibility); 3477 } 3478 const hasActiveFlyout = !!activeFlyout; 3479 3480 // If the dropdown is hidden (display: none), we can't get accurate bounds 3481 // So let's use a simpler approach - if we're moving to another submenu item, keep open 3482 const isMovingToSubmenuItem = relatedTarget && ( 3483 relatedTarget.classList.contains('horizontal-submenu-item') || 3484 relatedTarget.closest('.horizontal-submenu-item') 3485 ); 3486 console.log('Is moving to submenu item:', isMovingToSubmenuItem); 3487 console.log('RelatedTarget is submenu item:', relatedTarget ? relatedTarget.classList.contains('horizontal-submenu-item') : 'null'); 3488 console.log('RelatedTarget has submenu item ancestor:', relatedTarget ? !!relatedTarget.closest('.horizontal-submenu-item') : 'null'); 3489 3490 // If we're moving to another submenu item within the same dropdown, al
3490ways keep open 3491 // OR if there's an active flyout (regardless of where the mouse is), keep open 3492 // Simple approach: if mouse is within dropdown bounds OR there's an active flyout, keep open 3493 const shouldKeepOpen = isMouseInDropdownBounds || hasActiveFlyout || isMovingWithinDropdown || isMovingToSubmenuItem; 3494 3495 console.log('Is moving within dropdown:', isMovingWithinDropdown); 3496 console.log('Is mouse in dropdown bounds:', isMouseInDropdownBounds); 3497 console.log('Has active flyout:', hasActiveFlyout); 3498 console.log('Should keep open:', shouldKeepOpen); 3499 3500 if (!shouldKeepOpen) { 3501 console.log('SubMenu mouseleave - starting timer (moving outside dropdown)'); 3502 parentMenuItem.leaveTimer = setTimeout(() => { 3503 // Check if there's an active flyout - if so, keep parent open 3504 const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]'); 3505 console.log('Timer fired - active flyout:', activeFlyout); 3506 if (!activeFlyout) { 3507 console.log('Closing parent dropdown - no active flyout'); 3508 parentMenuItem.classList.remove('expanded'); 3509 const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon'); 3510 if (currentIcon) currentIcon.classList.remove('rotated'); 3511 } else { 3512 console.log('Keeping parent dropdown open - flyout is active'); 3513 } 3514 }, 200); 3515 } else { 3516 console.log('SubMenu mouseleave - not starting timer (moving within dropdown)'); 3517 } 3518 } 3519 }); 3520 } else { 3521 menuItem.classList.remove('has-children'); 3522 if (toggleIconContainer.parentNode) { 3523 toggleIconContainer.remove(); 3524 } 3525 } 3526 } 3527 } 3528 3529 // Click listener for items that are direct navigation links (not folders with children) 3530 if (!menuItem.classList.contains('has-children') || isNavigableParent) { 3531 menuItem.addEventListener('click', async (event) => { 3532 if (isNavigableParent && hasVisibleChildren && !titleEl.contains(event.target)) { 3533 return; 3534 } 3535 event.stopPropagation(); 3536 const clickedItemId = galleryItem.id; // Capture the ID of the clicked item 3537 3538 // Check if it's an external URL first 3539 if (galleryItem.isExternal && galleryItem.url) { 3540 window.open(galleryItem.url, '_blank', 'noopener,noreferrer'); 3541 return; 3542 } 3543 3544 if (isPageMenuItem(galleryItem)) { 3545 if (typeof loadPage === 'function') { 3546 loadPage(clickedItemId, event); // loadPage is synchronous 3547 if (typeof updateActiveStatesHorizontal === 'function') { 3548 console.log("Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId); 3549 // Use a minimal delay even for sync operations if page rendering has its own async microtasks 3550 setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50); 3551 } 3552 } 3553 } else { // It's a gallery 3554 if (typeof loadGallery === 'function') { 3555 console.log("Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId); 3556 await loadGallery(clickedItemId, event); // Await the asynchronous loadGallery 3557 console.log("Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay."); 3558 if (typeof updateActiveStatesHorizontal === 'function') { 3559 setTimeout(() => { 3560 console.log("[Delayed Update from Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId); 3561 updateActiveStatesHorizontal(clickedItemId); // Pass the captured clickedItemId 3562 }, 100); // Delay to allow gallery system to settle 3563 } 3564 } 3565 } 3566
3567 // Close any other open dropdowns and flyouts 3568 document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => { 3569 // Don't close the parent if a child within its dropdown was clicked (already handled by submenu click) 3570 if (openItem !== menuItem.closest('.horizontal-menu-item.expanded')) { 3571 openItem.classList.remove('expanded'); 3572 openItem.setAttribute('aria-expanded', 'false'); 3573 const icon = openItem.querySelector('.icon.toggle-icon'); 3574 if (icon) icon.classList.remove('rotated'); 3575 } 3576 }); 3577 3578 // Close all flyouts 3579 document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => { 3580 flyout.style.display = 'none'; 3581 flyout.style.opacity = '0'; 3582 flyout.style.visibility = 'hidden'; 3583 }); 3584 3585 // Remove expanded class from all submenu items 3586 document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((subItem) => { 3587 subItem.classList.remove('expanded'); 3588 }); 3589 }); 3590 } else { 3591 // Keyboard: Enter/Space toggles hover-style dropdown without changing mouse behavior 3592 menuItem.setAttribute('tabindex', '0'); 3593 menuItem.addEventListener('keydown', function(event) { 3594 if (event.key !== 'Enter' && event.key !== ' ') return; 3595 event.preventDefault(); 3596 const isOpen = this.classList.contains('expanded'); 3597 document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => { 3598 if (openItem !== this) { 3599 openItem.classList.remove('expanded'); 3600 openItem.setAttribute('aria-expanded', 'false'); 3601 const otherIcon = openItem.querySelector('.icon.toggle-icon'); 3602 if (otherIcon) otherIcon.classList.remove('rotated'); 3603 } 3604 }); 3605 this.classList.toggle('expanded', !isOpen); 3606 this.setAttribute('aria-expanded', !isOpen ? 'true' : 'false'); 3607 const currentIcon = this.querySelector('.icon.toggle-icon'); 3608 if (currentIcon) currentIcon.classList.toggle('rotated', !isOpen); 3609 }); 3610 } 3611 return menuItem; 3612} 3613 3614/** 3615* Creates a single horizontal submenu item. 3616* (UPDATED to handle nested children and async loadGallery) 3617* @param {object} galleryItem - The gallery item data for the submenu. 3618* @returns {HTMLElement | null} The created submenu item element, or null if not rendered. 3619*/ 3620function createHorizontalSubMenuItem(galleryItem) { 3621 if (galleryItem.visible === false || galleryItem.isSpacer) { 3622 return null; 3623 } 3624 3625 const subMenuItem = document.createElement('li'); 3626 subMenuItem.className = 'horizontal-submenu-item'; 3627 subMenuItem.dataset.id = String(galleryItem.id); 3628 subMenuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false'); 3629 3630 const subVisibleChildren = (galleryItem.children || []).filter(child => child.visible !== false && !child.isSpacer); 3631 const isNavigableParent = isNavigableMenuParent(galleryItem); 3632 let titleSpan; 3633 if (subVisibleChildren.length > 0 && !isNavigableParent) { 3634 titleSpan = document.createElement('span'); 3635 titleSpan.className = 'horizontal-menu-label'; 3636 titleSpan.textContent = galleryItem.title; 3637 } else { 3638 titleSpan = document.createElement('a'); 3639 titleSpan.className = 'horizontal-menu-label'; 3640 titleSpan.href = typeof getMenuItemHref === 'function' ? getMenuItemHref(galleryItem) : '#'; 3641 titleSpan.textContent = galleryItem.title; 3642 if (galleryItem.isExternal) { 3643 titleSpan.target = '_blank'; 3644 titleSpan.rel = 'noopener noreferrer'; 3645 } else { 3646 titleSpan.addEventListener('click', (e) => { e.preventDefault(); }); 3647 } 3648 } 3649 subMenuItem.appendChild(titleSpan); 3650 3651 const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId)); 3652 if (galleryItem.id === currentGlobalActiveId) { 3653 subMenuItem.classList.add('active'); 3654 if (titleSpan.tagName === 'A') titleSpan.setAttribute('aria-current', 'page'); 3655 } 3656 3657 // Check if this submenu item has children (nested sub-sub items) 3658 if (galleryItem.children && galleryItem.children.length > 0) { 3659 const visibleChildren = subVisibleChildren; 3660 3661 if (visibleChildren.length > 0) { 3662 subMenuItem.classList.add('has-children'); 3663 3664 // Add arrow indicator for nested items (same as top-level menu) 3665 const arrowSpan = document.createElement(isNavigableParent ? 'button' : 'span'); 3666 arrowSpan.className = 'submenu-arrow'; 3667 if (isNavigableParent) { 3668 arrowSpan.type = 'button'; 3669 arrowSpan.setAttribute('aria-label', 'Show submenu for ' + galleryItem.title); 3670 arrowSpan.setAttribute('aria-expanded', 'false'); 3671 arrowSpan.addEventListener('click', function(event) { 3672 event.preventDefault(); 3673 event.stopPropagation(); 3674 const isOpen = subMenuItem.classList.contains('expanded'); 3675 if (isOpen) { 3676 closeHorizontalNestedFlyout(subMenuItem); 3677 } else { 3678 openHorizontalNestedFlyout(subMenuItem); 3679 } 3680 }); 3681 } 3682 const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 3683 svgIcon.setAttribute("class", "icon toggle-icon"); 3684 svgIcon.setAttribute("viewBox", "0 0 24 24"); 3685 svgIcon.setAttribute("fill", "none"); 3686 svgIcon.setAttribute("stroke", "currentColor"); 3687 svgIcon.setAttribute("stroke-width", "2"); 3688 const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); 3689 polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow
3690 svgIcon.appendChild(polyline); 3691 arrowSpan.appendChild(svgIcon); 3692 subMenuItem.appendChild(arrowSpan); 3693 3694 // Create nested submenu as a true flyout 3695 const nestedSubMenu = document.createElement('div'); 3696 nestedSubMenu.className = 'horizontal-nested-dropdown'; 3697 3698 // Create a list container for the nested items 3699 const nestedList = document.createElement('ul'); 3700 nestedList.className = 'horizontal-nested-list'; 3701 3702 visibleChildren.forEach((childItem) => { 3703 const nestedSubMenuItem = createHorizontalSubMenuItem(childItem); // Recursive call 3704 if (nestedSubMenuItem) { 3705 nestedList.appendChild(nestedSubMenuItem); 3706 } 3707 }); 3708 3709 nestedSubMenu.appendChild(nestedList); 3710 3711 if (nestedSubMenu.hasChildNodes()) { 3712 // Append to document body for true flyout behavior 3713 document.body.appendChild(nestedSubMenu); 3714 // Store reference to the flyout for cleanup 3715 const flyoutId = 'flyout-' + Date.now() + '-' + Math.random(); 3716 subMenuItem.setAttribute('data-flyout-id', flyoutId); 3717 nestedSubMenu.setAttribute('data-flyout-id', flyoutId); 3718 3719 console.log('Created flyout with ID:', flyoutId, 'for item:', galleryItem.title); // Debug 3720 console.log('Flyout element:', nestedSubMenu); // Debug 3721 3722 // Add hover events for nested submenu 3723 let nestedLeaveTimer; 3724 3725 // Shared open/close used by hover AND keyboard â mouse path unchanged 3726 function openHorizontalNestedFlyout(hostItem) { 3727 if (!hostItem) return; 3728 if (hostItem.nestedLeaveTimer) { 3729 clearTimeout(hostItem.nestedLeaveTimer); 3730 } 3731 const parentMenuItem = hostItem.closest('.horizontal-menu-item'); 3732 if (parentMenuItem && parentMenuItem.leaveTimer) { 3733 clearTimeout(parentMenuItem.leaveTimer); 3734 } 3735 document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((openItem) => { 3736 if (openItem !== hostItem) { 3737 openItem.classList.remove('expanded'); 3738 openItem.setAttribute('aria-expanded', 'false'); 3739 const otherFlyoutId = openItem.getAttribute('data-flyout-id'); 3740 if (otherFlyoutId) { 3741 const otherFlyout = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + otherFlyoutId + '"]'); 3742 if (otherFlyout) { 3743 otherFlyout.style.display = 'none'; 3744 otherFlyout.style.opacity = '0'; 3745 otherFlyout.style.visibility = 'hidden'; 3746 } 3747 } 3748 } 3749 }); 3750 hostItem.classList.add('expanded'); 3751 hostItem.setAttribute('aria-expanded', 'true'); 3752 const hostArrow = hostItem.querySelector('.submenu-arrow'); 3753 if (hostArrow && isNavigableParent) hostArrow.setAttribute('aria-expanded', 'true'); 3754 const hostFlyoutId = hostItem.getAttribute('data-flyout-id'); 3755 if (!hostFlyoutId) return; 3756 const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]'); 3757 if (!nestedDropdown) return; 3758 const rect = hostItem.getBoundingClientRect(); 3759 const parentDropdown = hostItem.closest('.horizontal-dropdown'); 3760 const parentRect = parentDropdown ? parentDropdown.getBoundingClientRect() : rect; 3761 const leftPos = parentRect.right - 1; 3762 const topPos = rect.top; 3763 nestedDropdown.style.left = leftPos + 'px'; 3764 nestedDropdown.style.top = topPos + 'px'; 3765 nestedDropdown.style.display = 'block'; 3766 nestedDropdown.style.opacity = '1'; 3767 nestedDropdown.style.visibility = 'visible'; 3768 nestedDropdown.style.position = 'fixed'; 3769 nestedDropdown.style.zIndex = '1251'; 3770 setTimeout(() => { 3771 const dropdownRect = nestedDropdown.getBoundingClientRect(); 3772 if (dropdownRect.right > window.innerWidth) { 3773 nestedDropdown.style.left = (parentRect.left - dropdownRect.width - 5) + 'px'; 3774 } 3775 }, 10); 3776 } 3777 3778 function closeHorizontalNestedFlyout(hostItem) { 3779 if (!hostItem) return; 3780 hostItem.classList.remove('expanded'); 3781 hostItem.setAttribute('aria-expanded', 'false'); 3782 const hostArrow = hostItem.querySelector('.submenu-arrow'); 3783 if (hostArrow && isNavigableParent) hostArrow.setAttribute('aria-expanded', 'false'); 3784 const hostFlyoutId = hostItem.getAttribute('data-flyout-id'); 3785 if (!hostFlyoutId) return; 3786 const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]'); 3787 if (nestedDropdown) { 3788 nestedDropdown.style.display = 'none'; 3789 nestedDropdown.style.opacity = '0'; 3790 nestedDropdown.style.visibility = 'hidden'; 3791 } 3792 } 3793 3794 // Keyboard opens the same flyout hover shows â attrs on label so mouse click focus stays natural 3795 if (titleSpan && !isNavigableParent) { 3796 titleSpan.setAttribute('tabindex', '0'); 3797 titleSpan.setAttribute('role', 'button'); 3798 titleSpan.setAttribute('aria-haspopup', 'true'); 3799 titleSpan.setAttribute('aria-expanded', 'false'); 3800 titleSpan.addEventListener('keydown', function(event) { 3801 if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowRight') { 3802 event.preventDefault(); 3803 event.stopPropagation(); 3804 openHorizontalNestedFlyout(subMenuItem); 3805 titleSpan.setAttribute('aria-expanded', 'true'); 3806 const hostFlyoutId = subMenuItem.getAttribute('data-flyout-id'); 3807 const nestedDropdown = hostFlyoutId 3808 ? document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]') 3809 : null; 3810 const firstLink = nestedDropdown && nestedDropdown.querySelector('a.horizontal-menu
3810-label'); 3811 if (firstLink && typeof firstLink.focus === 'function') { 3812 try { firstLink.focus(); } catch (e) {} 3813 } 3814 } else if (event.key === 'Escape' || event.key === 'ArrowLeft') { 3815 event.preventDefault(); 3816 event.stopPropagation(); 3817 closeHorizontalNestedFlyout(subMenuItem); 3818 titleSpan.setAttribute('aria-expanded', 'false'); 3819 } 3820 }); 3821 } 3822 3823 subMenuItem.addEventListener('mouseenter', function() { 3824 openHorizontalNestedFlyout(this); 3825 if (titleSpan) titleSpan.setAttribute('aria-expanded', 'true'); 3826 }); 3827 3828 subMenuItem.addEventListener('mouseleave', function(event) { 3829 // Check if we're moving to the flyout 3830 const flyoutId = this.getAttribute('data-flyout-id'); 3831 const relatedTarget = event.relatedTarget; 3832 const isMovingToFlyout = relatedTarget && flyoutId && 3833 relatedTarget.closest('.horizontal-nested-dropdown[data-flyout-id="' + flyoutId + '"]'); 3834 3835 // Also check if we're moving within the dropdown 3836 const parentMenuItem = this.closest('.horizontal-menu-item'); 3837 const isMovingWithinDropdown = relatedTarget && parentMenuItem && parentMenuItem.contains(relatedTarget); 3838 3839 console.log('Submenu item mouseleave - moving to flyout:', isMovingToFlyout); 3840 console.log('Submenu item mouseleave - moving within dropdown:', isMovingWithinDropdown); 3841 3842 if (!isMovingToFlyout && !isMovingWithinDropdown) { 3843 this.nestedLeaveTimer = setTimeout(() => { 3844 closeHorizontalNestedFlyout(this); 3845 if (titleSpan) titleSpan.setAttribute('aria-expanded', 'false'); 3846 3847 // Now that the flyout is closed, allow the parent dropdown to close 3848 const parentMenuItem = this.closest('.horizontal-menu-item'); 3849 if (parentMenuItem) { 3850 setTimeout(() => { 3851 const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]'); 3852 if (!activeFlyout) { 3853 parentMenuItem.classList.remove('expanded'); 3854 const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon'); 3855 if (currentIcon) currentIcon.classList.remove('rotated'); 3856 } 3857 }, 50); 3858 } 3859 }, 200); 3860 } else { 3861 console.log('Submenu item mouseleave - not starting timer (moving to flyout)'); 3862 } 3863 }); 3864 3865 nestedSubMenu.addEventListener('mouseenter', function() { 3866 console.log('Flyout mouseenter - clearing timers'); 3867 clearTimeout(nestedLeaveTimer); 3868 3869 // Clear the parent dropdown's leave timer to keep it open 3870 const flyoutId = this.getAttribute('data-flyout-id'); 3871 if (flyoutId) { 3872 const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]'); 3873 if (parentSubItem) { 3874 const parentMenuItem = parentSubItem.closest('.horizontal-menu-item'); 3875 if (parentMenuItem && parentMenuItem.leaveTimer) { 3876 console.log('Clearing parent menu item leave timer'); 3877 clearTimeout(parentMenuItem.leaveTimer); 3878 parentMenuItem.leaveTimer = null; 3879 } else { 3880 console.log('No parent menu item leave timer to clear'); 3881 } 3882 if (parentSubItem.nestedLeaveTimer) { 3883 console.log('Clearing submenu item nested leave timer'); 3884 clearTimeout(parentSubItem.nestedLeaveTimer); 3885 parentSubItem.nestedLeaveTimer = null; 3886 } 3887 } 3888 } 3889 }); 3890 3891 nestedSubMenu.addEventListener('mouseleave', function() { 3892 const flyoutId = this.getAttribute('data-flyout-id'); 3893 if (flyoutId) { 3894 const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]'); 3895 if (parentSubItem) { 3896 nestedLeaveTimer = setTimeout(() => { 3897 closeHorizontalNestedFlyout(parentSubItem); 3898 const label = parentSubItem.querySelector('.horizontal-menu
3898-label'); 3899 if (label) label.setAttribute('aria-expanded', 'false'); 3900 }, 200); 3901 } 3902 } 3903 }); 3904 } 3905 } 3906 } 3907 3908 // Click listener for navigation (only if no children or if it's a direct link) 3909 if (!subMenuItem.classList.contains('has-children') || isPageMenuItem(galleryItem) || isNavigableParent) { 3910 subMenuItem.addEventListener('click', async (event) => { 3911 if (isNavigableParent && subVisibleChildren.length > 0 && !titleSpan.contains(event.target)) { 3912 return; 3913 } 3914 event.stopPropagation(); 3915 const clickedItemId = galleryItem.id; 3916 3917 // Check if it's an external URL first 3918 if (galleryItem.isExternal && galleryItem.url) { 3919 window.open(galleryItem.url, '_blank', 'noopener,noreferrer'); 3920 return; 3921 } 3922 3923 if (isPageMenuItem(galleryItem)) { 3924 if (typeof loadPage === 'function') { 3925 loadPage(clickedItemId, event); 3926 if (typeof updateActiveStatesHorizontal === 'function') { 3927 console.log("SubMenu Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId); 3928 setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50); 3929 } 3930 } 3931 } else { // It's a gallery 3932 if (typeof loadGallery === 'function') { 3933 console.log("SubMenu Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId); 3934 await loadGallery(clickedItemId, event); 3935 console.log("SubMenu Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay."); 3936 if (typeof updateActiveStatesHorizontal === 'function') { 3937 setTimeout(() => { 3938 console.log("[Delayed Update from SubMenu Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId); 3939 updateActiveStatesHorizontal(clickedItemId); 3940 }, 100); 3941 } 3942 } 3943 } 3944 3945 // Close all parent dropdowns and flyouts after navigation 3946 document.querySelectorAll('.horizontal-menu-item.expanded, .horizontal-submenu-item.expanded').forEach((openItem) => { 3947 openItem.classList.remove('expanded'); 3948 const icon = openItem.querySelector('.icon.toggle-icon'); 3949 if (icon) icon.classList.remove('rotated'); 3950 }); 3951 3952 // Close all flyouts 3953 document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => { 3954 flyout.style.display = 'none'; 3955 flyout.style.opacity = '0'; 3956 flyout.style.visibility = 'hidden'; 3957 }); 3958 }); 3959 } 3960 3961 return subMenuItem; 3962} 3963 3964/** 3965* Updates the 'active' class for items in the horizontal menu. 3966*/ 3967 3968function updateActiveStatesHorizontal(forcedActiveId = null) { 3969 // Prioritize forcedActiveId if provided, otherwise use global state. 3970 // Ensure the ID is treated as a number for comparison. 3971 const targetId = forcedActiveId !== null 3972 ? parseInt(String(forcedActiveId)) 3973 : parseInt(String(window.activeGalleryId || activeGalleryId)); 3974 3975 console.log("[Horizontal Menu Update] Initiated. Target Active ID:", targetId, 3976 (forcedActiveId !== null ? `(Forced: ${forcedActiveId})` : "(Global)"), 3977 "Type:", typeof targetId); 3978 3979 const menuItems = document.querySelectorAll('.horizontal-menu-item, .horizontal-submenu-item'); 3980 3981 if (menuItems.length === 0 && !isNaN(targetId)) { // Check if targetId is a valid number 3982 console.warn("[Horizontal Menu Update] No horizontal menu items found in DOM to update. Target ID was:", targetId); 3983 return; 3984 } 3985 3986 let activeItemSet = false; 3987 menuItems.forEach(item => { 3988 const itemIdStr = item.dataset.id; 3989 if (!itemIdStr) { 3990 // console.warn("[Horizontal Menu Update] Menu item found without a data-id attribute:", item); 3991 return; 3992 } 3993 const itemId = parseInt(itemIdStr); 3994 3995 if (itemId === targetId) { 3996 if (!item.classList.contains('active')) { 3997 item.classList.add('active'); 3998 console.log(`[Horizontal Menu Update] ADDED .active to item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`); 3999 } 4000 activeItemSet = true; 4001 } else { 4002 if (item.classList.contains('active')) { 4003 item.classList.remove('active'); 4004 console.log(`[Horizontal Menu Update] REMOVED .active from item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`); 4005 } 4006 } 4007 }); 4008 4009 if (!isNaN(targetId) && !activeItemSet) { // Check if targetId is a valid number 4010 console.warn(`[Horizontal Menu Update] Target Active ID was ${targetId}, but NO matching horizontal menu item was made active. Check data-id attributes and item visibility.`); 4011 } 4012 4013 const activeSubItem = document.querySelector('.horizontal-submenu-item.active'); 4014 if (activeSubItem) { 4015 const parentTopItem = activeSubItem.closest('.horizontal-menu
4015-item.has-children'); 4016 if (parentTopItem && !parentTopItem.classList.contains('active')) { 4017 // parentTopItem.classList.add('active-parent'); // Optional class for styling parent 4018 } 4019 } 4020 console.log("[Horizontal Menu Update] Completed for Target ID:", targetId); 4021} 4022 4023 4024 // Listener for closing dropdowns when clicking outside 4025 document.addEventListener('click', function(event) { 4026 if (currentMenuLayout === 'horizontal') { 4027 const openSubmenus = document.querySelectorAll('.horizontal-menu-item.expanded'); 4028 let clickedInsideSubmenuOrParent = false; 4029 4030 openSubmenus.forEach(submenuContainer => { 4031 // Check if click is on the submenu container OR its direct parent menu item 4032 if (submenuContainer.contains(event.target) || 4033 (submenuContainer.parentElement && submenuContainer.parentElement.contains(event.target) && submenuContainer.parentElement.classList.contains('horizontal-menu-item'))) { 4034 clickedInsideSubmenuOrParent = true; 4035 } 4036 }); 4037 4038 // If click is on another top-level item that is NOT expanded, also don't close 4039 const targetMenuItem = event.target.closest('.horizontal-menu-item'); 4040 if (targetMenuItem && !targetMenuItem.classList.contains('expanded') && targetMenuItem.classList.contains('has-children')) { 4041 clickedInsideSubmenuOrParent = true; // Don't close if clicking another parent to open it 4042 } 4043 4044 4045 if (!clickedInsideSubmenuOrParent) { 4046 openSubmenus.forEach(submenuContainer => { 4047 submenuContainer.classList.remove('expanded'); 4048 }); 4049 } 4050 } 4051 }); 4052 4053document.addEventListener('menuLayoutChanged', function(e) { 4054 const newLayout = e.detail.layout; 4055 console.log('EVENT: menuLayoutChanged detected. New layout:', newLayout, 'Current isEditing state:', isEditing); 4056 4057 // Update the global currentMenuLayout variable if you have one 4058 if (typeof currentMenuLayout !== 'undefined') { 4059 currentMenuLayout = newLayout; 4060 } 4061 4062 const sidebar = document.querySelector('.sidebar'); // Ensure sidebar DOM element is selected 4063 4064 // Always clear and re-render the appropriate menu structure for the new layout. 4065 // This is important for style changes (like font size) that affect dimensions. 4066 const galleryTreeContainer = document.getElementById('galleryTree'); 4067 const mobileHeaderContent = document.querySelector('.mobile-header-content'); 4068 4069 if (galleryTreeContainer) { 4070 galleryTreeContainer.innerHTML = ''; // Clear existing tree 4071 } 4072 if (mobileHeaderContent) { 4073 const horizontalMenuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container'); 4074 if (horizontalMenuContainer) { 4075 horizontalMenuContainer.innerHTML = ''; // Clear only items, not the container itself 4076 } 4077 } 4078 4079 // Render the correct menu structure based on the new layout 4080 if (newLayout === 'horizontal') { 4081 if (typeof renderHorizontalMenu === 'function') { 4082 renderHorizontalMenu(); 4083 } 4084 // CSS rules will handle sidebar visibility based on .edit-mode-active and .menu-layout-horizontal 4085 } else { // 'sidebar' or 'top' 4086 if (typeof renderGalleries === 'function') { 4087 renderGalleries(); 4088 } 4089 // CSS rules will handle sidebar visibility 4090 } 4091 4092 // If currently in edit mode, ensure the UI components for editing are correctly displayed 4093 // for the new layout, without fully toggling the mode off and on. 4094 if (isEditing && typeof window.updateGlobalEditState === 'function') { 4095 console.log('menuLayoutChanged: In edit mode. Ensuring edit UI is consistent for new layout.'); 4096 4097 // 1. Forcefully ensure global state and body/sidebar classes are correct for edit mode. 4098 // updateGlobalEditState should handle adding .edit-mode-active to body and .editing to sidebar. 4099 window.updateGlobalEditState(true); 4100 4101 // 2. Re-initialize sortables. It's often safer to destroy existing ones first. 4102 if (typeof initializeNestedSortables === 'function') { 4103 if (typeof destroyNestedSortables === 'function') { 4104 destroyNestedSortables(); 4105 } 4106 setTimeout(initializeNestedSortables, 100); // Delay for DOM updates 4107 } 4108 // Also reinitialize for SidebarManager if it exists and handles its own sortables 4109 if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') { 4110 setTimeout(() => window.SidebarManager._reinitializeNestedSortables(), 150); 4111 } 4112 4113 // 3. Ensure edit controls (the top bar of buttons in the sidebar) are visible
4114 const editControls = document.querySelector('.edit-controls'); 4115 if (editControls) { 4116 editControls.style.display = 'flex'; // Or your default display type for these controls 4117 editControls.classList.add('visible'); 4118 } 4119 4120 // 4. If the layout is now horizontal, the sidebar (for editing) should be visible. 4121 // The CSS rule `body.menu-layout-horizontal.edit-mode-active .sidebar { display: block !important; }` 4122 // should handle this. This log is for confirmation. 4123 if (newLayout === 'horizontal' && sidebar) { 4124 console.log('menuLayoutChanged: Horizontal layout in edit mode, sidebar should be visible via CSS.'); 4125 } 4126 4127 // 5. If PageManager is active and a page is loaded, ensure its edit mode is also set. 4128 if (window.PageManager && typeof window.PageManager.setEditMode === 'function' && 4129 window.PageManager.getCurrentPageId && window.PageManager.getCurrentPageId()) { 4130 window.PageManager.setEditMode(true); 4131 } 4132 4133 } else if (!isEditing) { 4134 // If not in edit mode, CSS should handle the view state. 4135 // This block can be used for any explicit view mode adjustments if CSS isn't sufficient. 4136 console.log('menuLayoutChanged: Not in edit mode. CSS will handle view state.'); 4137 } 4138 4139 // Update mobile title and close mobile menu if open, regardless of edit mode 4140 if (typeof updateMobileTitle === 'function') { 4141 updateMobileTitle(); 4142 } 4143 if (typeof closeMobileMenu === 'function') { 4144 closeMobileMenu(); 4145 } 4146 4147 // Re-render the active page so row-band insets and recipe layout match the new menu shell 4148 if (window.PageManager && typeof window.PageManager.renderElements === 'function' && window.PageManager.currentPageId) { 4149 console.log('menuLayoutChanged: Re-rendering page', window.PageManager.currentPageId); 4150 window.PageManager.renderElements(window.PageManager.currentPageId); 4151 } 4152 if (typeof ensureCorrectLayoutApplied === 'function') { 4153 setTimeout(ensureCorrectLayoutApplied, 0); 4154 } 4155}); 4156 4157 4158// Make sure getFirstSidebarElementForHeader is available if called from other scripts 4159if (typeof window.getFirstSidebarElementForHeader === 'undefined') { 4160 window.getFirstSidebarElementForHeader = getFirstSidebarElementForHeader; 4161} 4162 4163// Make sure getFirstSocialElementForHeader is available if called from other scripts 4164if (typeof window.getFirstSocialElementForHeader === 'undefined') { 4165 window.getFirstSocialElementForHeader = getFirstSocialElementForHeader; 4166} 4167 4168// Make sure renderSocialIcons is available if called from other scripts 4169if (typeof window.renderSocialIcons === 'undefined') { 4170 window.renderSocialIcons = renderSocialIcons; 4171} 4172if (typeof window.renderHorizontalMenu === 'undefined') { 4173 window.renderHorizontalMenu = renderHorizontalMenu; 4174} 4175if (typeof window.createHorizontalMenuItem === 'undefined') { 4176 window.createHorizontalMenuItem = createHorizontalMenuItem; 4177} 4178if (typeof window.createHorizontalSubMenuItem === 'undefined') { 4179 window.createHorizontalSubMenuItem = createHorizontalSubMenuItem; 4180} 4181if (typeof window.updateActiveStatesHorizontal === 'undefined') { 4182 window.updateActiveStatesHorizontal = updateActiveStatesHorizontal; 4183} 4184// Ensure toggleEditMode is globally available if it's the primary one. 4185if (typeof window.toggleEditMode === 'undefined' || window.toggleEditMode.toString().length < 100) { // Heuristic to check if it's a placeholder 4186 window.toggleEditMode = toggleEditMode; 4187} 4188 4189 4190// Helper function to toggle edit controls visibility 4191function toggleEditControlsVisibility(show) { 4192 const editControls = document.querySelector('.edit-controls'); 4193 if (editControls) { 4194 if (show) { 4195 editControls.classList.add('visible'); 4196 editControls.style.display = 'flex'; 4197 } else { 4198 editControls.classList.remove('visible'); 4199 editControls.style.display = 'none'; 4200 } 4201 } 4202} 4203 4204/** Reliable + shortcut across US/Intl layouts, numpad, and browsers (key/keyCode differs). */ 4205function isHydraPlusKey(e) { 4206 if (e.repeat) return false;
4207 if (e.ctrlKey || e.metaKey || e.altKey) return false; 4208 if (e.key === '+' || e.key === 'Add') return true; 4209 if (e.code === 'NumpadAdd') return true; 4210 if ((e.code === 'Equal' || e.key === '=') && e.shiftKey) return true; 4211 if (e.keyCode === 187 && e.shiftKey) return true; 4212 if (e.keyCode === 107) return true; 4213 return false; 4214} 4215 4216function handleHydraPlusShortcut() { 4217 try { 4218 const token = window.didToken || localStorage.getItem('hydra_auth_token'); 4219 const isAdmin = window.isAdmin || localStorage.getItem('hydra_is_admin') === 'true'; 4220 if (token && isAdmin) { 4221 console.log('User already logged in - toggling edit mode'); 4222 if (typeof window.toggleEditMode === 'function') { 4223 window.toggleEditMode(); 4224 } else if (typeof toggleEditMode === 'function') { 4225 toggleEditMode(); 4226 } else { 4227 console.warn('toggleEditMode function not found'); 4228 } 4229 } else { 4230 const p = startOTPLogin(); 4231 if (p && typeof p.then === 'function') { 4232 p.catch(function(err) { 4233 console.error('startOTPLogin failed:', err); 4234 }); 4235 } 4236 } 4237 } catch (err) { 4238 console.error('Hydra + key handler error:', err); 4239 } 4240} 4241 4242document.addEventListener('keydown', function(e) { 4243 if (!isHydraPlusKey(e)) return; 4244 e.preventDefault(); 4245 handleHydraPlusShortcut(); 4246}, true); 4247 4248// Custom HTML pages load in a sandboxed iframe; + is captured there unless forwarded via postMessage. 4249function isHydraCustomPageHtmlPlusMessage(e) { 4250 if (!e.data || e.data.source !== 'hydra-custom-page-html' || e.data.type !== 'plus-key') return false; 4251 if (e.origin === window.location.origin) return true; 4252 // Sandboxed iframes (no allow-same-origin) post with origin "null" â accept only from our iframe. 4253 if (e.origin !== 'null' || !e.source) return false; 4254 const iframes = document.querySelectorAll('iframe.page-custom-html-iframe'); 4255 for (let i = 0; i < iframes.length; i++) { 4256 if (iframes[i].contentWindow === e.source) return true; 4257 } 4258 return false; 4259} 4260 4261window.addEventListener('message', function(e) { 4262 if (!isHydraCustomPageHtmlPlusMessage(e)) return; 4263 handleHydraPlusShortcut(); 4264}); 4265 4266/** Option+/ (÷) opens Help â Pathways. Option+? opens Help â Ask (help admin only). */ 4267function isHydraHelpKey(e) { 4268 if (e.repeat) return false; 4269 if (e.ctrlKey || e.metaKey) return false; 4270 if (!e.altKey) return false; 4271 if (e.shiftKey) return false; 4272 if (e.key === '÷') return true; 4273 if (e.code === 'Slash' && e.altKey) return true; 4274 return false; 4275} 4276 4277function isHydraHelpAskKey(e) { 4278 if (e.repeat) return false; 4279 if (e.ctrlKey || e.metaKey) return false; 4280 if (!e.altKey || !e.shiftKey) return false; 4281 if (e.key === '?' || e.key === '¿') return true; 4282 if (e.code === 'Slash') return true; 4283 return false; 4284} 4285 4286document.addEventListener('keydown', function(e) { 4287 if (!window.HYDRA_HELP_ENABLED) return; 4288 var t = e.target; 4289 if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return; 4290 if (isHydraHelpAskKey(e)) { 4291 e.preventDefault(); 4292 e.stopPropagation(); 4293 openHydraHelp({ initialTab: 'ask' }); 4294 return; 4295 } 4296 if (!isHydraHelpKey(e)) return; 4297 e.preventDefault(); 4298 e.stopPropagation(); 4299 openHydraHelp(); 4300}, true); 4301 4302// Email OTP Authentication with Enhanced Debugging 4303 4304 4305// Add this function to the client-side code 4306// Improved client-side backup function with better token handling 4307async function createLoginBackup() { 4308 try { 4309 console.log('Attempting to create login backup...'); 4310 4311 // Get authentication info from multiple possible sources 4312 let token = window.didToken || localStorage.getItem('hydra_auth_token'); 4313 const email = localStorage.getItem('hydra_auth_email') || 4314 (window.userMetadata ? window.userMetadata.email : '') || 4315 ''; 4316 4317 // Check if we have authentication 4318 if (!token) { 4319 console.error('No auth token available for backup'); 4320 return; 4321 } 4322 4323 if (!email) { 4324 console.error('No email available for backup - make sure it was stored during login'); 4325 return; 4326 } 4327 4328 console.log(`Creating backup with token (${token.length} chars) for ${email}`); 4329 4330 // Format the token correctly - VERY IMPORTANT 4331 // If it's already a hydra token (starts with hydra:), use it as is 4332 // Otherwise, add the hydra: prefix 4333 const formattedToken = token.startsWith('hydra:') ? token : `hydra:${token}`; 4334 4335 console.log(`Using formatted token: ${formattedToken.substring(0, 15)}...`); 4336 4337 // Get the API URL with preview handling if needed 4338 const backupUrl = typeof getApiUrl === 'function' ? 4339 getApiUrl('/api/create-backup') : 4340 '/api/create-backup'; 4341 4342 console.log(`Sending backup request to: ${backupUrl}`); 4343 4344 // IMPORTANT: Make sure to add the Bearer prefix to the Authorization header 4345 const response = await fetch(backupUrl, { 4346 method: 'POST', 4347 headers: { 4348 'Authorization': `Bearer ${formattedToken}`, 4349 'Content-Type': 'application/json', 4350 'X-User-Email': email, // Include email as fallback 4351 'X-API-Request': 'true' // Mark as API request 4352 } 4353 }); 4354 4355 // Handle response 4356 if (!response.ok) { 4357 const errorText = await response.text(); 4358 let errorData;
4359 try { 4360 // Try to parse as JSON 4361 errorData = JSON.parse(errorText); 4362 console.error('Backup failed:', errorData.error, errorData); 4363 } catch { 4364 // Not JSON, log raw text 4365 console.error('Backup failed with status', response.status, errorText); 4366 } 4367 return; 4368 } 4369 4370 // Parse successful response 4371 const result = await response.json(); 4372 console.log(`â Backup created successfully: ${result.backup}`); 4373 } catch (error) { 4374 console.error('Error in backup process:', error); 4375 } 4376} 4377 4378// Initialize Magic with detailed logging 4379function initMagic() { 4380 try { 4381 // If Magic is already initialized, return it 4382 if (window.magic) { 4383 return window.magic; 4384 } 4385 4386 console.log('Initializing Magic SDK...'); 4387 4388 // Initialize with minimal options 4389 const magic = new Magic('pk_live_E0B68BA6DCA8C75F', { 4390 testMode: false 4391 }); 4392 4393 // Store reference globally 4394 window.magic = magic; 4395 4396 console.log('Magic SDK initialized successfully'); 4397 return magic; 4398 } catch (error) { 4399 console.error('Error initializing Magic:', error); 4400 throw error; 4401 } 4402} 4403 4404// Start OTP login flow with proper event handling 4405async function startOTPLogin() { 4406 try { 4407 // Initialize Magic 4408 const magic = initMagic(); 4409 4410 // Get email from user (styled overlay â same auth path as native prompt) 4411 const email = await showEmailLoginOverlay(); 4412 if (!email) return; // User cancelled 4413 4414 console.log('Starting Email OTP login for:', email); 4415 showLoadingOverlay('Sending verification code...'); 4416 4417 // Check if Magic's event enum types are available - for newer Magic SDK versions 4418 // If not available, fall back to string-based events 4419 const useEnums = typeof LoginWithEmailOTPEventOnReceived !== 'undefined' && 4420 typeof LoginWithEmailOTPEventEmit !== 'undefined'; 4421 4422 console.log('Using enum-based events:', useEnums); 4423 4424 // Create a timeout handler 4425 let emailSentTimeout = setTimeout(() => { 4426 console.log('No email-otp-sent event received, using fallback UI'); 4427 hideLoadingOverlay(); 4428 showOTPInputOverlay(email); 4429 }, 10000); 4430 4431 // Create OTP login handle with showUI: false 4432 const handle = magic.auth.loginWithEmailOTP({ 4433 email, 4434 showUI: false, 4435 deviceCheckUI: false 4436 }); 4437 4438 // Track authentication state 4439 window.pendingAuth = { 4440 email, 4441 handle 4442 }; 4443 4444 // Listen for events - using both enum-based and string-based event handling 4445 console.log('Setting up OTP event handlers...'); 4446 4447 // Email OTP sent event 4448 if (useEnums) { 4449 handle.on(LoginWithEmailOTPEventOnReceived.EmailOTPSent, () => { 4450 console.log('EVENT: EmailOTPSent (enum) - Email was sent successfully'); 4451 clearTimeout(emailSentTimeout); 4452 hideLoadingOverlay(); 4453 showOTPInputOverlay(email); 4454 }); 4455 } 4456 4457 // Also listen for string-based event (for backward compatibility) 4458 handle.on('email-otp-sent', () => { 4459 console.log('EVENT: email-otp-sent - Email was sent successfully'); 4460 clearTimeout(emailSentTimeout); 4461 hideLoadingOverlay(); 4462 showOTPInputOverlay(email); 4463 }); 4464 4465 // Invalid OTP event 4466 if (useEnums) { 4467 handle.on(LoginWithEmailOTPEventOnReceived.InvalidEmailOtp, () => { 4468 console.log('EVENT: InvalidEmailOtp (enum) - Invalid code entered'); 4469 showErrorMessage('Invalid verification code. Please try again.'); 4470 updateOTPInputForRetry(); 4471 }); 4472 } 4473 4474 // Also listen for string-based event 4475 handle.on('invalid-email-otp', () => { 4476 console.log('EVENT: invalid-email-otp - Invalid code entered'); 4477 showErrorMessage('Invalid verification code. Please try again.'); 4478 updateOTPInputForRetry(); 4479 }); 4480 4481 async function handleLoginSuccess(result) { 4482 console.log('OTP verification successful, checking admin status...'); 4483 hideOTPInputOverlay(); 4484 hideLoadingOverlay(); 4485 4486 try { 4487 window.didToken = result; 4488 console.log('DID Token received:', result ? (result.substring(0, 20) + '...') : 'none'); 4489 4490 const userInfo = await magic.user.getInfo(); 4491 window.userMetadata = userInfo; 4492 console.log('User info retrieved:', userInfo); 4493 4494 const isUserAdmin = await checkAdminStatus(result); 4495 4496 if (!isUserAdmin && !window.isAdmin) { 4497 console.log('User is not on admin list, login denied'); 4498 showErrorMessage('Login failed'); 4499 clearHydraClientAuthStorage(); 4500 resetAuthUI();
4501 try { 4502 await magic.user.logout(); 4503 } catch (logoutError) { 4504 console.error('Error logging out non-admin user from Magic:', logoutError); 4505 } 4506 window.pendingAuth = null; 4507 return; 4508 } 4509 4510 console.log('Admin status check completed'); 4511 window.isAuthenticated = true; 4512 isAuthenticated = true; 4513 showSuccessMessage('Login successful!'); 4514 4515 // Store the ID token (JWT) for API calls 4516 try { 4517 const idToken = await magic.user.getIdToken(); 4518 if (idToken && idToken.includes('.')) { 4519 console.log('Got proper JWT token from Magic.link'); 4520 localStorage.setItem('hydra_auth_token', idToken); 4521 } else if (userInfo && userInfo.email) { 4522 const userToken = { 4523 email: userInfo.email, 4524 iat: Math.floor(Date.now() / 1000), 4525 exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60) 4526 }; 4527 const header = btoa(JSON.stringify({ typ: 'JWT', alg: 'none' })); 4528 const payload = btoa(JSON.stringify(userToken)); 4529 const jwtToken = header + '.' + payload + '.nosignature'; 4530 localStorage.setItem('hydra_auth_token', jwtToken); 4531 localStorage.setItem('hydra_auth_email', userInfo.email); 4532 } 4533 localStorage.setItem('hydra_did_token', result); 4534 } catch (tokenError) { 4535 console.error('Error getting ID token:', tokenError); 4536 localStorage.setItem('hydra_auth_token', result); 4537 } 4538 4539 // IMPROVED UI UPDATE APPROACH - Use multiple techniques for redundancy 4540 4541 // 1. Add state classes to body 4542 document.body.classList.add('hydra-authenticated'); 4543 if (window.isAdmin) { 4544 document.body.classList.add('hydra-admin'); 4545 } 4546 4547 // 2. Direct DOM manipulation 4548 const logoutButton = document.getElementById('logoutButton'); 4549 const editButton = document.getElementById('editButton'); 4550 4551 // Create new buttons if they don't exist 4552 if (!logoutButton) { 4553 console.log('Creating logout button'); 4554 const newLogoutButton = document.createElement('button'); 4555 newLogoutButton.id = 'logoutButton'; 4556 newLogoutButton.className = 'btn'; 4557 newLogoutButton.textContent = 'Logout'; 4558 newLogoutButton.onclick = logout; 4559 newLogoutButton.style.display = 'block'; 4560 4561 const sidebarHeader = document.querySelector('.sidebar-header'); 4562 if (sidebarHeader) { 4563 sidebarHeader.appendChild(newLogoutButton); 4564 } else { 4565 document.body.appendChild(newLogoutButton); 4566 } 4567 } else { 4568 logoutButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;'; 4569 } 4570 4571 if (!editButton && window.isAdmin) { 4572 console.log('Creating edit button'); 4573 const newEditButton = document.createElement('button'); 4574 newEditButton.id = 'editButton'; 4575 newEditButton.className = 'btn btn-primary'; 4576 newEditButton.innerHTML = ` 4577 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 4578 <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> 4579 <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path> 4580 </svg> 4581 Edit 4582 `; 4583 newEditButton.onclick = toggleEditMode; 4584 newEditButton.style.display = 'block'; 4585 4586 const sidebarHeader = document.querySelector('.sidebar-header'); 4587 if (sidebarHeader) { 4588 sidebarHeader.appendChild(newEditButton); 4589 } else { 4590 document.body.appendChild(newEditButton); 4591 } 4592 } else if (editButton && window.isAdmin) { 4593 editButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;'; 4594 } 4595 4596 // 3. Dispatch event for other listeners 4597 document.dispatchEvent(new CustomEvent('login-complete', { 4598 detail: { 4599 isAdmin: window.isAdmin, 4600 email: window.userMetadata ? window.userMetadata.email : null 4601 } 4602 })); 4603 4604 window.pendingAuth = null; 4605 4606 if (typeof checkAuth === 'function') { 4607 console.log('Running existing checkAuth function'); 4608 setTimeout(checkAuth, 500); 4609 } 4610 } catch (error) { 4611 console.error('Error in login success handling:', error); 4612 showErrorMessage('Login failed'); 4613 clearHydraClientAuthStorage(); 4614 resetAuthUI(); 4615 window.pendingAuth = null; 4616 } 4617} 4618 4619 4620 // Completion event 4621 handle.on('done', async (result) => { 4622 await handleLoginSuccess(result); 4623 }); 4624 4625 // Error event 4626 handle.on('error', (error) => { 4627 console.error('EVENT: error - OTP login error:', error); 4628 hideLoadingOverlay(); 4629 hideOTPInputOverlay(); 4630 showErrorMessage('Login failed: ' + (error.message || 'Unknown error')); 4631 4632 // Clear timeout if still active 4633 clearTimeout(emailSentTimeout); 4634 4635 // Clean up 4636 window.pendingAuth = null; 4637 }); 4638 4639 console.log('OTP event handlers set up successfully'); 4640 4641 } catch (error) { 4642 console.error('Error starting OTP login:', error); 4643 hideLoadingOverlay(); 4644 hideOTPInputOverlay(); 4645 showErrorMessage('Error starting login: ' + error.message); 4646 } 4647} 4648
4649// Submit OTP code with support for enum-based events 4650function submitOTP(code) { 4651 try { 4652 // Validate code input 4653 if (!code || code.trim() === '') { 4654 showErrorMessage('Please enter the verification code.'); 4655 return; 4656 } 4657 4658 // Check if we have a pending authentication 4659 if (!window.pendingAuth || !window.pendingAuth.handle) { 4660 showErrorMessage('No active login session. Please try again.'); 4661 hideOTPInputOverlay(); 4662 return; 4663 } 4664 4665 console.log('Submitting OTP code...'); 4666 showLoadingOverlay('Verifying code...'); 4667 4668 // Check if enum types are available 4669 const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined'; 4670 4671 // Verify the OTP code 4672 if (useEnums) { 4673 window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.VerifyEmailOtp, code); 4674 } else { 4675 window.pendingAuth.handle.emit('verify-email-otp', code); 4676 } 4677 4678 } catch (error) { 4679 console.error('Error submitting OTP:', error); 4680 hideLoadingOverlay(); 4681 showErrorMessage('Error verifying code: ' + error.message); 4682 } 4683} 4684 4685// Cancel OTP login with support for enum-based events 4686function cancelOTPLogin() { 4687 try { 4688 // Check if we have a pending authentication 4689 if (window.pendingAuth && window.pendingAuth.handle) { 4690 // Check if enum types are available 4691 const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined'; 4692 4693 // Emit cancel event 4694 if (useEnums) { 4695 window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.Cancel); 4696 } else { 4697 window.pendingAuth.handle.emit('cancel'); 4698 } 4699 4700 console.log('OTP login cancelled'); 4701 } 4702 4703 // Clean up 4704 window.pendingAuth = null; 4705 4706 // Hide any overlays 4707 hideEmailLoginOverlay(); 4708 hideOTPInputOverlay(); 4709 hideLoadingOverlay(); 4710 } catch (error) { 4711 console.error('Error cancelling OTP login:', error); 4712 } 4713} 4714 4715// Update OTP input for retry 4716function updateOTPInputForRetry() {
4717 const otpInput = document.getElementById('otp-input'); 4718 if (otpInput) { 4719 otpInput.value = ''; 4720 otpInput.focus(); 4721 4722 // Add a shake animation for visual feedback 4723 otpInput.classList.add('shake'); 4724 4725 // Remove the animation class after it completes 4726 setTimeout(() => { 4727 otpInput.classList.remove('shake'); 4728 }, 500); 4729 } 4730 4731 // Hide loading overlay if it's visible 4732 hideLoadingOverlay(); 4733} 4734 4735function isValidLoginEmail(value) { 4736 const trimmed = String(value || '').trim(); 4737 if (!trimmed || trimmed.indexOf(' ') >= 0) return false; 4738 const at = trimmed.indexOf('@'); 4739 if (at <= 0 || at !== trimmed.lastIndexOf('@')) return false; 4740 const local = trimmed.slice(0, at); 4741 const domain = trimmed.slice(at + 1); 4742 if (!local || !domain || domain.indexOf('.') < 1) return false; 4743 if (domain.startsWith('.') || domain.endsWith('.')) return false; 4744 return true; 4745} 4746 4747function showEmailLoginOverlay() { 4748 hideEmailLoginOverlay(); 4749 4750 return new Promise((resolve) => { 4751 const overlay = document.createElement('div'); 4752 overlay.id = 'email-login-overlay'; 4753 overlay.className = 'pe-auth-overlay'; 4754 4755 const content = document.createElement('div'); 4756 content.className = 'pe-auth-shell'; 4757 4758 const logo = document.createElement('img'); 4759 logo.src = 'https://cdn.neonsky.app/neon-sky-logo.png'; 4760 logo.alt = 'Neon Sky Logo'; 4761 logo.className = 'pe-auth-shell__logo'; 4762 4763 const description = document.createElement('p'); 4764 description.className = 'pe-auth-shell__desc'; 4765 description.textContent = 'Enter your email address to receive a verification code.'; 4766 4767 const form = document.createElement('form'); 4768 form.noValidate = true; 4769 form.onsubmit = function(e) { 4770 e.preventDefault(); 4771 const raw = input.value.trim(); 4772 if (!isValidLoginEmail(raw)) { 4773 input.classList.add('shake'); 4774 setTimeout(() => input.classList.remove('shake'), 500); 4775 return; 4776 } 4777 hideEmailLoginOverlay(); 4778 resolve(raw); 4779 }; 4780 4781 const input = document.createElement('input'); 4782 input.id = 'email-login-input'; 4783 input.type = 'email'; 4784 input.name = 'email'; 4785 input.placeholder = '[email protected]'; 4786 input.autocomplete = 'email'; 4787 input.inputMode = 'email'; 4788 input.required = true; 4789 input.className = 'pe-auth-shell__input pe-auth-shell__input--email'; 4790 const savedEmail = localStorage.getItem('hydra_auth_email'); 4791 if (savedEmail && isValidLoginEmail(savedEmail)) { 4792 input.value = savedEmail.trim(); 4793 } 4794 form.appendChild(input); 4795 4796 const buttonContainer = document.createElement('div'); 4797 buttonContainer.className = 'pe-auth-actions'; 4798 4799 const continueButton = document.createElement('button'); 4800 continueButton.type = 'submit'; 4801 continueButton.textContent = 'Continue'; 4802 continueButton.className = 'pe-btn pe-btn--primary'; 4803 buttonContainer.appendChild(continueButton); 4804 4805 const cancelButton = document.createElement('button'); 4806 cancelButton.type = 'button'; 4807 cancelButton.textContent = 'Cancel'; 4808 cancelButton.className = 'pe-btn'; 4809 cancelButton.onclick = function() { 4810 hideEmailLoginOverlay(); 4811 resolve(null); 4812 }; 4813 buttonContainer.appendChild(cancelButton); 4814 form.appendChild(buttonContainer); 4815 4816 content.appendChild(logo); 4817 content.appendChild(description); 4818 content.appendChild(form); 4819 overlay.appendChild(content); 4820 document.body.appendChild(overlay); 4821 4822 setTimeout(() => { 4823 input.focus(); 4824 if (input.value) { 4825 input.select(); 4826 } 4827 }, 100); 4828 }); 4829} 4830 4831function hideEmailLoginOverlay() { 4832 const overlay = document.getElementById('email-login-overlay'); 4833 if (overlay && overlay.parentNode) { 4834 overlay.parentNode.removeChild(overlay); 4835 } 4836} 4837 4838// Show OTP input overlay - improved version 4839function showOTPInputOverlay(email) { 4840 hideOTPInputOverlay(); 4841 hideEmailLoginOverlay(); 4842 4843 const overlay = document.createElement('div'); 4844 overlay.id = 'otp-input-overlay'; 4845 overlay.className = 'pe-auth-overlay'; 4846 4847 const content = document.createElement('div'); 4848 content.className = 'pe-auth-shell'; 4849 4850 const logo = document.createElement('img'); 4851 logo.src = 'https://cdn.neonsky.app/neon-sky-logo.png'; 4852 logo.alt = 'Neon Sky Logo'; 4853 logo.className = 'pe-auth-shell__logo'; 4854 4855 const description = document.createElement('p'); 4856 description.className = 'pe-auth-shell__desc'; 4857 const descPrefix = document.createTextNode("We've sent a verification code to "); 4858 const descEmail = document.createElement('strong'); 4859 descEmail.textContent = String(email || ''); 4860 const descSuffix = document.createTextNode('. Please check your email and enter the code below:'); 4861 description.appendChild(descPrefix); 4862 description.appendChild(descEmail); 4863 description.appendChild(document.createElement('br')); 4864 description.appendChild(descSuffix); 4865 4866 const form = document.createElement('form'); 4867 form.onsubmit = function(e) { 4868 e.preventDefault();
4869 const otpInput = document.getElementById('otp-input'); 4870 if (otpInput && otpInput.value) { 4871 submitOTP(otpInput.value); 4872 } 4873 }; 4874 4875 const input = document.createElement('input'); 4876 input.id = 'otp-input'; 4877 input.type = 'text'; 4878 input.placeholder = 'Enter code'; 4879 input.pattern = '[0-9]*'; 4880 input.inputMode = 'numeric'; 4881 input.autocomplete = 'one-time-code'; 4882 input.className = 'pe-auth-shell__input'; 4883 form.appendChild(input); 4884 4885 const buttonContainer = document.createElement('div'); 4886 buttonContainer.className = 'pe-auth-actions'; 4887 4888 const verifyButton = document.createElement('button'); 4889 verifyButton.type = 'submit'; 4890 verifyButton.textContent = 'Verify'; 4891 verifyButton.className = 'pe-btn pe-btn--primary'; 4892 buttonContainer.appendChild(verifyButton); 4893 4894 const cancelButton = document.createElement('button'); 4895 cancelButton.type = 'button'; 4896 cancelButton.textContent = 'Cancel'; 4897 cancelButton.className = 'pe-btn'; 4898 cancelButton.onclick = function() { 4899 cancelOTPLogin(); 4900 }; 4901 buttonContainer.appendChild(cancelButton); 4902 form.appendChild(buttonContainer); 4903 4904 const resendContainer = document.createElement('div'); 4905 resendContainer.className = 'pe-auth-resend'; 4906 4907 const resendText = document.createElement('span'); 4908 resendText.textContent = "Didn't receive the code? "; 4909 4910 const resendLink = document.createElement('a'); 4911 resendLink.textContent = 'Resend'; 4912 resendLink.href = '#'; 4913 resendLink.onclick = function(e) { 4914 e.preventDefault(); 4915 cancelOTPLogin(); 4916 setTimeout(() => { 4917 startOTPLogin(); 4918 }, 500); 4919 }; 4920 4921 resendContainer.appendChild(resendText); 4922 resendContainer.appendChild(resendLink); 4923 form.appendChild(resendContainer); 4924 4925 content.appendChild(logo); 4926 content.appendChild(description); 4927 content.appendChild(form); 4928 overlay.appendChild(content); 4929 document.body.appendChild(overlay); 4930 4931 setTimeout(() => { 4932 input.focus(); 4933 }, 100); 4934} 4935 4936// Hide OTP input overlay 4937function hideOTPInputOverlay() {
4938 const overlay = document.getElementById('otp-input-overlay'); 4939 if (overlay && overlay.parentNode) { 4940 overlay.parentNode.removeChild(overlay); 4941 } 4942} 4943 4944// Utility function to show loading overlay 4945function showLoadingOverlay(message = 'Loading...') { 4946 hideLoadingOverlay(); 4947 4948 const overlay = document.createElement('div'); 4949 overlay.id = 'loading-overlay'; 4950 overlay.className = 'pe-auth-overlay'; 4951 4952 const content = document.createElement('div'); 4953 content.className = 'pe-auth-shell'; 4954 4955 const loaderContainer = document.createElement('div'); 4956 loaderContainer.className = 'pe-auth-shell__loader'; 4957 loaderContainer.innerHTML = ` 4958 <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24"> 4959 <defs> 4960 <filter id="svgSpinnersGooeyBalls20"> 4961 <feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/> 4962 <feColorMatrix in="y" result="z" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7"/> 4963 <feBlend in="SourceGraphic" in2="z"/> 4964 </filter> 4965 </defs> 4966 <g filter="url(#svgSpinnersGooeyBalls20)"> 4967 <circle cx="5" cy="12" r="4" fill="currentColor"> 4968 <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/> 4969 </circle> 4970 <circle cx="19" cy="12" r="4" fill="currentColor"> 4971 <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/> 4972 </circle> 4973 <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/> 4974 </g> 4975 </svg> 4976 `; 4977 content.appendChild(loaderContainer); 4978 4979 const messageEl = document.createElement('p'); 4980 messageEl.className = 'pe-auth-shell__message'; 4981 messageEl.textContent = message; 4982 content.appendChild(messageEl); 4983 4984 overlay.appendChild(content); 4985 document.body.appendChild(overlay); 4986} 4987 4988// Utility function to hide loading overlay 4989function hideLoadingOverlay() { 4990 const overlay = document.getElementById('loading-overlay'); 4991 if (overlay && overlay.parentNode) { 4992 overlay.parentNode.removeChild(overlay); 4993 } 4994} 4995 4996// Utility function to show success message 4997function showSuccessMessage(message, duration = 3000) { 4998 const existingToast = document.getElementById('success-toast'); 4999 if (existingToast && existingToast.parentNode) { 5000 existingToast.parentNode.removeChild(existingToast); 5001 } 5002 5003 const toast = document.createElement('div'); 5004 toast.id = 'success-toast'; 5005 toast.className = 'pe-auth-toast'; 5006 toast.textContent = message; 5007 5008 document.body.appendChild(toast); 5009 5010 setTimeout(() => { 5011 if (toast.parentNode) { 5012 toast.parentNode.removeChild(toast); 5013 } 5014 }, duration); 5015} 5016 5017// Utility function to show error message 5018function showErrorMessage(message, duration = 5000) { 5019 const existingToast = document.getElementById('error-toast'); 5020 if (existingToast && existingToast.parentNode) { 5021 existingToast.parentNode.removeChild(existingToast); 5022 } 5023 5024 const toast = document.createElement('div'); 5025 toast.id = 'error-toast'; 5026 toast.className = 'pe-auth-toast pe-auth-toast--error'; 5027 toast.textContent = message; 5028 5029 document.body.appendChild(toast); 5030 5031 setTimeout(() => { 5032 if (toast.parentNode) { 5033 toast.parentNode.removeChild(toast); 5034 } 5035 }, duration); 5036} 5037 5038// UPDATED: Check authentication status with class-based approach 5039async function checkAuth() { 5040 try { 5041 if (!magic) { 5042 console.log('Magic not initialized, skipping auth check'); 5043 return; 5044 } 5045 5046 // Prevent multiple simultaneous auth checks 5047 if (window._checkingAuth) return; 5048 window._checkingAuth = true; 5049 5050 console.log('Checking auth status...'); 5051 5052 isAuthenticated = await magic.user.isLoggedIn(); 5053 console.log('isLoggedIn check:', isAuthenticated); 5054 5055 if (isAuthenticated) { 5056 // Add the authenticated class 5057 document.body.classList.add('hydra-authenticated'); 5058 5059 // Get user metadata 5060 userMetadata = await magic.user.getInfo(); 5061 console.log('User info:', userMetadata); 5062 5063 // Get ID token for API calls (JWT format) 5064 const rawDidToken = await magic.user.getIdToken(); 5065 console.log('ID token acquired, length:', rawDidToken.length); 5066
5067 // Do NOT write the raw DID token to localStorage here â checkAdminStatus will 5068 // receive the server-issued hydra token and store that instead, which is what 5069 // the save function needs. Writing the DID token now would overwrite a valid 5070 // hydra token that may already be stored from the OTP login flow. 5071 5072 // Check if user is admin; on success this updates both localStorage and didToken 5073 // with a proper hydra token via checkAdminStatus â TokenManager.storeToken 5074 await checkAdminStatus(rawDidToken); 5075 } else { 5076 // Magic has no session â still try Hydra OTP token so /_manage and return visits work 5077 const hydraOnly = localStorage.getItem('hydra_auth_token'); 5078 if (hydraOnly) { 5079 try { 5080 await checkAdminStatus(hydraOnly); 5081 } catch (e) { 5082 console.error('Hydra token restore after Magic logout failed:', e); 5083 } 5084 } else { 5085 resetAuthUI(); 5086 } 5087 } 5088 5089 // Release lock 5090 window._checkingAuth = false; 5091 } catch (error) { 5092 console.error('Error checking authentication:', error); 5093 window._checkingAuth = false; 5094 resetAuthUI(); 5095 } 5096} 5097 5098 5099async function checkAdminStatus(token) { 5100 try { 5101 console.log('Checking admin status...'); 5102 5103 // Add user email to headers if available 5104 let userEmail = null; 5105 if (window.userMetadata && window.userMetadata.email) { 5106 userEmail = window.userMetadata.email; 5107 console.log('Using email from userMetadata:', userEmail); 5108 } else { 5109 userEmail = localStorage.getItem('hydra_auth_email'); // Fallback to localStorage 5110 if (userEmail) console.log('Using email from localStorage:', userEmail); 5111 } 5112 5113 if (!token) { 5114 console.log('No token provided to checkAdminStatus'); 5115 return false; // Indicate failure 5116 } 5117 5118 // Format the token correctly for the API call 5119 let formattedToken = token; 5120 // Ensure Bearer prefix is added for Magic tokens, or use hydra: prefix 5121 if (!token.startsWith('hydra:') && !token.startsWith('Bearer ')) { 5122 formattedToken = `Bearer ${token}`; // Assume Magic/JWT needs Bearer 5123 } else if (token.startsWith('hydra:')) { 5124 formattedToken = `Bearer ${token}`; // API expects Bearer even for hydra tokens 5125 } 5126 // If it already starts with Bearer, use as is 5127 5128 // Get the API URL, handling preview mode 5129 const apiUrl = typeof getApiUrl === 'function' ? 5130 getApiUrl('/api/check-admin') : 5131 '/api/check-admin'; 5132 5133 console.log('Sending admin check request to:', apiUrl); 5134 5135 // Create request headers 5136 const headers = { 5137 'Authorization': formattedToken, 5138 'X-Hydra-Request': 'true' // Indicate this might be part of Hydra flow 5139 }; 5140 5141 if (userEmail) { 5142 headers['X-User-Email'] = userEmail; 5143 } 5144 5145 console.log('Admin check headers:', { 5146 'Authorization': `${formattedToken.substring(0, 20)}...`, 5147 'X-User-Email': userEmail || 'Not available' 5148 }); 5149 5150 // Add timeout to the fetch request 5151 const controller = new AbortController(); 5152 const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout 5153 5154 let data; 5155 try { 5156 const response = await fetch(apiUrl, { 5157 method: 'GET', 5158 headers: headers, 5159 signal: controller.signal 5160 }); 5161 5162 clearTimeout(timeoutId); // Clear timeout if fetch succeeds 5163 5164 console.log('Admin check response status:', response.status); 5165 5166 // Get the response as text first for better debugging 5167 const responseText = await response.text(); 5168 console.log('Admin check response length:', responseText.length); 5169 console.log('Admin check response text (first 100 chars):', responseText.substring(0, 100) + '...'); 5170 5171 5172 try { 5173 data = JSON.parse(responseText); 5174 console.log('Admin check data:', data); 5175 } catch (parseError) { 5176 console.error('Error parsing admin check response:', parseError); 5177 // Handle cases where the response might not be JSON (e.g., HTML error page) 5178 // For preview URLs, let's still assume admin to allow editing flow 5179 if (window.location.hostname === 'preview.neonsky.app') { 5180 console.log('Preview URL detected, assuming admin status due to non-JSON response'); 5181 isAdmin = true; 5182 document.body.classList.add('hydra-admin'); 5183 localStorage.setItem('hydra_is_admin', 'true'); 5184 forceShowEditUI(); // Attempt to show UI 5185 return true; // Indicate potential success for preview 5186 } 5187 throw new Error(`Failed to parse admin check response: ${responseText.substring(0, 100)}...`); 5188 } 5189 5190 if (!response.ok) { 5191 // Throw an error with details from the parsed JSON if available 5192 throw new Error(`Admin check failed: ${response.status} - ${data?.error || responseText.substring(0, 50)}`); 5193 } 5194 5195 // Process the admin status 5196 isAdmin = data.isAdmin; 5197 window.isAdmin = isAdmin; // Update global flag 5198 5199 // Update classes based on admin status 5200 if (isAdmin) { 5201 document.body.classList.add('hydra-admin'); 5202 localStorage.setItem('hydra_is_admin', 'true'); 5203 // Store email if we got it from the server response 5204 if (data.email) { 5205 localStorage.setItem('hydra_auth_email', data.email); 5206 } else if (userEmail) { 5207 localStorage.setItem('hydra_auth_email', userEmail); // Store the email we used 5208 } 5209 } else { 5210 document.body.classList.remove('hydra-admin'); 5211 localStorage.removeItem('hydra_is_admin'); 5212 localStorage.removeItem('hydra_auth_email'); // Clear email if not admin 5213 } 5214 5215 // Update siteId if provided 5216 if (data.siteId) { 5217 siteId = data.siteId; 5218 window.siteId = siteId; 5219 if (window.Parameters) window.Parameters.siteId = siteId; 5220 } 5221 5222 // Store new token if provided (Hydra token) 5223 if (data.hydraToken) { 5224 console.log('Received hydra token:', data.hydraToken.substring(0, 10) + '...'); 5225 const cleanHydraToken = data.hydraToken.startsWith('hydra:') ? data.hydraToken.substring(6) : data.hydraToken; 5226 // Always update the local didToken variable so saves immediately use the hydra token 5227 didToken = cleanHydraToken; 5228 // Store token globally and in localStorage using TokenManager if available 5229 if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') { 5230 window.TokenManager.storeToken(data.hydraToken, data.email || userEmail || ''); 5231 } else { 5232 // Fallback storage 5233 localStorage.setItem('hydra_auth_token', cleanHydraToken); 5234 if (data.email || userEmail) { 5235 localStorage.setItem('hydra_auth_email', data.email || userEmail); 5236 } 5237 } 5238 console.log('Stored credentials in localStorage'); 5239 } 5240 5241 // Update UI based on admin status 5242 if (isAdmin) { 5243 console.log('Admin status confirmed, showing edit UI'); 5244 forceShowEditUI(); // Ensure UI elements are visible 5245 // --- MODIFICATION START --- 5246 // If layout is horizontal, re-render the menu to show controls 5247 let currentLayout = 'sidebar'; // Default 5248 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 5249 currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; 5250 } 5251 if (currentLayout === 'horizontal') { 5252 console.log('checkAdminStatus: Horizontal layout detected, re-rendering horizontal menu.'); 5253 if (typeof renderHorizontalMenu === 'function') { 5254 renderHorizontalMenu(); 5255 } else { 5256 console.warn("checkAdminStatus: renderHorizontalMenu function not found."); 5257 } 5258 } 5259 // --- MODIFICATION END --- 5260 createLoginBackup(); // Create backup after successful login/admin check 5261 } else { 5262 console.log('User is not admin, resetting UI.'); 5263 resetAuthUI(); // Ensure non-admin UI state 5264 } 5265 5266 return isAdmin; // Return the admin status 5267 5268 } catch (fetchError) { 5269 clearTimeout(timeoutId); // Clear timeout on error as well 5270 console.error('Fetch error during admin check:', fetchError); 5271 5272 // For preview URLs, still allow editing as a fallback 5273 if (window.location.hostname === 'preview.neonsky.app') { 5274 console.log('Preview URL + fetch error, still enabling editing'); 5275 isAdmin = true; 5276 window.isAdmin = true; 5277 document.body.classList.add('hydra-admin'); 5278 localStorage.setItem('hydra_is_admin', 'true'); 5279 forceShowEditUI(); 5280 return true; // Indicate potential success for preview 5281 } 5282 5283 // For non-preview, throw the error to indicate failure 5284 throw fetchError; 5285 } 5286 } catch (error) { 5287 console.error('Error in checkAdminStatus:', error); 5288 // Don't show alert here, let the calling function handle UI feedback 5289 // Reset UI to non-admin state on error 5290 resetAuthUI(); 5291 return false; // Indicate failure 5292 } 5293} 5294 5295// UPDATED: Force showing edit UI with class-based approach 5296function forceShowEditUI() { 5297 console.log('Forcing admin/auth state for UI'); 5298 5299 // Ensure global state flags are set 5300 isAuthenticated = true; 5301 isAdmin = true; 5302 window.isAuthenticated = true; 5303 window.isAdmin = true; 5304 5305 // Add admin/auth classes to body - CSS will handle button visibility 5306 document.body.classList.add('hydra-authenticated', 'hydra-admin'); 5307 5308 // Store admin status in localStorage 5309 localStorage.setItem('hydra_is_admin', 'true'); 5310 5311 // Show edit controls container ONLY if already in edit mode 5312 if (window.isInEditMode && window.isInEditMode()) {
5313 const editControls = document.querySelector('.edit-controls'); 5314 if (editControls) { 5315 editControls.classList.add('visible'); 5316 editControls.style.display = 'flex'; // Make sure container is flex 5317 } 5318 // Ensure controls inside items are visible if already editing 5319 document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => { 5320 if (el.closest('.sidebar.editing') || el.closest('.page-container.editing')) { 5321 el.style.display = 'flex'; 5322 } 5323 }); 5324 5325 // Show logout button in edit controls when in edit mode 5326 const logoutButton = document.getElementById('logoutButton'); 5327 if (logoutButton) { 5328 logoutButton.style.display = 'flex'; 5329 logoutButton.classList.add('visible'); 5330 } 5331 } else { 5332 // Ensure edit controls container is hidden if not in edit mode 5333 const editControls = document.querySelector('.edit-controls'); 5334 if (editControls) { 5335 editControls.classList.remove('visible'); 5336 editControls.style.display = 'none'; 5337 } 5338 5339 // Hide logout button when not in edit mode 5340 const logoutButton = document.getElementById('logoutButton'); 5341 if (logoutButton) { 5342 logoutButton.style.display = 'none'; 5343 logoutButton.classList.remove('visible'); 5344 } 5345 } 5346 5347 console.log('Admin/Auth classes set. CSS should handle button visibility.'); 5348} 5349 5350// UPDATED: Reset auth UI with class-based approach 5351function resetAuthUI() { 5352 document.body.classList.remove('hydra-authenticated'); 5353 document.body.classList.remove('hydra-admin'); 5354 5355 5356 const logoutButton = document.getElementById('logoutButton'); 5357 if (logoutButton) { 5358 logoutButton.style.display = 'none'; 5359 logoutButton.classList.remove('visible'); 5360 } 5361 5362 const editButton = document.getElementById('editButton'); 5363 if (editButton) { 5364 editButton.style.display = 'none'; 5365 editButton.classList.remove('visible'); 5366 } 5367 5368 // Remove admin status from localStorage 5369 localStorage.removeItem('hydra_is_admin'); 5370 localStorage.removeItem('hydra_auth_token'); 5371 5372 if (isEditing) { 5373 toggleEditMode(); // Exit edit mode if active 5374 } 5375} 5376 5377// Debug function - add to your code during testing 5378function debugUIClasses() { 5379 console.log('Current body classes:', document.body.className); 5380 console.log('hydra-initialized:', document.body.classList.contains('hydra-initialized')); 5381 console.log('hydra-authenticated:', document.body.classList.contains('hydra-authenticated')); 5382 console.log('hydra-admin:', document.body.classList.contains('hydra-admin')); 5383 console.log('edit-mode-active:', document.body.classList.contains('edit-mode-active')); 5384} 5385 5386 5387 5388async function logout() { 5389 console.log('Logout initiated'); 5390 5391 try { 5392 // Show loading indication (optional) 5393 showLoadingOverlay('Logging out...'); 5394 5395 // 1. Properly logout from Magic SDK 5396 if (window.magic) { 5397 try { 5398 await window.magic.user.logout(); 5399 console.log('Magic SDK logout successful'); 5400 } catch (magicError) { 5401 console.error('Error during Magic logout:', magicError); 5402 // Continue with other logout steps even if Magic SDK logout fails 5403 } 5404 } 5405 5406 // 2. Reset all authentication state variables 5407 clearHydraClientAuthStorage(); 5408 5409 // 3. Reset all UI state - remove authentication classes from body 5410 document.body.classList.remove('hydra-authenticated'); 5411 document.body.classList.remove('hydra-admin'); 5412 5413 // 5. If in edit mode, exit it first 5414 if (document.body.classList.contains('edit-mode-active')) { 5415 console.log('Exiting edit mode before logout'); 5416 // If toggleEditMode exists, call it to exit edit mode 5417 if (typeof toggleEditMode === 'function') { 5418 try { 5419 toggleEditMode(); 5420 } catch (editError) { 5421 console.error('Error exiting edit mode:', editError); 5422 } 5423 } 5424 5425 // Ensure edit mode class is removed regardless 5426 document.body.classList.remove('edit-mode-active'); 5427 } 5428 5429 // 6. Ensure all edit UI elements are hidden 5430 5431 // Hide edit and logout buttons 5432 const editButton = document.getElementById('editButton'); 5433 if (editButton) { 5434 editButton.classList.remove('visible'); 5435 editButton.style.display = 'none'; 5436 } 5437 5438 const logoutButton = document.getElementById('logoutButton'); 5439 if (logoutButton) { 5440 logoutButton.classList.remove('visible'); 5441 logoutButton.style.display = 'none'; 5442 } 5443 5444 // Hide edit controls
5445 const editControls = document.querySelector('.edit-controls'); 5446 if (editControls) { 5447 editControls.style.display = 'none'; 5448 editControls.classList.remove('visible'); 5449 } 5450 5451 // Hide sidebar header if it should be hidden when logged out 5452 const sidebarHeader = document.querySelector('.sidebar-header'); 5453 if (sidebarHeader) { 5454 sidebarHeader.style.display = 'none'; 5455 } 5456 5457 // Hide all element controls 5458 document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => { 5459 el.style.display = 'none'; 5460 }); 5461 5462 // 7. Reset editor state if applicable 5463 if (window.Parameters) { 5464 window.Parameters.isInEditor = false; 5465 } 5466 5467 // Hide any editing forms that might be open 5468 const forms = document.querySelectorAll('.add-form, .edit-form, .element-edit-form, #menuStyleEditor, #metadataEditor, #importClassicForm, #sidebarElementForm'); 5469 forms.forEach(form => { 5470 form.style.display = 'none'; 5471 if (form.classList.contains('visible')) { 5472 form.classList.remove('visible'); 5473 } 5474 }); 5475 5476 // Remove editing class from sidebar 5477 const sidebar = document.querySelector('.sidebar'); 5478 if (sidebar) { 5479 sidebar.classList.remove('editing'); 5480 } 5481 5482 // 8. Clear any page-specific state 5483 if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { 5484 window.PageManager.clearCurrentPage(); 5485 } 5486 5487 // 9. Notify any components that need to know about logout 5488 document.dispatchEvent(new CustomEvent('user-logout', { 5489 detail: { timestamp: Date.now() } 5490 })); 5491 5492 // 10. Show success message 5493 hideLoadingOverlay(); 5494 showSuccessMessage('Successfully logged out'); 5495 5496 console.log('Logout complete'); 5497 5498 // 11. Optional: Refresh the page after a short delay for a clean state 5499 // Uncomment the following lines if you want the page to refresh after logout 5500 /* 5501 setTimeout(() => { 5502 window.location.reload(); 5503 }, 1500); 5504 */ 5505 5506 } catch (error) { 5507 console.error('Error during logout process:', error); 5508 hideLoadingOverlay(); 5509 showErrorMessage('Error during logout. Please refresh the page.'); 5510 } 5511} 5512 5513// Ensure the logout button is properly connected to the logout function 5514document.addEventListener('DOMContentLoaded', function() { 5515 const logoutButton = document.getElementById('logoutButton'); 5516 if (logoutButton) { 5517 // Remove any existing event listeners to avoid duplicates 5518 const newButton = logoutButton.cloneNode(true); 5519 logoutButton.parentNode.replaceChild(newButton, logoutButton); 5520 5521 // Add fresh event listener 5522 newButton.addEventListener('click', function(e) { 5523 e.preventDefault(); 5524 e.stopPropagation(); 5525 logout(); 5526 }); 5527 } 5528}); 5529 5530function stopAutoAdvanceTimer() { 5531 if (window.currentAutoAdvanceTimerId) { 5532 clearTimeout(window.currentAutoAdvanceTimerId); 5533 window.currentAutoAdvanceTimerId = null; 5534 console.log('Auto-advance timer stopped.'); 5535 } 5536} 5537window.stopAutoAdvanceTimer = stopAutoAdvanceTimer; 5538 5539function beginHydraContentLoad(type, itemId, pageId) { 5540 const generation = (Number(window.__hydraContentLoadGeneration) || 0) + 1; 5541 window.__hydraContentLoadGeneration = generation; 5542 window.__neonGalleryInitEpoch = (Number(window.__neonGalleryInitEpoch) || 0) + 1; 5543 window.__hydraContentLoadOwner = { 5544 generation, 5545 type, 5546 itemId: itemId === undefined || itemId === null ? '' : String(itemId), 5547 pageId: pageId === undefined || pageId === null ? '' : String(pageId) 5548 }; 5549 return generation; 5550} 5551 5552function isHydraContentLoadCurrent(generation, type, itemId) { 5553 if (generation != null && Number(generation) !== Number(window.__hydraContentLoadGeneration || 0)) { 5554 return false; 5555 } 5556 if (type && window._activeContentType && window._activeContentType !== type) { 5557 return false; 5558 } 5559 if (itemId != null && itemId !== '' && window.activeGalleryId != null && 5560 String(window.activeGalleryId) !== String(itemId)) { 5561 return false; 5562 } 5563 return true; 5564} 5565 5566window.beginHydraContentLoad = beginHydraContentLoad; 5567window.isHydraContentLoadCurrent = isHydraContentLoadCurrent; 5568 5569function dismissGalleryPasswordOverlay() { 5570 if (window.PageManager && typeof window.PageManager.removeGalleryPasswordOverlay === 'function') { 5571 window.PageManager.removeGalleryPasswordOverlay(); 5572 return; 5573 } 5574 document.querySelectorAll('.gallery-password-protection-overlay').forEach(function(overlay) { 5575 if (overlay._hydraPasswordAbort && typeof overlay._hydraPasswordAbort.abort === 'function') { 5576 overlay._hydraPasswordAbort.abort(); 5577 } 5578 overlay.remove(); 5579 }); 5580} 5581window.dismissGalleryPasswordOverlay = dismissGalleryPasswordOverlay; 5582 5583function toggleEditMode() { 5584 // Prevent multiple simultaneous calls 5585 if (window._editModeToggleInProgress) { 5586 console.log('Edit mode toggle already in progress, skipping'); 5587 return; 5588 } 5589 window._editModeToggleInProgress = true; 5590 5591 console.log("toggleEditMode called. Current window.isEditing:", window.isEditing, "Current window.isInEditMode():", window.isInEditMode ? window.isInEditMode() : 'undefined'); 5592 5593 stopAutoAdvanceTimer(); // Assumes this function exists and stops any slideshow/auto-advance 5594 5595 // Authentication and Authorization Checks 5596 if (!window.isAuthenticated && !(localStorage.getItem('hydra_is_admin') === 'true')) { 5597 alert('You must be logged in to edit'); 5598 window._editModeToggleInProgress = false;
5599 return; 5600 } 5601 if (localStorage.getItem('hydra_is_admin') === 'true' && !window.isAdmin) { 5602 window.isAdmin = true; // Synchronize global flag 5603 } 5604 if (!window.isAdmin) { 5605 alert('You must be an admin to edit'); 5606 window._editModeToggleInProgress = false; 5607 return; 5608 } 5609 5610 const currentEditState = window.isInEditMode ? window.isInEditMode() : (typeof window.isEditing !== 'undefined' ? window.isEditing : false); 5611 const newEditState = !currentEditState; 5612 5613 console.log(`Changing edit mode from ${currentEditState} to: ${newEditState}`); 5614 5615 // Apply body class for edit mode styling 5616 if (newEditState) { 5617 console.log('Entering edit mode - setting body class and theme'); 5618 document.body.setAttribute('data-theme', 'pico'); // Optional: if Pico theme is used for edit mode 5619 document.body.classList.add('edit-mode-active'); 5620 } else { 5621 console.log('Exiting edit mode - removing body class and theme'); 5622 document.body.removeAttribute('data-theme'); 5623 document.body.classList.remove('edit-mode-active'); 5624 } 5625 5626 // Update the global state and dispatch event 5627 if (typeof window.updateGlobalEditState === 'function') { 5628 window.updateGlobalEditState(newEditState); 5629 } else { 5630 window.isEditing = newEditState; // Fallback 5631 document.dispatchEvent(new CustomEvent('edit-mode-changed', { detail: { editing: newEditState } })); 5632 if (!newEditState && typeof window.closeOptionsPanel === 'function') { 5633 window.closeOptionsPanel(); 5634 } 5635 } 5636 5637 if (document.body.classList.contains('menu-layout-horizontal') && typeof syncContentMarginToHorizontalHeader === 'function') { 5638 setTimeout(syncContentMarginToHorizontalHeader, 50); 5639 setTimeout(syncContentMarginToHorizontalHeader, 350); 5640 } 5641 5642 const sidebarEditControls = document.querySelector('.edit-controls'); 5643 5644 if (newEditState) { 5645 // Entering edit mode 5646 if (sidebarEditControls) { 5647 sidebarEditControls.style.display = 'flex'; 5648 sidebarEditControls.classList.add('visible'); 5649 } 5650 5651 // Show logout button in edit controls when entering edit mode 5652 const logoutButton = document.getElementById('logoutButton'); 5653 if (logoutButton) { 5654 logoutButton.style.display = 'flex'; 5655 logoutButton.classList.add('visible'); 5656 } 5657 5658 // Always render the tree-like menu in the sidebar for editing 5659 if (typeof window.renderGalleries === 'function') { 5660 console.log('toggleEditMode (Edit): Rendering sidebar menu (gallery tree) for editing.'); 5661 window.renderGalleries(); // This will show all items, including those with visible:false 5662 } 5663 5664 setTimeout(() => { 5665 if (typeof window.initializeNestedSortables === 'function') { 5666 console.log('Initializing nested sortables for edit mode'); 5667 window.initializeNestedSortables(); 5668 } 5669 if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') { 5670 window.SidebarManager._reinitializeNestedSortables(); 5671 } 5672 }, 150); 5673 5674 // If the currently active item was invisible, reload it now that we are in edit mode. 5675 if(window.galleries && window.activeGalleryId){ 5676 const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId); 5677 if (currentGalleryItem && currentGalleryItem.visible === false) { 5678 console.log('toggleEditMode (Edit): Active item was invisible, reloading it.'); 5679 if (currentGalleryItem.isPage && window.loadPage) { 5680 window.loadPage(window.activeGalleryId); 5681 } else if(window.loadGallery) { 5682 window.loadGallery(window.activeGalleryId); 5683 } 5684 } 5685 } 5686 if(typeof window.showStyleEditor === 'function' && 5687 !(window.MenuStyleCustomizer && window.MenuStyleCustomizer._overlayOpen)) { 5688 window.showStyleEditor(false); // Ensure style editor is hidden initially 5689 } 5690 5691 } else { 5692 // Exiting edit mode (going to Live view) 5693 if (sidebarEditControls) { 5694 sidebarEditControls.style.display = 'none'; 5695 sidebarEditControls.classList.remove('visible'); 5696 } 5697 5698 // Hide logout button when exiting edit mode 5699 const logoutButton = document.getElementById('logoutButton'); 5700 if (logoutButton) { 5701 logoutButton.style.display = 'none'; 5702 logoutButton.classList.remove('visible'); 5703 } 5704 if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer._prepareOverlayForLiveView === 'function') { 5705 window.MenuStyleCustomizer._prepareOverlayForLiveView(); 5706 } 5707 if(typeof window.closeAllForms === 'function') window.closeAllForms(); 5708 5709 if (typeof window.destroyNestedSortables === 'function') { 5710 window.destroyNestedSortables(); 5711 } else if (window.sortableInstances && Array.isArray(window.sortableInstances)) {
5712 window.sortableInstances.forEach(instance => instance.destroy()); 5713 window.sortableInstances = []; 5714 } 5715 5716 // Re-render the menu based on the actual view mode layout. 5717 // These functions should internally handle not showing items with visible:false. 5718 let currentLayout = 'sidebar'; 5719 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 5720 currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; 5721 } 5722 if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') { 5723 window.renderHorizontalMenu(); 5724 } else if (typeof window.renderGalleries === 'function') { 5725 window.renderGalleries(); 5726 } 5727 5728 // Re-render sidebar elements (social, text, etc.) so edit-only UI (grid, dashed slots) is replaced by live view 5729 if (window.SidebarManager && typeof window.SidebarManager._syncWithGlobalEditState === 'function' && typeof window.SidebarManager.renderElements === 'function') { 5730 window.SidebarManager._syncWithGlobalEditState(); 5731 window.SidebarManager.renderElements(); 5732 } 5733 5734 // Handle the currently active gallery/page. 5735 if (window.galleries && window.activeGalleryId) { 5736 const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId); 5737 if (currentGalleryItem) { 5738 // The content for currentGalleryItem should remain loaded, 5739 // even if currentGalleryItem.visible is false. 5740 // The menu rendering above will hide it from the list if it's invisible. 5741 5742 // Apply page-specific menu hiding rules. 5743 const bodyEl = document.body; 5744 // When exiting edit mode, isInEditMode() will effectively be false for this check. 5745 if (shouldApplyMenuHiddenForEmbedOrGallery(currentGalleryItem)) { 5746 bodyEl.classList.add('menu-hidden-on-page'); 5747 } else { 5748 bodyEl.classList.remove('menu-hidden-on-page'); 5749 } 5750 applyMenuOverlayClassForGallery(currentGalleryItem); 5751 // Ensure active states in the (potentially now filtered) menu are updated. 5752 // If the active item is invisible, it won't be marked active in the menu, which is fine. 5753 if(typeof window.updateActiveStates === 'function') window.updateActiveStates(); 5754 if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal(); 5755 5756 } else { 5757 // activeGalleryId points to a non-existent item, so clear it and the content. 5758 console.log('toggleEditMode (Live): activeGalleryId points to a non-existent item. Clearing content.'); 5759 window.activeGalleryId = null; 5760 const galleryContainer = document.querySelector('.gallery-container'); 5761 if (galleryContainer) galleryContainer.innerHTML = ''; 5762 if(typeof window.updateActiveStates === 'function') window.updateActiveStates(); 5763 if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal(); 5764 } 5765 } 5766 5767 // If, after all that, no content is effectively active (e.g., initial load to root, 5768 // or activeGalleryId became null because the item was deleted) 5769 // then try to load a default page (home page or first visible gallery). 5770 const galleryContainer = document.querySelector('.gallery-container'); 5771 // Check if activeGalleryId is null OR if it's set but the container is empty (e.g. item was deleted) 5772 const noActiveContent = window.activeGalleryId === null || 5773 (galleryContainer && galleryContainer.innerHTML.trim() === ''); 5774 5775 if (noActiveContent && window.galleries) { 5776 console.log("toggleEditMode (Live): No active content, attempting to load default page."); 5777 const homePage = window.galleries.find(g => g.isHomePage === true); 5778 let loadedDefault = false;
5779 if (homePage) { 5780 // Load home page regardless of its visibility status for this default loading logic 5781 console.log(`toggleEditMode (Live): Loading home page: ${homePage.title}`); 5782 if (homePage.isPage && window.loadPage) { window.loadPage(homePage.id); loadedDefault = true; } 5783 else if (window.loadGallery) { window.loadGallery(homePage.id); loadedDefault = true; } 5784 } 5785 5786 if (!loadedDefault) { 5787 // If no home page, load the first *visible* gallery/page in live view. 5788 const firstVisibleGallery = window.galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu); 5789 if (firstVisibleGallery) { 5790 console.log(`toggleEditMode (Live): No home page, loading first visible item: ${firstVisibleGallery.title}`); 5791 if (firstVisibleGallery.isPage && window.loadPage) { 5792 window.loadPage(firstVisibleGallery.id); 5793 } else if(window.loadGallery) { 5794 window.loadGallery(firstVisibleGallery.id); 5795 } 5796 } else { 5797 console.log('toggleEditMode (Live): No home page and no visible items to load.'); 5798 } 5799 } 5800 } 5801 5802 if (window.MenuStyleCustomizer && 5803 window.MenuStyleCustomizer._overlayOpen && 5804 typeof window.MenuStyleCustomizer.applyStyles === 'function') { 5805 window.MenuStyleCustomizer.applyStyles(); 5806 } 5807 } 5808 window._editModeToggleInProgress = false; 5809} 5810 5811 5812 5813/** 5814 * Improved toggleSidebarElementForm - Closes other forms first 5815 */ 5816function toggleSidebarElementForm() { 5817 const formId = 'sidebarElementForm'; 5818 const form = document.getElementById(formId); 5819 5820 if (!form) return; 5821 5822 // If this form is already open, just close everything 5823 if (window.currentOpenForm === formId) { 5824 closeAllForms(); 5825 return; 5826 } 5827 5828 // Otherwise close all forms and open this one 5829 closeAllForms(); 5830 5831 // Show the form 5832 form.style.display = 'block'; 5833 form.classList.add('visible'); 5834 5835 // Set this as the current open form 5836 window.currentOpenForm = formId; 5837} 5838 5839// New function to add a sidebar element 5840function addSidebarElement() { 5841const typeInput = document.querySelector('input[name="sidebarElementType"]:checked'); 5842if (!typeInput) { 5843alert('Please select an element type'); 5844return; 5845} 5846 5847const type = typeInput.value; 5848 5849// Use the SidebarManager to add the element 5850if (window.SidebarManager) { 5851const newElementId = window.SidebarManager.addElement(type); 5852toggleSidebarElementForm(); 5853 5854if (newElementId) { 5855 // Save the changes to KV store 5856 saveGalleries().then(() => { 5857 console.log(`New ${type} element added successfully with ID: ${newElementId}`); 5858 }).catch(error => { 5859 console.error('Error saving new element:', error); 5860 }); 5861} 5862} else { 5863alert('SidebarManager not available'); 5864} 5865} 5866 5867 5868// Global variable to store metadata 5869window.siteMetadata = { 5870 title: '', 5871 description: '', 5872 googleAnalytics: '', 5873 noIndex: false, 5874 favicon: '', 5875 ogImageUrl: '', 5876 contentLanguage: 'en', // Default to English 5877 copyrightEnabled: false, 5878 copyrightText: '', 5879 customPublicSiteEnabled: false, 5880 customPublicSiteHtmlR2Key: '', 5881 customPublicSiteHtmlFilename: '', 5882 siteWideBasicAuth: undefined 5883}; 5884 5885// Global variable to store uploaded favicon URL temporarily 5886window.uploadedFaviconUrl = ''; 5887 5888function updateMetadataCopyrightFieldState() { 5889 const toggle = document.getElementById('metadataCopyrightEnabled'); 5890 const textInput = document.getElementById('metadataCopyrightText'); 5891 const isEnabled = !!(toggle && toggle.checked); 5892 5893 if (textInput) { 5894 textInput.disabled = !isEnabled; 5895 } 5896 5897 // Intentionally leave the surrounding form group in place; the disabled attribute on the input 5898 // provides the visual cue in PicoCSS. 5899} 5900 5901/** 5902 * Improved toggleMetadataEditor - Closes other forms first 5903 */ 5904function toggleMetadataEditor() { 5905 const formId = 'metadataEditor'; 5906 const editor = document.getElementById(formId); 5907 5908 if (!editor) return; 5909 5910 // If this form is already open, just close everything 5911 if (window.currentOpenForm === formId) { 5912 closeAllForms(); 5913 return; 5914 } 5915 5916 // Otherwise close all forms and open this one 5917 closeAllForms(); 5918 5919 // Load current values before showing 5920 loadMetadataValues(); 5921 editor.style.display = 'block'; 5922 5923 // Set this as the current open form 5924 window.currentOpenForm = formId; 5925 if (typeof syncCustomPublicSiteBypassVisibility === 'function') { 5926 syncCustomPublicSiteBypassVisibility(); 5927 } 5928} 5929 5930 5931// Load metadata values into the form 5932function loadMetadataValues() { 5933 // Get the current values from the global object 5934 const metadata = window.siteMetadata || {}; 5935 console.log('loadMetadataValues: Loading metadata:', JSON.stringify(metadata)); 5936 console.log('loadMetadataValues: Google Analytics from metadata:', metadata.googleAnalytics); 5937 5938 console.warn('=== FAVICON LOAD DEBUG ==='); 5939 console.warn('window.siteMetadata:', JSON.stringify(window.siteMetadata)); 5940 console.warn('metadata object:', JSON.stringify(metadata)); 5941 console.warn('metadata.favicon value:', metadata.favicon); 5942 console.warn('metadata.favicon type:', typeof metadata.favicon); 5943 5944 // Populate form fields 5945 document.getElementById('metaTitle').value = metadata.title || ''; 5946 document.getElementById('metaDescription').value = metadata.description || ''; 5947 document.getElementById('googleAnalytics').value = metadata.googleAnalytics || ''; 5948 document.getElementById('noIndexToggle').checked = metadata.noIndex || false; 5949 5950 // Schema / Identity fields 5951 const schemaType = metadata.schemaType || ''; 5952 const personRadio = document.getElementById('schemaTypePerson'); 5953 const orgRadio = document.getElementById('schemaTypeOrganization'); 5954 if (personRadio) personRadio.checked = schemaType === 'Person'; 5955 if (orgRadio) orgRadio.checked = schemaType === 'Organization'; 5956 const jobTitleEl = document.getElementById('schemaJobTitle'); 5957 if (jobTitleEl) jobTitleEl.value = metadata.schemaJobTitle || ''; 5958 const cityEl = document.getElementById('schemaCity'); 5959 if (cityEl) cityEl.value = metadata.schemaCity || ''; 5960 const emailEl = document.getElementById('schemaEmail'); 5961 if (emailEl) emailEl.value = metadata.schemaEmail || ''; 5962 const imageLicenseUrlEl = document.getElementById('imageLicenseUrl'); 5963 if (imageLicenseUrlEl) imageLicenseUrlEl.value = metadata.imageLicenseUrl || ''; 5964 const acquireLicensePageUrlEl = document.getElementById('acquireLicensePageUrl'); 5965 if (acquireLicensePageUrlEl) acquireLicensePageUrlEl.value = metadata.acquireLicensePageUrl || ''; 5966 if (typeof updateSchemaFields === 'function') updateSchemaFields(); 5967 5968 const copyrightToggle = document.getElementById('metadataCopyrightEnabled'); 5969 const copyrightInput = document.getElementById('metadataCopyrightText'); 5970 if (copyrightToggle) { 5971 const enabled = !!metadata.copyrightEnabled; 5972 copyrightToggle.checked = enabled; 5973 if (copyrightInput) { 5974 copyrightInput.value = metadata.copyrightText || ''; 5975 } 5976 } 5977 updateMetadataCopyrightFieldState(); 5978 5979 const customPubToggle = document.getElementById('customPublicSiteEnabled'); 5980 if (customPubToggle) { 5981 customPubToggle.checked = !!metadata.customPublicSiteEnabled; 5982 } 5983 const customPubKeyEl = document.getElementById('customPublicSiteHtmlR2Key'); 5984 if (customPubKeyEl) { 5985 customPubKeyEl.value = metadata.customPublicSiteHtmlR2Key || ''; 5986 } 5987 const customPubFileLabel = document.getElementById('customPublicSiteHtmlFilenameLabel'); 5988 if (customPubFileLabel) { 5989 customPubFileLabel.textContent = metadata.customPublicSiteHtmlFilename 5990 ? ('Current file: ' + metadata.customPublicSiteHtmlFilename) 5991 : 'No HTML file uploaded yet.'; 5992 } 5993 5994 const swAuth = metadata.siteWideBasicAuth; 5995 const swEnabled = !!(swAuth && swAuth.username && swAuth.password); 5996 const swEnableEl = document.getElementById('siteWideBasicAuthEnabled'); 5997 if (swEnableEl) swEnableEl.checked = swEnabled; 5998 const swUserEl = document.getElementById('siteWideBasicAuthUsername'); 5999 if (swUserEl) swUserEl.value = swAuth && swAuth.username ? swAuth.username : ''; 6000 const swPassEl = document.getElementById('siteWideBasicAuthPassword');
6001 if (swPassEl) swPassEl.value = swAuth && swAuth.password ? swAuth.password : ''; 6002 const swRealmEl = document.getElementById('siteWideBasicAuthRealm'); 6003 if (swRealmEl) swRealmEl.value = swAuth && swAuth.realm ? swAuth.realm : ''; 6004 if (typeof updateSiteWideBasicAuthFieldState === 'function') updateSiteWideBasicAuthFieldState(); 6005 6006 // Set content language dropdown (defaults to 'en') 6007 const contentLanguageSelect = document.getElementById('contentLanguage'); 6008 if (contentLanguageSelect) { 6009 contentLanguageSelect.value = metadata.contentLanguage || 'en'; 6010 console.log('Loaded content language:', contentLanguageSelect.value); 6011 } 6012 6013 const faviconInput = document.getElementById('faviconUrl'); 6014 if (faviconInput) { 6015 // Check if we have a recently uploaded favicon URL 6016 const faviconValue = metadata.favicon || window.uploadedFaviconUrl || ''; 6017 faviconInput.value = faviconValue; 6018 console.warn('Set favicon input value to:', faviconInput.value); 6019 console.warn('Used global uploaded favicon URL:', window.uploadedFaviconUrl); 6020 } else { 6021 console.error('Favicon input element not found!'); 6022 } 6023 6024 // Show favicon preview if there's an existing favicon 6025 if (metadata.favicon) { 6026 console.warn('Showing favicon preview for:', metadata.favicon); 6027 showFaviconPreview(metadata.favicon); 6028 } else { 6029 console.warn('No favicon to preview'); 6030 } 6031 6032 const ogInput = document.getElementById('ogImageUrl'); 6033 if (ogInput) { 6034 const ogVal = (metadata.ogImageUrl || '').trim(); 6035 ogInput.value = ogVal; 6036 } 6037 const ogBtnText = document.getElementById('uploadOgImageBtnText'); 6038 if (ogBtnText) { 6039 ogBtnText.textContent = (metadata.ogImageUrl || '').trim() ? 'Change image' : 'Upload image'; 6040 } 6041 if ((metadata.ogImageUrl || '').trim()) { 6042 showOgImagePreview(metadata.ogImageUrl.trim()); 6043 } else { 6044 const ogCont = document.getElementById('ogImagePreviewContainer'); 6045 if (ogCont) ogCont.innerHTML = ''; 6046 } 6047 6048 console.log('loadMetadataValues: Google Analytics field value after setting:', document.getElementById('googleAnalytics').value); 6049} 6050 6051// Save metadata values 6052function saveMetadata() { 6053 // Get values from form 6054 const googleAnalyticsElement = document.getElementById('googleAnalytics'); 6055 console.log('Google Analytics element found:', !!googleAnalyticsElement); 6056 console.log('Google Analytics element value:', googleAnalyticsElement ? googleAnalyticsElement.value : 'ELEMENT NOT FOUND'); 6057 6058 const faviconInput = document.getElementById('faviconUrl'); 6059 const faviconValue = faviconInput ? faviconInput.value : ''; 6060 const ogImageInput = document.getElementById('ogImageUrl'); 6061 const ogImageValue = ogImageInput ? (ogImageInput.value || '').trim() : ''; 6062 6063 const contentLanguageSelect = document.getElementById('contentLanguage'); 6064 const contentLanguageValue = contentLanguageSelect ? contentLanguageSelect.value : 'en'; 6065 6066 const copyrightToggle = document.getElementById('metadataCopyrightEnabled'); 6067 const copyrightInput = document.getElementById('metadataCopyrightText'); 6068 const copyrightEnabled = copyrightToggle ? copyrightToggle.checked : false; 6069 const copyrightText = copyrightInput ? copyrightInput.value.trim() : ''; 6070 6071 console.warn('=== FAVICON SAVE DEBUG ==='); 6072 console.warn('Favicon input element exists:', !!faviconInput); 6073 console.warn('Favicon input value:', faviconValue); 6074 console.warn('Favicon input value length:', faviconValue.length); 6075 6076 console.warn('=== CONTENT LANGUAGE SAVE ==='); 6077 console.warn('Content language selected:', contentLanguageValue); 6078 6079 const rawGa = googleAnalyticsElement ? googleAnalyticsElement.value : ''; 6080 // Normalize GA to ID only (client may not have normalizeGoogleAnalyticsId in scope) 6081 let gaId = (rawGa && typeof rawGa === 'string') ? rawGa.trim() : ''; 6082 if (gaId && !/^G-[A-Z0-9]+$/i.test(gaId) && !/^UA-d+-d+$/.test(gaId)) { 6083 const match = gaId.match(/(G-[A-Z0-9]+|UA-d+-d+)/i); 6084 gaId = match ? match[1] : ''; 6085 } 6086 const customPubKeyInput = document.getElementById('customPublicSiteHtmlR2Key'); 6087 const customPubFilename = document.getElementById('customPublicSiteHtmlFilenameStore'); 6088 const schemaPersonRadio = document.getElementById('schemaTypePerson'); 6089 const schemaOrgRadio = document.getElementById('schemaTypeOrganization'); 6090 const schemaType = schemaPersonRadio && schemaPersonRadio.checked ? 'Person' 6091 : schemaOrgRadio && schemaOrgRadio.checked ? 'Organization' 6092 : ''; 6093 const schemaJobTitleEl = document.getElementById('schemaJobTitle'); 6094 const schemaCityEl = document.getElementById('schemaCity'); 6095 const schemaEmailEl = document.getElementById('schemaEmail'); 6096 const imageLicenseUrlEl = document.getElementById('imageLicenseUrl'); 6097 const acquireLicensePageUrlEl = document.getElementById('acquireLicensePageUrl'); 6098 const imageLicenseUrlRaw = imageLicenseUrlEl ? imageLicenseUrlEl.value.trim() : ''; 6099 const acquireLicensePageUrlRaw = acquireLicensePageUrlEl ? acquireLicensePageUrlEl.value.trim() : ''; 6100 6101 const normalizePublicAbsoluteUrl = (value) => { 6102 if (!value) return ''; 6103 try { 6104 const parsed = new URL(value); 6105 if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return ''; 6106 return parsed.href; 6107 } catch { 6108 return ''; 6109 } 6110 }; 6111 const imageLicenseUrl = normalizePublicAbsoluteUrl(imageLicenseUrlRaw); 6112 const acquireLicensePageUrl = normalizePublicAbsoluteUrl(acquireLicensePageUrlRaw); 6113 if ((imageLicenseUrlRaw || acquireLicensePageUrlRaw) && (!imageLicenseUrl || !acquireLicensePageUrl)) { 6114 alert('Image licensing: enter two public http(s) URLs, or leave both fields empty.'); 6115 return; 6116 } 6117 6118 const siteWideBasicAuthEnabled = !!(document.getElementById('siteWideBasicAuthEnabled') && document.getElementById('siteWideBasicAuthEnabled').checked); 6119 const swUser = document.getElementById('siteWideBasicAuthUsername') ? document.getElementById('siteWideBasicAuthUsername').value.trim() : ''; 6120 const swPassEl = document.getElementById('siteWideBasicAuthPassword');
6121 const swPass = swPassEl ? swPassEl.value : ''; 6122 const swRealm = document.getElementById('siteWideBasicAuthRealm') ? document.getElementById('siteWideBasicAuthRealm').value.trim() : ''; 6123 6124 if (siteWideBasicAuthEnabled && (!swUser || !swPass)) { 6125 alert('Site-wide password: enter both username and password, or turn off the option.'); 6126 return; 6127 } 6128 6129 const metadata = { 6130 title: document.getElementById('metaTitle').value, 6131 description: document.getElementById('metaDescription').value, 6132 googleAnalytics: gaId, 6133 noIndex: document.getElementById('noIndexToggle').checked, 6134 favicon: faviconValue, 6135 ogImageUrl: ogImageValue, 6136 contentLanguage: contentLanguageValue, 6137 copyrightEnabled, 6138 copyrightText, 6139 customPublicSiteEnabled: !!(document.getElementById('customPublicSiteEnabled') && document.getElementById('customPublicSiteEnabled').checked), 6140 customPublicSiteHtmlR2Key: customPubKeyInput ? customPubKeyInput.value.trim() : '', 6141 customPublicSiteHtmlFilename: customPubFilename ? customPubFilename.value.trim() : '', 6142 schemaType: schemaType || undefined, 6143 schemaJobTitle: schemaJobTitleEl ? schemaJobTitleEl.value.trim() : '', 6144 schemaCity: schemaCityEl ? schemaCityEl.value.trim() : '',
6145 schemaEmail: schemaEmailEl ? schemaEmailEl.value.trim() : '', 6146 ...(imageLicenseUrl && acquireLicensePageUrl ? { imageLicenseUrl, acquireLicensePageUrl } : {}) 6147 }; 6148 6149 if (siteWideBasicAuthEnabled && swUser && swPass) { 6150 metadata.siteWideBasicAuth = { 6151 username: swUser, 6152 password: swPass, 6153 ...(swRealm ? { realm: swRealm } : {}) 6154 }; 6155 } 6156 6157 console.warn('Metadata object being created:', JSON.stringify(metadata)); 6158 console.warn('Favicon in metadata object:', metadata.favicon); 6159 6160 // Show normalized GA ID in the form so user sees what was saved (e.g. after pasting full snippet) 6161 if (googleAnalyticsElement && metadata.googleAnalytics !== rawGa.trim()) { 6162 googleAnalyticsElement.value = metadata.googleAnalytics; 6163 } 6164 6165 // Update global metadata object immediately 6166 window.siteMetadata = metadata; 6167 console.log("Updated window.siteMetadata:", JSON.stringify(window.siteMetadata)); 6168 if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') { 6169 window.SidebarManager.updateMetadataFooter(); 6170 } 6171 6172 // Ensure we have the latest galleries, sidebar elements, and styles 6173 // before constructing the customData object. 6174 6175 // Synchronize galleries first if the function exists 6176 if (typeof synchronizeGalleries === 'function') { 6177 synchronizeGalleries(); 6178 console.log('Galleries synchronized within saveMetadata'); 6179 } 6180 6181 // Retrieve current galleries (use window.galleries as the source of truth after sync) 6182 const currentGalleries = window.galleries || galleries || []; 6183 console.log(`Current galleries count before constructing customData: ${currentGalleries.length}`); 6184 6185 // Retrieve current sidebar elements 6186 const currentSidebarElements = window.SidebarManager && typeof window.SidebarManager.getElementsForSave === 'function' 6187 ? window.SidebarManager.getElementsForSave() 6188 : null; 6189 6190 // Retrieve current menu styles 6191 const currentMenuStyles = window.MenuStyleCustomizer ? window.MenuStyleCustomizer.settings : {}; 6192 6193 // Now construct customData using the retrieved current state 6194 if (window.saveGalleries) { 6195 const customData = { 6196 galleries: currentGalleries, // Use the synchronized/retrieved galleries 6197 siteMetadata: metadata, // Use the metadata gathered from the form 6198 menuStyles: currentMenuStyles // Use the retrieved menu styles 6199 }; 6200 if (Array.isArray(currentSidebarElements) && currentSidebarElements.length > 0) { 6201 customData.sidebarElements = currentSidebarElements; 6202 } 6203 6204 6205 console.log(`Saving metadata with preserved galleries: ${customData.galleries.length}`); 6206 console.log("Metadata being saved:", JSON.stringify(metadata)); 6207 console.log("Google Analytics specifically:", metadata.googleAnalytics); 6208 console.log("Sidebar elements being saved:", Array.isArray(customData.sidebarElements) ? customData.sidebarElements.length : '(omitted)'); 6209 console.log("Menu styles being saved:", JSON.stringify(customData.menuStyles)); 6210 6211 // Save with our correctly constructed custom data object 6212 window.saveGalleries(customData) 6213 .then(() => { 6214 alert('Metadata saved successfully'); 6215 toggleMetadataEditor(); // Hide the editor 6216 }) 6217 .catch(error => { 6218 console.error('Error saving metadata:', error); 6219 alert('Error saving metadata. Please try again.'); 6220 }); 6221 } else { 6222 alert('Save function not available'); 6223 } 6224} 6225 6226 6227// Function to add new entries to the style editor 6228function showStyleEditor(display = false) { 6229 if (display && window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.openOverlay === 'function') { 6230 window.MenuStyleCustomizer.openOverlay(); 6231 window.currentOpenForm = 'menuStyleEditor'; 6232 return; 6233 } 6234 hideStyleEditor(); 6235} 6236 6237// New function to hide the style editor 6238function hideStyleEditor() { 6239 if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.closeOverlay === 'function') { 6240 window.MenuStyleCustomizer.closeOverlay(); 6241 } 6242 const styleEditor = document.getElementById('menuStyleEditor'); 6243 if (styleEditor) { 6244 styleEditor.style.display = 'none'; 6245 } 6246} 6247 6248 6249/** 6250 * Improved toggleAddForm - Closes other forms first and ensures proper sizing 6251 */ 6252function toggleAddForm() { 6253 const formId = 'addForm'; 6254 const form = document.getElementById(formId); 6255 6256 if (!form) return; 6257 6258 // If this form is already open, just close everything 6259 if (window.currentOpenForm === formId) { 6260 closeAllForms(); 6261 return; 6262 } 6263 6264 // Otherwise close all forms and open this one 6265 closeAllForms(); 6266 6267 // Determine the appropriate max-height based on content 6268 // First check if we're showing the page form by checking the radio button 6269 const isPageSelected = document.querySelector('input[name="galleryType"][value="page"]')?.checked; 6270 6271 // Allow extra space for the page form 6272 const maxHeight = isPageSelected ? '600px' : '500px'; 6273 6274 // Show the form with proper max height 6275 form.style.maxHeight = maxHeight; 6276 form.style.overflow = 'visible';
6276 // Ensure all content is visible 6277 form.classList.add('visible'); 6278 6279 // Also add inline styles to ensure visibility during animation 6280 form.style.padding = '15px'; 6281 6282 // Force recalculation of max-height after UI is visible 6283 setTimeout(() => { 6284 // Get actual content height + padding 6285 const contentHeight = form.scrollHeight + 30; // Add padding 6286 6287 // Set max-height based on content with minimum of 500px 6288 form.style.maxHeight = Math.max(contentHeight, 500) + 'px'; 6289 6290 console.log(`Setting add form max height to ${form.style.maxHeight}`); 6291 }, 50); 6292 6293 // Populate parent options 6294 const parentSelect = document.getElementById('galleryParent'); 6295 if (parentSelect) { 6296 parentSelect.innerHTML = '<option value="">None (Top Level)</option>'; 6297 6298 // Add all galleries that could be parents (including submenus) 6299 galleries.forEach(gallery => { 6300 parentSelect.innerHTML += `<option value="${gallery.id}">${gallery.title}</option>`; 6301 }); 6302 } 6303 6304 // Set this as the current open form 6305 window.currentOpenForm = formId; 6306} 6307 6308 6309// Store the currently open form ID 6310window.currentOpenForm = null; 6311 6312/** 6313 * Close a child panel opened on top of the Style Customizer (palette explorer, custom code, color mixer). 6314 * Returns true when a child was closed so callers can keep the Style Customizer open. 6315 */ 6316function closeTopmostStyleCustomizerChildOverlay() { 6317 if (document.querySelector('.pe-palette-explorer-overlay')) { 6318 if (window.PaletteExplorer && typeof window.PaletteExplorer.close === 'function') { 6319 const wasOpen = !!document.querySelector('.pe-palette-explorer-overlay'); 6320 window.PaletteExplorer.close(); 6321 if (wasOpen && document.querySelector('.pe-palette-explorer-overlay')) { 6322 return false; 6323 } 6324 return wasOpen; 6325 } 6326 } 6327 if (document.querySelector('.pe-style-history-overlay, .sc-style-history-overlay') && 6328 window.MenuStyleCustomizer && 6329 typeof window.MenuStyleCustomizer.closeStyleHistoryOverlay === 'function') { 6330 window.MenuStyleCustomizer.closeStyleHistoryOverlay(); 6331 return true; 6332 } 6333 if (document.querySelector('.pe-font-explorer-overlay') && 6334 window.MenuStyleCustomizer && 6335 typeof window.MenuStyleCustomizer.closeFontExplorer === 'function') { 6336 window.MenuStyleCustomizer.closeFontExplorer(); 6337 return true; 6338 } 6339 if (document.querySelector('.pe-custom-code-overlay') && 6340 window.MenuStyleCustomizer && 6341 typeof window.MenuStyleCustomizer.closeCustomCodeOverlay === 'function') { 6342 window.MenuStyleCustomizer.closeCustomCodeOverlay(); 6343 return true; 6344 } 6345 const colorMixer = document.getElementById('colorMixerPopup'); 6346 if (colorMixer && colorMixer.classList.contains('is-open')) { 6347 colorMixer.classList.remove('is-open'); 6348 return true; 6349 } 6350 return false; 6351} 6352 6353/** 6354 * Close all editor forms with improved handling for add form 6355 */ 6356function closeAllForms() { 6357 if (typeof closeTopmostStyleCustomizerChildOverlay === 'function') { 6358 while (closeTopmostStyleCustomizerChildOverlay()) { 6359 // Close nested Style Customizer panels (palette, custom code, color mixer) first. 6360 } 6361 } 6362 6363 const keepMinimizedStyleCustomizer = !!(window.MenuStyleCustomizer && 6364 window.MenuStyleCustomizer._overlayOpen && 6365 window.MenuStyleCustomizer._overlayMinimized); 6366 6367 if (!keepMinimizedStyleCustomizer && 6368 window.MenuStyleCustomizer && 6369 typeof window.MenuStyleCustomizer.closeOverlay === 'function') { 6370 const styleOverlayWasOpen = window.MenuStyleCustomizer._overlayOpen; 6371 window.MenuStyleCustomizer.closeOverlay(); 6372 if (styleOverlayWasOpen && window.MenuStyleCustomizer._overlayOpen) { 6373 return; 6374 } 6375 } 6376 6377 // Add Form - Special handling to ensure proper animation 6378 const addForm = document.getElementById('addForm'); 6379 if (addForm) { 6380 addForm.classList.remove('visible'); 6381 6382 // Also force style reset after animation completes 6383 setTimeout(() => { 6384 if (!addForm.classList.contains('visible')) { 6385 addForm.style.maxHeight = '0'; 6386 addForm.style.overflow = 'hidden'; 6387 addForm.style.padding = '0 15px'; 6388 } 6389 }, 350); // Set slightly longer than CSS transition 6390 } 6391 6392 // Style Editor (do not hide the overlay host while the minimized mutate bar is live) 6393 const styleEditor = document.getElementById('menuStyleEditor'); 6394 if (styleEditor && !keepMinimizedStyleCustomizer) { 6395 styleEditor.style.display = 'none'; 6396 } 6397 6398 // Metadata Editor 6399 const metadataEditor = document.getElementById('metadataEditor'); 6400 if (metadataEditor) { 6401 metadataEditor.style.display = 'none'; 6402 } 6403 if (typeof syncCustomPublicSiteBypassVisibility === 'function') { 6404 syncCustomPublicSiteBypassVisibility(); 6405 } 6406 6407 // Sidebar Element Form 6408 const sidebarElementForm = document.getElementById('sidebarElementForm'); 6409 if (sidebarElementForm) { 6410 sidebarElementForm.style.display = 'none'; 6411 sidebarElementForm.classList.remove('visible'); 6412 } 6413 6414 // Import Classic Form 6415 const importClassicForm = document.getElementById('importClassicForm'); 6416 if (importClassicForm) { 6417 importClassicForm.style.display = 'none'; 6418 importClassicForm.classList.remove('visible'); 6419 } 6420 6421 // Clear the current open form 6422 window.currentOpenForm = null; 6423} 6424function downloadJson() { 6425 const data = JSON.stringify(galleries, null, 2); 6426 const blob = new Blob([data], { type: 'application/json' }); 6427 const url = URL.createObjectURL(blob); 6428 const a = document.createElement('a'); 6429 a.href = url; 6430 a.download = 'galleries.json'; 6431 document.body.appendChild(a); 6432 a.click(); 6433 document.body.removeChild(a); 6434 URL.revokeObjectURL(url); 6435} 6436 6437let galleriesBackup = []; 6438 6439 6440 6441/** 6442 * Improved version of initializeNestedSortables with a better nesting approach 6443 */ 6444function initializeNestedSortables() { 6445 console.log('Initializing nested sortables with improved nesting logic...'); 6446 6447 // Destroy leftover instances before binding again. A second Sortable on the 6448 // same list makes items preview during drag and snap back on release. 6449 document.querySelectorAll('#galleryTree .nested-sortable').forEach((el) => { 6450 try { 6451 const existing = typeof Sortable !== 'undefined' && Sortable.get ? Sortable.get(el) : null; 6452 if (existing && typeof existing.destroy === 'function') existing.destroy(); 6453 } catch (error) { 6454 console.warn('Could not destroy existing nested sortable:', error); 6455 } 6456 }); 6457 6458 // Create a backup of galleries 6459 try { 6460 galleriesBackup = JSON.parse(JSON.stringify(galleries)); 6461 } catch (error) { 6462 console.error('Error creating galleries backup:', error); 6463 galleriesBackup = []; 6464 } 6465 6466 // Find all nested sortable lists 6467 const nestedSortables = document.querySelectorAll('.nested-sortable'); 6468 if (nestedSortables.length === 0) { 6469 console.warn('No nested sortable elements found! Check your HTML
6469structure.'); 6470 return; 6471 } 6472 6473 // Helper: compute depth based on ancestor nested lists 6474 function computeDepth(liEl) { 6475 let depth = 0; 6476 let node = liEl; 6477 while (node) { 6478 const parentOl = node.closest('ol.nested-sortable'); 6479 if (!parentOl) break; 6480 const parentLi = parentOl.closest('li'); 6481 if (parentLi) { 6482 depth += 1; 6483 node = parentLi; 6484 } else { 6485 break; 6486 } 6487 } 6488 // Top-level depth should be 0 6489 return Math.max(depth - 1, 0); 6490 } 6491 6492 // Helper: apply indentation styling to an li and its subtree 6493 function applyIndentationFromDepth(rootLi) { 6494 if (!rootLi) return; 6495 const applyToItem = (li) => { 6496 const depth = computeDepth(li); 6497 // Update data-nesting-level attribute 6498 li.setAttribute('data-nesting-level', depth); 6499 // Apply padding-left style 6500 const content = li.querySelector('.gallery-item-content'); 6501 if (content) { 6502 content.style.paddingLeft = (depth * 16) + 'px'; 6503 } 6504 }; 6505 applyToItem(rootLi); 6506 rootLi.querySelectorAll('li').forEach(applyToItem); 6507 } 6508 6509 // Helper: ensure parent has proper state and nested list 6510 function ensureParentChildState(parentLi) { 6511 if (!parentLi) return null; 6512 parentLi.classList.add('has-children'); 6513 let nestedList = parentLi.querySelector('ol.nested-sortable'); 6514 if (!nestedList) { 6515 nestedList = document.createElement('ol'); 6516 nestedList.className = 'nested-sortable'; 6517 parentLi.appendChild(nestedList); 6518 } 6519 return nestedList; 6520 } 6521 6522 // Helper: clean parent if it has no children 6523 function cleanupParentIfEmpty(parentLi) { 6524 if (!parentLi) return; 6525 const nestedList = parentLi.querySelector(':scope > ol.nested-sortable'); 6526 const parentId = parseInt(parentLi.dataset && parentLi.dataset.id, 10); 6527 const parentItem = !isNaN(parentId) ? galleries.find(g => g.id === parentId) : null; 6528 // Opted-in pages/galleries must keep an empty drop zone and chevron. 6529 if (isNavigableMenuParent(parentItem)) { 6530 parentLi.classList.add('has-children'); 6531 return; 6532 } 6533 if (nestedList && nestedList.children.length === 0) { 6534 nestedList.remove(); 6535 parentLi.classList.remove('has-children'); 6536 } 6537 } 6538 6539 // Initialize each sortable with improved nesting settings 6540 let sortableInstances = []; 6541 6542 for (let i = 0; i < nestedSortables.length; i++) { 6543 try { 6544 const container = nestedSortables[i]; 6545 6546 // Track where the placeholder indicates the item should be placed 6547 // This helps us detect when SortableJS incorrectly auto-nests based on mouse position 6548 let intendedPlacementContainer = null; 6549 let intendedRelatedElement = null; 6550 6551 // Track last move to prevent oscillation 6552 let lastMoveTime = 0; 6553 let lastMoveRelatedId = null; 6554 const MOVE_DEBOUNCE_MS = 50; // Minimum time between moves (in milliseconds) 6555 6556 // Track if we're dragging the last item in a folder (to prevent boundary oscillation) 6557 let isLastItemInFolder = false; 6558 let draggedItemOriginalContainer = null; 6559 6560 // Track container changes to prevent rapid switching (hysteresis) 6561 let firstPositionFixLoopActive = false; // Prevent multiple fix loops 6562 let lastContainerDecision = null; // 'inside' or 'outside' 6563 let lastContainerDecisionTime = 0; 6564 const CONTAINER_SWITCH_DEBOUNCE_MS = 150; // Longer debounce for container switches 6565 6566 const sortable = new Sortable(container, { 6567 group: 'nested', 6568 animation: 150, // Reduced animation during drag to reduce lag 6569 easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', // Smooth easing function 6570 fallbackOnBody: false, // Disable forced fallback on desktop for better performance 6571 forceFallback: false, // Use native drag on desktop 6572 6573 // CRITICAL: These settings make nesting much easier 6574 // Higher swap threshold makes it easier to drag over items 6575 swapThreshold: 0.65, // Increased to reduce oscillation within same folder 6576 6577 // IMPROVED NESTING SETTINGS 6578 // Used when determining if an item should be nested 6579 invertSwap: false, // Disable to reduce oscillation 6580 direction: 'vertical', // Lock direction to vertical to reduce oscillation 6581 6582 // Use handle for dragging 6583 handle: '.nested-sortable-handle', 6584 6585 // Filter out only the toggle button - this prevents SortableJS from intercepting clicks on it 6586 // The folder text (.menu-item.folder) is NOT filtered, so it can be used for both: 6587 // - Quick click â toggles folder (onclick handler fires before drag starts) 6588 // - Click and drag â drags the item (SortableJS handles the drag) 6589 // The delay helps distinguish between click and drag, but on desktop with delayOnTouchOnly: true, 6590 // the onclick should fire quickly enough before SortableJS intercepts 6591 filter: '.submenu-toggle, .submenu-toggle *, .toggle-icon', 6592 preventOnFilter: true, // Prevent drag when clicking filtered elements (toggle button only) 6593 6594 // Distinguish between click and drag 6595 delay: 120, 6596 delayOnTouchOnly: true, 6597 6598 // Empty container settings 6599 emptyInsertThreshold: 10, 6600 6601 // Add custom ghost class 6602 ghostClass: 'sortable-ghost', 6603 chosenClass: 'sortable-chosen', 6604 dragClass: 'sortable-drag', 6605 6606 // Make sure we listen to external move events 6607 dragoverBubble: true, 6608 6609 // Better drag preview stability 6610 scroll: true, 6611 scrollSensitivity: 100, 6612 scrollSpeed: 20, 6613 6614 // Reduce oscillation by making placeholder movement less sensitive 6615 forceFallback: false, // Use native drag for better performance 6616 fallbackTolerance: 0, // No tolerance - use exact position 6617 6618 /** 6619 * NESTING LOGIC: Prevent auto-nesting based on hover 6620 * Instead, rely on SortableJS's placeholder position which is accurate 6621 * We'll verify and correct placement in onEnd based on actual DOM position 6622 */ 6623 onMove: function(evt, originalEvent) { 6624 const dragged = evt.dragged; 6625 const related = evt.related; 6626 6627 if (!dragged || !related) return true; 6628 6629 // Safety check: Ensure items have data-id attributes 6630 // This can be missing if item was just added and DOM not fully updated 6631 if (!dragged.dataset.id || !related.dataset.id) { 6632 console.warn('â ï¸ [Drag] Item missing data-id attribute, aborting drag', { 6633 draggedHasId: !!dragged.dataset.id, 6634 relatedHasId: !!related.dataset.id 6635 }); 6636 return false;
6637 } 6638 6639 // Get the IDs to check for circular references 6640 const draggedId = parseInt(dragged.dataset.id); 6641 const relatedId = parseInt(related.dataset.id); 6642 6643 // Don't allow dropping inside itself or its descendants 6644 if (draggedId === relatedId || hasAncestor(relatedId, draggedId)) { 6645 return false; 6646 } 6647 6648 const currentTime = Date.now(); 6649 const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); 6650 const draggedParent = draggedItemOriginalContainer || dragged.parentElement; 6651 6652 // Determine if we're moving inside or outside a folder 6653 // IMPORTANT: Check if related element IS the folder item itself (parent li) 6654 // When targeting first position, SortableJS might use the folder item as related 6655 const isRelatedFolderItem = related.classList && related.classList.contains('has-children'); 6656 6657 // CRITICAL: Handle empty folder case FIRST - before any other logic 6658 // When dragging into an empty folder, related is the folder item itself 6659 // This is essentially the same as targeting first position, but for an empty folder 6660 // We need to allow this immediately, bypassing all debounce and boundary checks 6661 if (isRelatedFolderItem) { 6662 const folderNestedSortable = related.querySelector(':scope > ol.nested-sortable'); 6663 const relatedIsNavigableParent = related.classList.contains('navigable-parent-item'); 6664 // Navigable parents look like normal links. Only treat them as an 6665 // empty folder when the pointer is actually over the empty list. 6666 let allowEmptyFolderInsert = !!(folderNestedSortable && folderNestedSortable.children.length === 0); 6667 if (allowEmptyFolderInsert && relatedIsNavigableParent && originalEvent) { 6668 const listRect = folderNestedSortable.getBoundingClientRect(); 6669 const pointerY = originalEvent.clientY; 6670 allowEmptyFolderInsert = pointerY >= listRect.top - 6 && pointerY <= listRect.bottom + 10; 6671 } 6672 if (allowEmptyFolderInsert) { 6673 // Empty folder detected - this is a drop into empty folder (first position) 6674 // Always allow it, bypass all debounce and boundary logic 6675 lastMoveTime = currentTime; 6676 lastMoveRelatedId = relatedId; 6677 intendedPlacementContainer = folderNestedSortable; 6678 intendedRelatedElement = null; // No related element in empty folder 6679 // Mark as moving inside folder for tracking 6680 lastContainerDecision = 'inside'; 6681 lastContainerDecisionTime = currentTime; 6682 6683 // CRITICAL: Manually move placeholder into empty folder's nested-sortable 6684 // SortableJS might not do this automatically for empty containers 6685 requestAnimationFrame(() => { 6686 const placeholder = document.querySelector('.sortable-ghost'); 6687 if (placeholder && placeholder.parentElement !== folderNestedSortable) { 6688 // Move placeholder into the empty folder 6689 folderNestedSortable.appendChild(placeholder); 6690 } 6691 6692 // Update ghost indentation for empty folder 6693 const ghost = document.querySelector('.sortable-ghost'); 6694 if (ghost) { 6695 const ghostContent = ghost.querySelector('.gallery-item-content'); 6696 if (ghostContent) { 6697 // Calculate depth from parent folder 6698 const parentFolder = folderNestedSortable.closest('li.has-children'); 6699 if (parentFolder) { 6700 const targetDepth = computeDepth(parentFolder) + 1; 6701 ghostContent.style.paddingLeft = (targetDepth * 16) + 'px'; 6702 } 6703 } 6704 } 6705 }); 6706 6707 // Return true immediately to allow the move 6708 return true; 6709 } 6710 } 6711 6712 let actualRelatedParent = related.parentElement; 6713 6714 // If related IS the folder item, we need to get its nested sortable container 6715 if (isRelatedFolderItem) { 6716 const nestedSortable = related.querySelector('ol.nested-sortable'); 6717 if (nestedSortable) { 6718 actualRelatedParent = nestedSortable; 6719 } 6720 } 6721 6722 const relatedParent = actualRelatedParent; 6723 6724 const isMovingInsideFolder = relatedParent && 6725 relatedParent.classList.contains('nested-sortable') && 6726 relatedParent !== rootContainer; 6727 const isMovingOutsideFolder = !isRelatedFolderItem && relatedParent === rootContainer; 6728 const currentDecision = isMovingInsideFolder ? 'inside' : (isMovingOutsideFolder ? 'outside' : null); 6729 6730 // Determine if we're moving within the same folder - this is the key check 6731 // If related IS the folder item, we're still moving within the same folder 6732 let isMovingWithinSameFolder = (isMovingInsideFolder && 6733 draggedParent && 6734 draggedParent.classList.contains('nested-sortable') && 6735 draggedParent !== rootContainer && 6736 relatedParent === draggedParent) || 6737 // OR: if related is the folder item itself and we're dragging from that folder 6738 (isRelatedFolderItem && 6739 draggedParent && 6740 draggedParent.classList.contains('nested-sortable') && 6741 draggedParent !== rootContainer && 6742 related.querySelector('ol.nested-sortable') === draggedParent); 6743 6744 // Special case: If related is the folder item, we're definitely trying to move within that folder 6745 // This happens when targeting first position - SortableJS uses the folder item as related 6746 if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { 6747 const folderNestedSortable = related.querySelector('ol.nested-sortable'); 6748 if (folderNestedSortable === draggedParent) { 6749 // We're dragging within the folder, and related is the folder item 6750 // This means we're targeting first position - FORCE it to be treated as same-folder move 6751 // FORCE same-folder detection - override any previous calculation 6752 isMovingWithinSameFolder = true; 6753 } 6754 } 6755 6756 // DEBUG: Log placeholder position changes ONLY when dragging from within a folder AND something interesting happens 6757 // (Don't log every single move - too noisy) 6758 if (draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { 6759 const folderItems = Array.from(draggedParent.children); 6760 const draggedIndex = folderItems.indexOf(dragged); 6761 const relatedIndex = folderItems.indexOf(related); 6762 6763 // Get placeholder element to see where it actually is 6764 const placeholder = document.querySelector('.sortable-ghost'); 6765 const placeholderParent = placeholder ? placeholder.parentElement : null; 6766 const placeholderIndex = placeholderParent && placeholderParent.classList.contains('nested-sortable') 6767 ? Array.from(placeholderParent.children).indexOf(placeholder) 6768 : -1; 6769 6770 const folderItem = draggedParent.closest('li.has-children'); 6771 const folderItemId = folderItem ? folderItem.dataset.id : null; 6772 const firstItemInFolder = folderItems.length > 0 ? folderItems[0] : null; 6773 const firstItemInFolderId = firstItemInFolder ? firstItemInFolder.dataset.id : null; 6774 6775 // Check if we're trying to target first position 6776 // related could be the first item OR the folder item itself (when SortableJS uses folder as related) 6777 const isFirstPosition = folderItems.length > 0 && related === firstItemInFolder; 6778 const isRelatedFolderItemCheck = related === folderItem; 6779 const isTargetingFirstViaFolderItem = isRelatedFolderItemCheck && !!folderItem; // FIXED: was assigning folderItem instead of boolean 6780 6781 // When targeting first via folder item, placeholder should be at index 0 6782 // But we're seeing it at index 1 - this is the issue! 6783 const shouldBeAtFirstPosition = isTargetingFirstViaFolderItem && placeholderIndex === 1 && draggedIndex > 0; 6784 6785 // Check if placeholder is outside the folder when it should be inside 6786 const placeholderIsOutside = placeholderParent === rootContainer || (placeholderParent && !placeholderParent.classList.contains('nested-sortable')); 6787 const shouldBeInside = draggedIndex >= 0; // We're dragging from inside 6788 6789 // Only log when there's a potential issue or interesting state change 6790 const placeholderMismatch = placeholderIndex !== relatedIndex && placeholderIndex >= 0 && relatedIndex >= 0; 6791 const jumpingOut = !isMovingWithinSameFolder && draggedIndex >= 0 && relatedIndex < 0; 6792 const jumpingOutToRoot = placeholderIsOutside && shouldBeInside; 6793 6794 // First position issue: trying to target first but: 6795 // - placeholder is outside OR 6796 // - placeholder is at index 1 instead of 0 (when targeting first via folder item) 6797 const firstPositionIssue = (isFirstPosition || isTargetingFirstViaFolderItem) && (jumpingOut || jumpingOutToRoot || placeholderIsOutside || shouldBeAtFirstPosition); 6798 6799 // ALWAYS log when targeting first position via folder item (this is the key case we're debugging) 6800 // OR when placeholder jumps out when it should stay in 6801 // OR when placeholder should be at first position but isn't 6802 const shouldLog = placeholderMismatch || jumpingOut || jumpingOutToRoot || firstPositionIssue || isTargetingFirstViaFolderItem || shouldBeAtFirstPosition; 6803 6804 if (shouldLog) { 6805 console.error('ð [Placeholder Position]', { 6806 draggedId, 6807 draggedIndex, 6808 relatedId, 6809 relatedIndex, 6810 placeholderIndex, 6811 relatedElementTag: related.tagName, 6812 isRelatedFolderItem: isRelatedFolderItemCheck, 6813 isTargetingFirstViaFolderItem, 6814 isMovingWithinSameFolder, 6815 placeholderMismatch, 6816 jumpingOut, 6817 jumpingOutToRoot, 6818 isFirstPosition, 6819 placeholderIsOutside, 6820 placeholderParentType: placeholderParent === rootContainer ? 'ROOT' : (placeholderParent && placeholderParent.classList.contains('nested-sortable') ? 'FOLDER' : 'OTHER'), 6821 folderItemId, 6822 firstItemInFolderId, 6823 firstPositionIssue, 6824 shouldBeAtFirstPosition, 6825 expectedPlaceholderIndex: isTargetingFirstViaFolderItem ? 0 : placeholderIndex 6826 }); 6827 } 6828 } 6829 6830 // CRITICAL: For moves within the same folder, skip ALL the complex boundary logic 6831 // This allows smooth reordering just like items not in folders 6832 if (isMovingWithinSameFolder) { 6833 // Simple debounce only - same as root level items 6834 const timeSinceLastMove = currentTime - lastMoveTime; 6835 // Only prevent very rapid oscillation to same element 6836 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5 && relatedId === lastMoveRelatedId) { 6837 return false;
6838 } 6839 // Allow all moves within same folder - update tracking and continue 6840 6841 // SPECIAL FIX: When targeting first position via folder item, fix placeholder position 6842 // SortableJS places it at index 1 when related is the folder item, but we want it at index 0 6843 // BUT: Only apply this fix when actually near/at first position to avoid being too aggressive 6844 if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { 6845 const folderItems = Array.from(draggedParent.children); 6846 const firstItem = folderItems.length > 0 ? folderItems[0] : null; 6847 6848 // Only apply fix if we're actually near first position (draggedIndex is small, indicating we're moving up) 6849 // This prevents the fix from being too aggressive when moving to other positions 6850 const isNearFirstPosition = draggedIndex <= 2; // Allow fix when within first 3 positions 6851 6852 if (firstItem && firstItem !== dragged && isNearFirstPosition) { 6853 // Fix immediately (synchronously) first - don't wait for next frame 6854 const placeholder = document.querySelector('.sortable-ghost'); 6855 if (placeholder && placeholder.parentElement === draggedParent) { 6856 const currentIndex = Array.from(draggedParent.children).indexOf(placeholder); 6857 // Only fix if placeholder is at index 1 when we're targeting first position 6858 // Don't force it if it's at index 0 or beyond index 2 (user might be moving to position 2 or 3) 6859 if (currentIndex === 1 && draggedIndex <= 1) { 6860 draggedParent.insertBefore(placeholder, firstItem); 6861 } 6862 } 6863 } else { 6864 // Not near first position - stop any active fix loop 6865 firstPositionFixLoopActive = false; 6866 } 6867 } else { 6868 // Not targeting first position anymore - reset flag 6869 firstPositionFixLoopActive = false; 6870 } 6871 6872 lastMoveTime = currentTime; 6873 lastMoveRelatedId = relatedId; 6874 // Skip all the boundary case logic below - just continue to allow the move 6875 // Fall through to update intendedPlacementContainer and return true 6876 } else { 6877 // Only apply complex boundary logic for moves BETWEEN folders or to/from root 6878 6879 // Hysteresis: Prevent rapid switching between inside/outside folder 6880 const targetFolderItems = isMovingInsideFolder ? Array.from(relatedParent.children) : []; 6881 const isFirstPositionInFolder = isMovingInsideFolder && 6882 targetFolderItems.length > 0 && 6883 related === targetFolderItems[0]; 6884 // Special case: Empty folder - allow easy dropping into it 6885 const isEmptyFolder = isMovingInsideFolder && targetFolderItems.length === 0; 6886 6887 if (currentDecision && lastContainerDecision && currentDecision !== lastContainerDecision) { 6888 const timeSinceContainerSwitch = currentTime - lastContainerDecisionTime; 6889 6890 // Make first position easier to target from outside 6891 if (isFirstPositionInFolder) { 6892 // Moving to first position from outside - use shorter debounce 6893 const effectiveDebounce = CONTAINER_SWITCH_DEBOUNCE_MS * 0.3; 6894 if (timeSinceContainerSwitch < effectiveDebounce) {
6895 // Only block if it's very rapid (oscillation) 6896 if (timeSinceContainerSwitch < MOVE_DEBOUNCE_MS) { 6897 return false; 6898 } 6899 } 6900 } else if (isEmptyFolder) { 6901 // Empty folder - allow easy dropping, minimal debounce 6902 // Only block very rapid oscillation 6903 if (timeSinceContainerSwitch < MOVE_DEBOUNCE_MS * 0.2) { 6904 return false; 6905 } 6906 } else { 6907 // Normal container switch - apply full hysteresis 6908 if (timeSinceContainerSwitch < CONTAINER_SWITCH_DEBOUNCE_MS) { 6909 return false; 6910 } 6911 } 6912 } 6913 } 6914 6915 // Special handling for boundary cases (last item in folder, or dragging into folder) 6916 // BUT: Skip all this logic if we're moving within the same folder (already handled above) 6917 if (!isMovingWithinSameFolder && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { 6918 const folderItem = draggedParent.closest('li.has-children'); 6919 const currentFolderItems = Array.from(draggedParent.children); 6920 const isFirstPositionInCurrentFolder = currentFolderItems.length > 0 && 6921 currentFolderItems[0] !== dragged && 6922 (related === currentFolderItems[0] || 6923 (isMovingInsideFolder && relatedParent === draggedParent && 6924 related === currentFolderItems[0])); 6925 6926 // Case 0: This case is now handled earlier for same-folder moves 6927 // Only handle cross-folder moves here 6928 6929 // Case 1: Dragging last item out of folder 6930 if (isLastItemInFolder && isMovingOutsideFolder) { 6931 const timeSinceLastMove = currentTime - lastMoveTime; 6932 6933 // BUT: If we're trying to move to first position, don't block it 6934 // Check if the related element is the folder itself or right after it 6935 if (folderItem) { 6936 const folderNextSibling = folderItem.nextElementSibling; 6937 // If we're moving to position right after folder, it might be trying to go to first position 6938 // Allow it if it's been a reasonable time 6939 if (folderNextSibling && related === folderNextSibling) { 6940 // This is ambiguous - could be first position or outside 6941 // Require more time but not as much as pure outside move 6942 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) { 6943 if (relatedId === lastMoveRelatedId) { 6944 return false; 6945 } 6946 } 6947 } else { 6948 // Definitely moving outside - require longer debounce 6949 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) { 6950 if (relatedId === lastMoveRelatedId) { 6951 return false; // Reject if oscillating 6952 } 6953 } 6954 } 6955 } else { 6956 // No folder item found - apply standard debounce 6957 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) { 6958 if (relatedId === lastMoveRelatedId) { 6959 return false; 6960 } 6961 } 6962 } 6963 } 6964 6965 // Case 2: Dragging item into folder from outside 6966 if (isMovingInsideFolder && relatedParent !== draggedParent) { 6967 const targetFolderItems = Array.from(relatedParent.children); 6968 const isEmptyFolder = targetFolderItems.length === 0; 6969 const isNearBottom = targetFolderItems.length > 0 && 6970 (related === targetFolderItems[targetFolderItems.length - 1] || 6971 targetFolderItems.indexOf(related) >= targetFolderItems.length - 1); 6972 const isFirstPos = targetFolderItems.length > 0 && related === targetFolderItems[0]; 6973 6974 // Special handling for empty folder - allow easy dropping 6975 if (isEmptyFolder) { 6976 const timeSinceLastMove = currentTime - lastMoveTime; 6977 // Very minimal debounce for empty folder - only prevent rapid oscillation 6978 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.2 && relatedId === lastMoveRelatedId) { 6979 return false;
6980 } 6981 // Allow move into empty folder - don't block it 6982 } else if (isFirstPos) { 6983 // Moving to first position - make it easy 6984 const timeSinceLastMove = currentTime - lastMoveTime; 6985 // Very short debounce for first position 6986 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.3 && relatedId === lastMoveRelatedId) { 6987 return false; 6988 } 6989 // Allow move to first position 6990 } else if (isNearBottom) { 6991 // For bottom position, still use some debounce to prevent oscillation 6992 const timeSinceLastMove = currentTime - lastMoveTime; 6993 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.5) { 6994 if (relatedId === lastMoveRelatedId) { 6995 return false; 6996 } 6997 } 6998 } 6999 } 7000 7001 // Case 2.5: Prevent jumping OUT of folder when trying to move to first/last position within 7002 // This is tricky - SortableJS might detect it as outside when we're actually targeting first/last 7003 if (isMovingOutsideFolder && folderItem && draggedParent === draggedItemOriginalContainer) { 7004 // We're dragging from within a folder but detected as moving outside 7005 // Check if the related element is adjacent to the folder (which might indicate targeting first/last) 7006 const folderIndex = Array.from(rootContainer.children).indexOf(folderItem); 7007 const relatedIndex = Array.from(rootContainer.children).indexOf(related); 7008 7009 // If related is immediately before or after folder, might be targeting first/last 7010 if (folderIndex >= 0 && relatedIndex >= 0) { 7011 const isAdjacentToFolder = (relatedIndex === folderIndex - 1) || (relatedIndex === folderIndex + 1); 7012 7013 if (isAdjacentToFolder) { 7014 // Might be targeting first/last position - be lenient 7015 const timeSinceLastMove = currentTime - lastMoveTime;
7016 // Only block if it's very rapid oscillation 7017 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.4 && relatedId === lastMoveRelatedId) { 7018 return false; 7019 } 7020 // Otherwise allow - user might be trying to target first/last position 7021 // Don't block based on this check 7022 } 7023 } 7024 } 7025 } 7026 7027 // Case 3: Dragging from root into folder 7028 if (draggedParent === rootContainer && isMovingInsideFolder) { 7029 const targetFolderItems = Array.from(relatedParent.children); 7030 const isEmptyFolder = targetFolderItems.length === 0; 7031 const isNearBottom = targetFolderItems.length > 0 && 7032 (related === targetFolderItems[targetFolderItems.length - 1] || 7033 targetFolderItems.indexOf(related) >= targetFolderItems.length - 1); 7034 const isFirstPosition = targetFolderItems.length > 0 && related === targetFolderItems[0]; 7035 7036 // Special handling for empty folder - allow easy dropping 7037 if (isEmptyFolder) { 7038 const timeSinceLastMove = currentTime - lastMoveTime; 7039 // Very minimal debounce for empty folder - only prevent rapid oscillation 7040 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.2) { 7041 if (relatedId === lastMoveRelatedId) { 7042 return false; 7043 } 7044 } 7045 // Allow move into empty folder - don't block it 7046 } else if (isFirstPosition) { 7047 // Special handling for first position - make it easier to target 7048 const timeSinceLastMove = currentTime - lastMoveTime; 7049 // Make first position easier - only prevent very rapid oscillation 7050 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5) { 7051 if (relatedId === lastMoveRelatedId) { 7052 return false; 7053 } 7054 } 7055 // Allow move to first position - don't block it 7056 // Continue to track but don't apply strict debouncing 7057 } else if (isNearBottom) { 7058 // Keep strict debouncing for bottom position 7059 const timeSinceLastMove = currentTime - lastMoveTime; 7060 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) { 7061 if (relatedId === lastMoveRelatedId) { 7062 return false; 7063 } 7064 } 7065 } 7066 } 7067 7068 // Case 4: Prevent jumping FROM first position to outside folder (but allow easy access TO first position) 7069 // If we're at first position and trying to move outside, require deliberate movement 7070 if (isMovingInsideFolder) { 7071 const currentFolderItems = Array.from(relatedParent.children); 7072 const isFirstPos = currentFolderItems.length > 0 && related === currentFolderItems[0]; 7073 7074 // If we're moving FROM first position to outside (reverse hysteresis) 7075 if (isFirstPos && lastContainerDecision === 'outside') { 7076 const timeSinceLastMove = currentTime - lastMoveTime; 7077 // If we just moved outside, require more time before allowing back to first position 7078 // BUT: if it's been a reasonable time, allow it (don't make it too hard) 7079 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.8 && relatedId === lastMoveRelatedId) { 7080 // Very rapid - might be oscillation 7081 return false; 7082 } 7083 } 7084 } 7085 7086 // Case 5: Prevent oscillation between first and second position within folder 7087 // BUT: Only apply if NOT moving within same folder (to allow smooth reordering) 7088 if (isMovingInsideFolder && relatedParent && !isMovingWithinSameFolder) { 7089 const folderItems = Array.from(relatedParent.children); 7090 const isFirstPos = folderItems.length > 0 && related === folderItems[0]; 7091 const isSecondPos = folderItems.length > 1 && related === folderItems[1]; 7092 7093 // If moving from first to second (or vice versa) very rapidly, prevent oscillation 7094 if ((isFirstPos || isSecondPos) && lastMoveRelatedId) { 7095 const lastRelated = document.querySelector('#galleryTree li[data-id="' + lastMoveRelatedId + '"]') || document.querySelector('li[data-id="' + lastMoveRelatedId + '"]'); 7096 if (lastRelated && lastRelated.parentElement === relatedParent) { 7097 const lastRelatedIndex = folderItems.indexOf(lastRelated); 7098 const isOscillating = (isFirstPos && lastRelatedIndex === 1) || (isSecondPos && lastRelatedIndex === 0); 7099 7100 if (isOscillating) { 7101 const timeSinceLastMove = currentTime - lastMoveTime; 7102 // Allow moving to first position easily, but prevent rapid oscillation between first/second 7103 if (isSecondPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.2) { 7104 return false; // Prevent jumping from first to second too easily 7105 } 7106 // But allow first position more easily 7107 if (isFirstPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.6 && relatedId === lastMoveRelatedId) { 7108 return false; // Only prevent very rapid oscillation 7109 } 7110 } 7111 } 7112 } 7113 } 7114 7115 // Prevent rapid oscillation when dragging within same container 7116 // NOTE: For same-folder moves, we already updated tracking above, so skip this
7116section 7117 if (!isMovingWithinSameFolder) { 7118 const timeSinceLastMove = currentTime - lastMoveTime; 7119 7120 // If moving to the same related element very quickly, reject to prevent oscillation 7121 if (timeSinceLastMove < MOVE_DEBOUNCE_MS && relatedId === lastMoveRelatedId) { 7122 return false; 7123 } 7124 7125 // Update tracking variables (only for non-same-folder moves) 7126 lastMoveTime = currentTime; 7127 lastMoveRelatedId = relatedId; 7128 } 7129 // For same-folder moves, tracking was already updated in the earlier block 7130 7131 // Update container decision tracking 7132 if (currentDecision) { 7133 if (currentDecision !== lastContainerDecision) { 7134 lastContainerDecisionTime = currentTime; 7135 } 7136 lastContainerDecision = currentDecision; 7137 } 7138 7139 // Track where the placeholder indicates the item should be placed 7140 // The related element is next to where the placeholder will be positioned 7141 // Check if it's at root level or nested level 7142 if (related) { 7143 intendedRelatedElement = related; 7144 const relatedParent = related.parentElement; 7145 const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); 7146 7147 // Determine if the placeholder is at root level or nested level 7148 if (relatedParent === rootContainer) { 7149 // Placeholder is at root level (not nested) 7150 intendedPlacementContainer = rootContainer; 7151 } else if (relatedParent && relatedParent.classList.contains('nested-sortable')) { 7152 // Placeholder is in a nested list (inside a folder) 7153 intendedPlacementContainer = relatedParent; 7154 } 7155 7156 // Update ghost element indentation to match the target position 7157 requestAnimationFrame(() => { 7158 const ghost = document.querySelector('.sortable-ghost'); 7159 if (ghost && intendedPlacementContainer) { 7160 const ghostContent = ghost.querySelector('.gallery-item-content'); 7161 if (ghostContent) { 7162 // Calculate the depth where the ghost will be placed 7163 // Use the related element to determine the depth (if it exists and is in the target container) 7164 let targetDepth = 0; 7165 if (intendedPlacementContainer === rootContainer) { 7166 targetDepth = 0; // Root level 7167 } else if (related && related.parentElement === intendedPlacementContainer) { 7168 // Use the related element's depth to determine target depth 7169 targetDepth = computeDepth(related); 7170 } else { 7171 // Fallback: find first child in the container or calculate from parent folder 7172 const firstChild = intendedPlacementContainer.querySelector('li'); 7173 if (firstChild) { 7174 targetDepth = computeDepth(firstChild); 7175 } else { 7176 // Empty container (like empty folder) - calculate from parent folder 7177 const parentFolder = intendedPlacementContainer.closest('li.has-children'); 7178 if (parentFolder) { 7179 targetDepth = computeDepth(parentFolder) + 1; 7180 } 7181 } 7182 } 7183 7184 // Special handling: If intendedPlacementContainer is empty and placeholder isn't in it yet, 7185 // ensure placeholder is moved there and indentation is correct 7186 if (intendedPlacementContainer.children.length === 0) { 7187 const placeholder = document.querySelector('.sortable-ghost'); 7188 if (placeholder && placeholder.parentElement !== intendedPlacementContainer) { 7189 // Move placeholder into empty container 7190 intendedPlacementContainer.appendChild(placeholder); 7191 } 7192 } 7193 ghostContent.style.paddingLeft = (targetDepth * 16) + 'px'; 7194 } 7195 } 7196 }); 7197 } 7198 7199 // Let SortableJS handle the placement based on placeholder position 7200 // We'll verify and correct if needed in onEnd 7201 return true; // Allow the move 7202 }, 7203 7204 // Capture indentation before drag starts (fires when item is chosen) 7205 onChoose: function(evt) { 7206 if (!evt.item) return; 7207 7208 const draggedItem = evt.item; 7209 const content = draggedItem.querySelector('.gallery-item-content'); 7210 7211 // Store the original padding-left for the ghost 7212 if (content) { 7213 const computedStyle = window.getComputedStyle(content); 7214 const originalPaddingLeft = computedStyle.paddingLeft; 7215 draggedItem.dataset.originalPaddingLeft = originalPaddingLeft; 7216 } 7217 }, 7218 7219 // Visual feedback on drag start 7220 onStart: function(evt) { 7221 if (!evt.item) return; 7222 7223 // Don't set dimensions on the dragged item - let it maintain natural size 7224 // Setting width/height can cause layout shifts and menu expansion 7225 const draggedItem = evt.item; 7226 7227 // Store original container and check if it's the last item in folder 7228 draggedItemOriginalContainer = draggedItem.parentElement; 7229 const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); 7230 if (draggedItemOriginalContainer && 7231 draggedItemOriginalContainer.classList.contains('nested-sortable') && 7232 draggedItemOriginalContainer !== rootContainer) { 7233 const siblings = Array.from(draggedItemOriginalContainer.children); 7234 isLastItemInFolder = siblings.length > 0 && siblings[siblings.length - 1] === draggedItem; 7235 } else { 7236 isLastItemInFolder = false;
7237 } 7238 7239 // Apply the stored padding-left to the ghost element 7240 // The ghost is created by SortableJS at this point 7241 requestAnimationFrame(() => { 7242 const ghost = document.querySelector('.sortable-ghost'); 7243 if (ghost) { 7244 const ghostContent = ghost.querySelector('.gallery-item-content'); 7245 const originalPaddingLeft = draggedItem.dataset.originalPaddingLeft; 7246 if (ghostContent && originalPaddingLeft) { 7247 ghostContent.style.paddingLeft = originalPaddingLeft; 7248 } 7249 } 7250 }); 7251 7252 // Reset oscillation tracking variables 7253 lastMoveTime = 0; 7254 lastMoveRelatedId = null; 7255 lastContainerDecision = null; 7256 lastContainerDecisionTime = 0; 7257 7258 // Create a backup 7259 try { 7260 window._dragStartElementIds = []; 7261 document.querySelectorAll('#galleryTree .nested-sortable li').forEach(li => { 7262 if (li.dataset.id) { 7263 window._dragStartElementIds.push(li.dataset.id); 7264 } 7265 }); 7266 7267 galleriesBackup = JSON.parse(JSON.stringify(galleries)); 7268 } catch (error) { 7269 console.error('Error creating backup:', error); 7270 } 7271 7272 // Add indicator classes 7273 document.body.classList.add('menu-item-dragging'); 7274 7275 // Add hover indicator class to submenu items 7276 document.querySelectorAll('.has-children').forEach(item => { 7277 item.classList.add('potential-parent'); 7278 }); 7279 7280 // Prevent layout recalculation during drag 7281 document.body.style.setProperty('--is-dragging', '1'); 7282 }, 7283 7284 // Cleanup on end 7285 onEnd: function(evt) { 7286 // Verify and correct placement based on placeholder's intended position 7287 // SortableJS may auto-nest incorrectly based on mouse position, even if placeholder shows root level 7288 const dragged = evt.item; 7289 const previousParentLi = evt.from ? evt.from.closest('li') : null; 7290 const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); 7291 7292 if (!dragged || !rootContainer) { 7293 intendedPlacementContainer = null; 7294 intendedRelatedElement = null; 7295 // Restore normal sizing 7296 dragged.style.width = ''; 7297 dragged.style.height = ''; 7298 dragged.style.minHeight = ''; 7299 document.body.style.removeProperty('--is-dragging'); 7300 return; 7301 } 7302 7303 // Get the actual parent container where SortableJS placed the item 7304 const actualParentList = dragged.parentElement; 7305 7306 // Restore normal sizing - use requestAnimationFrame to prevent layout thrashing 7307 requestAnimationFrame(() => { 7308 dragged.style.width = ''; 7309 dragged.style.height = ''; 7310 dragged.style.minHeight = ''; 7311 document.body.style.removeProperty('--is-dragging'); 7312 }); 7313 7314 if (intendedPlacementContainer && !document.body.contains(intendedPlacementContainer)) { 7315 intendedPlacementContainer = null; 7316 intendedRelatedElement = null; 7317 } 7318 7319 // Check if the intended placement (where placeholder showed) differs from actual placement 7320 // This happens when SortableJS auto-nests based on mouse position over a folder 7321 if (intendedPlacementContainer && actualParentList !== intendedPlacementContainer) { 7322 // The item ended up in a different container than the placeholder indicated 7323 // Move it to where the placeholder showed it should be (next to intendedRelatedElement) 7324 if (intendedPlacementContainer === rootContainer) { 7325 // Placeholder was at root level - move item to root, positioned next to related element 7326 if (intendedRelatedElement && intendedRelatedElement.parentElement === rootContainer) { 7327 // Insert before the related element (where placeholder was) 7328 rootContainer.insertBefore(dragged, intendedRelatedElement); 7329 console.log("Corrected placement: moved from folder to root level where placeholder indicated"); 7330 } else if (intendedRelatedElement) {
7331 // Related element exists but not at root - find its folder and insert before it at root 7332 const folderItem = intendedRelatedElement.closest('li.has-children'); 7333 if (folderItem && folderItem.parentElement === rootContainer) { 7334 rootContainer.insertBefore(dragged, folderItem); 7335 console.log("Corrected placement: moved to root level above folder where placeholder indicated"); 7336 } else { 7337 rootContainer.appendChild(dragged); 7338 } 7339 } else { 7340 rootContainer.appendChild(dragged); 7341 } 7342 // Update indentation since it's now at root 7343 applyIndentationFromDepth(dragged); 7344 } else { 7345 // Placeholder was in a nested list - move it there 7346 if (intendedRelatedElement && intendedRelatedElement.parentElement === intendedPlacementContainer) { 7347 // Insert before the related element in the nested list 7348 intendedPlacementContainer.insertBefore(dragged, intendedRelatedElement); 7349 } else { 7350 intendedPlacementContainer.appendChild(dragged); 7351 } 7352 console.log("Corrected placement: moved to nested list where placeholder indicated"); 7353 // Update indentation for nested item 7354 applyIndentationFromDepth(dragged); 7355 7356 // Ensure parent folder has proper structure 7357 const folderItem = intendedPlacementContainer.closest('li.has-children'); 7358 if (folderItem) { 7359 const nestedList = folderItem.querySelector('ol.nested-sortable'); 7360 if (nestedList && intendedPlacementContainer !== nestedList) { 7361 nestedList.appendChild(dragged); 7362 } 7363 7364 const parentDepth = computeDepth(folderItem); 7365 const nestedDepth = parentDepth + 1; 7366 7367 if (nestedList) { 7368 nestedList.className = 'nested-sortable nested-level-' + nestedDepth; 7369 } 7370 7371 folderItem.classList.add('has-children'); 7372 } 7373 } 7374 } else if (actualParentList && actualParentList.classList.contains('nested-sortable') && actualParentList !== rootContainer) { 7375 // Item is correctly in a nested list - ensure proper structure 7376 const folderItem = actualParentList.closest('li.has-children'); 7377 if (folderItem) { 7378 const nestedList = folderItem.querySelector('ol.nested-sortable'); 7379 if (nestedList && actualParentList !== nestedList) { 7380 nestedList.appendChild(dragged); 7381 } 7382 7383 const parentDepth = computeDepth(folderItem); 7384 const nestedDepth = parentDepth + 1; 7385 7386 if (nestedList) { 7387 nestedList.className = 'nested-sortable nested-level-' + nestedDepth; 7388 } 7389 7390 folderItem.classList.add('has-children'); 7391 } 7392 applyIndentationFromDepth(dragged); 7393 } else { 7394 // Item is correctly at root level 7395 applyIndentationFromDepth(dragged); 7396 } 7397 7398 // Reset tracking variables 7399 intendedPlacementContainer = null; 7400 intendedRelatedElement = null; 7401 isLastItemInFolder = false; 7402 draggedItemOriginalContainer = null; 7403 lastContainerDecision = null; 7404 lastContainerDecisionTime = 0; 7405 7406 // Apply indentation updates for ALL affected items after drag 7407 // Use requestAnimationFrame to batch DOM updates and prevent visual jumps 7408 requestAnimationFrame(() => { 7409 if (dragged) {
7410 // Update the dragged item and all its descendants 7411 applyIndentationFromDepth(dragged); 7412 7413 // Update all items in the new container (siblings of dragged item) 7414 const newContainer = dragged.parentElement; 7415 if (newContainer && newContainer.classList.contains('nested-sortable')) { 7416 const siblings = Array.from(newContainer.children); 7417 siblings.forEach(sibling => { 7418 if (sibling !== dragged && sibling.tagName === 'LI') { 7419 applyIndentationFromDepth(sibling); 7420 } 7421 }); 7422 } 7423 7424 // Update all items in the old container (if it still exists and has items) 7425 if (evt.from && evt.from.classList.contains('nested-sortable')) { 7426 const oldSiblings = Array.from(evt.from.children); 7427 oldSiblings.forEach(sibling => { 7428 if (sibling.tagName === 'LI') { 7429 applyIndentationFromDepth(sibling); 7430 } 7431 }); 7432 } 7433 7434 // Also update all items in the entire tree to ensure consistency 7435 // This handles edge cases where indentation might be off 7436 const allItems = document.querySelectorAll('#galleryTree .nested-sortable li'); 7437 allItems.forEach(li => { 7438 const depth = computeDepth(li); 7439 li.setAttribute('data-nesting-level', depth); 7440 const content = li.querySelector('.gallery-item-content'); 7441 if (content) { 7442 content.style.paddingLeft = (depth * 16) + 'px'; 7443 } 7444 }); 7445 } 7446 }); 7447 7448 // Clean up previous parent if it lost its last child 7449 if (previousParentLi) { 7450 cleanupParentIfEmpty(previousParentLi); 7451 } 7452 7453 // Remove all indicator classes 7454 document.body.classList.remove('menu-item-dragging'); 7455 document.querySelectorAll('.potential-parent, .will-accept-child, .will-be-nested').forEach(el => { 7456 el.classList.remove('potential-parent', 'will-accept-child', 'will-be-nested'); 7457 }); 7458 7459 // Standard cleanup for ghost elements 7460 document.querySelectorAll('.sortable-ghost, .sortable-chosen, .sortable-drag').forEach(item => { 7461 item.classList.remove('sortable-ghost', 'sortable-chosen', 'sortable-drag'); 7462 }); 7463 7464 // Check if drop was outside a valid container 7465 if (!evt.to || !evt.to.classList.contains('nested-sortable')) { 7466 console.warn('Item was dropped outside a valid container - reverting'); 7467 restoreFromBackup(); 7468 return; 7469 } 7470 7471 // Verify no elements were lost 7472 try { 7473 // Check DOM elements 7474 const endElementIds = []; 7475 document.querySelectorAll('#galleryTree .nested-sortable li').forEach(li => { 7476 if (li.dataset.id) { 7477 endElementIds.push(li.dataset.id); 7478 } 7479 }); 7480 7481 if (window._dragStartElementIds && window._dragStartElementIds.length > 0) { 7482 const startCount = window._dragStartElementIds.length; 7483 const endCount = endElementIds.length; 7484 7485 if (endCount < startCount) { 7486 console.error(`DOM elements were lost during drag! Original: ${startCount}, New: ${endCount}`); 7487 restoreFromBackup(); 7488 return; 7489 } 7490 } 7491 7492 // Update the data structure 7493 updateGalleryStructure(); 7494 7495 // Verify data model 7496 const originalCount = countGalleries(galleriesBackup); 7497 const newCount = countGalleries(galleries); 7498 7499 if (newCount < originalCount) { 7500 console.error(`Items were lost! Original: ${originalCount}, New: ${newCount}`); 7501 restoreFromBackup(); 7502 return; 7503 } 7504 7505 // updateGalleryStructure already persists; do not save or re-render again 7506 // here. A second save/render can rebind Sortable and snap the drop back. 7507 } catch (error) { 7508 console.error('Error updating gallery structure:', error); 7509 restoreFromBackup(); 7510 } 7511 } 7512 }); 7513 7514 sortableInstances.push(sortable); 7515 } catch (error) { 7516 console.error(`Error initializing sortable for container ${i}:`, error); 7517 } 7518 } 7519 7520 // Add event delegation for folder toggles (will be added by setupFolderToggleHandler) 7521 setupFolderToggleHandler(); 7522 7523 return sortableInstances; 7524} 7525 7526// Helper function to set up folder toggle event delegation 7527// This must be called after galleries are rendered, in both view and edit modes 7528function setupFolderToggleHandler() { 7529 const galleryTree = document.getElementById('galleryTree'); 7530 if (galleryTree && !galleryTree.dataset.folderToggleHandlerAdded) { 7531 galleryTree.addEventListener('click', function(e) { 7532 7533 const target = e.target; 7534 // Resolve the clicked row first â do not use target.closest('.menu-item.folder'), 7535 // which matches ancestor folder labels when clicking nested pages/galleries. 7536 const listItem = target.closest('li[data-id]'); 7537 if (!listItem) { 7538 return; 7539 } 7540 7541 const contentDiv = listItem.querySelector(':scope > .gallery-item-content'); 7542 if (!contentDiv) { 7543 return; 7544 } 7545 7546 const toggleButton = contentDiv.querySelector('.submenu-toggle'); 7547 const folderText = contentDiv.querySelector('.menu-item.folder'); 7548 const isToggleClick = toggleButton && toggleButton.contains(target); 7549 const isFolderTextClick = folderText && folderText.contains(target); 7550 7551 7552 if (isToggleClick || isFolderTextClick) { 7553 const folderId = parseInt(listItem.dataset.id, 10); 7554 7555 if (folderId && !isNaN(folderId)) { 7556 // Prevent SortableJS from intercepting this click 7557 e.stopPropagation(); 7558 e.stopImmediatePropagation(); 7559 toggleFolder(folderId, e); 7560 } 7561 } 7562 }, true); // Use capture phase to run before SortableJS 7563 7564 galleryTree.dataset.folderToggleHandlerAdded = 'true'; 7565 } 7566} 7567 7568// NEW HELPER FUNCTION: Gets the nesting level of an element 7569function getElementNestingLevel(element) { 7570 let level = 0; 7571 let current = element; 7572 7573 // Count how many nested-sortable parents this element has 7574 while (current && current.parentElement) { 7575 if (current.parentElement.classList && current.parentElement.classList.contains('nested-sortable')) { 7576 level++; 7577 } 7578 current = current.parentElement; 7579 } 7580 7581 return level; 7582} 7583 7584// NEW HELPER FUNCTION: Determines if an item would become nested 7585function wouldBeNestedItem(draggedEl, relatedEl, event) { 7586 if (!relatedEl || !draggedEl) return false;
7587 7588 // Get mouse position 7589 const mouseX = event.clientX; 7590 7591 // Get the bounding rect of the related element 7592 const rect = relatedEl.getBoundingClientRect(); 7593 7594 // Determine the nesting threshold - how far from the left edge 7595 // the mouse needs to be to consider it a nesting operation 7596 const nestingThreshold = 25; // pixels from left edge 7597 7598 // Check if mouse is within the nesting threshold from the left edge 7599 const distanceFromLeft = mouseX - rect.left; 7600 7601 // Check if we should nest (mouse is within threshold of left edge) 7602 const wouldNest = distanceFromLeft <= nestingThreshold; 7603 7604 // Log the calculation for debugging 7605 console.log(`Nesting check: distance=${distanceFromLeft}px, threshold=${nestingThreshold}px, would nest=${wouldNest}`); 7606 7607 return wouldNest; 7608} 7609 7610/** 7611 * Helper function to find gallery by ID in a tree structure 7612 * @param {Array} galleries - The gallery array to search 7613 * @param {number} id - The ID to find 7614 * @returns {Object|null} - The found gallery or null 7615 */ 7616function findGalleryById(galleries, id) { 7617 if (window.HelpSystem && typeof window.HelpSystem.isDemoId === 'function' && window.HelpSystem.isDemoId(id)) { 7618 return window.HelpSystem.getDemoRecord(id); 7619 } 7620 if (!Array.isArray(galleries)) return null; 7621 7622 // First check at the current level 7623 const directMatch = galleries.find(g => g.id == id); 7624 if (directMatch) return directMatch; 7625 7626 // Then check in children 7627 for (const gallery of galleries) { 7628 if (gallery.children && gallery.children.length > 0) { 7629 const childMatch = findGalleryById(gallery.children, id); 7630 if (childMatch) return childMatch; 7631 } 7632 } 7633 7634 return null; 7635} 7636 7637/** Detect page menu items even when isPage flag was not persisted */ 7638function isPageMenuItem(item) { 7639 if (!item) return false; 7640 if (item.isPage === true) return true; 7641 if (item.pageId && String(item.pageId).indexOf('page_') === 0) return true; 7642 if (Array.isArray(item.pageElements) && item.pageElements.length > 0) return true; 7643 return false; 7644} 7645 7646/** 7647 * Return true only for pages/galleries that explicitly opted into navigable-parent behavior. 7648 * Never infer this from children or parentId; legacy items must retain their existing behavior. 7649 */ 7650function isNavigableMenuParent(item) { 7651 if (!item || item.actsAsMenuParent !== true) return false; 7652 if (item.isFolder === true || item.isSubmenu === true || item.isSpacer === true || item.isExternal === true) { 7653 return false; 7654 } 7655 return isPageMenuItem(item) || item.isIntegrated === true; 7656} 7657 7658/** 7659 * Get all gallery IDs from a tree structure 7660 * @param {Array} galleries - Array of galleries 7661 * @returns {Array} - Flat array of all IDs 7662 */ 7663function getAllGalleryIds(galleries) { 7664 if (!Array.isArray(galleries)) return []; 7665 7666 let ids = []; 7667 7668 galleries.forEach(gallery => { 7669 if (gallery && gallery.id) { 7670 ids.push(gallery.id); 7671 } 7672 7673 // Add child IDs if any 7674 if (gallery.children && Array.isArray(gallery.children)) { 7675 ids = ids.concat(getAllGalleryIds(gallery.children)); 7676 } 7677 }); 7678 7679 return ids; 7680} 7681 7682/** 7683 * Enhanced count function that handles edge cases 7684 * @param {Array} galleryArray - The array to count 7685 * @returns {number} - The total count of galleries 7686 */ 7687function countGalleries(galleryArray) { 7688 if (!Array.isArray(galleryArray)) return 0; 7689 7690 // Menu items are stored flat and also get a derived children[] at render time. 7691 // Counting children recursively double-counts those items and falsely triggers 7692 // restoreFromBackup after a successful drop. 7693 const ids = new Set(); 7694 function walk(items) { 7695 if (!Array.isArray(items)) return; 7696 items.forEach(function(gallery) { 7697 if (!gallery) return; 7698 if (gallery.id != null) ids.add(gallery.id); 7699 if (gallery.children && Array.isArray(gallery.children)) walk(gallery.children); 7700 }); 7701 } 7702 walk(galleryArray); 7703 return ids.size; 7704} 7705 7706/** 7707 * Enhanced backup restoration function with better debugging and notification 7708 */ 7709function restoreFromBackup() { 7710 console.warn('Restoring galleries from backup due to invalid operation'); 7711 7712 // Verify we have a valid backup 7713 if (!galleriesBackup || !Array.isArray(galleriesBackup) || galleriesBackup.length === 0) { 7714 console.error('No valid backup available for restoration'); 7715 7716 // Create a backup from the current DOM as a last resort 7717 try { 7718 console.log('Attempting to rebuild from DOM structure...'); 7719 rebuildGalleriesFromDOM(); 7720 return; 7721 } catch (rebuildError) { 7722 console.error('Failed to rebuild from DOM:', rebuildError); 7723 // Continue with normal user notification 7724 } 7725 } else { 7726 // Restore from backup 7727 galleries = JSON.parse(JSON.stringify(galleriesBackup)); 7728 } 7729 7730 // Re-render the tree with the restored data 7731 renderGalleries(); 7732 7733 // Force reinitialize nested sortables after restoration 7734 setTimeout(() => { 7735 initializeNestedSortables(); 7736 }, 300); 7737} 7738 7739/** 7740 * Emergency recovery function that tries to rebuild galleries from DOM 7741 * This is a last resort when backup restoration fails 7742 */ 7743function rebuildGalleriesFromDOM() { 7744 console.log('EMERGENCY RECOVERY: Attempting to rebuild galleries from DOM structure'); 7745 7746 // Create a new galleries array 7747 const rebuiltGalleries = []; 7748 7749 // This will track items we've already processed 7750 const processedIds = new Set(); 7751 7752 // Function to process a container and its children 7753 function processContainer(container, parentId = null) { 7754 // Get all immediate list items 7755 const items = container.querySelectorAll(':scope > li'); 7756
7757 items.forEach(item => { 7758 // Get item ID from data attribute 7759 const id = parseInt(item.dataset.id); 7760 if (isNaN(id)) return; // Skip if no valid ID 7761 7762 // Skip already processed items to avoid duplicates 7763 if (processedIds.has(id)) return; 7764 processedIds.add(id); 7765 7766 // Try to find the original gallery data 7767 let galleryData = null; 7768 7769 // First check current galleries array 7770 const existingGallery = findGalleryById(galleries, id); 7771 if (existingGallery) { 7772 galleryData = { ...existingGallery }; 7773 delete galleryData.children; // We'll rebuild the hierarchy 7774 } 7775 // If not found, check if we have a backup 7776 else if (galleriesBackup && Array.isArray(galleriesBackup)) { 7777 const backupGallery = findGalleryById(galleriesBackup, id); 7778 if (backupGallery) { 7779 galleryData = { ...backupGallery }; 7780 delete galleryData.children; // We'll rebuild the hierarchy 7781 } 7782 } 7783 7784 // If we still don't have data, create minimal placeholder 7785 if (!galleryData) { 7786 // Try to extract title from DOM 7787 const titleElement = item.querySelector('.menu-item'); 7788 const title = titleElement ? titleElement.textContent.trim() : `Item ${id}`; 7789 7790 galleryData = { 7791 id: id, 7792 title: title, 7793 visible: !item.classList.contains('hidden-gallery') 7794 }; 7795 } 7796 7797 // Update parent ID to match DOM structure 7798 galleryData.parentId = parentId; 7799 7800 // Add to rebuilt galleries 7801 rebuiltGalleries.push(galleryData); 7802 7803 // Process children if any 7804 const childContainer = item.querySelector('ol.nested-sortable'); 7805 if (childContainer) { 7806 processContainer(childContainer, id); 7807 } 7808 }); 7809 } 7810 7811 // Start with root container 7812 const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); 7813 if (rootContainer) { 7814 processContainer(rootContainer); 7815 7816 // Replace global galleries array with our rebuilt one 7817 galleries = rebuiltGalleries; 7818 7819 // Re-render with the rebuilt data 7820 renderGalleries(); 7821 7822 console.log(`RECOVERY COMPLETE: Rebuilt ${rebuiltGalleries.length} items from DOM`); 7823 return true; 7824 } else { 7825 console.error('RECOVERY FAILED: Root container not found'); 7826 return false; 7827 } 7828} 7829 7830// Make sure these functions are available to the global scope 7831window.initializeNestedSortables = initializeNestedSortables; 7832window.restoreFromBackup = restoreFromBackup; 7833window.getElementNestingLevel = getElementNestingLevel; 7834window.wouldBeNestedItem = wouldBeNestedItem; 7835 7836 7837 7838function hasDescendant(itemId, possibleDescendantId) { 7839// Check if possibleDescendantId is a descendant of itemId 7840const children = galleries.filter(g => g.parentId === itemId); 7841 7842if (children.some(child => child.id === possibleDescendantId)) { 7843return true; 7844} 7845 7846return children.some(child => hasDescendant(child.id, possibleDescendantId)); 7847} 7848 7849function updateGalleryStructure() { 7850// Create a new array to hold the updated gallery structure 7851const newGalleries = []; 7852 7853// Function to recursively process nested lists 7854function processNestedList(container, parentId = null) { 7855if (!container || !container.children) { 7856 console.warn('Invalid container in processNestedList', container); 7857 return; 7858} 7859 7860const items = container.children; 7861 7862Array.from(items).forEach(item => { 7863 if (item.tagName !== 'LI') return; // Skip non-list items 7864 7865 const id = parseInt(item.dataset.id); 7866 if (isNaN(id)) { 7867 console.warn('Invalid ID in list item:', item); 7868 return; 7869 } 7870 7871 const gallery = galleries.find(g => g.id === id); 7872 7873 if (gallery) { 7874 // Create a new gallery object without children 7875 const newGallery = { ...gallery }; 7876 if (newGallery.children) delete newGallery.children; 7877 7878 // Update parent ID 7879 newGallery.parentId = parentId; 7880 7881 // Add to new galleries array 7882 newGalleries.push(newGallery); 7883 7884 // Process any nested OL with class 'nested-sortable' within this item 7885 const nestedList = item.querySelector('ol.nested-sortable'); 7886 if (nestedList) { 7887 processNestedList(nestedList, id); 7888 } 7889 } else { 7890 console.warn(`Gallery with ID ${id} not found in galleries array`); 7891 } 7892}); 7893} 7894 7895// Start processing from the root list 7896const rootList = document.querySelector('#galleryTree > ol.nested-sortable'); 7897if (rootList) { 7898try { 7899 processNestedList(rootList); 7900 7901 // Verify that we haven't lost any galleries 7902 if (newGalleries.length < galleries.length) { 7903 console.error(`Gallery count mismatch! Original: ${galleries.length}, New: ${newGalleries.length}`); 7904 7905 // Find missing galleries 7906 const originalIds = galleries.map(g => g.id); 7907 const newIds = newGalleries.map(g => g.id); 7908 const missingIds = originalIds.filter(id => !newIds.includes(id)); 7909 7910 if (missingIds.length > 0) { 7911 console.error('Missing gallery IDs:', missingIds); 7912 7913 // Add missing galleries to the new structure 7914 missingIds.forEach(id => { 7915 const missingGallery = galleries.find(g => g.id === id); 7916 if (missingGallery) { 7917 // Add it as a top-level item 7918 const newGallery = { ...missingGallery, parentId: null }; 7919 if (newGallery.children) delete newGallery.children; 7920 newGalleries.push(newGallery); 7921 console.log(`Rescued gallery: ${newGallery.title} (ID: ${newGallery.id})`); 7922 } 7923 }); 7924 } 7925 } 7926 7927 // Update the galleries array with the new structure 7928 galleries = newGalleries; 7929 if (typeof window !== 'undefined') { 7930 window.galleries = galleries; 7931 } 7932 7933 // Save the updated structure 7934 saveGalleries(); 7935} catch (error) { 7936 console.error('Error in updateGalleryStructure:', error); 7937 throw error; // Re-throw to trigger the backup restore 7938} 7939} else { 7940console.warn('Root list not found'); 7941throw new Error('Root list not found'); // Throw error to trigger backup restore 7942} 7943} 7944 7945function toggleSubmenu(id, event) { 7946// Ensure the event doesn't interfere with drag operations 7947event.stopPropagation(); 7948 7949const listItem = getGalleryTreeListItem(id, event); 7950if (listItem) { 7951// Toggle the expanded class 7952listItem.classList.toggle('expanded'); 7953 7954// Find the submenu toggle icon and rotate it 7955const toggleIcon = listItem.querySelector('.toggle-icon'); 7956if (toggleIcon) { 7957 toggleIcon.classList.toggle('rotated'); 7958} 7959} 7960} 7961 7962/** 7963 * Improved slugify function with robust character handling 7964 * @param {string} text - The text to convert to a slug 7965 * @returns {string} A URL-friendly slug 7966 */ 7967function slugify(text) { 7968 // Guard against null or empty input 7969 if (!text || text.trim() === '') { 7970 return 'page-' + Date.now(); 7971 } 7972 7973 // First convert to lowercase 7974 let slug = text.toLowerCase(); 7975 7976 // Replace spaces with hyphens 7977 slug = slug.split(' ').join('-'); 7978 7979 // Filter out unwanted characters (keep only a-z, 0-9, and hyphens) 7980 let filtered = ''; 7981 for (let i = 0; i < slug.length; i++) { 7982 const char = slug.charAt(i); 7983 if ((char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char === '-') { 7984 filtered += char; 7985 } 7986 } 7987 slug = filtered; 7988 7989 /* 7990 // Clean up multiple hyphens 7991 while (slug.indexOf('--') !== -1) { 7992 slug = slug.replace('--', '-'); 7993 } 7994 */ 7995 7996 // Remove leading and trailing hyphens 7997 if (slug.startsWith('-')) { 7998 slug = slug.substring(1); 7999 } 8000 if (slug.endsWith('-')) { 8001 slug = slug.substring(0, slug.length - 1); 8002 } 8003 8004 // If slug is empty after processing, use a default 8005 if (!slug || slug === '-') { 8006 return 'page-' + Date.now(); 8007 } 8008 8009 return slug; 8010} 8011 8012// Update the addPage function to use our new robust slugify function 8013/**
8014 * Adds a new page to the galleries array 8015 * @returns {number|null} The new page ID or null if error 8016 */ 8017function addPage() { 8018 try { 8019 // Get the page title 8020 const titleInput = document.getElementById('galleryTitle'); 8021 if (!titleInput) { 8022 console.error('Page title input not found'); 8023 return; 8024 } 8025 const title = titleInput.value.trim(); 8026 8027 // Validate title is not empty 8028 if (!title) { 8029 alert('Please enter a page title'); 8030 return; 8031 } 8032 8033 // Get parent ID with error handling 8034 const parentSelect = document.getElementById('galleryParent'); 8035 const parentId = parentSelect && parentSelect.value ? parseInt(parentSelect.value) : null; 8036 8037 // Generate a unique ID for the page 8038 const id = Date.now(); 8039 const pageId = `page_${id}`; 8040 8041 // Calculate slug using our more robust slugify function 8042 const pageSlug = slugify(title); 8043 8044 // Create the page object with explicit properties 8045 const page = { 8046 id: id, 8047 title: title, 8048 isPage: true, 8049 isIntegrated: true, 8050 isSubmenu: false, 8051 pageId: pageId, 8052 visible: true, 8053 parentId: parentId, 8054 siteId: siteId, 8055 // Set slug and URL explicitly 8056 slug: pageSlug, 8057 url: '/' + pageSlug 8058 }; 8059 8060 console.log(`Creating new page with ID: ${id}, title: "${title}"`); 8061 8062 // Add to galleries array 8063 galleries.push(page); 8064 8065 // CRITICAL: Ensure window.galleries is synchronized 8066 if (typeof window.galleries === 'undefined') { 8067 window.galleries = galleries; 8068 } else { 8069 // Make sure the page is in window.galleries too 8070 const existingIndex = window.galleries.findIndex(g => g.id === id); 8071 if (existingIndex >= 0) { 8072 window.galleries[existingIndex] = page; 8073 } else { 8074 window.galleries.push(page); 8075 } 8076 } 8077 8078 // Initialize empty page elements array in PageManager 8079 if (window.PageManager) { 8080 window.PageManager.elements[pageId] = []; 8081 } 8082 8083 // Update UI first 8084 renderGalleries(); 8085 clearAddForm(); 8086 toggleAddForm(); 8087 8088 // Set as active page 8089 console.log(`Setting new page "${page.title}" as active`); 8090 activeGalleryId = id; 8091 window.activeGalleryId = id; 8092 8093 // Update active states in the menu 8094 updateActiveStates(); 8095 8096 // Save to server 8097 saveGalleries().then(() => { 8098 console.log(`Page "${page.title}" saved successfully`); 8099 8100 // Double check that our gallery is still in the array 8101 const pageExists = galleries.some(g => g.id === id); 8102 console.log(`Page exists in galleries array: ${pageExists}`); 8103 8104 if (pageExists) { 8105 // Load the new page after a slight delay to ensure DOM is updated 8106 setTimeout(() => { 8107 console.log(`Loading new page "${page.title}" with ID: ${id}`); 8108 loadPage(id); 8109 }, 50); 8110 } else { 8111 console.error(`Page with ID ${id} not found in galleries array after save`); 8112 } 8113 }).catch(error => { 8114 alert('Error saving page. Please try again.'); 8115 }); 8116 8117 return id; 8118 } catch (error) { 8119 console.error('Error adding page:', error); 8120 alert('Error adding page. Please try again.'); 8121 return null; 8122 } 8123} 8124 8125/** 8126 * Ensures a slug is unique among existing galleries 8127 * @param {string} slug - The base slug to check 8128 * @param {Array} existingGalleries - Array of existing gallery configs 8129 * @returns {string} A unique slug 8130 */ 8131function ensureUniqueSlug(slug, existingGalleries) { 8132 // Guard against invalid slugs 8133 if (!slug || slug === '-') { 8134 slug = 'page-' + Date.now(); 8135 } 8136 8137 let finalSlug = slug; 8138 let counter = 1; 8139 8140 // Check if slug already exists 8141 while (existingGalleries.some(g => g.slug === finalSlug)) { 8142 finalSlug = `${slug}-${counter}`; 8143 counter++; 8144 } 8145 8146 return finalSlug; 8147} 8148 8149/** 8150 * Adds a new gallery based on the selected type 8151 * @returns {number|null} The new gallery ID or null if error 8152 */ 8153function addGallery() { 8154 try { 8155 // Get the gallery title 8156 const titleInput = document.getElementById('galleryTitle'); 8157 if (!titleInput) { 8158 console.error('Gallery title input not found'); 8159 return; 8160 } 8161 const title = titleInput.value.trim(); 8162 8163 // Get the selected gallery type 8164 const galleryTypeRadio = document.querySelector('input[name="galleryType"]:checked'); 8165 if (!galleryTypeRadio) { 8166 console.error('No gallery type selected'); 8167 alert('Please select a gallery type'); 8168 return; 8169 } 8170 8171 const galleryType = galleryTypeRadio.value; 8172 console.log("Selected gallery type:", galleryType); 8173 8174 // If this is a page, use the addPage function 8175 if (galleryType === 'page') { 8176 console.log("Creating page instead of gallery"); 8177 return addPage(); 8178 } 8179 8180 // For spacer type, we don't require a title 8181 if (galleryType !== 'spacer' && !title) { 8182 alert('Please enter a gallery title'); 8183 return; 8184 } 8185 8186 // Continue with gallery creation logic 8187 // Generate unique ID 8188 const id = Date.now(); 8189 8190 // Get parent gallery if selected 8191 const parentSelect = document.getElementById('galleryParent'); 8192 const parentId = parentSelect.value ? parseInt(parentSelect.value) : null; 8193 8194 // Get URL if this is an external gallery 8195 const galleryUrl = document.getElementById('galleryUrl'); 8196 const url = galleryTypeRadio.value === 'external' && galleryUrl 8197 ? galleryUrl.value.trim() 8198 : ""; 8199 8200 // Set basic properties 8201 const gallery = { 8202 id: id, 8203 title: title || "Spacer", // Use "Spacer" as internal title if no title provided 8204 parentId: parentId, 8205 visible: true 8206 }; 8207 8208 // Set type-specific properties 8209 if (galleryTypeRadio.value === 'spacer') { 8210 // Spacer properties 8211 gallery.isSpacer = true; 8212 gallery.title = title || "Spacer"; // For internal reference 8213 } else if (galleryTypeRadio.value === 'integrated') { 8214 // Generate pageId for integrated galleries 8215 gallery.isIntegrated = true; 8216 gallery.pageId = generatePageId(); 8217 8218 // Generate and ensure unique slug 8219 const baseSlug = slugify(title); 8220 gallery.slug = ensureUniqueSlug(baseSlug, galleries); 8221 gallery.url = "/" + gallery.slug;
8222 8223 // Get the siteId from multiple possible sources 8224 const siteId = window.Parameters?.siteId || 8225 window.siteId || 8226 document.querySelector('meta[name="hydra-site-id"]')?.getAttribute('content') || 8227 ''; 8228 8229 // Add galleryOptions with GUID=siteId as the manualCollectionName 8230 gallery.galleryOptions = { 8231 manualCollectionName: `GUID=${siteId}`, 8232 isIntegratedGallery: true, 8233 pageId: gallery.pageId, // Store pageId for reference 8234 siteId: siteId 8235 }; 8236 } else if (galleryTypeRadio.value === 'folder') { 8237 gallery.isFolder = true; 8238 gallery.isCollapsed = true; // Make folders closed by default 8239 } else if (galleryTypeRadio.value === 'external') { 8240 // External URL 8241 gallery.url = url; 8242 gallery.isExternal = true; // Add this flag to identify external URLs 8243 } 8244 8245 console.log(`Creating new gallery with ID: ${id}, title: "${title}"`); 8246 8247 // Add to galleries array 8248 galleries.push(gallery); 8249 8250 // CRITICAL: Ensure window.galleries is synchronized 8251 if (typeof window.galleries === 'undefined') { 8252 window.galleries = galleries; 8253 } else { 8254 // Make sure the gallery is in window.galleries too 8255 const existingIndex = window.galleries.findIndex(g => g.id === id); 8256 if (existingIndex >= 0) { 8257 window.galleries[existingIndex] = gallery; 8258 } else { 8259 window.galleries.push(gallery); 8260 } 8261 } 8262 8263 // Update UI first 8264 renderGalleries(); 8265 clearAddForm(); 8266 toggleAddForm(); 8267 8268 // Save galleries after UI update 8269 saveGalleries().then(() => { 8270 console.log(`Gallery "${gallery.title}" saved successfully`); 8271 8272 // Double check that our gallery is still in the array 8273 const galleryExists = galleries.some(g => g.id === id); 8274 console.log(`Gallery exists in galleries array: ${galleryExists}`); 8275 8276 // For non-folder types, load the new gallery content after a slight delay 8277 if (!gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && gallery.visible !== false) { 8278 setTimeout(() => { 8279 console.log(`Loading new gallery "${gallery.title}" with ID: ${id}`); 8280 if (gallery.isIntegrated) { 8281 loadGallery(id); 8282 } else if (gallery.isExternal && gallery.url) { 8283 // For external URLs, set the iframe src and mark active after navigation target is ready 8284 activeGalleryId = id; 8285 window.activeGalleryId = id; 8286 updateActiveStates(); 8287 const frame = document.getElementById('galleryFrame'); 8288 if (frame) frame.src = gallery.url; 8289 } 8290 }, 50); 8291 } 8292 }).catch(error => { 8293 console.error(`Error saving gallery "${gallery.title}":`, error); 8294 alert('Error saving gallery. Please try again.'); 8295 }); 8296 8297 // Return the new gallery ID 8298 return id; 8299 } catch (error) { 8300 console.error('Error adding gallery:', error); 8301 alert('Error adding gallery. Please try again.'); 8302 } 8303} 8304 8305function debugDOMState(message) { 8306console.log(`[DEBUG] ${message}`); 8307const galleryContainer = document.querySelector('.gallery-container'); 8308const iframe = document.getElementById('galleryFrame'); 8309const pageContainers = document.querySelectorAll('.page-container'); 8310 8311console.log(`- Gallery container: ${galleryContainer ? 'found' : 'not found'}`); 8312console.log(`- iframe: ${iframe ? 'found' : 'not found'}, display: ${iframe?.style.display}`); 8313console.log(`- Page containers: ${pageContainers.length} found`); 8314 8315pageContainers.forEach((container, index) => { 8316console.log(` - Container ${index}: display: ${container.style.display}, visibility: ${container.style.visibility}`); 8317}); 8318} 8319 8320 8321/** 8322* Global helper: whether edit tools are explicitly toggled open. 8323* Do not use Parameters.isInEditor â that is also true for logged-in admins (cache bypass). 8324* Do not infer from sidebar/page .editing alone (can desync from the toggle). 8325*/ 8326window.isInEditMode = function() { 8327 if (document.body.classList.contains('edit-mode-active')) { 8328 return true; 8329 } 8330 if (typeof window.isEditing === 'boolean') { 8331 return window.isEditing; 8332 } 8333 return false;
8334}; 8335 8336/** 8337* This function synchronizes edit mode state across all components 8338*/ 8339window.updateGlobalEditState = function(isEditing) { 8340console.log('Updating global edit state:', isEditing); 8341 8342// Update global variable 8343window.isEditing = isEditing; 8344 8345// Update Parameters object if it exists 8346if (window.Parameters) { 8347window.Parameters.isInEditor = isEditing; 8348} 8349 8350// Update sidebar class 8351const sidebar = document.querySelector('.sidebar'); 8352if (sidebar) { 8353if (isEditing) { 8354 sidebar.classList.add('editing'); 8355} else { 8356 sidebar.classList.remove('editing'); 8357} 8358} 8359 8360// Update page containers 8361const pageContainers = document.querySelectorAll('.page-container'); 8362pageContainers.forEach(container => { 8363if (isEditing) { 8364 container.classList.add('editing'); 8365} else { 8366 container.classList.remove('editing'); 8367} 8368}); 8369 8370// Notify all components via event 8371document.dispatchEvent(new CustomEvent('edit-mode-changed', { 8372detail: { editing: isEditing } 8373})); 8374 8375if (document.body.classList.contains('menu-layout-horizontal') && typeof window.syncContentMarginToHorizontalHeader === 'function') { 8376 window.syncContentMarginToHorizontalHeader(); 8377} 8378 8379if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 8380 window.syncGalleryOptionsButtonVisibility(); 8381} 8382 8383// Gallery Options panel styling depends on body.edit-mode-active â hide it when leaving edit mode 8384if (!isEditing && typeof window.closeOptionsPanel === 'function') { 8385 window.closeOptionsPanel(); 8386} 8387}; 8388 8389/** Show or hide gallery options (#editorButton) based on edit mode and active content type */ 8390window.syncGalleryOptionsButtonVisibility = function() { 8391 const btn = document.getElementById('editorButton'); 8392 if (!btn) return; 8393 8394 const inEditMode = document.body.classList.contains('edit-mode-active'); 8395 // When content type is explicitly gallery, ignore leftover .page-container nodes 8396 // (common when Help sandbox switches from a page to Example Gallery). 8397 const onPageView = window._activeContentType === 'gallery' 8398 ? false 8399 : (window._activeContentType === 'page' || 8400 !!document.querySelector('.gallery-container .page-container')); 8401 let isActiveGallery = false; 8402 const activeId = window.activeGalleryId; 8403 const list = window.galleries || (typeof galleries !== 'undefined' ? galleries : null); 8404 const activeItem = activeId != null && list && typeof findGalleryById === 'function' 8405 ? findGalleryById(list, activeId) 8406 : (Array.isArray(list) ? list.find(function(g) { return g && g.id === activeId; }) : null); 8407 if (activeId != null && inEditMode && !onPageView) { 8408 isActiveGallery = !!(activeItem && !isPageMenuItem(activeItem) && !activeItem.isFolder && !activeItem.isSpacer && !activeItem.isExternal); 8409 } 8410 8411 const shouldShow = inEditMode && !onPageView && isActiveGallery; 8412 if (shouldShow) { 8413 if (window.Parameters) { 8414 window.Parameters.isInEditor = true; 8415 } 8416 btn.classList.remove('gallery-options-force-hidden'); 8417 btn.style.removeProperty('display'); 8418 btn.style.setProperty('display', 'flex', 'important'); 8419 btn.style.pointerEvents = 'auto'; 8420 } else { 8421 btn.classList.add('gallery-options-force-hidden'); 8422 btn.style.setProperty('display', 'none', 'important'); 8423 btn.style.pointerEvents = ''; 8424 } 8425}; 8426 8427document.addEventListener('gallery-loaded', function() { 8428 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 8429 window.syncGalleryOptionsButtonVisibility(); 8430 } 8431}); 8432 8433document.addEventListener('page-loaded', function() { 8434 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 8435 window.syncGalleryOptionsButtonVisibility(); 8436 } 8437}); 8438 8439/** 8440 * Wait for PageManager after CDNâproxy late script load (ISP/Firefox clients). 8441 * Home/page bootstrap can run before the proxy copy of page-manager-v260720-021.js executes. 8442 */ 8443function whenPageManagerReady(callback, options) { 8444 const maxAttempts = (options && options.maxAttempts) || 50; 8445 const intervalMs = (options && options.intervalMs) || 100; 8446 let attempts = 0; 8447 const run = function() { 8448 if (window.PageManager && typeof window.PageManager.loadPage === 'function') { 8449 callback(window.PageManager); 8450 return; 8451 } 8452 attempts += 1; 8453 if (attempts >= maxAttempts) { 8454 console.error('â [loadPage] Timed out waiting for PageManager after', attempts, 'attempts');
8455 if (options && typeof options.onTimeout === 'function') options.onTimeout(); 8456 return; 8457 } 8458 if (attempts === 1 || attempts % 10 === 0) { 8459 console.warn('â³ [loadPage] Waiting for PageManager (CDN proxy late load?) attempt', attempts + '/' + maxAttempts); 8460 } 8461 setTimeout(run, intervalMs); 8462 }; 8463 run(); 8464} 8465 8466/** 8467 * loadPage function that safely handles undefined galleries 8468 * @param {number} id - The gallery ID 8469 * @param {Event} event - Optional event object 8470 */ 8471function loadPage(id, event){ 8472 if (event && event.target && event.target.isConnected === false) { 8473 console.log('loadPage: Ignoring navigation from detached element'); 8474 return; 8475 } 8476 if (event) { 8477 event.preventDefault(); 8478 event.stopPropagation(); 8479 const now = Date.now(); 8480 const lastCallTime = window._lastPageLoadTime || 0; 8481 window._lastPageLoadTime = now; 8482 if (now - lastCallTime < 100) { 8483 console.log('Ignoring duplicate loadPage call'); 8484 return; 8485 } 8486 } 8487 console.log('loadPage: Loading page with gallery ID:', id); 8488 8489 stopAutoAdvanceTimer(); 8490 if (typeof dismissGalleryPasswordOverlay === 'function') { 8491 dismissGalleryPasswordOverlay(); 8492 } 8493 8494 let galleriesData = window.galleries || galleries; 8495 const gallery = findGalleryById(galleriesData, id); 8496 8497 if (!gallery) { 8498 console.warn('No gallery found with ID:', id, 'for page load.'); 8499 return; 8500 } 8501 8502 const contentLoadGeneration = beginHydraContentLoad('page', gallery.id, gallery.pageId || `page_${id}`); 8503 8504 window.activeGalleryId = id; 8505 activeGalleryId = id; 8506 window._activeContentType = 'page'; 8507 if (window.EnhancedGallerySystem) { 8508 window.EnhancedGallerySystem.currentGalleryId = null; 8509 } 8510 8511 if (typeof window.removeGalleryScriptsWithPause === 'function') { 8512 window.removeGalleryScriptsWithPause(); 8513 } 8514 8515 const galleryContainer = document.querySelector('.gallery-container'); 8516 if (galleryContainer) { 8517 galleryContainer.innerHTML = ''; 8518 } 8519 8520 if (!gallery.pageId) gallery.pageId = `page_${id}`; 8521 if (!gallery.isPage) gallery.isPage = true; 8522 8523 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 8524 window.syncGalleryOptionsButtonVisibility(); 8525 } 8526 8527 if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {}; 8528 window._pageIdToGalleryId[gallery.pageId] = id; 8529 8530 if (gallery.pageElements && Array.isArray(gallery.pageElements)) { 8531 if (window.PageManager && window.PageManager.elements) { 8532 window.PageManager.elements[gallery.pageId] = [...gallery.pageElements]; 8533 } 8534 } else if (window.PageManager && window.PageManager.elements && 8535 window.PageManager.elements[gallery.pageId] && 8536 window.PageManager.elements[gallery.pageId].length > 0) { 8537 gallery.pageElements = [...window.PageManager.elements[gallery.pageId]]; 8538 } 8539 8540 if (window.PageManager && typeof window.PageManager.loadPage === 'function') { 8541 try { 8542 window.PageManager.loadPage(gallery.pageId); 8543 const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false; 8544 if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') { 8545 window.PageManager.setEditMode(isCurrentlyEditing); 8546 } 8547 } catch (error) { 8548 console.error('Error loading page with PageManager:', error); 8549 } 8550 } else { 8551 console.error('PageManager not found yet â waiting for CDN proxy script'); 8552 whenPageManagerReady(function() { 8553 try { 8554 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) { 8555 console.log('loadPage: Skipping stale PageManager retry for', gallery.pageId); 8556 return; 8557 } 8558 if (gallery.pageElements && Array.isArray(gallery.pageElements) && window.PageManager.elements) { 8559 window.PageManager.elements[gallery.pageId] = [...gallery.pageElements]; 8560 } 8561 window.PageManager.loadPage(gallery.pageId); 8562 console.log('loadPage: PageManager became available â page render retried'); 8563 } catch (error) { 8564 console.error('Error loading page with PageManager after wait:', error); 8565 } 8566 }); 8567 } 8568 8569 // Apply menu visibility 8570 const bodyEl = document.body; 8571 if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 8572 bodyEl.classList.add('menu-hidden-on-page'); 8573 } else { 8574 bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden 8575 } 8576 applyMenuOverlayClassForGallery(gallery); 8577 8578 if (typeof updateActiveStates === 'function') updateActiveStates(); 8579 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 8580 if (typeof closeMobileMenu === 'function') closeMobileMenu(); 8581 // Close the gallery options panel when navigating to a different page 8582 if (typeof window.closeOptionsPanel === 'function') { 8583 window.closeOptionsPanel(); 8584 } 8585 if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery); 8586 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 8587 window.syncGalleryOptionsButtonVisibility(); 8588 } 8589 if (typeof window !== 'undefined' && typeof document !== 'undefined') { 8590 window.dispatchEvent(new CustomEvent('page-loaded', { 8591 detail: { pageId: gallery.pageId, galleryId: id } 8592 })); 8593 } 8594} 8595 8596 8597 8598 8599 8600function generatePageId() { 8601const characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; 8602let result = ''; 8603const charactersLength = characters.length; 8604for (let i = 0; i < 8; i++) { 8605 result += characters.charAt(Math.floor(Math.random() * charactersLength)); 8606} 8607return result; 8608}; 8609 8610 8611function normalizeNameForComparison(name) { 8612if (!name) {
8613console.log("Empty name passed to normalization"); 8614return ''; 8615} 8616 8617console.log(`Normalizing name: "${name}"`); 8618 8619// Step 1: Convert to lowercase 8620let normalized = name.toLowerCase(); 8621console.log(`After lowercase: "${normalized}"`); 8622 8623// Step 2: Replace spaces with hyphens (without regex) 8624normalized = normalized.split(' ').join('-'); 8625console.log(`After space replacement: "${normalized}"`); 8626 8627// Step 3: Filter out unwanted characters (without regex) 8628let filtered = ''; 8629for (let i = 0; i < normalized.length; i++) { 8630const char = normalized.charAt(i); 8631// Keep only a-z, 0-9, and hyphens 8632if ((char >= 'a' && char <= 'z') || 8633 (char >= '0' && char <= '9') || 8634 char === '-') { 8635 filtered += char; 8636} 8637} 8638normalized = filtered; 8639console.log(`After character filtering: "${normalized}"`); 8640 8641// Step 4: Clean up multiple hyphens (without regex) 8642while (normalized.indexOf('--') !== -1) { 8643normalized = normalized.split('--').join('-'); 8644} 8645 8646// Step 5: Remove leading and trailing hyphens (without regex) 8647if (normalized.charAt(0) === '-') { 8648normalized = normalized.substring(1); 8649} 8650if (normalized.charAt(normalized.length - 1) === '-') { 8651normalized = normalized.substring(0, normalized.length - 1); 8652} 8653 8654// Check if the result is valid 8655if (!normalized || normalized === '-') { 8656console.log(`WARNING: Normalization produced invalid result: "${normalized}"`); 8657} else { 8658console.log(`Final normalized slug: "${normalized}"`); 8659} 8660 8661return normalized; 8662} 8663 8664function ensureUniqueSlug(slug, existingGalleries) { 8665if (!slug) return 'gallery'; 8666 8667let finalSlug = slug; 8668let counter = 1; 8669 8670// Check if slug exists in any gallery 8671const slugExists = function(s) { 8672for (let i = 0; i < existingGalleries.length; i++) { 8673 if (existingGalleries[i].slug === s) { 8674 return true; 8675 } 8676} 8677return false; 8678}; 8679 8680// Keep incrementing counter until unique 8681while (slugExists(finalSlug)) { 8682finalSlug = slug + '-' + counter; 8683counter++; 8684} 8685 8686return finalSlug; 8687} 8688 8689/** 8690 * Gathers the current complete site configuration from the client-side state 8691 * and triggers a download of the data as a single JSON file. 8692 * This provides a simple way to back up or export the current site structure. 8693 */ 8694function downloadCurrentSiteJSON() { 8695 try { 8696 console.log('Gathering current site data for download...'); 8697 8698 // Get the current hostname to use in the filename. 8699 const hostname = window.location.hostname.replace('www.', ''); 8700 8701 // --- FIX: Ensure we get the most up-to-date menu styles --- 8702 // The MenuStyleCustomizer.settings object is the most reliable source 8703 // for the current styles being used on the client. 8704 const currentMenuStyles = window.MenuStyleCustomizer?.settings || {}; 8705 8706 // Get current galleries 8707 const currentGalleries = window.galleries || galleries || []; 8708 8709 // --- STREAMLINED: Process galleries without including image data --- 8710 const enhancedGalleries = currentGalleries.map(gallery => { 8711 // If this is a Classic collection gallery, add note about manual retrieval 8712 if (gallery.galleryOptions?.manualCollectionName?.startsWith('GUID=')) { 8713 const classicGuid = gallery.galleryOptions.manualCollectionName.substring(5); 8714 console.log(`Processing Classic collection gallery: ${gallery.title} (GUID: ${classicGuid})`); 8715 8716 return { 8717 ...gallery, 8718 _note: `This gallery references Classic collection GUID=${classicGuid}. To get the full image data with captions, you may need to manually fetch the Classic collection JSON from: https://storage.neonsky.app/sites:site_${classicGuid}.json`, 8719 _classicGuid: classicGuid, 8720 _requiresClassicData: true 8721 }; 8722 } 8723 8724 return gallery; 8725 }); 8726 8727 // Assemble the complete site configuration object from global state variables. 8728 // This structure mirrors what the `save-config` API endpoint expects, ensuring 8729 // the exported file is a complete and re-importable replica. 8730 const siteData = { 8731 // The core menu/page structure with enhanced Classic collection data. 8732 galleries: enhancedGalleries, 8733 8734 // Admin emails are managed server-side for security. We export the list if it's 8735 // available on the client from a previous check, but the server remains the 8736 // source of truth. A re-import will not overwrite the admin list. 8737 adminEmails: window.siteConfig?.adminEmails || [], 8738 8739 // The unique ID for the site. 8740 siteId: window.siteId || window.Parameters?.siteId || '', 8741 8742 // Custom styling rules for the menu. 8743 menuStyles: currentMenuStyles, 8744 8745 // All elements configured to appear in the sidebar. 8746 sidebarElements: window.SidebarManager?.elements || [], 8747 8748 // Global site metadata for SEO and analytics. 8749 siteMetadata: window.siteMetadata || {}, 8750 8751 // Add metadata about Classic collections that need manual data retrieval 8752 _classicCollections: enhancedGalleries 8753 .filter(g => g._requiresClassicData) 8754 .map(g => ({ 8755 title: g.title, 8756 guid: g._classicGuid, 8757 note: g._note 8758 })) 8759 }; 8760 8761 console.log('Enhanced site data collected for export:', siteData); 8762 8763 // Convert the JavaScript object to a formatted JSON string. 8764 const jsonString = JSON.stringify(siteData, null, 2); 8765 8766 // Create a Blob, which is a file-like object of immutable, raw data. 8767 const blob = new Blob([jsonString], { type: 'application/json' }); 8768 8769 // Create a temporary link element to trigger the browser's download functionality. 8770 const url = URL.createObjectURL(blob); 8771 const a = document.createElement('a'); 8772 a.href = url; 8773 a.download = `${hostname}-config.json`; // e.g., 'yoursite.com-config.json' 8774 document.body.appendChild(a); 8775 a.click(); 8776 8777 // Clean up by removing the temporary link and revoking the object URL to free up memory. 8778 document.body.removeChild(a); 8779 URL.revokeObjectURL(url); 8780 8781 console.log('Enhanced JSON download initiated successfully.'); 8782 8783 // Show a helpful message about Classic collections 8784 const classicCount = siteData._classicCollections?.length || 0; 8785 8786 if (classicCount > 0) { 8787 console.log(`Export Summary: ${classicCount} Classic collection(s) detected. The exported JSON includes gallery structure and settings, but for full image data with captions, you may
8787need to manually fetch the Classic collection JSON files.`); 8788 } 8789 8790 } catch (error) { 8791 console.error('Error creating JSON download:', error); 8792 alert('An error occurred while preparing the download. Please check the console.'); 8793 } 8794} 8795 8796// Build Hydra Classic-style NDJSON (first line: metadata; second line: imageMetadata array) 8797function buildHydraClassicNdjson(classicGuid, images) { 8798 const header = { 8799 title: 'Untitled', 8800 description: '', 8801 version: '1', 8802 isClassic: true, 8803 isClassicCollection: true, 8804 classicGuid: 'GUID=' + String(classicGuid || '') 8805 }; 8806 8807 const imageArray = Array.isArray(images) ? images.map(function(img, idx) { 8808 var path = ''; 8809 if (img) { 8810 path = img.image || img.link || img.originalUrl || (img.urls && img.urls.original) || ''; 8811 } 8812 // Derive filename base from any provided path/filename and normalize extension to .jpg 8813 var fnameBase = ''; 8814 if (typeof path === 'string' && path.length > 0) { 8815 var slash = path.lastIndexOf('/'); 8816 var raw = slash >= 0 ? path.substring(slash + 1) : path; 8817 // remove query/hash 8818 var q = raw.indexOf('?'); if (q >= 0) raw = raw.substring(0, q); 8819 var h = raw.indexOf('#'); if (h >= 0) raw = raw.substring(0, h); 8820 // strip extension if present 8821 var dot = raw.lastIndexOf('.'); 8822 fnameBase = dot > 0 ? raw.substring(0, dot) : raw; 8823 } else if (img && typeof img.filename === 'string') { 8824 var rf = img.filename; 8825 var q2 = rf.indexOf('?'); if (q2 >= 0) rf = rf.substring(0, q2); 8826 var h2 = rf.indexOf('#'); if (h2 >= 0) rf = rf.substring(0, h2); 8827 var d2 = rf.lastIndexOf('.'); 8828 fnameBase = d2 > 0 ? rf.substring(0, d2) : rf; 8829 } 8830 var ext = (img && img.metadata && typeof img.metadata.extension === 'string' && img.metadata.extension.startsWith('.')) 8831 ? img.metadata.extension : '.jpg'; 8832 var filename = fnameBase ? (fnameBase + ext) : ''; 8833 // Build CDN image URL using site GUID: https://cdn.neonsky.app/{siteGuid}/images/{filename} 8834 // Note: classicGuid is a site GUID, not an image GUID - it identifies the site, and images are stored under that site's path 8835 var guidRaw = String(classicGuid || '').replace(/^GUID=/, ''); 8836 var cdnImageUrl = filename && guidRaw ? ('https://cdn.neonsky.app/' + guidRaw + '/images/' + filename) : path; 8837 var rec = { 8838 caption: (img && (img.caption || img.description)) || '', 8839 description: (img && (img.description || img.caption)) || '', 8840 isPreview: idx === 1, 8841 title: (img && img.title) || '', 8842 'alt-text': (img && (img['alt-text'] || img.altText || '')) || '', 8843 image: cdnImageUrl || '', 8844 link: '', 8845 'link-text': '', 8846 filename: filename, 8847 isClassic: true, 8848 isClassicCollection: true, 8849 classicGuid: 'GUID=' + String(classicGuid || '') 8850 }; 8851 return rec; 8852 }) : []; 8853 8854 // Build NDJSON as: header line, imageMetadata array line, then one line per full image record 8855 var body = { imageMetadata: imageArray }; 8856 var out = [JSON.stringify(header), JSON.stringify(body)]; 8857 for (var k = 0; k < imageArray.length; k++) { 8858 out.push(JSON.stringify(imageArray[k])); 8859 } 8860 return out.join('\n') + '\n'; 8861} 8862 8863async function uploadNdjsonToTigris(siteId, pageId, ndjsonContent) { 8864 // Dual upload strategy: Fly.io (uncompressed) + Irys (compressed if <800KB) 8865 const fileName = String(siteId || '') + '_' + String(pageId || '') + '.ndjson'; 8866 const flyUrl = 'https://hydra-press-v2.fly.dev/publish'; 8867 const irysUrl = 'https://hydra-press-v2.fly.dev/upload-irys'; 8868 8869 console.error('Starting dual NDJSON upload for:', fileName); 8870 console.error('Content length:', ndjsonContent.length, 'characters'); 8871 8872 // COMPRESSION: Compress for Irys only (not Tigris) 8873 console.error('ð Compressing NDJSON with gzip for Irys...'); 8874 const encoder = new TextEncoder(); 8875 const ndjsonBytes = encoder.encode(ndjsonContent); 8876 const compressedStream = new Response(ndjsonBytes).body.pipeThrough(new CompressionStream('gzip')); 8877 const compressedBlob = await new Response(compressedStream).blob(); 8878 const compressedBytes = new Uint8Array(await compressedBlob.arrayBuffer()); 8879 8880 const originalSize = ndjsonBytes.length; 8881 const compressedSize = compressedBytes.length; 8882 const compressionRatio = ((1 - compressedSize / originalSize) * 100).toFixed(2); 8883 8884 console.error('ð Compression results:', { 8885 original: originalSize + ' bytes (' + (originalSize / 1024).toFixed(2) + ' KB)', 8886 compressed: compressedSize + ' bytes (' + (compressedSize / 1024).toFixed(2) + ' KB)', 8887 ratio: compressionRatio + '%' 8888 }); 8889 8890 const IRYS_SIZE_LIMIT = 800 * 1024; // 800KB limit for compressed Irys upload 8891 const shouldUploadToIrys = compressedSize < IRYS_SIZE_LIMIT; 8892 8893 console.error('Irys upload eligible: ' + shouldUploadToIrys + ' (compressed: ' + (compressedSize / 1024).toFixed(2) + ' KB)'); 8894 8895 // STEP 1: Upload to Irys (non-blocking, compressed if <800KB) 8896 let irysResult = null; 8897 if (shouldUploadToIrys) { 8898 try { 8899 console.error('ð Starting Irys upload (compressed, non-blocking)...'); 8900 8901 // Convert compressed bytes to base64 8902 const compressedBase64 = btoa(String.fromCharCode(...compressedBytes)); 8903 8904 const irysResp = await fetch(irysUrl, { 8905 method: 'POST', 8906 headers: { 'content-type': 'application/json' }, 8907 body: JSON.stringify({ 8908 compressedData: compressedBase64, 8909 originalSize: originalSize, 8910 compressedSize: compressedSize, 8911 siteId: siteId, 8912 pageId: pageId 8913 }) 8914 }); 8915 8916 if (irysResp.ok) { 8917 irysResult = await irysResp.json(); 8918 console.error('â Irys upload successful:', irysResult.txId); 8919 console.error(' Irys Gateway:', irysResult.urls?.irys); 8920 console.error(' ar.io Gateway:', irysResult.urls?.arweave); 8921 8922 // Log wallet balance and cost 8923 if (irysResult.walletBalance !== undefined) { 8924 console.error('ð° Wallet Balance:', irysResult.walletBalance.toFixed(6), 'AR'); 8925 } 8926 if (irysResult.uploadCost !== undefined) { 8927 const costUSD = (irysResult.uploadCost * 20).toFixed(4); 8928 if (irysResult.uploadCost === 0) { 8929 console.error('ð¸ Upload Cost: FREE (0 AR)'); 8930 } else { 8931 console.error('ð¸ Upload Cost:', irysResult.uploadCost.toFixed(6), 'AR (~$' + costUSD + ' USD at $20/AR)'); 8932 } 8933 } 8934 } else { 8935 const errorText = await irysResp.text().catch(() => 'Unknown error'); 8936 console.warn('â ï¸ Irys upload failed (non-critical):', irysResp.status, errorText); 8937 } 8938 } catch (irysError) { 8939 console.warn('â ï¸ Irys upload error (non-critical):', irysError.message); 8940 // Continue - Irys failure doesn't block Fly.io upload 8941 } 8942 } else { 8943 console.error('âï¸ Skipping Irys upload (compressed size exceeds 800KB limit)'); 8944 } 8945 8946 // STEP 2: Upload to Fly.io (CRITICAL - must succeed, uncompressed) 8947 // Hydra sites require Bearer auth - get token from TokenManager/localStorage (same as Edition Publisher) 8948 try { 8949 console.error('ð Starting Fly.io upload (uncompressed)...'); 8950 const flyHeaders = { 'content-type': 'application/json' }; 8951 let authToken = null; 8952 try { 8953 if (window.TokenManager) { 8954 if (typeof window.TokenManager.getHydraAuthToken === 'function') { 8955 try { authToken = window.TokenManager.getHydraAuthToken(); } catch (e) {} 8956 }
8957 if (!authToken && typeof window.TokenManager.getBestToken === 'function') { 8958 try { 8959 const tw = window.TokenManager.getBestToken(); 8960 authToken = tw && tw.startsWith('Bearer ') ? tw.substring(7) : tw; 8961 } catch (e) {} 8962 } 8963 } 8964 if (!authToken) authToken = localStorage.getItem('hydra_auth_token'); 8965 if (!authToken) authToken = window.authToken; 8966 if (authToken) { 8967 flyHeaders['Authorization'] = authToken.includes('.') || authToken.startsWith('hydra:') 8968 ? 'Bearer ' + authToken 8969 : (authToken.startsWith('Bearer ') ? authToken : 'Bearer hydra:' + authToken); 8970 console.error('ð Fly.io auth: token attached'); 8971 } else { 8972 console.warn('â ï¸ No auth token - Fly.io may reject Hydra site upload (401)'); 8973 } 8974 } catch (authErr) { 8975 console.warn('â ï¸ Auth token lookup failed:', authErr.message); 8976 } 8977 const flyResp = await fetch(flyUrl, { 8978 method: 'POST', 8979 headers: flyHeaders, 8980 body: JSON.stringify({ ndjsonContent, dataFileName: fileName }) 8981 }); 8982 8983 console.log('Server response status:', flyResp.status); 8984 8985 if (!flyResp.ok) { 8986 const errorText = await flyResp.text().catch(() => 'Unknown error'); 8987 console.error('â Fly.io upload failed', flyResp.status, 'for', fileName + ':', errorText); 8988 if (flyResp.status === 401) { 8989 console.error('ð¡ Tip: Open the Edition Publisher (or log in as admin) first to authenticate for Fly.io uploads.'); 8990 } 8991 return { success: false, irysResult: null }; 8992 } 8993 8994 const flyData = await flyResp.json().catch(() => null); 8995 if (flyData && flyData.urls) { 8996 console.log('â Fly.io upload successful:', flyData.urls); 8997 console.log(' Tigris URL:', flyData.urls.urlTigris); 8998 8999 // Return comprehensive result 9000 const result = { 9001 success: true, 9002 flyUrls: flyData.urls, 9003 irysResult: irysResult, 9004 uploadedTo: irysResult ? ['fly', 'irys'] : ['fly'], 9005 fileSize: compressedSize, // Irys compressed size 9006 originalFileSize: originalSize // Original uncompressed size 9007 }; 9008 9009 console.log('ð¦ Upload complete:', { 9010 destinations: result.uploadedTo.join(' + '), 9011 txId: irysResult?.txId || 'N/A', 9012 flyUrl: flyData.urls.urlTigris 9013 }); 9014 9015 return result; 9016 } else { 9017 console.warn('Fly.io upload response missing URLs:', flyData); 9018 return { success: false, irysResult: null }; 9019 } 9020 } catch (flyError) { 9021 console.error('â Fly.io upload error for', fileName + ':', flyError); 9022 return { success: false, irysResult: null }; 9023 } 9024} 9025 9026async function importClassicGalleries() { 9027 // Browser-scoped helper: mirrors server-side behavior with escaped patterns for template-safe JS 9028 function cleanDescriptionLeadingLines(text) { 9029 if (!text || typeof text !== 'string') return text; 9030 9031 let result = text; 9032 9033 // Remove leading empty TEXTFORMAT blocks: <TEXTFORMAT><P><FONT></FONT></P></TEXTFORMAT> 9034 result = result.replace( 9035 new RegExp( 9036 '^(\s*\<TEXTFORMAT[^\>]*\>\s*\<P[^\>]*\>\s*\<FONT[^\>]*\>\s*\<\/FONT\>\s*\<\/P\>\s*\<\/TEXTFORMAT\>\s*)+', 9037 'i' 9038 ), 9039 '' 9040 ); 9041 9042 // Remove leading empty P tags 9043 result = result.replace( 9044 new RegExp('^(\s*\<P[^\>]*\>\s*\<\/P\>\s*)+', 'i'), 9045 '' 9046 ); 9047 9048 // Remove leading empty FONT tags 9049 result = result.replace( 9050 new RegExp('^(\s*\<FONT[^\>]*\>\s*\<\/FONT\>\s*)+', 'i'), 9051 '' 9052 ); 9053 9054 // Remove leading BR tags 9055 result = result.replace( 9056 new RegExp('^(\s*\<br\s*\/\?>\s*)+', 'i'), 9057 '' 9058 ); 9059 9060 // Remove any remaining leading whitespace 9061 return result.replace(new RegExp('^\s+', 'i'), '').trim(); 9062 } 9063 9064 // Browser-scoped helper: fix multi-line links (template-safe escaped regex) 9065 function fixMultilineLinks(text) { 9066 if (!text || typeof text !== 'string') return text; 9067 return text.replace( 9068 new RegExp('<a[^>]*>.*?Click.*?<\/a><a[^>]*>.*?HERE.*?<\/a><a[^>]*>.*?to Add to Cart.*?<\/a>', 'gis'), 9069 function (match) { 9070 const first = match.match(new RegExp('<a href="([^"]*)" target="([^"]*)"', 'i')); 9071 if (first) { 9072 return '<a href="' + first[1] + '" target="' + first[2] + '">
9072Click HERE to Add to Cart</a>'; 9073 } 9074 return match; 9075 } 9076 ); 9077 } 9078 9079 // Browser-scoped helper: convert plain text link phrase to HTML link 9080 function convertPlainTextLinks(text) { 9081 if (!text || typeof text !== 'string') return text; 9082 return text.replace( 9083 new RegExp('Click HERE to Add to Cart', 'g'), 9084 '<a href="#" target="_blank">Click HERE to Add to Cart</a>' 9085 ); 9086 } 9087 // Get input elements, including the new checkbox 9088 const guidInputEl = document.getElementById('classicGuid'); 9089 const pastedJsonEl = document.getElementById('pastedJson'); 9090 const parentIdInputEl = document.getElementById('importParent'); 9091 const createSubmenuCheckboxEl = document.getElementById('createSubmenu'); 9092 const submenuTitleInputEl = document.getElementById('submenuTitle'); 9093 const replaceMenuCheckboxEl = document.getElementById('replaceMenuCheckbox'); 9094 9095 // Get values from inputs 9096 const guidInput = guidInputEl instanceof HTMLInputElement ? guidInputEl.value.trim() : ''; 9097 const pastedJson = pastedJsonEl instanceof HTMLTextAreaElement ? pastedJsonEl.value.trim() : ''; 9098 const replaceMenu = replaceMenuCheckboxEl instanceof HTMLInputElement ? replaceMenuCheckboxEl.checked : false; 9099 9100 let parentId = parentIdInputEl instanceof HTMLSelectElement && parentIdInputEl.value ? parseInt(parentIdInputEl.value) : null; 9101 let createSubmenu = createSubmenuCheckboxEl instanceof HTMLInputElement ? createSubmenuCheckboxEl.checked : false; 9102 let submenuTitle = submenuTitleInputEl instanceof HTMLInputElement ? submenuTitleInputEl.value.trim() : ''; 9103 9104 // If replacing the menu, ignore parent/submenu settings 9105 if (replaceMenu) { 9106 parentId = null; 9107 createSubmenu = false; 9108 } 9109 9110 // Validate submenu title if checkbox is checked 9111 if (createSubmenu && !submenuTitle) { 9112 alert('Please provide a title for the submenu'); 9113 return; 9114 } 9115 9116 // Show status display 9117 const statusEl = document.getElementById('importStatus'); 9118 const progressBarEl = statusEl ? statusEl.querySelector('.progress-bar') : null; 9119 const statusMessageEl = statusEl ? statusEl.querySelector('.status-message') : null; 9120 const progressBar = progressBarEl instanceof HTMLElement ? progressBarEl : null; 9121 const statusMessage = statusMessageEl instanceof HTMLElement ? statusMessageEl : null; 9122 9123 if (statusEl && progressBar && statusMessage) { 9124 statusEl.style.display = 'block'; 9125 statusEl.classList.add('importing'); 9126 progressBar.style.width = '10%'; 9127 statusMessage.textContent = 'Preparing to import...'; 9128 } 9129 9130 try {
9131 let classicData; 9132 let classicGuid; // Will hold the primary GUID for image path lookups 9133 9134 // --- LOGIC TO GET DATA: Prioritize Pasted JSON --- 9135 if (pastedJson) { 9136 if (statusMessage) statusMessage.textContent = 'Parsing pasted JSON...'; 9137 classicData = JSON.parse(pastedJson); 9138 9139 if (!classicData.galleries || !Array.isArray(classicData.galleries)) { 9140 throw new Error('Pasted JSON is invalid: "galleries" array not found.'); 9141 } 9142 // Use the first available GUID from the pasted data as the default for images 9143 classicGuid = classicData.galleries[0]?.classicGuid || classicData.siteId; 9144 9145 } else if (guidInput) { 9146 // Check if this is the new import_ format 9147 if (guidInput.startsWith('import_')) { 9148 if (statusMessage) statusMessage.textContent = 'Fetching data via import_ format...'; 9149 const importGuid = guidInput.substring(7); // Remove 'import_' prefix 9150 9151 const jsonUrl = `https://hydra.neonsky.app/json-imports/${guidInput}.json`; 9152 const response = await fetch(jsonUrl); 9153 if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`); 9154 9155 classicData = await response.json(); 9156 9157 if (!classicData.galleries || !Array.isArray(classicData.galleries)) {
9158 throw new Error('Fetched import JSON is invalid: "galleries" array not found.'); 9159 } 9160 9161 // HYBRID APPROACH: Also fetch the original GUID JSON for image metadata 9162 if (statusMessage) statusMessage.textContent = 'Fetching image metadata from original JSON...'; 9163 9164 const originalJsonUrl = `https://cdn.neonsky.app/sites:site_${importGuid}.json`; 9165 9166 try { 9167 const originalResponse = await fetch(originalJsonUrl); 9168 if (originalResponse.ok) { 9169 const originalData = await originalResponse.json(); 9170 9171 // Get the default language from site configuration 9172 let defaultLanguageID = 'en'; // fallback 9173 if (originalData.languages && Array.isArray(originalData.languages)) { 9174 const defaultLang = originalData.languages.find(lang => lang.isDefault === true); 9175 if (defaultLang) { 9176 defaultLanguageID = defaultLang.id; 9177 } 9178 } 9179 9180 // Merge image metadata from original JSON into import JSON galleries 9181 if (originalData.galleries && Array.isArray(originalData.galleries)) { 9182 classicData.galleries = classicData.galleries.map(importGallery => { 9183 // Find matching gallery in original data by name or categoryID 9184 9185 const originalGallery = originalData.galleries.find(orig => 9186 orig.title === importGallery.title || // Primary match by title 9187 orig.name === importGallery.title || // Fallback: orig.name matches import.title 9188 orig.categoryID === importGallery.categoryID // Fallback: categoryID match 9189 ); 9190 9191 9192 if (originalGallery && originalGallery.images && Array.isArray(originalGallery.images)) { 9193 9194 // Process all images from original gallery with metadata extraction 9195 const processedImages = originalGallery.images.map(originalImage => { 9196 // Apply EXACT same metadata processing logic as Edition Publisher 9197 let title = originalImage.title || ''; 9198 let dateline = originalImage.dateline || ''; 9199 let caption = originalImage.caption || ''; 9200 9201 // Check languageVersions array for metadata (this is where most data is stored) 9202 if (originalImage.languageVersions && originalImage.languageVersions.length > 0) { 9203 // Look for site's default language first, then fall back to first available 9204 let langData = originalImage.languageVersions.find(lang => lang.languageID === defaultLanguageID); 9205 if (!langData) { 9206 langData = originalImage.languageVersions[0]; 9207 } 9208 9209 // For language-enabled JSON files, prioritize languageVersions data 9210 // Only fall back to top-level if languageVersions data is empty 9211 if (!title || title.trim() === '') title = langData.title || ''; 9212 if (!dateline || dateline.trim() === '') dateline = langData.dateline || ''; 9213 if (!caption || caption.trim() === '') caption = langData.caption || ''; 9214 } 9215 9216 // Convert Flash TEXTFORMAT to HTML (properly escaped for template literals) 9217 let description = caption; 9218 if (caption && caption.includes('TEXTFORMAT')) { 9219 try { 9220 // Use properly escaped HTML characters for template literal context 9221 description = caption 9222 .replace(/\<TEXTFORMAT[^\>]*\>/g, '') 9223 .replace(/\<\/TEXTFORMAT\>/g, '') 9224 .replace(/\<P[^\>]*\>/g, '\<p\>') 9225 .replace(/\<\/P\>/g, '\<\/p\>') 9226 .replace(/\<FONT[^\>]*\>/g, '') 9227 .replace(/\<\/FONT\>/g, '') 9228 .replace(/\<A HREF="([^"]*)"[^\>]*\>/g, '\<a href="$1" target="_blank"\>') 9229 .replace(/\<\/A\>/g, '\<\/a\>') 9230 .replace(/\<U\>/g, '\<u\>') 9231 .replace(/\<\/U\>/g, '\<\/u\>') 9232 .replace(/\<B\>/g, '\<strong\>') 9233 .replace(/\<\/B\>/g, '\<\/strong\>') 9234 .replace(/\<I\>/g, '\<em\>') 9235 .replace(/\<\/I\>/g, '\<\/em\>') 9236 .replace(/"/g, '"') 9237 .replace(/'/g, "'") 9238 .replace(/\s+/g, ' ') 9239 .trim(); 9240 } catch (error) { 9241 console.warn('TEXTFORMAT conversion failed, using original caption:', error); 9242 description = caption; 9243 } 9244 } 9245 9246 // Prepend dateline to description if it exists (convert to uppercase) 9247 if (dateline) { 9248 const uppercaseDateline = dateline.toUpperCase(); 9249 description = `${uppercaseDateline} ${description}`.trim(); 9250 } 9251 9252 // Apply description fixes 9253 description = cleanDescriptionLeadingLines(description); 9254 description = fixMultilineLinks(description); 9255 description = convertPlainTextLinks(description); 9256 9257 const imageEntry = { 9258 // Match EXACT structure from Edition Publisher hydraImageEntry 9259 image: originalImage.image || originalImage.urls?.original || '', 9260 caption: description, // Use processed description as caption 9261 description: description, // ALSO set description field (like Edition Publisher) 9262 title: title, 9263 'alt-text': title, // Use title as alt-text like Edition Publisher 9264 link: originalImage.urls?.original || '', 9265 'link-text': originalImage.urls?.original || '', 9266 isPreview: false, 9267 filename: originalImage.filename || '', 9268 originalUrl: originalImage.image || originalImage.urls?.original || '', 9269 dateline: dateline 9270 }; 9271 9272 return imageEntry; 9273 }); 9274 9275 return { 9276 ...importGallery, // Keep import gallery structure (order, settings, etc.) 9277 images: processedImages // Replace with processed original images 9278 }; 9279 } 9280 return importGallery; 9281 }); 9282 } 9283 } else { 9284 console.warn('Could not fetch original JSON for metadata, using import data only'); 9285 } 9286 } catch (error) { 9287 console.warn('Error fetching original JSON for metadata:', error); 9288 } 9289 9290 // Extract original GUID for image path lookups 9291 // If importGuid has a suffix (e.g., "4bd5ebf7bda9d-all"), extract the base GUID 9292 // GUIDs are typically 13 characters, so split on common separators 9293 let baseGuid = importGuid; 9294 const guidMatch = importGuid.match(/^([a-f0-9]{13})(?:-.*)?$/i); 9295 if (guidMatch) { 9296 baseGuid = guidMatch[1]; 9297 } else { 9298 // Fallback: try to extract first 13-character hex string 9299 const hexMatch = importGuid.match(/^([a-f0-9]{13})/i); 9300 if (hexMatch) { 9301 baseGuid = hexMatch[1]; 9302 } 9303 } 9304 9305 // Use the base GUID for image path lookups (e.g., /images/filename.jpg) 9306 classicGuid = baseGuid; 9307 console.log('Successfully loaded hybrid import data with import GUID:', importGuid); 9308 console.log('Using base GUID for image paths:', classicGuid); 9309 9310 } else { 9311 // Standard GUID format - existing logic 9312 if (statusMessage) statusMessage.textContent = 'Fetching data via GUID...';
9313 classicGuid = guidInput.startsWith('GUID=') ? guidInput.substring(5) : guidInput; 9314 console.log('Detected standard GUID format, GUID:', classicGuid); 9315 9316 const jsonUrl = `https://storage.neonsky.app/sites:site_${classicGuid}.json`; 9317 console.log('Fetching classic JSON from:', jsonUrl); 9318 const response = await fetch(jsonUrl); 9319 9320 if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`); 9321 classicData = await response.json(); 9322 9323 if (!classicData.galleries || !Array.isArray(classicData.galleries)) { 9324 throw new Error('Fetched JSON is invalid: "galleries" array not found.'); 9325 } 9326 9327 console.log('Successfully loaded classic data with GUID:', classicGuid); 9328 } 9329 9330 } else { 9331 alert('Please either paste site JSON or enter a Classic GUID to import.'); 9332 if (statusEl) statusEl.style.display = 'none'; 9333 return; 9334 } 9335 9336 const galleriesForImport = classicData.galleries; 9337 if (statusMessage) statusMessage.textContent = `Found ${galleriesForImport.length} items to import...`; 9338 9339 // import_ format: galleries come from DB/import JSON, not classic site - treat as new Hydra NDJSON galleries 9340 const isImportFormat = !!(guidInput && guidInput.startsWith('import_')); 9341 9342 // --- HIERARCHY RECONSTRUCTION LOGIC --- 9343 9344 const currentSiteId = window.Parameters?.siteId || siteId; 9345 let submenuParentId = parentId; 9346 const tempNewItems = []; 9347 const originalIdToNewIdMap = {}; 9348 9349 // Create the optional top-level submenu if requested 9350 if (createSubmenu) { 9351 const submenuItem = { 9352 id: Date.now(), 9353 title: submenuTitle, 9354 isSubmenu: true, 9355 isCollapsed: true, 9356 visible: true, 9357 parentId: parentId, 9358 siteId: currentSiteId, 9359 children: [] 9360 }; 9361 galleries.push(submenuItem); 9362 submenuParentId = submenuItem.id; 9363 } 9364 9365 // --- Pass 1: Create new items with correct data and map old IDs to new IDs --- 9366 for (let index = 0; index < galleriesForImport.length; index++) { 9367 const classicItem = galleriesForImport[index]; 9368 const newItemId = Date.now() + index + 100; 9369 originalIdToNewIdMap[classicItem.id] = newItemId; 9370 9371 if (progressBar) progressBar.style.width = `${40 + Math.round(index * 50 / galleriesForImport.length)}%`; 9372 if (statusMessage) statusMessage.textContent = `Processing item ${index + 1} of ${galleriesForImport.length}...`; 9373 9374 const title = classicItem.title || classicItem.languageVersions?.[0]?.title || classicItem.name || `Item ${classicItem.categoryID}`; 9375 9376 let slug; 9377 // If pasting from JSON, *always* regenerate the slug from the title to ensure it's clean. 9378 // Otherwise (for GUID import), trust the existing slug if it's there. 9379 if (pastedJson) { 9380 slug = normalizeNameForComparison(title); 9381 } else if (classicItem.slug && classicItem.slug.trim() !== '') { 9382 slug = classicItem.slug; 9383 } else { 9384 slug = normalizeNameForComparison(title); 9385 } 9386const finalSlug = ensureUniqueSlug(slug || 'gallery', [...galleries, ...tempNewItems]); // Ensure uniqueness 9387 9388 // Determine the GUID to use for this specific item's image paths 9389 const itemGuid = classicItem.classicGuid || classicGuid; 9390 9391 let newItem; 9392 9393 // --- Check if it's an External URL --- 9394 if (classicItem.isExternal === true && classicItem.url) { 9395 console.log(`Creating external URL menu item: "${title}" -> ${classicItem.url}`); 9396 newItem = { 9397 id: newItemId, 9398 title, 9399 url: classicItem.url, 9400 isExternal: true, 9401 visible: classicItem.visible !== false, 9402 parentId: null, 9403 siteId: currentSiteId, 9404 slug: finalSlug, 9405 importSource: 'external-url' 9406 }; 9407 } 9408 // --- Check if it's a Page and build its elements --- 9409 else if (classicItem.isPage === true || classicItem.type === 3 || classicItem.behaviorID === 3) { 9410 const pageUniqueId = `page_${newItemId}`; 9411 9412 let pageElements = []; 9413 9414 // ================================================================= 9415 // ENHANCED: Handle pageElements for both pasted JSON and import_guid.json format 9416 // ================================================================= 9417 if (classicItem.pageElements && Array.isArray(classicItem.pageElements)) { 9418 console.log(`Using existing pageElements for page: "${title}"`); 9419 9420 // Check if we have both pageElements and images - create two-column layout 9421 if (classicItem.images && classicItem.images.length > 0) { 9422 console.log(`Creating two-column layout for page: "${title}" with ${classicItem.images.length} images`); 9423 9424 // Create metadata element 9425 pageElements.push({ 9426 id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0, 9427 metaTitle: classicItem.meta?.title || title, 9428 metaDescription: classicItem.meta?.description || "", 9429 metaKeywords: classicItem.meta?.keywords || "" 9430 }); 9431 9432 // Create two-column container 9433 const leftColumnElements = []; 9434 const rightColumnElements = []; 9435 9436 // Add images to left column 9437 const slides = classicItem.images.map((img) => { 9438 const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg'); 9439 const imageKey = `${itemGuid}/images/${imageFilename}`; 9440 const imageUrl = `https://storage.neonsky.app/${imageKey}`; 9441 9442 return { 9443 imageUrl, 9444 imageKey, 9445 imageFilename, 9446 imageType: img.metadata?.mimetype || 'image/jpeg', 9447 caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))), 9448 title: img.title || '', 9449 byline: img.byline || '', 9450 dateline: img.dateline || '', 9451 altText: img.metadata?.alt_text || '' 9452 }; 9453 }); 9454 9455 leftColumnElements.push({ 9456 id: newItemId + 3100, type: "slideshow", title: "Page Images", visible: true, position: 0, 9457 slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true 9458 }); 9459 9460 // Add text content to right column
9461 classicItem.pageElements.forEach((el, elIndex) => { 9462 if (el.type === 'text' && el.content) { 9463 rightColumnElements.push({ 9464 id: newItemId + 3200 + elIndex, type: "text", title: "Text Block", visible: true, position: elIndex, 9465 textContent: el.content, textWidth: 70 9466 }); 9467 } 9468 }); 9469 9470 const columns = [ 9471 { id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements }, 9472 { id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements } 9473 ]; 9474 9475 pageElements.push({ 9476 id: newItemId + 3000, type: "column-container", title: "Two Column Layout", visible: true, position: 1, 9477 columns, 9478 backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false, showSingleRandomImage: false, randomStart: false }, 9479 backgroundVideo: { enabled: false, videoUrl: '', videoUrls: [], randomizeVideo: false, videoHeight: 50 } 9480 }); 9481 9482 } else { 9483 // No images, just transform the pageElements as before 9484 pageElements = classicItem.pageElements.map((el, elIndex) => { 9485 const elementId = Date.now() + index + 1000 + elIndex; 9486 9487 // Handle import_guid.json format (type + content) 9488 if (el.type && el.content) { 9489 if (el.type === 'text') { 9490 return { 9491 id: elementId, 9492 type: 'text', 9493 title: 'Text Block', 9494 visible: true, 9495 position: elIndex, 9496 textContent: el.content, 9497 textWidth: 100 9498 }; 9499 } else if (el.type === 'metadata') { 9500 return { 9501 id: elementId, 9502 type: 'metadata', 9503 title: 'Metadata', 9504 visible: true, 9505 position: elIndex, 9506 metaTitle: el.metaTitle || title, 9507 metaDescription: el.metaDescription || '', 9508 metaKeywords: el.metaKeywords || '' 9509 }; 9510 } 9511 } 9512 9513 // Handle existing format (already has id, position, etc.) 9514 return { 9515 ...el, 9516 id: elementId, 9517 position: el.position !== undefined ? el.position : elIndex 9518 }; 9519 }); 9520 } 9521 9522 } else { 9523 // Otherwise (for GUID-based import), generate the page structure from scratch. 9524 console.log(`Generating new pageElements for page: "${title}"`); 9525 pageElements.push({ 9526 id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0, 9527 metaTitle: classicItem.meta?.title || title, 9528 metaDescription: classicItem.meta?.description || "", 9529 metaKeywords: classicItem.meta?.keywords || "" 9530 }); 9531 9532 // Add images if they exist in the page 9533 if (classicItem.images && classicItem.images.length > 0) { 9534 const slides = classicItem.images.map((img) => { 9535 const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg'); 9536 const imageKey = `${itemGuid}/images/${imageFilename}`; 9537 const imageUrl = `https://storage.neonsky.app/${imageKey}`; 9538 9539 return { 9540 imageUrl, 9541 imageKey, 9542 imageFilename, 9543 imageType: img.metadata?.mimetype || 'image/jpeg', 9544 caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))), 9545 title: img.title || '', 9546 byline: img.byline || '', 9547 dateline: img.dateline || '', 9548 altText: img.metadata?.alt_text || '' 9549 }; 9550 }); 9551 9552 pageElements.push({ 9553 id: newItemId + 1500, type: "slideshow", title: "Page Images", visible: true, position: 1, 9554 slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 100, slideshowHeight: 60, showFullImages: true 9555 }); 9556 } 9557 9558 pageElements.push({ 9559 id: newItemId + 2000, type: "text", title: "Text Block Spacer", visible: true, position: 2, 9560 textContent: "<p> </p><p><br></p>", textWidth: 100 9561 }); 9562 9563 const columns = []; 9564 const leftColumnElements = []; 9565 const rightColumnElements = []; 9566 9567 if (classicItem.images && classicItem.images.length > 0) { 9568 const slides = classicItem.images.map((img) => { 9569 const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg'); 9570 const imageKey = `${itemGuid}/images/${imageFilename}`; 9571 const imageUrl = `https://storage.neonsky.app/${imageKey}`; 9572 9573 // Preserve all image metadata including captions, titles, etc. 9574 return { 9575 imageUrl, 9576 imageKey, 9577 imageFilename, 9578 imageType: img.metadata?.mimetype || 'image/jpeg', 9579 // Preserve caption information - handle both direct and language version storage patterns 9580 caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || img.languageVersions?.[0]?.caption || ''))), 9581 title: img.title || img.languageVersions?.[0]?.title || '', 9582 byline: img.byline || img.languageVersions?.[0]?.byline || '', 9583 dateline: img.dateline || img.languageVersions?.[0]?.dateline || '', 9584 altText: img.metadata?.alt_text || '', 9585 // Preserve any additional metadata 9586 metaTitle: img.metadata?.meta_title || '', 9587 metaDescription: img.metadata?.meta_description || '', 9588 metaKeywords: img.metadata?.meta_keywords || '', 9589 // Preserve gallery-specific data 9590 sortOrder: img.galleryData?.sortOrder || 0, 9591 linkURL: img.galleryData?.linkURL || null, 9592 linkTarget: img.galleryData?.linkTarget || '_self' 9593 }; 9594 }); 9595 leftColumnElements.push({ 9596 id: newItemId + 3100, type: "slideshow", title: "Image Slideshow", visible: true, position: 0, 9597 slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true 9598 }); 9599 } 9600 9601 let rawDetails = classicItem.details || classicItem.languageVersions?.[0]?.details || ""; 9602 if (rawDetails && typeof rawDetails === 'string') { 9603 // ================================================================= 9604 // CHANGE 2: Fix for "Invalid regular expression flags" error. 9605 // Changed from /.../gi literal to new RegExp('...', 'gi') constructor 9606 // to prevent syntax errors. 9607 // ================================================================= 9608 let processedText = rawDetails.replace(new RegExp('<TEXTFORMAT.*?>', 'gi'), '').replace(new RegExp('</TEXTFORMAT>', 'gi'), ''); 9609 processedText = processedText.replace(new RegExp('<P ALIGN=".*?"', 'gi'), '<p').replace(new RegExp('</P>', 'gi'), '</p>'); 9610 processedText = processedText.replace(new RegExp('<FONT.*?>', 'gi'), '').replace(new RegExp('</FONT>', 'gi'), ''); 9611 processedText = processedText.replace(new RegExp('<B>
9611', 'gi'), '<strong>').replace(new RegExp('</B>', 'gi'), '</strong>'); 9612 processedText = processedText.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'); 9613 9614 // Apply description fixes to processed text 9615 processedText = cleanDescriptionLeadingLines(processedText); 9616 processedText = fixMultilineLinks(processedText); 9617 processedText = convertPlainTextLinks(processedText); 9618 9619 rightColumnElements.push({ 9620 id: newItemId + 3200, type: "text", title: "Text Block Content", visible: true, position: 0, 9621 textContent: processedText, textWidth: 70 9622 }); 9623 } 9624 9625 columns.push({ id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements }); 9626 columns.push({ id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements }); 9627 9628 pageElements.push({ 9629 id: newItemId + 3000, type: "column-container", title: "Imported Columns", visible: true, position: 2, 9630 columns, 9631 backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false, showSingleRandomImage: false, randomStart: false }, 9632 backgroundVideo: { enabled: false, videoUrl: '', videoUrls: [], randomizeVideo: false, videoHeight: 50 } 9633 }); 9634 } 9635 9636 newItem = { 9637 id: newItemId, title, isPage: true, isIntegrated: true, isSubmenu: false, pageId: pageUniqueId, visible: classicItem.visible !== false, parentId: null, 9638 siteId: currentSiteId, slug: finalSlug, url: `/${finalSlug}`, pageElements, 9639 classicCategoryId: classicItem.categoryID, classicGuid: itemGuid, importSource: 'classic-page' 9640 }; 9641 } else if (classicItem.isFolder === true) { 9642 // --- Handle folders from import_guid.json format --- 9643 console.log(`Creating folder: "${title}"`); 9644 newItem = { 9645 id: newItemId, 9646 title, 9647 isFolder: true, 9648 isCollapsed: classicItem.isCollapsed !== false, 9649 visible: classicItem.visible !== false, 9650 parentId: null, 9651 siteId: currentSiteId, 9652 slug: finalSlug, 9653 importSource: 'folder' 9654 }; 9655 } else { 9656 // --- This is a Gallery --- 9657 const galleryPageId = generatePageId(); 9658 9659 // --- ENHANCED: Try to fetch and include actual image data with captions --- 9660 let enhancedGalleryOptions = { 9661 manualCollectionName: `GUID=${itemGuid}`, 9662 categoryId: classicItem.categoryID, 9663 importedFromClassic: true, 9664 siteId: currentSiteId, 9665 pageId: galleryPageId 9666 }; 9667 9668 // --- ENHANCED: Preserve gallery display settings from Classic JSON --- 9669 if (classicItem.galleryOptions) { 9670 console.log(`Preserving gallery settings for: ${title}`); 9671 9672 // Preserve layout and display settings 9673 const preservedSettings = [ 9674 'startInSingles', 'layoutType', 'columns', 'spacing', 9675 'showDescription', 'showFilename', 'displayAllInfo', 9676 'navigationMode', 'showTextBlock', 'fixedHeroImage', 9677 'zoomInLightbox', 'lightboxOnMobile', 'fadeDuration', 9678 'fadeInDuration', 'descriptionTextColor', 'gridImageOverlayColor', 9679 'gridImageOverlayOpacity', 'lightboxBgColor', 'lightboxBgOpacity', 9680 'lightboxCloseColor', 'lightboxArrowColor', 'useTitles', 9681 'useLinks', 'desktopTitleDisplayMode', 'titleTextAlign', 9682 'showDescriptionInOverlay', 'includeRolloverImageInOverlay', 9683 'filterMenuEnabled', 'filterMenuCollectionNames', 'filterMenuTitles', 9684 'filterMenuStyle', 'rolloverSwap', 'rolloverCollectionNames', 9685 'openMultipleLightboxes', 'batchSize', 'autoplaySingles', 9686 'autoplayDuration', 'autoplayTransition', 'autoplayRandomStart' 9687 ]; 9688
9689 preservedSettings.forEach(setting => { 9690 if (classicItem.galleryOptions[setting] !== undefined) { 9691 enhancedGalleryOptions[setting] = classicItem.galleryOptions[setting]; 9692 } 9693 }); 9694 9695 // Preserve text content 9696 if (classicItem.galleryOptions.horizontalScrollerText) { 9697 enhancedGalleryOptions.horizontalScrollerText = classicItem.galleryOptions.horizontalScrollerText; 9698 } 9699 9700 // Preserve JSON collections if present 9701 if (classicItem.galleryOptions.jsonCollections) { 9702 enhancedGalleryOptions.jsonCollections = classicItem.galleryOptions.jsonCollections; 9703 } else { 9704 // Create jsonCollections structure with processed image metadata 9705 const collectionKey = 'GUID=' + itemGuid; 9706 const processedImages = classicItem.images || []; 9707 9708 enhancedGalleryOptions.jsonCollections = { 9709 [collectionKey]: { 9710 isClassicCollection: true, 9711 guid: collectionKey, 9712 metadata: processedImages, 9713 totalItems: processedImages.length 9714 } 9715 }; 9716 9717 console.log('Created jsonCollections for ' + title + ' with ' + processedImages.length + ' images'); 9718 9719 // Build and try to upload NDJSON for this gallery. 9720 // If upload succeeds, switch to metadata = {} so frontend fetches NDJSON. 9721 // Skip NDJSON generation for galleries with no images to avoid creating empty files 9722 if (processedImages.length > 0) { 9723 try { 9724 console.log('Building and uploading NDJSON for gallery:', title, '(' + processedImages.length + ' images)'); 9725 const ndjsonContent = buildHydraClassicNdjson(itemGuid, processedImages); 9726 console.log('Generated NDJSON content length:', ndjsonContent.length, 'characters'); 9727 9728 const uploadResult = await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent); 9729 if (uploadResult && uploadResult.success) { 9730 try { 9731 // import_ format: treat as new Hydra NDJSON gallery (not classic-dependent) 9732 if (isImportFormat) { 9733 const hydraCollectionKey = 'GUID=' + currentSiteId; 9734 enhancedGalleryOptions.manualCollectionName = hydraCollectionKey; 9735 enhancedGalleryOptions.jsonCollections = { 9736 [hydraCollectionKey]: { 9737 isClassicCollection: false, 9738 guid: hydraCollectionKey, 9739 metadata: {}, 9740 totalItems: processedImages.length 9741 } 9742 }; 9743 enhancedGalleryOptions.siteAlias = window.Parameters?.SiteAlias || window.location?.hostname || currentSiteId; 9744 enhancedGalleryOptions.initialPageUuid = galleryPageId; 9745 enhancedGalleryOptions.isHydra = true; 9746 enhancedGalleryOptions.flyIoSaveTimestamp = Date.now(); 9747 console.log('NDJSON uploaded for import_ gallery "' + title + '" - configured as Hydra NDJSON (not classic)'); 9748 } else { 9749 enhancedGalleryOptions.jsonCollections[collectionKey].metadata = {}; 9750 enhancedGalleryOptions.jsonCollections[collectionKey].totalItems = processedImages.length; 9751 } 9752 9753 // Store dual upload metadata if Irys was successful 9754 if (uploadResult.irysResult && uploadResult.irysResult.txId) { 9755 enhancedGalleryOptions.txId = uploadResult.irysResult.txId; 9756 enhancedGalleryOptions.permaURL = uploadResult.irysResult.txId; // Plain txId for racing 9757 enhancedGalleryOptions.isPerma = true; 9758 enhancedGalleryOptions.uploadedTo = uploadResult.uploadedTo; 9759 enhancedGalleryOptions.fileSize = uploadResult.fileSize; 9760 enhancedGalleryOptions.siteId = currentSiteId; 9761 enhancedGalleryOptions.pageId = galleryPageId; 9762 console.log('â Dual upload complete - Irys txId:', uploadResult.irysResult.txId); 9763 console.log(' Uploaded to:', uploadResult.uploadedTo.join(' + ')); 9764 } else { 9765 console.log('â Single upload (Fly.io only) - No Irys data'); 9766 } 9767 } catch (applyErr) { 9768 console.warn('Failed to switch metadata to {} after NDJSON upload:', applyErr); 9769 } 9770 } else { 9771 console.warn('NDJSON upload failed for "' + title + '"; keeping inline metadata array'); 9772 } 9773 } catch (e) { 9774 console.warn('NDJSON build/upload skipped for "' + title + '" due to error:', e); 9775 } 9776 } else {
9777 console.log('Skipping NDJSON generation for "' + title + '" - no images to process'); 9778 } 9779 } 9780 9781 // Preserve additional metadata 9782 if (classicItem.galleryOptions.siteAlias) { 9783 enhancedGalleryOptions.siteAlias = classicItem.galleryOptions.siteAlias; 9784 } 9785 if (classicItem.galleryOptions.initialPageUuid) { 9786 enhancedGalleryOptions.initialPageUuid = classicItem.galleryOptions.initialPageUuid; 9787 } 9788 if (classicItem.galleryOptions.initialPageAlias) { 9789 enhancedGalleryOptions.initialPageAlias = classicItem.galleryOptions.initialPageAlias; 9790 } 9791 if (classicItem.galleryOptions.timestamp) { 9792 enhancedGalleryOptions.timestamp = classicItem.galleryOptions.timestamp; 9793 } 9794 } else if (isImportFormat && (classicItem.images || []).length > 0) { 9795 // import_ format without galleryOptions: still create NDJSON for new Hydra galleries 9796 const processedImages = classicItem.images || []; 9797 const collectionKey = 'GUID=' + itemGuid; 9798 enhancedGalleryOptions.jsonCollections = { 9799 [collectionKey]: { 9800 isClassicCollection: true, 9801 guid: collectionKey, 9802 metadata: processedImages, 9803 totalItems: processedImages.length 9804 } 9805 }; 9806 console.log('Created jsonCollections for import_ gallery (no galleryOptions):', title, '(' + processedImages.length + ' images)'); 9807 try { 9808 const ndjsonContent = buildHydraClassicNdjson(itemGuid, processedImages); 9809 const uploadResult = await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent); 9810 if (uploadResult && uploadResult.success) { 9811 const hydraCollectionKey = 'GUID=' + currentSiteId; 9812 enhancedGalleryOptions.manualCollectionName = hydraCollectionKey; 9813 enhancedGalleryOptions.jsonCollections = { 9814 [hydraCollectionKey]: { 9815 isClassicCollection: false, 9816 guid: hydraCollectionKey, 9817 metadata: {}, 9818 totalItems: processedImages.length 9819 } 9820 }; 9821 enhancedGalleryOptions.siteAlias = window.Parameters?.SiteAlias || window.location?.hostname || currentSiteId; 9822 enhancedGalleryOptions.initialPageUuid = galleryPageId; 9823 enhancedGalleryOptions.isHydra = true; 9824 enhancedGalleryOptions.flyIoSaveTimestamp = Date.now(); 9825 if (uploadResult.irysResult?.txId) { 9826 enhancedGalleryOptions.txId = uploadResult.irysResult.txId; 9827 enhancedGalleryOptions.permaURL = uploadResult.irysResult.txId; 9828 enhancedGalleryOptions.isPerma = true; 9829 enhancedGalleryOptions.uploadedTo = uploadResult.uploadedTo || ['fly']; 9830 } 9831 console.log('NDJSON uploaded for import_ gallery "' + title + '" (no galleryOptions) - configured as Hydra NDJSON'); 9832 } 9833 } catch (e) { 9834 console.warn('NDJSON build/upload skipped for import_ gallery "' + title + '":', e); 9835 } 9836 } 9837 9838 // Note: For import from pasted JSON, we don't include image data to avoid overwriting existing captions 9839 // Image data is only included when importing from Classic GUID directly 9840 console.log(`Gallery ${title} imported with structure and settings preserved (image data not included to avoid overwriting existing captions)`); 9841 9842 newItem = { 9843 id: newItemId, title, url: `/${finalSlug}`, isIntegrated: true, isPage: false, isSubmenu: false, visible: classicItem.visible !== false, parentId: null, 9844 siteId: currentSiteId, pageId: galleryPageId, classicCategoryId: classicItem.categoryID, classicGuid: itemGuid, 9845 importSource: 'classic-gallery', slug: finalSlug, normalizedName: finalSlug, 9846 galleryOptions: enhancedGalleryOptions 9847 }; 9848 } 9849 9850 tempNewItems.push(newItem); 9851 } 9852 9853 // --- Pass 2: Link children to parents using the ID map ---
9854 tempNewItems.forEach(newItem => { 9855 const originalId = Object.keys(originalIdToNewIdMap).find(key => originalIdToNewIdMap[key] === newItem.id); 9856 const originalItem = galleriesForImport.find(g => String(g.id) === String(originalId)); 9857 9858 if (originalItem && originalItem.parentId) { 9859 const newParentId = originalIdToNewIdMap[originalItem.parentId]; 9860 if (newParentId) { 9861 newItem.parentId = newParentId; 9862 const parentItem = tempNewItems.find(p => p.id === newParentId); 9863 if (parentItem) { 9864 parentItem.isSubmenu = true; 9865 parentItem.isCollapsed = true; 9866 } 9867 } else { 9868 newItem.parentId = submenuParentId; 9869 } 9870 } else { 9871 newItem.parentId = submenuParentId; 9872 } 9873 }); 9874 9875 if (replaceMenu) { 9876 console.log("Replacing existing menu structure."); 9877 // If replacing, overwrite the entire galleries array. 9878 galleries = tempNewItems; 9879 } else { 9880 console.log("Appending to existing menu structure."); 9881 // If not replacing, push the new items to the existing array. 9882 galleries.push(...tempNewItems); 9883 } 9884 9885 9886 9887 if (progressBar) progressBar.style.width = '90%'; 9888 if (statusMessage) statusMessage.textContent = 'Saving changes...'; 9889 9890 // Pass import flag to skip localStorage for large import operations 9891 await saveGalleries({ isImport: true }); 9892 9893 if (progressBar) progressBar.style.width = '100%'; 9894 if (statusMessage) statusMessage.textContent = `Successfully imported ${tempNewItems.length} items!`; 9895 if (statusEl) statusEl.classList.remove('importing'); 9896 9897 setTimeout(() => { 9898 renderGalleries(); 9899 toggleImportClassicForm(); 9900 if (progressBar) progressBar.style.width = '0'; 9901 if (statusEl) statusEl.style.display = 'none'; 9902 if (confirm('Import complete! Would you like to reload the page to ensure everything is displayed correctly?')) { 9903 window.location.reload(); 9904 } 9905 }, 1000); 9906 9907 } catch (error) { 9908 console.error('Error importing classic galleries:', error); 9909 if (statusMessage) statusMessage.textContent = `Error: ${error.message}`; 9910 if (progressBar) { 9911 progressBar.style.width = '100%'; 9912 progressBar.style.backgroundColor = '#dc3545'; 9913 } 9914 if (statusEl) statusEl.classList.remove('importing'); 9915 } 9916} 9917 9918 9919 9920/** 9921 * Improved toggleImportClassicForm - Closes other forms first 9922 */ 9923function toggleImportClassicForm() { 9924 // First check if user is logged in and in edit mode 9925 if (!isAuthenticated || !isAdmin || !window.isInEditMode()) { 9926 alert('You must be logged in as an admin and in edit mode to import galleries.'); 9927 return; 9928 } 9929 9930 const formId = 'importClassicForm'; 9931 const form = document.getElementById(formId); 9932 9933 if (!form) { 9934 console.error('Import form element not found'); 9935 return; 9936 } 9937 9938 // If this form is already open, just close everything 9939 if (window.currentOpenForm === formId) { 9940 closeAllForms(); 9941 return; 9942 } 9943 9944 // Otherwise close all forms and open this one 9945 closeAllForms(); 9946 9947 // Show the form 9948 form.style.display = 'block'; 9949 9950 // Use setTimeout to allow the display change to take effect before adding the class 9951 setTimeout(() => { 9952 form.classList.add('visible'); 9953 }, 10); 9954 9955 // Populate parent options 9956 const parentSelect = document.getElementById('importParent'); 9957 if (parentSelect) { 9958 parentSelect.innerHTML = '<option value="">Top Level (No Parent)</option>'; 9959 9960 // Add all galleries that could be parents (including submenus) 9961 galleries.forEach(gallery => { 9962 // Only include items that are not already marked as imported 9963 if (!gallery.importSource) { 9964 parentSelect.innerHTML += `<option value="${gallery.id}">${gallery.title}</option>`; 9965 } 9966 }); 9967 } 9968 9969 // Set default values 9970 const submenuTitle = document.getElementById('submenuTitle'); 9971 if (submenuTitle && !submenuTitle.value) { 9972 submenuTitle.value = 'Classic Galleries'; 9973 } 9974 9975 // Reset any previous import status 9976 const statusEl = document.getElementById('importStatus'); 9977 if (statusEl) { 9978 statusEl.style.display = 'none'; 9979 const progressBar = statusEl.querySelector('.progress-bar'); 9980 if (progressBar) { 9981 progressBar.style.width = '0'; 9982 progressBar.style.backgroundColor = '#4682B4'; // Reset to blue 9983 } 9984 } 9985 9986 // Set this as the current open form 9987 window.currentOpenForm = formId; 9988} 9989 9990// Add event listener for the radio buttons to adjust height when page is selected 9991document.addEventListener('DOMContentLoaded', function() { 9992 const galleryTypeRadios = document.querySelectorAll('input[name="galleryType"]'); 9993 galleryTypeRadios.forEach(radio => { 9994 radio.addEventListener('change', function() { 9995 const addForm = document.getElementById('addForm'); 9996 if (addForm && addForm.classList.contains('visible')) { 9997 const isPageSelected = this.value === 'page'; 9998 9999 // Give more height for page type 10000 if (isPageSelected) { 10001 addForm.style.maxHeight = '600px'; 10002 } else { 10003 // For other types, measure content height 10004 const contentHeight = addForm.scrollHeight + 30; 10005 addForm.style.maxHeight = Math.max(contentHeight, 500) + 'px'; 10006 } 10007 } 10008 }); 10009 }); 10010}); 10011 10012 10013/** 10014 * Improved toggleStyleEditor - Closes other forms first 10015 */ 10016function toggleStyleEditor(display = false, event) { 10017 const formId = 'menuStyleEditor'; 10018 const overlayOpen = window.MenuStyleCustomizer && window.MenuStyleCustomizer._overlayOpen; 10019 10020 if (overlayOpen && window.MenuStyleCustomizer._overlayMinimized && display !== false) { 10021 if (typeof window.MenuStyleCustomizer._toggleOverlayMinimized === 'function') { 10022 window.MenuStyleCustomizer._toggleOverlayMinimized(false); 10023 } 10024 return; 10025 } 10026 10027 if (display === false || window.currentOpenForm === formId || overlayOpen) { 10028 closeAllForms(); 10029 return; 10030 } 10031 10032 closeAllForms(); 10033 10034 if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.toggleOverlay === 'function') { 10035 window.MenuStyleCustomizer.toggleOverlay(); 10036 } 10037} 10038 10039 10040/** 10041 * This function closes all forms when escape key is pressed 10042 */ 10043document.addEventListener('keydown', function(e) { 10044 if (e.key === 'Escape' && window.currentOpenForm) { 10045 if (closeTopmostStyleCustomizerChildOverlay()) { 10046 return; 10047 } 10048 if (window.MenuStyleCustomizer && 10049 window.MenuStyleCustomizer._overlayOpen && 10050 window.MenuStyleCustomizer._overlayMinimized) { 10051 return; 10052 } 10053 closeAllForms(); 10054 } 10055}); 10056 10057/** 10058 * This function closes all forms when clicking outside any form 10059 */ 10060document.addEventListener('click', function(e) { 10061 if (!window.currentOpenForm) return; 10062 10063 if (window.MenuStyleCustomizer && 10064 window.MenuStyleCustomizer._overlayOpen && 10065 window.MenuStyleCustomizer._overlayMinimized) { 10066 return; 10067 } 10068 10069 // Keep Style Customizer and its nested panels open during in-overlay interactions. 10070 if (e.target.closest( 10071 '.pe-style-customizer-overlay, .pe-custom-code-overlay, .pe-palette-explorer-overlay, .pe-font-explorer-overlay, .pe-style-history-overlay, .sc-style-history-overlay, .pe-unsaved-changes-overlay, #colorMixerPopup, #artPaletteBtn, #exploreFontsBtn, #customCodeBtn, #colorMixerDot' 10072 )) { 10073 return; 10074 } 10075 10076 if (e.target.closest('.image-uploader-overlay')) { 10077 return; 10078 } 10079 10080 const currentForm = document.getElementById(window.currentOpenForm); 10081 if (!currentForm) { 10082 if (e.target.closest( 10083 '.pe-style-customizer-overlay, .pe-custom-code-overlay, .pe-palette-explorer-overlay, .pe-font-explorer-overlay, .pe-style-history-overlay, .sc-style-history-overlay, .pe-unsaved-changes-overlay, #colorMixerPopup' 10084 )) { 10085 return; 10086 } 10087 closeAllForms(); 10088 return; 10089 } 10090 10091 const isInsideForm = currentForm.contains(e.target);
10092 const isFormToggleButton = e.target.closest('[onclick*="toggle"]') !== null; 10093 10094 if (isInsideForm || isFormToggleButton) { 10095 return; 10096 } 10097 10098 closeAllForms(); 10099}); 10100 10101// Override the original functions with our improved versions 10102window.toggleAddForm = toggleAddForm; 10103window.toggleStyleEditor = toggleStyleEditor; 10104window.toggleMetadataEditor = toggleMetadataEditor; 10105window.toggleSidebarElementForm = toggleSidebarElementForm; 10106window.toggleImportClassicForm = toggleImportClassicForm; 10107window.closeAllForms = closeAllForms; 10108window.closeTopmostStyleCustomizerChildOverlay = closeTopmostStyleCustomizerChildOverlay; 10109 10110 10111/** 10112 * Opens the edit form for a gallery item. 10113 * (UPDATED to include Meta Title and Meta Description fields, removed Parent Menu) 10114 * @param {number} id - The ID of the gallery item. 10115 * @param {Event} event - The click event. 10116 */ 10117function editGallery(id, event) { 10118 event.stopPropagation(); // Prevent event bubbling 10119 const gallery = findGalleryById(galleries, id); 10120 const li = getGalleryTreeListItem(id, event); 10121 10122 if (gallery && li) { 10123 const showUrlField = gallery.isPage || gallery.isIntegrated; 10124 const showExternalUrlField = gallery.isExternal; 10125 const showMenuParentOption = !gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && !gallery.isExternal && 10126 (isPageMenuItem(gallery) || gallery.isIntegrated === true); 10127 /** Sidebar meta title/description apply to gallery routes only; pages use the page metadata element. */ 10128 const showSidebarSeoFields = !gallery.isPage; 10129 const currentSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : ''); 10130 const currentExternalUrl = gallery.url || ''; 10131 10132 // Clear existing content and rebuild form to ensure event listeners are fresh if needed 10133 li.innerHTML = ''; // Clear previous form if any 10134 10135 const formDiv = document.createElement('div'); 10136 formDiv.className = 'edit-form'; 10137 formDiv.innerHTML = ` 10138 <div class="form-group"> 10139 <label>Title:</label> 10140 <input type="text" value="${gallery.title || ''}" class="edit-title"> 10141 </div> 10142 10143 ${showUrlField ? ` 10144 <div class="form-group"> 10145 <label>URL Slug:</label> 10146 <input type="text" value="${currentSlug}" class="edit-slug" placeholder="e.g., my-gallery"> 10147 <small>Path after domain name (e.g., yoursite.com<b>/my-gallery</b>). Use lowercase letters, numbers, and hyphens.</small> 10148 </div> 10149 ` : ''} 10150 10151 ${showExternalUrlField ? ` 10152 <div class="form-group"> 10153 <label>External URL:</label> 10154 <input type="text" value="${currentExternalUrl}" class="edit-external-url" placeholder="https://example.com"> 10155 <small>Full URL for the external link (e.g., https://example.com/page).</small> 10156 </div> 10157 ` : ''} 10158 10159 ${showSidebarSeoFields ? ` 10160 <div class="form-group"> 10161 <label>Meta Title (Optional):</label> 10162 <input type="text" value="${gallery.metaTitle || ''}" class="edit-meta-title" placeholder="Title for search results (if different from main title)"> 10163 <small>Overrides site title for this item in search results/browser tabs.</small> 10164 </div> 10165 10166 <div class="form-group"> 10167 <label>Meta Description (Optional):</label> 10168 <textarea class="edit-meta-description" placeholder="Description for search results (150-160 chars recommended)">${gallery.metaDescription || ''}</textarea> 10169 <small>Overrides site description for this item in search results.</small> 10170 </div> 10171 ` : ` 10172 <div class="form-group"> 10173 <p class="text-muted" style="margin: 0 0 8px 0; font-size: 12px; line-height: 1.4;"> 10174 Search title and description for this page are set in <strong>Page â metadata</strong> (the metadata block on the page), not here. 10175 </p> 10176 </div> 10177 `} 10178 10179 <div class="form-group"> 10180 <label style="display: flex; align-items: center; gap: 8px;"> 10181 <input type="checkbox" class="edit-hide-menu" ${gallery.hideMenuOnPage ? 'checked' : ''} style="width: auto; margin-bottom: 0;" onchange="onPageMenuChromeToggle(this)"> 10182 Hide menu when viewing this page 10183 </label> 10184 <small>Allows for a full-screen page or gallery layout. Menu remains visible in edit mode.</small> 10185 </div> 10186 10187 <div class="form-group"> 10188 <label style="display: flex; align-items: center; gap: 8px;"> 10189 <input type="checkbox" class="edit-overlay-menu" ${gallery.overlayMenuOnPage ? 'checked' : ''} style="width: auto; margin-bottom: 0;" onchange="onPageMenuChromeToggle(this)"> 10190 Show menu as overlay on this page 10191 </label> 10192 <small>Keeps menu items visible over a full-bleed page. Menu background is hidden. Solid menu remains in edit mode.</small> 10193 </div> 10194 10195 ${showMenuParentOption ? ` 10196 <div class="form-group"> 10197 <label style="display: flex; align-items: center; gap: 8px;"> 10198 <input type="checkbox" class="edit-acts-as-menu-parent" ${gallery.actsAsMenuParent === true ? 'checked' : ''} style="width: auto; margin-bottom: 0;"> 10199 Allow nested menu items 10200 </label> 10201 <small>Keeps this ${isPageMenuItem(gallery) ? 'page' : 'gallery'} clickable while allowing it to expand and contain sub-items.</small> 10202 </div> 10203 ` : ''} 10204 10205 <div class="form-group"> 10206 <label style="display: flex; align-items: center; gap: 8px;"> 10207 <input type="checkbox" class="edit-password-protected" ${gallery.passwordProtected ? 'checked' : ''} style="width: auto; margin-bottom: 0;"> 10208 Password protect this page 10209 </label> 10210 <small>Visitors will need to enter a password to access this page.</small> 10211 </div> 10212 10213 <div class="form-group password-field" style="display: ${gallery.passwordProtected ? 'block' : 'none'};"> 10214 <label>Password:</label> 10215 <input type="password" value="${gallery.password || ''}" class="edit-password" placeholder="Enter password"> 10216 <small>Password visitors must enter to access this page.</small> 10217 </div> 10218 10219 <div class="form-group"> 10220 <label style="display: flex; align-items: center; gap: 8px;"> 10221 <input type="checkbox" class="edit-no-index" ${(() => { 10222 // Explicit setting takes priority;
10222 otherwise default to checked for hidden non-home pages 10223 if (gallery.noIndex === true) return 'checked'; 10224 if (gallery.noIndex === false) return ''; 10225 return (gallery.visible === false && !gallery.isHomePage) ? 'checked' : ''; 10226 })()} style="width: auto; margin-bottom: 0;"> 10227 Hide from search engines (noindex) 10228 </label> 10229 <small>When checked, search engines will not index this page. Automatically enabled for hidden pages â uncheck to allow indexing even when hidden.</small> 10230 </div> 10231 10232 <div class="edit-form-actions"> 10233 <button class="btn btn-primary" id="saveEditBtn-${id}">Save</button> 10234 <button class="btn" id="cancelEditBtn-${id}">Cancel</button> 10235 </div> 10236 <br> 10237 `; 10238 li.appendChild(formDiv); // Append the new form 10239 10240 // Add event listeners programmatically 10241 const saveButton = formDiv.querySelector(`#saveEditBtn-${id}`); 10242 if (saveButton) { 10243 saveButton.addEventListener('click', (e) => saveEdit(id, e)); 10244 } 10245 10246 const cancelButton = formDiv.querySelector(`#cancelEditBtn-${id}`); 10247 if (cancelButton) { 10248 cancelButton.addEventListener('click', () => renderGalleries()); 10249 } 10250 10251 // Add password protection toggle functionality 10252 const passwordProtectedCheckbox = formDiv.querySelector('.edit-password-protected'); 10253 const passwordField = formDiv.querySelector('.password-field'); 10254 if (passwordProtectedCheckbox && passwordField) { 10255 passwordProtectedCheckbox.addEventListener('change', function() { 10256 passwordField.style.display = this.checked ? 'block' : 'none'; 10257 }); 10258 } 10259 10260 li.classList.add('edit-mode'); 10261 } else { 10262 console.error("Could not find gallery or list item for ID:", id); 10263 } 10264} 10265 10266/** 10267 * Resolve the correct sidebar #galleryTree li for an id. Horizontal dropdowns also use 10268 * li[data-id] (.horizontal-submenu-item); document.querySelector would return that node first 10269 * and break nested folder toggles in edit mode while horizontal layout is on. 10270 * @param {number} id - Gallery ID 10271 * @param {Event|null} event - Optional click event (preferred: closest li inside #galleryTree) 10272 * @returns {HTMLElement|null} 10273 */ 10274function getGalleryTreeListItem(id, event) { 10275 var numericId = typeof id === 'number' ? id : parseInt(String(id), 10); 10276 if (isNaN(numericId)) return null; 10277 var galleryTreeEl = document.getElementById('galleryTree'); 10278 if (event && event.target && galleryTreeEl) { 10279 var clicked = event.target.closest('li[data-id]'); 10280 if (clicked && galleryTreeEl.contains(clicked)) { 10281 var cid = parseInt(clicked.getAttribute('data-id') || '', 10); 10282 if (cid === numericId) return clicked; 10283 } 10284 } 10285 if (galleryTreeEl) { 10286 var scoped = galleryTreeEl.querySelector('li[data-id="' + numericId + '"]'); 10287 if (scoped) return scoped; 10288 } 10289 return document.querySelector('li[data-id="' + numericId + '"]'); 10290} 10291 10292/** 10293 * Handles click on a folder item to show/hide its contents 10294 * @param {number} id - The gallery ID 10295 * @param {Event} event - The click event 10296 */ 10297function toggleFolder(id, event) { 10298 // Ensure the event doesn't interfere with drag operations 10299 if (event) { 10300 event.stopPropagation(); 10301 } 10302 10303 10304 const gallery = galleries.find(g => g.id === id); 10305 if (!gallery) { 10306 return; 10307 } 10308 10309 if (gallery.isFolder !== true && gallery.isSubmenu !== true && !isNavigableMenuParent(gallery)) { 10310 return; 10311 } 10312 10313 10314 // Toggle the collapsed state 10315 gallery.isCollapsed = !gallery.isCollapsed; 10316 10317 // Find the list item in the DOM (#galleryTree first â not horizontal li.horizontal-submenu-item) 10318 const listItem = getGalleryTreeListItem(id, event); 10319 if (!listItem) { 10320 return; 10321 } 10322 10323 10324 // Toggle the expanded class FIRST - this is important for CSS to work correctly 10325 listItem.classList.toggle('expanded', !gallery.isCollapsed); 10326 10327 // Keep ARIA in sync for folder label + chevron (screen readers / keyboard) 10328 const folderExpanded = !gallery.isCollapsed; 10329 listItem.querySelectorAll('.menu-item.folder, .submenu-toggle').forEach((el) => { 10330 el.setAttribute('aria-expanded', folderExpanded ? 'true' : 'false'); 10331 }); 10332 10333 // Force a reflow after toggling the class to ensure CSS recalculates 10334 void listItem.offsetHeight; 10335 10336 // Find the nested list - SIMPLIFIED: Check if we're in sidebar first, then horizontal 10337 // CRITICAL: Check #galleryTree first - this is the definitive indicator of sidebar layout 10338 // Use contains() instead of closest() because closest() only checks ancestors 10339 const galleryTree = document.getElementById('galleryTree'); 10340 const isInGalleryTree = galleryTree && galleryTree.contains(listItem); 10341 const isInSidebar = listItem.closest('.sidebar') || listItem.closest('#sidebar') || isInGalleryTree; 10342 // Only consider it horizontal menu if it's explicitly in .horizontal-menu
10342-container AND not in galleryTree 10343 const isInHorizontalMenu = listItem.closest('.horizontal-menu-container') && !isInGalleryTree && !isInSidebar; 10344 10345 let nestedList = null; 10346 10347 // If we're in sidebar (#galleryTree), ONLY look for ol.nested-sortable - ignore horizontal structures 10348 // CRITICAL: Prioritize sidebar detection - if we're in galleryTree, we're definitely in sidebar 10349 if (isInGalleryTree) { 10350 nestedList = listItem.querySelector('ol.nested-sortable'); 10351 10352 // Also check direct children as fallback 10353 if (!nestedList) { 10354 const directChildren = Array.from(listItem.children); 10355 nestedList = directChildren.find(child => child.tagName === 'OL' && child.classList.contains('nested-sortable')); 10356 } 10357 } else if (isInSidebar) { 10358 // Fallback: if we're in .sidebar but not in #galleryTree, still treat as sidebar 10359 nestedList = listItem.querySelector('ol.nested-sortable'); 10360 if (!nestedList) { 10361 const directChildren = Array.from(listItem.children); 10362 nestedList = directChildren.find(child => child.tagName === 'OL' && child.classList.contains('nested-sortable')); 10363 } 10364 } 10365 10366 // For horizontal layout, check horizontal structures (ONLY if NOT in sidebar/galleryTree) 10367 if (!nestedList && isInHorizontalMenu && !isInGalleryTree && !isInSidebar) { 10368 nestedList = listItem.querySelector('.horizontal-submenu'); 10369 if (!nestedList) { 10370 nestedList = listItem.querySelector('.horizontal-dropdown'); 10371 } 10372 if (!nestedList) { 10373 // For horizontal nested dropdowns, they're appended to document.body, not the list item 10374 // They're linked via data-flyout-id attribute 10375 const flyoutId = listItem.getAttribute('data-flyout-id'); 10376 if (flyoutId) { 10377 // Look specifically for div.horizontal-nested-dropdown, not the list item itself 10378 const allElementsWithId = document.querySelectorAll('[data-flyout-id="' + flyoutId + '"]'); 10379 10380 // Find the actual dropdown div, not the list item 10381 const flyout = Array.from(allElementsWithId).find(el => 10382 el.classList.contains('horizontal-nested-dropdown') && el !== listItem 10383 ); 10384 10385 10386 if (flyout) { 10387 nestedList = flyout.querySelector('.horizontal-nested-list') || flyout; 10388 } 10389 } 10390 } 10391 if (!nestedList) { 10392 nestedList = listItem.querySelector('.horizontal-nested-dropdown'); 10393 } 10394 } 10395 10396 // Fallback: try all selectors if not found yet 10397 if (!nestedList) { 10398 nestedList = listItem.querySelector('ol.nested-sortable'); 10399 } 10400 if (!nestedList) { 10401 // Debug: Log the actual DOM structure to understand what's there 10402 10403 // Try to find any nested structure at all 10404 const anyNested = listItem.querySelector('ol, ul, div[class*="submenu"], div[class*="nested"], div[class*="dropdown"]'); 10405 if (anyNested) { 10406 nestedList = anyNested; // Use it anyway 10407 } 10408 10409 // Last resort: Check if nested list exists as a direct child but wasn't found by querySelector 10410 // This can happen if the element was removed/re-added or if there's a timing issue 10411 const directChildren = Array.from(listItem.children); 10412 const olChild = directChildren.find(child => child.tagName === 'OL'); 10413 if (olChild) { 10414 nestedList = olChild; 10415 } 10416 } 10417 10418 if (!nestedList) { 10419 10420 // If the nested list doesn't exist, we need to create it 10421 // This can happen if children aren't rendered until the folder is opened 10422 // First try flat array 10423 let gallery = galleries.find(g => g.id === id); 10424 10425 // If gallery not found or has no children in flat array, build tree structure to find it 10426 if (!gallery || !gallery.children || gallery.children.length === 0) { 10427 10428 // Build tree structure from flat array 10429 const buildTree = (items, parentId = null) => { 10430 return items 10431 .filter(item => item.parentId === parentId) 10432 .map(item => ({ 10433 ...item, 10434 children: buildTree(items, item.id) 10435 })); 10436 }; 10437 10438 const galleryTree = buildTree(galleries); 10439 10440 // Recursive function to find gallery in tree 10441 const findInTree = (items) => { 10442 for (const item of items) { 10443 if (item.id === id) { 10444 return item; 10445 } 10446 if (item.children && item.children.length > 0) { 10447 const found = findInTree(item.children); 10448 if (found) return found; 10449 } 10450 } 10451 return null; 10452 }; 10453 10454 const treeGallery = findInTree(galleryTree); 10455 if (treeGallery) { 10456 gallery = treeGallery; 10457 } 10458 } 10459 10460 if (gallery && gallery.children && gallery.children.length > 0) { 10461 10462 // Determine nesting level from list item 10463 const currentLevel = parseInt(listItem.getAttribute('data-nesting-level') || '0', 10); 10464 const nextLevel = currentLevel + 1; 10465 10466 // Create the nested list element 10467 nestedList = document.createElement('ol'); 10468 nestedList.id = 'gallery-children-' + id; 10469 nestedList.className = 'nested-sortable'; 10470 if (nextLevel > 0) { 10471 nestedList.classList.add('nested-level-' + nextLevel); 10472 } 10473 10474 // Render the children - ensure we're in the right context (not in a horizontal dropdown wrapper)
10475 gallery.children.forEach(child => { 10476 const childHtml = renderGalleryItem(child, nextLevel, 20); 10477 const tempDiv = document.createElement('div'); 10478 tempDiv.innerHTML = childHtml; 10479 const childLi = tempDiv.querySelector('li'); 10480 if (childLi) { 10481 nestedList.appendChild(childLi); 10482 } 10483 }); 10484 10485 // CRITICAL: Only check for horizontal dropdowns if we're actually in a horizontal menu context 10486 // If we're in #galleryTree (sidebar), skip all horizontal menu logic 10487 const galleryTree = document.getElementById('galleryTree'); 10488 const isInGalleryTree = galleryTree && galleryTree.contains(listItem); 10489 const isInSidebar = listItem.closest('.sidebar') || listItem.closest('#sidebar') || isInGalleryTree; 10490 10491 if (!isInGalleryTree && !isInSidebar) { 10492 const isInHorizontalMenu = listItem.closest('.horizontal-menu-container'); 10493 if (isInHorizontalMenu) { 10494 const horizontalDropdown = listItem.closest('.horizontal-dropdown, .horizontal-nested-dropdown'); 10495 if (horizontalDropdown && horizontalDropdown !== listItem) { 10496 horizontalDropdown.style.setProperty('display', 'block', 'important'); 10497 horizontalDropdown.style.setProperty('visibility', 'visible', 'important'); 10498 horizontalDropdown.style.setProperty('opacity', '1', 'important'); 10499 } 10500 } 10501 } 10502 10503 // CRITICAL: Ensure list item itself is visible and has correct display 10504 listItem.style.setProperty('display', 'list-item', 'important'); 10505 listItem.style.setProperty('visibility', 'visible', 'important'); 10506 10507 // CRITICAL: Ensure ALL parent elements up to the sidebar are visible 10508 // BUT: Don't make .horizontal-menu-container visible - that's the entire horizontal menu! 10509 let parent = listItem.parentElement; 10510 let depth = 0; 10511 while (parent && depth < 10) { 10512 // Skip .horizontal-menu-container - we don't want to show the entire horizontal menu 10513 if (parent.classList.contains('horizontal-menu-container')) { 10514 parent = parent.parentElement; 10515 depth++; 10516 continue; 10517 } 10518 10519 const parentStyles = window.getComputedStyle(parent); 10520 if (parentStyles.display === 'none' || parentStyles.visibility === 'hidden' || parentStyles.opacity === '0') { 10521 if (parentStyles.display === 'none') { 10522 parent.style.setProperty('display', 'block', 'important'); 10523 } 10524 if (parentStyles.visibility === 'hidden') { 10525 parent.style.setProperty('visibility', 'visible', 'important'); 10526 } 10527 if (parentStyles.opacity === '0') { 10528 parent.style.setProperty('opacity', '1', 'important'); 10529 } 10530 } 10531 // Stop at sidebar or galleryTree 10532 if (parent.classList.contains('sidebar') || parent.id === 'galleryTree' || parent.id === 'sidebar') { 10533 break; 10534 } 10535 parent = parent.parentElement; 10536 depth++; 10537 } 10538 10539 // Append directly to the list item (not to any wrapper) 10540 // Make sure we're not appending to a hidden parent 10541 listItem.appendChild(nestedList); 10542 10543 // Immediately set the nested list to be visible since we're opening it 10544 nestedList.style.setProperty('display', 'block', 'important'); 10545 nestedList.style.setProperty('visibility', 'visible', 'important'); 10546 nestedList.style.setProperty('opacity', '1', 'important'); 10547 nestedList.style.setProperty('height', 'auto', 'important'); 10548 nestedList.style.setProperty('max-height', 'none', 'important'); 10549 10550 // Force immediate reflow after appending 10551 void nestedList.offsetHeight; 10552 void listItem.offsetHeight; 10553 } 10554 } 10555 10556 if (nestedList) { 10557 if (!gallery.isCollapsed) { 10558 // Show the nested list - ensure parent has expanded class FIRST 10559 // CRITICAL: Add expanded class BEFORE any style operations 10560 listItem.classList.add('expanded'); 10561 10562 // Verify expanded class was added 10563 if (!listItem.classList.contains('expanded')) { 10564 listItem.className = listItem.className + ' expanded'; 10565 } 10566 10567 // CRITICAL: Ensure parent li is visible (it might be hidden by CSS) 10568 const parentVisibility = window.getComputedStyle(listItem).visibility; 10569 if (parentVisibility === 'hidden') { 10570 listItem.style.setProperty('visibility', 'visible', 'important'); 10571 } 10572 10573 // Force a reflow to ensure CSS recalculates after adding expanded class 10574 void listItem.offsetHeight; 10575 void nestedList.offsetHeight; 10576 10577 // Show the nested list with !important to override CSS 10578 // Set these BEFORE removing any existing inline styles to avoid flicker 10579 nestedList.style.setProperty('display', 'block', 'important'); 10580 nestedList.style.setProperty('visibility', 'visible', 'important'); 10581 nestedList.style.setProperty('opacity', '1', 'important'); 10582 nestedList.style.setProperty('height', 'auto', 'important'); 10583 nestedList.style.setProperty('max-height', 'none', 'important'); 10584 nestedList.style.setProperty('overflow', 'visible', 'important'); 10585 10586 // Also ensure the nested list is not clipped by parent overflow
10587 // BUT: Don't modify .horizontal-menu-container overflow - that's the entire horizontal menu! 10588 let checkParent = nestedList.parentElement; 10589 let checkDepth = 0; 10590 while (checkParent && checkDepth < 5) { 10591 // Skip .horizontal-menu-container - we don't want to modify the entire horizontal menu 10592 if (checkParent.classList.contains('horizontal-menu-container')) { 10593 checkParent = checkParent.parentElement; 10594 checkDepth++; 10595 continue; 10596 } 10597 10598 const parentOverflow = window.getComputedStyle(checkParent).overflow; 10599 if (parentOverflow === 'hidden') { 10600 checkParent.style.setProperty('overflow', 'visible', 'important'); 10601 } 10602 if (checkParent.classList.contains('sidebar') || checkParent.id === 'galleryTree') { 10603 break; 10604 } 10605 checkParent = checkParent.parentElement; 10606 checkDepth++; 10607 } 10608 10609 // Force another reflow after setting styles 10610 void nestedList.offsetHeight; 10611 10612 const finalRect = nestedList.getBoundingClientRect(); 10613 10614 // If the nested list has zero dimensions, it's not actually visible 10615 if (finalRect.width === 0 || finalRect.height === 0) { 10616 // Force all parent elements to be visible 10617 // BUT: Don't modify .horizontal-menu-container - that's the entire horizontal menu! 10618 let forceParent = nestedList.parentElement; 10619 while (forceParent && forceParent !== document.body) { 10620 // Skip .horizontal-menu-container - we don't want to show the entire horizontal menu 10621 if (forceParent.classList.contains('horizontal-menu-container')) { 10622 forceParent = forceParent.parentElement; 10623 continue; 10624 } 10625 10626 forceParent.style.setProperty('display', 'block', 'important'); 10627 forceParent.style.setProperty('visibility', 'visible', 'important'); 10628 forceParent.style.setProperty('opacity', '1', 'important'); 10629 forceParent.style.setProperty('overflow', 'visible', 'important'); 10630 if (forceParent.classList.contains('sidebar') || forceParent.id === 'galleryTree') { 10631 break; 10632 } 10633 forceParent = forceParent.parentElement; 10634 } 10635 void nestedList.offsetHeight; 10636 } 10637 10638 // Check for any parent elements that might be hiding the nested list and fix them 10639 let parent = nestedList.parentElement; 10640 let depth = 0; 10641 while (parent && depth < 5) { 10642 const parentStyles = window.getComputedStyle(parent); 10643 if (parentStyles.display === 'none' || parentStyles.visibility === 'hidden' || parentStyles.opacity === '0') { 10644 10645 // Fix hidden parent elements 10646 if (parentStyles.display === 'none') { 10647 parent.style.setProperty('display', 'block', 'important'); 10648 } 10649 if (parentStyles.visibility === 'hidden') { 10650 parent.style.setProperty('visibility', 'visible', 'important'); 10651 } 10652 if (parentStyles.opacity === '0') { 10653 parent.style.setProperty('opacity', '1', 'important'); 10654 } 10655 } 10656 parent = parent.parentElement; 10657 depth++; 10658 } 10659 10660 } else { 10661 // Hide the nested list 10662 nestedList.style.setProperty('display', 'none', 'important'); 10663 nestedList.style.setProperty('visibility', 'hidden', 'important'); 10664 nestedList.style.setProperty('opacity', '0', 'important'); 10665 } 10666 10667 // Force multiple reflows to ensure the browser applies all changes 10668 void listItem.offsetHeight; 10669 void nestedList.offsetHeight; 10670 void listItem.offsetWidth; 10671 void nestedList.offsetWidth; 10672 10673 // Force a more aggressive repaint by temporarily toggling a class 10674 // This is especially important for sidebar layout where CSS rules might be interfer
10674ing 10675 listItem.classList.add('force-reflow'); 10676 void listItem.offsetHeight; 10677 listItem.classList.remove('force-reflow'); 10678 10679 // Force reflow again after class manipulation 10680 void listItem.offsetHeight; 10681 void nestedList.offsetHeight; 10682 10683 // Use requestAnimationFrame with multiple frames to ensure visual update 10684 // This ensures the browser has time to recalculate layout and repaint 10685 requestAnimationFrame(() => { 10686 void nestedList.offsetHeight; 10687 // Second frame to ensure all CSS has been recalculated 10688 requestAnimationFrame(() => { 10689 void nestedList.offsetHeight; 10690 // Verify the final state 10691 const finalDisplay = window.getComputedStyle(nestedList).display; 10692 const finalVisibility = window.getComputedStyle(nestedList).visibility; 10693 10694 // If still not visible, force one more repaint cycle 10695 if (!gallery.isCollapsed && finalDisplay === 'none') { 10696 nestedList.style.setProperty('display', 'block', 'important'); 10697 nestedList.style.setProperty('visibility', 'visible', 'important'); 10698 void nestedList.offsetHeight; 10699 requestAnimationFrame(() => { 10700 void nestedList.offsetHeight; 10701 }); 10702 } 10703 }); 10704 }); 10705 } 10706 10707 // Find the submenu toggle icon and rotate it 10708 const toggleIcon = listItem.querySelector('.toggle-icon'); 10709 if (toggleIcon) { 10710 toggleIcon.classList.toggle('rotated', !gallery.isCollapsed); 10711 } 10712 10713 10714 // Folder open/closed is a local UI preference â do not POST the full site config. 10715 try { 10716 const folderStates = JSON.parse(localStorage.getItem('folder_states') || '{}'); 10717 folderStates[id] = !gallery.isCollapsed; 10718 localStorage.setItem('folder_states', JSON.stringify(folderStates)); 10719 } catch (e) { 10720 console.error('Error saving folder state to localStorage:', e); 10721 } 10722} 10723 10724window.toggleFolder = toggleFolder; 10725 10726/** 10727 * Toggle password protection for a gallery/page 10728 * @param {number} id - The gallery ID 10729 * @param {Event} event - The click event 10730 */ 10731function toggleGalleryPasswordProtection(id, event) { 10732 event.stopPropagation(); 10733 10734 const gallery = findGalleryById(galleries, id); 10735 if (!gallery) { 10736 console.error("Could not find gallery for password protection toggle, ID:", id); 10737 return; 10738 } 10739 10740 if (gallery.passwordProtected) { 10741 // Remove password protection 10742 if (confirm('Remove password protection from this page?')) { 10743 gallery.passwordProtected = false; 10744 delete gallery.password; 10745 saveGalleries(); 10746 renderGalleries(); 10747 } 10748 } else { 10749 // Add password protection 10750 const password = prompt('Enter a password for this page:'); 10751 if (password && password.trim()) { 10752 gallery.passwordProtected = true; 10753 gallery.password = password.trim(); 10754 saveGalleries(); 10755 renderGalleries(); 10756 } 10757 } 10758} 10759 10760window.toggleGalleryPasswordProtection = toggleGalleryPasswordProtection; 10761 10762function onPageMenuChromeToggle(checkbox) { 10763 if (!checkbox || !checkbox.checked) return; 10764 const root = checkbox.closest ? checkbox.closest('li') : null; 10765 if (!root) return; 10766 const siblingClass = checkbox.classList.contains('edit-hide-menu') ? '.edit-overlay-menu' : '.edit-hide-menu'; 10767 const sibling = root.querySelector(siblingClass); 10768 if (sibling && sibling !== checkbox) sibling.checked = false; 10769} 10770window.onPageMenuChromeToggle = onPageMenuChromeToggle; 10771 10772/** 10773 * Saves the edited gallery item details. 10774 * (UPDATED to handle Meta Title and Meta Description, removed Parent Menu logic) 10775 * @param {number} id - The ID of the gallery item. 10776 * @param {Event} event - The click event. 10777 */ 10778function saveEdit(id, event) { 10779 event.stopPropagation(); 10780 const li = getGalleryTreeListItem(id, event); 10781 if (!li) { 10782 console.error("Could not find list item for saveEdit, ID:", id); 10783 return; 10784 } 10785 10786 const titleInput = li.querySelector('.edit-title'); 10787 const slugInput = li.querySelector('.edit-slug'); 10788 const externalUrlInput = li.querySelector('.edit-external-url'); 10789 const metaTitleInput = li.querySelector('.edit-meta-title'); 10790 const metaDescriptionInput = li.querySelector('.edit-meta-description'); 10791 const hideMenuInput = li.querySelector('.edit-hide-menu'); 10792 const overlayMenuInput = li.querySelector('.edit-overlay-menu'); 10793 const actsAsMenuParentInput = li.querySelector('.edit-acts-as-menu-parent'); 10794 const passwordProtectedInput = li.querySelector('.edit-password-protected'); 10795 const passwordInput = li.querySelector('.edit-password'); 10796 const noIndexInput = li.querySelector('.edit-no-index'); 10797 10798 const title = titleInput ? titleInput.value.trim() : ''; 10799 const newSlugRaw = slugInput ? slugInput.value.trim() : null; 10800 const externalUrl = externalUrlInput ? externalUrlInput.value.trim() : ''; 10801 const metaTitle = metaTitleInput ? metaTitleInput.value.trim() : ''; 10802 const metaDescription = metaDescriptionInput ? metaDescriptionInput.value.trim() : ''; 10803 const hideMenu = hideMenuInput ? hideMenuInput.checked : false;
10804 const overlayMenu = overlayMenuInput ? overlayMenuInput.checked : false; 10805 const passwordProtected = passwordProtectedInput ? passwordProtectedInput.checked : false; 10806 const password = passwordInput ? passwordInput.value.trim() : ''; 10807 // noIndex is stored explicitly so it survives visibility changes without reverting 10808 const noIndex = noIndexInput ? noIndexInput.checked : undefined; 10809 10810 const gallery = findGalleryById(galleries, id); 10811 if (!gallery) { 10812 console.error("Could not find gallery object for saveEdit, ID:", id); 10813 renderGalleries(); 10814 return; 10815 } 10816 10817 if (!title && !gallery.isSpacer) { 10818 alert('Please enter a title'); 10819 return; 10820 } 10821 10822 // Validate password protection 10823 if (passwordProtected && !password) { 10824 alert('Please enter a password when password protection is enabled'); 10825 return; 10826 } 10827 10828 // Only mutate the opt-in property when the user explicitly changes this checkbox. 10829 // This preserves the serialized shape and behavior of every existing item. 10830 if (actsAsMenuParentInput) { 10831 const wasNavigableMenuParent = gallery.actsAsMenuParent === true; 10832 const wantsNavigableMenuParent = actsAsMenuParentInput.checked === true; 10833 if (wasNavigableMenuParent && !wantsNavigableMenuParent) { 10834 const hasNestedItems = galleries.some(item => item.parentId === gallery.id); 10835 if (hasNestedItems) { 10836 alert('Move all nested menu items out of this item before turning off âAllow nested menu itemsâ.'); 10837 return; 10838 } 10839 delete gallery.actsAsMenuParent; 10840 } else if (!wasNavigableMenuParent && wantsNavigableMenuParent) { 10841 gallery.actsAsMenuParent = true; 10842 } 10843 } 10844 10845 gallery.title = title; 10846 // Sidebar SEO fields are only shown for galleries; do not overwrite stored values when saving a page. 10847 if (!gallery.isPage) { 10848 gallery.metaTitle = metaTitle; 10849 gallery.metaDescription = metaDescription; 10850 } 10851 // Hide-menu wins if both are somehow checked; overlay is opt-in and omitted when off. 10852 if (hideMenu) { 10853 gallery.hideMenuOnPage = true; 10854 delete gallery.overlayMenuOnPage; 10855 } else { 10856 gallery.hideMenuOnPage = hideMenu; 10857 if (overlayMenu) { 10858 gallery.overlayMenuOnPage = true; 10859 } else { 10860 delete gallery.overlayMenuOnPage; 10861 } 10862 } 10863 // Persist the explicit noIndex choice so it survives future visibility toggles 10864 if (noIndex !== undefined) gallery.noIndex = noIndex; 10865 10866 // Save password protection settings 10867 gallery.passwordProtected = passwordProtected; 10868 if (passwordProtected) { 10869 gallery.password = password; 10870 } else { 10871 delete gallery.password; // Remove password if protection is disabled 10872 } 10873 10874 if ((gallery.isPage || gallery.isIntegrated) && newSlugRaw !== null) { 10875 const originalSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : ''); 10876 let sanitizedSlug = slugify(newSlugRaw); 10877 10878 if (!sanitizedSlug) { 10879 sanitizedSlug = slugify(gallery.title); 10880 } 10881 10882 if (sanitizedSlug !== originalSlug) { 10883 const otherGalleries = galleries.filter(g => g.id !== id); 10884 gallery.slug = ensureUniqueSlug(sanitizedSlug, otherGalleries); 10885 gallery.url = `/${gallery.slug}`; 10886 } else { 10887 if (gallery.url !== `/${gallery.slug}`) { 10888 gallery.url = `/${gallery.slug}`; 10889 } 10890 } 10891 } 10892 10893 // Update external URL if this is an external link 10894 if (gallery.isExternal && externalUrl) { 10895 gallery.url = externalUrl; 10896 } 10897 10898 saveGalleries(); 10899 renderGalleries(); 10900 10901 // If the currently active gallery was just edited, re-apply menu visibility 10902 if (gallery.id === activeGalleryId) { 10903 const bodyEl = document.body; 10904 if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 10905 bodyEl.classList.add('menu-hidden-on-page'); 10906 } else { 10907 bodyEl.classList.remove('menu-hidden-on-page'); 10908 } 10909 applyMenuOverlayClassForGallery(gallery); 10910 } 10911} 10912// Helper function to check if a gallery is an ancestor of another 10913function hasAncestor(galleryId, ancestorId) { 10914if (galleryId === ancestorId) return true; 10915 10916const gallery = galleries.find(g => g.id === galleryId); 10917if (!gallery || !gallery.parentId) return false; 10918 10919if (gallery.parentId === ancestorId) return true; 10920 10921return hasAncestor(gallery.parentId, ancestorId); 10922} 10923 10924function deleteGallery(id, event) { 10925 event.stopPropagation(); 10926 const gallery = galleries.find(g => g.id === id); 10927 const title = gallery && (gallery.title || gallery.slug || 'this item') || 'this item'; 10928 if (!confirm('Are you sure you want to delete "' + title + '"?')) { 10929 return; 10930 }
10931 const overlay = showDeletionOverlay(); 10932 overlay.updateMessage('Saving...'); 10933 galleries = galleries.filter(g => g.id !== id); 10934 if (typeof window !== 'undefined') { 10935 window.galleries = galleries; 10936 } 10937 if (activeGalleryId === id) { 10938 activeGalleryId = null; 10939 const frame = document.getElementById('galleryFrame'); 10940 if (frame) frame.src = ''; 10941 } 10942 saveGalleries() 10943 .then(function(success) { 10944 renderGalleries(); 10945 overlay.hide(); 10946 if (success === false) { 10947 alert('Failed to delete. Please try again.'); 10948 } 10949 }) 10950 .catch(function(err) { 10951 overlay.hide(); 10952 console.error('Error deleting item:', err); 10953 alert('Failed to delete. Please try again.'); 10954 }); 10955} 10956 10957function debugNestedStructure() { 10958console.group('Current Gallery Structure'); 10959 10960function printGallery(gallery, level = 0) { 10961const indent = ' '.repeat(level); 10962console.log(`${indent}${gallery.title} (ID: ${gallery.id}, Parent: ${gallery.parentId || 'none'})`); 10963 10964if (gallery.children && gallery.children.length > 0) { 10965 gallery.children.forEach(child => printGallery(child, level + 1)); 10966} 10967} 10968 // Create a tree structure for debugging 10969const galleryTree = createGalleryTree(galleries); 10970galleryTree.forEach(gallery => printGallery(gallery)); 10971 10972console.groupEnd(); 10973} 10974 10975function toggleHome(id, event) { 10976 event.stopPropagation(); 10977 10978 console.log('Setting home page to gallery:', id); 10979 10980 // Find gallery 10981 const gallery = galleries.find(g => g.id === id); 10982 if (!gallery) return; 10983 10984 // Check if this gallery is already set as home 10985 const alreadyHome = gallery.isHomePage === true; 10986 10987 // Set all galleries to not be the home page 10988 galleries.forEach(g => { 10989 g.isHomePage = false; 10990 }); 10991 10992 // Toggle this gallery's home status - if it was already home, we're unsetting it 10993 gallery.isHomePage = !alreadyHome; 10994 10995 // Save changes 10996 saveGalleries(); 10997 10998 // Update UI 10999 renderGalleries(); 11000 11001 // If this gallery is now the home page, display a message 11002 if (gallery.isHomePage) { 11003 const title = gallery.title || 'This page'; 11004 showSuccessMessage(`${title} is now set as the Home page`); 11005 } 11006} 11007 11008// Function to toggle gallery visibility 11009function toggleVisibility(id, event) { 11010 event.stopPropagation(); 11011 11012 const gallery = galleries.find(g => g.id === id); 11013 if (gallery) { 11014 // Toggle the visibility 11015 gallery.visible = gallery.visible === false ? true : false; 11016 11017 // Save the change to server 11018 saveGalleries(); 11019 11020 // Update UI - important to toggle the class on the button itself 11021 const toggle = event.currentTarget; 11022 if (toggle) { 11023 if (gallery.visible === false) { 11024 toggle.classList.add('hidden'); 11025 11026 // Update the SVG icon 11027 toggle.querySelector('svg').innerHTML = '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line>'; 11028 } else { 11029 toggle.classList.remove('hidden'); 11030 11031 // Update the SVG icon 11032 toggle.querySelector('svg').innerHTML = '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle>'; 11033 } 11034 } 11035 11036 // Update parent row class (#galleryTree row, not horizontal submenu li) 11037 const listItem = getGalleryTreeListItem(id, event); 11038 if (listItem) { 11039 if (gallery.visible === false) { 11040 listItem.classList.add('hidden-gallery'); 11041 } else { 11042 listItem.classList.remove('hidden-gallery'); 11043 } 11044 } 11045 11046 // Only clear iframe if not in edit mode 11047 if (id === activeGalleryId && gallery.visible === false && !isEditing) { 11048 document.getElementById('galleryFrame').src = ''; 11049 } 11050 11051 // Re-render menus based on current layout 11052 const currentLayout = document.body.classList.contains('menu-layout-horizontal') ? 'horizontal' : 11053 document.body.classList.contains('menu-layout-top') ? 'top' : 'sidebar'; 11054 11055 if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') { 11056 window.renderHorizontalMenu(); 11057 } 11058 11059 // Also re-render the sidebar gallery tree to ensure hidden-gallery class is properly applied 11060 // This is especially important when horizontal layout is active but we're viewing the sidebar in edit mode 11061 if (typeof window.renderGalleries === 'function') { 11062 window.renderGalleries(); 11063 } else if (typeof renderGalleries === 'function') { 11064 renderGalleries(); 11065 } 11066 } 11067} 11068 11069 11070 11071/** 11072* Simplified loadGallery function that follows the same content swap pattern 11073* @param {number} id - The gallery ID 11074* @param {Event} event - Optional event object 11075*/ 11076// Enhanced loadGallery function with script cleanup 11077async function loadGallery(id, event) { 11078 11079// Stop event propagation if provided; preventDefault keeps <a href> SPA navigation 11080if (event) { 11081 if (typeof event.preventDefault === 'function') event.preventDefault(); 11082 event.stopPropagation(); 11083} 11084 11085const galleryContainer = document.querySelector('.gallery-container'); 11086if (galleryContainer) { 11087 // Apply immediate hiding styles 11088 galleryContainer.style.opacity = '0'; 11089} 11090 11091console.log('Loading gallery with ID:', id); 11092
11093if (typeof dismissGalleryPasswordOverlay === 'function') { 11094 dismissGalleryPasswordOverlay(); 11095} 11096 11097// Find gallery by ID 11098const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible 11099 11100if (!gallery) { 11101 console.warn('No gallery found with ID:', id); 11102 if (galleryContainer) { 11103 galleryContainer.style.opacity = '1'; 11104 } 11105 return; 11106} 11107 11108// Check if this is a page, if so use loadPage instead 11109if (isPageMenuItem(gallery)) { 11110 // Use the loadPage function defined in this scope (or window.loadPage if you prefer) 11111 return loadPage(id, event); 11112} 11113 11114// Skip submenu items 11115if (gallery.isSubmenu) { 11116 console.log('Gallery is submenu, skipping URL update'); 11117 toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available 11118 if (galleryContainer) { 11119 galleryContainer.style.opacity = '1'; 11120 } // Show container 11121 return; 11122} 11123 11124console.log('Found gallery:', gallery.title); 11125 11126window._activeContentType = 'gallery'; 11127beginHydraContentLoad('gallery', gallery.id, gallery.pageId); 11128if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { 11129 window.PageManager.clearCurrentPage(); 11130} 11131 11132// CRITICAL: Set active gallery ID consistently in both global and window scope 11133console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')'); 11134window.activeGalleryId = id; 11135activeGalleryId = id; // Ensure both variables are synchronized 11136 11137// Save reference to which gallery we're loading (for later verification) 11138window.currentLoadingGalleryId = id; 11139 11140// CRITICAL: Thorough cleanup with await to ensure completion 11141if (typeof window.removeGalleryScriptsWithPause === 'function') { 11142 await window.removeGalleryScriptsWithPause(); 11143} 11144 11145 11146// Clear all existing content from gallery container 11147if (!galleryContainer) { 11148 console.error('Gallery container not found'); 11149 return; 11150} 11151 11152// Force clear all content 11153galleryContainer.innerHTML = ''; 11154 11155 11156 // CRITICAL: Double-check active gallery ID before updating UI 11157 if (window.activeGalleryId !== id || activeGalleryId !== id) { 11158 console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id); 11159 window.activeGalleryId = id; 11160 activeGalleryId = id; 11161 } 11162 11163// Apply menu visibility based on the gallery's setting and edit mode 11164const bodyEl = document.body; 11165if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 11166 bodyEl.classList.add('menu-hidden-on-page'); 11167} else { 11168 bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden 11169} 11170applyMenuOverlayClassForGallery(gallery); 11171 11172// Update UI state using explicit window function references or local fallbacks 11173if (typeof window.updateActiveStates === 'function') { 11174 window.updateActiveStates(); 11175} else if (typeof updateActiveStates === 'function') { 11176 updateActiveStates(); 11177} 11178 11179if (typeof window.updateMobileTitle === 'function') { 11180 window.updateMobileTitle(); 11181} else if (typeof updateMobileTitle === 'function') { 11182 updateMobileTitle(); 11183} 11184 11185if (typeof window.closeMobileMenu === 'function') { 11186 window.closeMobileMenu(); 11187} else if (typeof closeMobileMenu === 'function') { 11188 closeMobileMenu(); 11189} 11190 11191// Update URL with gallery slug 11192if (typeof window.updateURLWithGallerySlug === 'function') { 11193 window.updateURLWithGallerySlug(gallery); 11194} else if (typeof updateURLWithGallerySlug === 'function') { 11195 updateURLWithGallerySlug(gallery); 11196} 11197 11198 // Create gallery container and load content 11199const neonGalleryContainer = document.createElement('div'); 11200neonGalleryContainer.id = 'neon-gallery-container'; 11201neonGalleryContainer.className = 'gallery-direct-content'; 11202neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString()); 11203neonGalleryContainer.style.width = '100%'; 11204neonGalleryContainer.style.height = '100%'; 11205galleryContainer.appendChild(neonGalleryContainer); 11206 11207// Add a cache-busting parameter to ensure script is freshly loaded 11208const timestamp = Date.now(); 11209 11210// Set up gallery parameters 11211window.Parameters = { 11212 SiteAlias: window.location.hostname, 11213 InitialPageUuid: gallery.pageId, 11214 InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available 11215 isInEditor: window.isEditing || false, // Assuming window.isEditing is available 11216 siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available 11217 isHydra: true,
11218 galleryInstanceId: timestamp, 11219 // Added loadedGalleryId as per your original function 11220 loadedGalleryId: gallery.id 11221}; 11222 11223// Debug: Check what galleryOptions contains 11224console.error('ð INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL); 11225 11226// Set up gallery config 11227window.neonGalleryConfig = { 11228 useData: true, 11229 useCDN: true, 11230 version: 'live', 11231 manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod", 11232 layoutType: gallery.galleryOptions?.layoutType || "grid", 11233 // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence 11234 // This ensures that only properties from the current gallery are included, not from previous galleries 11235 ...(gallery.galleryOptions || {}), 11236 // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script 11237 // These fields are required for the gallery to determine if it should use Load Network racing 11238 loadTxId: gallery.galleryOptions?.loadTxId || null, 11239 loadPermaURL: gallery.galleryOptions?.loadPermaURL || null, 11240 // Cloudflare CDN NDJSON (unique id) â when present, gallery loads CDN-first (3s) then Fly 11241 cdnNdjsonId: gallery.galleryOptions?.cdnNdjsonId || null, 11242 cdnNdjsonUrl: gallery.galleryOptions?.cdnNdjsonUrl || null, 11243 cdnNdjsonStableUrl: gallery.galleryOptions?.cdnNdjsonStableUrl || gallery.galleryOptions?.embedPointerUrl || null, 11244 embedPointerUrl: gallery.galleryOptions?.embedPointerUrl || gallery.galleryOptions?.cdnNdjsonStableUrl || null, 11245 cdnSaveTimestamp: gallery.galleryOptions?.cdnSaveTimestamp || null, 11246 siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available 11247 // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread 11248 galleryId: gallery.id, 11249 galleryInstanceId: timestamp 11250}; 11251 11252console.error('ð INDEX.TS DEBUG - After spread, window.neonGalleryConfig.isPerma:', window.neonGalleryConfig.isPerma, 'permaURL:', window.neonGalleryConfig.permaURL, 'loadTxId:', window.neonGalleryConfig.loadTxId, 'loadPermaURL:', window.neonGalleryConfig.loadPermaURL, 'cdnNdjsonId:', window.neonGalleryConfig.cdnNdjsonId); 11253 11254// Create and add the gallery script with cache-busting 11255// Use Worker route (avoids CDN SSL issues) 11256const script = document.createElement('script'); 11257const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; 11258script.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; // Using versioned file 11259script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute 11260script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute 11261console.log('Loading gallery-main script from Worker:', script.src); 11262script.onerror = function() { 11263 console.error('â Gallery script failed, retrying via proxy:', script.src); 11264 const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 11265 script.src = proxyUrl; 11266 script.onerror = function() { 11267 console.error('â Proxy failed, trying storage:', proxyUrl); 11268 script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app'); 11269 }; 11270}; 11271 11272// Added script.onload from your original function 11273script.onload = function() { 11274 if (window.currentLoadingGalleryId !== gallery.id) { 11275 console.warn('Gallery ID mismatch! Expected:', gallery.id, 11276 'Current:', window.currentLoadingGalleryId); 11277 } 11278 console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`); 11279}; 11280 11281 11282if (galleryContainer) { 11283 setTimeout(() => { 11284 galleryContainer.style.opacity = '1'; 11285 }, 50); // Small delay to ensure content is ready 11286 } 11287 11288console.log(`Loading fresh gallery script for: ${gallery.title}`); 11289document.body.appendChild(script); 11290} 11291 11292 11293 11294function removeGalleryScriptsWithPause() { 11295 return new Promise(resolve => { 11296 console.log('Performing comprehensive gallery cleanup (v2 - refined selectors)'); 11297 11298 // 1. Capture and clear any gallery data in localStorage (remains the same) 11299 try { 11300 const localStorageKeys = Object.keys(localStorage); 11301 const galleryStorageKeys = localStorageKeys.filter(key => 11302 key.includes('gallery') || 11303 key.includes('neon') || 11304 key.includes('lightbox') 11305 ); 11306 galleryStorageKeys.forEach(key => { 11307 localStorage.removeItem(key); 11308 }); 11309 } catch (e) { 11310 console.warn('Error clearing localStorage:', e); 11311 } 11312 11313 // @ts-ignore 11314 window._galleryPageContext = null; // (remains the same) 11315 11316 // Keep HP gallery CSS. Page-element edit/view remounts skip HP reload; 11317 // stripping neon-gallery-hp-hydra.css zeros masonry-item / container margins. 11318 const galleryStyles = document.querySelectorAll('style[data-gallery], link[href*="neon-gallery"]');
11319 galleryStyles.forEach(style => { 11320 const href = (style && style.href) ? String(style.href) : ''; 11321 if (style && style.id === 'neon-gallery-css') return; 11322 if (href.indexOf('neon-gallery-hp-hydra') !== -1) return; 11323 if (style && style.parentNode) { 11324 style.parentNode.removeChild(style); 11325 } 11326 }); 11327 11328 // 4. Remove lightbox elements (more specific selectors now) 11329 // These are specific IDs and classes for lightbox components. 11330 const lightboxComponentSelectors = [ 11331 '#neon-lightbox', // Main lightbox container by ID 11332 '.neon-lightbox', // Alternative class for main container 11333 '.lightbox-container',// General container class if used 11334 // Add any other *specific* classes or IDs your lightbox system uses for its top-level elements 11335 ]; 11336 const lightboxElements = document.querySelectorAll(lightboxComponentSelectors.join(', ')); 11337 lightboxElements.forEach(lightboxEl => { 11338 if (lightboxEl && lightboxEl.parentNode && lightboxEl !== document.body && lightboxEl !== document.documentElement) { 11339 // console.log('Removing lightbox component:', lightboxEl.id || lightboxEl.className); 11340 lightboxEl.parentNode.removeChild(lightboxEl); 11341 } 11342 }); 11343 11344 // 5. Remove other gallery-specific DOM elements 11345 // REMOVED: '[class*="lightbox"]', '[id*="lightbox"]' from this list 11346 // The selector '[class*="neon-"]' is still broad but less likely to match body unless you add "neon-" class to body. 11347 const galleryElementSelectors = [ 11348 '.neon-gallery-wrapper', '.gallery-container .fullscreen-overlay', 11349 '.neon-gallery-modal', '.gallery-tooltip', '.neon-gallery-context-menu', 11350 '.neon-thumbnails', '.neon-image', '.neon-caption', 11351 '.neon-controls', '.neon-pagination', 11352 '[class*="neon-"]', // BE CAUTIOUS: If this matches body or critical layout elements, it can cause issues. 11353 '[data-gallery-id]', '[data-image-id]', '.image-grid', '.image-masonry' 11354 // Ensure none of these selectors inadvertently match document.body or critical layout containers 11355 ]; 11356 const galleryElements = document.querySelectorAll(galleryElementSelectors.join(', ')); 11357 11358 if (galleryElements.length > 0) { 11359 galleryElements.forEach(el => { 11360 // CRITICAL FIX: Add checks to ensure we don't remove body or html 11361 if (el && el.parentNode && el !== document.body && el !== document.documentElement) { 11362 // console.log('Removing gallery-specific DOM element:', el.id || el.className); 11363 el.parentNode.removeChild(el); 11364 } else if (el === document.body || el === document.documentElement) { 11365 console.warn(`[CRITICAL WARNING] Attempt to remove document.body or document.documentElement blocked by selector: ${el.id || el.className}. Review your selectors.`); 11366 } 11367 }); 11368 } 11369 11370 // 6. Find and remove ALL gallery scripts (remains the same) 11371 const scripts = document.querySelectorAll( 11372 'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' + 11373 'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]' 11374 ); 11375 scripts.forEach(script => { 11376 if (script && script.parentNode) { 11377 // console.log('Removing script:', script.src || script.getAttribute('data-gallery-id')); 11378 script.parentNode.removeChild(script); 11379 } 11380 }); 11381 11382 // 6b. Specifically remove gallery options scripts (but NOT CSS - CSS is needed across galleries) 11383 const optionsScripts = document.querySelectorAll( 11384 'script[src*="galleryOptions"], script[data-options-js]' 11385 ); 11386 optionsScripts.forEach(script => { 11387 if (script && script.parentNode) { 11388 console.log('Removing gallery options script:', script.src || script.getAttribute('data-options-js')); 11389 script.parentNode.removeChild(script); 11390 } 11391 }); 11392 11393 // NOTE: We intentionally do NOT remove gallery options CSS here 11394 // The CSS is needed across galleries and doesn't cause conflicts like scripts do 11395 11396 setTimeout(() => { 11397 const remainingScripts = document.querySelectorAll( 11398 'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' + 11399 'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]' 11400 ); 11401 if (remainingScripts.length > 0) { 11402 // console.warn(`[DEBUG] ${remainingScripts.length} gallery scripts still found after removal attempt.`); 11403 } 11404 }, 50); 11405 11406 // 7. Reset ALL known global state variables used by the gallery (remains the same) 11407 const resetGlobals = [ 11408 'neonGalleryInitComplete', 'neonGalleryInitInProgress', 11409 'neonGalleryLoaded', 'neonGalleryConfig', 'neonGalleryState', 11410 'neonGalleryCache', 'neonGalleryImages', 'neonGallerySettings', 11411 'neonLightbox', 'neonGalleryEventListeners', 'neonGalleryInstance', 11412 'currentGalleryId', 'galleryData', 'imageCache', 'thumbnailCache', 11413 ]; 11414
11415 resetGlobals.forEach(prop => { 11416 // @ts-ignore 11417 if (window[prop] !== undefined) { 11418 // @ts-ignore 11419 window[prop] = null; 11420 } 11421 }); 11422 // Keep the bootstrap wipe flag. Clearing it let a re-injected main script 11423 // replace PageManager embed config with the default "mod" collection. 11424 if (typeof window.resetNeonGalleryInitGates === 'function') { 11425 window.resetNeonGalleryInitGates('removeGalleryScriptsWithPause'); 11426 } 11427 11428 // 7b. Reset gallery options panel state 11429 if (window._editorButtonVisibilityInterval) { 11430 clearInterval(window._editorButtonVisibilityInterval); 11431 window._editorButtonVisibilityInterval = null; 11432 } 11433 const editorBtn = document.getElementById('editorButton'); 11434 if (editorBtn) { 11435 editorBtn.classList.add('gallery-options-force-hidden'); 11436 editorBtn.style.setProperty('display', 'none', 'important'); 11437 editorBtn.style.pointerEvents = ''; 11438 } 11439 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 11440 window.syncGalleryOptionsButtonVisibility(); 11441 } 11442 // Reset optionsVisible flag if it exists on window (for cross-module access) 11443 // @ts-ignore 11444 if (window.optionsVisible !== undefined) { 11445 // @ts-ignore 11446 window.optionsVisible = false; 11447 } 11448 11449 // Reset options-related globals 11450 const optionsGlobals = [ 11451 'generateOptionsUI', 'openOptionsOverlay', 'showOptionsPanel', 11452 'hideOptionsPanel', 'toggleOptionsOverlay', 'optionsVisible' 11453 ]; 11454 optionsGlobals.forEach(prop => { 11455 // @ts-ignore 11456 if (window[prop] !== undefined && typeof window[prop] !== 'function') { 11457 // Only reset non-function properties (like flags) 11458 // Functions should remain available 11459 // @ts-ignore 11460 if (prop === 'optionsVisible') { 11461 // @ts-ignore 11462 window[prop] = false; 11463 } 11464 } 11465 }); 11466 11467 // Reset EnhancedGallerySystem so a pending initPromise cannot hang forever 11468 // after scripts it was waiting on were just removed from the DOM. 11469 if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.resetInitializationState === 'function') { 11470 window.EnhancedGallerySystem.resetInitializationState(); 11471 } 11472 11473 // Reset PageManager page-embedded gallery tracking 11474 window.neonGalleryScriptLoaded = false; 11475 window.pageManagerGalleryInitBlocked = false; 11476 if (window.pageManagerGalleryContainers) { 11477 window.pageManagerGalleryContainers.clear(); 11478 } 11479 11480 // Remove orphaned lightbox keyboard listeners (survive script tag removal) 11481 if (typeof window.removeAllLightboxKeyboardListeners === 'function') { 11482 window.removeAllLightboxKeyboardListeners(); 11483 } else if (window.currentLightboxKeyboardListener) { 11484 document.removeEventListener('keydown', window.currentLightboxKeyboardListener); 11485 window.currentLightboxKeyboardListener = null; 11486 window.lightboxKeyboardListenerActive = false; 11487 } 11488 11489 // Short pause before resolving 11490 setTimeout(resolve, 150); 11491 }); 11492} 11493 11494 11495// Fixed clearAddForm function with proper variable names 11496function clearAddForm() { 11497// Clear the title input 11498const titleInput = document.getElementById('galleryTitle'); 11499if (titleInput) { 11500titleInput.value = ''; 11501} 11502 11503// Clear the URL input 11504const urlInput = document.getElementById('galleryUrl'); 11505if (urlInput) { 11506urlInput.value = ''; 11507} 11508 11509// Reset the parent selection 11510const parentSelect = document.getElementById('galleryParent'); 11511if (parentSelect) { 11512parentSelect.value = ''; 11513} 11514 11515// Reset radio buttons to Gallery (integrated) as default 11516const galleryRadio = document.querySelector('input[name="galleryType"][value="integrated"]'); 11517if (galleryRadio) { 11518galleryRadio.checked = true; 11519} 11520 11521// Hide URL input container when default is Gallery 11522const urlInputContainer = document.getElementById('urlInputContainer'); 11523if (urlInputContainer) { 11524urlInputContainer.style.display = 'none'; 11525} else { 11526console.log('URL input container not found'); 11527} 11528} 11529 11530/** 11531 * Ensures window.galleries is synchronized with the global galleries variable 11532 * Call this function after any modification to gallery settings 11533 * 11534 * @returns {boolean} True if synchronization was successful 11535 */ 11536/** 11537 * Enhanced synchronizeGalleries function that checks ALL galleries 11538 * not just imported ones, and ensures settings are preserved 11539 * @returns {boolean} Success indicator 11540 */ 11541function synchronizeGalleries() { 11542 console.log('Starting enhanced gallery synchronization...'); 11543 11544 if (typeof galleries !== 'undefined' && Array.isArray(galleries)) { 11545 // First check if window.galleries exists and is different from galleries 11546 const needsSync = !window.galleries || 11547 !Array.isArray(window.galleries) || 11548 window.galleries.length !== galleries.length; 11549 11550 // ENHANCED: Check ALL galleries for options preservation, not just imported ones 11551 if (!needsSync && window.galleries.length > 0) { 11552 // Find all galleries with galleryOptions 11553 const galleriesWithOptions = galleries.filter(g => 11554 g.galleryOptions && Object.keys(g.galleryOptions).length > 4 11555 ); 11556 11557 if (galleriesWithOptions.length > 0) { 11558 // Check ALL galleries with substantial options, not just a sample 11559 let syncNeeded = false;
11560 11561 // Use for loop so we can break early if we find a mismatch 11562 for (let i = 0; i < galleriesWithOptions.length; i++) { 11563 const gallery = galleriesWithOptions[i]; 11564 const windowGallery = window.galleries.find(g => g.id === gallery.id); 11565 11566 if (windowGallery) { 11567 // Get options depth 11568 const galleryOptionsDepth = gallery.galleryOptions ? 11569 Object.keys(gallery.galleryOptions).length : 0; 11570 11571 const windowOptionsDepth = windowGallery.galleryOptions ? 11572 Object.keys(windowGallery.galleryOptions).length : 0; 11573 11574 // If options depth differs significantly, we need to sync 11575 if (Math.abs(galleryOptionsDepth - windowOptionsDepth) > 2) { 11576 console.log(`Gallery sync needed: Options mismatch detected for ${gallery.title}`); 11577 console.log(`Options count: global=${galleryOptionsDepth}, window=${windowOptionsDepth}`); 11578 11579 syncNeeded = true; 11580 break; // No need to check more galleries 11581 } 11582 11583 // Additional check: Look for specific important gallery settings that should be preserved 11584 // Only do this check for galleries with substantial settings 11585 if (galleryOptionsDepth > 5 && windowOptionsDepth > 5) { 11586 // Critical keys that indicate gallery customization 11587 const criticalKeys = ['columns', 'spacing', 'layoutType', 'startInSingles', 11588 'autoplaySingles', 'autoplayRandomStart', 'lightboxBgColor']; 11589 11590 // Check if any critical keys are missing in window.galleries 11591 const missingKeys = criticalKeys.filter(key => 11592 gallery.galleryOptions[key] !== undefined && 11593 windowGallery.galleryOptions[key] === undefined 11594 ); 11595 11596 if (missingKeys.length > 0) { 11597 console.log(`Gallery sync needed: Critical settings missing in window.galleries for ${gallery.title}`); 11598 console.log(`Missing keys: ${missingKeys.join(', ')}`); 11599 11600 syncNeeded = true; 11601 break; // No need to check more galleries 11602 } 11603 } 11604 } 11605 } 11606 11607 // If we detected a need to sync, do it now 11608 if (syncNeeded) { 11609 // Ensure we preserve rich gallery settings and per-page custom HTML fields 11610 window.galleries = galleries.map(gallery => { 11611 const windowGallery = window.galleries.find(g => g.id === gallery.id); 11612 if (!windowGallery) return gallery; 11613 11614 let merged = { ...gallery }; 11615 11616 if (windowGallery.galleryOptions && gallery.galleryOptions) { 11617 const globalOptionsDepth = Object.keys(gallery.galleryOptions).length; 11618 const windowOptionsDepth = Object.keys(windowGallery.galleryOptions).length; 11619 if (windowOptionsDepth > globalOptionsDepth) { 11620 merged = { 11621 ...merged, 11622 galleryOptions: { 11623 ...windowGallery.galleryOptions, 11624 ...gallery.galleryOptions 11625 } 11626 }; 11627 } 11628 } 11629 11630 if (windowGallery.customPageHtmlR2Key || windowGallery.customPageHtmlEnabled) { 11631 merged = { 11632 ...merged, 11633 customPageHtmlEnabled: windowGallery.customPageHtmlEnabled, 11634 customPageHtmlR2Key: windowGallery.customPageHtmlR2Key, 11635 customPageHtmlFilename: windowGallery.customPageHtmlFilename 11636 }; 11637 } 11638 11639 return merged; 11640 }); 11641 11642 console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`); 11643 return true; 11644 } 11645 } 11646 } 11647 11648 // If we determined sync is needed (or as a precaution), update window.galleries 11649 if (needsSync || !window.galleries) { 11650 if (window.galleries && Array.isArray(window.galleries)) { 11651 galleries = galleries.map(function(gallery) { 11652 const windowGallery = window.galleries.find(function(g) { return g && g.id === gallery.id; }); 11653 if (!windowGallery) return gallery; 11654 if (windowGallery.customPageHtmlR2Key || windowGallery.customPageHtmlEnabled) { 11655 return { 11656 ...gallery, 11657 customPageHtmlEnabled: windowGallery.customPageHtmlEnabled, 11658 customPageHtmlR2Key: windowGallery.customPageHtmlR2Key, 11659 customPageHtmlFilename: windowGallery.customPageHtmlFilename 11660 }; 11661 } 11662 return gallery; 11663 }); 11664 } 11665 window.galleries = [...galleries]; // Create a shallow copy to ensure different reference 11666 console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`); 11667 } else { 11668 console.log('Gallery synchronization check: No sync needed'); 11669 } 11670 11671 // Verify that all galleries with rich settings have their full options preserved 11672 const richGalleries = galleries.filter(g => 11673 g.galleryOptions && Object.keys(g.galleryOptions).length > 5 11674 ); 11675 11676 console.log(`Status: Found ${richGalleries.length} galleries with rich settings`); 11677 11678 // Final verification - keep both arrays aligned without dropping page-level fields 11679 if (window.galleries !== galleries) { 11680 console.warn('References not identical after sync - merging page fields into global galleries'); 11681 if (window.galleries && Array.isArray(window.galleries)) { 11682 galleries = galleries.map(function(gallery) { 11683 const windowGallery = window.galleries.find(function(g) { return g && g.id === gallery.id; }); 11684 if (!windowGallery) return gallery; 11685 return { 11686 ...gallery, 11687 customPageHtmlEnabled: windowGallery.customPageHtmlEnabled ?? gallery.customPageHtmlEnabled, 11688 customPageHtmlR2Key: windowGallery.customPageHtmlR2Key ?? gallery.customPageHtmlR2Key, 11689 customPageHtmlFilename: windowGallery.customPageHtmlFilename ?? gallery.customPageHtmlFilename, 11690 hideMenuOnPage: windowGallery.hideMenuOnPage ?? gallery.hideMenuOnPage, 11691 overlayMenuOnPage: windowGallery.overlayMenuOnPage ?? gallery.overlayMenuOnPage 11692 }; 11693 }); 11694 } 11695 window.galleries = galleries; 11696 return true; 11697 } 11698 11699 return true; 11700 } else { 11701 console.warn('Cannot synchronize galleries: global galleries variable is undefined or not an array'); 11702 11703 // If we have window.galleries but no global galleries, try to reverse-sync 11704 if (window.galleries && Array.isArray(window.galleries) && window.galleries.length > 0) { 11705 console.log('Attempting reverse sync: updating global galleries from window.galleries');
11706 try { 11707 // This is risky and should only happen in unusual circumstances 11708 galleries = window.galleries; 11709 return true; 11710 } catch (e) { 11711 console.error('Could not reverse-sync galleries:', e); 11712 } 11713 } 11714 11715 return false; 11716 } 11717} 11718 11719 11720 11721/** 11722 * Loads NDJSON content from the original gallery 11723 * @param {string} siteId - The site ID 11724 * @param {string} pageId - The page ID of the original gallery 11725 * @returns {Promise<string|null>} - The NDJSON content or null if not found/error 11726 */ 11727async function loadGalleryNDJSON(siteId, pageId) { 11728 console.error('[NDJSON Duplication] loadGalleryNDJSON called with siteId:', siteId, 'pageId:', pageId); 11729 11730 if (!siteId || !pageId) { 11731 console.error('[NDJSON Duplication] ERROR: Cannot load NDJSON - missing siteId or pageId', { siteId: siteId, pageId: pageId }); 11732 return null; 11733 } 11734 11735 try { 11736 const ndjsonFilename = `${siteId}_${pageId}.ndjson`; 11737 const url = `https://fly.storage.tigris.dev/ns-bridge-pub/${ndjsonFilename}`; 11738 11739 console.error('[NDJSON Duplication] Attempting to load NDJSON from:', url); 11740 console.error('[NDJSON Duplication] Filename:', ndjsonFilename); 11741 11742 const response = await fetch(url); 11743 11744 console.error('[NDJSON Duplication] Fetch response status:', response.status, response.statusText); 11745 11746 if (!response.ok) { 11747 if (response.status === 404) { 11748 console.error('[NDJSON Duplication] ERROR: NDJSON file not found (404) - gallery may not have been published yet'); 11749 console.error('[NDJSON Duplication] Attempted URL:', url); 11750 return null; 11751 } 11752 const errorText = await response.text().catch(() => 'Could not read error text'); 11753 console.error('[NDJSON Duplication] ERROR: Failed to load NDJSON:', response.status, response.statusText, errorText); 11754 throw new Error(`Failed to load NDJSON: ${response.statusText}`); 11755 } 11756 11757 const ndjsonContent = await response.text(); 11758 console.error('[NDJSON Duplication] SUCCESS: Loaded NDJSON, length:', ndjsonContent.length, 'characters'); 11759 console.error('[NDJSON Duplication] First 200 chars:', ndjsonContent.substring(0, 200)); 11760 return ndjsonContent; 11761 } catch (error) { 11762 console.error('[NDJSON Duplication] ERROR: Exception loading NDJSON:', error); 11763 console.error('[NDJSON Duplication] Error message:', error.message); 11764 console.error('[NDJSON Duplication] Error stack:', error.stack); 11765 return null; 11766 } 11767} 11768 11769/** 11770 * Duplicates and publishes NDJSON for a gallery 11771 * @param {Object} originalGallery - The original gallery object 11772 * @param {Object} newGallery - The new gallery object with new pageId 11773 * @returns {Promise<boolean>} - Success indicator 11774 */ 11775async function duplicateGalleryNDJSON(originalGallery, newGallery) { 11776 console.error('[NDJSON Duplication] ===== STARTING duplicateGalleryNDJSON ====='); 11777 console.error('[NDJSON Duplication] Original gallery:', { 11778 id: originalGallery.id, 11779 title: originalGallery.title, 11780 pageId: originalGallery.pageId, 11781 classicGuid: originalGallery.classicGuid, 11782 hasGalleryOptions: !!originalGallery.galleryOptions, 11783 manualCollectionName: originalGallery.galleryOptions?.manualCollectionName, 11784 isClassicCollection: originalGallery.galleryOptions?.isClassicCollection 11785 }); 11786 console.error('[NDJSON Duplication] New gallery:', { 11787 id: newGallery.id, 11788 title: newGallery.title, 11789 pageId: newGallery.pageId 11790 }); 11791 11792 try { 11793 // Check if this is an NDJSON-based gallery (not classic collection) 11794 // IMPORTANT: A gallery can have a GUID-style manualCollectionName but still use NDJSON files 11795 // Only skip if it's explicitly marked as a classic collection OR has no pageId (which means no NDJSON) 11796 // Having a pageId means it uses NDJSON files, so we should duplicate them 11797 const isClassicCollection = originalGallery.galleryOptions?.isClassicCollection === true; 11798 11799 console.error('[NDJSON Duplication] Classic collection check:', { 11800 hasClassicGuid: !!originalGallery.classicGuid, 11801 manualCollectionName: originalGallery.galleryOptions?.manualCollectionName, 11802 startsWithGUID: originalGallery.galleryOptions?.manualCollectionName?.startsWith('G
11802UID='), 11803 isClassicCollectionFlag: originalGallery.galleryOptions?.isClassicCollection, 11804 hasPageId: !!originalGallery.pageId, 11805 isClassicCollection: isClassicCollection, 11806 willSkip: isClassicCollection && !originalGallery.pageId 11807 }); 11808 11809 // Only skip if explicitly marked as classic collection AND has no pageId 11810 // If it has a pageId, it uses NDJSON files and we should duplicate them 11811 if (isClassicCollection && !originalGallery.pageId) { 11812 console.error('[NDJSON Duplication] SKIP: Gallery is explicitly marked as classic collection with no pageId, skipping NDJSON duplication'); 11813 return true; // Not an error, just not applicable 11814 } 11815 11816 // Check if original gallery has a pageId 11817 if (!originalGallery.pageId) { 11818 console.error('[NDJSON Duplication] SKIP: Original gallery has no pageId, skipping NDJSON duplication'); 11819 console.error('[NDJSON Duplication] Original gallery keys:', Object.keys(originalGallery)); 11820 return true; // Not an error, just not applicable 11821 } 11822 11823 // Get siteId 11824 const siteId = window.siteId || originalGallery.siteId; 11825 console.error('[NDJSON Duplication] SiteId resolution:', { 11826 windowSiteId: window.siteId, 11827 gallerySiteId: originalGallery.siteId, 11828 resolvedSiteId: siteId 11829 }); 11830 11831 if (!siteId) { 11832 console.error('[NDJSON Duplication] ERROR: Cannot duplicate NDJSON - missing siteId'); 11833 console.error('[NDJSON Duplication] window.siteId:', window.siteId); 11834 console.error('[NDJSON Duplication] originalGallery.siteId:', originalGallery.siteId); 11835 return false; 11836 } 11837 11838 // Load original NDJSON 11839 console.error('[NDJSON Duplication] Loading original NDJSON for duplication...'); 11840 console.error('[NDJSON Duplication] Using siteId:', siteId, 'pageId:', originalGallery.pageId); 11841 const originalNdjson = await loadGalleryNDJSON(siteId, originalGallery.pageId); 11842 11843 if (!originalNdjson) { 11844 console.error('[NDJSON Duplication] ERROR: Could not load original NDJSON, gallery may not have been published yet'); 11845 console.error('[NDJSON Duplication] This means the original gallery\'s NDJSON file was not found'); 11846 return false; 11847 } 11848 11849 // Publish the duplicated NDJSON with new pageId using the same endpoint as edition publisher 11850 console.error('[NDJSON Duplication] Publishing duplicated NDJSON with new pageId:', newGallery.pageId); 11851 const dataFileName = `${siteId}_${newGallery.pageId}.ndjson`; 11852 console.error('[NDJSON Duplication] New filename:', dataFileName); 11853 console.error('[NDJSON Duplication] NDJSON content length:', originalNdjson.length); 11854 11855 try { 11856 // Hydra sites require Bearer auth (same token resolution as Edition Publisher) 11857 const publishHeaders = { 'Content-Type': 'application/json' }; 11858 let authToken = null; 11859 try { 11860 if (window.TokenManager) { 11861 if (typeof window.TokenManager.getHydraAuthToken === 'function') { 11862 try { authToken = window.TokenManager.getHydraAuthToken(); } catch (e) {} 11863 } 11864 if (!authToken && typeof window.TokenManager.getBestToken === 'function') { 11865 try { 11866 const tokenWithBearer = window.TokenManager.getBestToken(); 11867 authToken = tokenWithBearer && tokenWithBearer.startsWith('Bearer ') 11868 ? tokenWithBearer.substring(7) 11869 : tokenWithBearer; 11870 } catch (e) {} 11871 } 11872 } 11873 if (!authToken) authToken = localStorage.getItem('hydra_auth_token'); 11874 if (!authToken) authToken = window.authToken || window.didToken; 11875 if (authToken) { 11876 publishHeaders['Authorization'] = authToken.includes('.') || authToken.startsWith('hydra:') 11877 ? 'Bearer ' + authToken 11878 : (authToken.startsWith('Bearer ') ? authToken : 'Bearer hydra:' + authToken); 11879 console.error('[NDJSON Duplication] Auth token attached for publish'); 11880 } else { 11881 console.error('[NDJSON Duplication] WARNING: No auth token â publish will likely fail with 401'); 11882 } 11883 } catch (authErr) { 11884 console.error('[NDJSON Duplication] Auth token lookup failed:', authErr.message); 11885 } 11886 11887 // Call the publish endpoint (same as edition publisher) 11888 console.error('[NDJSON Duplication] Calling publish endpoint: https://hydra-press-v2.fly.dev/publish'); 11889 const response = await fetch('https://hydra-press-v2.fly.dev/publish', { 11890 method: 'POST', 11891 headers: publishHeaders, 11892 body: JSON.stringify({ 11893 ndjsonContent: originalNdjson, 11894 dataFileName: dataFileName 11895 }), 11896 }); 11897 11898 console.error('[NDJSON Duplication] Publish response status:', response.status, response.statusText); 11899 console.error('[NDJSON Duplication] Response headers:', Object.fromEntries(response.headers.entries())); 11900 11901 if (!response.ok) { 11902 const errorText = await response.text().catch(() => 'Unknown error'); 11903 console.error('[NDJSON Duplication] ERROR: Failed to publish duplicated NDJSON:', response.status, errorText); 11904 console.error('[NDJSON Duplication] Response status:', response.status); 11905 console.error('[NDJSON Duplication] Response statusText:', response.statusText); 11906 return false;
11907 } 11908 11909 const result = await response.json(); 11910 console.error('[NDJSON Duplication] SUCCESS: Published duplicated NDJSON'); 11911 console.error('[NDJSON Duplication] Publish result:', JSON.stringify(result, null, 2)); 11912 console.error('[NDJSON Duplication] Publish result URLs:', result.urls); 11913 11914 // Note: The publish endpoint doesn't return Irys txId, so we don't update galleryOptions 11915 // with txId/permaURL here. If Irys upload is needed, it would need to be done separately. 11916 11917 console.error('[NDJSON Duplication] ===== duplicateGalleryNDJSON COMPLETED SUCCESSFULLY ====='); 11918 return true; 11919 } catch (error) { 11920 console.error('[NDJSON Duplication] ERROR: Exception publishing duplicated NDJSON:', error); 11921 console.error('[NDJSON Duplication] Error message:', error.message); 11922 console.error('[NDJSON Duplication] Error stack:', error.stack); 11923 return false; 11924 } 11925 } catch (error) { 11926 console.error('[NDJSON Duplication] ERROR: Unexpected error in duplicateGalleryNDJSON:', error); 11927 console.error('[NDJSON Duplication] Error message:', error.message); 11928 console.error('[NDJSON Duplication] Error stack:', error.stack); 11929 return false; 11930 } 11931} 11932 11933/** 11934 * Creates and shows a loading overlay for gallery deletion (same pattern as duplication). 11935 * @returns {Object} Object with hide() and updateMessage() to control the overlay 11936 */ 11937function showDeletionOverlay() { 11938 const existingOverlay = document.getElementById('deletion-overlay'); 11939 if (existingOverlay) existingOverlay.remove(); 11940 if (!document.querySelector('#deletion-overlay-styles')) { 11941 const style = document.createElement('style'); 11942 style.id = 'deletion-overlay-styles'; 11943 style.textContent = ` 11944 #deletion-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 99999999; display: flex; align-items: center; justify-content: center; cursor: not-allowed; pointer-events: all; } 11945 #deletion-overlay .deletion-message { background: rgba(255,255,255,0.95); padding: 30px 50px; font-family: Arial,sans-serif; font-size: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; flex-direction: column; align-items: center; gap: 20px; } 11946 #deletion-overlay .deletion-spinner { width: 80px; height: 80px; color: #444; } 11947 `; 11948 document.head.appendChild(style); 11949 } 11950 const overlayEl = document.createElement('div'); 11951 overlayEl.id = 'deletion-overlay'; 11952 const messageEl = document.createElement('div'); 11953 messageEl.className = 'deletion-message'; 11954 const spinnerEl = document.createElement('div'); 11955 spinnerEl.className = 'deletion-spinner'; 11956 spinnerEl.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24"><defs><filter id="svgSpinnersDeletion"><feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/><feColorMatrix in="y" result="z" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7"/><feBlend in="SourceGraphic" in2="z"/></filter></defs><g filter="url(#svgSpinnersDeletion)"><circle cx="5" cy="12" r="4" fill="currentColor"><animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/></circle><circle cx="19" cy="12" r="4" fill="currentColor"><animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/></circle><animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></g></svg>`; 11957 const textEl = document.createElement('div'); 11958 textEl.textContent = 'Deleting...'; 11959 textEl.style.textAlign = 'center'; 11960 messageEl.appendChild(spinnerEl); 11961 messageEl.appendChild(textEl); 11962 overlayEl.appendChild(messageEl); 11963 document.body.appendChild(overlayEl); 11964 return { 11965 hide: function() { if (overlayEl && overlayEl.parentNode) overlayEl.parentNode.removeChild(overlayEl); }, 11966 updateMessage: function(msg) { textEl.textContent = msg; } 11967 }; 11968} 11969 11970/** 11971 * Creates and shows a loading overlay for gallery duplication 11972 * @returns {Object} Object with hide function to remove the overlay 11973 */ 11974function showDuplicationOverlay() { 11975 // Remove any existing overlay first 11976 const existingOverlay = document.getElementById('duplication-overlay'); 11977 if (existingOverlay) { 11978 existingOverlay.remove(); 11979 } 11980 11981 // Add styles if not already present 11982 if (!document.querySelector('#duplication-overlay-styles')) { 11983 const style = document.createElement('style'); 11984 style.id = 'duplication-overlay-styles'; 11985 style.textContent = ` 11986 #duplication-overlay { 11987 position: fixed; 11988 top: 0; 11989 left: 0; 11990 right: 0; 11991 bottom: 0; 11992 background: rgba(0, 0, 0, 0.5); 11993 z-index: 99999999; 11994 display: flex; 11995 align-items: center; 11996 justify-content: center;
11997 cursor: not-allowed; 11998 pointer-events: all; 11999 } 12000 #duplication-overlay .duplication-message { 12001 background: rgba(255, 255, 255, 0.95); 12002 padding: 30px 50px; 12003 font-family: Arial, sans-serif; 12004 font-size: 16px; 12005 box-shadow: 0 4px 12px rgba(0,0,0,0.15); 12006 display: flex; 12007 flex-direction: column; 12008 align-items: center; 12009 gap: 20px; 12010 } 12011 #duplication-overlay .duplication-spinner { 12012 width: 80px; 12013 height: 80px; 12014 color: #444444; 12015 } 12016 `; 12017 document.head.appendChild(style); 12018 } 12019 12020 const overlay = document.createElement('div'); 12021 overlay.id = 'duplication-overlay'; 12022 12023 const message = document.createElement('div'); 12024 message.className = 'duplication-message'; 12025 12026 const spinner = document.createElement('div'); 12027 spinner.className = 'duplication-spinner'; 12028 spinner.innerHTML = ` 12029 <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24"> 12030 <defs> 12031 <filter id="svgSpinnersGooeyBalls20"> 12032 <feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/> 12033 <feColorMatrix in="y" result="z" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7"/> 12034 <feBlend in="SourceGraphic" in2="z"/> 12035 </filter> 12036 </defs> 12037 <g filter="url(#svgSpinnersGooeyBalls20)"> 12038 <circle cx="5" cy="12" r="4" fill="currentColor"> 12039 <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/> 12040 </circle> 12041 <circle cx="19" cy="12" r="4" fill="currentColor"> 12042 <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/> 12043 </circle> 12044 <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/> 12045 </g> 12046 </svg> 12047 `; 12048 12049 const text = document.createElement('div'); 12050 text.textContent = 'Duplicating gallery...'; 12051 text.style.textAlign = 'center'; 12052 12053 message.appendChild(spinner); 12054 message.appendChild(text); 12055 overlay.appendChild(message); 12056 document.body.appendChild(overlay); 12057 12058 return { 12059 hide: function() { 12060 if (overlay && overlay.parentNode) { 12061 overlay.parentNode.removeChild(overlay); 12062 } 12063 }, 12064 updateMessage: function(newMessage) { 12065 text.textContent = newMessage; 12066 } 12067 }; 12068} 12069 12070/** 12071 * Duplicates a gallery or page item. 12072 * @param {number} id - The ID of the item to duplicate. 12073 * @param {Event} event - The click event. 12074 */ 12075async function duplicateGalleryItem(id, event) { 12076 event.stopPropagation(); 12077 console.log(`Duplicating item with ID: ${id}`); 12078 12079 // Show loading overlay 12080 const overlay = showDuplicationOverlay(); 12081 12082 // Find the original item in the global galleries array 12083 const originalItem = findGalleryById(galleries, id); // Use helper if available, otherwise simple find 12084 12085 if (!originalItem) { 12086 console.error(`Cannot duplicate: Item with ID ${id} not found.`); 12087 overlay.hide(); 12088 alert('Error: Could not find the item to duplicate.'); 12089 return; 12090 } 12091 12092 if (isPageMenuItem(originalItem)) { 12093 overlay.updateMessage('Duplicating page...'); 12094 } 12095 12096 try { 12097 // --- Create a Deep Copy --- 12098 // Using JSON.parse/stringify is a common way for simple object structures 12099 // Be cautious if your objects have Dates, functions, Maps, Sets, etc. 12100 const newItem = JSON.parse(JSON.stringify(originalItem)); 12101 12102 // --- Modify Copied Item --- 12103 newItem.id = Date.now() + Math.floor(Math.random() * 1000); // Generate a new unique ID 12104 newItem.title = `${originalItem.title}-copy`; // Append "-copy" to title 12105 12106 // Generate new pageId if it's a page or integrated gallery 12107 if (newItem.isPage || newItem.isIntegrated) { 12108 newItem.pageId = generatePageId(); // Assign a new unique pageId 12109 } 12110 12111 // Generate new slug and URL 12112 const newBaseSlug = slugify(newItem.title); // Use the new title 12113 newItem.slug = ensureUniqueSlug(newBaseSlug, galleries); // Ensure uniqueness 12114 newItem.url = `/${newItem.slug}`; // Update URL based on new slug 12115 12116 // Reset children (don't duplicate children for simplicity) 12117 newItem.children = []; 12118 12119 // Deep clone galleryOptions and pageElements, update pageId if needed 12120 if (newItem.galleryOptions) { 12121 newItem.galleryOptions = JSON.parse(JSON.stringify(newItem.galleryOptions)); 12122 // Update pageId within options if it exists and matches the old one 12123 if (newItem.galleryOptions.pageId && newItem.galleryOptions.pageId === originalItem.pageId) { 12124 newItem.galleryOptions.pageId = newItem.pageId; 12125 } 12126 // Clear permanent-storage pointers so duplicate loads its own Tigris NDJSON, not the original 12127 ['txId', 'permaURL', 'loadTxId', 'loadPermaURL', 'loadNetworkUrl', 'r2StorageUrl', 12128 'cdnNdjsonId', 'cdnNdjsonUrl', 'cdnNdjsonStableUrl', 'embedPointerUrl', 'cdnSaveTimestamp', 12129 'flyIoSaveTimestamp', 'loadNetworkSaveTimestamp', 'irysSaveTimestamp', 'timestampsMatch', 'isPerma'] 12130 .forEach(function(field) { delete newItem.galleryOptions[field]; }); 12131 } 12132 if (newItem.pageElements) { 12133 newItem.pageElements = JSON.parse(JSON.stringify(newItem.pageElements)); 12134 // Give new IDs to duplicated page elements
12135 newItem.pageElements.forEach(el => { 12136 el.id = Date.now() + Math.floor(Math.random() * 10000); 12137 }); 12138 } 12139 12140 // Reset home page status 12141 newItem.isHomePage = false; 12142 12143 // --- Duplicate NDJSON if applicable (galleries only; pages copy content via pageElements) --- 12144 const isDuplicatingPage = isPageMenuItem(originalItem); 12145 console.error('[Gallery Duplication] Checking if NDJSON duplication is needed...', { 12146 isDuplicatingPage, 12147 newItemPageId: newItem.pageId, 12148 originalItemPageId: originalItem.pageId 12149 }); 12150 12151 if (!isDuplicatingPage && newItem.pageId && originalItem.pageId) { 12152 overlay.updateMessage('Duplicating gallery files...'); 12153 console.error('[Gallery Duplication] Both galleries have pageIds, attempting to duplicate NDJSON file...'); 12154 const ndjsonDuplicated = await duplicateGalleryNDJSON(originalItem, newItem); 12155 if (!ndjsonDuplicated) { 12156 console.error('[Gallery Duplication] WARNING: NDJSON duplication failed, but continuing with gallery duplication'); 12157 console.error('[Gallery Duplication] User can republish the gallery later if needed'); 12158 alert('Gallery menu item was created, but gallery images could not be copied. Open Edition Publisher on this gallery and publish to load images.'); 12159 // Don't block the duplication if NDJSON fails - user can republish later 12160 } else { 12161 console.error('[Gallery Duplication] SUCCESS: NDJSON duplication completed successfully'); 12162 } 12163 } else { 12164 console.error('[Gallery Duplication] SKIP: Skipping NDJSON duplication:', { 12165 isDuplicatingPage, 12166 hasNewPageId: !!newItem.pageId, 12167 hasOriginalPageId: !!originalItem.pageId, 12168 reason: isDuplicatingPage ? 'Page items use pageElements, not NDJSON' : 12169 !newItem.pageId ? 'New gallery has no pageId' : 'Original gallery has no pageId' 12170 }); 12171 } 12172 12173 overlay.updateMessage(isDuplicatingPage ? 'Saving duplicated page...' : 'Saving duplicated gallery...'); 12174 12175 // --- Insert into Galleries Array --- 12176 // Find the index of the original item 12177 const originalIndex = galleries.findIndex(item => item.id === id); 12178 12179 if (originalIndex > -1) { 12180 // Insert the new item right after the original 12181 galleries.splice(originalIndex + 1, 0, newItem); 12182 console.log(`Inserted duplicate "${newItem.title}" after "${originalItem.title}"`); 12183 12184 // Update positions for all items after the insertion point 12185 for (let i = originalIndex + 1; i < galleries.length; i++) { 12186 galleries[i].position = i; // Re-assign position based on new index 12187 } 12188 12189 } else { 12190 // Fallback: Add to the end if original couldn't be found (shouldn't happen) 12191 console.warn(`Original item ${id} not found in array, adding duplicate to the end.`); 12192 newItem.position = galleries.length; 12193 galleries.push(newItem); 12194 } 12195 12196 console.log('Rendering galleries immediately after duplication...'); 12197 renderGalleries(); 12198 // Update active states if necessary (though unlikely needed here) 12199 updateActiveStates(); 12200 12201 // --- Save and Refresh --- 12202 console.log('Saving duplicated item...'); 12203 saveGalleries(null, { addedNewItem: true }) // Pass flag to trigger refresh 12204 .then(success => { 12205 overlay.hide(); // Hide overlay before page refresh 12206 if (success) { 12207 console.log('Duplicate saved successfully. Page will refresh.'); 12208 // No need to call renderGalleries() here because saveGalleries will trigger a reload. 12209 } else { 12210 console.error('Failed to save the duplicated item.'); 12211 alert('Error: Could not save the duplicated item.'); 12212 // Optionally try to revert the galleries array change here if save fails 12213 } 12214 }) 12215 .catch(error => { 12216 overlay.hide(); // Hide overlay on error 12217 console.error('Error saving duplicated item:', error); 12218 alert('Error saving duplicated item.'); 12219 }); 12220 12221 } catch (error) { 12222 overlay.hide(); // Hide overlay on error 12223 console.error('Error duplicating item:', error); 12224 alert('An error occurred while duplicating the item.'); 12225 } 12226} 12227 12228/** 12229 * saveGalleries function that always saves FULL data 12230 * Ensures gallery settings are preserved in both localStorage and server 12231 * 12232 * @param {Object} customData - Optional custom data to save instead of galleries 12233 * @returns {Promise<boolean>} - Success indicator 12234 */ 12235 12236function attachLiveSidebarElements(data) { 12237 if (!data || typeof data !== 'object') return data; 12238 var hasGetter = !!(window.SidebarManager && typeof window.SidebarManager.getElementsForSave === 'function'); 12239 if (hasGetter) { 12240 var live = window.SidebarManager.getElementsForSave(); 12241 if (Array.isArray(live) && live.length > 0) { 12242 data.sidebarElements = live; 12243 return data; 12244 } 12245 delete data.sidebarElements; 12246 return data; 12247 } 12248 if (Array.isArray(data.sidebarElements) && data.sidebarElements.length === 0) { 12249 delete data.sidebarElements; 12250 } 12251 return data; 12252} 12253 12254async function saveGalleries(customData) { 12255 if (!isAuthenticated && localStorage.getItem('hydra_is_admin') !== 'true') { 12256 alert('You must be logged in as an admin to save changes'); 12257 return false;
12258 } 12259 12260 if (window.HelpSystem && typeof window.HelpSystem.isSandboxActive === 'function' && window.HelpSystem.isSandboxActive()) { 12261 console.warn('Save blocked: help sandbox is active'); 12262 return false; 12263 } 12264 12265 function _hasHelpDemoInTree(items) { 12266 if (!Array.isArray(items)) return false; 12267 for (var i = 0; i < items.length; i++) { 12268 var g = items[i]; 12269 if (!g) continue; 12270 if (g.isHelpDemo || Number(g.id) < 0) return true; 12271 if (g.children && _hasHelpDemoInTree(g.children)) return true; 12272 } 12273 return false; 12274 } 12275 12276 if (window.activeGalleryId != null && Number(window.activeGalleryId) < 0) { 12277 console.warn('Save blocked: active help demo gallery'); 12278 return false; 12279 } 12280 12281 var savePayload = customData && customData.galleries ? customData.galleries : galleries; 12282 if (_hasHelpDemoInTree(savePayload)) { 12283 console.warn('Save blocked: payload contains help demo records'); 12284 return false; 12285 } 12286 12287 // Prevent multiple simultaneous saves â queue the latest payload instead of dropping it 12288 if (window._saveInProgress) { 12289 console.log('Save already in progress, queueing latest payload'); 12290 if (customData) { 12291 window._pendingSaveGalleriesData = attachLiveSidebarElements(customData); 12292 } 12293 return new Promise(function(resolve) { 12294 window._pendingSaveResolvers = window._pendingSaveResolvers || []; 12295 window._pendingSaveResolvers.push(resolve); 12296 }); 12297 } 12298 12299 window._saveInProgress = true; 12300 12301 try { 12302 // CRITICAL: Synchronize galleries before saving to ensure we use the latest data 12303 if (typeof synchronizeGalleries === 'function') { 12304 synchronizeGalleries(); 12305 console.log('Galleries synchronized before saving'); 12306 } 12307 12308 // Try to get a valid token from multiple sources 12309 let token = null; 12310 let tokenSource = 'none'; 12311 12312 // First try the global didToken (traditional approach) 12313 if (didToken) { 12314 console.log('Using global didToken for save operation'); 12315 token = didToken; 12316 tokenSource = 'didToken'; 12317 } 12318 // Next try TokenManager if available 12319 else if (window.TokenManager && typeof window.TokenManager.getToken === 'function') { 12320 token = window.TokenManager.getToken(); 12321 12322 // Remove Bearer prefix if present (we'll add it later) 12323 if (token && token.startsWith('Bearer ')) { 12324 token = token.substring(7); 12325 } 12326 12327 console.log('Using TokenManager token for save operation, length:', token ? token.length : 0); 12328 tokenSource = 'TokenManager'; 12329 } 12330 // Fallback to localStorage 12331 else if (localStorage.getItem('hydra_auth_token')) { 12332 token = localStorage.getItem('hydra_auth_token'); 12333 console.log('Using localStorage token for save operation, length:', token.length); 12334 tokenSource = 'localStorage'; 12335 } 12336 // Last resort: try to get a fresh token 12337 else if (magic && magic.user) { 12338 try { 12339 console.log('Attempting to get fresh token from Magic'); 12340 token = await magic.user.getIdToken(); 12341 console.log('Obtained fresh token from Magic:', token ? 'success' : 'failed'); 12342 tokenSource = token ? 'magic' : 'none'; 12343 } catch (e) { 12344 console.error('Error getting token from Magic:', e); 12345 tokenSource = 'magic_error'; 12346 } 12347 } 12348 12349 // [AUTH_SAVE] Log for S+J download - token state and logged-in email before request 12350 const loggedInEmail = (window.userMetadata && window.userMetadata.email) || 12351 localStorage.getItem('hydra_auth_email') || 12352 ''; 12353 console.log('[AUTH_SAVE] Token resolved:', { 12354 loggedInEmail: loggedInEmail || '(none)', 12355 tokenSource, 12356 tokenLength: token ? token.length : 0, 12357 tokenPrefix: token ? token.substring(0, 15) + '...' : '(none)', 12358 hasToken: !!token, 12359 isAuthenticated, 12360 hydraIsAdmin: localStorage.getItem('hydra_is_admin') === 'true', 12361 hasLocalStorageToken: !!localStorage.getItem('hydra_auth_token'), 12362 hasTokenManager: !!(window.TokenManager && typeof window.TokenManager.getToken === 'function') 12363 }); 12364 12365 if (!token) { 12366 console.error('[AUTH_SAVE] No token available - save will fail. Sources checked: didToken, TokenManager, localStorage, magic.'); 12367 throw new Error('Could not find a valid authentication token. Please log in again.'); 12368 } 12369 12370 // IMPORTANT: Try to save to localStorage with isolated error handling 12371 // This ensures localStorage failures don't prevent server saves 12372 if (!customData) { // Only try localStorage for regular gallery saves
12373 try { 12374 // FIXED: Always save the FULL galleries data to localStorage 12375 localStorage.setItem('galleries', JSON.stringify(galleries)); 12376 console.log(`Saved ${galleries.length} galleries to localStorage (FULL format with complete settings)`); 12377 } catch (storageError) { 12378 // Now storage errors are isolated and non-fatal 12379 console.warn('LocalStorage quota exceeded, proceeding without local backup', storageError); 12380 } 12381 } else if (customData && customData.isImport) { 12382 // Skip localStorage for import operations - data too large 12383 console.log('Skipping localStorage for import operation - data too large for local storage'); 12384 } 12385 12386 // IMPORTANT: Ensure all pages have proper URLs and slugs before saving 12387 galleries.forEach(gallery => { 12388 if (gallery.isPage === true) { 12389 const pageSlug = slugify(gallery.title); 12390 12391 // Make sure slug and URL are set correctly 12392 if (!gallery.slug || gallery.slug === '') { 12393 gallery.slug = pageSlug; 12394 } 12395 12396 // Make sure URL points to slug 12397 if (!gallery.url || gallery.url === '/' || !gallery.url.includes(gallery.slug)) { 12398 gallery.url = `/${pageSlug}`; 12399 } 12400 } 12401 }); 12402 12403 // Set up the data to save - ensure we're using the complete data 12404 let siteData; 12405 console.log('saveGalleries: customData received:', JSON.stringify(customData)); 12406 console.log('saveGalleries: siteMetadata in customData:', customData?.siteMetadata); 12407 console.log('saveGalleries: TITLE in customData:', customData?.siteMetadata?.title); 12408 console.log('saveGalleries: GOOGLE ANALYTICS in customData:', customData?.siteMetadata?.googleAnalytics); 12409 if (customData) { 12410 // CRITICAL FIX: Ensure gallery settings are preserved when saving from sidebar 12411 // If customData contains galleries (which it should for sidebar-initiated saves), 12412 // merge them with any existing settings to ensure nothing is lost 12413 if (customData.galleries && Array.isArray(customData.galleries)) { 12414 // First, ensure we preserve all current gallery settings from the existing galleries 12415 const existingGalleries = (typeof galleries !== 'undefined' ? galleries : window.galleries) || []; 12416 12417 // Create merged galleries array with preserved settings 12418 const mergedGalleries = customData.galleries.map(gallery => { 12419 const existingGallery = existingGalleries.find(g => g.id === gallery.id); 12420 12421 // If this gallery exists in the current dataset, ensure all galleryOptions are preserved 12422 if (existingGallery && existingGallery.galleryOptions && gallery.galleryOptions) { 12423 // Calculate settings depth 12424 const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length; 12425 const serverOptionsDepth = Object.keys(gallery.galleryOptions).length; 12426 12427 // If server has fewer settings, merge them with existing settings 12428 if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) { 12429 console.log(`Preserving rich settings for ${gallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`); 12430 12431 // Create a merged gallery with preserved settings 12432 return { 12433 ...gallery, 12434 galleryOptions: { 12435 ...existingGallery.galleryOptions, // Start with all existing settings 12436 ...gallery.galleryOptions // Override with any new server settings 12437 } 12438 }; 12439 } 12440 } 12441 12442 // If no preservation needed, use server gallery as is 12443 return gallery; 12444 }); 12445 12446 // Use the merged galleries in customData 12447 siteData = { 12448 ...customData, 12449 galleries: mergedGalleries 12450 }; 12451 12452 console.log(`Settings-preservation merge completed: ${mergedGalleries.length} galleries`); 12453 console.log('saveGalleries: siteData after merge:', JSON.stringify(siteData)); 12454 console.log('saveGalleries: siteMetadata after merge:', siteData.siteMetadata); 12455 console.log('saveGalleries: TITLE after merge:', siteData.siteMetadata?.title); 12456 console.log('saveGalleries: GOOGLE ANALYTICS after merge:', siteData.siteMetadata?.googleAnalytics); 12457 } else { 12458 siteData = customData; 12459 } 12460 } else { 12461 const galleriesForSave = (typeof window !== 'undefined' && window.galleries && Array.isArray(window.galleries)) 12462 ? window.galleries 12463 : galleries; 12464 siteData = { 12465 galleries: galleriesForSave 12466 }; 12467 12468 if (window.siteMetadata) { 12469 siteData.siteMetadata = window.siteMetadata; 12470 } 12471 12472 if (window.SidebarManager) { 12473 const liveSidebar = typeof window.SidebarManager.getElementsForSave === 'function' 12474 ? window.SidebarManager.getElementsForSave() 12475 : null; 12476 if (Array.isArray(liveSidebar) && liveSidebar.length > 0) { 12477 siteData.sidebarElements = liveSidebar; 12478 } 12479 } 12480 } 12481 12482 attachLiveSidebarElements(siteData); 12483 12484 // Ensure galleries are never saved as pages: any item with galleryOptions is a gallery and must persist with isPage false.
12485 // (loadPage can set isPage=true in memory; sidebar/full saves would otherwise persist that and convert TEAM etc. to pages.) 12486 if (siteData.galleries && Array.isArray(siteData.galleries)) { 12487 siteData.galleries = siteData.galleries.map(g => { 12488 if (g && g.galleryOptions && typeof g.galleryOptions === 'object') { 12489 return { ...g, isPage: false }; 12490 } 12491 return g; 12492 }); 12493 } 12494 12495 // Get the save URL 12496 const saveUrl = typeof getApiUrl === 'function' ? 12497 getApiUrl('/api/save-config') : 12498 '/api/save-config'; 12499 12500 // Get user email from various sources 12501 const userEmail = (window.userMetadata && window.userMetadata.email) || 12502 localStorage.getItem('hydra_auth_email') || 12503 ''; 12504 12505 // Ensure token has proper format 12506 const formattedToken = token.startsWith('hydra:') ? token : 12507 (token.includes('.') ? token : `hydra:${token}`); 12508 12509 console.log('Making server save request...'); 12510 console.log('saveGalleries: Final siteData being sent to server:', JSON.stringify(siteData)); 12511 console.log('saveGalleries: Final siteMetadata being sent:', siteData.siteMetadata); 12512 console.log('saveGalleries: FINAL TITLE being sent:', siteData.siteMetadata?.title); 12513 console.log('saveGalleries: FINAL GOOGLE ANALYTICS being sent:', siteData.siteMetadata?.googleAnalytics); 12514 12515 // [AUTH_SAVE] Log for S+J download - request details (no full token) 12516 console.log('[AUTH_SAVE] Sending save request:', { 12517 saveUrl, 12518 authHeaderPrefix: `Bearer ${formattedToken.substring(0, 20)}...`, 12519 userEmail: userEmail || '(empty)', 12520 tokenSource, 12521 formattedTokenLength: formattedToken.length 12522 }); 12523 12524 // Make the request 12525 let response = await fetch(saveUrl, { 12526 method: 'POST', 12527 headers: { 12528 'Content-Type': 'application/json', 12529 'Authorization': `Bearer ${formattedToken}`, 12530 'X-User-Email': userEmail, 12531 'X-API-Request': 'true' // Always include this header for API requests 12532 }, 12533 body: JSON.stringify(siteData) 12534 }); 12535 12536 console.log('Save response status:', response.status); 12537 12538 // Process the response 12539 let responseText = await response.text(); 12540 console.log('Server response text (first 500 chars):', responseText.substring(0, 500)); 12541 let responseData; 12542 12543 try { 12544 responseData = JSON.parse(responseText); 12545 console.log('Server response parsed successfully:', responseData); 12546 if (responseData.debug) { 12547 console.log('Server debug info:', responseData.debug); 12548 console.log('Server debug - requestBodyKeys:', responseData.debug.requestBodyKeys); 12549 console.log('Server debug - hasSiteMetadata:', responseData.debug.hasSiteMetadata); 12550 console.log('Server debug - siteMetadata:', responseData.debug.siteMetadata); 12551 console.log('Server debug - finalSiteMetadata:', responseData.debug.finalSiteMetadata); 12552 console.log('Server debug - googleAnalytics:', responseData.debug.googleAnalytics); 12553 } 12554 } catch (jsonError) { 12555 if (response.status === 200 && responseText.trim().startsWith('<!DOCTYPE html>')) { 12556 responseData = { success: true }; 12557 } else { 12558 throw new Error('Invalid response format: ' + responseText.substring(0, 100)); 12559 } 12560 } 12561 12562 if (!response.ok && !responseData.success) { 12563 // [AUTH_SAVE] Log for S+J download - capture full 401/auth failure details 12564 const authSaveFailure = { 12565 status: response.status, 12566 statusText: response.statusText, 12567 error: responseData && responseData.error, 12568 details: responseData && responseData.details, 12569 authMethod: responseData && responseData.authMethod, 12570 fullResponse: responseData, 12571 responseTextPreview: responseText.substring(0, 300) 12572 }; 12573 console.error('[AUTH_SAVE] Save request failed:', authSaveFailure); 12574 12575 // Retry once on 401: token may have expired - try refresh or fresh Magic token 12576 if (response.status === 401) {
12577 console.log('[AUTH_SAVE] 401 received - attempting token refresh before retry...'); 12578 let newToken = null; 12579 if (window.TokenManager && typeof window.TokenManager.refreshToken === 'function') { 12580 newToken = await window.TokenManager.refreshToken(); 12581 } 12582 if (!newToken && magic && magic.user) { 12583 try { 12584 newToken = await magic.user.getIdToken(); 12585 } catch (e) { 12586 console.warn('[AUTH_SAVE] Magic getIdToken failed:', e); 12587 } 12588 } 12589 if (newToken) { 12590 if (newToken.startsWith('Bearer ')) newToken = newToken.substring(7); 12591 token = newToken; 12592 didToken = newToken; 12593 localStorage.setItem('hydra_auth_token', newToken); 12594 if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') { 12595 window.TokenManager.storeToken(newToken.startsWith('hydra:') ? newToken : `hydra:${newToken}`, userEmail); 12596 } 12597 console.log('[AUTH_SAVE] Got fresh token, retrying save...'); 12598 const retryResponse = await fetch(saveUrl, { 12599 method: 'POST', 12600 headers: { 12601 'Content-Type': 'application/json', 12602 'Authorization': `Bearer ${token.startsWith('hydra:') ? token : (token.includes('.') ? token : `hydra:${token}`)}`, 12603 'X-User-Email': userEmail, 12604 'X-API-Request': 'true' 12605 }, 12606 body: JSON.stringify(siteData) 12607 }); 12608 const retryText = await retryResponse.text(); 12609 let retryData; 12610 try { retryData = JSON.parse(retryText); } catch (e) { 12611 if (retryResponse.status === 200 && retryText.startsWith('<!DOCTYPE')) retryData = { success: true }; 12612 else throw new Error('Invalid retry response'); 12613 } 12614 if (retryResponse.ok || retryData.success) { 12615 response = retryResponse; 12616 responseText = retryText; 12617 responseData = retryData; 12618 console.log('[AUTH_SAVE] Retry succeeded'); 12619 } else { 12620 console.error('[AUTH_SAVE] Retry also failed:', retryResponse.status, retryData); 12621 throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`); 12622 } 12623 } else { 12624 throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`); 12625 } 12626 } else { 12627 throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`); 12628 } 12629 } 12630 12631 console.log('Configuration saved successfully'); 12632 12633 // Update local lastKnownConfigTimestamp after successful save 12634 if (responseData.lastModified) { 12635 window.lastKnownConfigTimestamp = responseData.lastModified; 12636 console.error('[CONFLICT DETECT] â Updated local config timestamp from save response:', window.lastKnownConfigTimestamp); 12637 } else { 12638 console.error('[CONFLICT DETECT] â ï¸ No lastModified in save response, fetching from server...'); 12639 // If server doesn't return timestamp, fetch it 12640 try { 12641 const timestampUrl = typeof getApiUrl === 'function' ? 12642 getApiUrl('/api/config-timestamp') : 12643 '/api/config-timestamp'; 12644 const timestampResponse = await fetch(timestampUrl); 12645 if (timestampResponse.ok) { 12646 const timestampData = await timestampResponse.json(); 12647 if (timestampData.lastModified) { 12648 window.lastKnownConfigTimestamp = timestampData.lastModified; 12649 console.error('[CONFLICT DETECT] â Fetched and updated local config timestamp:', window.lastKnownConfigTimestamp); 12650 } else { 12651 console.error('[CONFLICT DETECT] â ï¸ No lastModified in fetched timestamp data:', timestampData); 12652 } 12653 } else { 12654 console.error('[CONFLICT DETECT] â ï¸ Failed to fetch timestamp after save, status:', timestampResponse.status); 12655 } 12656 } catch (err) { 12657 console.error('[CONFLICT DETECT] â ï¸ Could not fetch timestamp after save:', err); 12658 } 12659 } 12660 12661 // Store new token if provided 12662 if (responseData.hydraToken) { 12663 console.log('Received new hydra token from server'); 12664 12665 // Update the global didToken 12666 didToken = responseData.hydraToken.startsWith('hydra:') ? 12667 responseData.hydraToken.substring(6) : responseData.hydraToken; 12668 12669 // Update localStorage 12670 localStorage.setItem('hydra_auth_token', didToken); 12671 12672 // Update TokenManager if available 12673 if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') { 12674 window.TokenManager.storeToken(responseData.hydraToken, userEmail); 12675 } 12676 } 12677 12678 // Process server response data if needed 12679 if (responseData.galleries) { 12680 // If server returns updated galleries data, update the global galleries variable 12681 if (Array.isArray(responseData.galleries) && responseData.galleries.length > 0) { 12682 // CRITICAL FIX: Before replacing galleries with server response, 12683 // preserve any gallery settings that might be lost in the response 12684 const updatedGalleries = responseData.galleries.map(serverGallery => { 12685 // Find the matching gallery in our current data 12686 const existingGallery = galleries.find(g => g.id === serverGallery.id); 12687 12688 // Check if we need to preserve gallery settings 12689 if (existingGallery && existingGallery.galleryOptions && serverGallery.galleryOptions) { 12690 // Calculate settings depth 12691 const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length; 12692 const serverOptionsDepth = Object.keys(serverGallery.galleryOptions).length; 12693 12694 // If server has fewer settings, merge them with existing settings 12695 if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) { 12696 console.log(`Preserving rich settings for ${serverGallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`); 12697 12698 // Create a merged gallery with preserved settings 12699 return { 12700 ...serverGallery, 12701 galleryOptions: { 12702 ...existingGallery.galleryOptions, // Start with all existing settings 12703 ...serverGallery.galleryOptions // Override with any new server settings 12704 } 12705 }; 12706 } 12707 } 12708 12709 // If no preservation needed, use server gallery as is 12710 return serverGallery; 12711 }); 12712 12713 // Update the galleries variable with our preserved data 12714 galleries = updatedGalleries; 12715 console.log(`Updated galleries from server response: ${galleries.length} galleries`); 12716 12717 // IMPORTANT: Synchronize again after receiving server response 12718 if (typeof synchronizeGalleries === 'function') { 12719 synchronizeGalleries(); 12720 console.log('Galleries re-synchronized after server response'); 12721 } 12722 12723 // Keep sidebar UI in sync with current galleries (e.g. after delete) 12724 if (typeof renderGalleries === 'function') { 12725 renderGalleries(); 12726 } 12727 12728 // Update localStorage with full data 12729 try { 12730 localStorage.setItem('galleries', JSON.stringify(galleries)); 12731 console.log('Updated localStorage with server gallery data (FULL format)'); 12732 } catch (e) { 12733 console.warn('Could not update localStorage with server gallery data', e); 12734 } 12735 } 12736 } 12737
12738 // CRITICAL FIX: Clear all sessionStorage caches after successful save 12739 // This ensures that when users open the site in a new tab, they get fresh data 12740 try { 12741 console.log('Clearing sessionStorage caches after successful save...'); 12742 const keysToRemove = []; 12743 for (let i = 0; i < sessionStorage.length; i++) { 12744 const key = sessionStorage.key(i); 12745 if (key && (key.includes('gallery_data_') || key.includes('neonGallery_'))) { 12746 keysToRemove.push(key); 12747 } 12748 } 12749 keysToRemove.forEach(key => { 12750 sessionStorage.removeItem(key); 12751 console.log('Cleared sessionStorage key: ' + key); 12752 }); 12753 console.log('Cleared ' + keysToRemove.length + ' sessionStorage cache entries'); 12754 } catch (cacheError) { 12755 console.warn('Error clearing sessionStorage cache:', cacheError); 12756 // Don't fail the save if cache clearing fails 12757 } 12758 12759 window._saveInProgress = false; 12760 12761 if (!customData || !Array.isArray(customData.sidebarElements)) { 12762 document.dispatchEvent(new CustomEvent('sidebar-save-requested')); 12763 } 12764 12765 // Flush any save queued while this request was in flight 12766 if (window._pendingSaveGalleriesData) { 12767 const pending = window._pendingSaveGalleriesData; 12768 window._pendingSaveGalleriesData = null; 12769 const resolvers = window._pendingSaveResolvers || []; 12770 window._pendingSaveResolvers = []; 12771 return saveGalleries(pending).then(function(result) { 12772 resolvers.forEach(function(r) { r(result); }); 12773 return result; 12774 }); 12775 } 12776 12777 const idleResolvers = window._pendingSaveResolvers || []; 12778 window._pendingSaveResolvers = []; 12779 idleResolvers.forEach(function(r) { r(true); }); 12780 12781 return true; 12782 } catch (error) { 12783 // [AUTH_SAVE] Log for S+J download - capture exception (network, parse, or thrown after 401) 12784 console.error('[AUTH_SAVE] Save threw exception:', { 12785 message: error.message, 12786 name: error.name, 12787 stack: error.stack 12788 }); 12789 console.error('Error saving configuration:', error); 12790 alert('Failed to save changes: ' + error.message); 12791 window._saveInProgress = false; 12792 const failResolvers = window._pendingSaveResolvers || []; 12793 window._pendingSaveResolvers = []; 12794 failResolvers.forEach(function(r) { r(false); }); 12795 return false; 12796 } 12797} 12798 12799// Helper function to get API URL for preview sites 12800function getApiUrl(endpoint) { 12801 // Check if we're on a preview URL 12802 const isPreviewUrl = window.location.hostname === 'preview.neonsky.app'; 12803 12804 if (isPreviewUrl) { 12805 // Extract the site GUID from the path (first segment after domain) 12806 const pathParts = window.location.pathname.split('/').filter(Boolean); 12807 if (pathParts.length > 0) { 12808 const siteGuid = pathParts[0]; 12809 // Include the site GUID in the API URL 12810 return `/${siteGuid}${endpoint}`; 12811 } 12812 } 12813 12814 // Regular case - return the endpoint as is 12815 return endpoint; 12816} 12817 12818function updateActiveStates() { 12819 // Get the most current active gallery ID, ensuring consistency between both variables 12820 const currentActiveId = window.activeGalleryId || activeGalleryId; 12821 12822 // Ensure both variables are synchronized 12823 window.activeGalleryId = currentActiveId; 12824 activeGalleryId = currentActiveId; 12825 12826 console.log("updateActiveStates: Using active gallery ID:", currentActiveId); 12827 12828 // Special case for invisible home page - don't try to highlight it in the menu 12829 const activeGallery = galleries.find(g => g.id === currentActiveId); 12830 if (activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true) { 12831 console.log("Active gallery is invisible home page - not highlighting in menu"); 12832 // We're not returning here, so it will still update the visibility classes 12833 // but won't try to find an element to mark as active 12834 } 12835 12836 // Fix selector issue - look for both .tree and .sidebar 12837 document.querySelectorAll('.tree li, .sidebar li').forEach(li => { 12838 if (!li.dataset.id) return; // Skip items without data-id attribute 12839 12840 const id = parseInt(li.dataset.id); 12841 if (isNaN(id)) return; // Skip if ID is not a number 12842 12843 const gallery = galleries.find(g => g.id === id); 12844 12845 // Update active state using the most current ID 12846 const isActive = id === currentActiveId; 12847 12848 // Add or remove the active class 12849 if (isActive) { 12850 li.classList.add('active'); 12851 console.log(`Setting active class for element: ${id} (${gallery ? gallery.title : 'Unknown'})`); 12852 } else { 12853 li.classList.remove('active'); 12854 } 12855 12856 // Update visibility class if gallery exists 12857 if (gallery) { 12858 li.classList.toggle('hidden-gallery', gallery.visible === false); 12859 12860 // Update visibility toggle icon if it exists 12861 const toggleButton = li.querySelector('.visibility-toggle'); 12862 if (toggleButton) { 12863 toggleButton.classList.toggle('hidden', gallery.visible === false); 12864 } 12865 } 12866 }); 12867 12868 // Verify that the active state was actually set for the current activeGalleryId 12869 const activeElements = document.querySelectorAll('.active'); 12870 if (activeElements.length === 0 && currentActiveId) { 12871 // Only log a warning if we're not dealing with an invisible home page 12872 const activeGallery = galleries.find(g => g.id === currentActiveId); 12873 if (!(activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true)) { 12874 console.warn(`No active elements found after update for ID: ${currentActiveId}`); 12875 } 12876 } 12877 12878 // Log active items for debugging 12879 console.log("Active gallery ID after update:", currentActiveId);
12880 activeElements.forEach(el => 12881 console.log("Active element:", el.textContent.trim()) 12882 ); 12883} 12884 12885 12886function debugGalleryStructure() { 12887console.group('Gallery Structure Debug'); 12888 12889// Log the raw data structure 12890console.log('Raw galleries array:', JSON.parse(JSON.stringify(galleries))); 12891 12892// Log the tree structure 12893const treeStructure = createGalleryTree(galleries); 12894 12895// Helper to print the tree 12896function printTree(items, level = 0) { 12897const indent = ' '.repeat(level); 12898items.forEach(item => { 12899 console.log(`${indent}${item.title} (ID: ${item.id}, Parent: ${item.parentId || 'ROOT'})`); 12900 if (item.children && item.children.length > 0) { 12901 printTree(item.children, level + 1); 12902 } 12903}); 12904} 12905 12906console.log('Tree structure:'); 12907printTree(treeStructure); 12908 12909// Log the DOM structure 12910console.log('DOM structure:'); 12911const treeEl = document.getElementById('galleryTree'); 12912console.log(treeEl); 12913 12914console.groupEnd(); 12915} 12916 12917function createGalleryTree(galleries) { 12918console.log("Creating gallery tree with preservation safeguards..."); 12919 12920// First, create a map of galleries by id for quick lookup 12921const galleryMap = {}; 12922const processedIds = new Set(); 12923 12924// First pass: Create clean copies without children 12925galleries.forEach(gallery => { 12926if (!gallery || !gallery.id) return; // Skip invalid galleries 12927 12928// Create a copy of the gallery object without the children field 12929const cleanGallery = { ...gallery }; 12930delete cleanGallery.children; 12931 12932// Initialize an empty children array 12933cleanGallery.children = []; 12934 12935// Add to map - even if we've seen this ID before, keep the latest version 12936if (processedIds.has(gallery.id)) { 12937 console.warn(`Multiple galleries with ID ${gallery.id} - using latest version`); 12938} 12939 12940processedIds.add(gallery.id); 12941galleryMap[gallery.id] = cleanGallery; 12942}); 12943 12944// Second pass: Assign children to their parents 12945const rootGalleries = []; 12946 12947// To detect already-processed children 12948const childrenAssigned = new Set(); 12949 12950galleries.forEach(gallery => { 12951if (!gallery || !gallery.id) return; // Skip invalid galleries 12952 12953const galleryId = gallery.id; 12954 12955// Skip if this gallery isn't in our map (could be invalid) 12956if (!galleryMap[galleryId]) return; 12957 12958// Skip if we've already assigned this gallery as a child 12959if (childrenAssigned.has(galleryId)) return; 12960 12961if (gallery.parentId && galleryMap[gallery.parentId]) { 12962 // This is a child gallery, but first check it doesn't create a circular reference 12963 12964 // Find all parents in the chain up to the root 12965 let currentParent = galleryMap[gallery.parentId]; 12966 let safeToAdd = true; 12967 let chainIds = [galleryId, gallery.parentId]; 12968 12969 while (currentParent.parentId) { 12970 // If we've seen this ID in the chain, it would create a circular reference 12971 if (chainIds.includes(currentParent.parentId)) { 12972 console.warn(`Would create circular reference for gallery ${gallery.title} - making it a root gallery`); 12973 safeToAdd = false; 12974 break; 12975 } 12976 12977 chainIds.push(currentParent.parentId); 12978 currentParent = galleryMap[currentParent.parentId]; 12979 12980 // If parent doesn't exist in the map, break the chain 12981 if (!currentParent) break; 12982 } 12983 12984 if (safeToAdd) { 12985 // Safe to add as child 12986 galleryMap[gallery.parentId].children.push(galleryMap[galleryId]); 12987 childrenAssigned.add(galleryId); 12988 } else { 12989 // Not safe - make it a root gallery 12990 rootGalleries.push(galleryMap[galleryId]); 12991 } 12992} else { 12993 // This is a root gallery (no parent or parent doesn't exist) 12994 if (!childrenAssigned.has(galleryId)) { 12995 rootGalleries.push(galleryMap[galleryId]); 12996 } 12997} 12998}); 12999 13000// Final verification 13001const allGalleryIds = new Set(galleries.map(g => g.id)); 13002const treeGalleryIds = new Set(); 13003 13004// Function to collect all IDs in the tree 13005const collectIds = (items) => { 13006items.forEach(item => { 13007 treeGalleryIds.add(item.id); 13008 if (item.children && item.children.length > 0) { 13009 collectIds(item.children); 13010 } 13011}); 13012}; 13013 13014collectIds(rootGalleries); 13015 13016// Check if any galleries are missing from the tree 13017const missingIds = []; 13018allGalleryIds.forEach(id => { 13019if (!treeGalleryIds.has(id)) { 13020 missingIds.push(id); 13021} 13022}); 13023 13024if (missingIds.length > 0) { 13025console.warn(`${missingIds.length} galleries are missing from the tree structure`); 13026 13027// Add the missing galleries as root items 13028missingIds.forEach(id => { 13029 const gallery = galleryMap[id]; 13030 if (gallery && !childrenAssigned.has(id)) { 13031 console.log(`Adding missing gallery to root: ${gallery.title}`); 13032 rootGalleries.push(gallery); 13033 } 13034}); 13035} 13036 13037console.log(`Created tree with ${rootGalleries.length} root galleries`); 13038return rootGalleries; 13039} 13040 13041function alignFolderStates() { 13042 console.log("Aligning folder data states with collapsed visual appearance"); 13043 let changed = 0; 13044
13045 galleries.forEach(gallery => { 13046 if ((gallery.isFolder === true || gallery.isSubmenu === true) && gallery.isCollapsed === false) { 13047 gallery.isCollapsed = true; 13048 changed++; 13049 } 13050 }); 13051 13052} 13053 13054 13055 13056function renderGalleryItem(gallery, level = 0, maxLevel = 10) { 13057 // Prevent infinite recursion by limiting depth 13058 if (level >= maxLevel) { 13059 console.warn(`Maximum nesting level reached for gallery: ${gallery.title}`); 13060 return ''; 13061 } 13062 13063 // Add debug info to console 13064 console.log(`Rendering gallery item: ${gallery.title}, isEditing=${isEditing}, level=${level}`); 13065 13066 // Skip hidden items in view mode (but not spacers) 13067 if (!isEditing && gallery.visible === false && !gallery.isSpacer) { 13068 return ''; 13069 } 13070 13071const isPage = isPageMenuItem(gallery); 13072const isFolder = gallery.isFolder === true || gallery.isSubmenu === true; // Support both folder and legacy submenu 13073const isNavigableParent = isNavigableMenuParent(gallery); 13074const isSpacer = gallery.isSpacer === true; 13075const isExternal = gallery.isExternal === true; 13076 13077// CRITICAL FIX: Ensure children are always populated for folders 13078// If gallery.children is missing or empty, but this is a folder, build children from flat galleries array 13079// This ensures the nested <ol> is always rendered with children in the DOM, even in view mode 13080if (isFolder) { 13081 // Always ensure children array exists 13082 if (!gallery.children) { 13083 gallery.children = []; 13084 } 13085 // If children array is empty, build it from flat galleries array using parentId 13086 if (gallery.children.length === 0) { 13087 const childrenFromFlat = galleries.filter(g => g.parentId === gallery.id); 13088 if (childrenFromFlat.length > 0) { 13089 gallery.children = childrenFromFlat; 13090 console.log(`[renderGalleryItem] Built children for folder ${gallery.title}: ${gallery.children.length} children`); 13091 } 13092 } 13093} 13094 13095// Parent-capable items need their chevron and empty nested drop zone immediately, 13096// before the first child has been added. 13097const hasChildren = isFolder || isNavigableParent || gallery.children?.length > 0; 13098const hasChildrenClass = hasChildren ? 'has-children' : ''; 13099 13100// Standard collapsed state check 13101const isCollapsed = gallery.isCollapsed === true; 13102 13103// Simple expanded class logic - if the folder is not collapsed, add 'expanded' class 13104const isExpandedClass = (isFolder || isNavigableParent) && !isCollapsed ? 'expanded' : ''; 13105 13106// Add additional classes for special types 13107const spacerClass = isSpacer ? 'spacer-item' : ''; 13108const externalClass = isExternal ? 'external-link' : ''; 13109const folderClass = isFolder ? 'folder-item' : ''; 13110const navigableParentClass = isNavigableParent ? 'navigable-parent-item' : ''; 13111 13112 const duplicateIconSvg = ` 13113 <svg xmlns="[http://www.w3.org/2000/svg](http://www.w3.org/2000/svg)" width="24" height="24" viewBox="0 0 24 24"> 13114 <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"> 13115 <path d="M16.964 8.982c-.003-2.95-.047-4.478-.906-5.524a4 4 0 0 0-.553-.554C14.4 2 12.76 2 9.48 2s-4.92 0-6.024.905a4 4 0 0 0-.553.554C1.998 4.56 1.998 6.2 1.998 9.48s0 4.92.906 6.023q.25.304.553.553c1.047.86 2.575.904 5.525.906"/> 13116 <path d="m14.028 9.025l2.966-.043m-2.98 13.02l2.966-.043m4.992-7.937l-.028 2.96M9.01 14.036l-.028 2.96m2.505-7.971c-.832.149-2.17.302-2.477 2.024m10.485 10.91c.835-.137 2.174-.27 2.508-1.986M19.495 9.025c.832.149 2.17.302 2.477 2.024M11.5 21.957c-.833-.148-2.17-.301-2.478-2.023"/> 13117 </g> 13118 </svg> 13119 `; 13120 13121 13122 13123 // NEW: Add nesting level as a data attribute for styling 13124 const nestingLevelAttr = `data-nesting-level="${level}"`; 13125 13126 // Accessible href for SPA items (loadPage/loadGallery still preventDefault) 13127 const menuHref = gallery.isHomePage 13128 ? '/' 13129 : (gallery.url && String(gallery.url).startsWith('/') 13130 ? gallery.url 13131 : (gallery.slug ? ('/' + gallery.slug) : '#')); 13132 const childrenListId = 'gallery-children-' + gallery.id; 13133 const folderExpandedAttr = !isCollapsed ? 'true' : 'false'; 13134 const ariaCurrentAttr = (gallery.id === activeGalleryId && !isFolder && !isSpacer) 13135 ? ' aria-current="page"' 13136 : ''; 13137 13138 // Create the gallery item HTML with enhanced nesting indicators 13139 const html = ` 13140<li data-id="${gallery.id}" 13141 ${nestingLevelAttr} 13142 class="${gallery.id === activeGalleryId ? 'active' : ''} 13143 ${gallery.visible === false ? 'hidden-gallery' : ''} 13144 ${hasChildrenClass} ${isExpandedClass} ${spacerClass} ${externalClass} ${folderClass} ${navigableParentClass}">
13145 <div class="gallery-item-content nested-sortable-handle"> 13146 ${hasChildren ? ` 13147 <button type="button" class="submenu-toggle" onclick="toggleFolder(${gallery.id}, event)" aria-expanded="${folderExpandedAttr}" aria-controls="${childrenListId}" aria-label="Expand or collapse ${String(gallery.title || 'folder').replace(/"/g, '"')}"> 13148 <svg class="icon toggle-icon ${!isCollapsed ? 'rotated' : ''}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> 13149 <polyline points="9 6 15 12 9 18"></polyline> 13150 </svg> 13151 </button> 13152 ` : ''} 13153 13154 <!-- Menu item with different rendering based on type --> 13155 ${isSpacer ? ` 13156 <!-- Spacer with non-breaking space to ensure height --> 13157 <span class="menu-item spacer" aria-hidden="true"> </span> 13158 ` : isExternal ? ` 13159 <!-- External URL uses an anchor tag with target="_blank" --> 13160 <a href="${gallery.url}" class="menu-item external-link" target="_blank" rel="noopener noreferrer"${ariaCurrentAttr}> 13161 ${gallery.title} 13162 </a> 13163 ` : isFolder ? ` 13164 <!-- Folder item with toggle functionality (button for keyboard / SR) --> 13165 <button type="button" class="menu-item folder" data-folder="true" 13166 onclick="toggleFolder(${gallery.id}, event)" aria-expanded="${folderExpandedAttr}" aria-controls="${childrenListId}"> 13167 ${gallery.title} 13168 </button> 13169 ` : ` 13170 <!-- Real link for SR/keyboard; SPA handlers keep click behavior --> 13171 <a href="${menuHref}" class="menu-item" onclick="${isPage ? `loadPage(${gallery.id}, event)` : `loadGallery(${gallery.id}, event)`}"${ariaCurrentAttr}> 13172 ${gallery.title} 13173 </a> 13174 `} 13175 13176<div class="controls"> 13177 ${!isSpacer ? ` 13178 <button class="btn visibility-toggle ${gallery.visible === false ? 'hidden' : ''}" onclick="toggleVisibility(${gallery.id}, event)"> 13179 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> 13180 ${gallery.visible !== false ? 13181 '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle>' : 13182 '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line>' 13183 } 13184 </svg> 13185 </button> 13186 13187 <button class="btn password-toggle ${gallery.passwordProtected ? 'protected' : ''}" onclick="toggleGalleryPasswordProtection(${gallery.id}, event)" title="${gallery.passwordProtected ? 'Remove Password Protection' : 'Add Password Protection'}"> 13188 <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24"> 13189 ${gallery.passwordProtected ? 13190 '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"><path d="M12 16.5v-2m-7.732 4.345c.225 1.67 1.608 2.979 3.292 3.056c1.416.065 2.855.099 4.44.099s3.024-.034 4.44-.1c1.684-.076 3.067-1.385 3.292-3.055c.147-1.09.268-2.207.268-3.345s-.121-2.255-.268-3.345c-.225-1.67-1.608-2.979-3.292-3.056A95 95 0 0 0 12 9c-1.585 0-3.024.034-4.44.1c-1.684.076-3.067 1.385-3.292 3.055C4.12 13.245 4 14.362 4 15.5s.121 2.255.268 3.345"/><path d="M7.5 9V6.5a4.5 4.5 0 0 1 9 0V9"/></g>' : 13191 '<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 16.5v-2m-7.732 4.345c.225 1.67 1.608 2.979 3.292 3.056c1.416.065 2.855.099 4.44.099s3.024-.034 4.44-.1c1.684-.076 3.067-1.385 3.292-3.055c.147-1.09.268-2.207.268-3.345s-.121-2.255-.268-3.345c-.225-1.67-1.608-2.979-3.292-3.056A95 95 0 0 0 12 9c-1.585 0-3.024.034-4.44.1c-1.684.076-3.067 1.385-3.292 3.055C4.12 13.245 4 14.362 4 15.5s.121 2.255.268 3.345M7.5 9V6.5A4.5 4.5 0 0 1 12 2c1.96 0 3.5 1.5 4 3" color="currentColor"/>' 13192 } 13193 </svg> 13194 </button> 13195 13196 <button class="btn edit-btn" onclick="editGallery(${gallery.id}, event)">
13197 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> 13198 <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> 13199 <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path> 13200 </svg> 13201 </button> 13202 ` : ''} 13203 13204 <button class="btn duplicate-btn" onclick="duplicateGalleryItem(${gallery.id}, event)" title="Duplicate"> 13205 ${duplicateIconSvg} 13206 </button> 13207 13208 13209 <button class="btn delete-btn" onclick="deleteGallery(${gallery.id}, event)"> 13210 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> 13211 <polyline points="3 6 5 6 21 6"></polyline> 13212 <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path> 13213 </svg> 13214 </button> 13215${!isSpacer ? ` 13216<!-- Add home toggle button --> 13217<button class="btn home-toggle ${gallery.isHomePage ? 'active' : ''}" onclick="toggleHome(${gallery.id}, event)" title="${gallery.isHomePage ? 'Currently set as Home page' : 'Set as Home page'}"> 13218 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> 13219 ${gallery.isHomePage ? 13220 '<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m3.966 5.798l.603-1.401c.501-1.163.751-1.745 1.242-2.07C6.302 2 6.927 2 8.177 2h7.646c1.25 0 1.875 0 2.366.326c.49.326.741.908 1.242 2.07l.603 1.402c.817 1.896 1.225 2.844.788 3.523S19.366 10 17.328 10H6.671c-2.037 0-3.056 0-3.493-.679s-.029-1.627.788-3.523M4.5 10v5c0 1.886 0 2.828.586 3.414c.471.472 1.174.564 2.414.582M19.5 10v5c0 1.886 0 2.828-.586 3.414c-.471.472-1.174.564-2.414.582M13 14l-2.594 3.067c-.317.41-.475.616-.377.775c.098.158.383.158.954.158h2.034c.57 0 .856 0 .954.158c.098.159-.06.364-.377.775L10.984 22M10 2l-1 8m5-8l1 8M4 6h16" color="currentColor"/>' : 13221 '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"><path d="M12 17h.009M20 8.5v5c0 3.771 0 5.657-1.172 6.828S15.771 21.5 12 21.5s-5.657 0-6.828-1.172S4 17.271 4 13.5v-5"/><path d="m22 10.5l-4.343-4.165C14.99 3.778 13.657 2.5 12 2.5S9.01 3.778 6.343 6.335L2 10.5"/></g>' 13222 } 13223 </svg> 13224</button> 13225` : ''} 13226 </div> 13227 </div> 13228 <ol id="${childrenListId}" class="nested-sortable ${level > 0 ? 'nested-level-' + level : ''}"> 13229 ${gallery.children && gallery.children.length > 0 ? 13230 gallery.children.map(child => renderGalleryItem(child, level + 1, maxLevel)).join('') : ''} 13231 </ol> 13232</li> 13233`; 13234 13235 return html; 13236} 13237 13238// Additional helper function to add visual indicators during drag operations 13239function addDragVisualFeedback() { 13240 // Add event listener for drag operations 13241 document.addEventListener('dragover', function(e) { 13242 // Find the closest nested sortable list 13243 const nearestList = e.target.closest('.nested-sortable'); 13244 if (!nearestList) return; 13245 13246 // Remove active-drop-target class from all lists 13247 document.querySelectorAll('.active-drop-target').forEach(el => { 13248 el.classList.remove('active-drop-target'); 13249 }); 13250 13251 // Add it to the current list 13252 nearestList.classList.add('active-drop-target'); 13253 13254 // Calculate if this would be a nested position 13255 // This is a simplified calculation - SortableJS has its own logic, 13256 // but this helps provide additional visual feedback 13257 const rect = nearestList.getBoundingClientRect(); 13258 const distanceFromLeft = e.clientX - rect.left; 13259 13260 // If we're close to the left edge of a nested list, add a class 13261 if (distanceFromLeft < 30) { 13262 nearestList.classList.add('potential-parent'); 13263 } else { 13264 nearestList.classList.remove('potential-parent'); 13265 } 13266 }); 13267 13268 // Remove classes when drag ends 13269 document.addEventListener('dragend', function() { 13270 document.querySelectorAll('.active-drop-target, .potential-parent').forEach(el => { 13271 el.classList.remove('active-drop-target', 'potential-parent'); 13272 }); 13273 }); 13274} 13275 13276// Make sure the addDragVisualFeedback function is called when the document is ready 13277document.addEventListener('DOMContentLoaded', function() { 13278 // Initialize the visual feedback helper 13279 addDragVisualFeedback(); 13280 13281 // If we're in edit mode, make sure the nested sortables are initialized 13282 if (document.body.classList.contains('edit-mode-active')) { 13283 setTimeout(function() { 13284 initializeNestedSortables(); 13285 }, 300); 13286 } 13287}); 13288 13289// Update the toggleEditMode function to ensure proper initialization of sortables 13290const originalToggleEditMode = window.toggleEditMode; 13291window.toggleEditMode = function() { 13292 originalToggleEditMode.apply(this, arguments); // Call original function 13293 13294 stopAutoAdvanceTimer(); 13295 13296 const sidebar = document.querySelector('.sidebar');
13297 const editControls = document.querySelector('.edit-controls'); 13298 13299 if (currentMenuLayout === 'horizontal') { 13300 if (document.body.classList.contains('edit-mode-active')) { 13301 // In horizontal layout and edit mode is ON: show sidebar and edit controls 13302 if(sidebar) sidebar.style.display = 'block'; // Or 'flex' if it's a flex container 13303 if(editControls) editControls.style.display = 'flex'; 13304 // Re-render the sidebar menu for editing 13305 renderGalleries(); 13306 } else { 13307 // In horizontal layout and edit mode is OFF: hide sidebar 13308 if(sidebar) sidebar.style.display = 'none'; 13309 // Re-render the horizontal menu for viewing 13310 renderHorizontalMenu(); 13311 } 13312 } else { 13313 // For other layouts (sidebar, top), ensure sidebar is visible 13314 if(sidebar) sidebar.style.display = 'block'; // Or 'flex' 13315 } 13316}; 13317 13318function verifyGalleryStructure() { 13319console.log("Verifying gallery structure integrity..."); 13320 13321// Count number of galleries 13322const galleryCount = galleries.length; 13323console.log(`Total galleries in flat array: ${galleryCount}`); 13324 13325// Create tree and count galleries in tree 13326const tree = createGalleryTree([...galleries]); 13327let treeCount = 0; 13328 13329function countGalleriesInTree(items) { 13330items.forEach(item => { 13331 treeCount++; 13332 if (item.children && item.children.length > 0) { 13333 countGalleriesInTree(item.children); 13334 } 13335}); 13336} 13337 13338countGalleriesInTree(tree); 13339console.log(`Total galleries in tree structure: ${treeCount}`); 13340 13341if (treeCount !== galleryCount) { 13342console.warn(`GALLERY COUNT MISMATCH: ${galleryCount} in array vs ${treeCount} in tree`); 13343return false; 13344} else { 13345console.log("Gallery structure is consistent"); 13346return true; 13347} 13348} 13349 13350function renderGalleries() { 13351console.log("Rendering galleries..."); 13352 13353 // Align with the explicit edit toggle only (not admin login / stale sidebar.editing) 13354 if (document.body.classList.contains('edit-mode-active')) { 13355 isEditing = true; 13356 } else if (typeof window.isEditing === 'boolean') { 13357 isEditing = window.isEditing; 13358 } 13359 13360 var galleriesSource = galleries; 13361 if (window.HelpSystem && typeof window.HelpSystem.getRenderGalleries === 'function') { 13362 var helpList = window.HelpSystem.getRenderGalleries(); 13363 if (helpList) galleriesSource = helpList; 13364 } 13365 13366try { 13367// Only fix circular references if absolutely necessary 13368const foundCircular = fixCircularReferences(); 13369 13370// Only create a new tree if we had to fix circular references 13371let galleryTree; 13372if (foundCircular) { 13373 console.log("Creating fresh gallery tree after fixing circular references"); 13374 galleryTree = createGalleryTree(galleriesSource); 13375} else { 13376 console.log("Using existing structure to create gallery tree"); 13377 galleryTree = createGalleryTree(galleriesSource); 13378} 13379 13380// Render the tree - IMPORTANT: Add null check 13381const tree = document.getElementById('galleryTree'); 13382if (!tree) { 13383 console.warn('galleryTree element not found, cannot render galleries'); 13384 return; // Exit the function if the element doesn't exist 13385} 13386 13387// Render the tree with maximum depth protection 13388tree.innerHTML = ` 13389 <ol class="nested-sortable"> 13390 ${galleryTree.map(gallery => renderGalleryItem(gallery, 0, 20)).join('')} 13391 </ol> 13392`; 13393 13394// Initialize sortable for all nested sortable elements 13395if (isEditing) { 13396 // Use requestAnimationFrame to ensure DOM is fully updated before initializing sortables 13397 // This prevents drag issues when items are added and immediately dragged 13398 requestAnimationFrame(() => { 13399 initializeNestedSortables(); 13400 }); 13401} 13402 13403console.log("Galleries rendered successfully"); 13404 13405// Set up folder toggle handler after rendering (works in both view and edit modes) 13406setupFolderToggleHandler(); 13407 13408// Verify structure integrity after rendering 13409verifyGalleryStructure(); 13410 13411if (typeof syncSeoNavA11y === 'function') syncSeoNavA11y(); 13412} catch (error) { 13413console.error("Error rendering galleries:", error); 13414} 13415} 13416 13417/** Prefer visible menu links for AT; keep SEO nav in DOM for crawlers. */ 13418function syncSeoNavA11y() { 13419 const seoNav = document.querySelector('.hydra-seo-nav'); 13420 if (!seoNav) return; 13421 const hasVisibleLinks = 13422 document.querySelectorAll('#galleryTree a.menu-item').length > 0 ||
13423 document.querySelectorAll('.horizontal-menu-container a.horizontal-menu-label').length > 0; 13424 const hideFromAt = hasVisibleLinks; 13425 seoNav.setAttribute('aria-hidden', hideFromAt ? 'true' : 'false'); 13426 // aria-hidden alone is not enough â focusable descendants must leave the tab order 13427 seoNav.querySelectorAll('a, button, [tabindex]').forEach((el) => { 13428 if (hideFromAt) { 13429 if (!el.hasAttribute('data-seo-prev-tabindex')) { 13430 el.setAttribute('data-seo-prev-tabindex', el.hasAttribute('tabindex') ? el.getAttribute('tabindex') : ''); 13431 } 13432 el.setAttribute('tabindex', '-1'); 13433 } else { 13434 const prev = el.getAttribute('data-seo-prev-tabindex'); 13435 if (prev === null) return; 13436 if (prev === '') el.removeAttribute('tabindex'); 13437 else el.setAttribute('tabindex', prev); 13438 el.removeAttribute('data-seo-prev-tabindex'); 13439 } 13440 }); 13441} 13442 13443function getMenuItemHref(galleryItem) { 13444 if (!galleryItem) return '#'; 13445 if (galleryItem.isExternal && galleryItem.url) return galleryItem.url; 13446 if (galleryItem.isHomePage) return '/'; 13447 if (galleryItem.url && String(galleryItem.url).startsWith('/')) return galleryItem.url; 13448 if (galleryItem.slug) return '/' + galleryItem.slug; 13449 return '#'; 13450} 13451 13452 13453function fixCircularReferences() { 13454console.log("Selectively checking for circular references..."); 13455 13456// Only process galleries that need checking: 13457// 1. Imported galleries (they're new and might cause issues) 13458// 2. Galleries with extremely deep nesting 13459// 3. Actual circular references 13460 13461const processed = new Set(); 13462 13463// First pass: Identify actual circular references without modifying structure 13464const circularIds = new Set(); 13465const suspiciousIds = new Set(); 13466 13467galleries.forEach(gallery => { 13468if (!gallery || !gallery.id) return; 13469 13470// Skip galleries we've already processed 13471if (processed.has(gallery.id)) return; 13472 13473// Check for self-reference - this is always wrong 13474if (gallery.parentId === gallery.id) { 13475 console.warn(`Self-reference detected: Gallery ${gallery.title} (${gallery.id}) is its own parent`); 13476 circularIds.add(gallery.id); 13477 return; 13478} 13479 13480// Skip galleries without parents 13481if (!gallery.parentId) { 13482 processed.add(gallery.id); 13483 return; 13484} 13485 13486// Follow the parent chain to detect circular references 13487const parentChain = [gallery.id]; 13488let currentId = gallery.parentId; 13489let loopCount = 0; 13490let foundCircular = false; 13491 13492while (currentId && loopCount < 20) { // Reasonable limit to prevent infinite loops 13493 loopCount++; 13494 13495 // If we've seen this ID before, it's a circular reference 13496 if (parentChain.includes(currentId)) { 13497 console.warn(`Circular reference detected in chain for gallery: ${gallery.title}`); 13498 circularIds.add(gallery.id); 13499 foundCircular = true; 13500 break; 13501 } 13502 13503 parentChain.push(currentId); 13504 13505 // Move up to the next parent 13506 const parent = galleries.find(g => g.id === currentId); 13507 currentId = parent?.parentId; 13508 13509 // If we can't find the parent, we can stop 13510 if (!parent) break; 13511} 13512 13513// If we hit our loop limit but didn't find a circular reference, 13514// it's suspicious but not definitely circular 13515if (loopCount >= 20 && !foundCircular) { 13516 console.warn(`Suspiciously deep parent chain for gallery ${gallery.title}, marking for review`); 13517 suspiciousIds.add(gallery.id); 13518} 13519 13520// Mark this gallery and all its ancestors as processed 13521parentChain.forEach(id => processed.add(id)); 13522}); 13523 13524// Second pass: Only fix the identified circular references 13525if (circularIds.size > 0) { 13526console.log(`Fixing ${circularIds.size} confirmed circular references`); 13527 13528galleries.forEach(gallery => { 13529 if (circularIds.has(gallery.id)) { 13530 console.log(`Breaking circular reference for gallery: ${gallery.title}`); 13531 gallery.parentId = null; 13532 } 13533}); 13534} else { 13535console.log("No circular references found"); 13536} 13537 13538// For suspicious galleries, don't automatically fix them 13539if (suspiciousIds.size > 0) { 13540console.warn(`Found ${suspiciousIds.size} galleries with unusually deep nesting - not fixing automatically`); 13541} 13542 13543return circularIds.size > 0; // Return true if we fixed anything 13544} 13545 13546// Deep linking implementation that uses the existing slug property 13547 13548// Function to get the slug for a gallery 13549function getGallerySlug(gallery) { 13550// First try to use the existing slug property 13551if (gallery.slug) { 13552console.log(`Using existing gallery slug: ${gallery.slug}`); 13553return gallery.slug;
13554} 13555 13556// If no slug exists, generate one from the title as fallback 13557if (gallery.title) { 13558const generatedSlug = gallery.title 13559 .toLowerCase() 13560 .replace(/[^ws-]/g, '') 13561 .replace(/s+/g, '-') 13562 .replace(/--+/g, '-') 13563 .trim(); 13564 13565console.log(`Generated slug from title: ${generatedSlug}`); 13566return generatedSlug || 'gallery'; 13567} 13568 13569// Default fallback 13570console.warn('Gallery has no slug or title, using default'); 13571return 'gallery'; 13572} 13573 13574// Function to update the URL when a gallery is loaded 13575// Update the URL when a gallery is loaded 13576function updateURLWithGallerySlug(gallery) { 13577 if (!gallery) return; 13578 13579 // Keep bookmarkable /_manage URL â do not rewrite to / or /slug when loading galleries/pages 13580 const __hydraPath = window.location.pathname; 13581 if (__hydraPath === '/_manage' || __hydraPath === '/_manage/') { 13582 console.log('[updateURLWithGallerySlug] On /_manage editor path; skipping slug URL update.'); 13583 return; 13584 } 13585 13586 // Get the slug for this gallery/page 13587 let slug = gallery.slug; 13588 13589 if (!slug && gallery.title) { 13590 slug = gallery.title.toLowerCase().replace(/s+/g, '-').replace(/[^w-]+/g, '').replace(/--+/g, '-').trim(); 13591 } 13592 13593 // Check if we're in preview mode (add this new code) 13594 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 13595 13596 // Check if current history state already has this galleryId 13597 const currentState = window.history.state; 13598 const currentPath = window.location.pathname; 13599 const isAlreadyCurrentPage = currentState && currentState.galleryId === gallery.id && 13600 ((isPreviewMode && currentPath.includes(slug)) || (!isPreviewMode && currentPath === '/' + slug)); 13601 13602 if (isPreviewMode) { 13603 // Extract GUID from current URL path - first segment after domain 13604 const pathParts = window.location.pathname.split('/').filter(Boolean); 13605 const siteGuid = pathParts[0]; 13606 13607 // Create history entry if we have a valid GUID, and it's not already the current state. 13608 // Home pages use /{guid}/ (canonical preview home), matching live's '/' special-case. 13609 if (siteGuid && (gallery.isHomePage || slug)) { 13610 const targetPath = gallery.isHomePage ? ('/' + siteGuid + '/') : ('/' + siteGuid + '/' + slug); 13611 const alreadyAtPreviewHome = gallery.isHomePage && ( 13612 currentPath === '/' + siteGuid || currentPath === '/' + siteGuid + '/' 13613 ); 13614 // Only push if URL is different OR if state doesn't match (allows multiple navigations) 13615 if ((!alreadyAtPreviewHome && currentPath !== targetPath) || !isAlreadyCurrentPage) { 13616 console.log('ð [updateURLWithGallerySlug] Updating URL to ' + targetPath + ' (preview mode), galleryId: ' + gallery.id); 13617 13618 // Update browser URL without reloading, preserving the GUID 13619 window.history.pushState( 13620 { galleryId: gallery.id }, 13621 gallery.title || 'Gallery', 13622 targetPath 13623 ); 13624 13625 // Update page title 13626 document.title = formatGalleryDocumentTitle(gallery.title); 13627 } else { 13628 console.log('ð [updateURLWithGallerySlug] URL and state already match, skipping history update (preview mode)'); 13629 } 13630 } 13631 13632 return; // Exit early, we've handled the preview case 13633 } 13634 13635 // Regular (non-preview) URL handling 13636 if (slug) { 13637 // If this gallery is the designated home page, keep the URL as '/' (canonical home URL) 13638 const targetPath = gallery.isHomePage ? '/' : '/' + slug; 13639 // Only push if URL is different OR if state doesn't match (allows multiple navigations) 13640 if (currentPath !== targetPath || !isAlreadyCurrentPage) { 13641 console.log('ð [updateURLWithGallerySlug] Updating URL to ' + targetPath + ', galleryId: ' + gallery.id); 13642 window.history.pushState( 13643 { galleryId: gallery.id }, 13644 gallery.title || 'Gallery', 13645 targetPath 13646 ); 13647 document.title = formatGalleryDocumentTitle(gallery.title); 13648 } else { 13649 console.log('ð [updateURLWithGallerySlug] URL and state already match, skipping history update'); 13650 } 13651 } 13652} 13653 13654 13655function loadHomePage() { 13656 // Find a gallery marked as home page (regardless of visibility) 13657 const homePage = galleries.find(gallery => gallery.isHomePage === true); 13658 13659 if (homePage) {
13660 console.log(`Loading home page: ${homePage.title} (Visible: ${homePage.visible !== false})`); 13661 13662 // Update active gallery ID 13663 activeGalleryId = homePage.id; 13664 13665 // Add a special flag to indicate we're loading the home page 13666 // This allows us to bypass visibility checks 13667 window._loadingHomePage = true; 13668 13669 // Determine if this is a page or gallery and load appropriately 13670 if (homePage.isPage) { 13671 loadPage(homePage.id); 13672 } else { 13673 loadGallery(homePage.id); 13674 } 13675 13676 // Clear the flag after loading 13677 setTimeout(() => { 13678 window._loadingHomePage = false; 13679 }, 100); 13680 13681 // Update UI state 13682 updateActiveStates(); 13683 updateMobileTitle(); 13684 if (typeof closeMobileMenu === 'function') closeMobileMenu(); 13685 13686 return true; 13687 } 13688 13689 console.log('No home page defined'); 13690 return false; 13691} 13692 13693// 4. Update the handleURLNavigation function to check for home page 13694// Find the existing handleURLNavigation function and modify as follows: 13695// In index.ts 13696function handleURLNavigation() { 13697 const path = window.location.pathname.substring(1); // Remove leading slash 13698 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 13699 let slug = path; 13700 let siteGuid = null; 13701 13702 // /_manage is the Hydra editor entry â treat like site root for slug resolution (not a gallery slug) 13703 if (!isPreviewMode && (path === '_manage' || path.startsWith('_manage/'))) { 13704 console.log('[handleURLNavigation] /_manage detected â using root navigation behavior'); 13705 slug = ''; 13706 } 13707 13708 if (isPreviewMode && path) { 13709 const pathParts = path.split('/'); 13710 if (pathParts.length >= 1) { 13711 siteGuid = pathParts[0]; // Path could be just GUID/ or GUID/slug 13712 } 13713 if (pathParts.length >= 2) { 13714 slug = pathParts[1]; // Slug is the part after GUID 13715 console.log(`Preview URL detected, using slug: "${slug}" under GUID: "${siteGuid}"`); 13716 } else { 13717 // Only GUID is present, or path is empty after GUID 13718 slug = ''; // Treat as root/home for the given GUID 13719 console.log(`Preview URL with only GUID: "${siteGuid}", checking for home page or default.`); 13720 } 13721 } 13722 13723 if (!slug) { // Handles root path ('/') or preview URL with only GUID ('/GUID/') 13724 console.log("No specific slug in path (root or GUID-only preview), checking for home page."); 13725 if (!loadHomePage() && galleries.length > 0) { 13726 const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu); 13727 if (firstVisibleGallery) { 13728 console.log("No home page, loading first visible item:", firstVisibleGallery.title); 13729 if (firstVisibleGallery.isPage) { 13730 loadPage(firstVisibleGallery.id); 13731 } else { 13732 loadGallery(firstVisibleGallery.id); 13733 } 13734 } else { 13735 console.log("No home page and no visible items to load for this path."); 13736 } 13737 } 13738 } else { 13739 console.log(`Handling URL navigation for slug: "${slug}"` + (isPreviewMode ? ` (Preview GUID: ${siteGuid})` : "")); 13740 // findGalleryByPath should be able to find the item using the slug, 13741 // handling preview mode internally if necessary or by being passed the correct slug. 13742 const matchingGallery = findGalleryByPath(slug, isPreviewMode, siteGuid); 13743 13744 if (matchingGallery) { 13745 console.log('Loading content from URL: "' + matchingGallery.title + '" (ID: ' + matchingGallery.id + ')'); 13746 console.log('ð [handleURLNavigation] Gallery properties - isPage:', matchingGallery.isPage, 'pageId:', matchingGallery.pageId, 'has pageElements:', !!(matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements))); 13747 13748 if (matchingGallery.isSubmenu) { 13749 console.log('Gallery is a submenu, skipping direct load. Parent expansion might be needed.'); 13750 // Potentially, you might want to find and expand its parent here if the UI supports it. 13751 } else { 13752 // Set activeGalleryId *before* calling loadPage/loadGallery 13753 activeGalleryId = matchingGallery.id; 13754 window.activeGalleryId = matchingGallery.id; 13755 13756 // Determine if this is a page by checking multiple indicators 13757 // Check isPage flag, pageId, or pageElements array 13758 const isPage = matchingGallery.isPage === true || 13759 (matchingGallery.pageId && matchingGallery.pageId.startsWith('page_')) || 13760 (matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements) && matchingGallery.pageElements.length > 0); 13761 13762 if (isPage) { 13763 console.log('ð [handleURLNavigation] Detected as PAGE, loading page: "' + matchingGallery.title + '"'); 13764 const pageLoader = typeof window.loadPage === 'function' ? window.loadPage : loadPage; 13765 pageLoader(matchingGallery.id); 13766 } else { 13767 console.log('ð [handleURLNavigation] Detected as GALLERY, loading gallery: "' + matchingGallery.title + '"'); 13768 const galleryLoader = typeof window.loadGallery === 'function' ? window.loadGallery : loadGallery; 13769 galleryLoader(matchingGallery.id, null, { forceReload: true }); 13770 } 13771 } 13772 } else { 13773 console.log(`No matching gallery found for slug: "${slug}". Checking for home page as fallback.`); 13774 if (!loadHomePage()) { // Try loading home page 13775 const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu); 13776 if (firstVisibleGallery) { 13777 console.log(`Loading first visible gallery instead: "${firstVisibleGallery.title}"`); 13778 if (firstVisibleGallery.isPage) { 13779 loadPage(firstVisibleGallery.id); 13780 } else { 13781 loadGallery(firstVisibleGallery.id); 13782 } 13783 } else { 13784 console.log("No matching gallery, no home page, and no visible items to load."); 13785 // Optionally, display a 404 message in the gallery-container 13786 const galleryContainer = document.querySelector('.gallery-container'); 13787 if (galleryContainer) { 13788 galleryContainer.innerHTML = '<div>Page not found.</div>'; 13789 galleryContainer.style.opacity = '1'; 13790 } 13791 } 13792 } 13793 } 13794 } 13795 13796 // Update UI states after initiating load. 13797 // These functions should ideally use the now-set activeGalleryId. 13798 if (typeof updateActiveStates === 'function') updateActiveStates(); // For sidebar/tree 13799 if (typeof updateMobileTitle === 'function') updateMobileTitle(); // For mobile header 13800 13801 // **** ADDED FOR DEEP LINKING HORIZONTAL MENU **** 13802 // Determine current layout (it might not be set by MenuStyleCustomizer yet on direct load) 13803 let currentLayout = 'sidebar'; // Default assumption 13804 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) { 13805 currentLayout = window.MenuStyleCustomizer.settings.menuLayout; 13806 } else { 13807 // Fallback: check body class if MenuStyleCustomizer hasn't initialized settings 13808 if (document.body.classList.contains('menu-layout-horizontal')) { 13809 currentLayout = 'horizontal'; 13810 } 13811 } 13812 13813 console.log(`handleURLNavigation: Determined current layout as: ${currentLayout}`); 13814 13815 if (currentLayout === 'horizontal') { 13816 if (typeof updateActiveStatesHorizontal === 'function') { 13817 console.log("handleURLNavigation: Explicitly calling updateActiveStatesHorizontal for horizontal layout on direct navigation."); 13818 // It's possible menu items aren't rendered yet by renderHorizontalMenu if this is a very early call. 13819 // A small delay might be needed, or ensure renderHorizontalMenu has run. 13820 // For now, call it directly. If items aren't there, it won't do anything harmful. 13821 setTimeout(() => { // Add a slight delay to allow menu rendering 13822 updateActiveStatesHorizontal(); 13823 }, 100); // Adjust delay if needed, or find a more robust way to ensure menu is rendered. 13824 } else { 13825 console.warn("handleURLNavigation: updateActiveStatesHorizontal function not found for horizontal layout."); 13826 } 13827 } 13828} 13829 13830/** 13831* Enhanced loadPage function that properly cleans up gallery content 13832* @param {number} id - The page ID 13833* @param {Event} event - Optional event object 13834*/ 13835window.loadPage = function(id, event) { 13836 if (event && event.target && event.target.isConnected === false) { 13837 console.log('ð [loadPage] Ignoring navigation from detached element'); 13838 return; 13839 } 13840 if (event) { 13841 event.preventDefault(); 13842 event.stopPropagation(); 13843 const now = Date.now(); 13844 const lastCallTime = window._lastPageLoadTime || 0; 13845 window._lastPageLoadTime = now; 13846 if (now - lastCallTime < 100) {
13847 console.log('Ignoring duplicate loadPage call'); 13848 return; 13849 } 13850 } 13851 console.error('ð [loadPage] Loading page with gallery ID:', id); 13852 13853 stopAutoAdvanceTimer(); 13854 if (typeof dismissGalleryPasswordOverlay === 'function') { 13855 dismissGalleryPasswordOverlay(); 13856 } 13857 13858 let galleriesData = window.galleries || galleries; 13859 const gallery = findGalleryById(galleriesData, id); 13860 13861 if (!gallery) { 13862 console.warn('No gallery found with ID:', id, 'for page load.'); 13863 return; 13864 } 13865 13866 const contentLoadGeneration = beginHydraContentLoad('page', gallery.id, gallery.pageId || `page_${id}`); 13867 13868 window.activeGalleryId = id; 13869 activeGalleryId = id; 13870 window._activeContentType = 'page'; 13871 if (window.EnhancedGallerySystem) { 13872 window.EnhancedGallerySystem.currentGalleryId = null; 13873 } 13874 13875 if (typeof window.removeGalleryScriptsWithPause === 'function') { 13876 window.removeGalleryScriptsWithPause(); 13877 } 13878 13879 const galleryContainer = document.querySelector('.gallery-container'); 13880 if (galleryContainer) { 13881 // Clear the container but ensure it's visible 13882 galleryContainer.innerHTML = ''; 13883 galleryContainer.style.display = 'block'; 13884 galleryContainer.style.visibility = 'visible'; 13885 galleryContainer.style.opacity = '1'; 13886 console.log('ð [loadPage] Cleared and ensured gallery-container is visible'); 13887 } else { 13888 console.error('ð [loadPage] gallery-container not found!'); 13889 } 13890 13891 if (!gallery.pageId) gallery.pageId = `page_${id}`; 13892 if (!gallery.isPage) gallery.isPage = true; 13893 13894 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 13895 window.syncGalleryOptionsButtonVisibility(); 13896 } 13897 13898 if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {}; 13899 window._pageIdToGalleryId[gallery.pageId] = id; 13900 13901 const seedPageElementsOntoManager = function() { 13902 if (gallery.pageElements && Array.isArray(gallery.pageElements)) { 13903 if (window.PageManager && window.PageManager.elements) { 13904 window.PageManager.elements[gallery.pageId] = [...gallery.pageElements]; 13905 } 13906 } else if (window.PageManager && window.PageManager.elements && 13907 window.PageManager.elements[gallery.pageId] && 13908 window.PageManager.elements[gallery.pageId].length > 0) { 13909 gallery.pageElements = [...window.PageManager.elements[gallery.pageId]]; 13910 } 13911 }; 13912 seedPageElementsOntoManager(); 13913 13914 const invokePageManagerLoad = function() { 13915 seedPageElementsOntoManager(); 13916 try { 13917 console.error('ð [loadPage] Calling PageManager.loadPage for pageId:', gallery.pageId); 13918 console.error('ð [loadPage] Gallery container exists:', !!galleryContainer, 'Container HTML length:', galleryContainer ? galleryContainer.innerHTML.length : 0); 13919 console.error('ð [loadPage] Page elements count:', gallery.pageElements ? gallery.pageElements.length : 0); 13920 console.error('ð [loadPage] PageManager.elements[' + gallery.pageId + '] exists:', !!(window.PageManager.elements && window.PageManager.elements[gallery.pageId])); 13921 13922 // Small delay to ensure DOM is ready, especially when navigating back via history 13923 setTimeout(() => { 13924 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) { 13925 console.log('ð [loadPage] Skipping stale delayed PageManager load for:', gallery.pageId); 13926 return; 13927 } 13928 13929 // Verify container still exists 13930 const containerCheck = document.querySelector('.gallery-container'); 13931 console.error('ð [loadPage] Container check before loadPage:', !!containerCheck, 'Same as original:', containerCheck === galleryContainer); 13932 13933 // PageManager.loadPage is async - we need to properly handle it 13934 try { 13935 const loadPromise = window.PageManager.loadPage(gallery.pageId); 13936 console.error('ð [loadPage] PageManager.loadPage called, returned:', typeof loadPromise, 'is promise:', loadPromise && typeof loadPromise.then === 'function'); 13937 13938 if (loadPromise && typeof loadPromise.then === 'function') { 13939 loadPromise.then(() => { 13940 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) { 13941 console.log('ð [loadPage] Ignoring stale PageManager completion for:', gallery.pageId); 13942 return; 13943 } 13944 13945 console.error('ð [loadPage] PageManager.loadPage promise resolved'); 13946 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 13947 window.syncGalleryOptionsButtonVisibility(); 13948 } 13949 // Ensure container visibility after PageManager has initialized 13950 if (galleryContainer) { 13951 const pageContainer = galleryContainer.querySelector('.page-container'); 13952 if (pageContainer) { 13953 pageContainer.style.display = 'block'; 13954 pageContainer.style.visibility = 'visible'; 13955 pageContainer.style.opacity = '1'; 13956 const elementCount = pageContainer.querySelectorAll('.page-element').length; 13957 console.error('ð [loadPage] Page container found and made visible, has', elementCount, 'elements'); 13958 if (elementCount === 0) { 13959 console.error('ð [loadPage] WARNING: Page container exists but has no elements!'); 13960 console.error('ð [loadPage] PageManager.elements[' + gallery.pageId + ']:', window.PageManager.elements[gallery.pageId]); 13961 } 13962 } else { 13963 console.error('â [loadPage] Page container not found after PageManager.loadPage'); 13964 console.error('â [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500)); 13965 console.error('â [loadPage] Gallery container children:', galleryContainer.children.length); 13966 } 13967 } else { 13968 console.error('â [loadPage] Gallery container is null after PageManager.loadPage'); 13969 } 13970 }).catch((error) => { 13971 console.error('â [loadPage] Error in PageManager.loadPage promise:', error); 13972 console.error('â [loadPage] Error stack:', error.stack); 13973 }); 13974 } else {
13975 // If loadPage doesn't return a promise, check after a delay 13976 console.error('ð [loadPage] loadPage did not return a promise, checking after delay'); 13977 setTimeout(() => { 13978 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) { 13979 console.log('ð [loadPage] Ignoring stale non-promise PageManager completion for:', gallery.pageId); 13980 return; 13981 } 13982 if (galleryContainer) { 13983 const pageContainer = galleryContainer.querySelector('.page-container'); 13984 if (pageContainer) { 13985 pageContainer.style.display = 'block'; 13986 pageContainer.style.visibility = 'visible'; 13987 pageContainer.style.opacity = '1'; 13988 console.error('ð [loadPage] Ensured page-container is visible (non-promise path)'); 13989 } else { 13990 console.error('â [loadPage] Page container not found (non-promise path)'); 13991 console.error('â [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500)); 13992 } 13993 } 13994 }, 200); 13995 } 13996 } catch (loadError) { 13997 console.error('â [loadPage] Exception calling PageManager.loadPage:', loadError); 13998 console.error('â [loadPage] Error stack:', loadError.stack); 13999 } 14000 14001 const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false; 14002 if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') { 14003 window.PageManager.setEditMode(isCurrentlyEditing); 14004 } 14005 }, 50); // Small delay to ensure DOM is ready 14006 } catch (error) { 14007 console.error('â [loadPage] Error in loadPage function:', error); 14008 console.error('â [loadPage] Error stack:', error.stack); 14009 } 14010 }; 14011 14012 if (window.PageManager && typeof window.PageManager.loadPage === 'function') { 14013 invokePageManagerLoad(); 14014 } else { 14015 console.error('â [loadPage] PageManager not found yet â waiting for CDN proxy script'); 14016 console.error('â [loadPage] window.PageManager:', !!window.PageManager); 14017 console.error('â [loadPage] typeof loadPage:', window.PageManager ? typeof window.PageManager.loadPage : 'N/A'); 14018 whenPageManagerReady(function() { 14019 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) { 14020 console.log('ð [loadPage] Skipping stale PageManager ready retry for:', gallery.pageId); 14021 return; 14022 } 14023 console.log('â [loadPage] PageManager became available â retrying page render'); 14024 invokePageManagerLoad(); 14025 }); 14026 } 14027 14028 // Apply menu visibility 14029 const bodyEl = document.body; 14030 if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 14031 bodyEl.classList.add('menu-hidden-on-page'); 14032 } else { 14033 bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden 14034 } 14035 applyMenuOverlayClassForGallery(gallery); 14036 14037 if (typeof updateActiveStates === 'function') updateActiveStates(); 14038 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 14039 if (typeof closeMobileMenu === 'function') closeMobileMenu(); 14040 // Close the gallery options panel when navigating to a different page 14041 if (typeof window.closeOptionsPanel === 'function') { 14042 window.closeOptionsPanel(); 14043 } 14044 if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery); 14045 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 14046 window.syncGalleryOptionsButtonVisibility(); 14047 } 14048 if (typeof window !== 'undefined' && typeof document !== 'undefined') { 14049 window.dispatchEvent(new CustomEvent('page-loaded', { 14050 detail: { pageId: gallery.pageId, galleryId: id } 14051 })); 14052 } 14053} 14054 14055 14056 14057/** 14058* Ensures all required gallery styles are loaded 14059*/ 14060function ensureGalleryStylesLoaded() { 14061window.ensureGalleryStylesLoaded = ensureGalleryStylesLoaded; 14062// Use Worker route for gallery CSS (avoids CDN SSL issues) 14063const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; 14064const requiredStyles = [ 14065{ id: 'neon-gallery-css', href: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css' }, 14066{ id: 'quill-css', href: 'https://cdn.quilljs.com/1.3.6/quill.snow.css' } 14067]; 14068console.log('Loading gallery CSS from Worker:', galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css'); 14069
14070requiredStyles.forEach(style => { 14071if (!document.getElementById(style.id)) { 14072 const link = document.createElement('link'); 14073 link.id = style.id; 14074 link.rel = 'stylesheet'; 14075 link.href = style.href; 14076 if (style.href.includes('cdn.neonsky.app')) { 14077 link.onerror = function() { 14078 console.error('â Gallery CSS failed, retrying via proxy:', link.href); 14079 // Set flag if not already set 14080 if (!window.useCdnProxy) { 14081 window.useCdnProxy = true; 14082 console.error('ð© CDN_PROXY_FLAG SET: Gallery CSS failed, using proxy for all subsequent requests'); 14083 } 14084 const proxyUrl = link.href.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 14085 link.href = proxyUrl; 14086 link.onerror = function() { 14087 console.error('â Proxy failed, trying storage:', proxyUrl); 14088 link.href = link.href.replace('cdn.neonsky.app', 'storage.neonsky.app'); 14089 }; 14090 }; 14091 } 14092 document.head.appendChild(link); 14093 console.log(`Added gallery style: ${style.id}`); 14094} 14095}); 14096} 14097 14098 14099/** 14100* Ensures all required gallery scripts are loaded 14101* @returns {Promise} A promise that resolves when all scripts are loaded 14102*/ 14103function ensureGalleryScriptsLoaded() { 14104const requiredScripts = [ 14105{ id: 'crypto-js', src: 'https://cdn.jsdelivr.net/npm/[email protected]/crypto-js.min.js' }, 14106{ id: 'quill-js', src: 'https://cdn.quilljs.com/1.3.6/quill.min.js' }, 14107 { id: 'quill-integration', src: 'https://cdn.neonsky.app/quill-integration-v260923-002.js' } 14108]; 14109 14110const promises = requiredScripts.map(script => { 14111return new Promise((resolve, reject) => { 14112 // If script is already loaded, resolve immediately 14113 if (document.getElementById(script.id)) { 14114 resolve(); 14115 return; 14116 } 14117 14118 // Create and load the script 14119 const scriptElement = document.createElement('script'); 14120 scriptElement.id = script.id; 14121 scriptElement.src = script.src; 14122 scriptElement.onload = resolve; 14123 scriptElement.onerror = () => reject(new Error(`Failed to load script: ${script.src}`)); 14124 document.head.appendChild(scriptElement); 14125}); 14126}); 14127 14128return Promise.all(promises); 14129} 14130 14131/** 14132 * Creates a clean gallery context without using an iframe 14133 * @param {Object} gallery - The gallery object 14134 * @param {HTMLElement} container - The container to load the gallery into 14135 */ 14136function createGalleryContext(gallery, container) { 14137 console.log('Creating gallery context for:', gallery.title); 14138 14139 // CRITICAL: Ensure gallery CSS is loaded BEFORE loading the gallery script 14140 // This ensures styles are available even if NDJSON fails and falls back to classic JSON 14141 ensureGalleryStylesLoaded(); 14142 14143 // Generate the page alias safely 14144 const pageAlias = gallery.slug || gallery.title.toLowerCase().replace(/s+/g, '-'); 14145 14146 // Get our gallery options 14147 const galleryOptions = gallery.galleryOptions || {}; 14148 14149 // Get token data 14150 let authToken = null; 14151 // First try TokenManager if available 14152 if (window.TokenManager && typeof window.TokenManager.getToken === 'function') { 14153 authToken = window.TokenManager.getToken(); 14154 } 14155 // Then try global token 14156 else if (window.didToken) { 14157 authToken = window.didToken; 14158 } 14159 14160 // Set up Parameters for the gallery 14161 const galleryParameters = { 14162 SiteAlias: window.location.hostname, 14163 InitialPageUuid: gallery.pageId, 14164 InitialPageAlias: pageAlias, 14165 isInEditor: window.isEditing || false, 14166 siteId: window.siteId || '', 14167 isHydra: true 14168 }; 14169 14170 // If we have a token, add it 14171 if (authToken) { 14172 galleryParameters.hydraAuthToken = authToken; 14173 } 14174 14175 // Preserve original window.Parameters 14176 const originalParameters = window.Parameters; 14177 14178 // REMOVED: Loading indicator - no longer showing the loading animation 14179 14180 // Load the main gallery script - Use Worker route (avoids CDN SSL issues) 14181 const galleryScript = document.createElement('script'); 14182 const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; 14183 galleryScript.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; 14184 console.log('Loading gallery-main script from Worker:', galleryScript.src); 14185 galleryScript.onerror = function() { 14186 console.error('â Gallery script failed, retrying via proxy:', galleryScript.src); 14187 const proxyUrl = galleryScript.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 14188 galleryScript.src = proxyUrl; 14189 galleryScript.onerror = function() { 14190 console.error('â Proxy failed, trying storage:', proxyUrl); 14191 galleryScript.src = galleryScript.src.replace('cdn.neonsky.app', 'storage.neonsky.app'); 14192 }; 14193 }; 14194 14195 // Setup the gallery configuration 14196 const neonGalleryConfig = { 14197 useData: true, 14198 useCDN: true, 14199 version: 'live', 14200 manualCollectionName: "mod", 14201 layoutType: "grid", 14202 ...galleryOptions, 14203 siteId: window.siteId || '' 14204 }; 14205 14206 // Execute in a safe way that minimizes global namespace pollution 14207 try { 14208 // Set up the global variables needed by the gallery script 14209 window.Parameters = galleryParameters; 14210 window.neonGalleryConfig = neonGalleryConfig; 14211 14212 // Monitor for script load/error 14213 galleryScript.onload = () => { 14214 console.log('Gallery script loaded successfully for:', gallery.title); 14215 14216 // REMOVED: Don't need to remove the loading indicator since we don't add it 14217 }; 14218 14219 galleryScript.onerror = () => { 14220 console.error('Failed to load gallery script for:', gallery.title); 14221 //container.innerHTML = '<div class="gallery-error">Error loading gallery. Please try again later.</div>'; 14222 // Restore original parameters 14223 window.Parameters = originalParameters; 14224 }; 14225 14226 // Add the script to load the gallery 14227 document.body.appendChild(galleryScript); 14228 14229 // Set up a cleanup function that will be called when another gallery is loaded 14230 container.cleanup = () => { 14231 // Restore original parameters when cleaning up 14232 window.Parameters = originalParameters; 14233 14234 // Remove the gallery script to prevent conflicts 14235 galleryScript.remove(); 14236 14237 // Clear main gallery container 14238 if (container.parentNode) { 14239 container.innerHTML = ''; 14240 } 14241 }; 14242 } catch (error) { 14243 console.error('Error creating gallery context:', error); 14244 //container.innerHTML = '<div class="gallery-error">Error initializing gallery. Please try again later.</div>'; 14245 // Restore original parameters 14246 window.Parameters = originalParameters; 14247 } 14248} 14249 14250/** 14251* Only used for the PageManager's initializeDOM - ensures clean content swap 14252*/ 14253function cleanGalleryContainer() { 14254const galleryContainer = document.querySelector('.gallery-container'); 14255if (galleryContainer) { 14256galleryContainer.innerHTML = ''; 14257} 14258} 14259 14260 14261 14262/** 14263* Initializes the page using the PageManager 14264* To be used by PageManager.initializeDOM 14265*/ 14266function initPageContent(pageId) { 14267// First clear the container completely 14268cleanGalleryContainer(); 14269 14270// Then load the page content 14271if (window.PageManager && window.PageManager.loadPage) { 14272window.PageManager.loadPage(pageId); 14273} 14274} 14275 14276 14277/** 14278* Cleanup gallery content when changing between galleries or pages 14279*/ 14280function cleanupGalleryContent() { 14281// Get the gallery container 14282const galleryContainer = document.querySelector('.gallery-container'); 14283if (!galleryContainer) return; 14284 14285// Find the direct gallery content 14286const directContent = galleryContainer.querySelector('.gallery-direct-content'); 14287if (directContent && typeof directContent.cleanup === 'function') { 14288// Call the cleanup function 14289directContent.cleanup(); 14290} 14291 14292// Clear the container 14293galleryContainer.innerHTML = ''; 14294} 14295 14296 14297 14298 14299/** 14300* Simplified loadGallery function that follows the same content swap pattern 14301* @param {number} id - The gallery ID 14302* @param {Event} event - Optional event object 14303*/ 14304 14305// Override loadGallery to update URL 14306window.loadGallery = async function(id, event) { 14307 14308 14309// Stop event propagation if provided 14310if (event) { 14311 event.stopPropagation(); 14312} 14313 14314const galleryContainer = document.querySelector('.gallery-container'); 14315if (galleryContainer) { 14316 // Apply immediate hiding styles 14317 galleryContainer.style.opacity = '0'; 14318} 14319 14320console.log('Loading gallery with ID:', id); 14321 14322stopAutoAdvanceTimer();
14323if (typeof dismissGalleryPasswordOverlay === 'function') { 14324 dismissGalleryPasswordOverlay(); 14325} 14326 14327// Find gallery by ID 14328const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible 14329 14330if (!gallery) { 14331 console.warn('No gallery found with ID:', id); 14332 if (galleryContainer) { 14333 galleryContainer.style.opacity = '1'; 14334 } // Show container if gallery not found 14335 return; 14336} 14337 14338// Check if this is a page, if so use loadPage instead 14339if (isPageMenuItem(gallery)) { 14340 // Use the loadPage function defined in this scope (or window.loadPage if you prefer) 14341 return loadPage(id, event); 14342} 14343 14344// Skip submenu items 14345if (gallery.isSubmenu) { 14346 console.log('Gallery is submenu, skipping URL update'); 14347 toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available 14348 if (galleryContainer) { 14349 galleryContainer.style.opacity = '1'; 14350 } // Show container 14351 return; 14352} 14353 14354console.log('Found gallery:', gallery.title); 14355 14356window._activeContentType = 'gallery'; 14357beginHydraContentLoad('gallery', gallery.id, gallery.pageId); 14358if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { 14359 window.PageManager.clearCurrentPage(); 14360} 14361 14362// CRITICAL: Set active gallery ID consistently in both global and window scope 14363console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')'); 14364window.activeGalleryId = id; 14365activeGalleryId = id; // Ensure both variables are synchronized 14366 14367// Save reference to which gallery we're loading (for later verification) 14368window.currentLoadingGalleryId = id; 14369 14370// CRITICAL: Thorough cleanup with await to ensure completion 14371if (typeof window.removeGalleryScriptsWithPause === 'function') { 14372 await window.removeGalleryScriptsWithPause(); 14373} 14374 14375 14376// Clear all existing content from gallery container 14377if (!galleryContainer) { 14378 console.error('Gallery container not found'); 14379 return; 14380} 14381 14382// Force clear all content 14383galleryContainer.innerHTML = ''; 14384 14385 14386// CRITICAL: Double-check active gallery ID before updating UI 14387if (window.activeGalleryId !== id || activeGalleryId !== id) { 14388 console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id); 14389 window.activeGalleryId = id; 14390 activeGalleryId = id; 14391} 14392 14393// Apply menu visibility based on the gallery's setting and edit mode 14394const bodyEl = document.body; 14395if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 14396 bodyEl.classList.add('menu-hidden-on-page'); 14397} else { 14398 bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden 14399} 14400applyMenuOverlayClassForGallery(gallery); 14401 14402// Update UI state using explicit window function references or local fallbacks 14403if (typeof window.updateActiveStates === 'function') { 14404 window.updateActiveStates(); 14405} else if (typeof updateActiveStates === 'function') { 14406 updateActiveStates(); 14407} 14408 14409if (typeof window.updateMobileTitle === 'function') { 14410 window.updateMobileTitle(); 14411} else if (typeof updateMobileTitle === 'function') { 14412 updateMobileTitle(); 14413} 14414 14415if (typeof window.closeMobileMenu === 'function') { 14416 window.closeMobileMenu(); 14417} else if (typeof closeMobileMenu === 'function') { 14418 closeMobileMenu(); 14419} 14420 14421// Update URL with gallery slug 14422if (typeof window.updateURLWithGallerySlug === 'function') { 14423 window.updateURLWithGallerySlug(gallery); 14424} else if (typeof updateURLWithGallerySlug === 'function') { 14425 updateURLWithGallerySlug(gallery); 14426} 14427 14428 // Create gallery container and load content 14429const neonGalleryContainer = document.createElement('div'); 14430neonGalleryContainer.id = 'neon-gallery-container'; 14431neonGalleryContainer.className = 'gallery-direct-content'; 14432neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString()); 14433neonGalleryContainer.style.width = '100%'; 14434neonGalleryContainer.style.height = '100%'; 14435galleryContainer.appendChild(neonGalleryContainer); 14436 14437// Add a cache-busting parameter to ensure script is freshly loaded 14438const timestamp = Date.now(); 14439 14440// Set up gallery parameters 14441window.Parameters = { 14442 SiteAlias: window.location.hostname, 14443 InitialPageUuid: gallery.pageId, 14444 InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available 14445 isInEditor: window.isEditing || false, // Assuming window.isEditing is available 14446 siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available 14447 isHydra: true,
14448 galleryInstanceId: timestamp, 14449 // Added loadedGalleryId as per your original function 14450 loadedGalleryId: gallery.id 14451}; 14452 14453// Debug: Check what galleryOptions contains 14454console.error('ð INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL); 14455 14456// Set up gallery config 14457window.neonGalleryConfig = { 14458 useData: true, 14459 useCDN: true, 14460 version: 'live', 14461 manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod", 14462 layoutType: gallery.galleryOptions?.layoutType || "grid", 14463 // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence 14464 // This ensures that only properties from the current gallery are included, not from previous galleries 14465 ...(gallery.galleryOptions || {}), 14466 // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script 14467 // These fields are required for the gallery to determine if it should use Load Network racing 14468 loadTxId: gallery.galleryOptions?.loadTxId || null, 14469 loadPermaURL: gallery.galleryOptions?.loadPermaURL || null, 14470 // Cloudflare CDN NDJSON (unique id) â when present, gallery loads CDN-first (3s) then Fly 14471 cdnNdjsonId: gallery.galleryOptions?.cdnNdjsonId || null, 14472 cdnNdjsonUrl: gallery.galleryOptions?.cdnNdjsonUrl || null, 14473 cdnNdjsonStableUrl: gallery.galleryOptions?.cdnNdjsonStableUrl || gallery.galleryOptions?.embedPointerUrl || null, 14474 embedPointerUrl: gallery.galleryOptions?.embedPointerUrl || gallery.galleryOptions?.cdnNdjsonStableUrl || null, 14475 cdnSaveTimestamp: gallery.galleryOptions?.cdnSaveTimestamp || null, 14476 siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available 14477 // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread 14478 galleryId: gallery.id, 14479 galleryInstanceId: timestamp 14480}; 14481 14482console.error('ð INDEX.TS DEBUG - After spread, window.neonGalleryConfig.isPerma:', window.neonGalleryConfig.isPerma, 'permaURL:', window.neonGalleryConfig.permaURL, 'loadTxId:', window.neonGalleryConfig.loadTxId, 'loadPermaURL:', window.neonGalleryConfig.loadPermaURL, 'cdnNdjsonId:', window.neonGalleryConfig.cdnNdjsonId); 14483 14484// Create and add the gallery script with cache-busting 14485// Use Worker route (avoids CDN SSL issues) 14486const script = document.createElement('script'); 14487const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; 14488script.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; // Using versioned file 14489script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute 14490script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute 14491console.log('Loading gallery-main script from Worker:', script.src); 14492script.onerror = function() { 14493 console.error('â Gallery script failed, retrying via proxy:', script.src); 14494 const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 14495 script.src = proxyUrl; 14496 script.onerror = function() { 14497 console.error('â Proxy failed, trying storage:', proxyUrl); 14498 script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app'); 14499 }; 14500}; 14501 14502// Added script.onload from your original function 14503script.onload = function() { 14504 if (window.currentLoadingGalleryId !== gallery.id) { 14505 console.warn('Gallery ID mismatch! Expected:', gallery.id, 14506 'Current:', window.currentLoadingGalleryId); 14507 } 14508 console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`); 14509}; 14510 14511 14512if (galleryContainer) { 14513 setTimeout(() => { 14514 galleryContainer.style.opacity = '1'; 14515 }, 50); // Small delay to ensure content is ready 14516 } 14517 14518console.log(`Loading fresh gallery script for: ${gallery.title}`); 14519document.body.appendChild(script); 14520} 14521 14522// Function to ensure gallery container is visible 14523function ensureGalleryVisibility() { 14524 const galleryContainer = document.querySelector('.gallery-container'); 14525 if (galleryContainer && galleryContainer.style.opacity === '0') { 14526 console.log('ensureGalleryVisibility: Setting gallery container opacity to 1'); 14527 galleryContainer.style.opacity = '1'; 14528 } 14529} 14530 14531// Handle browser back/forward navigation 14532window.addEventListener('popstate', async function(event) { // Make it async 14533 if (window.__hydraPopstateLock) { 14534 return; 14535 } 14536 window.__hydraPopstateLock = true; 14537 try { 14538 console.error('ð [Popstate] Event triggered. State:', event.state, 'Current URL:', window.location.pathname); 14539 let galleryIdToLoad = null; 14540 let galleryToLoad = null; 14541 14542 // First, try to get galleryId from event.state 14543 if (event.state && event.state.galleryId) { 14544 galleryIdToLoad = event.state.galleryId; 14545 console.error('ð [Popstate] Found galleryId in event.state:', galleryIdToLoad); 14546 // Ensure galleries array is available and use findGalleryById helper 14547 const currentGalleries = window.galleries || galleries || []; 14548 galleryToLoad = findGalleryById(currentGalleries, galleryIdToLoad); 14549 } 14550 14551 if (window.__closingNestedLightbox) { 14552 window.__closingNestedLightbox = false;
14553 if (typeof updateActiveStates === 'function') updateActiveStates(); 14554 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 14555 return; 14556 } 14557 14558 if (window.lightboxNavigationStack?.length > 0 && 14559 typeof window.closeNestedGalleryLightbox === 'function') { 14560 const stackTop = window.lightboxNavigationStack[window.lightboxNavigationStack.length - 1]; 14561 const parentId = stackTop?.parentGalleryId; 14562 if (parentId && (event.state?.galleryId === parentId || galleryToLoad?.id === parentId)) { 14563 window.closeNestedGalleryLightbox({ fromPopstate: true }); 14564 if (typeof updateActiveStates === 'function') updateActiveStates(); 14565 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 14566 return; 14567 } 14568 } 14569 14570 if (typeof dismissGalleryPasswordOverlay === 'function') { 14571 dismissGalleryPasswordOverlay(); 14572 } 14573 14574 // If no gallery found from state, resolve from URL slug (handles history entries with null state) 14575 if (!galleryToLoad) { 14576 console.error('ð [Popstate] No galleryId in event.state or gallery not found, reading from URL:', window.location.pathname); 14577 const path = window.location.pathname.substring(1); 14578 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 14579 let slug = path; 14580 let siteGuid = null; 14581 if (!isPreviewMode && (path === '_manage' || path.startsWith('_manage/'))) { 14582 slug = ''; 14583 } 14584 if (isPreviewMode && path) { 14585 const pathParts = path.split('/'); 14586 if (pathParts.length >= 1) siteGuid = pathParts[0]; 14587 if (pathParts.length >= 2) slug = pathParts[1]; 14588 else slug = ''; 14589 } 14590 if (slug && typeof findGalleryByPath === 'function') { 14591 galleryToLoad = findGalleryByPath(slug, isPreviewMode, siteGuid); 14592 if (galleryToLoad) { 14593 console.error('ð [Popstate] Resolved from URL slug: "' + galleryToLoad.title + '" (ID: ' + galleryToLoad.id + ')'); 14594 } 14595 } 14596 } 14597 14598 // If we have a gallery from state or URL, load it 14599 let contentLoaded = false; 14600 if (galleryToLoad) { 14601 console.error('ð [Popstate] Found gallery in history: "' + galleryToLoad.title + '" (ID: ' + galleryToLoad.id + ')'); 14602 console.error('ð [Popstate] Gallery properties - isPage:', galleryToLoad.isPage, 'pageId:', galleryToLoad.pageId, 'has pageElements:', !!(galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements))); 14603 14604 activeGalleryId = galleryToLoad.id; 14605 window.activeGalleryId = galleryToLoad.id; 14606 14607 // Browser history navigation is explicit user intent â load content regardless of menu visibility. 14608 // Determine if this is a page by checking multiple indicators 14609 const isPage = galleryToLoad.isPage === true || 14610 (galleryToLoad.pageId && galleryToLoad.pageId.startsWith('page_')) || 14611 (galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements) && galleryToLoad.pageElements.length > 0); 14612 14613 if (isPage) { 14614 console.error('ð [Popstate] Detected as PAGE, loading page "' + galleryToLoad.title + '"'); 14615 const pageLoader = typeof window.loadPage === 'function' ? window.loadPage : loadPage; 14616 if (typeof pageLoader === 'function') { 14617 console.error('ð [Popstate] Calling loadPage for ID:', galleryToLoad.id); 14618 pageLoader(galleryToLoad.id); 14619 contentLoaded = true; 14620 } else { 14621 console.error('â [Popstate] loadPage function not available'); 14622 } 14623 } else { 14624 console.error('ð [Popstate] Detected as GALLERY, loading gallery "' + galleryToLoad.title + '"'); 14625 // Clear page state left over from prior page navigation 14626 if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { 14627 window.PageManager.clearCurrentPage(); 14628 } 14629 window._activeContentType = 'gallery'; 14630 const galleryLoader = typeof window.loadGallery === 'function' ? window.loadGallery : loadGallery; 14631 if (typeof galleryLoader === 'function') { 14632 await galleryLoader(galleryToLoad.id, null, { forceReload: true }); 14633 contentLoaded = true; 14634 } else { 14635 console.error('â [Popstate] loadGallery function not available'); 14636 } 14637 } 14638 } else { 14639 console.error('â [Popstate] No gallery found and handleURLNavigation not available'); 14640 } 14641 14642 // Update UI states after loading (or attempting to load) content 14643 if (typeof updateActiveStates === 'function') updateActiveStates(); 14644 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 14645 14646 // Ensure gallery visibility after popstate navigation 14647 setTimeout(ensureGalleryVisibility, 1000); 14648 14649 let currentLayout = 'sidebar'; 14650 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) { 14651 currentLayout = window.MenuStyleCustomizer.settings.menuLayout; 14652 } else if (document.body.classList.contains('menu-layout-horizontal')) { 14653 currentLayout = 'horizontal'; 14654 } 14655 14656 if (currentLayout === 'horizontal') { 14657 if (typeof updateActiveStatesHorizontal === 'function') { 14658 console.log("Popstate: Calling updateActiveStatesHorizontal for horizontal layout with a delay."); 14659 setTimeout(() => { 14660 console.log("[Delayed Update from Popstate] Calling updateActiveStatesHorizontal."); 14661 updateActiveStatesHorizontal(); 14662 }, 150); // Slightly longer delay for popstate, as full content might be re-initializing 14663 } 14664 } 14665 } finally { 14666 setTimeout(function() { window.__hydraPopstateLock = false; }, 300); 14667 } 14668}); 14669 14670// Check for direct URL access on page load (fallback when bootstrap did not route) 14671document.addEventListener('DOMContentLoaded', function() { 14672setTimeout(function() { 14673console.log('Checking for deep links after DOM content loaded'); 14674var __p = window.location.pathname; 14675if (__p !== '/' && __p !== '/_manage' && __p !== '/_manage/' && !window.__hydraBootstrapRouteAttempted) { 14676 handleURLNavigation(); 14677} 14678// Ensure gallery visibility after URL navigation 14679setTimeout(ensureGalleryVisibility, 1000); 14680}, 500); 14681}); 14682 14683document.addEventListener('page-save-requested', function(e) { 14684const data = e.detail; 14685if (data && data.pageId && data.elements) { 14686// Find the gallery/page that corresponds to this pageId 14687const page = galleries.find(g => g.pageId === data.pageId); 14688if (page) { 14689 // Store the page elements 14690 page.pageElements = data.elements; 14691 14692 // Save to server 14693 saveGalleries(); 14694} 14695} 14696}); 14697 14698document.addEventListener('DOMContentLoaded', function() { 14699function preloadRichTextEditor() { 14700 if (window.RichTextEditor && typeof window.RichTextEditor._ensureEditorDependencies === 'function') { 14701 console.log('Preloading Rich Text Editor dependencies'); 14702 return window.RichTextEditor._ensureEditorDependencies() 14703 .then(() => console.log('Rich Text Editor dependencies preloaded successfully')) 14704 .catch(error => console.warn('Failed to preload Rich Text Editor:', error)); 14705 } 14706 14707 if (window.PageManager && typeof window.PageManager._ensureRichTextEditor === 'function') { 14708 return window.PageManager._ensureRichTextEditor() 14709 .catch(error => console.warn('Failed to preload Rich Text Editor via PageManager:', error)); 14710 } 14711 14712 return null; 14713} 14714 14715preloadRichTextEditor(); 14716setTimeout(preloadRichTextEditor, 1000); 14717setTimeout(preloadRichTextEditor, 3000); 14718}); 14719 14720document.addEventListener('DOMContentLoaded', function() { 14721const submenuCheck = document.getElementById('createSubmenu'); 14722const submenuTitleField = document.getElementById('submenuTitle'); 14723const submenuTitleGroup = submenuTitleField?.parentElement; 14724 14725if (submenuCheck && submenuTitleGroup) { 14726submenuCheck.addEventListener('change', function() { 14727 submenuTitleGroup.style.display = this.checked ? 'block' : 'none'; 14728}); 14729} 14730}); 14731 14732// Also check immediately if document is already loaded (fallback only â avoid duplicate loadPage vs bootstrap) 14733if (document.readyState !== 'loading') { 14734setTimeout(function() { 14735console.log('Document already loaded, checking for deep links'); 14736var __p2 = window.location.pathname; 14737if (__p2 !== '/' && __p2 !== '/_manage' && __p2 !== '/_manage/' && !window.__hydraBootstrapRouteAttempted) { 14738 handleURLNavigation(); 14739} 14740}, 300); 14741} 14742 14743// ============================================ 14744// CONCURRENT EDIT CONFLICT DETECTION 14745// ============================================ 14746(function() { 14747 // Initialize local timestamp tracking 14748 window.lastKnownConfigTimestamp = null; 14749 let conflictPollInterval = null; 14750 14751 // Initialize timestamp on page load 14752 async function initializeConfigTimestamp() { 14753 try { 14754 const timestampUrl = typeof getApiUrl === 'function' ? 14755 getApiUrl('/api/config-timestamp') : 14756 '/api/config-timestamp'; 14757 console.error('[CONFLICT DETECT] Initializing config timestamp from:', timestampUrl); 14758 const response = await fetch(timestampUrl); 14759 if (response.ok) { 14760 const data = await response.json(); 14761 if (data.lastModified) { 14762 window.lastKnownConfigTimestamp = data.lastModified; 14763 console.error('[CONFLICT DETECT] Initialized config timestamp:', window.lastKnownConfigTimestamp); 14764 } else { 14765 console.error('[CONFLICT DETECT] No lastModified in response:', data); 14766 } 14767 } else { 14768 console.error('[CONFLICT DETECT] Failed to initialize timestamp, status:', response.status); 14769 } 14770 } catch (error) { 14771 console.error('[CONFLICT DETECT] Could not initialize config timestamp:', error); 14772 } 14773 } 14774
14775 // Check for conflicts by polling server 14776 async function checkForConflicts() { 14777 // Only poll when in edit mode 14778 if (!document.body.classList.contains('edit-mode-active')) { 14779 console.error('[CONFLICT DETECT] Skipping poll - not in edit mode'); 14780 return; 14781 } 14782 14783 try { 14784 const timestampUrl = typeof getApiUrl === 'function' ? 14785 getApiUrl('/api/config-timestamp') : 14786 '/api/config-timestamp'; 14787 console.error('[CONFLICT DETECT] Polling for conflicts from:', timestampUrl); 14788 console.error('[CONFLICT DETECT] Current local timestamp:', window.lastKnownConfigTimestamp); 14789 14790 const response = await fetch(timestampUrl); 14791 14792 if (response.ok) { 14793 const data = await response.json(); 14794 const serverTimestamp = data.lastModified; 14795 console.error('[CONFLICT DETECT] Server timestamp:', serverTimestamp); 14796 14797 // If we have a local timestamp and it differs from server, show conflict 14798 if (window.lastKnownConfigTimestamp !== null && 14799 serverTimestamp !== null && 14800 serverTimestamp !== window.lastKnownConfigTimestamp) { 14801 console.error('[CONFLICT DETECT] â ï¸ CONFLICT DETECTED! Local:', window.lastKnownConfigTimestamp, 'Server:', serverTimestamp); 14802 showConflictIndicator(); 14803 } else { 14804 console.error('[CONFLICT DETECT] No conflict - timestamps match or null'); 14805 hideConflictIndicator(); 14806 } 14807 } else { 14808 console.error('[CONFLICT DETECT] Poll failed with status:', response.status); 14809 } 14810 } catch (error) { 14811 console.error('[CONFLICT DETECT] Error checking for conflicts:', error); 14812 } 14813 } 14814 14815 // Show conflict indicator icon 14816 function showConflictIndicator() { 14817 const indicator = document.getElementById('conflictIndicator'); 14818 console.error('[CONFLICT DETECT] Showing conflict indicator, element found:', !!indicator); 14819 if (indicator) { 14820 indicator.style.display = 'flex'; 14821 indicator.style.visibility = 'visible'; 14822 indicator.style.opacity = '1'; 14823 indicator.style.background = '#d32f2f'; 14824 indicator.style.borderColor = '#d32f2f'; 14825 console.error('[CONFLICT DETECT] Conflict indicator displayed'); 14826 } else { 14827 console.error('[CONFLICT DETECT] â ï¸ Conflict indicator element not found in DOM!'); 14828 } 14829 } 14830 14831 // Hide conflict indicator icon 14832 function hideConflictIndicator() { 14833 const indicator = document.getElementById('conflictIndicator'); 14834 if (indicator) { 14835 indicator.style.display = 'none'; 14836 indicator.style.visibility = 'hidden'; 14837 indicator.style.opacity = '0'; 14838 } 14839 } 14840 14841 // Start/stop polling based on edit mode 14842 function updatePolling() { 14843 const isEditMode = document.body.classList.contains('edit-mode-active'); 14844 14845 // Always hide indicator when not in edit mode 14846 if (!isEditMode) { 14847 hideConflictIndicator(); 14848 } 14849 14850 if (isEditMode && !conflictPollInterval) { 14851 // Start polling every 10 seconds 14852 conflictPollInterval = setInterval(checkForConflicts, 10000); 14853 console.error('[CONFLICT DETECT] â Started conflict detection polling (every 10s)'); 14854 14855 // Also check immediately 14856 checkForConflicts(); 14857 } else if (!isEditMode && conflictPollInterval) { 14858 // Stop polling when not in edit mode 14859 clearInterval(conflictPollInterval); 14860 conflictPollInterval = null; 14861 hideConflictIndicator(); 14862 console.error('[CONFLICT DETECT] Stopped conflict detection polling'); 14863 } 14864 } 14865 14866 // Initialize on page load 14867 if (document.readyState === 'loading') { 14868 document.addEventListener('DOMContentLoaded', function() { 14869 initializeConfigTimestamp(); 14870 // Watch for edit mode changes 14871 const editModeObserver = new MutationObserver(updatePolling); 14872 editModeObserver.observe(document.body, { 14873 attributes: true, 14874 attributeFilter: ['class'] 14875 }); 14876 updatePolling(); 14877 }); 14878 } else { 14879 initializeConfigTimestamp(); 14880 // Watch for edit mode changes 14881 const editModeObserver = new MutationObserver(updatePolling); 14882 editModeObserver.observe(document.body, { 14883 attributes: true, 14884 attributeFilter: ['class'] 14885 }); 14886 updatePolling(); 14887 } 14888})(); 14889 14890</script>
14890 14891</head> 14892<body class="menu-layout-sidebar"> 14893 <a class="hydra-skip-link" href="#hydra-main-content">Skip to content</a> 14894<nav class="hydra-seo-nav" aria-label="Site navigation"> 14895 <ul> 14896 <li><a href="/in-the-same-space">In the Same Space</a></li> 14897 <li><a href="/book">Book</a></li> 14898 <li><a href="/selected-images">Selected images</a></li> 14899 <li><a href="/news">News</a></li> 14900 <li><a href="/latino-muslims">Latino Muslims</a></li> 14901 <li><a href="/refugee-camps-crisis-greece-nytimes">Refugee Camps Crisis Greece-NYTimes</a></li> 14902 <li><a href="/refugee-crisis-greece-wall-street-journal">Refugee Crisis Greece-Wall Street Journal</a></li> 14903 <li><a href="/soul-train-nytimes">Soul Train-NYTimes</a></li> 14904 <li><a href="/philly-fight-night-nytimes">Philly Fight Night-NYTimes</a></li> 14905 <li><a href="/the-decline-of-perama-shipping-yards--the-financial-times">The Decline of Perama Shipping Yards- The Financial Times</a></li> 14906 <li><a href="/centenarians-in-ikaria-the-guardian">Centenarians in Ikaria-The Guardian</a></li> 14907 <li><a href="/syrian-refugees-in-bulgaria-the-new-statesman">Syrian Refugees in Bulgaria-The New Statesman</a></li> 14908 <li><a href="/the-mermaid-madonna">The Mermaid Madonna</a></li> 14909 <li><a href="/party-like-there-is-no-tomorrow">Party Like There is No Tomorrow</a></li> 14910 <li><a href="/selected-tearsheets">Selected Tearsheets</a></li> 14911 <li><a href="/biography-awards">Biography-Awards</a></li> 14912 <li><a href="/nytimes-lens-blog-articles">NYTimes Lens Blog Articles</a></li> 14913 <li><a href="/contact-and-current-location">Contact and current location</a></li> 14914 </ul> 14915</nav> 14916<noscript class="hydra-gallery-noscript"> 14917 <article> 14918 <h1>Book | Eirini Vourloumis</h1> 14919 <div class="gallery-overview"><p style="line-height: 0.5; margin-bottom: 0.5em;"><span style="font-size: 14px;"><b>In Waiting</b></span></p><p style="line-height: 0.5;"> 14920</p><p style="margin-bottom: 0.5em; line-height: 0.5;"><strong style="font-size: 14px;">Published by Hatje Cantz</strong></p><p style="margin-bottom: 0.5em; line-height: 0.5;"><strong style="font-size: 14px;">Release Date,October 2017</strong></p><p style="margin-bottom: 0.5em; line-height: 0.5;"><span style="font-size: 14px;">Text(s) by James Estrin, Panos Kokkinias, Eirini Voulourmis</span></p><p style="margin-bottom: 0.5em; line-height: 0.5;"><span style="font-size: 14px;">100 pg, 42 ills.</span></p><p style="margin-bottom: 0.5em; line-height: 0.5;"><span style="font-size: 14px;">hardcover</span></p><p style="margin-bottom: 0.5em; line-height: 0.5;"><span style="font-size: 14px;">9.00 x 24.00 cm</span></p><p style="line-height: 0.5; margin-bottom: 0.5em;"><span style="font-size: 14px;">SBN 978-3-7757-4340-2</span></p><p style="line-height: 1;"> 14921</p><p style="line-height: 1; margin-bottom: 0.5em;"><span style="font-size: 14px;">âIn Waiting,â is an examination of Athens and its role as a physical stage for the economic crisis. Primarily, I am exploring the interiors of government buildings, institutions, and schools to question how these spaces reflect modern Greek culture and character. The locations I chose are familiar microcosms for Athenians that have now become the public stage for a forced re-negotiation of Greek identity. I am attempting to personally rationalize how the deceptively tedious components of urban life factor into the economic crisis. I am specifically attracted to the particular aesthetic and surreal atmosphere I encounter in these spaces. These interiors also reveal an almost antiquated and bare aesthetic quality; they evoke in their deÌcor a feeling of dependency on and idolization of the past. Spaces, which once seemed banal or unimportant, now reveal nuances of Greek reality and have social and political impl
14921ications. Like the Greek people, these places exist in anticipation of their future.</span></p></div> 14922 <ul class="gallery-images"> 14923 <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW1.jpg" alt="IW1" loading="lazy"></li> 14924 <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW2.jpg" alt="IW2" loading="lazy"></li> 14925 <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW3.jpg" alt="IW3" loading="lazy"></li> 14926 <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW4.jpg" alt="IW4" loading="lazy"></li> 14927 <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW5.jpg" alt="IW5" loading="lazy"></li> 14928 <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW6.jpg" alt="IW6" loading="lazy"></li> 14929 <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW7.jpg" alt="IW7" loading="lazy"></li> 14930 <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW8.jpg" alt="IW8" loading="lazy"></li> 14931 <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW9.jpg" alt="IW9" loading="lazy"></li> 14932 <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW10.jpg" alt="IW10" loading="lazy"></li> 14933 <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW11.jpg" alt="IW11" loading="lazy"></li> 14934 <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW12.jpg" alt="IW12" loading="lazy"></li> 14935 <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW13.jpg" alt="IW13" loading="lazy"></li> 14936 <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW14.jpg" alt="IW14" loading="lazy"></li> 14937 <li><img src="https://cdn.neonsky.app/4bd5ec0aa36d9/images/IW15.jpg" alt="IW15" loading="lazy"></li> 14938 </ul> 14939 <p><a href="https://eiriniphoto.com/book">View full gallery</a></p> 14940 </article> 14941</noscript> 14942 14943 <div class="global-header-controls"> 14944 14945 <button id="editButton" class="btn-icon btn-primary" onclick="toggleEditMode()" style="display: none;" data-help-id="edit-toggle"> 14946 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 14947 <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"> 14948 <path d="m16.214 4.982l1.402-1.401a1.982 1.982 0 0 1 2.803 2.803l-1.401 1.402m-2.804-2.804l-5.234 5.234c-1.045 1.046-1.568 1.568-1.924 2.205S8.342 14.561 8 16c1.438-.342 2.942-.7 3.579-1.056s1.16-.879 2.205-1.924l5.234-5.234m-2.804-2.804l2.804 2.804"/> 14949 <path d="M21 12c0 4.243 0 6.364-1.318 7.682S16.242 21 12 21s-6.364 0-7.682-1.318S3 16.242 3 12s0-6.364 1.318-7.682S7.758 3 12 3"/> 14950 </g> 14951 </svg> 14952 </button> 14953 <button id="conflictIndicator" class="btn-icon btn-primary" onclick="window.location.reload()" style="display: none;" data-custom-tooltip="Changes detected in another window" data-tooltip-position="right"> 14954 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="color: white;"> 14955 <path fill="currentColor" d="M2 12c0 5 4 9 9 9c2.4 0 4.7-.9 6.4-2.6l-1.5-1.5c-1.3 1.4-3 2.1-4.9 2.1c-6.2 0-9.4-7.5-4.9-11.9S18 5.8 18 12h-3l4 4h.1l3.9-4h-3c0-5-4-9-9-9s-9 4-9 9m8 3h2v2h-2zm0-8h2v6h-2z"/> 14956 </svg> 14957 </button> 14958 </div> 14959 14960 <div class="container"> 14961 <div class="mobile-header"> 14962 <div class="mobile-header-content"> 14963 <div class="horizontal-header-logo"></div> 14964 <div class="horizontal-menu-container"></div> 14965 </div> 14966 <button id="hamburgerBtn" class="hamburger-btn" type="button" aria-label="Menu" aria-expanded="false" aria-controls="siteSidebar"> 14967 <span class="hamburger-icon" aria-hidden="true"> 14968 <span class="hamburger-line"></span> 14969 <span class="hamburger-line"></span> 14970 <span class="hamburger-line"></span> 14971 </span> 14972 </button> 14973 </div> 14974 <div id="mobileBackdrop" class="mobile-backdrop"></div> 14975 14976 <div class="sidebar" id="siteSidebar" role="navigation" aria-label="Site"> 14977 <div class="sidebar-header"> 14978</div> 14979 14980 14981 <div class="edit-controls" data-help-id="edit-toolbar"> 14982 <button type="button" class="btn-icon btn-primary" onclick="openHydraHelp()" data-custom-tooltip="Help" data-help-id="help-toolbar"> 14983 <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> 14984 <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.195 8.765A4 4 0 1 1 12 14v1m.05 4v.1h-.1V19z" /> 14985 </svg> 14986 </button> 14987 <button class="btn-icon btn-primary" onclick="toggleStyleEditor(undefined, event)" data-custom-tooltip="Edit Style" data-help-id="style-toolbar"> 14988 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 14989 <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m19 12.13l-6.061 6.077c-1.783 1.788-2.675 2.682-3.77 2.78q-.27.025-.543 0c-1.094-.098-1.986-.992-3.769-2.78l-2.02-2.026a2.87 2.87 0 0 1 0-4.052m16.163 0l-8.082-8.103M19 12.129H2.837m8.081-8.103l-8.081 8.103m8.081-8.103L8.898 2M22 20a2 2 0 1 1-4 0c0-1.105 2-3 2-3s2 1.895 2 3" color="currentColor"/> 14990 </svg> 14991 </button> 14992 <button class="btn-icon btn-primary" onclick="toggleAddForm()" data-custom-tooltip="Add"> 14993 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 14994 <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"> 14995 <path d="M13 2h.273c3.26 0 4.892 0 6.024.798c.324.228.612.5.855.805c.848 1.066.848 2.6.848 5.67v2.545c0 2.963 0 4.445-.469 5.628c-.754 1.903-2.348 3.403-4.37 4.113c-1.257.441-2.83.441-5.98.441c-1.798 0-2.698 0-3.416-.252c-1.155-.406-2.066-1.263-2.497-2.35C4 18.722 4 17.875 4 16.182V12"/> 14996 <path d="M21 12a3.333 3.333 0 0 1-3.333 3.333c-.666 0-1.451-.116-2.098.057a1.67 1.67 0 0 0-1.179 1.179c-.173.647-.057 1.432-.057 2.098A3.333 3.333 0 0 1 11 22m0-16H3m4-4v8"/> 14997 </g> 14998 </svg> 14999 </button> 15000 <button class="btn-icon btn-primary" onclick="toggleSidebarElementForm(
15000)" data-custom-tooltip="Add Element"> 15001 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 15002 <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M18 2v8m4-4h-8M2 6c0-1.4 0-2.1.272-2.635a2.5 2.5 0 0 1 1.093-1.093C3.9 2 4.6 2 6 2s2.1 0 2.635.272a2.5 2.5 0 0 1 1.093 1.093C10 3.9 10 4.6 10 6s0 2.1-.272 2.635a2.5 2.5 0 0 1-1.093 1.093C8.1 10 7.4 10 6 10s-2.1 0-2.635-.272a2.5 2.5 0 0 1-1.093-1.093C2 8.1 2 7.4 2 6m0 12c0-1.4 0-2.1.272-2.635a2.5 2.5 0 0 1 1.093-1.092C3.9 14 4.6 14 6 14s2.1 0 2.635.273a2.5 2.5 0 0 1 1.093 1.092C10 15.9 10 16.6 10 18s0 2.1-.272 2.635a2.5 2.5 0 0 1-1.093 1.092C8.1 22 7.4 22 6 22s-2.1 0-2.635-.273a2.5 2.5 0 0 1-1.093-1.092C2 20.1 2 19.4 2 18m12 0c0-1.4 0-2.1.273-2.635a2.5 2.5 0 0 1 1.092-1.092C15.9 14 16.6 14 18 14s2.1 0 2.635.273a2.5 2.5 0 0 1 1.092 1.092C22 15.9 22 16.6 22 18s0 2.1-.273 2.635a2.5 2.5 0 0 1-1.092 1.092C20.1 22 19.4 22 18 22s-2.1 0-2.635-.273a2.5 2.5 0 0 1-1.092-1.092C14 20.1 14 19.4 14 18" color="currentColor"/> 15003 </svg> 15004 </button> 15005<button class="btn-icon btn-primary" onclick="toggleMetadataEditor()" data-custom-tooltip="Site Metadata"> 15006<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"><path d="M7.5 4.945H16a1.5 1.5 0 0 1 1.5 1.5v1.5m-2.5 5H9m3 4H9"/><path d="M18.497 2H6.307c-.496 0-1.005.073-1.406.368c-1.274.935-2.256 3.02-.273 4.903c.556.528 1.334.72 2.099.72h11.557c.793 0 2.216.113 2.216 2.536v7.454c0 2.22-1.79 4.019-3.997 4.019h-9.03c-2.204 0-3.807-1.557-3.933-3.929L3.506 5.166"/></g></svg> 15007</button> 15008<button id="logoutButton" class="btn-icon btn-primary" onclick="logout()" data-custom-tooltip="Log Out" style="display: none;"> 15009 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 15010 <path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m11 3l-.663.234c-2.578.91-3.868 1.365-4.602 2.403S5 8.043 5 10.777v2.445c0 2.735 0 4.102.735 5.14c.734 1.039 2.024 1.494 4.602 2.404L11 21m10-9H11m10 0c0-.7-1.994-2.008-2.5-2.5M21 12c0 .7-1.994 2.008-2.5 2.5" color="currentColor"/> 15011 </svg> 15012 </button> 15013
15013<script> 15014 15015// Initialize mobile menu functionality 15016function initMobileMenu() { 15017 const hamburgerBtn = document.getElementById('hamburgerBtn'); 15018 const sidebar = document.querySelector('.sidebar'); 15019 const mobileBackdrop = document.getElementById('mobileBackdrop'); 15020 15021 // --- ADDED CHECK: Only update header if layout is NOT horizontal --- 15022 let currentLayout = 'sidebar'; // Default 15023 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 15024 currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; 15025 } 15026 15027 // --- END ADDED CHECK --- 15028 15029 // Ensure backdrop is properly hidden for horizontal layout on page load 15030 if (mobileBackdrop && document.body.classList.contains('menu-layout-horizontal')) { 15031 mobileBackdrop.classList.remove('mobile-open'); 15032 mobileBackdrop.classList.remove('active'); 15033 // Clear any inline overrides - CSS controls backdrop visibility via .mobile-open 15034 mobileBackdrop.style.display = ''; 15035 mobileBackdrop.style.pointerEvents = ''; 15036 mobileBackdrop.style.opacity = ''; 15037 mobileBackdrop.style.visibility = ''; 15038 } 15039 15040 // Toggle mobile menu when hamburger is clicked 15041 hamburgerBtn.addEventListener('click', function(e) { 15042 e.stopPropagation(); 15043 toggleMobileMenu(); // Assumes this function exists 15044 }); 15045 15046 // Close mobile menu when backdrop is clicked 15047 if (mobileBackdrop) { 15048 mobileBackdrop.addEventListener('click', function() { 15049 closeMobileMenu(); // Assumes this function exists 15050 }); 15051 } 15052 15053 // Escape closes mobile menu (keyboard / SR) 15054 document.addEventListener('keydown', function(e) { 15055 if (e.key !== 'Escape') return; 15056 const sb = document.querySelector('.sidebar'); 15057 if (sb && sb.classList.contains('mobile-open') && typeof closeMobileMenu === 'function') { 15058 closeMobileMenu(); 15059 } 15060 }); 15061 15062 // Handle window resize events to ensure proper state 15063 window.addEventListener('resize', function() { 15064 // If we're not on mobile anymore, remove mobile-open class 15065 if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) { 15066 closeMobileMenu(); // Assumes this function exists 15067 } 15068 }); 15069} 15070 15071document.addEventListener('keydown', function(e) { 15072 // Check for caret (Shift+6) 15073 if (e.key === '^' || (e.key === '6' && e.shiftKey)) { 15074 // Only work in edit mode 15075 if (window.isInEditMode && window.isInEditMode()) { 15076 console.log('Import shortcut detected'); 15077 toggleImportClassicForm(); 15078 e.preventDefault(); 15079 } 15080 } 15081}); 15082 15083// Enhanced tooltip management with automatic timeouts 15084 15085// Store active tooltip timeouts by ID 15086window.tooltipTimeouts = {}; 15087 15088function showTooltip(trigger) { 15089 const tooltipText = trigger.getAttribute('data-custom-tooltip'); 15090 if (!tooltipText) return; 15091 15092 // Get or create the main tooltip container 15093 let container = document.getElementById('tooltip-container'); 15094 if (!container) { 15095 console.log("Tooltip container not found, creating one."); 15096 container = document.createElement('div'); // Assign to 'container' 15097 container.id = 'tooltip-container'; 15098 // CSS for #tooltip-container should make it position:fixed, top:0, left:0, pointer-events:none, etc. 15099 document.body.appendChild(container); 15100 } 15101 15102 const tooltipId = `tooltip-${trigger.id || Math.random().toString(36).substr(2, 9)}`; 15103 15104 // Remove any existing tooltip for this trigger 15105 const existingTooltip = document.getElementById(tooltipId); 15106 if (existingTooltip) { 15107 existingTooltip.remove(); 15108 } 15109 15110 // Clear any existing timeout for this tooltip 15111 if (!window.tooltipTimeouts) { 15112 window.tooltipTimeouts = {}; // Initialize if it doesn't exist 15113 } 15114 15115 if (window.tooltipTimeouts[tooltipId]) { 15116 clearTimeout(window.tooltipTimeouts[tooltipId]); 15117 delete window.tooltipTimeouts[tooltipId]; 15118 } 15119 15120 // Create tooltip element 15121 const tooltip = document.createElement('div'); 15122 tooltip.className = 'tooltip'; // Ensure your CSS styles .tooltip 15123 tooltip.id = tooltipId; 15124 tooltip.textContent = tooltipText; 15125 15126 // Add to container (now 'container' is guaranteed to be an element) 15127 container.appendChild(tooltip); 15128 15129 // Position tooltip 15130 if (typeof positionTooltip === 'function') { 15131 positionTooltip(tooltip, trigger); 15132 } else { 15133 console.error("positionTooltip function is not defined."); 15134 tooltip.remove(); // Clean up if we can't position 15135 return; 15136 } 15137 15138 // Show with animation (slight delay for CSS transition) 15139 setTimeout(() => { 15140 tooltip.style.opacity = '1'; 15141 // The transform for animation should be handled by CSS transitions 15142 // e.g., .tooltip.visible { transform: translateX(-50%) translateY(-5px); } 15143 // Or ensure positionTooltip sets final transform if it needs to override CSS. 15144 // For now, assuming CSS handles the animated transform from its initial state. 15145 }, 10); 15146 15147 trigger.setAttribute('data-active-tooltip', tooltipId); 15148 15149 // Set automatic timeout to hide after 3 seconds 15150 window.tooltipTimeouts[tooltipId] = setTimeout(() => { 15151 hideTooltip(trigger); // Ensure hideTooltip function exists 15152 }, 3000); 15153 15154 // Also set up a global timer to ensure all tooltips are dismissed 15155 if (window.globalTooltipTimer) { 15156 clearTimeout(window.globalTooltipTimer); 15157 } 15158 window.globalTooltipTimer = setTimeout(() => { 15159 // Force hide all visible tooltips after 3 seconds 15160 const allTooltips = document.querySelectorAll('.tooltip'); 15161 allTooltips.forEach(tooltip => { 15162 if (tooltip.style.opacity !== '0') { 15163 tooltip.style.opacity = '0'; 15164 setTimeout(() => { 15165 if (tooltip.parentNode) { 15166 tooltip.parentNode.removeChild(tooltip); 15167 } 15168 }, 200); 15169 } 15170 }); 15171 // Clear all timeout references 15172 if (window.tooltipTimeouts) { 15173 for (const id in window.tooltipTimeouts) { 15174 clearTimeout(window.tooltipTimeouts[id]); 15175 } 15176 window.tooltipTimeouts = {}; 15177 } 15178 }, 3000); 15179} 15180 15181// Function to hide a tooltip 15182function hideTooltip(trigger) { 15183 const tooltipId = trigger.getAttribute('data-active-tooltip'); 15184 if (!tooltipId) return; 15185 15186 const tooltip = document.getElementById(tooltipId); 15187 if (tooltip) { 15188 // Fade out 15189 tooltip.style.opacity = '0'; 15190 tooltip.style.transform = 'translateX(-50%) translateY(0)'; 15191 15192 // Remove after animation 15193 setTimeout(() => { 15194 if (tooltip.parentNode) { 15195 tooltip.parentNode.removeChild(tooltip); 15196 } 15197 }, 200); 15198 } 15199 15200 // Clear any active timeout for this tooltip 15201 if (window.tooltipTimeouts && window.tooltipTimeouts[tooltipId]) { 15202 clearTimeout(window.tooltipTimeouts[tooltipId]); 15203 delete window.tooltipTimeouts[tooltipId]; 15204 } 15205 15206 // Clear reference 15207 trigger.removeAttribute('data-active-tooltip'); 15208 15209 // If this was the last tooltip, clear the global timer 15210 const remainingTooltips = document.querySelectorAll('.tooltip'); 15211 if (remainingTooltips.length === 0 && window.globalTooltipTimer) { 15212 clearTimeout(window.globalTooltipTimer); 15213 window.globalTooltipTimer = null; 15214 } 15215} 15216 15217// Function to initialize tooltips across the document 15218function initTooltipSystem() { 15219 // Check if container already exists 15220 let tooltipContainer = document.getElementById('tooltip-container'); 15221 15222 // Create it if it doesn't exist 15223 if (!tooltipContainer) { 15224 tooltipContainer = document.createElement('div'); 15225 tooltipContainer.id = 'tooltip-container'; 15226 document.body.appendChild(tooltipContainer); 15227 } 15228 15229 // Find all elements with data-custom-tooltip attribute 15230 const tooltipTriggers = document.querySelectorAll('[data-custom-tooltip]'); 15231 15232 // Add event listeners to each trigger
15233 tooltipTriggers.forEach(trigger => { 15234 // Skip if already initialized 15235 if (trigger.hasAttribute('data-custom-tooltip-initialized')) return; 15236 15237 // Mark as initialized 15238 trigger.setAttribute('data-custom-tooltip-initialized', 'true'); 15239 15240 // MouseEnter: Show tooltip 15241 trigger.addEventListener('mouseenter', () => { 15242 showTooltip(trigger); 15243 }); 15244 15245 // MouseLeave: Hide tooltip 15246 trigger.addEventListener('mouseleave', () => { 15247 hideTooltip(trigger); 15248 }); 15249 15250 // Click: Hide tooltip immediately 15251 trigger.addEventListener('click', () => { 15252 hideTooltip(trigger); 15253 }); 15254 15255 // Also handle focus/blur for accessibility 15256 trigger.addEventListener('focus', () => { 15257 showTooltip(trigger); 15258 }); 15259 15260 trigger.addEventListener('blur', () => { 15261 hideTooltip(trigger); 15262 }); 15263 }); 15264 15265 // Set up a failsafe function to clear any orphaned tooltips 15266 window.clearAllTooltips = function() { 15267 const container = document.getElementById('tooltip-container'); 15268 if (container) { 15269 container.innerHTML = ''; 15270 } 15271 15272 // Clear all timeouts 15273 if (window.tooltipTimeouts) { 15274 for (const id in window.tooltipTimeouts) { 15275 clearTimeout(window.tooltipTimeouts[id]); 15276 } 15277 window.tooltipTimeouts = {}; 15278 } 15279 15280 // Clear global timer 15281 if (window.globalTooltipTimer) { 15282 clearTimeout(window.globalTooltipTimer); 15283 window.globalTooltipTimer = null; 15284 } 15285 15286 // Clear all data-active-tooltip attributes 15287 document.querySelectorAll('[data-active-tooltip]').forEach(el => { 15288 el.removeAttribute('data-active-tooltip'); 15289 }); 15290 }; 15291 15292 // Initialize new tooltips when DOM changes 15293 setupMutationObserver(); 15294 15295 // Add a global function to manually dismiss all tooltips (for testing) 15296 window.dismissAllTooltips = function() { 15297 const allTooltips = document.querySelectorAll('.tooltip'); 15298 allTooltips.forEach(tooltip => { 15299 if (tooltip.style.opacity !== '0') { 15300 tooltip.style.opacity = '0'; 15301 setTimeout(() => { 15302 if (tooltip.parentNode) { 15303 tooltip.parentNode.removeChild(tooltip); 15304 } 15305 }, 200); 15306 } 15307 }); 15308 15309 // Clear all timeout references 15310 if (window.tooltipTimeouts) { 15311 for (const id in window.tooltipTimeouts) { 15312 clearTimeout(window.tooltipTimeouts[id]); 15313 } 15314 window.tooltipTimeouts = {}; 15315 } 15316 15317 // Clear global timer 15318 if (window.globalTooltipTimer) { 15319 clearTimeout(window.globalTooltipTimer); 15320 window.globalTooltipTimer = null; 15321 } 15322 15323 // Clear all data-active-tooltip attributes 15324 document.querySelectorAll('[data-active-tooltip]').forEach(el => { 15325 el.removeAttribute('data-active-tooltip'); 15326 }); 15327 }; 15328} 15329 15330function positionTooltip(tooltip, trigger) { 15331 const triggerRect = trigger.getBoundingClientRect(); // Viewport-relative coordinates 15332 15333 // To accurately get tooltip dimensions, temporarily make it visible but off-screen. 15334 // The #tooltip-container is position:fixed, so the tooltip's absolute positioning 15335 // will be relative to the viewport. 15336 tooltip.style.visibility = 'hidden'; 15337 tooltip.style.display = 'block'; // Or 'inline-block' if that's its intended final display 15338 // No need to change position to 'absolute' here if its CSS is already 'position: absolute;' 15339 // within the 'fixed' #tooltip-container. 15340 15341 const tooltipHeight = tooltip.offsetHeight; 15342 const tooltipWidth = tooltip.offsetWidth; 15343 15344 // Restore visibility; opacity transition will handle the fade-in. 15345 tooltip.style.visibility = ''; 15346 // tooltip.style.display = ''; // Let CSS manage display if opacity is used for showing/hiding 15347 15348 // Check if tooltip should be positioned to the right (for conflict indicator) 15349 const tooltipPosition = trigger.getAttribute('data-tooltip-position'); 15350 if (tooltipPosition === 'right') { 15351 // Position to the right of the trigger 15352 const viewportWidth = window.innerWidth; 15353 const viewportHeight = window.innerHeight; 15354 let left = triggerRect.right + 10; // 10px gap from the right edge of trigger 15355 let top = triggerRect.top + (triggerRect.height / 2) - (tooltipHeight / 2); // Vertically centered 15356 15357 // Recalculate tooltip dimensions after allowing text wrap 15358 tooltip.style.maxWidth = '200px'; 15359 tooltip.style.whiteSpace = 'normal'; 15360 tooltip.style.wordWrap = 'break-word'; 15361 const tooltipWidthAfterWrap = tooltip.offsetWidth; 15362 const tooltipHeightAfterWrap = tooltip.offsetHeight; 15363 15364 // If tooltip goes off-screen to the right, try positioning to the left of the button 15365 if (left + tooltipWidthAfterWrap > viewportWidth - 5) { 15366 left = triggerRect.left - tooltipWidthAfterWrap - 10; // Position to the left of button 15367 // If it still goes off the left edge, position it at the left edge with padding 15368 if (left < 5) { 15369 left = 5; // Keep it at least 5px from the left edge 15370 // The tooltip will wrap to fit within the available space 15371 tooltip.style.maxWidth = `${viewportWidth - left - 10}px`; 15372 } 15373 } else {
15374 // Tooltip fits on the right, but make sure it doesn't go off-screen 15375 if (left + tooltipWidthAfterWrap > viewportWidth - 5) { 15376 tooltip.style.maxWidth = `${viewportWidth - left - 10}px`; 15377 } 15378 } 15379 15380 // Recalculate after max-width adjustment 15381 const finalTooltipWidth = tooltip.offsetWidth; 15382 const finalTooltipHeight = tooltip.offsetHeight; 15383 15384 // Ensure tooltip doesn't go off-screen on the right side 15385 if (left + finalTooltipWidth > viewportWidth - 5) { 15386 left = viewportWidth - finalTooltipWidth - 5; 15387 } 15388 15389 // Adjust if tooltip goes off-screen vertically 15390 if (top < 5) { 15391 top = 5; 15392 } else if (top + finalTooltipHeight > viewportHeight - 5) { 15393 top = viewportHeight - finalTooltipHeight - 5; 15394 } 15395 15396 tooltip.style.left = `${left}px`; 15397 tooltip.style.top = `${top}px`; 15398 tooltip.classList.remove('tooltip-bottom'); // No arrow needed for side positioning 15399 return; 15400 } 15401 15402 // Calculate desired horizontal center point of the trigger 15403 const triggerCenter = triggerRect.left + triggerRect.width / 2; 15404 15405 // Calculate tooltip's left position to center it under/over trigger's center 15406 let left = triggerCenter - tooltipWidth / 2; 15407 15408 // Default position: above the trigger 15409 // Arrow height (8px) + desired gap (e.g., 2px) = 10px total offset 15410 let top = triggerRect.top - tooltipHeight - 10; 15411 tooltip.classList.remove('tooltip-bottom'); // Manages arrow direction via CSS 15412 15413 // Check if it goes off-screen at the top (add a small buffer, e.g., 5px) 15414 if (top < 5) { 15415 // Position below the trigger 15416 top = triggerRect.bottom + 10; // 10px gap (arrow height + small space) 15417 tooltip.classList.add('tooltip-bottom'); 15418 } 15419 15420 // Boundary checks for left position to prevent going off-screen horizontally 15421 const viewportWidth = window.innerWidth; 15422 if (left < 5) { // 5px buffer from left edge 15423 left = 5; 15424 } else if (left + tooltipWidth > viewportWidth - 5) { // 5px buffer from right edge 15425 left = viewportWidth - tooltipWidth - 5; 15426 } 15427 15428 tooltip.style.left = `${left}px`; 15429 tooltip.style.top = `${top}px`; 15430 15431 // The CSS for .tooltip might have `transform: translateX(-50%)` for initial centering. 15432 // Since JS now calculates the exact 'left' for centering, 15433 // we can clear any such transform that might interfere. 15434 // However, if your CSS animation relies on a transform, adjust accordingly. 15435 // If your CSS only uses opacity for transition, this is fine: 15436 tooltip.style.transform = ''; 15437} 15438 15439// Watch for new tooltip triggers added to the DOM 15440function setupMutationObserver() { 15441 // Check if MutationObserver is available 15442 if (!window.MutationObserver) return; 15443 15444 // Create observer instance 15445 const observer = new MutationObserver((mutations) => { 15446 let shouldInit = false; 15447 15448 // Check if relevant nodes were added 15449 mutations.forEach(mutation => { 15450 if (mutation.type === 'childList') { 15451 mutation.addedNodes.forEach(node => { 15452 // Check if it's an Element and has data-custom-tooltip or contains elements with data-custom-tooltip 15453 if (node.nodeType === 1) { // ELEMENT_NODE 15454 if (node.hasAttribute && node.hasAttribute('data-custom-tooltip')) { 15455 shouldInit = true; 15456 } else if (node.querySelectorAll) { 15457 const tooltips = node.querySelectorAll('[data-custom-tooltip]'); 15458 if (tooltips.length) shouldInit = true; 15459 } 15460 } 15461 }); 15462 } 15463 }); 15464 15465 // If relevant nodes found, reinitialize tooltips 15466 if (shouldInit) { 15467 initTooltipSystem(); 15468 } 15469 }); 15470 15471 // Start observing the entire document with the configured parameters 15472 observer.observe(document.body, { 15473 childList: true, 15474 subtree: true 15475 }); 15476} 15477 15478// Initialize when DOM is ready 15479document.addEventListener('DOMContentLoaded', initTooltipSystem); 15480 15481// Also initialize immediately if document already loaded 15482if (document.readyState === 'complete' || document.readyState === 'interactive') { 15483 initTooltipSystem(); 15484} 15485 15486// Add a global safety function that runs every minute to clear any tooltips that might have gotten stuck 15487setInterval(() => { 15488 // Get all visible tooltips 15489 const visibleTooltips = document.querySelectorAll('.tooltip[style*="opacity: 1"]'); 15490 15491 // If any are found, check how long they've been visible 15492 if (visibleTooltips.length > 0) { 15493 console.log('Found visible tooltips during cleanup check, clearing them'); 15494 window.clearAllTooltips(); 15495 } 15496}, 5000); // 15497 15498// Mobile drawer a11y: Tab cycle + restore focus. No visual/mouse behavior changes. 15499var _mobileMenuFocusTrapHandler = null; 15500 15501function getMobileMenuFocusables(sidebar) { 15502 if (!sidebar) return []; 15503 return Array.prototype.filter.call( 15504 sidebar.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not
15504([tabindex="-1"])'), 15505 function (el) { 15506 if (el.getAttribute('aria-hidden') === 'true') return false; 15507 var style = typeof getComputedStyle === 'function' ? getComputedStyle(el) : null; 15508 if (style && (style.display === 'none' || style.visibility === 'hidden')) return false; 15509 return el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement; 15510 } 15511 ); 15512} 15513 15514function releaseMobileMenuFocusTrap() { 15515 if (_mobileMenuFocusTrapHandler) { 15516 document.removeEventListener('keydown', _mobileMenuFocusTrapHandler, true); 15517 _mobileMenuFocusTrapHandler = null; 15518 } 15519} 15520 15521function installMobileMenuFocusTrap(sidebar) { 15522 releaseMobileMenuFocusTrap(); 15523 if (!sidebar) return; 15524 _mobileMenuFocusTrapHandler = function (event) { 15525 if (event.key !== 'Tab') return; 15526 var sb = document.querySelector('.sidebar'); 15527 if (!sb || !sb.classList.contains('mobile-open')) return; 15528 var focusables = getMobileMenuFocusables(sb); 15529 var hamburger = document.getElementById('hamburgerBtn'); 15530 // Include hamburger so Shift+Tab from first item returns to the control that opened the drawer 15531 var cycle = hamburger ? [hamburger].concat(focusables.filter(function (el) { return el !== hamburger; })) : focusables; 15532 if (!cycle.length) return; 15533 var first = cycle[0]; 15534 var last = cycle[cycle.length - 1]; 15535 if (event.shiftKey) { 15536 if (document.activeElement === first || (sb.contains(document.activeElement) === false && document.activeElement !== hamburger)) { 15537 event.preventDefault(); 15538 last.focus(); 15539 } 15540 } else if (document.activeElement === last) { 15541 event.preventDefault(); 15542 first.focus(); 15543 } 15544 }; 15545 document.addEventListener('keydown', _mobileMenuFocusTrapHandler, true); 15546} 15547 15548// Toggle mobile menu state 15549function toggleMobileMenu() { 15550const sidebar = document.querySelector('.sidebar'); 15551const hamburgerBtn = document.getElementById('hamburgerBtn'); 15552const mobileBackdrop = document.getElementById('mobileBackdrop'); 15553 15554// Check if we're in horizontal layout mode 15555const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal'); 15556 15557sidebar.classList.toggle('mobile-open'); 15558hamburgerBtn.classList.toggle('active'); 15559const isOpen = sidebar.classList.contains('mobile-open'); 15560hamburgerBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 15561 15562if (mobileBackdrop) { 15563 if (isHorizontalLayout) { 15564 // For horizontal layout on mobile, use normal backdrop (tap outside to close) 15565 // Clear any inline overrides so CSS can control visibility 15566 mobileBackdrop.style.display = ''; 15567 mobileBackdrop.style.pointerEvents = ''; 15568 mobileBackdrop.style.opacity = ''; 15569 mobileBackdrop.style.visibility = ''; 15570 mobileBackdrop.classList.toggle('mobile-open', isOpen); 15571 setTimeout(() => { 15572 mobileBackdrop.classList.toggle('active', isOpen); 15573 }, 10); 15574 } else { 15575 // For other layouts, use normal backdrop behavior 15576 mobileBackdrop.classList.toggle('mobile-open'); 15577 // Slight delay to show opacity transition 15578 setTimeout(() => { 15579 mobileBackdrop.classList.toggle('active', isOpen); 15580 }, 10); 15581 } 15582} 15583 15584// Prevent body scrolling when menu is open 15585document.body.style.overflow = isOpen ? 'hidden' : ''; 15586 15587if (isOpen) { 15588 // Trap Tab only â do not steal focus on mouse/touch open (avoids focus rings / scroll jumps) 15589 installMobileMenuFocusTrap(sidebar); 15590} else { 15591 releaseMobileMenuFocusTrap(); 15592 // Restore focus only if keyboard focus was inside the drawer 15593 if (hamburgerBtn && sidebar && sidebar.contains(document.activeElement)) { 15594 try { hamburgerBtn.focus(); } catch (e) {} 15595 } 15596} 15597} 15598 15599// Close the mobile menu 15600function closeMobileMenu() { 15601const sidebar = document.querySelector('.sidebar'); 15602const hamburgerBtn = document.getElementById('hamburgerBtn'); 15603const mobileBackdrop = document.getElementById('mobileBackdrop'); 15604const wasOpen = sidebar && sidebar.classList.contains('mobile-open'); 15605 15606// Check if we're in horizontal layout mode 15607const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal'); 15608 15609sidebar.classList.remove('mobile-open'); 15610hamburgerBtn.classList.remove('active'); 15611if (hamburgerBtn) hamburgerBtn.setAttribute('aria-expanded', 'false'); 15612 15613if (mobileBackdrop) { 15614 if (isHorizontalLayout) { 15615 // For horizontal layout, clear inline overrides and remove mobile-open 15616 mobileBackdrop.style.display = ''; 15617 mobileBackdrop.style.pointerEvents = ''; 15618 mobileBackdrop.style.opacity = ''; 15619 mobileBackdrop.style.visibility = ''; 15620 mobileBackdrop.classList.remove('mobile-open'); 15621 mobileBackdrop.classList.remove('active'); 15622 } else { 15623 // For other layouts, use normal backdrop behavior 15624 mobileBackdrop.classList.remove('active'); 15625 setTimeout(() => { 15626 mobileBackdrop.classList.remove('mobile-open'); 15627 }, 300); // Match transition duration 15628 } 15629} 15630
15631// Re-enable body scrolling 15632document.body.style.overflow = ''; 15633releaseMobileMenuFocusTrap(); 15634// Return focus to hamburger only if focus was inside the drawer (keyboard/SR); mouse users unchanged 15635if (wasOpen && hamburgerBtn && sidebar && sidebar.contains(document.activeElement)) { 15636 try { hamburgerBtn.focus(); } catch (e) {} 15637} 15638} 15639 15640function updateMobileHeaderContent() { 15641 const currentLayout = typeof getEffectiveMenuLayout === 'function' 15642 ? getEffectiveMenuLayout() 15643 : (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings 15644 ? window.MenuStyleCustomizer.settings.menuLayout || 'sidebar' 15645 : (document.body.classList.contains('menu-layout-horizontal') ? 'horizontal' : 'sidebar')); 15646 15647 const isMobileView = window.innerWidth <= 768; // Assuming 768px is your mobile breakpoint 15648 15649 const mobileHeaderContent = document.querySelector('.mobile-header-content'); 15650 if (!mobileHeaderContent) { 15651 console.warn("updateMobileHeaderContent: .mobile-header-content element not found."); 15652 return; 15653 } 15654 15655 // On mobile, center the content unless Space Colony (left-aligned title) 15656 if (isMobileView) { 15657 const isSpaceColonyTheme = typeof isSpaceColonySiteTheme === 'function' && isSpaceColonySiteTheme(); 15658 mobileHeaderContent.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center'; 15659 // Also ensure logo-menu-container doesn't grow 15660 const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container'); 15661 if (logoMenuContainer) { 15662 logoMenuContainer.style.flexGrow = '0'; 15663 logoMenuContainer.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center'; 15664 } 15665 } 15666 15667 console.log(`updateMobileHeaderContent: Layout: ${currentLayout}, Mobile: ${isMobileView}`); 15668 15669 // Clear previous generic mobile content (like centered logo/title) 15670 const existingMobileImage = mobileHeaderContent.querySelector('.mobile-header-image'); 15671 const existingMobileText = mobileHeaderContent.querySelector('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title'); 15672 if (existingMobileImage) { 15673 // The img is always appended directly (never wrapped in an <a>). Remove it straight. 15674 // Also handle any legacy <a>-wrapped images left from older deployments. 15675 const imageParent = existingMobileImage.parentElement; 15676 if (imageParent && imageParent.tagName === 'A' && imageParent.parentElement === mobileHeaderContent) { 15677 imageParent.remove(); 15678 } else { 15679 existingMobileImage.remove(); 15680 } 15681 } 15682 // Sweep any stray empty <a> tags left by older deployments that used the wrapper approach 15683 Array.from(mobileHeaderContent.children).forEach(function(child) { 15684 if (child.tagName === 'A' && !child.querySelector('.mobile-header-image') && !(child.textContent || '').trim()) { 15685 child.remove(); 15686 } 15687 }); 15688 if (existingMobileText) existingMobileText.remove(); 15689 15690 if (currentLayout === 'horizontal' && !isMobileView) { 15691 // Desktop Horizontal Layout: 15692 // The logo is handled by renderHorizontalMenu placing it in .horizontal-header-logo. 15693 // The menu items are in .horizontal-menu-container. 15694 // Remove stray mobile-only title/image nodes (duplicate of horizontal-header-logo). 15695 mobileHeaderContent.querySelectorAll('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title, .mobile-header-image').forEach(function(el) { 15696 el.remove(); 15697 }); 15698 console.log("updateMobileHeaderContent: Desktop horizontal layout, re-rendering horizontal menu to ensure sidebar elements are available."); 15699 if (typeof renderHorizontalMenu === 'function') { 15700 // Wait for sidebar elements to be available before rendering 15701 const waitForSidebarElements = (attempts = 0) => { 15702 const maxAttempts = 10; 15703 if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 15704 console.log("updateMobileHeaderContent: Sidebar elements available, rendering horizontal menu."); 15705 renderHorizontalMenu(); 15706 } else if (attempts < maxAttempts) { 15707 const attemptNum = attempts + 1; 15708 console.log("updateMobileHeaderContent: Waiting for sidebar elements (attempt " + attemptNum + "/" + maxAttempts + ")..."); 15709 setTimeout(() => waitForSidebarElements(attempts + 1), 100); 15710 } else { 15711 console.warn("updateMobileHeaderContent: Sidebar elements not available after waiting, rendering anyway."); 15712 renderHorizontalMenu(); 15713 } 15714 }; 15715 waitForSidebarElements(); 15716 } 15717 return; 15718 } 15719 15720 // For Horizontal layout on mobile, we need to clear the horizontal menu items 15721 // and show only the mobile header logo/title 15722 if (currentLayout === 'horizontal' && isMobileView) { 15723 console.log("updateMobileHeaderContent: Mobile horizontal layout - clearing horizontal menu items and showing mobile logo."); 15724 15725 // Clear horizontal menu items from mobile header 15726 const horizontalMenuContainer = mobileHeaderContent.querySelector('.horizontal-menu
15726-container'); 15727 if (horizontalMenuContainer) { 15728 console.log("updateMobileHeaderContent: Clearing horizontal menu container"); 15729 horizontalMenuContainer.innerHTML = ''; 15730 } else { 15731 console.log("updateMobileHeaderContent: No horizontal menu container found to clear"); 15732 } 15733 15734 // Clear horizontal header logo from mobile header 15735 const horizontalHeaderLogo = mobileHeaderContent.querySelector('.horizontal-header-logo'); 15736 if (horizontalHeaderLogo) { 15737 console.log("updateMobileHeaderContent: Clearing horizontal header logo"); 15738 horizontalHeaderLogo.innerHTML = ''; 15739 } else { 15740 console.log("updateMobileHeaderContent: No horizontal header logo found to clear"); 15741 } 15742 15743 // Also clear any existing horizontal menu items that might be directly in mobile-header-content 15744 const existingHorizontalMenuItems = mobileHeaderContent.querySelectorAll('.horizontal-menu-item'); 15745 if (existingHorizontalMenuItems.length > 0) { 15746 console.log('updateMobileHeaderContent: Removing ' + existingHorizontalMenuItems.length + ' existing horizontal menu items'); 15747 existingHorizontalMenuItems.forEach(item => item.remove()); 15748 } 15749 } 15750 15751 // For Sidebar, Top, OR Horizontal layout on mobile: 15752 // Populate .mobile-header-content with the first sidebar element (logo/title). 15753 // The CSS will hide .horizontal-menu-container and .horizontal-header-logo on mobile for horizontal layout. 15754 15755 console.log("updateMobileHeaderContent: Populating with first sidebar element."); 15756 15757 // For horizontal layout on mobile, use the header-specific function to get logo/title 15758 // For other layouts, use the general function 15759 let firstElement; 15760 if (currentLayout === 'horizontal' && isMobileView) { 15761 firstElement = getFirstSidebarElementForHeader(); // Gets logo/title specifically 15762 console.log("updateMobileHeaderContent: Using getFirstSidebarElementForHeader for mobile horizontal layout"); 15763 } else { 15764 firstElement = getFirstSidebarElement(); // Gets any first element 15765 console.log("updateMobileHeaderContent: Using getFirstSidebarElement for other layouts"); 15766 } 15767 15768 if (firstElement) { 15769 console.log('updateMobileHeaderContent: First element found:', JSON.stringify(firstElement)); 15770 15771 if (firstElement.type === 'image' && (firstElement.imageData || firstElement.imageUrl)) { 15772 console.log('updateMobileHeaderContent: Creating mobile header image'); 15773 const img = document.createElement('img'); 15774 img.src = firstElement.imageUrl || firstElement.imageData; 15775 img.alt = resolveBrandLogoAlt(firstElement); 15776 img.className = 'mobile-header-image sidebar-image'; // For specific styling if needed 15777 15778 let mobileDisplayNode = img; 15779 if (firstElement.colorizeWithSiteColors && 15780 window.SidebarManager && 15781 typeof window.SidebarManager._buildSidebarImageDisplayNode === 'function') { 15782 const buildData = Object.assign({}, firstElement); 15783 if (!buildData.imageWidth) { 15784 buildData.imageWidth = 'auto'; 15785 } 15786 mobileDisplayNode = window.SidebarManager._buildSidebarImageDisplayNode(img, buildData); 15787 } 15788 15789 // Opt-in per-image padding wrapper (no-op when unset) 15790 if (window.SidebarManager && 15791 typeof window.SidebarManager._wrapWithImagePadding === 'function') { 15792 mobileDisplayNode = window.SidebarManager._wrapWithImagePadding(mobileDisplayNode, firstElement); 15793 } 15794 15795 // Handle linking: always append the img directly so existing CSS centering is 15796 // not disrupted by an <a> wrapper (which the many !important positioning rules 15797 // were not written to handle). Navigation is wired via a click handler. 15798 if (firstElement.linkType && firstElement.linkType !== 'none') { 15799 let finalLinkHref = '#'; 15800 let isValidLink = false;
15801 const linkTarget = firstElement.linkTarget || '_self'; 15802 15803 if (firstElement.linkType === 'external' && firstElement.linkUrl) { 15804 finalLinkHref = firstElement.linkUrl; 15805 isValidLink = true; 15806 } else if (firstElement.linkType === 'internal' && firstElement.linkPageId) { 15807 const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []); 15808 const targetPage = allGalleries.find(g => g.id === parseInt(firstElement.linkPageId)); 15809 15810 if (targetPage) { 15811 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 15812 if (isPreviewMode) { 15813 const pathParts = window.location.pathname.split('/').filter(Boolean); 15814 const siteGuid = pathParts[0]; 15815 if (siteGuid && targetPage.slug) { 15816 finalLinkHref = '/' + siteGuid + '/' + targetPage.slug; 15817 isValidLink = true; 15818 } else if (targetPage.url && targetPage.url.startsWith('/')) { 15819 finalLinkHref = targetPage.url; 15820 isValidLink = true; 15821 } 15822 } else { 15823 if (targetPage.slug) { 15824 finalLinkHref = '/' + targetPage.slug; 15825 isValidLink = true; 15826 } else if (targetPage.url && targetPage.url.startsWith('/')) { 15827 finalLinkHref = targetPage.url; 15828 isValidLink = true; 15829 } 15830 } 15831 } 15832 } 15833 15834 if (isValidLink) { 15835 mobileDisplayNode.style.cursor = 'pointer'; 15836 const capturedHref = finalLinkHref; 15837 const capturedTarget = linkTarget; 15838 const capturedLinkType = firstElement.linkType; 15839 const capturedPageId = firstElement.linkPageId; 15840 15841 mobileDisplayNode.addEventListener('click', (e) => { 15842 e.preventDefault(); 15843 if (capturedLinkType === 'internal' && capturedPageId) { 15844 const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []); 15845 const targetPage = allGalleries.find(g => g.id === parseInt(capturedPageId)); 15846 if (targetPage) { 15847 if (isPageMenuItem(targetPage) && typeof window.loadPage === 'function') { 15848 window.loadPage(targetPage.id); 15849 } else if (typeof window.loadGallery === 'function') { 15850 window.loadGallery(targetPage.id); 15851 } else { 15852 window.location.href = capturedHref; 15853 } 15854 } else { 15855 window.location.href = capturedHref; 15856 } 15857 } else { 15858 if (capturedTarget === '_blank') { 15859 window.open(capturedHref, '_blank'); 15860 } else { 15861 window.location.href = capturedHref; 15862 } 15863 } 15864 }); 15865 } 15866 } 15867 mobileHeaderContent.appendChild(mobileDisplayNode); 15868 } else if (firstElement.type === 'text' && firstElement.textContent) { 15869 console.log('updateMobileHeaderContent: Creating mobile header text'); 15870 const textDiv = document.createElement('div'); 15871 textDiv.className = 'mobile-header-text'; // For specific styling 15872 textDiv.innerHTML = firstElement.textContent; // Use innerHTML if textContent can contain HTML 15873 const titleEl = textDiv.querySelector('.site-title-text, h1, h2, p:last-of-type, p:only-of-type'); 15874 if (titleEl && !titleEl.classList.contains('site-title-text')) { 15875 titleEl.classList.add('site-title-text'); 15876 } 15877 mobileHeaderContent.appendChild(textDiv); 15878 } else if (firstElement.type === 'menu' && galleries && galleries.length > 0) { 15879 console.log('updateMobileHeaderContent: Creating mobile header menu title'); 15880 // Fallback for 'menu' type, find the first visible gallery title 15881 const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer); 15882 const titleText = firstVisibleGallery ? firstVisibleGallery.title : (firstElement.title || 'Menu'); 15883 const titleDiv = document.createElement('div'); 15884 titleDiv.className = 'mobile-header-menu-title'; // For specific styling 15885 titleDiv.textContent = titleText; 15886 mobileHeaderContent.appendChild(titleDiv); 15887 } else { // Default fallback 15888 console.log('updateMobileHeaderContent: Creating mobile header title (fallback)'); 15889 const titleDiv = document.createElement('div'); 15890 titleDiv.className = 'mobile-header-title'; // For specific styling 15891 titleDiv.textContent = firstElement.title || (galleries && galleries.length > 0 && galleries[0].title) || 'Menu'; 15892 mobileHeaderContent.appendChild(titleDiv); 15893 } 15894 } else { 15895 // Fallback if no sidebar elements exist 15896 const defaultTitle = document.createElement('div'); 15897 defaultTitle.className = 'mobile-header-title'; 15898 defaultTitle.textContent = 'Menu'; 15899 mobileHeaderContent.appendChild(defaultTitle); 15900 console.log("updateMobileHeaderContent: No first sidebar element found, showing default title."); 15901 } 15902 15903 if (typeof syncContentMarginToHorizontalHeader === 'function') { 15904 syncContentMarginToHorizontalHeader(); 15905 } 15906} 15907 15908// Helper function to get the first element in the sidebar 15909function getFirstSidebarElement() { 15910// Check if window.SidebarManager exists and has elements 15911if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 15912// Filter to only visible elements 15913const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false && el.type !== 'spacer'); 15914 15915if (visibleElements.length > 0) { 15916 // Sort by position and return the first one 15917 return visibleElements.sort((a, b) => a.position - b.position)[0]; 15918} 15919} 15920 15921// If no sidebar elements, return first gallery as fallback 15922if (galleries.length > 0) { 15923const firstVisibleGallery = galleries.find(g => g.visible !== false); 15924if (firstVisibleGallery) { 15925 return { 15926 type: 'menu', 15927 title: firstVisibleGallery.title 15928 }; 15929} 15930} 15931 15932// Default fallback 15933return { 15934type: 'text', 15935title: 'Menu' 15936}; 15937} 15938// KEEP YOUR ORIGINAL initMobileMenu, toggleMobileMenu, closeMobileMenu FUNCTIONS EXACTLY AS THEY ARE 15939// Just replace the updateMobileTitle function and add our initialization code 15940 15941// Replace the existing updateMobileTitle function with this one 15942function updateMobileTitle() { 15943// Always call our implementation that shows the top sidebar element 15944updateMobileHeaderContent(); 15945} 15946 15947// Add window resize listener to handle mobile header content updates 15948window.addEventListener('resize', () => { 15949 // Debounce the resize event to avoid excessive calls 15950 clearTimeout(window.resizeTimeout); 15951 window.resizeTimeout = setTimeout(() => { 15952 if (typeof updateMobileHeaderContent === 'function') { 15953 updateMobileHeaderContent(); 15954 } 15955 if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') { 15956 applyHorizontalLogoMenuLayoutStyles(); 15957 } 15958 if (typeof syncContentMarginToHorizontalHeader === 'function') { 15959 syncContentMarginToHorizontalHeader(); 15960 } 15961 }, 250); 15962}); 15963 15964//separate initialization function that doesn't replace the original initMobileMenu 15965function initTopElementDisplay() { 15966console.log("Initializing top element display"); 15967 15968// Run immediately 15969updateMobileHeaderContent(); 15970 15971if (typeof syncContentMarginToHorizontalHeader === 'function') { 15972 setTimeout(syncContentMarginToHorizontalHeader, 150); 15973} 15974 15975// Also hook into loadGallery to ensure it runs whenever galleries change 15976const originalLoadGallery = window.loadGallery; 15977if (typeof originalLoadGallery === 'function') { 15978window.loadGallery = function(id, event, options) { 15979 // Forward all args (including post-save { forceReload: true }) to the underlying loader 15980 const result = originalLoadGallery(id, event, options); 15981 updateMobileHeaderContent(); 15982 return result; 15983}; 15984} 15985} 15986 15987 15988// Initialize our functionality after a short delay to ensure SidebarManager is ready 15989document.addEventListener('DOMContentLoaded', function() { 15990// Short delay to ensure SidebarManager is initialized 15991setTimeout(initTopElementDisplay, 100); 15992 15993// Also run a bit later to catch any late-loading elements 15994setTimeout(updateMobileHeaderContent, 500); 15995}); 15996 15997// Run immediately if document is already loaded 15998if (document.readyState === 'complete' || document.readyState === 'interactive') { 15999setTimeout(initTopElementDisplay, 100); 16000} 16001 16002// Add a final fallback to ensure gallery visibility on window load 16003window.addEventListener('load', function() { 16004 setTimeout(ensureGalleryVisibility, 2000); 16005}); 16006 16007 16008(function() { 16009 /** 16010 * EnhancedGallerySystem - Comprehensive gallery script and state management 16011 */ 16012 class EnhancedGallerySystem { 16013 constructor() { 16014 // Core state tracking 16015 this.initialized = false;
16016 this.initPromise = null; 16017 this.isLoading = false; 16018 this.currentGalleryId = null; 16019 this.version = '260923-002'; // Increment when gallery scripts change (matches current versioned filenames) 16020 16021 // Track loaded resources 16022 this.loadedScripts = new Set(); 16023 this.loadedStyles = new Set(); 16024 16025 // Resources to ensure are loaded - Use Worker route (avoids CDN SSL issues) 16026 const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; 16027 this.essentialResources = { 16028 scripts: [ 16029 { id: 'neon-gallery-main', src: galleryScriptsBase + 'neon-gallery-main-v260923-002.js' }, 16030 { id: 'neon-gallery-hp', src: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.js' } 16031 ], 16032 styles: [ 16033 { id: 'neon-gallery-css', href: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css' } 16034 ] 16035 }; 16036 16037 // Keep track of original initialization functions 16038 this.originalInitFunctions = {}; 16039 16040 // Keep reference to gallery state between changes 16041 this.previousGalleryState = null; 16042 16043 // Track last initialization time per gallery ID to prevent duplicates 16044 this.lastInitializationTime = new Map(); 16045 this._loadInProgress = null; 16046 this._loadingGalleryId = null; 16047 } 16048 16049 /** 16050 * Setup the correct gallery container structure 16051 * @param {HTMLElement} container - The main container element 16052 * @returns {boolean} - Success indicator 16053 */ 16054setupCorrectGalleryContainer(container) { 16055 console.log('EnhancedGallerySystem: Setting up correct gallery container structure'); 16056 16057 // Validate container 16058 if (!container) { 16059 console.error('EnhancedGallerySystem: Main gallery container is null'); 16060 return false; 16061 } 16062 16063 try { 16064 // Clear the container 16065 container.innerHTML = ''; 16066 16067 // Create the exact container structure expected by the gallery script 16068 const neonContainer = document.createElement('div'); 16069 neonContainer.id = 'neon-gallery-container'; 16070 neonContainer.style.width = '100%'; 16071 neonContainer.style.height = '100%'; 16072 neonContainer.style.position = 'relative'; 16073 16074 // Add the container to the DOM 16075 container.appendChild(neonContainer); 16076 16077 // Set visibility 16078 container.style.opacity = '1'; 16079 container.style.display = 'block'; 16080 16081 console.log('EnhancedGallerySystem: Gallery container structure created'); 16082 return true; 16083 } catch (error) { 16084 console.error('EnhancedGallerySystem: Error setting up gallery container:', error); 16085 return false; 16086 } 16087} 16088 16089 /** 16090 * Ensure #neon-gallery-container exists before gallery scripts execute. 16091 * neon-gallery-main bails early without it; hp may leave neonGalleryUtils as {}. 16092 */ 16093 ensureGalleryContainerPlaceholder(container) { 16094 if (document.getElementById('neon-gallery-container')) { 16095 return document.getElementById('neon-gallery-container'); 16096 } 16097 const host = container || document.querySelector('.gallery-container') || document.getElementById('galleryFrame')?.parentElement; 16098 if (!host) { 16099 return null; 16100 } 16101 const neonContainer = document.createElement('div'); 16102 neonContainer.id = 'neon-gallery-container'; 16103 neonContainer.className = 'gallery-direct-content'; 16104 neonContainer.style.width = '100%'; 16105 neonContainer.style.height = '100%'; 16106 neonContainer.style.position = 'relative'; 16107 host.appendChild(neonContainer); 16108 console.log('EnhancedGallerySystem: Created placeholder neon-gallery-container before script load'); 16109 return neonContainer; 16110 } 16111 16112 /** 16113 * Wait for neon-gallery-main to expose loadGallerySettings; reload main if needed. 16114 */ 16115 async ensureNeonGalleryUtilsReady(container) { 16116 const hasLoadGallerySettings = () => 16117 typeof window.neonGalleryUtils?.loadGallerySettings === 'function'; 16118 16119 if (hasLoadGallerySettings()) { 16120 return true; 16121 } 16122 16123 const mainScript = this.essentialResources.scripts.find(s => s.id === 'neon-gallery-main'); 16124 if (!mainScript) { 16125 return false; 16126 } 16127 16128 console.warn('EnhancedGallerySystem: neonGalleryUtils.loadGallerySettings missing â reloading main script'); 16129 this.ensureGalleryContainerPlaceholder(container); 16130 16131 document.querySelectorAll('script[src*="neon-gallery-main"]').forEach(el => { 16132 if (el.parentNode) el.parentNode.removeChild(el); 16133 }); 16134 this.loadedScripts.delete('neon-gallery-main');
16135 try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; } 16136 16137 await new Promise((resolve, reject) => { 16138 const scriptElement = document.createElement('script'); 16139 scriptElement.id = 'neon-gallery-main'; 16140 scriptElement.src = `${mainScript.src}?v=${this.version}&reload=${Date.now()}`; 16141 scriptElement.async = false; 16142 scriptElement.onload = () => { 16143 this.loadedScripts.add('neon-gallery-main'); 16144 resolve(); 16145 }; 16146 scriptElement.onerror = (err) => reject(err); 16147 document.body.appendChild(scriptElement); 16148 }); 16149 16150 for (let i = 0; i < 50; i++) { 16151 if (hasLoadGallerySettings()) { 16152 return true; 16153 } 16154 await new Promise(r => setTimeout(r, 100)); 16155 } 16156 16157 return hasLoadGallerySettings(); 16158 } 16159 16160 /** 16161 * Preload styles only â defer script loading until a gallery load provides a container. 16162 */ 16163 preloadStylesOnly() { 16164 return this.loadStyles(); 16165 } 16166 16167 /** 16168 * Reset initialization tracking when gallery scripts are removed from the DOM 16169 * (e.g. loadPage cleanup). Without this, a pending initPromise never resolves and 16170 * subsequent loadGallery calls hang with isLoading:true, initialized:false. 16171 */ 16172 resetInitializationState() { 16173 console.log('EnhancedGallerySystem: Resetting initialization state'); 16174 this.initPromise = null; 16175 this.initialized = false; 16176 this.isLoading = false; 16177 this.loadedScripts.clear(); 16178 this.loadedStyles.clear(); 16179 // Allow neon-gallery-main bootstrap wipe once again after scripts are re-injected 16180 try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; } 16181 } 16182 16183 /** 16184 * Initialize the gallery system 16185 * @returns {Promise} Resolves when all core dependencies are loaded 16186 */ 16187 initialize() { 16188 // Return existing promise if already initializing or complete 16189 if (this.initPromise) { 16190 return this.initPromise; 16191 } 16192 16193 console.log('EnhancedGallerySystem: Initializing gallery framework'); 16194 this.isLoading = true; 16195 16196 // Create a promise to track loading completion 16197 this.initPromise = (async () => { 16198 try { 16199 // CRITICAL: Initialize neonGalleryState before anything else 16200 // This prevents "Cannot set properties of undefined" errors 16201 if (!window.neonGalleryState) { 16202 window.neonGalleryState = { 16203 initialized: false, 16204 initializing: false, 16205 domInitialized: false, 16206 configApplied: false, 16207 initializationPromise: null 16208 }; 16209 console.log('EnhancedGallerySystem: Initialized neonGalleryState'); 16210 } 16211 16212 // 1. First ensure all essential styles are loaded 16213 await this.loadStyles(); 16214 16215 // 2. Preload all scripts to hint to the browser 16216 this.preloadScripts(); 16217 16218 // 3. Ensure gallery container exists before scripts that require it 16219 this.ensureGalleryContainerPlaceholder( 16220 document.querySelector('.gallery-container') 16221 ); 16222 16223 // 4. Load all essential scripts 16224 await this.loadScripts(); 16225 16226 // 5. Initialize the gallery events 16227 this.setupEvents(); 16228 16229 // Mark as fully initialized 16230 this.initialized = true; 16231 this.isLoading = false; 16232 16233 console.log('EnhancedGallerySystem: Successfully initialized'); 16234 return true; 16235 } catch (error) { 16236 console.error('EnhancedGallerySystem: Initialization failed:', error); 16237 this.isLoading = false; 16238 throw error; 16239 } 16240 })(); 16241 16242 return this.initPromise; 16243 } 16244 16245 /** 16246 * Load all necessary stylesheet resources 16247 */ 16248 async loadStyles() { 16249 console.log('EnhancedGallerySystem: Loading essential stylesheets'); 16250 16251 const stylePromises = this.essentialResources.styles.map(style => { 16252 // Skip if already loaded 16253 if (this.loadedStyles.has(style.id) || document.getElementById(style.id)) { 16254 console.log(`Style already loaded: ${style.id}`); 16255 this.loadedStyles.add(style.id); 16256 return Promise.resolve(); 16257 } 16258 16259 return new Promise((resolve, reject) => { 16260 const link = document.createElement('link'); 16261 link.id = style.id; 16262 link.rel = 'stylesheet'; 16263 // Use proxy URL if flag is set, otherwise use CDN URL 16264 let styleUrl = `${style.href}?v=${this.version}`; 16265 if (window.useCdnProxy && window.convertCdnToProxyUrl && style.href.includes('cdn.neonsky.app')) { 16266 styleUrl = window.convertCdnToProxyUrl(style.href) + '?v=' + this.version; 16267 console.log('ð Using proxy URL for style:', style.id, styleUrl); 16268 } 16269 16270 link.href = styleUrl; 16271 16272 link.onload = () => { 16273 console.log(`â Style loaded: ${style.id}`); 16274 this.loadedStyles.add(style.id); 16275 resolve(); 16276 }; 16277 16278 link.onerror = (err) => { 16279 console.error(`â Failed to load style: ${style.id}`, err); 16280 if (style.href.includes('cdn.neonsky.app') && !styleUrl.includes('/cdn-proxy/')) { 16281 console.error('ð Retrying via proxy:', style.href); 16282 const proxyUrl = window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(style.href) : style.href.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 16283 link.href = proxyUrl + '?v=' + this.version; 16284 link.onerror = function() { 16285 console.error('â Proxy failed, trying storage:', proxyUrl); 16286 link.href = link.href.replace('cdn.neonsky.app', 'storage.neonsky.app'); 16287 // Don't reject - let it try storage 16288 }; 16289 } else { 16290 reject(new Error(`Failed to load ${style.id}`)); 16291 } 16292 }; 16293 16294 document.head.appendChild(link); 16295 }); 16296 }); 16297 16298 // Wait for all styles to load 16299 await Promise.all(stylePromises); 16300 console.log('EnhancedGallerySystem: All stylesheets loaded'); 16301 } 16302 16303 /** 16304 * Preload scripts to improve browser loading priority 16305 */ 16306 preloadScripts() {
16307 this.essentialResources.scripts.forEach(script => { 16308 // Enhanced check for already preloaded scripts 16309 const scriptName = script.src.split('/').pop(); 16310 const alreadyPreloaded = document.querySelector(`link[rel="preload"][href*="${scriptName}"]`) || 16311 document.querySelector(`script[src*="${scriptName}"]`); 16312 16313 if (alreadyPreloaded) { 16314 console.log(`Script already preloaded or loaded: ${script.id}`); 16315 return; 16316 } 16317 16318 const preload = document.createElement('link'); 16319 preload.rel = 'preload'; 16320 preload.href = `${script.src}?v=${this.version}`; 16321 preload.as = 'script'; 16322 document.head.appendChild(preload); 16323 16324 console.log(`Script preloaded: ${script.id}`); 16325 }); 16326 } 16327 16328 /** 16329 * Load all necessary scripts 16330 */ 16331 async loadScripts() { 16332 console.log('EnhancedGallerySystem: Loading essential scripts'); 16333 16334 // Load sequentially (main before hp) so neon-gallery-main assigns window.neonGalleryUtils 16335 // before hp's initNeonGallery can run; parallel async scripts could execute hp before main finishes. 16336 const loadOne = (script) => { 16337 const alreadyLoaded = this.loadedScripts.has(script.id) || 16338 document.getElementById(script.id) || 16339 document.querySelector(`script[src*="${script.src.split('/').pop()}"]`); 16340 16341 if (alreadyLoaded) { 16342 const mainNeedsReload = 16343 script.id === 'neon-gallery-main' && 16344 typeof window.neonGalleryUtils?.loadGallerySettings !== 'function'; 16345 if (mainNeedsReload) { 16346 console.warn(`EnhancedGallerySystem: ${script.id} in DOM but neonGalleryUtils incomplete â reloading`); 16347 document.querySelectorAll('script[src*="neon-gallery-main"]').forEach(el => { 16348 if (el.parentNode) el.parentNode.removeChild(el); 16349 }); 16350 this.loadedScripts.delete(script.id); 16351 try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; } 16352 } else { 16353 console.log(`Script already loaded: ${script.id}`); 16354 this.loadedScripts.add(script.id); 16355 return Promise.resolve(); 16356 } 16357 } 16358 16359 return new Promise((resolve, reject) => { 16360 const scriptElement = document.createElement('script'); 16361 scriptElement.id = script.id; 16362 16363 let scriptUrl = `${script.src}?v=${this.version}`; 16364 if (window.useCdnProxy && window.convertCdnToProxyUrl && script.src.includes('cdn.neonsky.app')) { 16365 scriptUrl = window.convertCdnToProxyUrl(script.src) + '?v=' + this.version; 16366 console.log('ð Using proxy URL for script:', script.id, scriptUrl); 16367 } 16368 16369 scriptElement.src = scriptUrl; 16370 scriptElement.async = false; 16371 16372 scriptElement.onload = () => { 16373 console.log(`â Script loaded: ${script.id}`); 16374 this.loadedScripts.add(script.id); 16375 resolve(); 16376 }; 16377 16378 scriptElement.onerror = (err) => { 16379 console.error(`â Failed to load script: ${script.id}`, err); 16380 if (script.src.includes('cdn.neonsky.app') && !scriptUrl.includes('/cdn-proxy/')) { 16381 console.error('ð Retrying script via proxy:', script.id); 16382 const proxyUrl = window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(script.src) : script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 16383 scriptElement.src = proxyUrl + '?v=' + this.version; 16384 scriptElement.onerror = function() { 16385 console.error('â Proxy failed, trying storage:', proxyUrl); 16386 scriptElement.src = scriptElement.src.replace('cdn.neonsky.app', 'storage.neonsky.app'); 16387 }; 16388 } else { 16389 reject(new Error(`Failed to load ${script.id}`)); 16390 } 16391 }; 16392 16393 document.body.appendChild(scriptElement); 16394 }); 16395 }; 16396 16397 for (const script of this.essentialResources.scripts) { 16398 await loadOne(script); 16399 } 16400 console.log('EnhancedGallerySystem: All scripts loaded'); 16401 16402 // Save references to original initialization functions after scripts are loaded 16403 if (window.initNeonGallery && typeof window.initNeonGallery === 'function') { 16404 this.originalInitFunctions.initNeonGallery = window.initNeonGallery; 16405 } 16406 16407 if (window.reinitializeGalleryAfterSave && typeof window.reinitializeGalleryAfterSave === 'function') { 16408 this.originalInitFunctions.reinitializeGalleryAfterSave = window.reinitializeGalleryAfterSave; 16409 } 16410 } 16411 16412 /** 16413 * Set up enhanced event listeners 16414 */ 16415 setupEvents() { 16416 // Listen for gallery save events to capture latest state 16417 document.addEventListener('gallery-saved', (event) => { 16418 if (event.detail && event.detail.settings) { 16419 this.captureGalleryState(event.detail.settings); 16420 } 16421 }); 16422 16423 // Override existing functions to capture state changes 16424 this.enhanceExistingFunctions(); 16425 } 16426 16427 /** 16428 * Enhance existing gallery functions to work with our system 16429 */ 16430 enhanceExistingFunctions() { 16431 // Save original functions 16432 const originalInit = window.initNeonGallery; 16433 const originalReinit = window.reinitializeGalleryAfterSave; 16434 16435 // Only enhance if they exist 16436 if (typeof originalInit === 'function') { 16437 // Track if initNeonGallery is currently executing to prevent concurrent calls 16438 let isExecuting = false;
16439 let lastExecutionTime = 0; 16440 16441 window.initNeonGallery = async (...args) => { 16442 // Prevent duplicate initializations within 1 second 16443 const now = Date.now(); 16444 16445 // Check if already executing or called recently 16446 if (isExecuting) { 16447 console.log('EnhancedGallerySystem: initNeonGallery already executing, skipping duplicate call'); 16448 return Promise.resolve(); 16449 } 16450 16451 if (lastExecutionTime && (now - lastExecutionTime) < 1000) { 16452 console.log('EnhancedGallerySystem: Skipping duplicate initNeonGallery call (last call ' + (now - lastExecutionTime) + 'ms ago)'); 16453 return Promise.resolve(); 16454 } 16455 16456 // Set flag and timestamp immediately to prevent concurrent calls 16457 isExecuting = true; 16458 lastExecutionTime = now; 16459 this.lastInitializationTime.set('initNeonGallery', now); 16460 16461 try { 16462 // Call original with all arguments 16463 const result = await originalInit.apply(this, args); 16464 16465 // Capture state after initialization 16466 if (window.neonGalleryConfig) { 16467 this.captureGalleryState(window.neonGalleryConfig); 16468 } 16469 16470 return result; 16471 } finally { 16472 // Clear execution flag after a short delay to allow for async operations 16473 setTimeout(() => { 16474 isExecuting = false; 16475 }, 100); 16476 } 16477 }; 16478 } 16479 16480 if (typeof originalReinit === 'function') { 16481 window.reinitializeGalleryAfterSave = async (savedSettings) => { 16482 // Call original function 16483 const result = await originalReinit(savedSettings); 16484 16485 // Capture updated state 16486 this.captureGalleryState(savedSettings); 16487 16488 return result; 16489 }; 16490 } 16491 } 16492 16493 /** 16494 * Capture current gallery state for potential reuse 16495 */ 16496 captureGalleryState(settings) { 16497 if (!settings) return; 16498 16499 // Store a deep copy of the settings to avoid reference issues 16500 try { 16501 this.previousGalleryState = JSON.parse(JSON.stringify(settings)); 16502 console.log('EnhancedGallerySystem: Captured gallery state for possible reuse'); 16503 } catch (e) { 16504 console.error('EnhancedGallerySystem: Failed to capture gallery state:', e); 16505 } 16506 } 16507 16508 /** 16509 * Load a specific gallery using the enhanced system 16510 * @param {Object} gallery The gallery configuration 16511 * @param {HTMLElement} container The container to load the gallery into 16512 */ 16513 async loadGallery(gallery, container, options) { 16514 if (!gallery) { 16515 console.error('EnhancedGallerySystem: Invalid gallery configuration'); 16516 return false; 16517 } 16518 16519 const forceGalleryReload = options?.forceReload === true || window._forceGalleryReload === true; 16520 16521 // Coalesce concurrent loads for the same gallery â await instead of wiping mid-flight 16522 if (this._loadInProgress && this._loadingGalleryId === gallery.id && !forceGalleryReload) { 16523 const pageViewStillShowing = window._activeContentType === 'page' || 16524 !!document.querySelector('.gallery-container .page-container, .page-container'); 16525 if (!pageViewStillShowing) { 16526 console.log('EnhancedGallerySystem: Awaiting in-flight load for gallery ' + gallery.id); 16527 return this._loadInProgress; 16528 } 16529 console.log('EnhancedGallerySystem: Abandoning standalone in-flight load for page embed', gallery.id); 16530 this._loadInProgress = null; 16531 this._loadingGalleryId = null; 16532 } 16533 16534 const now = Date.now(); 16535 const lastInitTime = this.lastInitializationTime.get(gallery.id); 16536 if (!forceGalleryReload && lastInitTime && (now - lastInitTime) < 1000) { 16537 const hasContent = !!document.querySelector('.gallery-wrapper .masonry-item, .gallery-wrapper .horizontal-scroller, .gallery-wrapper .masonry'); 16538 const pageViewStillShowing = window._activeContentType === 'page' || 16539 !!document.querySelector('.gallery-container .page-container, .page-container'); 16540 if (hasContent && !pageViewStillShowing) { 16541 console.log('EnhancedGallerySystem: Gallery ' + gallery.id + ' recently loaded with content, skipping re-init'); 16542 if (container) container.style.opacity = '1'; 16543 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 16544 window.syncGalleryOptionsButtonVisibility(); 16545 } 16546 return true; 16547 } 16548 } 16549 16550 if (forceGalleryReload) { 16551 console.log('EnhancedGallerySystem: force reload requested for gallery ' + gallery.id); 16552 this.lastInitializationTime.delete(gallery.id); 16553 this._loadInProgress = null; 16554 this._loadingGalleryId = null; 16555 } 16556 16557 const loadTask = this._executeGalleryLoad(gallery, container, now, options); 16558 this._loadInProgress = loadTask; 16559 this._loadingGalleryId = gallery.id;
16560 try { 16561 return await loadTask; 16562 } finally { 16563 if (this._loadInProgress === loadTask) { 16564 this._loadInProgress = null; 16565 this._loadingGalleryId = null; 16566 } 16567 } 16568 } 16569 16570 async _executeGalleryLoad(gallery, container, startedAt, options) { 16571 this.lastInitializationTime.set(gallery.id, startedAt); 16572 const contentLoadGeneration = options && options.contentLoadGeneration != null 16573 ? options.contentLoadGeneration 16574 : window.__hydraContentLoadGeneration; 16575 const stillCurrent = () => typeof isHydraContentLoadCurrent === 'function' 16576 ? isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id) 16577 : contentLoadGeneration === window.__hydraContentLoadGeneration && 16578 (!window._activeContentType || window._activeContentType === 'gallery') && 16579 (window.activeGalleryId == null || String(window.activeGalleryId) === String(gallery.id)); 16580 16581 stopAutoAdvanceTimer(); 16582 16583 // CRITICAL: Verify container exists 16584 if (!container) { 16585 console.error('EnhancedGallerySystem: Gallery container is null or undefined'); 16586 return false; 16587 } 16588 16589 // Create a unique instance ID for this gallery load (needed before script init) 16590 const galleryInstanceId = Date.now(); 16591 16592 try { 16593 // Clean up previous gallery state and prepare container BEFORE loading scripts 16594 this.cleanupGalleryState(container); 16595 const containerSetup = this.setupCorrectGalleryContainer(container); 16596 if (!containerSetup) { 16597 throw new Error('Failed to set up gallery container'); 16598 } 16599 this.setupGalleryParameters(gallery, galleryInstanceId); 16600 } catch (prepError) { 16601 console.error('EnhancedGallerySystem: Failed to prepare gallery container:', prepError); 16602 return false; 16603 } 16604 16605 // Make sure the system is initialized (scripts load with container already in DOM) 16606 try { 16607 await this.initialize(); 16608 } catch (error) { 16609 console.error('EnhancedGallerySystem: Failed to initialize gallery framework:', error); 16610 //this.showErrorMessage(container, 'Failed to initialize gallery system. Please try again later.'); 16611 return false; 16612 } 16613 16614 if (!stillCurrent()) { 16615 console.log('EnhancedGallerySystem: Aborting stale load after framework initialize for', gallery.id); 16616 return false; 16617 } 16618 16619 if (!(await this.ensureNeonGalleryUtilsReady(container))) { 16620 console.error('EnhancedGallerySystem: neonGalleryUtils.loadGallerySettings unavailable after script load'); 16621 return false; 16622 } 16623 16624 if (!stillCurrent()) { 16625 console.log('EnhancedGallerySystem: Aborting stale load after utils ready for', gallery.id); 16626 return false; 16627 } 16628 16629 // Update current gallery tracking 16630 this.currentGalleryId = gallery.id; 16631 16632 console.log(`EnhancedGallerySystem: Loading gallery ${gallery.title} (ID: ${gallery.id}, Instance: ${galleryInstanceId})`); 16633 16634 try { 16635 // Determine which initialization method to use 16636 if (this.canUseReinitForGallery(gallery, options)) { 16637 const result = await this.reinitializeGallery(gallery, container, options); 16638 return stillCurrent() ? result : false;
16639 } else { 16640 const result = await this.initializeNewGallery(gallery, container, galleryInstanceId, options); 16641 return stillCurrent() ? result : false; 16642 } 16643 } catch (error) { 16644 console.error('EnhancedGallerySystem: Error loading gallery:', error); 16645 if (container) { 16646 console.error('EnhancedGallerySystem: Error loading gallery:', error); 16647 //this.showErrorMessage(container, 'There was a problem loading the gallery. Please try again later.'); 16648 } 16649 return false; 16650 } 16651} 16652 16653 normalizeGalleryIdentityValue(value) { 16654 if (value === undefined || value === null || value === '') return ''; 16655 return String(value); 16656 } 16657 16658 previousGalleryStateBelongsTo(gallery, previousState) { 16659 if (!gallery || !previousState) return false; 16660 16661 const targetGalleryId = this.normalizeGalleryIdentityValue(gallery.id); 16662 const targetPageId = this.normalizeGalleryIdentityValue(gallery.pageId); 16663 if (!targetGalleryId && !targetPageId) return false; 16664 16665 const previousGalleryId = this.normalizeGalleryIdentityValue( 16666 previousState.galleryId || previousState.loadedGalleryId 16667 ); 16668 const previousPageId = this.normalizeGalleryIdentityValue( 16669 previousState.pageId || previousState.initialPageUuid 16670 ); 16671 16672 if (!previousGalleryId && !previousPageId) return false; 16673 if (previousGalleryId && targetGalleryId && previousGalleryId !== targetGalleryId) { 16674 return false; 16675 } 16676 if (previousPageId && targetPageId && previousPageId !== targetPageId) { 16677 return false; 16678 } 16679 16680 return ( 16681 (previousGalleryId && targetGalleryId && previousGalleryId === targetGalleryId) || 16682 (previousPageId && targetPageId && previousPageId === targetPageId) 16683 ); 16684 } 16685 16686 /** 16687 * Determine if we can use reinitialization for this gallery 16688 */ 16689 canUseReinitForGallery(gallery, options) { 16690 if (options?.forceReload === true || window._forceGalleryReload === true) { 16691 return false; 16692 } 16693 // Check if reinitializeGalleryAfterSave function exists 16694 if (typeof window.reinitializeGalleryAfterSave !== 'function') { 16695 return false; 16696 } 16697 16698 // Check if we have a previous state to reuse 16699 if (!this.previousGalleryState) { 16700 return false; 16701 } 16702 16703 if (!this.previousGalleryStateBelongsTo(gallery, this.previousGalleryState)) { 16704 console.log('EnhancedGallerySystem: Cannot use reinit - previous gallery state does not match target gallery'); 16705 return false; 16706 } 16707 16708 // Check if the gallery has the necessary properties for reinit 16709 if (!gallery.pageId || !gallery.galleryOptions) { 16710 return false; 16711 } 16712 16713 // CRITICAL: Check if the gallery wrapper actually exists in the DOM 16714 // Reinitialization requires an existing gallery structure 16715 const galleryWrapper = document.querySelector('.gallery-wrapper'); 16716 const neonContainer = document.getElementById('neon-gallery-container'); 16717 if (!galleryWrapper || !neonContainer) { 16718 console.log('EnhancedGallerySystem: Cannot use reinit - gallery wrapper or container not found'); 16719 return false; 16720 } 16721 16722 return true; 16723 } 16724 16725 /** 16726 * Reinitialize a gallery using the reinitialization process 16727 */ 16728async reinitializeGallery(gallery, container, options) { 16729 console.log('EnhancedGallerySystem: Using reinitialization process for gallery', gallery.id); 16730 16731 try { 16732 // CRITICAL: Ensure CSS is loaded before attempting reinitialization 16733 ensureGalleryStylesLoaded(); 16734 16735 // We should already have the proper container, but just in case: 16736 if (!document.getElementById('neon-gallery-container')) { 16737 this.setupCorrectGalleryContainer(container); 16738 } 16739 16740 // CRITICAL: Ensure config is set up with all required fields before reinitialization 16741 const galleryOptions = gallery.galleryOptions || {}; 16742 window.neonGalleryConfig = { 16743 useData: true, 16744 useCDN: true, 16745 version: 'live', 16746 manualCollectionName: galleryOptions.manualCollectionName || "mod", 16747 layoutType: galleryOptions.layoutType || "grid", 16748 ...galleryOptions, 16749 siteId: window.siteId || gallery.siteId || '', 16750 galleryId: gallery.id, 16751 // Explicitly include all permanent storage fields 16752 isPerma: galleryOptions.isPerma, 16753 permaURL: galleryOptions.permaURL, 16754 loadTxId: galleryOptions.loadTxId, 16755 loadPermaURL: galleryOptions.loadPermaURL, 16756 containerSelector: '#neon-gallery-container' 16757 }; 16758 16759 // Prepare settings by merging previous state with new gallery options. 16760 // Identity fields always come from the menu target, never leftover snapshot ids. 16761 const settings = { 16762 ...this.previousGalleryState, 16763 ...gallery.galleryOptions, 16764 pageId: gallery.pageId, 16765 initialPageUuid: gallery.pageId, 16766 galleryId: gallery.id, 16767 loadedGalleryId: gallery.id, 16768 forceReload: true, 16769 containerSelector: '#neon-gallery-container' // Add this explicitly 16770 }; 16771 16772 // Call reinitializeGalleryAfterSave with the merged settings 16773 window._reinitializeGalleryFromSystem = true; 16774 let result; 16775 try { 16776 result = await window.reinitializeGalleryAfterSave(settings); 16777 } finally { 16778 window._reinitializeGalleryFromSystem = false;
16779 } 16780 16781 if (result) { 16782 console.log('EnhancedGallerySystem: Gallery reinitialization completed successfully'); 16783 // Fade in the container 16784 container.style.opacity = '1'; 16785 return true; 16786 } else { 16787 throw new Error('Reinitialization did not complete successfully'); 16788 } 16789 } catch (error) { 16790 console.error('EnhancedGallerySystem: Error reinitializing gallery:', error); 16791 // Fall back to standard initialization - ensure config is set up properly 16792 const galleryInstanceId = Date.now(); 16793 // Re-setup parameters and config for the fallback 16794 this.setupGalleryParameters(gallery, galleryInstanceId); 16795 return this.initializeNewGallery(gallery, container, galleryInstanceId, options); 16796 } 16797} 16798 16799 /** 16800 * Initialize a gallery from scratch (fallback method) 16801 */ 16802 async initializeNewGallery(gallery, container, galleryInstanceId, options) { 16803 console.log('EnhancedGallerySystem: Using standard initialization for gallery', gallery.id); 16804 const contentLoadGeneration = options && options.contentLoadGeneration != null 16805 ? options.contentLoadGeneration 16806 : window.__hydraContentLoadGeneration; 16807 const stillCurrent = () => typeof isHydraContentLoadCurrent === 'function' 16808 ? isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id) 16809 : (!window._activeContentType || window._activeContentType === 'gallery') && 16810 (window.activeGalleryId == null || String(window.activeGalleryId) === String(gallery.id)); 16811 16812 try { 16813 // CRITICAL: Ensure CSS is loaded BEFORE creating any gallery elements 16814 ensureGalleryStylesLoaded(); 16815 16816 // Clean up container 16817 if (container) { 16818 container.innerHTML = ''; 16819 } else { 16820 console.error('EnhancedGallerySystem: Container is null in initializeNewGallery'); 16821 return false; 16822 } 16823 16824 // Create gallery container with unique ID 16825 const galleryContainerId = `gallery-container-${gallery.id}-${galleryInstanceId}`; 16826 const neonGalleryContainer = document.createElement('div'); 16827 neonGalleryContainer.id = 'neon-gallery-container'; 16828 neonGalleryContainer.className = 'gallery-direct-content'; 16829 neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString()); 16830 neonGalleryContainer.setAttribute('data-instance-id', galleryInstanceId.toString()); 16831 neonGalleryContainer.setAttribute('data-unique-id', galleryContainerId); 16832 neonGalleryContainer.style.width = '100%'; 16833 neonGalleryContainer.style.height = '100%'; 16834 container.appendChild(neonGalleryContainer); 16835 16836 // CRITICAL: Initialize neonGalleryState if it doesn't exist 16837 if (!window.neonGalleryState) { 16838 window.neonGalleryState = { 16839 initialized: false, 16840 initializing: false, 16841 domInitialized: false, 16842 configApplied: false, 16843 initializationPromise: null 16844 }; 16845 } 16846 16847 // CRITICAL: Always ensure neonGalleryConfig is properly set with ALL required fields 16848 // Don't rely on conditional logic - always set it correctly here 16849 const galleryOptions = gallery.galleryOptions || {}; 16850 window.neonGalleryConfig = { 16851 useData: true, 16852 useCDN: true, 16853 version: 'live', 16854 manualCollectionName: galleryOptions.manualCollectionName || "mod", 16855 layoutType: galleryOptions.layoutType || "grid", 16856 ...galleryOptions, 16857 siteId: window.siteId || gallery.siteId || '', 16858 galleryId: gallery.id, 16859 galleryInstanceId: galleryInstanceId, 16860 containerSelector: '#neon-gallery-container', 16861 // Explicitly include all permanent storage fields (critical for proper initialization) 16862 isPerma: galleryOptions.isPerma, 16863 permaURL: galleryOptions.permaURL, 16864 loadTxId: galleryOptions.loadTxId, 16865 loadPermaURL: galleryOptions.loadPermaURL 16866 }; 16867 16868 // Call the initialization function directly if available 16869 if (typeof window.initNeonGallery === 'function') { 16870 console.log('EnhancedGallerySystem: Calling direct initialization method'); 16871 16872 // Additional check: prevent duplicate initNeonGallery calls within 1 second 16873 const initKey = 'init_' + gallery.id + '_' + galleryInstanceId; 16874 const lastInitCall = this.lastInitializationTime.get(initKey); 16875 const now = Date.now(); 16876 if (lastInitCall && (now - lastInitCall) < 1000) { 16877 console.log('EnhancedGallerySystem: Skipping duplicate initNeonGallery call for gallery ' + gallery.id + '
16877(last call ' + (now - lastInitCall) + 'ms ago)'); 16878 container.style.opacity = '1'; 16879 return true; 16880 } 16881 this.lastInitializationTime.set(initKey, now); 16882 16883 try { 16884 // Wait a tiny bit to ensure DOM updates have propagated 16885 await new Promise(resolve => setTimeout(resolve, 10)); 16886 if (!stillCurrent()) { 16887 console.log('EnhancedGallerySystem: Aborting stale initializeNewGallery before initNeonGallery'); 16888 return false; 16889 } 16890 16891 const result = await window.initNeonGallery(); 16892 if (!stillCurrent()) { 16893 console.log('EnhancedGallerySystem: Ignoring stale initializeNewGallery completion'); 16894 return false; 16895 } 16896 console.log('EnhancedGallerySystem: Direct initialization successful'); 16897 16898 // Save state for future loads 16899 if (window.neonGalleryConfig) { 16900 this.captureGalleryState(window.neonGalleryConfig); 16901 } 16902 16903 // Fade in the container 16904 container.style.opacity = '1'; 16905 return true; 16906 } catch (e) { 16907 if (!stillCurrent()) { 16908 console.log('EnhancedGallerySystem: Ignoring stale direct initialization error'); 16909 return false; 16910 } 16911 console.error('EnhancedGallerySystem: Direct initialization failed:', e); 16912 throw e; 16913 } 16914 } else { 16915 // If initNeonGallery isn't available, use event-based initialization 16916 console.log('EnhancedGallerySystem: Using event-based initialization fallback'); 16917 16918 // Create an event to trigger initialization 16919 const initEvent = new CustomEvent('neon-gallery-init', { 16920 detail: { 16921 galleryId: gallery.id, 16922 instanceId: galleryInstanceId, 16923 containerId: galleryContainerId 16924 } 16925 }); 16926 document.dispatchEvent(initEvent); 16927 16928 // Wait a short time to see if initialization completes 16929 await new Promise(resolve => setTimeout(resolve, 500)); 16930 if (!stillCurrent()) { 16931 console.log('EnhancedGallerySystem: Ignoring stale event-based gallery initialization'); 16932 return false; 16933 } 16934 16935 // Fade in regardless of result - gallery script should handle displaying errors 16936 container.style.opacity = '1'; 16937 return true; 16938 } 16939 } catch (error) { 16940 console.error('EnhancedGallerySystem: Error in standard initialization:', error); 16941 if (!stillCurrent()) { 16942 console.log('EnhancedGallerySystem: Ignoring stale standard initialization error'); 16943 return false; 16944 } 16945 if (container) { 16946 this.showErrorMessage(container, 'Failed to load gallery content. Please try again later.'); 16947 } 16948 return false; 16949 } 16950 } 16951 16952 /** 16953 * Clean up gallery state without removing loaded scripts 16954 */ 16955 cleanupGalleryState(container) { 16956 console.log('EnhancedGallerySystem: Cleaning up gallery state'); 16957 16958 // Stop any ongoing singles autoplay from the previous gallery 16959 if (typeof window.stopSinglesAutoplay === 'function') { 16960 try { 16961 window.stopSinglesAutoplay(); 16962 console.log('EnhancedGallerySystem: Called stopSinglesAutoplay() during cleanup.'); 16963 } catch (error) { 16964 console.warn('EnhancedGallerySystem: stopSinglesAutoplay() failed during cleanup:', error); 16965 } 16966 } else { 16967 console.warn('EnhancedGallerySystem: stopSinglesAutoplay() function not found during cleanup.'); 16968 } 16969 16970 // 1. Reset global gallery state variables 16971 const resetGlobals = [ 16972 'neonGalleryInitComplete', 'neonGalleryInitInProgress', 16973 'neonGalleryLoaded', 'neonGalleryConfig', 'neonGalleryCache', 'neonGalleryImages', 16974 'neonGallerySettings', 'neonLightbox', 'neonGalleryEventListeners', 16975 'neonGalleryInstance', 'currentGalleryId', 'galleryData', 16976 'imageCache', 'thumbnailCache' 16977 ]; 16978
16979 resetGlobals.forEach(prop => { 16980 if (window[prop] !== undefined) { 16981 window[prop] = null; 16982 } 16983 }); 16984 16985 // 2. Reset neonGalleryState if it's a proper object with properties 16986 if (window.neonGalleryState && typeof window.neonGalleryState === 'object') { 16987 window.neonGalleryState = { 16988 initialized: false, 16989 initializing: false, 16990 domInitialized: false, 16991 configApplied: false, 16992 initializationPromise: null 16993 }; 16994 } 16995 16996 // 3. Remove lightbox elements and overlays 16997 document.querySelectorAll('#neon-lightbox, .neon-lightbox, .lightbox-container').forEach(el => { 16998 if (el && el.parentNode) { 16999 el.parentNode.removeChild(el); 17000 } 17001 }); 17002 17003 const staleOptionsOverlay = document.getElementById('optionsOverlay'); 17004 if (staleOptionsOverlay && staleOptionsOverlay.parentNode) { 17005 staleOptionsOverlay.parentNode.removeChild(staleOptionsOverlay); 17006 } 17007 window.optionsVisible = false; 17008 17009 // 4. Remove any gallery-specific DOM elements 17010 document.querySelectorAll( 17011 '.neon-gallery-wrapper, .fullscreen-overlay, ' + 17012 '.neon-gallery-modal, .gallery-tooltip, .neon-gallery-context-menu, ' + 17013 '.neon-thumbnails, .neon-image, .neon-caption, ' + 17014 '.neon-controls, .neon-pagination' 17015 ).forEach(el => { 17016 if (el && el.parentNode) { 17017 el.parentNode.removeChild(el); 17018 } 17019 }); 17020 17021 // 5. Clear container if provided and valid 17022 if (container) { 17023 container.innerHTML = ''; 17024 container.style.opacity = '0'; 17025 } 17026 17027 // 6. Clear keyboard listeners to prevent multiple listeners from different galleries 17028 if (typeof window.removeAllLightboxKeyboardListeners === 'function') { 17029 window.removeAllLightboxKeyboardListeners(); 17030 console.log('EnhancedGallerySystem: Cleared all lightbox keyboard listeners during cleanup'); 17031 } else if (window.currentLightboxKeyboardListener) { 17032 document.removeEventListener('keydown', window.currentLightboxKeyboardListener); 17033 window.currentLightboxKeyboardListener = null; 17034 console.log('EnhancedGallerySystem: Cleared keyboard listeners during cleanup'); 17035 } 17036 window.lightboxKeyboardListenerActive = false; 17037 } 17038 17039 /** 17040 * Prepare container for gallery loading 17041 */ 17042 prepareContainer(container, gallery, instanceId) { 17043 // CRITICAL: Add null check for container 17044 if (!container) { 17045 console.error('EnhancedGallerySystem: Container is null in prepareContainer'); 17046 return; 17047 } 17048 17049 try { 17050 // Add attributes and data 17051 container.setAttribute('data-gallery-id', gallery.id.toString()); 17052 container.setAttribute('data-instance-id', instanceId.toString()); 17053 17054 // Add loading indicator 17055 const loadingIndicator = document.createElement('div'); 17056 loadingIndicator.className = 'gallery-loading'; 17057 loadingIndicator.innerHTML = ` 17058 <div class="spinner"></div> 17059 <div class="loading-text">Loading Gallery...</div> 17060 `; 17061 17062 container.appendChild(loadingIndicator); 17063 } catch (error) { 17064 console.error('EnhancedGallerySystem: Error preparing container:', error); 17065 } 17066 } 17067 17068 17069 17070 17071setupGalleryParameters(gallery, galleryInstanceId) { 17072 // Set up gallery parameters with enhanced uniqueness 17073 window.Parameters = window.Parameters || {}; 17074 17075 // Clone and extend existing Parameters to avoid losing important values 17076 const existingParams = { ...window.Parameters }; 17077 17078 window.Parameters = { 17079 ...existingParams, 17080 SiteAlias: window.location.hostname, 17081 InitialPageUuid: gallery.pageId, 17082 InitialPageAlias: gallery.slug || this.slugify(gallery.title), 17083 // CRITICAL FIX: Set isInEditor to true if user is logged in as admin OR in edit mode 17084 // This ensures gallery files bypass cache for any logged-in admin, not just when edit mode is active 17085 isInEditor: window.isEditing || 17086 localStorage.getItem('hydra_is_admin') === 'true' || 17087 window.isAdmin === true || 17088 document.body.classList.contains('hydra-admin'), 17089 siteId: window.siteId || gallery.siteId || '', 17090 isHydra: true, 17091 17092 // CRITICAL: These properties help the gallery script find the container 17093 galleryContainerId: 'neon-gallery-container', 17094 containerSelector: '#neon-gallery-container', 17095 17096 // Additional properties 17097 galleryInstanceId: galleryInstanceId, 17098 galleryUniqueId: `gallery-container-${gallery.id}-${galleryInstanceId}`, 17099 loadTimestamp: galleryInstanceId, 17100 loadedGalleryId: gallery.id 17101 }; 17102 17103 // Set up gallery config - CRITICAL: Include all permanent storage fields 17104 const galleryOptions = gallery.galleryOptions || {}; 17105 window.neonGalleryConfig = { 17106 useData: true, 17107 useCDN: true, 17108 version: 'live', 17109 manualCollectionName: galleryOptions.manualCollectionName || "mod", 17110 layoutType: galleryOptions.layoutType || "grid", 17111 ...galleryOptions, 17112 siteId: window.siteId || gallery.siteId || '', 17113 galleryId: gallery.id, 17114 galleryInstanceId: galleryInstanceId, 17115 17116 // Add container selector here too 17117 containerSelector: '#neon-gallery-container', 17118 17119 // CRITICAL: Explicitly include all permanent storage fields to ensure they're available 17120 // These are needed for proper gallery initialization and data loading 17121 isPerma: galleryOptions.isPerma, 17122 permaURL: galleryOptions.permaURL, 17123 loadTxId: galleryOptions.loadTxId, 17124 loadPermaURL: galleryOptions.loadPermaURL 17125 }; 17126} 17127 17128 /** 17129 * Show error message in container 17130 */ 17131 showErrorMessage(container, message) {
17132 // CRITICAL: Add null check for container 17133 if (!container) { 17134 console.error('EnhancedGallerySystem: Container is null in showErrorMessage'); 17135 return; 17136 } 17137 17138 try { 17139 // Clear container 17140 container.innerHTML = ''; 17141 17142 // Create error message element 17143 const errorElement = document.createElement('div'); 17144 errorElement.className = 'gallery-error'; 17145 errorElement.innerHTML = ` 17146 <h3>Error Loading Gallery</h3> 17147 <p>${message}</p> 17148 `; 17149 container.appendChild(errorElement); 17150 container.style.opacity = '1'; 17151 } catch (error) { 17152 console.error('EnhancedGallerySystem: Error showing error message:', error); 17153 } 17154 } 17155 17156 /** 17157 * Utility function to convert title to slug 17158 */ 17159 slugify(text) { 17160 return text 17161 .toLowerCase() 17162 .replace(/s+/g, '-') 17163 .replace(/[^w-]+/g, '') 17164 .replace(/--+/g, '-') 17165 .trim(); 17166 } 17167 } 17168 17169 // Create a singleton instance 17170 window.EnhancedGallerySystem = window.EnhancedGallerySystem || new EnhancedGallerySystem(); 17171 17172 /** 17173 * Enhanced loadGallery function using the EnhancedGallerySystem 17174 * This replaces the existing loadGallery function 17175 */ 17176const enhancedLoadGallery = async function(id, event, options) { 17177 const originalLoadGallery = window.originalLoadGallery; 17178 const forceGalleryReload = options?.forceReload === true || window._forceGalleryReload === true; 17179 17180 stopAutoAdvanceTimer(); 17181 if (typeof dismissGalleryPasswordOverlay === 'function') { 17182 dismissGalleryPasswordOverlay(); 17183 } 17184 17185 if (event && event.target && event.target.isConnected === false) { 17186 console.log('enhancedLoadGallery: Ignoring navigation from detached element'); 17187 return; 17188 } 17189 17190 if (event) { 17191 event.preventDefault(); 17192 event.stopPropagation(); 17193 } 17194 17195 let galleryContainer = document.querySelector('.gallery-container'); 17196 17197 if (!galleryContainer) { 17198 galleryContainer = document.getElementById('galleryFrame')?.parentElement || 17199 document.getElementById('main-content') || 17200 document.getElementById('content'); 17201 17202 if (!galleryContainer) { 17203 console.error('Gallery container not found, falling back to original function'); 17204 if (typeof originalLoadGallery === 'function') { 17205 return originalLoadGallery(id, event, options); 17206 } else { 17207 console.error('Gallery container not found and no fallback available'); 17208 return; 17209 } 17210 } 17211 } 17212 17213 // Bail out before hiding/clearing when the same gallery is already loading or rendered. 17214 // Post-save passes { forceReload: true } to bypass this path without affecting navigation. 17215 const egEarly = window.EnhancedGallerySystem; 17216 if (egEarly && !forceGalleryReload) { 17217 if (egEarly._loadInProgress && egEarly._loadingGalleryId === id) { 17218 const pageViewStillShowing = window._activeContentType === 'page' || 17219 !!document.querySelector('.gallery-container .page-container, .page-container'); 17220 if (pageViewStillShowing) { 17221 console.log('enhancedLoadGallery: abandoning standalone in-flight load for page view', id); 17222 egEarly._loadInProgress = null; 17223 egEarly._loadingGalleryId = null; 17224 } else { 17225 console.log('enhancedLoadGallery: awaiting in-flight load for gallery', id); 17226 await egEarly._loadInProgress; 17227 if (galleryContainer) galleryContainer.style.opacity = '1'; 17228 return; 17229 } 17230 } 17231 const hasRenderedGallery = !!document.querySelector( 17232 '.gallery-wrapper .masonry-item, .gallery-wrapper .horizontal-scroller, .gallery-wrapper .masonry' 17233 ); 17234 // Embedded galleries on a page share currentGalleryId; that is not a standalone gallery view. 17235 const pageViewStillShowing = window._activeContentType === 'page' || 17236 !!document.querySelector('.gallery-container .page-container, .page-container'); 17237 if (hasRenderedGallery && egEarly.currentGalleryId === id && !pageViewStillShowing) { 17238 console.log('enhancedLoadGallery: gallery already rendered, skipping reload'); 17239 if (galleryContainer) galleryContainer.style.opacity = '1'; 17240 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 17241 window.syncGalleryOptionsButtonVisibility(); 17242 } 17243 return; 17244 } 17245 } else if (forceGalleryReload) { 17246 console.log('enhancedLoadGallery: force reload requested, bypassing already-rendered skip'); 17247 if (egEarly?.lastInitializationTime?.delete) { 17248 egEarly.lastInitializationTime.delete(id); 17249 } 17250 if (egEarly) { 17251 egEarly._loadInProgress = null; 17252 egEarly._loadingGalleryId = null; 17253 } 17254 } 17255 17256 if (galleryContainer) { 17257 galleryContainer.style.opacity = '0'; 17258 } 17259 17260 console.log('Loading gallery with ID (enhancedLoadGallery):', id); 17261 17262 const gallery = findGalleryById(galleries, id); 17263 17264 if (!gallery) { 17265 console.warn('No gallery found with ID:', id); 17266 if (galleryContainer) { 17267 galleryContainer.innerHTML = ` 17268 <div class="gallery-error"> 17269 <h3>Gallery Not Found</h3> 17270 <p>The requested gallery could not be found.</p> 17271 </div> 17272 `; 17273 galleryContainer.style.opacity = '1'; 17274 } 17275 return; 17276 } 17277 17278 if (isPageMenuItem(gallery)) { 17279 return typeof window.loadPage === 'function' ? window.loadPage(id, event) : loadPage(id, event); 17280 } 17281 17282 window._activeContentType = 'gallery'; 17283 17284 if (gallery.isSubmenu) { 17285 console.log('Gallery is submenu, skipping URL update'); 17286 toggleSubmenu(id, event || { stopPropagation: () => {} }); 17287 if (galleryContainer){ 17288 galleryContainer.style.opacity = '1'; 17289 } 17290 return; 17291 } 17292 17293 const contentLoadGeneration = beginHydraContentLoad('gallery', gallery.id, gallery.pageId); 17294 if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { 17295 window.PageManager.clearCurrentPage(); 17296 } 17297 17298 console.log('Found gallery:', gallery.title); 17299 17300 // Check for gallery-level password protection (only for visitors, not in edit mode) 17301 if (!isInEditMode() && gallery.passwordProtected) { 17302 // Use PageManager's gallery password protection methods 17303 if (window.PageManager && typeof window.PageManager.checkGalleryPasswordAccess === 'function' && 17304 typeof window.PageManager.createGalleryPasswordOverlay === 'function') { 17305 17306 if (!window.PageManager.checkGalleryPasswordAccess(gallery)) { 17307 console.log('Gallery is password protected, showing overlay'); 17308 window.PageManager.createGalleryPasswordOverlay(gallery, (accessGranted) => { 17309 if (accessGranted) { 17310 // Reload the gallery now that access is granted 17311 enhancedLoadGallery(id, event); 17312 } 17313 }); 17314 return; // Exit early, will reload after password is entered 17315 } 17316 } 17317 } 17318 17319 window.activeGalleryId = id; 17320 activeGalleryId = id; 17321 17322 // CRITICAL: Clean up page-embedded gallery scripts/DOM before loading a standalone gallery. 17323 // loadPage() calls removeGalleryScriptsWithPause which can interrupt EnhancedGallerySystem.initialize(), 17324 // leaving isLoading:true with a never-resolving initPromise. Re-run cleanup here (matching original 17325 // loadGallery) and clear the container so page content does not block gallery rendering. 17326 if (typeof window.removeGalleryScriptsWithPause === 'function') { 17327 await window.removeGalleryScriptsWithPause(); 17328 } 17329 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) { 17330 console.log('enhancedLoadGallery: Aborting stale load after cleanup for', gallery.id); 17331 return; 17332 } 17333 if (galleryContainer) { 17334 galleryContainer.innerHTML = ''; 17335 } 17336 17337 const bodyEl = document.body; 17338 if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 17339 bodyEl.classList.add('menu-hidden-on-page'); 17340 } else { 17341 bodyEl.classList.remove('menu-hidden-on-page'); 17342 } 17343 applyMenuOverlayClassForGallery(gallery); 17344 17345 try { 17346 if (typeof updateActiveStates === 'function') updateActiveStates(); 17347 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 17348 if (typeof closeMobileMenu === 'function') closeMobileMenu(); 17349 // Close the gallery options panel when navigating to a different gallery 17350 if (typeof window.closeOptionsPanel === 'function' && !forceGalleryReload) { 17351 window.closeOptionsPanel(); 17352 } 17353 const staleOptionsOverlay = document.getElementById('optionsOverlay'); 17354 if (staleOptionsOverlay) { 17355 staleOptionsOverlay.remove(); 17356 } 17357 window.optionsVisible = false;
17358 if (typeof updateURLWithGallerySlug === 'function') { 17359 updateURLWithGallerySlug(gallery); 17360 } 17361 } catch (uiError) { 17362 console.error('Error updating UI state:', uiError); 17363 } 17364 17365 try { 17366 // Add detailed logging about EnhancedGallerySystem availability 17367 console.log('ð Checking EnhancedGallerySystem availability:', { 17368 exists: !!window.EnhancedGallerySystem, 17369 hasLoadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'), 17370 initialized: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.initialized), 17371 isLoading: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.isLoading) 17372 }); 17373 17374 if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function') { 17375 console.log('â EnhancedGallerySystem available, calling loadGallery'); 17376 const result = await window.EnhancedGallerySystem.loadGallery( 17377 gallery, 17378 galleryContainer, 17379 { 17380 ...(options || {}), 17381 ...(forceGalleryReload ? { forceReload: true } : {}), 17382 contentLoadGeneration 17383 } 17384 ); 17385 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) { 17386 console.log('enhancedLoadGallery: Ignoring stale gallery completion for', gallery.id); 17387 return; 17388 } 17389 console.log('ð EnhancedGallerySystem.loadGallery result:', result); 17390 17391 if (!result && typeof originalLoadGallery === 'function') { 17392 console.log('â ï¸ Enhanced gallery loading failed, falling back to original function'); 17393 return originalLoadGallery(id, event, options); 17394 } 17395 if (galleryContainer) { 17396 galleryContainer.style.opacity = '1'; 17397 } 17398 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 17399 window.syncGalleryOptionsButtonVisibility(); 17400 setTimeout(window.syncGalleryOptionsButtonVisibility, 350); 17401 } 17402 } else { 17403 console.error('â EnhancedGallerySystem or its loadGallery method is not available.', { 17404 EnhancedGallerySystem: !!window.EnhancedGallerySystem, 17405 loadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'), 17406 useCdnProxy: window.useCdnProxy || false 17407 }); 17408 17409 // Fallback: Try using createGalleryContext directly 17410 console.log('ð Attempting fallback: createGalleryContext'); 17411 if (typeof createGalleryContext === 'function') { 17412 createGalleryContext(gallery, galleryContainer); 17413 if (galleryContainer) { 17414 galleryContainer.style.opacity = '1'; 17415 } 17416 return; 17417 } 17418 17419 if (galleryContainer) { 17420 galleryContainer.innerHTML = `<div class="gallery-error">Gallery system not available. EnhancedGallerySystem: ${!!window.EnhancedGallerySystem}, loadGallery: ${!!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function')}</div>`; 17421 galleryContainer.style.opacity = '1'; 17422 } 17423 return; 17424 } 17425 } catch (error) { 17426 console.error('Error loading gallery:', error); 17427 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) { 17428 console.log('enhancedLoadGallery: Ignoring stale gallery error for', gallery.id); 17429 return; 17430 } 17431 if (galleryContainer) { 17432 try { 17433 galleryContainer.innerHTML = ` 17434 <div class="gallery-error"> 17435 <h3>Error Loading Gallery</h3> 17436 <p>There was a problem loading the gallery. Please try again later.</p> 17437 <p class="error-details">${error.message}</p> 17438 </div> 17439 `; 17440 galleryContainer.style.opacity = '1'; 17441 } catch (containerError) { 17442 console.error('Error showing error message in container:', containerError); 17443 } 17444 } 17445 if (typeof originalLoadGallery === 'function') { 17446 console.log('Falling back to original loadGallery after error'); 17447 return originalLoadGallery(id, event, options); 17448 } 17449 } 17450 }; 17451 17452 /** 17453 * Initialization function to set up the gallery system 17454 */ 17455 function initializeGallerySystem() { 17456 console.log('Initializing enhanced gallery system'); 17457 17458 // Store reference to original function before replacing 17459 if (typeof window.loadGallery === 'function') { 17460 window.originalLoadGallery = window.loadGallery; 17461 } 17462 17463 // Replace the existing loadGallery function with our optimized version 17464 window.loadGallery = enhancedLoadGallery; 17465 17466 // Add this CSS for loading/error states 17467 addGallerySystemStyles(); 17468 17469 // Preload styles only at boot â scripts load when first gallery opens (with container ready) 17470 window.EnhancedGallerySystem.preloadStylesOnly().catch(error => { 17471 console.warn('Gallery styles preloading failed:', error); 17472 }); 17473 } 17474 17475 /** 17476 * Add CSS styles for loading indicators and error messages 17477 */ 17478 function addGallerySystemStyles() { 17479 const styleId = 'enhanced-gallery-system-styles'; 17480 17481 // Skip if already added 17482 if (document.getElementById(styleId)) { 17483 return; 17484 } 17485 17486 const style = document.createElement('style'); 17487 style.id = styleId; 17488 style.textContent = ` 17489 /* Loading indicator */ 17490 .gallery-loading { 17491 display: flex; 17492 flex-direction: column; 17493 justify-content: center; 17494 align-items: center; 17495 width: 100%; 17496 height: 100%; 17497 min-height: 200px; 17498 opacity: 0 !important; 17499 } 17500 17501 .gallery-loading .spinner { 17502 width: 50px; 17503 height: 50px; 17504 border: 3px solid rgba(0, 0, 0, 0.1); 17505 border-radius: 50%; 17506 border-top-color: #4682B4; 17507 animation: spin 1s ease-in-out infinite; 17508 margin-bottom: 20px; 17509 opacity: 0 !important; 17510 } 17511 17512 @keyframes spin { 17513 to { transform: rotate(360deg); } 17514 } 17515 17516 .gallery-loading .loading-text { 17517 font-family: Arial, sans-serif; 17518 font-size: 16px; 17519 color: #555; 17520 opacity: 0; 17521 } 17522 17523 /* Error message */ 17524 .gallery-error { 17525 display: flex; 17526 flex-direction: column; 17527 justify-content: center; 17528 align-items: center; 17529 width: 100%; 17530 height: 100%; 17531 min-height: 200px; 17532 padding: 30px; 17533 text-align: center; 17534 } 17535 17536 .gallery-error h3 { 17537 font-family: Arial, sans-serif; 17538 font-size: 24px; 17539 color:rgba(211, 47, 47, 0.25); 17540 margin-bottom: 20px; 17541 } 17542 17543 .gallery-error p { 17544 font-family: Arial, sans-serif; 17545 font-size: 16px; 17546 color: #555; 17547 max-width: 600px; 17548 margin-bottom: 10px; 17549 } 17550 17551 .gallery-error .error-details { 17552 font-size: 14px; 17553 color: #888; 17554 font-style: italic; 17555 } 17556 `; 17557 17558 document.head.appendChild(style); 17559 } 17560 17561 // Initialize gallery system immediately if possible 17562 initializeGallerySystem(); 17563})(); 17564 17565 17566/** 17567 * Enhanced Gallery Link Interceptor 17568 * Handles clicks on gallery links to prevent full page reloads 17569 */ 17570(function() { 17571 // Store any loaded gallery data for quick reference 17572 const GALLERY_DATA_CACHE = { 17573 initialized: false, 17574 galleries: [], 17575 lastUpdate: 0 17576 }; 17577 17578 // Initialize when DOM is ready to catch early clicks 17579 document.addEventListener('DOMContentLoaded', initGalleryLinkInterceptor); 17580 17581 // Also initialize when window loads to ensure complete gallery data 17582 window.addEventListener('load', function() { 17583 // Use a small delay to ensure galleries have fully initialized 17584 setTimeout(initGalleryLinkInterceptor, 200); 17585 }); 17586 17587 // Additional init on first user interaction 17588 document.addEventListener('click', function initOnFirstClick() { 17589 initGalleryLinkInterceptor(); 17590 // Remove this listener after first click 17591 document.removeEventListener('click', initOnFirstClick); 17592 }, { once: true }); 17593 17594 // Main initialization function 17595 function initGalleryLinkInterceptor() { 17596 // Prevent multiple initializations 17597 if (window._galleryLinkInterceptorInitialized) { 17598 // If already initialized, just refresh the gallery data 17599 refreshGalleryCache(); 17600 return; 17601 } 17602 17603 window._galleryLinkInterceptorInitialized = true; 17604 console.log('Initializing enhanced gallery link interceptor'); 17605 17606 // Update our cache with current galleries data 17607 refreshGalleryCache(); 17608 17609 // Get primary containers that might contain links 17610 const galleryContainer = document.querySelector('.gallery-container'); 17611 const mainContainer = document.querySelector('.container') || document.body; 17612 17613 if (!galleryContainer && !mainContainer) { 17614 console.warn('No gallery containers found, link interceptor partially initialized'); 17615 } 17616 17617 // Add global click handler to catch all link clicks 17618 document.addEventListener('click', handleGlobalLinkClick, true); 17619 17620 // Try to intercept links from iframes - critical for gallery content 17621 interceptAllIframeLinks(); 17622 17623 // Set up a mutation observer to handle dynamically added content 17624 setupMutationObserver(); 17625 17626 // Log initialization with site details 17627 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 17628 console.log(`Gallery link interceptor initialized. Preview mode: ${isPreviewMode}`); 17629 17630 // Create global helper for debugging 17631 window.debugGalleryLinks = function() { 17632 return { 17633 inPreviewMode: isPreviewMode, 17634 interceptorInitialized: window._galleryLinkInterceptorInitialized, 17635 cachedGalleries: GALLERY_DATA_CACHE.galleries.length, 17636 urlPath: window.location.pathname, 17637 siteGuid: getSiteGuidFromPath() 17638 }; 17639 }; 17640 } 17641 17642 // Update the gallery cache when gallery data changes 17643function refreshGalleryCache() { 17644 // Initialize GALLERY_DATA_CACHE if it doesn't exist 17645 if (!window.GALLERY_DATA_CACHE) { 17646 window.GALLERY_DATA_CACHE = { 17647 initialized: false, 17648 galleries: [], 17649 lastUpdate: 0 17650 }; 17651 } 17652 17653 // Check for global galleries variable first 17654 if (typeof galleries !== 'undefined' && Array.isArray(galleries)) { 17655 // Use the global galleries array 17656 window.GALLERY_DATA_CACHE.galleries = galleries; 17657 window.GALLERY_DATA_CACHE.initialized = true; 17658 window.GALLERY_DATA_CACHE.lastUpdate = Date.now(); 17659 window.galleries = galleries; 17660 17661 console.log(`Gallery cache refreshed with ${galleries.length} galleries from global variable`); 17662 return; 17663 } 17664 17665 // Debug log 17666 console.log(`Gallery cache refreshed with ${GALLERY_DATA_CACHE.galleries.length} galleries`); 17667 } 17668 17669 // Global click handler - catches all link clicks 17670 function handleGlobalLinkClick(e) {
17671 // Find if a link was clicked 17672 const link = e.target.closest('a'); 17673 if (!link || !link.href) return; 17674 17675 // Skip if the link uses special targets or modifiers 17676 if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return; 17677 17678 // Skip for hash links or javascript: links 17679 if (link.href.includes('#') || link.href.startsWith('javascript:')) return; 17680 17681 try { 17682 // Parse the URL to work with it 17683 const url = new URL(link.href); 17684 17685 // Only intercept links to the same domain 17686 if (url.hostname !== window.location.hostname) return; 17687 17688 // Skip API and resource URLs 17689 if (url.pathname.includes('/api/') || isResourceUrl(url.pathname)) return; 17690 17691 console.log('Link intercepted:', url.pathname); 17692 17693 // For preview URLs, we need to handle the path specially 17694 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 17695 const siteGuid = getSiteGuidFromPath(); 17696 17697 console.log('Link interceptor - isPreviewMode:', isPreviewMode, 'siteGuid:', siteGuid); 17698 17699 // Site Graph card links carry data-sg-target-id â use SPA navigation (not preview iframe fallback) 17700 if (link.classList.contains('sg-card-link') && link.closest('.site-graph-element') && link.dataset.sgTargetId) { 17701 let galleriesData = (typeof galleries !== 'undefined' && Array.isArray(galleries)) 17702 ? galleries 17703 : window.galleries; 17704 const sgItem = Array.isArray(galleriesData) 17705 ? findGalleryById(galleriesData, link.dataset.sgTargetId) 17706 : null; 17707 if (sgItem) { 17708 e.preventDefault(); 17709 e.stopPropagation(); 17710 if (sgItem.isPage && window.loadPage) { 17711 window.loadPage(sgItem.id); 17712 } else if (window.loadGallery) { 17713 window.loadGallery(sgItem.id); 17714 } 17715 let targetUrl = url.href; 17716 if (isPreviewMode && siteGuid) { 17717 const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 17718 url.pathname : 17719 `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; 17720 const newUrl = new URL(url); 17721 newUrl.pathname = pathname; 17722 targetUrl = newUrl.href; 17723 } 17724 window.history.pushState( 17725 { galleryId: sgItem.id }, 17726 sgItem.title || document.title, 17727 targetUrl 17728 ); 17729 document.title = formatGalleryDocumentTitle(sgItem.title); 17730 window.scrollTo(0, 0); 17731 return; 17732 } 17733 } 17734 17735 // Check if this is internal navigation first 17736 let matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid); 17737 17738 console.log('Link interceptor - matchingItem found:', !!matchingItem, matchingItem ? matchingItem.title : 'none'); 17739 17740 if (matchingItem) { 17741 // Prevent default navigation (full page reload) 17742 e.preventDefault(); 17743 e.stopPropagation(); 17744 17745 console.log('Found matching gallery for link:', matchingItem.title); 17746 17747 const nestedLinkModeActive = typeof window.isNestedGalleryLinkModeEnabled === 'function' 17748 ? window.isNestedGalleryLinkModeEnabled() 17749 : !!(window.neonGalleryConfig?.useLinks && window.neonGalleryConfig?.openMultipleLightboxes); 17750 const isGridGalleryLink = !!(link.closest('.masonry-item') || 17751 (link.closest('.gallery-wrapper, #neon-gallery-container, .gallery-direct-content, .masonry') && 17752 link.querySelector('img') && 17753 !link.closest('#neon-lightbox, .neon-lightbox'))); 17754 17755 if (!matchingItem.isPage && 17756 nestedLinkModeActive && 17757 isGridGalleryLink && 17758 typeof window.openNestedGalleryLightbox === 'function') { 17759 void window.openNestedGalleryLightbox(matchingItem, window.activeGalleryId).then((opened) => { 17760 if (!opened) { 17761 console.warn('Nested gallery lightbox failed â staying on parent gallery (no loadGallery fallback)'); 17762 } 17763 }); 17764 return; 17765 } 17766 17767 // Use the existing navigation functions 17768 if (matchingItem.isPage && window.loadPage) { 17769 window.loadPage(matchingItem.id); 17770 } else if (window.loadGallery) { 17771 window.loadGallery(matchingItem.id); 17772 } 17773 17774 // Construct proper URL for preview mode 17775 let targetUrl = url.href; 17776 if (isPreviewMode && siteGuid) { 17777 // Ensure we keep the GUID in the URL path 17778 const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 17779 url.pathname : 17780 `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; 17781 17782 const newUrl = new URL(url); 17783 newUrl.pathname = pathname; 17784 targetUrl = newUrl.href; 17785 } 17786 17787 // Update URL without reload 17788 window.history.pushState( 17789 { galleryId: matchingItem.id }, 17790 matchingItem.title || document.title, 17791 targetUrl 17792 ); 17793 17794 // Update page title 17795 document.title = formatGalleryDocumentTitle(matchingItem.title); 17796 17797 // Scroll to top 17798 window.scrollTo(0, 0); 17799 17800 return; 17801 } 17802 // Special case for preview mode - even if no matching gallery found 17803 else if (isPreviewMode && siteGuid) { 17804 // We're in preview mode with a GUID - intercept the navigation 17805 // to maintain the GUID in the URL. Only do this when #galleryFrame 17806 // can load content; otherwise allow SPA onclick handlers to run. 17807 17808 console.log('No direct match but in preview mode - handling navigation'); 17809 17810 const frame = document.getElementById('galleryFrame'); 17811 17812 if (!frame) { 17813 console.warn('Gallery frame not found - allowing default/SPA handlers'); 17814 } else { 17815 // Prevent default navigation 17816 e.preventDefault(); 17817 e.stopPropagation(); 17818 17819 // Construct a URL that preserves the GUID 17820 let newPathname = url.pathname; 17821 if (!newPathname.startsWith(`/${siteGuid}/`)) { 17822 // Add the GUID to the path if not already there 17823 newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`; 17824 } 17825 17826 // Create the preview-friendly URL for browser history 17827 const browserUrl = new URL(window.location.origin); 17828 browserUrl.pathname = newPathname; 17829 17830 console.log('Constructing preview URL with GUID:', browserUrl.href); 17831 17832 // IMPORTANT: For gallery navigation, we need to create a special URL 17833 // that tells the gallery system to load the content directly 17834 console.log('Loading preview path in iframe:', newPathname); 17835 17836 // First update browser URL to show the correct path 17837 window.history.pushState(
17838 { customPath: newPathname }, 17839 document.title, 17840 browserUrl.href 17841 ); 17842 17843 // Create direct path to the gallery content 17844 const cleanPath = newPathname.replace(`/${siteGuid}/`, '/'); 17845 17846 // For direct gallery paths, we need to load the gallery with special parameters 17847 // This tells the gallery system to load content from this path 17848 const galleryUrl = `https://storage.neonsky.app/sites:site_${siteGuid}.json`; 17849 17850 // Two approaches to loading the content: 17851 17852 // 1. Use the gallery wrapper approach 17853 const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 17854 `?siteId=${siteGuid}` + 17855 `&path=${encodeURIComponent(cleanPath)}` + 17856 `&directPath=true` + 17857 `&t=${Date.now()}`; // Cache buster 17858 17859 console.log('Loading gallery via wrapper URL:', wrapperUrl); 17860 frame.src = wrapperUrl; 17861 17862 // Clear any active gallery ID since we're navigating directly 17863 if (window.activeGalleryId) { 17864 console.log('Clearing active gallery ID during direct navigation'); 17865 window.activeGalleryId = null; 17866 } 17867 17868 return; // Critical: prevent further navigation 17869 } 17870 } 17871 17872 console.log('No matching gallery or special handling - allow normal navigation'); 17873 } catch (error) { 17874 console.error('Error processing link click:', error); 17875 } 17876 } 17877 17878 // Handle all iframes on the page 17879 function interceptAllIframeLinks() { 17880 // Look for any iframe that might contain gallery content 17881 const iframes = document.querySelectorAll('iframe'); 17882 17883 iframes.forEach(iframe => { 17884 if (iframe.id === 'galleryFrame' || iframe.classList.contains('gallery-iframe')) { 17885 // Primary gallery iframe - high priority 17886 setupIframeIntercept(iframe, true); 17887 } else { 17888 // Any other iframe - still try to intercept 17889 setupIframeIntercept(iframe, false); 17890 } 17891 }); 17892 17893 console.log(`Attempted to intercept links in ${iframes.length} iframes`); 17894 } 17895 17896 // Setup link interception for a specific iframe 17897 function setupIframeIntercept(iframe, isPrimary) { 17898 try { 17899 // Skip if iframe is not fully loaded yet 17900 if (!iframe.contentWindow || !iframe.contentDocument) { 17901 // Add load event to try again when loaded 17902 iframe.addEventListener('load', function() { 17903 setupIframeIntercept(iframe, isPrimary); 17904 }); 17905 return; 17906 } 17907 17908 // Try to access the iframe content - might fail due to same-origin policy 17909 try { 17910 const iframeDoc = iframe.contentDocument; 17911 const iframeWin = iframe.contentWindow; 17912 17913 // Add event listener for all link clicks in the iframe 17914 iframeDoc.addEventListener('click', function(e) { 17915 const link = e.target.closest('a'); 17916 if (!link || !link.href) return; 17917 17918 // Skip special cases 17919 if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return; 17920 if (link.href.includes('#') || link.href.startsWith('javascript:')) return; 17921 17922 try { 17923 const url = new URL(link.href); 17924 17925 // Check if same domain or relative URL 17926 const isSameDomain = url.hostname === window.location.hostname || 17927 url.hostname === iframe.src.hostname || 17928 !url.hostname; 17929 17930 if (isSameDomain) { 17931 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 17932 const siteGuid = getSiteGuidFromPath(); 17933 17934 // First try to find a matching gallery/page 17935 const matchingItem = parent.findGalleryByPath ? 17936 parent.findGalleryByPath(url.pathname, isPreviewMode, siteGuid) : 17937 findGalleryByPath(url.pathname, isPreviewMode, siteGuid); 17938 17939 if (matchingItem) { 17940 // We found a match - prevent default and navigate 17941 e.preventDefault(); 17942 e.stopPropagation(); 17943 17944 console.log('Iframe: Found matching content for link:', matchingItem.title); 17945 17946 // Access parent window functions 17947 if (matchingItem.isPage && parent.loadPage) { 17948 parent.loadPage(matchingItem.id); 17949 } else if (parent.loadGallery) { 17950 parent.loadGallery(matchingItem.id); 17951 } 17952 17953 // Update URL in parent window 17954 let targetUrl = url.href; 17955 if (isPreviewMode && siteGuid) { 17956 // Ensure GUID is preserved in URL 17957 const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 17958 url.pathname : 17959 `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; 17960 17961 const newUrl = new URL(window.location.origin + pathname); 17962 targetUrl = newUrl.href; 17963 } 17964 17965 // Update browser URL 17966 parent.history.pushState( 17967 { galleryId: matchingItem.id }, 17968 matchingItem.title || document.title, 17969 targetUrl 17970 ); 17971 17972 // Update parent window title 17973 parent.document.title = formatGalleryDocumentTitle(matchingItem.title, parent); 17974 17975 // Scroll parent to top 17976 parent.scrollTo(0, 0); 17977 return; 17978 } 17979 // Special handling for preview mode - critical fix here! 17980 else if (isPreviewMode && siteGuid) { 17981 // Special handling for preview URLs 17982 e.preventDefault(); 17983 e.stopPropagation(); 17984 17985 console.log('Iframe: No direct match but in preview mode - handling navigation'); 17986 17987 // Construct a URL that preserves the GUID 17988 let newPathname = url.pathname; 17989 if (!newPathname.startsWith(`/${siteGuid}/`)) { 17990 newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`; 17991 } 17992 17993 // Create a URL for the browser history 17994 const browserUrl = new URL(window.location.origin); 17995 browserUrl.pathname = newPathname; 17996 17997 console.log('Iframe: Constructing preview URL with GUID:', browserUrl.href); 17998 17999 // Update browser URL in parent 18000 parent.history.pushState(
18001 { customPath: newPathname }, 18002 document.title, 18003 browserUrl.href 18004 ); 18005 18006 // Handle the navigation in the parent window 18007 if (isPrimary && parent.document.getElementById('galleryFrame')) { 18008 const galleryFrame = parent.document.getElementById('galleryFrame'); 18009 18010 // Create clean path for the gallery system 18011 const cleanPath = newPathname.replace(`/${siteGuid}/`, '/'); 18012 18013 // Create wrapper URL for gallery content 18014 const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 18015 `?siteId=${siteGuid}` + 18016 `&path=${encodeURIComponent(cleanPath)}` + 18017 `&directPath=true` + 18018 `&t=${Date.now()}`; // Cache buster 18019 18020 console.log('Iframe: Loading gallery via wrapper URL:', wrapperUrl); 18021 galleryFrame.src = wrapperUrl; 18022 18023 // Clear any active gallery ID since we're navigating directly 18024 if (parent.activeGalleryId) { 18025 console.log('Iframe: Clearing active gallery ID during direct navigation'); 18026 parent.activeGalleryId = null; 18027 } 18028 } else { 18029 // Fallback - navigate normally in case we can't handle it 18030 console.log('Iframe: Gallery frame not found or not primary, navigating normally'); 18031 window.location.href = browserUrl.href; 18032 } 18033 return; 18034 } 18035 } 18036 } catch (iframeError) { 18037 console.error('Error handling iframe link:', iframeError); 18038 } 18039 }); 18040 18041 if (isPrimary) { 18042 console.log('Successfully intercepted links in primary gallery iframe'); 18043 } 18044 } catch (sameOriginError) { 18045 // This is expected for cross-origin iframes 18046 if (isPrimary) { 18047 console.warn('Cannot access iframe content due to same-origin policy. Using fallback method.'); 18048 18049 // Apply a fallback method - add postMessage listener 18050 setupPostMessageListener(iframe); 18051 } 18052 } 18053 } catch (error) { 18054 console.warn('Error setting up iframe link interception:', error); 18055 } 18056 } 18057 18058 // Fallback: use postMessage for cross-origin iframes 18059 function setupPostMessageListener(iframe) { 18060 // Listen for messages from the iframe 18061 window.addEventListener('message', function(event) { 18062 try { 18063 // Verify the message is from our iframe 18064 if (event.source !== iframe.contentWindow) return; 18065 18066 const data = event.data; 18067 18068 // Check if this is a link click event 18069 if (data && data.type === 'linkClick' && data.href) { 18070 console.log('Received link click message from iframe:', data.href); 18071 18072 // Create a URL object to work with the link 18073 const url = new URL(data.href); 18074 18075 // Check if it's a local navigation 18076 if (url.hostname === window.location.hostname) { 18077 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 18078 const siteGuid = getSiteGuidFromPath(); 18079 18080 // Try to find a matching gallery 18081 const matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid); 18082 18083 if (matchingItem) { 18084 console.log('Found matching gallery for postMessage link:', matchingItem.title); 18085 18086 // Navigate using the appropriate function 18087 if (matchingItem.isPage && window.loadPage) { 18088 window.loadPage(matchingItem.id); 18089 } else if (window.loadGallery) { 18090 window.loadGallery(matchingItem.id); 18091 } 18092 18093 // Update URL without reload 18094 let targetUrl = url.href; 18095 if (isPreviewMode && siteGuid) { 18096 // Ensure GUID is preserved 18097 const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 18098 url.pathname : 18099 `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; 18100 18101 const newUrl = new URL(window.location.origin + pathname); 18102 targetUrl = newUrl.href; 18103 } 18104 18105 window.history.pushState( 18106 { galleryId: matchingItem.id }, 18107 matchingItem.title || document.title, 18108 targetUrl 18109 ); 18110 18111 // Update page title 18112 document.title = formatGalleryDocumentTitle(matchingItem.title); 18113 18114 return; 18115 } 18116 // Special handling for preview mode - Fixed! 18117 else if (isPreviewMode && siteGuid) { 18118 console.log('No direct match but in preview mode - handling navigation via postMessage'); 18119 18120 // Construct URL that preserves the GUID 18121 let newPathname = url.pathname; 18122 if (!newPathname.startsWith(`/${siteGuid}/`)) { 18123 newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`; 18124 } 18125 18126 // Create browser URL 18127 const browserUrl = new URL(window.location.origin); 18128 browserUrl.pathname = newPathname; 18129 18130 // Update browser URL 18131 window.history.pushState(
18132 { customPath: newPathname }, 18133 document.title, 18134 browserUrl.href 18135 ); 18136 18137 // Get the main gallery frame - NOT the iframe that sent the message 18138 const galleryFrame = document.getElementById('galleryFrame'); 18139 if (galleryFrame) { 18140 // Create clean path without GUID 18141 const cleanPath = newPathname.replace(`/${siteGuid}/`, '/'); 18142 18143 // Create wrapper URL 18144 const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 18145 `?siteId=${siteGuid}` + 18146 `&path=${encodeURIComponent(cleanPath)}` + 18147 `&directPath=true` + 18148 `&t=${Date.now()}`; // Cache buster 18149 18150 console.log('Loading gallery via wrapper URL (postMessage):', wrapperUrl); 18151 galleryFrame.src = wrapperUrl; 18152 18153 // Clear any active gallery ID 18154 if (window.activeGalleryId) { 18155 window.activeGalleryId = null; 18156 } 18157 18158 return; 18159 } else { 18160 console.warn('Gallery frame not found - cannot load content via postMessage'); 18161 } 18162 } 18163 } 18164 } 18165 } catch (error) { 18166 console.error('Error processing postMessage from iframe:', error); 18167 } 18168 }); 18169 18170 // Inject a script into the iframe if possible to send messages back 18171 try { 18172 // We might be able to inject a script tag even in cross-origin scenarios 18173 // if the iframe is not yet loaded or using document.write 18174 iframe.addEventListener('load', function() { 18175 try { 18176 const script = iframe.contentDocument.createElement('script'); 18177 script.textContent = ` 18178 // Link interception script injected from parent 18179 document.addEventListener('click', function(e) { 18180 const link = e.target.closest('a'); 18181 if (link && link.href && !link.target && !e.ctrlKey && !e.metaKey && !e.shiftKey) { 18182 // Send message to parent window 18183 window.parent.postMessage({ 18184 type: 'linkClick', 18185 href: link.href, 18186 pathname: link.pathname 18187 }, '*'); 18188 18189 // Prevent default navigation 18190 e.preventDefault(); 18191 } 18192 }); 18193 console.log('Gallery link interceptor injected from parent'); 18194 `; 18195 iframe.contentDocument.head.appendChild(script); 18196 } catch (e) { 18197 // This will likely fail for cross-origin iframes 18198 console.log('Could not inject script into iframe - expected for cross-origin frames'); 18199 } 18200 }); 18201 } catch (error) { 18202 console.warn('Error trying to inject script into iframe:', error); 18203 } 18204 } 18205 18206 // Observer for new content/iframes 18207 function setupMutationObserver() { 18208 if (!window.MutationObserver) return; 18209 18210 const observer = new MutationObserver(function(mutations) { 18211 let newIframesFound = false; 18212 18213 mutations.forEach(function(mutation) { 18214 if (mutation.type === 'childList' && mutation.addedNodes.length) { 18215 // Check if any new iframes were added 18216 mutation.addedNodes.forEach(function(node) { 18217 // Check if the node is an iframe or contains iframes 18218 if (node.nodeName === 'IFRAME') { 18219 newIframesFound = true; 18220 setupIframeIntercept(node, node.id === 'galleryFrame'); 18221 } else if (node.querySelectorAll) { 18222 const iframes = node.querySelectorAll('iframe'); 18223 if (iframes.length > 0) { 18224 newIframesFound = true; 18225 iframes.forEach(function(iframe) { 18226 setupIframeIntercept(iframe, iframe.id === 'galleryFrame'); 18227 }); 18228 } 18229 } 18230 }); 18231 } 18232 }); 18233 18234 // If new iframes were found, update our cache 18235 if (newIframesFound) { 18236 refreshGalleryCache(); 18237 } 18238 }); 18239 18240 // Observe the entire document 18241 observer.observe(document.body, { 18242 childList: true, 18243 subtree: true 18244 }); 18245 } 18246 18247 // Helper to check if a URL is a resource rather than a page 18248 function isResourceUrl(path) { 18249 const resourceExtensions = [ 18250 '.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', 18251 '.css', '.js', '.pdf', '.ico', '.ttf', '.woff', '.woff2' 18252 ]; 18253 return resourceExtensions.some(ext => path.toLowerCase().endsWith(ext)); 18254 } 18255 18256 // Get the site GUID from the path in preview mode 18257 function getSiteGuidFromPath() { 18258 if (window.location.hostname === 'preview.neonsky.app') { 18259 const pathParts = window.location.pathname.split('/').filter(
18259Boolean); 18260 if (pathParts.length > 0) { 18261 return pathParts[0]; 18262 } 18263 } 18264 return null; 18265 } 18266 18267 18268/** 18269 * Enhanced gallery path matching with improved accuracy: 18270 * 18271 * This function finds the appropriate gallery based on URL path with better 18272 * prioritization and more precise matching criteria. 18273 * 18274 * @param {string} path - The URL path to match 18275 * @param {boolean} isPreviewMode - Whether we're in preview mode 18276 * @param {string} siteGuid - The site GUID (for preview mode) 18277 * @returns {Object|null} The matching gallery or null if none found 18278 */ 18279function findGalleryByPath(path, isPreviewMode, siteGuid) { 18280 // Ensure path is a string 18281 if (typeof path !== 'string') { 18282 console.warn('Invalid path provided to findGalleryByPath:', path); 18283 return null; 18284 } 18285 18286 // Determine which galleries array to use - check global variable first 18287 let galleriesData; 18288 let galleryCount = 0; 18289 18290 if (typeof galleries !== 'undefined' && Array.isArray(galleries)) { 18291 galleriesData = galleries; 18292 galleryCount = galleries.length; 18293 } else if (typeof window.galleries !== 'undefined' && Array.isArray(window.galleries)) { 18294 galleriesData = window.galleries; 18295 galleryCount = window.galleries.length; 18296 } else if (window.GALLERY_DATA_CACHE && 18297 window.GALLERY_DATA_CACHE.initialized && 18298 Array.isArray(window.GALLERY_DATA_CACHE.galleries)) { 18299 galleriesData = window.GALLERY_DATA_CACHE.galleries; 18300 galleryCount = window.GALLERY_DATA_CACHE.galleries.length; 18301 } else { 18302 console.warn('No galleries array available'); 18303 return null; 18304 } 18305 18306 console.log(`Searching for match among ${galleryCount} galleries`); 18307 18308 // Strip leading/trailing slashes without regex â template-literal embedding mangles / escapes 18309 // (e.g. /^/|/$/g becomes /^/|/$/g â 0), which left "/" unnormalized and broke home matching. 18310 function stripEdgeSlashes(value) { 18311 return String(value || '').split('/').filter(Boolean).join('/'); 18312 } 18313 18314 // Normalize the path for comparison (handle trailing/leading slashes) 18315 let normalizedPath = stripEdgeSlashes(path).toLowerCase(); 18316 const originalNormalizedPath = normalizedPath; // Keep original for logging 18317 18318 // If this is a preview URL, we need to handle the path differently 18319 if (isPreviewMode && siteGuid) { 18320 // Check if the path starts with the GUID 18321 if (normalizedPath.startsWith(siteGuid.toLowerCase() + '/')) { 18322 // Remove the GUID prefix 18323 normalizedPath = normalizedPath.substring(siteGuid.length + 1); 18324 } else if (normalizedPath === siteGuid.toLowerCase()) { 18325 // This is just the GUID with no additional path 18326 normalizedPath = ''; 18327 } 18328 } 18329 18330 // Create a dash-normalized version (all sequences of dashes become single dashes) 18331 // This handles both double-dash patterns and any other dash inconsistencies 18332 const normalizedPathWithSingleDashes = normalizedPath.replace(/-+/g, '-'); 18333 18334 console.log(`Normalized path: "${normalizedPath}" (dash-normalized: "${normalizedPathWithSingleDashes}")`); 18335 18336 // For empty paths, try to find the homepage 18337 if (!normalizedPath) { 18338 // Look for a gallery marked as home page 18339 const homePage = galleriesData.find(g => g.isHomePage === true); 18340 if (homePage) { 18341 console.log('Empty path - returning homepage:', homePage.title); 18342 return homePage; 18343 } 18344 } 18345 18346 // Array to store potential matches with their confidence levels 18347 const potentialMatches = []; 18348 18349 // Process each gallery to find potential matches with confidence levels 18350 galleriesData.forEach(gallery => { 18351 // Skip invalid galleries 18352 if (!gallery || !gallery.id) return; 18353 18354 // Store matching details for debugging 18355 const matchInfo = { 18356 gallery, 18357 confidence: 0, 18358 matchType: null, 18359 usedNormalizedDashes: false 18360 }; 18361 18362 // 1. EXACT SLUG MATCH (highest confidence) 18363 if (gallery.slug) { 18364 const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase(); 18365 18366 // Try original path 18367 if (gallerySlug === normalizedPath) { 18368 matchInfo.confidence = 100; // Max confidence 18369 matchInfo.matchType = 'exact_slug'; 18370 potentialMatches.push({...matchInfo}); 18371 return; // Continue to next gallery 18372 } 18373 18374 // Try dash-normalized path 18375 const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-'); 18376 if (normalizedPathWithSingleDashes === gallerySlugWithSingleDashes) { 18377 matchInfo.confidence = 95; // Very high confidence 18378 matchInfo.matchType = 'exact_slug_normalized_dashes'; 18379 matchInfo.usedNormalizedDashes = true; 18380 potentialMatches.push({...matchInfo}); 18381 return; // Continue to next gallery 18382 } 18383 } 18384 18385 // 2. EXACT URL PATH MATCH (very high confidence) 18386 if (gallery.url) { 18387 let galleryPath = gallery.url.toLowerCase(); 18388 18389 if (galleryPath.startsWith('http')) { 18390 try { 18391 const urlObj = new URL(galleryPath); 18392 galleryPath = urlObj.pathname; 18393 } catch (e) { 18394 // If URL parsing fails, use the string as is 18395 } 18396 } 18397 18398 galleryPath = stripEdgeSlashes(galleryPath); 18399 18400 // Try original path 18401 if (galleryPath === normalizedPath) { 18402 matchInfo.confidence = 90; // Very high confidence 18403 matchInfo.matchType = 'exact_url_path'; 18404 potentialMatches.push({...matchInfo}); 18405 return; // Continue to next gallery 18406 } 18407 18408 // Try dash-normalized path 18409 const galleryPathWithSingleDashes = galleryPath.replace(/-+/g, '-'); 18410 if (normalizedPathWithSingleDashes === galleryPathWithSingleDashes) { 18411 matchInfo.confidence = 85; // High confidence 18412 matchInfo.matchType = 'exact_url_path_normalized_dashes'; 18413 matchInfo.usedNormalizedDashes = true; 18414 potentialMatches.push({...matchInfo}); 18415 return; // Continue to next gallery 18416 } 18417 } 18418 18419 // 3. TITLE SLUG MATCH (high confidence) 18420 if (gallery.title) { 18421 const titleSlug = gallery.title.toLowerCase() 18422 .replace(/s+/g, '-') 18423 .replace(/[^w-]+/g, '') 18424 .replace(/--+/g, '-') 18425 .trim(); 18426 18427 // Try original path 18428 if (titleSlug === normalizedPath) { 18429 matchInfo.confidence = 80; // High confidence 18430 matchInfo.matchType = 'title_slug_match'; 18431 potentialMatches.push({...matchInfo}); 18432 return; // Continue to next gallery 18433 } 18434 18435 // Try dash-normalized path 18436 const titleSlugWithSingleDashes = titleSlug.replace(/-+/g, '-'); 18437 if (normalizedPathWithSingleDashes === titleSlugWithSingleDashes) { 18438 matchInfo.confidence = 75; // Good confidence 18439 matchInfo.matchType = 'title_slug_match_normalized_dashes'; 18440 matchInfo.usedNormalizedDashes = true; 18441 potentialMatches.push({...matchInfo}); 18442 return; // Continue to next gallery 18443 } 18444 } 18445 18446 // 4. PARTIAL SLUG MATCH - BUT WITH MUCH STRICTER CRITERIA 18447 if (gallery.slug && normalizedPath.length > 5) { 18448 const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase(); 18449 const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-'); 18450 18451 // Only if paths are substantial (to avoid false positives) 18452 if (normalizedPath.length >= 5 && gallerySlug.length >= 5) { 18453 // Try matching significant portions with dash normalization 18454 18455 // If the normalized paths contain each other with substantial overlap 18456 if (normalizedPathWithSingleDashes.includes(gallerySlugWithSingleDashes) || 18457 gallerySlugWithSingleDashes.includes(normalizedPathWithSingleDashes)) { 18458 18459 // Calculate overlap ratio based on the shorter string 18460 const overlapRatio = Math.min(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length) / 18461 Math.max(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length); 18462 18463 // Only consider matches with substantial overlap (over 70%) 18464 if (overlapRatio >= 0.7) { 18465 const score = Math.round(60 + (overlapRatio * 20)); // 60-80 based on overlap 18466 18467 matchInfo.confidence = score; 18468 matchInfo.matchType = 'partial_match_normalized'; 18469 matchInfo.usedNormalizedDashes = true; 18470 matchInfo.overlapRatio = overlapRatio; 18471 potentialMatches.push({...matchInfo}); 18472 } 18473 } 18474 } 18475 } 18476 }); 18477 18478 // No potential matches found 18479 if (potentialMatches.length === 0) { 18480 console.log('No gallery match found for path:', path); 18481 return null; 18482 } 18483 18484 // Sort potential matches by confidence (highest first) 18485 potentialMatches.sort((a, b) => b.confidence - a.confidence); 18486 18487 // Get the best match 18488 const bestMatch = potentialMatches[0]; 18489 18490 // Log detailed debugging info about the match 18491 console.log(`Best match for "${normalizedPath}":`, { 18492 title: bestMatch.gallery.title, 18493 id: bestMatch.gallery.id, 18494 matchType: bestMatch.matchType, 18495 confidence: bestMatch.confidence, 18496 usedNormalizedDashes: bestMatch.usedNormalizedDashes, 18497 totalMatches: potentialMatches.length 18498 }); 18499 18500 // If we have multiple close matches, log them for debugging 18501 if (potentialMatches.length > 1 && 18502 potentialMatches[1].confidence >= bestMatch.confidence - 10) { 18503 console.log('Close alternative matches:', potentialMatches.slice(1, 3).map(m => ({ 18504 title: m.gallery.title, 18505 id: m.gallery.id, 18506 matchType: m.matchType, 18507 confidence: m.confidence 18508 }
18508))); 18509 } 18510 18511 // Return the best matching gallery 18512 return bestMatch.gallery; 18513} 18514 18515 // Make helpful functions available globally 18516 window.findGalleryByPath = findGalleryByPath; 18517 window.findGalleryById = findGalleryById; 18518 window.refreshGalleryCache = refreshGalleryCache; 18519 window.getSiteGuidFromPath = getSiteGuidFromPath; 18520 if (typeof getSiteGalleriesArray === 'function') { 18521 window.getSiteGalleriesArray = getSiteGalleriesArray; 18522 } 18523})(); 18524 18525 18526 18527 </script>
18527 18528 </div> 18529 18530<!-- Sidebar element form for adding new elements --> 18531<div class="sidebar-element-form pe-edit-shell" id="sidebarElementForm" style="display: none;" data-nosnippet> 18532<p class="pe-edit-shell__title">Add sidebar element</p> 18533<p class="pe-hint">Select the type of element to add:</p> 18534 18535<div class="radio-group"> 18536 <label> 18537 <input type="radio" name="sidebarElementType" value="text" checked> Text Block 18538 </label> 18539 <label> 18540 <input type="radio" name="sidebarElementType" value="image"> Image 18541 </label> 18542 <label> 18543 <input type="radio" name="sidebarElementType" value="social"> Social Icons 18544 </label> 18545 <label> 18546 <input type="radio" name="sidebarElementType" value="spacer"> Spacer 18547 </label> 18548</div> 18549 18550<div class="pe-footer-actions"> 18551<button type="button" class="pe-btn pe-btn--primary" onclick="addSidebarElement()">Add element</button> 18552<button type="button" class="pe-btn" onclick="toggleSidebarElementForm()">Cancel</button> 18553</div> 18554</div> 18555 18556 <div class="add-form pe-edit-shell" id="addForm" data-nosnippet> 18557 <p class="pe-edit-shell__title">Add to menu</p> 18558 <input type="text" id="galleryTitle" placeholder="Gallery Title"> 18559 <div class="radio-group"> 18560 <label> 18561 <input type="radio" name="galleryType" value="integrated" checked> Gallery 18562 </label> 18563 <label> 18564 <input type="radio" name="galleryType" value="page"> Page 18565 </label> 18566 <label> 18567 <input type="radio" name="galleryType" value="folder"> Folder 18568 </label> 18569 <label> 18570 <input type="radio" name="galleryType" value="spacer"> Spacer 18571 </label> 18572 <label> 18573 <input type="radio" name="galleryType" value="external"> External URL 18574 </label> 18575</div> 18576 <div id="urlInputContainer"> 18577 <input type="text" id="galleryUrl" placeholder="Gallery URL"> 18578 </div> 18579 <div id="galleryParentContainer" style="display: none;" aria-hidden="true"> 18580 <label for="galleryParent">Parent Menu:</label> 18581 <select id="galleryParent"> 18582 <option value="">None (Top Level)</option> 18583 </select> 18584 </div> 18585 <div class="pe-footer-actions"> 18586 <button type="button" class="pe-btn pe-btn--primary" onclick="addGallery()">Save</button> 18587 </div> 18588
18588<script> 18589 18590 18591 // Add event listeners to the radio buttons 18592 document.addEventListener('DOMContentLoaded', () => { 18593 const radioButtons = document.querySelectorAll('input[name="galleryType"]'); 18594 radioButtons.forEach(radio => { 18595 radio.addEventListener('change', function() { 18596 const urlContainer = document.getElementById('urlInputContainer'); 18597 if (this.value === 'external') { 18598 urlContainer.style.display = 'block'; 18599 } else { 18600 urlContainer.style.display = 'none'; 18601 } 18602 }); 18603 }); 18604 }); 18605 </script>
18605 18606 </div> 18607 18608<div class="metadata-editor pe-edit-shell" id="metadataEditor" style="display: none; max-height: 90vh; overflow-y: auto;" data-nosnippet> 18609 <h3 class="pe-edit-shell__title">Site Metadata</h3> 18610 18611 <div class="form-group"> 18612 <label for="metaTitle">Page Title (Search Results & Browsers)</label> 18613 <input type="text" id="metaTitle" class="form-control" placeholder="Enter site title for search results"> 18614 <small>Recommended length: 50-60 characters</small> 18615 </div> 18616 18617 <div class="form-group"> 18618 <label for="metaDescription">Meta Description</label> 18619 <textarea id="metaDescription" class="form-control" rows="3" placeholder="Enter site description for search results"></textarea> 18620 <small>Recommended length: 150-160 characters</small> 18621 </div> 18622 18623 <div class="form-group"> 18624 <label for="ogImageUrl">Social preview image (Open Graph / X / iMessage)</label> 18625 <input type="hidden" id="ogImageUrl" class="form-control" value=""> 18626 <button type="button" id="uploadOgImageBtn" class="pe-btn upload-image-btn"> 18627 <svg style="width: 16px; height: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 18628 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> 18629 <polyline points="17 8 12 3 7 8"></polyline> 18630 <line x1="12" y1="3" x2="12" y2="15"></line> 18631 </svg> 18632 <span id="uploadOgImageBtnText">Upload image</span> 18633 </button> 18634 <div id="ogImagePreviewContainer"></div> 18635 <small>Recommended about 1200Ã630px. Optional: when you upload an image here, it is used for link previews site-wide. If you leave this empty, the first image on each page or gallery (including the home page) is used instead.</small> 18636 </div> 18637 18638 <div class="form-group"> 18639 <label for="googleAnalytics">Google Analytics Code</label> 18640 <input type="text" id="googleAnalytics" class="form-control" placeholder="Enter GA-XXXXXXXXXX"> 18641 <small>Example: G-XXXXXXXXXX or UA-XXXXXXXX-X</small> 18642 </div> 18643 18644 <div class="form-group"> 18645 <label for="contentLanguage">Content Language</label> 18646 <select id="contentLanguage" class="form-control"> 18647 <option value="en">English (en)</option> 18648 <option value="es">Spanish / Español (es)</option> 18649 <option value="fr">French / Français (fr)</option> 18650 <option value="ne">Nepali / नà¥à¤ªà¤¾à¤²à¥ (ne)</option> 18651 <option value="de">German / Deutsch (de)</option> 18652 <option value="it">Italian / Italiano (it)</option> 18653 <option value="pt">Portuguese / Português (pt)</option> 18654 <option value="ru">Russian / Ð ÑÑÑкий (ru)</option> 18655 <option value="zh">Chinese / 䏿 (zh)</option> 18656 <option value="ja">Japanese / æ¥æ¬èª (ja)</option> 18657 <option value="ko">Korean / íêµì´ (ko)</option> 18658 <option value="ar">Arabic / Ø§ÙØ¹Ø±Ø¨ÙØ© (ar)</option> 18659 <option value="hi">Hindi / हिनà¥à¤¦à¥ (hi)</option> 18660 </select> 18661 <small>Set the primary language of your site content. Chrome will offer to translate if visitor's language is different.</small> 18662 </div> 18663 18664 <div class="form-group"> 18665 <label> 18666 <input type="checkbox" id="noIndexToggle"> 18667 Hide site from search engines (noindex) 18668 </label> 18669 <small>Use this if the site is under development or shouldn't appear in search results</small> 18670 </div> 18671 18672 <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;"> 18673 <h4 style="margin-bottom: 8px;">Schema / Identity</h4> 18674 <p style="font-size: 13px; color: #555; margin-bottom: 12px;">Helps Google understand what this site represents â used in Knowledge Panels and local search results.</p> 18675 <label style="display: block; margin-bottom: 6px;">This site represents:</label> 18676 <div style="display: flex; gap: 20px; margin-bottom: 4px;"> 18677 <label style="display: flex; align-items: center; gap: 6px; font-weight: normal;"> 18678 <input type="radio" name="schemaType" id="schemaTypePerson" value="Person" onchange="updateSchemaFields()"> Person / Individual 18679 </label> 18680 <label style="display: flex; align-items: center; gap: 6px; font-weight: normal;"> 18681 <input type="radio" name="schemaType" id="schemaTypeOrganization" value="Organization" onchange="updateSchemaFields()"> Organization / Business 18682 </label> 18683 </div> 18684 <small>Select "Person" for individual photographers, artists, consultants. Select "Organization" for studios, agencies, companies.</small> 18685 </div> 18686 18687 <div class="form-group" id="schemaJobTitleGroup" style="display: none;"> 18688 <label for="schemaJobTitle">Job Title</label> 18689 <input type="text" id="schemaJobTitle" class="form-control" placeholder=""> 18690 <small>Used in Google Knowledge Panels and local search (e.g. "photojournalist Minneapolis").</small> 18691 </div> 18692 18693 <div class="form-group" id="schemaCityGroup" style="display: none;"> 18694 <label for="schemaCity">City / Location</label> 18695 <input type="text" id="schemaCity" class="form-control" placeholder=""> 18696 <small>Improves visibility in location-based searches.</small> 18697 </div> 18698 18699 <div class="form-group" id="schemaEmailGroup" style="display: none;"> 18700 <label for="schemaEmail">Contact Email (for structured data)</label> 18701 <input type="text" id="schemaEmail" class="form-control" placeholder="e.g. [email protected]"> 18702 <small>Embedded in structured data only â not displayed on the page.</small> 18703 </div> 18704 18705 <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;"> 18706 <h4 style="margin-bottom: 8px;">
18706Image licensing (optional)</h4> 18707 <p style="font-size: 13px; color: #555; margin-bottom: 12px;">Adds license URLs to gallery ImageObject structured data for Google Images. Leave both blank to keep current behavior. No badge is shown on your site.</p> 18708 <label for="imageLicenseUrl">License page URL</label> 18709 <input type="url" id="imageLicenseUrl" class="form-control" placeholder="https://example.com/licensing"> 18710 <small>A public page that states how images may be used (your terms, or a Creative Commons license).</small> 18711 </div> 18712 18713 <div class="form-group"> 18714 <label for="acquireLicensePageUrl">Acquire license page URL</label> 18715 <input type="url" id="acquireLicensePageUrl" class="form-control" placeholder="https://example.com/contact"> 18716 <small>A public page where someone can request or buy a license. Both URLs are required, or leave both empty.</small> 18717 </div> 18718 18719 <div class="form-group"> 18720 <label> 18721 <input type="checkbox" id="metadataCopyrightEnabled"> 18722 Enable sidebar copyright footer 18723 </label> 18724 <small>When enabled, the copyright text shows at the bottom of the sidebar.</small> 18725 </div> 18726 18727 <div class="form-group" id="metadataCopyrightTextGroup"> 18728 <label for="metadataCopyrightText">Copyright Text</label> 18729 <input type="text" id="metadataCopyrightText" class="form-control" placeholder="© 2025 Your Name"> 18730 <small>This text appears in the sidebar footer and inside the copyright overlay.</small> 18731 </div> 18732 18733 <div class="form-group"> 18734 <label for="faviconUrl">Favicon</label> 18735 <input type="hidden" id="faviconUrl" class="form-control"> 18736 <button type="button" id="uploadFaviconBtn" class="pe-btn upload-image-btn"> 18737 <svg style="width: 16px; height: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 18738 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> 18739 <polyline points="17 8 12 3 7 8"></polyline> 18740 <line x1="12" y1="3" x2="12" y2="15"></line> 18741 </svg> 18742 <span id="uploadFaviconBtnText">Upload Favicon</span> 18743 </button> 18744 <div id="faviconPreviewContainer"></div> 18745 <small>Upload an image file for your site favicon (recommended: 32x32px or 16x16px, PNG or ICO format)</small> 18746 </div> 18747 18748 <div class="form-group" id="customPublicSiteBypassGroup" hidden style="display: none; border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;"> 18749 <h4 style="margin-bottom: 12px;">Custom HTML site (full bypass)</h4> 18750 <p style="font-size: 13px; color: #555; margin-bottom: 12px;"> 18751 Upload one self-contained HTML file (e.g. from an AI tool). When enabled, visitors to the site root see only that file â not the Hydra menu or gallery. 18752 To log in and edit, open <strong>/_manage</strong> on your domain, then press <strong>+</strong> (numpad + or Shift+= on US keyboards) to log in or toggle edit mode, then open Site Metadata from the edit bar. 18753 To log out in this browser, visit <strong>/_manage?logout=1</strong> or <strong>/_manage/logout</strong> (also works as <code>?hydra_logout=1</code> on any page). 18754 </p> 18755 <label> 18756 <input type="checkbox" id="customPublicSiteEnabled" /> 18757 Serve uploaded HTML as the public site (bypass Hydra shell on <code>/</code>) 18758 </label> 18759 <input type="hidden" id="customPublicSiteHtmlR2Key" value="" /> 18760 <input type="hidden" id="customPublicSiteHtmlFilenameStore" value="" /> 18761 <div class="form-group" style="margin-top: 10px;"> 18762 <button type="button" id="uploadCustomPublicHtmlBtn" class="pe-btn">Upload HTML file</button> 18763 <button type="button" id="removeCustomPublicHtmlBtn" class="pe-btn">Clear file</button> 18764 </div> 18765 <p id="customPublicSiteHtmlFilenameLabel" style="font-size: 13px; color: #666; margin-top: 8px;">No HTML file uploaded yet.</p> 18766 </div> 18767 18768 <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;"> 18769 <h4 style="margin-bottom: 8px;">Site-wide password (browser login)</h4> 18770 <p style="font-size: 13px; color: #555; margin-bottom: 12px;"> 18771 When enabled, visitors must enter this username and password in the browserâs dialog before any page or asset loads (same credentials on your live domain and on 18772 <code>preview.neonsky.app/<your-site-id>/</code>). Editors still use Magic login to edit. Turn off to make the site fully public again. 18773 </p> 18774 <label> 18775 <input type="checkbox" id="siteWideBasicAuthEnabled" onchange="updateSiteWideBasicAuthFieldState()" /> 18776 Require HTTP Basic password for the entire site 18777 </label> 18778 <div id="siteWideBasicAuthFields" style="display: none; margin-top: 12px;"> 18779 <div class="form-group"> 18780 <label for="siteWideBasicAuthUsername">Username</label> 18781 <input type="text" id="siteWideBasicAuthUsername" class="form-control" autocomplete="username" /> 18782 </div> 18783 <div class="form-group"> 18784 <label for="siteWideBasicAuthPassword">Password</label> 18785 <input type="password" id="siteWideBasicAuthPassword" class="form-control" autocomplete="new-password" /> 18786 <small>Stored in your site configuration. Use a strong password; anyone with it can view the site.</small> 18787 </div> 18788 <div class="form-group"> 18789 <label for="siteWideBasicAuthRealm">Realm (optional)</label> 18790 <input type="text" id="siteWideBasicAuthRealm" class="form-control" placeholder="Site" /> 18791 <small>Label shown in the browser login dialog.</small> 18792 </div> 18793 </div> 18794 </div> 18795 18796 <div class="form-actions pe-footer-actions"> 18797 <button type="button" class="pe-btn pe-btn--primary" onclick="saveMetadata()">Save Metadata</button> 18798 <button type="button" class="pe-btn" onclick="toggleMetadataEditor()">Cancel</button> 18799 </div> 18800</div> 18801
18802<script> 18803var customPublicSiteOptionHeld = false; 18804var customPublicSiteOptionKeyBound = false; 18805 18806function isMetadataEditorOpen() { 18807 var editor = document.getElementById('metadataEditor'); 18808 if (!editor) return false; 18809 if (window.currentOpenForm === 'metadataEditor') return true; 18810 return editor.style.display === 'block'; 18811} 18812 18813function syncCustomPublicSiteBypassVisibility() { 18814 var group = document.getElementById('customPublicSiteBypassGroup'); 18815 if (!group) return; 18816 var show = isMetadataEditorOpen() && customPublicSiteOptionHeld; 18817 group.hidden = !show; 18818 group.style.display = show ? 'block' : 'none'; 18819} 18820 18821function bindCustomPublicSiteOptionKeyListeners() { 18822 if (customPublicSiteOptionKeyBound) return; 18823 customPublicSiteOptionKeyBound = true; 18824 18825 document.addEventListener('keydown', function(e) { 18826 if (!e.altKey) return; 18827 customPublicSiteOptionHeld = true; 18828 syncCustomPublicSiteBypassVisibility(); 18829 }, true); 18830 18831 document.addEventListener('keyup', function(e) { 18832 var isOptionRelease = 18833 e.key === 'Alt' || 18834 e.key === 'Option' || 18835 e.code === 'AltLeft' || 18836 e.code === 'AltRight'; 18837 if (!isOptionRelease) return; 18838 customPublicSiteOptionHeld = false; 18839 syncCustomPublicSiteBypassVisibility(); 18840 }, true); 18841} 18842 18843window.syncCustomPublicSiteBypassVisibility = syncCustomPublicSiteBypassVisibility; 18844window.bindCustomPublicSiteOptionKeyListeners = bindCustomPublicSiteOptionKeyListeners; 18845 18846function updateSiteWideBasicAuthFieldState() { 18847 const en = document.getElementById('siteWideBasicAuthEnabled'); 18848 const fields = document.getElementById('siteWideBasicAuthFields'); 18849 if (fields) fields.style.display = en && en.checked ? 'block' : 'none'; 18850} 18851 18852function updateSchemaFields() { 18853 const isPerson = document.getElementById('schemaTypePerson') && document.getElementById('schemaTypePerson').checked; 18854 const isOrg = document.getElementById('schemaTypeOrganization') && document.getElementById('schemaTypeOrganization').checked; 18855 const anySelected = isPerson || isOrg; 18856 const jobTitleGroup = document.getElementById('schemaJobTitleGroup'); 18857 const cityGroup = document.getElementById('schemaCityGroup'); 18858 const emailGroup = document.getElementById('schemaEmailGroup'); 18859 if (jobTitleGroup) jobTitleGroup.style.display = isPerson ? 'block' : 'none'; 18860 if (cityGroup) cityGroup.style.display = anySelected ? 'block' : 'none'; 18861 if (emailGroup) emailGroup.style.display = anySelected ? 'block' : 'none'; 18862} 18863 18864function uploadCustomPublicHtmlFile() { 18865 const siteId = window.Parameters && window.Parameters.siteId ? window.Parameters.siteId : (window.siteId || ''); 18866 if (!siteId) { 18867 alert('Site ID not found. Refresh the page and try again.'); 18868 return; 18869 } 18870 const authToken = window.ImageUploader && window.ImageUploader.getAuthToken ? window.ImageUploader.getAuthToken() : null; 18871 if (!authToken) { 18872 alert('Please log in first (press + on the page).'); 18873 return; 18874 } 18875 const input = document.createElement('input'); 18876 input.type = 'file'; 18877 input.accept = '.html,.htm,text/html'; 18878 input.addEventListener('change', function() { 18879 const file = input.files && input.files[0]; 18880 if (!file) return; 18881 const formData = new FormData(); 18882 formData.append('file', file); 18883 formData.append('guid', siteId); 18884 formData.append('purpose', 'custom-public-site'); 18885 formData.append('pageId', '_site'); 18886 formData.append('elementId', 'custom-public-html'); 18887 formData.append('folderPath', ''); 18888 var relUpload = typeof window.getApiUrl === 'function' ? window.getApiUrl('/api/upload-file') : '/api/upload-file'; 18889 var uploadUrl = window.location && window.location.origin ? new URL(relUpload, window.location.origin).href : relUpload; 18890 fetch(uploadUrl, { 18891 method: 'POST', 18892 headers: { 'Authorization': authToken, 'X-User-Email': localStorage.getItem('hydra_auth_email') || '' }, 18893 body: formData 18894 }).then(function(r) { 18895 return r.text().then(function(text) { 18896 var ct = (r.headers.get('content-type') || '').toLowerCase(); 18897 if (ct.indexOf('application/json') === -1 && /<!DOCTYPE/i.test(text)) { 18898 throw new Error( 18899 'Server returned HTML instead of JSON â POST /api/upload-file is not reaching the Hydra worker. ' + 18900 'In Cloudflare, ensure this hostname has a Worker route (e.g. neonsky.app/*) so API traffic is not sent to another host or Pages.' 18901 ); 18902 } 18903 if (r.status === 403 && ct.indexOf('application/json') === -1) { 18904 throw new Error( 18905 'Upload blocked (403). If this is an HTML file on preview or your domain, Cloudflare WAF may be blocking the request â add a custom rule to skip WAF for POST URI path containing /api/upload-file.' 18906 ); 18907 } 18908 try { 18909 return JSON.parse(text); 18910 } catch (parseErr) { 18911 throw new Error('Server did not return JSON: ' + text.slice(0, 120) + (text.length > 120 ? '...' : '')); 18912 } 18913 }); 18914 }).then(function(res) { 18915 if (res.success && res.key) { 18916 var keyEl = document.getElementById('customPublicSiteHtmlR2Key'); 18917 var fnEl = document.getElementById('customPublicSiteHtmlFilenameStore'); 18918 var lbl = document.getElementById('customPublicSiteHtmlFilenameLabel'); 18919 if (keyEl) keyEl.value = res.key; 18920 if (fnEl) fnEl.value = file.name; 18921 if (lbl) lbl.textContent = 'Current file: ' + file.name + ' â click Save Metadata below to apply.'; 18922 window.siteMetadata = window.siteMetadata || {}; 18923 window.siteMetadata.customPublicSiteHtmlR2Key = res.key; 18924 window.siteMetadata.customPublicSiteHtmlFilename = file.name; 18925 } else { 18926 alert(res.error || res.details || 'Upload failed'); 18927 } 18928 }).catch(function(e) { 18929 alert('Upload failed: ' + (e && e.message ? e.message : 'Unknown error')); 18930 }); 18931 }); 18932 input.click(); 18933} 18934 18935function clearCustomPublicHtmlFile() { 18936 if (!confirm('Clear the uploaded HTML file reference? (You can upload again later.)')) return; 18937 var k = document.getElementById('customPublicSiteHtmlR2Key'); 18938 var fn = document.getElementById('customPublicSiteHtmlFilenameStore'); 18939 var lbl = document.getElementById('customPublicSiteHtmlFilenameLabel'); 18940 if (k) k.value = ''; 18941 if (fn) fn.value = ''; 18942 if (lbl) lbl.textContent = 'No HTML file uploaded yet.'; 18943 if (window.siteMetadata) { 18944 window.siteMetadata.customPublicSiteHtmlR2Key = ''; 18945 window.siteMetadata.customPublicSiteHtmlFilename = ''; 18946 } 18947} 18948 18949// Handle favicon upload button click 18950document.addEventListener('DOMContentLoaded', function() { 18951 const uploadBtn = document.getElementById('uploadFaviconBtn'); 18952 if (uploadBtn) { 18953 uploadBtn.addEventListener('click', function() { 18954 uploadFavicon(); 18955 }); 18956 } 18957 const uploadOgBtn = document.getElementById('uploadOgImageBtn'); 18958 if (uploadOgBtn) { 18959 uploadOgBtn.addEventListener('click', function() { 18960 uploadSiteOgImage(); 18961 }); 18962 } 18963 var upHtml = document.getElementById('uploadCustomPublicHtmlBtn'); 18964 if (upHtml) upHtml.addEventListener('click', uploadCustomPublicHtmlFile); 18965 var rmHtml = document.getElementById('removeCustomPublicHtmlBtn'); 18966 if (rmHtml) rmHtml.addEventListener('click', clearCustomPublicHtmlFile); 18967 bindCustomPublicSiteOptionKeyListeners(); 18968 syncCustomPublicSiteBypassVisibility(); 18969 18970 // Handle content language dropdown change 18971 const contentLanguageSelect = document.getElementById('contentLanguage'); 18972 if (contentLanguageSelect) { 18973 contentLanguageSelect.addEventListener('change', function() { 18974 console.warn('Content language changed to:', this.value); 18975 // Update global variable immediately 18976 window.siteMetadata.contentLanguage = this.value; 18977 console.warn('Updated window.siteMetadata.contentLanguage:', window.siteMetadata.contentLanguage); 18978 }); 18979 } 18980 18981 const copyrightToggle = document.getElementById('metadataCopyrightEnabled'); 18982 if (copyrightToggle) { 18983 copyrightToggle.addEventListener('change', updateMetadataCopyrightFieldState); 18984 } 18985 18986 updateMetadataCopyrightFieldState(); 18987}); 18988 18989// Function to handle favicon upload using ImageUploader 18990function uploadFavicon() { 18991 console.log('Starting favicon upload...'); 18992 18993 // Check if ImageUploader is available 18994 if (!window.ImageUploader || typeof window.ImageUploader.open !== 'function') { 18995 alert('Image uploader not available. Please enter a favicon URL manually.'); 18996 return; 18997 } 18998 18999 try { 19000 // Create options for the ImageUploader 19001 const options = { 19002 title: 'Upload Favicon', 19003 containerType: 'favicon', 19004 maxWidth: 64, // Favicons are typically small 19005 maxHeight: 64, 19006 quality: 0.9 19007 }; 19008 19009 // Call the ImageUploader 19010 window.ImageUploader.open(options, function(imageResult, imageInfo) { 19011 console.log('Favicon upload callback received:', imageInfo); 19012 19013 // Get the favicon URL input field 19014 let faviconInput = document.getElementById('faviconUrl'); 19015 console.warn('=== UPLOAD CALLBACK DEBUG ==='); 19016 console.warn('Favicon input element found:', !!faviconInput); 19017 console.warn('Favicon input element type:', faviconInput ? faviconInput.type : 'N/A'); 19018 console.warn('Favicon input element id:', faviconInput ? faviconInput.id : 'N/A'); 19019 19020 // If element not found, wait a bit and try again (metadata editor might not be open yet) 19021 if (!faviconInput) { 19022 console.warn('Favicon input not found, waiting 100ms and trying again...'); 19023 setTimeout(() => { 19024 faviconInput = document.getElementById('faviconUrl'); 19025 console.warn('Retry - Favicon input element found:', !!faviconInput); 19026 if (faviconInput) { 19027 console.warn('Retry - Setting favicon input value to:', imageInfo.url); 19028 faviconInput.value = imageInfo.url; 19029 console.warn('Retry - Favicon input value after setting:', faviconInput.value); 19030 } else { 19031 console.error('Retry - Favicon input field still not found'); 19032 } 19033 }, 100); 19034 return; 19035 } 19036 19037 // Set the uploaded image URL 19038 if (imageInfo && imageInfo.type === 'r2' && imageInfo.url) { 19039 // R2 storage - use the URL 19040 console.warn('Setting favicon input value to:', imageInfo.url); 19041 faviconInput.value = imageInfo.url; 19042 console.warn('Favicon input value after setting:', faviconInput.value); 19043 19044 // Also store in global variable as backup 19045 window.uploadedFaviconUrl = imageInfo.url; 19046 console.warn('Stored favicon URL in global variable:', window.uploadedFaviconUrl); 19047 19048 console.log('Favicon URL set to:', imageInfo.url); 19049 19050 // Update button text 19051 const btnText = document.getElementById('uploadFaviconBtnText'); 19052 if (btnText) { 19053 btnText.textContent = 'Change Favicon'; 19054 } 19055 19056 // Show preview 19057 showFaviconPreview(imageInfo.url); 19058 } else if (imageResult) { 19059 // Legacy base64 storage 19060 faviconInput.value = imageResult; 19061 console.log('Favicon set to base64 data'); 19062 19063 // Update button text 19064 const btnText = document.getElementById('uploadFaviconBtnText'); 19065 if (btnText) { 19066 btnText.textContent = 'Change Favicon'; 19067 } 19068 19069 // Show preview 19070 showFaviconPreview(imageResult); 19071 } else { 19072 console.error('No image data received from uploader'); 19073 alert('Upload failed. Please try again.'); 19074 return; 19075 } 19076 19077 // Show success message 19078 console.log('Favicon uploaded successfully'); 19079 }); 19080 19081 } catch (error) { 19082 console.error('Error opening favicon uploader:', error); 19083 alert('Error opening uploader: ' + (error.message || 'Unknown error')); 19084 } 19085} 19086 19087// Function to show a preview of the uploaded favicon 19088function showFaviconPreview(imageUrl) { 19089 const container = document.getElementById('faviconPreviewContainer'); 19090 if (!container) { 19091 console.error('Favicon preview container not found'); 19092 return; 19093 } 19094 19095 container.innerHTML = 19096 '<div style="margin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; display: flex; align-items: center; gap: 15px;">' + 19097 '<img src="' + imageUrl + '" alt="Favicon Preview" style="width: 32px; height: 32px; border: 1px solid #ccc; background: white; padding: 4px;">' + 19098 '<div style="flex: 1;">' + 19099 '<div style="font-size: 14px; color: #333; font-weight: 500;">Current Favicon</div>' + 19100 '<div style="font-size: 12px; color: #666; margin-top: 2px;">Preview (32x32px)</div>' + 19101 '</div>' + 19102 '<button type="button" onclick="removeFavicon()" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' + 19103 'Remove' + 19104 '</button>' + 19105 '</div>'; 19106} 19107 19108// Function to remove the favicon 19109function removeFavicon() { 19110 const faviconInput = document.getElementById('faviconUrl'); 19111 if (faviconInput) { 19112 faviconInput.value = ''; 19113 } 19114 19115 const container = document.getElementById('faviconPreviewContainer'); 19116 if (container) { 19117 container.innerHTML = ''; 19118 } 19119 19120 const btnText = document.getElementById('uploadFaviconBtnText'); 19121 if (btnText) { 19122 btnText.textContent = 'Upload Favicon'; 19123 } 19124 19125 console.log('Favicon removed'); 19126} 19127 19128function uploadSiteOgImage() { 19129 if (!window.ImageUploader || typeof window.ImageUploader.open !== 'function') { 19130 alert('Image uploader not available. Refresh the page and try again.'); 19131 return; 19132 } 19133 try { 19134 window.ImageUploader.open({ 19135 title: 'Upload social preview image', 19136 containerType: 'page', 19137 quality: 0.92 19138 }, function(imageResult, imageInfo) { 19139 const inp = document.getElementById('ogImageUrl'); 19140 const setUrl = function(url) { 19141 if (inp) inp.value = url; 19142 const btnText = document.getElementById('uploadOgImageBtnText'); 19143 if (btnText) btnText.textContent = 'Change image'; 19144 showOgImagePreview(url); 19145 window.siteMetadata = window.siteMetadata || {}; 19146 window.siteMetadata.ogImageUrl = url; 19147 }; 19148 if (imageInfo && imageInfo.url) { 19149 setUrl(imageInfo.url); 19150 } else if (imageResult && typeof imageResult === 'string' && imageResult.indexOf('data:') === 0) { 19151 setUrl(imageResult); 19152 } else { 19153 alert('Upload failed. Please try again.'); 19154 } 19155 }); 19156 } catch (error) { 19157 console.error('Error opening OG image uploader:', error); 19158 alert('Error opening uploader: ' + (error.message || 'Unknown error')); 19159 } 19160} 19161 19162function showOgImagePreview(imageUrl) { 19163 const container = document.getElementById('ogImagePreviewContainer'); 19164 if (!container) return; 19165 container.innerHTML = 19166 '<div style="margin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; display: flex; align-items: center; gap: 15px;">' + 19167 '<img src="' + imageUrl + '" alt="Social preview" style="max-width: 200px; max-height: 105px; width: auto; height: auto; border: 1px solid #ccc; object-fit: contain; background: white;">' + 19168 '<div style="flex: 1;">' + 19169 '<div style="font-size: 14px; color: #333; font-weight: 500;">Current social preview image</div>' + 19170 '<div style="font-size: 12px; color: #666; margin-top: 2px;">This image overrides the first page/gallery image for social previews</div>' + 19171 '</div>' + 19172 '<button type="button" onclick="removeOgImage()" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' + 19173 'Remove' + 19174 '</button>' + 19175 '</div>'; 19176} 19177 19178function removeOgImage() { 19179 const inp = document.getElementById('ogImageUrl'); 19180 if (inp) inp.value = ''; 19181 if (window.siteMetadata) window.siteMetadata.ogImageUrl = ''; 19182 const container = document.getElementById('ogImagePreviewContainer'); 19183 if (container) container.innerHTML = ''; 19184 const btnText = document.getElementById('uploadOgImageBtnText'); 19185 if (btnText) btnText.textContent = 'Upload image'; 19186} 19187</script>
19187 19188 19189 <!-- Import Classic Form --> 19190<div class="import-classic-form" id="importClassicForm" style="display: none;" data-nosnippet> 19191 <h3>Import Galleries from JSON or Classic GUID</h3> 19192 <p>Paste the full site JSON into the text area below, <strong>OR</strong> enter the GUID of a classic gallery collection.</p> 19193 19194 <div class="form-group"> 19195 <label for="pastedJson">Pasted Site JSON:</label> 19196 <textarea id="pastedJson" rows="6" placeholder="Paste the entire site JSON here..."></textarea> 19197 </div> 19198 19199 <div class="import-separator">OR</div> 19200 19201 <div class="form-group"> 19202 <label for="classicGuid">Classic GUID or Import ID:</label> 19203 <input type="text" id="classicGuid" placeholder="Enter GUID (e.g., 4bd5ebf1b1418) or import ID (e.g., import_4c86452fb530b)"> 19204 <small>Use a GUID to fetch from classic site, or use import_ format to fetch from R2 storage.</small> 19205 </div> 19206 19207 <div class="form-group"> 19208 <label> 19209 <input type="checkbox" id="replaceMenuCheckbox" onchange="document.getElementById('importParent').disabled = this.checked; document.getElementById('createSubmenu').disabled = this.checked;"> 19210 Replace existing menu structure 19211 </label> 19212 <small>If checked, the current menu will be deleted and replaced with the imported items. If unchecked, imported items will be added to the current menu.</small> 19213 </div> 19214 19215 <div class="form-group"> 19216 <label for="importParent">Import under:</label> 19217 <select id="importParent"> 19218 <option value="">Top Level (No Parent)</option> 19219 </select> 19220 </div> 19221 19222 <div class="form-group"> 19223 <label> 19224 <input type="checkbox" id="createSubmenu" checked> 19225 Create submenu for all imported galleries 19226 </label> 19227 </div> 19228 19229 <div class="form-group" id="submenuTitleContainer"> 19230 <label for="submenuTitle">Submenu title:</label> 19231 <input type="text" id="submenuTitle" placeholder="e.g., Classic Galleries"> 19232 </div> 19233 19234 <div id="importStatus" class="import-status" style="display: none;"> 19235 <div class="import-progress"><div class="progress-bar"></div></div> 19236 <div class="status-message">Processing...</div> 19237 </div> 19238 19239 <div class="form-actions"> 19240 <button class="btn btn-primary" onclick="importClassicGalleries()">Import from Paste/GUID</button> 19241 <button class="btn" onclick="downloadCurrentSiteJSON()">Download Current Site JSON</button> 19242 <button class="btn" onclick="toggleImportClassicForm()">Cancel</button> 19243 </div> 19244</div> 19245 <div class="tree-container"> 19246 <div id="galleryTree" data-help-id="menu-tree"></div> 19247 </div> 19248 19249 <!-- Sidebar Elements Container will be inserted here by the sidebar-manager.js --> 19250 <div class="sidebar-elements-container"></div> 19251</div> 19252<div class="gallery-container" id="hydra-main-content" role="main" tabindex="-1"> 19253 <iframe class="gallery-iframe" id="galleryFrame" title="Book | Eirini Vourloumis"></iframe> 19254</div> 19255</div> 19256 19257<!-- Cookie Notice (compliance mode â only when Display Cookie Notice is enabled) --> 19258 19259
19260<script> 19261// Reveal content once layout is applied, CSS is loaded, AND gallery is initialized 19262(function() { 19263 let hasRevealed = false; // Ensure we only reveal once per page load 19264 19265 function revealContent() { 19266 if (hasRevealed) return; // Prevent multiple calls 19267 hasRevealed = true; 19268 document.documentElement.classList.add('hydra-ready'); 19269 document.body.classList.add('hydra-ready'); 19270 console.log('â Content revealed - hydra-ready class added'); 19271 } 19272 19273 // Check if this page has a gallery container 19274 function hasGalleryContainer() { 19275 return !!document.getElementById('neon-gallery-container') || 19276 !!document.querySelector('.gallery-wrapper') || 19277 !!document.querySelector('.masonry-container'); 19278 } 19279 19280 // Check if gallery is already initialized 19281 function isGalleryInitialized() { 19282 return window.neonGalleryState?.initialized || 19283 window.neonGalleryState?.domInitialized || 19284 (window.neonGalleryUtils && window.neonGalleryUtils.isInitialized && window.neonGalleryUtils.isInitialized()); 19285 } 19286 19287 // Main reveal logic 19288 function attemptReveal() { 19289 const hasGallery = hasGalleryContainer(); 19290 const galleryInitialized = isGalleryInitialized(); 19291 19292 // If no gallery, reveal immediately after CSS loads 19293 if (!hasGallery) { 19294 if (document.readyState === 'complete') { 19295 revealContent(); 19296 } else { 19297 window.addEventListener('load', revealContent, { once: true }); 19298 } 19299 return; 19300 } 19301 19302 // If gallery exists and is initialized, reveal 19303 if (hasGallery && galleryInitialized) { 19304 revealContent(); 19305 return; 19306 } 19307 19308 // If gallery exists but not initialized, wait for initialization 19309 if (hasGallery && !galleryInitialized) { 19310 console.log('â³ Waiting for gallery initialization before revealing content...'); 19311 19312 // Listen for gallery-loaded event 19313 window.addEventListener('gallery-loaded', function() { 19314 console.log('â Gallery-loaded event received'); 19315 setTimeout(revealContent, 100); // Small delay to ensure DOM is updated 19316 }, { once: true }); 19317 19318 // Listen for page-loaded event 19319 window.addEventListener('page-loaded', function() { 19320 console.log('â Page-loaded event received'); 19321 setTimeout(revealContent, 100); 19322 }, { once: true }); 19323 19324 // Fallback: Poll for gallery initialization (max 10 seconds) 19325 let pollAttempts = 0; 19326 const maxPollAttempts = 100; // 10 seconds (100 * 100ms) 19327 const pollInterval = setInterval(function() { 19328 pollAttempts++; 19329 if (isGalleryInitialized()) { 19330 console.log('â Gallery initialized detected via polling'); 19331 clearInterval(pollInterval); 19332 revealContent(); 19333 } else if (pollAttempts >= maxPollAttempts) { 19334 console.warn('â ï¸ Gallery initialization timeout - revealing content anyway'); 19335 clearInterval(pollInterval); 19336 revealContent(); // Reveal anyway to prevent blank screen 19337 } 19338 }, 100); 19339 19340 // Safety fallback: Always reveal after 8 seconds max (prevents permanent blank screen) 19341 setTimeout(function() { 19342 if (!hasRevealed) { 19343 console.warn('â ï¸ Safety timeout reached - revealing content to prevent blank screen'); 19344 revealContent(); 19345 } 19346 }, 8000); 19347 } 19348 } 19349 19350 // Start the reveal process 19351 if (document.readyState === 'loading') { 19352 document.addEventListener('DOMContentLoaded', function() { 19353 // Wait a bit for scripts to initialize 19354 setTimeout(attemptReveal, 100); 19355 }); 19356 } else if (document.readyState === 'interactive' || document.readyState === 'complete') { 19357 // DOM already ready, wait a bit for scripts 19358 setTimeout(attemptReveal, 100); 19359 } else { 19360 // Fallback 19361 window.addEventListener('load', attemptReveal, { once: true }); 19362 } 19363})(); 19364</script>
19364 19365
19366<script> 19367// ============================================ 19368// LANGUAGE TRANSLATION SYSTEM 19369// ============================================ 19370 19371(function() { 19372 // CRITICAL: Only initialize translation system if a languages element exists 19373 // This ensures sites without languages element have ZERO overhead 19374 const checkForLanguagesElement = function() { 19375 // Wait for SidebarManager to be initialized 19376 if (!window.SidebarManager || !window.SidebarManager.elements) { 19377 return false; 19378 } 19379 19380 // Check if any languages element exists 19381 const hasLanguagesElement = window.SidebarManager.elements.some(el => el.type === 'languages'); 19382 19383 if (!hasLanguagesElement) { 19384 console.warn('No languages element found - translation system disabled'); 19385 return false; 19386 } 19387 19388 return true; 19389 }; 19390 19391 // Check for languages element, retry if SidebarManager not ready yet 19392 let initAttempts = 0; 19393 const maxAttempts = 10; 19394 19395 function tryInitialize() { 19396 if (checkForLanguagesElement()) { 19397 console.warn('â Languages element found - initializing translation system'); 19398 initializeTranslationSystem(); 19399 } else { 19400 initAttempts++; 19401 if (initAttempts < maxAttempts) { 19402 // Retry in 500ms (total max wait: 5 seconds) 19403 setTimeout(tryInitialize, 500); 19404 } else { 19405 console.warn('Translation system not needed for this site - no languages element found after 5 seconds'); 19406 } 19407 } 19408 } 19409 19410 // Start checking after initial delay 19411 setTimeout(tryInitialize, 500); 19412 19413 function initializeTranslationSystem() { 19414 console.warn('â Translation system initialized successfully'); 19415 19416 let currentLanguage = localStorage.getItem('hydra_language') || 'en'; 19417 const originalTexts = new Map(); 19418 19419 // Expose test function for debugging 19420 window.testTranslation = function(langCode) { 19421 console.warn('𧪠Manual translation test triggered for:', langCode); 19422 console.warn('ð Dispatching test event...'); 19423 window.dispatchEvent(new CustomEvent('hydra:language-change', { 19424 detail: { code: langCode || 'es', name: 'Test' } 19425 })); 19426 console.warn('â Test event dispatched - check if listener caught it above'); 19427 }; 19428 19429 console.warn('â Event listener for "hydra:language-change" is now active'); 19430 19431 // Watch for edit mode changes and restore default language 19432 const editModeObserver = new MutationObserver(function(mutations) { 19433 mutations.forEach(function(mutation) { 19434 if (mutation.attributeName === 'class') { 19435 const isEditMode = document.body.classList.contains('edit-mode-active'); 19436 if (isEditMode) { 19437 console.warn('Edit mode activated - restoring default language'); 19438 // Restore original text 19439 restoreOriginalText(); 19440 19441 // Update language buttons to show default language 19442 const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages'); 19443 const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en'; 19444 19445 document.querySelectorAll('.language-button-live').forEach(btn => { 19446 if (btn.getAttribute('data-lang-code') === defaultLang) { 19447 btn.style.background = 'var(--menu-active-color, #4682B4)'; 19448 } else { 19449 btn.style.background = 'transparent'; 19450 } 19451 }); 19452 } 19453 } 19454 }); 19455 }); 19456 19457 // Start observing body for class changes 19458 if (document.body) { 19459 editModeObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] }); 19460 } 19461 19462 // Watch for dynamically loaded content (galleries, pages, etc.) and translate it 19463 const contentObserver = new MutationObserver(function(mutations) { 19464 // Only process if we're not in default language and not in edit mode 19465 const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages'); 19466 const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en'; 19467 const isEditMode = document.body.classList.contains('edit-mode-active'); 19468 19469 if (currentLanguage === defaultLang || isEditMode) { 19470 return; // Don't translate if in default language or edit mode 19471 } 19472 19473 // Check if new text content was added 19474 let hasNewTextContent = false;
19475 mutations.forEach(function(mutation) { 19476 if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 19477 mutation.addedNodes.forEach(function(node) { 19478 if (node.nodeType === Node.ELEMENT_NODE) { 19479 // Check if the added element contains text we should translate 19480 const textElements = node.querySelectorAll('p, h1, h2, h3, .page-text'); 19481 if (textElements.length > 0 || (node.textContent && node.textContent.trim().length > 0)) { 19482 hasNewTextContent = true; 19483 } 19484 } 19485 }); 19486 } 19487 }); 19488 19489 if (hasNewTextContent) { 19490 console.warn('New content detected - translating to:', currentLanguage); 19491 // Debounce to avoid translating multiple times rapidly 19492 clearTimeout(window._translationDebounce); 19493 window._translationDebounce = setTimeout(function() { 19494 translatePage(currentLanguage, defaultLang); 19495 }, 300); 19496 } 19497 }); 19498 19499 // Observe the gallery container and page content areas for new content 19500 const galleryContainer = document.querySelector('.gallery-container'); 19501 if (galleryContainer) { 19502 contentObserver.observe(galleryContainer, { 19503 childList: true, 19504 subtree: true 19505 }); 19506 } 19507 19508 // Also observe the main content area 19509 const mainContent = document.querySelector('.container'); 19510 if (mainContent) { 19511 contentObserver.observe(mainContent, { 19512 childList: true, 19513 subtree: true 19514 }); 19515 } 19516 19517 // Listen for language change events 19518 window.addEventListener('hydra:language-change', async function(e) { 19519 const { code, name } = e.detail; 19520 console.warn('ð Language changed to:', code, name); 19521 19522 // CRITICAL: Ignore language changes in edit mode 19523 if (document.body.classList.contains('edit-mode-active')) { 19524 console.warn('â ï¸ Edit mode active - ignoring language change'); 19525 // Reset to default language 19526 const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages'); 19527 const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en'; 19528 19529 // Update button states to show default language 19530 document.querySelectorAll('.language-button-live').forEach(btn => { 19531 if (btn.getAttribute('data-lang-code') === defaultLang) { 19532 btn.style.background = 'var(--menu-active-color, #4682B4)'; 19533 } else { 19534 btn.style.background = 'transparent'; 19535 } 19536 }); 19537 19538 // Show message to user 19539 console.warn('Language switching is disabled in edit mode'); 19540 return; 19541 } 19542 19543 // Update all language buttons 19544 document.querySelectorAll('.language-button-live').forEach(btn => { 19545 if (btn.getAttribute('data-lang-code') === code) { 19546 btn.style.background = 'var(--menu-active-color, #4682B4)'; 19547 } else { 19548 btn.style.background = 'transparent'; 19549 } 19550 }); 19551 19552 // Save preference 19553 localStorage.setItem('hydra_language', code); 19554 currentLanguage = code; 19555 19556 // Get default language from languages element 19557 const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages'); 19558 const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en'; 19559 19560 if (code === defaultLang) { 19561 // Restore original language 19562 restoreOriginalText(); 19563 } else { 19564 // Translate page 19565 await translatePage(code, defaultLang); 19566 } 19567 }); 19568 19569 async function translatePage(targetLang, sourceLang) { 19570 console.warn(`ð Translating page from ${sourceLang} to ${targetLang}`); 19571 19572 // Elements to translate 19573 const selectors = [ 19574 '.sidebar li a:not(.edit-mode-active .sidebar li a)', // Menu items (not in edit mode) 19575 '.page-text:not(.edit-mode-active .page-text)', // Page content (not in edit mode) 19576 'h1:not(.edit-mode-active h1)', 19577 'h2:not(.edit-mode-active h2)', 19578 'h3:not(.edit-mode-active h3)', 19579 'p:not(.edit-mode-active p)' 19580 ]; 19581 19582 const elements = document.querySelectorAll(selectors.join(', ')); 19583 19584 for (const element of elements) { 19585 // Skip if already translated to this language 19586 if (element.dataset.translatedTo === targetLang) continue; 19587 19588 // Skip if element is empty 19589 const text = element.textContent.trim(); 19590 if (!text || text.length === 0) continue; 19591 19592 // Store original text 19593 if (!originalTexts.has(element)) { 19594 originalTexts.set(element, text); 19595 } 19596 19597 // Check translation cache 19598 const cacheKey = `translate_${text}_${targetLang}`; 19599 let translated = getFromCache(cacheKey); 19600 19601 if (translated) { 19602 element.textContent = translated; 19603 element.dataset.translatedTo = targetLang; 19604 } else { 19605 // Translate 19606 try { 19607 translated = await translate(text, sourceLang, targetLang); 19608 if (translated && translated !== text) { 19609 element.textContent = translated; 19610 element.dataset.translatedTo = targetLang; 19611 saveToCache(cacheKey, translated); 19612 } 19613 } catch (error) { 19614 console.error('Translation error:', error); 19615 } 19616 } 19617 } 19618 } 19619 19620 function restoreOriginalText() {
19621 originalTexts.forEach((originalText, element) => { 19622 if (element && element.isConnected) { 19623 element.textContent = originalText; 19624 delete element.dataset.translatedTo; 19625 } 19626 }); 19627 } 19628 19629 async function translate(text, sourceLang, targetLang) { 19630 if (!text || text.trim().length === 0) return text; 19631 19632 // Try Browser Translation API (Free, Chrome 130+) 19633 if ('translation' in self && 'createTranslator' in self.translation) { 19634 try { 19635 console.warn('ð Using Browser Translation API'); 19636 const translator = await self.translation.createTranslator({ 19637 sourceLanguage: sourceLang, 19638 targetLanguage: targetLang 19639 }); 19640 const result = await translator.translate(text); 19641 console.warn('â Translation successful via Browser API'); 19642 return result; 19643 } catch (error) { 19644 console.warn('â Browser translation failed:', error); 19645 } 19646 } else { 19647 console.warn('â ï¸ Browser Translation API not available'); 19648 console.warn(' To enable in Chrome: chrome://flags/#translation-api'); 19649 console.warn(' OR add Google Translate API key to use paid translation'); 19650 } 19651 19652 // TODO: Add Google Translate API fallback here 19653 // For now, return original text 19654 console.warn('â ï¸ No translation provider configured - returning original text'); 19655 return text; 19656 } 19657 19658 // Cache management 19659 function getFromCache(key) { 19660 try { 19661 const item = localStorage.getItem(`hydra_${key}`); 19662 if (!item) return null; 19663 19664 const { value, expiry } = JSON.parse(item); 19665 if (expiry && Date.now() > expiry) { 19666 localStorage.removeItem(`hydra_${key}`); 19667 return null; 19668 } 19669 return value; 19670 } catch { 19671 return null; 19672 } 19673 } 19674 19675 function saveToCache(key, value) { 19676 try { 19677 const item = { 19678 value, 19679 expiry: Date.now() + (30 * 24 * 60 * 60 * 1000) // 30 days 19680 }; 19681 localStorage.setItem(`hydra_${key}`, JSON.stringify(item)); 19682 } catch (error) { 19683 console.warn('Cache save failed:', error); 19684 } 19685 } 19686 19687 } // End of initializeTranslationSystem 19688})(); 19689</script>
19689 19690 19691 19692
19693<script> 19694window.HYDRA_HELP_ENABLED = true; 19695function loadHelpSystemScript() { 19696 if (window.HelpSystem && window.HelpSystem.openHub) return Promise.resolve(); 19697 return new Promise(function(resolve, reject) { 19698 var existing = document.querySelector('script[data-hydra-help-system]'); 19699 if (existing) { 19700 existing.addEventListener('load', function() { resolve(); }); 19701 existing.addEventListener('error', reject); 19702 return; 19703 } 19704 var s = document.createElement('script'); 19705 s.src = '/api/help-system.js?v=686c85345749'; 19706 s.async = true; 19707 s.setAttribute('data-hydra-help-system', '1'); 19708 s.onload = function() { resolve(); }; 19709 s.onerror = reject; 19710 document.head.appendChild(s); 19711 }); 19712} 19713function openHydraHelp(opts) { 19714 opts = opts || {}; 19715 if (!window.HYDRA_HELP_ENABLED) return; 19716 var token = window.didToken || localStorage.getItem('hydra_auth_token'); 19717 var isAdmin = window.isAdmin || localStorage.getItem('hydra_is_admin') === 'true'; 19718 if (!token || !isAdmin) return; 19719 var p = loadHelpSystemScript(); 19720 Promise.resolve(p).then(function() { 19721 if (window.HelpSystem && window.HelpSystem.init) return window.HelpSystem.init(); 19722 }).then(function() { 19723 if (window.HelpSystem && window.HelpSystem.openHub) { 19724 window.HelpSystem.openHub(opts.initialTab ? { initialTab: opts.initialTab } : undefined); 19725 } 19726 }).catch(function(err) { 19727 console.error('HelpSystem init failed:', err); 19728 }); 19729} 19730window.openHydraHelp = openHydraHelp; 19731</script>
19731 19732
19733<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"5f8c17677ba84f8d88ba3d46ef47b608","spa":2}' crossorigin="anonymous"></script>
19733 19734</body> 19735</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.