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="f21s3uk4"> 7 8 <!-- Primary Meta Tags --> 9 <title>ATHLETES | People - Adena Stevens Photography</title> 10 <meta name="description" content="ATHLETES â People - Adena Stevens Photography."> 11 <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"> 12 <meta name="author" content="adenastevens.com"> 13 <meta name="language" content="en"> 14 15 <!-- Canonical URL --> 16 <link rel="canonical" href="https://adenastevens.com/athletes"> 17 18 <!-- Favicon --> 19 20 21 22 23 24 <!-- Open Graph / Facebook / iMessage / Discord --> 25 <meta property="og:type" content="article"> 26 <meta property="og:url" content="https://adenastevens.com/athletes"> 27 <meta property="og:title" content="ATHLETES | People - Adena Stevens Photography"> 28 <meta property="og:description" content="ATHLETES â People - Adena Stevens Photography."> 29 <meta property="og:site_name" content="adenastevens.com"> 30 <meta property="og:locale" content="en_US"> 31 <meta property="og:image" content="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_jdi7x8_Athletes-001_basketballA.jpg"> 32 <meta property="og:image:secure_url" content="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_jdi7x8_Athletes-001_basketballA.jpg"> 33 <meta property="og:image:width" content="1200"> 34 <meta property="og:image:height" content="630"> 35 <meta property="og:image:type" content="image/jpeg"> 36 <meta property="og:image:alt" content="ATHLETES | People - Adena Stevens Photography"> 37 38 <!-- Twitter / X Card --> 39 <meta name="twitter:card" content="summary_large_image"> 40 <meta name="twitter:site" content="@adenastevens.com"> 41 <meta name="twitter:url" content="https://adenastevens.com/athletes"> 42 <meta name="twitter:title" content="ATHLETES | People - Adena Stevens Photography"> 43 <meta name="twitter:description" content="ATHLETES â People - Adena Stevens Photography."> 44 <meta name="twitter:image" content="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_jdi7x8_Athletes-001_basketballA.jpg"> 45 <meta name="twitter:image:alt" content="ATHLETES | People - Adena Stevens Photography"> 46 47 <!-- Additional SEO Meta Tags --> 48 <meta name="theme-color" content="#000000"> 49 <meta name="msapplication-TileColor" content="#000000"> 50 <meta name="apple-mobile-web-app-capable" content="yes"> 51 <meta name="apple-mobile-web-app-status-bar-style" content="default"> 52 <meta name="apple-mobile-web-app-title" content="ATHLETES | People - Adena Stevens Photography"> 53 54 <!-- Security Headers --> 55 <meta http-equiv="X-Content-Type-Options" content="nosniff"> 56 <meta http-equiv="X-XSS-Protection" content="1; mode=block"> 57 <style> 58 /* FOUC gate: opacity on <html> cannot be pierced by child visibility:!important (submenu CSS) */ 59 html:not(.hydra-ready) { opacity: 0 !important; } 60 /* Critical: hide nested menus before menu-styles.css arrives */ 61 li:not(.expanded) > ol.nested-sortable { display: none !important; } 62 .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; } 63 .hydra-seo-nav ul { list-style:none;margin:0;padding:0; } 64 .hydra-skip-link { position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden; } 65 </style> 66 <noscript><style>html { opacity: 1 !important; }</style></noscript> 67
67<script type="application/ld+json"> 68 { 69 "@context": "https://schema.org", 70 "@type": [ 71 "WebPage", 72 "ImageGallery" 73 ], 74 "name": "ATHLETES | People - Adena Stevens Photography", 75 "description": "ATHLETES â People - Adena Stevens Photography.", 76 "url": "https://adenastevens.com/athletes", 77 "datePublished": "2026-10-05T22:01:27.500Z", 78 "dateModified": "2026-10-05T22:01:27.500Z", 79 "inLanguage": "en-US", 80 "isPartOf": { 81 "@type": "WebSite", 82 "@id": "https://adenastevens.com/#website", 83 "name": "adenastevens.com", 84 "url": "https://adenastevens.com/", 85 "description": "ATHLETES â People - Adena Stevens Photography.", 86 "publisher": { 87 "@type": "Organization", 88 "name": "adenastevens.com", 89 "url": "https://adenastevens.com/" 90 } 91 }, 92 "breadcrumb": { 93 "@type": "BreadcrumbList", 94 "itemListElement": [ 95 { 96 "@type": "ListItem", 97 "position": 1, 98 "name": "Home", 99 "item": "https://adenastevens.com/" 100 }, 101 { 102 "@type": "ListItem", 103 "position": 2, 104 "name": "ATHLETES | People - Adena Stevens Photography", 105 "item": "https://adenastevens.com/athletes" 106 } 107 ] 108 }, 109 "associatedMedia": [ 110 { 111 "@type": "ImageObject", 112 "name": "f21s3uk4 an5m6dbk jdi7x8 Athletes 001 basketballA", 113 "description": "", 114 "caption": "f21s3uk4 an5m6dbk jdi7x8 Athletes 001 basketballA", 115 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_jdi7x8_Athletes-001_basketballA.jpg" 116 }, 117 { 118 "@type": "ImageObject", 119 "name": "f21s3uk4 an5m6dbk 8bymb5 Athletes 002 stevens 018", 120 "description": "", 121 "caption": "f21s3uk4 an5m6dbk 8bymb5 Athletes 002 stevens 018", 122 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_8bymb5_Athletes-002_stevens_018.jpg" 123 }, 124 { 125 "@type": "ImageObject", 126 "name": "f21s3uk4 an5m6dbk kbdx46 Athletes 003 hsgsoc099d 1920w", 127 "description": "", 128 "caption": "f21s3uk4 an5m6dbk kbdx46 Athletes 003 hsgsoc099d 1920w", 129 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_kbdx46_Athletes-003_hsgsoc099d-1920w.jpg" 130 }, 131 { 132 "@type": "ImageObject", 133 "name": "f21s3uk4 an5m6dbk ggcqhp Athletes 004 boys soccer 1920w", 134 "description": "", 135 "caption": "f21s3uk4 an5m6dbk ggcqhp Athletes 004 boys soccer 1920w", 136 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_ggcqhp_Athletes-004_boys_soccer-1920w.jpg" 137 }, 138 { 139 "@type": "ImageObject", 140 "name": "f21s3uk4 an5m6dbk a9ajqg Athletes 005 sports04", 141 "description": "", 142 "caption": "f21s3uk4 an5m6dbk a9ajqg Athletes 005 sports04", 143 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_a9ajqg_Athletes-005_sports04.jpg" 144 }, 145 { 146 "@type": "ImageObject", 147 "name": "f21s3uk4 an5m6dbk 40o8tu Athletes 006 softball2 1920w", 148 "description": "", 149 "caption": "f21s3uk4 an5m6dbk 40o8tu Athletes 006 softball2 1920w", 150 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_40o8tu_Athletes-006_softball2-1920w.jpg" 151 }, 152 { 153 "@type": "ImageObject", 154 "name": "f21s3uk4 an5m6dbk pfew5a Athletes 007 basketball3", 155 "description": "", 156 "caption": "f21s3uk4 an5m6dbk pfew5a Athletes 007 basketball3", 157 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_pfew5a_Athletes-007_basketball3.jpg" 158 }, 159 { 160 "@type": "ImageObject", 161 "name": "f21s3uk4 an5m6dbk qy7wn9 Athletes 008 college basketball 01", 162 "description": "", 163 "caption": "f21s3uk4 an5m6dbk qy7wn9 Athletes 008 college basketball 01", 164 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_qy7wn9_Athletes-008_college-basketball-01.jpg" 165 }, 166 { 167 "@type": "ImageObject", 168 "name": "f21s3uk4 an5m6dbk ds305y Athletes 010 sports07", 169 "description": "", 170 "caption": "f21s3uk4 an5m6dbk ds305y Athletes 010 sports07", 171 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_ds305y_Athletes-010_sports07.jpg" 172 }, 173 { 174 "@type": "ImageObject", 175 "name": "f21s3uk4 an5m6dbk vbj1zs Athletes 011 volleyballA 1920w", 176 "description": "", 177 "caption": "f21s3uk4 an5m6dbk vbj1zs Athletes 011 volleyballA 1920w", 178 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_vbj1zs_Athletes-011_volleyballA-1920w.jpg" 179 }, 180 { 181 "@type": "ImageObject", 182 "name": "f21s3uk4 an5m6dbk 3yl151 Athletes 012 baseball3", 183 "description": "", 184 "caption": "f21s3uk4 an5m6dbk 3yl151 Athletes 012 baseball3", 185 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_3yl151_Athletes-012_baseball3.jpg" 186 }, 187 { 188 "@type": "ImageObject", 189 "name": "f21s3uk4 an5m6dbk 60t74z Athletes 013 volleyball 1920w", 190 "description": "", 191 "caption": "f21s3uk4 an5m6dbk 60t74z Athletes 013 volleyball 1920w", 192 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_60t74z_Athletes-013_volleyball-1920w.jpg" 193 }, 194 { 195 "@type": "ImageObject", 196 "name": "f21s3uk4 an5m6dbk f2yfb3 Athletes 014 golfAS0531a 1920w", 197 "description": "", 198 "caption": "f21s3uk4 an5m6dbk f2yfb3 Athletes 014 golfAS0531a 1920w", 199 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_f2yfb3_Athletes-014_golfAS0531a-1920w.jpg" 200 }, 201 { 202 "@type": "ImageObject", 203 "name": "f21s3uk4 an5m6dbk o2kuzl Athletes 015 christine caron", 204 "description": "", 205 "caption": "f21s3uk4 an5m6dbk o2kuzl Athletes 015 christine caron", 206 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_o2kuzl_Athletes-015_christine_caron.jpg" 207 }, 208 { 209 "@type": "ImageObject", 210 "name": "f21s3uk4 an5m6dbk r6n52d Athletes 016 softball3", 211 "description": "", 212 "caption": "f21s3uk4 an5m6dbk r6n52d Athletes 016 softball3", 213 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_r6n52d_Athletes-016_softball3.jpg" 214 }, 215 { 216 "@type": "ImageObject", 217 "name": "f21s3uk4 an5m6dbk udxl72 Athletes 017 stevens 033", 218 "description": "", 219 "caption": "f21s3uk4 an5m6dbk udxl72 Athletes 017 stevens 033", 220 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_udxl72_Athletes-017_stevens_033.jpg" 221 }, 222 { 223 "@type": "ImageObject", 224 "name": "f21s3uk4 an5m6dbk trbiqh Athletes 018 row2 1920w", 225 "description": "", 226 "caption": "f21s3uk4 an5m6dbk trbiqh Athletes 018 row2 1920w", 227 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_trbiqh_Athletes-018_row2-1920w.jpg" 228 }, 229 { 230 "@type": "ImageObject", 231 "name": "f21s3uk4 an5m6dbk 2f1ypc Athletes 019 fencing 1920w", 232 "description": "", 233 "caption": "f21s3uk4 an5m6dbk 2f1ypc Athletes 019 fencing 1920w", 234 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_2f1ypc_Athletes-019_fencing-1920w.jpg" 235 }, 236 { 237 "@type": "ImageObject", 238 "name": "f21s3uk4 an5m6dbk hhiwjj Athletes 020 cheer01 1920w", 239 "description": "", 240 "caption": "f21s3uk4 an5m6dbk hhiwjj Athletes 020 cheer01 1920w", 241 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_hhiwjj_Athletes-020_cheer01-1920w.jpg" 242 }, 243 { 244 "@type": "ImageObject", 245 "name": "f21s3uk4 an5m6dbk f2upki Athletes 021 julian hightower", 246 "description": "", 247 "caption": "f21s3uk4 an5m6dbk f2upki Athletes 021 julian hightower", 248 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_f2upki_Athletes-021_julian_hightower.jpg" 249 }, 250 { 251 "@type": "ImageObject", 252 "name": "f21s3uk4 an5m6dbk 82az38 Athletes 022 softball 1920w", 253 "description": "", 254 "caption": "f21s3uk4 an5m6dbk 82az38 Athletes 022 softball 1920w", 255 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_82az38_Athletes-022_softball-1920w.jpg" 256 }, 257 { 258 "@type": "ImageObject", 259 "name": "f21s3uk4 an5m6dbk kvy9i5 Athletes 023 volleyball2 1920w", 260 "description": "", 261 "caption": "f21s3uk4 an5m6dbk kvy9i5 Athletes 023 volleyball2 1920w", 262 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_kvy9i5_Athletes-023_volleyball2-1920w.jpg" 263 }, 264 { 265 "@type": "ImageObject", 266 "name": "f21s3uk4 an5m6dbk bstl5x Athletes 024 sports01 1920w", 267 "description": "", 268 "caption": "f21s3uk4 an5m6dbk bstl5x Athletes 024 sports01 1920w", 269 "contentUrl": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_bstl5x_Athletes-024_sports01-1920w.jpg" 270 }, 271 { 272 "@type": "ImageObject", 273 "name": "f21s3uk4 an5m6dbk 2oajxn basketball portrait", 274 "description": "Dothan Bardichev", 275 "caption": "Dothan Bardichev", 276 "contentUrl": "https://storage.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_2oajxn_basketball-portrait.jpg" 277 } 278 ], 279 "numberOfItems": 24, 280 "primaryImageOfPage": { 281 "@type": "ImageObject", 282 "url": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_jdi7x8_Athletes-001_basketballA.jpg", 283 "width": 1200, 284 "height": 630, 285 "caption": "ATHLETES | People - Adena Stevens Photography" 286 }, 287 "image": { 288 "@type": "ImageObject", 289 "url": "https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_jdi7x8_Athletes-001_basketballA.jpg", 290 "width": 1200, 291 "height": 630, 292 "caption": "ATHLETES | People - Adena Stevens Photography" 293 } 294} 295 </script>
295 296 297 298 <!-- Preview mode helper script --> 299
299<script> 300 // Immediately execute to patch request methods before any other script runs 301 (function() { 302 // Detect preview mode by checking hostname 303 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 304 305 if (isPreviewMode) { 306 console.log("[Preview Helper] Preview mode detected"); 307 308 // Extract GUID from URL path (first segment) 309 const pathParts = window.location.pathname.split('/').filter(Boolean); 310 const previewGuid = pathParts.length > 0 ? pathParts[0] : ''; 311 312 if (!previewGuid) { 313 console.warn("[Preview Helper] No GUID found in path:", window.location.pathname); 314 return; // Exit if no GUID found 315 } 316 317 console.log("[Preview Helper] Using GUID:", previewGuid); 318 319 // Store these in window for other scripts 320 window.isPreviewMode = true; 321 window.previewGuid = previewGuid; 322 window.isHydraEnvironment = true; 323 window.Parameters = window.Parameters || {}; 324 window.Parameters.isHydra = true; 325 326 // Helper function that adds the GUID to API URLs and relative links 327 window.getApiUrl = function(endpoint) { 328 // For relative paths 329 if (endpoint.startsWith('/api/')) { 330 const newUrl = '/' + previewGuid + endpoint; 331 console.log("[Preview Helper] Transformed API URL:", endpoint, "â", newUrl); 332 return newUrl; 333 } 334 return endpoint; 335 }; 336 337 // Helper function that adds the GUID to any relative link 338 window.getPreviewUrl = function(url) { 339 // For relative paths (starts with /) that aren't already prefixed with GUID 340 if (url.startsWith('/') && !url.startsWith('/' + previewGuid + '/')) { 341 const newUrl = '/' + previewGuid + url; 342 console.log("[Preview Helper] Transformed relative link:", url, "â", newUrl); 343 return newUrl; 344 } 345 return url; 346 }; 347 348 // PATCH FETCH API with enhanced URL handling 349 const originalFetch = window.fetch; 350 window.fetch = function(resource, init) { 351 // Only transform string URLs 352 if (typeof resource === 'string') { 353 // Case 1: Handle relative paths starting with /api/ 354 if (resource.startsWith('/api/')) { 355 const newUrl = window.getApiUrl(resource); 356 console.log("[Preview Helper] Patched fetch call (API):", resource, "â", newUrl); 357 return originalFetch(newUrl, init); 358 } 359 360 // Case 2: Handle other relative paths (for content/gallery links) 361 else if (resource.startsWith('/') && !resource.startsWith('/' + previewGuid + '/')) { 362 const newUrl = window.getPreviewUrl(resource); 363 console.log("[Preview Helper] Patched fetch call (relative):", resource, "â", newUrl); 364 return originalFetch(newUrl, init); 365 } 366 367 // Case 3: Handle absolute URLs to the same domain containing /api/ 368 else if (resource.includes(window.location.host) && resource.includes('/api/')) { 369 try { 370 const urlObj = new URL(resource); 371 if (urlObj.hostname === window.location.hostname) { 372 const path = urlObj.pathname; 373 // Already has /{guid}/api/... (e.g. from new URL(getApiUrl(...), origin)) â do not double-prefix 374 if (path.startsWith('/' + previewGuid + '/')) { 375 return originalFetch(resource, init); 376 } 377 const newPath = '/' + previewGuid + path; 378 urlObj.pathname = newPath; 379 const newUrl = urlObj.toString(); 380 console.log("[Preview Helper] Patched fetch call (absolute API):", resource, "â", newUrl); 381 return originalFetch(newUrl, init); 382 } 383 } catch (e) { 384 console.error("[Preview Helper] Error processing URL:", e); 385 } 386 } 387 388 // Case 4: Handle absolute URLs to the same domain (non-API) 389 else if (resource.includes(window.location.host)) { 390 try { 391 const urlObj = new URL(resource); 392 if (urlObj.hostname === window.location.hostname && 393 !urlObj.pathname.startsWith('/' + previewGuid + '/')) { 394 const path = urlObj.pathname; 395 const newPath = '/' + previewGuid + path; 396 urlObj.pathname = newPath; 397 const newUrl = urlObj.toString(); 398 console.log("[Preview Helper] Patched fetch call (absolute):", resource, "â", newUrl); 399 return originalFetch(newUrl, init); 400 } 401 } catch (e) { 402 console.error("[Preview Helper] Error processing URL:", e); 403 } 404 } 405 } 406 return originalFetch(resource, init); 407 }; 408 console.log("[Preview Helper] Patched fetch API"); 409 410 // PATCH XMLHttpRequest with enhanced URL handling 411 const originalOpen = XMLHttpRequest.prototype.open; 412 XMLHttpRequest.prototype.open = function(method, url, ...rest) { 413 // Only transform string URLs 414 if (typeof url === 'string') { 415 // Case 1: Handle relative paths starting with /api/ 416 if (url.startsWith('/api/')) { 417 const newUrl = window.getApiUrl(url); 418 console.log("[Preview Helper] Patched XMLHttpRequest (API):", url, "â", newUrl); 419 return originalOpen.call(this, method, newUrl, ...rest); 420 } 421 422 // Case 2: Handle other relative paths 423 else if (url.startsWith('/') && !url.startsWith('/' + previewGuid + '/')) { 424 const newUrl = window.getPreviewUrl(url); 425 console.log("[Preview Helper] Patched XMLHttpRequest (relative):", url, "â", newUrl); 426 return originalOpen.call(this, method, newUrl, ...rest); 427 } 428 429 // Case 3: Handle absolute URLs to the same domain containing /api/ 430 else if (url.includes(window.location.host) && url.includes('/api/')) { 431 try { 432 const urlObj = new URL(url); 433 if (urlObj.hostname === window.location.hostname) { 434 const path = urlObj.pathname; 435 if (path.startsWith('/' + previewGuid + '/')) { 436 return originalOpen.call(this, method, url, ...rest); 437 } 438 const newPath = '/' + previewGuid + path; 439 urlObj.pathname = newPath; 440 const newUrl = urlObj.toString(); 441 console.log("[Preview Helper] Patched XMLHttpRequest (absolute API):", url, "â", newUrl); 442 return originalOpen.call(this, method, newUrl, ...rest); 443 } 444 } catch (e) { 445 console.error("[Preview Helper] Error processing URL:", e); 446 } 447 } 448 449 // Case 4: Handle absolute URLs to the same domain (non-API) 450 else if (url.includes(window.location.host)) { 451 try { 452 const urlObj = new URL(url); 453 if (urlObj.hostname === window.location.hostname && 454 !urlObj.pathname.startsWith('/' + previewGuid + '/')) { 455 const path = urlObj.pathname; 456 const newPath = '/' + previewGuid + path; 457 urlObj.pathname = newPath; 458 const newUrl = urlObj.toString(); 459 console.log("[Preview Helper] Patched XMLHttpRequest (absolute):", url, "â", newUrl); 460 return originalOpen.call(this, method, newUrl, ...rest); 461 } 462 } catch (e) { 463 console.error("[Preview Helper] Error processing URL:", e); 464 } 465 } 466 } 467 return originalOpen.call(this, method, url, ...rest); 468 }; 469 console.log("[Preview Helper] Patched XMLHttpRequest"); 470 471 // Note: Link click handling is done by the existing gallery link interceptor 472 // The fetch and XMLHttpRequest patches above handle programmatic requests 473 474 // Add debugging aid 475 window.debugPreviewUrls = function() { 476 return { 477 hostname: window.location.hostname, 478 pathname: window.location.pathname, 479 guid: window.previewGuid, 480 isPreviewMode: window.isPreviewMode, 481 sampleTransformations: { 482 relativeApi: window.getApiUrl('/api/menu-styles'), 483 relativeLink: window.getPreviewUrl('/gallery'), 484 absolute: new URL('/api/save-config', window.location.origin).toString().replace('/api/', '/' + previewGuid + '/api/') 485 } 486 }; 487 }; 488 489 // Test the transformation immediately 490 console.log("[Preview Helper] Test API transformation:", window.getApiUrl('/api/menu-styles')); 491 console.log("[Preview Helper] Test relative link transformation:", window.getPreviewUrl('/gallery')); 492 console.log("[Preview Helper] Test absolute URL transformation:", 493 new URL('/api/save-config', window.location.origin).toString().replace('/api/', '/' + previewGuid + '/api/')); 494 } 495 })(); 496 </script>
496 497 498 499 500 501<!-- Early CDN retry helpers: MUST run before CDN script/link tags. 502 Changing script.src after a load error is unreliable (esp. Firefox defer); 503 always insert a new element pointing at /cdn-proxy/. -->
504<script> 505(function() { 506 window.useCdnProxy = window.useCdnProxy || false; 507 window.__hydraCdnRetryLog = window.__hydraCdnRetryLog || []; 508 function toProxy(url) { 509 if (!url || url.indexOf('cdn.neonsky.app') === -1) return url; 510 return url.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 511 } 512 function noteRetry(kind, fromUrl, toUrl) { 513 try { 514 window.useCdnProxy = true; 515 var entry = { kind: kind, from: fromUrl, to: toUrl, t: Date.now() }; 516 window.__hydraCdnRetryLog.push(entry); 517 console.warn('ð CDN failed, retrying via new element:', kind, fromUrl, '->', toUrl); 518 } catch (e) {} 519 } 520 window.__hydraRetryCdnScript = function(el) { 521 try { 522 if (!el || el.getAttribute('data-retried') === 'true') return; 523 var original = el.src || ''; 524 if (original.indexOf('cdn.neonsky.app') === -1) return; 525 el.setAttribute('data-retried', 'true'); 526 el.onerror = null; 527 var proxyUrl = toProxy(original); 528 noteRetry('script', original, proxyUrl); 529 var s = document.createElement('script'); 530 s.src = proxyUrl; 531 s.setAttribute('data-retried', 'true'); 532 if (el.id) s.id = el.id; 533 // Dynamic scripts ignore defer; omit it so the proxy copy executes on load. 534 if (el.parentNode) { 535 el.parentNode.insertBefore(s, el.nextSibling); 536 } else { 537 (document.head || document.documentElement).appendChild(s); 538 } 539 } catch (e) {} 540 }; 541 window.__hydraRetryCdnLink = function(el) { 542 try { 543 if (!el || el.getAttribute('data-retried') === 'true') return; 544 var original = el.href || ''; 545 if (original.indexOf('cdn.neonsky.app') === -1) return; 546 el.setAttribute('data-retried', 'true'); 547 el.onerror = null; 548 var proxyUrl = toProxy(original); 549 noteRetry('link', original, proxyUrl); 550 var link = document.createElement('link'); 551 link.rel = el.rel || 'stylesheet'; 552 if (el.id) link.id = el.id; 553 link.href = proxyUrl; 554 link.setAttribute('data-retried', 'true'); 555 if (el.parentNode) { 556 el.parentNode.insertBefore(link, el.nextSibling); 557 } else { 558 (document.head || document.documentElement).appendChild(link); 559 } 560 } catch (e) {} 561 }; 562})(); 563</script>
563 564<link rel="stylesheet" href="https://cdn.neonsky.app/menu-styles-v260923-002.css?t=1791237687500" onerror="window.__hydraRetryCdnLink(this)">
565<script src="https://assets.lemonsqueezy.com/lemon.js" defer></script>
vendor: 1 bytes, line 565
565
566<script src="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js"></script>
vendor: 1 bytes, line 566
566
567<script src="https://cdn.jsdelivr.net/npm/magic-sdk"></script>
vendor: 1 bytes, line 567
567
568<script src="https://cdn.jsdelivr.net/npm/@magic-ext/auth"></script>
vendor: 1 bytes, line 568
568
569<script src="https://cdn.neonsky.app/style-customizer-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
569 570<link rel="stylesheet" href="https://cdn.neonsky.app/palette-explorer-v260923-002.css?t=1791237687500" onerror="window.__hydraRetryCdnLink(this)">
571<script src="https://cdn.neonsky.app/palette-explorer-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 571
571
572<script src="https://cdn.neonsky.app/sidebar-manager-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 572
572
573<script src="https://cdn.neonsky.app/image-processor-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 573
573
574<script src="https://cdn.neonsky.app/image-uploader-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 574
574
575<script src="https://cdn.neonsky.app/page-layout-presets-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 575
575
576<script src="https://cdn.neonsky.app/site-graph-presets-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 576
576
577<script src="https://cdn.neonsky.app/site-graph-sync-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 577
577
578<script src="https://cdn.neonsky.app/page-manager-v260923-002.js?t=1791237687500" defer onerror="window.__hydraRetryCdnScript(this)"></script>
578 579<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css"> 580<link rel="stylesheet" href="https://cdn.neonsky.app/page-styles-v260923-002.css?t=1791237687500" onerror="window.__hydraRetryCdnLink(this)"> 581 582 583 584<!-- CDN Fallback Handler for CSS and JS files -->
585<script> 586(function() { 587 // Global flag: Set to true when any CDN resource fails and uses proxy 588 window.useCdnProxy = window.useCdnProxy || false; 589 590 // Global base URL function: Returns proxy base if flag is set, otherwise CDN base 591 window.getCdnBaseUrl = function() { 592 return window.useCdnProxy 593 ? window.location.origin + '/cdn-proxy/' 594 : 'https://cdn.neonsky.app/'; 595 }; 596 597 // Utility function to convert CDN URLs to proxy URLs 598 window.convertCdnToProxyUrl = function(cdnUrl) { 599 if (!cdnUrl || !cdnUrl.includes('cdn.neonsky.app')) return cdnUrl; 600 return cdnUrl.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 601 }; 602 603 // Utility function to construct CDN URLs (uses proxy if flag is set) 604 window.buildCdnUrl = function(path) { 605 return window.getCdnBaseUrl() + path; 606 }; 607 608 // Resource tracking for debugging 609 window._resourceTracking = window._resourceTracking || { 610 resources: [], 611 proxyFlagChanges: [], 612 imageLoads: [], 613 networkErrors: [] 614 }; 615 616 // Track when proxy flag changes - use a wrapper function 617 let proxyFlagChangeCount = 0; 618 let _useCdnProxyValue = window.useCdnProxy || false; 619 620 // Create a setter function that tracks changes 621 window._setUseCdnProxy = function(newValue, reason) { 622 const oldValue = _useCdnProxyValue; 623 if (oldValue !== newValue) { 624 proxyFlagChangeCount++; 625 const changeInfo = { 626 timestamp: new Date().toISOString(), 627 changeNumber: proxyFlagChangeCount, 628 oldValue: oldValue, 629 newValue: newValue, 630 reason: reason || 'Unknown', 631 stack: new Error().stack?.split('\n').slice(1, 6).join('\n') || 'No stack' 632 }; 633 window._resourceTracking.proxyFlagChanges.push(changeInfo); 634 console.error('ð PROXY_FLAG_CHANGE:', JSON.stringify(changeInfo, null, 2)); 635 // Keep only last 50 changes 636 if (window._resourceTracking.proxyFlagChanges.length > 50) { 637 window._resourceTracking.proxyFlagChanges.shift(); 638 } 639 _useCdnProxyValue = newValue; 640 window.useCdnProxy = newValue; 641 } 642 }; 643 644 // Track resource loading 645 function trackResource(url, type, status, details) { 646 const entry = { 647 timestamp: new Date().toISOString(), 648 url: url, 649 type: type, // 'script', 'link', 'image', etc. 650 status: status, // 'loading', 'success', 'failed', 'proxy', 'storage' 651 details: details || {}, 652 useCdnProxy: window.useCdnProxy 653 }; 654 window._resourceTracking.resources.push(entry); 655 console.error('ð RESOURCE_TRACK:', JSON.stringify(entry, null, 2)); 656 // Keep only last 200 resources 657 if (window._resourceTracking.resources.length > 200) { 658 window._resourceTracking.resources.shift(); 659 } 660 } 661 662 // Track image loads 663 function trackImageLoad(url, status, source) { 664 const entry = { 665 timestamp: new Date().toISOString(), 666 url: url, 667 status: status, // 'loading', 'success', 'failed' 668 source: source, // 'cdn', 'proxy', 'storage' 669 useCdnProxy: window.useCdnProxy 670 }; 671 window._resourceTracking.imageLoads.push(entry); 672 console.error('ð¼ï¸ IMAGE_LOAD:', JSON.stringify(entry, null, 2)); 673 // Keep only last 100 image loads 674 if (window._resourceTracking.imageLoads.length > 100) { 675 window._resourceTracking.imageLoads.shift(); 676 } 677 } 678 679 // Track network errors 680 function trackNetworkError(url, error, context) { 681 const entry = { 682 timestamp: new Date().toISOString(), 683 url: url, 684 error: { 685 message: error?.message || 'Unknown error', 686 name: error?.name || 'Unknown', 687 type: error?.type || 'unknown', 688 toString: String(error) 689 }, 690 context: context || {}, 691 useCdnProxy: window.useCdnProxy 692 }; 693 window._resourceTracking.networkErrors.push(entry); 694 console.error('â NETWORK_ERROR:', JSON.stringify(entry, null, 2)); 695 // Keep only last 100 errors 696 if (window._resourceTracking.networkErrors.length > 100) { 697 window._resourceTracking.networkErrors.shift(); 698 } 699 } 700 701 // Expose tracking functions globally 702 window.trackResource = trackResource; 703 window.trackImageLoad = trackImageLoad; 704 window.trackNetworkError = trackNetworkError; 705 706 // Function to retry failed resources via proxy 707 // For ISP-affected clients, we assume CDN failures are network/SSL errors and set the proxy flag 708 // The proxy can handle both network errors and 404s, so it's safer to err on the side of using it 709 function retryViaProxy(element, originalSrc, errorEvent) { 710 if (!originalSrc) originalSrc = element.href || element.src; 711 if (!originalSrc || !originalSrc.includes('cdn.neonsky.app')) return; 712 713 // Prevent multiple retries 714 if (element.getAttribute('data-retried')) { 715 return; 716 } 717 element.setAttribute('data-retried', 'true'); 718 719 // Check if element still exists in DOM 720 if (!element.parentNode) { 721 console.warn('â ï¸ Element no longer in DOM, skipping proxy retry:', originalSrc); 722 return; 723 } 724 725 // For ISP-affected clients, CDN failures are almost always network/SSL errors, not 404s 726 // We'll be aggressive and set the flag for any CDN failure to ensure images also use proxy 727 // Check error message if available, but default to assuming network error 728 const hasExplicitNetworkError = errorEvent && errorEvent.message && ( 729 errorEvent.message.includes('Failed to fetch') || 730 errorEvent.message.includes('SSL') || 731 errorEvent.message.includes('secure connection') || 732 errorEvent.message.includes('network') || 733 errorEvent.message.includes('ERR_') ||
734 errorEvent.message.includes('SSL_ERROR') 735 ); 736 737 // Track the error 738 trackNetworkError(originalSrc, errorEvent, { 739 elementType: element.tagName, 740 hasExplicitNetworkError: hasExplicitNetworkError, 741 errorMessage: errorEvent?.message || 'No error message', 742 errorName: errorEvent?.name || 'No error name' 743 }); 744 745 // Set flag if we have explicit network error, or if errorEvent is missing/null (browser silent failure) 746 // This covers ISP-affected clients where SSL handshake fails before HTTP request 747 const reason = hasExplicitNetworkError 748 ? 'Explicit network/SSL error detected' 749 : (!errorEvent || !errorEvent.message) 750 ? 'Browser silent failure (likely SSL/network)' 751 : 'CDN resource failed (using proxy as fallback)'; 752 753 if (hasExplicitNetworkError || !errorEvent || !errorEvent.message) { 754 window._setUseCdnProxy(true, reason); 755 console.error('ð© CDN_PROXY_FLAG SET: CDN failure detected, using proxy for all subsequent requests'); 756 console.error(' Error details:', errorEvent ? (errorEvent.message || 'No error message (likely SSL/network failure)') : 'No error event'); 757 } else { 758 // Even for other errors, set the flag to be safe - proxy can handle 404s too 759 window._setUseCdnProxy(true, reason); 760 console.error('ð© CDN_PROXY_FLAG SET: CDN resource failed, using proxy as fallback'); 761 } 762 763 const proxyUrl = originalSrc.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 764 console.error('ð CDN failed, retrying via proxy:', originalSrc, '->', proxyUrl); 765 766 // Track the retry 767 trackResource(originalSrc, element.tagName.toLowerCase(), 'failed', { 768 retryUrl: proxyUrl, 769 error: errorEvent?.message || 'Unknown' 770 }); 771 772 if (element.tagName === 'SCRIPT') { 773 const newScript = document.createElement('script'); 774 newScript.src = proxyUrl; 775 // Do not copy defer/async onto dynamically inserted scripts. After a CDN 776 // failure, DOMContentLoaded may already have fired; deferred dynamic 777 // scripts can fail to execute (seen on Firefox ISP/CDN-fail clients). 778 newScript.setAttribute('data-retried', 'true'); 779 newScript.onload = function() { 780 trackResource(proxyUrl, 'script', 'proxy-success', { originalUrl: originalSrc }); 781 }; 782 newScript.onerror = function() { 783 trackResource(proxyUrl, 'script', 'proxy-failed', { originalUrl: originalSrc }); 784 console.error('â Proxy also failed, trying storage:', proxyUrl); 785 const storageUrl = originalSrc.replace('cdn.neonsky.app', 'storage.neonsky.app'); 786 trackResource(storageUrl, 'script', 'storage-fallback', { originalUrl: originalSrc, proxyUrl: proxyUrl }); 787 newScript.src = storageUrl; 788 newScript.onload = function() { 789 trackResource(storageUrl, 'script', 'storage-success', { originalUrl: originalSrc }); 790 }; 791 }; 792 element.parentNode.replaceChild(newScript, element); 793 } else if (element.tagName === 'LINK') { 794 const newLink = document.createElement('link'); 795 newLink.rel = element.rel; 796 newLink.href = proxyUrl; 797 newLink.setAttribute('data-retried', 'true'); 798 newLink.onload = function() { 799 trackResource(proxyUrl, 'link', 'proxy-success', { originalUrl: originalSrc }); 800 }; 801 newLink.onerror = function() { 802 trackResource(proxyUrl, 'link', 'proxy-failed', { originalUrl: originalSrc }); 803 console.error('â Proxy also failed, trying storage:', proxyUrl); 804 const storageUrl = originalSrc.replace('cdn.neonsky.app', 'storage.neonsky.app'); 805 trackResource(storageUrl, 'link', 'storage-fallback', { originalUrl: originalSrc, proxyUrl: proxyUrl }); 806 newLink.href = storageUrl; 807 newLink.onload = function() { 808 trackResource(storageUrl, 'link', 'storage-success', { originalUrl: originalSrc }); 809 }; 810 }; 811 element.parentNode.replaceChild(newLink, element); 812 } 813 } 814 815 // Monitor script and link tags for errors 816 document.addEventListener('error', function(e) { 817 const target = e.target; 818 if ((target.tagName === 'SCRIPT' || target.tagName === 'LINK') && 819 target.src && target.src.includes('cdn.neonsky.app')) { 820 retryViaProxy(target, target.src, e); 821 } else if (target.tagName === 'LINK' && target.href && target.href.includes('cdn.neonsky.app')) { 822 retryViaProxy(target, target.href, e); 823 } 824 }, true); 825 826 // Also check existing scripts/links that may have failed before this script loaded 827 // For ISP-affected clients, we assume CDN failures are network/SSL errors 828 setTimeout(function() { 829 document.querySelectorAll('script[src*="cdn.neonsky.app"], link[href*="cdn.neonsky.app"]').forEach(function(el) { 830 if (el.tagName === 'SCRIPT' && !el.getAttribute('data-loaded') && !el.getAttribute('data-retried')) { 831 el.addEventListener('error', function(errorEvent) { 832 // For ISP-affected clients, assume any CDN failure is a network/SSL error 833 // The proxy can handle both network errors and 404s 834 retryViaProxy(el, el.src, errorEvent); 835 }); 836 } else if (el.tagName === 'LINK' && !el.sheet && !el.getAttribute('data-loaded') && !el.getAttribute('data-retried')) { 837 // CSS link failed if it has no stylesheet - check after a delay 838 setTimeout(function() { 839 if (!el.sheet && !el.getAttribute('data-retried')) { 840 // For ISP-affected clients, assume any CDN failure is a network/SSL error 841 retryViaProxy(el, el.href, null); 842 } 843 }, 2000); 844 } 845 }); 846 }, 100); 847})(); 848</script>
848 849 850<!-- Log Capture System - S+J to download logs (works even if gallery doesn't load) -->
851<script> 852(function() { 853 'use strict'; 854 855 // Only initialize if not already initialized 856 if (window._indexLogCaptureInitialized) { 857 return; 858 } 859 window._indexLogCaptureInitialized = true; 860 861 // Wait for DOM to be ready 862 function initLogCapture() { 863 // Store original console methods 864 const originalConsole = { 865 log: console.log, 866 warn: console.warn, 867 error: console.error, 868 info: console.info 869 }; 870 871 // Array to store all console messages 872 window._capturedLogs = window._capturedLogs || []; 873 874 // Override console methods to capture logs 875 ['log', 'warn', 'error', 'info'].forEach(method => { 876 console[method] = function(...args) { 877 // Call original method 878 originalConsole[method].apply(console, args); 879 880 // Capture the log with timestamp 881 window._capturedLogs.push({ 882 timestamp: new Date().toISOString(), 883 level: method.toUpperCase(), 884 message: args.map(function(arg) { 885 if (arg !== null && arg !== undefined && Object(arg) === arg) { 886 try { 887 return JSON.stringify(arg, null, 2); 888 } catch (e) { 889 return String(arg); 890 } 891 } 892 return String(arg); 893 }).join(' ') 894 }); 895 896 // Keep only last 2000 logs to prevent memory issues 897 if (window._capturedLogs.length > 2000) { 898 window._capturedLogs.shift(); 899 } 900 }; 901 }); 902 903 // Add keyboard shortcut to download logs (S+J) 904 let pressedKeys = new Set(); 905 906 document.addEventListener('keydown', function(event) { 907 const key = (event.key || '').toLowerCase(); 908 909 if (key === 's' || key === 'j') { 910 pressedKeys.add(key); 911 912 // Check if both S and J are pressed 913 if (pressedKeys.has('s') && pressedKeys.has('j')) { 914 event.preventDefault(); 915 console.log('S+J detected - downloading logs...'); 916 917 // Show a brief visual indicator 918 const indicator = document.createElement('div'); 919 indicator.style.cssText = 920 'position: fixed; top: 20px; right: 20px; z-index: 10000; ' + 921 'background: #4CAF50; color: white; padding: 10px 20px; ' + 922 'border-radius: 5px; font-family: Arial, sans-serif; ' + 923 'font-size: 14px; font-weight: bold; ' + 924 'box-shadow: 0 2px 10px rgba(0,0,0,0.3);'; 925 indicator.textContent = 'ð¥ Downloading logs...'; 926 document.body.appendChild(indicator); 927 928 setTimeout(() => { 929 if (indicator.parentNode) { 930 indicator.parentNode.removeChild(indicator); 931 } 932 }, 2000); 933 934 downloadLogs(); 935 // Clear the set after triggering 936 pressedKeys.clear(); 937 } 938 } 939 }); 940 941 document.addEventListener('keyup', function(event) { 942 const key = (event.key || '').toLowerCase(); 943 if (key === 's' || key === 'j') { 944 pressedKeys.delete(key); 945 } 946 }); 947 948 console.log('ð Log capture system initialized. Press S+J simultaneously to download logs.'); 949 } 950 951 // Download captured logs as a text file 952 function downloadLogs() { 953 try { 954 // Get all scripts and links, categorized 955 const allScripts = Array.from(document.querySelectorAll('script[src]')).map(s => s.src); 956 const allLinks = Array.from(document.querySelectorAll('link[href]')).map(l => l.href); 957 958 // Get additional debug info 959 const debugInfo = { 960 url: window.location.href, 961 userAgent: navigator.userAgent, 962 timestamp: new Date().toISOString(), 963 useCdnProxy: window.useCdnProxy || false, 964 windowLocation: window.location.toString(), 965 documentReadyState: document.readyState, 966 // All scripts/links (not just CDN) 967 allScriptsLoaded: allScripts, 968 allLinksLoaded: allLinks, 969 // CDN-specific (for backward compatibility and CDN debugging) 970 scriptsLoaded: allScripts.filter(src => src.includes('cdn.neonsky.app')), 971 linksLoaded: allLinks.filter(href => href.includes('cdn.neonsky.app')), 972 // Gallery scripts (loaded from Worker) 973 galleryScriptsLoaded: allScripts.filter(src => src.includes('/gallery-scripts/')), 974 galleryLinksLoaded: allLinks.filter(href => href.includes('/gallery-scripts/')), 975 // Failed resources 976 failedScripts: Array.from(document.querySelectorAll('script[src*="cdn.neonsky.app"][data-retried]')).map(s => s.src), 977 failedLinks: Array.from(document.querySelectorAll('link[href*="cdn.neonsky.app"][data-retried]')).map(l => l.href), 978 // Proxy resources 979 proxyScriptsLoaded: allScripts.filter(src => src.includes('/cdn-proxy/')), 980 proxyLinksLoaded: allLinks.filter(href => href.includes('/cdn-proxy/')), 981 // Resource tracking data 982 resourceTracking: window._resourceTracking || { 983 resources: [], 984 proxyFlagChanges: [], 985 imageLoads: [], 986 networkErrors: [] 987 }, 988 // Performance timing 989 performanceTiming: performance.timing ? { 990 navigationStart: performance.timing.navigationStart, 991 domContentLoaded: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart, 992 loadComplete: performance.timing.loadEventEnd - performance.timing.navigationStart, 993 domInteractive: performance.timing.domInteractive - performance.timing.navigationStart 994 } : null, 995 // Style / logo diagnostics (layout/color bug triage) 996 styleDiagnostics: (function() { 997 try { 998 var rootStyle = getComputedStyle(document.documentElement); 999 var logoNodes = Array.from(document.querySelectorAll('.sidebar img, .sidebar-element img, img[src*="robert-vizzini"], .horizontal-header-logo img')); 1000 return {
1001 siteBackgroundColor: rootStyle.getPropertyValue('--site-background-color').trim(), 1002 menuBackgroundColor: rootStyle.getPropertyValue('--menu-background-color').trim(), 1003 menuColor: rootStyle.getPropertyValue('--menu-color').trim(), 1004 inlineSiteBg: document.documentElement.style.getPropertyValue('--site-background-color'), 1005 inlineMenuBg: document.documentElement.style.getPropertyValue('--menu-background-color'), 1006 hasMenuStyleCustomizer: !!window.MenuStyleCustomizer, 1007 menuStyleCustomizerInitialized: !!(window.MenuStyleCustomizer && window.MenuStyleCustomizer._initialized), 1008 menuStyleSettingsBg: window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings 1009 ? { 1010 siteBackgroundColor: window.MenuStyleCustomizer.settings.siteBackgroundColor, 1011 menuBackgroundColor: window.MenuStyleCustomizer.settings.menuBackgroundColor, 1012 color: window.MenuStyleCustomizer.settings.color 1013 } 1014 : null, 1015 hasSidebarManager: !!window.SidebarManager, 1016 sidebarElementCount: window.SidebarManager && window.SidebarManager.elements 1017 ? window.SidebarManager.elements.length 1018 : 0, 1019 hasPageManager: !!window.PageManager, 1020 pageManagerHasLoadPage: !!(window.PageManager && typeof window.PageManager.loadPage === 'function'), 1021 pageElementCount: (function() { 1022 try { 1023 return document.querySelectorAll('.gallery-container .page-element, .page-container .page-element').length; 1024 } catch (e) { return -1; } 1025 })(), 1026 pageContainerHtmlLength: (function() { 1027 try { 1028 var pc = document.querySelector('.gallery-container .page-container, .page-container'); 1029 return pc ? (pc.innerHTML || '').length : 0; 1030 } catch (e) { return -1; } 1031 })(), 1032 activeContentType: window._activeContentType || null, 1033 logoImageCount: logoNodes.length, 1034 logoImages: logoNodes.slice(0, 5).map(function(img) { 1035 return { 1036 src: img.currentSrc || img.src || '', 1037 naturalWidth: img.naturalWidth || 0, 1038 display: getComputedStyle(img).display, 1039 complete: !!img.complete 1040 }; 1041 }), 1042 earlyCdnRetries: window.__hydraCdnRetryLog || [] 1043 }; 1044 } catch (e) { 1045 return { error: String(e && e.message ? e.message : e) }; 1046 } 1047 })() 1048 }; 1049 1050 // Create log content 1051 const logContent = [ 1052 '=== INDEX.TS DEBUG LOGS ===', 1053 'Generated: ' + debugInfo.timestamp, 1054 'URL: ' + debugInfo.url, 1055 'User Agent: ' + debugInfo.userAgent, 1056 'useCdnProxy Flag: ' + debugInfo.useCdnProxy, 1057 'Document Ready State: ' + debugInfo.documentReadyState, 1058 '================================', 1059 'TIP: Search for [AUTH_SAVE] in CONSOLE LOGS below for authentication/save failure details.', 1060 'TIP: Search STYLE DIAGNOSTICS for layout/color/logo triage (backgrounds, MenuStyleCustomizer, SidebarManager, logo).', 1061 '================================', 1062 '', 1063 '=== DEBUG INFO ===', 1064 JSON.stringify(debugInfo, null, 2), 1065 '', 1066 '=== STYLE DIAGNOSTICS ===', 1067 JSON.stringify(debugInfo.styleDiagnostics, null, 2), 1068 '', 1069 '=== RESOURCE TRACKING SUMMARY ===', 1070 'Total Resources Tracked: ' + (debugInfo.resourceTracking.resources.length || 0), 1071 'Proxy Flag Changes: ' + (debugInfo.resourceTracking.proxyFlagChanges.length || 0), 1072 'Image Loads Tracked: ' + (debugInfo.resourceTracking.imageLoads.length || 0), 1073 'Network Errors Tracked: ' + (debugInfo.resourceTracking.networkErrors.length || 0), 1074 '', 1075 '=== PROXY FLAG CHANGES ===', 1076 JSON.stringify(debugInfo.resourceTracking.proxyFlagChanges, null, 2), 1077 '', 1078 '=== RESOURCE LOADING TRACKING ===', 1079 JSON.stringify(debugInfo.resourceTracking.resources, null, 2), 1080 '', 1081 '=== IMAGE LOADING TRACKING ===', 1082 JSON.stringify(debugInfo.resourceTracking.imageLoads, null, 2), 1083 '', 1084 '=== NETWORK ERRORS ===', 1085 JSON.stringify(debugInfo.resourceTracking.networkErrors, null, 2), 1086 '', 1087 '=== CONSOLE LOGS ===', 1088 ...(window._capturedLogs || []).map(log => 1089 '[' + log.timestamp + '] ' + log.level + ': ' + log.message 1090 ) 1091 ].join('\n'); 1092 1093 // Create and download file 1094 const blob = new Blob([logContent], { type: 'text/plain' }); 1095 const url = URL.createObjectURL(blob); 1096 const a = document.createElement('a'); 1097 a.href = url; 1098 a.download = 'index-debug-logs-' + new Date().toISOString().replace(/[:.]/g, '-') + '.txt';
1099 document.body.appendChild(a); 1100 a.click(); 1101 document.body.removeChild(a); 1102 URL.revokeObjectURL(url); 1103 1104 console.log('â Debug logs downloaded successfully!'); 1105 } catch (error) { 1106 console.error('â Failed to download logs:', error); 1107 alert('Failed to download logs. Error: ' + (error.message || 'Unknown error')); 1108 } 1109 } 1110 1111 // Initialize when DOM is ready 1112 if (document.readyState === 'loading') { 1113 document.addEventListener('DOMContentLoaded', initLogCapture); 1114 } else { 1115 // DOM already ready, initialize immediately 1116 setTimeout(initLogCapture, 100); 1117 } 1118})(); 1119</script>
1119 1120 1121<!-- Add auth token if available --> 1122 1123 1124<style> 1125/* Override Pico.css body padding to prevent jumping between pages */ 1126body { 1127 padding: 0 !important; 1128 margin: 0 !important; 1129 display: block !important; 1130} 1131 1132/* Scope Pico.css to only affect elements in edit mode */ 1133:root:not([data-theme="pico"]) { 1134--pico-applied: dark; 1135} 1136 1137/* Add data-theme="pico" to body when in edit mode */ 1138body.edit-mode-active { 1139data-theme: dark; 1140} 1141 1142/* Custom styles to ensure Pico only affects editor UI, not content */ 1143.sidebar.editing button, 1144.edit-controls button, 1145.element-header button, 1146.text-editor button, 1147.image-uploader button { 1148--pico-applied: dark; 1149} 1150 1151/* Hide default browser-styling when Pico is active */ 1152[data-theme="pico"] button.btn { 1153appearance: dark; 1154-webkit-appearance: dark; 1155} 1156 1157/* Ensure content isn't affected by Pico styles */ 1158.gallery-container, 1159.page-text, 1160.sidebar-text { 1161--pico-applied: none !important; 1162} 1163 1164/* Conflict Indicator Styles */ 1165#conflictIndicator { 1166 background: #d32f2f !important; 1167 border: 1px solid #d32f2f !important; 1168 border-radius: 4px; 1169 padding: 8px; 1170 cursor: pointer; 1171 transition: opacity 0.2s; 1172 margin-left: 8px; 1173 display: flex; /* Use flex for centering, but hidden by default via inline style */ 1174 align-items: center; 1175 justify-content: center; 1176} 1177 1178#conflictIndicator:hover { 1179 background: #e53935 !important; 1180 border-color: #e53935 !important; 1181 opacity: 0.9; 1182} 1183 1184#conflictIndicator svg { 1185 display: block; 1186 width: 24px; 1187 height: 24px; 1188 margin: 0 auto; 1189 color: white !important; 1190} 1191 1192/* Tooltip styles for conflict indicator - allow wrapping */ 1193#tooltip-container .tooltip[data-tooltip-for="conflictIndicator"], 1194#tooltip-container .tooltip { 1195 max-width: 200px; 1196 word-wrap: break-word; 1197 white-space: normal; 1198 line-height: 1.4; 1199} 1200 1201.global-header-controls { 1202 display: flex; 1203 align-items: center; 1204 gap: 8px; 1205} 1206 1207.sidebar-header { 1208 display: flex; 1209 align-items: center; 1210 gap: 8px; 1211} 1212 1213/* Cookie Notice â paper UI (public-safe; does not depend on edit-mode pe-* scopes) */ 1214.cookie-notice { 1215 --cn-paper: #f6f5f1; 1216 --cn-paper-elevated: #fdfcfa; 1217 --cn-ink: #2c2a27; 1218 --cn-ink-muted: #6b6760; 1219 --cn-ink-faint: #9b968d; 1220 --cn-rule: rgba(44, 42, 39, 0.1); 1221 --cn-rule-strong: rgba(44, 42, 39, 0.16); 1222 --cn-shadow: 0 -8px 28px rgba(44, 42, 39, 0.1); 1223 --cn-radius: 3px; 1224 --cn-font: "DM Sans", system-ui, -apple-system, sans-serif; 1225 1226 position: fixed; 1227 bottom: 0; 1228 left: 0; 1229 right: 0; 1230 z-index: 10000; 1231 display: flex; 1232 align-items: center; 1233 justify-content: space-between; 1234 flex-wrap: wrap; 1235 gap: 0.85rem 1.25rem; 1236 padding: 1.1rem 1.35rem 1.2rem; 1237 font-family: var(--cn-font); 1238 font-size: 14px; 1239 line-height: 1.45; 1240 color: var(--cn-ink); 1241 background: var(--cn-paper-elevated); 1242 border-top: 1px solid var(--cn-rule); 1243 box-shadow: var(--cn-shadow); 1244} 1245 1246.cookie-notice.hidden { 1247 display: none; 1248} 1249 1250.cookie-notice-text { 1251 flex: 1 1 220px; 1252 min-width: 0; 1253 margin: 0; 1254 color: var(--cn-ink-muted); 1255 font-size: 13px; 1256} 1257 1258.cookie-notice-text a.cookie-notice-policy { 1259 color: var(--cn-ink); 1260 font-weight: 600; 1261 text-decoration: underline; 1262 text-underline-offset: 2px; 1263} 1264 1265.cookie-notice-text a.cookie-notice-policy:hover { 1266 color: var(--cn-ink-muted); 1267} 1268 1269.cookie-notice-buttons { 1270 display: flex; 1271 flex-wrap: wrap; 1272 align-items: center; 1273 justify-content: flex-end; 1274 gap: 0.5rem; 1275} 1276 1277.cookie-notice .pe-btn { 1278 display: inline-flex; 1279 align-items: center; 1280 justify-content: center; 1281 gap: 0.4rem; 1282 margin: 0; 1283 padding: 0.45rem 0.85rem; 1284 font-family: var(--cn-font); 1285 font-size: 11px; 1286 font-weight: 600; 1287 letter-spacing: 0.1em; 1288 text-transform: uppercase; 1289 color: var(--cn-ink-muted); 1290 background: var(--cn-paper); 1291 border: 1px solid var(--cn-rule-strong); 1292 border-radius: var(--cn-radius); 1293 box-shadow: none; 1294 cursor: pointer; 1295 transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease; 1296} 1297 1298.cookie-notice .pe-btn:hover { 1299 color: var(--cn-ink); 1300 background: var(--cn-paper-elevated); 1301 border-color: rgba(44, 42, 39, 0.22); 1302} 1303 1304.cookie-notice .pe-btn--primary { 1305 color: var(--cn-ink); 1306 background: var(--cn-paper); 1307 border-color: var(--cn-rule-strong); 1308} 1309 1310.cookie-notice .pe-btn--primary:hover {
1311 color: var(--cn-ink); 1312 background: var(--cn-paper-elevated); 1313 border-color: rgba(44, 42, 39, 0.22); 1314} 1315 1316#hydraDoNotSellLink { 1317 --cn-paper: #f6f5f1; 1318 --cn-paper-elevated: #fdfcfa; 1319 --cn-ink: #2c2a27; 1320 --cn-ink-muted: #6b6760; 1321 --cn-rule-strong: rgba(44, 42, 39, 0.16); 1322 --cn-radius: 3px; 1323 --cn-font: "DM Sans", system-ui, -apple-system, sans-serif; 1324 1325 position: fixed; 1326 bottom: 10px; 1327 left: 12px; 1328 z-index: 9999; 1329 display: inline-flex; 1330 align-items: center; 1331 justify-content: center; 1332 margin: 0; 1333 padding: 0.45rem 0.85rem; 1334 font-family: var(--cn-font); 1335 font-size: 11px; 1336 font-weight: 600; 1337 letter-spacing: 0.1em; 1338 text-transform: uppercase; 1339 color: var(--cn-ink-muted); 1340 background: var(--cn-paper-elevated); 1341 border: 1px solid var(--cn-rule-strong); 1342 border-radius: var(--cn-radius); 1343 box-shadow: 0 2px 8px rgba(44, 42, 39, 0.06); 1344 cursor: pointer; 1345} 1346 1347#hydraDoNotSellLink.hidden { 1348 display: none; 1349} 1350 1351#hydraDoNotSellLink:hover { 1352 color: var(--cn-ink); 1353 background: var(--cn-paper); 1354 border-color: rgba(44, 42, 39, 0.22); 1355} 1356 1357/* FOUC reveal (paired with early <head> html:not(.hydra-ready) { opacity: 0 }) */ 1358html.hydra-ready { 1359 opacity: 1; 1360 transition: opacity 0.2s ease; 1361} 1362/* Keep body class for any legacy selectors; visibility no longer used (submenu CSS pierces it) */ 1363body.hydra-ready { 1364 visibility: visible; 1365} 1366 1367/* Admin panel typography */ 1368.sidebar-element-form, 1369.sidebar-element-form *, 1370.add-form, 1371.add-form *, 1372.metadata-editor, 1373.metadata-editor * { 1374 font-family: "Hanken Grotesk", sans-serif; 1375 font-weight: 200; 1376 letter-spacing: 0.02em; 1377} 1378 1379.sidebar-element-form h3, 1380.sidebar-element-form label, 1381.add-form label, 1382.metadata-editor h3, 1383.metadata-editor label { 1384 text-transform: uppercase; 1385 letter-spacing: 0.04em; 1386 font-weight: 300; 1387} 1388 1389.sidebar-element-form .form-actions .btn, 1390.sidebar-element-form button, 1391.add-form .form-actions .btn, 1392.add-form button, 1393.metadata-editor .form-actions .btn, 1394.metadata-editor button { 1395 text-transform: uppercase; 1396 letter-spacing: 0.08em; 1397 font-weight: 300; 1398} 1399 1400.sidebar-element-form input, 1401.sidebar-element-form select, 1402.sidebar-element-form textarea, 1403.sidebar-element-form button, 1404.add-form input, 1405.add-form select, 1406.add-form textarea, 1407.add-form button, 1408.metadata-editor input, 1409.metadata-editor select, 1410.metadata-editor textarea, 1411.metadata-editor button { 1412 font-family: "Hanken Grotesk", sans-serif; 1413 font-weight: 200; 1414 letter-spacing: 0.02em; 1415 border-radius: 0; 1416} 1417</style> 1418
1419<script> 1420// Flag to prevent duplicate initialization 1421window.hydraInitialized = false; 1422 1423function ensureAdminPanelFont() { 1424 const head = document.head || document.getElementsByTagName('head')[0]; 1425 if (!head) { 1426 return; 1427 } 1428 1429 const linkDefinitions = [ 1430 { 1431 id: 'admin-panels-font-preconnect-googleapis', 1432 rel: 'preconnect', 1433 href: 'https://fonts.googleapis.com' 1434 }, 1435 { 1436 id: 'admin-panels-font-preconnect-gstatic', 1437 rel: 'preconnect', 1438 href: 'https://fonts.gstatic.com', 1439 crossOrigin: 'anonymous' 1440 }, 1441 { 1442 id: 'admin-panels-font-stylesheet', 1443 rel: 'stylesheet', 1444 href: 'https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,200;1,200&display=swap' 1445 }, 1446 { 1447 id: 'admin-panels-font-barlow', 1448 rel: 'stylesheet', 1449 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' 1450 } 1451 ]; 1452 1453 for (const definition of linkDefinitions) { 1454 if (!document.getElementById(definition.id)) { 1455 const link = document.createElement('link'); 1456 link.id = definition.id; 1457 link.rel = definition.rel; 1458 link.href = definition.href; 1459 1460 if (definition.crossOrigin) { 1461 link.crossOrigin = definition.crossOrigin; 1462 } 1463 1464 head.appendChild(link); 1465 } 1466 } 1467} 1468 1469ensureAdminPanelFont(); 1470 1471 window.Parameters = window.Parameters || {}; 1472 window.Parameters.siteId = "f21s3uk4"; 1473 window.Parameters.SiteId = "f21s3uk4"; // Also set capitalized version 1474 window.siteId = "f21s3uk4"; 1475 window.TURNSTILE_SITE_KEY = "0x4AAAAAADyHXlv6RTUIaDOp"; 1476 window.NEON_SKY_FILES = window.NEON_SKY_FILES || {}; 1477 window.NEON_SKY_FILES.QUILL_INTEGRATION = 'quill-integration-v260923-002.js'; 1478 window.NEON_SKY_FILES.SITE_THEMES = 'site-themes-v260923-002.css'; 1479 window.NEON_SKY_FILES.SITE_THEMES_MANIFEST = 'site-themes-v260923-002.json'; 1480 1481// Initialize menu styles from server 1482document.addEventListener('DOMContentLoaded', function() { 1483// Apply initial menu styles from server 1484const initialMenuStyles = {"fontFamily":"'Hanken Grotesk', sans-serif","fontSize":"16px","color":"#333","activeColor":"#8f8f8f","hoverColor":"#9e9e9e","verticalSpacing":"2px","menuBackgroundColor":"#ffffff","menuBackgroundHidden":false,"siteBackgroundColor":"#ffffff","siteBackgroundUseGradient":false,"siteGradientRandomizeOnLoad":false,"artPaletteSelection":null,"menuBackgroundImageUrl":"","siteBackgroundImageUrl":"","mobileHeaderHeight":"180px","sidebarWidth":"15vw","sidebarPaddingTop":"0","sidebarPaddingRight":"0px","sidebarPaddingBottom":"0px","sidebarPaddingLeft":"0px","globalPagePaddingTopEnabled":false,"globalPagePaddingLeftEnabled":false,"globalPagePaddingRightEnabled":false,"globalPagePaddingBottomEnabled":false,"globalPagePaddingTop":"","globalPagePaddingLeft":"","globalPagePaddingRight":"","globalPagePaddingBottom":"","menuLayout":"horizontal","horizontalMenuAlignment":"center","horizontalHeaderLogoLayout":"stacked","horizontalHeaderShadow":false,"disableRightClick":false,"showCookieNotice":false,"cookieNoticeText":"This website uses cookies to ensure you get the best experience on our website.","privacyPolicyUrl":"","alignElementsAtBottom":false,"customCSS":"","customJS":"","siteThemeId":null,"siteThemeOptions":{},"fontName":"Hanken Grotesk"}; 1485window.__hydraStyleFromQuery = null; 1486const initialSiteThemeRuntimeVars = null; 1487 1488// Set CSS custom properties 1489// ?hydra-style= preview: apply merged menuStyles (skip site-theme runtime vars that would override). 1490if (initialSiteThemeRuntimeVars && !window.__hydraStyleFromQuery) { 1491 Object.keys(initialSiteThemeRuntimeVars).forEach(function(key) { 1492 document.documentElement.style.setProperty(key, initialSiteThemeRuntimeVars[key]); 1493 }); 1494 if (initialMenuStyles && initialMenuStyles.verticalSpacing) { 1495 document.documentElement.style.setProperty('--menu-vertical-spacing', initialMenuStyles.verticalSpacing); 1496 } 1497 // Match menu chrome / Gallery Options overview preview (light weight)
1498 document.documentElement.style.setProperty('--menu-font-weight', '200'); 1499 if (initialMenuStyles && typeof initialMenuStyles.secondaryFontSize === 'string' && initialMenuStyles.secondaryFontSize.trim() !== '') { 1500 document.documentElement.style.setProperty('--gallery-description-font-size', initialMenuStyles.secondaryFontSize.trim()); 1501 } 1502} else if (initialMenuStyles) { 1503 if (initialMenuStyles.fontFamily) { 1504 document.documentElement.style.setProperty('--menu-font-family', initialMenuStyles.fontFamily); 1505 } 1506 if (initialMenuStyles.fontSize) { 1507 document.documentElement.style.setProperty('--menu-font-size', initialMenuStyles.fontSize); 1508 } 1509 // Only set when Secondary was explicitly saved; otherwise descriptions follow --menu-font-size 1510 if (typeof initialMenuStyles.secondaryFontSize === 'string' && initialMenuStyles.secondaryFontSize.trim() !== '') { 1511 document.documentElement.style.setProperty('--gallery-description-font-size', initialMenuStyles.secondaryFontSize.trim()); 1512 } 1513 document.documentElement.style.setProperty('--menu-font-weight', '200'); 1514 if (initialMenuStyles.color) { 1515 document.documentElement.style.setProperty('--menu-color', initialMenuStyles.color); 1516 } 1517 if (initialMenuStyles.activeColor) { 1518 document.documentElement.style.setProperty('--menu-active-color', initialMenuStyles.activeColor); 1519 } 1520 if (initialMenuStyles.hoverColor) { 1521 document.documentElement.style.setProperty('--menu-hover-color', initialMenuStyles.hoverColor); 1522 } 1523 // Background colors must be applied here too: MenuStyleCustomizer may load late/fail 1524 // after CDNâproxy retry, and CSS defaults are white/#fafafa. 1525 if (initialMenuStyles.siteBackgroundColor) { 1526 document.documentElement.style.setProperty('--site-background-color', initialMenuStyles.siteBackgroundColor); 1527 } 1528 if (initialMenuStyles.menuBackgroundColor && !initialMenuStyles.menuBackgroundHidden) { 1529 document.documentElement.style.setProperty('--menu-background-color', initialMenuStyles.menuBackgroundColor); 1530 } 1531 if (initialMenuStyles.verticalSpacing) { 1532 document.documentElement.style.setProperty('--menu-vertical-spacing', initialMenuStyles.verticalSpacing); 1533 } 1534 if (initialMenuStyles.menuBackgroundImageUrl && String(initialMenuStyles.menuBackgroundImageUrl).trim() !== '') { 1535 var menuBgImg = String(initialMenuStyles.menuBackgroundImageUrl).trim(); 1536 document.documentElement.style.setProperty('--menu-background-image', 'url(' + JSON.stringify(menuBgImg) + ')'); 1537 } 1538 var resolvedSiteBg = ''; 1539 if (window.SiteGradientGenerator && typeof window.SiteGradientGenerator.resolveSiteBackgroundImage === 'function') { 1540 resolvedSiteBg = window.SiteGradientGenerator.resolveSiteBackgroundImage(initialMenuStyles, { allowRandomize: true }); 1541 } else if (initialMenuStyles.siteBackgroundImageUrl) { 1542 resolvedSiteBg = String(initialMenuStyles.siteBackgroundImageUrl).trim(); 1543 } 1544 if (resolvedSiteBg) { 1545 var peIsCssGradient = String(resolvedSiteBg).toLowerCase().indexOf('gradient(') !== -1; 1546 var siteBgImgCss = peIsCssGradient 1547 ? resolvedSiteBg 1548 : 'url(' + JSON.stringify(resolvedSiteBg) + ')'; 1549 document.documentElement.style.setProperty('--site-background-image', siteBgImgCss); 1550 var siteBgSize = 'cover'; 1551 if (peIsCssGradient) { 1552 siteBgSize = window.SiteGradientGenerator && window.SiteGradientGenerator.gradientBackgroundSize 1553 ? window.SiteGradientGenerator.gradientBackgroundSize(resolvedSiteBg) 1554 : '100% 100%'; 1555 document.body.classList.add('has-site-background-gradient'); 1556 } 1557 document.documentElement.style.setProperty('--site-background-size', siteBgSize); 1558 } 1559 if (initialMenuStyles.mobileHeaderHeight && String(initialMenuStyles.mobileHeaderHeight).trim() !== '') { 1560 var mhh0 = String(initialMenuStyles.mobileHeaderHeight).trim(); 1561 /* 0 / 0vh / 0px collapse logo max-height calcs; use the same floor as getUserHorizontalHeaderHeightPx */ 1562 var mhhParsed = parseInt(mhh0, 10); 1563 if (Number.isNaN(mhhParsed) || mhhParsed < 1) { 1564 mhh0 = '60px'; 1565 } 1566 document.documentElement.style.setProperty('--mobile-header-height', mh
1566h0); 1567 document.documentElement.style.setProperty('--content-margin-top', mhh0); 1568 document.documentElement.style.setProperty('--top-bar-height-setting', mhh0); 1569 } 1570} 1571 1572if (initialMenuStyles && initialMenuStyles.menuBackgroundHidden) { 1573 document.body.classList.add('menu-background-hidden'); 1574 document.documentElement.style.setProperty('--menu-background-color', 'transparent'); 1575 document.documentElement.style.setProperty('--menu-background-image', 'none'); 1576} 1577 1578if (initialMenuStyles && initialMenuStyles.menuTextUppercase) { 1579 document.body.classList.add('menu-text-uppercase'); 1580 if (!document.getElementById('menu-text-uppercase-style')) { 1581 var menuUpperStyle = document.createElement('style'); 1582 menuUpperStyle.id = 'menu-text-uppercase-style'; 1583 menuUpperStyle.textContent = '.menu-item,.horizontal-menu-label,a.horizontal-menu-label{text-transform:uppercase!important;font-weight:200!important;}'; 1584 document.head.appendChild(menuUpperStyle); 1585 } 1586} 1587 1588// Store initial styles in MenuStyleCustomizer if it exists 1589if (window.MenuStyleCustomizer) { 1590 window.MenuStyleCustomizer.settings = { 1591 ...window.MenuStyleCustomizer.defaults, 1592 ...initialMenuStyles 1593 }; 1594} 1595}); 1596</script>
vendor: 1 bytes, line 1596
1596
1597<script> 1598let magic = null; 1599let userMetadata = null; 1600let isAuthenticated = false; 1601let isAdmin = false; 1602let didToken = null; 1603let siteId = "f21s3uk4"; 1604 1605/** Clear Hydra auth tokens/flags from this browser (used by logout button and ?logout=1 links). */ 1606function clearHydraClientAuthStorage() { 1607 try { 1608 localStorage.removeItem('hydra_is_admin'); 1609 localStorage.removeItem('hydra_auth_token'); 1610 localStorage.removeItem('hydra_auth_email'); 1611 localStorage.removeItem('hydra_did_token'); 1612 localStorage.removeItem('hydra_auth_site'); 1613 localStorage.removeItem('hydra_auth_last_refresh'); 1614 localStorage.removeItem('hydra_auth_auto_refresh'); 1615 } catch (e) {} 1616 if (window.TokenManager && typeof window.TokenManager.clearToken === 'function') { 1617 try { window.TokenManager.clearToken(); } catch (e) {} 1618 } 1619 isAuthenticated = false; 1620 isAdmin = false; 1621 didToken = null; 1622 userMetadata = null; 1623 window.isAuthenticated = false; 1624 window.isAdmin = false; 1625 window.didToken = null; 1626} 1627 1628function hydraUrlRequestsLogout() { 1629 try { 1630 var params = new URLSearchParams(window.location.search); 1631 var path = (window.location.pathname || '/').replace(/\/+$/, '') || '/'; 1632 if (params.get('logout') === '1' || params.get('logout') === 'true') return true; 1633 if (params.get('hydra_logout') === '1' || params.get('hydra_logout') === 'true') return true; 1634 return path === '/_manage/logout'; 1635 } catch (e) { 1636 return false; 1637 } 1638} 1639 1640function hydraCleanLogoutFromUrl() { 1641 try { 1642 var params = new URLSearchParams(window.location.search); 1643 var path = (window.location.pathname || '/').replace(/\/+$/, '') || '/'; 1644 var hadQuery = params.has('logout') || params.has('hydra_logout'); 1645 params.delete('logout'); 1646 params.delete('hydra_logout'); 1647 var nextPath = path === '/_manage/logout' ? '/_manage' : window.location.pathname; 1648 var qs = params.toString(); 1649 var next = nextPath + (qs ? '?' + qs : '') + (window.location.hash || ''); 1650 if (hadQuery || path === '/_manage/logout') { 1651 history.replaceState({}, '', next); 1652 } 1653 } catch (e) {} 1654} 1655 1656(function hydraLogoutFromUrlEarly() { 1657 if (!hydraUrlRequestsLogout()) return; 1658 clearHydraClientAuthStorage(); 1659 try { sessionStorage.setItem('hydra_logout_cleared', '1'); } catch (e) {} 1660 hydraCleanLogoutFromUrl(); 1661 console.log('[Hydra] Auth cleared via logout link/query.'); 1662})(); 1663 1664function hydraUrlRequestsRefresh() { 1665 try { 1666 var params = new URLSearchParams(window.location.search); 1667 var v = params.get('refresh'); 1668 return v === '1' || v === 'true'; 1669 } catch (e) { 1670 return false; 1671 } 1672} 1673 1674function hydraClearGalleryClientCache() { 1675 try { 1676 Object.keys(localStorage).filter(function (key) { 1677 return key.includes('gallery') || key.includes('neon') || key.includes('lightbox'); 1678 }
1678).forEach(function (key) { 1679 localStorage.removeItem(key); 1680 }); 1681 } catch (e) {} 1682 try { 1683 delete window.__neonGalleryMainBootstrapWipeDone; 1684 } catch (e) { 1685 window.__neonGalleryMainBootstrapWipeDone = false; 1686 } 1687 window._forceGalleryReload = true; 1688} 1689 1690function hydraCleanRefreshFromUrl() { 1691 try { 1692 var params = new URLSearchParams(window.location.search); 1693 if (!params.has('refresh')) return; 1694 params.delete('refresh'); 1695 var qs = params.toString(); 1696 var next = window.location.pathname + (qs ? '?' + qs : '') + (window.location.hash || ''); 1697 history.replaceState({}, '', next); 1698 } catch (e) {} 1699} 1700 1701(function hydraRefreshFromUrlEarly() { 1702 if (!hydraUrlRequestsRefresh()) return; 1703 clearHydraClientAuthStorage(); 1704 hydraClearGalleryClientCache(); 1705 try { sessionStorage.setItem('hydra_refresh_cleared', '1'); } catch (e) {} 1706 hydraCleanRefreshFromUrl(); 1707 console.log('[Hydra] Gallery cache/auth cleared via ?refresh=1 â forcing fresh load.'); 1708})(); 1709 1710// State Management â site-wide KV metadata for the editor (not per-page SEO) 1711window.siteMetadata = {"title":"People - Adena Stevens Photography","description":"Hoboken and New York City photographer who specializes in portraits, corporate events, sports, and higher education photography.","googleAnalytics":"","noIndex":false,"favicon":"","ogImageUrl":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","customPublicSiteEnabled":false,"customPublicSiteHtmlR2Key":"","customPublicSiteHtmlFilename":"","schemaJobTitle":"","schemaCity":"","schemaEmail":""}; 1712console.log('Client Initialized site metadata:', window.siteMetadata); 1713 1714/** Match SSR title format: "{Gallery Title} | {Site Title}" */ 1715function formatGalleryDocumentTitle(galleryTitle, win) { 1716 win = win || window; 1717 var meta = win.siteMetadata; 1718 var siteTitle = (meta && meta.title && String(meta.title).trim()) || win.location.hostname; 1719 return (galleryTitle || 'Gallery') + ' | ' + siteTitle; 1720} 1721 1722console.log('Client Google Analytics from metadataToUse:', ''); 1723console.log('Client Google Analytics in window.siteMetadata:', window.siteMetadata.googleAnalytics); 1724console.log('Client Content Language:', window.siteMetadata.contentLanguage); 1725 1726// State Management 1727let galleries = [{"id":1776787399607,"title":"PEOPLE","parentId":null,"visible":true,"isIntegrated":true,"pageId":"aqipsifn","slug":"people","url":"/people","galleryOptions":{"manualCollectionName":"GUID=f21s3uk4","isIntegratedGallery":true,"pageId":"aqipsifn","siteId":"f21s3uk4","siteAlias":"www.adenastevens.com","initialPageUuid":"aqipsifn","initialPageAlias":"people","timestamp":1784745905380,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"grid-rows","useRowsLayout":true,"layoutType":"grid","columns":3,"itemsPerRow":3,"spacing":0.5,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","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,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":20,"layout":{"type":"grid","selection":"grid-rows","columns":3,"spacing":0.5,"useRowsLayout":true,"itemsPerRow":3,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=f21s3uk4":{"isClassicCollection":true,"guid":"GUID=f21s3uk4","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"nIcc-lvFuaURuazmETyFzghLEpUWRqkT-z_CJ0J-z7Q","txId":"nIcc-lvFuaURuazmETyFzghLEpUWRqkT-z_CJ0J-z7Q","flyIoSaveTimestamp":1784745032781,"irysSaveTimestamp":1776787802103,"timestampsMatch":false,"allTimestampsMatch":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"infoTextPlacement":"below","applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"isHydra":true},"siteId":"f21s3uk4","siteAlias":null,"isPage":false,"metaTitle":"People - Adena Stevens Photography","metaDescription":"Hoboken and New York City photographer who specializes in portraits, corporate events, sports, and education photography.","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false,"isH
1727omePage":true},{"id":1776787908728,"title":"ATHLETES","parentId":null,"visible":true,"isIntegrated":true,"pageId":"an5m6dbk","slug":"athletes","url":"/athletes","galleryOptions":{"manualCollectionName":"GUID=f21s3uk4","isIntegratedGallery":true,"pageId":"an5m6dbk","siteId":"f21s3uk4","siteAlias":"www.adenastevens.com","initialPageUuid":"an5m6dbk","initialPageAlias":"athletes","timestamp":1784746002720,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"grid-rows","useRowsLayout":true,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":0.5,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","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,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":20,"layout":{"type":"grid","selection":"grid-rows","columns":3,"spacing":0.5,"useRowsLayout":true,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=f21s3uk4":{"isClassicCollection":true,"guid":"GUID=f21s3uk4","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"eNdQ_l2SwodRuaXsr-jTuxxa96R9AW6sy06luw5O8f0","txId":"eNdQ_l2SwodRuaXsr-jTuxxa96R9AW6sy06luw5O8f0","flyIoSaveTimestamp":1784746004956,"irysSaveTimestamp":1776891304144,"timestampsMatch":false,"allTimestampsMatch":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"infoTextPlacement":"below","applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"isHydra":true},"siteId":"f21s3uk4","siteAlias":null,"isPage":false,"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false,"isHomePage":false},{"id":1776788654891,"title":"HIGHER ED","parentId":null,"visible":true,"isIntegrated":true,"pageId":"4munwywc","slug":"higher-ed","url":"/higher-ed","galleryOptions":{"manualCollectionName":"GUID=f21s3uk4","isIntegratedGallery":true,"pageId":"4munwywc","siteId":"f21s3uk4","siteAlias":"www.adenastevens.com","initialPageUuid":"4munwywc","initialPageAlias":"higher-ed","timestamp":1784750131910,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"grid-rows","useRowsLayout":true,"layoutType":"grid","columns":3,"itemsPerRow":3,"spacing":0.5,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","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":"
1727center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":20,"layout":{"type":"grid","selection":"grid-rows","columns":3,"spacing":0.5,"useRowsLayout":true,"itemsPerRow":3,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=f21s3uk4":{"isClassicCollection":true,"guid":"GUID=f21s3uk4","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"PQBERRwMcq5Oz5RTDOsGPowGUVADiXCA15aU429WwfA","txId":"PQBERRwMcq5Oz5RTDOsGPowGUVADiXCA15aU429WwfA","flyIoSaveTimestamp":1784750134352,"irysSaveTimestamp":1776789026027,"timestampsMatch":false,"allTimestampsMatch":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"infoTextPlacement":"below","applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"isHydra":true,"cdnNdjsonId":"d58486801774441798b792ef1337e7ec","cdnNdjsonUrl":"https://cdn.neonsky.app/d58486801774441798b792ef1337e7ec.ndjson","cdnNdjsonStableUrl":"https://embed.neonsky.app/ndjson/f21s3uk4_4munwywc.json","cdnSaveTimestamp":1784750138993},"siteId":"f21s3uk4","siteAlias":null,"isPage":false,"metaTitle":"Higher Ed - Adena Stevens Photography","metaDescription":"Hoboken and New York City photographer who specializes in portraits, corporate events, sports, and education photography.","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false,"isHomePage":false},{"id":1776790218826,"title":"BRANDING","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1776790218826","visible":true,"parentId":null,"siteId":"f21s3uk4","slug":"branding","url":"/branding","pageElements":[{"id":1776790221711,"type":"metadata","visible":true,"position":0,"metaTitle":"","metaDescription":"","metaKeywords":"","pagePaddingTop":"6vh","pagePaddingLeft":"3vw","pagePaddingRight":"3vw"},{"id":1776790228823,"type":"column-container","title":"Columns","visible":true,"position":1,"columns":[{"id":1776790231928,"hAlign":"left","vAlign":"top","elements":[{"id":1776790255465,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776794266298_Branding-001_yoga_branding_02-1920w.jpeg","imageKey":"f21s3uk4/1776794266298_Branding-001_yoga_branding_02-1920w.jpeg","imageFilename":"Branding-001_yoga_branding_02-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70},{"id":1776790228781,"hAlign":"left","vAlign":"top","elements":[{"id":1776790256151,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p><br></p><p><span style=\"font-size: 24px;\">Sam</span></p><p><span style=\"font-size: 24px;\">Bloom with Sam</span></p><p><br></p><p><span style=\"font-size: 16px;\">Sam is a yoga instructor in Jersey City and Hoboken. Her IG handle is bloomwithsam so I knew we had to include flowers in some of her photos. She wanted a combination of portraits and yoga poses to be able to use on social media as well as marketing for workshops. </span></p>","textWidth":100}],"widthPercent":30}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1776818922606,"type":"spacer","title":"Spacer Copy","visible":true,"position":2,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776793979457,"type":"column-container","title":"Columns Copy","visible":true,"position":3,"columns":[{"id":1776793987904,"hAlign":"left","vAlign":"top","elements":[{"id":1776793983652,"type":"text","t
1727itle":"Text Block","visible":true,"position":0,"textContent":"<p><span style=\"font-size: 24px;\">Dana</span></p><p><span style=\"font-size: 24px;\">Brooklyn Resume Studio</span></p><p><br></p><p><span style=\"font-size: 16px;\">Every few years, Dana refreshes her headshots and this time we did them at The Yard in Gowanus where her business is located. The shoot was a combination of portraits and also photos showing Dana working and interacting with clients. </span></p>","textWidth":100}],"widthPercent":30},{"id":1776793986478,"hAlign":"left","vAlign":"top","elements":[{"id":1776793983215,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776790406434_Branding-002_brooklyn_branding-1920w.jpeg","imageKey":"f21s3uk4/1776790406434_Branding-002_brooklyn_branding-1920w.jpeg","imageFilename":"Branding-002_brooklyn_branding-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1776818911697,"type":"spacer","title":"Spacer Copy","visible":true,"position":4,"spacerHeightVh":4.5,"spacerWidthVw":100},{"id":1776794700387,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":5,"columns":[{"id":1776794692712,"hAlign":"left","vAlign":"top","elements":[{"id":1776794698621,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776790949606_Branding-003_qigong_fall_01-1920w.jpeg","imageKey":"f21s3uk4/1776790949606_Branding-003_qigong_fall_01-1920w.jpeg","imageFilename":"Branding-003_qigong_fall_01-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70},{"id":1776794693084,"hAlign":"left","vAlign":"top","elements":[{"id":1776794693608,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p><span style=\"font-size: 24px;\">Erin Kumpf</span></p><p><span style=\"font-size: 24px;\">Fall shoot - QiGong</span></p><p><br></p><p><span style=\"font-size: 16px;\">Erin Kumpf is not only an amazing acupuncturist, but also practices and teaches QiGong. We did 4 seasonal shoots - starting with fall.</span></p><p><br></p><p><span style=\"font-size: 16px;\">QiGong (pronounced âChee gongâ) is a branch of Chinese Medicine (along with acupuncture, herbs, nutrition, tui na) with roots in Taoism that incorporates gentle stretches, strengthening postures, flowing movement, breath work and meditation. </span></p>","textWidth":100,"fadeInOnScroll":false}],"widthPercent":30}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1784750541377,"type":"spacer","title":"Spacer Copy Copy","visible":true,"position":6,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776797627294,"type":"column-container","title":"Columns Copy Copy Copy","visible":true,"position":7,"columns":[{"id":1776797634311,"hAlign":"left","vAlign":"top","elements":[{"id":1776797630209,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<h2><span style=\"font-size: 24px;\">Litzky PR</span></h2><p><span style=\"font-size: 16px;\">Litzky PR needed to update their website. We did individual headshots with a portable studio on site that I set up in a conference room, plus a team photo. </span></p><p><br></p>","textWidth":100,"fadeInOnScroll":false}],"widthPercent":30},{"id":1776797632058,"hAlign":"left","vAlign":"top","elements":[{"id":1776797635205,"type":"image","t
1727itle":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1784746790007_litzkyPR.webp","imageKey":"f21s3uk4/1784746790007_litzkyPR.webp","imageFilename":"litzkyPR.webp","imageType":"image/webp","altText":"","linkPageId":"","linkTarget":"_self","linkAutoAdvance":false,"linkAdvanceDuration":5,"fadeInOnScroll":false,"imageFit":"contain","imageAspectRatio":"auto","imageFrameHeight":"","imageBorderColor":"#1c1814","imageBorderOpacity":0,"imageBorderWidthPx":1,"imageBorderRadiusPx":0}],"widthPercent":70}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1784750524758,"type":"spacer","title":"Spacer Copy Copy","visible":false,"position":8,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776818882765,"type":"spacer","title":"Spacer","visible":true,"position":9,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776794214081,"type":"column-container","title":"Columns Copy","visible":true,"position":10,"columns":[{"id":1776794216867,"hAlign":"left","vAlign":"top","elements":[{"id":1776794220215,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776794156190_Branding-010_acupuncture_herbs_branding-1920w.jpeg","imageKey":"f21s3uk4/1776794156190_Branding-010_acupuncture_herbs_branding-1920w.jpeg","imageFilename":"Branding-010_acupuncture_herbs_branding-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70},{"id":1776794221664,"hAlign":"left","vAlign":"top","elements":[{"id":1776794214755,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p><span style=\"font-size: 24px;\">Nurturing Life Acupuncture & Wellness</span></p><p><br></p><p><span style=\"font-size: 16px;\">Client was updating website and needed photos of products for online store as well as photos of their space in Hoboken. </span></p>","textWidth":100}],"widthPercent":30}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1776868577370,"type":"spacer","title":"Spacer Copy","visible":false,"position":11,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776818876115,"type":"spacer","title":"Spacer","visible":true,"position":12,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776790942163,"type":"column-container","title":"Columns Copy","visible":true,"position":13,"columns":[{"id":1776790935131,"hAlign":"left","vAlign":"top","elements":[{"id":1776790937957,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<h2><span style=\"font-size: 24px;\">Kure Pilates</span></h2><p><span style=\"font-size: 16px;\">Photos were for a rebranding of their website. Client needed photos of the instructors and the client space. </span></p><p><br></p>","textWidth":100}],"widthPercent":30},{"id":1776790941706,"hAlign":"left","vAlign":"top","elements":[{"id":1776790939982,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776794784457_Branding-004_kure_pilates_branding-1920w.jpeg","imageKey":"f21s3uk4/1776794784457_Branding-004_kure_pilates_branding-1920w.jpeg","imageFilename":"Branding-004_kure_pilates_branding-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1784750493885,"type":"spacer","title":"Spacer Copy","visible":true,"position":14,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776797555900,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":15,"columns":[{"id":1776797554306,"hAlign":"left","vAlign":"top","elements":[{"id":1776797557772,"type":"image","t
1727itle":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776797562874_Branding-006_utica_plumbing-1920w.jpeg","imageKey":"f21s3uk4/1776797562874_Branding-006_utica_plumbing-1920w.jpeg","imageFilename":"Branding-006_utica_plumbing-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70},{"id":1776797554697,"hAlign":"left","vAlign":"top","elements":[{"id":1776797558714,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<h2><span style=\"font-size: 24px;\">Utica Avenue Plumbing Supply</span></h2><p><span style=\"font-size: 16px;\">When Utica Avenue Plumbing Supply decided to refresh their website, they needed headshots of everyone on staff. I was able to bring a mini studio to them and photograph everyone against the same neutral backdrop, which made adding the new photos to the website easy for their designer. </span></p><p><br></p>","textWidth":100}],"widthPercent":30}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1776818851299,"type":"spacer","title":"Spacer","visible":true,"position":16,"spacerHeightVh":5,"spacerWidthVw":100},{"id":1776795167767,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":17,"columns":[{"id":1776795163538,"hAlign":"left","vAlign":"top","elements":[{"id":1776795170591,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<h2><span style=\"font-size: 24px;\">Marsh & McLennan</span></h2><p><span style=\"font-size: 16px;\">Client celebrated the integration of their company with JLT. They needed office interior photographs that illustrated specific signage and the office spaces. </span></p><p><br></p>","textWidth":100}],"widthPercent":30},{"id":1776795163478,"hAlign":"left","vAlign":"top","elements":[{"id":1776795164737,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776797659640_Branding-007_nyc_office_branding-1920w.jpeg","imageKey":"f21s3uk4/1776797659640_Branding-007_nyc_office_branding-1920w.jpeg","imageFilename":"Branding-007_nyc_office_branding-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1776818828216,"type":"spacer","title":"Spacer","visible":true,"position":18,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776818599600,"type":"column-container","title":"Columns Copy Copy Copy","visible":true,"position":19,"columns":[{"id":1776818595572,"hAlign":"left","vAlign":"top","elements":[{"id":1776818600362,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776818639782_Branding-009_yoga_branding_01-1920w.jpeg","imageKey":"f21s3uk4/1776818639782_Branding-009_yoga_branding_01-1920w.jpeg","imageFilename":"Branding-009_yoga_branding_01-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70},{"id":1776818592802,"hAlign":"left","vAlign":"top","elements":[{"id":1776818599089,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<h2><span style=\"font-size: 24px;\">Lena</span></h2><p><span style=\"font-size: 16px;\">Lena was recently certified in teaching yoga and needed photos to share with the yoga studios where she would be teaching. </span></p><p><br></p>","textWidth":100}],"widthPercent":30}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}},{"id":1776818824001,"type":"spacer","title":"Spacer","visible":true,"position":20,"spacerHeightVh":6,"spacerWidthVw":100},{"id":1776818687062,"type":"column-container","title":"Columns Copy Copy Copy","visible":true,"position":21,"columns":[{"id":1776818686103,"hAlign":"left","vAlign":"top","elements":[{"id":1776818693758,"type":"text","t
1727itle":"Text Block","visible":true,"position":0,"textContent":"<h2><span style=\"font-size: 24px;\">Princeton University Economics Department</span></h2><p><span style=\"font-size: 16px;\">Headshots of Princeton University Economics Department's faculty and staff. The courtyard next to their building lent itself to a nice soft background that showed the campus but the focus remained on the individuals. </span></p><p><br></p>","textWidth":100}],"widthPercent":30},{"id":1776818694725,"hAlign":"left","vAlign":"top","elements":[{"id":1776818692788,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","imageUrl":"https://storage.neonsky.app/f21s3uk4/1776818779413_Branding-008_princeton_university_econ-1920w.jpeg","imageKey":"f21s3uk4/1776818779413_Branding-008_princeton_university_econ-1920w.jpeg","imageFilename":"Branding-008_princeton_university_econ-1920w.jpeg","imageType":"image/jpeg"}],"widthPercent":70}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoHeight":50,"randomizeVideo":false,"videoUrls":[]}}],"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false,"isHomePage":false},{"id":1776789594485,"title":"PLACES + THINGS","parentId":null,"visible":true,"isIntegrated":true,"pageId":"roh6f7ad","slug":"places--things","url":"/places--things","galleryOptions":{"manualCollectionName":"GUID=f21s3uk4","isIntegratedGallery":true,"pageId":"roh6f7ad","siteId":"f21s3uk4","siteAlias":"www.adenastevens.com","initialPageUuid":"roh6f7ad","initialPageAlias":"places--things","timestamp":1784750891268,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"grid-rows","useRowsLayout":true,"layoutType":"grid","columns":3,"itemsPerRow":3,"spacing":1,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","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,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":20,"layout":{"type":"grid","selection":"grid-rows","columns":3,"spacing":1,"useRowsLayout":true,"itemsPerRow":3,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=f21s3uk4":{"isClassicCollection":true,"guid":"GUID=f21s3uk4","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"I9aliryvb2GghRsEWtlLHtqq8UZVRrNuvLPoZv9ckvg","txId":"I9aliryvb2GghRsEWtlLHtqq8UZVRrNuvLPoZv9ckvg","flyIoSaveTimestamp":1776789866535,"irysSaveTimestamp":1776789866535,"timestampsMatch":false,"allTimestampsMatch":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"infoTextPlacement":"below","applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false},"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false,"siteId":"f21s3uk4","siteAlias":null,"isPage":false,"isH
1727omePage":false},{"id":1776819147538,"title":"ABOUT","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"4kuqohzp","visible":true,"parentId":null,"siteId":"f21s3uk4","slug":"about","url":"/about","pageElements":[{"id":1776819153165,"type":"metadata","visible":true,"position":1,"metaTitle":"","metaDescription":"","metaKeywords":"","pagePaddingTop":"10vh"},{"id":1776819149731,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"<p style=\"line-height: 1.5;\" class=\"ql-align-center\"><br></p><p style=\"line-height: 1.5;\"><span style=\"color: rgb(0, 0, 0); font-size: 16px;\"><span class=\"ql-cursor\"></span>Adena Stevens is a portrait and corporate event photographer based in Hoboken, New Jersey. With over two decades of experience, she specializes in portraits, corporate events, branding and lifestyle, and higher education photography in New Jersey and NYC. </span></p><p style=\"line-height: 1.5;\"><br></p><p style=\"line-height: 1.5;\"><span style=\"color: rgb(0, 0, 0); font-size: 16px;\">Adena earned a Bachelor of Science degree in photojournalism from the S.I. Newhouse School of Public Communications at Syracuse University. Her past experience includes staff photographer at both Asbury Park Press in Neptune, NJ and The Day in New London, CT, and digital media manager at Yeshiva University in New York City. </span></p><p style=\"line-height: 1.5;\"><span style=\"color: rgb(0, 0, 0); font-size: 16px;\"></span></p><p style=\"line-height: 1.5;\"><span style=\"color: rgb(0, 0, 0); font-size: 16px;\">Partial client list includes Princeton University, Yeshiva University, Ramapo College, Friends Seminary, Robert Half, and Brooklyn Resume Studio. </span></p><p><br></p>","textWidth":50,"fadeInOnScroll":false}],"isHomePage":false,"position":6,"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false},{"id":1776819067063,"title":"CONTACT","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1776819067063","visible":true,"parentId":null,"siteId":"f21s3uk4","slug":"contact","url":"/contact","pageElements":[{"id":1776819071298,"type":"metadata","visible":true,"position":1,"metaTitle":"","metaDescription":"","metaKeywords":"","pagePaddingTop":"10vh"},{"id":1776819076552,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"<p style=\"line-height: 1.5;\" class=\"ql-align-center\"><br></p><h2 class=\"ql-align-center\" style=\"line-height: 0.5;\"><span class=\"ql-cursor\"></span><span style=\"font-size: 16px;\">Adena Stevens Photography </span></h2><h2 class=\"ql-align-center\" style=\"line-height: 0.5;\"><span style=\"font-size: 16px;\">315.559.7631 </span></h2><h2 class=\"ql-align-center\" style=\"line-height: 0.5;\"><a href=\"mailto:[email protected]\" target=\"_blank\" style=\"color: rgb(147, 147, 147); font-size: 16px;\">[email protected]</a><span style=\"font-size: 16px;\"> </span></h2><p><br></p><h2 style=\"line-height: 0.5;\" class=\"ql-align-center\"><span style=\"font-family: "Hanken Grotesk", sans-serif; font-size: 16px;\">New Jersey & NYC</span></h2><p><br></p>","textWidth":63,"fadeInOnScroll":false}],"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false,"isHomePage":false},{"id":1776891434268,"title":"test","parentId":null,"visible":false,"isIntegrated":true,"pageId":"waa2k0e0","slug":"test","url":"/test","galleryOptions":{"manualCollectionName":"GUID=f21s3uk4","isIntegratedGallery":true,"pageId":"waa2k0e0","siteId":"f21s3uk4","siteAlias":"adenastevens.com","initialPageUuid":"waa2k0e0","initialPageAlias":"test","timestamp":1784752742345,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"descriptionsOnDemand":false,"showCounter":false,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","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":"
1727center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"filterMenuCollectionNames":"","filterMenuTitles":"","filterMenuStyle":"dropdown","rolloverSwap":false,"rolloverCollectionNames":"","openMultipleLightboxes":false,"batchSize":20,"layout":{"type":"grid","selection":"grid-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=f21s3uk4":{"isClassicCollection":true,"guid":"GUID=f21s3uk4","metadata":{},"totalItems":0}},"isPerma":false,"isHydra":true,"flyIoSaveTimestamp":1784752744958,"irysSaveTimestamp":1776891467155,"allTimestampsMatch":false,"timestampsMatch":false,"txId":"djDv4jUjWAiA9FJrQHUsuOL9t-SVRbhqiKSpt_fSfsM","permaURL":"djDv4jUjWAiA9FJrQHUsuOL9t-SVRbhqiKSpt_fSfsM","fileSize":2377,"originalFileSize":10320,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"infoTextPlacement":"below","applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"cdnNdjsonId":"6d8e35677b4741f19746c26104040278","cdnNdjsonUrl":"https://cdn.neonsky.app/6d8e35677b4741f19746c26104040278.ndjson","cdnNdjsonStableUrl":"https://embed.neonsky.app/ndjson/f21s3uk4_waa2k0e0.json","embedPointerUrl":"https://embed.neonsky.app/ndjson/f21s3uk4_waa2k0e0.json","cdnSaveTimestamp":1784752746601,"uploadedTo":["fly","cdn"]},"isPage":false}]; 1728window.galleries = galleries; 1729 1730function getSiteGalleriesArray() { 1731 if (typeof galleries !== 'undefined' && Array.isArray(galleries)) { 1732 return galleries; 1733 } 1734 if (typeof window.galleries !== 'undefined' && Array.isArray(window.galleries)) { 1735 return window.galleries; 1736 } 1737 if (window.GALLERY_DATA_CACHE && 1738 window.GALLERY_DATA_CACHE.initialized && 1739 Array.isArray(window.GALLERY_DATA_CACHE.galleries)) { 1740 return window.GALLERY_DATA_CACHE.galleries; 1741 } 1742 return []; 1743} 1744window.getSiteGalleriesArray = getSiteGalleriesArray; 1745 1746let activeGalleryId = null; 1747let sortableInstance = null; 1748let isEditing = false; 1749let initialPageInfo = {"id":1776787908728,"type":"gallery","pageId":"an5m6dbk","slug":"athletes","url":"/athletes"}; 1750 1751 1752// Store the current menu layout type (must be JSON-encoded â bare sidebar/horizontal is invalid JS) 1753let currentMenuLayout = null; 1754 1755/** True when this document is loaded in the chromeless iframe (?neon_embed=1 or body.neon-page-embed). */ 1756function isNeonPageEmbedShell() { 1757 try { 1758 if (typeof document !== 'undefined' && document.body && document.body.classList.contains('neon-page-embed')) 1759 return true; 1760 var _spEmbed = new URLSearchParams(window.location.search); 1761 return _spEmbed.get('neon_embed') === '1'; 1762 } catch (_eEmbed) { return false; } 1763} 1764 1765/** 1766 * Mirrors "hide menu on this page": in embed shells always hide menus (unless editing), 1767 * otherwise use each gallery/page flag. 1768 */ 1769function shouldApplyMenuHiddenForEmbedOrGallery(gallery) { 1770 var editing = typeof window.isInEditMode === 'function' && window.isInEditMode(); 1771 if (editing) 1772 return !!(gallery && gallery.hideMenuOnPage); 1773 return !!(gallery && gallery.hideMenuOnPage) || isNeonPageEmbedShell(); 1774} 1775 1776function shouldApplyMenuOverlayOnPage(gallery) { 1777 if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) 1778 return false; 1779 return !!(gallery && gallery.overlayMenuOnPage); 1780} 1781 1782function isOverlayMenuChromeActive() { 1783 return document.body.classList.contains('menu-overlay-on-page') 1784 && !document.body.classList.contains('edit-mode-active'); 1785} 1786 1787/** Style Customizer paints sidebar with inline background-color !important; beat that only while overlay is active. */ 1788function syncOverlayMenuInlineChrome() { 1789 var sidebar = document.querySelector('.sidebar'); 1790 var header = document.querySelector('.mobile-header'); 1791 if (isOverlayMenuChromeActive()) { 1792 if (sidebar) { 1793 sidebar.style.setProperty('background-color', 'transparent', 'important'); 1794 sidebar.style.setProperty('background-image', 'none', 'important'); 1795 } 1796 if (header) { 1797 header.style.setProperty('background-color', 'transparent', 'important'); 1798 header.style.setProperty('background-image', 'none', 'important'); 1799 } 1800 return; 1801 } 1802 if (sidebar) { 1803 var menuBg = (getComputedStyle(document.documentElement).getPropertyValue('--menu-background-color') || '').trim() || '#fafafa'; 1804 var hidden = document.body.classList.contains('menu-background-hidden'); 1805 sidebar.style.setProperty('background-color', hidden ? 'transparent' : menuBg, 'important'); 1806 if (hidden) { 1807 sidebar.style.setProperty('background-image', 'none', 'important'); 1808 } else { 1809 sidebar.style.removeProperty('background-image'); 1810 } 1811 } 1812 if (header) { 1813 header.style.removeProperty('background-color'); 1814 header.style.removeProperty('background-image'); 1815 } 1816} 1817 1818function applyMenuOverlayClassForGallery(gallery) { 1819 if (shouldApplyMenuOverlayOnPage(gallery)) { 1820 document.body.classList.add('menu-overlay-on-page'); 1821 } else { 1822 document.body.classList.remove('menu-overlay-on-page'); 1823 } 1824 syncOverlayMenuInlineChrome(); 1825} 1826 1827function hookMenuStyleCustomizerForOverlay() { 1828 var customizer = window.MenuStyleCustomizer; 1829 if (!customizer || customizer.__overlayBgHooked || typeof customizer.applyStyles !== 'function') 1830 return; 1831 var original = customizer.applyStyles.bind(customizer); 1832 customizer.applyStyles = function() { 1833 original.apply(this, arguments); 1834 if (isOverlayMenuChromeActive()) 1835 syncOverlayMenuInlineChrome(); 1836 }; 1837 customizer.__overlayBgHooked = true; 1838} 1839window.applyMenuOverlayClassForGallery = applyMenuOverlayClassForGallery; 1840 1841document.addEventListener('DOMContentLoaded', () => { 1842 if (window.hydraInitialized) { 1843 console.log("Hydra already initialized, skipping"); 1844 return; 1845 } 1846 window.hydraInitialized = true; 1847 document.body.classList.add('hydra-initialized'); 1848 try { 1849 if (sessionStorage.getItem('hydra_refresh_cleared') === '1') { 1850 sessionStorage.removeItem('hydra_refresh_cleared'); 1851 if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.resetInitializationState === 'function') { 1852 window.EnhancedGallerySystem.resetInitializationState(); 1853 } 1854 console.log('[Hydra] Applied ?refresh=1 â gallery framework reset for clean load.'); 1855 } 1856 } catch (e) {} 1857 try { 1858 var neonEmbedParams = new URLSearchParams(window.location.search); 1859 if (neonEmbedParams.get('neon_embed') === '1') { 1860 document.body.classList.add('neon-page-embed', 'menu-hidden-on-page'); 1861 } 1862 } catch (_neonEmbedErr) {} 1863 hookMenuStyleCustomizerForOverlay(); 1864 setTimeout(hookMenuStyleCustomizerForOverlay, 0); 1865 setTimeout(hookMenuStyleCustomizerForOverlay, 500); 1866 if (isOverlayMenuChromeActive()) 1867 syncOverlayMenuInlineChrome(); 1868 window.Parameters = window.Parameters || {}; 1869 if (localStorage.getItem('hydra_is_admin') === 'true') { 1870 document.body.classList.add('hydra-admin', 'hydra-authenticated'); 1871 isAdmin = true; 1872 isAuthenticated = true; 1873 console.log('Restored admin status from localStorage'); 1874 1875 // CRITICAL FIX: Clear all caches except auth tokens for logged-in admins 1876 // This ensures admins always see fresh data when opening new tabs 1877 try { 1878 console.log('Admin detected - clearing all caches except auth tokens...'); 1879 1880 // Save auth tokens first 1881 const savedAuthToken = localStorage.getItem('hydra_auth_token'); 1882 const savedAuthEmail = localStorage.getItem('hydra_auth_email'); 1883 const savedIsAdmin = localStorage.getItem('hydra_is_admin'); 1884 const savedDidToken = localStorage.getItem('hydra_did_token'); 1885 1886 // Clear ALL localStorage 1887 localStorage.clear(); 1888 1889 // Restore auth tokens 1890 if (savedAuthToken) localStorage.setItem('hydra_auth_token', savedAuthToken); 1891 if (savedAuthEmail) localStorage.setItem('hydra_auth_email', savedAuthEmail); 1892 if (savedIsAdmin) localStorage.setItem('hydra_is_admin', savedIsAdmin); 1893 if (savedDidToken) localStorage.setItem('hydra_did_token', savedDidToken); 1894 1895 // Clear ALL sessionStorage (auth tokens are not stored here) 1896 sessionStorage.clear(); 1897 1898 console.log('Cleared all caches except auth tokens for admin user'); 1899 } catch (cacheError) { 1900 console.warn('Error clearing caches on admin page load:', cacheError); 1901 // Don't fail page load if cache clearing fails 1902 } 1903 } 1904 1905 function ensureSidebarElementsAndRender() { 1906 console.log('Ensuring sidebar elements are available before rendering...'); 1907 1908 // Restore from siteConfig when hydrate missed or init invented a default menu 1909 if (window.SidebarManager) { 1910 var current = window.SidebarManager.elements; 1911 var missingOrDefault = !current || current.length === 0 || window.SidebarManager._createdDefaultElements; 1912 if (missingOrDefault && typeof siteConfig !== 'undefined' && siteConfig.sidebarElements && siteConfig.sidebarElements.length > 0) { 1913 console.log('Initializing SidebarManager elements from siteConfig'); 1914 window.SidebarManager.elements = siteConfig.sidebarElements; 1915 window.SidebarManager._elementsReady = true; 1916 window.SidebarManager._createdDefaultElements = false;
1917 if (typeof window.SidebarManager.renderElements === 'function') { 1918 window.SidebarManager.renderElements(); 1919 } 1920 } 1921 } 1922 1923 // Determine layout and render 1924 const currentMenuLayout = getEffectiveMenuLayout(); 1925 1926 if (currentMenuLayout === 'horizontal') { 1927 console.log('Rendering horizontal menu with ensured sidebar elements'); 1928 renderHorizontalMenu(); 1929 } else { 1930 renderGalleries(); 1931 } 1932} 1933 1934 setTimeout(() => { 1935 console.log("Initializing galleries:", galleries.length); 1936 // Collapse folders before first paint so nested items never flash as expanded plain text 1937 alignFolderStates(); 1938 1939 // CHANGED: Use the new function instead of direct rendering 1940 setTimeout(ensureSidebarElementsAndRender, 100); // Small delay to ensure sidebar init 1941 1942 initMobileMenu(); 1943 1944 /** If true, deep-link timeouts below must not call handleURLNavigation again (duplicate loadPage / blink). */ 1945 let bootstrapRouteAttempted = false; 1946 1947 // Handle direct URL navigation 1948 if (initialPageInfo) { 1949 console.log("Loading initial page from direct URL:", initialPageInfo); 1950 1951 // Find the gallery by ID 1952 const gallery = galleries.find(g => g.id === initialPageInfo.id); 1953 1954 if (gallery) { 1955 // Set active gallery ID 1956 activeGalleryId = gallery.id; 1957 1958 if (initialPageInfo.type === 'page') { 1959 console.log("Loading page directly:", gallery.title); 1960 loadPage(gallery.id); 1961 } else { 1962 console.log("Loading gallery directly:", gallery.title); 1963 loadGallery(gallery.id); 1964 } 1965 bootstrapRouteAttempted = true; 1966 } 1967 } else if (window.location.pathname === '/') { 1968 // No specific path, try to load home page 1969 console.log("No specific path, checking for home page"); 1970 if (!loadHomePage() && galleries.length > 0) { 1971 // Default to first gallery if no home page is set 1972 loadGallery(galleries[0].id); 1973 } 1974 bootstrapRouteAttempted = true; 1975 } else if (galleries.length > 0) { 1976 // Path exists but no matching initialPageInfo, try to handle URL path 1977 handleURLNavigation(); 1978 bootstrapRouteAttempted = true; 1979 } 1980 window.__hydraBootstrapRouteAttempted = bootstrapRouteAttempted; 1981 1982 // Log what was rendered 1983 console.log("Menu content:", document.getElementById('galleryTree')?.innerHTML || "Gallery tree not found"); 1984 1985 // Start with edit controls properly set 1986 toggleEditControlsVisibility(isAdmin && document.body.classList.contains('edit-mode-active')); 1987 1988 // Explicitly hide the import classic form too 1989 const importForm = document.getElementById('importClassicForm'); 1990 if (importForm) { 1991 importForm.style.display = 'none'; 1992 importForm.classList.remove('visible'); 1993 } 1994 1995 // Try to restore token from localStorage 1996 if (!didToken) { 1997 didToken = localStorage.getItem('hydra_auth_token'); 1998 if (didToken) { 1999 console.log('Restored auth token from localStorage'); 2000 } 2001 } 2002 2003 // Initialize Magic and check authentication 2004 2005 2006 // Initialize sidebar elements if available 2007 if (window.SidebarManager) { 2008 // Pass the initial elements from server config if available 2009 if (true) { 2010 window.SidebarManager.elements = [{"id":1776787283176,"type":"menu","title":"Menu","visible":true,"position":1},{"id":1776819336850,"type":"image","title":"Image","visible":true,"position":0,"imageData":"https://storage.neonsky.app/f21s3uk4/1776867536436_adena-stevens-photography-p.png","imageWidth":"100%","imageAlignment":"center","linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","linkAutoAdvance":false,"hideFromMobileMenu":true},{"id":1776878709559,"type":"social","title":"Social","visible":true,"position":2,"socialIconSize":16,"socialIcons":[{"type":"email","url":"mailto:[email protected]"},{"type":"instagram","url":"https://www.instagram.com/adenastevensphoto"}],"socialAlignment":"center"}]; 2011 if (window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 2012 window.SidebarManager._elementsReady = true; 2013 window.SidebarManager._createdDefaultElements = false;
2014 } 2015 } 2016 window.SidebarManager.init(); 2017 } 2018 2019 2020 // Initialize metadata from server config (site-wide only â not current page SEO) 2021window.siteMetadata = {"title":"People - Adena Stevens Photography","description":"Hoboken and New York City photographer who specializes in portraits, corporate events, sports, and higher education photography.","googleAnalytics":"","noIndex":false,"favicon":"","ogImageUrl":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","customPublicSiteEnabled":false,"customPublicSiteHtmlR2Key":"","customPublicSiteHtmlFilename":"","schemaJobTitle":"","schemaCity":"","schemaEmail":""}; 2022console.log('Client Initialized site metadata (second init):', window.siteMetadata); 2023console.log('Client Google Analytics from metadataToUse (second init):', ''); 2024console.log('Client Google Analytics in window.siteMetadata (second init):', window.siteMetadata.googleAnalytics); 2025console.log('Client Content Language (second init):', window.siteMetadata.contentLanguage); 2026 2027 // Update copyright footer after metadata is set
2027(important for main domain) 2028 if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') { 2029 window.SidebarManager.updateMetadataFooter(); 2030 } 2031 2032 2033 2034 const submenuCheck = document.getElementById('createSubmenu'); 2035 const submenuTitleField = document.getElementById('submenuTitle'); 2036 const submenuTitleGroup = submenuTitleField?.parentElement; 2037 2038 if (submenuCheck && submenuTitleGroup) { 2039 submenuCheck.addEventListener('change', function() { 2040 submenuTitleGroup.style.display = this.checked ? 'block' : 'none'; 2041 }); 2042 } 2043 2044 // Check authentication state after initialization 2045 if (isAuthenticated && isAdmin) { 2046 console.log('Already authenticated and admin, showing edit UI'); 2047 forceShowEditUI(); 2048 } else if (didToken) { 2049 console.log('Have token, checking admin status'); 2050 checkAuth(); 2051 } 2052 }, 100); // Small delay to ensure everything is loaded 2053 2054 setTimeout(() => { 2055 if (window.location.pathname === '/' || 2056 window.location.pathname === '/_manage' || window.location.pathname === '/_manage/' || 2057 (window.location.hostname === 'preview.neonsky.app' && 2058 window.location.pathname.split('/').length <= 2)) { 2059 // Root URL or preview URL with only GUID - check for home page 2060 // Skip if bootstrap already routed (themed sites boot slower and would hit a duplicate load) 2061 if (!initialPageInfo && !window.__hydraBootstrapRouteAttempted) { 2062 loadHomePage(); 2063 } 2064 } 2065 }, 500); 2066 2067 // Dedicated editor URL: open edit tools or OTP login without requiring + key 2068 setTimeout(function hydraManageEditorEntry() { 2069 var p = window.location.pathname; 2070 if (p !== '/_manage' && p !== '/_manage/') return; 2071 try { 2072 if (sessionStorage.getItem('hydra_logout_cleared') === '1') { 2073 sessionStorage.removeItem('hydra_logout_cleared'); 2074 console.log('[Hydra] Logout link cleared auth; skipping auto-login on /_manage.'); 2075 return; 2076 } 2077 } catch (e) {} 2078 if (document.body.classList.contains('edit-mode-active')) return; 2079 try { 2080 var token = window.didToken || localStorage.getItem('hydra_auth_token'); 2081 var admin = window.isAdmin === true || localStorage.getItem('hydra_is_admin') === 'true'; 2082 if (token && admin) { 2083 if (typeof window.toggleEditMode === 'function') window.toggleEditMode(); 2084 return; 2085 } 2086 if (token && typeof checkAdminStatus === 'function') { 2087 checkAdminStatus(token).then(function() { 2088 if (window.isAdmin && typeof window.toggleEditMode === 'function' && !document.body.classList.contains('edit-mode-active')) { 2089 window.toggleEditMode(); 2090 } 2091 }).catch(function() {}); 2092 return; 2093 } 2094 if (typeof startOTPLogin === 'function') { 2095 var pr = startOTPLogin(); 2096 if (pr && typeof pr.catch === 'function') pr.catch(function() {}); 2097 } 2098 } catch (e) { 2099 console.error('hydraManageEditorEntry:', e); 2100 } 2101 }, 2200); 2102}); 2103 2104/** 2105 * Accessible name for header/sidebar brand images (no visual change). 2106 * Prefers element title or site metadata over generic "Image" / filenames. 2107 */ 2108function resolveBrandLogoAlt(element) { 2109 if (window.SidebarManager && typeof window.SidebarManager._resolveBrandImageAlt === 'function') { 2110 return window.SidebarManager._resolveBrandImageAlt(element || {}); 2111 } 2112 const siteTitle = 2113 (window.siteMetadata && String(window.siteMetadata.title || '').trim()) || 2114 (document.title && document.title.includes('|') ? document.title.split('|').pop().trim() : '') || 2115 String(location.hostname || '').replace(/^www\./i, ''); 2116 const title = element && element.title ? String(element.title).trim() : ''; 2117 if (title && !/^image(\s*\d*)?$/i.test(title)) return title; 2118 return siteTitle || 'Site logo'; 2119} 2120 2121/** 2122 * Gets the first visible sidebar element (image or text) to be used as a logo or title. 2123 * @returns {object|null} The first suitable sidebar element or null. 2124 */ 2125 2126function getFirstSidebarElementForHeader() { 2127 if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 2128 const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible 2129 if (visibleElements.length > 0) { 2130 const sortedElements = visibleElements.sort((a, b) => a.position - b.position); 2131 2132 // Debug: Log all element types found 2133 const elementTypes = sortedElements.map(el => ({ type: el.type, id: el.id, title: el.title })); 2134 console.log('[getFirstSidebarElementForHeader] Visible elements found:', elementTypes); 2135 2136 for (let el of sortedElements) { 2137 if (el.type === 'image' || el.type === 'text') { // Filter 2: type 2138 console.log('[getFirstSidebarElementForHeader] Found matching element:', { type: el.type, id: el.id, title: el.title, linkType: el.linkType, linkUrl: el.linkUrl, linkPageId: el.linkPageId }); 2139 return el; 2140 } 2141 } 2142 // If loop finishes, no visible image or text found among visible elements 2143 console.log('[getFirstSidebarElementForHeader] Found visible elements, but none were type image/text. Element types found:', elementTypes.map(e => e.type)); 2144 return null; 2145 } else { 2146 console.log('[getFirstSidebarElementForHeader] No elements with visible !== false found in SidebarManager.elements. Total elements:', window.SidebarManager.elements.length); 2147 return null; 2148 } 2149 } 2150 console.log('[getFirstSidebarElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.'); 2151 return null; 2152} 2153 2154/** 2155 * Gets the first visible social icon element from sidebar elements. 2156 * @returns {object|null} The first suitable social element or null. 2157 */ 2158function getFirstSocialElementForHeader() { 2159 if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 2160 const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible 2161 if (visibleElements.length > 0) { 2162 const sortedElements = visibleElements.sort((a, b) => a.position - b.position); 2163 for (let el of sortedElements) { 2164 if (el.type === 'social') { // Filter for social type 2165 return el; 2166 } 2167 } 2168 // If loop finishes, no visible social element found 2169 console.log('[getFirstSocialElementForHeader] Found visible elements, but none were type social.'); 2170 return null; 2171 } else { 2172 console.log('[getFirstSocialElementForHeader] No elements with visible !== false found in SidebarManager.elements.'); 2173 return null; 2174 } 2175 } 2176 console.log('[getFirstSocialElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.'); 2177 return null; 2178} 2179 2180function isSpaceColonySiteTheme() { 2181 const themeId = document.body && document.body.dataset.siteTheme; 2182 return themeId === 'space-colony'; 2183} 2184 2185/** 2186 * Apply horizontal bar logo placement (inline vs stacked) from Menu Style > Bar logo & menu. 2187 * Sets body.horizontal-header-logo-stacked and flex on .logo-menu-container; updates --mobile-header-height when stacked. 2188 */ 2189function applyHorizontalLogoMenuLayoutStyles() { 2190 const sc = window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings; 2191 const mode = (sc && sc.horizontalHeaderLogoLayout) || 'inline'; 2192 const isSpaceColonyTheme = isSpaceColonySiteTheme(); 2193 document.body.classList.toggle('horizontal-header-logo-stacked', mode === 'stacked'); 2194 const w = document.querySelector('.logo-menu-container'); 2195 if (w) { 2196 const isMobile = window.innerWidth <= 768; 2197 w.style.display = 'flex'; 2198 w.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : 'center'; 2199 if (mode === 'stacked') { 2200 w.style.flexDirection = 'column'; 2201 w.style.gap = isMobile ? '8px' : '12px'; 2202 w.style.width = '100%'; 2203 w.style.boxSizing = 'border-box';
2204 w.style.flexGrow = isMobile ? '0' : '1'; 2205 w.style.minWidth = '0'; 2206 w.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : 'flex-start'; 2207 } else { 2208 w.style.flexDirection = 'row'; 2209 w.style.gap = '20px'; 2210 w.style.width = ''; 2211 w.style.boxSizing = ''; 2212 w.style.minWidth = ''; 2213 if (isMobile) { 2214 w.style.flexGrow = '0'; 2215 w.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center'; 2216 } else { 2217 w.style.flexGrow = ''; 2218 w.style.justifyContent = ''; 2219 } 2220 } 2221 } 2222 const mhc = document.querySelector('.mobile-header-content'); 2223 if (mhc) { 2224 const isMobile = window.innerWidth <= 768; 2225 if (isSpaceColonyTheme && !isMobile) { 2226 mhc.style.alignItems = 'flex-end'; 2227 mhc.style.flexGrow = '0'; 2228 mhc.style.width = ''; 2229 } else if (isSpaceColonyTheme && isMobile) { 2230 mhc.style.alignItems = 'flex-start'; 2231 mhc.style.justifyContent = 'flex-start'; 2232 mhc.style.flexGrow = ''; 2233 mhc.style.width = '100%'; 2234 } else if (!isMobile && mode === 'stacked') { 2235 mhc.style.alignItems = 'flex-start'; 2236 mhc.style.flexGrow = ''; 2237 mhc.style.width = ''; 2238 } else { 2239 mhc.style.alignItems = ''; 2240 mhc.style.flexGrow = ''; 2241 mhc.style.width = ''; 2242 } 2243 } 2244 if (typeof window.syncContentMarginToHorizontalHeader === 'function') { 2245 window.syncContentMarginToHorizontalHeader(); 2246 } 2247} 2248window.applyHorizontalLogoMenuLayoutStyles = applyHorizontalLogoMenuLayoutStyles; 2249 2250function parseHorizontalHeaderHeightPx(value, fallback) { 2251 if (value === null || value === undefined) { 2252 return fallback; 2253 } 2254 const parsed = parseInt(String(value).trim(), 10); 2255 if (Number.isNaN(parsed) || parsed < 1) { 2256 return fallback; 2257 } 2258 return parsed; 2259} 2260 2261function getUserHorizontalHeaderHeightPx() { 2262 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 2263 const fromSetting = parseHorizontalHeaderHeightPx(window.MenuStyleCustomizer.settings.mobileHeaderHeight, 0); 2264 if (fromSetting > 0) { 2265 return fromSetting; 2266 } 2267 } 2268 const fromVar = getComputedStyle(document.documentElement).getPropertyValue('--top-bar-height-setting').trim(); 2269 const parsedVar = parseHorizontalHeaderHeightPx(fromVar, 0); 2270 if (parsedVar > 0) { 2271 return parsedVar; 2272 } 2273 const themeMin = getComputedStyle(document.documentElement).getPropertyValue('--st-header-height').trim(); 2274 const parsedTheme = parseHorizontalHeaderHeightPx(themeMin, 0); 2275 if (parsedTheme > 0) { 2276 return parsedTheme; 2277 } 2278 return 60; 2279} 2280 2281function setHorizontalHeaderHeightVars(headerPx, contentMarginPx) { 2282 const headerVal = headerPx + 'px'; 2283 const marginVal = contentMarginPx + 'px'; 2284 const currentHeader = getComputedStyle(document.documentElement).getPropertyValue('--mobile-header-height').trim(); 2285 const currentMargin = getComputedStyle(document.documentElement).getPropertyValue('--content-margin-top').trim(); 2286 if (currentHeader === headerVal && currentMargin === marginVal) { 2287 return; 2288 } 2289 document.documentElement.style.setProperty('--mobile-header-height', headerVal); 2290 document.documentElement.style.setProperty('--content-margin-top', marginVal); 2291} 2292 2293function measureHorizontalHeaderNaturalHeight(header) { 2294 const el = header; 2295 const prevHeight = el.style.getPropertyValue('height'); 2296 const prevMinHeight = el.style.getPropertyValue('min-height'); 2297 /* Beat CSS min-height: var(--mobile-header-height) !important so a prior wrap 2298 doesnât floor the measurement when the viewport widens back to one row. */ 2299 el.style.setProperty('height', 'auto', 'important'); 2300 el.style.setProperty('min-height', '0', 'important'); 2301 const measured = Math.ceil(el.getBoundingClientRect().height); 2302 if (prevHeight) { 2303 el.style.setProperty('height', prevHeight); 2304 } else { 2305 el.style.removeProperty('height'); 2306 } 2307 if (prevMinHeight) { 2308 el.style.setProperty('min-height', prevMinHeight); 2309 } else { 2310 el.style.removeProperty('min-height'); 2311 } 2312 return measured; 2313} 2314 2315function applyHorizontalHeaderContentInset() { 2316 if (!document.body.classList.contains('menu-layout-horizontal')) { 2317 return; 2318 } 2319 if (window._horizontalHeaderInsetApplying) { 2320 return; 2321 } 2322 const header = document.querySelector('.mobile-header'); 2323 if (!header) { 2324 return; 2325 } 2326 2327 window._horizontalHeaderInsetApplying = true; 2328 try { 2329 const userH = getUserHorizontalHeaderHeightPx();
2330 /* Keep logo-cap var as a real px height. Sites can save mobileHeaderHeight as 0vh; 2331 leaving that on --top-bar-height-setting makes calc(var(...) - 8px) â 0 and hides the logo. */ 2332 document.documentElement.style.setProperty('--top-bar-height-setting', userH + 'px'); 2333 const hasExpandedMenu = header.querySelector('.horizontal-menu-item.expanded, .horizontal-menu-item.submenu-open'); 2334 const isStackedLogo = document.body.classList.contains('horizontal-header-logo-stacked'); 2335 const isPhone = window.innerWidth <= 768; 2336 2337 /* 2338 * Phone hamburger chrome: fixed Style Customizer height (link row is hidden). 2339 * Desktop inline may wrap beside the logo â measure like stacked so the bar can grow. 2340 * Stacked / open submenu: always measure. 2341 */ 2342 if (!hasExpandedMenu && !isStackedLogo && isPhone) { 2343 setHorizontalHeaderHeightVars(userH, userH); 2344 return; 2345 } 2346 2347 /* Desktop inline (wrap-capable), stacked logo, or open submenu */ 2348 const measured = measureHorizontalHeaderNaturalHeight(header); 2349 if (!measured || measured < 1) { 2350 setHorizontalHeaderHeightVars(userH, userH); 2351 return; 2352 } 2353 2354 let maxH = Math.max(200, Math.round(window.innerHeight * 0.5)); 2355 const clamped = Math.min(Math.max(measured, userH), maxH); 2356 /* Extra content margin only when bar grew past the user setting (wrapped / stacked / open menu) */ 2357 const contentMargin = clamped > userH ? clamped + 8 : clamped; 2358 setHorizontalHeaderHeightVars(clamped, contentMargin); 2359 } finally { 2360 window._horizontalHeaderInsetApplying = false; 2361 } 2362} 2363 2364function ensureHorizontalHeaderHeightObserver() { 2365 if (typeof ResizeObserver === 'undefined') { 2366 return; 2367 } 2368 const header = document.querySelector('.mobile-header'); 2369 if (!header) { 2370 return; 2371 } 2372 if (window._horizontalHeaderResizeObserver) { 2373 window._horizontalHeaderResizeObserver.disconnect(); 2374 } 2375 window._horizontalHeaderResizeObserver = new ResizeObserver(function() { 2376 applyHorizontalHeaderContentInset(); 2377 }); 2378 window._horizontalHeaderResizeObserver.observe(header); 2379} 2380 2381function syncContentMarginToHorizontalHeader() { 2382 if (!document.body.classList.contains('menu-layout-horizontal')) { 2383 return; 2384 } 2385 requestAnimationFrame(function() { 2386 requestAnimationFrame(function() { 2387 applyHorizontalHeaderContentInset(); 2388 ensureHorizontalHeaderHeightObserver(); 2389 }); 2390 }); 2391} 2392window.syncContentMarginToHorizontalHeader = syncContentMarginToHorizontalHeader; 2393window.applyHorizontalHeaderContentInset = applyHorizontalHeaderContentInset; 2394 2395function getEffectiveMenuLayout() { 2396 if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.getEffectiveMenuLayout === 'function') { 2397 return window.MenuStyleCustomizer.getEffectiveMenuLayout(); 2398 } 2399 if (document.body.classList.contains('menu-layout-horizontal')) return 'horizontal'; 2400 if (document.body.classList.contains('menu-layout-top')) return 'top'; 2401 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 2402 return window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; 2403 } 2404 return 'sidebar'; 2405} 2406 2407function ensureCorrectLayoutApplied() { 2408 // Check if MenuStyleCustomizer and its necessary parts exist 2409 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') { 2410 // Prefer in-editor settings (e.g. Style History preview) over theme/body heuristics 2411 const fromSettings = window.MenuStyleCustomizer.settings.menuLayout; 2412 const currentLayout = fromSettings || getEffectiveMenuLayout(); 2413 console.log(`Ensuring layout is applied: ${currentLayout}`); 2414 2415 // Apply the layout styles (CSS classes, display properties) 2416 window.MenuStyleCustomizer._applyMenuLayout(currentLayout); 2417 2418 // --- Force Header Structure Rebuild for Horizontal Layout --- 2419 if (currentLayout === 'horizontal') { 2420 const mobileHeader = document.querySelector('.mobile-header'); 2421 if (mobileHeader) { 2422 let mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content'); 2423 // Ensure the main content container exists 2424 if (!mobileHeaderContent) { 2425 mobileHeaderContent = document.createElement('div'); 2426 mobileHeaderContent.className = 'mobile-header-content'; 2427 const hamburgerBtn = mobileHeader.querySelector('.hamburger-btn'); 2428 if (hamburgerBtn) { 2429 mobileHeader.insertBefore(mobileHeaderContent, hamburgerBtn); 2430 } else { 2431 mobileHeader.appendChild(mobileHeaderContent); 2432 } 2433 } 2434 // Ensure specific sub-containers exist, creating them if necessary 2435 // This guarantees the structure is present before renderHorizontalMenu fills them. 2436 if (!mobileHeaderContent.querySelector('.horizontal-header-logo')) { 2437 const logoContainer = document.createElement('div'); 2438 logoContainer.className = 'horizontal-header-logo'; 2439 mobileHeaderContent.insertBefore(logoContainer, mobileHeaderContent.firstChild); 2440 } 2441 if (!mobileHeaderContent.querySelector('.horizontal-menu
2441-container')) { 2442 const menuContainer = document.createElement('div'); 2443 menuContainer.className = 'horizontal-menu-container'; 2444 const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo'); 2445 if (logoContainer) { 2446 logoContainer.insertAdjacentElement('afterend', menuContainer); 2447 } else { // Fallback if logo container wasn't found/created 2448 mobileHeaderContent.insertBefore(menuContainer, mobileHeaderContent.firstChild); 2449 } 2450 } 2451 // Apply flexbox styling to position social container at far right 2452 mobileHeaderContent.style.display = 'flex'; 2453 const isSpaceColonyTheme = isSpaceColonySiteTheme(); 2454 const isMobile = window.innerWidth <= 768; 2455 mobileHeaderContent.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : (isSpaceColonyTheme && isMobile ? 'flex-start' : 'center'); 2456 mobileHeaderContent.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : (isSpaceColonyTheme ? 'flex-start' : 'space-between'); 2457 if (isSpaceColonyTheme && !isMobile) { 2458 mobileHeaderContent.style.flexGrow = '0'; 2459 mobileHeaderContent.style.width = ''; 2460 } else { 2461 mobileHeaderContent.style.flexGrow = ''; 2462 mobileHeaderContent.style.width = '100%'; 2463 } 2464 2465 // Create a flex container for logo and menu items 2466 const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container'); 2467 if (!logoMenuContainer) { 2468 console.log('[ensureCorrectLayoutApplied] Creating logo-menu wrapper...'); 2469 const logoMenuWrapper = document.createElement('div'); 2470 logoMenuWrapper.className = 'logo-menu-container'; 2471 // Move logo and menu into the wrapper 2472 const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo'); 2473 const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container'); 2474 2475 console.log('[ensureCorrectLayoutApplied] Found logo container:', !!logoContainer); 2476 console.log('[ensureCorrectLayoutApplied] Found menu container:', !!menuContainer); 2477 2478 if (logoContainer && menuContainer) { 2479 // Insert wrapper before the logo 2480 mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer); 2481 // Move both containers into the wrapper 2482 logoMenuWrapper.appendChild(logoContainer); 2483 logoMenuWrapper.appendChild(menuContainer); 2484 console.log('[ensureCorrectLayoutApplied] Successfully moved logo and menu into wrapper'); 2485 } else { 2486 console.warn('[ensureCorrectLayoutApplied] Missing logo or menu container, cannot create wrapper'); 2487 } 2488 } else { 2489 console.log('[ensureCorrectLayoutApplied] Logo-menu wrapper already exists'); 2490 } 2491 if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') { 2492 applyHorizontalLogoMenuLayoutStyles(); 2493 } 2494 2495 // Add social icons container for horizontal layout (AFTER logo-menu wrapper is created) 2496 if (!mobileHeaderContent.querySelector('.horizontal-social-container')) { 2497 const socialContainer = document.createElement('div'); 2498 socialContainer.className = 'horizontal-social-container'; 2499 // Insert at the end (far right) of the mobile header content 2500 mobileHeaderContent.appendChild(socialContainer); 2501 } 2502 } 2503 } 2504 // --- End Header Structure Rebuild --- 2505 2506 2507 // Re-render the correct menu structure based on the applied layout 2508 if (currentLayout === 'horizontal') { 2509 // If horizontal layout, render the horizontal menu 2510 if (typeof renderHorizontalMenu === 'function') { 2511 renderHorizontalMenu(); 2512 } else { 2513 console.warn("renderHorizontalMenu function not found."); 2514 } 2515 2516 // After rendering horizontal menu, update mobile header content 2517 // This ensures mobile header shows logo instead of menu items on mobile 2518 setTimeout(() => { 2519 if (typeof updateMobileHeaderContent === 'function') { 2520 updateMobileHeaderContent(); 2521 } 2522 }, 100); 2523 } else { 2524 // For sidebar or top layouts, render the standard gallery tree 2525 if (typeof renderGalleries === 'function') { 2526 renderGalleries(); 2527 } else { 2528 console.warn("renderGalleries function not found."); 2529 } 2530 2531 // Update mobile header content for other layouts too 2532 setTimeout(() => { 2533 if (typeof updateMobileHeaderContent === 'function') { 2534 updateMobileHeaderContent(); 2535 } 2536 }, 100); 2537 } 2538 } else { 2539 // Log a warning if the necessary components aren't available 2540 console.warn("MenuStyleCustomizer or its methods not available to re-apply layout."); 2541 } 2542} 2543 2544function applyLayoutStylesOnly() { 2545 // Check if MenuStyleCustomizer and its necessary parts exist 2546 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') { 2547 // Get the current layout setting, default to 'sidebar' if not set 2548 const currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; 2549 console.log(`APPLY STYLES: Applying styles for layout: ${currentLayout}`); 2550 // Apply the layout styles (CSS classes, display properties) 2551 window.MenuStyleCustomizer._applyMenuLayout(currentLayout); // This function should ONLY set body classes/styles 2552 } else { 2553 console.warn("APPLY STYLES: MenuStyleCustomizer or its methods not available."); 2554 } 2555} 2556 2557// Add this function to ensure sidebar content for mobile 2558function ensureSidebarContentForMobile() { 2559 const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal'); 2560 const isMobileView = window.innerWidth <= 768; 2561 2562 if (isHorizontalLayout && isMobileView) { 2563 console.log('Ensuring sidebar content for mobile horizontal layout'); 2564 2565 // Make sure the sidebar tree is populated 2566 const galleryTreeContainer = document.getElementById('galleryTree'); 2567 if (galleryTreeContainer && (!galleryTreeContainer.innerHTML || galleryTreeContainer.innerHTML.trim() === '')) { 2568 console.log('Sidebar tree is empty, populating with galleries'); 2569 2570 // Render the galleries in the sidebar tree for mobile navigation 2571 if (typeof renderGalleries === 'function') { 2572 renderGalleries(); 2573 } 2574 } 2575 2576 // Also ensure sidebar elements are rendered 2577 if (window.SidebarManager && typeof window.SidebarManager.renderElements === 'function') { 2578 console.log('Re-rendering sidebar elements for mobile'); 2579 window.SidebarManager.renderElements(); 2580 } 2581 } 2582} 2583 2584function renderTextLogo(container, htmlContentFromSidebarElement) { 2585 if (!container) { 2586 console.error("[RenderTextLogo] Error: Logo container not found."); 2587 return; 2588 } 2589 2590 container.innerHTML = ''; 2591 2592 const raw = (htmlContentFromSidebarElement || '').trim(); 2593 if (!raw) { 2594 container.innerHTML = '<span class="site-title-text">Menu</span>'; 2595 } else if (!/<[a-z][sS]*>/i.test(raw)) { 2596 // Plain text â wrap so theme CSS can target .site-title-text 2597 container.innerHTML = '<span class="site-title-text">' + raw + '</span>'; 2598 } else { 2599 container.innerHTML = raw; 2600 const titleEl = container.querySelector('.site-title-text, h1, h2, p:last-of-type, p:only-of-type'); 2601 if (titleEl && !titleEl.classList.contains('site-title-text')) { 2602 titleEl.classList.add('site-title-text'); 2603 } 2604 } 2605
2606 // Do not force heading/paragraph margins to 0 â menu-styles.css matches 2607 // .sidebar-text rhythm (h1 margin-bottom 0.5em). Zeroing made the bar tighter. 2608 2609 console.log(`[RenderTextLogo] Text logo HTML rendered directly into container.`); 2610} 2611 2612function renderHorizontalMenu() { 2613 console.log("[RenderHorizontalMenu] Function CALLED. Current edit mode:", typeof isEditing !== 'undefined' ? isEditing : 'isEditing_undefined'); 2614 2615 // CRITICAL: Check if we're actually in horizontal layout (theme overlay counts) 2616 const currentLayout = getEffectiveMenuLayout(); 2617 2618 const isHorizontalLayout = currentLayout === 'horizontal'; 2619 console.log("[RenderHorizontalMenu] Current layout:", currentLayout, "isHorizontalLayout:", isHorizontalLayout); 2620 2621 // If not horizontal layout, only handle logo/alignment, then return early 2622 if (!isHorizontalLayout) { 2623 console.log("[RenderHorizontalMenu] Not in horizontal layout, only handling logo/alignment, skipping menu item creation"); 2624 // Still handle logo/alignment for sidebar layout 2625 const mobileHeader = document.querySelector('.mobile-header'); 2626 if (mobileHeader) { 2627 const logoContainer = mobileHeader.querySelector('.horizontal-header-logo'); 2628 if (logoContainer) { 2629 // Handle logo rendering only (code below will handle this) 2630 // But we'll return before creating menu items 2631 } 2632 } 2633 // Return early - don't create horizontal menu items in sidebar layout 2634 return; 2635 } 2636 2637 // Wait for sidebar elements to be available before rendering 2638 if (!window.SidebarManager || !window.SidebarManager.elements || window.SidebarManager.elements.length === 0) { 2639 console.log("[RenderHorizontalMenu] Sidebar elements not yet available, waiting..."); 2640 const waitForSidebarElements = (attempts = 0) => { 2641 const maxAttempts = 20; // Increased attempts for slower loads 2642 if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 2643 console.log("[RenderHorizontalMenu] Sidebar elements now available, proceeding with render."); 2644 renderHorizontalMenu(); 2645 } else if (attempts < maxAttempts) { 2646 const attemptNum = attempts + 1; 2647 setTimeout(() => waitForSidebarElements(attempts + 1), 100); 2648 } else { 2649 console.warn("[RenderHorizontalMenu] Sidebar elements not available after waiting, rendering anyway."); 2650 // Continue with render even if sidebar elements aren't loaded 2651 } 2652 }; 2653 waitForSidebarElements(); 2654 return; 2655 } 2656 2657 const mobileHeader = document.querySelector('.mobile-header'); 2658 if (!mobileHeader) { 2659 console.error("[RenderHorizontalMenu] CRITICAL: .mobile-header element NOT FOUND."); 2660 return; 2661 } 2662 2663 // Apply flexbox styling to position social container at far right 2664 const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content'); 2665 if (mobileHeaderContent) { 2666 mobileHeaderContent.style.display = 'flex'; 2667 const isSpaceColonyTheme = isSpaceColonySiteTheme(); 2668 const isMobile = window.innerWidth <= 768; 2669 mobileHeaderContent.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : (isSpaceColonyTheme && isMobile ? 'flex-start' : 'center'); 2670 mobileHeaderContent.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : (isSpaceColonyTheme ? 'flex-start' : 'space-between'); 2671 if (isSpaceColonyTheme && !isMobile) { 2672 mobileHeaderContent.style.flexGrow = '0'; 2673 mobileHeaderContent.style.width = ''; 2674 } else { 2675 mobileHeaderContent.style.flexGrow = ''; 2676 mobileHeaderContent.style.width = '100%'; 2677 } 2678 2679 // Create a flex container for logo and menu items 2680 const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container'); 2681 if (!logoMenuContainer) { 2682 console.log('[RenderHorizontalMenu] Creating logo-menu wrapper...'); 2683 const logoMenuWrapper = document.createElement('div'); 2684 logoMenuWrapper.className = 'logo-menu-container'; 2685 // Move logo and menu into the wrapper 2686 const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo'); 2687 const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu
2687-container'); 2688 2689 console.log('[RenderHorizontalMenu] Found logo container:', !!logoContainer); 2690 console.log('[RenderHorizontalMenu] Found menu container:', !!menuContainer); 2691 2692 if (logoContainer && menuContainer) { 2693 // Insert wrapper before the logo 2694 mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer); 2695 // Move both containers into the wrapper 2696 logoMenuWrapper.appendChild(logoContainer); 2697 logoMenuWrapper.appendChild(menuContainer); 2698 console.log('[RenderHorizontalMenu] Successfully moved logo and menu into wrapper'); 2699 } else { 2700 console.warn('[RenderHorizontalMenu] Missing logo or menu container, cannot create wrapper'); 2701 } 2702 } else { 2703 console.log('[RenderHorizontalMenu] Logo-menu wrapper already exists'); 2704 } 2705 if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') { 2706 applyHorizontalLogoMenuLayoutStyles(); 2707 } 2708 } 2709 2710 const logoContainer = mobileHeader.querySelector('.horizontal-header-logo'); 2711 if (!logoContainer) { 2712 console.error("[RenderHorizontalMenu] CRITICAL: .horizontal-header-logo container NOT FOUND within .mobile-header."); 2713 return; 2714 } 2715 2716 console.log("[RenderHorizontalMenu] Found .horizontal-header-logo container"); 2717 logoContainer.innerHTML = ''; // Clear existing logo content first 2718 2719 // FIXED: Check if we're on mobile - if so, don't render the horizontal logo 2720 const isMobileView = window.innerWidth <= 768; // Adjust breakpoint as needed 2721 2722 if (isMobileView) { 2723 console.log("[RenderHorizontalMenu] Mobile view detected - skipping horizontal logo rendering"); 2724 // On mobile, the logo will be handled by updateMobileHeaderContent() 2725 } else { 2726 console.log("[RenderHorizontalMenu] Desktop view - rendering horizontal logo"); 2727 2728 // Get the logo element data 2729 let logoElementData = null; 2730 try { 2731 logoElementData = getFirstSidebarElementForHeader(); 2732 console.log('[RenderHorizontalMenu] getFirstSidebarElementForHeader() returned:', JSON.stringify(logoElementData)); 2733 } catch (error) { 2734 console.error('[RenderHorizontalMenu] Error getting sidebar element:', error); 2735 logoElementData = getFirstSidebarElementFallback(); 2736 } 2737 2738 if (logoElementData) { 2739 if (logoElementData.type === 'image' && (logoElementData.imageData || logoElementData.imageUrl)) { 2740 console.log('[RenderHorizontalMenu] Attempting to render IMAGE logo with src:', logoElementData.imageData || logoElementData.imageUrl); 2741 2742 const img = document.createElement('img'); 2743 img.src = logoElementData.imageUrl || logoElementData.imageData; 2744 img.alt = resolveBrandLogoAlt(logoElementData); 2745 img.className = 'sidebar-image'; 2746 2747 // Apply comprehensive styling to ensure visibility 2748 img.style.display = 'block'; 2749 2750 // --- FIX --- 2751 // The line below was removed. It was applying a fixed max-height of 50px, 2752 // overriding your CSS. Removing it allows your stylesheet to take control. 2753 // img.style.maxHeight = '50px'; 2754 2755 img.style.objectFit = 'contain'; 2756 img.style.minWidth = '1px'; 2757 img.style.minHeight = '0'; 2758 2759 let logoDisplayNode = img; 2760 if (logoElementData.colorizeWithSiteColors && 2761 window.SidebarManager && 2762 typeof window.SidebarManager._buildSidebarImageDisplayNode === 'function') { 2763 const buildData = Object.assign({}, logoElementData); 2764 if (!buildData.imageWidth) { 2765 buildData.imageWidth = 'auto'; 2766 } 2767 logoDisplayNode = window.SidebarManager._buildSidebarImageDisplayNode(img, buildData); 2768 } else { 2769 img.style.width = 'auto'; 2770 img.style.visibility = 'visible'; 2771 img.style.opacity = '1'; 2772 } 2773 2774 // Opt-in per-image padding wrapper (no-op when unset; does not alter .horizontal-header-logo CSS) 2775 if (window.SidebarManager && 2776 typeof window.SidebarManager._wrapWithImagePadding === 'function') { 2777 logoDisplayNode = window.SidebarManager._wrapWithImagePadding(logoDisplayNode, logoElementData); 2778 } 2779 2780 // Enhanced event listeners for debugging 2781 img.onload = () => { 2782 console.log('[RenderHorizontalMenu] Image successfully LOADED'); 2783 }; 2784 2785 img.onerror = (error) => { 2786 console.error('[RenderHorizontalMenu] Image FAILED to load:', error); 2787 renderTextLogo(logoContainer, logoElementData.title || 'Logo'); 2788 }; 2789 2790 try { 2791 // Handle linking (similar to sidebar-manager.js) 2792 // Only add links in view mode, not edit mode 2793 // Explicit edit toggle only â Parameters.isInEditor is also true for logged-in admins 2794 const isEditing = document.body.classList.contains('edit-mode-active') || 2795 (typeof window.isInEditMode === 'function' && window.isInEditMode()); 2796 2797 // Debug: Log link properties and element data 2798 console.log('[RenderHorizontalMenu] Full logoElementData:', logoElementData); 2799 console.log('[RenderHorizontalMenu] Image link properties:', { 2800 linkType: logoElementData.linkType, 2801 linkUrl: logoElementData.linkUrl, 2802 linkPageId: logoElementData.linkPageId, 2803 linkTarget: logoElementData.linkTarget, 2804 isEditing: isEditing, 2805 hasLinkType: !!logoElementData.linkType, 2806 linkTypeValue: logoElementData.linkType, 2807 linkTypeNotNone: logoElementData.linkType && logoElementData.linkType !== 'none', 2808 notEditing: !isEditing, 2809 bodyHasEditClass: document.body.classList.contains('edit-mode-active'), 2810 isInEditMode: typeof window.isInEditMode === 'function' ? window.isInEditMode() : 'N/A', 2811 ParametersIsInEditor: window.Parameters ? window.Parameters.isInEditor : 'N/A' 2812 }); 2813 2814 // Check for link - handle undefined, null, empty string, or 'none' 2815 const hasLink = logoElementData.linkType && 2816 logoElementData.linkType !== 'none' && 2817 logoElementData.linkType !== '' && 2818 !isEditing; 2819 2820 console.log('[RenderHorizontalMenu] Has link?', hasLink, 'linkType:', logoElementData.linkType); 2821 2822 if (hasLink) { 2823 const link = document.createElement('a'); 2824 link.target = logoElementData.linkTarget || '_self'; 2825 let finalLinkHref = '#'; 2826 let isValidLink = false;
2827 2828 if (logoElementData.linkType === 'external' && logoElementData.linkUrl) { 2829 finalLinkHref = logoElementData.linkUrl; 2830 if (link.target === '_blank') { 2831 link.rel = 'noopener noreferrer'; 2832 } 2833 isValidLink = true; 2834 } else if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) { 2835 const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []); 2836 const linkPageIdNum = parseInt(logoElementData.linkPageId); 2837 console.log('[RenderHorizontalMenu] Looking for internal page:', { 2838 linkPageId: logoElementData.linkPageId, 2839 linkPageIdNum: linkPageIdNum, 2840 allGalleriesLength: allGalleries ? allGalleries.length : 0, 2841 galleryIds: allGalleries ? allGalleries.map(g => ({ id: g.id, title: g.title })).slice(0, 5) : [] 2842 }); 2843 2844 const targetPage = allGalleries.find(g => g.id === linkPageIdNum); 2845 2846 console.log('[RenderHorizontalMenu] Target page found:', targetPage ? { id: targetPage.id, title: targetPage.title, slug: targetPage.slug, url: targetPage.url } : null); 2847 2848 if (targetPage) { 2849 // Handle preview mode URLs 2850 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 2851 console.log('[RenderHorizontalMenu] Preview mode:', isPreviewMode); 2852 2853 if (isPreviewMode) { 2854 const pathParts = window.location.pathname.split('/').filter(Boolean); 2855 const siteGuid = pathParts[0]; 2856 console.log('[RenderHorizontalMenu] Preview mode path parts:', pathParts, 'siteGuid:', siteGuid, 'targetPage.slug:', targetPage.slug); 2857 2858 if (siteGuid && targetPage.slug) { 2859 finalLinkHref = '/' + siteGuid + '/' + targetPage.slug; 2860 isValidLink = true; 2861 console.log('[RenderHorizontalMenu] Set preview href:', finalLinkHref); 2862 } else if (targetPage.url && targetPage.url.startsWith('/')) { 2863 finalLinkHref = targetPage.url; 2864 isValidLink = true; 2865 console.log('[RenderHorizontalMenu] Set preview href from url:', finalLinkHref); 2866 } 2867 } else { 2868 if (targetPage.slug) { 2869 finalLinkHref = '/' + targetPage.slug; 2870 isValidLink = true; 2871 console.log('[RenderHorizontalMenu] Set non-preview href:', finalLinkHref); 2872 } else if (targetPage.url && targetPage.url.startsWith('/')) { 2873 finalLinkHref = targetPage.url; 2874 isValidLink = true; 2875 console.log('[RenderHorizontalMenu] Set non-preview href from url:', finalLinkHref); 2876 } 2877 } 2878 } else { 2879 console.warn('[RenderHorizontalMenu] Target page not found for linkPageId:', logoElementData.linkPageId); 2880 } 2881 } 2882 2883 if (isValidLink) { 2884 link.href = finalLinkHref; 2885 // Ensure link is clickable 2886 link.style.pointerEvents = 'auto'; 2887 link.style.cursor = 'pointer'; 2888 link.style.textDecoration = 'none'; 2889 link.style.display = 'flex'; 2890 link.style.alignItems = 'center'; 2891 link.style.height = 'auto'; 2892 link.style.maxHeight = '100%'; 2893 link.style.minHeight = '0'; 2894 link.style.lineHeight = '0'; 2895 console.log('[RenderHorizontalMenu] Link created with href:', finalLinkHref, 'target:', link.target, 'styles applied'); 2896 2897 // For internal links, add click handler to use site navigation 2898 if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) { 2899 // Store linkPageId in a variable that won't be affected by closure 2900 const linkPageId = logoElementData.linkPageId; 2901 const linkHref = finalLinkHref; 2902 2903 // Add click handler with both capture and bubble phases to ensure it fires 2904 const clickHandler = (e) => { 2905 console.log('[RenderHorizontalMenu] Link clicked! linkPageId:', linkPageId, 'event:', e); 2906 e.preventDefault(); 2907 e.stopPropagation(); 2908 e.stopImmediatePropagation(); 2909 2910 const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []); 2911 const linkPageIdNum = parseInt(linkPageId); 2912 const targetPage = allGalleries.find(g => g.id === linkPageIdNum); 2913 2914 console.log('[RenderHorizontalMenu] Click handler - targetPage found:', targetPage ? { id: targetPage.id, title: targetPage.title, isPage: targetPage.isPage } : null); 2915 console.log('[RenderHorizontalMenu] Click handler - loadPage function:', typeof window.loadPage, 'loadGallery function:', typeof window.loadGallery); 2916 2917 if (targetPage) { 2918 if (isPageMenuItem(targetPage) && typeof window.loadPage === 'function') { 2919 console.log('[RenderHorizontalMenu] Calling loadPage with id:', targetPage.id); 2920 window.loadPage(targetPage.id); 2921 } else if (typeof window.loadGallery === 'function') { 2922 console.log('[RenderHorizontalMenu] Calling loadGallery with id:', targetPage.id); 2923 window.loadGallery(targetPage.id); 2924 } else { 2925 // Fallback to standard navigation 2926 console.log('[RenderHorizontalMenu] Fallback to window.location.href:', linkHref); 2927 window.location.href = linkHref; 2928 } 2929 } else { 2930 // Fallback to standard navigation if page not found 2931 console.warn('[RenderHorizontalMenu] Target page not found in click handler, using fallback href:', linkHref); 2932 window.location.href = linkHref; 2933 } 2934 2935 return false;
2936 }; 2937 2938 // Add handler in capture phase to catch event early 2939 link.addEventListener('click', clickHandler, true); 2940 // Also add in bubble phase as backup 2941 link.addEventListener('click', clickHandler, false); 2942 2943 // Also add mousedown as backup in case click is being prevented 2944 link.addEventListener('mousedown', (e) => { 2945 if (e.button === 0) { // Left click only 2946 console.log('[RenderHorizontalMenu] MouseDown detected on link, triggering click handler'); 2947 clickHandler(e); 2948 } 2949 }); 2950 2951 console.log('[RenderHorizontalMenu] Click handlers attached to link'); 2952 } 2953 2954 link.appendChild(logoDisplayNode); 2955 logoContainer.appendChild(link); 2956 console.log('[RenderHorizontalMenu] SUCCESSFULLY appended linked image to logoContainer. Link element:', link, 'href:', link.href, 'parent:', logoContainer); 2957 2958 // Verify link is in DOM and clickable 2959 setTimeout(() => { 2960 const verifyLink = logoContainer.querySelector('a'); 2961 if (verifyLink) { 2962 console.log('[RenderHorizontalMenu] Link verification - Found in DOM:', { 2963 href: verifyLink.href, 2964 pointerEvents: window.getComputedStyle(verifyLink).pointerEvents, 2965 cursor: window.getComputedStyle(verifyLink).cursor, 2966 zIndex: window.getComputedStyle(verifyLink).zIndex, 2967 position: window.getComputedStyle(verifyLink).position 2968 }); 2969 } else { 2970 console.warn('[RenderHorizontalMenu] Link verification - Link NOT found in DOM!'); 2971 } 2972 }, 100); 2973 } else { 2974 console.warn('[RenderHorizontalMenu] Link is not valid, appending image without link. isValidLink:', isValidLink); 2975 logoContainer.appendChild(logoDisplayNode); 2976 console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.'); 2977 } 2978 } else { 2979 logoContainer.appendChild(logoDisplayNode); 2980 console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.'); 2981 } 2982 } catch (e) { 2983 console.error('[RenderHorizontalMenu] Error during logoContainer.appendChild(img):', e); 2984 renderTextLogo(logoContainer, logoElementData.title || 'Logo'); 2985 } 2986 2987 } else if (logoElementData.type === 'text' && logoElementData.textContent) { 2988 console.log('[RenderHorizontalMenu] Attempting to render TEXT logo:', logoElementData.textContent); 2989 renderTextLogo(logoContainer, logoElementData.textContent); 2990 2991 } else { 2992 console.log('[RenderHorizontalMenu] Using title as fallback logo'); 2993 renderTextLogo(logoContainer, logoElementData.title || 'Menu'); 2994 } 2995 } else { 2996 console.log("[RenderHorizontalMenu] No logoElementData available. Using default text."); 2997 renderTextLogo(logoContainer, 'Menu'); 2998 } 2999 } 3000 3001 // Render menu items (only on desktop, not on mobile) 3002 const menuContainer = mobileHeader.querySelector('.horizontal-menu-container'); 3003 if (!menuContainer) { 3004 console.error("[RenderHorizontalMenu] .horizontal-menu-container NOT FOUND within .mobile-header."); 3005 } else { 3006 menuContainer.innerHTML = ''; 3007 3008 // Apply alignment class from settings 3009 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 3010 const rawAlign = window.MenuStyleCustomizer.settings.horizontalMenuAlignment || 'left'; 3011 const alignment = ['left', 'center', 'right'].indexOf(rawAlign) >= 0 ? rawAlign : 'left'; 3012 menuContainer.classList.remove('menu-align-left', 'menu-align-right', 'menu-align-center'); 3013 menuContainer.classList.add('menu-align-' + alignment); 3014 console.log('[RenderHorizontalMenu] Applied alignment class: menu-align-'
3014+ alignment); 3015 } 3016 3017 // Only populate menu items on desktop, not on mobile 3018 if (!isMobileView) { 3019 let galleryTree = []; 3020 if (typeof createGalleryTree === 'function' && typeof galleries !== 'undefined') { 3021 galleryTree = createGalleryTree(galleries); 3022 } else { 3023 console.warn("[RenderHorizontalMenu] createGalleryTree function or galleries array is undefined."); 3024 } 3025 3026 galleryTree.forEach(item => { 3027 const menuItemElement = createHorizontalMenuItem(item); 3028 if (menuItemElement) { 3029 menuContainer.appendChild(menuItemElement); 3030 } 3031 }); 3032 if (typeof syncSeoNavA11y === 'function') syncSeoNavA11y(); 3033 } else { 3034 console.log("[RenderHorizontalMenu] Mobile view - skipping horizontal menu item population"); 3035 } 3036 } 3037 3038 // Render social icons (only on desktop, not on mobile) 3039 let socialContainer = mobileHeader.querySelector('.horizontal-social-container'); 3040 if (!socialContainer) { 3041 // Fallback: create the social container if it doesn't exist 3042 console.log("[RenderHorizontalMenu] .horizontal-social-container not found, creating it..."); 3043 const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content'); 3044 if (mobileHeaderContent) { 3045 socialContainer = document.createElement('div'); 3046 socialContainer.className = 'horizontal-social-container'; 3047 mobileHeaderContent.appendChild(socialContainer); 3048 console.log("[RenderHorizontalMenu] Created .horizontal-social-container"); 3049 } else { 3050 console.error("[RenderHorizontalMenu] .mobile-header-content not found, cannot create social container"); 3051 } 3052 } 3053 3054 if (socialContainer) { 3055 socialContainer.innerHTML = ''; 3056 3057 // Only populate social icons on desktop, not on mobile 3058 if (!isMobileView) { 3059 const socialElement = getFirstSocialElementForHeader(); 3060 if (socialElement && socialElement.type === 'social') { 3061 console.log('[RenderHorizontalMenu] Found social element, rendering social icons'); 3062 renderSocialIcons(socialContainer, socialElement); 3063 } else { 3064 console.log('[RenderHorizontalMenu] No social element found or element is not social type'); 3065 // Container remains empty - no impact on layout 3066 } 3067 } else { 3068 console.log("[RenderHorizontalMenu] Mobile view - skipping social icons population"); 3069 } 3070 } 3071 3072 3073 3074 if (typeof updateActiveStatesHorizontal === 'function') { 3075 updateActiveStatesHorizontal(); 3076 } 3077 3078 if (!isMobileView && mobileHeaderContent) { 3079 mobileHeaderContent.querySelectorAll('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title').forEach(function(el) { 3080 if (!el.closest('.horizontal-header-logo')) { 3081 el.remove(); 3082 } 3083 }); 3084 } 3085 3086setTimeout(() => { 3087 ensureSidebarContentForMobile(); 3088}, 100); 3089 3090 if (typeof syncContentMarginToHorizontalHeader === 'function') { 3091 setTimeout(function() { syncContentMarginToHorizontalHeader(); }, 200); 3092 } 3093 3094 console.log("[RenderHorizontalMenu] Function COMPLETED."); 3095} 3096 3097/** 3098 * Renders social icons in the horizontal header container. 3099 * @param {HTMLElement} container - The container to render social icons in 3100 * @param {object} socialElement - The social element data from sidebar 3101 */
3102function renderSocialIcons(container, socialElement) { 3103 console.log('[RenderSocialIcons] Rendering social icons:', socialElement); 3104 3105 try { 3106 // Safety check: ensure we have a valid social element 3107 if (!socialElement || socialElement.type !== 'social') { 3108 console.log('[RenderSocialIcons] Invalid social element - skipping rendering'); 3109 return; 3110 } 3111 3112 /* Placeholders without URLs still need menu color (CSS also sets this). */ 3113 container.style.color = 'var(--menu-color, #333)'; 3114 3115 // Get icon size with proper fallbacks 3116 let iconSize = 30; 3117 if (typeof socialElement.socialIconSize === 'number' && !isNaN(socialElement.socialIconSize)) { 3118 iconSize = socialElement.socialIconSize; 3119 } else if (typeof socialElement.socialIconSize === 'string' && !isNaN(parseInt(socialElement.socialIconSize))) { 3120 iconSize = parseInt(socialElement.socialIconSize); 3121 } 3122 3123 // Render icons directly from element data 3124 if (socialElement.socialIcons && Array.isArray(socialElement.socialIcons)) { 3125 socialElement.socialIcons.forEach(icon => { 3126 // Create icon wrapper (color via CSS --menu-color so placeholders without URLs stay visible) 3127 const iconWrapper = document.createElement('div'); 3128 iconWrapper.className = 'social-icon-wrapper'; 3129 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;'; 3130 3131 const applySvgSizing = (host) => { 3132 const svg = host.querySelector('svg'); 3133 if (!svg) return; 3134 svg.style.width = '100%'; 3135 svg.style.height = '100%'; 3136 svg.setAttribute('width', '100%'); 3137 svg.setAttribute('height', '100%'); 3138 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); 3139 svg.setAttribute('aria-hidden', 'true'); 3140 svg.setAttribute('focusable', 'false'); 3141 svg.style.color = 'inherit'; 3142 svg.style.fill = 'currentColor'; 3143 svg.style.stroke = 'currentColor'; 3144 }; 3145 3146 const hasUrl = !!(icon.url && String(icon.url).trim()); 3147 3148 // Add link if URL exists; otherwise still render the glyph (placeholder icons) 3149 if (hasUrl) { 3150 const link = document.createElement('a'); 3151 link.href = icon.url; 3152 link.target = '_blank'; 3153 link.rel = 'noopener noreferrer'; 3154 link.classList.add('horizontal-social-link'); 3155 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;'; 3156 const socialLabel = (icon.alt && String(icon.alt).trim()) 3157 || (window.SidebarManager && typeof window.SidebarManager._socialIconTypeLabel === 'function' 3158 ? window.SidebarManager._socialIconTypeLabel(icon.type) 3159 : (icon.type && icon.type !== 'custom' ? String(icon.type).replace(/_/g, ' ') : '')) 3160 || 'Social link'; 3161 const socialLabelPretty = socialLabel.charAt(0).toUpperCase() + socialLabel.slice(1); 3162 link.setAttribute('aria-label', socialLabelPretty + ' (opens in new tab)'); 3163 3164 if (icon.type === 'custom' && icon.iconUrl) { 3165 // Render uploaded image 3166 const img = document.createElement('img'); 3167 img.src = icon.iconUrl; 3168 img.alt = ''; 3169 img.setAttribute('aria-hidden', 'true'); 3170 img.style.width = '100%'; 3171 img.style.height = '100%'; 3172 img.style.objectFit = 'contain'; 3173 link.appendChild(img); 3174 } else { 3175 // Add SVG - use SidebarManager's method if available, otherwise create basic SVG 3176 if (window.SidebarManager && typeof window.SidebarManager._getSocialIconSvg === 'function') { 3177 link.innerHTML = window.SidebarManager._getSocialIconSvg(icon.type); 3178 } else { 3179 // Fallback: create a simple SVG placeholder 3180 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>'; 3181 } 3182 applySvgSizing(link); 3183 } 3184 iconWrapper.appendChild(link); 3185 } else {
3186 // Placeholder: no URL â still paint with menu color via container inheritance 3187 iconWrapper.setAttribute('aria-hidden', 'true'); 3188 if (icon.type === 'custom' && icon.iconUrl) { 3189 const img = document.createElement('img'); 3190 img.src = icon.iconUrl; 3191 img.alt = ''; 3192 img.style.width = '100%'; 3193 img.style.height = '100%'; 3194 img.style.objectFit = 'contain'; 3195 iconWrapper.appendChild(img); 3196 } else { 3197 if (window.SidebarManager && typeof window.SidebarManager._getSocialIconSvg === 'function') { 3198 iconWrapper.innerHTML = window.SidebarManager._getSocialIconSvg(icon.type); 3199 } else { 3200 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>'; 3201 } 3202 applySvgSizing(iconWrapper); 3203 } 3204 } 3205 3206 container.appendChild(iconWrapper); 3207 console.log('[RenderSocialIcons] Rendered social icon:', icon.type, icon.url); 3208 }); 3209 } else { 3210 console.log('[RenderSocialIcons] No social icons found in element'); 3211 } 3212 } catch (error) { 3213 console.error('[RenderSocialIcons] Error rendering social icons:', error); 3214 } 3215} 3216 3217/** 3218 * Gets the appropriate icon text or class for a social platform. 3219 * @param {string} platform - The social platform name 3220 * @returns {string} The icon text or class 3221 */ 3222function getSocialIconText(platform) { 3223 const iconMap = { 3224 'facebook': 'f', 3225 'twitter': 't', 3226 'instagram': 'i', 3227 'linkedin': 'in', 3228 'youtube': 'yt', 3229 'pinterest': 'p', 3230 'tiktok': 'tt', 3231 'snapchat': 'sc', 3232 'whatsapp': 'wa', 3233 'telegram': 'tg', 3234 'discord': 'd', 3235 'github': 'gh', 3236 'email': '@', 3237 'phone': 'ð', 3238 'website': 'ð' 3239 }; 3240 3241 return iconMap[platform.toLowerCase()] || 'â¢'; 3242} 3243 3244 3245 3246 3247/** 3248 * Creates a single top-level horizontal menu item and its submenu if it has children. 3249 * (UPDATED to handle async loadGallery and pass clickedItemId to a delayed updateActiveStatesHorizontal) 3250 * @param {object} galleryItem - The gallery item data. 3251 * @returns {HTMLElement | null} The created menu item element, or null if not rendered. 3252 */ 3253function createHorizontalMenuItem(galleryItem) { 3254 if (galleryItem.visible === false || galleryItem.isSpacer) { 3255 return null; 3256 } 3257 3258 const menuItem = document.createElement('div'); 3259 menuItem.className = 'horizontal-menu-item'; 3260 menuItem.dataset.id = String(galleryItem.id); 3261 menuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false'); 3262 3263 const hasVisibleChildren = !!(galleryItem.children && galleryItem.children.filter(c => c.visible !== false && !c.isSpacer).length); 3264 const isNavigableParent = isNavigableMenuParent(galleryItem); 3265 let titleEl; 3266 if (hasVisibleChildren && !isNavigableParent) { 3267 titleEl = document.createElement('span'); 3268 titleEl.className = 'horizontal-menu-label'; 3269 titleEl.textContent = galleryItem.title; 3270 menuItem.setAttribute('aria-haspopup', 'true'); 3271 menuItem.setAttribute('aria-expanded', 'false'); 3272 } else { 3273 titleEl = document.createElement('a'); 3274 titleEl.className = 'horizontal-menu-label'; 3275 titleEl.href = typeof getMenuItemHref === 'function' ? getMenuItemHref(galleryItem) : '#'; 3276 titleEl.textContent = galleryItem.title; 3277 if (galleryItem.isExternal) { 3278 titleEl.target = '_blank'; 3279 titleEl.rel = 'noopener noreferrer'; 3280 } else { 3281 titleEl.addEventListener('click', (e) => { e.preventDefault(); }); 3282 } 3283 } 3284 menuItem.appendChild(titleEl); 3285 3286 // Ensure activeGalleryId is treated as a number for comparison during initial render 3287 const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId)); 3288 if (galleryItem.id === currentGlobalActiveId) { 3289 menuItem.classList.add('active'); 3290 } 3291 3292 let leaveTimer; // For mouseleave timeout to close dropdown 3293 3294 if (galleryItem.children && galleryItem.children.length > 0) { 3295 const visibleChildren = galleryItem.children.filter(child => child.visible !== false && !child.isSpacer); 3296 3297 if (visibleChildren.length > 0) { 3298 menuItem.classList.add('has-children'); 3299 if (isNavigableParent) { 3300 menuItem.setAttribute('aria-haspopup', 'true'); 3301 menuItem.setAttribute('aria-expanded', 'false'); 3302 } 3303 3304 const toggleIconContainer = document.createElement(isNavigableParent ? 'button' : 'span'); 3305 toggleIconContainer.className = 'submenu-toggle-horizontal'; 3306 if (isNavigableParent) { 3307 toggleIconContainer.type = 'button'; 3308 toggleIconContainer.setAttribute('aria-label', 'Show submenu for ' + galleryItem.title); 3309 toggleIconContainer.setAttribute('aria-expanded', 'false'); 3310 toggleIconContainer.addEventListener('click', function(event) { 3311 event.preventDefault(); 3312 event.stopPropagation(); 3313 const willExpand = !menuItem.classList.contains('expanded'); 3314 menuItem.classList.toggle('expanded', willExpand); 3315 menuItem.setAttribute('aria-expanded', willExpand ? 'true' : 'false'); 3316 toggleIconContainer.setAttribute('aria-expanded', willExpand ? 'true' : 'false'); 3317 const icon = toggleIconContainer.querySelector('.icon.toggle-icon'); 3318 if (icon) icon.classList.toggle('rotated', willExpand); 3319 }); 3320 } 3321 const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 3322 svgIcon.setAttribute("class", "icon toggle-icon"); 3323 svgIcon.setAttribute("viewBox", "0 0 24 24"); 3324 svgIcon.setAttribute("fill", "none"); 3325 svgIcon.setAttribute("stroke", "currentColor"); 3326 svgIcon.setAttribute("stroke-width", "2"); 3327 const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); 3328 polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow
3329 svgIcon.appendChild(polyline); 3330 toggleIconContainer.appendChild(svgIcon); 3331 menuItem.appendChild(toggleIconContainer); 3332 3333 const subMenu = document.createElement('div'); 3334 subMenu.className = 'horizontal-dropdown'; 3335 const subMenuList = document.createElement('ul'); 3336 subMenuList.className = 'horizontal-dropdown-list'; 3337 3338 visibleChildren.forEach((childItem) => { 3339 const subMenuItemElement = createHorizontalSubMenuItem(childItem); // Recursive call 3340 if (subMenuItemElement) { 3341 subMenuList.appendChild(subMenuItemElement); 3342 } 3343 }); 3344 3345 if (subMenuList.hasChildNodes()) { 3346 subMenu.appendChild(subMenuList); 3347 menuItem.appendChild(subMenu); 3348 3349 menuItem.addEventListener('mouseenter', function() { 3350 clearTimeout(leaveTimer); 3351 document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => { 3352 if (openItem !== this) { 3353 openItem.classList.remove('expanded'); 3354 openItem.setAttribute('aria-expanded', 'false'); 3355 const otherIcon = openItem.querySelector('.icon.toggle-icon'); 3356 if (otherIcon) otherIcon.classList.remove('rotated'); 3357 } 3358 }); 3359 this.classList.add('expanded'); 3360 this.setAttribute('aria-expanded', 'true'); 3361 if (isNavigableParent) toggleIconContainer.setAttribute('aria-expanded', 'true'); 3362 const currentIcon = this.querySelector('.icon.toggle-icon'); 3363 if (currentIcon) currentIcon.classList.add('rotated'); 3364 }); 3365 3366 menuItem.addEventListener('mouseleave', function(event) { 3367 // Check if we're moving to the dropdown or a flyout 3368 const relatedTarget = event.relatedTarget; 3369 const isMovingToDropdown = relatedTarget && ( 3370 relatedTarget.closest('.horizontal-dropdown') || 3371 relatedTarget.classList.contains('horizontal-submenu-item') || 3372 relatedTarget.closest('.horizontal-submenu-item') 3373 ); 3374 const isMovingToFlyout = relatedTarget && ( 3375 relatedTarget.classList.contains('horizontal-nested-dropdown') || 3376 relatedTarget.closest('.horizontal-nested-dropdown') 3377 ); 3378 3379 if (!isMovingToDropdown && !isMovingToFlyout) { 3380 console.log('Main menu item mouseleave - starting timer'); 3381 leaveTimer = setTimeout(() => { 3382 // Check if there's an active flyout - if so, keep parent open 3383 // Use the same multiple detection methods as the submenu handler 3384 const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') || 3385 document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') || 3386 document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])'); 3387 console.log('Main menu timer fired - active flyout:', activeFlyout); 3388 if (!activeFlyout) { 3389 console.log('Main menu closing dropdown - no active flyout'); 3390 this.classList.remove('expanded'); 3391 this.setAttribute('aria-expanded', 'false'); 3392 if (isNavigableParent) toggleIconContainer.setAttribute('aria-expanded', 'false'); 3393 const currentIcon = this.querySelector('.icon.toggle-icon'); 3394 if (currentIcon) currentIcon.classList.remove('rotated'); 3395 } else { 3396 console.log('Main menu keeping dropdown open - flyout is active'); 3397 } 3398 }, 200); 3399 } else { 3400 console.log('Main menu item mouseleave - not starting timer (moving to dropdown or flyout)'); 3401 } 3402 }); 3403 3404 subMenu.addEventListener('mouseenter', function() { 3405 const parentMenuItem = this.closest('.horizontal-menu
3405-item'); 3406 if (parentMenuItem && parentMenuItem.leaveTimer) { 3407 clearTimeout(parentMenuItem.leaveTimer); 3408 parentMenuItem.leaveTimer = null; 3409 } 3410 }); 3411 3412 subMenu.addEventListener('mouseleave', function(event) { 3413 const parentMenuItem = this.closest('.horizontal-menu-item'); 3414 if (parentMenuItem) { 3415 // Check if we're moving to another element within the same dropdown 3416 const relatedTarget = event.relatedTarget; 3417 console.log('SubMenu mouseleave - relatedTarget:', relatedTarget); 3418 console.log('SubMenu mouseleave - relatedTarget classes:', relatedTarget ? relatedTarget.className : 'null'); 3419 3420 // Check if we're moving to another element within the same dropdown 3421 const currentDropdown = this.closest('.horizontal-dropdown') || this.closest('.horizontal-submenu')?.closest('.horizontal-dropdown'); 3422 console.log('Current dropdown found:', !!currentDropdown); 3423 console.log('Current dropdown element:', currentDropdown); 3424 console.log('RelatedTarget:', relatedTarget); 3425 console.log('RelatedTarget closest dropdown:', relatedTarget ? relatedTarget.closest('.horizontal-dropdown') : 'null'); 3426 3427 // Check if relatedTarget is within the same dropdown container 3428 const isMovingWithinDropdown = relatedTarget && currentDropdown && ( 3429 relatedTarget.closest('.horizontal-dropdown') === currentDropdown || 3430 relatedTarget.classList.contains('horizontal-submenu-item') || 3431 relatedTarget.closest('.horizontal-submenu-item') || 3432 currentDropdown.contains(relatedTarget) 3433 ); 3434 3435 // Alternative approach: check if relatedTarget is within the same parent menu item 3436 const parentMenuContainer = this.closest('.horizontal-menu-item'); 3437 const isMovingWithinParent = relatedTarget && parentMenuContainer && parentMenuContainer.contains(relatedTarget); 3438 console.log('Is moving within parent menu item:', isMovingWithinParent); 3439 console.log('Current dropdown contains relatedTarget:', currentDropdown ? currentDropdown.contains(relatedTarget) : 'no dropdown'); 3440 console.log('RelatedTarget closest dropdown === currentDropdown:', relatedTarget && currentDropdown ? relatedTarget.closest('.horizontal-dropdown') === currentDropdown : 'null'); 3441 3442 // Also check if mouse is still within the dropdown bounds (in case relatedTarget is null or outside) 3443 // Get the parent menu item's dropdown container 3444 const parentMenuItem = this.closest('.horizontal-menu-item'); 3445 console.log('Parent menu item found:', !!parentMenuItem); 3446 const dropdownContainer = parentMenuItem ? parentMenuItem.querySelector('.horizontal-dropdown') : null; 3447 console.log('Dropdown container found:', !!dropdownContainer); 3448 if (dropdownContainer) { 3449 console.log('Dropdown container classes:', dropdownContainer.className); 3450 console.log('Dropdown container display:', window.getComputedStyle(dropdownContainer).display); 3451 console.log('Dropdown container position:', window.getComputedStyle(dropdownContainer).position); 3452 } 3453 3454 // Get bounds from the visible dropdown (not the hidden one) 3455 let dropdownRect = { left: 0, right: 0, top: 0, bottom: 0 }; 3456 if (dropdownContainer) { 3457 const computedStyle = window.getComputedStyle(dropdownContainer); 3458 if (computedStyle.display !== 'none') { 3459 dropdownRect = dropdownContainer.getBoundingClientRect(); 3460 } else { 3461 // If dropdown is hidden, try to get bounds from the parent menu item 3462 const parentRect = parentMenuItem.getBoundingClientRect(); 3463 dropdownRect = parentRect; 3464 } 3465 } 3466 const mouseX = event.clientX; 3467 const mouseY = event.clientY; 3468 const isMouseInDropdownBounds = mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && 3469 mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom; 3470
3471 console.log('Dropdown bounds:', { 3472 left: dropdownRect.left, 3473 right: dropdownRect.right, 3474 top: dropdownRect.top, 3475 bottom: dropdownRect.bottom, 3476 mouseX, 3477 mouseY, 3478 mouseInBounds: mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom 3479 }); 3480 3481 // If there's an active flyout, always keep the parent dropdown open 3482 // Check multiple ways to detect active flyouts 3483 const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') || 3484 document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') || 3485 document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])'); 3486 console.log('Active flyout found:', !!activeFlyout); 3487 if (activeFlyout) { 3488 console.log('Active flyout display style:', activeFlyout.style.display); 3489 console.log('Active flyout computed display:', window.getComputedStyle(activeFlyout).display); 3490 console.log('Active flyout visibility style:', activeFlyout.style.visibility); 3491 } 3492 const hasActiveFlyout = !!activeFlyout; 3493 3494 // If the dropdown is hidden (display: none), we can't get accurate bounds 3495 // So let's use a simpler approach - if we're moving to another submenu item, keep open 3496 const isMovingToSubmenuItem = relatedTarget && ( 3497 relatedTarget.classList.contains('horizontal-submenu-item') || 3498 relatedTarget.closest('.horizontal-submenu-item') 3499 ); 3500 console.log('Is moving to submenu item:', isMovingToSubmenuItem); 3501 console.log('RelatedTarget is submenu item:', relatedTarget ? relatedTarget.classList.contains('horizontal-submenu-item') : 'null'); 3502 console.log('RelatedTarget has submenu item ancestor:', relatedTarget ? !!relatedTarget.closest('.horizontal-submenu-item') : 'null'); 3503 3504 // If we're moving to another submenu item within the same dropdown, al
3504ways keep open 3505 // OR if there's an active flyout (regardless of where the mouse is), keep open 3506 // Simple approach: if mouse is within dropdown bounds OR there's an active flyout, keep open 3507 const shouldKeepOpen = isMouseInDropdownBounds || hasActiveFlyout || isMovingWithinDropdown || isMovingToSubmenuItem; 3508 3509 console.log('Is moving within dropdown:', isMovingWithinDropdown); 3510 console.log('Is mouse in dropdown bounds:', isMouseInDropdownBounds); 3511 console.log('Has active flyout:', hasActiveFlyout); 3512 console.log('Should keep open:', shouldKeepOpen); 3513 3514 if (!shouldKeepOpen) { 3515 console.log('SubMenu mouseleave - starting timer (moving outside dropdown)'); 3516 parentMenuItem.leaveTimer = setTimeout(() => { 3517 // Check if there's an active flyout - if so, keep parent open 3518 const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]'); 3519 console.log('Timer fired - active flyout:', activeFlyout); 3520 if (!activeFlyout) { 3521 console.log('Closing parent dropdown - no active flyout'); 3522 parentMenuItem.classList.remove('expanded'); 3523 const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon'); 3524 if (currentIcon) currentIcon.classList.remove('rotated'); 3525 } else { 3526 console.log('Keeping parent dropdown open - flyout is active'); 3527 } 3528 }, 200); 3529 } else { 3530 console.log('SubMenu mouseleave - not starting timer (moving within dropdown)'); 3531 } 3532 } 3533 }); 3534 } else { 3535 menuItem.classList.remove('has-children'); 3536 if (toggleIconContainer.parentNode) { 3537 toggleIconContainer.remove(); 3538 } 3539 } 3540 } 3541 } 3542 3543 // Click listener for items that are direct navigation links (not folders with children) 3544 if (!menuItem.classList.contains('has-children') || isNavigableParent) { 3545 menuItem.addEventListener('click', async (event) => { 3546 if (isNavigableParent && hasVisibleChildren && !titleEl.contains(event.target)) { 3547 return; 3548 } 3549 event.stopPropagation(); 3550 const clickedItemId = galleryItem.id; // Capture the ID of the clicked item 3551 3552 // Check if it's an external URL first 3553 if (galleryItem.isExternal && galleryItem.url) { 3554 window.open(galleryItem.url, '_blank', 'noopener,noreferrer'); 3555 return; 3556 } 3557 3558 if (isPageMenuItem(galleryItem)) { 3559 if (typeof loadPage === 'function') { 3560 loadPage(clickedItemId, event); // loadPage is synchronous 3561 if (typeof updateActiveStatesHorizontal === 'function') { 3562 console.log("Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId); 3563 // Use a minimal delay even for sync operations if page rendering has its own async microtasks 3564 setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50); 3565 } 3566 } 3567 } else { // It's a gallery 3568 if (typeof loadGallery === 'function') { 3569 console.log("Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId); 3570 await loadGallery(clickedItemId, event); // Await the asynchronous loadGallery 3571 console.log("Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay."); 3572 if (typeof updateActiveStatesHorizontal === 'function') { 3573 setTimeout(() => { 3574 console.log("[Delayed Update from Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId); 3575 updateActiveStatesHorizontal(clickedItemId); // Pass the captured clickedItemId 3576 }, 100); // Delay to allow gallery system to settle 3577 } 3578 } 3579 } 3580
3581 // Close any other open dropdowns and flyouts 3582 document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => { 3583 // Don't close the parent if a child within its dropdown was clicked (already handled by submenu click) 3584 if (openItem !== menuItem.closest('.horizontal-menu-item.expanded')) { 3585 openItem.classList.remove('expanded'); 3586 openItem.setAttribute('aria-expanded', 'false'); 3587 const icon = openItem.querySelector('.icon.toggle-icon'); 3588 if (icon) icon.classList.remove('rotated'); 3589 } 3590 }); 3591 3592 // Close all flyouts 3593 document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => { 3594 flyout.style.display = 'none'; 3595 flyout.style.opacity = '0'; 3596 flyout.style.visibility = 'hidden'; 3597 }); 3598 3599 // Remove expanded class from all submenu items 3600 document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((subItem) => { 3601 subItem.classList.remove('expanded'); 3602 }); 3603 }); 3604 } else { 3605 // Keyboard: Enter/Space toggles hover-style dropdown without changing mouse behavior 3606 menuItem.setAttribute('tabindex', '0'); 3607 menuItem.addEventListener('keydown', function(event) { 3608 if (event.key !== 'Enter' && event.key !== ' ') return; 3609 event.preventDefault(); 3610 const isOpen = this.classList.contains('expanded'); 3611 document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => { 3612 if (openItem !== this) { 3613 openItem.classList.remove('expanded'); 3614 openItem.setAttribute('aria-expanded', 'false'); 3615 const otherIcon = openItem.querySelector('.icon.toggle-icon'); 3616 if (otherIcon) otherIcon.classList.remove('rotated'); 3617 } 3618 }); 3619 this.classList.toggle('expanded', !isOpen); 3620 this.setAttribute('aria-expanded', !isOpen ? 'true' : 'false'); 3621 const currentIcon = this.querySelector('.icon.toggle-icon'); 3622 if (currentIcon) currentIcon.classList.toggle('rotated', !isOpen); 3623 }); 3624 } 3625 return menuItem; 3626} 3627 3628/** 3629* Creates a single horizontal submenu item. 3630* (UPDATED to handle nested children and async loadGallery) 3631* @param {object} galleryItem - The gallery item data for the submenu. 3632* @returns {HTMLElement | null} The created submenu item element, or null if not rendered. 3633*/ 3634function createHorizontalSubMenuItem(galleryItem) { 3635 if (galleryItem.visible === false || galleryItem.isSpacer) { 3636 return null; 3637 } 3638 3639 const subMenuItem = document.createElement('li'); 3640 subMenuItem.className = 'horizontal-submenu-item'; 3641 subMenuItem.dataset.id = String(galleryItem.id); 3642 subMenuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false'); 3643 3644 const subVisibleChildren = (galleryItem.children || []).filter(child => child.visible !== false && !child.isSpacer); 3645 const isNavigableParent = isNavigableMenuParent(galleryItem); 3646 let titleSpan; 3647 if (subVisibleChildren.length > 0 && !isNavigableParent) { 3648 titleSpan = document.createElement('span'); 3649 titleSpan.className = 'horizontal-menu-label'; 3650 titleSpan.textContent = galleryItem.title; 3651 } else { 3652 titleSpan = document.createElement('a'); 3653 titleSpan.className = 'horizontal-menu-label'; 3654 titleSpan.href = typeof getMenuItemHref === 'function' ? getMenuItemHref(galleryItem) : '#'; 3655 titleSpan.textContent = galleryItem.title; 3656 if (galleryItem.isExternal) { 3657 titleSpan.target = '_blank'; 3658 titleSpan.rel = 'noopener noreferrer'; 3659 } else { 3660 titleSpan.addEventListener('click', (e) => { e.preventDefault(); }); 3661 } 3662 } 3663 subMenuItem.appendChild(titleSpan); 3664 3665 const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId)); 3666 if (galleryItem.id === currentGlobalActiveId) { 3667 subMenuItem.classList.add('active'); 3668 if (titleSpan.tagName === 'A') titleSpan.setAttribute('aria-current', 'page'); 3669 } 3670 3671 // Check if this submenu item has children (nested sub-sub items) 3672 if (galleryItem.children && galleryItem.children.length > 0) { 3673 const visibleChildren = subVisibleChildren; 3674 3675 if (visibleChildren.length > 0) { 3676 subMenuItem.classList.add('has-children'); 3677 3678 // Add arrow indicator for nested items (same as top-level menu) 3679 const arrowSpan = document.createElement(isNavigableParent ? 'button' : 'span'); 3680 arrowSpan.className = 'submenu-arrow'; 3681 if (isNavigableParent) { 3682 arrowSpan.type = 'button'; 3683 arrowSpan.setAttribute('aria-label', 'Show submenu for ' + galleryItem.title); 3684 arrowSpan.setAttribute('aria-expanded', 'false'); 3685 arrowSpan.addEventListener('click', function(event) { 3686 event.preventDefault(); 3687 event.stopPropagation(); 3688 const isOpen = subMenuItem.classList.contains('expanded'); 3689 if (isOpen) { 3690 closeHorizontalNestedFlyout(subMenuItem); 3691 } else { 3692 openHorizontalNestedFlyout(subMenuItem); 3693 } 3694 }); 3695 } 3696 const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 3697 svgIcon.setAttribute("class", "icon toggle-icon"); 3698 svgIcon.setAttribute("viewBox", "0 0 24 24"); 3699 svgIcon.setAttribute("fill", "none"); 3700 svgIcon.setAttribute("stroke", "currentColor"); 3701 svgIcon.setAttribute("stroke-width", "2"); 3702 const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); 3703 polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow
3704 svgIcon.appendChild(polyline); 3705 arrowSpan.appendChild(svgIcon); 3706 subMenuItem.appendChild(arrowSpan); 3707 3708 // Create nested submenu as a true flyout 3709 const nestedSubMenu = document.createElement('div'); 3710 nestedSubMenu.className = 'horizontal-nested-dropdown'; 3711 3712 // Create a list container for the nested items 3713 const nestedList = document.createElement('ul'); 3714 nestedList.className = 'horizontal-nested-list'; 3715 3716 visibleChildren.forEach((childItem) => { 3717 const nestedSubMenuItem = createHorizontalSubMenuItem(childItem); // Recursive call 3718 if (nestedSubMenuItem) { 3719 nestedList.appendChild(nestedSubMenuItem); 3720 } 3721 }); 3722 3723 nestedSubMenu.appendChild(nestedList); 3724 3725 if (nestedSubMenu.hasChildNodes()) { 3726 // Append to document body for true flyout behavior 3727 document.body.appendChild(nestedSubMenu); 3728 // Store reference to the flyout for cleanup 3729 const flyoutId = 'flyout-' + Date.now() + '-' + Math.random(); 3730 subMenuItem.setAttribute('data-flyout-id', flyoutId); 3731 nestedSubMenu.setAttribute('data-flyout-id', flyoutId); 3732 3733 console.log('Created flyout with ID:', flyoutId, 'for item:', galleryItem.title); // Debug 3734 console.log('Flyout element:', nestedSubMenu); // Debug 3735 3736 // Add hover events for nested submenu 3737 let nestedLeaveTimer; 3738 3739 // Shared open/close used by hover AND keyboard â mouse path unchanged 3740 function openHorizontalNestedFlyout(hostItem) { 3741 if (!hostItem) return; 3742 if (hostItem.nestedLeaveTimer) { 3743 clearTimeout(hostItem.nestedLeaveTimer); 3744 } 3745 const parentMenuItem = hostItem.closest('.horizontal-menu-item'); 3746 if (parentMenuItem && parentMenuItem.leaveTimer) { 3747 clearTimeout(parentMenuItem.leaveTimer); 3748 } 3749 document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((openItem) => { 3750 if (openItem !== hostItem) { 3751 openItem.classList.remove('expanded'); 3752 openItem.setAttribute('aria-expanded', 'false'); 3753 const otherFlyoutId = openItem.getAttribute('data-flyout-id'); 3754 if (otherFlyoutId) { 3755 const otherFlyout = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + otherFlyoutId + '"]'); 3756 if (otherFlyout) { 3757 otherFlyout.style.display = 'none'; 3758 otherFlyout.style.opacity = '0'; 3759 otherFlyout.style.visibility = 'hidden'; 3760 } 3761 } 3762 } 3763 }); 3764 hostItem.classList.add('expanded'); 3765 hostItem.setAttribute('aria-expanded', 'true'); 3766 const hostArrow = hostItem.querySelector('.submenu-arrow'); 3767 if (hostArrow && isNavigableParent) hostArrow.setAttribute('aria-expanded', 'true'); 3768 const hostFlyoutId = hostItem.getAttribute('data-flyout-id'); 3769 if (!hostFlyoutId) return; 3770 const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]'); 3771 if (!nestedDropdown) return; 3772 const rect = hostItem.getBoundingClientRect(); 3773 const parentDropdown = hostItem.closest('.horizontal-dropdown'); 3774 const parentRect = parentDropdown ? parentDropdown.getBoundingClientRect() : rect; 3775 const leftPos = parentRect.right - 1; 3776 const topPos = rect.top; 3777 nestedDropdown.style.left = leftPos + 'px'; 3778 nestedDropdown.style.top = topPos + 'px'; 3779 nestedDropdown.style.display = 'block'; 3780 nestedDropdown.style.opacity = '1'; 3781 nestedDropdown.style.visibility = 'visible'; 3782 nestedDropdown.style.position = 'fixed'; 3783 nestedDropdown.style.zIndex = '1251'; 3784 setTimeout(() => { 3785 const dropdownRect = nestedDropdown.getBoundingClientRect(); 3786 if (dropdownRect.right > window.innerWidth) { 3787 nestedDropdown.style.left = (parentRect.left - dropdownRect.width - 5) + 'px'; 3788 } 3789 }, 10); 3790 } 3791 3792 function closeHorizontalNestedFlyout(hostItem) { 3793 if (!hostItem) return; 3794 hostItem.classList.remove('expanded'); 3795 hostItem.setAttribute('aria-expanded', 'false'); 3796 const hostArrow = hostItem.querySelector('.submenu-arrow'); 3797 if (hostArrow && isNavigableParent) hostArrow.setAttribute('aria-expanded', 'false'); 3798 const hostFlyoutId = hostItem.getAttribute('data-flyout-id'); 3799 if (!hostFlyoutId) return; 3800 const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]'); 3801 if (nestedDropdown) { 3802 nestedDropdown.style.display = 'none'; 3803 nestedDropdown.style.opacity = '0'; 3804 nestedDropdown.style.visibility = 'hidden'; 3805 } 3806 } 3807 3808 // Keyboard opens the same flyout hover shows â attrs on label so mouse click focus stays natural 3809 if (titleSpan && !isNavigableParent) { 3810 titleSpan.setAttribute('tabindex', '0'); 3811 titleSpan.setAttribute('role', 'button'); 3812 titleSpan.setAttribute('aria-haspopup', 'true'); 3813 titleSpan.setAttribute('aria-expanded', 'false'); 3814 titleSpan.addEventListener('keydown', function(event) { 3815 if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowRight') { 3816 event.preventDefault(); 3817 event.stopPropagation(); 3818 openHorizontalNestedFlyout(subMenuItem); 3819 titleSpan.setAttribute('aria-expanded', 'true'); 3820 const hostFlyoutId = subMenuItem.getAttribute('data-flyout-id'); 3821 const nestedDropdown = hostFlyoutId 3822 ? document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]') 3823 : null; 3824 const firstLink = nestedDropdown && nestedDropdown.querySelector('a.horizontal-menu
3824-label'); 3825 if (firstLink && typeof firstLink.focus === 'function') { 3826 try { firstLink.focus(); } catch (e) {} 3827 } 3828 } else if (event.key === 'Escape' || event.key === 'ArrowLeft') { 3829 event.preventDefault(); 3830 event.stopPropagation(); 3831 closeHorizontalNestedFlyout(subMenuItem); 3832 titleSpan.setAttribute('aria-expanded', 'false'); 3833 } 3834 }); 3835 } 3836 3837 subMenuItem.addEventListener('mouseenter', function() { 3838 openHorizontalNestedFlyout(this); 3839 if (titleSpan) titleSpan.setAttribute('aria-expanded', 'true'); 3840 }); 3841 3842 subMenuItem.addEventListener('mouseleave', function(event) { 3843 // Check if we're moving to the flyout 3844 const flyoutId = this.getAttribute('data-flyout-id'); 3845 const relatedTarget = event.relatedTarget; 3846 const isMovingToFlyout = relatedTarget && flyoutId && 3847 relatedTarget.closest('.horizontal-nested-dropdown[data-flyout-id="' + flyoutId + '"]'); 3848 3849 // Also check if we're moving within the dropdown 3850 const parentMenuItem = this.closest('.horizontal-menu-item'); 3851 const isMovingWithinDropdown = relatedTarget && parentMenuItem && parentMenuItem.contains(relatedTarget); 3852 3853 console.log('Submenu item mouseleave - moving to flyout:', isMovingToFlyout); 3854 console.log('Submenu item mouseleave - moving within dropdown:', isMovingWithinDropdown); 3855 3856 if (!isMovingToFlyout && !isMovingWithinDropdown) { 3857 this.nestedLeaveTimer = setTimeout(() => { 3858 closeHorizontalNestedFlyout(this); 3859 if (titleSpan) titleSpan.setAttribute('aria-expanded', 'false'); 3860 3861 // Now that the flyout is closed, allow the parent dropdown to close 3862 const parentMenuItem = this.closest('.horizontal-menu-item'); 3863 if (parentMenuItem) { 3864 setTimeout(() => { 3865 const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]'); 3866 if (!activeFlyout) { 3867 parentMenuItem.classList.remove('expanded'); 3868 const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon'); 3869 if (currentIcon) currentIcon.classList.remove('rotated'); 3870 } 3871 }, 50); 3872 } 3873 }, 200); 3874 } else { 3875 console.log('Submenu item mouseleave - not starting timer (moving to flyout)'); 3876 } 3877 }); 3878 3879 nestedSubMenu.addEventListener('mouseenter', function() { 3880 console.log('Flyout mouseenter - clearing timers'); 3881 clearTimeout(nestedLeaveTimer); 3882 3883 // Clear the parent dropdown's leave timer to keep it open 3884 const flyoutId = this.getAttribute('data-flyout-id'); 3885 if (flyoutId) { 3886 const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]'); 3887 if (parentSubItem) { 3888 const parentMenuItem = parentSubItem.closest('.horizontal-menu-item'); 3889 if (parentMenuItem && parentMenuItem.leaveTimer) { 3890 console.log('Clearing parent menu item leave timer'); 3891 clearTimeout(parentMenuItem.leaveTimer); 3892 parentMenuItem.leaveTimer = null; 3893 } else { 3894 console.log('No parent menu item leave timer to clear'); 3895 } 3896 if (parentSubItem.nestedLeaveTimer) { 3897 console.log('Clearing submenu item nested leave timer'); 3898 clearTimeout(parentSubItem.nestedLeaveTimer); 3899 parentSubItem.nestedLeaveTimer = null; 3900 } 3901 } 3902 } 3903 }); 3904 3905 nestedSubMenu.addEventListener('mouseleave', function() { 3906 const flyoutId = this.getAttribute('data-flyout-id'); 3907 if (flyoutId) { 3908 const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]'); 3909 if (parentSubItem) { 3910 nestedLeaveTimer = setTimeout(() => { 3911 closeHorizontalNestedFlyout(parentSubItem); 3912 const label = parentSubItem.querySelector('.horizontal-menu
3912-label'); 3913 if (label) label.setAttribute('aria-expanded', 'false'); 3914 }, 200); 3915 } 3916 } 3917 }); 3918 } 3919 } 3920 } 3921 3922 // Click listener for navigation (only if no children or if it's a direct link) 3923 if (!subMenuItem.classList.contains('has-children') || isPageMenuItem(galleryItem) || isNavigableParent) { 3924 subMenuItem.addEventListener('click', async (event) => { 3925 if (isNavigableParent && subVisibleChildren.length > 0 && !titleSpan.contains(event.target)) { 3926 return; 3927 } 3928 event.stopPropagation(); 3929 const clickedItemId = galleryItem.id; 3930 3931 // Check if it's an external URL first 3932 if (galleryItem.isExternal && galleryItem.url) { 3933 window.open(galleryItem.url, '_blank', 'noopener,noreferrer'); 3934 return; 3935 } 3936 3937 if (isPageMenuItem(galleryItem)) { 3938 if (typeof loadPage === 'function') { 3939 loadPage(clickedItemId, event); 3940 if (typeof updateActiveStatesHorizontal === 'function') { 3941 console.log("SubMenu Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId); 3942 setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50); 3943 } 3944 } 3945 } else { // It's a gallery 3946 if (typeof loadGallery === 'function') { 3947 console.log("SubMenu Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId); 3948 await loadGallery(clickedItemId, event); 3949 console.log("SubMenu Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay."); 3950 if (typeof updateActiveStatesHorizontal === 'function') { 3951 setTimeout(() => { 3952 console.log("[Delayed Update from SubMenu Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId); 3953 updateActiveStatesHorizontal(clickedItemId); 3954 }, 100); 3955 } 3956 } 3957 } 3958 3959 // Close all parent dropdowns and flyouts after navigation 3960 document.querySelectorAll('.horizontal-menu-item.expanded, .horizontal-submenu-item.expanded').forEach((openItem) => { 3961 openItem.classList.remove('expanded'); 3962 const icon = openItem.querySelector('.icon.toggle-icon'); 3963 if (icon) icon.classList.remove('rotated'); 3964 }); 3965 3966 // Close all flyouts 3967 document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => { 3968 flyout.style.display = 'none'; 3969 flyout.style.opacity = '0'; 3970 flyout.style.visibility = 'hidden'; 3971 }); 3972 }); 3973 } 3974 3975 return subMenuItem; 3976} 3977 3978/** 3979* Updates the 'active' class for items in the horizontal menu. 3980*/ 3981 3982function updateActiveStatesHorizontal(forcedActiveId = null) { 3983 // Prioritize forcedActiveId if provided, otherwise use global state. 3984 // Ensure the ID is treated as a number for comparison. 3985 const targetId = forcedActiveId !== null 3986 ? parseInt(String(forcedActiveId)) 3987 : parseInt(String(window.activeGalleryId || activeGalleryId)); 3988 3989 console.log("[Horizontal Menu Update] Initiated. Target Active ID:", targetId, 3990 (forcedActiveId !== null ? `(Forced: ${forcedActiveId})` : "(Global)"), 3991 "Type:", typeof targetId); 3992 3993 const menuItems = document.querySelectorAll('.horizontal-menu-item, .horizontal-submenu-item'); 3994 3995 if (menuItems.length === 0 && !isNaN(targetId)) { // Check if targetId is a valid number 3996 console.warn("[Horizontal Menu Update] No horizontal menu items found in DOM to update. Target ID was:", targetId); 3997 return; 3998 } 3999 4000 let activeItemSet = false; 4001 menuItems.forEach(item => { 4002 const itemIdStr = item.dataset.id; 4003 if (!itemIdStr) { 4004 // console.warn("[Horizontal Menu Update] Menu item found without a data-id attribute:", item); 4005 return; 4006 } 4007 const itemId = parseInt(itemIdStr); 4008 4009 if (itemId === targetId) { 4010 if (!item.classList.contains('active')) { 4011 item.classList.add('active'); 4012 console.log(`[Horizontal Menu Update] ADDED .active to item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`); 4013 } 4014 activeItemSet = true; 4015 } else { 4016 if (item.classList.contains('active')) { 4017 item.classList.remove('active'); 4018 console.log(`[Horizontal Menu Update] REMOVED .active from item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`); 4019 } 4020 } 4021 }); 4022 4023 if (!isNaN(targetId) && !activeItemSet) { // Check if targetId is a valid number 4024 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.`); 4025 } 4026 4027 const activeSubItem = document.querySelector('.horizontal-submenu-item.active'); 4028 if (activeSubItem) { 4029 const parentTopItem = activeSubItem.closest('.horizontal-menu
4029-item.has-children'); 4030 if (parentTopItem && !parentTopItem.classList.contains('active')) { 4031 // parentTopItem.classList.add('active-parent'); // Optional class for styling parent 4032 } 4033 } 4034 console.log("[Horizontal Menu Update] Completed for Target ID:", targetId); 4035} 4036 4037 4038 // Listener for closing dropdowns when clicking outside 4039 document.addEventListener('click', function(event) { 4040 if (currentMenuLayout === 'horizontal') { 4041 const openSubmenus = document.querySelectorAll('.horizontal-menu-item.expanded'); 4042 let clickedInsideSubmenuOrParent = false; 4043 4044 openSubmenus.forEach(submenuContainer => { 4045 // Check if click is on the submenu container OR its direct parent menu item 4046 if (submenuContainer.contains(event.target) || 4047 (submenuContainer.parentElement && submenuContainer.parentElement.contains(event.target) && submenuContainer.parentElement.classList.contains('horizontal-menu-item'))) { 4048 clickedInsideSubmenuOrParent = true; 4049 } 4050 }); 4051 4052 // If click is on another top-level item that is NOT expanded, also don't close 4053 const targetMenuItem = event.target.closest('.horizontal-menu-item'); 4054 if (targetMenuItem && !targetMenuItem.classList.contains('expanded') && targetMenuItem.classList.contains('has-children')) { 4055 clickedInsideSubmenuOrParent = true; // Don't close if clicking another parent to open it 4056 } 4057 4058 4059 if (!clickedInsideSubmenuOrParent) { 4060 openSubmenus.forEach(submenuContainer => { 4061 submenuContainer.classList.remove('expanded'); 4062 }); 4063 } 4064 } 4065 }); 4066 4067document.addEventListener('menuLayoutChanged', function(e) { 4068 const newLayout = e.detail.layout; 4069 console.log('EVENT: menuLayoutChanged detected. New layout:', newLayout, 'Current isEditing state:', isEditing); 4070 4071 // Update the global currentMenuLayout variable if you have one 4072 if (typeof currentMenuLayout !== 'undefined') { 4073 currentMenuLayout = newLayout; 4074 } 4075 4076 const sidebar = document.querySelector('.sidebar'); // Ensure sidebar DOM element is selected 4077 4078 // Always clear and re-render the appropriate menu structure for the new layout. 4079 // This is important for style changes (like font size) that affect dimensions. 4080 const galleryTreeContainer = document.getElementById('galleryTree'); 4081 const mobileHeaderContent = document.querySelector('.mobile-header-content'); 4082 4083 if (galleryTreeContainer) { 4084 galleryTreeContainer.innerHTML = ''; // Clear existing tree 4085 } 4086 if (mobileHeaderContent) { 4087 const horizontalMenuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container'); 4088 if (horizontalMenuContainer) { 4089 horizontalMenuContainer.innerHTML = ''; // Clear only items, not the container itself 4090 } 4091 } 4092 4093 // Render the correct menu structure based on the new layout 4094 if (newLayout === 'horizontal') { 4095 if (typeof renderHorizontalMenu === 'function') { 4096 renderHorizontalMenu(); 4097 } 4098 // CSS rules will handle sidebar visibility based on .edit-mode-active and .menu-layout-horizontal 4099 } else { // 'sidebar' or 'top' 4100 if (typeof renderGalleries === 'function') { 4101 renderGalleries(); 4102 } 4103 // CSS rules will handle sidebar visibility 4104 } 4105 4106 // If currently in edit mode, ensure the UI components for editing are correctly displayed 4107 // for the new layout, without fully toggling the mode off and on. 4108 if (isEditing && typeof window.updateGlobalEditState === 'function') { 4109 console.log('menuLayoutChanged: In edit mode. Ensuring edit UI is consistent for new layout.'); 4110 4111 // 1. Forcefully ensure global state and body/sidebar classes are correct for edit mode. 4112 // updateGlobalEditState should handle adding .edit-mode-active to body and .editing to sidebar. 4113 window.updateGlobalEditState(true); 4114 4115 // 2. Re-initialize sortables. It's often safer to destroy existing ones first. 4116 if (typeof initializeNestedSortables === 'function') { 4117 if (typeof destroyNestedSortables === 'function') { 4118 destroyNestedSortables(); 4119 } 4120 setTimeout(initializeNestedSortables, 100); // Delay for DOM updates 4121 } 4122 // Also reinitialize for SidebarManager if it exists and handles its own sortables 4123 if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') { 4124 setTimeout(() => window.SidebarManager._reinitializeNestedSortables(), 150); 4125 } 4126 4127 // 3. Ensure edit controls (the top bar of buttons in the sidebar) are visible
4128 const editControls = document.querySelector('.edit-controls'); 4129 if (editControls) { 4130 editControls.style.display = 'flex'; // Or your default display type for these controls 4131 editControls.classList.add('visible'); 4132 } 4133 4134 // 4. If the layout is now horizontal, the sidebar (for editing) should be visible. 4135 // The CSS rule `body.menu-layout-horizontal.edit-mode-active .sidebar { display: block !important; }` 4136 // should handle this. This log is for confirmation. 4137 if (newLayout === 'horizontal' && sidebar) { 4138 console.log('menuLayoutChanged: Horizontal layout in edit mode, sidebar should be visible via CSS.'); 4139 } 4140 4141 // 5. If PageManager is active and a page is loaded, ensure its edit mode is also set. 4142 if (window.PageManager && typeof window.PageManager.setEditMode === 'function' && 4143 window.PageManager.getCurrentPageId && window.PageManager.getCurrentPageId()) { 4144 window.PageManager.setEditMode(true); 4145 } 4146 4147 } else if (!isEditing) { 4148 // If not in edit mode, CSS should handle the view state. 4149 // This block can be used for any explicit view mode adjustments if CSS isn't sufficient. 4150 console.log('menuLayoutChanged: Not in edit mode. CSS will handle view state.'); 4151 } 4152 4153 // Update mobile title and close mobile menu if open, regardless of edit mode 4154 if (typeof updateMobileTitle === 'function') { 4155 updateMobileTitle(); 4156 } 4157 if (typeof closeMobileMenu === 'function') { 4158 closeMobileMenu(); 4159 } 4160 4161 // Re-render the active page so row-band insets and recipe layout match the new menu shell 4162 if (window.PageManager && typeof window.PageManager.renderElements === 'function' && window.PageManager.currentPageId) { 4163 console.log('menuLayoutChanged: Re-rendering page', window.PageManager.currentPageId); 4164 window.PageManager.renderElements(window.PageManager.currentPageId); 4165 } 4166 if (typeof ensureCorrectLayoutApplied === 'function') { 4167 setTimeout(ensureCorrectLayoutApplied, 0); 4168 } 4169}); 4170 4171 4172// Make sure getFirstSidebarElementForHeader is available if called from other scripts 4173if (typeof window.getFirstSidebarElementForHeader === 'undefined') { 4174 window.getFirstSidebarElementForHeader = getFirstSidebarElementForHeader; 4175} 4176 4177// Make sure getFirstSocialElementForHeader is available if called from other scripts 4178if (typeof window.getFirstSocialElementForHeader === 'undefined') { 4179 window.getFirstSocialElementForHeader = getFirstSocialElementForHeader; 4180} 4181 4182// Make sure renderSocialIcons is available if called from other scripts 4183if (typeof window.renderSocialIcons === 'undefined') { 4184 window.renderSocialIcons = renderSocialIcons; 4185} 4186if (typeof window.renderHorizontalMenu === 'undefined') { 4187 window.renderHorizontalMenu = renderHorizontalMenu; 4188} 4189if (typeof window.createHorizontalMenuItem === 'undefined') { 4190 window.createHorizontalMenuItem = createHorizontalMenuItem; 4191} 4192if (typeof window.createHorizontalSubMenuItem === 'undefined') { 4193 window.createHorizontalSubMenuItem = createHorizontalSubMenuItem; 4194} 4195if (typeof window.updateActiveStatesHorizontal === 'undefined') { 4196 window.updateActiveStatesHorizontal = updateActiveStatesHorizontal; 4197} 4198// Ensure toggleEditMode is globally available if it's the primary one. 4199if (typeof window.toggleEditMode === 'undefined' || window.toggleEditMode.toString().length < 100) { // Heuristic to check if it's a placeholder 4200 window.toggleEditMode = toggleEditMode; 4201} 4202 4203 4204// Helper function to toggle edit controls visibility 4205function toggleEditControlsVisibility(show) { 4206 const editControls = document.querySelector('.edit-controls'); 4207 if (editControls) { 4208 if (show) { 4209 editControls.classList.add('visible'); 4210 editControls.style.display = 'flex'; 4211 } else { 4212 editControls.classList.remove('visible'); 4213 editControls.style.display = 'none'; 4214 } 4215 } 4216} 4217 4218/** Reliable + shortcut across US/Intl layouts, numpad, and browsers (key/keyCode differs). */ 4219function isHydraPlusKey(e) { 4220 if (e.repeat) return false;
4221 if (e.ctrlKey || e.metaKey || e.altKey) return false; 4222 if (e.key === '+' || e.key === 'Add') return true; 4223 if (e.code === 'NumpadAdd') return true; 4224 if ((e.code === 'Equal' || e.key === '=') && e.shiftKey) return true; 4225 if (e.keyCode === 187 && e.shiftKey) return true; 4226 if (e.keyCode === 107) return true; 4227 return false; 4228} 4229 4230function handleHydraPlusShortcut() { 4231 try { 4232 const token = window.didToken || localStorage.getItem('hydra_auth_token'); 4233 const isAdmin = window.isAdmin || localStorage.getItem('hydra_is_admin') === 'true'; 4234 if (token && isAdmin) { 4235 console.log('User already logged in - toggling edit mode'); 4236 if (typeof window.toggleEditMode === 'function') { 4237 window.toggleEditMode(); 4238 } else if (typeof toggleEditMode === 'function') { 4239 toggleEditMode(); 4240 } else { 4241 console.warn('toggleEditMode function not found'); 4242 } 4243 } else { 4244 const p = startOTPLogin(); 4245 if (p && typeof p.then === 'function') { 4246 p.catch(function(err) { 4247 console.error('startOTPLogin failed:', err); 4248 }); 4249 } 4250 } 4251 } catch (err) { 4252 console.error('Hydra + key handler error:', err); 4253 } 4254} 4255 4256document.addEventListener('keydown', function(e) { 4257 if (!isHydraPlusKey(e)) return; 4258 e.preventDefault(); 4259 handleHydraPlusShortcut(); 4260}, true); 4261 4262// Custom HTML pages load in a sandboxed iframe; + is captured there unless forwarded via postMessage. 4263function isHydraCustomPageHtmlPlusMessage(e) { 4264 if (!e.data || e.data.source !== 'hydra-custom-page-html' || e.data.type !== 'plus-key') return false; 4265 if (e.origin === window.location.origin) return true; 4266 // Sandboxed iframes (no allow-same-origin) post with origin "null" â accept only from our iframe. 4267 if (e.origin !== 'null' || !e.source) return false; 4268 const iframes = document.querySelectorAll('iframe.page-custom-html-iframe'); 4269 for (let i = 0; i < iframes.length; i++) { 4270 if (iframes[i].contentWindow === e.source) return true; 4271 } 4272 return false; 4273} 4274 4275window.addEventListener('message', function(e) { 4276 if (!isHydraCustomPageHtmlPlusMessage(e)) return; 4277 handleHydraPlusShortcut(); 4278}); 4279 4280/** Option+/ (÷) opens Help â Pathways. Option+? opens Help â Ask (help admin only). */ 4281function isHydraHelpKey(e) { 4282 if (e.repeat) return false; 4283 if (e.ctrlKey || e.metaKey) return false; 4284 if (!e.altKey) return false; 4285 if (e.shiftKey) return false; 4286 if (e.key === '÷') return true; 4287 if (e.code === 'Slash' && e.altKey) return true; 4288 return false; 4289} 4290 4291function isHydraHelpAskKey(e) { 4292 if (e.repeat) return false; 4293 if (e.ctrlKey || e.metaKey) return false; 4294 if (!e.altKey || !e.shiftKey) return false; 4295 if (e.key === '?' || e.key === '¿') return true; 4296 if (e.code === 'Slash') return true; 4297 return false; 4298} 4299 4300document.addEventListener('keydown', function(e) { 4301 if (!window.HYDRA_HELP_ENABLED) return; 4302 var t = e.target; 4303 if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return; 4304 if (isHydraHelpAskKey(e)) { 4305 e.preventDefault(); 4306 e.stopPropagation(); 4307 openHydraHelp({ initialTab: 'ask' }); 4308 return; 4309 } 4310 if (!isHydraHelpKey(e)) return; 4311 e.preventDefault(); 4312 e.stopPropagation(); 4313 openHydraHelp(); 4314}, true); 4315 4316// Email OTP Authentication with Enhanced Debugging 4317 4318 4319// Add this function to the client-side code 4320// Improved client-side backup function with better token handling 4321async function createLoginBackup() { 4322 try { 4323 console.log('Attempting to create login backup...'); 4324 4325 // Get authentication info from multiple possible sources 4326 let token = window.didToken || localStorage.getItem('hydra_auth_token'); 4327 const email = localStorage.getItem('hydra_auth_email') || 4328 (window.userMetadata ? window.userMetadata.email : '') || 4329 ''; 4330 4331 // Check if we have authentication 4332 if (!token) { 4333 console.error('No auth token available for backup'); 4334 return; 4335 } 4336 4337 if (!email) { 4338 console.error('No email available for backup - make sure it was stored during login'); 4339 return; 4340 } 4341 4342 console.log(`Creating backup with token (${token.length} chars) for ${email}`); 4343 4344 // Format the token correctly - VERY IMPORTANT 4345 // If it's already a hydra token (starts with hydra:), use it as is 4346 // Otherwise, add the hydra: prefix 4347 const formattedToken = token.startsWith('hydra:') ? token : `hydra:${token}`; 4348 4349 console.log(`Using formatted token: ${formattedToken.substring(0, 15)}...`); 4350 4351 // Get the API URL with preview handling if needed 4352 const backupUrl = typeof getApiUrl === 'function' ? 4353 getApiUrl('/api/create-backup') : 4354 '/api/create-backup'; 4355 4356 console.log(`Sending backup request to: ${backupUrl}`); 4357 4358 // IMPORTANT: Make sure to add the Bearer prefix to the Authorization header 4359 const response = await fetch(backupUrl, { 4360 method: 'POST', 4361 headers: { 4362 'Authorization': `Bearer ${formattedToken}`, 4363 'Content-Type': 'application/json', 4364 'X-User-Email': email, // Include email as fallback 4365 'X-API-Request': 'true' // Mark as API request 4366 } 4367 }); 4368 4369 // Handle response 4370 if (!response.ok) { 4371 const errorText = await response.text(); 4372 let errorData;
4373 try { 4374 // Try to parse as JSON 4375 errorData = JSON.parse(errorText); 4376 console.error('Backup failed:', errorData.error, errorData); 4377 } catch { 4378 // Not JSON, log raw text 4379 console.error('Backup failed with status', response.status, errorText); 4380 } 4381 return; 4382 } 4383 4384 // Parse successful response 4385 const result = await response.json(); 4386 console.log(`â Backup created successfully: ${result.backup}`); 4387 } catch (error) { 4388 console.error('Error in backup process:', error); 4389 } 4390} 4391 4392// Initialize Magic with detailed logging 4393function initMagic() { 4394 try { 4395 // If Magic is already initialized, return it 4396 if (window.magic) { 4397 return window.magic; 4398 } 4399 4400 console.log('Initializing Magic SDK...'); 4401 4402 // Initialize with minimal options 4403 const magic = new Magic('pk_live_E0B68BA6DCA8C75F', { 4404 testMode: false 4405 }); 4406 4407 // Store reference globally 4408 window.magic = magic; 4409 4410 console.log('Magic SDK initialized successfully'); 4411 return magic; 4412 } catch (error) { 4413 console.error('Error initializing Magic:', error); 4414 throw error; 4415 } 4416} 4417 4418// Start OTP login flow with proper event handling 4419async function startOTPLogin() { 4420 try { 4421 // Initialize Magic 4422 const magic = initMagic(); 4423 4424 // Get email from user (styled overlay â same auth path as native prompt) 4425 const email = await showEmailLoginOverlay(); 4426 if (!email) return; // User cancelled 4427 4428 console.log('Starting Email OTP login for:', email); 4429 showLoadingOverlay('Sending verification code...'); 4430 4431 // Check if Magic's event enum types are available - for newer Magic SDK versions 4432 // If not available, fall back to string-based events 4433 const useEnums = typeof LoginWithEmailOTPEventOnReceived !== 'undefined' && 4434 typeof LoginWithEmailOTPEventEmit !== 'undefined'; 4435 4436 console.log('Using enum-based events:', useEnums); 4437 4438 // Create a timeout handler 4439 let emailSentTimeout = setTimeout(() => { 4440 console.log('No email-otp-sent event received, using fallback UI'); 4441 hideLoadingOverlay(); 4442 showOTPInputOverlay(email); 4443 }, 10000); 4444 4445 // Create OTP login handle with showUI: false 4446 const handle = magic.auth.loginWithEmailOTP({ 4447 email, 4448 showUI: false, 4449 deviceCheckUI: false 4450 }); 4451 4452 // Track authentication state 4453 window.pendingAuth = { 4454 email, 4455 handle 4456 }; 4457 4458 // Listen for events - using both enum-based and string-based event handling 4459 console.log('Setting up OTP event handlers...'); 4460 4461 // Email OTP sent event 4462 if (useEnums) { 4463 handle.on(LoginWithEmailOTPEventOnReceived.EmailOTPSent, () => { 4464 console.log('EVENT: EmailOTPSent (enum) - Email was sent successfully'); 4465 clearTimeout(emailSentTimeout); 4466 hideLoadingOverlay(); 4467 showOTPInputOverlay(email); 4468 }); 4469 } 4470 4471 // Also listen for string-based event (for backward compatibility) 4472 handle.on('email-otp-sent', () => { 4473 console.log('EVENT: email-otp-sent - Email was sent successfully'); 4474 clearTimeout(emailSentTimeout); 4475 hideLoadingOverlay(); 4476 showOTPInputOverlay(email); 4477 }); 4478 4479 // Invalid OTP event 4480 if (useEnums) { 4481 handle.on(LoginWithEmailOTPEventOnReceived.InvalidEmailOtp, () => { 4482 console.log('EVENT: InvalidEmailOtp (enum) - Invalid code entered'); 4483 showErrorMessage('Invalid verification code. Please try again.'); 4484 updateOTPInputForRetry(); 4485 }); 4486 } 4487 4488 // Also listen for string-based event 4489 handle.on('invalid-email-otp', () => { 4490 console.log('EVENT: invalid-email-otp - Invalid code entered'); 4491 showErrorMessage('Invalid verification code. Please try again.'); 4492 updateOTPInputForRetry(); 4493 }); 4494 4495 async function handleLoginSuccess(result) { 4496 console.log('OTP verification successful, checking admin status...'); 4497 hideOTPInputOverlay(); 4498 hideLoadingOverlay(); 4499 4500 try { 4501 window.didToken = result; 4502 console.log('DID Token received:', result ? (result.substring(0, 20) + '...') : 'none'); 4503 4504 const userInfo = await magic.user.getInfo(); 4505 window.userMetadata = userInfo; 4506 console.log('User info retrieved:', userInfo); 4507 4508 const isUserAdmin = await checkAdminStatus(result); 4509 4510 if (!isUserAdmin && !window.isAdmin) { 4511 console.log('User is not on admin list, login denied'); 4512 showErrorMessage('Login failed'); 4513 clearHydraClientAuthStorage(); 4514 resetAuthUI();
4515 try { 4516 await magic.user.logout(); 4517 } catch (logoutError) { 4518 console.error('Error logging out non-admin user from Magic:', logoutError); 4519 } 4520 window.pendingAuth = null; 4521 return; 4522 } 4523 4524 console.log('Admin status check completed'); 4525 window.isAuthenticated = true; 4526 isAuthenticated = true; 4527 showSuccessMessage('Login successful!'); 4528 4529 // Store the ID token (JWT) for API calls 4530 try { 4531 const idToken = await magic.user.getIdToken(); 4532 if (idToken && idToken.includes('.')) { 4533 console.log('Got proper JWT token from Magic.link'); 4534 localStorage.setItem('hydra_auth_token', idToken); 4535 } else if (userInfo && userInfo.email) { 4536 const userToken = { 4537 email: userInfo.email, 4538 iat: Math.floor(Date.now() / 1000), 4539 exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60) 4540 }; 4541 const header = btoa(JSON.stringify({ typ: 'JWT', alg: 'none' })); 4542 const payload = btoa(JSON.stringify(userToken)); 4543 const jwtToken = header + '.' + payload + '.nosignature'; 4544 localStorage.setItem('hydra_auth_token', jwtToken); 4545 localStorage.setItem('hydra_auth_email', userInfo.email); 4546 } 4547 localStorage.setItem('hydra_did_token', result); 4548 } catch (tokenError) { 4549 console.error('Error getting ID token:', tokenError); 4550 localStorage.setItem('hydra_auth_token', result); 4551 } 4552 4553 // IMPROVED UI UPDATE APPROACH - Use multiple techniques for redundancy 4554 4555 // 1. Add state classes to body 4556 document.body.classList.add('hydra-authenticated'); 4557 if (window.isAdmin) { 4558 document.body.classList.add('hydra-admin'); 4559 } 4560 4561 // 2. Direct DOM manipulation 4562 const logoutButton = document.getElementById('logoutButton'); 4563 const editButton = document.getElementById('editButton'); 4564 4565 // Create new buttons if they don't exist 4566 if (!logoutButton) { 4567 console.log('Creating logout button'); 4568 const newLogoutButton = document.createElement('button'); 4569 newLogoutButton.id = 'logoutButton'; 4570 newLogoutButton.className = 'btn'; 4571 newLogoutButton.textContent = 'Logout'; 4572 newLogoutButton.onclick = logout; 4573 newLogoutButton.style.display = 'block'; 4574 4575 const sidebarHeader = document.querySelector('.sidebar-header'); 4576 if (sidebarHeader) { 4577 sidebarHeader.appendChild(newLogoutButton); 4578 } else { 4579 document.body.appendChild(newLogoutButton); 4580 } 4581 } else { 4582 logoutButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;'; 4583 } 4584 4585 if (!editButton && window.isAdmin) { 4586 console.log('Creating edit button'); 4587 const newEditButton = document.createElement('button'); 4588 newEditButton.id = 'editButton'; 4589 newEditButton.className = 'btn btn-primary'; 4590 newEditButton.innerHTML = ` 4591 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 4592 <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> 4593 <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path> 4594 </svg> 4595 Edit 4596 `; 4597 newEditButton.onclick = toggleEditMode; 4598 newEditButton.style.display = 'block'; 4599 4600 const sidebarHeader = document.querySelector('.sidebar-header'); 4601 if (sidebarHeader) { 4602 sidebarHeader.appendChild(newEditButton); 4603 } else { 4604 document.body.appendChild(newEditButton); 4605 } 4606 } else if (editButton && window.isAdmin) { 4607 editButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;'; 4608 } 4609 4610 // 3. Dispatch event for other listeners 4611 document.dispatchEvent(new CustomEvent('login-complete', { 4612 detail: { 4613 isAdmin: window.isAdmin, 4614 email: window.userMetadata ? window.userMetadata.email : null 4615 } 4616 })); 4617 4618 window.pendingAuth = null; 4619 4620 if (typeof checkAuth === 'function') { 4621 console.log('Running existing checkAuth function'); 4622 setTimeout(checkAuth, 500); 4623 } 4624 } catch (error) { 4625 console.error('Error in login success handling:', error); 4626 showErrorMessage('Login failed'); 4627 clearHydraClientAuthStorage(); 4628 resetAuthUI(); 4629 window.pendingAuth = null; 4630 } 4631} 4632 4633 4634 // Completion event 4635 handle.on('done', async (result) => { 4636 await handleLoginSuccess(result); 4637 }); 4638 4639 // Error event 4640 handle.on('error', (error) => { 4641 console.error('EVENT: error - OTP login error:', error); 4642 hideLoadingOverlay(); 4643 hideOTPInputOverlay(); 4644 showErrorMessage('Login failed: ' + (error.message || 'Unknown error')); 4645 4646 // Clear timeout if still active 4647 clearTimeout(emailSentTimeout); 4648 4649 // Clean up 4650 window.pendingAuth = null; 4651 }); 4652 4653 console.log('OTP event handlers set up successfully'); 4654 4655 } catch (error) { 4656 console.error('Error starting OTP login:', error); 4657 hideLoadingOverlay(); 4658 hideOTPInputOverlay(); 4659 showErrorMessage('Error starting login: ' + error.message); 4660 } 4661} 4662
4663// Submit OTP code with support for enum-based events 4664function submitOTP(code) { 4665 try { 4666 // Validate code input 4667 if (!code || code.trim() === '') { 4668 showErrorMessage('Please enter the verification code.'); 4669 return; 4670 } 4671 4672 // Check if we have a pending authentication 4673 if (!window.pendingAuth || !window.pendingAuth.handle) { 4674 showErrorMessage('No active login session. Please try again.'); 4675 hideOTPInputOverlay(); 4676 return; 4677 } 4678 4679 console.log('Submitting OTP code...'); 4680 showLoadingOverlay('Verifying code...'); 4681 4682 // Check if enum types are available 4683 const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined'; 4684 4685 // Verify the OTP code 4686 if (useEnums) { 4687 window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.VerifyEmailOtp, code); 4688 } else { 4689 window.pendingAuth.handle.emit('verify-email-otp', code); 4690 } 4691 4692 } catch (error) { 4693 console.error('Error submitting OTP:', error); 4694 hideLoadingOverlay(); 4695 showErrorMessage('Error verifying code: ' + error.message); 4696 } 4697} 4698 4699// Cancel OTP login with support for enum-based events 4700function cancelOTPLogin() { 4701 try { 4702 // Check if we have a pending authentication 4703 if (window.pendingAuth && window.pendingAuth.handle) { 4704 // Check if enum types are available 4705 const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined'; 4706 4707 // Emit cancel event 4708 if (useEnums) { 4709 window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.Cancel); 4710 } else { 4711 window.pendingAuth.handle.emit('cancel'); 4712 } 4713 4714 console.log('OTP login cancelled'); 4715 } 4716 4717 // Clean up 4718 window.pendingAuth = null; 4719 4720 // Hide any overlays 4721 hideEmailLoginOverlay(); 4722 hideOTPInputOverlay(); 4723 hideLoadingOverlay(); 4724 } catch (error) { 4725 console.error('Error cancelling OTP login:', error); 4726 } 4727} 4728 4729// Update OTP input for retry 4730function updateOTPInputForRetry() {
4731 const otpInput = document.getElementById('otp-input'); 4732 if (otpInput) { 4733 otpInput.value = ''; 4734 otpInput.focus(); 4735 4736 // Add a shake animation for visual feedback 4737 otpInput.classList.add('shake'); 4738 4739 // Remove the animation class after it completes 4740 setTimeout(() => { 4741 otpInput.classList.remove('shake'); 4742 }, 500); 4743 } 4744 4745 // Hide loading overlay if it's visible 4746 hideLoadingOverlay(); 4747} 4748 4749function isValidLoginEmail(value) { 4750 const trimmed = String(value || '').trim(); 4751 if (!trimmed || trimmed.indexOf(' ') >= 0) return false; 4752 const at = trimmed.indexOf('@'); 4753 if (at <= 0 || at !== trimmed.lastIndexOf('@')) return false; 4754 const local = trimmed.slice(0, at); 4755 const domain = trimmed.slice(at + 1); 4756 if (!local || !domain || domain.indexOf('.') < 1) return false; 4757 if (domain.startsWith('.') || domain.endsWith('.')) return false; 4758 return true; 4759} 4760 4761function showEmailLoginOverlay() { 4762 hideEmailLoginOverlay(); 4763 4764 return new Promise((resolve) => { 4765 const overlay = document.createElement('div'); 4766 overlay.id = 'email-login-overlay'; 4767 overlay.className = 'pe-auth-overlay'; 4768 4769 const content = document.createElement('div'); 4770 content.className = 'pe-auth-shell'; 4771 4772 const logo = document.createElement('img'); 4773 logo.src = 'https://cdn.neonsky.app/neon-sky-logo.png'; 4774 logo.alt = 'Neon Sky Logo'; 4775 logo.className = 'pe-auth-shell__logo'; 4776 4777 const description = document.createElement('p'); 4778 description.className = 'pe-auth-shell__desc'; 4779 description.textContent = 'Enter your email address to receive a verification code.'; 4780 4781 const form = document.createElement('form'); 4782 form.noValidate = true; 4783 form.onsubmit = function(e) { 4784 e.preventDefault(); 4785 const raw = input.value.trim(); 4786 if (!isValidLoginEmail(raw)) { 4787 input.classList.add('shake'); 4788 setTimeout(() => input.classList.remove('shake'), 500); 4789 return; 4790 } 4791 hideEmailLoginOverlay(); 4792 resolve(raw); 4793 }; 4794 4795 const input = document.createElement('input'); 4796 input.id = 'email-login-input'; 4797 input.type = 'email'; 4798 input.name = 'email'; 4799 input.placeholder = '[email protected]'; 4800 input.autocomplete = 'email'; 4801 input.inputMode = 'email'; 4802 input.required = true; 4803 input.className = 'pe-auth-shell__input pe-auth-shell__input--email'; 4804 const savedEmail = localStorage.getItem('hydra_auth_email'); 4805 if (savedEmail && isValidLoginEmail(savedEmail)) { 4806 input.value = savedEmail.trim(); 4807 } 4808 form.appendChild(input); 4809 4810 const buttonContainer = document.createElement('div'); 4811 buttonContainer.className = 'pe-auth-actions'; 4812 4813 const continueButton = document.createElement('button'); 4814 continueButton.type = 'submit'; 4815 continueButton.textContent = 'Continue'; 4816 continueButton.className = 'pe-btn pe-btn--primary'; 4817 buttonContainer.appendChild(continueButton); 4818 4819 const cancelButton = document.createElement('button'); 4820 cancelButton.type = 'button'; 4821 cancelButton.textContent = 'Cancel'; 4822 cancelButton.className = 'pe-btn'; 4823 cancelButton.onclick = function() { 4824 hideEmailLoginOverlay(); 4825 resolve(null); 4826 }; 4827 buttonContainer.appendChild(cancelButton); 4828 form.appendChild(buttonContainer); 4829 4830 content.appendChild(logo); 4831 content.appendChild(description); 4832 content.appendChild(form); 4833 overlay.appendChild(content); 4834 document.body.appendChild(overlay); 4835 4836 setTimeout(() => { 4837 input.focus(); 4838 if (input.value) { 4839 input.select(); 4840 } 4841 }, 100); 4842 }); 4843} 4844 4845function hideEmailLoginOverlay() { 4846 const overlay = document.getElementById('email-login-overlay'); 4847 if (overlay && overlay.parentNode) { 4848 overlay.parentNode.removeChild(overlay); 4849 } 4850} 4851 4852// Show OTP input overlay - improved version 4853function showOTPInputOverlay(email) { 4854 hideOTPInputOverlay(); 4855 hideEmailLoginOverlay(); 4856 4857 const overlay = document.createElement('div'); 4858 overlay.id = 'otp-input-overlay'; 4859 overlay.className = 'pe-auth-overlay'; 4860 4861 const content = document.createElement('div'); 4862 content.className = 'pe-auth-shell'; 4863 4864 const logo = document.createElement('img'); 4865 logo.src = 'https://cdn.neonsky.app/neon-sky-logo.png'; 4866 logo.alt = 'Neon Sky Logo'; 4867 logo.className = 'pe-auth-shell__logo'; 4868 4869 const description = document.createElement('p'); 4870 description.className = 'pe-auth-shell__desc'; 4871 const descPrefix = document.createTextNode("We've sent a verification code to "); 4872 const descEmail = document.createElement('strong'); 4873 descEmail.textContent = String(email || ''); 4874 const descSuffix = document.createTextNode('. Please check your email and enter the code below:'); 4875 description.appendChild(descPrefix); 4876 description.appendChild(descEmail); 4877 description.appendChild(document.createElement('br')); 4878 description.appendChild(descSuffix); 4879 4880 const form = document.createElement('form'); 4881 form.onsubmit = function(e) { 4882 e.preventDefault();
4883 const otpInput = document.getElementById('otp-input'); 4884 if (otpInput && otpInput.value) { 4885 submitOTP(otpInput.value); 4886 } 4887 }; 4888 4889 const input = document.createElement('input'); 4890 input.id = 'otp-input'; 4891 input.type = 'text'; 4892 input.placeholder = 'Enter code'; 4893 input.pattern = '[0-9]*'; 4894 input.inputMode = 'numeric'; 4895 input.autocomplete = 'one-time-code'; 4896 input.className = 'pe-auth-shell__input'; 4897 form.appendChild(input); 4898 4899 const buttonContainer = document.createElement('div'); 4900 buttonContainer.className = 'pe-auth-actions'; 4901 4902 const verifyButton = document.createElement('button'); 4903 verifyButton.type = 'submit'; 4904 verifyButton.textContent = 'Verify'; 4905 verifyButton.className = 'pe-btn pe-btn--primary'; 4906 buttonContainer.appendChild(verifyButton); 4907 4908 const cancelButton = document.createElement('button'); 4909 cancelButton.type = 'button'; 4910 cancelButton.textContent = 'Cancel'; 4911 cancelButton.className = 'pe-btn'; 4912 cancelButton.onclick = function() { 4913 cancelOTPLogin(); 4914 }; 4915 buttonContainer.appendChild(cancelButton); 4916 form.appendChild(buttonContainer); 4917 4918 const resendContainer = document.createElement('div'); 4919 resendContainer.className = 'pe-auth-resend'; 4920 4921 const resendText = document.createElement('span'); 4922 resendText.textContent = "Didn't receive the code? "; 4923 4924 const resendLink = document.createElement('a'); 4925 resendLink.textContent = 'Resend'; 4926 resendLink.href = '#'; 4927 resendLink.onclick = function(e) { 4928 e.preventDefault(); 4929 cancelOTPLogin(); 4930 setTimeout(() => { 4931 startOTPLogin(); 4932 }, 500); 4933 }; 4934 4935 resendContainer.appendChild(resendText); 4936 resendContainer.appendChild(resendLink); 4937 form.appendChild(resendContainer); 4938 4939 content.appendChild(logo); 4940 content.appendChild(description); 4941 content.appendChild(form); 4942 overlay.appendChild(content); 4943 document.body.appendChild(overlay); 4944 4945 setTimeout(() => { 4946 input.focus(); 4947 }, 100); 4948} 4949 4950// Hide OTP input overlay 4951function hideOTPInputOverlay() {
4952 const overlay = document.getElementById('otp-input-overlay'); 4953 if (overlay && overlay.parentNode) { 4954 overlay.parentNode.removeChild(overlay); 4955 } 4956} 4957 4958// Utility function to show loading overlay 4959function showLoadingOverlay(message = 'Loading...') { 4960 hideLoadingOverlay(); 4961 4962 const overlay = document.createElement('div'); 4963 overlay.id = 'loading-overlay'; 4964 overlay.className = 'pe-auth-overlay'; 4965 4966 const content = document.createElement('div'); 4967 content.className = 'pe-auth-shell'; 4968 4969 const loaderContainer = document.createElement('div'); 4970 loaderContainer.className = 'pe-auth-shell__loader'; 4971 loaderContainer.innerHTML = ` 4972 <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24"> 4973 <defs> 4974 <filter id="svgSpinnersGooeyBalls20"> 4975 <feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/> 4976 <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"/> 4977 <feBlend in="SourceGraphic" in2="z"/> 4978 </filter> 4979 </defs> 4980 <g filter="url(#svgSpinnersGooeyBalls20)"> 4981 <circle cx="5" cy="12" r="4" fill="currentColor"> 4982 <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/> 4983 </circle> 4984 <circle cx="19" cy="12" r="4" fill="currentColor"> 4985 <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/> 4986 </circle> 4987 <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/> 4988 </g> 4989 </svg> 4990 `; 4991 content.appendChild(loaderContainer); 4992 4993 const messageEl = document.createElement('p'); 4994 messageEl.className = 'pe-auth-shell__message'; 4995 messageEl.textContent = message; 4996 content.appendChild(messageEl); 4997 4998 overlay.appendChild(content); 4999 document.body.appendChild(overlay); 5000} 5001 5002// Utility function to hide loading overlay 5003function hideLoadingOverlay() { 5004 const overlay = document.getElementById('loading-overlay'); 5005 if (overlay && overlay.parentNode) { 5006 overlay.parentNode.removeChild(overlay); 5007 } 5008} 5009 5010// Utility function to show success message 5011function showSuccessMessage(message, duration = 3000) { 5012 const existingToast = document.getElementById('success-toast'); 5013 if (existingToast && existingToast.parentNode) { 5014 existingToast.parentNode.removeChild(existingToast); 5015 } 5016 5017 const toast = document.createElement('div'); 5018 toast.id = 'success-toast'; 5019 toast.className = 'pe-auth-toast'; 5020 toast.textContent = message; 5021 5022 document.body.appendChild(toast); 5023 5024 setTimeout(() => { 5025 if (toast.parentNode) { 5026 toast.parentNode.removeChild(toast); 5027 } 5028 }, duration); 5029} 5030 5031// Utility function to show error message 5032function showErrorMessage(message, duration = 5000) { 5033 const existingToast = document.getElementById('error-toast'); 5034 if (existingToast && existingToast.parentNode) { 5035 existingToast.parentNode.removeChild(existingToast); 5036 } 5037 5038 const toast = document.createElement('div'); 5039 toast.id = 'error-toast'; 5040 toast.className = 'pe-auth-toast pe-auth-toast--error'; 5041 toast.textContent = message; 5042 5043 document.body.appendChild(toast); 5044 5045 setTimeout(() => { 5046 if (toast.parentNode) { 5047 toast.parentNode.removeChild(toast); 5048 } 5049 }, duration); 5050} 5051 5052// UPDATED: Check authentication status with class-based approach 5053async function checkAuth() { 5054 try { 5055 if (!magic) { 5056 console.log('Magic not initialized, skipping auth check'); 5057 return; 5058 } 5059 5060 // Prevent multiple simultaneous auth checks 5061 if (window._checkingAuth) return; 5062 window._checkingAuth = true; 5063 5064 console.log('Checking auth status...'); 5065 5066 isAuthenticated = await magic.user.isLoggedIn(); 5067 console.log('isLoggedIn check:', isAuthenticated); 5068 5069 if (isAuthenticated) { 5070 // Add the authenticated class 5071 document.body.classList.add('hydra-authenticated'); 5072 5073 // Get user metadata 5074 userMetadata = await magic.user.getInfo(); 5075 console.log('User info:', userMetadata); 5076 5077 // Get ID token for API calls (JWT format) 5078 const rawDidToken = await magic.user.getIdToken(); 5079 console.log('ID token acquired, length:', rawDidToken.length); 5080
5081 // Do NOT write the raw DID token to localStorage here â checkAdminStatus will 5082 // receive the server-issued hydra token and store that instead, which is what 5083 // the save function needs. Writing the DID token now would overwrite a valid 5084 // hydra token that may already be stored from the OTP login flow. 5085 5086 // Check if user is admin; on success this updates both localStorage and didToken 5087 // with a proper hydra token via checkAdminStatus â TokenManager.storeToken 5088 await checkAdminStatus(rawDidToken); 5089 } else { 5090 // Magic has no session â still try Hydra OTP token so /_manage and return visits work 5091 const hydraOnly = localStorage.getItem('hydra_auth_token'); 5092 if (hydraOnly) { 5093 try { 5094 await checkAdminStatus(hydraOnly); 5095 } catch (e) { 5096 console.error('Hydra token restore after Magic logout failed:', e); 5097 } 5098 } else { 5099 resetAuthUI(); 5100 } 5101 } 5102 5103 // Release lock 5104 window._checkingAuth = false; 5105 } catch (error) { 5106 console.error('Error checking authentication:', error); 5107 window._checkingAuth = false; 5108 resetAuthUI(); 5109 } 5110} 5111 5112 5113async function checkAdminStatus(token) { 5114 try { 5115 console.log('Checking admin status...'); 5116 5117 // Add user email to headers if available 5118 let userEmail = null; 5119 if (window.userMetadata && window.userMetadata.email) { 5120 userEmail = window.userMetadata.email; 5121 console.log('Using email from userMetadata:', userEmail); 5122 } else { 5123 userEmail = localStorage.getItem('hydra_auth_email'); // Fallback to localStorage 5124 if (userEmail) console.log('Using email from localStorage:', userEmail); 5125 } 5126 5127 if (!token) { 5128 console.log('No token provided to checkAdminStatus'); 5129 return false; // Indicate failure 5130 } 5131 5132 // Format the token correctly for the API call 5133 let formattedToken = token; 5134 // Ensure Bearer prefix is added for Magic tokens, or use hydra: prefix 5135 if (!token.startsWith('hydra:') && !token.startsWith('Bearer ')) { 5136 formattedToken = `Bearer ${token}`; // Assume Magic/JWT needs Bearer 5137 } else if (token.startsWith('hydra:')) { 5138 formattedToken = `Bearer ${token}`; // API expects Bearer even for hydra tokens 5139 } 5140 // If it already starts with Bearer, use as is 5141 5142 // Get the API URL, handling preview mode 5143 const apiUrl = typeof getApiUrl === 'function' ? 5144 getApiUrl('/api/check-admin') : 5145 '/api/check-admin'; 5146 5147 console.log('Sending admin check request to:', apiUrl); 5148 5149 // Create request headers 5150 const headers = { 5151 'Authorization': formattedToken, 5152 'X-Hydra-Request': 'true' // Indicate this might be part of Hydra flow 5153 }; 5154 5155 if (userEmail) { 5156 headers['X-User-Email'] = userEmail; 5157 } 5158 5159 console.log('Admin check headers:', { 5160 'Authorization': `${formattedToken.substring(0, 20)}...`, 5161 'X-User-Email': userEmail || 'Not available' 5162 }); 5163 5164 // Add timeout to the fetch request 5165 const controller = new AbortController(); 5166 const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout 5167 5168 let data; 5169 try { 5170 const response = await fetch(apiUrl, { 5171 method: 'GET', 5172 headers: headers, 5173 signal: controller.signal 5174 }); 5175 5176 clearTimeout(timeoutId); // Clear timeout if fetch succeeds 5177 5178 console.log('Admin check response status:', response.status); 5179 5180 // Get the response as text first for better debugging 5181 const responseText = await response.text(); 5182 console.log('Admin check response length:', responseText.length); 5183 console.log('Admin check response text (first 100 chars):', responseText.substring(0, 100) + '...'); 5184 5185 5186 try { 5187 data = JSON.parse(responseText); 5188 console.log('Admin check data:', data); 5189 } catch (parseError) { 5190 console.error('Error parsing admin check response:', parseError); 5191 // Handle cases where the response might not be JSON (e.g., HTML error page) 5192 // For preview URLs, let's still assume admin to allow editing flow 5193 if (window.location.hostname === 'preview.neonsky.app') { 5194 console.log('Preview URL detected, assuming admin status due to non-JSON response'); 5195 isAdmin = true; 5196 document.body.classList.add('hydra-admin'); 5197 localStorage.setItem('hydra_is_admin', 'true'); 5198 forceShowEditUI(); // Attempt to show UI 5199 return true; // Indicate potential success for preview 5200 } 5201 throw new Error(`Failed to parse admin check response: ${responseText.substring(0, 100)}...`); 5202 } 5203 5204 if (!response.ok) { 5205 // Throw an error with details from the parsed JSON if available 5206 throw new Error(`Admin check failed: ${response.status} - ${data?.error || responseText.substring(0, 50)}`); 5207 } 5208 5209 // Process the admin status 5210 isAdmin = data.isAdmin; 5211 window.isAdmin = isAdmin; // Update global flag 5212 5213 // Update classes based on admin status 5214 if (isAdmin) { 5215 document.body.classList.add('hydra-admin'); 5216 localStorage.setItem('hydra_is_admin', 'true'); 5217 // Store email if we got it from the server response 5218 if (data.email) { 5219 localStorage.setItem('hydra_auth_email', data.email); 5220 } else if (userEmail) { 5221 localStorage.setItem('hydra_auth_email', userEmail); // Store the email we used 5222 } 5223 } else { 5224 document.body.classList.remove('hydra-admin'); 5225 localStorage.removeItem('hydra_is_admin'); 5226 localStorage.removeItem('hydra_auth_email'); // Clear email if not admin 5227 } 5228 5229 // Update siteId if provided 5230 if (data.siteId) { 5231 siteId = data.siteId; 5232 window.siteId = siteId; 5233 if (window.Parameters) window.Parameters.siteId = siteId; 5234 } 5235 5236 // Store new token if provided (Hydra token) 5237 if (data.hydraToken) { 5238 console.log('Received hydra token:', data.hydraToken.substring(0, 10) + '...'); 5239 const cleanHydraToken = data.hydraToken.startsWith('hydra:') ? data.hydraToken.substring(6) : data.hydraToken; 5240 // Always update the local didToken variable so saves immediately use the hydra token 5241 didToken = cleanHydraToken; 5242 // Store token globally and in localStorage using TokenManager if available 5243 if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') { 5244 window.TokenManager.storeToken(data.hydraToken, data.email || userEmail || ''); 5245 } else { 5246 // Fallback storage 5247 localStorage.setItem('hydra_auth_token', cleanHydraToken); 5248 if (data.email || userEmail) { 5249 localStorage.setItem('hydra_auth_email', data.email || userEmail); 5250 } 5251 } 5252 console.log('Stored credentials in localStorage'); 5253 } 5254 5255 // Update UI based on admin status 5256 if (isAdmin) { 5257 console.log('Admin status confirmed, showing edit UI'); 5258 forceShowEditUI(); // Ensure UI elements are visible 5259 // --- MODIFICATION START --- 5260 // If layout is horizontal, re-render the menu to show controls 5261 let currentLayout = 'sidebar'; // Default 5262 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 5263 currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; 5264 } 5265 if (currentLayout === 'horizontal') { 5266 console.log('checkAdminStatus: Horizontal layout detected, re-rendering horizontal menu.'); 5267 if (typeof renderHorizontalMenu === 'function') { 5268 renderHorizontalMenu(); 5269 } else { 5270 console.warn("checkAdminStatus: renderHorizontalMenu function not found."); 5271 } 5272 } 5273 // --- MODIFICATION END --- 5274 createLoginBackup(); // Create backup after successful login/admin check 5275 } else { 5276 console.log('User is not admin, resetting UI.'); 5277 resetAuthUI(); // Ensure non-admin UI state 5278 } 5279 5280 return isAdmin; // Return the admin status 5281 5282 } catch (fetchError) { 5283 clearTimeout(timeoutId); // Clear timeout on error as well 5284 console.error('Fetch error during admin check:', fetchError); 5285 5286 // For preview URLs, still allow editing as a fallback 5287 if (window.location.hostname === 'preview.neonsky.app') { 5288 console.log('Preview URL + fetch error, still enabling editing'); 5289 isAdmin = true; 5290 window.isAdmin = true; 5291 document.body.classList.add('hydra-admin'); 5292 localStorage.setItem('hydra_is_admin', 'true'); 5293 forceShowEditUI(); 5294 return true; // Indicate potential success for preview 5295 } 5296 5297 // For non-preview, throw the error to indicate failure 5298 throw fetchError; 5299 } 5300 } catch (error) { 5301 console.error('Error in checkAdminStatus:', error); 5302 // Don't show alert here, let the calling function handle UI feedback 5303 // Reset UI to non-admin state on error 5304 resetAuthUI(); 5305 return false; // Indicate failure 5306 } 5307} 5308 5309// UPDATED: Force showing edit UI with class-based approach 5310function forceShowEditUI() { 5311 console.log('Forcing admin/auth state for UI'); 5312 5313 // Ensure global state flags are set 5314 isAuthenticated = true; 5315 isAdmin = true; 5316 window.isAuthenticated = true; 5317 window.isAdmin = true; 5318 5319 // Add admin/auth classes to body - CSS will handle button visibility 5320 document.body.classList.add('hydra-authenticated', 'hydra-admin'); 5321 5322 // Store admin status in localStorage 5323 localStorage.setItem('hydra_is_admin', 'true'); 5324 5325 // Show edit controls container ONLY if already in edit mode 5326 if (window.isInEditMode && window.isInEditMode()) {
5327 const editControls = document.querySelector('.edit-controls'); 5328 if (editControls) { 5329 editControls.classList.add('visible'); 5330 editControls.style.display = 'flex'; // Make sure container is flex 5331 } 5332 // Ensure controls inside items are visible if already editing 5333 document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => { 5334 if (el.closest('.sidebar.editing') || el.closest('.page-container.editing')) { 5335 el.style.display = 'flex'; 5336 } 5337 }); 5338 5339 // Show logout button in edit controls when in edit mode 5340 const logoutButton = document.getElementById('logoutButton'); 5341 if (logoutButton) { 5342 logoutButton.style.display = 'flex'; 5343 logoutButton.classList.add('visible'); 5344 } 5345 } else { 5346 // Ensure edit controls container is hidden if not in edit mode 5347 const editControls = document.querySelector('.edit-controls'); 5348 if (editControls) { 5349 editControls.classList.remove('visible'); 5350 editControls.style.display = 'none'; 5351 } 5352 5353 // Hide logout button when not in edit mode 5354 const logoutButton = document.getElementById('logoutButton'); 5355 if (logoutButton) { 5356 logoutButton.style.display = 'none'; 5357 logoutButton.classList.remove('visible'); 5358 } 5359 } 5360 5361 console.log('Admin/Auth classes set. CSS should handle button visibility.'); 5362} 5363 5364// UPDATED: Reset auth UI with class-based approach 5365function resetAuthUI() { 5366 document.body.classList.remove('hydra-authenticated'); 5367 document.body.classList.remove('hydra-admin'); 5368 5369 5370 const logoutButton = document.getElementById('logoutButton'); 5371 if (logoutButton) { 5372 logoutButton.style.display = 'none'; 5373 logoutButton.classList.remove('visible'); 5374 } 5375 5376 const editButton = document.getElementById('editButton'); 5377 if (editButton) { 5378 editButton.style.display = 'none'; 5379 editButton.classList.remove('visible'); 5380 } 5381 5382 // Remove admin status from localStorage 5383 localStorage.removeItem('hydra_is_admin'); 5384 localStorage.removeItem('hydra_auth_token'); 5385 5386 if (isEditing) { 5387 toggleEditMode(); // Exit edit mode if active 5388 } 5389} 5390 5391// Debug function - add to your code during testing 5392function debugUIClasses() { 5393 console.log('Current body classes:', document.body.className); 5394 console.log('hydra-initialized:', document.body.classList.contains('hydra-initialized')); 5395 console.log('hydra-authenticated:', document.body.classList.contains('hydra-authenticated')); 5396 console.log('hydra-admin:', document.body.classList.contains('hydra-admin')); 5397 console.log('edit-mode-active:', document.body.classList.contains('edit-mode-active')); 5398} 5399 5400 5401 5402async function logout() { 5403 console.log('Logout initiated'); 5404 5405 try { 5406 // Show loading indication (optional) 5407 showLoadingOverlay('Logging out...'); 5408 5409 // 1. Properly logout from Magic SDK 5410 if (window.magic) { 5411 try { 5412 await window.magic.user.logout(); 5413 console.log('Magic SDK logout successful'); 5414 } catch (magicError) { 5415 console.error('Error during Magic logout:', magicError); 5416 // Continue with other logout steps even if Magic SDK logout fails 5417 } 5418 } 5419 5420 // 2. Reset all authentication state variables 5421 clearHydraClientAuthStorage(); 5422 5423 // 3. Reset all UI state - remove authentication classes from body 5424 document.body.classList.remove('hydra-authenticated'); 5425 document.body.classList.remove('hydra-admin'); 5426 5427 // 5. If in edit mode, exit it first 5428 if (document.body.classList.contains('edit-mode-active')) { 5429 console.log('Exiting edit mode before logout'); 5430 // If toggleEditMode exists, call it to exit edit mode 5431 if (typeof toggleEditMode === 'function') { 5432 try { 5433 toggleEditMode(); 5434 } catch (editError) { 5435 console.error('Error exiting edit mode:', editError); 5436 } 5437 } 5438 5439 // Ensure edit mode class is removed regardless 5440 document.body.classList.remove('edit-mode-active'); 5441 } 5442 5443 // 6. Ensure all edit UI elements are hidden 5444 5445 // Hide edit and logout buttons 5446 const editButton = document.getElementById('editButton'); 5447 if (editButton) { 5448 editButton.classList.remove('visible'); 5449 editButton.style.display = 'none'; 5450 } 5451 5452 const logoutButton = document.getElementById('logoutButton'); 5453 if (logoutButton) { 5454 logoutButton.classList.remove('visible'); 5455 logoutButton.style.display = 'none'; 5456 } 5457 5458 // Hide edit controls
5459 const editControls = document.querySelector('.edit-controls'); 5460 if (editControls) { 5461 editControls.style.display = 'none'; 5462 editControls.classList.remove('visible'); 5463 } 5464 5465 // Hide sidebar header if it should be hidden when logged out 5466 const sidebarHeader = document.querySelector('.sidebar-header'); 5467 if (sidebarHeader) { 5468 sidebarHeader.style.display = 'none'; 5469 } 5470 5471 // Hide all element controls 5472 document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => { 5473 el.style.display = 'none'; 5474 }); 5475 5476 // 7. Reset editor state if applicable 5477 if (window.Parameters) { 5478 window.Parameters.isInEditor = false; 5479 } 5480 5481 // Hide any editing forms that might be open 5482 const forms = document.querySelectorAll('.add-form, .edit-form, .element-edit-form, #menuStyleEditor, #metadataEditor, #importClassicForm, #sidebarElementForm'); 5483 forms.forEach(form => { 5484 form.style.display = 'none'; 5485 if (form.classList.contains('visible')) { 5486 form.classList.remove('visible'); 5487 } 5488 }); 5489 5490 // Remove editing class from sidebar 5491 const sidebar = document.querySelector('.sidebar'); 5492 if (sidebar) { 5493 sidebar.classList.remove('editing'); 5494 } 5495 5496 // 8. Clear any page-specific state 5497 if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { 5498 window.PageManager.clearCurrentPage(); 5499 } 5500 5501 // 9. Notify any components that need to know about logout 5502 document.dispatchEvent(new CustomEvent('user-logout', { 5503 detail: { timestamp: Date.now() } 5504 })); 5505 5506 // 10. Show success message 5507 hideLoadingOverlay(); 5508 showSuccessMessage('Successfully logged out'); 5509 5510 console.log('Logout complete'); 5511 5512 // 11. Optional: Refresh the page after a short delay for a clean state 5513 // Uncomment the following lines if you want the page to refresh after logout 5514 /* 5515 setTimeout(() => { 5516 window.location.reload(); 5517 }, 1500); 5518 */ 5519 5520 } catch (error) { 5521 console.error('Error during logout process:', error); 5522 hideLoadingOverlay(); 5523 showErrorMessage('Error during logout. Please refresh the page.'); 5524 } 5525} 5526 5527// Ensure the logout button is properly connected to the logout function 5528document.addEventListener('DOMContentLoaded', function() { 5529 const logoutButton = document.getElementById('logoutButton'); 5530 if (logoutButton) { 5531 // Remove any existing event listeners to avoid duplicates 5532 const newButton = logoutButton.cloneNode(true); 5533 logoutButton.parentNode.replaceChild(newButton, logoutButton); 5534 5535 // Add fresh event listener 5536 newButton.addEventListener('click', function(e) { 5537 e.preventDefault(); 5538 e.stopPropagation(); 5539 logout(); 5540 }); 5541 } 5542}); 5543 5544function stopAutoAdvanceTimer() { 5545 if (window.currentAutoAdvanceTimerId) { 5546 clearTimeout(window.currentAutoAdvanceTimerId); 5547 window.currentAutoAdvanceTimerId = null; 5548 console.log('Auto-advance timer stopped.'); 5549 } 5550} 5551window.stopAutoAdvanceTimer = stopAutoAdvanceTimer; 5552 5553function beginHydraContentLoad(type, itemId, pageId) { 5554 const generation = (Number(window.__hydraContentLoadGeneration) || 0) + 1; 5555 window.__hydraContentLoadGeneration = generation; 5556 window.__neonGalleryInitEpoch = (Number(window.__neonGalleryInitEpoch) || 0) + 1; 5557 window.__hydraContentLoadOwner = { 5558 generation, 5559 type, 5560 itemId: itemId === undefined || itemId === null ? '' : String(itemId), 5561 pageId: pageId === undefined || pageId === null ? '' : String(pageId) 5562 }; 5563 return generation; 5564} 5565 5566function isHydraContentLoadCurrent(generation, type, itemId) { 5567 if (generation != null && Number(generation) !== Number(window.__hydraContentLoadGeneration || 0)) { 5568 return false; 5569 } 5570 if (type && window._activeContentType && window._activeContentType !== type) { 5571 return false; 5572 } 5573 if (itemId != null && itemId !== '' && window.activeGalleryId != null && 5574 String(window.activeGalleryId) !== String(itemId)) { 5575 return false; 5576 } 5577 return true; 5578} 5579 5580window.beginHydraContentLoad = beginHydraContentLoad; 5581window.isHydraContentLoadCurrent = isHydraContentLoadCurrent; 5582 5583function dismissGalleryPasswordOverlay() { 5584 if (window.PageManager && typeof window.PageManager.removeGalleryPasswordOverlay === 'function') { 5585 window.PageManager.removeGalleryPasswordOverlay(); 5586 return; 5587 } 5588 document.querySelectorAll('.gallery-password-protection-overlay').forEach(function(overlay) { 5589 if (overlay._hydraPasswordAbort && typeof overlay._hydraPasswordAbort.abort === 'function') { 5590 overlay._hydraPasswordAbort.abort(); 5591 } 5592 overlay.remove(); 5593 }); 5594} 5595window.dismissGalleryPasswordOverlay = dismissGalleryPasswordOverlay; 5596 5597function toggleEditMode() { 5598 // Prevent multiple simultaneous calls 5599 if (window._editModeToggleInProgress) { 5600 console.log('Edit mode toggle already in progress, skipping'); 5601 return; 5602 } 5603 window._editModeToggleInProgress = true; 5604 5605 console.log("toggleEditMode called. Current window.isEditing:", window.isEditing, "Current window.isInEditMode():", window.isInEditMode ? window.isInEditMode() : 'undefined'); 5606 5607 stopAutoAdvanceTimer(); // Assumes this function exists and stops any slideshow/auto-advance 5608 5609 // Authentication and Authorization Checks 5610 if (!window.isAuthenticated && !(localStorage.getItem('hydra_is_admin') === 'true')) { 5611 alert('You must be logged in to edit'); 5612 window._editModeToggleInProgress = false;
5613 return; 5614 } 5615 if (localStorage.getItem('hydra_is_admin') === 'true' && !window.isAdmin) { 5616 window.isAdmin = true; // Synchronize global flag 5617 } 5618 if (!window.isAdmin) { 5619 alert('You must be an admin to edit'); 5620 window._editModeToggleInProgress = false; 5621 return; 5622 } 5623 5624 const currentEditState = window.isInEditMode ? window.isInEditMode() : (typeof window.isEditing !== 'undefined' ? window.isEditing : false); 5625 const newEditState = !currentEditState; 5626 5627 console.log(`Changing edit mode from ${currentEditState} to: ${newEditState}`); 5628 5629 // Apply body class for edit mode styling 5630 if (newEditState) { 5631 console.log('Entering edit mode - setting body class and theme'); 5632 document.body.setAttribute('data-theme', 'pico'); // Optional: if Pico theme is used for edit mode 5633 document.body.classList.add('edit-mode-active'); 5634 } else { 5635 console.log('Exiting edit mode - removing body class and theme'); 5636 document.body.removeAttribute('data-theme'); 5637 document.body.classList.remove('edit-mode-active'); 5638 } 5639 5640 // Update the global state and dispatch event 5641 if (typeof window.updateGlobalEditState === 'function') { 5642 window.updateGlobalEditState(newEditState); 5643 } else { 5644 window.isEditing = newEditState; // Fallback 5645 document.dispatchEvent(new CustomEvent('edit-mode-changed', { detail: { editing: newEditState } })); 5646 if (!newEditState && typeof window.closeOptionsPanel === 'function') { 5647 window.closeOptionsPanel(); 5648 } 5649 } 5650 5651 if (document.body.classList.contains('menu-layout-horizontal') && typeof syncContentMarginToHorizontalHeader === 'function') { 5652 setTimeout(syncContentMarginToHorizontalHeader, 50); 5653 setTimeout(syncContentMarginToHorizontalHeader, 350); 5654 } 5655 5656 const sidebarEditControls = document.querySelector('.edit-controls'); 5657 5658 if (newEditState) { 5659 // Entering edit mode 5660 if (sidebarEditControls) { 5661 sidebarEditControls.style.display = 'flex'; 5662 sidebarEditControls.classList.add('visible'); 5663 } 5664 5665 // Show logout button in edit controls when entering edit mode 5666 const logoutButton = document.getElementById('logoutButton'); 5667 if (logoutButton) { 5668 logoutButton.style.display = 'flex'; 5669 logoutButton.classList.add('visible'); 5670 } 5671 5672 // Always render the tree-like menu in the sidebar for editing 5673 if (typeof window.renderGalleries === 'function') { 5674 console.log('toggleEditMode (Edit): Rendering sidebar menu (gallery tree) for editing.'); 5675 window.renderGalleries(); // This will show all items, including those with visible:false 5676 } 5677 5678 setTimeout(() => { 5679 if (typeof window.initializeNestedSortables === 'function') { 5680 console.log('Initializing nested sortables for edit mode'); 5681 window.initializeNestedSortables(); 5682 } 5683 if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') { 5684 window.SidebarManager._reinitializeNestedSortables(); 5685 } 5686 }, 150); 5687 5688 // If the currently active item was invisible, reload it now that we are in edit mode. 5689 if(window.galleries && window.activeGalleryId){ 5690 const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId); 5691 if (currentGalleryItem && currentGalleryItem.visible === false) { 5692 console.log('toggleEditMode (Edit): Active item was invisible, reloading it.'); 5693 if (currentGalleryItem.isPage && window.loadPage) { 5694 window.loadPage(window.activeGalleryId); 5695 } else if(window.loadGallery) { 5696 window.loadGallery(window.activeGalleryId); 5697 } 5698 } 5699 } 5700 if(typeof window.showStyleEditor === 'function' && 5701 !(window.MenuStyleCustomizer && window.MenuStyleCustomizer._overlayOpen)) { 5702 window.showStyleEditor(false); // Ensure style editor is hidden initially 5703 } 5704 5705 } else { 5706 // Exiting edit mode (going to Live view) 5707 if (sidebarEditControls) { 5708 sidebarEditControls.style.display = 'none'; 5709 sidebarEditControls.classList.remove('visible'); 5710 } 5711 5712 // Hide logout button when exiting edit mode 5713 const logoutButton = document.getElementById('logoutButton'); 5714 if (logoutButton) { 5715 logoutButton.style.display = 'none'; 5716 logoutButton.classList.remove('visible'); 5717 } 5718 if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer._prepareOverlayForLiveView === 'function') { 5719 window.MenuStyleCustomizer._prepareOverlayForLiveView(); 5720 } 5721 if(typeof window.closeAllForms === 'function') window.closeAllForms(); 5722 5723 if (typeof window.destroyNestedSortables === 'function') { 5724 window.destroyNestedSortables(); 5725 } else if (window.sortableInstances && Array.isArray(window.sortableInstances)) {
5726 window.sortableInstances.forEach(instance => instance.destroy()); 5727 window.sortableInstances = []; 5728 } 5729 5730 // Re-render the menu based on the actual view mode layout. 5731 // These functions should internally handle not showing items with visible:false. 5732 let currentLayout = 'sidebar'; 5733 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 5734 currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; 5735 } 5736 if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') { 5737 window.renderHorizontalMenu(); 5738 } else if (typeof window.renderGalleries === 'function') { 5739 window.renderGalleries(); 5740 } 5741 5742 // Re-render sidebar elements (social, text, etc.) so edit-only UI (grid, dashed slots) is replaced by live view 5743 if (window.SidebarManager && typeof window.SidebarManager._syncWithGlobalEditState === 'function' && typeof window.SidebarManager.renderElements === 'function') { 5744 window.SidebarManager._syncWithGlobalEditState(); 5745 window.SidebarManager.renderElements(); 5746 } 5747 5748 // Handle the currently active gallery/page. 5749 if (window.galleries && window.activeGalleryId) { 5750 const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId); 5751 if (currentGalleryItem) { 5752 // The content for currentGalleryItem should remain loaded, 5753 // even if currentGalleryItem.visible is false. 5754 // The menu rendering above will hide it from the list if it's invisible. 5755 5756 // Apply page-specific menu hiding rules. 5757 const bodyEl = document.body; 5758 // When exiting edit mode, isInEditMode() will effectively be false for this check. 5759 if (shouldApplyMenuHiddenForEmbedOrGallery(currentGalleryItem)) { 5760 bodyEl.classList.add('menu-hidden-on-page'); 5761 } else { 5762 bodyEl.classList.remove('menu-hidden-on-page'); 5763 } 5764 applyMenuOverlayClassForGallery(currentGalleryItem); 5765 // Ensure active states in the (potentially now filtered) menu are updated. 5766 // If the active item is invisible, it won't be marked active in the menu, which is fine. 5767 if(typeof window.updateActiveStates === 'function') window.updateActiveStates(); 5768 if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal(); 5769 5770 } else { 5771 // activeGalleryId points to a non-existent item, so clear it and the content. 5772 console.log('toggleEditMode (Live): activeGalleryId points to a non-existent item. Clearing content.'); 5773 window.activeGalleryId = null; 5774 const galleryContainer = document.querySelector('.gallery-container'); 5775 if (galleryContainer) galleryContainer.innerHTML = ''; 5776 if(typeof window.updateActiveStates === 'function') window.updateActiveStates(); 5777 if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal(); 5778 } 5779 } 5780 5781 // If, after all that, no content is effectively active (e.g., initial load to root, 5782 // or activeGalleryId became null because the item was deleted) 5783 // then try to load a default page (home page or first visible gallery). 5784 const galleryContainer = document.querySelector('.gallery-container'); 5785 // Check if activeGalleryId is null OR if it's set but the container is empty (e.g. item was deleted) 5786 const noActiveContent = window.activeGalleryId === null || 5787 (galleryContainer && galleryContainer.innerHTML.trim() === ''); 5788 5789 if (noActiveContent && window.galleries) { 5790 console.log("toggleEditMode (Live): No active content, attempting to load default page."); 5791 const homePage = window.galleries.find(g => g.isHomePage === true); 5792 let loadedDefault = false;
5793 if (homePage) { 5794 // Load home page regardless of its visibility status for this default loading logic 5795 console.log(`toggleEditMode (Live): Loading home page: ${homePage.title}`); 5796 if (homePage.isPage && window.loadPage) { window.loadPage(homePage.id); loadedDefault = true; } 5797 else if (window.loadGallery) { window.loadGallery(homePage.id); loadedDefault = true; } 5798 } 5799 5800 if (!loadedDefault) { 5801 // If no home page, load the first *visible* gallery/page in live view. 5802 const firstVisibleGallery = window.galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu); 5803 if (firstVisibleGallery) { 5804 console.log(`toggleEditMode (Live): No home page, loading first visible item: ${firstVisibleGallery.title}`); 5805 if (firstVisibleGallery.isPage && window.loadPage) { 5806 window.loadPage(firstVisibleGallery.id); 5807 } else if(window.loadGallery) { 5808 window.loadGallery(firstVisibleGallery.id); 5809 } 5810 } else { 5811 console.log('toggleEditMode (Live): No home page and no visible items to load.'); 5812 } 5813 } 5814 } 5815 5816 if (window.MenuStyleCustomizer && 5817 window.MenuStyleCustomizer._overlayOpen && 5818 typeof window.MenuStyleCustomizer.applyStyles === 'function') { 5819 window.MenuStyleCustomizer.applyStyles(); 5820 } 5821 } 5822 window._editModeToggleInProgress = false; 5823} 5824 5825 5826 5827/** 5828 * Improved toggleSidebarElementForm - Closes other forms first 5829 */ 5830function toggleSidebarElementForm() { 5831 const formId = 'sidebarElementForm'; 5832 const form = document.getElementById(formId); 5833 5834 if (!form) return; 5835 5836 // If this form is already open, just close everything 5837 if (window.currentOpenForm === formId) { 5838 closeAllForms(); 5839 return; 5840 } 5841 5842 // Otherwise close all forms and open this one 5843 closeAllForms(); 5844 5845 // Show the form 5846 form.style.display = 'block'; 5847 form.classList.add('visible'); 5848 5849 // Set this as the current open form 5850 window.currentOpenForm = formId; 5851} 5852 5853// New function to add a sidebar element 5854function addSidebarElement() { 5855const typeInput = document.querySelector('input[name="sidebarElementType"]:checked'); 5856if (!typeInput) { 5857alert('Please select an element type'); 5858return; 5859} 5860 5861const type = typeInput.value; 5862 5863// Use the SidebarManager to add the element 5864if (window.SidebarManager) { 5865const newElementId = window.SidebarManager.addElement(type); 5866toggleSidebarElementForm(); 5867 5868if (newElementId) { 5869 // Save the changes to KV store 5870 saveGalleries().then(() => { 5871 console.log(`New ${type} element added successfully with ID: ${newElementId}`); 5872 }).catch(error => { 5873 console.error('Error saving new element:', error); 5874 }); 5875} 5876} else { 5877alert('SidebarManager not available'); 5878} 5879} 5880 5881 5882// Global variable to store metadata 5883window.siteMetadata = { 5884 title: '', 5885 description: '', 5886 googleAnalytics: '', 5887 noIndex: false, 5888 favicon: '', 5889 ogImageUrl: '', 5890 contentLanguage: 'en', // Default to English 5891 copyrightEnabled: false, 5892 copyrightText: '', 5893 customPublicSiteEnabled: false, 5894 customPublicSiteHtmlR2Key: '', 5895 customPublicSiteHtmlFilename: '', 5896 siteWideBasicAuth: undefined 5897}; 5898 5899// Global variable to store uploaded favicon URL temporarily 5900window.uploadedFaviconUrl = ''; 5901 5902function updateMetadataCopyrightFieldState() { 5903 const toggle = document.getElementById('metadataCopyrightEnabled'); 5904 const textInput = document.getElementById('metadataCopyrightText'); 5905 const isEnabled = !!(toggle && toggle.checked); 5906 5907 if (textInput) { 5908 textInput.disabled = !isEnabled; 5909 } 5910 5911 // Intentionally leave the surrounding form group in place; the disabled attribute on the input 5912 // provides the visual cue in PicoCSS. 5913} 5914 5915/** 5916 * Improved toggleMetadataEditor - Closes other forms first 5917 */ 5918function toggleMetadataEditor() { 5919 const formId = 'metadataEditor'; 5920 const editor = document.getElementById(formId); 5921 5922 if (!editor) return; 5923 5924 // If this form is already open, just close everything 5925 if (window.currentOpenForm === formId) { 5926 closeAllForms(); 5927 return; 5928 } 5929 5930 // Otherwise close all forms and open this one 5931 closeAllForms(); 5932 5933 // Load current values before showing 5934 loadMetadataValues(); 5935 editor.style.display = 'block'; 5936 5937 // Set this as the current open form 5938 window.currentOpenForm = formId; 5939 if (typeof syncCustomPublicSiteBypassVisibility === 'function') { 5940 syncCustomPublicSiteBypassVisibility(); 5941 } 5942} 5943 5944 5945// Load metadata values into the form 5946function loadMetadataValues() { 5947 // Get the current values from the global object 5948 const metadata = window.siteMetadata || {}; 5949 console.log('loadMetadataValues: Loading metadata:', JSON.stringify(metadata)); 5950 console.log('loadMetadataValues: Google Analytics from metadata:', metadata.googleAnalytics); 5951 5952 console.warn('=== FAVICON LOAD DEBUG ==='); 5953 console.warn('window.siteMetadata:', JSON.stringify(window.siteMetadata)); 5954 console.warn('metadata object:', JSON.stringify(metadata)); 5955 console.warn('metadata.favicon value:', metadata.favicon); 5956 console.warn('metadata.favicon type:', typeof metadata.favicon); 5957 5958 // Populate form fields 5959 document.getElementById('metaTitle').value = metadata.title || ''; 5960 document.getElementById('metaDescription').value = metadata.description || ''; 5961 document.getElementById('googleAnalytics').value = metadata.googleAnalytics || ''; 5962 document.getElementById('noIndexToggle').checked = metadata.noIndex || false; 5963 5964 // Schema / Identity fields 5965 const schemaType = metadata.schemaType || ''; 5966 const personRadio = document.getElementById('schemaTypePerson'); 5967 const orgRadio = document.getElementById('schemaTypeOrganization'); 5968 if (personRadio) personRadio.checked = schemaType === 'Person'; 5969 if (orgRadio) orgRadio.checked = schemaType === 'Organization'; 5970 const jobTitleEl = document.getElementById('schemaJobTitle'); 5971 if (jobTitleEl) jobTitleEl.value = metadata.schemaJobTitle || ''; 5972 const cityEl = document.getElementById('schemaCity'); 5973 if (cityEl) cityEl.value = metadata.schemaCity || ''; 5974 const emailEl = document.getElementById('schemaEmail'); 5975 if (emailEl) emailEl.value = metadata.schemaEmail || ''; 5976 const imageLicenseUrlEl = document.getElementById('imageLicenseUrl'); 5977 if (imageLicenseUrlEl) imageLicenseUrlEl.value = metadata.imageLicenseUrl || ''; 5978 const acquireLicensePageUrlEl = document.getElementById('acquireLicensePageUrl'); 5979 if (acquireLicensePageUrlEl) acquireLicensePageUrlEl.value = metadata.acquireLicensePageUrl || ''; 5980 if (typeof updateSchemaFields === 'function') updateSchemaFields(); 5981 5982 const copyrightToggle = document.getElementById('metadataCopyrightEnabled'); 5983 const copyrightInput = document.getElementById('metadataCopyrightText'); 5984 if (copyrightToggle) { 5985 const enabled = !!metadata.copyrightEnabled; 5986 copyrightToggle.checked = enabled; 5987 if (copyrightInput) { 5988 copyrightInput.value = metadata.copyrightText || ''; 5989 } 5990 } 5991 updateMetadataCopyrightFieldState(); 5992 5993 const customPubToggle = document.getElementById('customPublicSiteEnabled'); 5994 if (customPubToggle) { 5995 customPubToggle.checked = !!metadata.customPublicSiteEnabled; 5996 } 5997 const customPubKeyEl = document.getElementById('customPublicSiteHtmlR2Key'); 5998 if (customPubKeyEl) { 5999 customPubKeyEl.value = metadata.customPublicSiteHtmlR2Key || ''; 6000 } 6001 const customPubFileLabel = document.getElementById('customPublicSiteHtmlFilenameLabel'); 6002 if (customPubFileLabel) { 6003 customPubFileLabel.textContent = metadata.customPublicSiteHtmlFilename 6004 ? ('Current file: ' + metadata.customPublicSiteHtmlFilename) 6005 : 'No HTML file uploaded yet.'; 6006 } 6007 6008 const swAuth = metadata.siteWideBasicAuth; 6009 const swEnabled = !!(swAuth && swAuth.username && swAuth.password); 6010 const swEnableEl = document.getElementById('siteWideBasicAuthEnabled'); 6011 if (swEnableEl) swEnableEl.checked = swEnabled; 6012 const swUserEl = document.getElementById('siteWideBasicAuthUsername'); 6013 if (swUserEl) swUserEl.value = swAuth && swAuth.username ? swAuth.username : ''; 6014 const swPassEl = document.getElementById('siteWideBasicAuthPassword');
6015 if (swPassEl) swPassEl.value = swAuth && swAuth.password ? swAuth.password : ''; 6016 const swRealmEl = document.getElementById('siteWideBasicAuthRealm'); 6017 if (swRealmEl) swRealmEl.value = swAuth && swAuth.realm ? swAuth.realm : ''; 6018 if (typeof updateSiteWideBasicAuthFieldState === 'function') updateSiteWideBasicAuthFieldState(); 6019 6020 // Set content language dropdown (defaults to 'en') 6021 const contentLanguageSelect = document.getElementById('contentLanguage'); 6022 if (contentLanguageSelect) { 6023 contentLanguageSelect.value = metadata.contentLanguage || 'en'; 6024 console.log('Loaded content language:', contentLanguageSelect.value); 6025 } 6026 6027 const faviconInput = document.getElementById('faviconUrl'); 6028 if (faviconInput) { 6029 // Check if we have a recently uploaded favicon URL 6030 const faviconValue = metadata.favicon || window.uploadedFaviconUrl || ''; 6031 faviconInput.value = faviconValue; 6032 console.warn('Set favicon input value to:', faviconInput.value); 6033 console.warn('Used global uploaded favicon URL:', window.uploadedFaviconUrl); 6034 } else { 6035 console.error('Favicon input element not found!'); 6036 } 6037 6038 // Show favicon preview if there's an existing favicon 6039 if (metadata.favicon) { 6040 console.warn('Showing favicon preview for:', metadata.favicon); 6041 showFaviconPreview(metadata.favicon); 6042 } else { 6043 console.warn('No favicon to preview'); 6044 } 6045 6046 const ogInput = document.getElementById('ogImageUrl'); 6047 if (ogInput) { 6048 const ogVal = (metadata.ogImageUrl || '').trim(); 6049 ogInput.value = ogVal; 6050 } 6051 const ogBtnText = document.getElementById('uploadOgImageBtnText'); 6052 if (ogBtnText) { 6053 ogBtnText.textContent = (metadata.ogImageUrl || '').trim() ? 'Change image' : 'Upload image'; 6054 } 6055 if ((metadata.ogImageUrl || '').trim()) { 6056 showOgImagePreview(metadata.ogImageUrl.trim()); 6057 } else { 6058 const ogCont = document.getElementById('ogImagePreviewContainer'); 6059 if (ogCont) ogCont.innerHTML = ''; 6060 } 6061 6062 console.log('loadMetadataValues: Google Analytics field value after setting:', document.getElementById('googleAnalytics').value); 6063} 6064 6065// Save metadata values 6066function saveMetadata() { 6067 // Get values from form 6068 const googleAnalyticsElement = document.getElementById('googleAnalytics'); 6069 console.log('Google Analytics element found:', !!googleAnalyticsElement); 6070 console.log('Google Analytics element value:', googleAnalyticsElement ? googleAnalyticsElement.value : 'ELEMENT NOT FOUND'); 6071 6072 const faviconInput = document.getElementById('faviconUrl'); 6073 const faviconValue = faviconInput ? faviconInput.value : ''; 6074 const ogImageInput = document.getElementById('ogImageUrl'); 6075 const ogImageValue = ogImageInput ? (ogImageInput.value || '').trim() : ''; 6076 6077 const contentLanguageSelect = document.getElementById('contentLanguage'); 6078 const contentLanguageValue = contentLanguageSelect ? contentLanguageSelect.value : 'en'; 6079 6080 const copyrightToggle = document.getElementById('metadataCopyrightEnabled'); 6081 const copyrightInput = document.getElementById('metadataCopyrightText'); 6082 const copyrightEnabled = copyrightToggle ? copyrightToggle.checked : false; 6083 const copyrightText = copyrightInput ? copyrightInput.value.trim() : ''; 6084 6085 console.warn('=== FAVICON SAVE DEBUG ==='); 6086 console.warn('Favicon input element exists:', !!faviconInput); 6087 console.warn('Favicon input value:', faviconValue); 6088 console.warn('Favicon input value length:', faviconValue.length); 6089 6090 console.warn('=== CONTENT LANGUAGE SAVE ==='); 6091 console.warn('Content language selected:', contentLanguageValue); 6092 6093 const rawGa = googleAnalyticsElement ? googleAnalyticsElement.value : ''; 6094 // Normalize GA to ID only (client may not have normalizeGoogleAnalyticsId in scope) 6095 let gaId = (rawGa && typeof rawGa === 'string') ? rawGa.trim() : ''; 6096 if (gaId && !/^G-[A-Z0-9]+$/i.test(gaId) && !/^UA-d+-d+$/.test(gaId)) { 6097 const match = gaId.match(/(G-[A-Z0-9]+|UA-d+-d+)/i); 6098 gaId = match ? match[1] : ''; 6099 } 6100 const customPubKeyInput = document.getElementById('customPublicSiteHtmlR2Key'); 6101 const customPubFilename = document.getElementById('customPublicSiteHtmlFilenameStore'); 6102 const schemaPersonRadio = document.getElementById('schemaTypePerson'); 6103 const schemaOrgRadio = document.getElementById('schemaTypeOrganization'); 6104 const schemaType = schemaPersonRadio && schemaPersonRadio.checked ? 'Person' 6105 : schemaOrgRadio && schemaOrgRadio.checked ? 'Organization' 6106 : ''; 6107 const schemaJobTitleEl = document.getElementById('schemaJobTitle'); 6108 const schemaCityEl = document.getElementById('schemaCity'); 6109 const schemaEmailEl = document.getElementById('schemaEmail'); 6110 const imageLicenseUrlEl = document.getElementById('imageLicenseUrl'); 6111 const acquireLicensePageUrlEl = document.getElementById('acquireLicensePageUrl'); 6112 const imageLicenseUrlRaw = imageLicenseUrlEl ? imageLicenseUrlEl.value.trim() : ''; 6113 const acquireLicensePageUrlRaw = acquireLicensePageUrlEl ? acquireLicensePageUrlEl.value.trim() : ''; 6114 6115 const normalizePublicAbsoluteUrl = (value) => { 6116 if (!value) return ''; 6117 try { 6118 const parsed = new URL(value); 6119 if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return ''; 6120 return parsed.href; 6121 } catch { 6122 return ''; 6123 } 6124 }; 6125 const imageLicenseUrl = normalizePublicAbsoluteUrl(imageLicenseUrlRaw); 6126 const acquireLicensePageUrl = normalizePublicAbsoluteUrl(acquireLicensePageUrlRaw); 6127 if ((imageLicenseUrlRaw || acquireLicensePageUrlRaw) && (!imageLicenseUrl || !acquireLicensePageUrl)) { 6128 alert('Image licensing: enter two public http(s) URLs, or leave both fields empty.'); 6129 return; 6130 } 6131 6132 const siteWideBasicAuthEnabled = !!(document.getElementById('siteWideBasicAuthEnabled') && document.getElementById('siteWideBasicAuthEnabled').checked); 6133 const swUser = document.getElementById('siteWideBasicAuthUsername') ? document.getElementById('siteWideBasicAuthUsername').value.trim() : ''; 6134 const swPassEl = document.getElementById('siteWideBasicAuthPassword');
6135 const swPass = swPassEl ? swPassEl.value : ''; 6136 const swRealm = document.getElementById('siteWideBasicAuthRealm') ? document.getElementById('siteWideBasicAuthRealm').value.trim() : ''; 6137 6138 if (siteWideBasicAuthEnabled && (!swUser || !swPass)) { 6139 alert('Site-wide password: enter both username and password, or turn off the option.'); 6140 return; 6141 } 6142 6143 const metadata = { 6144 title: document.getElementById('metaTitle').value, 6145 description: document.getElementById('metaDescription').value, 6146 googleAnalytics: gaId, 6147 noIndex: document.getElementById('noIndexToggle').checked, 6148 favicon: faviconValue, 6149 ogImageUrl: ogImageValue, 6150 contentLanguage: contentLanguageValue, 6151 copyrightEnabled, 6152 copyrightText, 6153 customPublicSiteEnabled: !!(document.getElementById('customPublicSiteEnabled') && document.getElementById('customPublicSiteEnabled').checked), 6154 customPublicSiteHtmlR2Key: customPubKeyInput ? customPubKeyInput.value.trim() : '', 6155 customPublicSiteHtmlFilename: customPubFilename ? customPubFilename.value.trim() : '', 6156 schemaType: schemaType || undefined, 6157 schemaJobTitle: schemaJobTitleEl ? schemaJobTitleEl.value.trim() : '', 6158 schemaCity: schemaCityEl ? schemaCityEl.value.trim() : '',
6159 schemaEmail: schemaEmailEl ? schemaEmailEl.value.trim() : '', 6160 ...(imageLicenseUrl && acquireLicensePageUrl ? { imageLicenseUrl, acquireLicensePageUrl } : {}) 6161 }; 6162 6163 if (siteWideBasicAuthEnabled && swUser && swPass) { 6164 metadata.siteWideBasicAuth = { 6165 username: swUser, 6166 password: swPass, 6167 ...(swRealm ? { realm: swRealm } : {}) 6168 }; 6169 } 6170 6171 console.warn('Metadata object being created:', JSON.stringify(metadata)); 6172 console.warn('Favicon in metadata object:', metadata.favicon); 6173 6174 // Show normalized GA ID in the form so user sees what was saved (e.g. after pasting full snippet) 6175 if (googleAnalyticsElement && metadata.googleAnalytics !== rawGa.trim()) { 6176 googleAnalyticsElement.value = metadata.googleAnalytics; 6177 } 6178 6179 // Update global metadata object immediately 6180 window.siteMetadata = metadata; 6181 console.log("Updated window.siteMetadata:", JSON.stringify(window.siteMetadata)); 6182 if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') { 6183 window.SidebarManager.updateMetadataFooter(); 6184 } 6185 6186 // Ensure we have the latest galleries, sidebar elements, and styles 6187 // before constructing the customData object. 6188 6189 // Synchronize galleries first if the function exists 6190 if (typeof synchronizeGalleries === 'function') { 6191 synchronizeGalleries(); 6192 console.log('Galleries synchronized within saveMetadata'); 6193 } 6194 6195 // Retrieve current galleries (use window.galleries as the source of truth after sync) 6196 const currentGalleries = window.galleries || galleries || []; 6197 console.log(`Current galleries count before constructing customData: ${currentGalleries.length}`); 6198 6199 // Retrieve current sidebar elements 6200 const currentSidebarElements = window.SidebarManager && typeof window.SidebarManager.getElementsForSave === 'function' 6201 ? window.SidebarManager.getElementsForSave() 6202 : null; 6203 6204 // Retrieve current menu styles 6205 const currentMenuStyles = window.MenuStyleCustomizer ? window.MenuStyleCustomizer.settings : {}; 6206 6207 // Now construct customData using the retrieved current state 6208 if (window.saveGalleries) { 6209 const customData = { 6210 galleries: currentGalleries, // Use the synchronized/retrieved galleries 6211 siteMetadata: metadata, // Use the metadata gathered from the form 6212 menuStyles: currentMenuStyles // Use the retrieved menu styles 6213 }; 6214 if (Array.isArray(currentSidebarElements) && currentSidebarElements.length > 0) { 6215 customData.sidebarElements = currentSidebarElements; 6216 } 6217 6218 6219 console.log(`Saving metadata with preserved galleries: ${customData.galleries.length}`); 6220 console.log("Metadata being saved:", JSON.stringify(metadata)); 6221 console.log("Google Analytics specifically:", metadata.googleAnalytics); 6222 console.log("Sidebar elements being saved:", Array.isArray(customData.sidebarElements) ? customData.sidebarElements.length : '(omitted)'); 6223 console.log("Menu styles being saved:", JSON.stringify(customData.menuStyles)); 6224 6225 // Save with our correctly constructed custom data object 6226 window.saveGalleries(customData) 6227 .then(() => { 6228 alert('Metadata saved successfully'); 6229 toggleMetadataEditor(); // Hide the editor 6230 }) 6231 .catch(error => { 6232 console.error('Error saving metadata:', error); 6233 alert('Error saving metadata. Please try again.'); 6234 }); 6235 } else { 6236 alert('Save function not available'); 6237 } 6238} 6239 6240 6241// Function to add new entries to the style editor 6242function showStyleEditor(display = false) { 6243 if (display && window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.openOverlay === 'function') { 6244 window.MenuStyleCustomizer.openOverlay(); 6245 window.currentOpenForm = 'menuStyleEditor'; 6246 return; 6247 } 6248 hideStyleEditor(); 6249} 6250 6251// New function to hide the style editor 6252function hideStyleEditor() { 6253 if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.closeOverlay === 'function') { 6254 window.MenuStyleCustomizer.closeOverlay(); 6255 } 6256 const styleEditor = document.getElementById('menuStyleEditor'); 6257 if (styleEditor) { 6258 styleEditor.style.display = 'none'; 6259 } 6260} 6261 6262 6263/** 6264 * Improved toggleAddForm - Closes other forms first and ensures proper sizing 6265 */ 6266function toggleAddForm() { 6267 const formId = 'addForm'; 6268 const form = document.getElementById(formId); 6269 6270 if (!form) return; 6271 6272 // If this form is already open, just close everything 6273 if (window.currentOpenForm === formId) { 6274 closeAllForms(); 6275 return; 6276 } 6277 6278 // Otherwise close all forms and open this one 6279 closeAllForms(); 6280 6281 // Determine the appropriate max-height based on content 6282 // First check if we're showing the page form by checking the radio button 6283 const isPageSelected = document.querySelector('input[name="galleryType"][value="page"]')?.checked; 6284 6285 // Allow extra space for the page form 6286 const maxHeight = isPageSelected ? '600px' : '500px'; 6287 6288 // Show the form with proper max height 6289 form.style.maxHeight = maxHeight; 6290 form.style.overflow = 'visible';
6290 // Ensure all content is visible 6291 form.classList.add('visible'); 6292 6293 // Also add inline styles to ensure visibility during animation 6294 form.style.padding = '15px'; 6295 6296 // Force recalculation of max-height after UI is visible 6297 setTimeout(() => { 6298 // Get actual content height + padding 6299 const contentHeight = form.scrollHeight + 30; // Add padding 6300 6301 // Set max-height based on content with minimum of 500px 6302 form.style.maxHeight = Math.max(contentHeight, 500) + 'px'; 6303 6304 console.log(`Setting add form max height to ${form.style.maxHeight}`); 6305 }, 50); 6306 6307 // Populate parent options 6308 const parentSelect = document.getElementById('galleryParent'); 6309 if (parentSelect) { 6310 parentSelect.innerHTML = '<option value="">None (Top Level)</option>'; 6311 6312 // Add all galleries that could be parents (including submenus) 6313 galleries.forEach(gallery => { 6314 parentSelect.innerHTML += `<option value="${gallery.id}">${gallery.title}</option>`; 6315 }); 6316 } 6317 6318 // Set this as the current open form 6319 window.currentOpenForm = formId; 6320} 6321 6322 6323// Store the currently open form ID 6324window.currentOpenForm = null; 6325 6326/** 6327 * Close a child panel opened on top of the Style Customizer (palette explorer, custom code, color mixer). 6328 * Returns true when a child was closed so callers can keep the Style Customizer open. 6329 */ 6330function closeTopmostStyleCustomizerChildOverlay() { 6331 if (document.querySelector('.pe-palette-explorer-overlay')) { 6332 if (window.PaletteExplorer && typeof window.PaletteExplorer.close === 'function') { 6333 const wasOpen = !!document.querySelector('.pe-palette-explorer-overlay'); 6334 window.PaletteExplorer.close(); 6335 if (wasOpen && document.querySelector('.pe-palette-explorer-overlay')) { 6336 return false; 6337 } 6338 return wasOpen; 6339 } 6340 } 6341 if (document.querySelector('.pe-style-history-overlay, .sc-style-history-overlay') && 6342 window.MenuStyleCustomizer && 6343 typeof window.MenuStyleCustomizer.closeStyleHistoryOverlay === 'function') { 6344 window.MenuStyleCustomizer.closeStyleHistoryOverlay(); 6345 return true; 6346 } 6347 if (document.querySelector('.pe-font-explorer-overlay') && 6348 window.MenuStyleCustomizer && 6349 typeof window.MenuStyleCustomizer.closeFontExplorer === 'function') { 6350 window.MenuStyleCustomizer.closeFontExplorer(); 6351 return true; 6352 } 6353 if (document.querySelector('.pe-custom-code-overlay') && 6354 window.MenuStyleCustomizer && 6355 typeof window.MenuStyleCustomizer.closeCustomCodeOverlay === 'function') { 6356 window.MenuStyleCustomizer.closeCustomCodeOverlay(); 6357 return true; 6358 } 6359 const colorMixer = document.getElementById('colorMixerPopup'); 6360 if (colorMixer && colorMixer.classList.contains('is-open')) { 6361 colorMixer.classList.remove('is-open'); 6362 return true; 6363 } 6364 return false; 6365} 6366 6367/** 6368 * Close all editor forms with improved handling for add form 6369 */ 6370function closeAllForms() { 6371 if (typeof closeTopmostStyleCustomizerChildOverlay === 'function') { 6372 while (closeTopmostStyleCustomizerChildOverlay()) { 6373 // Close nested Style Customizer panels (palette, custom code, color mixer) first. 6374 } 6375 } 6376 6377 const keepMinimizedStyleCustomizer = !!(window.MenuStyleCustomizer && 6378 window.MenuStyleCustomizer._overlayOpen && 6379 window.MenuStyleCustomizer._overlayMinimized); 6380 6381 if (!keepMinimizedStyleCustomizer && 6382 window.MenuStyleCustomizer && 6383 typeof window.MenuStyleCustomizer.closeOverlay === 'function') { 6384 const styleOverlayWasOpen = window.MenuStyleCustomizer._overlayOpen; 6385 window.MenuStyleCustomizer.closeOverlay(); 6386 if (styleOverlayWasOpen && window.MenuStyleCustomizer._overlayOpen) { 6387 return; 6388 } 6389 } 6390 6391 // Add Form - Special handling to ensure proper animation 6392 const addForm = document.getElementById('addForm'); 6393 if (addForm) { 6394 addForm.classList.remove('visible'); 6395 6396 // Also force style reset after animation completes 6397 setTimeout(() => { 6398 if (!addForm.classList.contains('visible')) { 6399 addForm.style.maxHeight = '0'; 6400 addForm.style.overflow = 'hidden'; 6401 addForm.style.padding = '0 15px'; 6402 } 6403 }, 350); // Set slightly longer than CSS transition 6404 } 6405 6406 // Style Editor (do not hide the overlay host while the minimized mutate bar is live) 6407 const styleEditor = document.getElementById('menuStyleEditor'); 6408 if (styleEditor && !keepMinimizedStyleCustomizer) { 6409 styleEditor.style.display = 'none'; 6410 } 6411 6412 // Metadata Editor 6413 const metadataEditor = document.getElementById('metadataEditor'); 6414 if (metadataEditor) { 6415 metadataEditor.style.display = 'none'; 6416 } 6417 if (typeof syncCustomPublicSiteBypassVisibility === 'function') { 6418 syncCustomPublicSiteBypassVisibility(); 6419 } 6420 6421 // Sidebar Element Form 6422 const sidebarElementForm = document.getElementById('sidebarElementForm'); 6423 if (sidebarElementForm) { 6424 sidebarElementForm.style.display = 'none'; 6425 sidebarElementForm.classList.remove('visible'); 6426 } 6427 6428 // Import Classic Form 6429 const importClassicForm = document.getElementById('importClassicForm'); 6430 if (importClassicForm) { 6431 importClassicForm.style.display = 'none'; 6432 importClassicForm.classList.remove('visible'); 6433 } 6434 6435 // Clear the current open form 6436 window.currentOpenForm = null; 6437} 6438function downloadJson() { 6439 const data = JSON.stringify(galleries, null, 2); 6440 const blob = new Blob([data], { type: 'application/json' }); 6441 const url = URL.createObjectURL(blob); 6442 const a = document.createElement('a'); 6443 a.href = url; 6444 a.download = 'galleries.json'; 6445 document.body.appendChild(a); 6446 a.click(); 6447 document.body.removeChild(a); 6448 URL.revokeObjectURL(url); 6449} 6450 6451let galleriesBackup = []; 6452 6453 6454 6455/** 6456 * Improved version of initializeNestedSortables with a better nesting approach 6457 */ 6458function initializeNestedSortables() { 6459 console.log('Initializing nested sortables with improved nesting logic...'); 6460 6461 // Destroy leftover instances before binding again. A second Sortable on the 6462 // same list makes items preview during drag and snap back on release. 6463 document.querySelectorAll('#galleryTree .nested-sortable').forEach((el) => { 6464 try { 6465 const existing = typeof Sortable !== 'undefined' && Sortable.get ? Sortable.get(el) : null; 6466 if (existing && typeof existing.destroy === 'function') existing.destroy(); 6467 } catch (error) { 6468 console.warn('Could not destroy existing nested sortable:', error); 6469 } 6470 }); 6471 6472 // Create a backup of galleries 6473 try { 6474 galleriesBackup = JSON.parse(JSON.stringify(galleries)); 6475 } catch (error) { 6476 console.error('Error creating galleries backup:', error); 6477 galleriesBackup = []; 6478 } 6479 6480 // Find all nested sortable lists 6481 const nestedSortables = document.querySelectorAll('.nested-sortable'); 6482 if (nestedSortables.length === 0) { 6483 console.warn('No nested sortable elements found! Check your HTML
6483structure.'); 6484 return; 6485 } 6486 6487 // Helper: compute depth based on ancestor nested lists 6488 function computeDepth(liEl) { 6489 let depth = 0; 6490 let node = liEl; 6491 while (node) { 6492 const parentOl = node.closest('ol.nested-sortable'); 6493 if (!parentOl) break; 6494 const parentLi = parentOl.closest('li'); 6495 if (parentLi) { 6496 depth += 1; 6497 node = parentLi; 6498 } else { 6499 break; 6500 } 6501 } 6502 // Top-level depth should be 0 6503 return Math.max(depth - 1, 0); 6504 } 6505 6506 // Helper: apply indentation styling to an li and its subtree 6507 function applyIndentationFromDepth(rootLi) { 6508 if (!rootLi) return; 6509 const applyToItem = (li) => { 6510 const depth = computeDepth(li); 6511 // Update data-nesting-level attribute 6512 li.setAttribute('data-nesting-level', depth); 6513 // Apply padding-left style 6514 const content = li.querySelector('.gallery-item-content'); 6515 if (content) { 6516 content.style.paddingLeft = (depth * 16) + 'px'; 6517 } 6518 }; 6519 applyToItem(rootLi); 6520 rootLi.querySelectorAll('li').forEach(applyToItem); 6521 } 6522 6523 // Helper: ensure parent has proper state and nested list 6524 function ensureParentChildState(parentLi) { 6525 if (!parentLi) return null; 6526 parentLi.classList.add('has-children'); 6527 let nestedList = parentLi.querySelector('ol.nested-sortable'); 6528 if (!nestedList) { 6529 nestedList = document.createElement('ol'); 6530 nestedList.className = 'nested-sortable'; 6531 parentLi.appendChild(nestedList); 6532 } 6533 return nestedList; 6534 } 6535 6536 // Helper: clean parent if it has no children 6537 function cleanupParentIfEmpty(parentLi) { 6538 if (!parentLi) return; 6539 const nestedList = parentLi.querySelector(':scope > ol.nested-sortable'); 6540 const parentId = parseInt(parentLi.dataset && parentLi.dataset.id, 10); 6541 const parentItem = !isNaN(parentId) ? galleries.find(g => g.id === parentId) : null; 6542 // Opted-in pages/galleries must keep an empty drop zone and chevron. 6543 if (isNavigableMenuParent(parentItem)) { 6544 parentLi.classList.add('has-children'); 6545 return; 6546 } 6547 if (nestedList && nestedList.children.length === 0) { 6548 nestedList.remove(); 6549 parentLi.classList.remove('has-children'); 6550 } 6551 } 6552 6553 // Initialize each sortable with improved nesting settings 6554 let sortableInstances = []; 6555 6556 for (let i = 0; i < nestedSortables.length; i++) { 6557 try { 6558 const container = nestedSortables[i]; 6559 6560 // Track where the placeholder indicates the item should be placed 6561 // This helps us detect when SortableJS incorrectly auto-nests based on mouse position 6562 let intendedPlacementContainer = null; 6563 let intendedRelatedElement = null; 6564 6565 // Track last move to prevent oscillation 6566 let lastMoveTime = 0; 6567 let lastMoveRelatedId = null; 6568 const MOVE_DEBOUNCE_MS = 50; // Minimum time between moves (in milliseconds) 6569 6570 // Track if we're dragging the last item in a folder (to prevent boundary oscillation) 6571 let isLastItemInFolder = false; 6572 let draggedItemOriginalContainer = null; 6573 6574 // Track container changes to prevent rapid switching (hysteresis) 6575 let firstPositionFixLoopActive = false; // Prevent multiple fix loops 6576 let lastContainerDecision = null; // 'inside' or 'outside' 6577 let lastContainerDecisionTime = 0; 6578 const CONTAINER_SWITCH_DEBOUNCE_MS = 150; // Longer debounce for container switches 6579 6580 const sortable = new Sortable(container, { 6581 group: 'nested', 6582 animation: 150, // Reduced animation during drag to reduce lag 6583 easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', // Smooth easing function 6584 fallbackOnBody: false, // Disable forced fallback on desktop for better performance 6585 forceFallback: false, // Use native drag on desktop 6586 6587 // CRITICAL: These settings make nesting much easier 6588 // Higher swap threshold makes it easier to drag over items 6589 swapThreshold: 0.65, // Increased to reduce oscillation within same folder 6590 6591 // IMPROVED NESTING SETTINGS 6592 // Used when determining if an item should be nested 6593 invertSwap: false, // Disable to reduce oscillation 6594 direction: 'vertical', // Lock direction to vertical to reduce oscillation 6595 6596 // Use handle for dragging 6597 handle: '.nested-sortable-handle', 6598 6599 // Filter out only the toggle button - this prevents SortableJS from intercepting clicks on it 6600 // The folder text (.menu-item.folder) is NOT filtered, so it can be used for both: 6601 // - Quick click â toggles folder (onclick handler fires before drag starts) 6602 // - Click and drag â drags the item (SortableJS handles the drag) 6603 // The delay helps distinguish between click and drag, but on desktop with delayOnTouchOnly: true, 6604 // the onclick should fire quickly enough before SortableJS intercepts 6605 filter: '.submenu-toggle, .submenu-toggle *, .toggle-icon', 6606 preventOnFilter: true, // Prevent drag when clicking filtered elements (toggle button only) 6607 6608 // Distinguish between click and drag 6609 delay: 120, 6610 delayOnTouchOnly: true, 6611 6612 // Empty container settings 6613 emptyInsertThreshold: 10, 6614 6615 // Add custom ghost class 6616 ghostClass: 'sortable-ghost', 6617 chosenClass: 'sortable-chosen', 6618 dragClass: 'sortable-drag', 6619 6620 // Make sure we listen to external move events 6621 dragoverBubble: true, 6622 6623 // Better drag preview stability 6624 scroll: true, 6625 scrollSensitivity: 100, 6626 scrollSpeed: 20, 6627 6628 // Reduce oscillation by making placeholder movement less sensitive 6629 forceFallback: false, // Use native drag for better performance 6630 fallbackTolerance: 0, // No tolerance - use exact position 6631 6632 /** 6633 * NESTING LOGIC: Prevent auto-nesting based on hover 6634 * Instead, rely on SortableJS's placeholder position which is accurate 6635 * We'll verify and correct placement in onEnd based on actual DOM position 6636 */ 6637 onMove: function(evt, originalEvent) { 6638 const dragged = evt.dragged; 6639 const related = evt.related; 6640 6641 if (!dragged || !related) return true; 6642 6643 // Safety check: Ensure items have data-id attributes 6644 // This can be missing if item was just added and DOM not fully updated 6645 if (!dragged.dataset.id || !related.dataset.id) { 6646 console.warn('â ï¸ [Drag] Item missing data-id attribute, aborting drag', { 6647 draggedHasId: !!dragged.dataset.id, 6648 relatedHasId: !!related.dataset.id 6649 }); 6650 return false;
6651 } 6652 6653 // Get the IDs to check for circular references 6654 const draggedId = parseInt(dragged.dataset.id); 6655 const relatedId = parseInt(related.dataset.id); 6656 6657 // Don't allow dropping inside itself or its descendants 6658 if (draggedId === relatedId || hasAncestor(relatedId, draggedId)) { 6659 return false; 6660 } 6661 6662 const currentTime = Date.now(); 6663 const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); 6664 const draggedParent = draggedItemOriginalContainer || dragged.parentElement; 6665 6666 // Determine if we're moving inside or outside a folder 6667 // IMPORTANT: Check if related element IS the folder item itself (parent li) 6668 // When targeting first position, SortableJS might use the folder item as related 6669 const isRelatedFolderItem = related.classList && related.classList.contains('has-children'); 6670 6671 // CRITICAL: Handle empty folder case FIRST - before any other logic 6672 // When dragging into an empty folder, related is the folder item itself 6673 // This is essentially the same as targeting first position, but for an empty folder 6674 // We need to allow this immediately, bypassing all debounce and boundary checks 6675 if (isRelatedFolderItem) { 6676 const folderNestedSortable = related.querySelector(':scope > ol.nested-sortable'); 6677 const relatedIsNavigableParent = related.classList.contains('navigable-parent-item'); 6678 // Navigable parents look like normal links. Only treat them as an 6679 // empty folder when the pointer is actually over the empty list. 6680 let allowEmptyFolderInsert = !!(folderNestedSortable && folderNestedSortable.children.length === 0); 6681 if (allowEmptyFolderInsert && relatedIsNavigableParent && originalEvent) { 6682 const listRect = folderNestedSortable.getBoundingClientRect(); 6683 const pointerY = originalEvent.clientY; 6684 allowEmptyFolderInsert = pointerY >= listRect.top - 6 && pointerY <= listRect.bottom + 10; 6685 } 6686 if (allowEmptyFolderInsert) { 6687 // Empty folder detected - this is a drop into empty folder (first position) 6688 // Always allow it, bypass all debounce and boundary logic 6689 lastMoveTime = currentTime; 6690 lastMoveRelatedId = relatedId; 6691 intendedPlacementContainer = folderNestedSortable; 6692 intendedRelatedElement = null; // No related element in empty folder 6693 // Mark as moving inside folder for tracking 6694 lastContainerDecision = 'inside'; 6695 lastContainerDecisionTime = currentTime; 6696 6697 // CRITICAL: Manually move placeholder into empty folder's nested-sortable 6698 // SortableJS might not do this automatically for empty containers 6699 requestAnimationFrame(() => { 6700 const placeholder = document.querySelector('.sortable-ghost'); 6701 if (placeholder && placeholder.parentElement !== folderNestedSortable) { 6702 // Move placeholder into the empty folder 6703 folderNestedSortable.appendChild(placeholder); 6704 } 6705 6706 // Update ghost indentation for empty folder 6707 const ghost = document.querySelector('.sortable-ghost'); 6708 if (ghost) { 6709 const ghostContent = ghost.querySelector('.gallery-item-content'); 6710 if (ghostContent) { 6711 // Calculate depth from parent folder 6712 const parentFolder = folderNestedSortable.closest('li.has-children'); 6713 if (parentFolder) { 6714 const targetDepth = computeDepth(parentFolder) + 1; 6715 ghostContent.style.paddingLeft = (targetDepth * 16) + 'px'; 6716 } 6717 } 6718 } 6719 }); 6720 6721 // Return true immediately to allow the move 6722 return true; 6723 } 6724 } 6725 6726 let actualRelatedParent = related.parentElement; 6727 6728 // If related IS the folder item, we need to get its nested sortable container 6729 if (isRelatedFolderItem) { 6730 const nestedSortable = related.querySelector('ol.nested-sortable'); 6731 if (nestedSortable) { 6732 actualRelatedParent = nestedSortable; 6733 } 6734 } 6735 6736 const relatedParent = actualRelatedParent; 6737 6738 const isMovingInsideFolder = relatedParent && 6739 relatedParent.classList.contains('nested-sortable') && 6740 relatedParent !== rootContainer; 6741 const isMovingOutsideFolder = !isRelatedFolderItem && relatedParent === rootContainer; 6742 const currentDecision = isMovingInsideFolder ? 'inside' : (isMovingOutsideFolder ? 'outside' : null); 6743 6744 // Determine if we're moving within the same folder - this is the key check 6745 // If related IS the folder item, we're still moving within the same folder 6746 let isMovingWithinSameFolder = (isMovingInsideFolder && 6747 draggedParent && 6748 draggedParent.classList.contains('nested-sortable') && 6749 draggedParent !== rootContainer && 6750 relatedParent === draggedParent) || 6751 // OR: if related is the folder item itself and we're dragging from that folder 6752 (isRelatedFolderItem && 6753 draggedParent && 6754 draggedParent.classList.contains('nested-sortable') && 6755 draggedParent !== rootContainer && 6756 related.querySelector('ol.nested-sortable') === draggedParent); 6757 6758 // Special case: If related is the folder item, we're definitely trying to move within that folder 6759 // This happens when targeting first position - SortableJS uses the folder item as related 6760 if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { 6761 const folderNestedSortable = related.querySelector('ol.nested-sortable'); 6762 if (folderNestedSortable === draggedParent) { 6763 // We're dragging within the folder, and related is the folder item 6764 // This means we're targeting first position - FORCE it to be treated as same-folder move 6765 // FORCE same-folder detection - override any previous calculation 6766 isMovingWithinSameFolder = true; 6767 } 6768 } 6769 6770 // DEBUG: Log placeholder position changes ONLY when dragging from within a folder AND something interesting happens 6771 // (Don't log every single move - too noisy) 6772 if (draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { 6773 const folderItems = Array.from(draggedParent.children); 6774 const draggedIndex = folderItems.indexOf(dragged); 6775 const relatedIndex = folderItems.indexOf(related); 6776 6777 // Get placeholder element to see where it actually is 6778 const placeholder = document.querySelector('.sortable-ghost'); 6779 const placeholderParent = placeholder ? placeholder.parentElement : null; 6780 const placeholderIndex = placeholderParent && placeholderParent.classList.contains('nested-sortable') 6781 ? Array.from(placeholderParent.children).indexOf(placeholder) 6782 : -1; 6783 6784 const folderItem = draggedParent.closest('li.has-children'); 6785 const folderItemId = folderItem ? folderItem.dataset.id : null; 6786 const firstItemInFolder = folderItems.length > 0 ? folderItems[0] : null; 6787 const firstItemInFolderId = firstItemInFolder ? firstItemInFolder.dataset.id : null; 6788 6789 // Check if we're trying to target first position 6790 // related could be the first item OR the folder item itself (when SortableJS uses folder as related) 6791 const isFirstPosition = folderItems.length > 0 && related === firstItemInFolder; 6792 const isRelatedFolderItemCheck = related === folderItem; 6793 const isTargetingFirstViaFolderItem = isRelatedFolderItemCheck && !!folderItem; // FIXED: was assigning folderItem instead of boolean 6794 6795 // When targeting first via folder item, placeholder should be at index 0 6796 // But we're seeing it at index 1 - this is the issue! 6797 const shouldBeAtFirstPosition = isTargetingFirstViaFolderItem && placeholderIndex === 1 && draggedIndex > 0; 6798 6799 // Check if placeholder is outside the folder when it should be inside 6800 const placeholderIsOutside = placeholderParent === rootContainer || (placeholderParent && !placeholderParent.classList.contains('nested-sortable')); 6801 const shouldBeInside = draggedIndex >= 0; // We're dragging from inside 6802 6803 // Only log when there's a potential issue or interesting state change 6804 const placeholderMismatch = placeholderIndex !== relatedIndex && placeholderIndex >= 0 && relatedIndex >= 0; 6805 const jumpingOut = !isMovingWithinSameFolder && draggedIndex >= 0 && relatedIndex < 0; 6806 const jumpingOutToRoot = placeholderIsOutside && shouldBeInside; 6807 6808 // First position issue: trying to target first but: 6809 // - placeholder is outside OR 6810 // - placeholder is at index 1 instead of 0 (when targeting first via folder item) 6811 const firstPositionIssue = (isFirstPosition || isTargetingFirstViaFolderItem) && (jumpingOut || jumpingOutToRoot || placeholderIsOutside || shouldBeAtFirstPosition); 6812 6813 // ALWAYS log when targeting first position via folder item (this is the key case we're debugging) 6814 // OR when placeholder jumps out when it should stay in 6815 // OR when placeholder should be at first position but isn't 6816 const shouldLog = placeholderMismatch || jumpingOut || jumpingOutToRoot || firstPositionIssue || isTargetingFirstViaFolderItem || shouldBeAtFirstPosition; 6817 6818 if (shouldLog) { 6819 console.error('ð [Placeholder Position]', { 6820 draggedId, 6821 draggedIndex, 6822 relatedId, 6823 relatedIndex, 6824 placeholderIndex, 6825 relatedElementTag: related.tagName, 6826 isRelatedFolderItem: isRelatedFolderItemCheck, 6827 isTargetingFirstViaFolderItem, 6828 isMovingWithinSameFolder, 6829 placeholderMismatch, 6830 jumpingOut, 6831 jumpingOutToRoot, 6832 isFirstPosition, 6833 placeholderIsOutside, 6834 placeholderParentType: placeholderParent === rootContainer ? 'ROOT' : (placeholderParent && placeholderParent.classList.contains('nested-sortable') ? 'FOLDER' : 'OTHER'), 6835 folderItemId, 6836 firstItemInFolderId, 6837 firstPositionIssue, 6838 shouldBeAtFirstPosition, 6839 expectedPlaceholderIndex: isTargetingFirstViaFolderItem ? 0 : placeholderIndex 6840 }); 6841 } 6842 } 6843 6844 // CRITICAL: For moves within the same folder, skip ALL the complex boundary logic 6845 // This allows smooth reordering just like items not in folders 6846 if (isMovingWithinSameFolder) { 6847 // Simple debounce only - same as root level items 6848 const timeSinceLastMove = currentTime - lastMoveTime; 6849 // Only prevent very rapid oscillation to same element 6850 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5 && relatedId === lastMoveRelatedId) { 6851 return false;
6852 } 6853 // Allow all moves within same folder - update tracking and continue 6854 6855 // SPECIAL FIX: When targeting first position via folder item, fix placeholder position 6856 // SortableJS places it at index 1 when related is the folder item, but we want it at index 0 6857 // BUT: Only apply this fix when actually near/at first position to avoid being too aggressive 6858 if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { 6859 const folderItems = Array.from(draggedParent.children); 6860 const firstItem = folderItems.length > 0 ? folderItems[0] : null; 6861 6862 // Only apply fix if we're actually near first position (draggedIndex is small, indicating we're moving up) 6863 // This prevents the fix from being too aggressive when moving to other positions 6864 const isNearFirstPosition = draggedIndex <= 2; // Allow fix when within first 3 positions 6865 6866 if (firstItem && firstItem !== dragged && isNearFirstPosition) { 6867 // Fix immediately (synchronously) first - don't wait for next frame 6868 const placeholder = document.querySelector('.sortable-ghost'); 6869 if (placeholder && placeholder.parentElement === draggedParent) { 6870 const currentIndex = Array.from(draggedParent.children).indexOf(placeholder); 6871 // Only fix if placeholder is at index 1 when we're targeting first position 6872 // Don't force it if it's at index 0 or beyond index 2 (user might be moving to position 2 or 3) 6873 if (currentIndex === 1 && draggedIndex <= 1) { 6874 draggedParent.insertBefore(placeholder, firstItem); 6875 } 6876 } 6877 } else { 6878 // Not near first position - stop any active fix loop 6879 firstPositionFixLoopActive = false; 6880 } 6881 } else { 6882 // Not targeting first position anymore - reset flag 6883 firstPositionFixLoopActive = false; 6884 } 6885 6886 lastMoveTime = currentTime; 6887 lastMoveRelatedId = relatedId; 6888 // Skip all the boundary case logic below - just continue to allow the move 6889 // Fall through to update intendedPlacementContainer and return true 6890 } else { 6891 // Only apply complex boundary logic for moves BETWEEN folders or to/from root 6892 6893 // Hysteresis: Prevent rapid switching between inside/outside folder 6894 const targetFolderItems = isMovingInsideFolder ? Array.from(relatedParent.children) : []; 6895 const isFirstPositionInFolder = isMovingInsideFolder && 6896 targetFolderItems.length > 0 && 6897 related === targetFolderItems[0]; 6898 // Special case: Empty folder - allow easy dropping into it 6899 const isEmptyFolder = isMovingInsideFolder && targetFolderItems.length === 0; 6900 6901 if (currentDecision && lastContainerDecision && currentDecision !== lastContainerDecision) { 6902 const timeSinceContainerSwitch = currentTime - lastContainerDecisionTime; 6903 6904 // Make first position easier to target from outside 6905 if (isFirstPositionInFolder) { 6906 // Moving to first position from outside - use shorter debounce 6907 const effectiveDebounce = CONTAINER_SWITCH_DEBOUNCE_MS * 0.3; 6908 if (timeSinceContainerSwitch < effectiveDebounce) {
6909 // Only block if it's very rapid (oscillation) 6910 if (timeSinceContainerSwitch < MOVE_DEBOUNCE_MS) { 6911 return false; 6912 } 6913 } 6914 } else if (isEmptyFolder) { 6915 // Empty folder - allow easy dropping, minimal debounce 6916 // Only block very rapid oscillation 6917 if (timeSinceContainerSwitch < MOVE_DEBOUNCE_MS * 0.2) { 6918 return false; 6919 } 6920 } else { 6921 // Normal container switch - apply full hysteresis 6922 if (timeSinceContainerSwitch < CONTAINER_SWITCH_DEBOUNCE_MS) { 6923 return false; 6924 } 6925 } 6926 } 6927 } 6928 6929 // Special handling for boundary cases (last item in folder, or dragging into folder) 6930 // BUT: Skip all this logic if we're moving within the same folder (already handled above) 6931 if (!isMovingWithinSameFolder && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { 6932 const folderItem = draggedParent.closest('li.has-children'); 6933 const currentFolderItems = Array.from(draggedParent.children); 6934 const isFirstPositionInCurrentFolder = currentFolderItems.length > 0 && 6935 currentFolderItems[0] !== dragged && 6936 (related === currentFolderItems[0] || 6937 (isMovingInsideFolder && relatedParent === draggedParent && 6938 related === currentFolderItems[0])); 6939 6940 // Case 0: This case is now handled earlier for same-folder moves 6941 // Only handle cross-folder moves here 6942 6943 // Case 1: Dragging last item out of folder 6944 if (isLastItemInFolder && isMovingOutsideFolder) { 6945 const timeSinceLastMove = currentTime - lastMoveTime; 6946 6947 // BUT: If we're trying to move to first position, don't block it 6948 // Check if the related element is the folder itself or right after it 6949 if (folderItem) { 6950 const folderNextSibling = folderItem.nextElementSibling; 6951 // If we're moving to position right after folder, it might be trying to go to first position 6952 // Allow it if it's been a reasonable time 6953 if (folderNextSibling && related === folderNextSibling) { 6954 // This is ambiguous - could be first position or outside 6955 // Require more time but not as much as pure outside move 6956 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) { 6957 if (relatedId === lastMoveRelatedId) { 6958 return false; 6959 } 6960 } 6961 } else { 6962 // Definitely moving outside - require longer debounce 6963 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) { 6964 if (relatedId === lastMoveRelatedId) { 6965 return false; // Reject if oscillating 6966 } 6967 } 6968 } 6969 } else { 6970 // No folder item found - apply standard debounce 6971 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) { 6972 if (relatedId === lastMoveRelatedId) { 6973 return false; 6974 } 6975 } 6976 } 6977 } 6978 6979 // Case 2: Dragging item into folder from outside 6980 if (isMovingInsideFolder && relatedParent !== draggedParent) { 6981 const targetFolderItems = Array.from(relatedParent.children); 6982 const isEmptyFolder = targetFolderItems.length === 0; 6983 const isNearBottom = targetFolderItems.length > 0 && 6984 (related === targetFolderItems[targetFolderItems.length - 1] || 6985 targetFolderItems.indexOf(related) >= targetFolderItems.length - 1); 6986 const isFirstPos = targetFolderItems.length > 0 && related === targetFolderItems[0]; 6987 6988 // Special handling for empty folder - allow easy dropping 6989 if (isEmptyFolder) { 6990 const timeSinceLastMove = currentTime - lastMoveTime; 6991 // Very minimal debounce for empty folder - only prevent rapid oscillation 6992 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.2 && relatedId === lastMoveRelatedId) { 6993 return false;
6994 } 6995 // Allow move into empty folder - don't block it 6996 } else if (isFirstPos) { 6997 // Moving to first position - make it easy 6998 const timeSinceLastMove = currentTime - lastMoveTime; 6999 // Very short debounce for first position 7000 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.3 && relatedId === lastMoveRelatedId) { 7001 return false; 7002 } 7003 // Allow move to first position 7004 } else if (isNearBottom) { 7005 // For bottom position, still use some debounce to prevent oscillation 7006 const timeSinceLastMove = currentTime - lastMoveTime; 7007 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.5) { 7008 if (relatedId === lastMoveRelatedId) { 7009 return false; 7010 } 7011 } 7012 } 7013 } 7014 7015 // Case 2.5: Prevent jumping OUT of folder when trying to move to first/last position within 7016 // This is tricky - SortableJS might detect it as outside when we're actually targeting first/last 7017 if (isMovingOutsideFolder && folderItem && draggedParent === draggedItemOriginalContainer) { 7018 // We're dragging from within a folder but detected as moving outside 7019 // Check if the related element is adjacent to the folder (which might indicate targeting first/last) 7020 const folderIndex = Array.from(rootContainer.children).indexOf(folderItem); 7021 const relatedIndex = Array.from(rootContainer.children).indexOf(related); 7022 7023 // If related is immediately before or after folder, might be targeting first/last 7024 if (folderIndex >= 0 && relatedIndex >= 0) { 7025 const isAdjacentToFolder = (relatedIndex === folderIndex - 1) || (relatedIndex === folderIndex + 1); 7026 7027 if (isAdjacentToFolder) { 7028 // Might be targeting first/last position - be lenient 7029 const timeSinceLastMove = currentTime - lastMoveTime;
7030 // Only block if it's very rapid oscillation 7031 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.4 && relatedId === lastMoveRelatedId) { 7032 return false; 7033 } 7034 // Otherwise allow - user might be trying to target first/last position 7035 // Don't block based on this check 7036 } 7037 } 7038 } 7039 } 7040 7041 // Case 3: Dragging from root into folder 7042 if (draggedParent === rootContainer && isMovingInsideFolder) { 7043 const targetFolderItems = Array.from(relatedParent.children); 7044 const isEmptyFolder = targetFolderItems.length === 0; 7045 const isNearBottom = targetFolderItems.length > 0 && 7046 (related === targetFolderItems[targetFolderItems.length - 1] || 7047 targetFolderItems.indexOf(related) >= targetFolderItems.length - 1); 7048 const isFirstPosition = targetFolderItems.length > 0 && related === targetFolderItems[0]; 7049 7050 // Special handling for empty folder - allow easy dropping 7051 if (isEmptyFolder) { 7052 const timeSinceLastMove = currentTime - lastMoveTime; 7053 // Very minimal debounce for empty folder - only prevent rapid oscillation 7054 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.2) { 7055 if (relatedId === lastMoveRelatedId) { 7056 return false; 7057 } 7058 } 7059 // Allow move into empty folder - don't block it 7060 } else if (isFirstPosition) { 7061 // Special handling for first position - make it easier to target 7062 const timeSinceLastMove = currentTime - lastMoveTime; 7063 // Make first position easier - only prevent very rapid oscillation 7064 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5) { 7065 if (relatedId === lastMoveRelatedId) { 7066 return false; 7067 } 7068 } 7069 // Allow move to first position - don't block it 7070 // Continue to track but don't apply strict debouncing 7071 } else if (isNearBottom) { 7072 // Keep strict debouncing for bottom position 7073 const timeSinceLastMove = currentTime - lastMoveTime; 7074 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) { 7075 if (relatedId === lastMoveRelatedId) { 7076 return false; 7077 } 7078 } 7079 } 7080 } 7081 7082 // Case 4: Prevent jumping FROM first position to outside folder (but allow easy access TO first position) 7083 // If we're at first position and trying to move outside, require deliberate movement 7084 if (isMovingInsideFolder) { 7085 const currentFolderItems = Array.from(relatedParent.children); 7086 const isFirstPos = currentFolderItems.length > 0 && related === currentFolderItems[0]; 7087 7088 // If we're moving FROM first position to outside (reverse hysteresis) 7089 if (isFirstPos && lastContainerDecision === 'outside') { 7090 const timeSinceLastMove = currentTime - lastMoveTime; 7091 // If we just moved outside, require more time before allowing back to first position 7092 // BUT: if it's been a reasonable time, allow it (don't make it too hard) 7093 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.8 && relatedId === lastMoveRelatedId) { 7094 // Very rapid - might be oscillation 7095 return false; 7096 } 7097 } 7098 } 7099 7100 // Case 5: Prevent oscillation between first and second position within folder 7101 // BUT: Only apply if NOT moving within same folder (to allow smooth reordering) 7102 if (isMovingInsideFolder && relatedParent && !isMovingWithinSameFolder) { 7103 const folderItems = Array.from(relatedParent.children); 7104 const isFirstPos = folderItems.length > 0 && related === folderItems[0]; 7105 const isSecondPos = folderItems.length > 1 && related === folderItems[1]; 7106 7107 // If moving from first to second (or vice versa) very rapidly, prevent oscillation 7108 if ((isFirstPos || isSecondPos) && lastMoveRelatedId) { 7109 const lastRelated = document.querySelector('#galleryTree li[data-id="' + lastMoveRelatedId + '"]') || document.querySelector('li[data-id="' + lastMoveRelatedId + '"]'); 7110 if (lastRelated && lastRelated.parentElement === relatedParent) { 7111 const lastRelatedIndex = folderItems.indexOf(lastRelated); 7112 const isOscillating = (isFirstPos && lastRelatedIndex === 1) || (isSecondPos && lastRelatedIndex === 0); 7113 7114 if (isOscillating) { 7115 const timeSinceLastMove = currentTime - lastMoveTime; 7116 // Allow moving to first position easily, but prevent rapid oscillation between first/second 7117 if (isSecondPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.2) { 7118 return false; // Prevent jumping from first to second too easily 7119 } 7120 // But allow first position more easily 7121 if (isFirstPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.6 && relatedId === lastMoveRelatedId) { 7122 return false; // Only prevent very rapid oscillation 7123 } 7124 } 7125 } 7126 } 7127 } 7128 7129 // Prevent rapid oscillation when dragging within same container 7130 // NOTE: For same-folder moves, we already updated tracking above, so skip this
7130section 7131 if (!isMovingWithinSameFolder) { 7132 const timeSinceLastMove = currentTime - lastMoveTime; 7133 7134 // If moving to the same related element very quickly, reject to prevent oscillation 7135 if (timeSinceLastMove < MOVE_DEBOUNCE_MS && relatedId === lastMoveRelatedId) { 7136 return false; 7137 } 7138 7139 // Update tracking variables (only for non-same-folder moves) 7140 lastMoveTime = currentTime; 7141 lastMoveRelatedId = relatedId; 7142 } 7143 // For same-folder moves, tracking was already updated in the earlier block 7144 7145 // Update container decision tracking 7146 if (currentDecision) { 7147 if (currentDecision !== lastContainerDecision) { 7148 lastContainerDecisionTime = currentTime; 7149 } 7150 lastContainerDecision = currentDecision; 7151 } 7152 7153 // Track where the placeholder indicates the item should be placed 7154 // The related element is next to where the placeholder will be positioned 7155 // Check if it's at root level or nested level 7156 if (related) { 7157 intendedRelatedElement = related; 7158 const relatedParent = related.parentElement; 7159 const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); 7160 7161 // Determine if the placeholder is at root level or nested level 7162 if (relatedParent === rootContainer) { 7163 // Placeholder is at root level (not nested) 7164 intendedPlacementContainer = rootContainer; 7165 } else if (relatedParent && relatedParent.classList.contains('nested-sortable')) { 7166 // Placeholder is in a nested list (inside a folder) 7167 intendedPlacementContainer = relatedParent; 7168 } 7169 7170 // Update ghost element indentation to match the target position 7171 requestAnimationFrame(() => { 7172 const ghost = document.querySelector('.sortable-ghost'); 7173 if (ghost && intendedPlacementContainer) { 7174 const ghostContent = ghost.querySelector('.gallery-item-content'); 7175 if (ghostContent) { 7176 // Calculate the depth where the ghost will be placed 7177 // Use the related element to determine the depth (if it exists and is in the target container) 7178 let targetDepth = 0; 7179 if (intendedPlacementContainer === rootContainer) { 7180 targetDepth = 0; // Root level 7181 } else if (related && related.parentElement === intendedPlacementContainer) { 7182 // Use the related element's depth to determine target depth 7183 targetDepth = computeDepth(related); 7184 } else { 7185 // Fallback: find first child in the container or calculate from parent folder 7186 const firstChild = intendedPlacementContainer.querySelector('li'); 7187 if (firstChild) { 7188 targetDepth = computeDepth(firstChild); 7189 } else { 7190 // Empty container (like empty folder) - calculate from parent folder 7191 const parentFolder = intendedPlacementContainer.closest('li.has-children'); 7192 if (parentFolder) { 7193 targetDepth = computeDepth(parentFolder) + 1; 7194 } 7195 } 7196 } 7197 7198 // Special handling: If intendedPlacementContainer is empty and placeholder isn't in it yet, 7199 // ensure placeholder is moved there and indentation is correct 7200 if (intendedPlacementContainer.children.length === 0) { 7201 const placeholder = document.querySelector('.sortable-ghost'); 7202 if (placeholder && placeholder.parentElement !== intendedPlacementContainer) { 7203 // Move placeholder into empty container 7204 intendedPlacementContainer.appendChild(placeholder); 7205 } 7206 } 7207 ghostContent.style.paddingLeft = (targetDepth * 16) + 'px'; 7208 } 7209 } 7210 }); 7211 } 7212 7213 // Let SortableJS handle the placement based on placeholder position 7214 // We'll verify and correct if needed in onEnd 7215 return true; // Allow the move 7216 }, 7217 7218 // Capture indentation before drag starts (fires when item is chosen) 7219 onChoose: function(evt) { 7220 if (!evt.item) return; 7221 7222 const draggedItem = evt.item; 7223 const content = draggedItem.querySelector('.gallery-item-content'); 7224 7225 // Store the original padding-left for the ghost 7226 if (content) { 7227 const computedStyle = window.getComputedStyle(content); 7228 const originalPaddingLeft = computedStyle.paddingLeft; 7229 draggedItem.dataset.originalPaddingLeft = originalPaddingLeft; 7230 } 7231 }, 7232 7233 // Visual feedback on drag start 7234 onStart: function(evt) { 7235 if (!evt.item) return; 7236 7237 // Don't set dimensions on the dragged item - let it maintain natural size 7238 // Setting width/height can cause layout shifts and menu expansion 7239 const draggedItem = evt.item; 7240 7241 // Store original container and check if it's the last item in folder 7242 draggedItemOriginalContainer = draggedItem.parentElement; 7243 const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); 7244 if (draggedItemOriginalContainer && 7245 draggedItemOriginalContainer.classList.contains('nested-sortable') && 7246 draggedItemOriginalContainer !== rootContainer) { 7247 const siblings = Array.from(draggedItemOriginalContainer.children); 7248 isLastItemInFolder = siblings.length > 0 && siblings[siblings.length - 1] === draggedItem; 7249 } else { 7250 isLastItemInFolder = false;
7251 } 7252 7253 // Apply the stored padding-left to the ghost element 7254 // The ghost is created by SortableJS at this point 7255 requestAnimationFrame(() => { 7256 const ghost = document.querySelector('.sortable-ghost'); 7257 if (ghost) { 7258 const ghostContent = ghost.querySelector('.gallery-item-content'); 7259 const originalPaddingLeft = draggedItem.dataset.originalPaddingLeft; 7260 if (ghostContent && originalPaddingLeft) { 7261 ghostContent.style.paddingLeft = originalPaddingLeft; 7262 } 7263 } 7264 }); 7265 7266 // Reset oscillation tracking variables 7267 lastMoveTime = 0; 7268 lastMoveRelatedId = null; 7269 lastContainerDecision = null; 7270 lastContainerDecisionTime = 0; 7271 7272 // Create a backup 7273 try { 7274 window._dragStartElementIds = []; 7275 document.querySelectorAll('#galleryTree .nested-sortable li').forEach(li => { 7276 if (li.dataset.id) { 7277 window._dragStartElementIds.push(li.dataset.id); 7278 } 7279 }); 7280 7281 galleriesBackup = JSON.parse(JSON.stringify(galleries)); 7282 } catch (error) { 7283 console.error('Error creating backup:', error); 7284 } 7285 7286 // Add indicator classes 7287 document.body.classList.add('menu-item-dragging'); 7288 7289 // Add hover indicator class to submenu items 7290 document.querySelectorAll('.has-children').forEach(item => { 7291 item.classList.add('potential-parent'); 7292 }); 7293 7294 // Prevent layout recalculation during drag 7295 document.body.style.setProperty('--is-dragging', '1'); 7296 }, 7297 7298 // Cleanup on end 7299 onEnd: function(evt) { 7300 // Verify and correct placement based on placeholder's intended position 7301 // SortableJS may auto-nest incorrectly based on mouse position, even if placeholder shows root level 7302 const dragged = evt.item; 7303 const previousParentLi = evt.from ? evt.from.closest('li') : null; 7304 const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); 7305 7306 if (!dragged || !rootContainer) { 7307 intendedPlacementContainer = null; 7308 intendedRelatedElement = null; 7309 // Restore normal sizing 7310 dragged.style.width = ''; 7311 dragged.style.height = ''; 7312 dragged.style.minHeight = ''; 7313 document.body.style.removeProperty('--is-dragging'); 7314 return; 7315 } 7316 7317 // Get the actual parent container where SortableJS placed the item 7318 const actualParentList = dragged.parentElement; 7319 7320 // Restore normal sizing - use requestAnimationFrame to prevent layout thrashing 7321 requestAnimationFrame(() => { 7322 dragged.style.width = ''; 7323 dragged.style.height = ''; 7324 dragged.style.minHeight = ''; 7325 document.body.style.removeProperty('--is-dragging'); 7326 }); 7327 7328 if (intendedPlacementContainer && !document.body.contains(intendedPlacementContainer)) { 7329 intendedPlacementContainer = null; 7330 intendedRelatedElement = null; 7331 } 7332 7333 // Check if the intended placement (where placeholder showed) differs from actual placement 7334 // This happens when SortableJS auto-nests based on mouse position over a folder 7335 if (intendedPlacementContainer && actualParentList !== intendedPlacementContainer) { 7336 // The item ended up in a different container than the placeholder indicated 7337 // Move it to where the placeholder showed it should be (next to intendedRelatedElement) 7338 if (intendedPlacementContainer === rootContainer) { 7339 // Placeholder was at root level - move item to root, positioned next to related element 7340 if (intendedRelatedElement && intendedRelatedElement.parentElement === rootContainer) { 7341 // Insert before the related element (where placeholder was) 7342 rootContainer.insertBefore(dragged, intendedRelatedElement); 7343 console.log("Corrected placement: moved from folder to root level where placeholder indicated"); 7344 } else if (intendedRelatedElement) {
7345 // Related element exists but not at root - find its folder and insert before it at root 7346 const folderItem = intendedRelatedElement.closest('li.has-children'); 7347 if (folderItem && folderItem.parentElement === rootContainer) { 7348 rootContainer.insertBefore(dragged, folderItem); 7349 console.log("Corrected placement: moved to root level above folder where placeholder indicated"); 7350 } else { 7351 rootContainer.appendChild(dragged); 7352 } 7353 } else { 7354 rootContainer.appendChild(dragged); 7355 } 7356 // Update indentation since it's now at root 7357 applyIndentationFromDepth(dragged); 7358 } else { 7359 // Placeholder was in a nested list - move it there 7360 if (intendedRelatedElement && intendedRelatedElement.parentElement === intendedPlacementContainer) { 7361 // Insert before the related element in the nested list 7362 intendedPlacementContainer.insertBefore(dragged, intendedRelatedElement); 7363 } else { 7364 intendedPlacementContainer.appendChild(dragged); 7365 } 7366 console.log("Corrected placement: moved to nested list where placeholder indicated"); 7367 // Update indentation for nested item 7368 applyIndentationFromDepth(dragged); 7369 7370 // Ensure parent folder has proper structure 7371 const folderItem = intendedPlacementContainer.closest('li.has-children'); 7372 if (folderItem) { 7373 const nestedList = folderItem.querySelector('ol.nested-sortable'); 7374 if (nestedList && intendedPlacementContainer !== nestedList) { 7375 nestedList.appendChild(dragged); 7376 } 7377 7378 const parentDepth = computeDepth(folderItem); 7379 const nestedDepth = parentDepth + 1; 7380 7381 if (nestedList) { 7382 nestedList.className = 'nested-sortable nested-level-' + nestedDepth; 7383 } 7384 7385 folderItem.classList.add('has-children'); 7386 } 7387 } 7388 } else if (actualParentList && actualParentList.classList.contains('nested-sortable') && actualParentList !== rootContainer) { 7389 // Item is correctly in a nested list - ensure proper structure 7390 const folderItem = actualParentList.closest('li.has-children'); 7391 if (folderItem) { 7392 const nestedList = folderItem.querySelector('ol.nested-sortable'); 7393 if (nestedList && actualParentList !== nestedList) { 7394 nestedList.appendChild(dragged); 7395 } 7396 7397 const parentDepth = computeDepth(folderItem); 7398 const nestedDepth = parentDepth + 1; 7399 7400 if (nestedList) { 7401 nestedList.className = 'nested-sortable nested-level-' + nestedDepth; 7402 } 7403 7404 folderItem.classList.add('has-children'); 7405 } 7406 applyIndentationFromDepth(dragged); 7407 } else { 7408 // Item is correctly at root level 7409 applyIndentationFromDepth(dragged); 7410 } 7411 7412 // Reset tracking variables 7413 intendedPlacementContainer = null; 7414 intendedRelatedElement = null; 7415 isLastItemInFolder = false; 7416 draggedItemOriginalContainer = null; 7417 lastContainerDecision = null; 7418 lastContainerDecisionTime = 0; 7419 7420 // Apply indentation updates for ALL affected items after drag 7421 // Use requestAnimationFrame to batch DOM updates and prevent visual jumps 7422 requestAnimationFrame(() => { 7423 if (dragged) {
7424 // Update the dragged item and all its descendants 7425 applyIndentationFromDepth(dragged); 7426 7427 // Update all items in the new container (siblings of dragged item) 7428 const newContainer = dragged.parentElement; 7429 if (newContainer && newContainer.classList.contains('nested-sortable')) { 7430 const siblings = Array.from(newContainer.children); 7431 siblings.forEach(sibling => { 7432 if (sibling !== dragged && sibling.tagName === 'LI') { 7433 applyIndentationFromDepth(sibling); 7434 } 7435 }); 7436 } 7437 7438 // Update all items in the old container (if it still exists and has items) 7439 if (evt.from && evt.from.classList.contains('nested-sortable')) { 7440 const oldSiblings = Array.from(evt.from.children); 7441 oldSiblings.forEach(sibling => { 7442 if (sibling.tagName === 'LI') { 7443 applyIndentationFromDepth(sibling); 7444 } 7445 }); 7446 } 7447 7448 // Also update all items in the entire tree to ensure consistency 7449 // This handles edge cases where indentation might be off 7450 const allItems = document.querySelectorAll('#galleryTree .nested-sortable li'); 7451 allItems.forEach(li => { 7452 const depth = computeDepth(li); 7453 li.setAttribute('data-nesting-level', depth); 7454 const content = li.querySelector('.gallery-item-content'); 7455 if (content) { 7456 content.style.paddingLeft = (depth * 16) + 'px'; 7457 } 7458 }); 7459 } 7460 }); 7461 7462 // Clean up previous parent if it lost its last child 7463 if (previousParentLi) { 7464 cleanupParentIfEmpty(previousParentLi); 7465 } 7466 7467 // Remove all indicator classes 7468 document.body.classList.remove('menu-item-dragging'); 7469 document.querySelectorAll('.potential-parent, .will-accept-child, .will-be-nested').forEach(el => { 7470 el.classList.remove('potential-parent', 'will-accept-child', 'will-be-nested'); 7471 }); 7472 7473 // Standard cleanup for ghost elements 7474 document.querySelectorAll('.sortable-ghost, .sortable-chosen, .sortable-drag').forEach(item => { 7475 item.classList.remove('sortable-ghost', 'sortable-chosen', 'sortable-drag'); 7476 }); 7477 7478 // Check if drop was outside a valid container 7479 if (!evt.to || !evt.to.classList.contains('nested-sortable')) { 7480 console.warn('Item was dropped outside a valid container - reverting'); 7481 restoreFromBackup(); 7482 return; 7483 } 7484 7485 // Verify no elements were lost 7486 try { 7487 // Check DOM elements 7488 const endElementIds = []; 7489 document.querySelectorAll('#galleryTree .nested-sortable li').forEach(li => { 7490 if (li.dataset.id) { 7491 endElementIds.push(li.dataset.id); 7492 } 7493 }); 7494 7495 if (window._dragStartElementIds && window._dragStartElementIds.length > 0) { 7496 const startCount = window._dragStartElementIds.length; 7497 const endCount = endElementIds.length; 7498 7499 if (endCount < startCount) { 7500 console.error(`DOM elements were lost during drag! Original: ${startCount}, New: ${endCount}`); 7501 restoreFromBackup(); 7502 return; 7503 } 7504 } 7505 7506 // Update the data structure 7507 updateGalleryStructure(); 7508 7509 // Verify data model 7510 const originalCount = countGalleries(galleriesBackup); 7511 const newCount = countGalleries(galleries); 7512 7513 if (newCount < originalCount) { 7514 console.error(`Items were lost! Original: ${originalCount}, New: ${newCount}`); 7515 restoreFromBackup(); 7516 return; 7517 } 7518 7519 // updateGalleryStructure already persists; do not save or re-render again 7520 // here. A second save/render can rebind Sortable and snap the drop back. 7521 } catch (error) { 7522 console.error('Error updating gallery structure:', error); 7523 restoreFromBackup(); 7524 } 7525 } 7526 }); 7527 7528 sortableInstances.push(sortable); 7529 } catch (error) { 7530 console.error(`Error initializing sortable for container ${i}:`, error); 7531 } 7532 } 7533 7534 // Add event delegation for folder toggles (will be added by setupFolderToggleHandler) 7535 setupFolderToggleHandler(); 7536 7537 return sortableInstances; 7538} 7539 7540// Helper function to set up folder toggle event delegation 7541// This must be called after galleries are rendered, in both view and edit modes 7542function setupFolderToggleHandler() { 7543 const galleryTree = document.getElementById('galleryTree'); 7544 if (galleryTree && !galleryTree.dataset.folderToggleHandlerAdded) { 7545 galleryTree.addEventListener('click', function(e) { 7546 7547 const target = e.target; 7548 // Resolve the clicked row first â do not use target.closest('.menu-item.folder'), 7549 // which matches ancestor folder labels when clicking nested pages/galleries. 7550 const listItem = target.closest('li[data-id]'); 7551 if (!listItem) { 7552 return; 7553 } 7554 7555 const contentDiv = listItem.querySelector(':scope > .gallery-item-content'); 7556 if (!contentDiv) { 7557 return; 7558 } 7559 7560 const toggleButton = contentDiv.querySelector('.submenu-toggle'); 7561 const folderText = contentDiv.querySelector('.menu-item.folder'); 7562 const isToggleClick = toggleButton && toggleButton.contains(target); 7563 const isFolderTextClick = folderText && folderText.contains(target); 7564 7565 7566 if (isToggleClick || isFolderTextClick) { 7567 const folderId = parseInt(listItem.dataset.id, 10); 7568 7569 if (folderId && !isNaN(folderId)) { 7570 // Prevent SortableJS from intercepting this click 7571 e.stopPropagation(); 7572 e.stopImmediatePropagation(); 7573 toggleFolder(folderId, e); 7574 } 7575 } 7576 }, true); // Use capture phase to run before SortableJS 7577 7578 galleryTree.dataset.folderToggleHandlerAdded = 'true'; 7579 } 7580} 7581 7582// NEW HELPER FUNCTION: Gets the nesting level of an element 7583function getElementNestingLevel(element) { 7584 let level = 0; 7585 let current = element; 7586 7587 // Count how many nested-sortable parents this element has 7588 while (current && current.parentElement) { 7589 if (current.parentElement.classList && current.parentElement.classList.contains('nested-sortable')) { 7590 level++; 7591 } 7592 current = current.parentElement; 7593 } 7594 7595 return level; 7596} 7597 7598// NEW HELPER FUNCTION: Determines if an item would become nested 7599function wouldBeNestedItem(draggedEl, relatedEl, event) { 7600 if (!relatedEl || !draggedEl) return false;
7601 7602 // Get mouse position 7603 const mouseX = event.clientX; 7604 7605 // Get the bounding rect of the related element 7606 const rect = relatedEl.getBoundingClientRect(); 7607 7608 // Determine the nesting threshold - how far from the left edge 7609 // the mouse needs to be to consider it a nesting operation 7610 const nestingThreshold = 25; // pixels from left edge 7611 7612 // Check if mouse is within the nesting threshold from the left edge 7613 const distanceFromLeft = mouseX - rect.left; 7614 7615 // Check if we should nest (mouse is within threshold of left edge) 7616 const wouldNest = distanceFromLeft <= nestingThreshold; 7617 7618 // Log the calculation for debugging 7619 console.log(`Nesting check: distance=${distanceFromLeft}px, threshold=${nestingThreshold}px, would nest=${wouldNest}`); 7620 7621 return wouldNest; 7622} 7623 7624/** 7625 * Helper function to find gallery by ID in a tree structure 7626 * @param {Array} galleries - The gallery array to search 7627 * @param {number} id - The ID to find 7628 * @returns {Object|null} - The found gallery or null 7629 */ 7630function findGalleryById(galleries, id) { 7631 if (window.HelpSystem && typeof window.HelpSystem.isDemoId === 'function' && window.HelpSystem.isDemoId(id)) { 7632 return window.HelpSystem.getDemoRecord(id); 7633 } 7634 if (!Array.isArray(galleries)) return null; 7635 7636 // First check at the current level 7637 const directMatch = galleries.find(g => g.id == id); 7638 if (directMatch) return directMatch; 7639 7640 // Then check in children 7641 for (const gallery of galleries) { 7642 if (gallery.children && gallery.children.length > 0) { 7643 const childMatch = findGalleryById(gallery.children, id); 7644 if (childMatch) return childMatch; 7645 } 7646 } 7647 7648 return null; 7649} 7650 7651/** Detect page menu items even when isPage flag was not persisted */ 7652function isPageMenuItem(item) { 7653 if (!item) return false; 7654 if (item.isPage === true) return true; 7655 if (item.pageId && String(item.pageId).indexOf('page_') === 0) return true; 7656 if (Array.isArray(item.pageElements) && item.pageElements.length > 0) return true; 7657 return false; 7658} 7659 7660/** 7661 * Return true only for pages/galleries that explicitly opted into navigable-parent behavior. 7662 * Never infer this from children or parentId; legacy items must retain their existing behavior. 7663 */ 7664function isNavigableMenuParent(item) { 7665 if (!item || item.actsAsMenuParent !== true) return false; 7666 if (item.isFolder === true || item.isSubmenu === true || item.isSpacer === true || item.isExternal === true) { 7667 return false; 7668 } 7669 return isPageMenuItem(item) || item.isIntegrated === true; 7670} 7671 7672/** 7673 * Get all gallery IDs from a tree structure 7674 * @param {Array} galleries - Array of galleries 7675 * @returns {Array} - Flat array of all IDs 7676 */ 7677function getAllGalleryIds(galleries) { 7678 if (!Array.isArray(galleries)) return []; 7679 7680 let ids = []; 7681 7682 galleries.forEach(gallery => { 7683 if (gallery && gallery.id) { 7684 ids.push(gallery.id); 7685 } 7686 7687 // Add child IDs if any 7688 if (gallery.children && Array.isArray(gallery.children)) { 7689 ids = ids.concat(getAllGalleryIds(gallery.children)); 7690 } 7691 }); 7692 7693 return ids; 7694} 7695 7696/** 7697 * Enhanced count function that handles edge cases 7698 * @param {Array} galleryArray - The array to count 7699 * @returns {number} - The total count of galleries 7700 */ 7701function countGalleries(galleryArray) { 7702 if (!Array.isArray(galleryArray)) return 0; 7703 7704 // Menu items are stored flat and also get a derived children[] at render time. 7705 // Counting children recursively double-counts those items and falsely triggers 7706 // restoreFromBackup after a successful drop. 7707 const ids = new Set(); 7708 function walk(items) { 7709 if (!Array.isArray(items)) return; 7710 items.forEach(function(gallery) { 7711 if (!gallery) return; 7712 if (gallery.id != null) ids.add(gallery.id); 7713 if (gallery.children && Array.isArray(gallery.children)) walk(gallery.children); 7714 }); 7715 } 7716 walk(galleryArray); 7717 return ids.size; 7718} 7719 7720/** 7721 * Enhanced backup restoration function with better debugging and notification 7722 */ 7723function restoreFromBackup() { 7724 console.warn('Restoring galleries from backup due to invalid operation'); 7725 7726 // Verify we have a valid backup 7727 if (!galleriesBackup || !Array.isArray(galleriesBackup) || galleriesBackup.length === 0) { 7728 console.error('No valid backup available for restoration'); 7729 7730 // Create a backup from the current DOM as a last resort 7731 try { 7732 console.log('Attempting to rebuild from DOM structure...'); 7733 rebuildGalleriesFromDOM(); 7734 return; 7735 } catch (rebuildError) { 7736 console.error('Failed to rebuild from DOM:', rebuildError); 7737 // Continue with normal user notification 7738 } 7739 } else { 7740 // Restore from backup 7741 galleries = JSON.parse(JSON.stringify(galleriesBackup)); 7742 } 7743 7744 // Re-render the tree with the restored data 7745 renderGalleries(); 7746 7747 // Force reinitialize nested sortables after restoration 7748 setTimeout(() => { 7749 initializeNestedSortables(); 7750 }, 300); 7751} 7752 7753/** 7754 * Emergency recovery function that tries to rebuild galleries from DOM 7755 * This is a last resort when backup restoration fails 7756 */ 7757function rebuildGalleriesFromDOM() { 7758 console.log('EMERGENCY RECOVERY: Attempting to rebuild galleries from DOM structure'); 7759 7760 // Create a new galleries array 7761 const rebuiltGalleries = []; 7762 7763 // This will track items we've already processed 7764 const processedIds = new Set(); 7765 7766 // Function to process a container and its children 7767 function processContainer(container, parentId = null) { 7768 // Get all immediate list items 7769 const items = container.querySelectorAll(':scope > li'); 7770
7771 items.forEach(item => { 7772 // Get item ID from data attribute 7773 const id = parseInt(item.dataset.id); 7774 if (isNaN(id)) return; // Skip if no valid ID 7775 7776 // Skip already processed items to avoid duplicates 7777 if (processedIds.has(id)) return; 7778 processedIds.add(id); 7779 7780 // Try to find the original gallery data 7781 let galleryData = null; 7782 7783 // First check current galleries array 7784 const existingGallery = findGalleryById(galleries, id); 7785 if (existingGallery) { 7786 galleryData = { ...existingGallery }; 7787 delete galleryData.children; // We'll rebuild the hierarchy 7788 } 7789 // If not found, check if we have a backup 7790 else if (galleriesBackup && Array.isArray(galleriesBackup)) { 7791 const backupGallery = findGalleryById(galleriesBackup, id); 7792 if (backupGallery) { 7793 galleryData = { ...backupGallery }; 7794 delete galleryData.children; // We'll rebuild the hierarchy 7795 } 7796 } 7797 7798 // If we still don't have data, create minimal placeholder 7799 if (!galleryData) { 7800 // Try to extract title from DOM 7801 const titleElement = item.querySelector('.menu-item'); 7802 const title = titleElement ? titleElement.textContent.trim() : `Item ${id}`; 7803 7804 galleryData = { 7805 id: id, 7806 title: title, 7807 visible: !item.classList.contains('hidden-gallery') 7808 }; 7809 } 7810 7811 // Update parent ID to match DOM structure 7812 galleryData.parentId = parentId; 7813 7814 // Add to rebuilt galleries 7815 rebuiltGalleries.push(galleryData); 7816 7817 // Process children if any 7818 const childContainer = item.querySelector('ol.nested-sortable'); 7819 if (childContainer) { 7820 processContainer(childContainer, id); 7821 } 7822 }); 7823 } 7824 7825 // Start with root container 7826 const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); 7827 if (rootContainer) { 7828 processContainer(rootContainer); 7829 7830 // Replace global galleries array with our rebuilt one 7831 galleries = rebuiltGalleries; 7832 7833 // Re-render with the rebuilt data 7834 renderGalleries(); 7835 7836 console.log(`RECOVERY COMPLETE: Rebuilt ${rebuiltGalleries.length} items from DOM`); 7837 return true; 7838 } else { 7839 console.error('RECOVERY FAILED: Root container not found'); 7840 return false; 7841 } 7842} 7843 7844// Make sure these functions are available to the global scope 7845window.initializeNestedSortables = initializeNestedSortables; 7846window.restoreFromBackup = restoreFromBackup; 7847window.getElementNestingLevel = getElementNestingLevel; 7848window.wouldBeNestedItem = wouldBeNestedItem; 7849 7850 7851 7852function hasDescendant(itemId, possibleDescendantId) { 7853// Check if possibleDescendantId is a descendant of itemId 7854const children = galleries.filter(g => g.parentId === itemId); 7855 7856if (children.some(child => child.id === possibleDescendantId)) { 7857return true; 7858} 7859 7860return children.some(child => hasDescendant(child.id, possibleDescendantId)); 7861} 7862 7863function updateGalleryStructure() { 7864// Create a new array to hold the updated gallery structure 7865const newGalleries = []; 7866 7867// Function to recursively process nested lists 7868function processNestedList(container, parentId = null) { 7869if (!container || !container.children) { 7870 console.warn('Invalid container in processNestedList', container); 7871 return; 7872} 7873 7874const items = container.children; 7875 7876Array.from(items).forEach(item => { 7877 if (item.tagName !== 'LI') return; // Skip non-list items 7878 7879 const id = parseInt(item.dataset.id); 7880 if (isNaN(id)) { 7881 console.warn('Invalid ID in list item:', item); 7882 return; 7883 } 7884 7885 const gallery = galleries.find(g => g.id === id); 7886 7887 if (gallery) { 7888 // Create a new gallery object without children 7889 const newGallery = { ...gallery }; 7890 if (newGallery.children) delete newGallery.children; 7891 7892 // Update parent ID 7893 newGallery.parentId = parentId; 7894 7895 // Add to new galleries array 7896 newGalleries.push(newGallery); 7897 7898 // Process any nested OL with class 'nested-sortable' within this item 7899 const nestedList = item.querySelector('ol.nested-sortable'); 7900 if (nestedList) { 7901 processNestedList(nestedList, id); 7902 } 7903 } else { 7904 console.warn(`Gallery with ID ${id} not found in galleries array`); 7905 } 7906}); 7907} 7908 7909// Start processing from the root list 7910const rootList = document.querySelector('#galleryTree > ol.nested-sortable'); 7911if (rootList) { 7912try { 7913 processNestedList(rootList); 7914 7915 // Verify that we haven't lost any galleries 7916 if (newGalleries.length < galleries.length) { 7917 console.error(`Gallery count mismatch! Original: ${galleries.length}, New: ${newGalleries.length}`); 7918 7919 // Find missing galleries 7920 const originalIds = galleries.map(g => g.id); 7921 const newIds = newGalleries.map(g => g.id); 7922 const missingIds = originalIds.filter(id => !newIds.includes(id)); 7923 7924 if (missingIds.length > 0) { 7925 console.error('Missing gallery IDs:', missingIds); 7926 7927 // Add missing galleries to the new structure 7928 missingIds.forEach(id => { 7929 const missingGallery = galleries.find(g => g.id === id); 7930 if (missingGallery) { 7931 // Add it as a top-level item 7932 const newGallery = { ...missingGallery, parentId: null }; 7933 if (newGallery.children) delete newGallery.children; 7934 newGalleries.push(newGallery); 7935 console.log(`Rescued gallery: ${newGallery.title} (ID: ${newGallery.id})`); 7936 } 7937 }); 7938 } 7939 } 7940 7941 // Update the galleries array with the new structure 7942 galleries = newGalleries; 7943 if (typeof window !== 'undefined') { 7944 window.galleries = galleries; 7945 } 7946 7947 // Save the updated structure 7948 saveGalleries(); 7949} catch (error) { 7950 console.error('Error in updateGalleryStructure:', error); 7951 throw error; // Re-throw to trigger the backup restore 7952} 7953} else { 7954console.warn('Root list not found'); 7955throw new Error('Root list not found'); // Throw error to trigger backup restore 7956} 7957} 7958 7959function toggleSubmenu(id, event) { 7960// Ensure the event doesn't interfere with drag operations 7961event.stopPropagation(); 7962 7963const listItem = getGalleryTreeListItem(id, event); 7964if (listItem) { 7965// Toggle the expanded class 7966listItem.classList.toggle('expanded'); 7967 7968// Find the submenu toggle icon and rotate it 7969const toggleIcon = listItem.querySelector('.toggle-icon'); 7970if (toggleIcon) { 7971 toggleIcon.classList.toggle('rotated'); 7972} 7973} 7974} 7975 7976/** 7977 * Improved slugify function with robust character handling 7978 * @param {string} text - The text to convert to a slug 7979 * @returns {string} A URL-friendly slug 7980 */ 7981function slugify(text) { 7982 // Guard against null or empty input 7983 if (!text || text.trim() === '') { 7984 return 'page-' + Date.now(); 7985 } 7986 7987 // First convert to lowercase 7988 let slug = text.toLowerCase(); 7989 7990 // Replace spaces with hyphens 7991 slug = slug.split(' ').join('-'); 7992 7993 // Filter out unwanted characters (keep only a-z, 0-9, and hyphens) 7994 let filtered = ''; 7995 for (let i = 0; i < slug.length; i++) { 7996 const char = slug.charAt(i); 7997 if ((char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char === '-') { 7998 filtered += char; 7999 } 8000 } 8001 slug = filtered; 8002 8003 /* 8004 // Clean up multiple hyphens 8005 while (slug.indexOf('--') !== -1) { 8006 slug = slug.replace('--', '-'); 8007 } 8008 */ 8009 8010 // Remove leading and trailing hyphens 8011 if (slug.startsWith('-')) { 8012 slug = slug.substring(1); 8013 } 8014 if (slug.endsWith('-')) { 8015 slug = slug.substring(0, slug.length - 1); 8016 } 8017 8018 // If slug is empty after processing, use a default 8019 if (!slug || slug === '-') { 8020 return 'page-' + Date.now(); 8021 } 8022 8023 return slug; 8024} 8025 8026// Update the addPage function to use our new robust slugify function 8027/**
8028 * Adds a new page to the galleries array 8029 * @returns {number|null} The new page ID or null if error 8030 */ 8031function addPage() { 8032 try { 8033 // Get the page title 8034 const titleInput = document.getElementById('galleryTitle'); 8035 if (!titleInput) { 8036 console.error('Page title input not found'); 8037 return; 8038 } 8039 const title = titleInput.value.trim(); 8040 8041 // Validate title is not empty 8042 if (!title) { 8043 alert('Please enter a page title'); 8044 return; 8045 } 8046 8047 // Get parent ID with error handling 8048 const parentSelect = document.getElementById('galleryParent'); 8049 const parentId = parentSelect && parentSelect.value ? parseInt(parentSelect.value) : null; 8050 8051 // Generate a unique ID for the page 8052 const id = Date.now(); 8053 const pageId = `page_${id}`; 8054 8055 // Calculate slug using our more robust slugify function 8056 const pageSlug = slugify(title); 8057 8058 // Create the page object with explicit properties 8059 const page = { 8060 id: id, 8061 title: title, 8062 isPage: true, 8063 isIntegrated: true, 8064 isSubmenu: false, 8065 pageId: pageId, 8066 visible: true, 8067 parentId: parentId, 8068 siteId: siteId, 8069 // Set slug and URL explicitly 8070 slug: pageSlug, 8071 url: '/' + pageSlug 8072 }; 8073 8074 console.log(`Creating new page with ID: ${id}, title: "${title}"`); 8075 8076 // Add to galleries array 8077 galleries.push(page); 8078 8079 // CRITICAL: Ensure window.galleries is synchronized 8080 if (typeof window.galleries === 'undefined') { 8081 window.galleries = galleries; 8082 } else { 8083 // Make sure the page is in window.galleries too 8084 const existingIndex = window.galleries.findIndex(g => g.id === id); 8085 if (existingIndex >= 0) { 8086 window.galleries[existingIndex] = page; 8087 } else { 8088 window.galleries.push(page); 8089 } 8090 } 8091 8092 // Initialize empty page elements array in PageManager 8093 if (window.PageManager) { 8094 window.PageManager.elements[pageId] = []; 8095 } 8096 8097 // Update UI first 8098 renderGalleries(); 8099 clearAddForm(); 8100 toggleAddForm(); 8101 8102 // Set as active page 8103 console.log(`Setting new page "${page.title}" as active`); 8104 activeGalleryId = id; 8105 window.activeGalleryId = id; 8106 8107 // Update active states in the menu 8108 updateActiveStates(); 8109 8110 // Save to server 8111 saveGalleries().then(() => { 8112 console.log(`Page "${page.title}" saved successfully`); 8113 8114 // Double check that our gallery is still in the array 8115 const pageExists = galleries.some(g => g.id === id); 8116 console.log(`Page exists in galleries array: ${pageExists}`); 8117 8118 if (pageExists) { 8119 // Load the new page after a slight delay to ensure DOM is updated 8120 setTimeout(() => { 8121 console.log(`Loading new page "${page.title}" with ID: ${id}`); 8122 loadPage(id); 8123 }, 50); 8124 } else { 8125 console.error(`Page with ID ${id} not found in galleries array after save`); 8126 } 8127 }).catch(error => { 8128 alert('Error saving page. Please try again.'); 8129 }); 8130 8131 return id; 8132 } catch (error) { 8133 console.error('Error adding page:', error); 8134 alert('Error adding page. Please try again.'); 8135 return null; 8136 } 8137} 8138 8139/** 8140 * Ensures a slug is unique among existing galleries 8141 * @param {string} slug - The base slug to check 8142 * @param {Array} existingGalleries - Array of existing gallery configs 8143 * @returns {string} A unique slug 8144 */ 8145function ensureUniqueSlug(slug, existingGalleries) { 8146 // Guard against invalid slugs 8147 if (!slug || slug === '-') { 8148 slug = 'page-' + Date.now(); 8149 } 8150 8151 let finalSlug = slug; 8152 let counter = 1; 8153 8154 // Check if slug already exists 8155 while (existingGalleries.some(g => g.slug === finalSlug)) { 8156 finalSlug = `${slug}-${counter}`; 8157 counter++; 8158 } 8159 8160 return finalSlug; 8161} 8162 8163/** 8164 * Adds a new gallery based on the selected type 8165 * @returns {number|null} The new gallery ID or null if error 8166 */ 8167function addGallery() { 8168 try { 8169 // Get the gallery title 8170 const titleInput = document.getElementById('galleryTitle'); 8171 if (!titleInput) { 8172 console.error('Gallery title input not found'); 8173 return; 8174 } 8175 const title = titleInput.value.trim(); 8176 8177 // Get the selected gallery type 8178 const galleryTypeRadio = document.querySelector('input[name="galleryType"]:checked'); 8179 if (!galleryTypeRadio) { 8180 console.error('No gallery type selected'); 8181 alert('Please select a gallery type'); 8182 return; 8183 } 8184 8185 const galleryType = galleryTypeRadio.value; 8186 console.log("Selected gallery type:", galleryType); 8187 8188 // If this is a page, use the addPage function 8189 if (galleryType === 'page') { 8190 console.log("Creating page instead of gallery"); 8191 return addPage(); 8192 } 8193 8194 // For spacer type, we don't require a title 8195 if (galleryType !== 'spacer' && !title) { 8196 alert('Please enter a gallery title'); 8197 return; 8198 } 8199 8200 // Continue with gallery creation logic 8201 // Generate unique ID 8202 const id = Date.now(); 8203 8204 // Get parent gallery if selected 8205 const parentSelect = document.getElementById('galleryParent'); 8206 const parentId = parentSelect.value ? parseInt(parentSelect.value) : null; 8207 8208 // Get URL if this is an external gallery 8209 const galleryUrl = document.getElementById('galleryUrl'); 8210 const url = galleryTypeRadio.value === 'external' && galleryUrl 8211 ? galleryUrl.value.trim() 8212 : ""; 8213 8214 // Set basic properties 8215 const gallery = { 8216 id: id, 8217 title: title || "Spacer", // Use "Spacer" as internal title if no title provided 8218 parentId: parentId, 8219 visible: true 8220 }; 8221 8222 // Set type-specific properties 8223 if (galleryTypeRadio.value === 'spacer') { 8224 // Spacer properties 8225 gallery.isSpacer = true; 8226 gallery.title = title || "Spacer"; // For internal reference 8227 } else if (galleryTypeRadio.value === 'integrated') { 8228 // Generate pageId for integrated galleries 8229 gallery.isIntegrated = true; 8230 gallery.pageId = generatePageId(); 8231 8232 // Generate and ensure unique slug 8233 const baseSlug = slugify(title); 8234 gallery.slug = ensureUniqueSlug(baseSlug, galleries); 8235 gallery.url = "/" + gallery.slug;
8236 8237 // Get the siteId from multiple possible sources 8238 const siteId = window.Parameters?.siteId || 8239 window.siteId || 8240 document.querySelector('meta[name="hydra-site-id"]')?.getAttribute('content') || 8241 ''; 8242 8243 // Add galleryOptions with GUID=siteId as the manualCollectionName 8244 gallery.galleryOptions = { 8245 manualCollectionName: `GUID=${siteId}`, 8246 isIntegratedGallery: true, 8247 pageId: gallery.pageId, // Store pageId for reference 8248 siteId: siteId 8249 }; 8250 } else if (galleryTypeRadio.value === 'folder') { 8251 gallery.isFolder = true; 8252 gallery.isCollapsed = true; // Make folders closed by default 8253 } else if (galleryTypeRadio.value === 'external') { 8254 // External URL 8255 gallery.url = url; 8256 gallery.isExternal = true; // Add this flag to identify external URLs 8257 } 8258 8259 console.log(`Creating new gallery with ID: ${id}, title: "${title}"`); 8260 8261 // Add to galleries array 8262 galleries.push(gallery); 8263 8264 // CRITICAL: Ensure window.galleries is synchronized 8265 if (typeof window.galleries === 'undefined') { 8266 window.galleries = galleries; 8267 } else { 8268 // Make sure the gallery is in window.galleries too 8269 const existingIndex = window.galleries.findIndex(g => g.id === id); 8270 if (existingIndex >= 0) { 8271 window.galleries[existingIndex] = gallery; 8272 } else { 8273 window.galleries.push(gallery); 8274 } 8275 } 8276 8277 // Update UI first 8278 renderGalleries(); 8279 clearAddForm(); 8280 toggleAddForm(); 8281 8282 // Save galleries after UI update 8283 saveGalleries().then(() => { 8284 console.log(`Gallery "${gallery.title}" saved successfully`); 8285 8286 // Double check that our gallery is still in the array 8287 const galleryExists = galleries.some(g => g.id === id); 8288 console.log(`Gallery exists in galleries array: ${galleryExists}`); 8289 8290 // For non-folder types, load the new gallery content after a slight delay 8291 if (!gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && gallery.visible !== false) { 8292 setTimeout(() => { 8293 console.log(`Loading new gallery "${gallery.title}" with ID: ${id}`); 8294 if (gallery.isIntegrated) { 8295 loadGallery(id); 8296 } else if (gallery.isExternal && gallery.url) { 8297 // For external URLs, set the iframe src and mark active after navigation target is ready 8298 activeGalleryId = id; 8299 window.activeGalleryId = id; 8300 updateActiveStates(); 8301 const frame = document.getElementById('galleryFrame'); 8302 if (frame) frame.src = gallery.url; 8303 } 8304 }, 50); 8305 } 8306 }).catch(error => { 8307 console.error(`Error saving gallery "${gallery.title}":`, error); 8308 alert('Error saving gallery. Please try again.'); 8309 }); 8310 8311 // Return the new gallery ID 8312 return id; 8313 } catch (error) { 8314 console.error('Error adding gallery:', error); 8315 alert('Error adding gallery. Please try again.'); 8316 } 8317} 8318 8319function debugDOMState(message) { 8320console.log(`[DEBUG] ${message}`); 8321const galleryContainer = document.querySelector('.gallery-container'); 8322const iframe = document.getElementById('galleryFrame'); 8323const pageContainers = document.querySelectorAll('.page-container'); 8324 8325console.log(`- Gallery container: ${galleryContainer ? 'found' : 'not found'}`); 8326console.log(`- iframe: ${iframe ? 'found' : 'not found'}, display: ${iframe?.style.display}`); 8327console.log(`- Page containers: ${pageContainers.length} found`); 8328 8329pageContainers.forEach((container, index) => { 8330console.log(` - Container ${index}: display: ${container.style.display}, visibility: ${container.style.visibility}`); 8331}); 8332} 8333 8334 8335/** 8336* Global helper: whether edit tools are explicitly toggled open. 8337* Do not use Parameters.isInEditor â that is also true for logged-in admins (cache bypass). 8338* Do not infer from sidebar/page .editing alone (can desync from the toggle). 8339*/ 8340window.isInEditMode = function() { 8341 if (document.body.classList.contains('edit-mode-active')) { 8342 return true; 8343 } 8344 if (typeof window.isEditing === 'boolean') { 8345 return window.isEditing; 8346 } 8347 return false;
8348}; 8349 8350/** 8351* This function synchronizes edit mode state across all components 8352*/ 8353window.updateGlobalEditState = function(isEditing) { 8354console.log('Updating global edit state:', isEditing); 8355 8356// Update global variable 8357window.isEditing = isEditing; 8358 8359// Update Parameters object if it exists 8360if (window.Parameters) { 8361window.Parameters.isInEditor = isEditing; 8362} 8363 8364// Update sidebar class 8365const sidebar = document.querySelector('.sidebar'); 8366if (sidebar) { 8367if (isEditing) { 8368 sidebar.classList.add('editing'); 8369} else { 8370 sidebar.classList.remove('editing'); 8371} 8372} 8373 8374// Update page containers 8375const pageContainers = document.querySelectorAll('.page-container'); 8376pageContainers.forEach(container => { 8377if (isEditing) { 8378 container.classList.add('editing'); 8379} else { 8380 container.classList.remove('editing'); 8381} 8382}); 8383 8384// Notify all components via event 8385document.dispatchEvent(new CustomEvent('edit-mode-changed', { 8386detail: { editing: isEditing } 8387})); 8388 8389if (document.body.classList.contains('menu-layout-horizontal') && typeof window.syncContentMarginToHorizontalHeader === 'function') { 8390 window.syncContentMarginToHorizontalHeader(); 8391} 8392 8393if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 8394 window.syncGalleryOptionsButtonVisibility(); 8395} 8396 8397// Gallery Options panel styling depends on body.edit-mode-active â hide it when leaving edit mode 8398if (!isEditing && typeof window.closeOptionsPanel === 'function') { 8399 window.closeOptionsPanel(); 8400} 8401}; 8402 8403/** Show or hide gallery options (#editorButton) based on edit mode and active content type */ 8404window.syncGalleryOptionsButtonVisibility = function() { 8405 const btn = document.getElementById('editorButton'); 8406 if (!btn) return; 8407 8408 const inEditMode = document.body.classList.contains('edit-mode-active'); 8409 // When content type is explicitly gallery, ignore leftover .page-container nodes 8410 // (common when Help sandbox switches from a page to Example Gallery). 8411 const onPageView = window._activeContentType === 'gallery' 8412 ? false 8413 : (window._activeContentType === 'page' || 8414 !!document.querySelector('.gallery-container .page-container')); 8415 let isActiveGallery = false; 8416 const activeId = window.activeGalleryId; 8417 const list = window.galleries || (typeof galleries !== 'undefined' ? galleries : null); 8418 const activeItem = activeId != null && list && typeof findGalleryById === 'function' 8419 ? findGalleryById(list, activeId) 8420 : (Array.isArray(list) ? list.find(function(g) { return g && g.id === activeId; }) : null); 8421 if (activeId != null && inEditMode && !onPageView) { 8422 isActiveGallery = !!(activeItem && !isPageMenuItem(activeItem) && !activeItem.isFolder && !activeItem.isSpacer && !activeItem.isExternal); 8423 } 8424 8425 const shouldShow = inEditMode && !onPageView && isActiveGallery; 8426 if (shouldShow) { 8427 if (window.Parameters) { 8428 window.Parameters.isInEditor = true; 8429 } 8430 btn.classList.remove('gallery-options-force-hidden'); 8431 btn.style.removeProperty('display'); 8432 btn.style.setProperty('display', 'flex', 'important'); 8433 btn.style.pointerEvents = 'auto'; 8434 } else { 8435 btn.classList.add('gallery-options-force-hidden'); 8436 btn.style.setProperty('display', 'none', 'important'); 8437 btn.style.pointerEvents = ''; 8438 } 8439}; 8440 8441document.addEventListener('gallery-loaded', function() { 8442 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 8443 window.syncGalleryOptionsButtonVisibility(); 8444 } 8445}); 8446 8447document.addEventListener('page-loaded', function() { 8448 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 8449 window.syncGalleryOptionsButtonVisibility(); 8450 } 8451}); 8452 8453/** 8454 * Wait for PageManager after CDNâproxy late script load (ISP/Firefox clients). 8455 * Home/page bootstrap can run before the proxy copy of page-manager-v260720-021.js executes. 8456 */ 8457function whenPageManagerReady(callback, options) { 8458 const maxAttempts = (options && options.maxAttempts) || 50; 8459 const intervalMs = (options && options.intervalMs) || 100; 8460 let attempts = 0; 8461 const run = function() { 8462 if (window.PageManager && typeof window.PageManager.loadPage === 'function') { 8463 callback(window.PageManager); 8464 return; 8465 } 8466 attempts += 1; 8467 if (attempts >= maxAttempts) { 8468 console.error('â [loadPage] Timed out waiting for PageManager after', attempts, 'attempts');
8469 if (options && typeof options.onTimeout === 'function') options.onTimeout(); 8470 return; 8471 } 8472 if (attempts === 1 || attempts % 10 === 0) { 8473 console.warn('â³ [loadPage] Waiting for PageManager (CDN proxy late load?) attempt', attempts + '/' + maxAttempts); 8474 } 8475 setTimeout(run, intervalMs); 8476 }; 8477 run(); 8478} 8479 8480/** 8481 * loadPage function that safely handles undefined galleries 8482 * @param {number} id - The gallery ID 8483 * @param {Event} event - Optional event object 8484 */ 8485function loadPage(id, event){ 8486 if (event && event.target && event.target.isConnected === false) { 8487 console.log('loadPage: Ignoring navigation from detached element'); 8488 return; 8489 } 8490 if (event) { 8491 event.preventDefault(); 8492 event.stopPropagation(); 8493 const now = Date.now(); 8494 const lastCallTime = window._lastPageLoadTime || 0; 8495 window._lastPageLoadTime = now; 8496 if (now - lastCallTime < 100) { 8497 console.log('Ignoring duplicate loadPage call'); 8498 return; 8499 } 8500 } 8501 console.log('loadPage: Loading page with gallery ID:', id); 8502 8503 stopAutoAdvanceTimer(); 8504 if (typeof dismissGalleryPasswordOverlay === 'function') { 8505 dismissGalleryPasswordOverlay(); 8506 } 8507 8508 let galleriesData = window.galleries || galleries; 8509 const gallery = findGalleryById(galleriesData, id); 8510 8511 if (!gallery) { 8512 console.warn('No gallery found with ID:', id, 'for page load.'); 8513 return; 8514 } 8515 8516 const contentLoadGeneration = beginHydraContentLoad('page', gallery.id, gallery.pageId || `page_${id}`); 8517 8518 window.activeGalleryId = id; 8519 activeGalleryId = id; 8520 window._activeContentType = 'page'; 8521 if (window.EnhancedGallerySystem) { 8522 window.EnhancedGallerySystem.currentGalleryId = null; 8523 } 8524 8525 if (typeof window.removeGalleryScriptsWithPause === 'function') { 8526 window.removeGalleryScriptsWithPause(); 8527 } 8528 8529 const galleryContainer = document.querySelector('.gallery-container'); 8530 if (galleryContainer) { 8531 galleryContainer.innerHTML = ''; 8532 } 8533 8534 if (!gallery.pageId) gallery.pageId = `page_${id}`; 8535 if (!gallery.isPage) gallery.isPage = true; 8536 8537 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 8538 window.syncGalleryOptionsButtonVisibility(); 8539 } 8540 8541 if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {}; 8542 window._pageIdToGalleryId[gallery.pageId] = id; 8543 8544 if (gallery.pageElements && Array.isArray(gallery.pageElements)) { 8545 if (window.PageManager && window.PageManager.elements) { 8546 window.PageManager.elements[gallery.pageId] = [...gallery.pageElements]; 8547 } 8548 } else if (window.PageManager && window.PageManager.elements && 8549 window.PageManager.elements[gallery.pageId] && 8550 window.PageManager.elements[gallery.pageId].length > 0) { 8551 gallery.pageElements = [...window.PageManager.elements[gallery.pageId]]; 8552 } 8553 8554 if (window.PageManager && typeof window.PageManager.loadPage === 'function') { 8555 try { 8556 window.PageManager.loadPage(gallery.pageId); 8557 const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false; 8558 if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') { 8559 window.PageManager.setEditMode(isCurrentlyEditing); 8560 } 8561 } catch (error) { 8562 console.error('Error loading page with PageManager:', error); 8563 } 8564 } else { 8565 console.error('PageManager not found yet â waiting for CDN proxy script'); 8566 whenPageManagerReady(function() { 8567 try { 8568 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) { 8569 console.log('loadPage: Skipping stale PageManager retry for', gallery.pageId); 8570 return; 8571 } 8572 if (gallery.pageElements && Array.isArray(gallery.pageElements) && window.PageManager.elements) { 8573 window.PageManager.elements[gallery.pageId] = [...gallery.pageElements]; 8574 } 8575 window.PageManager.loadPage(gallery.pageId); 8576 console.log('loadPage: PageManager became available â page render retried'); 8577 } catch (error) { 8578 console.error('Error loading page with PageManager after wait:', error); 8579 } 8580 }); 8581 } 8582 8583 // Apply menu visibility 8584 const bodyEl = document.body; 8585 if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 8586 bodyEl.classList.add('menu-hidden-on-page'); 8587 } else { 8588 bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden 8589 } 8590 applyMenuOverlayClassForGallery(gallery); 8591 8592 if (typeof updateActiveStates === 'function') updateActiveStates(); 8593 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 8594 if (typeof closeMobileMenu === 'function') closeMobileMenu(); 8595 // Close the gallery options panel when navigating to a different page 8596 if (typeof window.closeOptionsPanel === 'function') { 8597 window.closeOptionsPanel(); 8598 } 8599 if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery); 8600 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 8601 window.syncGalleryOptionsButtonVisibility(); 8602 } 8603 if (typeof window !== 'undefined' && typeof document !== 'undefined') { 8604 window.dispatchEvent(new CustomEvent('page-loaded', { 8605 detail: { pageId: gallery.pageId, galleryId: id } 8606 })); 8607 } 8608} 8609 8610 8611 8612 8613 8614function generatePageId() { 8615const characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; 8616let result = ''; 8617const charactersLength = characters.length; 8618for (let i = 0; i < 8; i++) { 8619 result += characters.charAt(Math.floor(Math.random() * charactersLength)); 8620} 8621return result; 8622}; 8623 8624 8625function normalizeNameForComparison(name) { 8626if (!name) {
8627console.log("Empty name passed to normalization"); 8628return ''; 8629} 8630 8631console.log(`Normalizing name: "${name}"`); 8632 8633// Step 1: Convert to lowercase 8634let normalized = name.toLowerCase(); 8635console.log(`After lowercase: "${normalized}"`); 8636 8637// Step 2: Replace spaces with hyphens (without regex) 8638normalized = normalized.split(' ').join('-'); 8639console.log(`After space replacement: "${normalized}"`); 8640 8641// Step 3: Filter out unwanted characters (without regex) 8642let filtered = ''; 8643for (let i = 0; i < normalized.length; i++) { 8644const char = normalized.charAt(i); 8645// Keep only a-z, 0-9, and hyphens 8646if ((char >= 'a' && char <= 'z') || 8647 (char >= '0' && char <= '9') || 8648 char === '-') { 8649 filtered += char; 8650} 8651} 8652normalized = filtered; 8653console.log(`After character filtering: "${normalized}"`); 8654 8655// Step 4: Clean up multiple hyphens (without regex) 8656while (normalized.indexOf('--') !== -1) { 8657normalized = normalized.split('--').join('-'); 8658} 8659 8660// Step 5: Remove leading and trailing hyphens (without regex) 8661if (normalized.charAt(0) === '-') { 8662normalized = normalized.substring(1); 8663} 8664if (normalized.charAt(normalized.length - 1) === '-') { 8665normalized = normalized.substring(0, normalized.length - 1); 8666} 8667 8668// Check if the result is valid 8669if (!normalized || normalized === '-') { 8670console.log(`WARNING: Normalization produced invalid result: "${normalized}"`); 8671} else { 8672console.log(`Final normalized slug: "${normalized}"`); 8673} 8674 8675return normalized; 8676} 8677 8678function ensureUniqueSlug(slug, existingGalleries) { 8679if (!slug) return 'gallery'; 8680 8681let finalSlug = slug; 8682let counter = 1; 8683 8684// Check if slug exists in any gallery 8685const slugExists = function(s) { 8686for (let i = 0; i < existingGalleries.length; i++) { 8687 if (existingGalleries[i].slug === s) { 8688 return true; 8689 } 8690} 8691return false; 8692}; 8693 8694// Keep incrementing counter until unique 8695while (slugExists(finalSlug)) { 8696finalSlug = slug + '-' + counter; 8697counter++; 8698} 8699 8700return finalSlug; 8701} 8702 8703/** 8704 * Gathers the current complete site configuration from the client-side state 8705 * and triggers a download of the data as a single JSON file. 8706 * This provides a simple way to back up or export the current site structure. 8707 */ 8708function downloadCurrentSiteJSON() { 8709 try { 8710 console.log('Gathering current site data for download...'); 8711 8712 // Get the current hostname to use in the filename. 8713 const hostname = window.location.hostname.replace('www.', ''); 8714 8715 // --- FIX: Ensure we get the most up-to-date menu styles --- 8716 // The MenuStyleCustomizer.settings object is the most reliable source 8717 // for the current styles being used on the client. 8718 const currentMenuStyles = window.MenuStyleCustomizer?.settings || {}; 8719 8720 // Get current galleries 8721 const currentGalleries = window.galleries || galleries || []; 8722 8723 // --- STREAMLINED: Process galleries without including image data --- 8724 const enhancedGalleries = currentGalleries.map(gallery => { 8725 // If this is a Classic collection gallery, add note about manual retrieval 8726 if (gallery.galleryOptions?.manualCollectionName?.startsWith('GUID=')) { 8727 const classicGuid = gallery.galleryOptions.manualCollectionName.substring(5); 8728 console.log(`Processing Classic collection gallery: ${gallery.title} (GUID: ${classicGuid})`); 8729 8730 return { 8731 ...gallery, 8732 _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`, 8733 _classicGuid: classicGuid, 8734 _requiresClassicData: true 8735 }; 8736 } 8737 8738 return gallery; 8739 }); 8740 8741 // Assemble the complete site configuration object from global state variables. 8742 // This structure mirrors what the `save-config` API endpoint expects, ensuring 8743 // the exported file is a complete and re-importable replica. 8744 const siteData = { 8745 // The core menu/page structure with enhanced Classic collection data. 8746 galleries: enhancedGalleries, 8747 8748 // Admin emails are managed server-side for security. We export the list if it's 8749 // available on the client from a previous check, but the server remains the 8750 // source of truth. A re-import will not overwrite the admin list. 8751 adminEmails: window.siteConfig?.adminEmails || [], 8752 8753 // The unique ID for the site. 8754 siteId: window.siteId || window.Parameters?.siteId || '', 8755 8756 // Custom styling rules for the menu. 8757 menuStyles: currentMenuStyles, 8758 8759 // All elements configured to appear in the sidebar. 8760 sidebarElements: window.SidebarManager?.elements || [], 8761 8762 // Global site metadata for SEO and analytics. 8763 siteMetadata: window.siteMetadata || {}, 8764 8765 // Add metadata about Classic collections that need manual data retrieval 8766 _classicCollections: enhancedGalleries 8767 .filter(g => g._requiresClassicData) 8768 .map(g => ({ 8769 title: g.title, 8770 guid: g._classicGuid, 8771 note: g._note 8772 })) 8773 }; 8774 8775 console.log('Enhanced site data collected for export:', siteData); 8776 8777 // Convert the JavaScript object to a formatted JSON string. 8778 const jsonString = JSON.stringify(siteData, null, 2); 8779 8780 // Create a Blob, which is a file-like object of immutable, raw data. 8781 const blob = new Blob([jsonString], { type: 'application/json' }); 8782 8783 // Create a temporary link element to trigger the browser's download functionality. 8784 const url = URL.createObjectURL(blob); 8785 const a = document.createElement('a'); 8786 a.href = url; 8787 a.download = `${hostname}-config.json`; // e.g., 'yoursite.com-config.json' 8788 document.body.appendChild(a); 8789 a.click(); 8790 8791 // Clean up by removing the temporary link and revoking the object URL to free up memory. 8792 document.body.removeChild(a); 8793 URL.revokeObjectURL(url); 8794 8795 console.log('Enhanced JSON download initiated successfully.'); 8796 8797 // Show a helpful message about Classic collections 8798 const classicCount = siteData._classicCollections?.length || 0; 8799 8800 if (classicCount > 0) { 8801 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
8801need to manually fetch the Classic collection JSON files.`); 8802 } 8803 8804 } catch (error) { 8805 console.error('Error creating JSON download:', error); 8806 alert('An error occurred while preparing the download. Please check the console.'); 8807 } 8808} 8809 8810// Build Hydra Classic-style NDJSON (first line: metadata; second line: imageMetadata array) 8811function buildHydraClassicNdjson(classicGuid, images) { 8812 const header = { 8813 title: 'Untitled', 8814 description: '', 8815 version: '1', 8816 isClassic: true, 8817 isClassicCollection: true, 8818 classicGuid: 'GUID=' + String(classicGuid || '') 8819 }; 8820 8821 const imageArray = Array.isArray(images) ? images.map(function(img, idx) { 8822 var path = ''; 8823 if (img) { 8824 path = img.image || img.link || img.originalUrl || (img.urls && img.urls.original) || ''; 8825 } 8826 // Derive filename base from any provided path/filename and normalize extension to .jpg 8827 var fnameBase = ''; 8828 if (typeof path === 'string' && path.length > 0) { 8829 var slash = path.lastIndexOf('/'); 8830 var raw = slash >= 0 ? path.substring(slash + 1) : path; 8831 // remove query/hash 8832 var q = raw.indexOf('?'); if (q >= 0) raw = raw.substring(0, q); 8833 var h = raw.indexOf('#'); if (h >= 0) raw = raw.substring(0, h); 8834 // strip extension if present 8835 var dot = raw.lastIndexOf('.'); 8836 fnameBase = dot > 0 ? raw.substring(0, dot) : raw; 8837 } else if (img && typeof img.filename === 'string') { 8838 var rf = img.filename; 8839 var q2 = rf.indexOf('?'); if (q2 >= 0) rf = rf.substring(0, q2); 8840 var h2 = rf.indexOf('#'); if (h2 >= 0) rf = rf.substring(0, h2); 8841 var d2 = rf.lastIndexOf('.'); 8842 fnameBase = d2 > 0 ? rf.substring(0, d2) : rf; 8843 } 8844 var ext = (img && img.metadata && typeof img.metadata.extension === 'string' && img.metadata.extension.startsWith('.')) 8845 ? img.metadata.extension : '.jpg'; 8846 var filename = fnameBase ? (fnameBase + ext) : ''; 8847 // Build CDN image URL using site GUID: https://cdn.neonsky.app/{siteGuid}/images/{filename} 8848 // Note: classicGuid is a site GUID, not an image GUID - it identifies the site, and images are stored under that site's path 8849 var guidRaw = String(classicGuid || '').replace(/^GUID=/, ''); 8850 var cdnImageUrl = filename && guidRaw ? ('https://cdn.neonsky.app/' + guidRaw + '/images/' + filename) : path; 8851 var rec = { 8852 caption: (img && (img.caption || img.description)) || '', 8853 description: (img && (img.description || img.caption)) || '', 8854 isPreview: idx === 1, 8855 title: (img && img.title) || '', 8856 'alt-text': (img && (img['alt-text'] || img.altText || '')) || '', 8857 image: cdnImageUrl || '', 8858 link: '', 8859 'link-text': '', 8860 filename: filename, 8861 isClassic: true, 8862 isClassicCollection: true, 8863 classicGuid: 'GUID=' + String(classicGuid || '') 8864 }; 8865 return rec; 8866 }) : []; 8867 8868 // Build NDJSON as: header line, imageMetadata array line, then one line per full image record 8869 var body = { imageMetadata: imageArray }; 8870 var out = [JSON.stringify(header), JSON.stringify(body)]; 8871 for (var k = 0; k < imageArray.length; k++) { 8872 out.push(JSON.stringify(imageArray[k])); 8873 } 8874 return out.join('\n') + '\n'; 8875} 8876 8877async function uploadNdjsonToTigris(siteId, pageId, ndjsonContent) { 8878 // Dual upload strategy: Fly.io (uncompressed) + Irys (compressed if <800KB) 8879 const fileName = String(siteId || '') + '_' + String(pageId || '') + '.ndjson'; 8880 const flyUrl = 'https://hydra-press-v2.fly.dev/publish'; 8881 const irysUrl = 'https://hydra-press-v2.fly.dev/upload-irys'; 8882 8883 console.error('Starting dual NDJSON upload for:', fileName); 8884 console.error('Content length:', ndjsonContent.length, 'characters'); 8885 8886 // COMPRESSION: Compress for Irys only (not Tigris) 8887 console.error('ð Compressing NDJSON with gzip for Irys...'); 8888 const encoder = new TextEncoder(); 8889 const ndjsonBytes = encoder.encode(ndjsonContent); 8890 const compressedStream = new Response(ndjsonBytes).body.pipeThrough(new CompressionStream('gzip')); 8891 const compressedBlob = await new Response(compressedStream).blob(); 8892 const compressedBytes = new Uint8Array(await compressedBlob.arrayBuffer()); 8893 8894 const originalSize = ndjsonBytes.length; 8895 const compressedSize = compressedBytes.length; 8896 const compressionRatio = ((1 - compressedSize / originalSize) * 100).toFixed(2); 8897 8898 console.error('ð Compression results:', { 8899 original: originalSize + ' bytes (' + (originalSize / 1024).toFixed(2) + ' KB)', 8900 compressed: compressedSize + ' bytes (' + (compressedSize / 1024).toFixed(2) + ' KB)', 8901 ratio: compressionRatio + '%' 8902 }); 8903 8904 const IRYS_SIZE_LIMIT = 800 * 1024; // 800KB limit for compressed Irys upload 8905 const shouldUploadToIrys = compressedSize < IRYS_SIZE_LIMIT; 8906 8907 console.error('Irys upload eligible: ' + shouldUploadToIrys + ' (compressed: ' + (compressedSize / 1024).toFixed(2) + ' KB)'); 8908 8909 // STEP 1: Upload to Irys (non-blocking, compressed if <800KB) 8910 let irysResult = null; 8911 if (shouldUploadToIrys) { 8912 try { 8913 console.error('ð Starting Irys upload (compressed, non-blocking)...'); 8914 8915 // Convert compressed bytes to base64 8916 const compressedBase64 = btoa(String.fromCharCode(...compressedBytes)); 8917 8918 const irysResp = await fetch(irysUrl, { 8919 method: 'POST', 8920 headers: { 'content-type': 'application/json' }, 8921 body: JSON.stringify({ 8922 compressedData: compressedBase64, 8923 originalSize: originalSize, 8924 compressedSize: compressedSize, 8925 siteId: siteId, 8926 pageId: pageId 8927 }) 8928 }); 8929 8930 if (irysResp.ok) { 8931 irysResult = await irysResp.json(); 8932 console.error('â Irys upload successful:', irysResult.txId); 8933 console.error(' Irys Gateway:', irysResult.urls?.irys); 8934 console.error(' ar.io Gateway:', irysResult.urls?.arweave); 8935 8936 // Log wallet balance and cost 8937 if (irysResult.walletBalance !== undefined) { 8938 console.error('ð° Wallet Balance:', irysResult.walletBalance.toFixed(6), 'AR'); 8939 } 8940 if (irysResult.uploadCost !== undefined) { 8941 const costUSD = (irysResult.uploadCost * 20).toFixed(4); 8942 if (irysResult.uploadCost === 0) { 8943 console.error('ð¸ Upload Cost: FREE (0 AR)'); 8944 } else { 8945 console.error('ð¸ Upload Cost:', irysResult.uploadCost.toFixed(6), 'AR (~$' + costUSD + ' USD at $20/AR)'); 8946 } 8947 } 8948 } else { 8949 const errorText = await irysResp.text().catch(() => 'Unknown error'); 8950 console.warn('â ï¸ Irys upload failed (non-critical):', irysResp.status, errorText); 8951 } 8952 } catch (irysError) { 8953 console.warn('â ï¸ Irys upload error (non-critical):', irysError.message); 8954 // Continue - Irys failure doesn't block Fly.io upload 8955 } 8956 } else { 8957 console.error('âï¸ Skipping Irys upload (compressed size exceeds 800KB limit)'); 8958 } 8959 8960 // STEP 2: Upload to Fly.io (CRITICAL - must succeed, uncompressed) 8961 // Hydra sites require Bearer auth - get token from TokenManager/localStorage (same as Edition Publisher) 8962 try { 8963 console.error('ð Starting Fly.io upload (uncompressed)...'); 8964 const flyHeaders = { 'content-type': 'application/json' }; 8965 let authToken = null; 8966 try { 8967 if (window.TokenManager) { 8968 if (typeof window.TokenManager.getHydraAuthToken === 'function') { 8969 try { authToken = window.TokenManager.getHydraAuthToken(); } catch (e) {} 8970 }
8971 if (!authToken && typeof window.TokenManager.getBestToken === 'function') { 8972 try { 8973 const tw = window.TokenManager.getBestToken(); 8974 authToken = tw && tw.startsWith('Bearer ') ? tw.substring(7) : tw; 8975 } catch (e) {} 8976 } 8977 } 8978 if (!authToken) authToken = localStorage.getItem('hydra_auth_token'); 8979 if (!authToken) authToken = window.authToken; 8980 if (authToken) { 8981 flyHeaders['Authorization'] = authToken.includes('.') || authToken.startsWith('hydra:') 8982 ? 'Bearer ' + authToken 8983 : (authToken.startsWith('Bearer ') ? authToken : 'Bearer hydra:' + authToken); 8984 console.error('ð Fly.io auth: token attached'); 8985 } else { 8986 console.warn('â ï¸ No auth token - Fly.io may reject Hydra site upload (401)'); 8987 } 8988 } catch (authErr) { 8989 console.warn('â ï¸ Auth token lookup failed:', authErr.message); 8990 } 8991 const flyResp = await fetch(flyUrl, { 8992 method: 'POST', 8993 headers: flyHeaders, 8994 body: JSON.stringify({ ndjsonContent, dataFileName: fileName }) 8995 }); 8996 8997 console.log('Server response status:', flyResp.status); 8998 8999 if (!flyResp.ok) { 9000 const errorText = await flyResp.text().catch(() => 'Unknown error'); 9001 console.error('â Fly.io upload failed', flyResp.status, 'for', fileName + ':', errorText); 9002 if (flyResp.status === 401) { 9003 console.error('ð¡ Tip: Open the Edition Publisher (or log in as admin) first to authenticate for Fly.io uploads.'); 9004 } 9005 return { success: false, irysResult: null }; 9006 } 9007 9008 const flyData = await flyResp.json().catch(() => null); 9009 if (flyData && flyData.urls) { 9010 console.log('â Fly.io upload successful:', flyData.urls); 9011 console.log(' Tigris URL:', flyData.urls.urlTigris); 9012 9013 // Return comprehensive result 9014 const result = { 9015 success: true, 9016 flyUrls: flyData.urls, 9017 irysResult: irysResult, 9018 uploadedTo: irysResult ? ['fly', 'irys'] : ['fly'], 9019 fileSize: compressedSize, // Irys compressed size 9020 originalFileSize: originalSize // Original uncompressed size 9021 }; 9022 9023 console.log('ð¦ Upload complete:', { 9024 destinations: result.uploadedTo.join(' + '), 9025 txId: irysResult?.txId || 'N/A', 9026 flyUrl: flyData.urls.urlTigris 9027 }); 9028 9029 return result; 9030 } else { 9031 console.warn('Fly.io upload response missing URLs:', flyData); 9032 return { success: false, irysResult: null }; 9033 } 9034 } catch (flyError) { 9035 console.error('â Fly.io upload error for', fileName + ':', flyError); 9036 return { success: false, irysResult: null }; 9037 } 9038} 9039 9040async function importClassicGalleries() { 9041 // Browser-scoped helper: mirrors server-side behavior with escaped patterns for template-safe JS 9042 function cleanDescriptionLeadingLines(text) { 9043 if (!text || typeof text !== 'string') return text; 9044 9045 let result = text; 9046 9047 // Remove leading empty TEXTFORMAT blocks: <TEXTFORMAT><P><FONT></FONT></P></TEXTFORMAT> 9048 result = result.replace( 9049 new RegExp( 9050 '^(\s*\<TEXTFORMAT[^\>]*\>\s*\<P[^\>]*\>\s*\<FONT[^\>]*\>\s*\<\/FONT\>\s*\<\/P\>\s*\<\/TEXTFORMAT\>\s*)+', 9051 'i' 9052 ), 9053 '' 9054 ); 9055 9056 // Remove leading empty P tags 9057 result = result.replace( 9058 new RegExp('^(\s*\<P[^\>]*\>\s*\<\/P\>\s*)+', 'i'), 9059 '' 9060 ); 9061 9062 // Remove leading empty FONT tags 9063 result = result.replace( 9064 new RegExp('^(\s*\<FONT[^\>]*\>\s*\<\/FONT\>\s*)+', 'i'), 9065 '' 9066 ); 9067 9068 // Remove leading BR tags 9069 result = result.replace( 9070 new RegExp('^(\s*\<br\s*\/\?>\s*)+', 'i'), 9071 '' 9072 ); 9073 9074 // Remove any remaining leading whitespace 9075 return result.replace(new RegExp('^\s+', 'i'), '').trim(); 9076 } 9077 9078 // Browser-scoped helper: fix multi-line links (template-safe escaped regex) 9079 function fixMultilineLinks(text) { 9080 if (!text || typeof text !== 'string') return text; 9081 return text.replace( 9082 new RegExp('<a[^>]*>.*?Click.*?<\/a><a[^>]*>.*?HERE.*?<\/a><a[^>]*>.*?to Add to Cart.*?<\/a>', 'gis'), 9083 function (match) { 9084 const first = match.match(new RegExp('<a href="([^"]*)" target="([^"]*)"', 'i')); 9085 if (first) { 9086 return '<a href="' + first[1] + '" target="' + first[2] + '">
9086Click HERE to Add to Cart</a>'; 9087 } 9088 return match; 9089 } 9090 ); 9091 } 9092 9093 // Browser-scoped helper: convert plain text link phrase to HTML link 9094 function convertPlainTextLinks(text) { 9095 if (!text || typeof text !== 'string') return text; 9096 return text.replace( 9097 new RegExp('Click HERE to Add to Cart', 'g'), 9098 '<a href="#" target="_blank">Click HERE to Add to Cart</a>' 9099 ); 9100 } 9101 // Get input elements, including the new checkbox 9102 const guidInputEl = document.getElementById('classicGuid'); 9103 const pastedJsonEl = document.getElementById('pastedJson'); 9104 const parentIdInputEl = document.getElementById('importParent'); 9105 const createSubmenuCheckboxEl = document.getElementById('createSubmenu'); 9106 const submenuTitleInputEl = document.getElementById('submenuTitle'); 9107 const replaceMenuCheckboxEl = document.getElementById('replaceMenuCheckbox'); 9108 9109 // Get values from inputs 9110 const guidInput = guidInputEl instanceof HTMLInputElement ? guidInputEl.value.trim() : ''; 9111 const pastedJson = pastedJsonEl instanceof HTMLTextAreaElement ? pastedJsonEl.value.trim() : ''; 9112 const replaceMenu = replaceMenuCheckboxEl instanceof HTMLInputElement ? replaceMenuCheckboxEl.checked : false; 9113 9114 let parentId = parentIdInputEl instanceof HTMLSelectElement && parentIdInputEl.value ? parseInt(parentIdInputEl.value) : null; 9115 let createSubmenu = createSubmenuCheckboxEl instanceof HTMLInputElement ? createSubmenuCheckboxEl.checked : false; 9116 let submenuTitle = submenuTitleInputEl instanceof HTMLInputElement ? submenuTitleInputEl.value.trim() : ''; 9117 9118 // If replacing the menu, ignore parent/submenu settings 9119 if (replaceMenu) { 9120 parentId = null; 9121 createSubmenu = false; 9122 } 9123 9124 // Validate submenu title if checkbox is checked 9125 if (createSubmenu && !submenuTitle) { 9126 alert('Please provide a title for the submenu'); 9127 return; 9128 } 9129 9130 // Show status display 9131 const statusEl = document.getElementById('importStatus'); 9132 const progressBarEl = statusEl ? statusEl.querySelector('.progress-bar') : null; 9133 const statusMessageEl = statusEl ? statusEl.querySelector('.status-message') : null; 9134 const progressBar = progressBarEl instanceof HTMLElement ? progressBarEl : null; 9135 const statusMessage = statusMessageEl instanceof HTMLElement ? statusMessageEl : null; 9136 9137 if (statusEl && progressBar && statusMessage) { 9138 statusEl.style.display = 'block'; 9139 statusEl.classList.add('importing'); 9140 progressBar.style.width = '10%'; 9141 statusMessage.textContent = 'Preparing to import...'; 9142 } 9143 9144 try {
9145 let classicData; 9146 let classicGuid; // Will hold the primary GUID for image path lookups 9147 9148 // --- LOGIC TO GET DATA: Prioritize Pasted JSON --- 9149 if (pastedJson) { 9150 if (statusMessage) statusMessage.textContent = 'Parsing pasted JSON...'; 9151 classicData = JSON.parse(pastedJson); 9152 9153 if (!classicData.galleries || !Array.isArray(classicData.galleries)) { 9154 throw new Error('Pasted JSON is invalid: "galleries" array not found.'); 9155 } 9156 // Use the first available GUID from the pasted data as the default for images 9157 classicGuid = classicData.galleries[0]?.classicGuid || classicData.siteId; 9158 9159 } else if (guidInput) { 9160 // Check if this is the new import_ format 9161 if (guidInput.startsWith('import_')) { 9162 if (statusMessage) statusMessage.textContent = 'Fetching data via import_ format...'; 9163 const importGuid = guidInput.substring(7); // Remove 'import_' prefix 9164 9165 const jsonUrl = `https://hydra.neonsky.app/json-imports/${guidInput}.json`; 9166 const response = await fetch(jsonUrl); 9167 if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`); 9168 9169 classicData = await response.json(); 9170 9171 if (!classicData.galleries || !Array.isArray(classicData.galleries)) {
9172 throw new Error('Fetched import JSON is invalid: "galleries" array not found.'); 9173 } 9174 9175 // HYBRID APPROACH: Also fetch the original GUID JSON for image metadata 9176 if (statusMessage) statusMessage.textContent = 'Fetching image metadata from original JSON...'; 9177 9178 const originalJsonUrl = `https://cdn.neonsky.app/sites:site_${importGuid}.json`; 9179 9180 try { 9181 const originalResponse = await fetch(originalJsonUrl); 9182 if (originalResponse.ok) { 9183 const originalData = await originalResponse.json(); 9184 9185 // Get the default language from site configuration 9186 let defaultLanguageID = 'en'; // fallback 9187 if (originalData.languages && Array.isArray(originalData.languages)) { 9188 const defaultLang = originalData.languages.find(lang => lang.isDefault === true); 9189 if (defaultLang) { 9190 defaultLanguageID = defaultLang.id; 9191 } 9192 } 9193 9194 // Merge image metadata from original JSON into import JSON galleries 9195 if (originalData.galleries && Array.isArray(originalData.galleries)) { 9196 classicData.galleries = classicData.galleries.map(importGallery => { 9197 // Find matching gallery in original data by name or categoryID 9198 9199 const originalGallery = originalData.galleries.find(orig => 9200 orig.title === importGallery.title || // Primary match by title 9201 orig.name === importGallery.title || // Fallback: orig.name matches import.title 9202 orig.categoryID === importGallery.categoryID // Fallback: categoryID match 9203 ); 9204 9205 9206 if (originalGallery && originalGallery.images && Array.isArray(originalGallery.images)) { 9207 9208 // Process all images from original gallery with metadata extraction 9209 const processedImages = originalGallery.images.map(originalImage => { 9210 // Apply EXACT same metadata processing logic as Edition Publisher 9211 let title = originalImage.title || ''; 9212 let dateline = originalImage.dateline || ''; 9213 let caption = originalImage.caption || ''; 9214 9215 // Check languageVersions array for metadata (this is where most data is stored) 9216 if (originalImage.languageVersions && originalImage.languageVersions.length > 0) { 9217 // Look for site's default language first, then fall back to first available 9218 let langData = originalImage.languageVersions.find(lang => lang.languageID === defaultLanguageID); 9219 if (!langData) { 9220 langData = originalImage.languageVersions[0]; 9221 } 9222 9223 // For language-enabled JSON files, prioritize languageVersions data 9224 // Only fall back to top-level if languageVersions data is empty 9225 if (!title || title.trim() === '') title = langData.title || ''; 9226 if (!dateline || dateline.trim() === '') dateline = langData.dateline || ''; 9227 if (!caption || caption.trim() === '') caption = langData.caption || ''; 9228 } 9229 9230 // Convert Flash TEXTFORMAT to HTML (properly escaped for template literals) 9231 let description = caption; 9232 if (caption && caption.includes('TEXTFORMAT')) { 9233 try { 9234 // Use properly escaped HTML characters for template literal context 9235 description = caption 9236 .replace(/\<TEXTFORMAT[^\>]*\>/g, '') 9237 .replace(/\<\/TEXTFORMAT\>/g, '') 9238 .replace(/\<P[^\>]*\>/g, '\<p\>') 9239 .replace(/\<\/P\>/g, '\<\/p\>') 9240 .replace(/\<FONT[^\>]*\>/g, '') 9241 .replace(/\<\/FONT\>/g, '') 9242 .replace(/\<A HREF="([^"]*)"[^\>]*\>/g, '\<a href="$1" target="_blank"\>') 9243 .replace(/\<\/A\>/g, '\<\/a\>') 9244 .replace(/\<U\>/g, '\<u\>') 9245 .replace(/\<\/U\>/g, '\<\/u\>') 9246 .replace(/\<B\>/g, '\<strong\>') 9247 .replace(/\<\/B\>/g, '\<\/strong\>') 9248 .replace(/\<I\>/g, '\<em\>') 9249 .replace(/\<\/I\>/g, '\<\/em\>') 9250 .replace(/"/g, '"') 9251 .replace(/'/g, "'") 9252 .replace(/\s+/g, ' ') 9253 .trim(); 9254 } catch (error) { 9255 console.warn('TEXTFORMAT conversion failed, using original caption:', error); 9256 description = caption; 9257 } 9258 } 9259 9260 // Prepend dateline to description if it exists (convert to uppercase) 9261 if (dateline) { 9262 const uppercaseDateline = dateline.toUpperCase(); 9263 description = `${uppercaseDateline} ${description}`.trim(); 9264 } 9265 9266 // Apply description fixes 9267 description = cleanDescriptionLeadingLines(description); 9268 description = fixMultilineLinks(description); 9269 description = convertPlainTextLinks(description); 9270 9271 const imageEntry = { 9272 // Match EXACT structure from Edition Publisher hydraImageEntry 9273 image: originalImage.image || originalImage.urls?.original || '', 9274 caption: description, // Use processed description as caption 9275 description: description, // ALSO set description field (like Edition Publisher) 9276 title: title, 9277 'alt-text': title, // Use title as alt-text like Edition Publisher 9278 link: originalImage.urls?.original || '', 9279 'link-text': originalImage.urls?.original || '', 9280 isPreview: false, 9281 filename: originalImage.filename || '', 9282 originalUrl: originalImage.image || originalImage.urls?.original || '', 9283 dateline: dateline 9284 }; 9285 9286 return imageEntry; 9287 }); 9288 9289 return { 9290 ...importGallery, // Keep import gallery structure (order, settings, etc.) 9291 images: processedImages // Replace with processed original images 9292 }; 9293 } 9294 return importGallery; 9295 }); 9296 } 9297 } else { 9298 console.warn('Could not fetch original JSON for metadata, using import data only'); 9299 } 9300 } catch (error) { 9301 console.warn('Error fetching original JSON for metadata:', error); 9302 } 9303 9304 // Extract original GUID for image path lookups 9305 // If importGuid has a suffix (e.g., "4bd5ebf7bda9d-all"), extract the base GUID 9306 // GUIDs are typically 13 characters, so split on common separators 9307 let baseGuid = importGuid; 9308 const guidMatch = importGuid.match(/^([a-f0-9]{13})(?:-.*)?$/i); 9309 if (guidMatch) { 9310 baseGuid = guidMatch[1]; 9311 } else { 9312 // Fallback: try to extract first 13-character hex string 9313 const hexMatch = importGuid.match(/^([a-f0-9]{13})/i); 9314 if (hexMatch) { 9315 baseGuid = hexMatch[1]; 9316 } 9317 } 9318 9319 // Use the base GUID for image path lookups (e.g., /images/filename.jpg) 9320 classicGuid = baseGuid; 9321 console.log('Successfully loaded hybrid import data with import GUID:', importGuid); 9322 console.log('Using base GUID for image paths:', classicGuid); 9323 9324 } else { 9325 // Standard GUID format - existing logic 9326 if (statusMessage) statusMessage.textContent = 'Fetching data via GUID...';
9327 classicGuid = guidInput.startsWith('GUID=') ? guidInput.substring(5) : guidInput; 9328 console.log('Detected standard GUID format, GUID:', classicGuid); 9329 9330 const jsonUrl = `https://storage.neonsky.app/sites:site_${classicGuid}.json`; 9331 console.log('Fetching classic JSON from:', jsonUrl); 9332 const response = await fetch(jsonUrl); 9333 9334 if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`); 9335 classicData = await response.json(); 9336 9337 if (!classicData.galleries || !Array.isArray(classicData.galleries)) { 9338 throw new Error('Fetched JSON is invalid: "galleries" array not found.'); 9339 } 9340 9341 console.log('Successfully loaded classic data with GUID:', classicGuid); 9342 } 9343 9344 } else { 9345 alert('Please either paste site JSON or enter a Classic GUID to import.'); 9346 if (statusEl) statusEl.style.display = 'none'; 9347 return; 9348 } 9349 9350 const galleriesForImport = classicData.galleries; 9351 if (statusMessage) statusMessage.textContent = `Found ${galleriesForImport.length} items to import...`; 9352 9353 // import_ format: galleries come from DB/import JSON, not classic site - treat as new Hydra NDJSON galleries 9354 const isImportFormat = !!(guidInput && guidInput.startsWith('import_')); 9355 9356 // --- HIERARCHY RECONSTRUCTION LOGIC --- 9357 9358 const currentSiteId = window.Parameters?.siteId || siteId; 9359 let submenuParentId = parentId; 9360 const tempNewItems = []; 9361 const originalIdToNewIdMap = {}; 9362 9363 // Create the optional top-level submenu if requested 9364 if (createSubmenu) { 9365 const submenuItem = { 9366 id: Date.now(), 9367 title: submenuTitle, 9368 isSubmenu: true, 9369 isCollapsed: true, 9370 visible: true, 9371 parentId: parentId, 9372 siteId: currentSiteId, 9373 children: [] 9374 }; 9375 galleries.push(submenuItem); 9376 submenuParentId = submenuItem.id; 9377 } 9378 9379 // --- Pass 1: Create new items with correct data and map old IDs to new IDs --- 9380 for (let index = 0; index < galleriesForImport.length; index++) { 9381 const classicItem = galleriesForImport[index]; 9382 const newItemId = Date.now() + index + 100; 9383 originalIdToNewIdMap[classicItem.id] = newItemId; 9384 9385 if (progressBar) progressBar.style.width = `${40 + Math.round(index * 50 / galleriesForImport.length)}%`; 9386 if (statusMessage) statusMessage.textContent = `Processing item ${index + 1} of ${galleriesForImport.length}...`; 9387 9388 const title = classicItem.title || classicItem.languageVersions?.[0]?.title || classicItem.name || `Item ${classicItem.categoryID}`; 9389 9390 let slug; 9391 // If pasting from JSON, *always* regenerate the slug from the title to ensure it's clean. 9392 // Otherwise (for GUID import), trust the existing slug if it's there. 9393 if (pastedJson) { 9394 slug = normalizeNameForComparison(title); 9395 } else if (classicItem.slug && classicItem.slug.trim() !== '') { 9396 slug = classicItem.slug; 9397 } else { 9398 slug = normalizeNameForComparison(title); 9399 } 9400const finalSlug = ensureUniqueSlug(slug || 'gallery', [...galleries, ...tempNewItems]); // Ensure uniqueness 9401 9402 // Determine the GUID to use for this specific item's image paths 9403 const itemGuid = classicItem.classicGuid || classicGuid; 9404 9405 let newItem; 9406 9407 // --- Check if it's an External URL --- 9408 if (classicItem.isExternal === true && classicItem.url) { 9409 console.log(`Creating external URL menu item: "${title}" -> ${classicItem.url}`); 9410 newItem = { 9411 id: newItemId, 9412 title, 9413 url: classicItem.url, 9414 isExternal: true, 9415 visible: classicItem.visible !== false, 9416 parentId: null, 9417 siteId: currentSiteId, 9418 slug: finalSlug, 9419 importSource: 'external-url' 9420 }; 9421 } 9422 // --- Check if it's a Page and build its elements --- 9423 else if (classicItem.isPage === true || classicItem.type === 3 || classicItem.behaviorID === 3) { 9424 const pageUniqueId = `page_${newItemId}`; 9425 9426 let pageElements = []; 9427 9428 // ================================================================= 9429 // ENHANCED: Handle pageElements for both pasted JSON and import_guid.json format 9430 // ================================================================= 9431 if (classicItem.pageElements && Array.isArray(classicItem.pageElements)) { 9432 console.log(`Using existing pageElements for page: "${title}"`); 9433 9434 // Check if we have both pageElements and images - create two-column layout 9435 if (classicItem.images && classicItem.images.length > 0) { 9436 console.log(`Creating two-column layout for page: "${title}" with ${classicItem.images.length} images`); 9437 9438 // Create metadata element 9439 pageElements.push({ 9440 id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0, 9441 metaTitle: classicItem.meta?.title || title, 9442 metaDescription: classicItem.meta?.description || "", 9443 metaKeywords: classicItem.meta?.keywords || "" 9444 }); 9445 9446 // Create two-column container 9447 const leftColumnElements = []; 9448 const rightColumnElements = []; 9449 9450 // Add images to left column 9451 const slides = classicItem.images.map((img) => { 9452 const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg'); 9453 const imageKey = `${itemGuid}/images/${imageFilename}`; 9454 const imageUrl = `https://storage.neonsky.app/${imageKey}`; 9455 9456 return { 9457 imageUrl, 9458 imageKey, 9459 imageFilename, 9460 imageType: img.metadata?.mimetype || 'image/jpeg', 9461 caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))), 9462 title: img.title || '', 9463 byline: img.byline || '', 9464 dateline: img.dateline || '', 9465 altText: img.metadata?.alt_text || '' 9466 }; 9467 }); 9468 9469 leftColumnElements.push({ 9470 id: newItemId + 3100, type: "slideshow", title: "Page Images", visible: true, position: 0, 9471 slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true 9472 }); 9473 9474 // Add text content to right column
9475 classicItem.pageElements.forEach((el, elIndex) => { 9476 if (el.type === 'text' && el.content) { 9477 rightColumnElements.push({ 9478 id: newItemId + 3200 + elIndex, type: "text", title: "Text Block", visible: true, position: elIndex, 9479 textContent: el.content, textWidth: 70 9480 }); 9481 } 9482 }); 9483 9484 const columns = [ 9485 { id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements }, 9486 { id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements } 9487 ]; 9488 9489 pageElements.push({ 9490 id: newItemId + 3000, type: "column-container", title: "Two Column Layout", visible: true, position: 1, 9491 columns, 9492 backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false, showSingleRandomImage: false, randomStart: false }, 9493 backgroundVideo: { enabled: false, videoUrl: '', videoUrls: [], randomizeVideo: false, videoHeight: 50 } 9494 }); 9495 9496 } else { 9497 // No images, just transform the pageElements as before 9498 pageElements = classicItem.pageElements.map((el, elIndex) => { 9499 const elementId = Date.now() + index + 1000 + elIndex; 9500 9501 // Handle import_guid.json format (type + content) 9502 if (el.type && el.content) { 9503 if (el.type === 'text') { 9504 return { 9505 id: elementId, 9506 type: 'text', 9507 title: 'Text Block', 9508 visible: true, 9509 position: elIndex, 9510 textContent: el.content, 9511 textWidth: 100 9512 }; 9513 } else if (el.type === 'metadata') { 9514 return { 9515 id: elementId, 9516 type: 'metadata', 9517 title: 'Metadata', 9518 visible: true, 9519 position: elIndex, 9520 metaTitle: el.metaTitle || title, 9521 metaDescription: el.metaDescription || '', 9522 metaKeywords: el.metaKeywords || '' 9523 }; 9524 } 9525 } 9526 9527 // Handle existing format (already has id, position, etc.) 9528 return { 9529 ...el, 9530 id: elementId, 9531 position: el.position !== undefined ? el.position : elIndex 9532 }; 9533 }); 9534 } 9535 9536 } else { 9537 // Otherwise (for GUID-based import), generate the page structure from scratch. 9538 console.log(`Generating new pageElements for page: "${title}"`); 9539 pageElements.push({ 9540 id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0, 9541 metaTitle: classicItem.meta?.title || title, 9542 metaDescription: classicItem.meta?.description || "", 9543 metaKeywords: classicItem.meta?.keywords || "" 9544 }); 9545 9546 // Add images if they exist in the page 9547 if (classicItem.images && classicItem.images.length > 0) { 9548 const slides = classicItem.images.map((img) => { 9549 const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg'); 9550 const imageKey = `${itemGuid}/images/${imageFilename}`; 9551 const imageUrl = `https://storage.neonsky.app/${imageKey}`; 9552 9553 return { 9554 imageUrl, 9555 imageKey, 9556 imageFilename, 9557 imageType: img.metadata?.mimetype || 'image/jpeg', 9558 caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))), 9559 title: img.title || '', 9560 byline: img.byline || '', 9561 dateline: img.dateline || '', 9562 altText: img.metadata?.alt_text || '' 9563 }; 9564 }); 9565 9566 pageElements.push({ 9567 id: newItemId + 1500, type: "slideshow", title: "Page Images", visible: true, position: 1, 9568 slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 100, slideshowHeight: 60, showFullImages: true 9569 }); 9570 } 9571 9572 pageElements.push({ 9573 id: newItemId + 2000, type: "text", title: "Text Block Spacer", visible: true, position: 2, 9574 textContent: "<p> </p><p><br></p>", textWidth: 100 9575 }); 9576 9577 const columns = []; 9578 const leftColumnElements = []; 9579 const rightColumnElements = []; 9580 9581 if (classicItem.images && classicItem.images.length > 0) { 9582 const slides = classicItem.images.map((img) => { 9583 const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg'); 9584 const imageKey = `${itemGuid}/images/${imageFilename}`; 9585 const imageUrl = `https://storage.neonsky.app/${imageKey}`; 9586 9587 // Preserve all image metadata including captions, titles, etc. 9588 return { 9589 imageUrl, 9590 imageKey, 9591 imageFilename, 9592 imageType: img.metadata?.mimetype || 'image/jpeg', 9593 // Preserve caption information - handle both direct and language version storage patterns 9594 caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || img.languageVersions?.[0]?.caption || ''))), 9595 title: img.title || img.languageVersions?.[0]?.title || '', 9596 byline: img.byline || img.languageVersions?.[0]?.byline || '', 9597 dateline: img.dateline || img.languageVersions?.[0]?.dateline || '', 9598 altText: img.metadata?.alt_text || '', 9599 // Preserve any additional metadata 9600 metaTitle: img.metadata?.meta_title || '', 9601 metaDescription: img.metadata?.meta_description || '', 9602 metaKeywords: img.metadata?.meta_keywords || '', 9603 // Preserve gallery-specific data 9604 sortOrder: img.galleryData?.sortOrder || 0, 9605 linkURL: img.galleryData?.linkURL || null, 9606 linkTarget: img.galleryData?.linkTarget || '_self' 9607 }; 9608 }); 9609 leftColumnElements.push({ 9610 id: newItemId + 3100, type: "slideshow", title: "Image Slideshow", visible: true, position: 0, 9611 slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true 9612 }); 9613 } 9614 9615 let rawDetails = classicItem.details || classicItem.languageVersions?.[0]?.details || ""; 9616 if (rawDetails && typeof rawDetails === 'string') { 9617 // ================================================================= 9618 // CHANGE 2: Fix for "Invalid regular expression flags" error. 9619 // Changed from /.../gi literal to new RegExp('...', 'gi') constructor 9620 // to prevent syntax errors. 9621 // ================================================================= 9622 let processedText = rawDetails.replace(new RegExp('<TEXTFORMAT.*?>', 'gi'), '').replace(new RegExp('</TEXTFORMAT>', 'gi'), ''); 9623 processedText = processedText.replace(new RegExp('<P ALIGN=".*?"', 'gi'), '<p').replace(new RegExp('</P>', 'gi'), '</p>'); 9624 processedText = processedText.replace(new RegExp('<FONT.*?>', 'gi'), '').replace(new RegExp('</FONT>', 'gi'), ''); 9625 processedText = processedText.replace(new RegExp('<B>
9625', 'gi'), '<strong>').replace(new RegExp('</B>', 'gi'), '</strong>'); 9626 processedText = processedText.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'); 9627 9628 // Apply description fixes to processed text 9629 processedText = cleanDescriptionLeadingLines(processedText); 9630 processedText = fixMultilineLinks(processedText); 9631 processedText = convertPlainTextLinks(processedText); 9632 9633 rightColumnElements.push({ 9634 id: newItemId + 3200, type: "text", title: "Text Block Content", visible: true, position: 0, 9635 textContent: processedText, textWidth: 70 9636 }); 9637 } 9638 9639 columns.push({ id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements }); 9640 columns.push({ id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements }); 9641 9642 pageElements.push({ 9643 id: newItemId + 3000, type: "column-container", title: "Imported Columns", visible: true, position: 2, 9644 columns, 9645 backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false, showSingleRandomImage: false, randomStart: false }, 9646 backgroundVideo: { enabled: false, videoUrl: '', videoUrls: [], randomizeVideo: false, videoHeight: 50 } 9647 }); 9648 } 9649 9650 newItem = { 9651 id: newItemId, title, isPage: true, isIntegrated: true, isSubmenu: false, pageId: pageUniqueId, visible: classicItem.visible !== false, parentId: null, 9652 siteId: currentSiteId, slug: finalSlug, url: `/${finalSlug}`, pageElements, 9653 classicCategoryId: classicItem.categoryID, classicGuid: itemGuid, importSource: 'classic-page' 9654 }; 9655 } else if (classicItem.isFolder === true) { 9656 // --- Handle folders from import_guid.json format --- 9657 console.log(`Creating folder: "${title}"`); 9658 newItem = { 9659 id: newItemId, 9660 title, 9661 isFolder: true, 9662 isCollapsed: classicItem.isCollapsed !== false, 9663 visible: classicItem.visible !== false, 9664 parentId: null, 9665 siteId: currentSiteId, 9666 slug: finalSlug, 9667 importSource: 'folder' 9668 }; 9669 } else { 9670 // --- This is a Gallery --- 9671 const galleryPageId = generatePageId(); 9672 9673 // --- ENHANCED: Try to fetch and include actual image data with captions --- 9674 let enhancedGalleryOptions = { 9675 manualCollectionName: `GUID=${itemGuid}`, 9676 categoryId: classicItem.categoryID, 9677 importedFromClassic: true, 9678 siteId: currentSiteId, 9679 pageId: galleryPageId 9680 }; 9681 9682 // --- ENHANCED: Preserve gallery display settings from Classic JSON --- 9683 if (classicItem.galleryOptions) { 9684 console.log(`Preserving gallery settings for: ${title}`); 9685 9686 // Preserve layout and display settings 9687 const preservedSettings = [ 9688 'startInSingles', 'layoutType', 'columns', 'spacing', 9689 'showDescription', 'showFilename', 'displayAllInfo', 9690 'navigationMode', 'showTextBlock', 'fixedHeroImage', 9691 'zoomInLightbox', 'lightboxOnMobile', 'fadeDuration', 9692 'fadeInDuration', 'descriptionTextColor', 'gridImageOverlayColor', 9693 'gridImageOverlayOpacity', 'lightboxBgColor', 'lightboxBgOpacity', 9694 'lightboxCloseColor', 'lightboxArrowColor', 'useTitles', 9695 'useLinks', 'desktopTitleDisplayMode', 'titleTextAlign', 9696 'showDescriptionInOverlay', 'includeRolloverImageInOverlay', 9697 'filterMenuEnabled', 'filterMenuCollectionNames', 'filterMenuTitles', 9698 'filterMenuStyle', 'rolloverSwap', 'rolloverCollectionNames', 9699 'openMultipleLightboxes', 'batchSize', 'autoplaySingles', 9700 'autoplayDuration', 'autoplayTransition', 'autoplayRandomStart' 9701 ]; 9702
9703 preservedSettings.forEach(setting => { 9704 if (classicItem.galleryOptions[setting] !== undefined) { 9705 enhancedGalleryOptions[setting] = classicItem.galleryOptions[setting]; 9706 } 9707 }); 9708 9709 // Preserve text content 9710 if (classicItem.galleryOptions.horizontalScrollerText) { 9711 enhancedGalleryOptions.horizontalScrollerText = classicItem.galleryOptions.horizontalScrollerText; 9712 } 9713 9714 // Preserve JSON collections if present 9715 if (classicItem.galleryOptions.jsonCollections) { 9716 enhancedGalleryOptions.jsonCollections = classicItem.galleryOptions.jsonCollections; 9717 } else { 9718 // Create jsonCollections structure with processed image metadata 9719 const collectionKey = 'GUID=' + itemGuid; 9720 const processedImages = classicItem.images || []; 9721 9722 enhancedGalleryOptions.jsonCollections = { 9723 [collectionKey]: { 9724 isClassicCollection: true, 9725 guid: collectionKey, 9726 metadata: processedImages, 9727 totalItems: processedImages.length 9728 } 9729 }; 9730 9731 console.log('Created jsonCollections for ' + title + ' with ' + processedImages.length + ' images'); 9732 9733 // Build and try to upload NDJSON for this gallery. 9734 // If upload succeeds, switch to metadata = {} so frontend fetches NDJSON. 9735 // Skip NDJSON generation for galleries with no images to avoid creating empty files 9736 if (processedImages.length > 0) { 9737 try { 9738 console.log('Building and uploading NDJSON for gallery:', title, '(' + processedImages.length + ' images)'); 9739 const ndjsonContent = buildHydraClassicNdjson(itemGuid, processedImages); 9740 console.log('Generated NDJSON content length:', ndjsonContent.length, 'characters'); 9741 9742 const uploadResult = await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent); 9743 if (uploadResult && uploadResult.success) { 9744 try { 9745 // import_ format: treat as new Hydra NDJSON gallery (not classic-dependent) 9746 if (isImportFormat) { 9747 const hydraCollectionKey = 'GUID=' + currentSiteId; 9748 enhancedGalleryOptions.manualCollectionName = hydraCollectionKey; 9749 enhancedGalleryOptions.jsonCollections = { 9750 [hydraCollectionKey]: { 9751 isClassicCollection: false, 9752 guid: hydraCollectionKey, 9753 metadata: {}, 9754 totalItems: processedImages.length 9755 } 9756 }; 9757 enhancedGalleryOptions.siteAlias = window.Parameters?.SiteAlias || window.location?.hostname || currentSiteId; 9758 enhancedGalleryOptions.initialPageUuid = galleryPageId; 9759 enhancedGalleryOptions.isHydra = true; 9760 enhancedGalleryOptions.flyIoSaveTimestamp = Date.now(); 9761 console.log('NDJSON uploaded for import_ gallery "' + title + '" - configured as Hydra NDJSON (not classic)'); 9762 } else { 9763 enhancedGalleryOptions.jsonCollections[collectionKey].metadata = {}; 9764 enhancedGalleryOptions.jsonCollections[collectionKey].totalItems = processedImages.length; 9765 } 9766 9767 // Store dual upload metadata if Irys was successful 9768 if (uploadResult.irysResult && uploadResult.irysResult.txId) { 9769 enhancedGalleryOptions.txId = uploadResult.irysResult.txId; 9770 enhancedGalleryOptions.permaURL = uploadResult.irysResult.txId; // Plain txId for racing 9771 enhancedGalleryOptions.isPerma = true; 9772 enhancedGalleryOptions.uploadedTo = uploadResult.uploadedTo; 9773 enhancedGalleryOptions.fileSize = uploadResult.fileSize; 9774 enhancedGalleryOptions.siteId = currentSiteId; 9775 enhancedGalleryOptions.pageId = galleryPageId; 9776 console.log('â Dual upload complete - Irys txId:', uploadResult.irysResult.txId); 9777 console.log(' Uploaded to:', uploadResult.uploadedTo.join(' + ')); 9778 } else { 9779 console.log('â Single upload (Fly.io only) - No Irys data'); 9780 } 9781 } catch (applyErr) { 9782 console.warn('Failed to switch metadata to {} after NDJSON upload:', applyErr); 9783 } 9784 } else { 9785 console.warn('NDJSON upload failed for "' + title + '"; keeping inline metadata array'); 9786 } 9787 } catch (e) { 9788 console.warn('NDJSON build/upload skipped for "' + title + '" due to error:', e); 9789 } 9790 } else {
9791 console.log('Skipping NDJSON generation for "' + title + '" - no images to process'); 9792 } 9793 } 9794 9795 // Preserve additional metadata 9796 if (classicItem.galleryOptions.siteAlias) { 9797 enhancedGalleryOptions.siteAlias = classicItem.galleryOptions.siteAlias; 9798 } 9799 if (classicItem.galleryOptions.initialPageUuid) { 9800 enhancedGalleryOptions.initialPageUuid = classicItem.galleryOptions.initialPageUuid; 9801 } 9802 if (classicItem.galleryOptions.initialPageAlias) { 9803 enhancedGalleryOptions.initialPageAlias = classicItem.galleryOptions.initialPageAlias; 9804 } 9805 if (classicItem.galleryOptions.timestamp) { 9806 enhancedGalleryOptions.timestamp = classicItem.galleryOptions.timestamp; 9807 } 9808 } else if (isImportFormat && (classicItem.images || []).length > 0) { 9809 // import_ format without galleryOptions: still create NDJSON for new Hydra galleries 9810 const processedImages = classicItem.images || []; 9811 const collectionKey = 'GUID=' + itemGuid; 9812 enhancedGalleryOptions.jsonCollections = { 9813 [collectionKey]: { 9814 isClassicCollection: true, 9815 guid: collectionKey, 9816 metadata: processedImages, 9817 totalItems: processedImages.length 9818 } 9819 }; 9820 console.log('Created jsonCollections for import_ gallery (no galleryOptions):', title, '(' + processedImages.length + ' images)'); 9821 try { 9822 const ndjsonContent = buildHydraClassicNdjson(itemGuid, processedImages); 9823 const uploadResult = await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent); 9824 if (uploadResult && uploadResult.success) { 9825 const hydraCollectionKey = 'GUID=' + currentSiteId; 9826 enhancedGalleryOptions.manualCollectionName = hydraCollectionKey; 9827 enhancedGalleryOptions.jsonCollections = { 9828 [hydraCollectionKey]: { 9829 isClassicCollection: false, 9830 guid: hydraCollectionKey, 9831 metadata: {}, 9832 totalItems: processedImages.length 9833 } 9834 }; 9835 enhancedGalleryOptions.siteAlias = window.Parameters?.SiteAlias || window.location?.hostname || currentSiteId; 9836 enhancedGalleryOptions.initialPageUuid = galleryPageId; 9837 enhancedGalleryOptions.isHydra = true; 9838 enhancedGalleryOptions.flyIoSaveTimestamp = Date.now(); 9839 if (uploadResult.irysResult?.txId) { 9840 enhancedGalleryOptions.txId = uploadResult.irysResult.txId; 9841 enhancedGalleryOptions.permaURL = uploadResult.irysResult.txId; 9842 enhancedGalleryOptions.isPerma = true; 9843 enhancedGalleryOptions.uploadedTo = uploadResult.uploadedTo || ['fly']; 9844 } 9845 console.log('NDJSON uploaded for import_ gallery "' + title + '" (no galleryOptions) - configured as Hydra NDJSON'); 9846 } 9847 } catch (e) { 9848 console.warn('NDJSON build/upload skipped for import_ gallery "' + title + '":', e); 9849 } 9850 } 9851 9852 // Note: For import from pasted JSON, we don't include image data to avoid overwriting existing captions 9853 // Image data is only included when importing from Classic GUID directly 9854 console.log(`Gallery ${title} imported with structure and settings preserved (image data not included to avoid overwriting existing captions)`); 9855 9856 newItem = { 9857 id: newItemId, title, url: `/${finalSlug}`, isIntegrated: true, isPage: false, isSubmenu: false, visible: classicItem.visible !== false, parentId: null, 9858 siteId: currentSiteId, pageId: galleryPageId, classicCategoryId: classicItem.categoryID, classicGuid: itemGuid, 9859 importSource: 'classic-gallery', slug: finalSlug, normalizedName: finalSlug, 9860 galleryOptions: enhancedGalleryOptions 9861 }; 9862 } 9863 9864 tempNewItems.push(newItem); 9865 } 9866 9867 // --- Pass 2: Link children to parents using the ID map ---
9868 tempNewItems.forEach(newItem => { 9869 const originalId = Object.keys(originalIdToNewIdMap).find(key => originalIdToNewIdMap[key] === newItem.id); 9870 const originalItem = galleriesForImport.find(g => String(g.id) === String(originalId)); 9871 9872 if (originalItem && originalItem.parentId) { 9873 const newParentId = originalIdToNewIdMap[originalItem.parentId]; 9874 if (newParentId) { 9875 newItem.parentId = newParentId; 9876 const parentItem = tempNewItems.find(p => p.id === newParentId); 9877 if (parentItem) { 9878 parentItem.isSubmenu = true; 9879 parentItem.isCollapsed = true; 9880 } 9881 } else { 9882 newItem.parentId = submenuParentId; 9883 } 9884 } else { 9885 newItem.parentId = submenuParentId; 9886 } 9887 }); 9888 9889 if (replaceMenu) { 9890 console.log("Replacing existing menu structure."); 9891 // If replacing, overwrite the entire galleries array. 9892 galleries = tempNewItems; 9893 } else { 9894 console.log("Appending to existing menu structure."); 9895 // If not replacing, push the new items to the existing array. 9896 galleries.push(...tempNewItems); 9897 } 9898 9899 9900 9901 if (progressBar) progressBar.style.width = '90%'; 9902 if (statusMessage) statusMessage.textContent = 'Saving changes...'; 9903 9904 // Pass import flag to skip localStorage for large import operations 9905 await saveGalleries({ isImport: true }); 9906 9907 if (progressBar) progressBar.style.width = '100%'; 9908 if (statusMessage) statusMessage.textContent = `Successfully imported ${tempNewItems.length} items!`; 9909 if (statusEl) statusEl.classList.remove('importing'); 9910 9911 setTimeout(() => { 9912 renderGalleries(); 9913 toggleImportClassicForm(); 9914 if (progressBar) progressBar.style.width = '0'; 9915 if (statusEl) statusEl.style.display = 'none'; 9916 if (confirm('Import complete! Would you like to reload the page to ensure everything is displayed correctly?')) { 9917 window.location.reload(); 9918 } 9919 }, 1000); 9920 9921 } catch (error) { 9922 console.error('Error importing classic galleries:', error); 9923 if (statusMessage) statusMessage.textContent = `Error: ${error.message}`; 9924 if (progressBar) { 9925 progressBar.style.width = '100%'; 9926 progressBar.style.backgroundColor = '#dc3545'; 9927 } 9928 if (statusEl) statusEl.classList.remove('importing'); 9929 } 9930} 9931 9932 9933 9934/** 9935 * Improved toggleImportClassicForm - Closes other forms first 9936 */ 9937function toggleImportClassicForm() { 9938 // First check if user is logged in and in edit mode 9939 if (!isAuthenticated || !isAdmin || !window.isInEditMode()) { 9940 alert('You must be logged in as an admin and in edit mode to import galleries.'); 9941 return; 9942 } 9943 9944 const formId = 'importClassicForm'; 9945 const form = document.getElementById(formId); 9946 9947 if (!form) { 9948 console.error('Import form element not found'); 9949 return; 9950 } 9951 9952 // If this form is already open, just close everything 9953 if (window.currentOpenForm === formId) { 9954 closeAllForms(); 9955 return; 9956 } 9957 9958 // Otherwise close all forms and open this one 9959 closeAllForms(); 9960 9961 // Show the form 9962 form.style.display = 'block'; 9963 9964 // Use setTimeout to allow the display change to take effect before adding the class 9965 setTimeout(() => { 9966 form.classList.add('visible'); 9967 }, 10); 9968 9969 // Populate parent options 9970 const parentSelect = document.getElementById('importParent'); 9971 if (parentSelect) { 9972 parentSelect.innerHTML = '<option value="">Top Level (No Parent)</option>'; 9973 9974 // Add all galleries that could be parents (including submenus) 9975 galleries.forEach(gallery => { 9976 // Only include items that are not already marked as imported 9977 if (!gallery.importSource) { 9978 parentSelect.innerHTML += `<option value="${gallery.id}">${gallery.title}</option>`; 9979 } 9980 }); 9981 } 9982 9983 // Set default values 9984 const submenuTitle = document.getElementById('submenuTitle'); 9985 if (submenuTitle && !submenuTitle.value) { 9986 submenuTitle.value = 'Classic Galleries'; 9987 } 9988 9989 // Reset any previous import status 9990 const statusEl = document.getElementById('importStatus'); 9991 if (statusEl) { 9992 statusEl.style.display = 'none'; 9993 const progressBar = statusEl.querySelector('.progress-bar'); 9994 if (progressBar) { 9995 progressBar.style.width = '0'; 9996 progressBar.style.backgroundColor = '#4682B4'; // Reset to blue 9997 } 9998 } 9999 10000 // Set this as the current open form 10001 window.currentOpenForm = formId; 10002} 10003 10004// Add event listener for the radio buttons to adjust height when page is selected 10005document.addEventListener('DOMContentLoaded', function() { 10006 const galleryTypeRadios = document.querySelectorAll('input[name="galleryType"]'); 10007 galleryTypeRadios.forEach(radio => { 10008 radio.addEventListener('change', function() { 10009 const addForm = document.getElementById('addForm'); 10010 if (addForm && addForm.classList.contains('visible')) { 10011 const isPageSelected = this.value === 'page'; 10012 10013 // Give more height for page type 10014 if (isPageSelected) { 10015 addForm.style.maxHeight = '600px'; 10016 } else { 10017 // For other types, measure content height 10018 const contentHeight = addForm.scrollHeight + 30; 10019 addForm.style.maxHeight = Math.max(contentHeight, 500) + 'px'; 10020 } 10021 } 10022 }); 10023 }); 10024}); 10025 10026 10027/** 10028 * Improved toggleStyleEditor - Closes other forms first 10029 */ 10030function toggleStyleEditor(display = false, event) { 10031 const formId = 'menuStyleEditor'; 10032 const overlayOpen = window.MenuStyleCustomizer && window.MenuStyleCustomizer._overlayOpen; 10033 10034 if (overlayOpen && window.MenuStyleCustomizer._overlayMinimized && display !== false) { 10035 if (typeof window.MenuStyleCustomizer._toggleOverlayMinimized === 'function') { 10036 window.MenuStyleCustomizer._toggleOverlayMinimized(false); 10037 } 10038 return; 10039 } 10040 10041 if (display === false || window.currentOpenForm === formId || overlayOpen) { 10042 closeAllForms(); 10043 return; 10044 } 10045 10046 closeAllForms(); 10047 10048 if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.toggleOverlay === 'function') { 10049 window.MenuStyleCustomizer.toggleOverlay(); 10050 } 10051} 10052 10053 10054/** 10055 * This function closes all forms when escape key is pressed 10056 */ 10057document.addEventListener('keydown', function(e) { 10058 if (e.key === 'Escape' && window.currentOpenForm) { 10059 if (closeTopmostStyleCustomizerChildOverlay()) { 10060 return; 10061 } 10062 if (window.MenuStyleCustomizer && 10063 window.MenuStyleCustomizer._overlayOpen && 10064 window.MenuStyleCustomizer._overlayMinimized) { 10065 return; 10066 } 10067 closeAllForms(); 10068 } 10069}); 10070 10071/** 10072 * This function closes all forms when clicking outside any form 10073 */ 10074document.addEventListener('click', function(e) { 10075 if (!window.currentOpenForm) return; 10076 10077 if (window.MenuStyleCustomizer && 10078 window.MenuStyleCustomizer._overlayOpen && 10079 window.MenuStyleCustomizer._overlayMinimized) { 10080 return; 10081 } 10082 10083 // Keep Style Customizer and its nested panels open during in-overlay interactions. 10084 if (e.target.closest( 10085 '.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' 10086 )) { 10087 return; 10088 } 10089 10090 if (e.target.closest('.image-uploader-overlay')) { 10091 return; 10092 } 10093 10094 const currentForm = document.getElementById(window.currentOpenForm); 10095 if (!currentForm) { 10096 if (e.target.closest( 10097 '.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' 10098 )) { 10099 return; 10100 } 10101 closeAllForms(); 10102 return; 10103 } 10104 10105 const isInsideForm = currentForm.contains(e.target);
10106 const isFormToggleButton = e.target.closest('[onclick*="toggle"]') !== null; 10107 10108 if (isInsideForm || isFormToggleButton) { 10109 return; 10110 } 10111 10112 closeAllForms(); 10113}); 10114 10115// Override the original functions with our improved versions 10116window.toggleAddForm = toggleAddForm; 10117window.toggleStyleEditor = toggleStyleEditor; 10118window.toggleMetadataEditor = toggleMetadataEditor; 10119window.toggleSidebarElementForm = toggleSidebarElementForm; 10120window.toggleImportClassicForm = toggleImportClassicForm; 10121window.closeAllForms = closeAllForms; 10122window.closeTopmostStyleCustomizerChildOverlay = closeTopmostStyleCustomizerChildOverlay; 10123 10124 10125/** 10126 * Opens the edit form for a gallery item. 10127 * (UPDATED to include Meta Title and Meta Description fields, removed Parent Menu) 10128 * @param {number} id - The ID of the gallery item. 10129 * @param {Event} event - The click event. 10130 */ 10131function editGallery(id, event) { 10132 event.stopPropagation(); // Prevent event bubbling 10133 const gallery = findGalleryById(galleries, id); 10134 const li = getGalleryTreeListItem(id, event); 10135 10136 if (gallery && li) { 10137 const showUrlField = gallery.isPage || gallery.isIntegrated; 10138 const showExternalUrlField = gallery.isExternal; 10139 const showMenuParentOption = !gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && !gallery.isExternal && 10140 (isPageMenuItem(gallery) || gallery.isIntegrated === true); 10141 /** Sidebar meta title/description apply to gallery routes only; pages use the page metadata element. */ 10142 const showSidebarSeoFields = !gallery.isPage; 10143 const currentSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : ''); 10144 const currentExternalUrl = gallery.url || ''; 10145 10146 // Clear existing content and rebuild form to ensure event listeners are fresh if needed 10147 li.innerHTML = ''; // Clear previous form if any 10148 10149 const formDiv = document.createElement('div'); 10150 formDiv.className = 'edit-form'; 10151 formDiv.innerHTML = ` 10152 <div class="form-group"> 10153 <label>Title:</label> 10154 <input type="text" value="${gallery.title || ''}" class="edit-title"> 10155 </div> 10156 10157 ${showUrlField ? ` 10158 <div class="form-group"> 10159 <label>URL Slug:</label> 10160 <input type="text" value="${currentSlug}" class="edit-slug" placeholder="e.g., my-gallery"> 10161 <small>Path after domain name (e.g., yoursite.com<b>/my-gallery</b>). Use lowercase letters, numbers, and hyphens.</small> 10162 </div> 10163 ` : ''} 10164 10165 ${showExternalUrlField ? ` 10166 <div class="form-group"> 10167 <label>External URL:</label> 10168 <input type="text" value="${currentExternalUrl}" class="edit-external-url" placeholder="https://example.com"> 10169 <small>Full URL for the external link (e.g., https://example.com/page).</small> 10170 </div> 10171 ` : ''} 10172 10173 ${showSidebarSeoFields ? ` 10174 <div class="form-group"> 10175 <label>Meta Title (Optional):</label> 10176 <input type="text" value="${gallery.metaTitle || ''}" class="edit-meta-title" placeholder="Title for search results (if different from main title)"> 10177 <small>Overrides site title for this item in search results/browser tabs.</small> 10178 </div> 10179 10180 <div class="form-group"> 10181 <label>Meta Description (Optional):</label> 10182 <textarea class="edit-meta-description" placeholder="Description for search results (150-160 chars recommended)">${gallery.metaDescription || ''}</textarea> 10183 <small>Overrides site description for this item in search results.</small> 10184 </div> 10185 ` : ` 10186 <div class="form-group"> 10187 <p class="text-muted" style="margin: 0 0 8px 0; font-size: 12px; line-height: 1.4;"> 10188 Search title and description for this page are set in <strong>Page â metadata</strong> (the metadata block on the page), not here. 10189 </p> 10190 </div> 10191 `} 10192 10193 <div class="form-group"> 10194 <label style="display: flex; align-items: center; gap: 8px;"> 10195 <input type="checkbox" class="edit-hide-menu" ${gallery.hideMenuOnPage ? 'checked' : ''} style="width: auto; margin-bottom: 0;" onchange="onPageMenuChromeToggle(this)"> 10196 Hide menu when viewing this page 10197 </label> 10198 <small>Allows for a full-screen page or gallery layout. Menu remains visible in edit mode.</small> 10199 </div> 10200 10201 <div class="form-group"> 10202 <label style="display: flex; align-items: center; gap: 8px;"> 10203 <input type="checkbox" class="edit-overlay-menu" ${gallery.overlayMenuOnPage ? 'checked' : ''} style="width: auto; margin-bottom: 0;" onchange="onPageMenuChromeToggle(this)"> 10204 Show menu as overlay on this page 10205 </label> 10206 <small>Keeps menu items visible over a full-bleed page. Menu background is hidden. Solid menu remains in edit mode.</small> 10207 </div> 10208 10209 ${showMenuParentOption ? ` 10210 <div class="form-group"> 10211 <label style="display: flex; align-items: center; gap: 8px;"> 10212 <input type="checkbox" class="edit-acts-as-menu-parent" ${gallery.actsAsMenuParent === true ? 'checked' : ''} style="width: auto; margin-bottom: 0;"> 10213 Allow nested menu items 10214 </label> 10215 <small>Keeps this ${isPageMenuItem(gallery) ? 'page' : 'gallery'} clickable while allowing it to expand and contain sub-items.</small> 10216 </div> 10217 ` : ''} 10218 10219 <div class="form-group"> 10220 <label style="display: flex; align-items: center; gap: 8px;"> 10221 <input type="checkbox" class="edit-password-protected" ${gallery.passwordProtected ? 'checked' : ''} style="width: auto; margin-bottom: 0;"> 10222 Password protect this page 10223 </label> 10224 <small>Visitors will need to enter a password to access this page.</small> 10225 </div> 10226 10227 <div class="form-group password-field" style="display: ${gallery.passwordProtected ? 'block' : 'none'};"> 10228 <label>Password:</label> 10229 <input type="password" value="${gallery.password || ''}" class="edit-password" placeholder="Enter password"> 10230 <small>Password visitors must enter to access this page.</small> 10231 </div> 10232 10233 <div class="form-group"> 10234 <label style="display: flex; align-items: center; gap: 8px;"> 10235 <input type="checkbox" class="edit-no-index" ${(() => { 10236 // Explicit setting takes priority;
10236 otherwise default to checked for hidden non-home pages 10237 if (gallery.noIndex === true) return 'checked'; 10238 if (gallery.noIndex === false) return ''; 10239 return (gallery.visible === false && !gallery.isHomePage) ? 'checked' : ''; 10240 })()} style="width: auto; margin-bottom: 0;"> 10241 Hide from search engines (noindex) 10242 </label> 10243 <small>When checked, search engines will not index this page. Automatically enabled for hidden pages â uncheck to allow indexing even when hidden.</small> 10244 </div> 10245 10246 <div class="edit-form-actions"> 10247 <button class="btn btn-primary" id="saveEditBtn-${id}">Save</button> 10248 <button class="btn" id="cancelEditBtn-${id}">Cancel</button> 10249 </div> 10250 <br> 10251 `; 10252 li.appendChild(formDiv); // Append the new form 10253 10254 // Add event listeners programmatically 10255 const saveButton = formDiv.querySelector(`#saveEditBtn-${id}`); 10256 if (saveButton) { 10257 saveButton.addEventListener('click', (e) => saveEdit(id, e)); 10258 } 10259 10260 const cancelButton = formDiv.querySelector(`#cancelEditBtn-${id}`); 10261 if (cancelButton) { 10262 cancelButton.addEventListener('click', () => renderGalleries()); 10263 } 10264 10265 // Add password protection toggle functionality 10266 const passwordProtectedCheckbox = formDiv.querySelector('.edit-password-protected'); 10267 const passwordField = formDiv.querySelector('.password-field'); 10268 if (passwordProtectedCheckbox && passwordField) { 10269 passwordProtectedCheckbox.addEventListener('change', function() { 10270 passwordField.style.display = this.checked ? 'block' : 'none'; 10271 }); 10272 } 10273 10274 li.classList.add('edit-mode'); 10275 } else { 10276 console.error("Could not find gallery or list item for ID:", id); 10277 } 10278} 10279 10280/** 10281 * Resolve the correct sidebar #galleryTree li for an id. Horizontal dropdowns also use 10282 * li[data-id] (.horizontal-submenu-item); document.querySelector would return that node first 10283 * and break nested folder toggles in edit mode while horizontal layout is on. 10284 * @param {number} id - Gallery ID 10285 * @param {Event|null} event - Optional click event (preferred: closest li inside #galleryTree) 10286 * @returns {HTMLElement|null} 10287 */ 10288function getGalleryTreeListItem(id, event) { 10289 var numericId = typeof id === 'number' ? id : parseInt(String(id), 10); 10290 if (isNaN(numericId)) return null; 10291 var galleryTreeEl = document.getElementById('galleryTree'); 10292 if (event && event.target && galleryTreeEl) { 10293 var clicked = event.target.closest('li[data-id]'); 10294 if (clicked && galleryTreeEl.contains(clicked)) { 10295 var cid = parseInt(clicked.getAttribute('data-id') || '', 10); 10296 if (cid === numericId) return clicked; 10297 } 10298 } 10299 if (galleryTreeEl) { 10300 var scoped = galleryTreeEl.querySelector('li[data-id="' + numericId + '"]'); 10301 if (scoped) return scoped; 10302 } 10303 return document.querySelector('li[data-id="' + numericId + '"]'); 10304} 10305 10306/** 10307 * Handles click on a folder item to show/hide its contents 10308 * @param {number} id - The gallery ID 10309 * @param {Event} event - The click event 10310 */ 10311function toggleFolder(id, event) { 10312 // Ensure the event doesn't interfere with drag operations 10313 if (event) { 10314 event.stopPropagation(); 10315 } 10316 10317 10318 const gallery = galleries.find(g => g.id === id); 10319 if (!gallery) { 10320 return; 10321 } 10322 10323 if (gallery.isFolder !== true && gallery.isSubmenu !== true && !isNavigableMenuParent(gallery)) { 10324 return; 10325 } 10326 10327 10328 // Toggle the collapsed state 10329 gallery.isCollapsed = !gallery.isCollapsed; 10330 10331 // Find the list item in the DOM (#galleryTree first â not horizontal li.horizontal-submenu-item) 10332 const listItem = getGalleryTreeListItem(id, event); 10333 if (!listItem) { 10334 return; 10335 } 10336 10337 10338 // Toggle the expanded class FIRST - this is important for CSS to work correctly 10339 listItem.classList.toggle('expanded', !gallery.isCollapsed); 10340 10341 // Keep ARIA in sync for folder label + chevron (screen readers / keyboard) 10342 const folderExpanded = !gallery.isCollapsed; 10343 listItem.querySelectorAll('.menu-item.folder, .submenu-toggle').forEach((el) => { 10344 el.setAttribute('aria-expanded', folderExpanded ? 'true' : 'false'); 10345 }); 10346 10347 // Force a reflow after toggling the class to ensure CSS recalculates 10348 void listItem.offsetHeight; 10349 10350 // Find the nested list - SIMPLIFIED: Check if we're in sidebar first, then horizontal 10351 // CRITICAL: Check #galleryTree first - this is the definitive indicator of sidebar layout 10352 // Use contains() instead of closest() because closest() only checks ancestors 10353 const galleryTree = document.getElementById('galleryTree'); 10354 const isInGalleryTree = galleryTree && galleryTree.contains(listItem); 10355 const isInSidebar = listItem.closest('.sidebar') || listItem.closest('#sidebar') || isInGalleryTree; 10356 // Only consider it horizontal menu if it's explicitly in .horizontal-menu
10356-container AND not in galleryTree 10357 const isInHorizontalMenu = listItem.closest('.horizontal-menu-container') && !isInGalleryTree && !isInSidebar; 10358 10359 let nestedList = null; 10360 10361 // If we're in sidebar (#galleryTree), ONLY look for ol.nested-sortable - ignore horizontal structures 10362 // CRITICAL: Prioritize sidebar detection - if we're in galleryTree, we're definitely in sidebar 10363 if (isInGalleryTree) { 10364 nestedList = listItem.querySelector('ol.nested-sortable'); 10365 10366 // Also check direct children as fallback 10367 if (!nestedList) { 10368 const directChildren = Array.from(listItem.children); 10369 nestedList = directChildren.find(child => child.tagName === 'OL' && child.classList.contains('nested-sortable')); 10370 } 10371 } else if (isInSidebar) { 10372 // Fallback: if we're in .sidebar but not in #galleryTree, still treat as sidebar 10373 nestedList = listItem.querySelector('ol.nested-sortable'); 10374 if (!nestedList) { 10375 const directChildren = Array.from(listItem.children); 10376 nestedList = directChildren.find(child => child.tagName === 'OL' && child.classList.contains('nested-sortable')); 10377 } 10378 } 10379 10380 // For horizontal layout, check horizontal structures (ONLY if NOT in sidebar/galleryTree) 10381 if (!nestedList && isInHorizontalMenu && !isInGalleryTree && !isInSidebar) { 10382 nestedList = listItem.querySelector('.horizontal-submenu'); 10383 if (!nestedList) { 10384 nestedList = listItem.querySelector('.horizontal-dropdown'); 10385 } 10386 if (!nestedList) { 10387 // For horizontal nested dropdowns, they're appended to document.body, not the list item 10388 // They're linked via data-flyout-id attribute 10389 const flyoutId = listItem.getAttribute('data-flyout-id'); 10390 if (flyoutId) { 10391 // Look specifically for div.horizontal-nested-dropdown, not the list item itself 10392 const allElementsWithId = document.querySelectorAll('[data-flyout-id="' + flyoutId + '"]'); 10393 10394 // Find the actual dropdown div, not the list item 10395 const flyout = Array.from(allElementsWithId).find(el => 10396 el.classList.contains('horizontal-nested-dropdown') && el !== listItem 10397 ); 10398 10399 10400 if (flyout) { 10401 nestedList = flyout.querySelector('.horizontal-nested-list') || flyout; 10402 } 10403 } 10404 } 10405 if (!nestedList) { 10406 nestedList = listItem.querySelector('.horizontal-nested-dropdown'); 10407 } 10408 } 10409 10410 // Fallback: try all selectors if not found yet 10411 if (!nestedList) { 10412 nestedList = listItem.querySelector('ol.nested-sortable'); 10413 } 10414 if (!nestedList) { 10415 // Debug: Log the actual DOM structure to understand what's there 10416 10417 // Try to find any nested structure at all 10418 const anyNested = listItem.querySelector('ol, ul, div[class*="submenu"], div[class*="nested"], div[class*="dropdown"]'); 10419 if (anyNested) { 10420 nestedList = anyNested; // Use it anyway 10421 } 10422 10423 // Last resort: Check if nested list exists as a direct child but wasn't found by querySelector 10424 // This can happen if the element was removed/re-added or if there's a timing issue 10425 const directChildren = Array.from(listItem.children); 10426 const olChild = directChildren.find(child => child.tagName === 'OL'); 10427 if (olChild) { 10428 nestedList = olChild; 10429 } 10430 } 10431 10432 if (!nestedList) { 10433 10434 // If the nested list doesn't exist, we need to create it 10435 // This can happen if children aren't rendered until the folder is opened 10436 // First try flat array 10437 let gallery = galleries.find(g => g.id === id); 10438 10439 // If gallery not found or has no children in flat array, build tree structure to find it 10440 if (!gallery || !gallery.children || gallery.children.length === 0) { 10441 10442 // Build tree structure from flat array 10443 const buildTree = (items, parentId = null) => { 10444 return items 10445 .filter(item => item.parentId === parentId) 10446 .map(item => ({ 10447 ...item, 10448 children: buildTree(items, item.id) 10449 })); 10450 }; 10451 10452 const galleryTree = buildTree(galleries); 10453 10454 // Recursive function to find gallery in tree 10455 const findInTree = (items) => { 10456 for (const item of items) { 10457 if (item.id === id) { 10458 return item; 10459 } 10460 if (item.children && item.children.length > 0) { 10461 const found = findInTree(item.children); 10462 if (found) return found; 10463 } 10464 } 10465 return null; 10466 }; 10467 10468 const treeGallery = findInTree(galleryTree); 10469 if (treeGallery) { 10470 gallery = treeGallery; 10471 } 10472 } 10473 10474 if (gallery && gallery.children && gallery.children.length > 0) { 10475 10476 // Determine nesting level from list item 10477 const currentLevel = parseInt(listItem.getAttribute('data-nesting-level') || '0', 10); 10478 const nextLevel = currentLevel + 1; 10479 10480 // Create the nested list element 10481 nestedList = document.createElement('ol'); 10482 nestedList.id = 'gallery-children-' + id; 10483 nestedList.className = 'nested-sortable'; 10484 if (nextLevel > 0) { 10485 nestedList.classList.add('nested-level-' + nextLevel); 10486 } 10487 10488 // Render the children - ensure we're in the right context (not in a horizontal dropdown wrapper)
10489 gallery.children.forEach(child => { 10490 const childHtml = renderGalleryItem(child, nextLevel, 20); 10491 const tempDiv = document.createElement('div'); 10492 tempDiv.innerHTML = childHtml; 10493 const childLi = tempDiv.querySelector('li'); 10494 if (childLi) { 10495 nestedList.appendChild(childLi); 10496 } 10497 }); 10498 10499 // CRITICAL: Only check for horizontal dropdowns if we're actually in a horizontal menu context 10500 // If we're in #galleryTree (sidebar), skip all horizontal menu logic 10501 const galleryTree = document.getElementById('galleryTree'); 10502 const isInGalleryTree = galleryTree && galleryTree.contains(listItem); 10503 const isInSidebar = listItem.closest('.sidebar') || listItem.closest('#sidebar') || isInGalleryTree; 10504 10505 if (!isInGalleryTree && !isInSidebar) { 10506 const isInHorizontalMenu = listItem.closest('.horizontal-menu-container'); 10507 if (isInHorizontalMenu) { 10508 const horizontalDropdown = listItem.closest('.horizontal-dropdown, .horizontal-nested-dropdown'); 10509 if (horizontalDropdown && horizontalDropdown !== listItem) { 10510 horizontalDropdown.style.setProperty('display', 'block', 'important'); 10511 horizontalDropdown.style.setProperty('visibility', 'visible', 'important'); 10512 horizontalDropdown.style.setProperty('opacity', '1', 'important'); 10513 } 10514 } 10515 } 10516 10517 // CRITICAL: Ensure list item itself is visible and has correct display 10518 listItem.style.setProperty('display', 'list-item', 'important'); 10519 listItem.style.setProperty('visibility', 'visible', 'important'); 10520 10521 // CRITICAL: Ensure ALL parent elements up to the sidebar are visible 10522 // BUT: Don't make .horizontal-menu-container visible - that's the entire horizontal menu! 10523 let parent = listItem.parentElement; 10524 let depth = 0; 10525 while (parent && depth < 10) { 10526 // Skip .horizontal-menu-container - we don't want to show the entire horizontal menu 10527 if (parent.classList.contains('horizontal-menu-container')) { 10528 parent = parent.parentElement; 10529 depth++; 10530 continue; 10531 } 10532 10533 const parentStyles = window.getComputedStyle(parent); 10534 if (parentStyles.display === 'none' || parentStyles.visibility === 'hidden' || parentStyles.opacity === '0') { 10535 if (parentStyles.display === 'none') { 10536 parent.style.setProperty('display', 'block', 'important'); 10537 } 10538 if (parentStyles.visibility === 'hidden') { 10539 parent.style.setProperty('visibility', 'visible', 'important'); 10540 } 10541 if (parentStyles.opacity === '0') { 10542 parent.style.setProperty('opacity', '1', 'important'); 10543 } 10544 } 10545 // Stop at sidebar or galleryTree 10546 if (parent.classList.contains('sidebar') || parent.id === 'galleryTree' || parent.id === 'sidebar') { 10547 break; 10548 } 10549 parent = parent.parentElement; 10550 depth++; 10551 } 10552 10553 // Append directly to the list item (not to any wrapper) 10554 // Make sure we're not appending to a hidden parent 10555 listItem.appendChild(nestedList); 10556 10557 // Immediately set the nested list to be visible since we're opening it 10558 nestedList.style.setProperty('display', 'block', 'important'); 10559 nestedList.style.setProperty('visibility', 'visible', 'important'); 10560 nestedList.style.setProperty('opacity', '1', 'important'); 10561 nestedList.style.setProperty('height', 'auto', 'important'); 10562 nestedList.style.setProperty('max-height', 'none', 'important'); 10563 10564 // Force immediate reflow after appending 10565 void nestedList.offsetHeight; 10566 void listItem.offsetHeight; 10567 } 10568 } 10569 10570 if (nestedList) { 10571 if (!gallery.isCollapsed) { 10572 // Show the nested list - ensure parent has expanded class FIRST 10573 // CRITICAL: Add expanded class BEFORE any style operations 10574 listItem.classList.add('expanded'); 10575 10576 // Verify expanded class was added 10577 if (!listItem.classList.contains('expanded')) { 10578 listItem.className = listItem.className + ' expanded'; 10579 } 10580 10581 // CRITICAL: Ensure parent li is visible (it might be hidden by CSS) 10582 const parentVisibility = window.getComputedStyle(listItem).visibility; 10583 if (parentVisibility === 'hidden') { 10584 listItem.style.setProperty('visibility', 'visible', 'important'); 10585 } 10586 10587 // Force a reflow to ensure CSS recalculates after adding expanded class 10588 void listItem.offsetHeight; 10589 void nestedList.offsetHeight; 10590 10591 // Show the nested list with !important to override CSS 10592 // Set these BEFORE removing any existing inline styles to avoid flicker 10593 nestedList.style.setProperty('display', 'block', 'important'); 10594 nestedList.style.setProperty('visibility', 'visible', 'important'); 10595 nestedList.style.setProperty('opacity', '1', 'important'); 10596 nestedList.style.setProperty('height', 'auto', 'important'); 10597 nestedList.style.setProperty('max-height', 'none', 'important'); 10598 nestedList.style.setProperty('overflow', 'visible', 'important'); 10599 10600 // Also ensure the nested list is not clipped by parent overflow
10601 // BUT: Don't modify .horizontal-menu-container overflow - that's the entire horizontal menu! 10602 let checkParent = nestedList.parentElement; 10603 let checkDepth = 0; 10604 while (checkParent && checkDepth < 5) { 10605 // Skip .horizontal-menu-container - we don't want to modify the entire horizontal menu 10606 if (checkParent.classList.contains('horizontal-menu-container')) { 10607 checkParent = checkParent.parentElement; 10608 checkDepth++; 10609 continue; 10610 } 10611 10612 const parentOverflow = window.getComputedStyle(checkParent).overflow; 10613 if (parentOverflow === 'hidden') { 10614 checkParent.style.setProperty('overflow', 'visible', 'important'); 10615 } 10616 if (checkParent.classList.contains('sidebar') || checkParent.id === 'galleryTree') { 10617 break; 10618 } 10619 checkParent = checkParent.parentElement; 10620 checkDepth++; 10621 } 10622 10623 // Force another reflow after setting styles 10624 void nestedList.offsetHeight; 10625 10626 const finalRect = nestedList.getBoundingClientRect(); 10627 10628 // If the nested list has zero dimensions, it's not actually visible 10629 if (finalRect.width === 0 || finalRect.height === 0) { 10630 // Force all parent elements to be visible 10631 // BUT: Don't modify .horizontal-menu-container - that's the entire horizontal menu! 10632 let forceParent = nestedList.parentElement; 10633 while (forceParent && forceParent !== document.body) { 10634 // Skip .horizontal-menu-container - we don't want to show the entire horizontal menu 10635 if (forceParent.classList.contains('horizontal-menu-container')) { 10636 forceParent = forceParent.parentElement; 10637 continue; 10638 } 10639 10640 forceParent.style.setProperty('display', 'block', 'important'); 10641 forceParent.style.setProperty('visibility', 'visible', 'important'); 10642 forceParent.style.setProperty('opacity', '1', 'important'); 10643 forceParent.style.setProperty('overflow', 'visible', 'important'); 10644 if (forceParent.classList.contains('sidebar') || forceParent.id === 'galleryTree') { 10645 break; 10646 } 10647 forceParent = forceParent.parentElement; 10648 } 10649 void nestedList.offsetHeight; 10650 } 10651 10652 // Check for any parent elements that might be hiding the nested list and fix them 10653 let parent = nestedList.parentElement; 10654 let depth = 0; 10655 while (parent && depth < 5) { 10656 const parentStyles = window.getComputedStyle(parent); 10657 if (parentStyles.display === 'none' || parentStyles.visibility === 'hidden' || parentStyles.opacity === '0') { 10658 10659 // Fix hidden parent elements 10660 if (parentStyles.display === 'none') { 10661 parent.style.setProperty('display', 'block', 'important'); 10662 } 10663 if (parentStyles.visibility === 'hidden') { 10664 parent.style.setProperty('visibility', 'visible', 'important'); 10665 } 10666 if (parentStyles.opacity === '0') { 10667 parent.style.setProperty('opacity', '1', 'important'); 10668 } 10669 } 10670 parent = parent.parentElement; 10671 depth++; 10672 } 10673 10674 } else { 10675 // Hide the nested list 10676 nestedList.style.setProperty('display', 'none', 'important'); 10677 nestedList.style.setProperty('visibility', 'hidden', 'important'); 10678 nestedList.style.setProperty('opacity', '0', 'important'); 10679 } 10680 10681 // Force multiple reflows to ensure the browser applies all changes 10682 void listItem.offsetHeight; 10683 void nestedList.offsetHeight; 10684 void listItem.offsetWidth; 10685 void nestedList.offsetWidth; 10686 10687 // Force a more aggressive repaint by temporarily toggling a class 10688 // This is especially important for sidebar layout where CSS rules might be interfer
10688ing 10689 listItem.classList.add('force-reflow'); 10690 void listItem.offsetHeight; 10691 listItem.classList.remove('force-reflow'); 10692 10693 // Force reflow again after class manipulation 10694 void listItem.offsetHeight; 10695 void nestedList.offsetHeight; 10696 10697 // Use requestAnimationFrame with multiple frames to ensure visual update 10698 // This ensures the browser has time to recalculate layout and repaint 10699 requestAnimationFrame(() => { 10700 void nestedList.offsetHeight; 10701 // Second frame to ensure all CSS has been recalculated 10702 requestAnimationFrame(() => { 10703 void nestedList.offsetHeight; 10704 // Verify the final state 10705 const finalDisplay = window.getComputedStyle(nestedList).display; 10706 const finalVisibility = window.getComputedStyle(nestedList).visibility; 10707 10708 // If still not visible, force one more repaint cycle 10709 if (!gallery.isCollapsed && finalDisplay === 'none') { 10710 nestedList.style.setProperty('display', 'block', 'important'); 10711 nestedList.style.setProperty('visibility', 'visible', 'important'); 10712 void nestedList.offsetHeight; 10713 requestAnimationFrame(() => { 10714 void nestedList.offsetHeight; 10715 }); 10716 } 10717 }); 10718 }); 10719 } 10720 10721 // Find the submenu toggle icon and rotate it 10722 const toggleIcon = listItem.querySelector('.toggle-icon'); 10723 if (toggleIcon) { 10724 toggleIcon.classList.toggle('rotated', !gallery.isCollapsed); 10725 } 10726 10727 10728 // Folder open/closed is a local UI preference â do not POST the full site config. 10729 try { 10730 const folderStates = JSON.parse(localStorage.getItem('folder_states') || '{}'); 10731 folderStates[id] = !gallery.isCollapsed; 10732 localStorage.setItem('folder_states', JSON.stringify(folderStates)); 10733 } catch (e) { 10734 console.error('Error saving folder state to localStorage:', e); 10735 } 10736} 10737 10738window.toggleFolder = toggleFolder; 10739 10740/** 10741 * Toggle password protection for a gallery/page 10742 * @param {number} id - The gallery ID 10743 * @param {Event} event - The click event 10744 */ 10745function toggleGalleryPasswordProtection(id, event) { 10746 event.stopPropagation(); 10747 10748 const gallery = findGalleryById(galleries, id); 10749 if (!gallery) { 10750 console.error("Could not find gallery for password protection toggle, ID:", id); 10751 return; 10752 } 10753 10754 if (gallery.passwordProtected) { 10755 // Remove password protection 10756 if (confirm('Remove password protection from this page?')) { 10757 gallery.passwordProtected = false; 10758 delete gallery.password; 10759 saveGalleries(); 10760 renderGalleries(); 10761 } 10762 } else { 10763 // Add password protection 10764 const password = prompt('Enter a password for this page:'); 10765 if (password && password.trim()) { 10766 gallery.passwordProtected = true; 10767 gallery.password = password.trim(); 10768 saveGalleries(); 10769 renderGalleries(); 10770 } 10771 } 10772} 10773 10774window.toggleGalleryPasswordProtection = toggleGalleryPasswordProtection; 10775 10776function onPageMenuChromeToggle(checkbox) { 10777 if (!checkbox || !checkbox.checked) return; 10778 const root = checkbox.closest ? checkbox.closest('li') : null; 10779 if (!root) return; 10780 const siblingClass = checkbox.classList.contains('edit-hide-menu') ? '.edit-overlay-menu' : '.edit-hide-menu'; 10781 const sibling = root.querySelector(siblingClass); 10782 if (sibling && sibling !== checkbox) sibling.checked = false; 10783} 10784window.onPageMenuChromeToggle = onPageMenuChromeToggle; 10785 10786/** 10787 * Saves the edited gallery item details. 10788 * (UPDATED to handle Meta Title and Meta Description, removed Parent Menu logic) 10789 * @param {number} id - The ID of the gallery item. 10790 * @param {Event} event - The click event. 10791 */ 10792function saveEdit(id, event) { 10793 event.stopPropagation(); 10794 const li = getGalleryTreeListItem(id, event); 10795 if (!li) { 10796 console.error("Could not find list item for saveEdit, ID:", id); 10797 return; 10798 } 10799 10800 const titleInput = li.querySelector('.edit-title'); 10801 const slugInput = li.querySelector('.edit-slug'); 10802 const externalUrlInput = li.querySelector('.edit-external-url'); 10803 const metaTitleInput = li.querySelector('.edit-meta-title'); 10804 const metaDescriptionInput = li.querySelector('.edit-meta-description'); 10805 const hideMenuInput = li.querySelector('.edit-hide-menu'); 10806 const overlayMenuInput = li.querySelector('.edit-overlay-menu'); 10807 const actsAsMenuParentInput = li.querySelector('.edit-acts-as-menu-parent'); 10808 const passwordProtectedInput = li.querySelector('.edit-password-protected'); 10809 const passwordInput = li.querySelector('.edit-password'); 10810 const noIndexInput = li.querySelector('.edit-no-index'); 10811 10812 const title = titleInput ? titleInput.value.trim() : ''; 10813 const newSlugRaw = slugInput ? slugInput.value.trim() : null; 10814 const externalUrl = externalUrlInput ? externalUrlInput.value.trim() : ''; 10815 const metaTitle = metaTitleInput ? metaTitleInput.value.trim() : ''; 10816 const metaDescription = metaDescriptionInput ? metaDescriptionInput.value.trim() : ''; 10817 const hideMenu = hideMenuInput ? hideMenuInput.checked : false;
10818 const overlayMenu = overlayMenuInput ? overlayMenuInput.checked : false; 10819 const passwordProtected = passwordProtectedInput ? passwordProtectedInput.checked : false; 10820 const password = passwordInput ? passwordInput.value.trim() : ''; 10821 // noIndex is stored explicitly so it survives visibility changes without reverting 10822 const noIndex = noIndexInput ? noIndexInput.checked : undefined; 10823 10824 const gallery = findGalleryById(galleries, id); 10825 if (!gallery) { 10826 console.error("Could not find gallery object for saveEdit, ID:", id); 10827 renderGalleries(); 10828 return; 10829 } 10830 10831 if (!title && !gallery.isSpacer) { 10832 alert('Please enter a title'); 10833 return; 10834 } 10835 10836 // Validate password protection 10837 if (passwordProtected && !password) { 10838 alert('Please enter a password when password protection is enabled'); 10839 return; 10840 } 10841 10842 // Only mutate the opt-in property when the user explicitly changes this checkbox. 10843 // This preserves the serialized shape and behavior of every existing item. 10844 if (actsAsMenuParentInput) { 10845 const wasNavigableMenuParent = gallery.actsAsMenuParent === true; 10846 const wantsNavigableMenuParent = actsAsMenuParentInput.checked === true; 10847 if (wasNavigableMenuParent && !wantsNavigableMenuParent) { 10848 const hasNestedItems = galleries.some(item => item.parentId === gallery.id); 10849 if (hasNestedItems) { 10850 alert('Move all nested menu items out of this item before turning off âAllow nested menu itemsâ.'); 10851 return; 10852 } 10853 delete gallery.actsAsMenuParent; 10854 } else if (!wasNavigableMenuParent && wantsNavigableMenuParent) { 10855 gallery.actsAsMenuParent = true; 10856 } 10857 } 10858 10859 gallery.title = title; 10860 // Sidebar SEO fields are only shown for galleries; do not overwrite stored values when saving a page. 10861 if (!gallery.isPage) { 10862 gallery.metaTitle = metaTitle; 10863 gallery.metaDescription = metaDescription; 10864 } 10865 // Hide-menu wins if both are somehow checked; overlay is opt-in and omitted when off. 10866 if (hideMenu) { 10867 gallery.hideMenuOnPage = true; 10868 delete gallery.overlayMenuOnPage; 10869 } else { 10870 gallery.hideMenuOnPage = hideMenu; 10871 if (overlayMenu) { 10872 gallery.overlayMenuOnPage = true; 10873 } else { 10874 delete gallery.overlayMenuOnPage; 10875 } 10876 } 10877 // Persist the explicit noIndex choice so it survives future visibility toggles 10878 if (noIndex !== undefined) gallery.noIndex = noIndex; 10879 10880 // Save password protection settings 10881 gallery.passwordProtected = passwordProtected; 10882 if (passwordProtected) { 10883 gallery.password = password; 10884 } else { 10885 delete gallery.password; // Remove password if protection is disabled 10886 } 10887 10888 if ((gallery.isPage || gallery.isIntegrated) && newSlugRaw !== null) { 10889 const originalSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : ''); 10890 let sanitizedSlug = slugify(newSlugRaw); 10891 10892 if (!sanitizedSlug) { 10893 sanitizedSlug = slugify(gallery.title); 10894 } 10895 10896 if (sanitizedSlug !== originalSlug) { 10897 const otherGalleries = galleries.filter(g => g.id !== id); 10898 gallery.slug = ensureUniqueSlug(sanitizedSlug, otherGalleries); 10899 gallery.url = `/${gallery.slug}`; 10900 } else { 10901 if (gallery.url !== `/${gallery.slug}`) { 10902 gallery.url = `/${gallery.slug}`; 10903 } 10904 } 10905 } 10906 10907 // Update external URL if this is an external link 10908 if (gallery.isExternal && externalUrl) { 10909 gallery.url = externalUrl; 10910 } 10911 10912 saveGalleries(); 10913 renderGalleries(); 10914 10915 // If the currently active gallery was just edited, re-apply menu visibility 10916 if (gallery.id === activeGalleryId) { 10917 const bodyEl = document.body; 10918 if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 10919 bodyEl.classList.add('menu-hidden-on-page'); 10920 } else { 10921 bodyEl.classList.remove('menu-hidden-on-page'); 10922 } 10923 applyMenuOverlayClassForGallery(gallery); 10924 } 10925} 10926// Helper function to check if a gallery is an ancestor of another 10927function hasAncestor(galleryId, ancestorId) { 10928if (galleryId === ancestorId) return true; 10929 10930const gallery = galleries.find(g => g.id === galleryId); 10931if (!gallery || !gallery.parentId) return false; 10932 10933if (gallery.parentId === ancestorId) return true; 10934 10935return hasAncestor(gallery.parentId, ancestorId); 10936} 10937 10938function deleteGallery(id, event) { 10939 event.stopPropagation(); 10940 const gallery = galleries.find(g => g.id === id); 10941 const title = gallery && (gallery.title || gallery.slug || 'this item') || 'this item'; 10942 if (!confirm('Are you sure you want to delete "' + title + '"?')) { 10943 return; 10944 }
10945 const overlay = showDeletionOverlay(); 10946 overlay.updateMessage('Saving...'); 10947 galleries = galleries.filter(g => g.id !== id); 10948 if (typeof window !== 'undefined') { 10949 window.galleries = galleries; 10950 } 10951 if (activeGalleryId === id) { 10952 activeGalleryId = null; 10953 const frame = document.getElementById('galleryFrame'); 10954 if (frame) frame.src = ''; 10955 } 10956 saveGalleries() 10957 .then(function(success) { 10958 renderGalleries(); 10959 overlay.hide(); 10960 if (success === false) { 10961 alert('Failed to delete. Please try again.'); 10962 } 10963 }) 10964 .catch(function(err) { 10965 overlay.hide(); 10966 console.error('Error deleting item:', err); 10967 alert('Failed to delete. Please try again.'); 10968 }); 10969} 10970 10971function debugNestedStructure() { 10972console.group('Current Gallery Structure'); 10973 10974function printGallery(gallery, level = 0) { 10975const indent = ' '.repeat(level); 10976console.log(`${indent}${gallery.title} (ID: ${gallery.id}, Parent: ${gallery.parentId || 'none'})`); 10977 10978if (gallery.children && gallery.children.length > 0) { 10979 gallery.children.forEach(child => printGallery(child, level + 1)); 10980} 10981} 10982 // Create a tree structure for debugging 10983const galleryTree = createGalleryTree(galleries); 10984galleryTree.forEach(gallery => printGallery(gallery)); 10985 10986console.groupEnd(); 10987} 10988 10989function toggleHome(id, event) { 10990 event.stopPropagation(); 10991 10992 console.log('Setting home page to gallery:', id); 10993 10994 // Find gallery 10995 const gallery = galleries.find(g => g.id === id); 10996 if (!gallery) return; 10997 10998 // Check if this gallery is already set as home 10999 const alreadyHome = gallery.isHomePage === true; 11000 11001 // Set all galleries to not be the home page 11002 galleries.forEach(g => { 11003 g.isHomePage = false; 11004 }); 11005 11006 // Toggle this gallery's home status - if it was already home, we're unsetting it 11007 gallery.isHomePage = !alreadyHome; 11008 11009 // Save changes 11010 saveGalleries(); 11011 11012 // Update UI 11013 renderGalleries(); 11014 11015 // If this gallery is now the home page, display a message 11016 if (gallery.isHomePage) { 11017 const title = gallery.title || 'This page'; 11018 showSuccessMessage(`${title} is now set as the Home page`); 11019 } 11020} 11021 11022// Function to toggle gallery visibility 11023function toggleVisibility(id, event) { 11024 event.stopPropagation(); 11025 11026 const gallery = galleries.find(g => g.id === id); 11027 if (gallery) { 11028 // Toggle the visibility 11029 gallery.visible = gallery.visible === false ? true : false; 11030 11031 // Save the change to server 11032 saveGalleries(); 11033 11034 // Update UI - important to toggle the class on the button itself 11035 const toggle = event.currentTarget; 11036 if (toggle) { 11037 if (gallery.visible === false) { 11038 toggle.classList.add('hidden'); 11039 11040 // Update the SVG icon 11041 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>'; 11042 } else { 11043 toggle.classList.remove('hidden'); 11044 11045 // Update the SVG icon 11046 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>'; 11047 } 11048 } 11049 11050 // Update parent row class (#galleryTree row, not horizontal submenu li) 11051 const listItem = getGalleryTreeListItem(id, event); 11052 if (listItem) { 11053 if (gallery.visible === false) { 11054 listItem.classList.add('hidden-gallery'); 11055 } else { 11056 listItem.classList.remove('hidden-gallery'); 11057 } 11058 } 11059 11060 // Only clear iframe if not in edit mode 11061 if (id === activeGalleryId && gallery.visible === false && !isEditing) { 11062 document.getElementById('galleryFrame').src = ''; 11063 } 11064 11065 // Re-render menus based on current layout 11066 const currentLayout = document.body.classList.contains('menu-layout-horizontal') ? 'horizontal' : 11067 document.body.classList.contains('menu-layout-top') ? 'top' : 'sidebar'; 11068 11069 if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') { 11070 window.renderHorizontalMenu(); 11071 } 11072 11073 // Also re-render the sidebar gallery tree to ensure hidden-gallery class is properly applied 11074 // This is especially important when horizontal layout is active but we're viewing the sidebar in edit mode 11075 if (typeof window.renderGalleries === 'function') { 11076 window.renderGalleries(); 11077 } else if (typeof renderGalleries === 'function') { 11078 renderGalleries(); 11079 } 11080 } 11081} 11082 11083 11084 11085/** 11086* Simplified loadGallery function that follows the same content swap pattern 11087* @param {number} id - The gallery ID 11088* @param {Event} event - Optional event object 11089*/ 11090// Enhanced loadGallery function with script cleanup 11091async function loadGallery(id, event) { 11092 11093// Stop event propagation if provided; preventDefault keeps <a href> SPA navigation 11094if (event) { 11095 if (typeof event.preventDefault === 'function') event.preventDefault(); 11096 event.stopPropagation(); 11097} 11098 11099const galleryContainer = document.querySelector('.gallery-container'); 11100if (galleryContainer) { 11101 // Apply immediate hiding styles 11102 galleryContainer.style.opacity = '0'; 11103} 11104 11105console.log('Loading gallery with ID:', id); 11106
11107if (typeof dismissGalleryPasswordOverlay === 'function') { 11108 dismissGalleryPasswordOverlay(); 11109} 11110 11111// Find gallery by ID 11112const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible 11113 11114if (!gallery) { 11115 console.warn('No gallery found with ID:', id); 11116 if (galleryContainer) { 11117 galleryContainer.style.opacity = '1'; 11118 } 11119 return; 11120} 11121 11122// Check if this is a page, if so use loadPage instead 11123if (isPageMenuItem(gallery)) { 11124 // Use the loadPage function defined in this scope (or window.loadPage if you prefer) 11125 return loadPage(id, event); 11126} 11127 11128// Skip submenu items 11129if (gallery.isSubmenu) { 11130 console.log('Gallery is submenu, skipping URL update'); 11131 toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available 11132 if (galleryContainer) { 11133 galleryContainer.style.opacity = '1'; 11134 } // Show container 11135 return; 11136} 11137 11138console.log('Found gallery:', gallery.title); 11139 11140window._activeContentType = 'gallery'; 11141beginHydraContentLoad('gallery', gallery.id, gallery.pageId); 11142if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { 11143 window.PageManager.clearCurrentPage(); 11144} 11145 11146// CRITICAL: Set active gallery ID consistently in both global and window scope 11147console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')'); 11148window.activeGalleryId = id; 11149activeGalleryId = id; // Ensure both variables are synchronized 11150 11151// Save reference to which gallery we're loading (for later verification) 11152window.currentLoadingGalleryId = id; 11153 11154// CRITICAL: Thorough cleanup with await to ensure completion 11155if (typeof window.removeGalleryScriptsWithPause === 'function') { 11156 await window.removeGalleryScriptsWithPause(); 11157} 11158 11159 11160// Clear all existing content from gallery container 11161if (!galleryContainer) { 11162 console.error('Gallery container not found'); 11163 return; 11164} 11165 11166// Force clear all content 11167galleryContainer.innerHTML = ''; 11168 11169 11170 // CRITICAL: Double-check active gallery ID before updating UI 11171 if (window.activeGalleryId !== id || activeGalleryId !== id) { 11172 console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id); 11173 window.activeGalleryId = id; 11174 activeGalleryId = id; 11175 } 11176 11177// Apply menu visibility based on the gallery's setting and edit mode 11178const bodyEl = document.body; 11179if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 11180 bodyEl.classList.add('menu-hidden-on-page'); 11181} else { 11182 bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden 11183} 11184applyMenuOverlayClassForGallery(gallery); 11185 11186// Update UI state using explicit window function references or local fallbacks 11187if (typeof window.updateActiveStates === 'function') { 11188 window.updateActiveStates(); 11189} else if (typeof updateActiveStates === 'function') { 11190 updateActiveStates(); 11191} 11192 11193if (typeof window.updateMobileTitle === 'function') { 11194 window.updateMobileTitle(); 11195} else if (typeof updateMobileTitle === 'function') { 11196 updateMobileTitle(); 11197} 11198 11199if (typeof window.closeMobileMenu === 'function') { 11200 window.closeMobileMenu(); 11201} else if (typeof closeMobileMenu === 'function') { 11202 closeMobileMenu(); 11203} 11204 11205// Update URL with gallery slug 11206if (typeof window.updateURLWithGallerySlug === 'function') { 11207 window.updateURLWithGallerySlug(gallery); 11208} else if (typeof updateURLWithGallerySlug === 'function') { 11209 updateURLWithGallerySlug(gallery); 11210} 11211 11212 // Create gallery container and load content 11213const neonGalleryContainer = document.createElement('div'); 11214neonGalleryContainer.id = 'neon-gallery-container'; 11215neonGalleryContainer.className = 'gallery-direct-content'; 11216neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString()); 11217neonGalleryContainer.style.width = '100%'; 11218neonGalleryContainer.style.height = '100%'; 11219galleryContainer.appendChild(neonGalleryContainer); 11220 11221// Add a cache-busting parameter to ensure script is freshly loaded 11222const timestamp = Date.now(); 11223 11224// Set up gallery parameters 11225window.Parameters = { 11226 SiteAlias: window.location.hostname, 11227 InitialPageUuid: gallery.pageId, 11228 InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available 11229 isInEditor: window.isEditing || false, // Assuming window.isEditing is available 11230 siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available 11231 isHydra: true,
11232 galleryInstanceId: timestamp, 11233 // Added loadedGalleryId as per your original function 11234 loadedGalleryId: gallery.id 11235}; 11236 11237// Debug: Check what galleryOptions contains 11238console.error('ð INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL); 11239 11240// Set up gallery config 11241window.neonGalleryConfig = { 11242 useData: true, 11243 useCDN: true, 11244 version: 'live', 11245 manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod", 11246 layoutType: gallery.galleryOptions?.layoutType || "grid", 11247 // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence 11248 // This ensures that only properties from the current gallery are included, not from previous galleries 11249 ...(gallery.galleryOptions || {}), 11250 // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script 11251 // These fields are required for the gallery to determine if it should use Load Network racing 11252 loadTxId: gallery.galleryOptions?.loadTxId || null, 11253 loadPermaURL: gallery.galleryOptions?.loadPermaURL || null, 11254 // Cloudflare CDN NDJSON (unique id) â when present, gallery loads CDN-first (3s) then Fly 11255 cdnNdjsonId: gallery.galleryOptions?.cdnNdjsonId || null, 11256 cdnNdjsonUrl: gallery.galleryOptions?.cdnNdjsonUrl || null, 11257 cdnNdjsonStableUrl: gallery.galleryOptions?.cdnNdjsonStableUrl || gallery.galleryOptions?.embedPointerUrl || null, 11258 embedPointerUrl: gallery.galleryOptions?.embedPointerUrl || gallery.galleryOptions?.cdnNdjsonStableUrl || null, 11259 cdnSaveTimestamp: gallery.galleryOptions?.cdnSaveTimestamp || null, 11260 siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available 11261 // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread 11262 galleryId: gallery.id, 11263 galleryInstanceId: timestamp 11264}; 11265 11266console.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); 11267 11268// Create and add the gallery script with cache-busting 11269// Use Worker route (avoids CDN SSL issues) 11270const script = document.createElement('script'); 11271const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; 11272script.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; // Using versioned file 11273script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute 11274script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute 11275console.log('Loading gallery-main script from Worker:', script.src); 11276script.onerror = function() { 11277 console.error('â Gallery script failed, retrying via proxy:', script.src); 11278 const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 11279 script.src = proxyUrl; 11280 script.onerror = function() { 11281 console.error('â Proxy failed, trying storage:', proxyUrl); 11282 script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app'); 11283 }; 11284}; 11285 11286// Added script.onload from your original function 11287script.onload = function() { 11288 if (window.currentLoadingGalleryId !== gallery.id) { 11289 console.warn('Gallery ID mismatch! Expected:', gallery.id, 11290 'Current:', window.currentLoadingGalleryId); 11291 } 11292 console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`); 11293}; 11294 11295 11296if (galleryContainer) { 11297 setTimeout(() => { 11298 galleryContainer.style.opacity = '1'; 11299 }, 50); // Small delay to ensure content is ready 11300 } 11301 11302console.log(`Loading fresh gallery script for: ${gallery.title}`); 11303document.body.appendChild(script); 11304} 11305 11306 11307 11308function removeGalleryScriptsWithPause() { 11309 return new Promise(resolve => { 11310 console.log('Performing comprehensive gallery cleanup (v2 - refined selectors)'); 11311 11312 // 1. Capture and clear any gallery data in localStorage (remains the same) 11313 try { 11314 const localStorageKeys = Object.keys(localStorage); 11315 const galleryStorageKeys = localStorageKeys.filter(key => 11316 key.includes('gallery') || 11317 key.includes('neon') || 11318 key.includes('lightbox') 11319 ); 11320 galleryStorageKeys.forEach(key => { 11321 localStorage.removeItem(key); 11322 }); 11323 } catch (e) { 11324 console.warn('Error clearing localStorage:', e); 11325 } 11326 11327 // @ts-ignore 11328 window._galleryPageContext = null; // (remains the same) 11329 11330 // Keep HP gallery CSS. Page-element edit/view remounts skip HP reload; 11331 // stripping neon-gallery-hp-hydra.css zeros masonry-item / container margins. 11332 const galleryStyles = document.querySelectorAll('style[data-gallery], link[href*="neon-gallery"]');
11333 galleryStyles.forEach(style => { 11334 const href = (style && style.href) ? String(style.href) : ''; 11335 if (style && style.id === 'neon-gallery-css') return; 11336 if (href.indexOf('neon-gallery-hp-hydra') !== -1) return; 11337 if (style && style.parentNode) { 11338 style.parentNode.removeChild(style); 11339 } 11340 }); 11341 11342 // 4. Remove lightbox elements (more specific selectors now) 11343 // These are specific IDs and classes for lightbox components. 11344 const lightboxComponentSelectors = [ 11345 '#neon-lightbox', // Main lightbox container by ID 11346 '.neon-lightbox', // Alternative class for main container 11347 '.lightbox-container',// General container class if used 11348 // Add any other *specific* classes or IDs your lightbox system uses for its top-level elements 11349 ]; 11350 const lightboxElements = document.querySelectorAll(lightboxComponentSelectors.join(', ')); 11351 lightboxElements.forEach(lightboxEl => { 11352 if (lightboxEl && lightboxEl.parentNode && lightboxEl !== document.body && lightboxEl !== document.documentElement) { 11353 // console.log('Removing lightbox component:', lightboxEl.id || lightboxEl.className); 11354 lightboxEl.parentNode.removeChild(lightboxEl); 11355 } 11356 }); 11357 11358 // 5. Remove other gallery-specific DOM elements 11359 // REMOVED: '[class*="lightbox"]', '[id*="lightbox"]' from this list 11360 // The selector '[class*="neon-"]' is still broad but less likely to match body unless you add "neon-" class to body. 11361 const galleryElementSelectors = [ 11362 '.neon-gallery-wrapper', '.gallery-container .fullscreen-overlay', 11363 '.neon-gallery-modal', '.gallery-tooltip', '.neon-gallery-context-menu', 11364 '.neon-thumbnails', '.neon-image', '.neon-caption', 11365 '.neon-controls', '.neon-pagination', 11366 '[class*="neon-"]', // BE CAUTIOUS: If this matches body or critical layout elements, it can cause issues. 11367 '[data-gallery-id]', '[data-image-id]', '.image-grid', '.image-masonry' 11368 // Ensure none of these selectors inadvertently match document.body or critical layout containers 11369 ]; 11370 const galleryElements = document.querySelectorAll(galleryElementSelectors.join(', ')); 11371 11372 if (galleryElements.length > 0) { 11373 galleryElements.forEach(el => { 11374 // CRITICAL FIX: Add checks to ensure we don't remove body or html 11375 if (el && el.parentNode && el !== document.body && el !== document.documentElement) { 11376 // console.log('Removing gallery-specific DOM element:', el.id || el.className); 11377 el.parentNode.removeChild(el); 11378 } else if (el === document.body || el === document.documentElement) { 11379 console.warn(`[CRITICAL WARNING] Attempt to remove document.body or document.documentElement blocked by selector: ${el.id || el.className}. Review your selectors.`); 11380 } 11381 }); 11382 } 11383 11384 // 6. Find and remove ALL gallery scripts (remains the same) 11385 const scripts = document.querySelectorAll( 11386 'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' + 11387 'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]' 11388 ); 11389 scripts.forEach(script => { 11390 if (script && script.parentNode) { 11391 // console.log('Removing script:', script.src || script.getAttribute('data-gallery-id')); 11392 script.parentNode.removeChild(script); 11393 } 11394 }); 11395 11396 // 6b. Specifically remove gallery options scripts (but NOT CSS - CSS is needed across galleries) 11397 const optionsScripts = document.querySelectorAll( 11398 'script[src*="galleryOptions"], script[data-options-js]' 11399 ); 11400 optionsScripts.forEach(script => { 11401 if (script && script.parentNode) { 11402 console.log('Removing gallery options script:', script.src || script.getAttribute('data-options-js')); 11403 script.parentNode.removeChild(script); 11404 } 11405 }); 11406 11407 // NOTE: We intentionally do NOT remove gallery options CSS here 11408 // The CSS is needed across galleries and doesn't cause conflicts like scripts do 11409 11410 setTimeout(() => { 11411 const remainingScripts = document.querySelectorAll( 11412 'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' + 11413 'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]' 11414 ); 11415 if (remainingScripts.length > 0) { 11416 // console.warn(`[DEBUG] ${remainingScripts.length} gallery scripts still found after removal attempt.`); 11417 } 11418 }, 50); 11419 11420 // 7. Reset ALL known global state variables used by the gallery (remains the same) 11421 const resetGlobals = [ 11422 'neonGalleryInitComplete', 'neonGalleryInitInProgress', 11423 'neonGalleryLoaded', 'neonGalleryConfig', 'neonGalleryState', 11424 'neonGalleryCache', 'neonGalleryImages', 'neonGallerySettings', 11425 'neonLightbox', 'neonGalleryEventListeners', 'neonGalleryInstance', 11426 'currentGalleryId', 'galleryData', 'imageCache', 'thumbnailCache', 11427 ]; 11428
11429 resetGlobals.forEach(prop => { 11430 // @ts-ignore 11431 if (window[prop] !== undefined) { 11432 // @ts-ignore 11433 window[prop] = null; 11434 } 11435 }); 11436 // Keep the bootstrap wipe flag. Clearing it let a re-injected main script 11437 // replace PageManager embed config with the default "mod" collection. 11438 if (typeof window.resetNeonGalleryInitGates === 'function') { 11439 window.resetNeonGalleryInitGates('removeGalleryScriptsWithPause'); 11440 } 11441 11442 // 7b. Reset gallery options panel state 11443 if (window._editorButtonVisibilityInterval) { 11444 clearInterval(window._editorButtonVisibilityInterval); 11445 window._editorButtonVisibilityInterval = null; 11446 } 11447 const editorBtn = document.getElementById('editorButton'); 11448 if (editorBtn) { 11449 editorBtn.classList.add('gallery-options-force-hidden'); 11450 editorBtn.style.setProperty('display', 'none', 'important'); 11451 editorBtn.style.pointerEvents = ''; 11452 } 11453 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 11454 window.syncGalleryOptionsButtonVisibility(); 11455 } 11456 // Reset optionsVisible flag if it exists on window (for cross-module access) 11457 // @ts-ignore 11458 if (window.optionsVisible !== undefined) { 11459 // @ts-ignore 11460 window.optionsVisible = false; 11461 } 11462 11463 // Reset options-related globals 11464 const optionsGlobals = [ 11465 'generateOptionsUI', 'openOptionsOverlay', 'showOptionsPanel', 11466 'hideOptionsPanel', 'toggleOptionsOverlay', 'optionsVisible' 11467 ]; 11468 optionsGlobals.forEach(prop => { 11469 // @ts-ignore 11470 if (window[prop] !== undefined && typeof window[prop] !== 'function') { 11471 // Only reset non-function properties (like flags) 11472 // Functions should remain available 11473 // @ts-ignore 11474 if (prop === 'optionsVisible') { 11475 // @ts-ignore 11476 window[prop] = false; 11477 } 11478 } 11479 }); 11480 11481 // Reset EnhancedGallerySystem so a pending initPromise cannot hang forever 11482 // after scripts it was waiting on were just removed from the DOM. 11483 if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.resetInitializationState === 'function') { 11484 window.EnhancedGallerySystem.resetInitializationState(); 11485 } 11486 11487 // Reset PageManager page-embedded gallery tracking 11488 window.neonGalleryScriptLoaded = false; 11489 window.pageManagerGalleryInitBlocked = false; 11490 if (window.pageManagerGalleryContainers) { 11491 window.pageManagerGalleryContainers.clear(); 11492 } 11493 11494 // Remove orphaned lightbox keyboard listeners (survive script tag removal) 11495 if (typeof window.removeAllLightboxKeyboardListeners === 'function') { 11496 window.removeAllLightboxKeyboardListeners(); 11497 } else if (window.currentLightboxKeyboardListener) { 11498 document.removeEventListener('keydown', window.currentLightboxKeyboardListener); 11499 window.currentLightboxKeyboardListener = null; 11500 window.lightboxKeyboardListenerActive = false; 11501 } 11502 11503 // Short pause before resolving 11504 setTimeout(resolve, 150); 11505 }); 11506} 11507 11508 11509// Fixed clearAddForm function with proper variable names 11510function clearAddForm() { 11511// Clear the title input 11512const titleInput = document.getElementById('galleryTitle'); 11513if (titleInput) { 11514titleInput.value = ''; 11515} 11516 11517// Clear the URL input 11518const urlInput = document.getElementById('galleryUrl'); 11519if (urlInput) { 11520urlInput.value = ''; 11521} 11522 11523// Reset the parent selection 11524const parentSelect = document.getElementById('galleryParent'); 11525if (parentSelect) { 11526parentSelect.value = ''; 11527} 11528 11529// Reset radio buttons to Gallery (integrated) as default 11530const galleryRadio = document.querySelector('input[name="galleryType"][value="integrated"]'); 11531if (galleryRadio) { 11532galleryRadio.checked = true; 11533} 11534 11535// Hide URL input container when default is Gallery 11536const urlInputContainer = document.getElementById('urlInputContainer'); 11537if (urlInputContainer) { 11538urlInputContainer.style.display = 'none'; 11539} else { 11540console.log('URL input container not found'); 11541} 11542} 11543 11544/** 11545 * Ensures window.galleries is synchronized with the global galleries variable 11546 * Call this function after any modification to gallery settings 11547 * 11548 * @returns {boolean} True if synchronization was successful 11549 */ 11550/** 11551 * Enhanced synchronizeGalleries function that checks ALL galleries 11552 * not just imported ones, and ensures settings are preserved 11553 * @returns {boolean} Success indicator 11554 */ 11555function synchronizeGalleries() { 11556 console.log('Starting enhanced gallery synchronization...'); 11557 11558 if (typeof galleries !== 'undefined' && Array.isArray(galleries)) { 11559 // First check if window.galleries exists and is different from galleries 11560 const needsSync = !window.galleries || 11561 !Array.isArray(window.galleries) || 11562 window.galleries.length !== galleries.length; 11563 11564 // ENHANCED: Check ALL galleries for options preservation, not just imported ones 11565 if (!needsSync && window.galleries.length > 0) { 11566 // Find all galleries with galleryOptions 11567 const galleriesWithOptions = galleries.filter(g => 11568 g.galleryOptions && Object.keys(g.galleryOptions).length > 4 11569 ); 11570 11571 if (galleriesWithOptions.length > 0) { 11572 // Check ALL galleries with substantial options, not just a sample 11573 let syncNeeded = false;
11574 11575 // Use for loop so we can break early if we find a mismatch 11576 for (let i = 0; i < galleriesWithOptions.length; i++) { 11577 const gallery = galleriesWithOptions[i]; 11578 const windowGallery = window.galleries.find(g => g.id === gallery.id); 11579 11580 if (windowGallery) { 11581 // Get options depth 11582 const galleryOptionsDepth = gallery.galleryOptions ? 11583 Object.keys(gallery.galleryOptions).length : 0; 11584 11585 const windowOptionsDepth = windowGallery.galleryOptions ? 11586 Object.keys(windowGallery.galleryOptions).length : 0; 11587 11588 // If options depth differs significantly, we need to sync 11589 if (Math.abs(galleryOptionsDepth - windowOptionsDepth) > 2) { 11590 console.log(`Gallery sync needed: Options mismatch detected for ${gallery.title}`); 11591 console.log(`Options count: global=${galleryOptionsDepth}, window=${windowOptionsDepth}`); 11592 11593 syncNeeded = true; 11594 break; // No need to check more galleries 11595 } 11596 11597 // Additional check: Look for specific important gallery settings that should be preserved 11598 // Only do this check for galleries with substantial settings 11599 if (galleryOptionsDepth > 5 && windowOptionsDepth > 5) { 11600 // Critical keys that indicate gallery customization 11601 const criticalKeys = ['columns', 'spacing', 'layoutType', 'startInSingles', 11602 'autoplaySingles', 'autoplayRandomStart', 'lightboxBgColor']; 11603 11604 // Check if any critical keys are missing in window.galleries 11605 const missingKeys = criticalKeys.filter(key => 11606 gallery.galleryOptions[key] !== undefined && 11607 windowGallery.galleryOptions[key] === undefined 11608 ); 11609 11610 if (missingKeys.length > 0) { 11611 console.log(`Gallery sync needed: Critical settings missing in window.galleries for ${gallery.title}`); 11612 console.log(`Missing keys: ${missingKeys.join(', ')}`); 11613 11614 syncNeeded = true; 11615 break; // No need to check more galleries 11616 } 11617 } 11618 } 11619 } 11620 11621 // If we detected a need to sync, do it now 11622 if (syncNeeded) { 11623 // Ensure we preserve rich gallery settings and per-page custom HTML fields 11624 window.galleries = galleries.map(gallery => { 11625 const windowGallery = window.galleries.find(g => g.id === gallery.id); 11626 if (!windowGallery) return gallery; 11627 11628 let merged = { ...gallery }; 11629 11630 if (windowGallery.galleryOptions && gallery.galleryOptions) { 11631 const globalOptionsDepth = Object.keys(gallery.galleryOptions).length; 11632 const windowOptionsDepth = Object.keys(windowGallery.galleryOptions).length; 11633 if (windowOptionsDepth > globalOptionsDepth) { 11634 merged = { 11635 ...merged, 11636 galleryOptions: { 11637 ...windowGallery.galleryOptions, 11638 ...gallery.galleryOptions 11639 } 11640 }; 11641 } 11642 } 11643 11644 if (windowGallery.customPageHtmlR2Key || windowGallery.customPageHtmlEnabled) { 11645 merged = { 11646 ...merged, 11647 customPageHtmlEnabled: windowGallery.customPageHtmlEnabled, 11648 customPageHtmlR2Key: windowGallery.customPageHtmlR2Key, 11649 customPageHtmlFilename: windowGallery.customPageHtmlFilename 11650 }; 11651 } 11652 11653 return merged; 11654 }); 11655 11656 console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`); 11657 return true; 11658 } 11659 } 11660 } 11661 11662 // If we determined sync is needed (or as a precaution), update window.galleries 11663 if (needsSync || !window.galleries) { 11664 if (window.galleries && Array.isArray(window.galleries)) { 11665 galleries = galleries.map(function(gallery) { 11666 const windowGallery = window.galleries.find(function(g) { return g && g.id === gallery.id; }); 11667 if (!windowGallery) return gallery; 11668 if (windowGallery.customPageHtmlR2Key || windowGallery.customPageHtmlEnabled) { 11669 return { 11670 ...gallery, 11671 customPageHtmlEnabled: windowGallery.customPageHtmlEnabled, 11672 customPageHtmlR2Key: windowGallery.customPageHtmlR2Key, 11673 customPageHtmlFilename: windowGallery.customPageHtmlFilename 11674 }; 11675 } 11676 return gallery; 11677 }); 11678 } 11679 window.galleries = [...galleries]; // Create a shallow copy to ensure different reference 11680 console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`); 11681 } else { 11682 console.log('Gallery synchronization check: No sync needed'); 11683 } 11684 11685 // Verify that all galleries with rich settings have their full options preserved 11686 const richGalleries = galleries.filter(g => 11687 g.galleryOptions && Object.keys(g.galleryOptions).length > 5 11688 ); 11689 11690 console.log(`Status: Found ${richGalleries.length} galleries with rich settings`); 11691 11692 // Final verification - keep both arrays aligned without dropping page-level fields 11693 if (window.galleries !== galleries) { 11694 console.warn('References not identical after sync - merging page fields into global galleries'); 11695 if (window.galleries && Array.isArray(window.galleries)) { 11696 galleries = galleries.map(function(gallery) { 11697 const windowGallery = window.galleries.find(function(g) { return g && g.id === gallery.id; }); 11698 if (!windowGallery) return gallery; 11699 return { 11700 ...gallery, 11701 customPageHtmlEnabled: windowGallery.customPageHtmlEnabled ?? gallery.customPageHtmlEnabled, 11702 customPageHtmlR2Key: windowGallery.customPageHtmlR2Key ?? gallery.customPageHtmlR2Key, 11703 customPageHtmlFilename: windowGallery.customPageHtmlFilename ?? gallery.customPageHtmlFilename, 11704 hideMenuOnPage: windowGallery.hideMenuOnPage ?? gallery.hideMenuOnPage, 11705 overlayMenuOnPage: windowGallery.overlayMenuOnPage ?? gallery.overlayMenuOnPage 11706 }; 11707 }); 11708 } 11709 window.galleries = galleries; 11710 return true; 11711 } 11712 11713 return true; 11714 } else { 11715 console.warn('Cannot synchronize galleries: global galleries variable is undefined or not an array'); 11716 11717 // If we have window.galleries but no global galleries, try to reverse-sync 11718 if (window.galleries && Array.isArray(window.galleries) && window.galleries.length > 0) { 11719 console.log('Attempting reverse sync: updating global galleries from window.galleries');
11720 try { 11721 // This is risky and should only happen in unusual circumstances 11722 galleries = window.galleries; 11723 return true; 11724 } catch (e) { 11725 console.error('Could not reverse-sync galleries:', e); 11726 } 11727 } 11728 11729 return false; 11730 } 11731} 11732 11733 11734 11735/** 11736 * Loads NDJSON content from the original gallery 11737 * @param {string} siteId - The site ID 11738 * @param {string} pageId - The page ID of the original gallery 11739 * @returns {Promise<string|null>} - The NDJSON content or null if not found/error 11740 */ 11741async function loadGalleryNDJSON(siteId, pageId) { 11742 console.error('[NDJSON Duplication] loadGalleryNDJSON called with siteId:', siteId, 'pageId:', pageId); 11743 11744 if (!siteId || !pageId) { 11745 console.error('[NDJSON Duplication] ERROR: Cannot load NDJSON - missing siteId or pageId', { siteId: siteId, pageId: pageId }); 11746 return null; 11747 } 11748 11749 try { 11750 const ndjsonFilename = `${siteId}_${pageId}.ndjson`; 11751 const url = `https://fly.storage.tigris.dev/ns-bridge-pub/${ndjsonFilename}`; 11752 11753 console.error('[NDJSON Duplication] Attempting to load NDJSON from:', url); 11754 console.error('[NDJSON Duplication] Filename:', ndjsonFilename); 11755 11756 const response = await fetch(url); 11757 11758 console.error('[NDJSON Duplication] Fetch response status:', response.status, response.statusText); 11759 11760 if (!response.ok) { 11761 if (response.status === 404) { 11762 console.error('[NDJSON Duplication] ERROR: NDJSON file not found (404) - gallery may not have been published yet'); 11763 console.error('[NDJSON Duplication] Attempted URL:', url); 11764 return null; 11765 } 11766 const errorText = await response.text().catch(() => 'Could not read error text'); 11767 console.error('[NDJSON Duplication] ERROR: Failed to load NDJSON:', response.status, response.statusText, errorText); 11768 throw new Error(`Failed to load NDJSON: ${response.statusText}`); 11769 } 11770 11771 const ndjsonContent = await response.text(); 11772 console.error('[NDJSON Duplication] SUCCESS: Loaded NDJSON, length:', ndjsonContent.length, 'characters'); 11773 console.error('[NDJSON Duplication] First 200 chars:', ndjsonContent.substring(0, 200)); 11774 return ndjsonContent; 11775 } catch (error) { 11776 console.error('[NDJSON Duplication] ERROR: Exception loading NDJSON:', error); 11777 console.error('[NDJSON Duplication] Error message:', error.message); 11778 console.error('[NDJSON Duplication] Error stack:', error.stack); 11779 return null; 11780 } 11781} 11782 11783/** 11784 * Duplicates and publishes NDJSON for a gallery 11785 * @param {Object} originalGallery - The original gallery object 11786 * @param {Object} newGallery - The new gallery object with new pageId 11787 * @returns {Promise<boolean>} - Success indicator 11788 */ 11789async function duplicateGalleryNDJSON(originalGallery, newGallery) { 11790 console.error('[NDJSON Duplication] ===== STARTING duplicateGalleryNDJSON ====='); 11791 console.error('[NDJSON Duplication] Original gallery:', { 11792 id: originalGallery.id, 11793 title: originalGallery.title, 11794 pageId: originalGallery.pageId, 11795 classicGuid: originalGallery.classicGuid, 11796 hasGalleryOptions: !!originalGallery.galleryOptions, 11797 manualCollectionName: originalGallery.galleryOptions?.manualCollectionName, 11798 isClassicCollection: originalGallery.galleryOptions?.isClassicCollection 11799 }); 11800 console.error('[NDJSON Duplication] New gallery:', { 11801 id: newGallery.id, 11802 title: newGallery.title, 11803 pageId: newGallery.pageId 11804 }); 11805 11806 try { 11807 // Check if this is an NDJSON-based gallery (not classic collection) 11808 // IMPORTANT: A gallery can have a GUID-style manualCollectionName but still use NDJSON files 11809 // Only skip if it's explicitly marked as a classic collection OR has no pageId (which means no NDJSON) 11810 // Having a pageId means it uses NDJSON files, so we should duplicate them 11811 const isClassicCollection = originalGallery.galleryOptions?.isClassicCollection === true; 11812 11813 console.error('[NDJSON Duplication] Classic collection check:', { 11814 hasClassicGuid: !!originalGallery.classicGuid, 11815 manualCollectionName: originalGallery.galleryOptions?.manualCollectionName, 11816 startsWithGUID: originalGallery.galleryOptions?.manualCollectionName?.startsWith('G
11816UID='), 11817 isClassicCollectionFlag: originalGallery.galleryOptions?.isClassicCollection, 11818 hasPageId: !!originalGallery.pageId, 11819 isClassicCollection: isClassicCollection, 11820 willSkip: isClassicCollection && !originalGallery.pageId 11821 }); 11822 11823 // Only skip if explicitly marked as classic collection AND has no pageId 11824 // If it has a pageId, it uses NDJSON files and we should duplicate them 11825 if (isClassicCollection && !originalGallery.pageId) { 11826 console.error('[NDJSON Duplication] SKIP: Gallery is explicitly marked as classic collection with no pageId, skipping NDJSON duplication'); 11827 return true; // Not an error, just not applicable 11828 } 11829 11830 // Check if original gallery has a pageId 11831 if (!originalGallery.pageId) { 11832 console.error('[NDJSON Duplication] SKIP: Original gallery has no pageId, skipping NDJSON duplication'); 11833 console.error('[NDJSON Duplication] Original gallery keys:', Object.keys(originalGallery)); 11834 return true; // Not an error, just not applicable 11835 } 11836 11837 // Get siteId 11838 const siteId = window.siteId || originalGallery.siteId; 11839 console.error('[NDJSON Duplication] SiteId resolution:', { 11840 windowSiteId: window.siteId, 11841 gallerySiteId: originalGallery.siteId, 11842 resolvedSiteId: siteId 11843 }); 11844 11845 if (!siteId) { 11846 console.error('[NDJSON Duplication] ERROR: Cannot duplicate NDJSON - missing siteId'); 11847 console.error('[NDJSON Duplication] window.siteId:', window.siteId); 11848 console.error('[NDJSON Duplication] originalGallery.siteId:', originalGallery.siteId); 11849 return false; 11850 } 11851 11852 // Load original NDJSON 11853 console.error('[NDJSON Duplication] Loading original NDJSON for duplication...'); 11854 console.error('[NDJSON Duplication] Using siteId:', siteId, 'pageId:', originalGallery.pageId); 11855 const originalNdjson = await loadGalleryNDJSON(siteId, originalGallery.pageId); 11856 11857 if (!originalNdjson) { 11858 console.error('[NDJSON Duplication] ERROR: Could not load original NDJSON, gallery may not have been published yet'); 11859 console.error('[NDJSON Duplication] This means the original gallery\'s NDJSON file was not found'); 11860 return false; 11861 } 11862 11863 // Publish the duplicated NDJSON with new pageId using the same endpoint as edition publisher 11864 console.error('[NDJSON Duplication] Publishing duplicated NDJSON with new pageId:', newGallery.pageId); 11865 const dataFileName = `${siteId}_${newGallery.pageId}.ndjson`; 11866 console.error('[NDJSON Duplication] New filename:', dataFileName); 11867 console.error('[NDJSON Duplication] NDJSON content length:', originalNdjson.length); 11868 11869 try { 11870 // Hydra sites require Bearer auth (same token resolution as Edition Publisher) 11871 const publishHeaders = { 'Content-Type': 'application/json' }; 11872 let authToken = null; 11873 try { 11874 if (window.TokenManager) { 11875 if (typeof window.TokenManager.getHydraAuthToken === 'function') { 11876 try { authToken = window.TokenManager.getHydraAuthToken(); } catch (e) {} 11877 } 11878 if (!authToken && typeof window.TokenManager.getBestToken === 'function') { 11879 try { 11880 const tokenWithBearer = window.TokenManager.getBestToken(); 11881 authToken = tokenWithBearer && tokenWithBearer.startsWith('Bearer ') 11882 ? tokenWithBearer.substring(7) 11883 : tokenWithBearer; 11884 } catch (e) {} 11885 } 11886 } 11887 if (!authToken) authToken = localStorage.getItem('hydra_auth_token'); 11888 if (!authToken) authToken = window.authToken || window.didToken; 11889 if (authToken) { 11890 publishHeaders['Authorization'] = authToken.includes('.') || authToken.startsWith('hydra:') 11891 ? 'Bearer ' + authToken 11892 : (authToken.startsWith('Bearer ') ? authToken : 'Bearer hydra:' + authToken); 11893 console.error('[NDJSON Duplication] Auth token attached for publish'); 11894 } else { 11895 console.error('[NDJSON Duplication] WARNING: No auth token â publish will likely fail with 401'); 11896 } 11897 } catch (authErr) { 11898 console.error('[NDJSON Duplication] Auth token lookup failed:', authErr.message); 11899 } 11900 11901 // Call the publish endpoint (same as edition publisher) 11902 console.error('[NDJSON Duplication] Calling publish endpoint: https://hydra-press-v2.fly.dev/publish'); 11903 const response = await fetch('https://hydra-press-v2.fly.dev/publish', { 11904 method: 'POST', 11905 headers: publishHeaders, 11906 body: JSON.stringify({ 11907 ndjsonContent: originalNdjson, 11908 dataFileName: dataFileName 11909 }), 11910 }); 11911 11912 console.error('[NDJSON Duplication] Publish response status:', response.status, response.statusText); 11913 console.error('[NDJSON Duplication] Response headers:', Object.fromEntries(response.headers.entries())); 11914 11915 if (!response.ok) { 11916 const errorText = await response.text().catch(() => 'Unknown error'); 11917 console.error('[NDJSON Duplication] ERROR: Failed to publish duplicated NDJSON:', response.status, errorText); 11918 console.error('[NDJSON Duplication] Response status:', response.status); 11919 console.error('[NDJSON Duplication] Response statusText:', response.statusText); 11920 return false;
11921 } 11922 11923 const result = await response.json(); 11924 console.error('[NDJSON Duplication] SUCCESS: Published duplicated NDJSON'); 11925 console.error('[NDJSON Duplication] Publish result:', JSON.stringify(result, null, 2)); 11926 console.error('[NDJSON Duplication] Publish result URLs:', result.urls); 11927 11928 // Note: The publish endpoint doesn't return Irys txId, so we don't update galleryOptions 11929 // with txId/permaURL here. If Irys upload is needed, it would need to be done separately. 11930 11931 console.error('[NDJSON Duplication] ===== duplicateGalleryNDJSON COMPLETED SUCCESSFULLY ====='); 11932 return true; 11933 } catch (error) { 11934 console.error('[NDJSON Duplication] ERROR: Exception publishing duplicated NDJSON:', error); 11935 console.error('[NDJSON Duplication] Error message:', error.message); 11936 console.error('[NDJSON Duplication] Error stack:', error.stack); 11937 return false; 11938 } 11939 } catch (error) { 11940 console.error('[NDJSON Duplication] ERROR: Unexpected error in duplicateGalleryNDJSON:', error); 11941 console.error('[NDJSON Duplication] Error message:', error.message); 11942 console.error('[NDJSON Duplication] Error stack:', error.stack); 11943 return false; 11944 } 11945} 11946 11947/** 11948 * Creates and shows a loading overlay for gallery deletion (same pattern as duplication). 11949 * @returns {Object} Object with hide() and updateMessage() to control the overlay 11950 */ 11951function showDeletionOverlay() { 11952 const existingOverlay = document.getElementById('deletion-overlay'); 11953 if (existingOverlay) existingOverlay.remove(); 11954 if (!document.querySelector('#deletion-overlay-styles')) { 11955 const style = document.createElement('style'); 11956 style.id = 'deletion-overlay-styles'; 11957 style.textContent = ` 11958 #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; } 11959 #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; } 11960 #deletion-overlay .deletion-spinner { width: 80px; height: 80px; color: #444; } 11961 `; 11962 document.head.appendChild(style); 11963 } 11964 const overlayEl = document.createElement('div'); 11965 overlayEl.id = 'deletion-overlay'; 11966 const messageEl = document.createElement('div'); 11967 messageEl.className = 'deletion-message'; 11968 const spinnerEl = document.createElement('div'); 11969 spinnerEl.className = 'deletion-spinner'; 11970 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>`; 11971 const textEl = document.createElement('div'); 11972 textEl.textContent = 'Deleting...'; 11973 textEl.style.textAlign = 'center'; 11974 messageEl.appendChild(spinnerEl); 11975 messageEl.appendChild(textEl); 11976 overlayEl.appendChild(messageEl); 11977 document.body.appendChild(overlayEl); 11978 return { 11979 hide: function() { if (overlayEl && overlayEl.parentNode) overlayEl.parentNode.removeChild(overlayEl); }, 11980 updateMessage: function(msg) { textEl.textContent = msg; } 11981 }; 11982} 11983 11984/** 11985 * Creates and shows a loading overlay for gallery duplication 11986 * @returns {Object} Object with hide function to remove the overlay 11987 */ 11988function showDuplicationOverlay() { 11989 // Remove any existing overlay first 11990 const existingOverlay = document.getElementById('duplication-overlay'); 11991 if (existingOverlay) { 11992 existingOverlay.remove(); 11993 } 11994 11995 // Add styles if not already present 11996 if (!document.querySelector('#duplication-overlay-styles')) { 11997 const style = document.createElement('style'); 11998 style.id = 'duplication-overlay-styles'; 11999 style.textContent = ` 12000 #duplication-overlay { 12001 position: fixed; 12002 top: 0; 12003 left: 0; 12004 right: 0; 12005 bottom: 0; 12006 background: rgba(0, 0, 0, 0.5); 12007 z-index: 99999999; 12008 display: flex; 12009 align-items: center; 12010 justify-content: center;
12011 cursor: not-allowed; 12012 pointer-events: all; 12013 } 12014 #duplication-overlay .duplication-message { 12015 background: rgba(255, 255, 255, 0.95); 12016 padding: 30px 50px; 12017 font-family: Arial, sans-serif; 12018 font-size: 16px; 12019 box-shadow: 0 4px 12px rgba(0,0,0,0.15); 12020 display: flex; 12021 flex-direction: column; 12022 align-items: center; 12023 gap: 20px; 12024 } 12025 #duplication-overlay .duplication-spinner { 12026 width: 80px; 12027 height: 80px; 12028 color: #444444; 12029 } 12030 `; 12031 document.head.appendChild(style); 12032 } 12033 12034 const overlay = document.createElement('div'); 12035 overlay.id = 'duplication-overlay'; 12036 12037 const message = document.createElement('div'); 12038 message.className = 'duplication-message'; 12039 12040 const spinner = document.createElement('div'); 12041 spinner.className = 'duplication-spinner'; 12042 spinner.innerHTML = ` 12043 <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24"> 12044 <defs> 12045 <filter id="svgSpinnersGooeyBalls20"> 12046 <feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/> 12047 <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"/> 12048 <feBlend in="SourceGraphic" in2="z"/> 12049 </filter> 12050 </defs> 12051 <g filter="url(#svgSpinnersGooeyBalls20)"> 12052 <circle cx="5" cy="12" r="4" fill="currentColor"> 12053 <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/> 12054 </circle> 12055 <circle cx="19" cy="12" r="4" fill="currentColor"> 12056 <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/> 12057 </circle> 12058 <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/> 12059 </g> 12060 </svg> 12061 `; 12062 12063 const text = document.createElement('div'); 12064 text.textContent = 'Duplicating gallery...'; 12065 text.style.textAlign = 'center'; 12066 12067 message.appendChild(spinner); 12068 message.appendChild(text); 12069 overlay.appendChild(message); 12070 document.body.appendChild(overlay); 12071 12072 return { 12073 hide: function() { 12074 if (overlay && overlay.parentNode) { 12075 overlay.parentNode.removeChild(overlay); 12076 } 12077 }, 12078 updateMessage: function(newMessage) { 12079 text.textContent = newMessage; 12080 } 12081 }; 12082} 12083 12084/** 12085 * Duplicates a gallery or page item. 12086 * @param {number} id - The ID of the item to duplicate. 12087 * @param {Event} event - The click event. 12088 */ 12089async function duplicateGalleryItem(id, event) { 12090 event.stopPropagation(); 12091 console.log(`Duplicating item with ID: ${id}`); 12092 12093 // Show loading overlay 12094 const overlay = showDuplicationOverlay(); 12095 12096 // Find the original item in the global galleries array 12097 const originalItem = findGalleryById(galleries, id); // Use helper if available, otherwise simple find 12098 12099 if (!originalItem) { 12100 console.error(`Cannot duplicate: Item with ID ${id} not found.`); 12101 overlay.hide(); 12102 alert('Error: Could not find the item to duplicate.'); 12103 return; 12104 } 12105 12106 if (isPageMenuItem(originalItem)) { 12107 overlay.updateMessage('Duplicating page...'); 12108 } 12109 12110 try { 12111 // --- Create a Deep Copy --- 12112 // Using JSON.parse/stringify is a common way for simple object structures 12113 // Be cautious if your objects have Dates, functions, Maps, Sets, etc. 12114 const newItem = JSON.parse(JSON.stringify(originalItem)); 12115 12116 // --- Modify Copied Item --- 12117 newItem.id = Date.now() + Math.floor(Math.random() * 1000); // Generate a new unique ID 12118 newItem.title = `${originalItem.title}-copy`; // Append "-copy" to title 12119 12120 // Generate new pageId if it's a page or integrated gallery 12121 if (newItem.isPage || newItem.isIntegrated) { 12122 newItem.pageId = generatePageId(); // Assign a new unique pageId 12123 } 12124 12125 // Generate new slug and URL 12126 const newBaseSlug = slugify(newItem.title); // Use the new title 12127 newItem.slug = ensureUniqueSlug(newBaseSlug, galleries); // Ensure uniqueness 12128 newItem.url = `/${newItem.slug}`; // Update URL based on new slug 12129 12130 // Reset children (don't duplicate children for simplicity) 12131 newItem.children = []; 12132 12133 // Deep clone galleryOptions and pageElements, update pageId if needed 12134 if (newItem.galleryOptions) { 12135 newItem.galleryOptions = JSON.parse(JSON.stringify(newItem.galleryOptions)); 12136 // Update pageId within options if it exists and matches the old one 12137 if (newItem.galleryOptions.pageId && newItem.galleryOptions.pageId === originalItem.pageId) { 12138 newItem.galleryOptions.pageId = newItem.pageId; 12139 } 12140 // Clear permanent-storage pointers so duplicate loads its own Tigris NDJSON, not the original 12141 ['txId', 'permaURL', 'loadTxId', 'loadPermaURL', 'loadNetworkUrl', 'r2StorageUrl', 12142 'cdnNdjsonId', 'cdnNdjsonUrl', 'cdnNdjsonStableUrl', 'embedPointerUrl', 'cdnSaveTimestamp', 12143 'flyIoSaveTimestamp', 'loadNetworkSaveTimestamp', 'irysSaveTimestamp', 'timestampsMatch', 'isPerma'] 12144 .forEach(function(field) { delete newItem.galleryOptions[field]; }); 12145 } 12146 if (newItem.pageElements) { 12147 newItem.pageElements = JSON.parse(JSON.stringify(newItem.pageElements)); 12148 // Give new IDs to duplicated page elements
12149 newItem.pageElements.forEach(el => { 12150 el.id = Date.now() + Math.floor(Math.random() * 10000); 12151 }); 12152 } 12153 12154 // Reset home page status 12155 newItem.isHomePage = false; 12156 12157 // --- Duplicate NDJSON if applicable (galleries only; pages copy content via pageElements) --- 12158 const isDuplicatingPage = isPageMenuItem(originalItem); 12159 console.error('[Gallery Duplication] Checking if NDJSON duplication is needed...', { 12160 isDuplicatingPage, 12161 newItemPageId: newItem.pageId, 12162 originalItemPageId: originalItem.pageId 12163 }); 12164 12165 if (!isDuplicatingPage && newItem.pageId && originalItem.pageId) { 12166 overlay.updateMessage('Duplicating gallery files...'); 12167 console.error('[Gallery Duplication] Both galleries have pageIds, attempting to duplicate NDJSON file...'); 12168 const ndjsonDuplicated = await duplicateGalleryNDJSON(originalItem, newItem); 12169 if (!ndjsonDuplicated) { 12170 console.error('[Gallery Duplication] WARNING: NDJSON duplication failed, but continuing with gallery duplication'); 12171 console.error('[Gallery Duplication] User can republish the gallery later if needed'); 12172 alert('Gallery menu item was created, but gallery images could not be copied. Open Edition Publisher on this gallery and publish to load images.'); 12173 // Don't block the duplication if NDJSON fails - user can republish later 12174 } else { 12175 console.error('[Gallery Duplication] SUCCESS: NDJSON duplication completed successfully'); 12176 } 12177 } else { 12178 console.error('[Gallery Duplication] SKIP: Skipping NDJSON duplication:', { 12179 isDuplicatingPage, 12180 hasNewPageId: !!newItem.pageId, 12181 hasOriginalPageId: !!originalItem.pageId, 12182 reason: isDuplicatingPage ? 'Page items use pageElements, not NDJSON' : 12183 !newItem.pageId ? 'New gallery has no pageId' : 'Original gallery has no pageId' 12184 }); 12185 } 12186 12187 overlay.updateMessage(isDuplicatingPage ? 'Saving duplicated page...' : 'Saving duplicated gallery...'); 12188 12189 // --- Insert into Galleries Array --- 12190 // Find the index of the original item 12191 const originalIndex = galleries.findIndex(item => item.id === id); 12192 12193 if (originalIndex > -1) { 12194 // Insert the new item right after the original 12195 galleries.splice(originalIndex + 1, 0, newItem); 12196 console.log(`Inserted duplicate "${newItem.title}" after "${originalItem.title}"`); 12197 12198 // Update positions for all items after the insertion point 12199 for (let i = originalIndex + 1; i < galleries.length; i++) { 12200 galleries[i].position = i; // Re-assign position based on new index 12201 } 12202 12203 } else { 12204 // Fallback: Add to the end if original couldn't be found (shouldn't happen) 12205 console.warn(`Original item ${id} not found in array, adding duplicate to the end.`); 12206 newItem.position = galleries.length; 12207 galleries.push(newItem); 12208 } 12209 12210 console.log('Rendering galleries immediately after duplication...'); 12211 renderGalleries(); 12212 // Update active states if necessary (though unlikely needed here) 12213 updateActiveStates(); 12214 12215 // --- Save and Refresh --- 12216 console.log('Saving duplicated item...'); 12217 saveGalleries(null, { addedNewItem: true }) // Pass flag to trigger refresh 12218 .then(success => { 12219 overlay.hide(); // Hide overlay before page refresh 12220 if (success) { 12221 console.log('Duplicate saved successfully. Page will refresh.'); 12222 // No need to call renderGalleries() here because saveGalleries will trigger a reload. 12223 } else { 12224 console.error('Failed to save the duplicated item.'); 12225 alert('Error: Could not save the duplicated item.'); 12226 // Optionally try to revert the galleries array change here if save fails 12227 } 12228 }) 12229 .catch(error => { 12230 overlay.hide(); // Hide overlay on error 12231 console.error('Error saving duplicated item:', error); 12232 alert('Error saving duplicated item.'); 12233 }); 12234 12235 } catch (error) { 12236 overlay.hide(); // Hide overlay on error 12237 console.error('Error duplicating item:', error); 12238 alert('An error occurred while duplicating the item.'); 12239 } 12240} 12241 12242/** 12243 * saveGalleries function that always saves FULL data 12244 * Ensures gallery settings are preserved in both localStorage and server 12245 * 12246 * @param {Object} customData - Optional custom data to save instead of galleries 12247 * @returns {Promise<boolean>} - Success indicator 12248 */ 12249 12250function attachLiveSidebarElements(data) { 12251 if (!data || typeof data !== 'object') return data; 12252 var hasGetter = !!(window.SidebarManager && typeof window.SidebarManager.getElementsForSave === 'function'); 12253 if (hasGetter) { 12254 var live = window.SidebarManager.getElementsForSave(); 12255 if (Array.isArray(live) && live.length > 0) { 12256 data.sidebarElements = live; 12257 return data; 12258 } 12259 delete data.sidebarElements; 12260 return data; 12261 } 12262 if (Array.isArray(data.sidebarElements) && data.sidebarElements.length === 0) { 12263 delete data.sidebarElements; 12264 } 12265 return data; 12266} 12267 12268async function saveGalleries(customData) { 12269 if (!isAuthenticated && localStorage.getItem('hydra_is_admin') !== 'true') { 12270 alert('You must be logged in as an admin to save changes'); 12271 return false;
12272 } 12273 12274 if (window.HelpSystem && typeof window.HelpSystem.isSandboxActive === 'function' && window.HelpSystem.isSandboxActive()) { 12275 console.warn('Save blocked: help sandbox is active'); 12276 return false; 12277 } 12278 12279 function _hasHelpDemoInTree(items) { 12280 if (!Array.isArray(items)) return false; 12281 for (var i = 0; i < items.length; i++) { 12282 var g = items[i]; 12283 if (!g) continue; 12284 if (g.isHelpDemo || Number(g.id) < 0) return true; 12285 if (g.children && _hasHelpDemoInTree(g.children)) return true; 12286 } 12287 return false; 12288 } 12289 12290 if (window.activeGalleryId != null && Number(window.activeGalleryId) < 0) { 12291 console.warn('Save blocked: active help demo gallery'); 12292 return false; 12293 } 12294 12295 var savePayload = customData && customData.galleries ? customData.galleries : galleries; 12296 if (_hasHelpDemoInTree(savePayload)) { 12297 console.warn('Save blocked: payload contains help demo records'); 12298 return false; 12299 } 12300 12301 // Prevent multiple simultaneous saves â queue the latest payload instead of dropping it 12302 if (window._saveInProgress) { 12303 console.log('Save already in progress, queueing latest payload'); 12304 if (customData) { 12305 window._pendingSaveGalleriesData = attachLiveSidebarElements(customData); 12306 } 12307 return new Promise(function(resolve) { 12308 window._pendingSaveResolvers = window._pendingSaveResolvers || []; 12309 window._pendingSaveResolvers.push(resolve); 12310 }); 12311 } 12312 12313 window._saveInProgress = true; 12314 12315 try { 12316 // CRITICAL: Synchronize galleries before saving to ensure we use the latest data 12317 if (typeof synchronizeGalleries === 'function') { 12318 synchronizeGalleries(); 12319 console.log('Galleries synchronized before saving'); 12320 } 12321 12322 // Try to get a valid token from multiple sources 12323 let token = null; 12324 let tokenSource = 'none'; 12325 12326 // First try the global didToken (traditional approach) 12327 if (didToken) { 12328 console.log('Using global didToken for save operation'); 12329 token = didToken; 12330 tokenSource = 'didToken'; 12331 } 12332 // Next try TokenManager if available 12333 else if (window.TokenManager && typeof window.TokenManager.getToken === 'function') { 12334 token = window.TokenManager.getToken(); 12335 12336 // Remove Bearer prefix if present (we'll add it later) 12337 if (token && token.startsWith('Bearer ')) { 12338 token = token.substring(7); 12339 } 12340 12341 console.log('Using TokenManager token for save operation, length:', token ? token.length : 0); 12342 tokenSource = 'TokenManager'; 12343 } 12344 // Fallback to localStorage 12345 else if (localStorage.getItem('hydra_auth_token')) { 12346 token = localStorage.getItem('hydra_auth_token'); 12347 console.log('Using localStorage token for save operation, length:', token.length); 12348 tokenSource = 'localStorage'; 12349 } 12350 // Last resort: try to get a fresh token 12351 else if (magic && magic.user) { 12352 try { 12353 console.log('Attempting to get fresh token from Magic'); 12354 token = await magic.user.getIdToken(); 12355 console.log('Obtained fresh token from Magic:', token ? 'success' : 'failed'); 12356 tokenSource = token ? 'magic' : 'none'; 12357 } catch (e) { 12358 console.error('Error getting token from Magic:', e); 12359 tokenSource = 'magic_error'; 12360 } 12361 } 12362 12363 // [AUTH_SAVE] Log for S+J download - token state and logged-in email before request 12364 const loggedInEmail = (window.userMetadata && window.userMetadata.email) || 12365 localStorage.getItem('hydra_auth_email') || 12366 ''; 12367 console.log('[AUTH_SAVE] Token resolved:', { 12368 loggedInEmail: loggedInEmail || '(none)', 12369 tokenSource, 12370 tokenLength: token ? token.length : 0, 12371 tokenPrefix: token ? token.substring(0, 15) + '...' : '(none)', 12372 hasToken: !!token, 12373 isAuthenticated, 12374 hydraIsAdmin: localStorage.getItem('hydra_is_admin') === 'true', 12375 hasLocalStorageToken: !!localStorage.getItem('hydra_auth_token'), 12376 hasTokenManager: !!(window.TokenManager && typeof window.TokenManager.getToken === 'function') 12377 }); 12378 12379 if (!token) { 12380 console.error('[AUTH_SAVE] No token available - save will fail. Sources checked: didToken, TokenManager, localStorage, magic.'); 12381 throw new Error('Could not find a valid authentication token. Please log in again.'); 12382 } 12383 12384 // IMPORTANT: Try to save to localStorage with isolated error handling 12385 // This ensures localStorage failures don't prevent server saves 12386 if (!customData) { // Only try localStorage for regular gallery saves
12387 try { 12388 // FIXED: Always save the FULL galleries data to localStorage 12389 localStorage.setItem('galleries', JSON.stringify(galleries)); 12390 console.log(`Saved ${galleries.length} galleries to localStorage (FULL format with complete settings)`); 12391 } catch (storageError) { 12392 // Now storage errors are isolated and non-fatal 12393 console.warn('LocalStorage quota exceeded, proceeding without local backup', storageError); 12394 } 12395 } else if (customData && customData.isImport) { 12396 // Skip localStorage for import operations - data too large 12397 console.log('Skipping localStorage for import operation - data too large for local storage'); 12398 } 12399 12400 // IMPORTANT: Ensure all pages have proper URLs and slugs before saving 12401 galleries.forEach(gallery => { 12402 if (gallery.isPage === true) { 12403 const pageSlug = slugify(gallery.title); 12404 12405 // Make sure slug and URL are set correctly 12406 if (!gallery.slug || gallery.slug === '') { 12407 gallery.slug = pageSlug; 12408 } 12409 12410 // Make sure URL points to slug 12411 if (!gallery.url || gallery.url === '/' || !gallery.url.includes(gallery.slug)) { 12412 gallery.url = `/${pageSlug}`; 12413 } 12414 } 12415 }); 12416 12417 // Set up the data to save - ensure we're using the complete data 12418 let siteData; 12419 console.log('saveGalleries: customData received:', JSON.stringify(customData)); 12420 console.log('saveGalleries: siteMetadata in customData:', customData?.siteMetadata); 12421 console.log('saveGalleries: TITLE in customData:', customData?.siteMetadata?.title); 12422 console.log('saveGalleries: GOOGLE ANALYTICS in customData:', customData?.siteMetadata?.googleAnalytics); 12423 if (customData) { 12424 // CRITICAL FIX: Ensure gallery settings are preserved when saving from sidebar 12425 // If customData contains galleries (which it should for sidebar-initiated saves), 12426 // merge them with any existing settings to ensure nothing is lost 12427 if (customData.galleries && Array.isArray(customData.galleries)) { 12428 // First, ensure we preserve all current gallery settings from the existing galleries 12429 const existingGalleries = (typeof galleries !== 'undefined' ? galleries : window.galleries) || []; 12430 12431 // Create merged galleries array with preserved settings 12432 const mergedGalleries = customData.galleries.map(gallery => { 12433 const existingGallery = existingGalleries.find(g => g.id === gallery.id); 12434 12435 // If this gallery exists in the current dataset, ensure all galleryOptions are preserved 12436 if (existingGallery && existingGallery.galleryOptions && gallery.galleryOptions) { 12437 // Calculate settings depth 12438 const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length; 12439 const serverOptionsDepth = Object.keys(gallery.galleryOptions).length; 12440 12441 // If server has fewer settings, merge them with existing settings 12442 if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) { 12443 console.log(`Preserving rich settings for ${gallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`); 12444 12445 // Create a merged gallery with preserved settings 12446 return { 12447 ...gallery, 12448 galleryOptions: { 12449 ...existingGallery.galleryOptions, // Start with all existing settings 12450 ...gallery.galleryOptions // Override with any new server settings 12451 } 12452 }; 12453 } 12454 } 12455 12456 // If no preservation needed, use server gallery as is 12457 return gallery; 12458 }); 12459 12460 // Use the merged galleries in customData 12461 siteData = { 12462 ...customData, 12463 galleries: mergedGalleries 12464 }; 12465 12466 console.log(`Settings-preservation merge completed: ${mergedGalleries.length} galleries`); 12467 console.log('saveGalleries: siteData after merge:', JSON.stringify(siteData)); 12468 console.log('saveGalleries: siteMetadata after merge:', siteData.siteMetadata); 12469 console.log('saveGalleries: TITLE after merge:', siteData.siteMetadata?.title); 12470 console.log('saveGalleries: GOOGLE ANALYTICS after merge:', siteData.siteMetadata?.googleAnalytics); 12471 } else { 12472 siteData = customData; 12473 } 12474 } else { 12475 const galleriesForSave = (typeof window !== 'undefined' && window.galleries && Array.isArray(window.galleries)) 12476 ? window.galleries 12477 : galleries; 12478 siteData = { 12479 galleries: galleriesForSave 12480 }; 12481 12482 if (window.siteMetadata) { 12483 siteData.siteMetadata = window.siteMetadata; 12484 } 12485 12486 if (window.SidebarManager) { 12487 const liveSidebar = typeof window.SidebarManager.getElementsForSave === 'function' 12488 ? window.SidebarManager.getElementsForSave() 12489 : null; 12490 if (Array.isArray(liveSidebar) && liveSidebar.length > 0) { 12491 siteData.sidebarElements = liveSidebar; 12492 } 12493 } 12494 } 12495 12496 attachLiveSidebarElements(siteData); 12497 12498 // Ensure galleries are never saved as pages: any item with galleryOptions is a gallery and must persist with isPage false.
12499 // (loadPage can set isPage=true in memory; sidebar/full saves would otherwise persist that and convert TEAM etc. to pages.) 12500 if (siteData.galleries && Array.isArray(siteData.galleries)) { 12501 siteData.galleries = siteData.galleries.map(g => { 12502 if (g && g.galleryOptions && typeof g.galleryOptions === 'object') { 12503 return { ...g, isPage: false }; 12504 } 12505 return g; 12506 }); 12507 } 12508 12509 // Get the save URL 12510 const saveUrl = typeof getApiUrl === 'function' ? 12511 getApiUrl('/api/save-config') : 12512 '/api/save-config'; 12513 12514 // Get user email from various sources 12515 const userEmail = (window.userMetadata && window.userMetadata.email) || 12516 localStorage.getItem('hydra_auth_email') || 12517 ''; 12518 12519 // Ensure token has proper format 12520 const formattedToken = token.startsWith('hydra:') ? token : 12521 (token.includes('.') ? token : `hydra:${token}`); 12522 12523 console.log('Making server save request...'); 12524 console.log('saveGalleries: Final siteData being sent to server:', JSON.stringify(siteData)); 12525 console.log('saveGalleries: Final siteMetadata being sent:', siteData.siteMetadata); 12526 console.log('saveGalleries: FINAL TITLE being sent:', siteData.siteMetadata?.title); 12527 console.log('saveGalleries: FINAL GOOGLE ANALYTICS being sent:', siteData.siteMetadata?.googleAnalytics); 12528 12529 // [AUTH_SAVE] Log for S+J download - request details (no full token) 12530 console.log('[AUTH_SAVE] Sending save request:', { 12531 saveUrl, 12532 authHeaderPrefix: `Bearer ${formattedToken.substring(0, 20)}...`, 12533 userEmail: userEmail || '(empty)', 12534 tokenSource, 12535 formattedTokenLength: formattedToken.length 12536 }); 12537 12538 // Make the request 12539 let response = await fetch(saveUrl, { 12540 method: 'POST', 12541 headers: { 12542 'Content-Type': 'application/json', 12543 'Authorization': `Bearer ${formattedToken}`, 12544 'X-User-Email': userEmail, 12545 'X-API-Request': 'true' // Always include this header for API requests 12546 }, 12547 body: JSON.stringify(siteData) 12548 }); 12549 12550 console.log('Save response status:', response.status); 12551 12552 // Process the response 12553 let responseText = await response.text(); 12554 console.log('Server response text (first 500 chars):', responseText.substring(0, 500)); 12555 let responseData; 12556 12557 try { 12558 responseData = JSON.parse(responseText); 12559 console.log('Server response parsed successfully:', responseData); 12560 if (responseData.debug) { 12561 console.log('Server debug info:', responseData.debug); 12562 console.log('Server debug - requestBodyKeys:', responseData.debug.requestBodyKeys); 12563 console.log('Server debug - hasSiteMetadata:', responseData.debug.hasSiteMetadata); 12564 console.log('Server debug - siteMetadata:', responseData.debug.siteMetadata); 12565 console.log('Server debug - finalSiteMetadata:', responseData.debug.finalSiteMetadata); 12566 console.log('Server debug - googleAnalytics:', responseData.debug.googleAnalytics); 12567 } 12568 } catch (jsonError) { 12569 if (response.status === 200 && responseText.trim().startsWith('<!DOCTYPE html>')) { 12570 responseData = { success: true }; 12571 } else { 12572 throw new Error('Invalid response format: ' + responseText.substring(0, 100)); 12573 } 12574 } 12575 12576 if (!response.ok && !responseData.success) { 12577 // [AUTH_SAVE] Log for S+J download - capture full 401/auth failure details 12578 const authSaveFailure = { 12579 status: response.status, 12580 statusText: response.statusText, 12581 error: responseData && responseData.error, 12582 details: responseData && responseData.details, 12583 authMethod: responseData && responseData.authMethod, 12584 fullResponse: responseData, 12585 responseTextPreview: responseText.substring(0, 300) 12586 }; 12587 console.error('[AUTH_SAVE] Save request failed:', authSaveFailure); 12588 12589 // Retry once on 401: token may have expired - try refresh or fresh Magic token 12590 if (response.status === 401) {
12591 console.log('[AUTH_SAVE] 401 received - attempting token refresh before retry...'); 12592 let newToken = null; 12593 if (window.TokenManager && typeof window.TokenManager.refreshToken === 'function') { 12594 newToken = await window.TokenManager.refreshToken(); 12595 } 12596 if (!newToken && magic && magic.user) { 12597 try { 12598 newToken = await magic.user.getIdToken(); 12599 } catch (e) { 12600 console.warn('[AUTH_SAVE] Magic getIdToken failed:', e); 12601 } 12602 } 12603 if (newToken) { 12604 if (newToken.startsWith('Bearer ')) newToken = newToken.substring(7); 12605 token = newToken; 12606 didToken = newToken; 12607 localStorage.setItem('hydra_auth_token', newToken); 12608 if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') { 12609 window.TokenManager.storeToken(newToken.startsWith('hydra:') ? newToken : `hydra:${newToken}`, userEmail); 12610 } 12611 console.log('[AUTH_SAVE] Got fresh token, retrying save...'); 12612 const retryResponse = await fetch(saveUrl, { 12613 method: 'POST', 12614 headers: { 12615 'Content-Type': 'application/json', 12616 'Authorization': `Bearer ${token.startsWith('hydra:') ? token : (token.includes('.') ? token : `hydra:${token}`)}`, 12617 'X-User-Email': userEmail, 12618 'X-API-Request': 'true' 12619 }, 12620 body: JSON.stringify(siteData) 12621 }); 12622 const retryText = await retryResponse.text(); 12623 let retryData; 12624 try { retryData = JSON.parse(retryText); } catch (e) { 12625 if (retryResponse.status === 200 && retryText.startsWith('<!DOCTYPE')) retryData = { success: true }; 12626 else throw new Error('Invalid retry response'); 12627 } 12628 if (retryResponse.ok || retryData.success) { 12629 response = retryResponse; 12630 responseText = retryText; 12631 responseData = retryData; 12632 console.log('[AUTH_SAVE] Retry succeeded'); 12633 } else { 12634 console.error('[AUTH_SAVE] Retry also failed:', retryResponse.status, retryData); 12635 throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`); 12636 } 12637 } else { 12638 throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`); 12639 } 12640 } else { 12641 throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`); 12642 } 12643 } 12644 12645 console.log('Configuration saved successfully'); 12646 12647 // Update local lastKnownConfigTimestamp after successful save 12648 if (responseData.lastModified) { 12649 window.lastKnownConfigTimestamp = responseData.lastModified; 12650 console.error('[CONFLICT DETECT] â Updated local config timestamp from save response:', window.lastKnownConfigTimestamp); 12651 } else { 12652 console.error('[CONFLICT DETECT] â ï¸ No lastModified in save response, fetching from server...'); 12653 // If server doesn't return timestamp, fetch it 12654 try { 12655 const timestampUrl = typeof getApiUrl === 'function' ? 12656 getApiUrl('/api/config-timestamp') : 12657 '/api/config-timestamp'; 12658 const timestampResponse = await fetch(timestampUrl); 12659 if (timestampResponse.ok) { 12660 const timestampData = await timestampResponse.json(); 12661 if (timestampData.lastModified) { 12662 window.lastKnownConfigTimestamp = timestampData.lastModified; 12663 console.error('[CONFLICT DETECT] â Fetched and updated local config timestamp:', window.lastKnownConfigTimestamp); 12664 } else { 12665 console.error('[CONFLICT DETECT] â ï¸ No lastModified in fetched timestamp data:', timestampData); 12666 } 12667 } else { 12668 console.error('[CONFLICT DETECT] â ï¸ Failed to fetch timestamp after save, status:', timestampResponse.status); 12669 } 12670 } catch (err) { 12671 console.error('[CONFLICT DETECT] â ï¸ Could not fetch timestamp after save:', err); 12672 } 12673 } 12674 12675 // Store new token if provided 12676 if (responseData.hydraToken) { 12677 console.log('Received new hydra token from server'); 12678 12679 // Update the global didToken 12680 didToken = responseData.hydraToken.startsWith('hydra:') ? 12681 responseData.hydraToken.substring(6) : responseData.hydraToken; 12682 12683 // Update localStorage 12684 localStorage.setItem('hydra_auth_token', didToken); 12685 12686 // Update TokenManager if available 12687 if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') { 12688 window.TokenManager.storeToken(responseData.hydraToken, userEmail); 12689 } 12690 } 12691 12692 // Process server response data if needed 12693 if (responseData.galleries) { 12694 // If server returns updated galleries data, update the global galleries variable 12695 if (Array.isArray(responseData.galleries) && responseData.galleries.length > 0) { 12696 // CRITICAL FIX: Before replacing galleries with server response, 12697 // preserve any gallery settings that might be lost in the response 12698 const updatedGalleries = responseData.galleries.map(serverGallery => { 12699 // Find the matching gallery in our current data 12700 const existingGallery = galleries.find(g => g.id === serverGallery.id); 12701 12702 // Check if we need to preserve gallery settings 12703 if (existingGallery && existingGallery.galleryOptions && serverGallery.galleryOptions) { 12704 // Calculate settings depth 12705 const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length; 12706 const serverOptionsDepth = Object.keys(serverGallery.galleryOptions).length; 12707 12708 // If server has fewer settings, merge them with existing settings 12709 if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) { 12710 console.log(`Preserving rich settings for ${serverGallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`); 12711 12712 // Create a merged gallery with preserved settings 12713 return { 12714 ...serverGallery, 12715 galleryOptions: { 12716 ...existingGallery.galleryOptions, // Start with all existing settings 12717 ...serverGallery.galleryOptions // Override with any new server settings 12718 } 12719 }; 12720 } 12721 } 12722 12723 // If no preservation needed, use server gallery as is 12724 return serverGallery; 12725 }); 12726 12727 // Update the galleries variable with our preserved data 12728 galleries = updatedGalleries; 12729 console.log(`Updated galleries from server response: ${galleries.length} galleries`); 12730 12731 // IMPORTANT: Synchronize again after receiving server response 12732 if (typeof synchronizeGalleries === 'function') { 12733 synchronizeGalleries(); 12734 console.log('Galleries re-synchronized after server response'); 12735 } 12736 12737 // Keep sidebar UI in sync with current galleries (e.g. after delete) 12738 if (typeof renderGalleries === 'function') { 12739 renderGalleries(); 12740 } 12741 12742 // Update localStorage with full data 12743 try { 12744 localStorage.setItem('galleries', JSON.stringify(galleries)); 12745 console.log('Updated localStorage with server gallery data (FULL format)'); 12746 } catch (e) { 12747 console.warn('Could not update localStorage with server gallery data', e); 12748 } 12749 } 12750 } 12751
12752 // CRITICAL FIX: Clear all sessionStorage caches after successful save 12753 // This ensures that when users open the site in a new tab, they get fresh data 12754 try { 12755 console.log('Clearing sessionStorage caches after successful save...'); 12756 const keysToRemove = []; 12757 for (let i = 0; i < sessionStorage.length; i++) { 12758 const key = sessionStorage.key(i); 12759 if (key && (key.includes('gallery_data_') || key.includes('neonGallery_'))) { 12760 keysToRemove.push(key); 12761 } 12762 } 12763 keysToRemove.forEach(key => { 12764 sessionStorage.removeItem(key); 12765 console.log('Cleared sessionStorage key: ' + key); 12766 }); 12767 console.log('Cleared ' + keysToRemove.length + ' sessionStorage cache entries'); 12768 } catch (cacheError) { 12769 console.warn('Error clearing sessionStorage cache:', cacheError); 12770 // Don't fail the save if cache clearing fails 12771 } 12772 12773 window._saveInProgress = false; 12774 12775 if (!customData || !Array.isArray(customData.sidebarElements)) { 12776 document.dispatchEvent(new CustomEvent('sidebar-save-requested')); 12777 } 12778 12779 // Flush any save queued while this request was in flight 12780 if (window._pendingSaveGalleriesData) { 12781 const pending = window._pendingSaveGalleriesData; 12782 window._pendingSaveGalleriesData = null; 12783 const resolvers = window._pendingSaveResolvers || []; 12784 window._pendingSaveResolvers = []; 12785 return saveGalleries(pending).then(function(result) { 12786 resolvers.forEach(function(r) { r(result); }); 12787 return result; 12788 }); 12789 } 12790 12791 const idleResolvers = window._pendingSaveResolvers || []; 12792 window._pendingSaveResolvers = []; 12793 idleResolvers.forEach(function(r) { r(true); }); 12794 12795 return true; 12796 } catch (error) { 12797 // [AUTH_SAVE] Log for S+J download - capture exception (network, parse, or thrown after 401) 12798 console.error('[AUTH_SAVE] Save threw exception:', { 12799 message: error.message, 12800 name: error.name, 12801 stack: error.stack 12802 }); 12803 console.error('Error saving configuration:', error); 12804 alert('Failed to save changes: ' + error.message); 12805 window._saveInProgress = false; 12806 const failResolvers = window._pendingSaveResolvers || []; 12807 window._pendingSaveResolvers = []; 12808 failResolvers.forEach(function(r) { r(false); }); 12809 return false; 12810 } 12811} 12812 12813// Helper function to get API URL for preview sites 12814function getApiUrl(endpoint) { 12815 // Check if we're on a preview URL 12816 const isPreviewUrl = window.location.hostname === 'preview.neonsky.app'; 12817 12818 if (isPreviewUrl) { 12819 // Extract the site GUID from the path (first segment after domain) 12820 const pathParts = window.location.pathname.split('/').filter(Boolean); 12821 if (pathParts.length > 0) { 12822 const siteGuid = pathParts[0]; 12823 // Include the site GUID in the API URL 12824 return `/${siteGuid}${endpoint}`; 12825 } 12826 } 12827 12828 // Regular case - return the endpoint as is 12829 return endpoint; 12830} 12831 12832function updateActiveStates() { 12833 // Get the most current active gallery ID, ensuring consistency between both variables 12834 const currentActiveId = window.activeGalleryId || activeGalleryId; 12835 12836 // Ensure both variables are synchronized 12837 window.activeGalleryId = currentActiveId; 12838 activeGalleryId = currentActiveId; 12839 12840 console.log("updateActiveStates: Using active gallery ID:", currentActiveId); 12841 12842 // Special case for invisible home page - don't try to highlight it in the menu 12843 const activeGallery = galleries.find(g => g.id === currentActiveId); 12844 if (activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true) { 12845 console.log("Active gallery is invisible home page - not highlighting in menu"); 12846 // We're not returning here, so it will still update the visibility classes 12847 // but won't try to find an element to mark as active 12848 } 12849 12850 // Fix selector issue - look for both .tree and .sidebar 12851 document.querySelectorAll('.tree li, .sidebar li').forEach(li => { 12852 if (!li.dataset.id) return; // Skip items without data-id attribute 12853 12854 const id = parseInt(li.dataset.id); 12855 if (isNaN(id)) return; // Skip if ID is not a number 12856 12857 const gallery = galleries.find(g => g.id === id); 12858 12859 // Update active state using the most current ID 12860 const isActive = id === currentActiveId; 12861 12862 // Add or remove the active class 12863 if (isActive) { 12864 li.classList.add('active'); 12865 console.log(`Setting active class for element: ${id} (${gallery ? gallery.title : 'Unknown'})`); 12866 } else { 12867 li.classList.remove('active'); 12868 } 12869 12870 // Update visibility class if gallery exists 12871 if (gallery) { 12872 li.classList.toggle('hidden-gallery', gallery.visible === false); 12873 12874 // Update visibility toggle icon if it exists 12875 const toggleButton = li.querySelector('.visibility-toggle'); 12876 if (toggleButton) { 12877 toggleButton.classList.toggle('hidden', gallery.visible === false); 12878 } 12879 } 12880 }); 12881 12882 // Verify that the active state was actually set for the current activeGalleryId 12883 const activeElements = document.querySelectorAll('.active'); 12884 if (activeElements.length === 0 && currentActiveId) { 12885 // Only log a warning if we're not dealing with an invisible home page 12886 const activeGallery = galleries.find(g => g.id === currentActiveId); 12887 if (!(activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true)) { 12888 console.warn(`No active elements found after update for ID: ${currentActiveId}`); 12889 } 12890 } 12891 12892 // Log active items for debugging 12893 console.log("Active gallery ID after update:", currentActiveId);
12894 activeElements.forEach(el => 12895 console.log("Active element:", el.textContent.trim()) 12896 ); 12897} 12898 12899 12900function debugGalleryStructure() { 12901console.group('Gallery Structure Debug'); 12902 12903// Log the raw data structure 12904console.log('Raw galleries array:', JSON.parse(JSON.stringify(galleries))); 12905 12906// Log the tree structure 12907const treeStructure = createGalleryTree(galleries); 12908 12909// Helper to print the tree 12910function printTree(items, level = 0) { 12911const indent = ' '.repeat(level); 12912items.forEach(item => { 12913 console.log(`${indent}${item.title} (ID: ${item.id}, Parent: ${item.parentId || 'ROOT'})`); 12914 if (item.children && item.children.length > 0) { 12915 printTree(item.children, level + 1); 12916 } 12917}); 12918} 12919 12920console.log('Tree structure:'); 12921printTree(treeStructure); 12922 12923// Log the DOM structure 12924console.log('DOM structure:'); 12925const treeEl = document.getElementById('galleryTree'); 12926console.log(treeEl); 12927 12928console.groupEnd(); 12929} 12930 12931function createGalleryTree(galleries) { 12932console.log("Creating gallery tree with preservation safeguards..."); 12933 12934// First, create a map of galleries by id for quick lookup 12935const galleryMap = {}; 12936const processedIds = new Set(); 12937 12938// First pass: Create clean copies without children 12939galleries.forEach(gallery => { 12940if (!gallery || !gallery.id) return; // Skip invalid galleries 12941 12942// Create a copy of the gallery object without the children field 12943const cleanGallery = { ...gallery }; 12944delete cleanGallery.children; 12945 12946// Initialize an empty children array 12947cleanGallery.children = []; 12948 12949// Add to map - even if we've seen this ID before, keep the latest version 12950if (processedIds.has(gallery.id)) { 12951 console.warn(`Multiple galleries with ID ${gallery.id} - using latest version`); 12952} 12953 12954processedIds.add(gallery.id); 12955galleryMap[gallery.id] = cleanGallery; 12956}); 12957 12958// Second pass: Assign children to their parents 12959const rootGalleries = []; 12960 12961// To detect already-processed children 12962const childrenAssigned = new Set(); 12963 12964galleries.forEach(gallery => { 12965if (!gallery || !gallery.id) return; // Skip invalid galleries 12966 12967const galleryId = gallery.id; 12968 12969// Skip if this gallery isn't in our map (could be invalid) 12970if (!galleryMap[galleryId]) return; 12971 12972// Skip if we've already assigned this gallery as a child 12973if (childrenAssigned.has(galleryId)) return; 12974 12975if (gallery.parentId && galleryMap[gallery.parentId]) { 12976 // This is a child gallery, but first check it doesn't create a circular reference 12977 12978 // Find all parents in the chain up to the root 12979 let currentParent = galleryMap[gallery.parentId]; 12980 let safeToAdd = true; 12981 let chainIds = [galleryId, gallery.parentId]; 12982 12983 while (currentParent.parentId) { 12984 // If we've seen this ID in the chain, it would create a circular reference 12985 if (chainIds.includes(currentParent.parentId)) { 12986 console.warn(`Would create circular reference for gallery ${gallery.title} - making it a root gallery`); 12987 safeToAdd = false; 12988 break; 12989 } 12990 12991 chainIds.push(currentParent.parentId); 12992 currentParent = galleryMap[currentParent.parentId]; 12993 12994 // If parent doesn't exist in the map, break the chain 12995 if (!currentParent) break; 12996 } 12997 12998 if (safeToAdd) { 12999 // Safe to add as child 13000 galleryMap[gallery.parentId].children.push(galleryMap[galleryId]); 13001 childrenAssigned.add(galleryId); 13002 } else { 13003 // Not safe - make it a root gallery 13004 rootGalleries.push(galleryMap[galleryId]); 13005 } 13006} else { 13007 // This is a root gallery (no parent or parent doesn't exist) 13008 if (!childrenAssigned.has(galleryId)) { 13009 rootGalleries.push(galleryMap[galleryId]); 13010 } 13011} 13012}); 13013 13014// Final verification 13015const allGalleryIds = new Set(galleries.map(g => g.id)); 13016const treeGalleryIds = new Set(); 13017 13018// Function to collect all IDs in the tree 13019const collectIds = (items) => { 13020items.forEach(item => { 13021 treeGalleryIds.add(item.id); 13022 if (item.children && item.children.length > 0) { 13023 collectIds(item.children); 13024 } 13025}); 13026}; 13027 13028collectIds(rootGalleries); 13029 13030// Check if any galleries are missing from the tree 13031const missingIds = []; 13032allGalleryIds.forEach(id => { 13033if (!treeGalleryIds.has(id)) { 13034 missingIds.push(id); 13035} 13036}); 13037 13038if (missingIds.length > 0) { 13039console.warn(`${missingIds.length} galleries are missing from the tree structure`); 13040 13041// Add the missing galleries as root items 13042missingIds.forEach(id => { 13043 const gallery = galleryMap[id]; 13044 if (gallery && !childrenAssigned.has(id)) { 13045 console.log(`Adding missing gallery to root: ${gallery.title}`); 13046 rootGalleries.push(gallery); 13047 } 13048}); 13049} 13050 13051console.log(`Created tree with ${rootGalleries.length} root galleries`); 13052return rootGalleries; 13053} 13054 13055function alignFolderStates() { 13056 console.log("Aligning folder data states with collapsed visual appearance"); 13057 let changed = 0; 13058
13059 galleries.forEach(gallery => { 13060 if ((gallery.isFolder === true || gallery.isSubmenu === true) && gallery.isCollapsed === false) { 13061 gallery.isCollapsed = true; 13062 changed++; 13063 } 13064 }); 13065 13066} 13067 13068 13069 13070function renderGalleryItem(gallery, level = 0, maxLevel = 10) { 13071 // Prevent infinite recursion by limiting depth 13072 if (level >= maxLevel) { 13073 console.warn(`Maximum nesting level reached for gallery: ${gallery.title}`); 13074 return ''; 13075 } 13076 13077 // Add debug info to console 13078 console.log(`Rendering gallery item: ${gallery.title}, isEditing=${isEditing}, level=${level}`); 13079 13080 // Skip hidden items in view mode (but not spacers) 13081 if (!isEditing && gallery.visible === false && !gallery.isSpacer) { 13082 return ''; 13083 } 13084 13085const isPage = isPageMenuItem(gallery); 13086const isFolder = gallery.isFolder === true || gallery.isSubmenu === true; // Support both folder and legacy submenu 13087const isNavigableParent = isNavigableMenuParent(gallery); 13088const isSpacer = gallery.isSpacer === true; 13089const isExternal = gallery.isExternal === true; 13090 13091// CRITICAL FIX: Ensure children are always populated for folders 13092// If gallery.children is missing or empty, but this is a folder, build children from flat galleries array 13093// This ensures the nested <ol> is always rendered with children in the DOM, even in view mode 13094if (isFolder) { 13095 // Always ensure children array exists 13096 if (!gallery.children) { 13097 gallery.children = []; 13098 } 13099 // If children array is empty, build it from flat galleries array using parentId 13100 if (gallery.children.length === 0) { 13101 const childrenFromFlat = galleries.filter(g => g.parentId === gallery.id); 13102 if (childrenFromFlat.length > 0) { 13103 gallery.children = childrenFromFlat; 13104 console.log(`[renderGalleryItem] Built children for folder ${gallery.title}: ${gallery.children.length} children`); 13105 } 13106 } 13107} 13108 13109// Parent-capable items need their chevron and empty nested drop zone immediately, 13110// before the first child has been added. 13111const hasChildren = isFolder || isNavigableParent || gallery.children?.length > 0; 13112const hasChildrenClass = hasChildren ? 'has-children' : ''; 13113 13114// Standard collapsed state check 13115const isCollapsed = gallery.isCollapsed === true; 13116 13117// Simple expanded class logic - if the folder is not collapsed, add 'expanded' class 13118const isExpandedClass = (isFolder || isNavigableParent) && !isCollapsed ? 'expanded' : ''; 13119 13120// Add additional classes for special types 13121const spacerClass = isSpacer ? 'spacer-item' : ''; 13122const externalClass = isExternal ? 'external-link' : ''; 13123const folderClass = isFolder ? 'folder-item' : ''; 13124const navigableParentClass = isNavigableParent ? 'navigable-parent-item' : ''; 13125 13126 const duplicateIconSvg = ` 13127 <svg xmlns="[http://www.w3.org/2000/svg](http://www.w3.org/2000/svg)" width="24" height="24" viewBox="0 0 24 24"> 13128 <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"> 13129 <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"/> 13130 <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"/> 13131 </g> 13132 </svg> 13133 `; 13134 13135 13136 13137 // NEW: Add nesting level as a data attribute for styling 13138 const nestingLevelAttr = `data-nesting-level="${level}"`; 13139 13140 // Accessible href for SPA items (loadPage/loadGallery still preventDefault) 13141 const menuHref = gallery.isHomePage 13142 ? '/' 13143 : (gallery.url && String(gallery.url).startsWith('/') 13144 ? gallery.url 13145 : (gallery.slug ? ('/' + gallery.slug) : '#')); 13146 const childrenListId = 'gallery-children-' + gallery.id; 13147 const folderExpandedAttr = !isCollapsed ? 'true' : 'false'; 13148 const ariaCurrentAttr = (gallery.id === activeGalleryId && !isFolder && !isSpacer) 13149 ? ' aria-current="page"' 13150 : ''; 13151 13152 // Create the gallery item HTML with enhanced nesting indicators 13153 const html = ` 13154<li data-id="${gallery.id}" 13155 ${nestingLevelAttr} 13156 class="${gallery.id === activeGalleryId ? 'active' : ''} 13157 ${gallery.visible === false ? 'hidden-gallery' : ''} 13158 ${hasChildrenClass} ${isExpandedClass} ${spacerClass} ${externalClass} ${folderClass} ${navigableParentClass}">
13159 <div class="gallery-item-content nested-sortable-handle"> 13160 ${hasChildren ? ` 13161 <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, '"')}"> 13162 <svg class="icon toggle-icon ${!isCollapsed ? 'rotated' : ''}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> 13163 <polyline points="9 6 15 12 9 18"></polyline> 13164 </svg> 13165 </button> 13166 ` : ''} 13167 13168 <!-- Menu item with different rendering based on type --> 13169 ${isSpacer ? ` 13170 <!-- Spacer with non-breaking space to ensure height --> 13171 <span class="menu-item spacer" aria-hidden="true"> </span> 13172 ` : isExternal ? ` 13173 <!-- External URL uses an anchor tag with target="_blank" --> 13174 <a href="${gallery.url}" class="menu-item external-link" target="_blank" rel="noopener noreferrer"${ariaCurrentAttr}> 13175 ${gallery.title} 13176 </a> 13177 ` : isFolder ? ` 13178 <!-- Folder item with toggle functionality (button for keyboard / SR) --> 13179 <button type="button" class="menu-item folder" data-folder="true" 13180 onclick="toggleFolder(${gallery.id}, event)" aria-expanded="${folderExpandedAttr}" aria-controls="${childrenListId}"> 13181 ${gallery.title} 13182 </button> 13183 ` : ` 13184 <!-- Real link for SR/keyboard; SPA handlers keep click behavior --> 13185 <a href="${menuHref}" class="menu-item" onclick="${isPage ? `loadPage(${gallery.id}, event)` : `loadGallery(${gallery.id}, event)`}"${ariaCurrentAttr}> 13186 ${gallery.title} 13187 </a> 13188 `} 13189 13190<div class="controls"> 13191 ${!isSpacer ? ` 13192 <button class="btn visibility-toggle ${gallery.visible === false ? 'hidden' : ''}" onclick="toggleVisibility(${gallery.id}, event)"> 13193 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> 13194 ${gallery.visible !== false ? 13195 '<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>' : 13196 '<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>' 13197 } 13198 </svg> 13199 </button> 13200 13201 <button class="btn password-toggle ${gallery.passwordProtected ? 'protected' : ''}" onclick="toggleGalleryPasswordProtection(${gallery.id}, event)" title="${gallery.passwordProtected ? 'Remove Password Protection' : 'Add Password Protection'}"> 13202 <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24"> 13203 ${gallery.passwordProtected ? 13204 '<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>' : 13205 '<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"/>' 13206 } 13207 </svg> 13208 </button> 13209 13210 <button class="btn edit-btn" onclick="editGallery(${gallery.id}, event)">
13211 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> 13212 <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> 13213 <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path> 13214 </svg> 13215 </button> 13216 ` : ''} 13217 13218 <button class="btn duplicate-btn" onclick="duplicateGalleryItem(${gallery.id}, event)" title="Duplicate"> 13219 ${duplicateIconSvg} 13220 </button> 13221 13222 13223 <button class="btn delete-btn" onclick="deleteGallery(${gallery.id}, event)"> 13224 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> 13225 <polyline points="3 6 5 6 21 6"></polyline> 13226 <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> 13227 </svg> 13228 </button> 13229${!isSpacer ? ` 13230<!-- Add home toggle button --> 13231<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'}"> 13232 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> 13233 ${gallery.isHomePage ? 13234 '<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"/>' : 13235 '<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>' 13236 } 13237 </svg> 13238</button> 13239` : ''} 13240 </div> 13241 </div> 13242 <ol id="${childrenListId}" class="nested-sortable ${level > 0 ? 'nested-level-' + level : ''}"> 13243 ${gallery.children && gallery.children.length > 0 ? 13244 gallery.children.map(child => renderGalleryItem(child, level + 1, maxLevel)).join('') : ''} 13245 </ol> 13246</li> 13247`; 13248 13249 return html; 13250} 13251 13252// Additional helper function to add visual indicators during drag operations 13253function addDragVisualFeedback() { 13254 // Add event listener for drag operations 13255 document.addEventListener('dragover', function(e) { 13256 // Find the closest nested sortable list 13257 const nearestList = e.target.closest('.nested-sortable'); 13258 if (!nearestList) return; 13259 13260 // Remove active-drop-target class from all lists 13261 document.querySelectorAll('.active-drop-target').forEach(el => { 13262 el.classList.remove('active-drop-target'); 13263 }); 13264 13265 // Add it to the current list 13266 nearestList.classList.add('active-drop-target'); 13267 13268 // Calculate if this would be a nested position 13269 // This is a simplified calculation - SortableJS has its own logic, 13270 // but this helps provide additional visual feedback 13271 const rect = nearestList.getBoundingClientRect(); 13272 const distanceFromLeft = e.clientX - rect.left; 13273 13274 // If we're close to the left edge of a nested list, add a class 13275 if (distanceFromLeft < 30) { 13276 nearestList.classList.add('potential-parent'); 13277 } else { 13278 nearestList.classList.remove('potential-parent'); 13279 } 13280 }); 13281 13282 // Remove classes when drag ends 13283 document.addEventListener('dragend', function() { 13284 document.querySelectorAll('.active-drop-target, .potential-parent').forEach(el => { 13285 el.classList.remove('active-drop-target', 'potential-parent'); 13286 }); 13287 }); 13288} 13289 13290// Make sure the addDragVisualFeedback function is called when the document is ready 13291document.addEventListener('DOMContentLoaded', function() { 13292 // Initialize the visual feedback helper 13293 addDragVisualFeedback(); 13294 13295 // If we're in edit mode, make sure the nested sortables are initialized 13296 if (document.body.classList.contains('edit-mode-active')) { 13297 setTimeout(function() { 13298 initializeNestedSortables(); 13299 }, 300); 13300 } 13301}); 13302 13303// Update the toggleEditMode function to ensure proper initialization of sortables 13304const originalToggleEditMode = window.toggleEditMode; 13305window.toggleEditMode = function() { 13306 originalToggleEditMode.apply(this, arguments); // Call original function 13307 13308 stopAutoAdvanceTimer(); 13309 13310 const sidebar = document.querySelector('.sidebar');
13311 const editControls = document.querySelector('.edit-controls'); 13312 13313 if (currentMenuLayout === 'horizontal') { 13314 if (document.body.classList.contains('edit-mode-active')) { 13315 // In horizontal layout and edit mode is ON: show sidebar and edit controls 13316 if(sidebar) sidebar.style.display = 'block'; // Or 'flex' if it's a flex container 13317 if(editControls) editControls.style.display = 'flex'; 13318 // Re-render the sidebar menu for editing 13319 renderGalleries(); 13320 } else { 13321 // In horizontal layout and edit mode is OFF: hide sidebar 13322 if(sidebar) sidebar.style.display = 'none'; 13323 // Re-render the horizontal menu for viewing 13324 renderHorizontalMenu(); 13325 } 13326 } else { 13327 // For other layouts (sidebar, top), ensure sidebar is visible 13328 if(sidebar) sidebar.style.display = 'block'; // Or 'flex' 13329 } 13330}; 13331 13332function verifyGalleryStructure() { 13333console.log("Verifying gallery structure integrity..."); 13334 13335// Count number of galleries 13336const galleryCount = galleries.length; 13337console.log(`Total galleries in flat array: ${galleryCount}`); 13338 13339// Create tree and count galleries in tree 13340const tree = createGalleryTree([...galleries]); 13341let treeCount = 0; 13342 13343function countGalleriesInTree(items) { 13344items.forEach(item => { 13345 treeCount++; 13346 if (item.children && item.children.length > 0) { 13347 countGalleriesInTree(item.children); 13348 } 13349}); 13350} 13351 13352countGalleriesInTree(tree); 13353console.log(`Total galleries in tree structure: ${treeCount}`); 13354 13355if (treeCount !== galleryCount) { 13356console.warn(`GALLERY COUNT MISMATCH: ${galleryCount} in array vs ${treeCount} in tree`); 13357return false; 13358} else { 13359console.log("Gallery structure is consistent"); 13360return true; 13361} 13362} 13363 13364function renderGalleries() { 13365console.log("Rendering galleries..."); 13366 13367 // Align with the explicit edit toggle only (not admin login / stale sidebar.editing) 13368 if (document.body.classList.contains('edit-mode-active')) { 13369 isEditing = true; 13370 } else if (typeof window.isEditing === 'boolean') { 13371 isEditing = window.isEditing; 13372 } 13373 13374 var galleriesSource = galleries; 13375 if (window.HelpSystem && typeof window.HelpSystem.getRenderGalleries === 'function') { 13376 var helpList = window.HelpSystem.getRenderGalleries(); 13377 if (helpList) galleriesSource = helpList; 13378 } 13379 13380try { 13381// Only fix circular references if absolutely necessary 13382const foundCircular = fixCircularReferences(); 13383 13384// Only create a new tree if we had to fix circular references 13385let galleryTree; 13386if (foundCircular) { 13387 console.log("Creating fresh gallery tree after fixing circular references"); 13388 galleryTree = createGalleryTree(galleriesSource); 13389} else { 13390 console.log("Using existing structure to create gallery tree"); 13391 galleryTree = createGalleryTree(galleriesSource); 13392} 13393 13394// Render the tree - IMPORTANT: Add null check 13395const tree = document.getElementById('galleryTree'); 13396if (!tree) { 13397 console.warn('galleryTree element not found, cannot render galleries'); 13398 return; // Exit the function if the element doesn't exist 13399} 13400 13401// Render the tree with maximum depth protection 13402tree.innerHTML = ` 13403 <ol class="nested-sortable"> 13404 ${galleryTree.map(gallery => renderGalleryItem(gallery, 0, 20)).join('')} 13405 </ol> 13406`; 13407 13408// Initialize sortable for all nested sortable elements 13409if (isEditing) { 13410 // Use requestAnimationFrame to ensure DOM is fully updated before initializing sortables 13411 // This prevents drag issues when items are added and immediately dragged 13412 requestAnimationFrame(() => { 13413 initializeNestedSortables(); 13414 }); 13415} 13416 13417console.log("Galleries rendered successfully"); 13418 13419// Set up folder toggle handler after rendering (works in both view and edit modes) 13420setupFolderToggleHandler(); 13421 13422// Verify structure integrity after rendering 13423verifyGalleryStructure(); 13424 13425if (typeof syncSeoNavA11y === 'function') syncSeoNavA11y(); 13426} catch (error) { 13427console.error("Error rendering galleries:", error); 13428} 13429} 13430 13431/** Prefer visible menu links for AT; keep SEO nav in DOM for crawlers. */ 13432function syncSeoNavA11y() { 13433 const seoNav = document.querySelector('.hydra-seo-nav'); 13434 if (!seoNav) return; 13435 const hasVisibleLinks = 13436 document.querySelectorAll('#galleryTree a.menu-item').length > 0 ||
13437 document.querySelectorAll('.horizontal-menu-container a.horizontal-menu-label').length > 0; 13438 const hideFromAt = hasVisibleLinks; 13439 seoNav.setAttribute('aria-hidden', hideFromAt ? 'true' : 'false'); 13440 // aria-hidden alone is not enough â focusable descendants must leave the tab order 13441 seoNav.querySelectorAll('a, button, [tabindex]').forEach((el) => { 13442 if (hideFromAt) { 13443 if (!el.hasAttribute('data-seo-prev-tabindex')) { 13444 el.setAttribute('data-seo-prev-tabindex', el.hasAttribute('tabindex') ? el.getAttribute('tabindex') : ''); 13445 } 13446 el.setAttribute('tabindex', '-1'); 13447 } else { 13448 const prev = el.getAttribute('data-seo-prev-tabindex'); 13449 if (prev === null) return; 13450 if (prev === '') el.removeAttribute('tabindex'); 13451 else el.setAttribute('tabindex', prev); 13452 el.removeAttribute('data-seo-prev-tabindex'); 13453 } 13454 }); 13455} 13456 13457function getMenuItemHref(galleryItem) { 13458 if (!galleryItem) return '#'; 13459 if (galleryItem.isExternal && galleryItem.url) return galleryItem.url; 13460 if (galleryItem.isHomePage) return '/'; 13461 if (galleryItem.url && String(galleryItem.url).startsWith('/')) return galleryItem.url; 13462 if (galleryItem.slug) return '/' + galleryItem.slug; 13463 return '#'; 13464} 13465 13466 13467function fixCircularReferences() { 13468console.log("Selectively checking for circular references..."); 13469 13470// Only process galleries that need checking: 13471// 1. Imported galleries (they're new and might cause issues) 13472// 2. Galleries with extremely deep nesting 13473// 3. Actual circular references 13474 13475const processed = new Set(); 13476 13477// First pass: Identify actual circular references without modifying structure 13478const circularIds = new Set(); 13479const suspiciousIds = new Set(); 13480 13481galleries.forEach(gallery => { 13482if (!gallery || !gallery.id) return; 13483 13484// Skip galleries we've already processed 13485if (processed.has(gallery.id)) return; 13486 13487// Check for self-reference - this is always wrong 13488if (gallery.parentId === gallery.id) { 13489 console.warn(`Self-reference detected: Gallery ${gallery.title} (${gallery.id}) is its own parent`); 13490 circularIds.add(gallery.id); 13491 return; 13492} 13493 13494// Skip galleries without parents 13495if (!gallery.parentId) { 13496 processed.add(gallery.id); 13497 return; 13498} 13499 13500// Follow the parent chain to detect circular references 13501const parentChain = [gallery.id]; 13502let currentId = gallery.parentId; 13503let loopCount = 0; 13504let foundCircular = false; 13505 13506while (currentId && loopCount < 20) { // Reasonable limit to prevent infinite loops 13507 loopCount++; 13508 13509 // If we've seen this ID before, it's a circular reference 13510 if (parentChain.includes(currentId)) { 13511 console.warn(`Circular reference detected in chain for gallery: ${gallery.title}`); 13512 circularIds.add(gallery.id); 13513 foundCircular = true; 13514 break; 13515 } 13516 13517 parentChain.push(currentId); 13518 13519 // Move up to the next parent 13520 const parent = galleries.find(g => g.id === currentId); 13521 currentId = parent?.parentId; 13522 13523 // If we can't find the parent, we can stop 13524 if (!parent) break; 13525} 13526 13527// If we hit our loop limit but didn't find a circular reference, 13528// it's suspicious but not definitely circular 13529if (loopCount >= 20 && !foundCircular) { 13530 console.warn(`Suspiciously deep parent chain for gallery ${gallery.title}, marking for review`); 13531 suspiciousIds.add(gallery.id); 13532} 13533 13534// Mark this gallery and all its ancestors as processed 13535parentChain.forEach(id => processed.add(id)); 13536}); 13537 13538// Second pass: Only fix the identified circular references 13539if (circularIds.size > 0) { 13540console.log(`Fixing ${circularIds.size} confirmed circular references`); 13541 13542galleries.forEach(gallery => { 13543 if (circularIds.has(gallery.id)) { 13544 console.log(`Breaking circular reference for gallery: ${gallery.title}`); 13545 gallery.parentId = null; 13546 } 13547}); 13548} else { 13549console.log("No circular references found"); 13550} 13551 13552// For suspicious galleries, don't automatically fix them 13553if (suspiciousIds.size > 0) { 13554console.warn(`Found ${suspiciousIds.size} galleries with unusually deep nesting - not fixing automatically`); 13555} 13556 13557return circularIds.size > 0; // Return true if we fixed anything 13558} 13559 13560// Deep linking implementation that uses the existing slug property 13561 13562// Function to get the slug for a gallery 13563function getGallerySlug(gallery) { 13564// First try to use the existing slug property 13565if (gallery.slug) { 13566console.log(`Using existing gallery slug: ${gallery.slug}`); 13567return gallery.slug;
13568} 13569 13570// If no slug exists, generate one from the title as fallback 13571if (gallery.title) { 13572const generatedSlug = gallery.title 13573 .toLowerCase() 13574 .replace(/[^ws-]/g, '') 13575 .replace(/s+/g, '-') 13576 .replace(/--+/g, '-') 13577 .trim(); 13578 13579console.log(`Generated slug from title: ${generatedSlug}`); 13580return generatedSlug || 'gallery'; 13581} 13582 13583// Default fallback 13584console.warn('Gallery has no slug or title, using default'); 13585return 'gallery'; 13586} 13587 13588// Function to update the URL when a gallery is loaded 13589// Update the URL when a gallery is loaded 13590function updateURLWithGallerySlug(gallery) { 13591 if (!gallery) return; 13592 13593 // Keep bookmarkable /_manage URL â do not rewrite to / or /slug when loading galleries/pages 13594 const __hydraPath = window.location.pathname; 13595 if (__hydraPath === '/_manage' || __hydraPath === '/_manage/') { 13596 console.log('[updateURLWithGallerySlug] On /_manage editor path; skipping slug URL update.'); 13597 return; 13598 } 13599 13600 // Get the slug for this gallery/page 13601 let slug = gallery.slug; 13602 13603 if (!slug && gallery.title) { 13604 slug = gallery.title.toLowerCase().replace(/s+/g, '-').replace(/[^w-]+/g, '').replace(/--+/g, '-').trim(); 13605 } 13606 13607 // Check if we're in preview mode (add this new code) 13608 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 13609 13610 // Check if current history state already has this galleryId 13611 const currentState = window.history.state; 13612 const currentPath = window.location.pathname; 13613 const isAlreadyCurrentPage = currentState && currentState.galleryId === gallery.id && 13614 ((isPreviewMode && currentPath.includes(slug)) || (!isPreviewMode && currentPath === '/' + slug)); 13615 13616 if (isPreviewMode) { 13617 // Extract GUID from current URL path - first segment after domain 13618 const pathParts = window.location.pathname.split('/').filter(Boolean); 13619 const siteGuid = pathParts[0]; 13620 13621 // Create history entry if we have a valid GUID, and it's not already the current state. 13622 // Home pages use /{guid}/ (canonical preview home), matching live's '/' special-case. 13623 if (siteGuid && (gallery.isHomePage || slug)) { 13624 const targetPath = gallery.isHomePage ? ('/' + siteGuid + '/') : ('/' + siteGuid + '/' + slug); 13625 const alreadyAtPreviewHome = gallery.isHomePage && ( 13626 currentPath === '/' + siteGuid || currentPath === '/' + siteGuid + '/' 13627 ); 13628 // Only push if URL is different OR if state doesn't match (allows multiple navigations) 13629 if ((!alreadyAtPreviewHome && currentPath !== targetPath) || !isAlreadyCurrentPage) { 13630 console.log('ð [updateURLWithGallerySlug] Updating URL to ' + targetPath + ' (preview mode), galleryId: ' + gallery.id); 13631 13632 // Update browser URL without reloading, preserving the GUID 13633 window.history.pushState( 13634 { galleryId: gallery.id }, 13635 gallery.title || 'Gallery', 13636 targetPath 13637 ); 13638 13639 // Update page title 13640 document.title = formatGalleryDocumentTitle(gallery.title); 13641 } else { 13642 console.log('ð [updateURLWithGallerySlug] URL and state already match, skipping history update (preview mode)'); 13643 } 13644 } 13645 13646 return; // Exit early, we've handled the preview case 13647 } 13648 13649 // Regular (non-preview) URL handling 13650 if (slug) { 13651 // If this gallery is the designated home page, keep the URL as '/' (canonical home URL) 13652 const targetPath = gallery.isHomePage ? '/' : '/' + slug; 13653 // Only push if URL is different OR if state doesn't match (allows multiple navigations) 13654 if (currentPath !== targetPath || !isAlreadyCurrentPage) { 13655 console.log('ð [updateURLWithGallerySlug] Updating URL to ' + targetPath + ', galleryId: ' + gallery.id); 13656 window.history.pushState( 13657 { galleryId: gallery.id }, 13658 gallery.title || 'Gallery', 13659 targetPath 13660 ); 13661 document.title = formatGalleryDocumentTitle(gallery.title); 13662 } else { 13663 console.log('ð [updateURLWithGallerySlug] URL and state already match, skipping history update'); 13664 } 13665 } 13666} 13667 13668 13669function loadHomePage() { 13670 // Find a gallery marked as home page (regardless of visibility) 13671 const homePage = galleries.find(gallery => gallery.isHomePage === true); 13672 13673 if (homePage) {
13674 console.log(`Loading home page: ${homePage.title} (Visible: ${homePage.visible !== false})`); 13675 13676 // Update active gallery ID 13677 activeGalleryId = homePage.id; 13678 13679 // Add a special flag to indicate we're loading the home page 13680 // This allows us to bypass visibility checks 13681 window._loadingHomePage = true; 13682 13683 // Determine if this is a page or gallery and load appropriately 13684 if (homePage.isPage) { 13685 loadPage(homePage.id); 13686 } else { 13687 loadGallery(homePage.id); 13688 } 13689 13690 // Clear the flag after loading 13691 setTimeout(() => { 13692 window._loadingHomePage = false; 13693 }, 100); 13694 13695 // Update UI state 13696 updateActiveStates(); 13697 updateMobileTitle(); 13698 if (typeof closeMobileMenu === 'function') closeMobileMenu(); 13699 13700 return true; 13701 } 13702 13703 console.log('No home page defined'); 13704 return false; 13705} 13706 13707// 4. Update the handleURLNavigation function to check for home page 13708// Find the existing handleURLNavigation function and modify as follows: 13709// In index.ts 13710function handleURLNavigation() { 13711 const path = window.location.pathname.substring(1); // Remove leading slash 13712 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 13713 let slug = path; 13714 let siteGuid = null; 13715 13716 // /_manage is the Hydra editor entry â treat like site root for slug resolution (not a gallery slug) 13717 if (!isPreviewMode && (path === '_manage' || path.startsWith('_manage/'))) { 13718 console.log('[handleURLNavigation] /_manage detected â using root navigation behavior'); 13719 slug = ''; 13720 } 13721 13722 if (isPreviewMode && path) { 13723 const pathParts = path.split('/'); 13724 if (pathParts.length >= 1) { 13725 siteGuid = pathParts[0]; // Path could be just GUID/ or GUID/slug 13726 } 13727 if (pathParts.length >= 2) { 13728 slug = pathParts[1]; // Slug is the part after GUID 13729 console.log(`Preview URL detected, using slug: "${slug}" under GUID: "${siteGuid}"`); 13730 } else { 13731 // Only GUID is present, or path is empty after GUID 13732 slug = ''; // Treat as root/home for the given GUID 13733 console.log(`Preview URL with only GUID: "${siteGuid}", checking for home page or default.`); 13734 } 13735 } 13736 13737 if (!slug) { // Handles root path ('/') or preview URL with only GUID ('/GUID/') 13738 console.log("No specific slug in path (root or GUID-only preview), checking for home page."); 13739 if (!loadHomePage() && galleries.length > 0) { 13740 const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu); 13741 if (firstVisibleGallery) { 13742 console.log("No home page, loading first visible item:", firstVisibleGallery.title); 13743 if (firstVisibleGallery.isPage) { 13744 loadPage(firstVisibleGallery.id); 13745 } else { 13746 loadGallery(firstVisibleGallery.id); 13747 } 13748 } else { 13749 console.log("No home page and no visible items to load for this path."); 13750 } 13751 } 13752 } else { 13753 console.log(`Handling URL navigation for slug: "${slug}"` + (isPreviewMode ? ` (Preview GUID: ${siteGuid})` : "")); 13754 // findGalleryByPath should be able to find the item using the slug, 13755 // handling preview mode internally if necessary or by being passed the correct slug. 13756 const matchingGallery = findGalleryByPath(slug, isPreviewMode, siteGuid); 13757 13758 if (matchingGallery) { 13759 console.log('Loading content from URL: "' + matchingGallery.title + '" (ID: ' + matchingGallery.id + ')'); 13760 console.log('ð [handleURLNavigation] Gallery properties - isPage:', matchingGallery.isPage, 'pageId:', matchingGallery.pageId, 'has pageElements:', !!(matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements))); 13761 13762 if (matchingGallery.isSubmenu) { 13763 console.log('Gallery is a submenu, skipping direct load. Parent expansion might be needed.'); 13764 // Potentially, you might want to find and expand its parent here if the UI supports it. 13765 } else { 13766 // Set activeGalleryId *before* calling loadPage/loadGallery 13767 activeGalleryId = matchingGallery.id; 13768 window.activeGalleryId = matchingGallery.id; 13769 13770 // Determine if this is a page by checking multiple indicators 13771 // Check isPage flag, pageId, or pageElements array 13772 const isPage = matchingGallery.isPage === true || 13773 (matchingGallery.pageId && matchingGallery.pageId.startsWith('page_')) || 13774 (matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements) && matchingGallery.pageElements.length > 0); 13775 13776 if (isPage) { 13777 console.log('ð [handleURLNavigation] Detected as PAGE, loading page: "' + matchingGallery.title + '"'); 13778 const pageLoader = typeof window.loadPage === 'function' ? window.loadPage : loadPage; 13779 pageLoader(matchingGallery.id); 13780 } else { 13781 console.log('ð [handleURLNavigation] Detected as GALLERY, loading gallery: "' + matchingGallery.title + '"'); 13782 const galleryLoader = typeof window.loadGallery === 'function' ? window.loadGallery : loadGallery; 13783 galleryLoader(matchingGallery.id, null, { forceReload: true }); 13784 } 13785 } 13786 } else { 13787 console.log(`No matching gallery found for slug: "${slug}". Checking for home page as fallback.`); 13788 if (!loadHomePage()) { // Try loading home page 13789 const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu); 13790 if (firstVisibleGallery) { 13791 console.log(`Loading first visible gallery instead: "${firstVisibleGallery.title}"`); 13792 if (firstVisibleGallery.isPage) { 13793 loadPage(firstVisibleGallery.id); 13794 } else { 13795 loadGallery(firstVisibleGallery.id); 13796 } 13797 } else { 13798 console.log("No matching gallery, no home page, and no visible items to load."); 13799 // Optionally, display a 404 message in the gallery-container 13800 const galleryContainer = document.querySelector('.gallery-container'); 13801 if (galleryContainer) { 13802 galleryContainer.innerHTML = '<div>Page not found.</div>'; 13803 galleryContainer.style.opacity = '1'; 13804 } 13805 } 13806 } 13807 } 13808 } 13809 13810 // Update UI states after initiating load. 13811 // These functions should ideally use the now-set activeGalleryId. 13812 if (typeof updateActiveStates === 'function') updateActiveStates(); // For sidebar/tree 13813 if (typeof updateMobileTitle === 'function') updateMobileTitle(); // For mobile header 13814 13815 // **** ADDED FOR DEEP LINKING HORIZONTAL MENU **** 13816 // Determine current layout (it might not be set by MenuStyleCustomizer yet on direct load) 13817 let currentLayout = 'sidebar'; // Default assumption 13818 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) { 13819 currentLayout = window.MenuStyleCustomizer.settings.menuLayout; 13820 } else { 13821 // Fallback: check body class if MenuStyleCustomizer hasn't initialized settings 13822 if (document.body.classList.contains('menu-layout-horizontal')) { 13823 currentLayout = 'horizontal'; 13824 } 13825 } 13826 13827 console.log(`handleURLNavigation: Determined current layout as: ${currentLayout}`); 13828 13829 if (currentLayout === 'horizontal') { 13830 if (typeof updateActiveStatesHorizontal === 'function') { 13831 console.log("handleURLNavigation: Explicitly calling updateActiveStatesHorizontal for horizontal layout on direct navigation."); 13832 // It's possible menu items aren't rendered yet by renderHorizontalMenu if this is a very early call. 13833 // A small delay might be needed, or ensure renderHorizontalMenu has run. 13834 // For now, call it directly. If items aren't there, it won't do anything harmful. 13835 setTimeout(() => { // Add a slight delay to allow menu rendering 13836 updateActiveStatesHorizontal(); 13837 }, 100); // Adjust delay if needed, or find a more robust way to ensure menu is rendered. 13838 } else { 13839 console.warn("handleURLNavigation: updateActiveStatesHorizontal function not found for horizontal layout."); 13840 } 13841 } 13842} 13843 13844/** 13845* Enhanced loadPage function that properly cleans up gallery content 13846* @param {number} id - The page ID 13847* @param {Event} event - Optional event object 13848*/ 13849window.loadPage = function(id, event) { 13850 if (event && event.target && event.target.isConnected === false) { 13851 console.log('ð [loadPage] Ignoring navigation from detached element'); 13852 return; 13853 } 13854 if (event) { 13855 event.preventDefault(); 13856 event.stopPropagation(); 13857 const now = Date.now(); 13858 const lastCallTime = window._lastPageLoadTime || 0; 13859 window._lastPageLoadTime = now; 13860 if (now - lastCallTime < 100) {
13861 console.log('Ignoring duplicate loadPage call'); 13862 return; 13863 } 13864 } 13865 console.error('ð [loadPage] Loading page with gallery ID:', id); 13866 13867 stopAutoAdvanceTimer(); 13868 if (typeof dismissGalleryPasswordOverlay === 'function') { 13869 dismissGalleryPasswordOverlay(); 13870 } 13871 13872 let galleriesData = window.galleries || galleries; 13873 const gallery = findGalleryById(galleriesData, id); 13874 13875 if (!gallery) { 13876 console.warn('No gallery found with ID:', id, 'for page load.'); 13877 return; 13878 } 13879 13880 const contentLoadGeneration = beginHydraContentLoad('page', gallery.id, gallery.pageId || `page_${id}`); 13881 13882 window.activeGalleryId = id; 13883 activeGalleryId = id; 13884 window._activeContentType = 'page'; 13885 if (window.EnhancedGallerySystem) { 13886 window.EnhancedGallerySystem.currentGalleryId = null; 13887 } 13888 13889 if (typeof window.removeGalleryScriptsWithPause === 'function') { 13890 window.removeGalleryScriptsWithPause(); 13891 } 13892 13893 const galleryContainer = document.querySelector('.gallery-container'); 13894 if (galleryContainer) { 13895 // Clear the container but ensure it's visible 13896 galleryContainer.innerHTML = ''; 13897 galleryContainer.style.display = 'block'; 13898 galleryContainer.style.visibility = 'visible'; 13899 galleryContainer.style.opacity = '1'; 13900 console.log('ð [loadPage] Cleared and ensured gallery-container is visible'); 13901 } else { 13902 console.error('ð [loadPage] gallery-container not found!'); 13903 } 13904 13905 if (!gallery.pageId) gallery.pageId = `page_${id}`; 13906 if (!gallery.isPage) gallery.isPage = true; 13907 13908 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 13909 window.syncGalleryOptionsButtonVisibility(); 13910 } 13911 13912 if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {}; 13913 window._pageIdToGalleryId[gallery.pageId] = id; 13914 13915 const seedPageElementsOntoManager = function() { 13916 if (gallery.pageElements && Array.isArray(gallery.pageElements)) { 13917 if (window.PageManager && window.PageManager.elements) { 13918 window.PageManager.elements[gallery.pageId] = [...gallery.pageElements]; 13919 } 13920 } else if (window.PageManager && window.PageManager.elements && 13921 window.PageManager.elements[gallery.pageId] && 13922 window.PageManager.elements[gallery.pageId].length > 0) { 13923 gallery.pageElements = [...window.PageManager.elements[gallery.pageId]]; 13924 } 13925 }; 13926 seedPageElementsOntoManager(); 13927 13928 const invokePageManagerLoad = function() { 13929 seedPageElementsOntoManager(); 13930 try { 13931 console.error('ð [loadPage] Calling PageManager.loadPage for pageId:', gallery.pageId); 13932 console.error('ð [loadPage] Gallery container exists:', !!galleryContainer, 'Container HTML length:', galleryContainer ? galleryContainer.innerHTML.length : 0); 13933 console.error('ð [loadPage] Page elements count:', gallery.pageElements ? gallery.pageElements.length : 0); 13934 console.error('ð [loadPage] PageManager.elements[' + gallery.pageId + '] exists:', !!(window.PageManager.elements && window.PageManager.elements[gallery.pageId])); 13935 13936 // Small delay to ensure DOM is ready, especially when navigating back via history 13937 setTimeout(() => { 13938 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) { 13939 console.log('ð [loadPage] Skipping stale delayed PageManager load for:', gallery.pageId); 13940 return; 13941 } 13942 13943 // Verify container still exists 13944 const containerCheck = document.querySelector('.gallery-container'); 13945 console.error('ð [loadPage] Container check before loadPage:', !!containerCheck, 'Same as original:', containerCheck === galleryContainer); 13946 13947 // PageManager.loadPage is async - we need to properly handle it 13948 try { 13949 const loadPromise = window.PageManager.loadPage(gallery.pageId); 13950 console.error('ð [loadPage] PageManager.loadPage called, returned:', typeof loadPromise, 'is promise:', loadPromise && typeof loadPromise.then === 'function'); 13951 13952 if (loadPromise && typeof loadPromise.then === 'function') { 13953 loadPromise.then(() => { 13954 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) { 13955 console.log('ð [loadPage] Ignoring stale PageManager completion for:', gallery.pageId); 13956 return; 13957 } 13958 13959 console.error('ð [loadPage] PageManager.loadPage promise resolved'); 13960 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 13961 window.syncGalleryOptionsButtonVisibility(); 13962 } 13963 // Ensure container visibility after PageManager has initialized 13964 if (galleryContainer) { 13965 const pageContainer = galleryContainer.querySelector('.page-container'); 13966 if (pageContainer) { 13967 pageContainer.style.display = 'block'; 13968 pageContainer.style.visibility = 'visible'; 13969 pageContainer.style.opacity = '1'; 13970 const elementCount = pageContainer.querySelectorAll('.page-element').length; 13971 console.error('ð [loadPage] Page container found and made visible, has', elementCount, 'elements'); 13972 if (elementCount === 0) { 13973 console.error('ð [loadPage] WARNING: Page container exists but has no elements!'); 13974 console.error('ð [loadPage] PageManager.elements[' + gallery.pageId + ']:', window.PageManager.elements[gallery.pageId]); 13975 } 13976 } else { 13977 console.error('â [loadPage] Page container not found after PageManager.loadPage'); 13978 console.error('â [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500)); 13979 console.error('â [loadPage] Gallery container children:', galleryContainer.children.length); 13980 } 13981 } else { 13982 console.error('â [loadPage] Gallery container is null after PageManager.loadPage'); 13983 } 13984 }).catch((error) => { 13985 console.error('â [loadPage] Error in PageManager.loadPage promise:', error); 13986 console.error('â [loadPage] Error stack:', error.stack); 13987 }); 13988 } else {
13989 // If loadPage doesn't return a promise, check after a delay 13990 console.error('ð [loadPage] loadPage did not return a promise, checking after delay'); 13991 setTimeout(() => { 13992 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) { 13993 console.log('ð [loadPage] Ignoring stale non-promise PageManager completion for:', gallery.pageId); 13994 return; 13995 } 13996 if (galleryContainer) { 13997 const pageContainer = galleryContainer.querySelector('.page-container'); 13998 if (pageContainer) { 13999 pageContainer.style.display = 'block'; 14000 pageContainer.style.visibility = 'visible'; 14001 pageContainer.style.opacity = '1'; 14002 console.error('ð [loadPage] Ensured page-container is visible (non-promise path)'); 14003 } else { 14004 console.error('â [loadPage] Page container not found (non-promise path)'); 14005 console.error('â [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500)); 14006 } 14007 } 14008 }, 200); 14009 } 14010 } catch (loadError) { 14011 console.error('â [loadPage] Exception calling PageManager.loadPage:', loadError); 14012 console.error('â [loadPage] Error stack:', loadError.stack); 14013 } 14014 14015 const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false; 14016 if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') { 14017 window.PageManager.setEditMode(isCurrentlyEditing); 14018 } 14019 }, 50); // Small delay to ensure DOM is ready 14020 } catch (error) { 14021 console.error('â [loadPage] Error in loadPage function:', error); 14022 console.error('â [loadPage] Error stack:', error.stack); 14023 } 14024 }; 14025 14026 if (window.PageManager && typeof window.PageManager.loadPage === 'function') { 14027 invokePageManagerLoad(); 14028 } else { 14029 console.error('â [loadPage] PageManager not found yet â waiting for CDN proxy script'); 14030 console.error('â [loadPage] window.PageManager:', !!window.PageManager); 14031 console.error('â [loadPage] typeof loadPage:', window.PageManager ? typeof window.PageManager.loadPage : 'N/A'); 14032 whenPageManagerReady(function() { 14033 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) { 14034 console.log('ð [loadPage] Skipping stale PageManager ready retry for:', gallery.pageId); 14035 return; 14036 } 14037 console.log('â [loadPage] PageManager became available â retrying page render'); 14038 invokePageManagerLoad(); 14039 }); 14040 } 14041 14042 // Apply menu visibility 14043 const bodyEl = document.body; 14044 if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 14045 bodyEl.classList.add('menu-hidden-on-page'); 14046 } else { 14047 bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden 14048 } 14049 applyMenuOverlayClassForGallery(gallery); 14050 14051 if (typeof updateActiveStates === 'function') updateActiveStates(); 14052 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 14053 if (typeof closeMobileMenu === 'function') closeMobileMenu(); 14054 // Close the gallery options panel when navigating to a different page 14055 if (typeof window.closeOptionsPanel === 'function') { 14056 window.closeOptionsPanel(); 14057 } 14058 if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery); 14059 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 14060 window.syncGalleryOptionsButtonVisibility(); 14061 } 14062 if (typeof window !== 'undefined' && typeof document !== 'undefined') { 14063 window.dispatchEvent(new CustomEvent('page-loaded', { 14064 detail: { pageId: gallery.pageId, galleryId: id } 14065 })); 14066 } 14067} 14068 14069 14070 14071/** 14072* Ensures all required gallery styles are loaded 14073*/ 14074function ensureGalleryStylesLoaded() { 14075window.ensureGalleryStylesLoaded = ensureGalleryStylesLoaded; 14076// Use Worker route for gallery CSS (avoids CDN SSL issues) 14077const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; 14078const requiredStyles = [ 14079{ id: 'neon-gallery-css', href: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css' }, 14080{ id: 'quill-css', href: 'https://cdn.quilljs.com/1.3.6/quill.snow.css' } 14081]; 14082console.log('Loading gallery CSS from Worker:', galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css'); 14083
14084requiredStyles.forEach(style => { 14085if (!document.getElementById(style.id)) { 14086 const link = document.createElement('link'); 14087 link.id = style.id; 14088 link.rel = 'stylesheet'; 14089 link.href = style.href; 14090 if (style.href.includes('cdn.neonsky.app')) { 14091 link.onerror = function() { 14092 console.error('â Gallery CSS failed, retrying via proxy:', link.href); 14093 // Set flag if not already set 14094 if (!window.useCdnProxy) { 14095 window.useCdnProxy = true; 14096 console.error('ð© CDN_PROXY_FLAG SET: Gallery CSS failed, using proxy for all subsequent requests'); 14097 } 14098 const proxyUrl = link.href.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 14099 link.href = proxyUrl; 14100 link.onerror = function() { 14101 console.error('â Proxy failed, trying storage:', proxyUrl); 14102 link.href = link.href.replace('cdn.neonsky.app', 'storage.neonsky.app'); 14103 }; 14104 }; 14105 } 14106 document.head.appendChild(link); 14107 console.log(`Added gallery style: ${style.id}`); 14108} 14109}); 14110} 14111 14112 14113/** 14114* Ensures all required gallery scripts are loaded 14115* @returns {Promise} A promise that resolves when all scripts are loaded 14116*/ 14117function ensureGalleryScriptsLoaded() { 14118const requiredScripts = [ 14119{ id: 'crypto-js', src: 'https://cdn.jsdelivr.net/npm/[email protected]/crypto-js.min.js' }, 14120{ id: 'quill-js', src: 'https://cdn.quilljs.com/1.3.6/quill.min.js' }, 14121 { id: 'quill-integration', src: 'https://cdn.neonsky.app/quill-integration-v260923-002.js' } 14122]; 14123 14124const promises = requiredScripts.map(script => { 14125return new Promise((resolve, reject) => { 14126 // If script is already loaded, resolve immediately 14127 if (document.getElementById(script.id)) { 14128 resolve(); 14129 return; 14130 } 14131 14132 // Create and load the script 14133 const scriptElement = document.createElement('script'); 14134 scriptElement.id = script.id; 14135 scriptElement.src = script.src; 14136 scriptElement.onload = resolve; 14137 scriptElement.onerror = () => reject(new Error(`Failed to load script: ${script.src}`)); 14138 document.head.appendChild(scriptElement); 14139}); 14140}); 14141 14142return Promise.all(promises); 14143} 14144 14145/** 14146 * Creates a clean gallery context without using an iframe 14147 * @param {Object} gallery - The gallery object 14148 * @param {HTMLElement} container - The container to load the gallery into 14149 */ 14150function createGalleryContext(gallery, container) { 14151 console.log('Creating gallery context for:', gallery.title); 14152 14153 // CRITICAL: Ensure gallery CSS is loaded BEFORE loading the gallery script 14154 // This ensures styles are available even if NDJSON fails and falls back to classic JSON 14155 ensureGalleryStylesLoaded(); 14156 14157 // Generate the page alias safely 14158 const pageAlias = gallery.slug || gallery.title.toLowerCase().replace(/s+/g, '-'); 14159 14160 // Get our gallery options 14161 const galleryOptions = gallery.galleryOptions || {}; 14162 14163 // Get token data 14164 let authToken = null; 14165 // First try TokenManager if available 14166 if (window.TokenManager && typeof window.TokenManager.getToken === 'function') { 14167 authToken = window.TokenManager.getToken(); 14168 } 14169 // Then try global token 14170 else if (window.didToken) { 14171 authToken = window.didToken; 14172 } 14173 14174 // Set up Parameters for the gallery 14175 const galleryParameters = { 14176 SiteAlias: window.location.hostname, 14177 InitialPageUuid: gallery.pageId, 14178 InitialPageAlias: pageAlias, 14179 isInEditor: window.isEditing || false, 14180 siteId: window.siteId || '', 14181 isHydra: true 14182 }; 14183 14184 // If we have a token, add it 14185 if (authToken) { 14186 galleryParameters.hydraAuthToken = authToken; 14187 } 14188 14189 // Preserve original window.Parameters 14190 const originalParameters = window.Parameters; 14191 14192 // REMOVED: Loading indicator - no longer showing the loading animation 14193 14194 // Load the main gallery script - Use Worker route (avoids CDN SSL issues) 14195 const galleryScript = document.createElement('script'); 14196 const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; 14197 galleryScript.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; 14198 console.log('Loading gallery-main script from Worker:', galleryScript.src); 14199 galleryScript.onerror = function() { 14200 console.error('â Gallery script failed, retrying via proxy:', galleryScript.src); 14201 const proxyUrl = galleryScript.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 14202 galleryScript.src = proxyUrl; 14203 galleryScript.onerror = function() { 14204 console.error('â Proxy failed, trying storage:', proxyUrl); 14205 galleryScript.src = galleryScript.src.replace('cdn.neonsky.app', 'storage.neonsky.app'); 14206 }; 14207 }; 14208 14209 // Setup the gallery configuration 14210 const neonGalleryConfig = { 14211 useData: true, 14212 useCDN: true, 14213 version: 'live', 14214 manualCollectionName: "mod", 14215 layoutType: "grid", 14216 ...galleryOptions, 14217 siteId: window.siteId || '' 14218 }; 14219 14220 // Execute in a safe way that minimizes global namespace pollution 14221 try { 14222 // Set up the global variables needed by the gallery script 14223 window.Parameters = galleryParameters; 14224 window.neonGalleryConfig = neonGalleryConfig; 14225 14226 // Monitor for script load/error 14227 galleryScript.onload = () => { 14228 console.log('Gallery script loaded successfully for:', gallery.title); 14229 14230 // REMOVED: Don't need to remove the loading indicator since we don't add it 14231 }; 14232 14233 galleryScript.onerror = () => { 14234 console.error('Failed to load gallery script for:', gallery.title); 14235 //container.innerHTML = '<div class="gallery-error">Error loading gallery. Please try again later.</div>'; 14236 // Restore original parameters 14237 window.Parameters = originalParameters; 14238 }; 14239 14240 // Add the script to load the gallery 14241 document.body.appendChild(galleryScript); 14242 14243 // Set up a cleanup function that will be called when another gallery is loaded 14244 container.cleanup = () => { 14245 // Restore original parameters when cleaning up 14246 window.Parameters = originalParameters; 14247 14248 // Remove the gallery script to prevent conflicts 14249 galleryScript.remove(); 14250 14251 // Clear main gallery container 14252 if (container.parentNode) { 14253 container.innerHTML = ''; 14254 } 14255 }; 14256 } catch (error) { 14257 console.error('Error creating gallery context:', error); 14258 //container.innerHTML = '<div class="gallery-error">Error initializing gallery. Please try again later.</div>'; 14259 // Restore original parameters 14260 window.Parameters = originalParameters; 14261 } 14262} 14263 14264/** 14265* Only used for the PageManager's initializeDOM - ensures clean content swap 14266*/ 14267function cleanGalleryContainer() { 14268const galleryContainer = document.querySelector('.gallery-container'); 14269if (galleryContainer) { 14270galleryContainer.innerHTML = ''; 14271} 14272} 14273 14274 14275 14276/** 14277* Initializes the page using the PageManager 14278* To be used by PageManager.initializeDOM 14279*/ 14280function initPageContent(pageId) { 14281// First clear the container completely 14282cleanGalleryContainer(); 14283 14284// Then load the page content 14285if (window.PageManager && window.PageManager.loadPage) { 14286window.PageManager.loadPage(pageId); 14287} 14288} 14289 14290 14291/** 14292* Cleanup gallery content when changing between galleries or pages 14293*/ 14294function cleanupGalleryContent() { 14295// Get the gallery container 14296const galleryContainer = document.querySelector('.gallery-container'); 14297if (!galleryContainer) return; 14298 14299// Find the direct gallery content 14300const directContent = galleryContainer.querySelector('.gallery-direct-content'); 14301if (directContent && typeof directContent.cleanup === 'function') { 14302// Call the cleanup function 14303directContent.cleanup(); 14304} 14305 14306// Clear the container 14307galleryContainer.innerHTML = ''; 14308} 14309 14310 14311 14312 14313/** 14314* Simplified loadGallery function that follows the same content swap pattern 14315* @param {number} id - The gallery ID 14316* @param {Event} event - Optional event object 14317*/ 14318 14319// Override loadGallery to update URL 14320window.loadGallery = async function(id, event) { 14321 14322 14323// Stop event propagation if provided 14324if (event) { 14325 event.stopPropagation(); 14326} 14327 14328const galleryContainer = document.querySelector('.gallery-container'); 14329if (galleryContainer) { 14330 // Apply immediate hiding styles 14331 galleryContainer.style.opacity = '0'; 14332} 14333 14334console.log('Loading gallery with ID:', id); 14335 14336stopAutoAdvanceTimer();
14337if (typeof dismissGalleryPasswordOverlay === 'function') { 14338 dismissGalleryPasswordOverlay(); 14339} 14340 14341// Find gallery by ID 14342const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible 14343 14344if (!gallery) { 14345 console.warn('No gallery found with ID:', id); 14346 if (galleryContainer) { 14347 galleryContainer.style.opacity = '1'; 14348 } // Show container if gallery not found 14349 return; 14350} 14351 14352// Check if this is a page, if so use loadPage instead 14353if (isPageMenuItem(gallery)) { 14354 // Use the loadPage function defined in this scope (or window.loadPage if you prefer) 14355 return loadPage(id, event); 14356} 14357 14358// Skip submenu items 14359if (gallery.isSubmenu) { 14360 console.log('Gallery is submenu, skipping URL update'); 14361 toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available 14362 if (galleryContainer) { 14363 galleryContainer.style.opacity = '1'; 14364 } // Show container 14365 return; 14366} 14367 14368console.log('Found gallery:', gallery.title); 14369 14370window._activeContentType = 'gallery'; 14371beginHydraContentLoad('gallery', gallery.id, gallery.pageId); 14372if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { 14373 window.PageManager.clearCurrentPage(); 14374} 14375 14376// CRITICAL: Set active gallery ID consistently in both global and window scope 14377console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')'); 14378window.activeGalleryId = id; 14379activeGalleryId = id; // Ensure both variables are synchronized 14380 14381// Save reference to which gallery we're loading (for later verification) 14382window.currentLoadingGalleryId = id; 14383 14384// CRITICAL: Thorough cleanup with await to ensure completion 14385if (typeof window.removeGalleryScriptsWithPause === 'function') { 14386 await window.removeGalleryScriptsWithPause(); 14387} 14388 14389 14390// Clear all existing content from gallery container 14391if (!galleryContainer) { 14392 console.error('Gallery container not found'); 14393 return; 14394} 14395 14396// Force clear all content 14397galleryContainer.innerHTML = ''; 14398 14399 14400// CRITICAL: Double-check active gallery ID before updating UI 14401if (window.activeGalleryId !== id || activeGalleryId !== id) { 14402 console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id); 14403 window.activeGalleryId = id; 14404 activeGalleryId = id; 14405} 14406 14407// Apply menu visibility based on the gallery's setting and edit mode 14408const bodyEl = document.body; 14409if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 14410 bodyEl.classList.add('menu-hidden-on-page'); 14411} else { 14412 bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden 14413} 14414applyMenuOverlayClassForGallery(gallery); 14415 14416// Update UI state using explicit window function references or local fallbacks 14417if (typeof window.updateActiveStates === 'function') { 14418 window.updateActiveStates(); 14419} else if (typeof updateActiveStates === 'function') { 14420 updateActiveStates(); 14421} 14422 14423if (typeof window.updateMobileTitle === 'function') { 14424 window.updateMobileTitle(); 14425} else if (typeof updateMobileTitle === 'function') { 14426 updateMobileTitle(); 14427} 14428 14429if (typeof window.closeMobileMenu === 'function') { 14430 window.closeMobileMenu(); 14431} else if (typeof closeMobileMenu === 'function') { 14432 closeMobileMenu(); 14433} 14434 14435// Update URL with gallery slug 14436if (typeof window.updateURLWithGallerySlug === 'function') { 14437 window.updateURLWithGallerySlug(gallery); 14438} else if (typeof updateURLWithGallerySlug === 'function') { 14439 updateURLWithGallerySlug(gallery); 14440} 14441 14442 // Create gallery container and load content 14443const neonGalleryContainer = document.createElement('div'); 14444neonGalleryContainer.id = 'neon-gallery-container'; 14445neonGalleryContainer.className = 'gallery-direct-content'; 14446neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString()); 14447neonGalleryContainer.style.width = '100%'; 14448neonGalleryContainer.style.height = '100%'; 14449galleryContainer.appendChild(neonGalleryContainer); 14450 14451// Add a cache-busting parameter to ensure script is freshly loaded 14452const timestamp = Date.now(); 14453 14454// Set up gallery parameters 14455window.Parameters = { 14456 SiteAlias: window.location.hostname, 14457 InitialPageUuid: gallery.pageId, 14458 InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available 14459 isInEditor: window.isEditing || false, // Assuming window.isEditing is available 14460 siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available 14461 isHydra: true,
14462 galleryInstanceId: timestamp, 14463 // Added loadedGalleryId as per your original function 14464 loadedGalleryId: gallery.id 14465}; 14466 14467// Debug: Check what galleryOptions contains 14468console.error('ð INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL); 14469 14470// Set up gallery config 14471window.neonGalleryConfig = { 14472 useData: true, 14473 useCDN: true, 14474 version: 'live', 14475 manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod", 14476 layoutType: gallery.galleryOptions?.layoutType || "grid", 14477 // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence 14478 // This ensures that only properties from the current gallery are included, not from previous galleries 14479 ...(gallery.galleryOptions || {}), 14480 // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script 14481 // These fields are required for the gallery to determine if it should use Load Network racing 14482 loadTxId: gallery.galleryOptions?.loadTxId || null, 14483 loadPermaURL: gallery.galleryOptions?.loadPermaURL || null, 14484 // Cloudflare CDN NDJSON (unique id) â when present, gallery loads CDN-first (3s) then Fly 14485 cdnNdjsonId: gallery.galleryOptions?.cdnNdjsonId || null, 14486 cdnNdjsonUrl: gallery.galleryOptions?.cdnNdjsonUrl || null, 14487 cdnNdjsonStableUrl: gallery.galleryOptions?.cdnNdjsonStableUrl || gallery.galleryOptions?.embedPointerUrl || null, 14488 embedPointerUrl: gallery.galleryOptions?.embedPointerUrl || gallery.galleryOptions?.cdnNdjsonStableUrl || null, 14489 cdnSaveTimestamp: gallery.galleryOptions?.cdnSaveTimestamp || null, 14490 siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available 14491 // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread 14492 galleryId: gallery.id, 14493 galleryInstanceId: timestamp 14494}; 14495 14496console.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); 14497 14498// Create and add the gallery script with cache-busting 14499// Use Worker route (avoids CDN SSL issues) 14500const script = document.createElement('script'); 14501const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; 14502script.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; // Using versioned file 14503script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute 14504script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute 14505console.log('Loading gallery-main script from Worker:', script.src); 14506script.onerror = function() { 14507 console.error('â Gallery script failed, retrying via proxy:', script.src); 14508 const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 14509 script.src = proxyUrl; 14510 script.onerror = function() { 14511 console.error('â Proxy failed, trying storage:', proxyUrl); 14512 script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app'); 14513 }; 14514}; 14515 14516// Added script.onload from your original function 14517script.onload = function() { 14518 if (window.currentLoadingGalleryId !== gallery.id) { 14519 console.warn('Gallery ID mismatch! Expected:', gallery.id, 14520 'Current:', window.currentLoadingGalleryId); 14521 } 14522 console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`); 14523}; 14524 14525 14526if (galleryContainer) { 14527 setTimeout(() => { 14528 galleryContainer.style.opacity = '1'; 14529 }, 50); // Small delay to ensure content is ready 14530 } 14531 14532console.log(`Loading fresh gallery script for: ${gallery.title}`); 14533document.body.appendChild(script); 14534} 14535 14536// Function to ensure gallery container is visible 14537function ensureGalleryVisibility() { 14538 const galleryContainer = document.querySelector('.gallery-container'); 14539 if (galleryContainer && galleryContainer.style.opacity === '0') { 14540 console.log('ensureGalleryVisibility: Setting gallery container opacity to 1'); 14541 galleryContainer.style.opacity = '1'; 14542 } 14543} 14544 14545// Handle browser back/forward navigation 14546window.addEventListener('popstate', async function(event) { // Make it async 14547 if (window.__hydraPopstateLock) { 14548 return; 14549 } 14550 window.__hydraPopstateLock = true; 14551 try { 14552 console.error('ð [Popstate] Event triggered. State:', event.state, 'Current URL:', window.location.pathname); 14553 let galleryIdToLoad = null; 14554 let galleryToLoad = null; 14555 14556 // First, try to get galleryId from event.state 14557 if (event.state && event.state.galleryId) { 14558 galleryIdToLoad = event.state.galleryId; 14559 console.error('ð [Popstate] Found galleryId in event.state:', galleryIdToLoad); 14560 // Ensure galleries array is available and use findGalleryById helper 14561 const currentGalleries = window.galleries || galleries || []; 14562 galleryToLoad = findGalleryById(currentGalleries, galleryIdToLoad); 14563 } 14564 14565 if (window.__closingNestedLightbox) { 14566 window.__closingNestedLightbox = false;
14567 if (typeof updateActiveStates === 'function') updateActiveStates(); 14568 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 14569 return; 14570 } 14571 14572 if (window.lightboxNavigationStack?.length > 0 && 14573 typeof window.closeNestedGalleryLightbox === 'function') { 14574 const stackTop = window.lightboxNavigationStack[window.lightboxNavigationStack.length - 1]; 14575 const parentId = stackTop?.parentGalleryId; 14576 if (parentId && (event.state?.galleryId === parentId || galleryToLoad?.id === parentId)) { 14577 window.closeNestedGalleryLightbox({ fromPopstate: true }); 14578 if (typeof updateActiveStates === 'function') updateActiveStates(); 14579 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 14580 return; 14581 } 14582 } 14583 14584 if (typeof dismissGalleryPasswordOverlay === 'function') { 14585 dismissGalleryPasswordOverlay(); 14586 } 14587 14588 // If no gallery found from state, resolve from URL slug (handles history entries with null state) 14589 if (!galleryToLoad) { 14590 console.error('ð [Popstate] No galleryId in event.state or gallery not found, reading from URL:', window.location.pathname); 14591 const path = window.location.pathname.substring(1); 14592 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 14593 let slug = path; 14594 let siteGuid = null; 14595 if (!isPreviewMode && (path === '_manage' || path.startsWith('_manage/'))) { 14596 slug = ''; 14597 } 14598 if (isPreviewMode && path) { 14599 const pathParts = path.split('/'); 14600 if (pathParts.length >= 1) siteGuid = pathParts[0]; 14601 if (pathParts.length >= 2) slug = pathParts[1]; 14602 else slug = ''; 14603 } 14604 if (slug && typeof findGalleryByPath === 'function') { 14605 galleryToLoad = findGalleryByPath(slug, isPreviewMode, siteGuid); 14606 if (galleryToLoad) { 14607 console.error('ð [Popstate] Resolved from URL slug: "' + galleryToLoad.title + '" (ID: ' + galleryToLoad.id + ')'); 14608 } 14609 } 14610 } 14611 14612 // If we have a gallery from state or URL, load it 14613 let contentLoaded = false; 14614 if (galleryToLoad) { 14615 console.error('ð [Popstate] Found gallery in history: "' + galleryToLoad.title + '" (ID: ' + galleryToLoad.id + ')'); 14616 console.error('ð [Popstate] Gallery properties - isPage:', galleryToLoad.isPage, 'pageId:', galleryToLoad.pageId, 'has pageElements:', !!(galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements))); 14617 14618 activeGalleryId = galleryToLoad.id; 14619 window.activeGalleryId = galleryToLoad.id; 14620 14621 // Browser history navigation is explicit user intent â load content regardless of menu visibility. 14622 // Determine if this is a page by checking multiple indicators 14623 const isPage = galleryToLoad.isPage === true || 14624 (galleryToLoad.pageId && galleryToLoad.pageId.startsWith('page_')) || 14625 (galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements) && galleryToLoad.pageElements.length > 0); 14626 14627 if (isPage) { 14628 console.error('ð [Popstate] Detected as PAGE, loading page "' + galleryToLoad.title + '"'); 14629 const pageLoader = typeof window.loadPage === 'function' ? window.loadPage : loadPage; 14630 if (typeof pageLoader === 'function') { 14631 console.error('ð [Popstate] Calling loadPage for ID:', galleryToLoad.id); 14632 pageLoader(galleryToLoad.id); 14633 contentLoaded = true; 14634 } else { 14635 console.error('â [Popstate] loadPage function not available'); 14636 } 14637 } else { 14638 console.error('ð [Popstate] Detected as GALLERY, loading gallery "' + galleryToLoad.title + '"'); 14639 // Clear page state left over from prior page navigation 14640 if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { 14641 window.PageManager.clearCurrentPage(); 14642 } 14643 window._activeContentType = 'gallery'; 14644 const galleryLoader = typeof window.loadGallery === 'function' ? window.loadGallery : loadGallery; 14645 if (typeof galleryLoader === 'function') { 14646 await galleryLoader(galleryToLoad.id, null, { forceReload: true }); 14647 contentLoaded = true; 14648 } else { 14649 console.error('â [Popstate] loadGallery function not available'); 14650 } 14651 } 14652 } else { 14653 console.error('â [Popstate] No gallery found and handleURLNavigation not available'); 14654 } 14655 14656 // Update UI states after loading (or attempting to load) content 14657 if (typeof updateActiveStates === 'function') updateActiveStates(); 14658 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 14659 14660 // Ensure gallery visibility after popstate navigation 14661 setTimeout(ensureGalleryVisibility, 1000); 14662 14663 let currentLayout = 'sidebar'; 14664 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) { 14665 currentLayout = window.MenuStyleCustomizer.settings.menuLayout; 14666 } else if (document.body.classList.contains('menu-layout-horizontal')) { 14667 currentLayout = 'horizontal'; 14668 } 14669 14670 if (currentLayout === 'horizontal') { 14671 if (typeof updateActiveStatesHorizontal === 'function') { 14672 console.log("Popstate: Calling updateActiveStatesHorizontal for horizontal layout with a delay."); 14673 setTimeout(() => { 14674 console.log("[Delayed Update from Popstate] Calling updateActiveStatesHorizontal."); 14675 updateActiveStatesHorizontal(); 14676 }, 150); // Slightly longer delay for popstate, as full content might be re-initializing 14677 } 14678 } 14679 } finally { 14680 setTimeout(function() { window.__hydraPopstateLock = false; }, 300); 14681 } 14682}); 14683 14684// Check for direct URL access on page load (fallback when bootstrap did not route) 14685document.addEventListener('DOMContentLoaded', function() { 14686setTimeout(function() { 14687console.log('Checking for deep links after DOM content loaded'); 14688var __p = window.location.pathname; 14689if (__p !== '/' && __p !== '/_manage' && __p !== '/_manage/' && !window.__hydraBootstrapRouteAttempted) { 14690 handleURLNavigation(); 14691} 14692// Ensure gallery visibility after URL navigation 14693setTimeout(ensureGalleryVisibility, 1000); 14694}, 500); 14695}); 14696 14697document.addEventListener('page-save-requested', function(e) { 14698const data = e.detail; 14699if (data && data.pageId && data.elements) { 14700// Find the gallery/page that corresponds to this pageId 14701const page = galleries.find(g => g.pageId === data.pageId); 14702if (page) { 14703 // Store the page elements 14704 page.pageElements = data.elements; 14705 14706 // Save to server 14707 saveGalleries(); 14708} 14709} 14710}); 14711 14712document.addEventListener('DOMContentLoaded', function() { 14713function preloadRichTextEditor() { 14714 if (window.RichTextEditor && typeof window.RichTextEditor._ensureEditorDependencies === 'function') { 14715 console.log('Preloading Rich Text Editor dependencies'); 14716 return window.RichTextEditor._ensureEditorDependencies() 14717 .then(() => console.log('Rich Text Editor dependencies preloaded successfully')) 14718 .catch(error => console.warn('Failed to preload Rich Text Editor:', error)); 14719 } 14720 14721 if (window.PageManager && typeof window.PageManager._ensureRichTextEditor === 'function') { 14722 return window.PageManager._ensureRichTextEditor() 14723 .catch(error => console.warn('Failed to preload Rich Text Editor via PageManager:', error)); 14724 } 14725 14726 return null; 14727} 14728 14729preloadRichTextEditor(); 14730setTimeout(preloadRichTextEditor, 1000); 14731setTimeout(preloadRichTextEditor, 3000); 14732}); 14733 14734document.addEventListener('DOMContentLoaded', function() { 14735const submenuCheck = document.getElementById('createSubmenu'); 14736const submenuTitleField = document.getElementById('submenuTitle'); 14737const submenuTitleGroup = submenuTitleField?.parentElement; 14738 14739if (submenuCheck && submenuTitleGroup) { 14740submenuCheck.addEventListener('change', function() { 14741 submenuTitleGroup.style.display = this.checked ? 'block' : 'none'; 14742}); 14743} 14744}); 14745 14746// Also check immediately if document is already loaded (fallback only â avoid duplicate loadPage vs bootstrap) 14747if (document.readyState !== 'loading') { 14748setTimeout(function() { 14749console.log('Document already loaded, checking for deep links'); 14750var __p2 = window.location.pathname; 14751if (__p2 !== '/' && __p2 !== '/_manage' && __p2 !== '/_manage/' && !window.__hydraBootstrapRouteAttempted) { 14752 handleURLNavigation(); 14753} 14754}, 300); 14755} 14756 14757// ============================================ 14758// CONCURRENT EDIT CONFLICT DETECTION 14759// ============================================ 14760(function() { 14761 // Initialize local timestamp tracking 14762 window.lastKnownConfigTimestamp = null; 14763 let conflictPollInterval = null; 14764 14765 // Initialize timestamp on page load 14766 async function initializeConfigTimestamp() { 14767 try { 14768 const timestampUrl = typeof getApiUrl === 'function' ? 14769 getApiUrl('/api/config-timestamp') : 14770 '/api/config-timestamp'; 14771 console.error('[CONFLICT DETECT] Initializing config timestamp from:', timestampUrl); 14772 const response = await fetch(timestampUrl); 14773 if (response.ok) { 14774 const data = await response.json(); 14775 if (data.lastModified) { 14776 window.lastKnownConfigTimestamp = data.lastModified; 14777 console.error('[CONFLICT DETECT] Initialized config timestamp:', window.lastKnownConfigTimestamp); 14778 } else { 14779 console.error('[CONFLICT DETECT] No lastModified in response:', data); 14780 } 14781 } else { 14782 console.error('[CONFLICT DETECT] Failed to initialize timestamp, status:', response.status); 14783 } 14784 } catch (error) { 14785 console.error('[CONFLICT DETECT] Could not initialize config timestamp:', error); 14786 } 14787 } 14788
14789 // Check for conflicts by polling server 14790 async function checkForConflicts() { 14791 // Only poll when in edit mode 14792 if (!document.body.classList.contains('edit-mode-active')) { 14793 console.error('[CONFLICT DETECT] Skipping poll - not in edit mode'); 14794 return; 14795 } 14796 14797 try { 14798 const timestampUrl = typeof getApiUrl === 'function' ? 14799 getApiUrl('/api/config-timestamp') : 14800 '/api/config-timestamp'; 14801 console.error('[CONFLICT DETECT] Polling for conflicts from:', timestampUrl); 14802 console.error('[CONFLICT DETECT] Current local timestamp:', window.lastKnownConfigTimestamp); 14803 14804 const response = await fetch(timestampUrl); 14805 14806 if (response.ok) { 14807 const data = await response.json(); 14808 const serverTimestamp = data.lastModified; 14809 console.error('[CONFLICT DETECT] Server timestamp:', serverTimestamp); 14810 14811 // If we have a local timestamp and it differs from server, show conflict 14812 if (window.lastKnownConfigTimestamp !== null && 14813 serverTimestamp !== null && 14814 serverTimestamp !== window.lastKnownConfigTimestamp) { 14815 console.error('[CONFLICT DETECT] â ï¸ CONFLICT DETECTED! Local:', window.lastKnownConfigTimestamp, 'Server:', serverTimestamp); 14816 showConflictIndicator(); 14817 } else { 14818 console.error('[CONFLICT DETECT] No conflict - timestamps match or null'); 14819 hideConflictIndicator(); 14820 } 14821 } else { 14822 console.error('[CONFLICT DETECT] Poll failed with status:', response.status); 14823 } 14824 } catch (error) { 14825 console.error('[CONFLICT DETECT] Error checking for conflicts:', error); 14826 } 14827 } 14828 14829 // Show conflict indicator icon 14830 function showConflictIndicator() { 14831 const indicator = document.getElementById('conflictIndicator'); 14832 console.error('[CONFLICT DETECT] Showing conflict indicator, element found:', !!indicator); 14833 if (indicator) { 14834 indicator.style.display = 'flex'; 14835 indicator.style.visibility = 'visible'; 14836 indicator.style.opacity = '1'; 14837 indicator.style.background = '#d32f2f'; 14838 indicator.style.borderColor = '#d32f2f'; 14839 console.error('[CONFLICT DETECT] Conflict indicator displayed'); 14840 } else { 14841 console.error('[CONFLICT DETECT] â ï¸ Conflict indicator element not found in DOM!'); 14842 } 14843 } 14844 14845 // Hide conflict indicator icon 14846 function hideConflictIndicator() { 14847 const indicator = document.getElementById('conflictIndicator'); 14848 if (indicator) { 14849 indicator.style.display = 'none'; 14850 indicator.style.visibility = 'hidden'; 14851 indicator.style.opacity = '0'; 14852 } 14853 } 14854 14855 // Start/stop polling based on edit mode 14856 function updatePolling() { 14857 const isEditMode = document.body.classList.contains('edit-mode-active'); 14858 14859 // Always hide indicator when not in edit mode 14860 if (!isEditMode) { 14861 hideConflictIndicator(); 14862 } 14863 14864 if (isEditMode && !conflictPollInterval) { 14865 // Start polling every 10 seconds 14866 conflictPollInterval = setInterval(checkForConflicts, 10000); 14867 console.error('[CONFLICT DETECT] â Started conflict detection polling (every 10s)'); 14868 14869 // Also check immediately 14870 checkForConflicts(); 14871 } else if (!isEditMode && conflictPollInterval) { 14872 // Stop polling when not in edit mode 14873 clearInterval(conflictPollInterval); 14874 conflictPollInterval = null; 14875 hideConflictIndicator(); 14876 console.error('[CONFLICT DETECT] Stopped conflict detection polling'); 14877 } 14878 } 14879 14880 // Initialize on page load 14881 if (document.readyState === 'loading') { 14882 document.addEventListener('DOMContentLoaded', function() { 14883 initializeConfigTimestamp(); 14884 // Watch for edit mode changes 14885 const editModeObserver = new MutationObserver(updatePolling); 14886 editModeObserver.observe(document.body, { 14887 attributes: true, 14888 attributeFilter: ['class'] 14889 }); 14890 updatePolling(); 14891 }); 14892 } else { 14893 initializeConfigTimestamp(); 14894 // Watch for edit mode changes 14895 const editModeObserver = new MutationObserver(updatePolling); 14896 editModeObserver.observe(document.body, { 14897 attributes: true, 14898 attributeFilter: ['class'] 14899 }); 14900 updatePolling(); 14901 } 14902})(); 14903 14904</script>
14904 14905</head> 14906<body class="menu-layout-horizontal"> 14907 <a class="hydra-skip-link" href="#hydra-main-content">Skip to content</a> 14908<nav class="hydra-seo-nav" aria-label="Site navigation"> 14909 <ul> 14910 <li><a href="/">PEOPLE</a></li> 14911 <li><a href="/athletes">ATHLETES</a></li> 14912 <li><a href="/higher-ed">HIGHER ED</a></li> 14913 <li><a href="/branding">BRANDING</a></li> 14914 <li><a href="/places--things">PLACES + THINGS</a></li> 14915 <li><a href="/about">ABOUT</a></li> 14916 <li><a href="/contact">CONTACT</a></li> 14917 </ul> 14918</nav> 14919<noscript class="hydra-gallery-noscript"> 14920 <article> 14921 <h1>ATHLETES | People - Adena Stevens Photography</h1> 14922 14923 <ul class="gallery-images"> 14924 <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_jdi7x8_Athletes-001_basketballA.jpg" alt="f21s3uk4 an5m6dbk jdi7x8 Athletes 001 basketballA" loading="lazy"></li> 14925 <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_8bymb5_Athletes-002_stevens_018.jpg" alt="f21s3uk4 an5m6dbk 8bymb5 Athletes 002 stevens 018" loading="lazy"></li> 14926 <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_kbdx46_Athletes-003_hsgsoc099d-1920w.jpg" alt="f21s3uk4 an5m6dbk kbdx46 Athletes 003 hsgsoc099d 1920w" loading="lazy"></li> 14927 <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_ggcqhp_Athletes-004_boys_soccer-1920w.jpg" alt="f21s3uk4 an5m6dbk ggcqhp Athletes 004 boys soccer 1920w" loading="lazy"></li> 14928 <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_a9ajqg_Athletes-005_sports04.jpg" alt="f21s3uk4 an5m6dbk a9ajqg Athletes 005 sports04" loading="lazy"></li> 14929 <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_40o8tu_Athletes-006_softball2-1920w.jpg" alt="f21s3uk4 an5m6dbk 40o8tu Athletes 006 softball2 1920w" loading="lazy"></li> 14930 <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_pfew5a_Athletes-007_basketball3.jpg" alt="f21s3uk4 an5m6dbk pfew5a Athletes 007 basketball3" loading="lazy"></li> 14931 <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_qy7wn9_Athletes-008_college-basketball-01.jpg" alt="f21s3uk4 an5m6dbk qy7wn9 Athletes 008 college basketball 01" loading="lazy"></li> 14932 <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_ds305y_Athletes-010_sports07.jpg" alt="f21s3uk4 an5m6dbk ds305y Athletes 010 sports07" loading="lazy"></li> 14933 <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_vbj1zs_Athletes-011_volleyballA-1920w.jpg" alt="f21s3uk4 an5m6dbk vbj1zs Athletes 011 volleyballA 1920w" loading="lazy"></li> 14934 <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_3yl151_Athletes-012_baseball3.jpg" alt="f21s3uk4 an5m6dbk 3yl151 Athletes 012 baseball3" loading="lazy"></li> 14935 <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_60t74z_Athletes-013_volleyball-1920w.jpg" alt="f21s3uk4 an5m6dbk 60t74z Athletes 013 volleyball 1920w" loading="lazy"></li> 14936 <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_f2yfb3_Athletes-014_golfAS0531a-1920w.jpg" alt="f21s3uk4 an5m6dbk f2yfb3 Athletes 014 golfAS0531a 1920w" loading="lazy"></li> 14937 <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_o2kuzl_Athletes-015_christine_caron.jpg" alt="f21s3uk4 an5m6dbk o2kuzl Athletes 015 christine caron" loading="lazy"></li> 14938 <li><img src="https://cdn.neonsky.app/f21s3uk4/images/f21s3uk4_an5m6dbk_r6n52d_Athletes-016_softball3.jpg" alt="f21s3uk4 an5m6dbk r6n52d Athletes 016 softball3" loading="lazy"></li> 14939 </ul> 14940 <p><a href="https://adenastevens.com/athletes">View full gallery</a></p> 14941 </article> 14942</noscript> 14943 14944 <div class="global-header-controls"> 14945 14946 <button id="editButton" class="btn-icon btn-primary" onclick="toggleEditMode()" style="display: none;" data-help-id="edit-toggle"> 14947 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 14948 <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"> 14949 <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"/> 14950 <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"/> 14951 </g> 14952 </svg> 14953 </button> 14954 <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"> 14955 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="color: white;">
14956 <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"/> 14957 </svg> 14958 </button> 14959 </div> 14960 14961 <div class="container"> 14962 <div class="mobile-header"> 14963 <div class="mobile-header-content"> 14964 <div class="horizontal-header-logo"></div> 14965 <div class="horizontal-menu-container"></div> 14966 </div> 14967 <button id="hamburgerBtn" class="hamburger-btn" type="button" aria-label="Menu" aria-expanded="false" aria-controls="siteSidebar"> 14968 <span class="hamburger-icon" aria-hidden="true"> 14969 <span class="hamburger-line"></span> 14970 <span class="hamburger-line"></span> 14971 <span class="hamburger-line"></span> 14972 </span> 14973 </button> 14974 </div> 14975 <div id="mobileBackdrop" class="mobile-backdrop"></div> 14976 14977 <div class="sidebar" id="siteSidebar" role="navigation" aria-label="Site"> 14978 <div class="sidebar-header"> 14979</div> 14980 14981 14982 <div class="edit-controls" data-help-id="edit-toolbar"> 14983 <button type="button" class="btn-icon btn-primary" onclick="openHydraHelp()" data-custom-tooltip="Help" data-help-id="help-toolbar"> 14984 <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> 14985 <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" /> 14986 </svg> 14987 </button> 14988 <button class="btn-icon btn-primary" onclick="toggleStyleEditor(undefined, event)" data-custom-tooltip="Edit Style" data-help-id="style-toolbar"> 14989 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 14990 <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"/> 14991 </svg> 14992 </button> 14993 <button class="btn-icon btn-primary" onclick="toggleAddForm()" data-custom-tooltip="Add"> 14994 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 14995 <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"> 14996 <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"/> 14997 <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"/> 14998 </g> 14999 </svg> 15000 </button> 15001 <button class="btn-icon btn-primary" onclick="toggleSidebarElementForm()" data-custom-tooltip="Add Element"> 15002 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 15003 <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"/> 15004 </svg> 15005 </button> 15006<button class="btn-icon btn-primary" onclick="toggleMetadataEditor()" data-custom-tooltip="Site Metadata"> 15007<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.
15007113 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> 15008</button> 15009<button id="logoutButton" class="btn-icon btn-primary" onclick="logout()" data-custom-tooltip="Log Out" style="display: none;"> 15010 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 15011 <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"/> 15012 </svg> 15013 </button> 15014
15014<script> 15015 15016// Initialize mobile menu functionality 15017function initMobileMenu() { 15018 const hamburgerBtn = document.getElementById('hamburgerBtn'); 15019 const sidebar = document.querySelector('.sidebar'); 15020 const mobileBackdrop = document.getElementById('mobileBackdrop'); 15021 15022 // --- ADDED CHECK: Only update header if layout is NOT horizontal --- 15023 let currentLayout = 'sidebar'; // Default 15024 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 15025 currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; 15026 } 15027 15028 // --- END ADDED CHECK --- 15029 15030 // Ensure backdrop is properly hidden for horizontal layout on page load 15031 if (mobileBackdrop && document.body.classList.contains('menu-layout-horizontal')) { 15032 mobileBackdrop.classList.remove('mobile-open'); 15033 mobileBackdrop.classList.remove('active'); 15034 // Clear any inline overrides - CSS controls backdrop visibility via .mobile-open 15035 mobileBackdrop.style.display = ''; 15036 mobileBackdrop.style.pointerEvents = ''; 15037 mobileBackdrop.style.opacity = ''; 15038 mobileBackdrop.style.visibility = ''; 15039 } 15040 15041 // Toggle mobile menu when hamburger is clicked 15042 hamburgerBtn.addEventListener('click', function(e) { 15043 e.stopPropagation(); 15044 toggleMobileMenu(); // Assumes this function exists 15045 }); 15046 15047 // Close mobile menu when backdrop is clicked 15048 if (mobileBackdrop) { 15049 mobileBackdrop.addEventListener('click', function() { 15050 closeMobileMenu(); // Assumes this function exists 15051 }); 15052 } 15053 15054 // Escape closes mobile menu (keyboard / SR) 15055 document.addEventListener('keydown', function(e) { 15056 if (e.key !== 'Escape') return; 15057 const sb = document.querySelector('.sidebar'); 15058 if (sb && sb.classList.contains('mobile-open') && typeof closeMobileMenu === 'function') { 15059 closeMobileMenu(); 15060 } 15061 }); 15062 15063 // Handle window resize events to ensure proper state 15064 window.addEventListener('resize', function() { 15065 // If we're not on mobile anymore, remove mobile-open class 15066 if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) { 15067 closeMobileMenu(); // Assumes this function exists 15068 } 15069 }); 15070} 15071 15072document.addEventListener('keydown', function(e) { 15073 // Check for caret (Shift+6) 15074 if (e.key === '^' || (e.key === '6' && e.shiftKey)) { 15075 // Only work in edit mode 15076 if (window.isInEditMode && window.isInEditMode()) { 15077 console.log('Import shortcut detected'); 15078 toggleImportClassicForm(); 15079 e.preventDefault(); 15080 } 15081 } 15082}); 15083 15084// Enhanced tooltip management with automatic timeouts 15085 15086// Store active tooltip timeouts by ID 15087window.tooltipTimeouts = {}; 15088 15089function showTooltip(trigger) { 15090 const tooltipText = trigger.getAttribute('data-custom-tooltip'); 15091 if (!tooltipText) return; 15092 15093 // Get or create the main tooltip container 15094 let container = document.getElementById('tooltip-container'); 15095 if (!container) { 15096 console.log("Tooltip container not found, creating one."); 15097 container = document.createElement('div'); // Assign to 'container' 15098 container.id = 'tooltip-container'; 15099 // CSS for #tooltip-container should make it position:fixed, top:0, left:0, pointer-events:none, etc. 15100 document.body.appendChild(container); 15101 } 15102 15103 const tooltipId = `tooltip-${trigger.id || Math.random().toString(36).substr(2, 9)}`; 15104 15105 // Remove any existing tooltip for this trigger 15106 const existingTooltip = document.getElementById(tooltipId); 15107 if (existingTooltip) { 15108 existingTooltip.remove(); 15109 } 15110 15111 // Clear any existing timeout for this tooltip 15112 if (!window.tooltipTimeouts) { 15113 window.tooltipTimeouts = {}; // Initialize if it doesn't exist 15114 } 15115 15116 if (window.tooltipTimeouts[tooltipId]) { 15117 clearTimeout(window.tooltipTimeouts[tooltipId]); 15118 delete window.tooltipTimeouts[tooltipId]; 15119 } 15120 15121 // Create tooltip element 15122 const tooltip = document.createElement('div'); 15123 tooltip.className = 'tooltip'; // Ensure your CSS styles .tooltip 15124 tooltip.id = tooltipId; 15125 tooltip.textContent = tooltipText; 15126 15127 // Add to container (now 'container' is guaranteed to be an element) 15128 container.appendChild(tooltip); 15129 15130 // Position tooltip 15131 if (typeof positionTooltip === 'function') { 15132 positionTooltip(tooltip, trigger); 15133 } else { 15134 console.error("positionTooltip function is not defined."); 15135 tooltip.remove(); // Clean up if we can't position 15136 return; 15137 } 15138 15139 // Show with animation (slight delay for CSS transition) 15140 setTimeout(() => { 15141 tooltip.style.opacity = '1'; 15142 // The transform for animation should be handled by CSS transitions 15143 // e.g., .tooltip.visible { transform: translateX(-50%) translateY(-5px); } 15144 // Or ensure positionTooltip sets final transform if it needs to override CSS. 15145 // For now, assuming CSS handles the animated transform from its initial state. 15146 }, 10); 15147 15148 trigger.setAttribute('data-active-tooltip', tooltipId); 15149 15150 // Set automatic timeout to hide after 3 seconds 15151 window.tooltipTimeouts[tooltipId] = setTimeout(() => { 15152 hideTooltip(trigger); // Ensure hideTooltip function exists 15153 }, 3000); 15154 15155 // Also set up a global timer to ensure all tooltips are dismissed 15156 if (window.globalTooltipTimer) { 15157 clearTimeout(window.globalTooltipTimer); 15158 } 15159 window.globalTooltipTimer = setTimeout(() => { 15160 // Force hide all visible tooltips after 3 seconds 15161 const allTooltips = document.querySelectorAll('.tooltip'); 15162 allTooltips.forEach(tooltip => { 15163 if (tooltip.style.opacity !== '0') { 15164 tooltip.style.opacity = '0'; 15165 setTimeout(() => { 15166 if (tooltip.parentNode) { 15167 tooltip.parentNode.removeChild(tooltip); 15168 } 15169 }, 200); 15170 } 15171 }); 15172 // Clear all timeout references 15173 if (window.tooltipTimeouts) { 15174 for (const id in window.tooltipTimeouts) { 15175 clearTimeout(window.tooltipTimeouts[id]); 15176 } 15177 window.tooltipTimeouts = {}; 15178 } 15179 }, 3000); 15180} 15181 15182// Function to hide a tooltip 15183function hideTooltip(trigger) { 15184 const tooltipId = trigger.getAttribute('data-active-tooltip'); 15185 if (!tooltipId) return; 15186 15187 const tooltip = document.getElementById(tooltipId); 15188 if (tooltip) { 15189 // Fade out 15190 tooltip.style.opacity = '0'; 15191 tooltip.style.transform = 'translateX(-50%) translateY(0)'; 15192 15193 // Remove after animation 15194 setTimeout(() => { 15195 if (tooltip.parentNode) { 15196 tooltip.parentNode.removeChild(tooltip); 15197 } 15198 }, 200); 15199 } 15200 15201 // Clear any active timeout for this tooltip 15202 if (window.tooltipTimeouts && window.tooltipTimeouts[tooltipId]) { 15203 clearTimeout(window.tooltipTimeouts[tooltipId]); 15204 delete window.tooltipTimeouts[tooltipId]; 15205 } 15206 15207 // Clear reference 15208 trigger.removeAttribute('data-active-tooltip'); 15209 15210 // If this was the last tooltip, clear the global timer 15211 const remainingTooltips = document.querySelectorAll('.tooltip'); 15212 if (remainingTooltips.length === 0 && window.globalTooltipTimer) { 15213 clearTimeout(window.globalTooltipTimer); 15214 window.globalTooltipTimer = null; 15215 } 15216} 15217 15218// Function to initialize tooltips across the document 15219function initTooltipSystem() { 15220 // Check if container already exists 15221 let tooltipContainer = document.getElementById('tooltip-container'); 15222 15223 // Create it if it doesn't exist 15224 if (!tooltipContainer) { 15225 tooltipContainer = document.createElement('div'); 15226 tooltipContainer.id = 'tooltip-container'; 15227 document.body.appendChild(tooltipContainer); 15228 } 15229 15230 // Find all elements with data-custom-tooltip attribute 15231 const tooltipTriggers = document.querySelectorAll('[data-custom-tooltip]'); 15232 15233 // Add event listeners to each trigger
15234 tooltipTriggers.forEach(trigger => { 15235 // Skip if already initialized 15236 if (trigger.hasAttribute('data-custom-tooltip-initialized')) return; 15237 15238 // Mark as initialized 15239 trigger.setAttribute('data-custom-tooltip-initialized', 'true'); 15240 15241 // MouseEnter: Show tooltip 15242 trigger.addEventListener('mouseenter', () => { 15243 showTooltip(trigger); 15244 }); 15245 15246 // MouseLeave: Hide tooltip 15247 trigger.addEventListener('mouseleave', () => { 15248 hideTooltip(trigger); 15249 }); 15250 15251 // Click: Hide tooltip immediately 15252 trigger.addEventListener('click', () => { 15253 hideTooltip(trigger); 15254 }); 15255 15256 // Also handle focus/blur for accessibility 15257 trigger.addEventListener('focus', () => { 15258 showTooltip(trigger); 15259 }); 15260 15261 trigger.addEventListener('blur', () => { 15262 hideTooltip(trigger); 15263 }); 15264 }); 15265 15266 // Set up a failsafe function to clear any orphaned tooltips 15267 window.clearAllTooltips = function() { 15268 const container = document.getElementById('tooltip-container'); 15269 if (container) { 15270 container.innerHTML = ''; 15271 } 15272 15273 // Clear all timeouts 15274 if (window.tooltipTimeouts) { 15275 for (const id in window.tooltipTimeouts) { 15276 clearTimeout(window.tooltipTimeouts[id]); 15277 } 15278 window.tooltipTimeouts = {}; 15279 } 15280 15281 // Clear global timer 15282 if (window.globalTooltipTimer) { 15283 clearTimeout(window.globalTooltipTimer); 15284 window.globalTooltipTimer = null; 15285 } 15286 15287 // Clear all data-active-tooltip attributes 15288 document.querySelectorAll('[data-active-tooltip]').forEach(el => { 15289 el.removeAttribute('data-active-tooltip'); 15290 }); 15291 }; 15292 15293 // Initialize new tooltips when DOM changes 15294 setupMutationObserver(); 15295 15296 // Add a global function to manually dismiss all tooltips (for testing) 15297 window.dismissAllTooltips = function() { 15298 const allTooltips = document.querySelectorAll('.tooltip'); 15299 allTooltips.forEach(tooltip => { 15300 if (tooltip.style.opacity !== '0') { 15301 tooltip.style.opacity = '0'; 15302 setTimeout(() => { 15303 if (tooltip.parentNode) { 15304 tooltip.parentNode.removeChild(tooltip); 15305 } 15306 }, 200); 15307 } 15308 }); 15309 15310 // Clear all timeout references 15311 if (window.tooltipTimeouts) { 15312 for (const id in window.tooltipTimeouts) { 15313 clearTimeout(window.tooltipTimeouts[id]); 15314 } 15315 window.tooltipTimeouts = {}; 15316 } 15317 15318 // Clear global timer 15319 if (window.globalTooltipTimer) { 15320 clearTimeout(window.globalTooltipTimer); 15321 window.globalTooltipTimer = null; 15322 } 15323 15324 // Clear all data-active-tooltip attributes 15325 document.querySelectorAll('[data-active-tooltip]').forEach(el => { 15326 el.removeAttribute('data-active-tooltip'); 15327 }); 15328 }; 15329} 15330 15331function positionTooltip(tooltip, trigger) { 15332 const triggerRect = trigger.getBoundingClientRect(); // Viewport-relative coordinates 15333 15334 // To accurately get tooltip dimensions, temporarily make it visible but off-screen. 15335 // The #tooltip-container is position:fixed, so the tooltip's absolute positioning 15336 // will be relative to the viewport. 15337 tooltip.style.visibility = 'hidden'; 15338 tooltip.style.display = 'block'; // Or 'inline-block' if that's its intended final display 15339 // No need to change position to 'absolute' here if its CSS is already 'position: absolute;' 15340 // within the 'fixed' #tooltip-container. 15341 15342 const tooltipHeight = tooltip.offsetHeight; 15343 const tooltipWidth = tooltip.offsetWidth; 15344 15345 // Restore visibility; opacity transition will handle the fade-in. 15346 tooltip.style.visibility = ''; 15347 // tooltip.style.display = ''; // Let CSS manage display if opacity is used for showing/hiding 15348 15349 // Check if tooltip should be positioned to the right (for conflict indicator) 15350 const tooltipPosition = trigger.getAttribute('data-tooltip-position'); 15351 if (tooltipPosition === 'right') { 15352 // Position to the right of the trigger 15353 const viewportWidth = window.innerWidth; 15354 const viewportHeight = window.innerHeight; 15355 let left = triggerRect.right + 10; // 10px gap from the right edge of trigger 15356 let top = triggerRect.top + (triggerRect.height / 2) - (tooltipHeight / 2); // Vertically centered 15357 15358 // Recalculate tooltip dimensions after allowing text wrap 15359 tooltip.style.maxWidth = '200px'; 15360 tooltip.style.whiteSpace = 'normal'; 15361 tooltip.style.wordWrap = 'break-word'; 15362 const tooltipWidthAfterWrap = tooltip.offsetWidth; 15363 const tooltipHeightAfterWrap = tooltip.offsetHeight; 15364 15365 // If tooltip goes off-screen to the right, try positioning to the left of the button 15366 if (left + tooltipWidthAfterWrap > viewportWidth - 5) { 15367 left = triggerRect.left - tooltipWidthAfterWrap - 10; // Position to the left of button 15368 // If it still goes off the left edge, position it at the left edge with padding 15369 if (left < 5) { 15370 left = 5; // Keep it at least 5px from the left edge 15371 // The tooltip will wrap to fit within the available space 15372 tooltip.style.maxWidth = `${viewportWidth - left - 10}px`; 15373 } 15374 } else {
15375 // Tooltip fits on the right, but make sure it doesn't go off-screen 15376 if (left + tooltipWidthAfterWrap > viewportWidth - 5) { 15377 tooltip.style.maxWidth = `${viewportWidth - left - 10}px`; 15378 } 15379 } 15380 15381 // Recalculate after max-width adjustment 15382 const finalTooltipWidth = tooltip.offsetWidth; 15383 const finalTooltipHeight = tooltip.offsetHeight; 15384 15385 // Ensure tooltip doesn't go off-screen on the right side 15386 if (left + finalTooltipWidth > viewportWidth - 5) { 15387 left = viewportWidth - finalTooltipWidth - 5; 15388 } 15389 15390 // Adjust if tooltip goes off-screen vertically 15391 if (top < 5) { 15392 top = 5; 15393 } else if (top + finalTooltipHeight > viewportHeight - 5) { 15394 top = viewportHeight - finalTooltipHeight - 5; 15395 } 15396 15397 tooltip.style.left = `${left}px`; 15398 tooltip.style.top = `${top}px`; 15399 tooltip.classList.remove('tooltip-bottom'); // No arrow needed for side positioning 15400 return; 15401 } 15402 15403 // Calculate desired horizontal center point of the trigger 15404 const triggerCenter = triggerRect.left + triggerRect.width / 2; 15405 15406 // Calculate tooltip's left position to center it under/over trigger's center 15407 let left = triggerCenter - tooltipWidth / 2; 15408 15409 // Default position: above the trigger 15410 // Arrow height (8px) + desired gap (e.g., 2px) = 10px total offset 15411 let top = triggerRect.top - tooltipHeight - 10; 15412 tooltip.classList.remove('tooltip-bottom'); // Manages arrow direction via CSS 15413 15414 // Check if it goes off-screen at the top (add a small buffer, e.g., 5px) 15415 if (top < 5) { 15416 // Position below the trigger 15417 top = triggerRect.bottom + 10; // 10px gap (arrow height + small space) 15418 tooltip.classList.add('tooltip-bottom'); 15419 } 15420 15421 // Boundary checks for left position to prevent going off-screen horizontally 15422 const viewportWidth = window.innerWidth; 15423 if (left < 5) { // 5px buffer from left edge 15424 left = 5; 15425 } else if (left + tooltipWidth > viewportWidth - 5) { // 5px buffer from right edge 15426 left = viewportWidth - tooltipWidth - 5; 15427 } 15428 15429 tooltip.style.left = `${left}px`; 15430 tooltip.style.top = `${top}px`; 15431 15432 // The CSS for .tooltip might have `transform: translateX(-50%)` for initial centering. 15433 // Since JS now calculates the exact 'left' for centering, 15434 // we can clear any such transform that might interfere. 15435 // However, if your CSS animation relies on a transform, adjust accordingly. 15436 // If your CSS only uses opacity for transition, this is fine: 15437 tooltip.style.transform = ''; 15438} 15439 15440// Watch for new tooltip triggers added to the DOM 15441function setupMutationObserver() { 15442 // Check if MutationObserver is available 15443 if (!window.MutationObserver) return; 15444 15445 // Create observer instance 15446 const observer = new MutationObserver((mutations) => { 15447 let shouldInit = false; 15448 15449 // Check if relevant nodes were added 15450 mutations.forEach(mutation => { 15451 if (mutation.type === 'childList') { 15452 mutation.addedNodes.forEach(node => { 15453 // Check if it's an Element and has data-custom-tooltip or contains elements with data-custom-tooltip 15454 if (node.nodeType === 1) { // ELEMENT_NODE 15455 if (node.hasAttribute && node.hasAttribute('data-custom-tooltip')) { 15456 shouldInit = true; 15457 } else if (node.querySelectorAll) { 15458 const tooltips = node.querySelectorAll('[data-custom-tooltip]'); 15459 if (tooltips.length) shouldInit = true; 15460 } 15461 } 15462 }); 15463 } 15464 }); 15465 15466 // If relevant nodes found, reinitialize tooltips 15467 if (shouldInit) { 15468 initTooltipSystem(); 15469 } 15470 }); 15471 15472 // Start observing the entire document with the configured parameters 15473 observer.observe(document.body, { 15474 childList: true, 15475 subtree: true 15476 }); 15477} 15478 15479// Initialize when DOM is ready 15480document.addEventListener('DOMContentLoaded', initTooltipSystem); 15481 15482// Also initialize immediately if document already loaded 15483if (document.readyState === 'complete' || document.readyState === 'interactive') { 15484 initTooltipSystem(); 15485} 15486 15487// Add a global safety function that runs every minute to clear any tooltips that might have gotten stuck 15488setInterval(() => { 15489 // Get all visible tooltips 15490 const visibleTooltips = document.querySelectorAll('.tooltip[style*="opacity: 1"]'); 15491 15492 // If any are found, check how long they've been visible 15493 if (visibleTooltips.length > 0) { 15494 console.log('Found visible tooltips during cleanup check, clearing them'); 15495 window.clearAllTooltips(); 15496 } 15497}, 5000); // 15498 15499// Mobile drawer a11y: Tab cycle + restore focus. No visual/mouse behavior changes. 15500var _mobileMenuFocusTrapHandler = null; 15501 15502function getMobileMenuFocusables(sidebar) { 15503 if (!sidebar) return []; 15504 return Array.prototype.filter.call( 15505 sidebar.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not
15505([tabindex="-1"])'), 15506 function (el) { 15507 if (el.getAttribute('aria-hidden') === 'true') return false; 15508 var style = typeof getComputedStyle === 'function' ? getComputedStyle(el) : null; 15509 if (style && (style.display === 'none' || style.visibility === 'hidden')) return false; 15510 return el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement; 15511 } 15512 ); 15513} 15514 15515function releaseMobileMenuFocusTrap() { 15516 if (_mobileMenuFocusTrapHandler) { 15517 document.removeEventListener('keydown', _mobileMenuFocusTrapHandler, true); 15518 _mobileMenuFocusTrapHandler = null; 15519 } 15520} 15521 15522function installMobileMenuFocusTrap(sidebar) { 15523 releaseMobileMenuFocusTrap(); 15524 if (!sidebar) return; 15525 _mobileMenuFocusTrapHandler = function (event) { 15526 if (event.key !== 'Tab') return; 15527 var sb = document.querySelector('.sidebar'); 15528 if (!sb || !sb.classList.contains('mobile-open')) return; 15529 var focusables = getMobileMenuFocusables(sb); 15530 var hamburger = document.getElementById('hamburgerBtn'); 15531 // Include hamburger so Shift+Tab from first item returns to the control that opened the drawer 15532 var cycle = hamburger ? [hamburger].concat(focusables.filter(function (el) { return el !== hamburger; })) : focusables; 15533 if (!cycle.length) return; 15534 var first = cycle[0]; 15535 var last = cycle[cycle.length - 1]; 15536 if (event.shiftKey) { 15537 if (document.activeElement === first || (sb.contains(document.activeElement) === false && document.activeElement !== hamburger)) { 15538 event.preventDefault(); 15539 last.focus(); 15540 } 15541 } else if (document.activeElement === last) { 15542 event.preventDefault(); 15543 first.focus(); 15544 } 15545 }; 15546 document.addEventListener('keydown', _mobileMenuFocusTrapHandler, true); 15547} 15548 15549// Toggle mobile menu state 15550function toggleMobileMenu() { 15551const sidebar = document.querySelector('.sidebar'); 15552const hamburgerBtn = document.getElementById('hamburgerBtn'); 15553const mobileBackdrop = document.getElementById('mobileBackdrop'); 15554 15555// Check if we're in horizontal layout mode 15556const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal'); 15557 15558sidebar.classList.toggle('mobile-open'); 15559hamburgerBtn.classList.toggle('active'); 15560const isOpen = sidebar.classList.contains('mobile-open'); 15561hamburgerBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 15562 15563if (mobileBackdrop) { 15564 if (isHorizontalLayout) { 15565 // For horizontal layout on mobile, use normal backdrop (tap outside to close) 15566 // Clear any inline overrides so CSS can control visibility 15567 mobileBackdrop.style.display = ''; 15568 mobileBackdrop.style.pointerEvents = ''; 15569 mobileBackdrop.style.opacity = ''; 15570 mobileBackdrop.style.visibility = ''; 15571 mobileBackdrop.classList.toggle('mobile-open', isOpen); 15572 setTimeout(() => { 15573 mobileBackdrop.classList.toggle('active', isOpen); 15574 }, 10); 15575 } else { 15576 // For other layouts, use normal backdrop behavior 15577 mobileBackdrop.classList.toggle('mobile-open'); 15578 // Slight delay to show opacity transition 15579 setTimeout(() => { 15580 mobileBackdrop.classList.toggle('active', isOpen); 15581 }, 10); 15582 } 15583} 15584 15585// Prevent body scrolling when menu is open 15586document.body.style.overflow = isOpen ? 'hidden' : ''; 15587 15588if (isOpen) { 15589 // Trap Tab only â do not steal focus on mouse/touch open (avoids focus rings / scroll jumps) 15590 installMobileMenuFocusTrap(sidebar); 15591} else { 15592 releaseMobileMenuFocusTrap(); 15593 // Restore focus only if keyboard focus was inside the drawer 15594 if (hamburgerBtn && sidebar && sidebar.contains(document.activeElement)) { 15595 try { hamburgerBtn.focus(); } catch (e) {} 15596 } 15597} 15598} 15599 15600// Close the mobile menu 15601function closeMobileMenu() { 15602const sidebar = document.querySelector('.sidebar'); 15603const hamburgerBtn = document.getElementById('hamburgerBtn'); 15604const mobileBackdrop = document.getElementById('mobileBackdrop'); 15605const wasOpen = sidebar && sidebar.classList.contains('mobile-open'); 15606 15607// Check if we're in horizontal layout mode 15608const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal'); 15609 15610sidebar.classList.remove('mobile-open'); 15611hamburgerBtn.classList.remove('active'); 15612if (hamburgerBtn) hamburgerBtn.setAttribute('aria-expanded', 'false'); 15613 15614if (mobileBackdrop) { 15615 if (isHorizontalLayout) { 15616 // For horizontal layout, clear inline overrides and remove mobile-open 15617 mobileBackdrop.style.display = ''; 15618 mobileBackdrop.style.pointerEvents = ''; 15619 mobileBackdrop.style.opacity = ''; 15620 mobileBackdrop.style.visibility = ''; 15621 mobileBackdrop.classList.remove('mobile-open'); 15622 mobileBackdrop.classList.remove('active'); 15623 } else { 15624 // For other layouts, use normal backdrop behavior 15625 mobileBackdrop.classList.remove('active'); 15626 setTimeout(() => { 15627 mobileBackdrop.classList.remove('mobile-open'); 15628 }, 300); // Match transition duration 15629 } 15630} 15631
15632// Re-enable body scrolling 15633document.body.style.overflow = ''; 15634releaseMobileMenuFocusTrap(); 15635// Return focus to hamburger only if focus was inside the drawer (keyboard/SR); mouse users unchanged 15636if (wasOpen && hamburgerBtn && sidebar && sidebar.contains(document.activeElement)) { 15637 try { hamburgerBtn.focus(); } catch (e) {} 15638} 15639} 15640 15641function updateMobileHeaderContent() { 15642 const currentLayout = typeof getEffectiveMenuLayout === 'function' 15643 ? getEffectiveMenuLayout() 15644 : (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings 15645 ? window.MenuStyleCustomizer.settings.menuLayout || 'sidebar' 15646 : (document.body.classList.contains('menu-layout-horizontal') ? 'horizontal' : 'sidebar')); 15647 15648 const isMobileView = window.innerWidth <= 768; // Assuming 768px is your mobile breakpoint 15649 15650 const mobileHeaderContent = document.querySelector('.mobile-header-content'); 15651 if (!mobileHeaderContent) { 15652 console.warn("updateMobileHeaderContent: .mobile-header-content element not found."); 15653 return; 15654 } 15655 15656 // On mobile, center the content unless Space Colony (left-aligned title) 15657 if (isMobileView) { 15658 const isSpaceColonyTheme = typeof isSpaceColonySiteTheme === 'function' && isSpaceColonySiteTheme(); 15659 mobileHeaderContent.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center'; 15660 // Also ensure logo-menu-container doesn't grow 15661 const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container'); 15662 if (logoMenuContainer) { 15663 logoMenuContainer.style.flexGrow = '0'; 15664 logoMenuContainer.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center'; 15665 } 15666 } 15667 15668 console.log(`updateMobileHeaderContent: Layout: ${currentLayout}, Mobile: ${isMobileView}`); 15669 15670 // Clear previous generic mobile content (like centered logo/title) 15671 const existingMobileImage = mobileHeaderContent.querySelector('.mobile-header-image'); 15672 const existingMobileText = mobileHeaderContent.querySelector('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title'); 15673 if (existingMobileImage) { 15674 // The img is always appended directly (never wrapped in an <a>). Remove it straight. 15675 // Also handle any legacy <a>-wrapped images left from older deployments. 15676 const imageParent = existingMobileImage.parentElement; 15677 if (imageParent && imageParent.tagName === 'A' && imageParent.parentElement === mobileHeaderContent) { 15678 imageParent.remove(); 15679 } else { 15680 existingMobileImage.remove(); 15681 } 15682 } 15683 // Sweep any stray empty <a> tags left by older deployments that used the wrapper approach 15684 Array.from(mobileHeaderContent.children).forEach(function(child) { 15685 if (child.tagName === 'A' && !child.querySelector('.mobile-header-image') && !(child.textContent || '').trim()) { 15686 child.remove(); 15687 } 15688 }); 15689 if (existingMobileText) existingMobileText.remove(); 15690 15691 if (currentLayout === 'horizontal' && !isMobileView) { 15692 // Desktop Horizontal Layout: 15693 // The logo is handled by renderHorizontalMenu placing it in .horizontal-header-logo. 15694 // The menu items are in .horizontal-menu-container. 15695 // Remove stray mobile-only title/image nodes (duplicate of horizontal-header-logo). 15696 mobileHeaderContent.querySelectorAll('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title, .mobile-header-image').forEach(function(el) { 15697 el.remove(); 15698 }); 15699 console.log("updateMobileHeaderContent: Desktop horizontal layout, re-rendering horizontal menu to ensure sidebar elements are available."); 15700 if (typeof renderHorizontalMenu === 'function') { 15701 // Wait for sidebar elements to be available before rendering 15702 const waitForSidebarElements = (attempts = 0) => { 15703 const maxAttempts = 10; 15704 if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 15705 console.log("updateMobileHeaderContent: Sidebar elements available, rendering horizontal menu."); 15706 renderHorizontalMenu(); 15707 } else if (attempts < maxAttempts) { 15708 const attemptNum = attempts + 1; 15709 console.log("updateMobileHeaderContent: Waiting for sidebar elements (attempt " + attemptNum + "/" + maxAttempts + ")..."); 15710 setTimeout(() => waitForSidebarElements(attempts + 1), 100); 15711 } else { 15712 console.warn("updateMobileHeaderContent: Sidebar elements not available after waiting, rendering anyway."); 15713 renderHorizontalMenu(); 15714 } 15715 }; 15716 waitForSidebarElements(); 15717 } 15718 return; 15719 } 15720 15721 // For Horizontal layout on mobile, we need to clear the horizontal menu items 15722 // and show only the mobile header logo/title 15723 if (currentLayout === 'horizontal' && isMobileView) { 15724 console.log("updateMobileHeaderContent: Mobile horizontal layout - clearing horizontal menu items and showing mobile logo."); 15725 15726 // Clear horizontal menu items from mobile header 15727 const horizontalMenuContainer = mobileHeaderContent.querySelector('.horizontal-menu
15727-container'); 15728 if (horizontalMenuContainer) { 15729 console.log("updateMobileHeaderContent: Clearing horizontal menu container"); 15730 horizontalMenuContainer.innerHTML = ''; 15731 } else { 15732 console.log("updateMobileHeaderContent: No horizontal menu container found to clear"); 15733 } 15734 15735 // Clear horizontal header logo from mobile header 15736 const horizontalHeaderLogo = mobileHeaderContent.querySelector('.horizontal-header-logo'); 15737 if (horizontalHeaderLogo) { 15738 console.log("updateMobileHeaderContent: Clearing horizontal header logo"); 15739 horizontalHeaderLogo.innerHTML = ''; 15740 } else { 15741 console.log("updateMobileHeaderContent: No horizontal header logo found to clear"); 15742 } 15743 15744 // Also clear any existing horizontal menu items that might be directly in mobile-header-content 15745 const existingHorizontalMenuItems = mobileHeaderContent.querySelectorAll('.horizontal-menu-item'); 15746 if (existingHorizontalMenuItems.length > 0) { 15747 console.log('updateMobileHeaderContent: Removing ' + existingHorizontalMenuItems.length + ' existing horizontal menu items'); 15748 existingHorizontalMenuItems.forEach(item => item.remove()); 15749 } 15750 } 15751 15752 // For Sidebar, Top, OR Horizontal layout on mobile: 15753 // Populate .mobile-header-content with the first sidebar element (logo/title). 15754 // The CSS will hide .horizontal-menu-container and .horizontal-header-logo on mobile for horizontal layout. 15755 15756 console.log("updateMobileHeaderContent: Populating with first sidebar element."); 15757 15758 // For horizontal layout on mobile, use the header-specific function to get logo/title 15759 // For other layouts, use the general function 15760 let firstElement; 15761 if (currentLayout === 'horizontal' && isMobileView) { 15762 firstElement = getFirstSidebarElementForHeader(); // Gets logo/title specifically 15763 console.log("updateMobileHeaderContent: Using getFirstSidebarElementForHeader for mobile horizontal layout"); 15764 } else { 15765 firstElement = getFirstSidebarElement(); // Gets any first element 15766 console.log("updateMobileHeaderContent: Using getFirstSidebarElement for other layouts"); 15767 } 15768 15769 if (firstElement) { 15770 console.log('updateMobileHeaderContent: First element found:', JSON.stringify(firstElement)); 15771 15772 if (firstElement.type === 'image' && (firstElement.imageData || firstElement.imageUrl)) { 15773 console.log('updateMobileHeaderContent: Creating mobile header image'); 15774 const img = document.createElement('img'); 15775 img.src = firstElement.imageUrl || firstElement.imageData; 15776 img.alt = resolveBrandLogoAlt(firstElement); 15777 img.className = 'mobile-header-image sidebar-image'; // For specific styling if needed 15778 15779 let mobileDisplayNode = img; 15780 if (firstElement.colorizeWithSiteColors && 15781 window.SidebarManager && 15782 typeof window.SidebarManager._buildSidebarImageDisplayNode === 'function') { 15783 const buildData = Object.assign({}, firstElement); 15784 if (!buildData.imageWidth) { 15785 buildData.imageWidth = 'auto'; 15786 } 15787 mobileDisplayNode = window.SidebarManager._buildSidebarImageDisplayNode(img, buildData); 15788 } 15789 15790 // Opt-in per-image padding wrapper (no-op when unset) 15791 if (window.SidebarManager && 15792 typeof window.SidebarManager._wrapWithImagePadding === 'function') { 15793 mobileDisplayNode = window.SidebarManager._wrapWithImagePadding(mobileDisplayNode, firstElement); 15794 } 15795 15796 // Handle linking: always append the img directly so existing CSS centering is 15797 // not disrupted by an <a> wrapper (which the many !important positioning rules 15798 // were not written to handle). Navigation is wired via a click handler. 15799 if (firstElement.linkType && firstElement.linkType !== 'none') { 15800 let finalLinkHref = '#'; 15801 let isValidLink = false;
15802 const linkTarget = firstElement.linkTarget || '_self'; 15803 15804 if (firstElement.linkType === 'external' && firstElement.linkUrl) { 15805 finalLinkHref = firstElement.linkUrl; 15806 isValidLink = true; 15807 } else if (firstElement.linkType === 'internal' && firstElement.linkPageId) { 15808 const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []); 15809 const targetPage = allGalleries.find(g => g.id === parseInt(firstElement.linkPageId)); 15810 15811 if (targetPage) { 15812 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 15813 if (isPreviewMode) { 15814 const pathParts = window.location.pathname.split('/').filter(Boolean); 15815 const siteGuid = pathParts[0]; 15816 if (siteGuid && targetPage.slug) { 15817 finalLinkHref = '/' + siteGuid + '/' + targetPage.slug; 15818 isValidLink = true; 15819 } else if (targetPage.url && targetPage.url.startsWith('/')) { 15820 finalLinkHref = targetPage.url; 15821 isValidLink = true; 15822 } 15823 } else { 15824 if (targetPage.slug) { 15825 finalLinkHref = '/' + targetPage.slug; 15826 isValidLink = true; 15827 } else if (targetPage.url && targetPage.url.startsWith('/')) { 15828 finalLinkHref = targetPage.url; 15829 isValidLink = true; 15830 } 15831 } 15832 } 15833 } 15834 15835 if (isValidLink) { 15836 mobileDisplayNode.style.cursor = 'pointer'; 15837 const capturedHref = finalLinkHref; 15838 const capturedTarget = linkTarget; 15839 const capturedLinkType = firstElement.linkType; 15840 const capturedPageId = firstElement.linkPageId; 15841 15842 mobileDisplayNode.addEventListener('click', (e) => { 15843 e.preventDefault(); 15844 if (capturedLinkType === 'internal' && capturedPageId) { 15845 const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []); 15846 const targetPage = allGalleries.find(g => g.id === parseInt(capturedPageId)); 15847 if (targetPage) { 15848 if (isPageMenuItem(targetPage) && typeof window.loadPage === 'function') { 15849 window.loadPage(targetPage.id); 15850 } else if (typeof window.loadGallery === 'function') { 15851 window.loadGallery(targetPage.id); 15852 } else { 15853 window.location.href = capturedHref; 15854 } 15855 } else { 15856 window.location.href = capturedHref; 15857 } 15858 } else { 15859 if (capturedTarget === '_blank') { 15860 window.open(capturedHref, '_blank'); 15861 } else { 15862 window.location.href = capturedHref; 15863 } 15864 } 15865 }); 15866 } 15867 } 15868 mobileHeaderContent.appendChild(mobileDisplayNode); 15869 } else if (firstElement.type === 'text' && firstElement.textContent) { 15870 console.log('updateMobileHeaderContent: Creating mobile header text'); 15871 const textDiv = document.createElement('div'); 15872 textDiv.className = 'mobile-header-text'; // For specific styling 15873 textDiv.innerHTML = firstElement.textContent; // Use innerHTML if textContent can contain HTML 15874 const titleEl = textDiv.querySelector('.site-title-text, h1, h2, p:last-of-type, p:only-of-type'); 15875 if (titleEl && !titleEl.classList.contains('site-title-text')) { 15876 titleEl.classList.add('site-title-text'); 15877 } 15878 mobileHeaderContent.appendChild(textDiv); 15879 } else if (firstElement.type === 'menu' && galleries && galleries.length > 0) { 15880 console.log('updateMobileHeaderContent: Creating mobile header menu title'); 15881 // Fallback for 'menu' type, find the first visible gallery title 15882 const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer); 15883 const titleText = firstVisibleGallery ? firstVisibleGallery.title : (firstElement.title || 'Menu'); 15884 const titleDiv = document.createElement('div'); 15885 titleDiv.className = 'mobile-header-menu-title'; // For specific styling 15886 titleDiv.textContent = titleText; 15887 mobileHeaderContent.appendChild(titleDiv); 15888 } else { // Default fallback 15889 console.log('updateMobileHeaderContent: Creating mobile header title (fallback)'); 15890 const titleDiv = document.createElement('div'); 15891 titleDiv.className = 'mobile-header-title'; // For specific styling 15892 titleDiv.textContent = firstElement.title || (galleries && galleries.length > 0 && galleries[0].title) || 'Menu'; 15893 mobileHeaderContent.appendChild(titleDiv); 15894 } 15895 } else { 15896 // Fallback if no sidebar elements exist 15897 const defaultTitle = document.createElement('div'); 15898 defaultTitle.className = 'mobile-header-title'; 15899 defaultTitle.textContent = 'Menu'; 15900 mobileHeaderContent.appendChild(defaultTitle); 15901 console.log("updateMobileHeaderContent: No first sidebar element found, showing default title."); 15902 } 15903 15904 if (typeof syncContentMarginToHorizontalHeader === 'function') { 15905 syncContentMarginToHorizontalHeader(); 15906 } 15907} 15908 15909// Helper function to get the first element in the sidebar 15910function getFirstSidebarElement() { 15911// Check if window.SidebarManager exists and has elements 15912if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 15913// Filter to only visible elements 15914const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false && el.type !== 'spacer'); 15915 15916if (visibleElements.length > 0) { 15917 // Sort by position and return the first one 15918 return visibleElements.sort((a, b) => a.position - b.position)[0]; 15919} 15920} 15921 15922// If no sidebar elements, return first gallery as fallback 15923if (galleries.length > 0) { 15924const firstVisibleGallery = galleries.find(g => g.visible !== false); 15925if (firstVisibleGallery) { 15926 return { 15927 type: 'menu', 15928 title: firstVisibleGallery.title 15929 }; 15930} 15931} 15932 15933// Default fallback 15934return { 15935type: 'text', 15936title: 'Menu' 15937}; 15938} 15939// KEEP YOUR ORIGINAL initMobileMenu, toggleMobileMenu, closeMobileMenu FUNCTIONS EXACTLY AS THEY ARE 15940// Just replace the updateMobileTitle function and add our initialization code 15941 15942// Replace the existing updateMobileTitle function with this one 15943function updateMobileTitle() { 15944// Always call our implementation that shows the top sidebar element 15945updateMobileHeaderContent(); 15946} 15947 15948// Add window resize listener to handle mobile header content updates 15949window.addEventListener('resize', () => { 15950 // Debounce the resize event to avoid excessive calls 15951 clearTimeout(window.resizeTimeout); 15952 window.resizeTimeout = setTimeout(() => { 15953 if (typeof updateMobileHeaderContent === 'function') { 15954 updateMobileHeaderContent(); 15955 } 15956 if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') { 15957 applyHorizontalLogoMenuLayoutStyles(); 15958 } 15959 if (typeof syncContentMarginToHorizontalHeader === 'function') { 15960 syncContentMarginToHorizontalHeader(); 15961 } 15962 }, 250); 15963}); 15964 15965//separate initialization function that doesn't replace the original initMobileMenu 15966function initTopElementDisplay() { 15967console.log("Initializing top element display"); 15968 15969// Run immediately 15970updateMobileHeaderContent(); 15971 15972if (typeof syncContentMarginToHorizontalHeader === 'function') { 15973 setTimeout(syncContentMarginToHorizontalHeader, 150); 15974} 15975 15976// Also hook into loadGallery to ensure it runs whenever galleries change 15977const originalLoadGallery = window.loadGallery; 15978if (typeof originalLoadGallery === 'function') { 15979window.loadGallery = function(id, event, options) { 15980 // Forward all args (including post-save { forceReload: true }) to the underlying loader 15981 const result = originalLoadGallery(id, event, options); 15982 updateMobileHeaderContent(); 15983 return result; 15984}; 15985} 15986} 15987 15988 15989// Initialize our functionality after a short delay to ensure SidebarManager is ready 15990document.addEventListener('DOMContentLoaded', function() { 15991// Short delay to ensure SidebarManager is initialized 15992setTimeout(initTopElementDisplay, 100); 15993 15994// Also run a bit later to catch any late-loading elements 15995setTimeout(updateMobileHeaderContent, 500); 15996}); 15997 15998// Run immediately if document is already loaded 15999if (document.readyState === 'complete' || document.readyState === 'interactive') { 16000setTimeout(initTopElementDisplay, 100); 16001} 16002 16003// Add a final fallback to ensure gallery visibility on window load 16004window.addEventListener('load', function() { 16005 setTimeout(ensureGalleryVisibility, 2000); 16006}); 16007 16008 16009(function() { 16010 /** 16011 * EnhancedGallerySystem - Comprehensive gallery script and state management 16012 */ 16013 class EnhancedGallerySystem { 16014 constructor() { 16015 // Core state tracking 16016 this.initialized = false;
16017 this.initPromise = null; 16018 this.isLoading = false; 16019 this.currentGalleryId = null; 16020 this.version = '260923-002'; // Increment when gallery scripts change (matches current versioned filenames) 16021 16022 // Track loaded resources 16023 this.loadedScripts = new Set(); 16024 this.loadedStyles = new Set(); 16025 16026 // Resources to ensure are loaded - Use Worker route (avoids CDN SSL issues) 16027 const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; 16028 this.essentialResources = { 16029 scripts: [ 16030 { id: 'neon-gallery-main', src: galleryScriptsBase + 'neon-gallery-main-v260923-002.js' }, 16031 { id: 'neon-gallery-hp', src: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.js' } 16032 ], 16033 styles: [ 16034 { id: 'neon-gallery-css', href: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css' } 16035 ] 16036 }; 16037 16038 // Keep track of original initialization functions 16039 this.originalInitFunctions = {}; 16040 16041 // Keep reference to gallery state between changes 16042 this.previousGalleryState = null; 16043 16044 // Track last initialization time per gallery ID to prevent duplicates 16045 this.lastInitializationTime = new Map(); 16046 this._loadInProgress = null; 16047 this._loadingGalleryId = null; 16048 } 16049 16050 /** 16051 * Setup the correct gallery container structure 16052 * @param {HTMLElement} container - The main container element 16053 * @returns {boolean} - Success indicator 16054 */ 16055setupCorrectGalleryContainer(container) { 16056 console.log('EnhancedGallerySystem: Setting up correct gallery container structure'); 16057 16058 // Validate container 16059 if (!container) { 16060 console.error('EnhancedGallerySystem: Main gallery container is null'); 16061 return false; 16062 } 16063 16064 try { 16065 // Clear the container 16066 container.innerHTML = ''; 16067 16068 // Create the exact container structure expected by the gallery script 16069 const neonContainer = document.createElement('div'); 16070 neonContainer.id = 'neon-gallery-container'; 16071 neonContainer.style.width = '100%'; 16072 neonContainer.style.height = '100%'; 16073 neonContainer.style.position = 'relative'; 16074 16075 // Add the container to the DOM 16076 container.appendChild(neonContainer); 16077 16078 // Set visibility 16079 container.style.opacity = '1'; 16080 container.style.display = 'block'; 16081 16082 console.log('EnhancedGallerySystem: Gallery container structure created'); 16083 return true; 16084 } catch (error) { 16085 console.error('EnhancedGallerySystem: Error setting up gallery container:', error); 16086 return false; 16087 } 16088} 16089 16090 /** 16091 * Ensure #neon-gallery-container exists before gallery scripts execute. 16092 * neon-gallery-main bails early without it; hp may leave neonGalleryUtils as {}. 16093 */ 16094 ensureGalleryContainerPlaceholder(container) { 16095 if (document.getElementById('neon-gallery-container')) { 16096 return document.getElementById('neon-gallery-container'); 16097 } 16098 const host = container || document.querySelector('.gallery-container') || document.getElementById('galleryFrame')?.parentElement; 16099 if (!host) { 16100 return null; 16101 } 16102 const neonContainer = document.createElement('div'); 16103 neonContainer.id = 'neon-gallery-container'; 16104 neonContainer.className = 'gallery-direct-content'; 16105 neonContainer.style.width = '100%'; 16106 neonContainer.style.height = '100%'; 16107 neonContainer.style.position = 'relative'; 16108 host.appendChild(neonContainer); 16109 console.log('EnhancedGallerySystem: Created placeholder neon-gallery-container before script load'); 16110 return neonContainer; 16111 } 16112 16113 /** 16114 * Wait for neon-gallery-main to expose loadGallerySettings; reload main if needed. 16115 */ 16116 async ensureNeonGalleryUtilsReady(container) { 16117 const hasLoadGallerySettings = () => 16118 typeof window.neonGalleryUtils?.loadGallerySettings === 'function'; 16119 16120 if (hasLoadGallerySettings()) { 16121 return true; 16122 } 16123 16124 const mainScript = this.essentialResources.scripts.find(s => s.id === 'neon-gallery-main'); 16125 if (!mainScript) { 16126 return false; 16127 } 16128 16129 console.warn('EnhancedGallerySystem: neonGalleryUtils.loadGallerySettings missing â reloading main script'); 16130 this.ensureGalleryContainerPlaceholder(container); 16131 16132 document.querySelectorAll('script[src*="neon-gallery-main"]').forEach(el => { 16133 if (el.parentNode) el.parentNode.removeChild(el); 16134 }); 16135 this.loadedScripts.delete('neon-gallery-main');
16136 try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; } 16137 16138 await new Promise((resolve, reject) => { 16139 const scriptElement = document.createElement('script'); 16140 scriptElement.id = 'neon-gallery-main'; 16141 scriptElement.src = `${mainScript.src}?v=${this.version}&reload=${Date.now()}`; 16142 scriptElement.async = false; 16143 scriptElement.onload = () => { 16144 this.loadedScripts.add('neon-gallery-main'); 16145 resolve(); 16146 }; 16147 scriptElement.onerror = (err) => reject(err); 16148 document.body.appendChild(scriptElement); 16149 }); 16150 16151 for (let i = 0; i < 50; i++) { 16152 if (hasLoadGallerySettings()) { 16153 return true; 16154 } 16155 await new Promise(r => setTimeout(r, 100)); 16156 } 16157 16158 return hasLoadGallerySettings(); 16159 } 16160 16161 /** 16162 * Preload styles only â defer script loading until a gallery load provides a container. 16163 */ 16164 preloadStylesOnly() { 16165 return this.loadStyles(); 16166 } 16167 16168 /** 16169 * Reset initialization tracking when gallery scripts are removed from the DOM 16170 * (e.g. loadPage cleanup). Without this, a pending initPromise never resolves and 16171 * subsequent loadGallery calls hang with isLoading:true, initialized:false. 16172 */ 16173 resetInitializationState() { 16174 console.log('EnhancedGallerySystem: Resetting initialization state'); 16175 this.initPromise = null; 16176 this.initialized = false; 16177 this.isLoading = false; 16178 this.loadedScripts.clear(); 16179 this.loadedStyles.clear(); 16180 // Allow neon-gallery-main bootstrap wipe once again after scripts are re-injected 16181 try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; } 16182 } 16183 16184 /** 16185 * Initialize the gallery system 16186 * @returns {Promise} Resolves when all core dependencies are loaded 16187 */ 16188 initialize() { 16189 // Return existing promise if already initializing or complete 16190 if (this.initPromise) { 16191 return this.initPromise; 16192 } 16193 16194 console.log('EnhancedGallerySystem: Initializing gallery framework'); 16195 this.isLoading = true; 16196 16197 // Create a promise to track loading completion 16198 this.initPromise = (async () => { 16199 try { 16200 // CRITICAL: Initialize neonGalleryState before anything else 16201 // This prevents "Cannot set properties of undefined" errors 16202 if (!window.neonGalleryState) { 16203 window.neonGalleryState = { 16204 initialized: false, 16205 initializing: false, 16206 domInitialized: false, 16207 configApplied: false, 16208 initializationPromise: null 16209 }; 16210 console.log('EnhancedGallerySystem: Initialized neonGalleryState'); 16211 } 16212 16213 // 1. First ensure all essential styles are loaded 16214 await this.loadStyles(); 16215 16216 // 2. Preload all scripts to hint to the browser 16217 this.preloadScripts(); 16218 16219 // 3. Ensure gallery container exists before scripts that require it 16220 this.ensureGalleryContainerPlaceholder( 16221 document.querySelector('.gallery-container') 16222 ); 16223 16224 // 4. Load all essential scripts 16225 await this.loadScripts(); 16226 16227 // 5. Initialize the gallery events 16228 this.setupEvents(); 16229 16230 // Mark as fully initialized 16231 this.initialized = true; 16232 this.isLoading = false; 16233 16234 console.log('EnhancedGallerySystem: Successfully initialized'); 16235 return true; 16236 } catch (error) { 16237 console.error('EnhancedGallerySystem: Initialization failed:', error); 16238 this.isLoading = false; 16239 throw error; 16240 } 16241 })(); 16242 16243 return this.initPromise; 16244 } 16245 16246 /** 16247 * Load all necessary stylesheet resources 16248 */ 16249 async loadStyles() { 16250 console.log('EnhancedGallerySystem: Loading essential stylesheets'); 16251 16252 const stylePromises = this.essentialResources.styles.map(style => { 16253 // Skip if already loaded 16254 if (this.loadedStyles.has(style.id) || document.getElementById(style.id)) { 16255 console.log(`Style already loaded: ${style.id}`); 16256 this.loadedStyles.add(style.id); 16257 return Promise.resolve(); 16258 } 16259 16260 return new Promise((resolve, reject) => { 16261 const link = document.createElement('link'); 16262 link.id = style.id; 16263 link.rel = 'stylesheet'; 16264 // Use proxy URL if flag is set, otherwise use CDN URL 16265 let styleUrl = `${style.href}?v=${this.version}`; 16266 if (window.useCdnProxy && window.convertCdnToProxyUrl && style.href.includes('cdn.neonsky.app')) { 16267 styleUrl = window.convertCdnToProxyUrl(style.href) + '?v=' + this.version; 16268 console.log('ð Using proxy URL for style:', style.id, styleUrl); 16269 } 16270 16271 link.href = styleUrl; 16272 16273 link.onload = () => { 16274 console.log(`â Style loaded: ${style.id}`); 16275 this.loadedStyles.add(style.id); 16276 resolve(); 16277 }; 16278 16279 link.onerror = (err) => { 16280 console.error(`â Failed to load style: ${style.id}`, err); 16281 if (style.href.includes('cdn.neonsky.app') && !styleUrl.includes('/cdn-proxy/')) { 16282 console.error('ð Retrying via proxy:', style.href); 16283 const proxyUrl = window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(style.href) : style.href.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 16284 link.href = proxyUrl + '?v=' + this.version; 16285 link.onerror = function() { 16286 console.error('â Proxy failed, trying storage:', proxyUrl); 16287 link.href = link.href.replace('cdn.neonsky.app', 'storage.neonsky.app'); 16288 // Don't reject - let it try storage 16289 }; 16290 } else { 16291 reject(new Error(`Failed to load ${style.id}`)); 16292 } 16293 }; 16294 16295 document.head.appendChild(link); 16296 }); 16297 }); 16298 16299 // Wait for all styles to load 16300 await Promise.all(stylePromises); 16301 console.log('EnhancedGallerySystem: All stylesheets loaded'); 16302 } 16303 16304 /** 16305 * Preload scripts to improve browser loading priority 16306 */ 16307 preloadScripts() {
16308 this.essentialResources.scripts.forEach(script => { 16309 // Enhanced check for already preloaded scripts 16310 const scriptName = script.src.split('/').pop(); 16311 const alreadyPreloaded = document.querySelector(`link[rel="preload"][href*="${scriptName}"]`) || 16312 document.querySelector(`script[src*="${scriptName}"]`); 16313 16314 if (alreadyPreloaded) { 16315 console.log(`Script already preloaded or loaded: ${script.id}`); 16316 return; 16317 } 16318 16319 const preload = document.createElement('link'); 16320 preload.rel = 'preload'; 16321 preload.href = `${script.src}?v=${this.version}`; 16322 preload.as = 'script'; 16323 document.head.appendChild(preload); 16324 16325 console.log(`Script preloaded: ${script.id}`); 16326 }); 16327 } 16328 16329 /** 16330 * Load all necessary scripts 16331 */ 16332 async loadScripts() { 16333 console.log('EnhancedGallerySystem: Loading essential scripts'); 16334 16335 // Load sequentially (main before hp) so neon-gallery-main assigns window.neonGalleryUtils 16336 // before hp's initNeonGallery can run; parallel async scripts could execute hp before main finishes. 16337 const loadOne = (script) => { 16338 const alreadyLoaded = this.loadedScripts.has(script.id) || 16339 document.getElementById(script.id) || 16340 document.querySelector(`script[src*="${script.src.split('/').pop()}"]`); 16341 16342 if (alreadyLoaded) { 16343 const mainNeedsReload = 16344 script.id === 'neon-gallery-main' && 16345 typeof window.neonGalleryUtils?.loadGallerySettings !== 'function'; 16346 if (mainNeedsReload) { 16347 console.warn(`EnhancedGallerySystem: ${script.id} in DOM but neonGalleryUtils incomplete â reloading`); 16348 document.querySelectorAll('script[src*="neon-gallery-main"]').forEach(el => { 16349 if (el.parentNode) el.parentNode.removeChild(el); 16350 }); 16351 this.loadedScripts.delete(script.id); 16352 try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; } 16353 } else { 16354 console.log(`Script already loaded: ${script.id}`); 16355 this.loadedScripts.add(script.id); 16356 return Promise.resolve(); 16357 } 16358 } 16359 16360 return new Promise((resolve, reject) => { 16361 const scriptElement = document.createElement('script'); 16362 scriptElement.id = script.id; 16363 16364 let scriptUrl = `${script.src}?v=${this.version}`; 16365 if (window.useCdnProxy && window.convertCdnToProxyUrl && script.src.includes('cdn.neonsky.app')) { 16366 scriptUrl = window.convertCdnToProxyUrl(script.src) + '?v=' + this.version; 16367 console.log('ð Using proxy URL for script:', script.id, scriptUrl); 16368 } 16369 16370 scriptElement.src = scriptUrl; 16371 scriptElement.async = false; 16372 16373 scriptElement.onload = () => { 16374 console.log(`â Script loaded: ${script.id}`); 16375 this.loadedScripts.add(script.id); 16376 resolve(); 16377 }; 16378 16379 scriptElement.onerror = (err) => { 16380 console.error(`â Failed to load script: ${script.id}`, err); 16381 if (script.src.includes('cdn.neonsky.app') && !scriptUrl.includes('/cdn-proxy/')) { 16382 console.error('ð Retrying script via proxy:', script.id); 16383 const proxyUrl = window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(script.src) : script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 16384 scriptElement.src = proxyUrl + '?v=' + this.version; 16385 scriptElement.onerror = function() { 16386 console.error('â Proxy failed, trying storage:', proxyUrl); 16387 scriptElement.src = scriptElement.src.replace('cdn.neonsky.app', 'storage.neonsky.app'); 16388 }; 16389 } else { 16390 reject(new Error(`Failed to load ${script.id}`)); 16391 } 16392 }; 16393 16394 document.body.appendChild(scriptElement); 16395 }); 16396 }; 16397 16398 for (const script of this.essentialResources.scripts) { 16399 await loadOne(script); 16400 } 16401 console.log('EnhancedGallerySystem: All scripts loaded'); 16402 16403 // Save references to original initialization functions after scripts are loaded 16404 if (window.initNeonGallery && typeof window.initNeonGallery === 'function') { 16405 this.originalInitFunctions.initNeonGallery = window.initNeonGallery; 16406 } 16407 16408 if (window.reinitializeGalleryAfterSave && typeof window.reinitializeGalleryAfterSave === 'function') { 16409 this.originalInitFunctions.reinitializeGalleryAfterSave = window.reinitializeGalleryAfterSave; 16410 } 16411 } 16412 16413 /** 16414 * Set up enhanced event listeners 16415 */ 16416 setupEvents() { 16417 // Listen for gallery save events to capture latest state 16418 document.addEventListener('gallery-saved', (event) => { 16419 if (event.detail && event.detail.settings) { 16420 this.captureGalleryState(event.detail.settings); 16421 } 16422 }); 16423 16424 // Override existing functions to capture state changes 16425 this.enhanceExistingFunctions(); 16426 } 16427 16428 /** 16429 * Enhance existing gallery functions to work with our system 16430 */ 16431 enhanceExistingFunctions() { 16432 // Save original functions 16433 const originalInit = window.initNeonGallery; 16434 const originalReinit = window.reinitializeGalleryAfterSave; 16435 16436 // Only enhance if they exist 16437 if (typeof originalInit === 'function') { 16438 // Track if initNeonGallery is currently executing to prevent concurrent calls 16439 let isExecuting = false;
16440 let lastExecutionTime = 0; 16441 16442 window.initNeonGallery = async (...args) => { 16443 // Prevent duplicate initializations within 1 second 16444 const now = Date.now(); 16445 16446 // Check if already executing or called recently 16447 if (isExecuting) { 16448 console.log('EnhancedGallerySystem: initNeonGallery already executing, skipping duplicate call'); 16449 return Promise.resolve(); 16450 } 16451 16452 if (lastExecutionTime && (now - lastExecutionTime) < 1000) { 16453 console.log('EnhancedGallerySystem: Skipping duplicate initNeonGallery call (last call ' + (now - lastExecutionTime) + 'ms ago)'); 16454 return Promise.resolve(); 16455 } 16456 16457 // Set flag and timestamp immediately to prevent concurrent calls 16458 isExecuting = true; 16459 lastExecutionTime = now; 16460 this.lastInitializationTime.set('initNeonGallery', now); 16461 16462 try { 16463 // Call original with all arguments 16464 const result = await originalInit.apply(this, args); 16465 16466 // Capture state after initialization 16467 if (window.neonGalleryConfig) { 16468 this.captureGalleryState(window.neonGalleryConfig); 16469 } 16470 16471 return result; 16472 } finally { 16473 // Clear execution flag after a short delay to allow for async operations 16474 setTimeout(() => { 16475 isExecuting = false; 16476 }, 100); 16477 } 16478 }; 16479 } 16480 16481 if (typeof originalReinit === 'function') { 16482 window.reinitializeGalleryAfterSave = async (savedSettings) => { 16483 // Call original function 16484 const result = await originalReinit(savedSettings); 16485 16486 // Capture updated state 16487 this.captureGalleryState(savedSettings); 16488 16489 return result; 16490 }; 16491 } 16492 } 16493 16494 /** 16495 * Capture current gallery state for potential reuse 16496 */ 16497 captureGalleryState(settings) { 16498 if (!settings) return; 16499 16500 // Store a deep copy of the settings to avoid reference issues 16501 try { 16502 this.previousGalleryState = JSON.parse(JSON.stringify(settings)); 16503 console.log('EnhancedGallerySystem: Captured gallery state for possible reuse'); 16504 } catch (e) { 16505 console.error('EnhancedGallerySystem: Failed to capture gallery state:', e); 16506 } 16507 } 16508 16509 /** 16510 * Load a specific gallery using the enhanced system 16511 * @param {Object} gallery The gallery configuration 16512 * @param {HTMLElement} container The container to load the gallery into 16513 */ 16514 async loadGallery(gallery, container, options) { 16515 if (!gallery) { 16516 console.error('EnhancedGallerySystem: Invalid gallery configuration'); 16517 return false; 16518 } 16519 16520 const forceGalleryReload = options?.forceReload === true || window._forceGalleryReload === true; 16521 16522 // Coalesce concurrent loads for the same gallery â await instead of wiping mid-flight 16523 if (this._loadInProgress && this._loadingGalleryId === gallery.id && !forceGalleryReload) { 16524 const pageViewStillShowing = window._activeContentType === 'page' || 16525 !!document.querySelector('.gallery-container .page-container, .page-container'); 16526 if (!pageViewStillShowing) { 16527 console.log('EnhancedGallerySystem: Awaiting in-flight load for gallery ' + gallery.id); 16528 return this._loadInProgress; 16529 } 16530 console.log('EnhancedGallerySystem: Abandoning standalone in-flight load for page embed', gallery.id); 16531 this._loadInProgress = null; 16532 this._loadingGalleryId = null; 16533 } 16534 16535 const now = Date.now(); 16536 const lastInitTime = this.lastInitializationTime.get(gallery.id); 16537 if (!forceGalleryReload && lastInitTime && (now - lastInitTime) < 1000) { 16538 const hasContent = !!document.querySelector('.gallery-wrapper .masonry-item, .gallery-wrapper .horizontal-scroller, .gallery-wrapper .masonry'); 16539 const pageViewStillShowing = window._activeContentType === 'page' || 16540 !!document.querySelector('.gallery-container .page-container, .page-container'); 16541 if (hasContent && !pageViewStillShowing) { 16542 console.log('EnhancedGallerySystem: Gallery ' + gallery.id + ' recently loaded with content, skipping re-init'); 16543 if (container) container.style.opacity = '1'; 16544 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 16545 window.syncGalleryOptionsButtonVisibility(); 16546 } 16547 return true; 16548 } 16549 } 16550 16551 if (forceGalleryReload) { 16552 console.log('EnhancedGallerySystem: force reload requested for gallery ' + gallery.id); 16553 this.lastInitializationTime.delete(gallery.id); 16554 this._loadInProgress = null; 16555 this._loadingGalleryId = null; 16556 } 16557 16558 const loadTask = this._executeGalleryLoad(gallery, container, now, options); 16559 this._loadInProgress = loadTask; 16560 this._loadingGalleryId = gallery.id;
16561 try { 16562 return await loadTask; 16563 } finally { 16564 if (this._loadInProgress === loadTask) { 16565 this._loadInProgress = null; 16566 this._loadingGalleryId = null; 16567 } 16568 } 16569 } 16570 16571 async _executeGalleryLoad(gallery, container, startedAt, options) { 16572 this.lastInitializationTime.set(gallery.id, startedAt); 16573 const contentLoadGeneration = options && options.contentLoadGeneration != null 16574 ? options.contentLoadGeneration 16575 : window.__hydraContentLoadGeneration; 16576 const stillCurrent = () => typeof isHydraContentLoadCurrent === 'function' 16577 ? isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id) 16578 : contentLoadGeneration === window.__hydraContentLoadGeneration && 16579 (!window._activeContentType || window._activeContentType === 'gallery') && 16580 (window.activeGalleryId == null || String(window.activeGalleryId) === String(gallery.id)); 16581 16582 stopAutoAdvanceTimer(); 16583 16584 // CRITICAL: Verify container exists 16585 if (!container) { 16586 console.error('EnhancedGallerySystem: Gallery container is null or undefined'); 16587 return false; 16588 } 16589 16590 // Create a unique instance ID for this gallery load (needed before script init) 16591 const galleryInstanceId = Date.now(); 16592 16593 try { 16594 // Clean up previous gallery state and prepare container BEFORE loading scripts 16595 this.cleanupGalleryState(container); 16596 const containerSetup = this.setupCorrectGalleryContainer(container); 16597 if (!containerSetup) { 16598 throw new Error('Failed to set up gallery container'); 16599 } 16600 this.setupGalleryParameters(gallery, galleryInstanceId); 16601 } catch (prepError) { 16602 console.error('EnhancedGallerySystem: Failed to prepare gallery container:', prepError); 16603 return false; 16604 } 16605 16606 // Make sure the system is initialized (scripts load with container already in DOM) 16607 try { 16608 await this.initialize(); 16609 } catch (error) { 16610 console.error('EnhancedGallerySystem: Failed to initialize gallery framework:', error); 16611 //this.showErrorMessage(container, 'Failed to initialize gallery system. Please try again later.'); 16612 return false; 16613 } 16614 16615 if (!stillCurrent()) { 16616 console.log('EnhancedGallerySystem: Aborting stale load after framework initialize for', gallery.id); 16617 return false; 16618 } 16619 16620 if (!(await this.ensureNeonGalleryUtilsReady(container))) { 16621 console.error('EnhancedGallerySystem: neonGalleryUtils.loadGallerySettings unavailable after script load'); 16622 return false; 16623 } 16624 16625 if (!stillCurrent()) { 16626 console.log('EnhancedGallerySystem: Aborting stale load after utils ready for', gallery.id); 16627 return false; 16628 } 16629 16630 // Update current gallery tracking 16631 this.currentGalleryId = gallery.id; 16632 16633 console.log(`EnhancedGallerySystem: Loading gallery ${gallery.title} (ID: ${gallery.id}, Instance: ${galleryInstanceId})`); 16634 16635 try { 16636 // Determine which initialization method to use 16637 if (this.canUseReinitForGallery(gallery, options)) { 16638 const result = await this.reinitializeGallery(gallery, container, options); 16639 return stillCurrent() ? result : false;
16640 } else { 16641 const result = await this.initializeNewGallery(gallery, container, galleryInstanceId, options); 16642 return stillCurrent() ? result : false; 16643 } 16644 } catch (error) { 16645 console.error('EnhancedGallerySystem: Error loading gallery:', error); 16646 if (container) { 16647 console.error('EnhancedGallerySystem: Error loading gallery:', error); 16648 //this.showErrorMessage(container, 'There was a problem loading the gallery. Please try again later.'); 16649 } 16650 return false; 16651 } 16652} 16653 16654 normalizeGalleryIdentityValue(value) { 16655 if (value === undefined || value === null || value === '') return ''; 16656 return String(value); 16657 } 16658 16659 previousGalleryStateBelongsTo(gallery, previousState) { 16660 if (!gallery || !previousState) return false; 16661 16662 const targetGalleryId = this.normalizeGalleryIdentityValue(gallery.id); 16663 const targetPageId = this.normalizeGalleryIdentityValue(gallery.pageId); 16664 if (!targetGalleryId && !targetPageId) return false; 16665 16666 const previousGalleryId = this.normalizeGalleryIdentityValue( 16667 previousState.galleryId || previousState.loadedGalleryId 16668 ); 16669 const previousPageId = this.normalizeGalleryIdentityValue( 16670 previousState.pageId || previousState.initialPageUuid 16671 ); 16672 16673 if (!previousGalleryId && !previousPageId) return false; 16674 if (previousGalleryId && targetGalleryId && previousGalleryId !== targetGalleryId) { 16675 return false; 16676 } 16677 if (previousPageId && targetPageId && previousPageId !== targetPageId) { 16678 return false; 16679 } 16680 16681 return ( 16682 (previousGalleryId && targetGalleryId && previousGalleryId === targetGalleryId) || 16683 (previousPageId && targetPageId && previousPageId === targetPageId) 16684 ); 16685 } 16686 16687 /** 16688 * Determine if we can use reinitialization for this gallery 16689 */ 16690 canUseReinitForGallery(gallery, options) { 16691 if (options?.forceReload === true || window._forceGalleryReload === true) { 16692 return false; 16693 } 16694 // Check if reinitializeGalleryAfterSave function exists 16695 if (typeof window.reinitializeGalleryAfterSave !== 'function') { 16696 return false; 16697 } 16698 16699 // Check if we have a previous state to reuse 16700 if (!this.previousGalleryState) { 16701 return false; 16702 } 16703 16704 if (!this.previousGalleryStateBelongsTo(gallery, this.previousGalleryState)) { 16705 console.log('EnhancedGallerySystem: Cannot use reinit - previous gallery state does not match target gallery'); 16706 return false; 16707 } 16708 16709 // Check if the gallery has the necessary properties for reinit 16710 if (!gallery.pageId || !gallery.galleryOptions) { 16711 return false; 16712 } 16713 16714 // CRITICAL: Check if the gallery wrapper actually exists in the DOM 16715 // Reinitialization requires an existing gallery structure 16716 const galleryWrapper = document.querySelector('.gallery-wrapper'); 16717 const neonContainer = document.getElementById('neon-gallery-container'); 16718 if (!galleryWrapper || !neonContainer) { 16719 console.log('EnhancedGallerySystem: Cannot use reinit - gallery wrapper or container not found'); 16720 return false; 16721 } 16722 16723 return true; 16724 } 16725 16726 /** 16727 * Reinitialize a gallery using the reinitialization process 16728 */ 16729async reinitializeGallery(gallery, container, options) { 16730 console.log('EnhancedGallerySystem: Using reinitialization process for gallery', gallery.id); 16731 16732 try { 16733 // CRITICAL: Ensure CSS is loaded before attempting reinitialization 16734 ensureGalleryStylesLoaded(); 16735 16736 // We should already have the proper container, but just in case: 16737 if (!document.getElementById('neon-gallery-container')) { 16738 this.setupCorrectGalleryContainer(container); 16739 } 16740 16741 // CRITICAL: Ensure config is set up with all required fields before reinitialization 16742 const galleryOptions = gallery.galleryOptions || {}; 16743 window.neonGalleryConfig = { 16744 useData: true, 16745 useCDN: true, 16746 version: 'live', 16747 manualCollectionName: galleryOptions.manualCollectionName || "mod", 16748 layoutType: galleryOptions.layoutType || "grid", 16749 ...galleryOptions, 16750 siteId: window.siteId || gallery.siteId || '', 16751 galleryId: gallery.id, 16752 // Explicitly include all permanent storage fields 16753 isPerma: galleryOptions.isPerma, 16754 permaURL: galleryOptions.permaURL, 16755 loadTxId: galleryOptions.loadTxId, 16756 loadPermaURL: galleryOptions.loadPermaURL, 16757 containerSelector: '#neon-gallery-container' 16758 }; 16759 16760 // Prepare settings by merging previous state with new gallery options. 16761 // Identity fields always come from the menu target, never leftover snapshot ids. 16762 const settings = { 16763 ...this.previousGalleryState, 16764 ...gallery.galleryOptions, 16765 pageId: gallery.pageId, 16766 initialPageUuid: gallery.pageId, 16767 galleryId: gallery.id, 16768 loadedGalleryId: gallery.id, 16769 forceReload: true, 16770 containerSelector: '#neon-gallery-container' // Add this explicitly 16771 }; 16772 16773 // Call reinitializeGalleryAfterSave with the merged settings 16774 window._reinitializeGalleryFromSystem = true; 16775 let result; 16776 try { 16777 result = await window.reinitializeGalleryAfterSave(settings); 16778 } finally { 16779 window._reinitializeGalleryFromSystem = false;
16780 } 16781 16782 if (result) { 16783 console.log('EnhancedGallerySystem: Gallery reinitialization completed successfully'); 16784 // Fade in the container 16785 container.style.opacity = '1'; 16786 return true; 16787 } else { 16788 throw new Error('Reinitialization did not complete successfully'); 16789 } 16790 } catch (error) { 16791 console.error('EnhancedGallerySystem: Error reinitializing gallery:', error); 16792 // Fall back to standard initialization - ensure config is set up properly 16793 const galleryInstanceId = Date.now(); 16794 // Re-setup parameters and config for the fallback 16795 this.setupGalleryParameters(gallery, galleryInstanceId); 16796 return this.initializeNewGallery(gallery, container, galleryInstanceId, options); 16797 } 16798} 16799 16800 /** 16801 * Initialize a gallery from scratch (fallback method) 16802 */ 16803 async initializeNewGallery(gallery, container, galleryInstanceId, options) { 16804 console.log('EnhancedGallerySystem: Using standard initialization for gallery', gallery.id); 16805 const contentLoadGeneration = options && options.contentLoadGeneration != null 16806 ? options.contentLoadGeneration 16807 : window.__hydraContentLoadGeneration; 16808 const stillCurrent = () => typeof isHydraContentLoadCurrent === 'function' 16809 ? isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id) 16810 : (!window._activeContentType || window._activeContentType === 'gallery') && 16811 (window.activeGalleryId == null || String(window.activeGalleryId) === String(gallery.id)); 16812 16813 try { 16814 // CRITICAL: Ensure CSS is loaded BEFORE creating any gallery elements 16815 ensureGalleryStylesLoaded(); 16816 16817 // Clean up container 16818 if (container) { 16819 container.innerHTML = ''; 16820 } else { 16821 console.error('EnhancedGallerySystem: Container is null in initializeNewGallery'); 16822 return false; 16823 } 16824 16825 // Create gallery container with unique ID 16826 const galleryContainerId = `gallery-container-${gallery.id}-${galleryInstanceId}`; 16827 const neonGalleryContainer = document.createElement('div'); 16828 neonGalleryContainer.id = 'neon-gallery-container'; 16829 neonGalleryContainer.className = 'gallery-direct-content'; 16830 neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString()); 16831 neonGalleryContainer.setAttribute('data-instance-id', galleryInstanceId.toString()); 16832 neonGalleryContainer.setAttribute('data-unique-id', galleryContainerId); 16833 neonGalleryContainer.style.width = '100%'; 16834 neonGalleryContainer.style.height = '100%'; 16835 container.appendChild(neonGalleryContainer); 16836 16837 // CRITICAL: Initialize neonGalleryState if it doesn't exist 16838 if (!window.neonGalleryState) { 16839 window.neonGalleryState = { 16840 initialized: false, 16841 initializing: false, 16842 domInitialized: false, 16843 configApplied: false, 16844 initializationPromise: null 16845 }; 16846 } 16847 16848 // CRITICAL: Always ensure neonGalleryConfig is properly set with ALL required fields 16849 // Don't rely on conditional logic - always set it correctly here 16850 const galleryOptions = gallery.galleryOptions || {}; 16851 window.neonGalleryConfig = { 16852 useData: true, 16853 useCDN: true, 16854 version: 'live', 16855 manualCollectionName: galleryOptions.manualCollectionName || "mod", 16856 layoutType: galleryOptions.layoutType || "grid", 16857 ...galleryOptions, 16858 siteId: window.siteId || gallery.siteId || '', 16859 galleryId: gallery.id, 16860 galleryInstanceId: galleryInstanceId, 16861 containerSelector: '#neon-gallery-container', 16862 // Explicitly include all permanent storage fields (critical for proper initialization) 16863 isPerma: galleryOptions.isPerma, 16864 permaURL: galleryOptions.permaURL, 16865 loadTxId: galleryOptions.loadTxId, 16866 loadPermaURL: galleryOptions.loadPermaURL 16867 }; 16868 16869 // Call the initialization function directly if available 16870 if (typeof window.initNeonGallery === 'function') { 16871 console.log('EnhancedGallerySystem: Calling direct initialization method'); 16872 16873 // Additional check: prevent duplicate initNeonGallery calls within 1 second 16874 const initKey = 'init_' + gallery.id + '_' + galleryInstanceId; 16875 const lastInitCall = this.lastInitializationTime.get(initKey); 16876 const now = Date.now(); 16877 if (lastInitCall && (now - lastInitCall) < 1000) { 16878 console.log('EnhancedGallerySystem: Skipping duplicate initNeonGallery call for gallery ' + gallery.id + '
16878(last call ' + (now - lastInitCall) + 'ms ago)'); 16879 container.style.opacity = '1'; 16880 return true; 16881 } 16882 this.lastInitializationTime.set(initKey, now); 16883 16884 try { 16885 // Wait a tiny bit to ensure DOM updates have propagated 16886 await new Promise(resolve => setTimeout(resolve, 10)); 16887 if (!stillCurrent()) { 16888 console.log('EnhancedGallerySystem: Aborting stale initializeNewGallery before initNeonGallery'); 16889 return false; 16890 } 16891 16892 const result = await window.initNeonGallery(); 16893 if (!stillCurrent()) { 16894 console.log('EnhancedGallerySystem: Ignoring stale initializeNewGallery completion'); 16895 return false; 16896 } 16897 console.log('EnhancedGallerySystem: Direct initialization successful'); 16898 16899 // Save state for future loads 16900 if (window.neonGalleryConfig) { 16901 this.captureGalleryState(window.neonGalleryConfig); 16902 } 16903 16904 // Fade in the container 16905 container.style.opacity = '1'; 16906 return true; 16907 } catch (e) { 16908 if (!stillCurrent()) { 16909 console.log('EnhancedGallerySystem: Ignoring stale direct initialization error'); 16910 return false; 16911 } 16912 console.error('EnhancedGallerySystem: Direct initialization failed:', e); 16913 throw e; 16914 } 16915 } else { 16916 // If initNeonGallery isn't available, use event-based initialization 16917 console.log('EnhancedGallerySystem: Using event-based initialization fallback'); 16918 16919 // Create an event to trigger initialization 16920 const initEvent = new CustomEvent('neon-gallery-init', { 16921 detail: { 16922 galleryId: gallery.id, 16923 instanceId: galleryInstanceId, 16924 containerId: galleryContainerId 16925 } 16926 }); 16927 document.dispatchEvent(initEvent); 16928 16929 // Wait a short time to see if initialization completes 16930 await new Promise(resolve => setTimeout(resolve, 500)); 16931 if (!stillCurrent()) { 16932 console.log('EnhancedGallerySystem: Ignoring stale event-based gallery initialization'); 16933 return false; 16934 } 16935 16936 // Fade in regardless of result - gallery script should handle displaying errors 16937 container.style.opacity = '1'; 16938 return true; 16939 } 16940 } catch (error) { 16941 console.error('EnhancedGallerySystem: Error in standard initialization:', error); 16942 if (!stillCurrent()) { 16943 console.log('EnhancedGallerySystem: Ignoring stale standard initialization error'); 16944 return false; 16945 } 16946 if (container) { 16947 this.showErrorMessage(container, 'Failed to load gallery content. Please try again later.'); 16948 } 16949 return false; 16950 } 16951 } 16952 16953 /** 16954 * Clean up gallery state without removing loaded scripts 16955 */ 16956 cleanupGalleryState(container) { 16957 console.log('EnhancedGallerySystem: Cleaning up gallery state'); 16958 16959 // Stop any ongoing singles autoplay from the previous gallery 16960 if (typeof window.stopSinglesAutoplay === 'function') { 16961 try { 16962 window.stopSinglesAutoplay(); 16963 console.log('EnhancedGallerySystem: Called stopSinglesAutoplay() during cleanup.'); 16964 } catch (error) { 16965 console.warn('EnhancedGallerySystem: stopSinglesAutoplay() failed during cleanup:', error); 16966 } 16967 } else { 16968 console.warn('EnhancedGallerySystem: stopSinglesAutoplay() function not found during cleanup.'); 16969 } 16970 16971 // 1. Reset global gallery state variables 16972 const resetGlobals = [ 16973 'neonGalleryInitComplete', 'neonGalleryInitInProgress', 16974 'neonGalleryLoaded', 'neonGalleryConfig', 'neonGalleryCache', 'neonGalleryImages', 16975 'neonGallerySettings', 'neonLightbox', 'neonGalleryEventListeners', 16976 'neonGalleryInstance', 'currentGalleryId', 'galleryData', 16977 'imageCache', 'thumbnailCache' 16978 ]; 16979
16980 resetGlobals.forEach(prop => { 16981 if (window[prop] !== undefined) { 16982 window[prop] = null; 16983 } 16984 }); 16985 16986 // 2. Reset neonGalleryState if it's a proper object with properties 16987 if (window.neonGalleryState && typeof window.neonGalleryState === 'object') { 16988 window.neonGalleryState = { 16989 initialized: false, 16990 initializing: false, 16991 domInitialized: false, 16992 configApplied: false, 16993 initializationPromise: null 16994 }; 16995 } 16996 16997 // 3. Remove lightbox elements and overlays 16998 document.querySelectorAll('#neon-lightbox, .neon-lightbox, .lightbox-container').forEach(el => { 16999 if (el && el.parentNode) { 17000 el.parentNode.removeChild(el); 17001 } 17002 }); 17003 17004 const staleOptionsOverlay = document.getElementById('optionsOverlay'); 17005 if (staleOptionsOverlay && staleOptionsOverlay.parentNode) { 17006 staleOptionsOverlay.parentNode.removeChild(staleOptionsOverlay); 17007 } 17008 window.optionsVisible = false; 17009 17010 // 4. Remove any gallery-specific DOM elements 17011 document.querySelectorAll( 17012 '.neon-gallery-wrapper, .fullscreen-overlay, ' + 17013 '.neon-gallery-modal, .gallery-tooltip, .neon-gallery-context-menu, ' + 17014 '.neon-thumbnails, .neon-image, .neon-caption, ' + 17015 '.neon-controls, .neon-pagination' 17016 ).forEach(el => { 17017 if (el && el.parentNode) { 17018 el.parentNode.removeChild(el); 17019 } 17020 }); 17021 17022 // 5. Clear container if provided and valid 17023 if (container) { 17024 container.innerHTML = ''; 17025 container.style.opacity = '0'; 17026 } 17027 17028 // 6. Clear keyboard listeners to prevent multiple listeners from different galleries 17029 if (typeof window.removeAllLightboxKeyboardListeners === 'function') { 17030 window.removeAllLightboxKeyboardListeners(); 17031 console.log('EnhancedGallerySystem: Cleared all lightbox keyboard listeners during cleanup'); 17032 } else if (window.currentLightboxKeyboardListener) { 17033 document.removeEventListener('keydown', window.currentLightboxKeyboardListener); 17034 window.currentLightboxKeyboardListener = null; 17035 console.log('EnhancedGallerySystem: Cleared keyboard listeners during cleanup'); 17036 } 17037 window.lightboxKeyboardListenerActive = false; 17038 } 17039 17040 /** 17041 * Prepare container for gallery loading 17042 */ 17043 prepareContainer(container, gallery, instanceId) { 17044 // CRITICAL: Add null check for container 17045 if (!container) { 17046 console.error('EnhancedGallerySystem: Container is null in prepareContainer'); 17047 return; 17048 } 17049 17050 try { 17051 // Add attributes and data 17052 container.setAttribute('data-gallery-id', gallery.id.toString()); 17053 container.setAttribute('data-instance-id', instanceId.toString()); 17054 17055 // Add loading indicator 17056 const loadingIndicator = document.createElement('div'); 17057 loadingIndicator.className = 'gallery-loading'; 17058 loadingIndicator.innerHTML = ` 17059 <div class="spinner"></div> 17060 <div class="loading-text">Loading Gallery...</div> 17061 `; 17062 17063 container.appendChild(loadingIndicator); 17064 } catch (error) { 17065 console.error('EnhancedGallerySystem: Error preparing container:', error); 17066 } 17067 } 17068 17069 17070 17071 17072setupGalleryParameters(gallery, galleryInstanceId) { 17073 // Set up gallery parameters with enhanced uniqueness 17074 window.Parameters = window.Parameters || {}; 17075 17076 // Clone and extend existing Parameters to avoid losing important values 17077 const existingParams = { ...window.Parameters }; 17078 17079 window.Parameters = { 17080 ...existingParams, 17081 SiteAlias: window.location.hostname, 17082 InitialPageUuid: gallery.pageId, 17083 InitialPageAlias: gallery.slug || this.slugify(gallery.title), 17084 // CRITICAL FIX: Set isInEditor to true if user is logged in as admin OR in edit mode 17085 // This ensures gallery files bypass cache for any logged-in admin, not just when edit mode is active 17086 isInEditor: window.isEditing || 17087 localStorage.getItem('hydra_is_admin') === 'true' || 17088 window.isAdmin === true || 17089 document.body.classList.contains('hydra-admin'), 17090 siteId: window.siteId || gallery.siteId || '', 17091 isHydra: true, 17092 17093 // CRITICAL: These properties help the gallery script find the container 17094 galleryContainerId: 'neon-gallery-container', 17095 containerSelector: '#neon-gallery-container', 17096 17097 // Additional properties 17098 galleryInstanceId: galleryInstanceId, 17099 galleryUniqueId: `gallery-container-${gallery.id}-${galleryInstanceId}`, 17100 loadTimestamp: galleryInstanceId, 17101 loadedGalleryId: gallery.id 17102 }; 17103 17104 // Set up gallery config - CRITICAL: Include all permanent storage fields 17105 const galleryOptions = gallery.galleryOptions || {}; 17106 window.neonGalleryConfig = { 17107 useData: true, 17108 useCDN: true, 17109 version: 'live', 17110 manualCollectionName: galleryOptions.manualCollectionName || "mod", 17111 layoutType: galleryOptions.layoutType || "grid", 17112 ...galleryOptions, 17113 siteId: window.siteId || gallery.siteId || '', 17114 galleryId: gallery.id, 17115 galleryInstanceId: galleryInstanceId, 17116 17117 // Add container selector here too 17118 containerSelector: '#neon-gallery-container', 17119 17120 // CRITICAL: Explicitly include all permanent storage fields to ensure they're available 17121 // These are needed for proper gallery initialization and data loading 17122 isPerma: galleryOptions.isPerma, 17123 permaURL: galleryOptions.permaURL, 17124 loadTxId: galleryOptions.loadTxId, 17125 loadPermaURL: galleryOptions.loadPermaURL 17126 }; 17127} 17128 17129 /** 17130 * Show error message in container 17131 */ 17132 showErrorMessage(container, message) {
17133 // CRITICAL: Add null check for container 17134 if (!container) { 17135 console.error('EnhancedGallerySystem: Container is null in showErrorMessage'); 17136 return; 17137 } 17138 17139 try { 17140 // Clear container 17141 container.innerHTML = ''; 17142 17143 // Create error message element 17144 const errorElement = document.createElement('div'); 17145 errorElement.className = 'gallery-error'; 17146 errorElement.innerHTML = ` 17147 <h3>Error Loading Gallery</h3> 17148 <p>${message}</p> 17149 `; 17150 container.appendChild(errorElement); 17151 container.style.opacity = '1'; 17152 } catch (error) { 17153 console.error('EnhancedGallerySystem: Error showing error message:', error); 17154 } 17155 } 17156 17157 /** 17158 * Utility function to convert title to slug 17159 */ 17160 slugify(text) { 17161 return text 17162 .toLowerCase() 17163 .replace(/s+/g, '-') 17164 .replace(/[^w-]+/g, '') 17165 .replace(/--+/g, '-') 17166 .trim(); 17167 } 17168 } 17169 17170 // Create a singleton instance 17171 window.EnhancedGallerySystem = window.EnhancedGallerySystem || new EnhancedGallerySystem(); 17172 17173 /** 17174 * Enhanced loadGallery function using the EnhancedGallerySystem 17175 * This replaces the existing loadGallery function 17176 */ 17177const enhancedLoadGallery = async function(id, event, options) { 17178 const originalLoadGallery = window.originalLoadGallery; 17179 const forceGalleryReload = options?.forceReload === true || window._forceGalleryReload === true; 17180 17181 stopAutoAdvanceTimer(); 17182 if (typeof dismissGalleryPasswordOverlay === 'function') { 17183 dismissGalleryPasswordOverlay(); 17184 } 17185 17186 if (event && event.target && event.target.isConnected === false) { 17187 console.log('enhancedLoadGallery: Ignoring navigation from detached element'); 17188 return; 17189 } 17190 17191 if (event) { 17192 event.preventDefault(); 17193 event.stopPropagation(); 17194 } 17195 17196 let galleryContainer = document.querySelector('.gallery-container'); 17197 17198 if (!galleryContainer) { 17199 galleryContainer = document.getElementById('galleryFrame')?.parentElement || 17200 document.getElementById('main-content') || 17201 document.getElementById('content'); 17202 17203 if (!galleryContainer) { 17204 console.error('Gallery container not found, falling back to original function'); 17205 if (typeof originalLoadGallery === 'function') { 17206 return originalLoadGallery(id, event, options); 17207 } else { 17208 console.error('Gallery container not found and no fallback available'); 17209 return; 17210 } 17211 } 17212 } 17213 17214 // Bail out before hiding/clearing when the same gallery is already loading or rendered. 17215 // Post-save passes { forceReload: true } to bypass this path without affecting navigation. 17216 const egEarly = window.EnhancedGallerySystem; 17217 if (egEarly && !forceGalleryReload) { 17218 if (egEarly._loadInProgress && egEarly._loadingGalleryId === id) { 17219 const pageViewStillShowing = window._activeContentType === 'page' || 17220 !!document.querySelector('.gallery-container .page-container, .page-container'); 17221 if (pageViewStillShowing) { 17222 console.log('enhancedLoadGallery: abandoning standalone in-flight load for page view', id); 17223 egEarly._loadInProgress = null; 17224 egEarly._loadingGalleryId = null; 17225 } else { 17226 console.log('enhancedLoadGallery: awaiting in-flight load for gallery', id); 17227 await egEarly._loadInProgress; 17228 if (galleryContainer) galleryContainer.style.opacity = '1'; 17229 return; 17230 } 17231 } 17232 const hasRenderedGallery = !!document.querySelector( 17233 '.gallery-wrapper .masonry-item, .gallery-wrapper .horizontal-scroller, .gallery-wrapper .masonry' 17234 ); 17235 // Embedded galleries on a page share currentGalleryId; that is not a standalone gallery view. 17236 const pageViewStillShowing = window._activeContentType === 'page' || 17237 !!document.querySelector('.gallery-container .page-container, .page-container'); 17238 if (hasRenderedGallery && egEarly.currentGalleryId === id && !pageViewStillShowing) { 17239 console.log('enhancedLoadGallery: gallery already rendered, skipping reload'); 17240 if (galleryContainer) galleryContainer.style.opacity = '1'; 17241 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 17242 window.syncGalleryOptionsButtonVisibility(); 17243 } 17244 return; 17245 } 17246 } else if (forceGalleryReload) { 17247 console.log('enhancedLoadGallery: force reload requested, bypassing already-rendered skip'); 17248 if (egEarly?.lastInitializationTime?.delete) { 17249 egEarly.lastInitializationTime.delete(id); 17250 } 17251 if (egEarly) { 17252 egEarly._loadInProgress = null; 17253 egEarly._loadingGalleryId = null; 17254 } 17255 } 17256 17257 if (galleryContainer) { 17258 galleryContainer.style.opacity = '0'; 17259 } 17260 17261 console.log('Loading gallery with ID (enhancedLoadGallery):', id); 17262 17263 const gallery = findGalleryById(galleries, id); 17264 17265 if (!gallery) { 17266 console.warn('No gallery found with ID:', id); 17267 if (galleryContainer) { 17268 galleryContainer.innerHTML = ` 17269 <div class="gallery-error"> 17270 <h3>Gallery Not Found</h3> 17271 <p>The requested gallery could not be found.</p> 17272 </div> 17273 `; 17274 galleryContainer.style.opacity = '1'; 17275 } 17276 return; 17277 } 17278 17279 if (isPageMenuItem(gallery)) { 17280 return typeof window.loadPage === 'function' ? window.loadPage(id, event) : loadPage(id, event); 17281 } 17282 17283 window._activeContentType = 'gallery'; 17284 17285 if (gallery.isSubmenu) { 17286 console.log('Gallery is submenu, skipping URL update'); 17287 toggleSubmenu(id, event || { stopPropagation: () => {} }); 17288 if (galleryContainer){ 17289 galleryContainer.style.opacity = '1'; 17290 } 17291 return; 17292 } 17293 17294 const contentLoadGeneration = beginHydraContentLoad('gallery', gallery.id, gallery.pageId); 17295 if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { 17296 window.PageManager.clearCurrentPage(); 17297 } 17298 17299 console.log('Found gallery:', gallery.title); 17300 17301 // Check for gallery-level password protection (only for visitors, not in edit mode) 17302 if (!isInEditMode() && gallery.passwordProtected) { 17303 // Use PageManager's gallery password protection methods 17304 if (window.PageManager && typeof window.PageManager.checkGalleryPasswordAccess === 'function' && 17305 typeof window.PageManager.createGalleryPasswordOverlay === 'function') { 17306 17307 if (!window.PageManager.checkGalleryPasswordAccess(gallery)) { 17308 console.log('Gallery is password protected, showing overlay'); 17309 window.PageManager.createGalleryPasswordOverlay(gallery, (accessGranted) => { 17310 if (accessGranted) { 17311 // Reload the gallery now that access is granted 17312 enhancedLoadGallery(id, event); 17313 } 17314 }); 17315 return; // Exit early, will reload after password is entered 17316 } 17317 } 17318 } 17319 17320 window.activeGalleryId = id; 17321 activeGalleryId = id; 17322 17323 // CRITICAL: Clean up page-embedded gallery scripts/DOM before loading a standalone gallery. 17324 // loadPage() calls removeGalleryScriptsWithPause which can interrupt EnhancedGallerySystem.initialize(), 17325 // leaving isLoading:true with a never-resolving initPromise. Re-run cleanup here (matching original 17326 // loadGallery) and clear the container so page content does not block gallery rendering. 17327 if (typeof window.removeGalleryScriptsWithPause === 'function') { 17328 await window.removeGalleryScriptsWithPause(); 17329 } 17330 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) { 17331 console.log('enhancedLoadGallery: Aborting stale load after cleanup for', gallery.id); 17332 return; 17333 } 17334 if (galleryContainer) { 17335 galleryContainer.innerHTML = ''; 17336 } 17337 17338 const bodyEl = document.body; 17339 if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 17340 bodyEl.classList.add('menu-hidden-on-page'); 17341 } else { 17342 bodyEl.classList.remove('menu-hidden-on-page'); 17343 } 17344 applyMenuOverlayClassForGallery(gallery); 17345 17346 try { 17347 if (typeof updateActiveStates === 'function') updateActiveStates(); 17348 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 17349 if (typeof closeMobileMenu === 'function') closeMobileMenu(); 17350 // Close the gallery options panel when navigating to a different gallery 17351 if (typeof window.closeOptionsPanel === 'function' && !forceGalleryReload) { 17352 window.closeOptionsPanel(); 17353 } 17354 const staleOptionsOverlay = document.getElementById('optionsOverlay'); 17355 if (staleOptionsOverlay) { 17356 staleOptionsOverlay.remove(); 17357 } 17358 window.optionsVisible = false;
17359 if (typeof updateURLWithGallerySlug === 'function') { 17360 updateURLWithGallerySlug(gallery); 17361 } 17362 } catch (uiError) { 17363 console.error('Error updating UI state:', uiError); 17364 } 17365 17366 try { 17367 // Add detailed logging about EnhancedGallerySystem availability 17368 console.log('ð Checking EnhancedGallerySystem availability:', { 17369 exists: !!window.EnhancedGallerySystem, 17370 hasLoadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'), 17371 initialized: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.initialized), 17372 isLoading: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.isLoading) 17373 }); 17374 17375 if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function') { 17376 console.log('â EnhancedGallerySystem available, calling loadGallery'); 17377 const result = await window.EnhancedGallerySystem.loadGallery( 17378 gallery, 17379 galleryContainer, 17380 { 17381 ...(options || {}), 17382 ...(forceGalleryReload ? { forceReload: true } : {}), 17383 contentLoadGeneration 17384 } 17385 ); 17386 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) { 17387 console.log('enhancedLoadGallery: Ignoring stale gallery completion for', gallery.id); 17388 return; 17389 } 17390 console.log('ð EnhancedGallerySystem.loadGallery result:', result); 17391 17392 if (!result && typeof originalLoadGallery === 'function') { 17393 console.log('â ï¸ Enhanced gallery loading failed, falling back to original function'); 17394 return originalLoadGallery(id, event, options); 17395 } 17396 if (galleryContainer) { 17397 galleryContainer.style.opacity = '1'; 17398 } 17399 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 17400 window.syncGalleryOptionsButtonVisibility(); 17401 setTimeout(window.syncGalleryOptionsButtonVisibility, 350); 17402 } 17403 } else { 17404 console.error('â EnhancedGallerySystem or its loadGallery method is not available.', { 17405 EnhancedGallerySystem: !!window.EnhancedGallerySystem, 17406 loadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'), 17407 useCdnProxy: window.useCdnProxy || false 17408 }); 17409 17410 // Fallback: Try using createGalleryContext directly 17411 console.log('ð Attempting fallback: createGalleryContext'); 17412 if (typeof createGalleryContext === 'function') { 17413 createGalleryContext(gallery, galleryContainer); 17414 if (galleryContainer) { 17415 galleryContainer.style.opacity = '1'; 17416 } 17417 return; 17418 } 17419 17420 if (galleryContainer) { 17421 galleryContainer.innerHTML = `<div class="gallery-error">Gallery system not available. EnhancedGallerySystem: ${!!window.EnhancedGallerySystem}, loadGallery: ${!!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function')}</div>`; 17422 galleryContainer.style.opacity = '1'; 17423 } 17424 return; 17425 } 17426 } catch (error) { 17427 console.error('Error loading gallery:', error); 17428 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) { 17429 console.log('enhancedLoadGallery: Ignoring stale gallery error for', gallery.id); 17430 return; 17431 } 17432 if (galleryContainer) { 17433 try { 17434 galleryContainer.innerHTML = ` 17435 <div class="gallery-error"> 17436 <h3>Error Loading Gallery</h3> 17437 <p>There was a problem loading the gallery. Please try again later.</p> 17438 <p class="error-details">${error.message}</p> 17439 </div> 17440 `; 17441 galleryContainer.style.opacity = '1'; 17442 } catch (containerError) { 17443 console.error('Error showing error message in container:', containerError); 17444 } 17445 } 17446 if (typeof originalLoadGallery === 'function') { 17447 console.log('Falling back to original loadGallery after error'); 17448 return originalLoadGallery(id, event, options); 17449 } 17450 } 17451 }; 17452 17453 /** 17454 * Initialization function to set up the gallery system 17455 */ 17456 function initializeGallerySystem() { 17457 console.log('Initializing enhanced gallery system'); 17458 17459 // Store reference to original function before replacing 17460 if (typeof window.loadGallery === 'function') { 17461 window.originalLoadGallery = window.loadGallery; 17462 } 17463 17464 // Replace the existing loadGallery function with our optimized version 17465 window.loadGallery = enhancedLoadGallery; 17466 17467 // Add this CSS for loading/error states 17468 addGallerySystemStyles(); 17469 17470 // Preload styles only at boot â scripts load when first gallery opens (with container ready) 17471 window.EnhancedGallerySystem.preloadStylesOnly().catch(error => { 17472 console.warn('Gallery styles preloading failed:', error); 17473 }); 17474 } 17475 17476 /** 17477 * Add CSS styles for loading indicators and error messages 17478 */ 17479 function addGallerySystemStyles() { 17480 const styleId = 'enhanced-gallery-system-styles'; 17481 17482 // Skip if already added 17483 if (document.getElementById(styleId)) { 17484 return; 17485 } 17486 17487 const style = document.createElement('style'); 17488 style.id = styleId; 17489 style.textContent = ` 17490 /* Loading indicator */ 17491 .gallery-loading { 17492 display: flex; 17493 flex-direction: column; 17494 justify-content: center; 17495 align-items: center; 17496 width: 100%; 17497 height: 100%; 17498 min-height: 200px; 17499 opacity: 0 !important; 17500 } 17501 17502 .gallery-loading .spinner { 17503 width: 50px; 17504 height: 50px; 17505 border: 3px solid rgba(0, 0, 0, 0.1); 17506 border-radius: 50%; 17507 border-top-color: #4682B4; 17508 animation: spin 1s ease-in-out infinite; 17509 margin-bottom: 20px; 17510 opacity: 0 !important; 17511 } 17512 17513 @keyframes spin { 17514 to { transform: rotate(360deg); } 17515 } 17516 17517 .gallery-loading .loading-text { 17518 font-family: Arial, sans-serif; 17519 font-size: 16px; 17520 color: #555; 17521 opacity: 0; 17522 } 17523 17524 /* Error message */ 17525 .gallery-error { 17526 display: flex; 17527 flex-direction: column; 17528 justify-content: center; 17529 align-items: center; 17530 width: 100%; 17531 height: 100%; 17532 min-height: 200px; 17533 padding: 30px; 17534 text-align: center; 17535 } 17536 17537 .gallery-error h3 { 17538 font-family: Arial, sans-serif; 17539 font-size: 24px; 17540 color:rgba(211, 47, 47, 0.25); 17541 margin-bottom: 20px; 17542 } 17543 17544 .gallery-error p { 17545 font-family: Arial, sans-serif; 17546 font-size: 16px; 17547 color: #555; 17548 max-width: 600px; 17549 margin-bottom: 10px; 17550 } 17551 17552 .gallery-error .error-details { 17553 font-size: 14px; 17554 color: #888; 17555 font-style: italic; 17556 } 17557 `; 17558 17559 document.head.appendChild(style); 17560 } 17561 17562 // Initialize gallery system immediately if possible 17563 initializeGallerySystem(); 17564})(); 17565 17566 17567/** 17568 * Enhanced Gallery Link Interceptor 17569 * Handles clicks on gallery links to prevent full page reloads 17570 */ 17571(function() { 17572 // Store any loaded gallery data for quick reference 17573 const GALLERY_DATA_CACHE = { 17574 initialized: false, 17575 galleries: [], 17576 lastUpdate: 0 17577 }; 17578 17579 // Initialize when DOM is ready to catch early clicks 17580 document.addEventListener('DOMContentLoaded', initGalleryLinkInterceptor); 17581 17582 // Also initialize when window loads to ensure complete gallery data 17583 window.addEventListener('load', function() { 17584 // Use a small delay to ensure galleries have fully initialized 17585 setTimeout(initGalleryLinkInterceptor, 200); 17586 }); 17587 17588 // Additional init on first user interaction 17589 document.addEventListener('click', function initOnFirstClick() { 17590 initGalleryLinkInterceptor(); 17591 // Remove this listener after first click 17592 document.removeEventListener('click', initOnFirstClick); 17593 }, { once: true }); 17594 17595 // Main initialization function 17596 function initGalleryLinkInterceptor() { 17597 // Prevent multiple initializations 17598 if (window._galleryLinkInterceptorInitialized) { 17599 // If already initialized, just refresh the gallery data 17600 refreshGalleryCache(); 17601 return; 17602 } 17603 17604 window._galleryLinkInterceptorInitialized = true; 17605 console.log('Initializing enhanced gallery link interceptor'); 17606 17607 // Update our cache with current galleries data 17608 refreshGalleryCache(); 17609 17610 // Get primary containers that might contain links 17611 const galleryContainer = document.querySelector('.gallery-container'); 17612 const mainContainer = document.querySelector('.container') || document.body; 17613 17614 if (!galleryContainer && !mainContainer) { 17615 console.warn('No gallery containers found, link interceptor partially initialized'); 17616 } 17617 17618 // Add global click handler to catch all link clicks 17619 document.addEventListener('click', handleGlobalLinkClick, true); 17620 17621 // Try to intercept links from iframes - critical for gallery content 17622 interceptAllIframeLinks(); 17623 17624 // Set up a mutation observer to handle dynamically added content 17625 setupMutationObserver(); 17626 17627 // Log initialization with site details 17628 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 17629 console.log(`Gallery link interceptor initialized. Preview mode: ${isPreviewMode}`); 17630 17631 // Create global helper for debugging 17632 window.debugGalleryLinks = function() { 17633 return { 17634 inPreviewMode: isPreviewMode, 17635 interceptorInitialized: window._galleryLinkInterceptorInitialized, 17636 cachedGalleries: GALLERY_DATA_CACHE.galleries.length, 17637 urlPath: window.location.pathname, 17638 siteGuid: getSiteGuidFromPath() 17639 }; 17640 }; 17641 } 17642 17643 // Update the gallery cache when gallery data changes 17644function refreshGalleryCache() { 17645 // Initialize GALLERY_DATA_CACHE if it doesn't exist 17646 if (!window.GALLERY_DATA_CACHE) { 17647 window.GALLERY_DATA_CACHE = { 17648 initialized: false, 17649 galleries: [], 17650 lastUpdate: 0 17651 }; 17652 } 17653 17654 // Check for global galleries variable first 17655 if (typeof galleries !== 'undefined' && Array.isArray(galleries)) { 17656 // Use the global galleries array 17657 window.GALLERY_DATA_CACHE.galleries = galleries; 17658 window.GALLERY_DATA_CACHE.initialized = true; 17659 window.GALLERY_DATA_CACHE.lastUpdate = Date.now(); 17660 window.galleries = galleries; 17661 17662 console.log(`Gallery cache refreshed with ${galleries.length} galleries from global variable`); 17663 return; 17664 } 17665 17666 // Debug log 17667 console.log(`Gallery cache refreshed with ${GALLERY_DATA_CACHE.galleries.length} galleries`); 17668 } 17669 17670 // Global click handler - catches all link clicks 17671 function handleGlobalLinkClick(e) {
17672 // Find if a link was clicked 17673 const link = e.target.closest('a'); 17674 if (!link || !link.href) return; 17675 17676 // Skip if the link uses special targets or modifiers 17677 if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return; 17678 17679 // Skip for hash links or javascript: links 17680 if (link.href.includes('#') || link.href.startsWith('javascript:')) return; 17681 17682 try { 17683 // Parse the URL to work with it 17684 const url = new URL(link.href); 17685 17686 // Only intercept links to the same domain 17687 if (url.hostname !== window.location.hostname) return; 17688 17689 // Skip API and resource URLs 17690 if (url.pathname.includes('/api/') || isResourceUrl(url.pathname)) return; 17691 17692 console.log('Link intercepted:', url.pathname); 17693 17694 // For preview URLs, we need to handle the path specially 17695 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 17696 const siteGuid = getSiteGuidFromPath(); 17697 17698 console.log('Link interceptor - isPreviewMode:', isPreviewMode, 'siteGuid:', siteGuid); 17699 17700 // Site Graph card links carry data-sg-target-id â use SPA navigation (not preview iframe fallback) 17701 if (link.classList.contains('sg-card-link') && link.closest('.site-graph-element') && link.dataset.sgTargetId) { 17702 let galleriesData = (typeof galleries !== 'undefined' && Array.isArray(galleries)) 17703 ? galleries 17704 : window.galleries; 17705 const sgItem = Array.isArray(galleriesData) 17706 ? findGalleryById(galleriesData, link.dataset.sgTargetId) 17707 : null; 17708 if (sgItem) { 17709 e.preventDefault(); 17710 e.stopPropagation(); 17711 if (sgItem.isPage && window.loadPage) { 17712 window.loadPage(sgItem.id); 17713 } else if (window.loadGallery) { 17714 window.loadGallery(sgItem.id); 17715 } 17716 let targetUrl = url.href; 17717 if (isPreviewMode && siteGuid) { 17718 const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 17719 url.pathname : 17720 `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; 17721 const newUrl = new URL(url); 17722 newUrl.pathname = pathname; 17723 targetUrl = newUrl.href; 17724 } 17725 window.history.pushState( 17726 { galleryId: sgItem.id }, 17727 sgItem.title || document.title, 17728 targetUrl 17729 ); 17730 document.title = formatGalleryDocumentTitle(sgItem.title); 17731 window.scrollTo(0, 0); 17732 return; 17733 } 17734 } 17735 17736 // Check if this is internal navigation first 17737 let matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid); 17738 17739 console.log('Link interceptor - matchingItem found:', !!matchingItem, matchingItem ? matchingItem.title : 'none'); 17740 17741 if (matchingItem) { 17742 // Prevent default navigation (full page reload) 17743 e.preventDefault(); 17744 e.stopPropagation(); 17745 17746 console.log('Found matching gallery for link:', matchingItem.title); 17747 17748 const nestedLinkModeActive = typeof window.isNestedGalleryLinkModeEnabled === 'function' 17749 ? window.isNestedGalleryLinkModeEnabled() 17750 : !!(window.neonGalleryConfig?.useLinks && window.neonGalleryConfig?.openMultipleLightboxes); 17751 const isGridGalleryLink = !!(link.closest('.masonry-item') || 17752 (link.closest('.gallery-wrapper, #neon-gallery-container, .gallery-direct-content, .masonry') && 17753 link.querySelector('img') && 17754 !link.closest('#neon-lightbox, .neon-lightbox'))); 17755 17756 if (!matchingItem.isPage && 17757 nestedLinkModeActive && 17758 isGridGalleryLink && 17759 typeof window.openNestedGalleryLightbox === 'function') { 17760 void window.openNestedGalleryLightbox(matchingItem, window.activeGalleryId).then((opened) => { 17761 if (!opened) { 17762 console.warn('Nested gallery lightbox failed â staying on parent gallery (no loadGallery fallback)'); 17763 } 17764 }); 17765 return; 17766 } 17767 17768 // Use the existing navigation functions 17769 if (matchingItem.isPage && window.loadPage) { 17770 window.loadPage(matchingItem.id); 17771 } else if (window.loadGallery) { 17772 window.loadGallery(matchingItem.id); 17773 } 17774 17775 // Construct proper URL for preview mode 17776 let targetUrl = url.href; 17777 if (isPreviewMode && siteGuid) { 17778 // Ensure we keep the GUID in the URL path 17779 const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 17780 url.pathname : 17781 `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; 17782 17783 const newUrl = new URL(url); 17784 newUrl.pathname = pathname; 17785 targetUrl = newUrl.href; 17786 } 17787 17788 // Update URL without reload 17789 window.history.pushState( 17790 { galleryId: matchingItem.id }, 17791 matchingItem.title || document.title, 17792 targetUrl 17793 ); 17794 17795 // Update page title 17796 document.title = formatGalleryDocumentTitle(matchingItem.title); 17797 17798 // Scroll to top 17799 window.scrollTo(0, 0); 17800 17801 return; 17802 } 17803 // Special case for preview mode - even if no matching gallery found 17804 else if (isPreviewMode && siteGuid) { 17805 // We're in preview mode with a GUID - intercept the navigation 17806 // to maintain the GUID in the URL. Only do this when #galleryFrame 17807 // can load content; otherwise allow SPA onclick handlers to run. 17808 17809 console.log('No direct match but in preview mode - handling navigation'); 17810 17811 const frame = document.getElementById('galleryFrame'); 17812 17813 if (!frame) { 17814 console.warn('Gallery frame not found - allowing default/SPA handlers'); 17815 } else { 17816 // Prevent default navigation 17817 e.preventDefault(); 17818 e.stopPropagation(); 17819 17820 // Construct a URL that preserves the GUID 17821 let newPathname = url.pathname; 17822 if (!newPathname.startsWith(`/${siteGuid}/`)) { 17823 // Add the GUID to the path if not already there 17824 newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`; 17825 } 17826 17827 // Create the preview-friendly URL for browser history 17828 const browserUrl = new URL(window.location.origin); 17829 browserUrl.pathname = newPathname; 17830 17831 console.log('Constructing preview URL with GUID:', browserUrl.href); 17832 17833 // IMPORTANT: For gallery navigation, we need to create a special URL 17834 // that tells the gallery system to load the content directly 17835 console.log('Loading preview path in iframe:', newPathname); 17836 17837 // First update browser URL to show the correct path 17838 window.history.pushState(
17839 { customPath: newPathname }, 17840 document.title, 17841 browserUrl.href 17842 ); 17843 17844 // Create direct path to the gallery content 17845 const cleanPath = newPathname.replace(`/${siteGuid}/`, '/'); 17846 17847 // For direct gallery paths, we need to load the gallery with special parameters 17848 // This tells the gallery system to load content from this path 17849 const galleryUrl = `https://storage.neonsky.app/sites:site_${siteGuid}.json`; 17850 17851 // Two approaches to loading the content: 17852 17853 // 1. Use the gallery wrapper approach 17854 const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 17855 `?siteId=${siteGuid}` + 17856 `&path=${encodeURIComponent(cleanPath)}` + 17857 `&directPath=true` + 17858 `&t=${Date.now()}`; // Cache buster 17859 17860 console.log('Loading gallery via wrapper URL:', wrapperUrl); 17861 frame.src = wrapperUrl; 17862 17863 // Clear any active gallery ID since we're navigating directly 17864 if (window.activeGalleryId) { 17865 console.log('Clearing active gallery ID during direct navigation'); 17866 window.activeGalleryId = null; 17867 } 17868 17869 return; // Critical: prevent further navigation 17870 } 17871 } 17872 17873 console.log('No matching gallery or special handling - allow normal navigation'); 17874 } catch (error) { 17875 console.error('Error processing link click:', error); 17876 } 17877 } 17878 17879 // Handle all iframes on the page 17880 function interceptAllIframeLinks() { 17881 // Look for any iframe that might contain gallery content 17882 const iframes = document.querySelectorAll('iframe'); 17883 17884 iframes.forEach(iframe => { 17885 if (iframe.id === 'galleryFrame' || iframe.classList.contains('gallery-iframe')) { 17886 // Primary gallery iframe - high priority 17887 setupIframeIntercept(iframe, true); 17888 } else { 17889 // Any other iframe - still try to intercept 17890 setupIframeIntercept(iframe, false); 17891 } 17892 }); 17893 17894 console.log(`Attempted to intercept links in ${iframes.length} iframes`); 17895 } 17896 17897 // Setup link interception for a specific iframe 17898 function setupIframeIntercept(iframe, isPrimary) { 17899 try { 17900 // Skip if iframe is not fully loaded yet 17901 if (!iframe.contentWindow || !iframe.contentDocument) { 17902 // Add load event to try again when loaded 17903 iframe.addEventListener('load', function() { 17904 setupIframeIntercept(iframe, isPrimary); 17905 }); 17906 return; 17907 } 17908 17909 // Try to access the iframe content - might fail due to same-origin policy 17910 try { 17911 const iframeDoc = iframe.contentDocument; 17912 const iframeWin = iframe.contentWindow; 17913 17914 // Add event listener for all link clicks in the iframe 17915 iframeDoc.addEventListener('click', function(e) { 17916 const link = e.target.closest('a'); 17917 if (!link || !link.href) return; 17918 17919 // Skip special cases 17920 if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return; 17921 if (link.href.includes('#') || link.href.startsWith('javascript:')) return; 17922 17923 try { 17924 const url = new URL(link.href); 17925 17926 // Check if same domain or relative URL 17927 const isSameDomain = url.hostname === window.location.hostname || 17928 url.hostname === iframe.src.hostname || 17929 !url.hostname; 17930 17931 if (isSameDomain) { 17932 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 17933 const siteGuid = getSiteGuidFromPath(); 17934 17935 // First try to find a matching gallery/page 17936 const matchingItem = parent.findGalleryByPath ? 17937 parent.findGalleryByPath(url.pathname, isPreviewMode, siteGuid) : 17938 findGalleryByPath(url.pathname, isPreviewMode, siteGuid); 17939 17940 if (matchingItem) { 17941 // We found a match - prevent default and navigate 17942 e.preventDefault(); 17943 e.stopPropagation(); 17944 17945 console.log('Iframe: Found matching content for link:', matchingItem.title); 17946 17947 // Access parent window functions 17948 if (matchingItem.isPage && parent.loadPage) { 17949 parent.loadPage(matchingItem.id); 17950 } else if (parent.loadGallery) { 17951 parent.loadGallery(matchingItem.id); 17952 } 17953 17954 // Update URL in parent window 17955 let targetUrl = url.href; 17956 if (isPreviewMode && siteGuid) { 17957 // Ensure GUID is preserved in URL 17958 const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 17959 url.pathname : 17960 `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; 17961 17962 const newUrl = new URL(window.location.origin + pathname); 17963 targetUrl = newUrl.href; 17964 } 17965 17966 // Update browser URL 17967 parent.history.pushState( 17968 { galleryId: matchingItem.id }, 17969 matchingItem.title || document.title, 17970 targetUrl 17971 ); 17972 17973 // Update parent window title 17974 parent.document.title = formatGalleryDocumentTitle(matchingItem.title, parent); 17975 17976 // Scroll parent to top 17977 parent.scrollTo(0, 0); 17978 return; 17979 } 17980 // Special handling for preview mode - critical fix here! 17981 else if (isPreviewMode && siteGuid) { 17982 // Special handling for preview URLs 17983 e.preventDefault(); 17984 e.stopPropagation(); 17985 17986 console.log('Iframe: No direct match but in preview mode - handling navigation'); 17987 17988 // Construct a URL that preserves the GUID 17989 let newPathname = url.pathname; 17990 if (!newPathname.startsWith(`/${siteGuid}/`)) { 17991 newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`; 17992 } 17993 17994 // Create a URL for the browser history 17995 const browserUrl = new URL(window.location.origin); 17996 browserUrl.pathname = newPathname; 17997 17998 console.log('Iframe: Constructing preview URL with GUID:', browserUrl.href); 17999 18000 // Update browser URL in parent 18001 parent.history.pushState(
18002 { customPath: newPathname }, 18003 document.title, 18004 browserUrl.href 18005 ); 18006 18007 // Handle the navigation in the parent window 18008 if (isPrimary && parent.document.getElementById('galleryFrame')) { 18009 const galleryFrame = parent.document.getElementById('galleryFrame'); 18010 18011 // Create clean path for the gallery system 18012 const cleanPath = newPathname.replace(`/${siteGuid}/`, '/'); 18013 18014 // Create wrapper URL for gallery content 18015 const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 18016 `?siteId=${siteGuid}` + 18017 `&path=${encodeURIComponent(cleanPath)}` + 18018 `&directPath=true` + 18019 `&t=${Date.now()}`; // Cache buster 18020 18021 console.log('Iframe: Loading gallery via wrapper URL:', wrapperUrl); 18022 galleryFrame.src = wrapperUrl; 18023 18024 // Clear any active gallery ID since we're navigating directly 18025 if (parent.activeGalleryId) { 18026 console.log('Iframe: Clearing active gallery ID during direct navigation'); 18027 parent.activeGalleryId = null; 18028 } 18029 } else { 18030 // Fallback - navigate normally in case we can't handle it 18031 console.log('Iframe: Gallery frame not found or not primary, navigating normally'); 18032 window.location.href = browserUrl.href; 18033 } 18034 return; 18035 } 18036 } 18037 } catch (iframeError) { 18038 console.error('Error handling iframe link:', iframeError); 18039 } 18040 }); 18041 18042 if (isPrimary) { 18043 console.log('Successfully intercepted links in primary gallery iframe'); 18044 } 18045 } catch (sameOriginError) { 18046 // This is expected for cross-origin iframes 18047 if (isPrimary) { 18048 console.warn('Cannot access iframe content due to same-origin policy. Using fallback method.'); 18049 18050 // Apply a fallback method - add postMessage listener 18051 setupPostMessageListener(iframe); 18052 } 18053 } 18054 } catch (error) { 18055 console.warn('Error setting up iframe link interception:', error); 18056 } 18057 } 18058 18059 // Fallback: use postMessage for cross-origin iframes 18060 function setupPostMessageListener(iframe) { 18061 // Listen for messages from the iframe 18062 window.addEventListener('message', function(event) { 18063 try { 18064 // Verify the message is from our iframe 18065 if (event.source !== iframe.contentWindow) return; 18066 18067 const data = event.data; 18068 18069 // Check if this is a link click event 18070 if (data && data.type === 'linkClick' && data.href) { 18071 console.log('Received link click message from iframe:', data.href); 18072 18073 // Create a URL object to work with the link 18074 const url = new URL(data.href); 18075 18076 // Check if it's a local navigation 18077 if (url.hostname === window.location.hostname) { 18078 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 18079 const siteGuid = getSiteGuidFromPath(); 18080 18081 // Try to find a matching gallery 18082 const matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid); 18083 18084 if (matchingItem) { 18085 console.log('Found matching gallery for postMessage link:', matchingItem.title); 18086 18087 // Navigate using the appropriate function 18088 if (matchingItem.isPage && window.loadPage) { 18089 window.loadPage(matchingItem.id); 18090 } else if (window.loadGallery) { 18091 window.loadGallery(matchingItem.id); 18092 } 18093 18094 // Update URL without reload 18095 let targetUrl = url.href; 18096 if (isPreviewMode && siteGuid) { 18097 // Ensure GUID is preserved 18098 const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 18099 url.pathname : 18100 `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; 18101 18102 const newUrl = new URL(window.location.origin + pathname); 18103 targetUrl = newUrl.href; 18104 } 18105 18106 window.history.pushState( 18107 { galleryId: matchingItem.id }, 18108 matchingItem.title || document.title, 18109 targetUrl 18110 ); 18111 18112 // Update page title 18113 document.title = formatGalleryDocumentTitle(matchingItem.title); 18114 18115 return; 18116 } 18117 // Special handling for preview mode - Fixed! 18118 else if (isPreviewMode && siteGuid) { 18119 console.log('No direct match but in preview mode - handling navigation via postMessage'); 18120 18121 // Construct URL that preserves the GUID 18122 let newPathname = url.pathname; 18123 if (!newPathname.startsWith(`/${siteGuid}/`)) { 18124 newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`; 18125 } 18126 18127 // Create browser URL 18128 const browserUrl = new URL(window.location.origin); 18129 browserUrl.pathname = newPathname; 18130 18131 // Update browser URL 18132 window.history.pushState(
18133 { customPath: newPathname }, 18134 document.title, 18135 browserUrl.href 18136 ); 18137 18138 // Get the main gallery frame - NOT the iframe that sent the message 18139 const galleryFrame = document.getElementById('galleryFrame'); 18140 if (galleryFrame) { 18141 // Create clean path without GUID 18142 const cleanPath = newPathname.replace(`/${siteGuid}/`, '/'); 18143 18144 // Create wrapper URL 18145 const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 18146 `?siteId=${siteGuid}` + 18147 `&path=${encodeURIComponent(cleanPath)}` + 18148 `&directPath=true` + 18149 `&t=${Date.now()}`; // Cache buster 18150 18151 console.log('Loading gallery via wrapper URL (postMessage):', wrapperUrl); 18152 galleryFrame.src = wrapperUrl; 18153 18154 // Clear any active gallery ID 18155 if (window.activeGalleryId) { 18156 window.activeGalleryId = null; 18157 } 18158 18159 return; 18160 } else { 18161 console.warn('Gallery frame not found - cannot load content via postMessage'); 18162 } 18163 } 18164 } 18165 } 18166 } catch (error) { 18167 console.error('Error processing postMessage from iframe:', error); 18168 } 18169 }); 18170 18171 // Inject a script into the iframe if possible to send messages back 18172 try { 18173 // We might be able to inject a script tag even in cross-origin scenarios 18174 // if the iframe is not yet loaded or using document.write 18175 iframe.addEventListener('load', function() { 18176 try { 18177 const script = iframe.contentDocument.createElement('script'); 18178 script.textContent = ` 18179 // Link interception script injected from parent 18180 document.addEventListener('click', function(e) { 18181 const link = e.target.closest('a'); 18182 if (link && link.href && !link.target && !e.ctrlKey && !e.metaKey && !e.shiftKey) { 18183 // Send message to parent window 18184 window.parent.postMessage({ 18185 type: 'linkClick', 18186 href: link.href, 18187 pathname: link.pathname 18188 }, '*'); 18189 18190 // Prevent default navigation 18191 e.preventDefault(); 18192 } 18193 }); 18194 console.log('Gallery link interceptor injected from parent'); 18195 `; 18196 iframe.contentDocument.head.appendChild(script); 18197 } catch (e) { 18198 // This will likely fail for cross-origin iframes 18199 console.log('Could not inject script into iframe - expected for cross-origin frames'); 18200 } 18201 }); 18202 } catch (error) { 18203 console.warn('Error trying to inject script into iframe:', error); 18204 } 18205 } 18206 18207 // Observer for new content/iframes 18208 function setupMutationObserver() { 18209 if (!window.MutationObserver) return; 18210 18211 const observer = new MutationObserver(function(mutations) { 18212 let newIframesFound = false; 18213 18214 mutations.forEach(function(mutation) { 18215 if (mutation.type === 'childList' && mutation.addedNodes.length) { 18216 // Check if any new iframes were added 18217 mutation.addedNodes.forEach(function(node) { 18218 // Check if the node is an iframe or contains iframes 18219 if (node.nodeName === 'IFRAME') { 18220 newIframesFound = true; 18221 setupIframeIntercept(node, node.id === 'galleryFrame'); 18222 } else if (node.querySelectorAll) { 18223 const iframes = node.querySelectorAll('iframe'); 18224 if (iframes.length > 0) { 18225 newIframesFound = true; 18226 iframes.forEach(function(iframe) { 18227 setupIframeIntercept(iframe, iframe.id === 'galleryFrame'); 18228 }); 18229 } 18230 } 18231 }); 18232 } 18233 }); 18234 18235 // If new iframes were found, update our cache 18236 if (newIframesFound) { 18237 refreshGalleryCache(); 18238 } 18239 }); 18240 18241 // Observe the entire document 18242 observer.observe(document.body, { 18243 childList: true, 18244 subtree: true 18245 }); 18246 } 18247 18248 // Helper to check if a URL is a resource rather than a page 18249 function isResourceUrl(path) { 18250 const resourceExtensions = [ 18251 '.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', 18252 '.css', '.js', '.pdf', '.ico', '.ttf', '.woff', '.woff2' 18253 ]; 18254 return resourceExtensions.some(ext => path.toLowerCase().endsWith(ext)); 18255 } 18256 18257 // Get the site GUID from the path in preview mode 18258 function getSiteGuidFromPath() { 18259 if (window.location.hostname === 'preview.neonsky.app') { 18260 const pathParts = window.location.pathname.split('/').filter(
18260Boolean); 18261 if (pathParts.length > 0) { 18262 return pathParts[0]; 18263 } 18264 } 18265 return null; 18266 } 18267 18268 18269/** 18270 * Enhanced gallery path matching with improved accuracy: 18271 * 18272 * This function finds the appropriate gallery based on URL path with better 18273 * prioritization and more precise matching criteria. 18274 * 18275 * @param {string} path - The URL path to match 18276 * @param {boolean} isPreviewMode - Whether we're in preview mode 18277 * @param {string} siteGuid - The site GUID (for preview mode) 18278 * @returns {Object|null} The matching gallery or null if none found 18279 */ 18280function findGalleryByPath(path, isPreviewMode, siteGuid) { 18281 // Ensure path is a string 18282 if (typeof path !== 'string') { 18283 console.warn('Invalid path provided to findGalleryByPath:', path); 18284 return null; 18285 } 18286 18287 // Determine which galleries array to use - check global variable first 18288 let galleriesData; 18289 let galleryCount = 0; 18290 18291 if (typeof galleries !== 'undefined' && Array.isArray(galleries)) { 18292 galleriesData = galleries; 18293 galleryCount = galleries.length; 18294 } else if (typeof window.galleries !== 'undefined' && Array.isArray(window.galleries)) { 18295 galleriesData = window.galleries; 18296 galleryCount = window.galleries.length; 18297 } else if (window.GALLERY_DATA_CACHE && 18298 window.GALLERY_DATA_CACHE.initialized && 18299 Array.isArray(window.GALLERY_DATA_CACHE.galleries)) { 18300 galleriesData = window.GALLERY_DATA_CACHE.galleries; 18301 galleryCount = window.GALLERY_DATA_CACHE.galleries.length; 18302 } else { 18303 console.warn('No galleries array available'); 18304 return null; 18305 } 18306 18307 console.log(`Searching for match among ${galleryCount} galleries`); 18308 18309 // Strip leading/trailing slashes without regex â template-literal embedding mangles / escapes 18310 // (e.g. /^/|/$/g becomes /^/|/$/g â 0), which left "/" unnormalized and broke home matching. 18311 function stripEdgeSlashes(value) { 18312 return String(value || '').split('/').filter(Boolean).join('/'); 18313 } 18314 18315 // Normalize the path for comparison (handle trailing/leading slashes) 18316 let normalizedPath = stripEdgeSlashes(path).toLowerCase(); 18317 const originalNormalizedPath = normalizedPath; // Keep original for logging 18318 18319 // If this is a preview URL, we need to handle the path differently 18320 if (isPreviewMode && siteGuid) { 18321 // Check if the path starts with the GUID 18322 if (normalizedPath.startsWith(siteGuid.toLowerCase() + '/')) { 18323 // Remove the GUID prefix 18324 normalizedPath = normalizedPath.substring(siteGuid.length + 1); 18325 } else if (normalizedPath === siteGuid.toLowerCase()) { 18326 // This is just the GUID with no additional path 18327 normalizedPath = ''; 18328 } 18329 } 18330 18331 // Create a dash-normalized version (all sequences of dashes become single dashes) 18332 // This handles both double-dash patterns and any other dash inconsistencies 18333 const normalizedPathWithSingleDashes = normalizedPath.replace(/-+/g, '-'); 18334 18335 console.log(`Normalized path: "${normalizedPath}" (dash-normalized: "${normalizedPathWithSingleDashes}")`); 18336 18337 // For empty paths, try to find the homepage 18338 if (!normalizedPath) { 18339 // Look for a gallery marked as home page 18340 const homePage = galleriesData.find(g => g.isHomePage === true); 18341 if (homePage) { 18342 console.log('Empty path - returning homepage:', homePage.title); 18343 return homePage; 18344 } 18345 } 18346 18347 // Array to store potential matches with their confidence levels 18348 const potentialMatches = []; 18349 18350 // Process each gallery to find potential matches with confidence levels 18351 galleriesData.forEach(gallery => { 18352 // Skip invalid galleries 18353 if (!gallery || !gallery.id) return; 18354 18355 // Store matching details for debugging 18356 const matchInfo = { 18357 gallery, 18358 confidence: 0, 18359 matchType: null, 18360 usedNormalizedDashes: false 18361 }; 18362 18363 // 1. EXACT SLUG MATCH (highest confidence) 18364 if (gallery.slug) { 18365 const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase(); 18366 18367 // Try original path 18368 if (gallerySlug === normalizedPath) { 18369 matchInfo.confidence = 100; // Max confidence 18370 matchInfo.matchType = 'exact_slug'; 18371 potentialMatches.push({...matchInfo}); 18372 return; // Continue to next gallery 18373 } 18374 18375 // Try dash-normalized path 18376 const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-'); 18377 if (normalizedPathWithSingleDashes === gallerySlugWithSingleDashes) { 18378 matchInfo.confidence = 95; // Very high confidence 18379 matchInfo.matchType = 'exact_slug_normalized_dashes'; 18380 matchInfo.usedNormalizedDashes = true; 18381 potentialMatches.push({...matchInfo}); 18382 return; // Continue to next gallery 18383 } 18384 } 18385 18386 // 2. EXACT URL PATH MATCH (very high confidence) 18387 if (gallery.url) { 18388 let galleryPath = gallery.url.toLowerCase(); 18389 18390 if (galleryPath.startsWith('http')) { 18391 try { 18392 const urlObj = new URL(galleryPath); 18393 galleryPath = urlObj.pathname; 18394 } catch (e) { 18395 // If URL parsing fails, use the string as is 18396 } 18397 } 18398 18399 galleryPath = stripEdgeSlashes(galleryPath); 18400 18401 // Try original path 18402 if (galleryPath === normalizedPath) { 18403 matchInfo.confidence = 90; // Very high confidence 18404 matchInfo.matchType = 'exact_url_path'; 18405 potentialMatches.push({...matchInfo}); 18406 return; // Continue to next gallery 18407 } 18408 18409 // Try dash-normalized path 18410 const galleryPathWithSingleDashes = galleryPath.replace(/-+/g, '-'); 18411 if (normalizedPathWithSingleDashes === galleryPathWithSingleDashes) { 18412 matchInfo.confidence = 85; // High confidence 18413 matchInfo.matchType = 'exact_url_path_normalized_dashes'; 18414 matchInfo.usedNormalizedDashes = true; 18415 potentialMatches.push({...matchInfo}); 18416 return; // Continue to next gallery 18417 } 18418 } 18419 18420 // 3. TITLE SLUG MATCH (high confidence) 18421 if (gallery.title) { 18422 const titleSlug = gallery.title.toLowerCase() 18423 .replace(/s+/g, '-') 18424 .replace(/[^w-]+/g, '') 18425 .replace(/--+/g, '-') 18426 .trim(); 18427 18428 // Try original path 18429 if (titleSlug === normalizedPath) { 18430 matchInfo.confidence = 80; // High confidence 18431 matchInfo.matchType = 'title_slug_match'; 18432 potentialMatches.push({...matchInfo}); 18433 return; // Continue to next gallery 18434 } 18435 18436 // Try dash-normalized path 18437 const titleSlugWithSingleDashes = titleSlug.replace(/-+/g, '-'); 18438 if (normalizedPathWithSingleDashes === titleSlugWithSingleDashes) { 18439 matchInfo.confidence = 75; // Good confidence 18440 matchInfo.matchType = 'title_slug_match_normalized_dashes'; 18441 matchInfo.usedNormalizedDashes = true; 18442 potentialMatches.push({...matchInfo}); 18443 return; // Continue to next gallery 18444 } 18445 } 18446 18447 // 4. PARTIAL SLUG MATCH - BUT WITH MUCH STRICTER CRITERIA 18448 if (gallery.slug && normalizedPath.length > 5) { 18449 const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase(); 18450 const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-'); 18451 18452 // Only if paths are substantial (to avoid false positives) 18453 if (normalizedPath.length >= 5 && gallerySlug.length >= 5) { 18454 // Try matching significant portions with dash normalization 18455 18456 // If the normalized paths contain each other with substantial overlap 18457 if (normalizedPathWithSingleDashes.includes(gallerySlugWithSingleDashes) || 18458 gallerySlugWithSingleDashes.includes(normalizedPathWithSingleDashes)) { 18459 18460 // Calculate overlap ratio based on the shorter string 18461 const overlapRatio = Math.min(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length) / 18462 Math.max(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length); 18463 18464 // Only consider matches with substantial overlap (over 70%) 18465 if (overlapRatio >= 0.7) { 18466 const score = Math.round(60 + (overlapRatio * 20)); // 60-80 based on overlap 18467 18468 matchInfo.confidence = score; 18469 matchInfo.matchType = 'partial_match_normalized'; 18470 matchInfo.usedNormalizedDashes = true; 18471 matchInfo.overlapRatio = overlapRatio; 18472 potentialMatches.push({...matchInfo}); 18473 } 18474 } 18475 } 18476 } 18477 }); 18478 18479 // No potential matches found 18480 if (potentialMatches.length === 0) { 18481 console.log('No gallery match found for path:', path); 18482 return null; 18483 } 18484 18485 // Sort potential matches by confidence (highest first) 18486 potentialMatches.sort((a, b) => b.confidence - a.confidence); 18487 18488 // Get the best match 18489 const bestMatch = potentialMatches[0]; 18490 18491 // Log detailed debugging info about the match 18492 console.log(`Best match for "${normalizedPath}":`, { 18493 title: bestMatch.gallery.title, 18494 id: bestMatch.gallery.id, 18495 matchType: bestMatch.matchType, 18496 confidence: bestMatch.confidence, 18497 usedNormalizedDashes: bestMatch.usedNormalizedDashes, 18498 totalMatches: potentialMatches.length 18499 }); 18500 18501 // If we have multiple close matches, log them for debugging 18502 if (potentialMatches.length > 1 && 18503 potentialMatches[1].confidence >= bestMatch.confidence - 10) { 18504 console.log('Close alternative matches:', potentialMatches.slice(1, 3).map(m => ({ 18505 title: m.gallery.title, 18506 id: m.gallery.id, 18507 matchType: m.matchType, 18508 confidence: m.confidence 18509 }
18509))); 18510 } 18511 18512 // Return the best matching gallery 18513 return bestMatch.gallery; 18514} 18515 18516 // Make helpful functions available globally 18517 window.findGalleryByPath = findGalleryByPath; 18518 window.findGalleryById = findGalleryById; 18519 window.refreshGalleryCache = refreshGalleryCache; 18520 window.getSiteGuidFromPath = getSiteGuidFromPath; 18521 if (typeof getSiteGalleriesArray === 'function') { 18522 window.getSiteGalleriesArray = getSiteGalleriesArray; 18523 } 18524})(); 18525 18526 18527 18528 </script>
18528 18529 </div> 18530 18531<!-- Sidebar element form for adding new elements --> 18532<div class="sidebar-element-form pe-edit-shell" id="sidebarElementForm" style="display: none;" data-nosnippet> 18533<p class="pe-edit-shell__title">Add sidebar element</p> 18534<p class="pe-hint">Select the type of element to add:</p> 18535 18536<div class="radio-group"> 18537 <label> 18538 <input type="radio" name="sidebarElementType" value="text" checked> Text Block 18539 </label> 18540 <label> 18541 <input type="radio" name="sidebarElementType" value="image"> Image 18542 </label> 18543 <label> 18544 <input type="radio" name="sidebarElementType" value="social"> Social Icons 18545 </label> 18546 <label> 18547 <input type="radio" name="sidebarElementType" value="spacer"> Spacer 18548 </label> 18549</div> 18550 18551<div class="pe-footer-actions"> 18552<button type="button" class="pe-btn pe-btn--primary" onclick="addSidebarElement()">Add element</button> 18553<button type="button" class="pe-btn" onclick="toggleSidebarElementForm()">Cancel</button> 18554</div> 18555</div> 18556 18557 <div class="add-form pe-edit-shell" id="addForm" data-nosnippet> 18558 <p class="pe-edit-shell__title">Add to menu</p> 18559 <input type="text" id="galleryTitle" placeholder="Gallery Title"> 18560 <div class="radio-group"> 18561 <label> 18562 <input type="radio" name="galleryType" value="integrated" checked> Gallery 18563 </label> 18564 <label> 18565 <input type="radio" name="galleryType" value="page"> Page 18566 </label> 18567 <label> 18568 <input type="radio" name="galleryType" value="folder"> Folder 18569 </label> 18570 <label> 18571 <input type="radio" name="galleryType" value="spacer"> Spacer 18572 </label> 18573 <label> 18574 <input type="radio" name="galleryType" value="external"> External URL 18575 </label> 18576</div> 18577 <div id="urlInputContainer"> 18578 <input type="text" id="galleryUrl" placeholder="Gallery URL"> 18579 </div> 18580 <div id="galleryParentContainer" style="display: none;" aria-hidden="true"> 18581 <label for="galleryParent">Parent Menu:</label> 18582 <select id="galleryParent"> 18583 <option value="">None (Top Level)</option> 18584 </select> 18585 </div> 18586 <div class="pe-footer-actions"> 18587 <button type="button" class="pe-btn pe-btn--primary" onclick="addGallery()">Save</button> 18588 </div> 18589
18589<script> 18590 18591 18592 // Add event listeners to the radio buttons 18593 document.addEventListener('DOMContentLoaded', () => { 18594 const radioButtons = document.querySelectorAll('input[name="galleryType"]'); 18595 radioButtons.forEach(radio => { 18596 radio.addEventListener('change', function() { 18597 const urlContainer = document.getElementById('urlInputContainer'); 18598 if (this.value === 'external') { 18599 urlContainer.style.display = 'block'; 18600 } else { 18601 urlContainer.style.display = 'none'; 18602 } 18603 }); 18604 }); 18605 }); 18606 </script>
18606 18607 </div> 18608 18609<div class="metadata-editor pe-edit-shell" id="metadataEditor" style="display: none; max-height: 90vh; overflow-y: auto;" data-nosnippet> 18610 <h3 class="pe-edit-shell__title">Site Metadata</h3> 18611 18612 <div class="form-group"> 18613 <label for="metaTitle">Page Title (Search Results & Browsers)</label> 18614 <input type="text" id="metaTitle" class="form-control" placeholder="Enter site title for search results"> 18615 <small>Recommended length: 50-60 characters</small> 18616 </div> 18617 18618 <div class="form-group"> 18619 <label for="metaDescription">Meta Description</label> 18620 <textarea id="metaDescription" class="form-control" rows="3" placeholder="Enter site description for search results"></textarea> 18621 <small>Recommended length: 150-160 characters</small> 18622 </div> 18623 18624 <div class="form-group"> 18625 <label for="ogImageUrl">Social preview image (Open Graph / X / iMessage)</label> 18626 <input type="hidden" id="ogImageUrl" class="form-control" value=""> 18627 <button type="button" id="uploadOgImageBtn" class="pe-btn upload-image-btn"> 18628 <svg style="width: 16px; height: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 18629 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> 18630 <polyline points="17 8 12 3 7 8"></polyline> 18631 <line x1="12" y1="3" x2="12" y2="15"></line> 18632 </svg> 18633 <span id="uploadOgImageBtnText">Upload image</span> 18634 </button> 18635 <div id="ogImagePreviewContainer"></div> 18636 <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> 18637 </div> 18638 18639 <div class="form-group"> 18640 <label for="googleAnalytics">Google Analytics Code</label> 18641 <input type="text" id="googleAnalytics" class="form-control" placeholder="Enter GA-XXXXXXXXXX"> 18642 <small>Example: G-XXXXXXXXXX or UA-XXXXXXXX-X</small> 18643 </div> 18644 18645 <div class="form-group"> 18646 <label for="contentLanguage">Content Language</label> 18647 <select id="contentLanguage" class="form-control"> 18648 <option value="en">English (en)</option> 18649 <option value="es">Spanish / Español (es)</option> 18650 <option value="fr">French / Français (fr)</option> 18651 <option value="ne">Nepali / नà¥à¤ªà¤¾à¤²à¥ (ne)</option> 18652 <option value="de">German / Deutsch (de)</option> 18653 <option value="it">Italian / Italiano (it)</option> 18654 <option value="pt">Portuguese / Português (pt)</option> 18655 <option value="ru">Russian / Ð ÑÑÑкий (ru)</option> 18656 <option value="zh">Chinese / 䏿 (zh)</option> 18657 <option value="ja">Japanese / æ¥æ¬èª (ja)</option> 18658 <option value="ko">Korean / íêµì´ (ko)</option> 18659 <option value="ar">Arabic / Ø§ÙØ¹Ø±Ø¨ÙØ© (ar)</option> 18660 <option value="hi">Hindi / हिनà¥à¤¦à¥ (hi)</option> 18661 </select> 18662 <small>Set the primary language of your site content. Chrome will offer to translate if visitor's language is different.</small> 18663 </div> 18664 18665 <div class="form-group"> 18666 <label> 18667 <input type="checkbox" id="noIndexToggle"> 18668 Hide site from search engines (noindex) 18669 </label> 18670 <small>Use this if the site is under development or shouldn't appear in search results</small> 18671 </div> 18672 18673 <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;"> 18674 <h4 style="margin-bottom: 8px;">Schema / Identity</h4> 18675 <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> 18676 <label style="display: block; margin-bottom: 6px;">This site represents:</label> 18677 <div style="display: flex; gap: 20px; margin-bottom: 4px;"> 18678 <label style="display: flex; align-items: center; gap: 6px; font-weight: normal;"> 18679 <input type="radio" name="schemaType" id="schemaTypePerson" value="Person" onchange="updateSchemaFields()"> Person / Individual 18680 </label> 18681 <label style="display: flex; align-items: center; gap: 6px; font-weight: normal;"> 18682 <input type="radio" name="schemaType" id="schemaTypeOrganization" value="Organization" onchange="updateSchemaFields()"> Organization / Business 18683 </label> 18684 </div> 18685 <small>Select "Person" for individual photographers, artists, consultants. Select "Organization" for studios, agencies, companies.</small> 18686 </div> 18687 18688 <div class="form-group" id="schemaJobTitleGroup" style="display: none;"> 18689 <label for="schemaJobTitle">Job Title</label> 18690 <input type="text" id="schemaJobTitle" class="form-control" placeholder=""> 18691 <small>Used in Google Knowledge Panels and local search (e.g. "photojournalist Minneapolis").</small> 18692 </div> 18693 18694 <div class="form-group" id="schemaCityGroup" style="display: none;"> 18695 <label for="schemaCity">City / Location</label> 18696 <input type="text" id="schemaCity" class="form-control" placeholder=""> 18697 <small>Improves visibility in location-based searches.</small> 18698 </div> 18699 18700 <div class="form-group" id="schemaEmailGroup" style="display: none;"> 18701 <label for="schemaEmail">Contact Email (for structured data)</label> 18702 <input type="text" id="schemaEmail" class="form-control" placeholder="e.g. [email protected]"> 18703 <small>Embedded in structured data only â not displayed on the page.</small> 18704 </div> 18705 18706 <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;"> 18707 <h4 style="margin-bottom: 8px;">
18707Image licensing (optional)</h4> 18708 <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> 18709 <label for="imageLicenseUrl">License page URL</label> 18710 <input type="url" id="imageLicenseUrl" class="form-control" placeholder="https://example.com/licensing"> 18711 <small>A public page that states how images may be used (your terms, or a Creative Commons license).</small> 18712 </div> 18713 18714 <div class="form-group"> 18715 <label for="acquireLicensePageUrl">Acquire license page URL</label> 18716 <input type="url" id="acquireLicensePageUrl" class="form-control" placeholder="https://example.com/contact"> 18717 <small>A public page where someone can request or buy a license. Both URLs are required, or leave both empty.</small> 18718 </div> 18719 18720 <div class="form-group"> 18721 <label> 18722 <input type="checkbox" id="metadataCopyrightEnabled"> 18723 Enable sidebar copyright footer 18724 </label> 18725 <small>When enabled, the copyright text shows at the bottom of the sidebar.</small> 18726 </div> 18727 18728 <div class="form-group" id="metadataCopyrightTextGroup"> 18729 <label for="metadataCopyrightText">Copyright Text</label> 18730 <input type="text" id="metadataCopyrightText" class="form-control" placeholder="© 2025 Your Name"> 18731 <small>This text appears in the sidebar footer and inside the copyright overlay.</small> 18732 </div> 18733 18734 <div class="form-group"> 18735 <label for="faviconUrl">Favicon</label> 18736 <input type="hidden" id="faviconUrl" class="form-control"> 18737 <button type="button" id="uploadFaviconBtn" class="pe-btn upload-image-btn"> 18738 <svg style="width: 16px; height: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 18739 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> 18740 <polyline points="17 8 12 3 7 8"></polyline> 18741 <line x1="12" y1="3" x2="12" y2="15"></line> 18742 </svg> 18743 <span id="uploadFaviconBtnText">Upload Favicon</span> 18744 </button> 18745 <div id="faviconPreviewContainer"></div> 18746 <small>Upload an image file for your site favicon (recommended: 32x32px or 16x16px, PNG or ICO format)</small> 18747 </div> 18748 18749 <div class="form-group" id="customPublicSiteBypassGroup" hidden style="display: none; border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;"> 18750 <h4 style="margin-bottom: 12px;">Custom HTML site (full bypass)</h4> 18751 <p style="font-size: 13px; color: #555; margin-bottom: 12px;"> 18752 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. 18753 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. 18754 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). 18755 </p> 18756 <label> 18757 <input type="checkbox" id="customPublicSiteEnabled" /> 18758 Serve uploaded HTML as the public site (bypass Hydra shell on <code>/</code>) 18759 </label> 18760 <input type="hidden" id="customPublicSiteHtmlR2Key" value="" /> 18761 <input type="hidden" id="customPublicSiteHtmlFilenameStore" value="" /> 18762 <div class="form-group" style="margin-top: 10px;"> 18763 <button type="button" id="uploadCustomPublicHtmlBtn" class="pe-btn">Upload HTML file</button> 18764 <button type="button" id="removeCustomPublicHtmlBtn" class="pe-btn">Clear file</button> 18765 </div> 18766 <p id="customPublicSiteHtmlFilenameLabel" style="font-size: 13px; color: #666; margin-top: 8px;">No HTML file uploaded yet.</p> 18767 </div> 18768 18769 <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;"> 18770 <h4 style="margin-bottom: 8px;">Site-wide password (browser login)</h4> 18771 <p style="font-size: 13px; color: #555; margin-bottom: 12px;"> 18772 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 18773 <code>preview.neonsky.app/<your-site-id>/</code>). Editors still use Magic login to edit. Turn off to make the site fully public again. 18774 </p> 18775 <label> 18776 <input type="checkbox" id="siteWideBasicAuthEnabled" onchange="updateSiteWideBasicAuthFieldState()" /> 18777 Require HTTP Basic password for the entire site 18778 </label> 18779 <div id="siteWideBasicAuthFields" style="display: none; margin-top: 12px;"> 18780 <div class="form-group"> 18781 <label for="siteWideBasicAuthUsername">Username</label> 18782 <input type="text" id="siteWideBasicAuthUsername" class="form-control" autocomplete="username" /> 18783 </div> 18784 <div class="form-group"> 18785 <label for="siteWideBasicAuthPassword">Password</label> 18786 <input type="password" id="siteWideBasicAuthPassword" class="form-control" autocomplete="new-password" /> 18787 <small>Stored in your site configuration. Use a strong password; anyone with it can view the site.</small> 18788 </div> 18789 <div class="form-group"> 18790 <label for="siteWideBasicAuthRealm">Realm (optional)</label> 18791 <input type="text" id="siteWideBasicAuthRealm" class="form-control" placeholder="Site" /> 18792 <small>Label shown in the browser login dialog.</small> 18793 </div> 18794 </div> 18795 </div> 18796 18797 <div class="form-actions pe-footer-actions"> 18798 <button type="button" class="pe-btn pe-btn--primary" onclick="saveMetadata()">Save Metadata</button> 18799 <button type="button" class="pe-btn" onclick="toggleMetadataEditor()">Cancel</button> 18800 </div> 18801</div> 18802
18803<script> 18804var customPublicSiteOptionHeld = false; 18805var customPublicSiteOptionKeyBound = false; 18806 18807function isMetadataEditorOpen() { 18808 var editor = document.getElementById('metadataEditor'); 18809 if (!editor) return false; 18810 if (window.currentOpenForm === 'metadataEditor') return true; 18811 return editor.style.display === 'block'; 18812} 18813 18814function syncCustomPublicSiteBypassVisibility() { 18815 var group = document.getElementById('customPublicSiteBypassGroup'); 18816 if (!group) return; 18817 var show = isMetadataEditorOpen() && customPublicSiteOptionHeld; 18818 group.hidden = !show; 18819 group.style.display = show ? 'block' : 'none'; 18820} 18821 18822function bindCustomPublicSiteOptionKeyListeners() { 18823 if (customPublicSiteOptionKeyBound) return; 18824 customPublicSiteOptionKeyBound = true; 18825 18826 document.addEventListener('keydown', function(e) { 18827 if (!e.altKey) return; 18828 customPublicSiteOptionHeld = true; 18829 syncCustomPublicSiteBypassVisibility(); 18830 }, true); 18831 18832 document.addEventListener('keyup', function(e) { 18833 var isOptionRelease = 18834 e.key === 'Alt' || 18835 e.key === 'Option' || 18836 e.code === 'AltLeft' || 18837 e.code === 'AltRight'; 18838 if (!isOptionRelease) return; 18839 customPublicSiteOptionHeld = false; 18840 syncCustomPublicSiteBypassVisibility(); 18841 }, true); 18842} 18843 18844window.syncCustomPublicSiteBypassVisibility = syncCustomPublicSiteBypassVisibility; 18845window.bindCustomPublicSiteOptionKeyListeners = bindCustomPublicSiteOptionKeyListeners; 18846 18847function updateSiteWideBasicAuthFieldState() { 18848 const en = document.getElementById('siteWideBasicAuthEnabled'); 18849 const fields = document.getElementById('siteWideBasicAuthFields'); 18850 if (fields) fields.style.display = en && en.checked ? 'block' : 'none'; 18851} 18852 18853function updateSchemaFields() { 18854 const isPerson = document.getElementById('schemaTypePerson') && document.getElementById('schemaTypePerson').checked; 18855 const isOrg = document.getElementById('schemaTypeOrganization') && document.getElementById('schemaTypeOrganization').checked; 18856 const anySelected = isPerson || isOrg; 18857 const jobTitleGroup = document.getElementById('schemaJobTitleGroup'); 18858 const cityGroup = document.getElementById('schemaCityGroup'); 18859 const emailGroup = document.getElementById('schemaEmailGroup'); 18860 if (jobTitleGroup) jobTitleGroup.style.display = isPerson ? 'block' : 'none'; 18861 if (cityGroup) cityGroup.style.display = anySelected ? 'block' : 'none'; 18862 if (emailGroup) emailGroup.style.display = anySelected ? 'block' : 'none'; 18863} 18864 18865function uploadCustomPublicHtmlFile() { 18866 const siteId = window.Parameters && window.Parameters.siteId ? window.Parameters.siteId : (window.siteId || ''); 18867 if (!siteId) { 18868 alert('Site ID not found. Refresh the page and try again.'); 18869 return; 18870 } 18871 const authToken = window.ImageUploader && window.ImageUploader.getAuthToken ? window.ImageUploader.getAuthToken() : null; 18872 if (!authToken) { 18873 alert('Please log in first (press + on the page).'); 18874 return; 18875 } 18876 const input = document.createElement('input'); 18877 input.type = 'file'; 18878 input.accept = '.html,.htm,text/html'; 18879 input.addEventListener('change', function() { 18880 const file = input.files && input.files[0]; 18881 if (!file) return; 18882 const formData = new FormData(); 18883 formData.append('file', file); 18884 formData.append('guid', siteId); 18885 formData.append('purpose', 'custom-public-site'); 18886 formData.append('pageId', '_site'); 18887 formData.append('elementId', 'custom-public-html'); 18888 formData.append('folderPath', ''); 18889 var relUpload = typeof window.getApiUrl === 'function' ? window.getApiUrl('/api/upload-file') : '/api/upload-file'; 18890 var uploadUrl = window.location && window.location.origin ? new URL(relUpload, window.location.origin).href : relUpload; 18891 fetch(uploadUrl, { 18892 method: 'POST', 18893 headers: { 'Authorization': authToken, 'X-User-Email': localStorage.getItem('hydra_auth_email') || '' }, 18894 body: formData 18895 }).then(function(r) { 18896 return r.text().then(function(text) { 18897 var ct = (r.headers.get('content-type') || '').toLowerCase(); 18898 if (ct.indexOf('application/json') === -1 && /<!DOCTYPE/i.test(text)) { 18899 throw new Error( 18900 'Server returned HTML instead of JSON â POST /api/upload-file is not reaching the Hydra worker. ' + 18901 'In Cloudflare, ensure this hostname has a Worker route (e.g. neonsky.app/*) so API traffic is not sent to another host or Pages.' 18902 ); 18903 } 18904 if (r.status === 403 && ct.indexOf('application/json') === -1) { 18905 throw new Error( 18906 '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.' 18907 ); 18908 } 18909 try { 18910 return JSON.parse(text); 18911 } catch (parseErr) { 18912 throw new Error('Server did not return JSON: ' + text.slice(0, 120) + (text.length > 120 ? '...' : '')); 18913 } 18914 }); 18915 }).then(function(res) { 18916 if (res.success && res.key) { 18917 var keyEl = document.getElementById('customPublicSiteHtmlR2Key'); 18918 var fnEl = document.getElementById('customPublicSiteHtmlFilenameStore'); 18919 var lbl = document.getElementById('customPublicSiteHtmlFilenameLabel'); 18920 if (keyEl) keyEl.value = res.key; 18921 if (fnEl) fnEl.value = file.name; 18922 if (lbl) lbl.textContent = 'Current file: ' + file.name + ' â click Save Metadata below to apply.'; 18923 window.siteMetadata = window.siteMetadata || {}; 18924 window.siteMetadata.customPublicSiteHtmlR2Key = res.key; 18925 window.siteMetadata.customPublicSiteHtmlFilename = file.name; 18926 } else { 18927 alert(res.error || res.details || 'Upload failed'); 18928 } 18929 }).catch(function(e) { 18930 alert('Upload failed: ' + (e && e.message ? e.message : 'Unknown error')); 18931 }); 18932 }); 18933 input.click(); 18934} 18935 18936function clearCustomPublicHtmlFile() { 18937 if (!confirm('Clear the uploaded HTML file reference? (You can upload again later.)')) return; 18938 var k = document.getElementById('customPublicSiteHtmlR2Key'); 18939 var fn = document.getElementById('customPublicSiteHtmlFilenameStore'); 18940 var lbl = document.getElementById('customPublicSiteHtmlFilenameLabel'); 18941 if (k) k.value = ''; 18942 if (fn) fn.value = ''; 18943 if (lbl) lbl.textContent = 'No HTML file uploaded yet.'; 18944 if (window.siteMetadata) { 18945 window.siteMetadata.customPublicSiteHtmlR2Key = ''; 18946 window.siteMetadata.customPublicSiteHtmlFilename = ''; 18947 } 18948} 18949 18950// Handle favicon upload button click 18951document.addEventListener('DOMContentLoaded', function() { 18952 const uploadBtn = document.getElementById('uploadFaviconBtn'); 18953 if (uploadBtn) { 18954 uploadBtn.addEventListener('click', function() { 18955 uploadFavicon(); 18956 }); 18957 } 18958 const uploadOgBtn = document.getElementById('uploadOgImageBtn'); 18959 if (uploadOgBtn) { 18960 uploadOgBtn.addEventListener('click', function() { 18961 uploadSiteOgImage(); 18962 }); 18963 } 18964 var upHtml = document.getElementById('uploadCustomPublicHtmlBtn'); 18965 if (upHtml) upHtml.addEventListener('click', uploadCustomPublicHtmlFile); 18966 var rmHtml = document.getElementById('removeCustomPublicHtmlBtn'); 18967 if (rmHtml) rmHtml.addEventListener('click', clearCustomPublicHtmlFile); 18968 bindCustomPublicSiteOptionKeyListeners(); 18969 syncCustomPublicSiteBypassVisibility(); 18970 18971 // Handle content language dropdown change 18972 const contentLanguageSelect = document.getElementById('contentLanguage'); 18973 if (contentLanguageSelect) { 18974 contentLanguageSelect.addEventListener('change', function() { 18975 console.warn('Content language changed to:', this.value); 18976 // Update global variable immediately 18977 window.siteMetadata.contentLanguage = this.value; 18978 console.warn('Updated window.siteMetadata.contentLanguage:', window.siteMetadata.contentLanguage); 18979 }); 18980 } 18981 18982 const copyrightToggle = document.getElementById('metadataCopyrightEnabled'); 18983 if (copyrightToggle) { 18984 copyrightToggle.addEventListener('change', updateMetadataCopyrightFieldState); 18985 } 18986 18987 updateMetadataCopyrightFieldState(); 18988}); 18989 18990// Function to handle favicon upload using ImageUploader 18991function uploadFavicon() { 18992 console.log('Starting favicon upload...'); 18993 18994 // Check if ImageUploader is available 18995 if (!window.ImageUploader || typeof window.ImageUploader.open !== 'function') { 18996 alert('Image uploader not available. Please enter a favicon URL manually.'); 18997 return; 18998 } 18999 19000 try { 19001 // Create options for the ImageUploader 19002 const options = { 19003 title: 'Upload Favicon', 19004 containerType: 'favicon', 19005 maxWidth: 64, // Favicons are typically small 19006 maxHeight: 64, 19007 quality: 0.9 19008 }; 19009 19010 // Call the ImageUploader 19011 window.ImageUploader.open(options, function(imageResult, imageInfo) { 19012 console.log('Favicon upload callback received:', imageInfo); 19013 19014 // Get the favicon URL input field 19015 let faviconInput = document.getElementById('faviconUrl'); 19016 console.warn('=== UPLOAD CALLBACK DEBUG ==='); 19017 console.warn('Favicon input element found:', !!faviconInput); 19018 console.warn('Favicon input element type:', faviconInput ? faviconInput.type : 'N/A'); 19019 console.warn('Favicon input element id:', faviconInput ? faviconInput.id : 'N/A'); 19020 19021 // If element not found, wait a bit and try again (metadata editor might not be open yet) 19022 if (!faviconInput) { 19023 console.warn('Favicon input not found, waiting 100ms and trying again...'); 19024 setTimeout(() => { 19025 faviconInput = document.getElementById('faviconUrl'); 19026 console.warn('Retry - Favicon input element found:', !!faviconInput); 19027 if (faviconInput) { 19028 console.warn('Retry - Setting favicon input value to:', imageInfo.url); 19029 faviconInput.value = imageInfo.url; 19030 console.warn('Retry - Favicon input value after setting:', faviconInput.value); 19031 } else { 19032 console.error('Retry - Favicon input field still not found'); 19033 } 19034 }, 100); 19035 return; 19036 } 19037 19038 // Set the uploaded image URL 19039 if (imageInfo && imageInfo.type === 'r2' && imageInfo.url) { 19040 // R2 storage - use the URL 19041 console.warn('Setting favicon input value to:', imageInfo.url); 19042 faviconInput.value = imageInfo.url; 19043 console.warn('Favicon input value after setting:', faviconInput.value); 19044 19045 // Also store in global variable as backup 19046 window.uploadedFaviconUrl = imageInfo.url; 19047 console.warn('Stored favicon URL in global variable:', window.uploadedFaviconUrl); 19048 19049 console.log('Favicon URL set to:', imageInfo.url); 19050 19051 // Update button text 19052 const btnText = document.getElementById('uploadFaviconBtnText'); 19053 if (btnText) { 19054 btnText.textContent = 'Change Favicon'; 19055 } 19056 19057 // Show preview 19058 showFaviconPreview(imageInfo.url); 19059 } else if (imageResult) { 19060 // Legacy base64 storage 19061 faviconInput.value = imageResult; 19062 console.log('Favicon set to base64 data'); 19063 19064 // Update button text 19065 const btnText = document.getElementById('uploadFaviconBtnText'); 19066 if (btnText) { 19067 btnText.textContent = 'Change Favicon'; 19068 } 19069 19070 // Show preview 19071 showFaviconPreview(imageResult); 19072 } else { 19073 console.error('No image data received from uploader'); 19074 alert('Upload failed. Please try again.'); 19075 return; 19076 } 19077 19078 // Show success message 19079 console.log('Favicon uploaded successfully'); 19080 }); 19081 19082 } catch (error) { 19083 console.error('Error opening favicon uploader:', error); 19084 alert('Error opening uploader: ' + (error.message || 'Unknown error')); 19085 } 19086} 19087 19088// Function to show a preview of the uploaded favicon 19089function showFaviconPreview(imageUrl) { 19090 const container = document.getElementById('faviconPreviewContainer'); 19091 if (!container) { 19092 console.error('Favicon preview container not found'); 19093 return; 19094 } 19095 19096 container.innerHTML = 19097 '<div style="margin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; display: flex; align-items: center; gap: 15px;">' + 19098 '<img src="' + imageUrl + '" alt="Favicon Preview" style="width: 32px; height: 32px; border: 1px solid #ccc; background: white; padding: 4px;">' + 19099 '<div style="flex: 1;">' + 19100 '<div style="font-size: 14px; color: #333; font-weight: 500;">Current Favicon</div>' + 19101 '<div style="font-size: 12px; color: #666; margin-top: 2px;">Preview (32x32px)</div>' + 19102 '</div>' + 19103 '<button type="button" onclick="removeFavicon()" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' + 19104 'Remove' + 19105 '</button>' + 19106 '</div>'; 19107} 19108 19109// Function to remove the favicon 19110function removeFavicon() { 19111 const faviconInput = document.getElementById('faviconUrl'); 19112 if (faviconInput) { 19113 faviconInput.value = ''; 19114 } 19115 19116 const container = document.getElementById('faviconPreviewContainer'); 19117 if (container) { 19118 container.innerHTML = ''; 19119 } 19120 19121 const btnText = document.getElementById('uploadFaviconBtnText'); 19122 if (btnText) { 19123 btnText.textContent = 'Upload Favicon'; 19124 } 19125 19126 console.log('Favicon removed'); 19127} 19128 19129function uploadSiteOgImage() { 19130 if (!window.ImageUploader || typeof window.ImageUploader.open !== 'function') { 19131 alert('Image uploader not available. Refresh the page and try again.'); 19132 return; 19133 } 19134 try { 19135 window.ImageUploader.open({ 19136 title: 'Upload social preview image', 19137 containerType: 'page', 19138 quality: 0.92 19139 }, function(imageResult, imageInfo) { 19140 const inp = document.getElementById('ogImageUrl'); 19141 const setUrl = function(url) { 19142 if (inp) inp.value = url; 19143 const btnText = document.getElementById('uploadOgImageBtnText'); 19144 if (btnText) btnText.textContent = 'Change image'; 19145 showOgImagePreview(url); 19146 window.siteMetadata = window.siteMetadata || {}; 19147 window.siteMetadata.ogImageUrl = url; 19148 }; 19149 if (imageInfo && imageInfo.url) { 19150 setUrl(imageInfo.url); 19151 } else if (imageResult && typeof imageResult === 'string' && imageResult.indexOf('data:') === 0) { 19152 setUrl(imageResult); 19153 } else { 19154 alert('Upload failed. Please try again.'); 19155 } 19156 }); 19157 } catch (error) { 19158 console.error('Error opening OG image uploader:', error); 19159 alert('Error opening uploader: ' + (error.message || 'Unknown error')); 19160 } 19161} 19162 19163function showOgImagePreview(imageUrl) { 19164 const container = document.getElementById('ogImagePreviewContainer'); 19165 if (!container) return; 19166 container.innerHTML = 19167 '<div style="margin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; display: flex; align-items: center; gap: 15px;">' + 19168 '<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;">' + 19169 '<div style="flex: 1;">' + 19170 '<div style="font-size: 14px; color: #333; font-weight: 500;">Current social preview image</div>' + 19171 '<div style="font-size: 12px; color: #666; margin-top: 2px;">This image overrides the first page/gallery image for social previews</div>' + 19172 '</div>' + 19173 '<button type="button" onclick="removeOgImage()" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' + 19174 'Remove' + 19175 '</button>' + 19176 '</div>'; 19177} 19178 19179function removeOgImage() { 19180 const inp = document.getElementById('ogImageUrl'); 19181 if (inp) inp.value = ''; 19182 if (window.siteMetadata) window.siteMetadata.ogImageUrl = ''; 19183 const container = document.getElementById('ogImagePreviewContainer'); 19184 if (container) container.innerHTML = ''; 19185 const btnText = document.getElementById('uploadOgImageBtnText'); 19186 if (btnText) btnText.textContent = 'Upload image'; 19187} 19188</script>
19188 19189 19190 <!-- Import Classic Form --> 19191<div class="import-classic-form" id="importClassicForm" style="display: none;" data-nosnippet> 19192 <h3>Import Galleries from JSON or Classic GUID</h3> 19193 <p>Paste the full site JSON into the text area below, <strong>OR</strong> enter the GUID of a classic gallery collection.</p> 19194 19195 <div class="form-group"> 19196 <label for="pastedJson">Pasted Site JSON:</label> 19197 <textarea id="pastedJson" rows="6" placeholder="Paste the entire site JSON here..."></textarea> 19198 </div> 19199 19200 <div class="import-separator">OR</div> 19201 19202 <div class="form-group"> 19203 <label for="classicGuid">Classic GUID or Import ID:</label> 19204 <input type="text" id="classicGuid" placeholder="Enter GUID (e.g., 4bd5ebf1b1418) or import ID (e.g., import_4c86452fb530b)"> 19205 <small>Use a GUID to fetch from classic site, or use import_ format to fetch from R2 storage.</small> 19206 </div> 19207 19208 <div class="form-group"> 19209 <label> 19210 <input type="checkbox" id="replaceMenuCheckbox" onchange="document.getElementById('importParent').disabled = this.checked; document.getElementById('createSubmenu').disabled = this.checked;"> 19211 Replace existing menu structure 19212 </label> 19213 <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> 19214 </div> 19215 19216 <div class="form-group"> 19217 <label for="importParent">Import under:</label> 19218 <select id="importParent"> 19219 <option value="">Top Level (No Parent)</option> 19220 </select> 19221 </div> 19222 19223 <div class="form-group"> 19224 <label> 19225 <input type="checkbox" id="createSubmenu" checked> 19226 Create submenu for all imported galleries 19227 </label> 19228 </div> 19229 19230 <div class="form-group" id="submenuTitleContainer"> 19231 <label for="submenuTitle">Submenu title:</label> 19232 <input type="text" id="submenuTitle" placeholder="e.g., Classic Galleries"> 19233 </div> 19234 19235 <div id="importStatus" class="import-status" style="display: none;"> 19236 <div class="import-progress"><div class="progress-bar"></div></div> 19237 <div class="status-message">Processing...</div> 19238 </div> 19239 19240 <div class="form-actions"> 19241 <button class="btn btn-primary" onclick="importClassicGalleries()">Import from Paste/GUID</button> 19242 <button class="btn" onclick="downloadCurrentSiteJSON()">Download Current Site JSON</button> 19243 <button class="btn" onclick="toggleImportClassicForm()">Cancel</button> 19244 </div> 19245</div> 19246 <div class="tree-container"> 19247 <div id="galleryTree" data-help-id="menu-tree"></div> 19248 </div> 19249 19250 <!-- Sidebar Elements Container will be inserted here by the sidebar-manager.js --> 19251 <div class="sidebar-elements-container"></div> 19252</div> 19253<div class="gallery-container" id="hydra-main-content" role="main" tabindex="-1"> 19254 <iframe class="gallery-iframe" id="galleryFrame" title="ATHLETES | People - Adena Stevens Photography"></iframe> 19255</div> 19256</div> 19257 19258<!-- Cookie Notice (compliance mode â only when Display Cookie Notice is enabled) --> 19259 19260
19261<script> 19262// Reveal content once layout is applied, CSS is loaded, AND gallery is initialized 19263(function() { 19264 let hasRevealed = false; // Ensure we only reveal once per page load 19265 19266 function revealContent() { 19267 if (hasRevealed) return; // Prevent multiple calls 19268 hasRevealed = true; 19269 document.documentElement.classList.add('hydra-ready'); 19270 document.body.classList.add('hydra-ready'); 19271 console.log('â Content revealed - hydra-ready class added'); 19272 } 19273 19274 // Check if this page has a gallery container 19275 function hasGalleryContainer() { 19276 return !!document.getElementById('neon-gallery-container') || 19277 !!document.querySelector('.gallery-wrapper') || 19278 !!document.querySelector('.masonry-container'); 19279 } 19280 19281 // Check if gallery is already initialized 19282 function isGalleryInitialized() { 19283 return window.neonGalleryState?.initialized || 19284 window.neonGalleryState?.domInitialized || 19285 (window.neonGalleryUtils && window.neonGalleryUtils.isInitialized && window.neonGalleryUtils.isInitialized()); 19286 } 19287 19288 // Main reveal logic 19289 function attemptReveal() { 19290 const hasGallery = hasGalleryContainer(); 19291 const galleryInitialized = isGalleryInitialized(); 19292 19293 // If no gallery, reveal immediately after CSS loads 19294 if (!hasGallery) { 19295 if (document.readyState === 'complete') { 19296 revealContent(); 19297 } else { 19298 window.addEventListener('load', revealContent, { once: true }); 19299 } 19300 return; 19301 } 19302 19303 // If gallery exists and is initialized, reveal 19304 if (hasGallery && galleryInitialized) { 19305 revealContent(); 19306 return; 19307 } 19308 19309 // If gallery exists but not initialized, wait for initialization 19310 if (hasGallery && !galleryInitialized) { 19311 console.log('â³ Waiting for gallery initialization before revealing content...'); 19312 19313 // Listen for gallery-loaded event 19314 window.addEventListener('gallery-loaded', function() { 19315 console.log('â Gallery-loaded event received'); 19316 setTimeout(revealContent, 100); // Small delay to ensure DOM is updated 19317 }, { once: true }); 19318 19319 // Listen for page-loaded event 19320 window.addEventListener('page-loaded', function() { 19321 console.log('â Page-loaded event received'); 19322 setTimeout(revealContent, 100); 19323 }, { once: true }); 19324 19325 // Fallback: Poll for gallery initialization (max 10 seconds) 19326 let pollAttempts = 0; 19327 const maxPollAttempts = 100; // 10 seconds (100 * 100ms) 19328 const pollInterval = setInterval(function() { 19329 pollAttempts++; 19330 if (isGalleryInitialized()) { 19331 console.log('â Gallery initialized detected via polling'); 19332 clearInterval(pollInterval); 19333 revealContent(); 19334 } else if (pollAttempts >= maxPollAttempts) { 19335 console.warn('â ï¸ Gallery initialization timeout - revealing content anyway'); 19336 clearInterval(pollInterval); 19337 revealContent(); // Reveal anyway to prevent blank screen 19338 } 19339 }, 100); 19340 19341 // Safety fallback: Always reveal after 8 seconds max (prevents permanent blank screen) 19342 setTimeout(function() { 19343 if (!hasRevealed) { 19344 console.warn('â ï¸ Safety timeout reached - revealing content to prevent blank screen'); 19345 revealContent(); 19346 } 19347 }, 8000); 19348 } 19349 } 19350 19351 // Start the reveal process 19352 if (document.readyState === 'loading') { 19353 document.addEventListener('DOMContentLoaded', function() { 19354 // Wait a bit for scripts to initialize 19355 setTimeout(attemptReveal, 100); 19356 }); 19357 } else if (document.readyState === 'interactive' || document.readyState === 'complete') { 19358 // DOM already ready, wait a bit for scripts 19359 setTimeout(attemptReveal, 100); 19360 } else { 19361 // Fallback 19362 window.addEventListener('load', attemptReveal, { once: true }); 19363 } 19364})(); 19365</script>
19365 19366
19367<script> 19368// ============================================ 19369// LANGUAGE TRANSLATION SYSTEM 19370// ============================================ 19371 19372(function() { 19373 // CRITICAL: Only initialize translation system if a languages element exists 19374 // This ensures sites without languages element have ZERO overhead 19375 const checkForLanguagesElement = function() { 19376 // Wait for SidebarManager to be initialized 19377 if (!window.SidebarManager || !window.SidebarManager.elements) { 19378 return false; 19379 } 19380 19381 // Check if any languages element exists 19382 const hasLanguagesElement = window.SidebarManager.elements.some(el => el.type === 'languages'); 19383 19384 if (!hasLanguagesElement) { 19385 console.warn('No languages element found - translation system disabled'); 19386 return false; 19387 } 19388 19389 return true; 19390 }; 19391 19392 // Check for languages element, retry if SidebarManager not ready yet 19393 let initAttempts = 0; 19394 const maxAttempts = 10; 19395 19396 function tryInitialize() { 19397 if (checkForLanguagesElement()) { 19398 console.warn('â Languages element found - initializing translation system'); 19399 initializeTranslationSystem(); 19400 } else { 19401 initAttempts++; 19402 if (initAttempts < maxAttempts) { 19403 // Retry in 500ms (total max wait: 5 seconds) 19404 setTimeout(tryInitialize, 500); 19405 } else { 19406 console.warn('Translation system not needed for this site - no languages element found after 5 seconds'); 19407 } 19408 } 19409 } 19410 19411 // Start checking after initial delay 19412 setTimeout(tryInitialize, 500); 19413 19414 function initializeTranslationSystem() { 19415 console.warn('â Translation system initialized successfully'); 19416 19417 let currentLanguage = localStorage.getItem('hydra_language') || 'en'; 19418 const originalTexts = new Map(); 19419 19420 // Expose test function for debugging 19421 window.testTranslation = function(langCode) { 19422 console.warn('𧪠Manual translation test triggered for:', langCode); 19423 console.warn('ð Dispatching test event...'); 19424 window.dispatchEvent(new CustomEvent('hydra:language-change', { 19425 detail: { code: langCode || 'es', name: 'Test' } 19426 })); 19427 console.warn('â Test event dispatched - check if listener caught it above'); 19428 }; 19429 19430 console.warn('â Event listener for "hydra:language-change" is now active'); 19431 19432 // Watch for edit mode changes and restore default language 19433 const editModeObserver = new MutationObserver(function(mutations) { 19434 mutations.forEach(function(mutation) { 19435 if (mutation.attributeName === 'class') { 19436 const isEditMode = document.body.classList.contains('edit-mode-active'); 19437 if (isEditMode) { 19438 console.warn('Edit mode activated - restoring default language'); 19439 // Restore original text 19440 restoreOriginalText(); 19441 19442 // Update language buttons to show default language 19443 const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages'); 19444 const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en'; 19445 19446 document.querySelectorAll('.language-button-live').forEach(btn => { 19447 if (btn.getAttribute('data-lang-code') === defaultLang) { 19448 btn.style.background = 'var(--menu-active-color, #4682B4)'; 19449 } else { 19450 btn.style.background = 'transparent'; 19451 } 19452 }); 19453 } 19454 } 19455 }); 19456 }); 19457 19458 // Start observing body for class changes 19459 if (document.body) { 19460 editModeObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] }); 19461 } 19462 19463 // Watch for dynamically loaded content (galleries, pages, etc.) and translate it 19464 const contentObserver = new MutationObserver(function(mutations) { 19465 // Only process if we're not in default language and not in edit mode 19466 const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages'); 19467 const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en'; 19468 const isEditMode = document.body.classList.contains('edit-mode-active'); 19469 19470 if (currentLanguage === defaultLang || isEditMode) { 19471 return; // Don't translate if in default language or edit mode 19472 } 19473 19474 // Check if new text content was added 19475 let hasNewTextContent = false;
19476 mutations.forEach(function(mutation) { 19477 if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 19478 mutation.addedNodes.forEach(function(node) { 19479 if (node.nodeType === Node.ELEMENT_NODE) { 19480 // Check if the added element contains text we should translate 19481 const textElements = node.querySelectorAll('p, h1, h2, h3, .page-text'); 19482 if (textElements.length > 0 || (node.textContent && node.textContent.trim().length > 0)) { 19483 hasNewTextContent = true; 19484 } 19485 } 19486 }); 19487 } 19488 }); 19489 19490 if (hasNewTextContent) { 19491 console.warn('New content detected - translating to:', currentLanguage); 19492 // Debounce to avoid translating multiple times rapidly 19493 clearTimeout(window._translationDebounce); 19494 window._translationDebounce = setTimeout(function() { 19495 translatePage(currentLanguage, defaultLang); 19496 }, 300); 19497 } 19498 }); 19499 19500 // Observe the gallery container and page content areas for new content 19501 const galleryContainer = document.querySelector('.gallery-container'); 19502 if (galleryContainer) { 19503 contentObserver.observe(galleryContainer, { 19504 childList: true, 19505 subtree: true 19506 }); 19507 } 19508 19509 // Also observe the main content area 19510 const mainContent = document.querySelector('.container'); 19511 if (mainContent) { 19512 contentObserver.observe(mainContent, { 19513 childList: true, 19514 subtree: true 19515 }); 19516 } 19517 19518 // Listen for language change events 19519 window.addEventListener('hydra:language-change', async function(e) { 19520 const { code, name } = e.detail; 19521 console.warn('ð Language changed to:', code, name); 19522 19523 // CRITICAL: Ignore language changes in edit mode 19524 if (document.body.classList.contains('edit-mode-active')) { 19525 console.warn('â ï¸ Edit mode active - ignoring language change'); 19526 // Reset to default language 19527 const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages'); 19528 const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en'; 19529 19530 // Update button states to show default language 19531 document.querySelectorAll('.language-button-live').forEach(btn => { 19532 if (btn.getAttribute('data-lang-code') === defaultLang) { 19533 btn.style.background = 'var(--menu-active-color, #4682B4)'; 19534 } else { 19535 btn.style.background = 'transparent'; 19536 } 19537 }); 19538 19539 // Show message to user 19540 console.warn('Language switching is disabled in edit mode'); 19541 return; 19542 } 19543 19544 // Update all language buttons 19545 document.querySelectorAll('.language-button-live').forEach(btn => { 19546 if (btn.getAttribute('data-lang-code') === code) { 19547 btn.style.background = 'var(--menu-active-color, #4682B4)'; 19548 } else { 19549 btn.style.background = 'transparent'; 19550 } 19551 }); 19552 19553 // Save preference 19554 localStorage.setItem('hydra_language', code); 19555 currentLanguage = code; 19556 19557 // Get default language from languages element 19558 const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages'); 19559 const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en'; 19560 19561 if (code === defaultLang) { 19562 // Restore original language 19563 restoreOriginalText(); 19564 } else { 19565 // Translate page 19566 await translatePage(code, defaultLang); 19567 } 19568 }); 19569 19570 async function translatePage(targetLang, sourceLang) { 19571 console.warn(`ð Translating page from ${sourceLang} to ${targetLang}`); 19572 19573 // Elements to translate 19574 const selectors = [ 19575 '.sidebar li a:not(.edit-mode-active .sidebar li a)', // Menu items (not in edit mode) 19576 '.page-text:not(.edit-mode-active .page-text)', // Page content (not in edit mode) 19577 'h1:not(.edit-mode-active h1)', 19578 'h2:not(.edit-mode-active h2)', 19579 'h3:not(.edit-mode-active h3)', 19580 'p:not(.edit-mode-active p)' 19581 ]; 19582 19583 const elements = document.querySelectorAll(selectors.join(', ')); 19584 19585 for (const element of elements) { 19586 // Skip if already translated to this language 19587 if (element.dataset.translatedTo === targetLang) continue; 19588 19589 // Skip if element is empty 19590 const text = element.textContent.trim(); 19591 if (!text || text.length === 0) continue; 19592 19593 // Store original text 19594 if (!originalTexts.has(element)) { 19595 originalTexts.set(element, text); 19596 } 19597 19598 // Check translation cache 19599 const cacheKey = `translate_${text}_${targetLang}`; 19600 let translated = getFromCache(cacheKey); 19601 19602 if (translated) { 19603 element.textContent = translated; 19604 element.dataset.translatedTo = targetLang; 19605 } else { 19606 // Translate 19607 try { 19608 translated = await translate(text, sourceLang, targetLang); 19609 if (translated && translated !== text) { 19610 element.textContent = translated; 19611 element.dataset.translatedTo = targetLang; 19612 saveToCache(cacheKey, translated); 19613 } 19614 } catch (error) { 19615 console.error('Translation error:', error); 19616 } 19617 } 19618 } 19619 } 19620 19621 function restoreOriginalText() {
19622 originalTexts.forEach((originalText, element) => { 19623 if (element && element.isConnected) { 19624 element.textContent = originalText; 19625 delete element.dataset.translatedTo; 19626 } 19627 }); 19628 } 19629 19630 async function translate(text, sourceLang, targetLang) { 19631 if (!text || text.trim().length === 0) return text; 19632 19633 // Try Browser Translation API (Free, Chrome 130+) 19634 if ('translation' in self && 'createTranslator' in self.translation) { 19635 try { 19636 console.warn('ð Using Browser Translation API'); 19637 const translator = await self.translation.createTranslator({ 19638 sourceLanguage: sourceLang, 19639 targetLanguage: targetLang 19640 }); 19641 const result = await translator.translate(text); 19642 console.warn('â Translation successful via Browser API'); 19643 return result; 19644 } catch (error) { 19645 console.warn('â Browser translation failed:', error); 19646 } 19647 } else { 19648 console.warn('â ï¸ Browser Translation API not available'); 19649 console.warn(' To enable in Chrome: chrome://flags/#translation-api'); 19650 console.warn(' OR add Google Translate API key to use paid translation'); 19651 } 19652 19653 // TODO: Add Google Translate API fallback here 19654 // For now, return original text 19655 console.warn('â ï¸ No translation provider configured - returning original text'); 19656 return text; 19657 } 19658 19659 // Cache management 19660 function getFromCache(key) { 19661 try { 19662 const item = localStorage.getItem(`hydra_${key}`); 19663 if (!item) return null; 19664 19665 const { value, expiry } = JSON.parse(item); 19666 if (expiry && Date.now() > expiry) { 19667 localStorage.removeItem(`hydra_${key}`); 19668 return null; 19669 } 19670 return value; 19671 } catch { 19672 return null; 19673 } 19674 } 19675 19676 function saveToCache(key, value) { 19677 try { 19678 const item = { 19679 value, 19680 expiry: Date.now() + (30 * 24 * 60 * 60 * 1000) // 30 days 19681 }; 19682 localStorage.setItem(`hydra_${key}`, JSON.stringify(item)); 19683 } catch (error) { 19684 console.warn('Cache save failed:', error); 19685 } 19686 } 19687 19688 } // End of initializeTranslationSystem 19689})(); 19690</script>
19690 19691 19692 19693
19694<script> 19695window.HYDRA_HELP_ENABLED = true; 19696function loadHelpSystemScript() { 19697 if (window.HelpSystem && window.HelpSystem.openHub) return Promise.resolve(); 19698 return new Promise(function(resolve, reject) { 19699 var existing = document.querySelector('script[data-hydra-help-system]'); 19700 if (existing) { 19701 existing.addEventListener('load', function() { resolve(); }); 19702 existing.addEventListener('error', reject); 19703 return; 19704 } 19705 var s = document.createElement('script'); 19706 s.src = '/api/help-system.js?v=686c85345749'; 19707 s.async = true; 19708 s.setAttribute('data-hydra-help-system', '1'); 19709 s.onload = function() { resolve(); }; 19710 s.onerror = reject; 19711 document.head.appendChild(s); 19712 }); 19713} 19714function openHydraHelp(opts) { 19715 opts = opts || {}; 19716 if (!window.HYDRA_HELP_ENABLED) return; 19717 var token = window.didToken || localStorage.getItem('hydra_auth_token'); 19718 var isAdmin = window.isAdmin || localStorage.getItem('hydra_is_admin') === 'true'; 19719 if (!token || !isAdmin) return; 19720 var p = loadHelpSystemScript(); 19721 Promise.resolve(p).then(function() { 19722 if (window.HelpSystem && window.HelpSystem.init) return window.HelpSystem.init(); 19723 }).then(function() { 19724 if (window.HelpSystem && window.HelpSystem.openHub) { 19725 window.HelpSystem.openHub(opts.initialTab ? { initialTab: opts.initialTab } : undefined); 19726 } 19727 }).catch(function(err) { 19728 console.error('HelpSystem init failed:', err); 19729 }); 19730} 19731window.openHydraHelp = openHydraHelp; 19732</script>
19732 19733
19734<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>
19734 19735</body> 19736</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.