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="52octf5t"> 7 8 <!-- Primary Meta Tags --> 9 <title>Life | Simple Image Page</title> 10 <meta name="description" content="Scroll Horizontally-->"> 11 <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1"> 12 <meta name="author" content="davidcarsonphotos.com"> 13 <meta name="language" content="en"> 14 15 <!-- Canonical URL --> 16 <link rel="canonical" href="https://davidcarsonphotos.com/life"> 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://davidcarsonphotos.com/life"> 27 <meta property="og:title" content="Life | Simple Image Page"> 28 <meta property="og:description" content="Scroll Horizontally-->"> 29 <meta property="og:site_name" content="davidcarsonphotos.com"> 30 <meta property="og:locale" content="en_US"> 31 <meta property="og:image" content="https://cdn.neonsky.app/4bd5ec04c2f91/images/161113-DC-SupermoonSTL.jpg"> 32 <meta property="og:image:secure_url" content="https://cdn.neonsky.app/4bd5ec04c2f91/images/161113-DC-SupermoonSTL.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="Life | Simple Image Page"> 37 38 <!-- Twitter / X Card --> 39 <meta name="twitter:card" content="summary_large_image">
40 <meta name="twitter:site" content="@davidcarsonphotos.com"> 41 <meta name="twitter:url" content="https://davidcarsonphotos.com/life"> 42 <meta name="twitter:title" content="Life | Simple Image Page"> 43 <meta name="twitter:description" content="Scroll Horizontally-->"> 44 <meta name="twitter:image" content="https://cdn.neonsky.app/4bd5ec04c2f91/images/161113-DC-SupermoonSTL.jpg"> 45 <meta name="twitter:image:alt" content="Life | Simple Image Page"> 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="Life | Simple Image Page"> 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": "Life | Simple Image Page", 75 "description": "Scroll Horizontally--\\u003E", 76 "url": "https://davidcarsonphotos.com/life", 77 "datePublished": "2026-10-04T05:56:30.101Z", 78 "dateModified": "2026-10-04T05:56:30.101Z", 79 "inLanguage": "en-US", 80 "isPartOf": { 81 "@type": "WebSite", 82 "@id": "https://davidcarsonphotos.com/#website", 83 "name": "davidcarsonphotos.com", 84 "url": "https://davidcarsonphotos.com/", 85 "description": "Scroll Horizontally--\\u003E", 86 "publisher": { 87 "@type": "Organization", 88 "name": "davidcarsonphotos.com", 89 "url": "https://davidcarsonphotos.com/" 90 } 91 }, 92 "breadcrumb": { 93 "@type": "BreadcrumbList", 94 "itemListElement": [ 95 { 96 "@type": "ListItem", 97 "position": 1, 98 "name": "Home", 99 "item": "https://davidcarsonphotos.com/" 100 }, 101 { 102 "@type": "ListItem", 103 "position": 2, 104 "name": "Life | Simple Image Page",
105 "item": "https://davidcarsonphotos.com/life" 106 } 107 ] 108 }, 109 "associatedMedia": [ 110 { 111 "@type": "ImageObject", 112 "name": "Supermoon St. Louis, Arch", 113 "description": "The supermoon rises over the Arch in St. Louis as seen from the Compton Hill Water Tower on Sunday, Nov. 13, 2016. The supermoon will peak on early Monday morning shortly before sunrise, it will be the nearest supermoon in almost 70 years and will be the largest until Nov. 25, 2034. A supermoon happens when a full moon makes its closest pass to Earth appearing up to 14 percent bigger and 30 percent brighter in the sky. Viewing of the moon should also be good on Monday after the sun sets and the moon rises out of the east.\\nPhoto by David Carson, [email protected]", 114 "caption": "Supermoon St. Louis, Arch", 115 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/161113-DC-SupermoonSTL.jpg" 116 }, 117 { 118 "@type": "ImageObject", 119 "name": "Prayer Shot", 120 "description": "", 121 "caption": "Prayer Shot", 122 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/34-60-08-CarsD-04a.jpg" 123 }, 124 { 125 "@type": "ImageObject", 126 "name": "Memorial Day scouts", 127 "description": "Cub Scout Nick Kollar, 8, from Eureka, places flags in front of head stones at Jefferson Barracks National Cemetery. Kollar was one of more than 5,000 local scouts who showed up to help place flags at the more than 139,000 head stones at the cemetery. This was the 61st annual Memorial Day \\\"Good Turn\\\" event that was sponsored by the Gravois Trail District of the Greater St. Louis Area Council, Boy Scouts of America.", 128 "caption": "Memorial Day scouts", 129 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-Scout-Flags-1a-copy.jpg" 130 }, 131 { 132 "@type": "ImageObject", 133 "name": "Flipping out", 134 "description": "Dwight Jones, 13, tumbles over an old mattress he and his friends were playing with in south St. Louis. Jones says he likes the energy of tumbling and learned his skills from his brother and lots of practice.\\nDavid Carson | Post-Dispatch", 135 "caption": "Flipping out", 136 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/57-CD-DC-Tumble-Kids1.jpg" 137 }, 138 { 139 "@type": "ImageObject", 140 "name": "Baby Robbins", 141 "description": "A Robin comes back to the nest to feed it's brood in the Compton Heights neighborhood of St. Louis. The babies hatched over the weekend and the parents have been busy feeding the three chicks.", 142 "caption": "Baby Robbins", 143 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/a04-CD-DC-Robins-1.jpg" 144 }, 145 { 146 "@type": "ImageObject", 147 "name": "Dock", 148 "description": "Friends Patrick Schertzer, left, Brett Ware, center, and Curtis Vitale take flying leaps into a man made lake at the New Town development in St. Charles County. Tempatures are expected to remain in the upper 80's and lower 90's for the through the weekend.", 149 "caption": "Dock", 150 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/54-CD-DC-New-Town-Kids.jpg" 151 }, 152 { 153 "@type": "ImageObject", 154 "name": "Frosty morning", 155 "description": "Ice crystals form on the windshield of a car in south St. Louis city on Tuesday. Frosty weather is expected again today as temperatures will struggle to get above freezing.", 156 "caption": "Frosty morning", 157 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-Frosty-December-2.jpg" 158 }, 159 { 160 "@type": "ImageObject", 161 "name": "Santa hatch", 162 "description": "After greeting children from the roof of the Hard Rock Cafe Santa Claus, played by Dana Wayne Morris of St. Louis, squeezes down a hatch and into the restaurant to hear the children's Christmas wishes.", 163 "caption": "Santa hatch", 164 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/Portfolio---022.jpg" 165 }, 166 { 167 "@type": "ImageObject", 168 "name": "070413 DC Fireworks 2A", 169 "description": "Fair St. Louis fireworks light up the St. Louis skyline on Thursday, July 4, 2013. This picture was shot from the roof of the Pierre Chouteau building in 4400 Lindell Blvd.", 170 "caption": "Fair St. Louis fireworks light up the St. Louis skyline on Thursday, July 4, 2013. This picture was shot from the roof of the Pierre Chouteau building in 4400 Lindell Blvd.", 171 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/070413-DC-Fireworks-2A.jpg" 172 }, 173 { 174 "@type": "ImageObject", 175 "name": "Memorial Day", 176 "description": "CIVIL WAR RE-ENACTOR Civil War re-enactor Peggie Avery plays the role of a Union Army widow during Memorial Day services at Jefferson Barracks National Cemetery.", 177 "caption": "Memorial Day", 178 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/Portfolio---018.jpg" 179 }, 180 { 181 "@type": "ImageObject", 182 "name": "00011184-GNN-004", 183 "description": "ABES Abraham Lincoln impersonators gather at the enterance to the dedication ceremonies for the Abraham Lincoln Presidential Museum in Springfield, Ill.", 184 "caption": "00011184-GNN-004", 185 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/64-abes-.jpg" 186 }, 187 { 188 "@type": "ImageObject", 189 "name": "Dance BWb", 190 "description": "", 191 "caption": "Dance BWb", 192 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/Dance-BWb.jpg" 193 }, 194 { 195 "@type": "ImageObject", 196 "name": "fashion show", 197 "description": "", 198 "caption": "fashion show", 199 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/fashion-show.jpg" 200 }, 201 { 202 "@type": "ImageObject", 203 "name": "Big Surf waves", 204 "description": "", 205 "caption": "Big Surf waves", 206 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/Big-Surf-waves.jpg" 207 }, 208 { 209 "@type": "ImageObject", 210 "name": "Rainbow hop", 211 "description": "Antonie Baker, 8, leaps through the spray of a cooling station that was set up in front of the Gamble Community Center in St. Louis. An excessive heat warning was issued by the National Weather Service for the St. Louis area.", 212 "caption": "Rainbow hop", 213 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-STL-Heat.jpg" 214 }, 215 { 216 "@type": "ImageObject", 217 "name": "Children of the corn", 218 "description": "Bethalto West Elementary School fourth grader Chris Williams, 9, is buried in the \\\"Corn Crib\\\" play area at the Great Godfrey Maze during a school field trip.", 219 "caption": "Children of the corn", 220 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-CHILDREN-of-Corn.jpg" 221 }, 222 { 223 "@type": "ImageObject", 224 "name": "Graffiti eye", 225 "description": "A graffiti artist from Kansas City works on putting the finishing touches on his work on the flood wall south of the Arch on Wharf Street in St. Louis. About 140 graffiti artist from around the country came to St. Louis to paint the flood wall.", 226 "caption": "Graffiti eye", 227 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/062313-DC-Open-S-20041378.jpg" 228 }, 229 { 230 "@type": "ImageObject", 231 "name": "131203 DC Santa", 232 "description": "Danny Wall, from Fenton, cruises down Interstate 70 West through downtown St. Louis on his way to deliver a present to a three-year-old boy in Granite City on Tuesday, Dec. 3, 2013. \\\"I love seeing the expression of joy on people's faces as I drive by\\\" said Wall who started playing the role of Santa Claus eight years ago when his first grandchildren were born. At first he visited just the homes of friends and family but he has visited
232with groups Boy Scouts, Girl Scouts, and children at Rankin Jordan. He says playing Santa Claus helps him get the Christmas spirit and relieve stress. Wall is a riding a 2008 Honda Goldwing Trike.\\nPhoto By David Carson, [email protected]", 233 "caption": "Danny Wall, from Fenton, cruises down Interstate 70 West through downtown St. Louis on his way to deliver a present to a three-year-old boy in Granite City on Tuesday, Dec. 3, 2013. \\\"I love seeing the expression of joy on people's faces as I drive by\\\" said Wall who started playing the role of Santa Claus eight years ago when his first grandchildren were born. At first he visited just the homes of friends and family but he has visited with groups Boy Scouts, Girl Scouts, and children at Rankin Jordan. He says playing Santa Claus helps him get the Christmas spirit and relieve stress. Wall is a riding a 2008 Honda Goldwing Trike.\\nPhoto By David Carson, [email protected]", 234 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/131203-DC-Santa.jpg" 235 }, 236 { 237 "@type": "ImageObject", 238 "name": "Burlesque", 239 "description": "Sturdy Gurlesque performs one of her burlesque routines that feature fire as part of the Albino Alley Cat Revue on stage at Rue 13 on Washington Ave in St. Louis.", 240 "caption": "Burlesque", 241 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/AE-DC-Burlesque-6.jpg" 242 }, 243 { 244 "@type": "ImageObject", 245 "name": "Hispanics in St. Ann", 246 "description": "Martin McDrano (center) and Alberta Mejia dance to the music at the Acapulco Restaurant and Lounge in St. Ann. Emestina Zegar, owner of the Acapulco, thinks fewer people are frequenting her business because they fear being stopped by the St. Ann police.", 247 "caption": "Hispanics in St. Ann", 248 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-St-Ann-Dancing.jpg" 249 }, 250 { 251 "@type": "ImageObject", 252 "name": "Walking dead", 253 "description": "Mathew Hebert (left), from Midnight Studios out of Mesa, Arizona, stops to talk with Janice Smith, from Ballwin, as he walks the floor of the 17th annual Halloween and Attractions Show at America's Center in St. Louis.", 254 "caption": "Walking dead", 255 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/2012-14-Haunt-1-update.jpg" 256 }, 257 { 258 "@type": "ImageObject", 259 "name": "Swim lessons", 260 "description": "Mekkah Richards, 15, helps her brother Abel Richards, 9, put on his swim cap before the start of event featuring Olympic gold medalist swimmer Cullen Jones at the Mathews Dickey Boys and Girls Club in St. Louis. The event, Make A Splash, put on by the USA Swimming Foundation, is designed to raise awareness about the importance of learning to swim. The program has helped provide more than 845,000 kids with swimming lessons.", 261 "caption": "Swim lessons", 262 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/2012-10-Mathews-Dickey-Swim.jpg" 263 }, 264 { 265 "@type": "ImageObject", 266 "name": "130911 DC OFallo 20215142", 267 "description": "Jeanne Tallman escorts her granddaughter Whitney Weeks, 3, as she hands a coloring she did to O'Fallon police officer Mike Plum after a ceremony honoring first responders in O'Fallon, Mo. on Wednesday, Sept. 11, 2013. \\\"She wanted him (Plum) to know she was thinking about them\\\" said Tallman who said the coloring paid tribute to the fire fighters and police officers who gave their lives on September 11. The City of O'Fallon, Mo. hosted a Patriot Day Ceremony, a Tribute to First Responders by a piece of the twisted steel from the World Trade Center at the Municipal Centre.\\nPhoto By David Carson, [email protected]", 268 "caption": "Jeanne Tallman escorts her granddaughter Whitney Weeks, 3, as she hands a coloring she did to O'Fallon police officer Mike Plum after a ceremony honoring first responders in O'Fallon, Mo. on Wednesday, Sept. 11, 2013. \\\"She wanted him (Plum) to know she was thinking about them\\\" said Tallman who said the coloring paid tribute to the fire fighters and police officers who gave their lives on September 11. The City of O'Fallon, Mo. hosted a Patriot Day Ceremony, a Tribute to First Responders by a piece of the twisted steel from the World Trade Center at the Municipal Centre.\\nPhoto By David Carson, [email protected]", 269 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/130911-DC-OFallo-20215142.jpg" 270 }, 271 { 272 "@type": "ImageObject", 273 "name": "35 60 07 CarsD 01a 2", 274 "description": "During their first introduction the Bouldin family tries to coax a smile out of Dmitry, right, a 5-year-old Russia orphan. Dmitry was assigned to stay with the family for three weeks to see if child and family were a match.", 275 "caption": "During their first introduction the Bouldin family tries to coax a smile out of Dmitry, right, a 5-year-old Russia orphan. Dmitry was assigned to stay with the family for three weeks to see if child and family were a match.", 276 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/35-60-07-CarsD-01a_2.jpg" 277 }, 278 { 279 "@type": "ImageObject", 280 "name": "2012 13 Contraceptive Choice4", 281 "description": "Tuesday June 28, 2011--Malachi Lawrence, 3, plays in the bedroom in his and his mother's, Deidre Woodberry, apartment in north St. Louis County. Woodberry participated in Contraceptive Choice Project trial so
281on after the birth of her son Malachi. Woodberry knew she didn't want to have more kids while she was trying to finish up her college degree.\\nDavid Carson [email protected]", 282 "caption": "Tuesday June 28, 2011--Malachi Lawrence, 3, plays in the bedroom in his and his mother's, Deidre Woodberry, apartment in north St. Louis County. Woodberry participated in Contraceptive Choice Project trial soon after the birth of her son Malachi. Woodberry knew she didn't want to have more kids while she was trying to finish up her college degree.\\nDavid Carson [email protected]", 283 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/2012-13-Contraceptive-Choice4.jpg" 284 }, 285 { 286 "@type": "ImageObject", 287 "name": "53 CD DC Hose Spray 2", 288 "description": "Thursday June 25, 2009--Gerrell Lawson, 12, basks in the spray of a garden hose as he plays with is friends is Hillsdale on Thursday. Thursday marked the eighth straight day of temperatures above 90 and Saturday is expect to top out close to 100 with some cooling into the upper 80s expected for Sunday.\\nDavid Carson [email protected]", 289 "caption": "Thursday June 25, 2009--Gerrell Lawson, 12, basks in the spray of a garden hose as he plays with is friends is Hillsdale on Thursday. Thursday marked the eighth straight day of temperatures above 90 and Saturday is expect to top out close to 100 with some cooling into the upper 80s expected for Sunday.\\nDavid Carson [email protected]", 290 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/53-CD-DC-Hose-Spray-2.jpg" 291 }, 292 { 293 "@type": "ImageObject", 294 "name": "070313 DC Fair STL1", 295 "description": "A Twin Beech airplane circles over the Arch trailing smoke after members of the Liberty Parachute Team jumped out from about 5,000 feet above St. Louis during a practice run for the Fair St. Louis air show on Wednesday, July 3, 2013. This year's fair will feature air shows at 12 p.m. and 4 p.m. on Thursday and Friday and at 5 p.m. on Saturday.\\nPhoto By David Carson, [email protected]", 296 "caption": "A Twin Beech airplane circles over the Arch trailing smoke after members of the Liberty Parachute Team jumped out from about 5,000 feet above St. Louis during a practice run for the Fair St. Louis air show on Wednesday, July 3, 2013. This year's fair will feature air shows at 12 p.m. and 4 p.m. on Thursday and Friday and at 5 p.m. on Saturday.\\nPhoto By David Carson, [email protected]", 297 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/070313-DC-Fair-STL1.jpg" 298 }, 299 { 300 "@type": "ImageObject", 301 "name": "a11 CD DC Glowing Globesb", 302 "description": "Sunday January 31, 2010--Glowing globes fill the splash and spray water fountain area of Citygarden, a public sculpture park in downtown St. Louis, on Sunday. The 25 globes are each lit by two LED lights and are programed to glow in an every changing a rainbow of hues. The globes will be turned on every night through the end of February from 5pm-midnight. The globes were installed by Hydro Dramatics, a St. Louis fountain design company. To see more photos of the glowing globes go to stltoday.com/multimedia .\\nDavid Carson [email protected]", 303 "caption": "Sunday January 31, 2010--Glowing globes fill the splash and spray water fountain area of Citygarden, a public sculpture park in downtown St. Louis, on Sunday. The 25 globes are each lit by two LED lights and are programed to glow in an every changing a rainbow of hues. The globes will be turned on every night through the end of February from 5pm-midnight. The globes were installed by Hydro Dramatics, a St. Louis fountain design company. To see more photos of the glowing globes go to stltoday.com/multimedia .\\nDavid Carson [email protected]", 304 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/a11-CD-DC-Glowing-Globesb.jpg" 305 }, 306 { 307 "@type": "ImageObject", 308 "name": "Portfolio 013", 309 "description": "Wednesday August 24, 2005--Honey bees flock to a field sun flowers off of Illinois Route 159 in Maryville on Wednesday. Temperatures should remain in the 80's for the rest of the week and weekend with a chance of storms through Saturday.\\nPHOTO BY DAVID CARSON/PD", 310 "caption": "Wednesday August 24, 2005--Honey bees flock to a field sun flowers off of Illinois Route 159 in Maryville on Wednesday. Temperatures should remain in the 80's for the rest of the week and weekend with a chance of storms through Saturday.\\nPHOTO BY DAVID CARSON/PD", 311 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/Portfolio---013.jpg" 312 }, 313 { 314 "@type": "ImageObject", 315 "name": "Portfolio 012", 316 "description": "A meteor streaks across the heavens above a farm field in Highland Illinois during the Leonid meteor shower. The field and tree are illuminated by light of from the full moon.", 317 "caption": "A meteor streaks across the heavens above a farm field in Highland Illinois during the Leonid meteor shower. The field and tree are illuminated by light of from the full moon.", 318 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/Portfolio---012.jpg" 319 }, 320 { 321 "@type": "ImageObject", 322 "name": "CD DC Slip Slide", 323 "description": "Grace Wickham (front), Rachel Hutson (center), Matt Wickham, and Reilly Newton get some help from Noah Cochran (left) as he hoses down the plastic sheeting while they launch themselves down a homemade Slip 'n Slide during a party at the Wickham's home in Weldon Spring on Monday May 28, 2012. The Wickhams made the slide from a 100 foot roll of plastic sheeting, some sprinklers to wet the slide and dish soap to make it slippery.\\nPhoto By David Carson, [email protected]", 324 "caption": "Grace Wickham (front), Rachel Hutson (center), Matt Wickham, and Reilly Newton get some help from Noah Cochran (left) as he hoses down the plastic sheeting while they launch themselves down a homemade Slip 'n Slide during a party at the Wickham's home in Weldon Spring on Monday May 28, 2012. The Wickhams made the slide from a 100 foot roll of plastic sheeting, some sprinklers to wet the slide and dish soap to make it slippery.\\nPhoto By David Carson, [email protected]", 325 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-Slip-Slide.jpg" 326 }, 327 { 328 "@type": "ImageObject", 329 "name": "CD DC SWIMMING HOLE 1", 330 "description": "Monday August 5, 2002--Jake Whitehead, 16, of Edwardsville, launches himself off a rope swing into a pond as his friend Shaun Fink, 16, of Glen Carbon wat
330ches. \\\"We're just hanging out, cooling off and having fun\\\" said Fink of their mid-afternoon dip in a swimming hole off of Oaklawn Road in Glen Carbon on Monday. Tempature will cool off into the upper 80's for Tuesday but return to the mid-90's later in the week.\\nPhoto By David Carson/PD", 331 "caption": "CD DC SWIMMING HOLE 1", 332 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/51-Portfolio---002.jpg" 333 }, 334 { 335 "@type": "ImageObject", 336 "name": "Arch Lightning", 337 "description": "Lightning streaks across the sky above the Gateway Memorial Arch in downtown St. Louis on Saturday.", 338 "caption": "Arch Lightning", 339 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/Portfolio---011.jpg" 340 }, 341 { 342 "@type": "ImageObject", 343 "name": "00035114-ENT-001", 344 "description": "Terrance Bullock, of St. Louis, walks under the steam spouting from the lions head on the Bee Hat Lofts along N. 11th Street on Thursday. Freezing temperatures with day time highs in the teens are expected to continue for the next three days. The steam was added to the building as an ornamental detail when it was renovated into lofts from 2005-2006.", 345 "caption": "00035114-ENT-001", 346 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/00035114-ENT-001.jpg" 347 }, 348 { 349 "@type": "ImageObject", 350 "name": "9912CircusElephant", 351 "description": "", 352 "caption": "9912CircusElephant", 353 "contentUrl": "https://cdn.neonsky.app/4bd5ec04c2f91/images/9912CircusElephant.jpg" 354 } 355 ], 356 "numberOfItems": 35, 357 "primaryImageOfPage": { 358 "@type": "ImageObject", 359 "url": "https://cdn.neonsky.app/4bd5ec04c2f91/images/161113-DC-SupermoonSTL.jpg", 360 "width": 1200, 361 "height": 630, 362 "caption": "Life | Simple Image Page" 363 }, 364 "image": { 365 "@type": "ImageObject", 366 "url": "https://cdn.neonsky.app/4bd5ec04c2f91/images/161113-DC-SupermoonSTL.jpg", 367 "width": 1200, 368 "height": 630, 369 "caption": "Life | Simple Image Page" 370 } 371} 372 </script>
372 373 374 375 <!-- Preview mode helper script --> 376
376<script> 377 // Immediately execute to patch request methods before any other script runs 378 (function() { 379 // Detect preview mode by checking hostname 380 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 381 382 if (isPreviewMode) { 383 console.log("[Preview Helper] Preview mode detected"); 384 385 // Extract GUID from URL path (first segment) 386 const pathParts = window.location.pathname.split('/').filter(Boolean); 387 const previewGuid = pathParts.length > 0 ? pathParts[0] : ''; 388 389 if (!previewGuid) { 390 console.warn("[Preview Helper] No GUID found in path:", window.location.pathname); 391 return; // Exit if no GUID found 392 } 393 394 console.log("[Preview Helper] Using GUID:", previewGuid); 395 396 // Store these in window for other scripts 397 window.isPreviewMode = true; 398 window.previewGuid = previewGuid; 399 window.isHydraEnvironment = true; 400 window.Parameters = window.Parameters || {}; 401 window.Parameters.isHydra = true; 402 403 // Helper function that adds the GUID to API URLs and relative links 404 window.getApiUrl = function(endpoint) { 405 // For relative paths 406 if (endpoint.startsWith('/api/')) { 407 const newUrl = '/' + previewGuid + endpoint; 408 console.log("[Preview Helper] Transformed API URL:", endpoint, "â", newUrl); 409 return newUrl; 410 } 411 return endpoint; 412 }; 413 414 // Helper function that adds the GUID to any relative link 415 window.getPreviewUrl = function(url) { 416 // For relative paths (starts with /) that aren't already prefixed with GUID 417 if (url.startsWith('/') && !url.startsWith('/' + previewGuid + '/')) { 418 const newUrl = '/' + previewGuid + url; 419 console.log("[Preview Helper] Transformed relative link:", url, "â", newUrl); 420 return newUrl; 421 } 422 return url; 423 }; 424 425 // PATCH FETCH API with enhanced URL handling 426 const originalFetch = window.fetch; 427 window.fetch = function(resource, init) { 428 // Only transform string URLs 429 if (typeof resource === 'string') { 430 // Case 1: Handle relative paths starting with /api/ 431 if (resource.startsWith('/api/')) { 432 const newUrl = window.getApiUrl(resource); 433 console.log("[Preview Helper] Patched fetch call (API):", resource, "â", newUrl); 434 return originalFetch(newUrl, init); 435 } 436 437 // Case 2: Handle other relative paths (for content/gallery links) 438 else if (resource.startsWith('/') && !resource.startsWith('/' + previewGuid + '/')) { 439 const newUrl = window.getPreviewUrl(resource); 440 console.log("[Preview Helper] Patched fetch call (relative):", resource, "â", newUrl); 441 return originalFetch(newUrl, init); 442 } 443 444 // Case 3: Handle absolute URLs to the same domain containing /api/ 445 else if (resource.includes(window.location.host) && resource.includes('/api/')) { 446 try { 447 const urlObj = new URL(resource); 448 if (urlObj.hostname === window.location.hostname) { 449 const path = urlObj.pathname; 450 // Already has /{guid}/api/... (e.g. from new URL(getApiUrl(...), origin)) â do not double-prefix 451 if (path.startsWith('/' + previewGuid + '/')) { 452 return originalFetch(resource, init); 453 } 454 const newPath = '/' + previewGuid + path; 455 urlObj.pathname = newPath; 456 const newUrl = urlObj.toString(); 457 console.log("[Preview Helper] Patched fetch call (absolute API):", resource, "â", newUrl); 458 return originalFetch(newUrl, init); 459 } 460 } catch (e) { 461 console.error("[Preview Helper] Error processing URL:", e); 462 } 463 } 464 465 // Case 4: Handle absolute URLs to the same domain (non-API) 466 else if (resource.includes(window.location.host)) { 467 try { 468 const urlObj = new URL(resource); 469 if (urlObj.hostname === window.location.hostname && 470 !urlObj.pathname.startsWith('/' + previewGuid + '/')) { 471 const path = urlObj.pathname; 472 const newPath = '/' + previewGuid + path; 473 urlObj.pathname = newPath; 474 const newUrl = urlObj.toString(); 475 console.log("[Preview Helper] Patched fetch call (absolute):", resource, "â", newUrl); 476 return originalFetch(newUrl, init); 477 } 478 } catch (e) { 479 console.error("[Preview Helper] Error processing URL:", e); 480 } 481 } 482 } 483 return originalFetch(resource, init); 484 }; 485 console.log("[Preview Helper] Patched fetch API"); 486 487 // PATCH XMLHttpRequest with enhanced URL handling 488 const originalOpen = XMLHttpRequest.prototype.open; 489 XMLHttpRequest.prototype.open = function(method, url, ...rest) { 490 // Only transform string URLs 491 if (typeof url === 'string') { 492 // Case 1: Handle relative paths starting with /api/ 493 if (url.startsWith('/api/')) { 494 const newUrl = window.getApiUrl(url); 495 console.log("[Preview Helper] Patched XMLHttpRequest (API):", url, "â", newUrl); 496 return originalOpen.call(this, method, newUrl, ...rest); 497 } 498 499 // Case 2: Handle other relative paths 500 else if (url.startsWith('/') && !url.startsWith('/' + previewGuid + '/')) { 501 const newUrl = window.getPreviewUrl(url); 502 console.log("[Preview Helper] Patched XMLHttpRequest (relative):", url, "â", newUrl); 503 return originalOpen.call(this, method, newUrl, ...rest); 504 } 505 506 // Case 3: Handle absolute URLs to the same domain containing /api/ 507 else if (url.includes(window.location.host) && url.includes('/api/')) { 508 try { 509 const urlObj = new URL(url); 510 if (urlObj.hostname === window.location.hostname) { 511 const path = urlObj.pathname; 512 if (path.startsWith('/' + previewGuid + '/')) { 513 return originalOpen.call(this, method, url, ...rest); 514 } 515 const newPath = '/' + previewGuid + path; 516 urlObj.pathname = newPath; 517 const newUrl = urlObj.toString(); 518 console.log("[Preview Helper] Patched XMLHttpRequest (absolute API):", url, "â", newUrl); 519 return originalOpen.call(this, method, newUrl, ...rest); 520 } 521 } catch (e) { 522 console.error("[Preview Helper] Error processing URL:", e); 523 } 524 } 525 526 // Case 4: Handle absolute URLs to the same domain (non-API) 527 else if (url.includes(window.location.host)) { 528 try { 529 const urlObj = new URL(url); 530 if (urlObj.hostname === window.location.hostname && 531 !urlObj.pathname.startsWith('/' + previewGuid + '/')) { 532 const path = urlObj.pathname; 533 const newPath = '/' + previewGuid + path; 534 urlObj.pathname = newPath; 535 const newUrl = urlObj.toString(); 536 console.log("[Preview Helper] Patched XMLHttpRequest (absolute):", url, "â", newUrl); 537 return originalOpen.call(this, method, newUrl, ...rest); 538 } 539 } catch (e) { 540 console.error("[Preview Helper] Error processing URL:", e); 541 } 542 } 543 } 544 return originalOpen.call(this, method, url, ...rest); 545 }; 546 console.log("[Preview Helper] Patched XMLHttpRequest"); 547 548 // Note: Link click handling is done by the existing gallery link interceptor 549 // The fetch and XMLHttpRequest patches above handle programmatic requests 550 551 // Add debugging aid 552 window.debugPreviewUrls = function() { 553 return { 554 hostname: window.location.hostname, 555 pathname: window.location.pathname, 556 guid: window.previewGuid, 557 isPreviewMode: window.isPreviewMode, 558 sampleTransformations: { 559 relativeApi: window.getApiUrl('/api/menu-styles'), 560 relativeLink: window.getPreviewUrl('/gallery'), 561 absolute: new URL('/api/save-config', window.location.origin).toString().replace('/api/', '/' + previewGuid + '/api/') 562 } 563 }; 564 }; 565 566 // Test the transformation immediately 567 console.log("[Preview Helper] Test API transformation:", window.getApiUrl('/api/menu-styles')); 568 console.log("[Preview Helper] Test relative link transformation:", window.getPreviewUrl('/gallery')); 569 console.log("[Preview Helper] Test absolute URL transformation:", 570 new URL('/api/save-config', window.location.origin).toString().replace('/api/', '/' + previewGuid + '/api/')); 571 } 572 })(); 573 </script>
573 574 575 576 577 578<!-- Early CDN retry helpers: MUST run before CDN script/link tags. 579 Changing script.src after a load error is unreliable (esp. Firefox defer); 580 always insert a new element pointing at /cdn-proxy/. -->
581<script> 582(function() { 583 window.useCdnProxy = window.useCdnProxy || false; 584 window.__hydraCdnRetryLog = window.__hydraCdnRetryLog || []; 585 function toProxy(url) { 586 if (!url || url.indexOf('cdn.neonsky.app') === -1) return url; 587 return url.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 588 } 589 function noteRetry(kind, fromUrl, toUrl) { 590 try { 591 window.useCdnProxy = true; 592 var entry = { kind: kind, from: fromUrl, to: toUrl, t: Date.now() }; 593 window.__hydraCdnRetryLog.push(entry); 594 console.warn('ð CDN failed, retrying via new element:', kind, fromUrl, '->', toUrl); 595 } catch (e) {} 596 } 597 window.__hydraRetryCdnScript = function(el) { 598 try { 599 if (!el || el.getAttribute('data-retried') === 'true') return; 600 var original = el.src || ''; 601 if (original.indexOf('cdn.neonsky.app') === -1) return; 602 el.setAttribute('data-retried', 'true'); 603 el.onerror = null; 604 var proxyUrl = toProxy(original); 605 noteRetry('script', original, proxyUrl); 606 var s = document.createElement('script'); 607 s.src = proxyUrl; 608 s.setAttribute('data-retried', 'true'); 609 if (el.id) s.id = el.id; 610 // Dynamic scripts ignore defer; omit it so the proxy copy executes on load. 611 if (el.parentNode) { 612 el.parentNode.insertBefore(s, el.nextSibling); 613 } else { 614 (document.head || document.documentElement).appendChild(s); 615 } 616 } catch (e) {} 617 }; 618 window.__hydraRetryCdnLink = function(el) { 619 try { 620 if (!el || el.getAttribute('data-retried') === 'true') return; 621 var original = el.href || ''; 622 if (original.indexOf('cdn.neonsky.app') === -1) return; 623 el.setAttribute('data-retried', 'true'); 624 el.onerror = null; 625 var proxyUrl = toProxy(original); 626 noteRetry('link', original, proxyUrl); 627 var link = document.createElement('link'); 628 link.rel = el.rel || 'stylesheet'; 629 if (el.id) link.id = el.id; 630 link.href = proxyUrl; 631 link.setAttribute('data-retried', 'true'); 632 if (el.parentNode) { 633 el.parentNode.insertBefore(link, el.nextSibling); 634 } else { 635 (document.head || document.documentElement).appendChild(link); 636 } 637 } catch (e) {} 638 }; 639})(); 640</script>
640 641<link rel="stylesheet" href="https://cdn.neonsky.app/menu-styles-v260923-002.css?t=1791093390101" onerror="window.__hydraRetryCdnLink(this)">
642<script src="https://assets.lemonsqueezy.com/lemon.js" defer></script>
vendor: 1 bytes, line 642
642
643<script src="https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js"></script>
vendor: 1 bytes, line 643
643
644<script src="https://cdn.jsdelivr.net/npm/magic-sdk"></script>
vendor: 1 bytes, line 644
644
645<script src="https://cdn.jsdelivr.net/npm/@magic-ext/auth"></script>
vendor: 1 bytes, line 645
645
646<script src="https://cdn.neonsky.app/style-customizer-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
646 647<link rel="stylesheet" href="https://cdn.neonsky.app/palette-explorer-v260923-002.css?t=1791093390101" onerror="window.__hydraRetryCdnLink(this)">
648<script src="https://cdn.neonsky.app/palette-explorer-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 648
648
649<script src="https://cdn.neonsky.app/sidebar-manager-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 649
649
650<script src="https://cdn.neonsky.app/image-processor-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 650
650
651<script src="https://cdn.neonsky.app/image-uploader-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 651
651
652<script src="https://cdn.neonsky.app/page-layout-presets-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 652
652
653<script src="https://cdn.neonsky.app/site-graph-presets-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 653
653
654<script src="https://cdn.neonsky.app/site-graph-sync-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
vendor: 1 bytes, line 654
654
655<script src="https://cdn.neonsky.app/page-manager-v260923-002.js?t=1791093390101" defer onerror="window.__hydraRetryCdnScript(this)"></script>
655 656<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css"> 657<link rel="stylesheet" href="https://cdn.neonsky.app/page-styles-v260923-002.css?t=1791093390101" onerror="window.__hydraRetryCdnLink(this)"> 658 659 660 661<!-- CDN Fallback Handler for CSS and JS files -->
662<script> 663(function() { 664 // Global flag: Set to true when any CDN resource fails and uses proxy 665 window.useCdnProxy = window.useCdnProxy || false; 666 667 // Global base URL function: Returns proxy base if flag is set, otherwise CDN base 668 window.getCdnBaseUrl = function() { 669 return window.useCdnProxy 670 ? window.location.origin + '/cdn-proxy/' 671 : 'https://cdn.neonsky.app/'; 672 }; 673 674 // Utility function to convert CDN URLs to proxy URLs 675 window.convertCdnToProxyUrl = function(cdnUrl) { 676 if (!cdnUrl || !cdnUrl.includes('cdn.neonsky.app')) return cdnUrl; 677 return cdnUrl.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 678 }; 679 680 // Utility function to construct CDN URLs (uses proxy if flag is set) 681 window.buildCdnUrl = function(path) { 682 return window.getCdnBaseUrl() + path; 683 }; 684 685 // Resource tracking for debugging 686 window._resourceTracking = window._resourceTracking || { 687 resources: [], 688 proxyFlagChanges: [], 689 imageLoads: [], 690 networkErrors: [] 691 }; 692 693 // Track when proxy flag changes - use a wrapper function 694 let proxyFlagChangeCount = 0; 695 let _useCdnProxyValue = window.useCdnProxy || false; 696 697 // Create a setter function that tracks changes 698 window._setUseCdnProxy = function(newValue, reason) { 699 const oldValue = _useCdnProxyValue; 700 if (oldValue !== newValue) { 701 proxyFlagChangeCount++; 702 const changeInfo = { 703 timestamp: new Date().toISOString(), 704 changeNumber: proxyFlagChangeCount, 705 oldValue: oldValue, 706 newValue: newValue, 707 reason: reason || 'Unknown', 708 stack: new Error().stack?.split('\n').slice(1, 6).join('\n') || 'No stack' 709 }; 710 window._resourceTracking.proxyFlagChanges.push(changeInfo); 711 console.error('ð PROXY_FLAG_CHANGE:', JSON.stringify(changeInfo, null, 2)); 712 // Keep only last 50 changes 713 if (window._resourceTracking.proxyFlagChanges.length > 50) { 714 window._resourceTracking.proxyFlagChanges.shift(); 715 } 716 _useCdnProxyValue = newValue; 717 window.useCdnProxy = newValue; 718 } 719 }; 720 721 // Track resource loading 722 function trackResource(url, type, status, details) { 723 const entry = { 724 timestamp: new Date().toISOString(), 725 url: url, 726 type: type, // 'script', 'link', 'image', etc. 727 status: status, // 'loading', 'success', 'failed', 'proxy', 'storage' 728 details: details || {}, 729 useCdnProxy: window.useCdnProxy 730 }; 731 window._resourceTracking.resources.push(entry); 732 console.error('ð RESOURCE_TRACK:', JSON.stringify(entry, null, 2)); 733 // Keep only last 200 resources 734 if (window._resourceTracking.resources.length > 200) { 735 window._resourceTracking.resources.shift(); 736 } 737 } 738 739 // Track image loads 740 function trackImageLoad(url, status, source) { 741 const entry = { 742 timestamp: new Date().toISOString(), 743 url: url, 744 status: status, // 'loading', 'success', 'failed' 745 source: source, // 'cdn', 'proxy', 'storage' 746 useCdnProxy: window.useCdnProxy 747 }; 748 window._resourceTracking.imageLoads.push(entry); 749 console.error('ð¼ï¸ IMAGE_LOAD:', JSON.stringify(entry, null, 2)); 750 // Keep only last 100 image loads 751 if (window._resourceTracking.imageLoads.length > 100) { 752 window._resourceTracking.imageLoads.shift(); 753 } 754 } 755 756 // Track network errors 757 function trackNetworkError(url, error, context) { 758 const entry = { 759 timestamp: new Date().toISOString(), 760 url: url, 761 error: { 762 message: error?.message || 'Unknown error', 763 name: error?.name || 'Unknown', 764 type: error?.type || 'unknown', 765 toString: String(error) 766 }, 767 context: context || {}, 768 useCdnProxy: window.useCdnProxy 769 }; 770 window._resourceTracking.networkErrors.push(entry); 771 console.error('â NETWORK_ERROR:', JSON.stringify(entry, null, 2)); 772 // Keep only last 100 errors 773 if (window._resourceTracking.networkErrors.length > 100) { 774 window._resourceTracking.networkErrors.shift(); 775 } 776 } 777 778 // Expose tracking functions globally 779 window.trackResource = trackResource; 780 window.trackImageLoad = trackImageLoad; 781 window.trackNetworkError = trackNetworkError; 782 783 // Function to retry failed resources via proxy 784 // For ISP-affected clients, we assume CDN failures are network/SSL errors and set the proxy flag 785 // The proxy can handle both network errors and 404s, so it's safer to err on the side of using it 786 function retryViaProxy(element, originalSrc, errorEvent) { 787 if (!originalSrc) originalSrc = element.href || element.src; 788 if (!originalSrc || !originalSrc.includes('cdn.neonsky.app')) return; 789 790 // Prevent multiple retries 791 if (element.getAttribute('data-retried')) { 792 return; 793 } 794 element.setAttribute('data-retried', 'true'); 795 796 // Check if element still exists in DOM 797 if (!element.parentNode) { 798 console.warn('â ï¸ Element no longer in DOM, skipping proxy retry:', originalSrc); 799 return; 800 } 801 802 // For ISP-affected clients, CDN failures are almost always network/SSL errors, not 404s 803 // We'll be aggressive and set the flag for any CDN failure to ensure images also use proxy 804 // Check error message if available, but default to assuming network error 805 const hasExplicitNetworkError = errorEvent && errorEvent.message && ( 806 errorEvent.message.includes('Failed to fetch') || 807 errorEvent.message.includes('SSL') || 808 errorEvent.message.includes('secure connection') || 809 errorEvent.message.includes('network') || 810 errorEvent.message.includes('ERR_') ||
811 errorEvent.message.includes('SSL_ERROR') 812 ); 813 814 // Track the error 815 trackNetworkError(originalSrc, errorEvent, { 816 elementType: element.tagName, 817 hasExplicitNetworkError: hasExplicitNetworkError, 818 errorMessage: errorEvent?.message || 'No error message', 819 errorName: errorEvent?.name || 'No error name' 820 }); 821 822 // Set flag if we have explicit network error, or if errorEvent is missing/null (browser silent failure) 823 // This covers ISP-affected clients where SSL handshake fails before HTTP request 824 const reason = hasExplicitNetworkError 825 ? 'Explicit network/SSL error detected' 826 : (!errorEvent || !errorEvent.message) 827 ? 'Browser silent failure (likely SSL/network)' 828 : 'CDN resource failed (using proxy as fallback)'; 829 830 if (hasExplicitNetworkError || !errorEvent || !errorEvent.message) { 831 window._setUseCdnProxy(true, reason); 832 console.error('ð© CDN_PROXY_FLAG SET: CDN failure detected, using proxy for all subsequent requests'); 833 console.error(' Error details:', errorEvent ? (errorEvent.message || 'No error message (likely SSL/network failure)') : 'No error event'); 834 } else { 835 // Even for other errors, set the flag to be safe - proxy can handle 404s too 836 window._setUseCdnProxy(true, reason); 837 console.error('ð© CDN_PROXY_FLAG SET: CDN resource failed, using proxy as fallback'); 838 } 839 840 const proxyUrl = originalSrc.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 841 console.error('ð CDN failed, retrying via proxy:', originalSrc, '->', proxyUrl); 842 843 // Track the retry 844 trackResource(originalSrc, element.tagName.toLowerCase(), 'failed', { 845 retryUrl: proxyUrl, 846 error: errorEvent?.message || 'Unknown' 847 }); 848 849 if (element.tagName === 'SCRIPT') { 850 const newScript = document.createElement('script'); 851 newScript.src = proxyUrl; 852 // Do not copy defer/async onto dynamically inserted scripts. After a CDN 853 // failure, DOMContentLoaded may already have fired; deferred dynamic 854 // scripts can fail to execute (seen on Firefox ISP/CDN-fail clients). 855 newScript.setAttribute('data-retried', 'true'); 856 newScript.onload = function() { 857 trackResource(proxyUrl, 'script', 'proxy-success', { originalUrl: originalSrc }); 858 }; 859 newScript.onerror = function() { 860 trackResource(proxyUrl, 'script', 'proxy-failed', { originalUrl: originalSrc }); 861 console.error('â Proxy also failed, trying storage:', proxyUrl); 862 const storageUrl = originalSrc.replace('cdn.neonsky.app', 'storage.neonsky.app'); 863 trackResource(storageUrl, 'script', 'storage-fallback', { originalUrl: originalSrc, proxyUrl: proxyUrl }); 864 newScript.src = storageUrl; 865 newScript.onload = function() { 866 trackResource(storageUrl, 'script', 'storage-success', { originalUrl: originalSrc }); 867 }; 868 }; 869 element.parentNode.replaceChild(newScript, element); 870 } else if (element.tagName === 'LINK') { 871 const newLink = document.createElement('link'); 872 newLink.rel = element.rel; 873 newLink.href = proxyUrl; 874 newLink.setAttribute('data-retried', 'true'); 875 newLink.onload = function() { 876 trackResource(proxyUrl, 'link', 'proxy-success', { originalUrl: originalSrc }); 877 }; 878 newLink.onerror = function() { 879 trackResource(proxyUrl, 'link', 'proxy-failed', { originalUrl: originalSrc }); 880 console.error('â Proxy also failed, trying storage:', proxyUrl); 881 const storageUrl = originalSrc.replace('cdn.neonsky.app', 'storage.neonsky.app'); 882 trackResource(storageUrl, 'link', 'storage-fallback', { originalUrl: originalSrc, proxyUrl: proxyUrl }); 883 newLink.href = storageUrl; 884 newLink.onload = function() { 885 trackResource(storageUrl, 'link', 'storage-success', { originalUrl: originalSrc }); 886 }; 887 }; 888 element.parentNode.replaceChild(newLink, element); 889 } 890 } 891 892 // Monitor script and link tags for errors 893 document.addEventListener('error', function(e) { 894 const target = e.target; 895 if ((target.tagName === 'SCRIPT' || target.tagName === 'LINK') && 896 target.src && target.src.includes('cdn.neonsky.app')) { 897 retryViaProxy(target, target.src, e); 898 } else if (target.tagName === 'LINK' && target.href && target.href.includes('cdn.neonsky.app')) { 899 retryViaProxy(target, target.href, e); 900 } 901 }, true); 902 903 // Also check existing scripts/links that may have failed before this script loaded 904 // For ISP-affected clients, we assume CDN failures are network/SSL errors 905 setTimeout(function() { 906 document.querySelectorAll('script[src*="cdn.neonsky.app"], link[href*="cdn.neonsky.app"]').forEach(function(el) { 907 if (el.tagName === 'SCRIPT' && !el.getAttribute('data-loaded') && !el.getAttribute('data-retried')) { 908 el.addEventListener('error', function(errorEvent) { 909 // For ISP-affected clients, assume any CDN failure is a network/SSL error 910 // The proxy can handle both network errors and 404s 911 retryViaProxy(el, el.src, errorEvent); 912 }); 913 } else if (el.tagName === 'LINK' && !el.sheet && !el.getAttribute('data-loaded') && !el.getAttribute('data-retried')) { 914 // CSS link failed if it has no stylesheet - check after a delay 915 setTimeout(function() { 916 if (!el.sheet && !el.getAttribute('data-retried')) { 917 // For ISP-affected clients, assume any CDN failure is a network/SSL error 918 retryViaProxy(el, el.href, null); 919 } 920 }, 2000); 921 } 922 }); 923 }, 100); 924})(); 925</script>
925 926 927<!-- Log Capture System - S+J to download logs (works even if gallery doesn't load) -->
928<script> 929(function() { 930 'use strict'; 931 932 // Only initialize if not already initialized 933 if (window._indexLogCaptureInitialized) { 934 return; 935 } 936 window._indexLogCaptureInitialized = true; 937 938 // Wait for DOM to be ready 939 function initLogCapture() { 940 // Store original console methods 941 const originalConsole = { 942 log: console.log, 943 warn: console.warn, 944 error: console.error, 945 info: console.info 946 }; 947 948 // Array to store all console messages 949 window._capturedLogs = window._capturedLogs || []; 950 951 // Override console methods to capture logs 952 ['log', 'warn', 'error', 'info'].forEach(method => { 953 console[method] = function(...args) { 954 // Call original method 955 originalConsole[method].apply(console, args); 956 957 // Capture the log with timestamp 958 window._capturedLogs.push({ 959 timestamp: new Date().toISOString(), 960 level: method.toUpperCase(), 961 message: args.map(function(arg) { 962 if (arg !== null && arg !== undefined && Object(arg) === arg) { 963 try { 964 return JSON.stringify(arg, null, 2); 965 } catch (e) { 966 return String(arg); 967 } 968 } 969 return String(arg); 970 }).join(' ') 971 }); 972 973 // Keep only last 2000 logs to prevent memory issues 974 if (window._capturedLogs.length > 2000) { 975 window._capturedLogs.shift(); 976 } 977 }; 978 }); 979 980 // Add keyboard shortcut to download logs (S+J) 981 let pressedKeys = new Set(); 982 983 document.addEventListener('keydown', function(event) { 984 const key = (event.key || '').toLowerCase(); 985 986 if (key === 's' || key === 'j') { 987 pressedKeys.add(key); 988 989 // Check if both S and J are pressed 990 if (pressedKeys.has('s') && pressedKeys.has('j')) { 991 event.preventDefault(); 992 console.log('S+J detected - downloading logs...'); 993 994 // Show a brief visual indicator 995 const indicator = document.createElement('div'); 996 indicator.style.cssText = 997 'position: fixed; top: 20px; right: 20px; z-index: 10000; ' + 998 'background: #4CAF50; color: white; padding: 10px 20px; ' + 999 'border-radius: 5px; font-family: Arial, sans-serif; ' + 1000 'font-size: 14px; font-weight: bold; ' + 1001 'box-shadow: 0 2px 10px rgba(0,0,0,0.3);'; 1002 indicator.textContent = 'ð¥ Downloading logs...'; 1003 document.body.appendChild(indicator); 1004 1005 setTimeout(() => { 1006 if (indicator.parentNode) { 1007 indicator.parentNode.removeChild(indicator); 1008 } 1009 }, 2000); 1010 1011 downloadLogs(); 1012 // Clear the set after triggering 1013 pressedKeys.clear(); 1014 } 1015 } 1016 }); 1017 1018 document.addEventListener('keyup', function(event) { 1019 const key = (event.key || '').toLowerCase(); 1020 if (key === 's' || key === 'j') { 1021 pressedKeys.delete(key); 1022 } 1023 }); 1024 1025 console.log('ð Log capture system initialized. Press S+J simultaneously to download logs.'); 1026 } 1027 1028 // Download captured logs as a text file 1029 function downloadLogs() { 1030 try { 1031 // Get all scripts and links, categorized 1032 const allScripts = Array.from(document.querySelectorAll('script[src]')).map(s => s.src); 1033 const allLinks = Array.from(document.querySelectorAll('link[href]')).map(l => l.href); 1034 1035 // Get additional debug info 1036 const debugInfo = { 1037 url: window.location.href, 1038 userAgent: navigator.userAgent, 1039 timestamp: new Date().toISOString(), 1040 useCdnProxy: window.useCdnProxy || false, 1041 windowLocation: window.location.toString(), 1042 documentReadyState: document.readyState, 1043 // All scripts/links (not just CDN) 1044 allScriptsLoaded: allScripts, 1045 allLinksLoaded: allLinks, 1046 // CDN-specific (for backward compatibility and CDN debugging) 1047 scriptsLoaded: allScripts.filter(src => src.includes('cdn.neonsky.app')), 1048 linksLoaded: allLinks.filter(href => href.includes('cdn.neonsky.app')), 1049 // Gallery scripts (loaded from Worker) 1050 galleryScriptsLoaded: allScripts.filter(src => src.includes('/gallery-scripts/')), 1051 galleryLinksLoaded: allLinks.filter(href => href.includes('/gallery-scripts/')), 1052 // Failed resources 1053 failedScripts: Array.from(document.querySelectorAll('script[src*="cdn.neonsky.app"][data-retried]')).map(s => s.src), 1054 failedLinks: Array.from(document.querySelectorAll('link[href*="cdn.neonsky.app"][data-retried]')).map(l => l.href), 1055 // Proxy resources 1056 proxyScriptsLoaded: allScripts.filter(src => src.includes('/cdn-proxy/')), 1057 proxyLinksLoaded: allLinks.filter(href => href.includes('/cdn-proxy/')), 1058 // Resource tracking data 1059 resourceTracking: window._resourceTracking || { 1060 resources: [], 1061 proxyFlagChanges: [], 1062 imageLoads: [], 1063 networkErrors: [] 1064 }, 1065 // Performance timing 1066 performanceTiming: performance.timing ? { 1067 navigationStart: performance.timing.navigationStart, 1068 domContentLoaded: performance.timing.domContentLoadedEventEnd - performance.timing.navigationStart, 1069 loadComplete: performance.timing.loadEventEnd - performance.timing.navigationStart, 1070 domInteractive: performance.timing.domInteractive - performance.timing.navigationStart 1071 } : null, 1072 // Style / logo diagnostics (layout/color bug triage) 1073 styleDiagnostics: (function() { 1074 try { 1075 var rootStyle = getComputedStyle(document.documentElement); 1076 var logoNodes = Array.from(document.querySelectorAll('.sidebar img, .sidebar-element img, img[src*="robert-vizzini"], .horizontal-header-logo img')); 1077 return {
1078 siteBackgroundColor: rootStyle.getPropertyValue('--site-background-color').trim(), 1079 menuBackgroundColor: rootStyle.getPropertyValue('--menu-background-color').trim(), 1080 menuColor: rootStyle.getPropertyValue('--menu-color').trim(), 1081 inlineSiteBg: document.documentElement.style.getPropertyValue('--site-background-color'), 1082 inlineMenuBg: document.documentElement.style.getPropertyValue('--menu-background-color'), 1083 hasMenuStyleCustomizer: !!window.MenuStyleCustomizer, 1084 menuStyleCustomizerInitialized: !!(window.MenuStyleCustomizer && window.MenuStyleCustomizer._initialized), 1085 menuStyleSettingsBg: window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings 1086 ? { 1087 siteBackgroundColor: window.MenuStyleCustomizer.settings.siteBackgroundColor, 1088 menuBackgroundColor: window.MenuStyleCustomizer.settings.menuBackgroundColor, 1089 color: window.MenuStyleCustomizer.settings.color 1090 } 1091 : null, 1092 hasSidebarManager: !!window.SidebarManager, 1093 sidebarElementCount: window.SidebarManager && window.SidebarManager.elements 1094 ? window.SidebarManager.elements.length 1095 : 0, 1096 hasPageManager: !!window.PageManager, 1097 pageManagerHasLoadPage: !!(window.PageManager && typeof window.PageManager.loadPage === 'function'), 1098 pageElementCount: (function() { 1099 try { 1100 return document.querySelectorAll('.gallery-container .page-element, .page-container .page-element').length; 1101 } catch (e) { return -1; } 1102 })(), 1103 pageContainerHtmlLength: (function() { 1104 try { 1105 var pc = document.querySelector('.gallery-container .page-container, .page-container'); 1106 return pc ? (pc.innerHTML || '').length : 0; 1107 } catch (e) { return -1; } 1108 })(), 1109 activeContentType: window._activeContentType || null, 1110 logoImageCount: logoNodes.length, 1111 logoImages: logoNodes.slice(0, 5).map(function(img) { 1112 return { 1113 src: img.currentSrc || img.src || '', 1114 naturalWidth: img.naturalWidth || 0, 1115 display: getComputedStyle(img).display, 1116 complete: !!img.complete 1117 }; 1118 }), 1119 earlyCdnRetries: window.__hydraCdnRetryLog || [] 1120 }; 1121 } catch (e) { 1122 return { error: String(e && e.message ? e.message : e) }; 1123 } 1124 })() 1125 }; 1126 1127 // Create log content 1128 const logContent = [ 1129 '=== INDEX.TS DEBUG LOGS ===', 1130 'Generated: ' + debugInfo.timestamp, 1131 'URL: ' + debugInfo.url, 1132 'User Agent: ' + debugInfo.userAgent, 1133 'useCdnProxy Flag: ' + debugInfo.useCdnProxy, 1134 'Document Ready State: ' + debugInfo.documentReadyState, 1135 '================================', 1136 'TIP: Search for [AUTH_SAVE] in CONSOLE LOGS below for authentication/save failure details.', 1137 'TIP: Search STYLE DIAGNOSTICS for layout/color/logo triage (backgrounds, MenuStyleCustomizer, SidebarManager, logo).', 1138 '================================', 1139 '', 1140 '=== DEBUG INFO ===', 1141 JSON.stringify(debugInfo, null, 2), 1142 '', 1143 '=== STYLE DIAGNOSTICS ===', 1144 JSON.stringify(debugInfo.styleDiagnostics, null, 2), 1145 '', 1146 '=== RESOURCE TRACKING SUMMARY ===', 1147 'Total Resources Tracked: ' + (debugInfo.resourceTracking.resources.length || 0), 1148 'Proxy Flag Changes: ' + (debugInfo.resourceTracking.proxyFlagChanges.length || 0), 1149 'Image Loads Tracked: ' + (debugInfo.resourceTracking.imageLoads.length || 0), 1150 'Network Errors Tracked: ' + (debugInfo.resourceTracking.networkErrors.length || 0), 1151 '', 1152 '=== PROXY FLAG CHANGES ===', 1153 JSON.stringify(debugInfo.resourceTracking.proxyFlagChanges, null, 2), 1154 '', 1155 '=== RESOURCE LOADING TRACKING ===', 1156 JSON.stringify(debugInfo.resourceTracking.resources, null, 2), 1157 '', 1158 '=== IMAGE LOADING TRACKING ===', 1159 JSON.stringify(debugInfo.resourceTracking.imageLoads, null, 2), 1160 '', 1161 '=== NETWORK ERRORS ===', 1162 JSON.stringify(debugInfo.resourceTracking.networkErrors, null, 2), 1163 '', 1164 '=== CONSOLE LOGS ===', 1165 ...(window._capturedLogs || []).map(log => 1166 '[' + log.timestamp + '] ' + log.level + ': ' + log.message 1167 ) 1168 ].join('\n'); 1169 1170 // Create and download file 1171 const blob = new Blob([logContent], { type: 'text/plain' }); 1172 const url = URL.createObjectURL(blob); 1173 const a = document.createElement('a'); 1174 a.href = url; 1175 a.download = 'index-debug-logs-' + new Date().toISOString().replace(/[:.]/g, '-') + '.txt';
1176 document.body.appendChild(a); 1177 a.click(); 1178 document.body.removeChild(a); 1179 URL.revokeObjectURL(url); 1180 1181 console.log('â Debug logs downloaded successfully!'); 1182 } catch (error) { 1183 console.error('â Failed to download logs:', error); 1184 alert('Failed to download logs. Error: ' + (error.message || 'Unknown error')); 1185 } 1186 } 1187 1188 // Initialize when DOM is ready 1189 if (document.readyState === 'loading') { 1190 document.addEventListener('DOMContentLoaded', initLogCapture); 1191 } else { 1192 // DOM already ready, initialize immediately 1193 setTimeout(initLogCapture, 100); 1194 } 1195})(); 1196</script>
1196 1197 1198<!-- Add auth token if available --> 1199 1200 1201<style> 1202/* Override Pico.css body padding to prevent jumping between pages */ 1203body { 1204 padding: 0 !important; 1205 margin: 0 !important; 1206 display: block !important; 1207} 1208 1209/* Scope Pico.css to only affect elements in edit mode */ 1210:root:not([data-theme="pico"]) { 1211--pico-applied: dark; 1212} 1213 1214/* Add data-theme="pico" to body when in edit mode */ 1215body.edit-mode-active { 1216data-theme: dark; 1217} 1218 1219/* Custom styles to ensure Pico only affects editor UI, not content */ 1220.sidebar.editing button, 1221.edit-controls button, 1222.element-header button, 1223.text-editor button, 1224.image-uploader button { 1225--pico-applied: dark; 1226} 1227 1228/* Hide default browser-styling when Pico is active */ 1229[data-theme="pico"] button.btn { 1230appearance: dark; 1231-webkit-appearance: dark; 1232} 1233 1234/* Ensure content isn't affected by Pico styles */ 1235.gallery-container, 1236.page-text, 1237.sidebar-text { 1238--pico-applied: none !important; 1239} 1240 1241/* Conflict Indicator Styles */ 1242#conflictIndicator { 1243 background: #d32f2f !important; 1244 border: 1px solid #d32f2f !important; 1245 border-radius: 4px; 1246 padding: 8px; 1247 cursor: pointer; 1248 transition: opacity 0.2s; 1249 margin-left: 8px; 1250 display: flex; /* Use flex for centering, but hidden by default via inline style */ 1251 align-items: center; 1252 justify-content: center; 1253} 1254 1255#conflictIndicator:hover { 1256 background: #e53935 !important; 1257 border-color: #e53935 !important; 1258 opacity: 0.9; 1259} 1260 1261#conflictIndicator svg { 1262 display: block; 1263 width: 24px; 1264 height: 24px; 1265 margin: 0 auto; 1266 color: white !important; 1267} 1268 1269/* Tooltip styles for conflict indicator - allow wrapping */ 1270#tooltip-container .tooltip[data-tooltip-for="conflictIndicator"], 1271#tooltip-container .tooltip { 1272 max-width: 200px; 1273 word-wrap: break-word; 1274 white-space: normal; 1275 line-height: 1.4; 1276} 1277 1278.global-header-controls { 1279 display: flex; 1280 align-items: center; 1281 gap: 8px; 1282} 1283 1284.sidebar-header { 1285 display: flex; 1286 align-items: center; 1287 gap: 8px; 1288} 1289 1290/* Cookie Notice â paper UI (public-safe; does not depend on edit-mode pe-* scopes) */ 1291.cookie-notice { 1292 --cn-paper: #f6f5f1; 1293 --cn-paper-elevated: #fdfcfa; 1294 --cn-ink: #2c2a27; 1295 --cn-ink-muted: #6b6760; 1296 --cn-ink-faint: #9b968d; 1297 --cn-rule: rgba(44, 42, 39, 0.1); 1298 --cn-rule-strong: rgba(44, 42, 39, 0.16); 1299 --cn-shadow: 0 -8px 28px rgba(44, 42, 39, 0.1); 1300 --cn-radius: 3px; 1301 --cn-font: "DM Sans", system-ui, -apple-system, sans-serif; 1302 1303 position: fixed; 1304 bottom: 0; 1305 left: 0; 1306 right: 0; 1307 z-index: 10000; 1308 display: flex; 1309 align-items: center; 1310 justify-content: space-between; 1311 flex-wrap: wrap; 1312 gap: 0.85rem 1.25rem; 1313 padding: 1.1rem 1.35rem 1.2rem; 1314 font-family: var(--cn-font); 1315 font-size: 14px; 1316 line-height: 1.45; 1317 color: var(--cn-ink); 1318 background: var(--cn-paper-elevated); 1319 border-top: 1px solid var(--cn-rule); 1320 box-shadow: var(--cn-shadow); 1321} 1322 1323.cookie-notice.hidden { 1324 display: none; 1325} 1326 1327.cookie-notice-text { 1328 flex: 1 1 220px; 1329 min-width: 0; 1330 margin: 0; 1331 color: var(--cn-ink-muted); 1332 font-size: 13px; 1333} 1334 1335.cookie-notice-text a.cookie-notice-policy { 1336 color: var(--cn-ink); 1337 font-weight: 600; 1338 text-decoration: underline; 1339 text-underline-offset: 2px; 1340} 1341 1342.cookie-notice-text a.cookie-notice-policy:hover { 1343 color: var(--cn-ink-muted); 1344} 1345 1346.cookie-notice-buttons { 1347 display: flex; 1348 flex-wrap: wrap; 1349 align-items: center; 1350 justify-content: flex-end; 1351 gap: 0.5rem; 1352} 1353 1354.cookie-notice .pe-btn { 1355 display: inline-flex; 1356 align-items: center; 1357 justify-content: center; 1358 gap: 0.4rem; 1359 margin: 0; 1360 padding: 0.45rem 0.85rem; 1361 font-family: var(--cn-font); 1362 font-size: 11px; 1363 font-weight: 600; 1364 letter-spacing: 0.1em; 1365 text-transform: uppercase; 1366 color: var(--cn-ink-muted); 1367 background: var(--cn-paper); 1368 border: 1px solid var(--cn-rule-strong); 1369 border-radius: var(--cn-radius); 1370 box-shadow: none; 1371 cursor: pointer; 1372 transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease; 1373} 1374 1375.cookie-notice .pe-btn:hover { 1376 color: var(--cn-ink); 1377 background: var(--cn-paper-elevated); 1378 border-color: rgba(44, 42, 39, 0.22); 1379} 1380 1381.cookie-notice .pe-btn--primary { 1382 color: var(--cn-ink); 1383 background: var(--cn-paper); 1384 border-color: var(--cn-rule-strong); 1385} 1386 1387.cookie-notice .pe-btn--primary:hover {
1388 color: var(--cn-ink); 1389 background: var(--cn-paper-elevated); 1390 border-color: rgba(44, 42, 39, 0.22); 1391} 1392 1393#hydraDoNotSellLink { 1394 --cn-paper: #f6f5f1; 1395 --cn-paper-elevated: #fdfcfa; 1396 --cn-ink: #2c2a27; 1397 --cn-ink-muted: #6b6760; 1398 --cn-rule-strong: rgba(44, 42, 39, 0.16); 1399 --cn-radius: 3px; 1400 --cn-font: "DM Sans", system-ui, -apple-system, sans-serif; 1401 1402 position: fixed; 1403 bottom: 10px; 1404 left: 12px; 1405 z-index: 9999; 1406 display: inline-flex; 1407 align-items: center; 1408 justify-content: center; 1409 margin: 0; 1410 padding: 0.45rem 0.85rem; 1411 font-family: var(--cn-font); 1412 font-size: 11px; 1413 font-weight: 600; 1414 letter-spacing: 0.1em; 1415 text-transform: uppercase; 1416 color: var(--cn-ink-muted); 1417 background: var(--cn-paper-elevated); 1418 border: 1px solid var(--cn-rule-strong); 1419 border-radius: var(--cn-radius); 1420 box-shadow: 0 2px 8px rgba(44, 42, 39, 0.06); 1421 cursor: pointer; 1422} 1423 1424#hydraDoNotSellLink.hidden { 1425 display: none; 1426} 1427 1428#hydraDoNotSellLink:hover { 1429 color: var(--cn-ink); 1430 background: var(--cn-paper); 1431 border-color: rgba(44, 42, 39, 0.22); 1432} 1433 1434/* FOUC reveal (paired with early <head> html:not(.hydra-ready) { opacity: 0 }) */ 1435html.hydra-ready { 1436 opacity: 1; 1437 transition: opacity 0.2s ease; 1438} 1439/* Keep body class for any legacy selectors; visibility no longer used (submenu CSS pierces it) */ 1440body.hydra-ready { 1441 visibility: visible; 1442} 1443 1444/* Admin panel typography */ 1445.sidebar-element-form, 1446.sidebar-element-form *, 1447.add-form, 1448.add-form *, 1449.metadata-editor, 1450.metadata-editor * { 1451 font-family: "Hanken Grotesk", sans-serif; 1452 font-weight: 200; 1453 letter-spacing: 0.02em; 1454} 1455 1456.sidebar-element-form h3, 1457.sidebar-element-form label, 1458.add-form label, 1459.metadata-editor h3, 1460.metadata-editor label { 1461 text-transform: uppercase; 1462 letter-spacing: 0.04em; 1463 font-weight: 300; 1464} 1465 1466.sidebar-element-form .form-actions .btn, 1467.sidebar-element-form button, 1468.add-form .form-actions .btn, 1469.add-form button, 1470.metadata-editor .form-actions .btn, 1471.metadata-editor button { 1472 text-transform: uppercase; 1473 letter-spacing: 0.08em; 1474 font-weight: 300; 1475} 1476 1477.sidebar-element-form input, 1478.sidebar-element-form select, 1479.sidebar-element-form textarea, 1480.sidebar-element-form button, 1481.add-form input, 1482.add-form select, 1483.add-form textarea, 1484.add-form button, 1485.metadata-editor input, 1486.metadata-editor select, 1487.metadata-editor textarea, 1488.metadata-editor button { 1489 font-family: "Hanken Grotesk", sans-serif; 1490 font-weight: 200; 1491 letter-spacing: 0.02em; 1492 border-radius: 0; 1493} 1494</style> 1495
1496<script> 1497// Flag to prevent duplicate initialization 1498window.hydraInitialized = false; 1499 1500function ensureAdminPanelFont() { 1501 const head = document.head || document.getElementsByTagName('head')[0]; 1502 if (!head) { 1503 return; 1504 } 1505 1506 const linkDefinitions = [ 1507 { 1508 id: 'admin-panels-font-preconnect-googleapis', 1509 rel: 'preconnect', 1510 href: 'https://fonts.googleapis.com' 1511 }, 1512 { 1513 id: 'admin-panels-font-preconnect-gstatic', 1514 rel: 'preconnect', 1515 href: 'https://fonts.gstatic.com', 1516 crossOrigin: 'anonymous' 1517 }, 1518 { 1519 id: 'admin-panels-font-stylesheet', 1520 rel: 'stylesheet', 1521 href: 'https://fonts.googleapis.com/css2?family=Hanken+Grotesk:ital,wght@0,200;1,200&display=swap' 1522 }, 1523 { 1524 id: 'admin-panels-font-barlow', 1525 rel: 'stylesheet', 1526 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' 1527 } 1528 ]; 1529 1530 for (const definition of linkDefinitions) { 1531 if (!document.getElementById(definition.id)) { 1532 const link = document.createElement('link'); 1533 link.id = definition.id; 1534 link.rel = definition.rel; 1535 link.href = definition.href; 1536 1537 if (definition.crossOrigin) { 1538 link.crossOrigin = definition.crossOrigin; 1539 } 1540 1541 head.appendChild(link); 1542 } 1543 } 1544} 1545 1546ensureAdminPanelFont(); 1547 1548 window.Parameters = window.Parameters || {}; 1549 window.Parameters.siteId = "52octf5t"; 1550 window.Parameters.SiteId = "52octf5t"; // Also set capitalized version 1551 window.siteId = "52octf5t"; 1552 window.TURNSTILE_SITE_KEY = "0x4AAAAAADyHXlv6RTUIaDOp"; 1553 window.NEON_SKY_FILES = window.NEON_SKY_FILES || {}; 1554 window.NEON_SKY_FILES.QUILL_INTEGRATION = 'quill-integration-v260923-002.js'; 1555 window.NEON_SKY_FILES.SITE_THEMES = 'site-themes-v260923-002.css'; 1556 window.NEON_SKY_FILES.SITE_THEMES_MANIFEST = 'site-themes-v260923-002.json'; 1557 1558// Initialize menu styles from server 1559document.addEventListener('DOMContentLoaded', function() { 1560// Apply initial menu styles from server 1561const initialMenuStyles = {"fontFamily":"'Times New Roman', serif","fontSize":"15px","color":"#878787","activeColor":"#ffffff","hoverColor":"#ffffff","verticalSpacing":"2px","menuBackgroundColor":"#000000","menuBackgroundHidden":false,"siteBackgroundColor":"#000000","siteBackgroundUseGradient":false,"siteGradientRandomizeOnLoad":false,"artPaletteSelection":null,"menuBackgroundImageUrl":"","siteBackgroundImageUrl":"","mobileHeaderHeight":"60px","sidebarWidth":"19vw","sidebarPaddingTop":"20px","sidebarPaddingRight":"0px","sidebarPaddingBottom":"0px","sidebarPaddingLeft":"0px","menuLayout":"sidebar","horizontalMenuAlignment":"left","horizontalHeaderLogoLayout":"inline","horizontalHeaderShadow":true,"disableRightClick":false,"showCookieNotice":false,"cookieNoticeText":"This website uses cookies to ensure you get the best experience on our website.","privacyPolicyUrl":"https://davidcarsonphotos.
1561com/cookies","alignElementsAtBottom":true,"customCSS":"","customJS":"","siteThemeId":null,"siteThemeOptions":{},"fontName":"Times New Roman"}; 1562window.__hydraStyleFromQuery = null; 1563const initialSiteThemeRuntimeVars = null; 1564 1565// Set CSS custom properties 1566// ?hydra-style= preview: apply merged menuStyles (skip site-theme runtime vars that would override). 1567if (initialSiteThemeRuntimeVars && !window.__hydraStyleFromQuery) { 1568 Object.keys(initialSiteThemeRuntimeVars).forEach(function(key) { 1569 document.documentElement.style.setProperty(key, initialSiteThemeRuntimeVars[key]); 1570 }); 1571 if (initialMenuStyles && initialMenuStyles.verticalSpacing) { 1572 document.documentElement.style.setProperty('--menu-vertical-spacing', initialMenuStyles.verticalSpacing); 1573 } 1574 // Match menu chrome / Gallery Options overview preview (light weight) 1575 document.documentElement.style.setProperty('--menu-font-weight', '200'); 1576 if (initialMenuStyles && typeof initialMenuStyles.secondaryFontSize === 'string' && initialMenuStyles.secondaryFontSize.trim() !== '') { 1577 document.documentElement.style.setProperty('--gallery-description-font-size', initialMenuStyles.secondaryFontSize.trim()); 1578 } 1579} else if (initialMenuStyles) { 1580 if (initialMenuStyles.fontFamily) { 1581 document.documentElement.style.setProperty('--menu-font-family', initialMenuStyles.fontFamily); 1582 } 1583 if (initialMenuStyles.fontSize) { 1584 document.documentElement.style.setProperty('--menu-font-size', initialMenuStyles.fontSize); 1585 } 1586 // Only set when Secondary was explicitly saved; otherwise descriptions follow --menu-font-size 1587 if (typeof initialMenuStyles.secondaryFontSize === 'string' && initialMenuStyles.secondaryFontSize.trim() !== '') { 1588 document.documentElement.style.setProperty('--gallery-description-font-size', initialMenuStyles.secondaryFontSize.trim()); 1589 } 1590 document.documentElement.style.setProperty('--menu-font-weight', '200'); 1591 if (initialMenuStyles.color) { 1592 document.documentElement.style.setProperty('--menu-color', initialMenuStyles.color); 1593 } 1594 if (initialMenuStyles.activeColor) { 1595 document.documentElement.style.setProperty('--menu-active-color', initialMenuStyles.activeColor); 1596 } 1597 if (initialMenuStyles.hoverColor) { 1598 document.documentElement.style.setProperty('--menu-hover-color', initialMenuStyles.hoverColor); 1599 } 1600 // Background colors must be applied here too: MenuStyleCustomizer may load late/fail 1601 // after CDNâproxy retry, and CSS defaults are white/#fafafa. 1602 if (initialMenuStyles.siteBackgroundColor) { 1603 document.documentElement.style.setProperty('--site-background-color', initialMenuStyles.siteBackgroundColor); 1604 } 1605 if (initialMenuStyles.menuBackgroundColor && !initialMenuStyles.menuBackgroundHidden) { 1606 document.documentElement.style.setProperty('--menu-background-color', initialMenuStyles.menuBackgroundColor); 1607 } 1608 if (initialMenuStyles.verticalSpacing) { 1609 document.documentElement.style.setProperty('--menu-vertical-spacing', initialMenuStyles.verticalSpacing); 1610 } 1611 if (initialMenuStyles.menuBackgroundImageUrl && String(initialMenuStyles.menuBackgroundImageUrl).trim() !== '') { 1612 var menuBgImg = String(initialMenuStyles.menuBackgroundImageUrl).trim(); 1613 document.documentElement.style.setProperty('--menu-background-image', 'url(' + JSON.stringify(menuBgImg) + ')'); 1614 } 1615 var resolvedSiteBg = ''; 1616 if (window.SiteGradientGenerator && typeof window.SiteGradientGenerator.resolveSiteBackgroundImage === 'function') { 1617 resolvedSiteBg = window.SiteGradientGenerator.resolveSiteBackgroundImage(initialMenuStyles, { allowRandomize: true }); 1618 } else if (initialMenuStyles.siteBackgroundImageUrl) { 1619 resolvedSiteBg = String(initialMenuStyles.siteBackgroundImageUrl).trim(); 1620 } 1621 if (resolvedSiteBg) { 1622 var peIsCssGradient = String(resolvedSiteBg).toLowerCase().indexOf('gradient(') !== -1; 1623 var siteBgImgCss = peIsCssGradient 1624 ? resolvedSiteBg 1625 : 'url(' + JSON.stringify(resolvedSiteBg) + ')'; 1626 document.documentElement.style.setProperty('--site-background-image', siteBgImgCss); 1627 var siteBgSize = 'cover'; 1628 if (peIsCssGradient) { 1629 siteBgSize = window.SiteGradientGenerator && window.SiteGradientGenerator.gradientBackgroun
1629dSize 1630 ? window.SiteGradientGenerator.gradientBackgroundSize(resolvedSiteBg) 1631 : '100% 100%'; 1632 document.body.classList.add('has-site-background-gradient'); 1633 } 1634 document.documentElement.style.setProperty('--site-background-size', siteBgSize); 1635 } 1636 if (initialMenuStyles.mobileHeaderHeight && String(initialMenuStyles.mobileHeaderHeight).trim() !== '') { 1637 var mhh0 = String(initialMenuStyles.mobileHeaderHeight).trim(); 1638 /* 0 / 0vh / 0px collapse logo max-height calcs; use the same floor as getUserHorizontalHeaderHeightPx */ 1639 var mhhParsed = parseInt(mhh0, 10); 1640 if (Number.isNaN(mhhParsed) || mhhParsed < 1) { 1641 mhh0 = '60px'; 1642 } 1643 document.documentElement.style.setProperty('--mobile-header-height', mhh0); 1644 document.documentElement.style.setProperty('--content-margin-top', mhh0); 1645 document.documentElement.style.setProperty('--top-bar-height-setting', mhh0); 1646 } 1647} 1648 1649if (initialMenuStyles && initialMenuStyles.menuBackgroundHidden) { 1650 document.body.classList.add('menu-background-hidden'); 1651 document.documentElement.style.setProperty('--menu-background-color', 'transparent'); 1652 document.documentElement.style.setProperty('--menu-background-image', 'none'); 1653} 1654 1655if (initialMenuStyles && initialMenuStyles.menuTextUppercase) { 1656 document.body.classList.add('menu-text-uppercase'); 1657 if (!document.getElementById('menu-text-uppercase-style')) { 1658 var menuUpperStyle = document.createElement('style'); 1659 menuUpperStyle.id = 'menu-text-uppercase-style'; 1660 menuUpperStyle.textContent = '.menu-item,.horizontal-menu-label,a.horizontal-menu-label{text-transform:uppercase!important;font-weight:200!important;}'; 1661 document.head.appendChild(menuUpperStyle); 1662 } 1663} 1664 1665// Store initial styles in MenuStyleCustomizer if it exists 1666if (window.MenuStyleCustomizer) { 1667 window.MenuStyleCustomizer.settings = { 1668 ...window.MenuStyleCustomizer.defaults, 1669 ...initialMenuStyles 1670 }; 1671} 1672}); 1673</script>
vendor: 1 bytes, line 1673
1673
1674<script> 1675let magic = null; 1676let userMetadata = null; 1677let isAuthenticated = false; 1678let isAdmin = false; 1679let didToken = null; 1680let siteId = "52octf5t"; 1681 1682/** Clear Hydra auth tokens/flags from this browser (used by logout button and ?logout=1 links). */ 1683function clearHydraClientAuthStorage() { 1684 try { 1685 localStorage.removeItem('hydra_is_admin'); 1686 localStorage.removeItem('hydra_auth_token'); 1687 localStorage.removeItem('hydra_auth_email'); 1688 localStorage.removeItem('hydra_did_token'); 1689 localStorage.removeItem('hydra_auth_site'); 1690 localStorage.removeItem('hydra_auth_last_refresh'); 1691 localStorage.removeItem('hydra_auth_auto_refresh'); 1692 } catch (e) {} 1693 if (window.TokenManager && typeof window.TokenManager.clearToken === 'function') { 1694 try { window.TokenManager.clearToken(); } catch (e) {} 1695 } 1696 isAuthenticated = false; 1697 isAdmin = false; 1698 didToken = null; 1699 userMetadata = null; 1700 window.isAuthenticated = false; 1701 window.isAdmin = false; 1702 window.didToken = null; 1703} 1704 1705function hydraUrlRequestsLogout() { 1706 try { 1707 var params = new URLSearchParams(window.location.search); 1708 var path = (window.location.pathname || '/').replace(/\/+$/, '') || '/'; 1709 if (params.get('logout') === '1' || params.get('logout') === 'true') return true; 1710 if (params.get('hydra_logout') === '1' || params.get('hydra_logout') === 'true') return true; 1711 return path === '/_manage/logout'; 1712 } catch (e) { 1713 return false; 1714 } 1715} 1716 1717function hydraCleanLogoutFromUrl() { 1718 try { 1719 var params = new URLSearchParams(window.location.search); 1720 var path = (window.location.pathname || '/').replace(/\/+$/, '') || '/'; 1721 var hadQuery = params.has('logout') || params.has('hydra_logout'); 1722 params.delete('logout'); 1723 params.delete('hydra_logout'); 1724 var nextPath = path === '/_manage/logout' ? '/_manage' : window.location.pathname; 1725 var qs = params.toString(); 1726 var next = nextPath + (qs ? '?' + qs : '') + (window.location.hash || ''); 1727 if (hadQuery || path === '/_manage/logout') { 1728 history.replaceState({}, '', next); 1729 } 1730 } catch (e) {} 1731} 1732 1733(function hydraLogoutFromUrlEarly() { 1734 if (!hydraUrlRequestsLogout()) return; 1735 clearHydraClientAuthStorage(); 1736 try { sessionStorage.setItem('hydra_logout_cleared', '1'); } catch (e) {} 1737 hydraCleanLogoutFromUrl(); 1738 console.log('[Hydra] Auth cleared via logout link/query.'); 1739})(); 1740 1741function hydraUrlRequestsRefresh() { 1742 try { 1743 var params = new URLSearchParams(window.location.search); 1744 var v = params.get('refresh'); 1745 return v === '1' || v === 'true'; 1746 } catch (e) { 1747 return false; 1748 } 1749} 1750 1751function hydraClearGalleryClientCache() { 1752 try { 1753 Object.keys(localStorage).filter(function (key) { 1754 return key.includes('gallery') || key.includes('neon') || key.includes('lightbox'); 1755 }
1755).forEach(function (key) { 1756 localStorage.removeItem(key); 1757 }); 1758 } catch (e) {} 1759 try { 1760 delete window.__neonGalleryMainBootstrapWipeDone; 1761 } catch (e) { 1762 window.__neonGalleryMainBootstrapWipeDone = false; 1763 } 1764 window._forceGalleryReload = true; 1765} 1766 1767function hydraCleanRefreshFromUrl() { 1768 try { 1769 var params = new URLSearchParams(window.location.search); 1770 if (!params.has('refresh')) return; 1771 params.delete('refresh'); 1772 var qs = params.toString(); 1773 var next = window.location.pathname + (qs ? '?' + qs : '') + (window.location.hash || ''); 1774 history.replaceState({}, '', next); 1775 } catch (e) {} 1776} 1777 1778(function hydraRefreshFromUrlEarly() { 1779 if (!hydraUrlRequestsRefresh()) return; 1780 clearHydraClientAuthStorage(); 1781 hydraClearGalleryClientCache(); 1782 try { sessionStorage.setItem('hydra_refresh_cleared', '1'); } catch (e) {} 1783 hydraCleanRefreshFromUrl(); 1784 console.log('[Hydra] Gallery cache/auth cleared via ?refresh=1 â forcing fresh load.'); 1785})(); 1786 1787// State Management â site-wide KV metadata for the editor (not per-page SEO) 1788window.siteMetadata = {"title":"Simple Image Page","description":"Davis Carson, Photojournalist","keywords":"","googleAnalytics":"","noIndex":false,"favicon":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","author":"davidcarsonphotos.
1788com","language":"en","locale":"en_US","siteName":"davidcarsonphotos.com","ogImageUrl":"","customPublicSiteEnabled":false,"customPublicSiteHtmlR2Key":"","customPublicSiteHtmlFilename":"","facebookPixel":""}; 1789console.log('Client Initialized site metadata:', window.siteMetadata); 1790 1791/** Match SSR title format: "{Gallery Title} | {Site Title}" */ 1792function formatGalleryDocumentTitle(galleryTitle, win) { 1793 win = win || window; 1794 var meta = win.siteMetadata; 1795 var siteTitle = (meta && meta.title && String(meta.title).trim()) || win.location.hostname; 1796 return (galleryTitle || 'Gallery') + ' | ' + siteTitle; 1797} 1798 1799console.log('Client Google Analytics from metadataToUse:', ''); 1800console.log('Client Google Analytics in window.siteMetadata:', window.siteMetadata.googleAnalytics); 1801console.log('Client Content Language:', window.siteMetadata.contentLanguage); 1802 1803// State Management 1804let galleries = [{"id":1772567355125,"title":"Spacer","parentId":null,"visible":true,"isSpacer":true,"isHomePage":false},{"id":1772567361348,"title":"Spacer-copy","parentId":null,"visible":true,"isSpacer":true,"slug":"spacer-copy","url":"/spacer-copy","isHomePage":false,"position":1},{"id":1772565718760,"title":"Intro","url":"/intro","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"52octf5t","pageId":"z2hg701f","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"intro","normalizedName":"intro","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"z2hg701f","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}}
1804,"siteAlias":"davidcarsonphotos.com","initialPageUuid":"z2hg701f","isHydra":true,"flyIoSaveTimestamp":1786937971591,"txId":"OZv46CLax9fEIMM2yLZOzCt8Z3QvFrtuDBnex7nBucQ","permaURL":"OZv46CLax9fEIMM2yLZOzCt8Z3QvFrtuDBnex7nBucQ","isPerma":false,"uploadedTo":["fly","irys","cdn"],"fileSize":1820,"initialPageAlias":"intro","timestamp":1786937969147,"startInSingles":true,"layoutSelection":"singles","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"autoplaySingles":true,"autoplayDuration":"4","autoplayTransition":"1.5","showDescription":false,"descriptionsOnDemand":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":"singles","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplayRandomStart":false,"infoTextPlacement":"below","showCounter":false,"applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"timestampsMatch":false,"allTimestampsMatch":false,"cdnNdjsonId":"79906437e8884709893c661400f670b4","cdnNdjsonUrl":"https://cdn.neonsky.app/79906437e8884709893c661400f670b4.ndjson","cdnNdjsonStableUrl":"https://embed.neonsky.app/ndjson/52octf5t_z2hg701f.json","cdnSaveTimestamp":1786937976414},"isHomePage":true,"position":2,"siteAlias":null,"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"noIndex":false,"passwordProtected":false},{"id":1772565719676,"title":"Ferguson Uprising, Michael Brown","url":"/ferguson-michael-brown","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"52octf5t","pageId":"h4vs00r2","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"ferguson-michael-brown","normalizedName":"ferguson,-michael-brown","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"h4vs00r2","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"h4vs00r2","isHydra":true,"flyIoSaveTimestamp":1772697331598,"txId":"faHhq9MjCWysxaltoziPvuh7XAk0jFirXkmI7XWOQE0","permaURL":"faHhq9MjCWysxaltoziPvuh7XAk0jFirXkmI7XWOQE0","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":18831,"initialPageAlias":"ferguson,-michael-brown","timestamp":1772697329572,"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":true,"descriptionsOnDemand":true,"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":"
1804center","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},"irysSaveTimestamp":1772697331598,"timestampsMatch":false,"allTimestampsMatch":false},"siteAlias":null,"isHomePage":false,"position":3,"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"passwordProtected":false},{"id":1772565721063,"title":"Life","url":"/life","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"52octf5t","pageId":"x5vv9kzu","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"life","normalizedName":"life","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"x5vv9kzu","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\">Scroll Horizontally--></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"x5vv9kzu","isHydra":true,"flyIoSaveTimestamp":1772565722095,"txId":"0TJktT9UWzJb_GkFCYUMWjx5mrpUlHzG0FaMGHctIGc","permaURL":"0TJktT9UWzJb_GkFCYUMWjx5mrpUlHzG0FaMGHctIGc","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":6406,"initialPageAlias":"life","timestamp":1772690762890,"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,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2}},"isHomePage":false,"position":5,"siteAlias":null},{"id":1772565722209,"title":"News","url":"/news","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"52octf5t","pageId":"3ap0yl43","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"news","normalizedName":"news","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"3ap0yl43","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"3ap0yl43","isHydra":true,"flyIoSaveTimestamp":1772690668558,"txId":"Rf2y2HM69QiqfOTmCYLWS-8PwVrT0DuWVflYrV-WjmE","permaURL":"Rf2y2HM69QiqfOTmCYLWS-8PwVrT0DuWVflYrV-WjmE","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":6489,"initialPageAlias":"news","timestamp":1772690666391,"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,"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":"
1804center","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},"irysSaveTimestamp":1772690668558,"timestampsMatch":false,"allTimestampsMatch":false},"isHomePage":false,"position":6,"siteAlias":null},{"id":1772565723282,"title":"Sports","url":"/sports","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"52octf5t","pageId":"qayurpwu","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"sports","normalizedName":"sports","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"qayurpwu","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}}
1804,"siteAlias":"davidcarsonphotos.com","initialPageUuid":"qayurpwu","isHydra":true,"flyIoSaveTimestamp":1788833179075,"txId":"hue2k314ECWhtMhW7KVOlD7TQ-ejFXiaqU8QcLB8W9g","permaURL":"hue2k314ECWhtMhW7KVOlD7TQ-ejFXiaqU8QcLB8W9g","isPerma":false,"uploadedTo":["fly","irys","cdn"],"fileSize":3600,"initialPageAlias":"sports","timestamp":1788833176896,"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,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"irysSaveTimestamp":1772696911082,"timestampsMatch":false,"allTimestampsMatch":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"alignSmallGalleriesVertically":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplayRandomStart":false,"infoTextPlacement":"below","showGridPricing":false,"showCounter":false,"applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"cdnNdjsonId":"c834124e51374bdbad09ae1f32657324","cdnNdjsonUrl":"https://cdn.neonsky.app/c834124e51374bdbad09ae1f32657324.ndjson","cdnNdjsonStableUrl":"https://embed.neonsky.app/ndjson/52octf5t_qayurpwu.json","cdnSaveTimestamp":1788833181464},"isHomePage":false,"position":7,"siteAlias":null},{"id":1772565724362,"title":"Portraits & Illustrations","url":"/portraits-&-illustrations","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":null,"siteId":"52octf5t","pageId":"2u8sjkcu","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"portraits-&-illustrations","normalizedName":"portraits-&-illustrations","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"2u8sjkcu","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"2u8sjkcu","isHydra":true,"flyIoSaveTimestamp":1772697457366,"txId":"CU8iF4QhYA3wEkf5Tjg9qXov680BpM4AMXg28mGUMAs","permaURL":"CU8iF4QhYA3wEkf5Tjg9qXov680BpM4AMXg28mGUMAs","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":3367,"initialPageAlias":"portraits-&-illustrations","timestamp":1772697717297,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":1,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"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":"
1804center","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":1,"useRowsLayout":false,"itemsPerRow":2},"irysSaveTimestamp":1772697457366,"timestampsMatch":false,"allTimestampsMatch":false},"siteAlias":null,"isHomePage":false,"position":8},{"id":1772565707963,"title":"Projects And Stories","isFolder":true,"isCollapsed":true,"visible":true,"parentId":null,"siteId":"52octf5t","slug":"projects-and-stories","importSource":"folder","isSubmenu":true,"isHomePage":false,"position":9},{"id":1772565707965,"title":"Heroin in St. Louis","url":"/heroin-in-st.-louis","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"p8ypi3dw","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"heroin-in-st.-louis","normalizedName":"heroin-in-st.-louis","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"p8ypi3dw","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"p8ypi3dw","isHydra":true,"flyIoSaveTimestamp":1772565710464,"txId":"ELFX5c5BFC4BgxN0LdXadmNzRKppGlnJYfbKOMDIMNA","permaURL":"ELFX5c5BFC4BgxN0LdXadmNzRKppGlnJYfbKOMDIMNA","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":3307,"initialPageAlias":"heroin-in-st.-louis","timestamp":1772566872634,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2}},"siteAlias":null,"isHomePage":false,"position":10},{"id":1772565711619,"title":"Death on the rails","url":"/death-on-the-rails","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"8psky2bf","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"death-on-the-rails","normalizedName":"death-on-the-rails","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"8psky2bf","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\">Each year about 500 people are killed walking the nation's railroad tracks. It is the leading cause of death on railroads, more common than motorists killed at crossings or workers killed in accidents. Walking on the tracks is trespassing, but little is done to stop people from crossing or walking along the tracks. The 3-day series \"Death on the Rails\" by the St. Louis Post-Dispatch and stltoday.com examines pedestrian deaths on railroads and how the industry fights regulation that could prevent many incidents.</p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"8psky2bf","isHydra":true,"flyIoSaveTimestamp":1772565712919,"txId":"AZnsVO6ex_VVB0x6F9HC2cUbzfM0FbaVkgkQK16g3Lc","permaURL":"AZnsVO6ex_VVB0x6F9HC2cUbzfM0FbaVkgkQK16g3Lc","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":3912,"initialPageAlias":"death-on-the-rails","timestamp":1772566909239,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"auto
1804playSingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"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}},"siteAlias":null,"isHomePage":false,"position":11},{"id":1772565710567,"title":"St. Louis, guns, shootings and crimes","url":"/st.-louis,-guns,-shootings-and-crimes","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"6axn7ko8","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"st.-louis,-guns,-shootings-and-crimes","normalizedName":"st.-louis,-guns,-shootings-and-crimes","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"6axn7ko8","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"6axn7ko8","isHydra":true,"flyIoSaveTimestamp":1772694942553,"txId":"brLUgvAUNvoskgxY2zOudVt-DcidhdUPgbsJfbEqnc8","permaURL":"brLUgvAUNvoskgxY2zOudVt-DcidhdUPgbsJfbEqnc8","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":11114,"initialPageAlias":"st.-louis,-guns,-shootings-and-crimes","timestamp":1772694940419,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2},"irysSaveTimestamp":1772694942553,"timestampsMatch":false,"allTimestampsMatch":false},"siteAlias":null,"isHomePage":false,"position":12},{"id":1772565714048,"title":"Afghanistan, October 2006","url":"/afghanistan,-october-2006","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"qkk8n04w","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"afghanistan,-october-2006","normalizedName":"afghanistan,-october-2006","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"qkk8n04w","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\">Afghanistan Oct. 2006</p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"qkk8n04w","isHydra":true,"flyIoSaveTimestamp":1772565715332,"txId":"mnnanRJZzMx5tG75pwYLOukx7y1qfLU6DHob7kMb9pE","permaURL":"mnnanRJZzMx5tG75pwYLOukx7y1qfLU6DHob7kMb9pE","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":1745,"initialPageAlias":"afghanistan,-october-2006","timestamp":1772566944338,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"auto
1804playSingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"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}},"siteAlias":null,"isHomePage":false,"position":13},{"id":1772565715439,"title":"Iraq, September 2004","url":"/iraq,-september-2004","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"isawjjvs","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"iraq,-september-2004","normalizedName":"iraq,-september-2004","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"isawjjvs","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\">Iraq, Sept. 2004</p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"isawjjvs","isHydra":true,"flyIoSaveTimestamp":1772565716471,"txId":"qsL7UTQXCrB-jvrQx8R101op4CDWhW5Tb6mSqUoicII","permaURL":"qsL7UTQXCrB-jvrQx8R101op4CDWhW5Tb6mSqUoicII","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":606,"initialPageAlias":"iraq,-september-2004","timestamp":1772567016136,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"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}},"siteAlias":null,"isHomePage":false,"position":14},{"id":1772565713024,"title":"Dominician baseball","url":"/dominician-baseball","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"fwy81c3o","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"dominician-baseball","normalizedName":"dominician-baseball","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"fwy81c3o","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\">St. Louis Cardinals Dominican baseball academy.</p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"fwy81c3o","isHydra":true,"flyIoSaveTimestamp":1772565713942,"txId":"uRx84C3Ati6R8zqFrFPCqO3zMRIDQDYdgwe8QRilH34","permaURL":"uRx84C3Ati6R8zqFrFPCqO3zMRIDQDYdgwe8QRilH34","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":554,"initialPageAlias":"dominician-baseball","timestamp":1772566929369,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"auto
1804playSingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"showFilename":false,"displayAllInfo":false,"thumbnailNavigation":false,"navigationMode":"dynamicCursor","showTextBlock":true,"overviewOnDemand":false,"fixedHeroImage":false,"zoomInLightbox":false,"lightboxOnMobile":false,"fadeDuration":"500","fadeInDuration":100,"descriptionTextColor":"0, 0, 0","gridImageOverlayColor":"130, 130, 130","gridImageOverlayOpacity":"0.5","lightboxBgColor":"255, 255, 255","lightboxBgOpacity":"1","lightboxCloseColor":"0, 0, 0","lightboxArrowColor":"0, 0, 0","useTitles":false,"useLinks":false,"desktopTitleDisplayMode":"overlay","titleTextAlign":"center","showDescriptionInOverlay":false,"includeRolloverImageInOverlay":true,"filterMenuEnabled":false,"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}},"siteAlias":null,"isHomePage":false,"position":15},{"id":1772565716579,"title":"Oliver, $6.50 an hour cop","url":"/oliver,-$6.50-an-hour-cop","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"v8rv72s5","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"oliver,-$6.50-an-hour-cop","normalizedName":"oliver,-$6.50-an-hour-cop","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"v8rv72s5","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"v8rv72s5","isHydra":true,"flyIoSaveTimestamp":1772565717528,"txId":"M_zsP24JKTZgPspIsxqnNPsREQYgQ_8Jo5exTGNLNJE","permaURL":"M_zsP24JKTZgPspIsxqnNPsREQYgQ_8Jo5exTGNLNJE","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":372,"initialPageAlias":"oliver,-$6.50-an-hour-cop","timestamp":1772567040504,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2}},"siteAlias":null,"isHomePage":false,"position":16},{"id":1772565717637,"title":"Gephardt's last run","url":"/gephardt's-last-run","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":true,"parentId":1772565707963,"siteId":"52octf5t","pageId":"n81q08tw","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"gephardt's-last-run","normalizedName":"gephardt's-last-run","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"n81q08tw","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"n81q08tw","isHydra":true,"flyIoSaveTimestamp":1772565718650,"txId":"wgtrY82DCDnbL4G1dwcIC-2YdQK8lqf3SMTn1qKg_CQ","permaURL":"wgtrY82DCDnbL4G1dwcIC-2YdQK8lqf3SMTn1qKg_CQ","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":601,"initialPageAlias":"gephardt's-last-run","timestamp":1772567057902,"startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"auto
1804playSingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":true,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2}},"siteAlias":null,"isHomePage":false,"position":17},{"id":1772860236672,"title":"Multimedia","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"4u3wa6ug","visible":true,"parentId":null,"siteId":"52octf5t","slug":"multimedia-copy","url":"/multimedia-copy","pageElements":[{"id":1772860236597,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Simple Image Page","metaDescription":"","metaKeywords":"","pagePaddingTop":"6vw","pagePaddingLeft":"3vw","pagePaddingRight":"3vw"},{"id":1772860238581,"type":"column-container","title":"Columns Copy","visible":true,"position":1,"columns":[{"id":1772813417004,"hAlign":"right","vAlign":"middle","elements":[{"id":1772813446837,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://youtu.be/lfmXWnoxbUs","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772813421664,"hAlign":"left","vAlign":"top","elements":[{"id":1746063362628,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-size: 24px; color: rgb(161, 161, 161);\">Death on the Rails</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px; color: rgb(161, 161, 161);\">Each year about 500 people are killed walking the nation's railroad tracks. It is the leading cause of death on railroads, more common than motorists killed at crossings or workers killed in accidents. Walking on the tracks is trespassing, but little is done to stop people from crossing or walking along the tracks. This video is part of the 3-day series \"Death on the Rails\" by the St. Louis Post-Dispatch and stltoday.com that examines pedestrian deaths on railroads.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772860240000,"type":"text","title":"Text Block Copy","visible":true,"position":2,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772862132747,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":3,"columns":[{"id":1772862129649,"hAlign":"right","vAlign":"middle","elements":[{"id":1772862130186,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://youtu.be/2gNvKl9-ATM","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772862128061,"hAlign":"left","vAlign":"top","elements":[{"id":1772862134367,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p><br></p><p style=\"line-height: 1;\"><span style=\"font-family: Times-New-Roman, serif; font-size: 24px;\">History of Corvette manufacturing in St. Louis, Mo. and the move to Bowling Green, Ky.</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">The world-famous Chevrolet Corvette was built in St. Louis </span><span style=\"font-family: Times-New-Roman, serif; font-size: 14px;\">for almost 28 years, from late 1953 (for the 1954 model year) until 1981. Former St. Louis General Motors employees who built the Corvette at the assembly plant on Union Boulevard talk about being forced to transfer to Bowling Green to keep their jobs.</span></p><p style=\"line-height: 1;\"><br></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}}
1804,{"id":1772862283234,"type":"text","title":"Text Block Copy Copy","visible":true,"position":4,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860262981,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":5,"columns":[{"id":1772860260199,"hAlign":"right","vAlign":"middle","elements":[{"id":1772860266820,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://youtu.be/dEQ0qerf4U8","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772860259698,"hAlign":"left","vAlign":"top","elements":[{"id":1772860268892,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-size: 24px;\">Five Alarm Warehouse Fire in St. Louis</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">One person was injured in a massive, wind-fueled fire that ripped through a vacant warehouse on St. Louis' riverfront. The five-alarm blaze in the five-story building collapsed in stages as the fire consumed the building, leaving nothing but a pile of rubble. Firefighters hosed down nearby buildings to keep the flames from spreading to nearby warehouses. The warehouse that burned was more than 100 years old.</span></p><p><br></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772863619228,"type":"text","title":"Text Block Copy Copy Copy","visible":true,"position":6,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772863622552,"type":"column-container","title":"Columns Copy Copy Copy","visible":true,"position":7,"columns":[{"id":1772863621621,"hAlign":"right","vAlign":"middle","elements":[{"id":1772863622688,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://vimeo.com/1171261180?share=copy&fl=sv&fe=ci","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772863622235,"hAlign":"left","vAlign":"top","elements":[{"id":1772863614564,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-family: Times-New-Roman, serif; font-size: 24px;\">Heroin Deaths Surge in St. Louis</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-family: Times-New-Roman, serif; font-size: 14px;\">Richard Skinner, a longtime heroin user, talks about the drugâs grip on him and how heroin, increasingly cut with cheap synthetic fentanyl, has made it far more dangerous.</span></p><p style=\"line-height: 1;\"><span style=\"font-family: Times-New-Roman, serif; font-size: 14px;\">St. Louis city recorded 256 overdose deaths in 2017, a staggering 94 percent increase from the previous year. Opioid deaths surged past homicides and the cityâs overdose rate, 80 deaths per 100,000 people, is more than double the rate elsewhere in the region. The broader St. Louis region recorded more than 650 opioid overdoses last year.</span></p><p style=\"line-height: 1;\"><br></p><p><br></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772867957150,"type":"text","title":"Text Block Copy Copy Copy Copy","visible":true,"position":8,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772867993768,"type":"column-container","title":"Columns Copy Copy Copy Copy","visible":true,"position":9,"columns":[{"id":1772867990527,"hAlign":"right","vAlign":"middle","elements":[{"id":1772867999813,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://youtu.be/kBf131UKbiE","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772867994747,"hAlign":"left","vAlign":"top","elements":[{"id":1772867994876,"type":"text","t
1804itle":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-family: Times-New-Roman, serif; font-size: 24px;\">Mother Rushes to Find Daughter After School Shooting</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">After a shooting at Central Visual & Performing Arts High School in St. Louis, Takisha Duncan hurries up an alley to reach her child, Taniya Lumpkin, at the evacuation point on the Schnucks parking lot on Monday, Oct. 24, 2022. The incident began when a gunman, a former student, with an AR-15 rifle and multiple high-capacity magazines, entered the school and started shooting. The gunman shot and killed a teenage girl and a health teacher and injured several others before police shot and killed him in a classroom.</span></p><p style=\"line-height: 1;\"><br></p><p><br></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772860238104,"type":"text","title":"Text Block Copy Copy","visible":true,"position":10,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860244080,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":11,"columns":[{"id":1772856055495,"hAlign":"right","vAlign":"middle","elements":[{"id":1772856057186,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://youtu.be/CbC3vUK-5ew","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772856052076,"hAlign":"left","vAlign":"top","elements":[{"id":1772856056420,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"color: rgb(161, 161, 161); font-size: 24px;\">Bear in Residential Neighborhood Causes Stir</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-family: Times-New-Roman, serif; font-size: 14px;\">Neighbors react to the excitement of a 150-pound juvenile black bear that wandered into Richmond Heights and climbed up a tree after roaming for days through suburban St. Louis County. The bear was tranquilized by the Missouri Department of Conservation, which said the 1.5 to 2-year-old bear will be taken far out into the country, checked for vital signs and then released when he awakens.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772862270867,"type":"text","title":"Text Block Copy Copy","visible":true,"position":12,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860238675,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":13,"columns":[{"id":1772813839175,"hAlign":"right","vAlign":"middle","elements":[{"id":1772813837584,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=VEj_gB9XIBI","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772813840171,"hAlign":"left","vAlign":"top","elements":[{"id":1772813834818,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">9-11 Tenth Anniversary Memorial on Art Hill in St. Louis</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Two hundred and sixty volunteers swarmed Art Hill on Saturday morning staking 2996 flags into the ground. On Sunday three different memorial services at the site will mark the 10th anniversary of 9/11 terrorist attacks.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}}
1804,{"id":1772860244884,"type":"text","title":"Text Block Copy","visible":true,"position":14,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860235877,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":15,"columns":[{"id":1772813989112,"hAlign":"right","vAlign":"middle","elements":[{"id":1772813984493,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=SWe62PmH-X8&list=UUUBk1Oye72NSo44VJD7ePAA480","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772813992713,"hAlign":"left","vAlign":"top","elements":[{"id":1772813984588,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-size: 24px; color: rgb(161, 161, 161);\">Haunt Show in St. Louis</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Scenes from the 17th Annual TransWorld Halloween and Attractions Show, a trade show for those in the haunted house business, that was held at the America's Center March 8-11, 2012.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772860241243,"type":"text","title":"Text Block Copy","visible":true,"position":16,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860241945,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":17,"columns":[{"id":1772814135498,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814136753,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=6L545mec9DQ","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814135007,"hAlign":"left","vAlign":"top","elements":[{"id":1772814133296,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-size: 24px; color: rgb(161, 161, 161);\">Skateboarding 2-year-old</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Benjamin Lomax, 2, first began skate boarding about a year ago when his father Johnnie Lomax put him on a board. At the skate park cries of \"look at him\" come rolling out of the crowd gathered along the sides.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772860238191,"type":"text","title":"Text Block Copy","visible":true,"position":18,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860236882,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":19,"columns":[{"id":1772814229438,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814231068,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=Ss-HBoyh8iM&list=UUUBk1Oye72NSo44VJD7ePAA","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814227991,"hAlign":"left","vAlign":"top","elements":[{"id":1772814227602,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">Funeral burnouts in honor of murder victim, Hawk Point, Missouri</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Hundreds of friends and family members of Jeffrey \"Smitty\" Smith showed up at his funeral in their hot rods and muscle cars. The mourners stopped in front of his business, where he was murdered, on their way to the cemetery to do burnouts in honor of Smith.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}}
1804,{"id":1772860243802,"type":"text","title":"Text Block Copy","visible":true,"position":20,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860244770,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":21,"columns":[{"id":1772814358037,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814354749,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=OPAyZ-ATScA&list=UUUBk1Oye72NSo44VJD7ePAA","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814359131,"hAlign":"left","vAlign":"top","elements":[{"id":1772814353720,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">Cardinals Fans Give Advice to Cubs Fans</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">St. Louis Cardinals fans have revelled for years in the Chicago Cubs fruitless flailings at trying to win the World Series. While the Cardinals have 10 world series the Cubs are on a 100 year drought. Hear what advice Cardinals fans would give to the Cubs and their fans to end their drought.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772860243221,"type":"text","title":"Text Block Copy","visible":true,"position":22,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772860238816,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":23,"columns":[{"id":1772814715830,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814711090,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=nj7xPdt3byM&list=UUUBk1Oye72NSo44VJD7ePAA","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814710542,"hAlign":"left","vAlign":"top","elements":[{"id":1772814709960,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">Time lapse of photo shoot with the St. Louis Rams Sam Bradford and Steven Jackson</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">EARTH CITY, MO --Time lapse of a photo shoot with the St. Louis Rams Sam Bradford and Steven Jackson at Rams Park in Earth City, Mo.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}}],"isHomePage":false,"position":18,"metaTitle":"a","metaDescription":"","hideMenuOnPage":false,"passwordProtected":false},{"id":1772689890484,"title":"Fur, Feathers and Friends","parentId":null,"visible":true,"isIntegrated":true,"pageId":"nzdjrvjz","slug":"page-1772690596779","url":"/page-1772690596779","galleryOptions":{"manualCollectionName":"GUID=52octf5t","isIntegratedGallery":true,"pageId":"nzdjrvjz","siteId":"52octf5t","siteAlias":"davidcarsonphotos.
1804com","initialPageUuid":"nzdjrvjz","initialPageAlias":"page-1772690596779","timestamp":1788833460542,"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,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"y_S_XHB56yOWngDNFkLWjjZI-M0xQ0wTOB9A0Td8U-c","txId":"y_S_XHB56yOWngDNFkLWjjZI-M0xQ0wTOB9A0Td8U-c","flyIoSaveTimestamp":1788833463321,"irysSaveTimestamp":1772695351614,"timestampsMatch":false,"allTimestampsMatch":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"alignSmallGalleriesVertically":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplayRandomStart":false,"infoTextPlacement":"below","showGridPricing":false,"showCounter":false,"applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"cdnNdjsonId":"064647d93c0c484cabc81f9a628b8036","cdnNdjsonUrl":"https://cdn.neonsky.app/064647d93c0c484cabc81f9a628b8036.ndjson","cdnNdjsonStableUrl":"https://embed.neonsky.app/ndjson/52octf5t_nzdjrvjz.json","cdnSaveTimestamp":1788833464820},"siteId":"52octf5t","siteAlias":null,"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"passwordProtected":false,"isHomePage":false,"isPage":false,"position":19},{"id":1772687280697,"title":"View From Above","parentId":null,"visible":true,"isIntegrated":true,"pageId":"df5cx0ld","slug":"view-from-above","url":"/view-from-above","galleryOptions":{"manualCollectionName":"GUID=52octf5t","isIntegratedGallery":true,"pageId":"df5cx0ld","siteId":"52octf5t","siteAlias":"preview.neonsky.app","initialPageUuid":"df5cx0ld","initialPageAlias":"view-from-above","timestamp":1772691423497,"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":true,"descriptionsOnDemand":true,"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":"
1804center","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},"jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"permaURL":"vhd1K6SOuTZYkm7cSkGsBKKU4FuOpTg2QUMxbdqf3_w","isPerma":false,"txId":"vhd1K6SOuTZYkm7cSkGsBKKU4FuOpTg2QUMxbdqf3_w","flyIoSaveTimestamp":1772691425978,"irysSaveTimestamp":1772691425978,"timestampsMatch":false,"allTimestampsMatch":false},"siteId":"52octf5t","siteAlias":null,"isHomePage":false,"isPage":false,"position":20},{"id":1772688050373,"title":"Sun, Moon and Stars","parentId":null,"visible":true,"isIntegrated":true,"pageId":"zxhv7umx","slug":"sun-moon-and-stars","url":"/sun-moon-and-stars","galleryOptions":{"manualCollectionName":"GUID=52octf5t","isIntegratedGallery":true,"pageId":"zxhv7umx","siteId":"52octf5t","siteAlias":"davidcarsonphotos.
1804com","initialPageUuid":"zxhv7umx","initialPageAlias":"sun-moon-and-stars","timestamp":1788833318853,"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,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"showDescriptionsInGrid":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square"},"jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"Z9mZThb_4MNTw5-IuwN81sPxK6wdEww7fkCgLZcgmSg","txId":"Z9mZThb_4MNTw5-IuwN81sPxK6wdEww7fkCgLZcgmSg","flyIoSaveTimestamp":1788833321220,"irysSaveTimestamp":1772695482773,"timestampsMatch":false,"allTimestampsMatch":false,"columnsGridImageMask":false,"columnsGridImageAspectRatio":"square","showDescriptionsInGrid":false,"alignSmallGalleriesVertically":false,"useDesktopGridOnMobile":false,"useDesktopHorizontalScrollOnMobile":false,"autoplayRandomStart":false,"infoTextPlacement":"below","showGridPricing":false,"showCounter":false,"applyOverviewTextStyles":false,"filterMenuCollectionGalleryPicker":"__filter_pick_blank__","rolloverCollectionGalleryPicker":"__rollover_pick_blank__","applyToAllGalleriesCheckbox":false,"cdnNdjsonId":"ee88e1a8e8ee4fec803b26434a6ad266","cdnNdjsonUrl":"https://cdn.neonsky.app/ee88e1a8e8ee4fec803b26434a6ad266.ndjson","cdnNdjsonStableUrl":"https://embed.neonsky.app/ndjson/52octf5t_zxhv7umx.json","cdnSaveTimestamp":1788833322805},"siteId":"52octf5t","siteAlias":null,"isHomePage":false,"isPage":false,"position":21},{"id":1772690804259,"title":"COVID Lockdown Life","parentId":null,"visible":false,"isIntegrated":true,"pageId":"pgtosxqy","slug":"covid-lockdown-life","url":"/covid-lockdown-life","galleryOptions":{"manualCollectionName":"GUID=52octf5t","isIntegratedGallery":true,"pageId":"pgtosxqy","siteId":"52octf5t","siteAlias":"preview.neonsky.app","initialPageUuid":"pgtosxqy","initialPageAlias":"covid-lockdown-life","timestamp":1772691907792,"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,"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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2},"jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"5Y0lvcEoYib9iBp53S9C00I2zoBNWV0wGhOLT4ZxSbo","txId":"5Y0lvcEoYib9iBp53S9C00I2zoBNWV0wGhOLT4ZxSbo","flyIoSaveTimestamp":1772691910042,"irysSaveTimestamp":1772691910042,"timestampsMatch":false,"allTimestampsMatch":false},"siteId":"52octf5t","siteAlias":null,"isHomePage":false,"isPage":false,"position":22},{"id":1772694181029,"title":"Weather","parentId":null,"visible":false,"isIntegrated":true,"pageId":"xwg9wr15","slug":"weather","url":"/weather","galleryOptions":{"manualCollectionName":"GUID=52octf5t","isIntegratedGallery":true,"pageId":"xwg9wr15","siteId":"52octf5t","siteAlias":"preview.neonsky.app","initialPageUuid":"xwg9wr15","initialPageAlias":"weather","timestamp":1772697260945,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"grid-columns","useRowsLayout":false,"layoutType":"grid","columns":3,"itemsPerRow":2,"spacing":2,"auto
1804playSingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":false,"descriptionsOnDemand":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-columns","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2},"jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"isPerma":false,"permaURL":"pLSfRoQ1g9UI4nSWyDizcnDYfUxLxlVxAbm9j2AY4fs","txId":"pLSfRoQ1g9UI4nSWyDizcnDYfUxLxlVxAbm9j2AY4fs","flyIoSaveTimestamp":1772697018478,"irysSaveTimestamp":1772697018478,"timestampsMatch":false,"allTimestampsMatch":false},"siteId":"52octf5t","siteAlias":null,"isHomePage":false,"isPage":false,"position":23},{"id":1772567277641,"title":"Spacer","parentId":null,"visible":true,"isSpacer":true,"isHomePage":false,"position":24},{"id":1772565727781,"title":"Bio","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1772565727781","visible":true,"parentId":null,"siteId":"52octf5t","slug":"bio","url":"/bio","pageElements":[{"id":1772565728781,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Bio","metaDescription":"","metaKeywords":"","pagePaddingTop":"6vw"},{"id":1772565730781,"type":"column-container","title":"Two Column Layout","visible":true,"position":1,"columns":[{"id":1772565731081,"hAlign":"right","vAlign":"middle","elements":[{"id":1772565730881,"type":"slideshow","title":"Page Images","visible":true,"position":0,"slides":[{"imageUrl":"https://storage.neonsky.app/4bd5ec04c2f91/images/avaanddad.jpg","imageKey":"4bd5ec04c2f91/images/avaanddad.jpg","imageFilename":"avaanddad.jpg","imageType":"image/jpeg","caption":"","title":"","byline":"","dateline":"","altText":""}],"slideDuration":5000,"transitionDuration":500,"slideshowWidth":88,"slideshowHeight":50,"showFullImages":true}]},{"id":1772565731181,"hAlign":"left","vAlign":"middle","elements":[{"id":1772565730981,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"margin-bottom: 0.5em;\"><span style=\"font-family: Helvetica, sans-serif; font-size: 14px;\">David Carson is a Pulitzer Prize-winning photojournalist with more than three decades of experience covering local communities, national stories and international events. He has been a staff photojournalist at the St. Louis Post-Dispatch since 2000 and was a 2025 John S. Knight Journalism Fellow at Stanford University. In 2026, he was inducted into the St. Louis Media History Hall of Fame. </span></p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\"><span style=\"font-family: Helvetica, sans-serif; font-size: 14px;\">Carson began his journalism career at 16, when his mother drove him to his first newspaper assignment because he wouldnât have his driverâs license for another two months. A Boston-area native, he worked at the Providence Journal-Bulletin, Naples Daily News and as a freelance photographer in New England before joining the Post-Dispatch.</span></p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\"><span style=\"font-family: Helvetica, sans-serif; font-size: 14px;\">During his career, Carson has covered wars in Iraq and Afghanistan, the aftermath of the September 11th terrorist attacks in New York, presidential debates, multiple World Series, a Super Bowl and some of the biggest stories in the St. Louis region. His photographs were featured extensively in the Post-Dispat
1804ch photo staffâs coverage of the protests in Ferguson, Missouri, which received the 2015 Pulitzer Prize for Breaking News Photography. In 2009, he was part of the team named a finalist for the Pulitzer Prize in Breaking News Reporting for their coverage of the Kirkwood City Hall shooting. </span></p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\"><span style=\"font-family: Helvetica, sans-serif; font-size: 14px;\"> His work has also been recognized by Pictures of the Year International, the National Press Photographers Association, the National Headliner Awards, Best of Photojournalism, Time magazineâs Top 100 Photos of the Year and he was awarded a regional Emmy for Advanced Media-Interactivity for his multimedia project Reporting for Duty. </span></p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\"><span style=\"font-family: Helvetica, sans-serif; font-size: 14px;\">As a John S. Knight Journalism Fellow at Stanford, Carson focused on artificial intelligence, image authentication and the ethics of emerging visual technology. His work included exploring the C2PA standard and Content Credentials as tools for building public trust in news photography.</span></p><p style=\"margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em;\"><span style=\"font-family: Helvetica, sans-serif; font-size: 14px;\">He frequently speaks at universities, journalism conferences and professional organizations about photojournalism, artificial intelligence, image authenticity, ethics and the future of visual journalism.</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><br></p>","textWidth":88,"fadeInOnScroll":false}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false,"randomStart":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoUrls":[],"randomizeVideo":false,"videoHeight":50}}],"classicGuid":"4bd5ec04c2f91","importSource":"classic-page","isHomePage":false,"position":25},{"id":1786937796125,"title":"Bio-copy","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"ucqgj4jf","visible":false,"parentId":null,"siteId":"52octf5t","slug":"bio-copy","url":"/bio-copy","pageElements":[{"id":1786937803028,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Bio","metaDescription":"","metaKeywords":"","pagePaddingTop":"6vw"},{"id":1786937799233,"type":"column-container","title":"Two Column Layout","visible":true,"position":1,"columns":[{"id":1772565731081,"hAlign":"right","vAlign":"middle","elements":[{"id":1772565730881,"type":"slideshow","title":"Page Images","visible":true,"position":0,"slides":[{"imageUrl":"https://storage.neonsky.app/4bd5ec04c2f91/images/avaanddad.jpg","imageKey":"4bd5ec04c2f91/images/avaanddad.jpg","imageFilename":"avaanddad.jpg","imageType":"image/jpeg","caption":"","title":"","byline":"","dateline":"","altText":""}],"slideDuration":5000,"transitionDuration":500,"slideshowWidth":88,"slideshowHeight":50,"showFullImages":true}]},{"id":1772565731181,"hAlign":"left","vAlign":"middle","elements":[{"id":1772565730981,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(143, 143, 143);\">David Carson is a Pulitzer Prize-winning multimedia photojournalist for the St. Louis Post-Dispatch and a 2025 John S. Knight Journalism Fellow at Stanford University.</span></p><p style=\"line-height: 1;\"><span style=\"color: rgb(143, 143, 143);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px; color: rgb(143, 143, 143);\">He began his career at 16 when his mother drove him to his first newspaper assignment because he wouldn't have his license for another two months. Carson, a Boston area native, has worked at the St. Louis Post-Dispatch since 2000. Before arriving in the midwest he worked at the Naples Daily News in Florida, at the Providence Journal-Bulletin in Rho
1804de Island, and as a freelance photographer in New England.</span></p><p style=\"line-height: 1;\"><span style=\"color: rgb(143, 143, 143);\">\n</span></p><p style=\"margin-bottom: 0.5em; line-height: 1;\"><span style=\"font-size: 14px; color: rgb(143, 143, 143);\">Carsonâs images are featured extensively in the </span><a href=\"http://www.pulitzer.org/works/2015-Breaking-News-Photography\" target=\"_blank\" style=\"font-size: 14px; color: rgb(143, 143, 143);\">2015 Pulitzer Prize for Breaking News Photography </a><span style=\"font-size: 14px; color: rgb(143, 143, 143);\">that was awarded to the St. Louis Post-Dispatch photo staff. He has also won numerous other awards for his work over the years. In 2008, his multimedia project \"Reporting for Duty\" won a regional Emmy award for Advanced Media â Interactivity. In 2009, he was part of a team that was recognized as a finalist for the Pulitzer Prize in the \"Breaking News Reporting\" category for their coverage of the Kirkwood City Hall shootings. He was also named the National Press Photographers Association Region 7 Photographer of the Year in 2011, 2010 and 2009.</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><br></p><p style=\"margin-bottom: 0.5em; line-height: 1;\"><span style=\"font-size: 14px; color: rgb(143, 143, 143);\">He attended the Rochester Institute of Technology, where he was a photojournalism major and an international relations minor. </span><span style=\"font-size: 14px;\">As a 2025 John S. Knight Journalism Fellow at Stanford, he was immersed in artificial intelligence, image authentication, and the ethics of emerging visual technology. </span></p><p style=\"margin-bottom: 0.5em; line-height: 1;\"><br></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 14px;\">He is a frequent lecturer at universities, journalism conferences, and professional organizations. </span></p>","textWidth":86}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self","showSingleRandomImage":false},"backgroundVideo":{"enabled":false,"videoUrl":"","videoUrls":[],"randomizeVideo":false,"videoHeight":50}}],"classicGuid":"4bd5ec04c2f91","importSource":"classic-page","isHomePage":false,"position":26,"children":[]},{"id":1772565727783,"title":"Contact","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1772565727783","visible":true,"parentId":null,"siteId":"52octf5t","slug":"contact","url":"/contact","pageElements":[{"id":1772567207231,"type":"metadata","visible":true,"position":0,"metaTitle":"","metaDescription":"","metaKeywords":"","pagePaddingTop":"8vw"},{"id":1772565728683,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"<p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">David Carson is a St. Louis, Mo. based photojournalist</span></p><p style=\"line-height: 1;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">David Carson</span></p><p style=\"line-height: 1;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">St. Louis, MO</span></p><p style=\"line-height: 1;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">314.393-0502 (Cell)</span></p><p style=\"line-height: 1;\"><span style=\"font-size: 16px; color: rgb(163, 163, 163);\">\n</span></p><p style=\"line-height: 1; margin-bottom: 0.5em;\"><a href=\"mailto:[email protected]\" target=\"_blank\" style=\"font-size: 16px; color: rgb(163, 163, 163);\">[email protected]</a></p>","textWidth":43}],"classicGuid":"4bd5ec04c2f91","importSource":"classic-page","isHomePage":false,"position":27},{"id":1772567674097,"title":"Client area","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1772567674097","visible":true,"parentId":null,"siteId":"52octf5t","slug":"david-start-here","url":"/david-start-here","pageElements":[{"id":1772567676530,"type":"metadata","visible":true,"position":1,"metaTitle":"","metaDescription":"","metaKeywords":"","pagePaddingTop":"3vw","pagePaddingLeft":"3vw","pagePaddingRight":"3vw"}
1804,{"id":1772567693649,"type":"text","title":"Text Block","visible":true,"position":1,"textContent":"<p>To log in...</p><p><br></p><p>1) Press the <span style=\"color: rgb(255, 0, 200);\"> </span><span style=\"font-size: 24px; color: rgb(255, 0, 200);\">+</span><span style=\"font-size: 24px;\"> </span> key.</p><p>2) Enter your email address.</p><p>3) the One Time Passcode that you receive...</p><p><br></p><p>...then click the Edit button in the top-left corner</p><p><br></p><p>There's a video how-to below. </p><p>Let us know if you need any help with this.</p><p><br></p><p>Jayson</p><p>Neon Sky</p><p><br></p>","textWidth":48},{"id":1772567707298,"type":"embedded-content","title":"Embed","visible":true,"position":2,"contentUrl":"https://share.descript.com/embed/FyubGmgsVIG","containerWidth":90,"containerHeight":88,"useAutoHeight":false}],"position":28,"isHomePage":false,"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"passwordProtected":true,"password":"9658"},{"id":1772565726859,"title":"Fellowship Portfolio","url":"/fellowship-portfolio","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":false,"parentId":null,"siteId":"52octf5t","pageId":"t3y74vss","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"fellowship-portfolio","normalizedName":"fellowship-portfolio","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"t3y74vss","horizontalScrollerText":"<p><br></p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":false,"guid":"GUID=52octf5t","metadata":{},"totalItems":10}},"siteAlias":"preview.neonsky.app","initialPageUuid":"t3y74vss","isHydra":true,"flyIoSaveTimestamp":1772565727663,"txId":"cAxSrjwRuX1_0FO1z_qqDIC942S9bRkU_ww515AuaJw","permaURL":"cAxSrjwRuX1_0FO1z_qqDIC942S9bRkU_ww515AuaJw","isPerma":true,"uploadedTo":["fly","irys"],"fileSize":2909},"isHomePage":false,"position":29},{"id":1772565725557,"title":"Multimedia-old","url":"/multimedia-old","isIntegrated":true,"isPage":false,"isSubmenu":false,"visible":false,"parentId":null,"siteId":"52octf5t","pageId":"spxeiduh","classicGuid":"4bd5ec04c2f91","importSource":"classic-gallery","slug":"multimedia-old","normalizedName":"multimedia","galleryOptions":{"manualCollectionName":"GUID=52octf5t","importedFromClassic":true,"siteId":"52octf5t","pageId":"spxeiduh","horizontalScrollerText":"<p style=\"line-height: 1; margin-bottom: 0.5em;\">click on links to see content for multimedia pieces</p>","jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"siteAlias":"preview.neonsky.app","initialPageUuid":"spxeiduh","isHydra":true,"flyIoSaveTimestamp":1772770756876,"txId":"j2S2vmRAyXqRpEyrbdKg6gcTr19ul9ZOdPzjnPDaN3o","permaURL":"j2S2vmRAyXqRpEyrbdKg6gcTr19ul9ZOdPzjnPDaN3o","isPerma":false,"uploadedTo":["fly","irys"],"fileSize":2109,"initialPageAlias":"multimedia","timestamp":1772810733445,"startInSingles":false,"layoutSelection":"grid-rows","useRowsLayout":true,"layoutType":"grid","columns":2,"itemsPerRow":2,"spacing":2,"autoplaySingles":false,"autoplayDuration":"4","autoplayTransition":"1","showDescription":true,"descriptionsOnDemand":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":2,"spacing":2,"useRowsLayout":true,"itemsPerRow":2},"irysSaveTimestamp":1772770756876,"allTimestampsMatch":false,"timestampsMatch":false,"originalFileSize":16638},"siteAlias":null,"isHomePage":false,"position":30,"metaTitle":"","metaDescription":"","hideMenuOnPage":false,"passwordProtected":false},{"id":1772569890233,"title":"test","parentId":null,"visible":false,"isIntegrated":true,"pageId":"40i169eb","slug":"test","url":"/test","galleryOptions":{"manualCollectionName":"GUID=52octf5t","isIntegratedGallery":true,"pageId":"40i169eb","siteId":"52octf5t","siteAlias":"preview.neonsky.app","initialPageUuid":"40i169eb","initialPageAlias":"test","timestamp":1772569929678,"horizontalScrollerText":"<p><br></p>","startInSingles":false,"layoutSelection":"horizontalScroller","useRowsLayout":false,"layoutType":"horizontalScroller","columns":3,"itemsPerRow":2,"spacing":2,"auto
1804playSingles":true,"autoplayDuration":"4","autoplayTransition":"1.5","showDescription":false,"descriptionsOnDemand":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":"horizontalScroller","selection":"horizontalScroller","columns":3,"spacing":2,"useRowsLayout":false,"itemsPerRow":2},"jsonCollections":{"GUID=52octf5t":{"isClassicCollection":true,"guid":"GUID=52octf5t","metadata":{},"totalItems":0}},"permaURL":"Y4v6fkGRKs77oMSGHkWruYb_ohIkk8xkM9SZJ0YbNUg","isPerma":false,"isHydra":true,"flyIoSaveTimestamp":1772569932458,"irysSaveTimestamp":1772569932458,"allTimestampsMatch":false,"timestampsMatch":false,"txId":"Y4v6fkGRKs77oMSGHkWruYb_ohIkk8xkM9SZJ0YbNUg","fileSize":2423,"originalFileSize":12768},"isPage":false,"position":31,"isHomePage":false},{"id":1772813312664,"title":"Multimedia-0ld 11:46 3/6","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1772813312664","visible":false,"parentId":null,"siteId":"52octf5t","slug":"multimedia","url":"/multimedia","pageElements":[{"id":1772813327834,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Simple Image Page","metaDescription":"","metaKeywords":"","pagePaddingTop":"6vw","pagePaddingLeft":"3vw","pagePaddingRight":"3vw"},{"id":1772813323391,"type":"column-container","title":"Columns Copy","visible":true,"position":1,"columns":[{"id":1772813417004,"hAlign":"right","vAlign":"middle","elements":[{"id":1772813446837,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://youtu.be/lfmXWnoxbUs","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772813421664,"hAlign":"left","vAlign":"top","elements":[{"id":1746063362628,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-size: 24px; color: rgb(161, 161, 161);\">Death on the Rails</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px; color: rgb(161, 161, 161);\">Each year about 500 people are killed walking the nation's railroad tracks. It is the leading cause of death on railroads, more common than motorists killed at crossings or workers killed in accidents. Walking on the tracks is trespassing, but little is done to stop people from crossing or walking along the tracks. This video is part of the 3-day series \"Death on the Rails\" by the St. Louis Post-Dispatch and stltoday.com that examines pedestrian deaths on railroads.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772856352038,"type":"text","title":"Text Block Copy Copy","visible":true,"position":2,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772856050444,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":3,"columns":[{"id":1772856055495,"hAlign":"right","vAlign":"middle","elements":[{"id":1772856057186,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://youtu.be/CbC3vUK-5ew","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772856052076,"hAlign":"left","vAlign":"top","elements":[{"id":1772856056420,"type":"text","t
1804itle":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"color: rgb(161, 161, 161); font-size: 24px;\">Bear in Residential Neighborhood Causes Stir</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-family: Times-New-Roman, serif; font-size: 14px;\">Neighbors react to the excitement of a 150-pound juvenile black bear that wandered into Richmond Heights and climbed up a tree after roaming for days through suburban St. Louis County. The bear was tranquilized by the Missouri Department of Conservation, which said the 1.5 to 2-year-old bear will be taken far out into the country, checked for vital signs and then released when he awakens.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772814949380,"type":"text","title":"Text Block Copy","visible":true,"position":4,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772815213424,"type":"text","title":"Text Block Copy","visible":true,"position":5,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772813838190,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":6,"columns":[{"id":1772813839175,"hAlign":"right","vAlign":"middle","elements":[{"id":1772813837584,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=VEj_gB9XIBI","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772813840171,"hAlign":"left","vAlign":"top","elements":[{"id":1772813834818,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">9-11 Tenth Anniversary Memorial on Art Hill in St. Louis</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Two hundred and sixty volunteers swarmed Art Hill on Saturday morning staking 2996 flags into the ground. On Sunday three different memorial services at the site will mark the 10th anniversary of 9/11 terrorist attacks.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772814986349,"type":"text","title":"Text Block Copy","visible":true,"position":7,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772813990990,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":8,"columns":[{"id":1772813989112,"hAlign":"right","vAlign":"middle","elements":[{"id":1772813984493,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=SWe62PmH-X8&list=UUUBk1Oye72NSo44VJD7ePAA480","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772813992713,"hAlign":"left","vAlign":"top","elements":[{"id":1772813984588,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-size: 24px; color: rgb(161, 161, 161);\">Haunt Show in St. Louis</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Scenes from the 17th Annual TransWorld Halloween and Attractions Show, a trade show for those in the haunted house business, that was held at the America's Center March 8-11, 2012.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772814982198,"type":"text","title":"Text Block Copy","visible":true,"position":9,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772814139628,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":10,"columns":[{"id":1772814135498,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814136753,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=6L545mec9DQ","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814135007,"hAlign":"left","vAlign":"top","elements":[{"id":1772814133296,"type":"text","t
1804itle":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\"><span style=\"font-size: 24px; color: rgb(161, 161, 161);\">Skateboarding 2-year-old</span></p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Benjamin Lomax, 2, first began skate boarding about a year ago when his father Johnnie Lomax put him on a board. At the skate park cries of \"look at him\" come rolling out of the crowd gathered along the sides.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772814978159,"type":"text","title":"Text Block Copy","visible":true,"position":11,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772814230805,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":12,"columns":[{"id":1772814229438,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814231068,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=Ss-HBoyh8iM&list=UUUBk1Oye72NSo44VJD7ePAA","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814227991,"hAlign":"left","vAlign":"top","elements":[{"id":1772814227602,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">Funeral burnouts in honor of murder victim, Hawk Point, Missouri</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">Hundreds of friends and family members of Jeffrey \"Smitty\" Smith showed up at his funeral in their hot rods and muscle cars. The mourners stopped in front of his business, where he was murdered, on their way to the cemetery to do burnouts in honor of Smith.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772814968187,"type":"text","title":"Text Block Copy","visible":true,"position":13,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772814360182,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":14,"columns":[{"id":1772814358037,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814354749,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=OPAyZ-ATScA&list=UUUBk1Oye72NSo44VJD7ePAA","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814359131,"hAlign":"left","vAlign":"top","elements":[{"id":1772814353720,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">Cardinals Fans Give Advice to Cubs Fans</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">St. Louis Cardinals fans have revelled for years in the Chicago Cubs fruitless flailings at trying to win the World Series. While the Cardinals have 10 world series the Cubs are on a 100 year drought. Hear what advice Cardinals fans would give to the Cubs and their fans to end their drought.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}},{"id":1772814957757,"type":"text","title":"Text Block Copy","visible":true,"position":15,"textContent":"<p><br></p><p><br></p>","textWidth":100},{"id":1772814711852,"type":"column-container","title":"Columns Copy Copy","visible":true,"position":16,"columns":[{"id":1772814715830,"hAlign":"right","vAlign":"middle","elements":[{"id":1772814711090,"type":"embedded-content","title":"Embed","visible":true,"position":0,"contentUrl":"https://www.youtube.com/watch?v=nj7xPdt3byM&list=UUUBk1Oye72NSo44VJD7ePAA","containerWidth":100,"containerHeight":50,"useAutoHeight":true}]},{"id":1772814710542,"hAlign":"left","vAlign":"top","elements":[{"id":1772814709960,"type":"text","t
1804itle":"Text Block","visible":true,"position":0,"textContent":"<p style=\"line-height: 1;\">Time lapse of photo shoot with the St. Louis Rams Sam Bradford and Steven Jackson</p><p style=\"line-height: 1;\"><br></p><p style=\"line-height: 1;\"><span style=\"font-size: 14px;\">EARTH CITY, MO --Time lapse of a photo shoot with the St. Louis Rams Sam Bradford and Steven Jackson at Rams Park in Earth City, Mo.</span></p>","textWidth":90}]}],"backgroundSlideshow":{"enabled":false,"slides":[],"slideDuration":5000,"transitionDuration":500,"slideshowHeight":50,"showFullImages":false,"linkType":"none","linkUrl":"","linkPageId":"","linkTarget":"_self"}}],"metaTitle":"a","metaDescription":"","hideMenuOnPage":false,"passwordProtected":false,"isHomePage":false,"position":32},{"id":1784494636274,"title":"cookies","isPage":true,"isIntegrated":true,"isSubmenu":false,"pageId":"page_1784494636274","visible":false,"parentId":null,"siteId":"52octf5t","slug":"cookies","url":"/cookies","pageElements":[{"id":1784494650001,"type":"metadata","title":"Metadata","visible":true,"position":0,"metaTitle":"Untitled","metaDescription":"","metaKeywords":""},{"id":1784494656939,"type":"column-container","title":"Columns Copy","visible":true,"position":1,"leftColumnElements":[{"id":1746063362551,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","imageUrl":"https://storage.neonsky.app/06imfsl5/1746063380838_heic2007a.jpg","imageKey":"06imfsl5/1746063380838_heic2007a.jpg","imageFilename":"heic2007a.jpg","imageType":"image/jpeg"}],"rightColumnElements":[{"id":1746063362628,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p class=\"ql-align-center\"><br></p><p class=\"ql-align-center\"><br></p><p class=\"ql-align-center\">Lorem ipsum dolor sit amet. <span style=\"color: rgb(168, 168, 168);\">Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. </span></p>","textWidth":70}]},{"id":1784494654903,"type":"column-container","title":"Columns Copy","visible":true,"position":2,"leftColumnElements":[{"id":1746063327303,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p class=\"ql-align-center\"><br></p><p class=\"ql-align-center\"><br></p><p class=\"ql-align-center\">Lorem ipsum dolor sit amet. <span style=\"color: rgb(168, 168, 168);\">Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. </span></p>","textWidth":70}],"rightColumnElements":[{"id":1746063325497,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"100%","imageAlignment":"center","imageUrl":"https://storage.neonsky.app/06imfsl5/1746063346838_heic0905a.jpg","imageKey":"06imfsl5/1746063346838_heic0905a.jpg","imageFilename":"heic0905a.jpg","imageType":"image/jpeg"}]},{"id":1784494649926,"type":"column-container","title":"Columns","visible":true,"position":3,"leftColumnElements":[{"id":1746063080696,"type":"image","title":"Image","visible":true,"position":0,"imageWidth":"86%","imageAlignment":"center","imageUrl":"https://storage.neonsky.app/rgttx9o6/1745782782090_heic0604a.jpg","imageKey":"rgttx9o6/1745782782090_heic0604a.jpg","imageFilename":"heic0604a.jpg","imageType":"image/jpeg"}],"rightColumnElements":[{"id":1746063085138,"type":"text","title":"Text Block","visible":true,"position":0,"textContent":"<p class=\"ql-align-center\">Lorem ipsum dolor sit amet. <span style=\"color: rgb(168, 168, 168);\">Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet. </span></p>","textWidth":70}]}],"position":33,"isHomePage":false}]; 1805window.galleries = galleries; 1806 1807function getSiteGalleriesArray() { 1808 if (typeof galleries !== 'undefined' && Array.isArray(galleries)) { 1809 return galleries; 1810 } 1811 if (typeof window.galleries !== 'undefined' && Array.isArray(window.galleries)) { 1812 return window.galleries; 1813 } 1814 if (window.GALLERY_DATA_CACHE && 1815 window.GALLERY_DATA_CACHE.initialized && 1816 Array.isArray(window.GALLERY_DATA_CACHE.galleries)) { 1817 return window.GALLERY_DATA_CACHE.galleries; 1818 } 1819 return []; 1820} 1821window.getSiteGalleriesArray = getSiteGalleriesArray; 1822 1823let activeGalleryId = null; 1824let sortableInstance = null; 1825let isEditing = false;
1826let initialPageInfo = {"id":1772565721063,"type":"gallery","pageId":"x5vv9kzu","slug":"life","url":"/life"}; 1827 1828 1829// Store the current menu layout type (must be JSON-encoded â bare sidebar/horizontal is invalid JS) 1830let currentMenuLayout = null; 1831 1832/** True when this document is loaded in the chromeless iframe (?neon_embed=1 or body.neon-page-embed). */ 1833function isNeonPageEmbedShell() { 1834 try { 1835 if (typeof document !== 'undefined' && document.body && document.body.classList.contains('neon-page-embed')) 1836 return true; 1837 var _spEmbed = new URLSearchParams(window.location.search); 1838 return _spEmbed.get('neon_embed') === '1'; 1839 } catch (_eEmbed) { return false; } 1840} 1841 1842/** 1843 * Mirrors "hide menu on this page": in embed shells always hide menus (unless editing), 1844 * otherwise use each gallery/page flag. 1845 */ 1846function shouldApplyMenuHiddenForEmbedOrGallery(gallery) { 1847 var editing = typeof window.isInEditMode === 'function' && window.isInEditMode(); 1848 if (editing) 1849 return !!(gallery && gallery.hideMenuOnPage); 1850 return !!(gallery && gallery.hideMenuOnPage) || isNeonPageEmbedShell(); 1851} 1852 1853function shouldApplyMenuOverlayOnPage(gallery) { 1854 if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) 1855 return false; 1856 return !!(gallery && gallery.overlayMenuOnPage); 1857} 1858 1859function isOverlayMenuChromeActive() { 1860 return document.body.classList.contains('menu-overlay-on-page') 1861 && !document.body.classList.contains('edit-mode-active'); 1862} 1863 1864/** Style Customizer paints sidebar with inline background-color !important; beat that only while overlay is active. */ 1865function syncOverlayMenuInlineChrome() { 1866 var sidebar = document.querySelector('.sidebar'); 1867 var header = document.querySelector('.mobile-header'); 1868 if (isOverlayMenuChromeActive()) { 1869 if (sidebar) { 1870 sidebar.style.setProperty('background-color', 'transparent', 'important'); 1871 sidebar.style.setProperty('background-image', 'none', 'important'); 1872 } 1873 if (header) { 1874 header.style.setProperty('background-color', 'transparent', 'important'); 1875 header.style.setProperty('background-image', 'none', 'important'); 1876 } 1877 return; 1878 } 1879 if (sidebar) { 1880 var menuBg = (getComputedStyle(document.documentElement).getPropertyValue('--menu-background-color') || '').trim() || '#fafafa'; 1881 var hidden = document.body.classList.contains('menu-background-hidden'); 1882 sidebar.style.setProperty('background-color', hidden ? 'transparent' : menuBg, 'important'); 1883 if (hidden) { 1884 sidebar.style.setProperty('background-image', 'none', 'important'); 1885 } else { 1886 sidebar.style.removeProperty('background-image'); 1887 } 1888 } 1889 if (header) { 1890 header.style.removeProperty('background-color'); 1891 header.style.removeProperty('background-image'); 1892 } 1893} 1894 1895function applyMenuOverlayClassForGallery(gallery) { 1896 if (shouldApplyMenuOverlayOnPage(gallery)) { 1897 document.body.classList.add('menu-overlay-on-page'); 1898 } else { 1899 document.body.classList.remove('menu-overlay-on-page'); 1900 } 1901 syncOverlayMenuInlineChrome(); 1902} 1903 1904function hookMenuStyleCustomizerForOverlay() { 1905 var customizer = window.MenuStyleCustomizer; 1906 if (!customizer || customizer.__overlayBgHooked || typeof customizer.applyStyles !== 'function') 1907 return; 1908 var original = customizer.applyStyles.bind(customizer); 1909 customizer.applyStyles = function() { 1910 original.apply(this, arguments); 1911 if (isOverlayMenuChromeActive()) 1912 syncOverlayMenuInlineChrome(); 1913 }; 1914 customizer.__overlayBgHooked = true; 1915} 1916window.applyMenuOverlayClassForGallery = applyMenuOverlayClassForGallery; 1917 1918document.addEventListener('DOMContentLoaded', () => { 1919 if (window.hydraInitialized) { 1920 console.log("Hydra already initialized, skipping"); 1921 return; 1922 } 1923 window.hydraInitialized = true; 1924 document.body.classList.add('hydra-initialized'); 1925 try { 1926 if (sessionStorage.getItem('hydra_refresh_cleared') === '1') { 1927 sessionStorage.removeItem('hydra_refresh_cleared'); 1928 if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.resetInitializationState === 'function') { 1929 window.EnhancedGallerySystem.resetInitializationState(); 1930 } 1931 console.log('[Hydra] Applied ?refresh=1 â gallery framework reset for clean load.'); 1932 } 1933 } catch (e) {} 1934 try { 1935 var neonEmbedParams = new URLSearchParams(window.location.search); 1936 if (neonEmbedParams.get('neon_embed') === '1') { 1937 document.body.classList.add('neon-page-embed', 'menu-hidden-on-page'); 1938 } 1939 } catch (_neonEmbedErr) {} 1940 hookMenuStyleCustomizerForOverlay(); 1941 setTimeout(hookMenuStyleCustomizerForOverlay, 0); 1942 setTimeout(hookMenuStyleCustomizerForOverlay, 500); 1943 if (isOverlayMenuChromeActive()) 1944 syncOverlayMenuInlineChrome(); 1945 window.Parameters = window.Parameters || {}; 1946 if (localStorage.getItem('hydra_is_admin') === 'true') { 1947 document.body.classList.add('hydra-admin', 'hydra-authenticated'); 1948 isAdmin = true; 1949 isAuthenticated = true; 1950 console.log('Restored admin status from localStorage'); 1951 1952 // CRITICAL FIX: Clear all caches except auth tokens for logged-in admins 1953 // This ensures admins always see fresh data when opening new tabs 1954 try { 1955 console.log('Admin detected - clearing all caches except auth tokens...'); 1956 1957 // Save auth tokens first 1958 const savedAuthToken = localStorage.getItem('hydra_auth_token'); 1959 const savedAuthEmail = localStorage.getItem('hydra_auth_email'); 1960 const savedIsAdmin = localStorage.getItem('hydra_is_admin'); 1961 const savedDidToken = localStorage.getItem('hydra_did_token'); 1962 1963 // Clear ALL localStorage 1964 localStorage.clear(); 1965 1966 // Restore auth tokens 1967 if (savedAuthToken) localStorage.setItem('hydra_auth_token', savedAuthToken); 1968 if (savedAuthEmail) localStorage.setItem('hydra_auth_email', savedAuthEmail); 1969 if (savedIsAdmin) localStorage.setItem('hydra_is_admin', savedIsAdmin); 1970 if (savedDidToken) localStorage.setItem('hydra_did_token', savedDidToken); 1971 1972 // Clear ALL sessionStorage (auth tokens are not stored here) 1973 sessionStorage.clear(); 1974 1975 console.log('Cleared all caches except auth tokens for admin user'); 1976 } catch (cacheError) { 1977 console.warn('Error clearing caches on admin page load:', cacheError); 1978 // Don't fail page load if cache clearing fails 1979 } 1980 } 1981 1982 function ensureSidebarElementsAndRender() { 1983 console.log('Ensuring sidebar elements are available before rendering...'); 1984 1985 // Restore from siteConfig when hydrate missed or init invented a default menu 1986 if (window.SidebarManager) { 1987 var current = window.SidebarManager.elements; 1988 var missingOrDefault = !current || current.length === 0 || window.SidebarManager._createdDefaultElements; 1989 if (missingOrDefault && typeof siteConfig !== 'undefined' && siteConfig.sidebarElements && siteConfig.sidebarElements.length > 0) { 1990 console.log('Initializing SidebarManager elements from siteConfig'); 1991 window.SidebarManager.elements = siteConfig.sidebarElements; 1992 window.SidebarManager._elementsReady = true; 1993 window.SidebarManager._createdDefaultElements = false;
1994 if (typeof window.SidebarManager.renderElements === 'function') { 1995 window.SidebarManager.renderElements(); 1996 } 1997 } 1998 } 1999 2000 // Determine layout and render 2001 const currentMenuLayout = getEffectiveMenuLayout(); 2002 2003 if (currentMenuLayout === 'horizontal') { 2004 console.log('Rendering horizontal menu with ensured sidebar elements'); 2005 renderHorizontalMenu(); 2006 } else { 2007 renderGalleries(); 2008 } 2009} 2010 2011 setTimeout(() => { 2012 console.log("Initializing galleries:", galleries.length); 2013 // Collapse folders before first paint so nested items never flash as expanded plain text 2014 alignFolderStates(); 2015 2016 // CHANGED: Use the new function instead of direct rendering 2017 setTimeout(ensureSidebarElementsAndRender, 100); // Small delay to ensure sidebar init 2018 2019 initMobileMenu(); 2020 2021 /** If true, deep-link timeouts below must not call handleURLNavigation again (duplicate loadPage / blink). */ 2022 let bootstrapRouteAttempted = false; 2023 2024 // Handle direct URL navigation 2025 if (initialPageInfo) { 2026 console.log("Loading initial page from direct URL:", initialPageInfo); 2027 2028 // Find the gallery by ID 2029 const gallery = galleries.find(g => g.id === initialPageInfo.id); 2030 2031 if (gallery) { 2032 // Set active gallery ID 2033 activeGalleryId = gallery.id; 2034 2035 if (initialPageInfo.type === 'page') { 2036 console.log("Loading page directly:", gallery.title); 2037 loadPage(gallery.id); 2038 } else { 2039 console.log("Loading gallery directly:", gallery.title); 2040 loadGallery(gallery.id); 2041 } 2042 bootstrapRouteAttempted = true; 2043 } 2044 } else if (window.location.pathname === '/') { 2045 // No specific path, try to load home page 2046 console.log("No specific path, checking for home page"); 2047 if (!loadHomePage() && galleries.length > 0) { 2048 // Default to first gallery if no home page is set 2049 loadGallery(galleries[0].id); 2050 } 2051 bootstrapRouteAttempted = true; 2052 } else if (galleries.length > 0) { 2053 // Path exists but no matching initialPageInfo, try to handle URL path 2054 handleURLNavigation(); 2055 bootstrapRouteAttempted = true; 2056 } 2057 window.__hydraBootstrapRouteAttempted = bootstrapRouteAttempted; 2058 2059 // Log what was rendered 2060 console.log("Menu content:", document.getElementById('galleryTree')?.innerHTML || "Gallery tree not found"); 2061 2062 // Start with edit controls properly set 2063 toggleEditControlsVisibility(isAdmin && document.body.classList.contains('edit-mode-active')); 2064 2065 // Explicitly hide the import classic form too 2066 const importForm = document.getElementById('importClassicForm'); 2067 if (importForm) { 2068 importForm.style.display = 'none'; 2069 importForm.classList.remove('visible'); 2070 } 2071 2072 // Try to restore token from localStorage 2073 if (!didToken) { 2074 didToken = localStorage.getItem('hydra_auth_token'); 2075 if (didToken) { 2076 console.log('Restored auth token from localStorage'); 2077 } 2078 } 2079 2080 // Initialize Magic and check authentication 2081 2082 2083 // Initialize sidebar elements if available 2084 if (window.SidebarManager) { 2085 // Pass the initial elements from server config if available 2086 if (true) { 2087 window.SidebarManager.elements = [{"id":1772567313574,"type":"image","title":"Image","visible":true,"position":0,"imageData":"https://storage.neonsky.app/52octf5t/1772567326035_david-c.png","imageWidth":"82%","imageAlignment":"left","linkType":"internal","linkPageId":"1772565718760","linkUrl":"","linkTarget":"_self","linkAutoAdvance":false,"hideFromMobileMenu":true},{"id":1772565640360,"type":"menu","title":"Menu","visible":true,"position":1},{"id":1772567429855,"type":"social","title":"Social","visible":true,"position":2,"socialIconSize":20,"socialIcons":[{"type":"x","url":"https://twitter.com/pdpj"},{"type":"instagram","url":"https://www.instagram.com/photocarson/"},{"type":"linkedin","url":"https://www.linkedin.com/in/david-carson-628a596a/"},{"type":"email","url":"https://[email protected]"}],"socialAlignment":"left"}]; 2088 if (window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 2089 window.SidebarManager._elementsReady = true; 2090 window.SidebarManager._createdDefaultElements = false;
2091 } 2092 } 2093 window.SidebarManager.init(); 2094 } 2095 2096 2097 // Initialize metadata from server config (site-wide only â not current page SEO) 2098window.siteMetadata = {"title":"Simple Image Page","description":"Davis Carson, Photojournalist","keywords":"","googleAnalytics":"","noIndex":false,"favicon":"","contentLanguage":"en","copyrightEnabled":false,"copyrightText":"","author":"davidcarsonphotos.com","language":"en","locale":"en_US","siteName":"davidcarsonphotos.com","ogImageUrl":"","customPublicSiteEnabled":false,"customPublicSiteHtmlR2Key":"","customPublicSiteHtmlFilename":"","facebookPixel":""}; 2099console.log('Client Initialized site metadata (second init):', window.siteMetadata); 2100console.log('Client Google Analytics from metadataToUse (second init):', ''); 2101console.log('Client Google Analytics in window.siteMetadata (second init):', window.siteMetadata.googleAnalytics); 2102console.log('Client Content Language (second init):', window.siteMetadata.contentLanguage); 2103 2104 // Update copyright footer after metadata is set
2104(important for main domain) 2105 if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') { 2106 window.SidebarManager.updateMetadataFooter(); 2107 } 2108 2109 2110 2111 const submenuCheck = document.getElementById('createSubmenu'); 2112 const submenuTitleField = document.getElementById('submenuTitle'); 2113 const submenuTitleGroup = submenuTitleField?.parentElement; 2114 2115 if (submenuCheck && submenuTitleGroup) { 2116 submenuCheck.addEventListener('change', function() { 2117 submenuTitleGroup.style.display = this.checked ? 'block' : 'none'; 2118 }); 2119 } 2120 2121 // Check authentication state after initialization 2122 if (isAuthenticated && isAdmin) { 2123 console.log('Already authenticated and admin, showing edit UI'); 2124 forceShowEditUI(); 2125 } else if (didToken) { 2126 console.log('Have token, checking admin status'); 2127 checkAuth(); 2128 } 2129 }, 100); // Small delay to ensure everything is loaded 2130 2131 setTimeout(() => { 2132 if (window.location.pathname === '/' || 2133 window.location.pathname === '/_manage' || window.location.pathname === '/_manage/' || 2134 (window.location.hostname === 'preview.neonsky.app' && 2135 window.location.pathname.split('/').length <= 2)) { 2136 // Root URL or preview URL with only GUID - check for home page 2137 // Skip if bootstrap already routed (themed sites boot slower and would hit a duplicate load) 2138 if (!initialPageInfo && !window.__hydraBootstrapRouteAttempted) { 2139 loadHomePage(); 2140 } 2141 } 2142 }, 500); 2143 2144 // Dedicated editor URL: open edit tools or OTP login without requiring + key 2145 setTimeout(function hydraManageEditorEntry() { 2146 var p = window.location.pathname; 2147 if (p !== '/_manage' && p !== '/_manage/') return; 2148 try { 2149 if (sessionStorage.getItem('hydra_logout_cleared') === '1') { 2150 sessionStorage.removeItem('hydra_logout_cleared'); 2151 console.log('[Hydra] Logout link cleared auth; skipping auto-login on /_manage.'); 2152 return; 2153 } 2154 } catch (e) {} 2155 if (document.body.classList.contains('edit-mode-active')) return; 2156 try { 2157 var token = window.didToken || localStorage.getItem('hydra_auth_token'); 2158 var admin = window.isAdmin === true || localStorage.getItem('hydra_is_admin') === 'true'; 2159 if (token && admin) { 2160 if (typeof window.toggleEditMode === 'function') window.toggleEditMode(); 2161 return; 2162 } 2163 if (token && typeof checkAdminStatus === 'function') { 2164 checkAdminStatus(token).then(function() { 2165 if (window.isAdmin && typeof window.toggleEditMode === 'function' && !document.body.classList.contains('edit-mode-active')) { 2166 window.toggleEditMode(); 2167 } 2168 }).catch(function() {}); 2169 return; 2170 } 2171 if (typeof startOTPLogin === 'function') { 2172 var pr = startOTPLogin(); 2173 if (pr && typeof pr.catch === 'function') pr.catch(function() {}); 2174 } 2175 } catch (e) { 2176 console.error('hydraManageEditorEntry:', e); 2177 } 2178 }, 2200); 2179}); 2180 2181/** 2182 * Accessible name for header/sidebar brand images (no visual change). 2183 * Prefers element title or site metadata over generic "Image" / filenames. 2184 */ 2185function resolveBrandLogoAlt(element) { 2186 if (window.SidebarManager && typeof window.SidebarManager._resolveBrandImageAlt === 'function') { 2187 return window.SidebarManager._resolveBrandImageAlt(element || {}); 2188 } 2189 const siteTitle = 2190 (window.siteMetadata && String(window.siteMetadata.title || '').trim()) || 2191 (document.title && document.title.includes('|') ? document.title.split('|').pop().trim() : '') || 2192 String(location.hostname || '').replace(/^www\./i, ''); 2193 const title = element && element.title ? String(element.title).trim() : ''; 2194 if (title && !/^image(\s*\d*)?$/i.test(title)) return title; 2195 return siteTitle || 'Site logo'; 2196} 2197 2198/** 2199 * Gets the first visible sidebar element (image or text) to be used as a logo or title. 2200 * @returns {object|null} The first suitable sidebar element or null. 2201 */ 2202 2203function getFirstSidebarElementForHeader() { 2204 if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 2205 const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible 2206 if (visibleElements.length > 0) { 2207 const sortedElements = visibleElements.sort((a, b) => a.position - b.position); 2208 2209 // Debug: Log all element types found 2210 const elementTypes = sortedElements.map(el => ({ type: el.type, id: el.id, title: el.title })); 2211 console.log('[getFirstSidebarElementForHeader] Visible elements found:', elementTypes); 2212 2213 for (let el of sortedElements) { 2214 if (el.type === 'image' || el.type === 'text') { // Filter 2: type 2215 console.log('[getFirstSidebarElementForHeader] Found matching element:', { type: el.type, id: el.id, title: el.title, linkType: el.linkType, linkUrl: el.linkUrl, linkPageId: el.linkPageId }); 2216 return el; 2217 } 2218 } 2219 // If loop finishes, no visible image or text found among visible elements 2220 console.log('[getFirstSidebarElementForHeader] Found visible elements, but none were type image/text. Element types found:', elementTypes.map(e => e.type)); 2221 return null; 2222 } else { 2223 console.log('[getFirstSidebarElementForHeader] No elements with visible !== false found in SidebarManager.elements. Total elements:', window.SidebarManager.elements.length); 2224 return null; 2225 } 2226 } 2227 console.log('[getFirstSidebarElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.'); 2228 return null; 2229} 2230 2231/** 2232 * Gets the first visible social icon element from sidebar elements. 2233 * @returns {object|null} The first suitable social element or null. 2234 */ 2235function getFirstSocialElementForHeader() { 2236 if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 2237 const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false); // Filter 1: visible 2238 if (visibleElements.length > 0) { 2239 const sortedElements = visibleElements.sort((a, b) => a.position - b.position); 2240 for (let el of sortedElements) { 2241 if (el.type === 'social') { // Filter for social type 2242 return el; 2243 } 2244 } 2245 // If loop finishes, no visible social element found 2246 console.log('[getFirstSocialElementForHeader] Found visible elements, but none were type social.'); 2247 return null; 2248 } else { 2249 console.log('[getFirstSocialElementForHeader] No elements with visible !== false found in SidebarManager.elements.'); 2250 return null; 2251 } 2252 } 2253 console.log('[getFirstSocialElementForHeader] SidebarManager, its elements, or elements array is empty/undefined.'); 2254 return null; 2255} 2256 2257function isSpaceColonySiteTheme() { 2258 const themeId = document.body && document.body.dataset.siteTheme; 2259 return themeId === 'space-colony'; 2260} 2261 2262/** 2263 * Apply horizontal bar logo placement (inline vs stacked) from Menu Style > Bar logo & menu. 2264 * Sets body.horizontal-header-logo-stacked and flex on .logo-menu-container; updates --mobile-header-height when stacked. 2265 */ 2266function applyHorizontalLogoMenuLayoutStyles() { 2267 const sc = window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings; 2268 const mode = (sc && sc.horizontalHeaderLogoLayout) || 'inline'; 2269 const isSpaceColonyTheme = isSpaceColonySiteTheme(); 2270 document.body.classList.toggle('horizontal-header-logo-stacked', mode === 'stacked'); 2271 const w = document.querySelector('.logo-menu-container'); 2272 if (w) { 2273 const isMobile = window.innerWidth <= 768; 2274 w.style.display = 'flex'; 2275 w.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : 'center'; 2276 if (mode === 'stacked') { 2277 w.style.flexDirection = 'column'; 2278 w.style.gap = isMobile ? '8px' : '12px'; 2279 w.style.width = '100%'; 2280 w.style.boxSizing = 'border-box';
2281 w.style.flexGrow = isMobile ? '0' : '1'; 2282 w.style.minWidth = '0'; 2283 w.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : 'flex-start'; 2284 } else { 2285 w.style.flexDirection = 'row'; 2286 w.style.gap = '20px'; 2287 w.style.width = ''; 2288 w.style.boxSizing = ''; 2289 w.style.minWidth = ''; 2290 if (isMobile) { 2291 w.style.flexGrow = '0'; 2292 w.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center'; 2293 } else { 2294 w.style.flexGrow = ''; 2295 w.style.justifyContent = ''; 2296 } 2297 } 2298 } 2299 const mhc = document.querySelector('.mobile-header-content'); 2300 if (mhc) { 2301 const isMobile = window.innerWidth <= 768; 2302 if (isSpaceColonyTheme && !isMobile) { 2303 mhc.style.alignItems = 'flex-end'; 2304 mhc.style.flexGrow = '0'; 2305 mhc.style.width = ''; 2306 } else if (isSpaceColonyTheme && isMobile) { 2307 mhc.style.alignItems = 'flex-start'; 2308 mhc.style.justifyContent = 'flex-start'; 2309 mhc.style.flexGrow = ''; 2310 mhc.style.width = '100%'; 2311 } else if (!isMobile && mode === 'stacked') { 2312 mhc.style.alignItems = 'flex-start'; 2313 mhc.style.flexGrow = ''; 2314 mhc.style.width = ''; 2315 } else { 2316 mhc.style.alignItems = ''; 2317 mhc.style.flexGrow = ''; 2318 mhc.style.width = ''; 2319 } 2320 } 2321 if (typeof window.syncContentMarginToHorizontalHeader === 'function') { 2322 window.syncContentMarginToHorizontalHeader(); 2323 } 2324} 2325window.applyHorizontalLogoMenuLayoutStyles = applyHorizontalLogoMenuLayoutStyles; 2326 2327function parseHorizontalHeaderHeightPx(value, fallback) { 2328 if (value === null || value === undefined) { 2329 return fallback; 2330 } 2331 const parsed = parseInt(String(value).trim(), 10); 2332 if (Number.isNaN(parsed) || parsed < 1) { 2333 return fallback; 2334 } 2335 return parsed; 2336} 2337 2338function getUserHorizontalHeaderHeightPx() { 2339 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 2340 const fromSetting = parseHorizontalHeaderHeightPx(window.MenuStyleCustomizer.settings.mobileHeaderHeight, 0); 2341 if (fromSetting > 0) { 2342 return fromSetting; 2343 } 2344 } 2345 const fromVar = getComputedStyle(document.documentElement).getPropertyValue('--top-bar-height-setting').trim(); 2346 const parsedVar = parseHorizontalHeaderHeightPx(fromVar, 0); 2347 if (parsedVar > 0) { 2348 return parsedVar; 2349 } 2350 const themeMin = getComputedStyle(document.documentElement).getPropertyValue('--st-header-height').trim(); 2351 const parsedTheme = parseHorizontalHeaderHeightPx(themeMin, 0); 2352 if (parsedTheme > 0) { 2353 return parsedTheme; 2354 } 2355 return 60; 2356} 2357 2358function setHorizontalHeaderHeightVars(headerPx, contentMarginPx) { 2359 const headerVal = headerPx + 'px'; 2360 const marginVal = contentMarginPx + 'px'; 2361 const currentHeader = getComputedStyle(document.documentElement).getPropertyValue('--mobile-header-height').trim(); 2362 const currentMargin = getComputedStyle(document.documentElement).getPropertyValue('--content-margin-top').trim(); 2363 if (currentHeader === headerVal && currentMargin === marginVal) { 2364 return; 2365 } 2366 document.documentElement.style.setProperty('--mobile-header-height', headerVal); 2367 document.documentElement.style.setProperty('--content-margin-top', marginVal); 2368} 2369 2370function measureHorizontalHeaderNaturalHeight(header) { 2371 const el = header; 2372 const prevHeight = el.style.getPropertyValue('height'); 2373 const prevMinHeight = el.style.getPropertyValue('min-height'); 2374 /* Beat CSS min-height: var(--mobile-header-height) !important so a prior wrap 2375 doesnât floor the measurement when the viewport widens back to one row. */ 2376 el.style.setProperty('height', 'auto', 'important'); 2377 el.style.setProperty('min-height', '0', 'important'); 2378 const measured = Math.ceil(el.getBoundingClientRect().height); 2379 if (prevHeight) { 2380 el.style.setProperty('height', prevHeight); 2381 } else { 2382 el.style.removeProperty('height'); 2383 } 2384 if (prevMinHeight) { 2385 el.style.setProperty('min-height', prevMinHeight); 2386 } else { 2387 el.style.removeProperty('min-height'); 2388 } 2389 return measured; 2390} 2391 2392function applyHorizontalHeaderContentInset() { 2393 if (!document.body.classList.contains('menu-layout-horizontal')) { 2394 return; 2395 } 2396 if (window._horizontalHeaderInsetApplying) { 2397 return; 2398 } 2399 const header = document.querySelector('.mobile-header'); 2400 if (!header) { 2401 return; 2402 } 2403 2404 window._horizontalHeaderInsetApplying = true; 2405 try { 2406 const userH = getUserHorizontalHeaderHeightPx();
2407 /* Keep logo-cap var as a real px height. Sites can save mobileHeaderHeight as 0vh; 2408 leaving that on --top-bar-height-setting makes calc(var(...) - 8px) â 0 and hides the logo. */ 2409 document.documentElement.style.setProperty('--top-bar-height-setting', userH + 'px'); 2410 const hasExpandedMenu = header.querySelector('.horizontal-menu-item.expanded, .horizontal-menu-item.submenu-open'); 2411 const isStackedLogo = document.body.classList.contains('horizontal-header-logo-stacked'); 2412 const isPhone = window.innerWidth <= 768; 2413 2414 /* 2415 * Phone hamburger chrome: fixed Style Customizer height (link row is hidden). 2416 * Desktop inline may wrap beside the logo â measure like stacked so the bar can grow. 2417 * Stacked / open submenu: always measure. 2418 */ 2419 if (!hasExpandedMenu && !isStackedLogo && isPhone) { 2420 setHorizontalHeaderHeightVars(userH, userH); 2421 return; 2422 } 2423 2424 /* Desktop inline (wrap-capable), stacked logo, or open submenu */ 2425 const measured = measureHorizontalHeaderNaturalHeight(header); 2426 if (!measured || measured < 1) { 2427 setHorizontalHeaderHeightVars(userH, userH); 2428 return; 2429 } 2430 2431 let maxH = Math.max(200, Math.round(window.innerHeight * 0.5)); 2432 const clamped = Math.min(Math.max(measured, userH), maxH); 2433 /* Extra content margin only when bar grew past the user setting (wrapped / stacked / open menu) */ 2434 const contentMargin = clamped > userH ? clamped + 8 : clamped; 2435 setHorizontalHeaderHeightVars(clamped, contentMargin); 2436 } finally { 2437 window._horizontalHeaderInsetApplying = false; 2438 } 2439} 2440 2441function ensureHorizontalHeaderHeightObserver() { 2442 if (typeof ResizeObserver === 'undefined') { 2443 return; 2444 } 2445 const header = document.querySelector('.mobile-header'); 2446 if (!header) { 2447 return; 2448 } 2449 if (window._horizontalHeaderResizeObserver) { 2450 window._horizontalHeaderResizeObserver.disconnect(); 2451 } 2452 window._horizontalHeaderResizeObserver = new ResizeObserver(function() { 2453 applyHorizontalHeaderContentInset(); 2454 }); 2455 window._horizontalHeaderResizeObserver.observe(header); 2456} 2457 2458function syncContentMarginToHorizontalHeader() { 2459 if (!document.body.classList.contains('menu-layout-horizontal')) { 2460 return; 2461 } 2462 requestAnimationFrame(function() { 2463 requestAnimationFrame(function() { 2464 applyHorizontalHeaderContentInset(); 2465 ensureHorizontalHeaderHeightObserver(); 2466 }); 2467 }); 2468} 2469window.syncContentMarginToHorizontalHeader = syncContentMarginToHorizontalHeader; 2470window.applyHorizontalHeaderContentInset = applyHorizontalHeaderContentInset; 2471 2472function getEffectiveMenuLayout() { 2473 if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.getEffectiveMenuLayout === 'function') { 2474 return window.MenuStyleCustomizer.getEffectiveMenuLayout(); 2475 } 2476 if (document.body.classList.contains('menu-layout-horizontal')) return 'horizontal'; 2477 if (document.body.classList.contains('menu-layout-top')) return 'top'; 2478 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 2479 return window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; 2480 } 2481 return 'sidebar'; 2482} 2483 2484function ensureCorrectLayoutApplied() { 2485 // Check if MenuStyleCustomizer and its necessary parts exist 2486 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') { 2487 // Prefer in-editor settings (e.g. Style History preview) over theme/body heuristics 2488 const fromSettings = window.MenuStyleCustomizer.settings.menuLayout; 2489 const currentLayout = fromSettings || getEffectiveMenuLayout(); 2490 console.log(`Ensuring layout is applied: ${currentLayout}`); 2491 2492 // Apply the layout styles (CSS classes, display properties) 2493 window.MenuStyleCustomizer._applyMenuLayout(currentLayout); 2494 2495 // --- Force Header Structure Rebuild for Horizontal Layout --- 2496 if (currentLayout === 'horizontal') { 2497 const mobileHeader = document.querySelector('.mobile-header'); 2498 if (mobileHeader) { 2499 let mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content'); 2500 // Ensure the main content container exists 2501 if (!mobileHeaderContent) { 2502 mobileHeaderContent = document.createElement('div'); 2503 mobileHeaderContent.className = 'mobile-header-content'; 2504 const hamburgerBtn = mobileHeader.querySelector('.hamburger-btn'); 2505 if (hamburgerBtn) { 2506 mobileHeader.insertBefore(mobileHeaderContent, hamburgerBtn); 2507 } else { 2508 mobileHeader.appendChild(mobileHeaderContent); 2509 } 2510 } 2511 // Ensure specific sub-containers exist, creating them if necessary 2512 // This guarantees the structure is present before renderHorizontalMenu fills them. 2513 if (!mobileHeaderContent.querySelector('.horizontal-header-logo')) { 2514 const logoContainer = document.createElement('div'); 2515 logoContainer.className = 'horizontal-header-logo'; 2516 mobileHeaderContent.insertBefore(logoContainer, mobileHeaderContent.firstChild); 2517 } 2518 if (!mobileHeaderContent.querySelector('.horizontal-menu
2518-container')) { 2519 const menuContainer = document.createElement('div'); 2520 menuContainer.className = 'horizontal-menu-container'; 2521 const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo'); 2522 if (logoContainer) { 2523 logoContainer.insertAdjacentElement('afterend', menuContainer); 2524 } else { // Fallback if logo container wasn't found/created 2525 mobileHeaderContent.insertBefore(menuContainer, mobileHeaderContent.firstChild); 2526 } 2527 } 2528 // Apply flexbox styling to position social container at far right 2529 mobileHeaderContent.style.display = 'flex'; 2530 const isSpaceColonyTheme = isSpaceColonySiteTheme(); 2531 const isMobile = window.innerWidth <= 768; 2532 mobileHeaderContent.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : (isSpaceColonyTheme && isMobile ? 'flex-start' : 'center'); 2533 mobileHeaderContent.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : (isSpaceColonyTheme ? 'flex-start' : 'space-between'); 2534 if (isSpaceColonyTheme && !isMobile) { 2535 mobileHeaderContent.style.flexGrow = '0'; 2536 mobileHeaderContent.style.width = ''; 2537 } else { 2538 mobileHeaderContent.style.flexGrow = ''; 2539 mobileHeaderContent.style.width = '100%'; 2540 } 2541 2542 // Create a flex container for logo and menu items 2543 const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container'); 2544 if (!logoMenuContainer) { 2545 console.log('[ensureCorrectLayoutApplied] Creating logo-menu wrapper...'); 2546 const logoMenuWrapper = document.createElement('div'); 2547 logoMenuWrapper.className = 'logo-menu-container'; 2548 // Move logo and menu into the wrapper 2549 const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo'); 2550 const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container'); 2551 2552 console.log('[ensureCorrectLayoutApplied] Found logo container:', !!logoContainer); 2553 console.log('[ensureCorrectLayoutApplied] Found menu container:', !!menuContainer); 2554 2555 if (logoContainer && menuContainer) { 2556 // Insert wrapper before the logo 2557 mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer); 2558 // Move both containers into the wrapper 2559 logoMenuWrapper.appendChild(logoContainer); 2560 logoMenuWrapper.appendChild(menuContainer); 2561 console.log('[ensureCorrectLayoutApplied] Successfully moved logo and menu into wrapper'); 2562 } else { 2563 console.warn('[ensureCorrectLayoutApplied] Missing logo or menu container, cannot create wrapper'); 2564 } 2565 } else { 2566 console.log('[ensureCorrectLayoutApplied] Logo-menu wrapper already exists'); 2567 } 2568 if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') { 2569 applyHorizontalLogoMenuLayoutStyles(); 2570 } 2571 2572 // Add social icons container for horizontal layout (AFTER logo-menu wrapper is created) 2573 if (!mobileHeaderContent.querySelector('.horizontal-social-container')) { 2574 const socialContainer = document.createElement('div'); 2575 socialContainer.className = 'horizontal-social-container'; 2576 // Insert at the end (far right) of the mobile header content 2577 mobileHeaderContent.appendChild(socialContainer); 2578 } 2579 } 2580 } 2581 // --- End Header Structure Rebuild --- 2582 2583 2584 // Re-render the correct menu structure based on the applied layout 2585 if (currentLayout === 'horizontal') { 2586 // If horizontal layout, render the horizontal menu 2587 if (typeof renderHorizontalMenu === 'function') { 2588 renderHorizontalMenu(); 2589 } else { 2590 console.warn("renderHorizontalMenu function not found."); 2591 } 2592 2593 // After rendering horizontal menu, update mobile header content 2594 // This ensures mobile header shows logo instead of menu items on mobile 2595 setTimeout(() => { 2596 if (typeof updateMobileHeaderContent === 'function') { 2597 updateMobileHeaderContent(); 2598 } 2599 }, 100); 2600 } else { 2601 // For sidebar or top layouts, render the standard gallery tree 2602 if (typeof renderGalleries === 'function') { 2603 renderGalleries(); 2604 } else { 2605 console.warn("renderGalleries function not found."); 2606 } 2607 2608 // Update mobile header content for other layouts too 2609 setTimeout(() => { 2610 if (typeof updateMobileHeaderContent === 'function') { 2611 updateMobileHeaderContent(); 2612 } 2613 }, 100); 2614 } 2615 } else { 2616 // Log a warning if the necessary components aren't available 2617 console.warn("MenuStyleCustomizer or its methods not available to re-apply layout."); 2618 } 2619} 2620 2621function applyLayoutStylesOnly() { 2622 // Check if MenuStyleCustomizer and its necessary parts exist 2623 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && typeof window.MenuStyleCustomizer._applyMenuLayout === 'function') { 2624 // Get the current layout setting, default to 'sidebar' if not set 2625 const currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; 2626 console.log(`APPLY STYLES: Applying styles for layout: ${currentLayout}`); 2627 // Apply the layout styles (CSS classes, display properties) 2628 window.MenuStyleCustomizer._applyMenuLayout(currentLayout); // This function should ONLY set body classes/styles 2629 } else { 2630 console.warn("APPLY STYLES: MenuStyleCustomizer or its methods not available."); 2631 } 2632} 2633 2634// Add this function to ensure sidebar content for mobile 2635function ensureSidebarContentForMobile() { 2636 const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal'); 2637 const isMobileView = window.innerWidth <= 768; 2638 2639 if (isHorizontalLayout && isMobileView) { 2640 console.log('Ensuring sidebar content for mobile horizontal layout'); 2641 2642 // Make sure the sidebar tree is populated 2643 const galleryTreeContainer = document.getElementById('galleryTree'); 2644 if (galleryTreeContainer && (!galleryTreeContainer.innerHTML || galleryTreeContainer.innerHTML.trim() === '')) { 2645 console.log('Sidebar tree is empty, populating with galleries'); 2646 2647 // Render the galleries in the sidebar tree for mobile navigation 2648 if (typeof renderGalleries === 'function') { 2649 renderGalleries(); 2650 } 2651 } 2652 2653 // Also ensure sidebar elements are rendered 2654 if (window.SidebarManager && typeof window.SidebarManager.renderElements === 'function') { 2655 console.log('Re-rendering sidebar elements for mobile'); 2656 window.SidebarManager.renderElements(); 2657 } 2658 } 2659} 2660 2661function renderTextLogo(container, htmlContentFromSidebarElement) { 2662 if (!container) { 2663 console.error("[RenderTextLogo] Error: Logo container not found."); 2664 return; 2665 } 2666 2667 container.innerHTML = ''; 2668 2669 const raw = (htmlContentFromSidebarElement || '').trim(); 2670 if (!raw) { 2671 container.innerHTML = '<span class="site-title-text">Menu</span>'; 2672 } else if (!/<[a-z][sS]*>/i.test(raw)) { 2673 // Plain text â wrap so theme CSS can target .site-title-text 2674 container.innerHTML = '<span class="site-title-text">' + raw + '</span>'; 2675 } else { 2676 container.innerHTML = raw; 2677 const titleEl = container.querySelector('.site-title-text, h1, h2, p:last-of-type, p:only-of-type'); 2678 if (titleEl && !titleEl.classList.contains('site-title-text')) { 2679 titleEl.classList.add('site-title-text'); 2680 } 2681 } 2682
2683 // Do not force heading/paragraph margins to 0 â menu-styles.css matches 2684 // .sidebar-text rhythm (h1 margin-bottom 0.5em). Zeroing made the bar tighter. 2685 2686 console.log(`[RenderTextLogo] Text logo HTML rendered directly into container.`); 2687} 2688 2689function renderHorizontalMenu() { 2690 console.log("[RenderHorizontalMenu] Function CALLED. Current edit mode:", typeof isEditing !== 'undefined' ? isEditing : 'isEditing_undefined'); 2691 2692 // CRITICAL: Check if we're actually in horizontal layout (theme overlay counts) 2693 const currentLayout = getEffectiveMenuLayout(); 2694 2695 const isHorizontalLayout = currentLayout === 'horizontal'; 2696 console.log("[RenderHorizontalMenu] Current layout:", currentLayout, "isHorizontalLayout:", isHorizontalLayout); 2697 2698 // If not horizontal layout, only handle logo/alignment, then return early 2699 if (!isHorizontalLayout) { 2700 console.log("[RenderHorizontalMenu] Not in horizontal layout, only handling logo/alignment, skipping menu item creation"); 2701 // Still handle logo/alignment for sidebar layout 2702 const mobileHeader = document.querySelector('.mobile-header'); 2703 if (mobileHeader) { 2704 const logoContainer = mobileHeader.querySelector('.horizontal-header-logo'); 2705 if (logoContainer) { 2706 // Handle logo rendering only (code below will handle this) 2707 // But we'll return before creating menu items 2708 } 2709 } 2710 // Return early - don't create horizontal menu items in sidebar layout 2711 return; 2712 } 2713 2714 // Wait for sidebar elements to be available before rendering 2715 if (!window.SidebarManager || !window.SidebarManager.elements || window.SidebarManager.elements.length === 0) { 2716 console.log("[RenderHorizontalMenu] Sidebar elements not yet available, waiting..."); 2717 const waitForSidebarElements = (attempts = 0) => { 2718 const maxAttempts = 20; // Increased attempts for slower loads 2719 if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 2720 console.log("[RenderHorizontalMenu] Sidebar elements now available, proceeding with render."); 2721 renderHorizontalMenu(); 2722 } else if (attempts < maxAttempts) { 2723 const attemptNum = attempts + 1; 2724 setTimeout(() => waitForSidebarElements(attempts + 1), 100); 2725 } else { 2726 console.warn("[RenderHorizontalMenu] Sidebar elements not available after waiting, rendering anyway."); 2727 // Continue with render even if sidebar elements aren't loaded 2728 } 2729 }; 2730 waitForSidebarElements(); 2731 return; 2732 } 2733 2734 const mobileHeader = document.querySelector('.mobile-header'); 2735 if (!mobileHeader) { 2736 console.error("[RenderHorizontalMenu] CRITICAL: .mobile-header element NOT FOUND."); 2737 return; 2738 } 2739 2740 // Apply flexbox styling to position social container at far right 2741 const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content'); 2742 if (mobileHeaderContent) { 2743 mobileHeaderContent.style.display = 'flex'; 2744 const isSpaceColonyTheme = isSpaceColonySiteTheme(); 2745 const isMobile = window.innerWidth <= 768; 2746 mobileHeaderContent.style.alignItems = isSpaceColonyTheme && !isMobile ? 'flex-end' : (isSpaceColonyTheme && isMobile ? 'flex-start' : 'center'); 2747 mobileHeaderContent.style.justifyContent = isMobile ? (isSpaceColonyTheme ? 'flex-start' : 'center') : (isSpaceColonyTheme ? 'flex-start' : 'space-between'); 2748 if (isSpaceColonyTheme && !isMobile) { 2749 mobileHeaderContent.style.flexGrow = '0'; 2750 mobileHeaderContent.style.width = ''; 2751 } else { 2752 mobileHeaderContent.style.flexGrow = ''; 2753 mobileHeaderContent.style.width = '100%'; 2754 } 2755 2756 // Create a flex container for logo and menu items 2757 const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container'); 2758 if (!logoMenuContainer) { 2759 console.log('[RenderHorizontalMenu] Creating logo-menu wrapper...'); 2760 const logoMenuWrapper = document.createElement('div'); 2761 logoMenuWrapper.className = 'logo-menu-container'; 2762 // Move logo and menu into the wrapper 2763 const logoContainer = mobileHeaderContent.querySelector('.horizontal-header-logo'); 2764 const menuContainer = mobileHeaderContent.querySelector('.horizontal-menu
2764-container'); 2765 2766 console.log('[RenderHorizontalMenu] Found logo container:', !!logoContainer); 2767 console.log('[RenderHorizontalMenu] Found menu container:', !!menuContainer); 2768 2769 if (logoContainer && menuContainer) { 2770 // Insert wrapper before the logo 2771 mobileHeaderContent.insertBefore(logoMenuWrapper, logoContainer); 2772 // Move both containers into the wrapper 2773 logoMenuWrapper.appendChild(logoContainer); 2774 logoMenuWrapper.appendChild(menuContainer); 2775 console.log('[RenderHorizontalMenu] Successfully moved logo and menu into wrapper'); 2776 } else { 2777 console.warn('[RenderHorizontalMenu] Missing logo or menu container, cannot create wrapper'); 2778 } 2779 } else { 2780 console.log('[RenderHorizontalMenu] Logo-menu wrapper already exists'); 2781 } 2782 if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') { 2783 applyHorizontalLogoMenuLayoutStyles(); 2784 } 2785 } 2786 2787 const logoContainer = mobileHeader.querySelector('.horizontal-header-logo'); 2788 if (!logoContainer) { 2789 console.error("[RenderHorizontalMenu] CRITICAL: .horizontal-header-logo container NOT FOUND within .mobile-header."); 2790 return; 2791 } 2792 2793 console.log("[RenderHorizontalMenu] Found .horizontal-header-logo container"); 2794 logoContainer.innerHTML = ''; // Clear existing logo content first 2795 2796 // FIXED: Check if we're on mobile - if so, don't render the horizontal logo 2797 const isMobileView = window.innerWidth <= 768; // Adjust breakpoint as needed 2798 2799 if (isMobileView) { 2800 console.log("[RenderHorizontalMenu] Mobile view detected - skipping horizontal logo rendering"); 2801 // On mobile, the logo will be handled by updateMobileHeaderContent() 2802 } else { 2803 console.log("[RenderHorizontalMenu] Desktop view - rendering horizontal logo"); 2804 2805 // Get the logo element data 2806 let logoElementData = null; 2807 try { 2808 logoElementData = getFirstSidebarElementForHeader(); 2809 console.log('[RenderHorizontalMenu] getFirstSidebarElementForHeader() returned:', JSON.stringify(logoElementData)); 2810 } catch (error) { 2811 console.error('[RenderHorizontalMenu] Error getting sidebar element:', error); 2812 logoElementData = getFirstSidebarElementFallback(); 2813 } 2814 2815 if (logoElementData) { 2816 if (logoElementData.type === 'image' && (logoElementData.imageData || logoElementData.imageUrl)) { 2817 console.log('[RenderHorizontalMenu] Attempting to render IMAGE logo with src:', logoElementData.imageData || logoElementData.imageUrl); 2818 2819 const img = document.createElement('img'); 2820 img.src = logoElementData.imageUrl || logoElementData.imageData; 2821 img.alt = resolveBrandLogoAlt(logoElementData); 2822 img.className = 'sidebar-image'; 2823 2824 // Apply comprehensive styling to ensure visibility 2825 img.style.display = 'block'; 2826 2827 // --- FIX --- 2828 // The line below was removed. It was applying a fixed max-height of 50px, 2829 // overriding your CSS. Removing it allows your stylesheet to take control. 2830 // img.style.maxHeight = '50px'; 2831 2832 img.style.objectFit = 'contain'; 2833 img.style.minWidth = '1px'; 2834 img.style.minHeight = '0'; 2835 2836 let logoDisplayNode = img; 2837 if (logoElementData.colorizeWithSiteColors && 2838 window.SidebarManager && 2839 typeof window.SidebarManager._buildSidebarImageDisplayNode === 'function') { 2840 const buildData = Object.assign({}, logoElementData); 2841 if (!buildData.imageWidth) { 2842 buildData.imageWidth = 'auto'; 2843 } 2844 logoDisplayNode = window.SidebarManager._buildSidebarImageDisplayNode(img, buildData); 2845 } else { 2846 img.style.width = 'auto'; 2847 img.style.visibility = 'visible'; 2848 img.style.opacity = '1'; 2849 } 2850 2851 // Opt-in per-image padding wrapper (no-op when unset; does not alter .horizontal-header-logo CSS) 2852 if (window.SidebarManager && 2853 typeof window.SidebarManager._wrapWithImagePadding === 'function') { 2854 logoDisplayNode = window.SidebarManager._wrapWithImagePadding(logoDisplayNode, logoElementData); 2855 } 2856 2857 // Enhanced event listeners for debugging 2858 img.onload = () => { 2859 console.log('[RenderHorizontalMenu] Image successfully LOADED'); 2860 }; 2861 2862 img.onerror = (error) => { 2863 console.error('[RenderHorizontalMenu] Image FAILED to load:', error); 2864 renderTextLogo(logoContainer, logoElementData.title || 'Logo'); 2865 }; 2866 2867 try { 2868 // Handle linking (similar to sidebar-manager.js) 2869 // Only add links in view mode, not edit mode 2870 // Explicit edit toggle only â Parameters.isInEditor is also true for logged-in admins 2871 const isEditing = document.body.classList.contains('edit-mode-active') || 2872 (typeof window.isInEditMode === 'function' && window.isInEditMode()); 2873 2874 // Debug: Log link properties and element data 2875 console.log('[RenderHorizontalMenu] Full logoElementData:', logoElementData); 2876 console.log('[RenderHorizontalMenu] Image link properties:', { 2877 linkType: logoElementData.linkType, 2878 linkUrl: logoElementData.linkUrl, 2879 linkPageId: logoElementData.linkPageId, 2880 linkTarget: logoElementData.linkTarget, 2881 isEditing: isEditing, 2882 hasLinkType: !!logoElementData.linkType, 2883 linkTypeValue: logoElementData.linkType, 2884 linkTypeNotNone: logoElementData.linkType && logoElementData.linkType !== 'none', 2885 notEditing: !isEditing, 2886 bodyHasEditClass: document.body.classList.contains('edit-mode-active'), 2887 isInEditMode: typeof window.isInEditMode === 'function' ? window.isInEditMode() : 'N/A', 2888 ParametersIsInEditor: window.Parameters ? window.Parameters.isInEditor : 'N/A' 2889 }); 2890 2891 // Check for link - handle undefined, null, empty string, or 'none' 2892 const hasLink = logoElementData.linkType && 2893 logoElementData.linkType !== 'none' && 2894 logoElementData.linkType !== '' && 2895 !isEditing; 2896 2897 console.log('[RenderHorizontalMenu] Has link?', hasLink, 'linkType:', logoElementData.linkType); 2898 2899 if (hasLink) { 2900 const link = document.createElement('a'); 2901 link.target = logoElementData.linkTarget || '_self'; 2902 let finalLinkHref = '#'; 2903 let isValidLink = false;
2904 2905 if (logoElementData.linkType === 'external' && logoElementData.linkUrl) { 2906 finalLinkHref = logoElementData.linkUrl; 2907 if (link.target === '_blank') { 2908 link.rel = 'noopener noreferrer'; 2909 } 2910 isValidLink = true; 2911 } else if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) { 2912 const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []); 2913 const linkPageIdNum = parseInt(logoElementData.linkPageId); 2914 console.log('[RenderHorizontalMenu] Looking for internal page:', { 2915 linkPageId: logoElementData.linkPageId, 2916 linkPageIdNum: linkPageIdNum, 2917 allGalleriesLength: allGalleries ? allGalleries.length : 0, 2918 galleryIds: allGalleries ? allGalleries.map(g => ({ id: g.id, title: g.title })).slice(0, 5) : [] 2919 }); 2920 2921 const targetPage = allGalleries.find(g => g.id === linkPageIdNum); 2922 2923 console.log('[RenderHorizontalMenu] Target page found:', targetPage ? { id: targetPage.id, title: targetPage.title, slug: targetPage.slug, url: targetPage.url } : null); 2924 2925 if (targetPage) { 2926 // Handle preview mode URLs 2927 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 2928 console.log('[RenderHorizontalMenu] Preview mode:', isPreviewMode); 2929 2930 if (isPreviewMode) { 2931 const pathParts = window.location.pathname.split('/').filter(Boolean); 2932 const siteGuid = pathParts[0]; 2933 console.log('[RenderHorizontalMenu] Preview mode path parts:', pathParts, 'siteGuid:', siteGuid, 'targetPage.slug:', targetPage.slug); 2934 2935 if (siteGuid && targetPage.slug) { 2936 finalLinkHref = '/' + siteGuid + '/' + targetPage.slug; 2937 isValidLink = true; 2938 console.log('[RenderHorizontalMenu] Set preview href:', finalLinkHref); 2939 } else if (targetPage.url && targetPage.url.startsWith('/')) { 2940 finalLinkHref = targetPage.url; 2941 isValidLink = true; 2942 console.log('[RenderHorizontalMenu] Set preview href from url:', finalLinkHref); 2943 } 2944 } else { 2945 if (targetPage.slug) { 2946 finalLinkHref = '/' + targetPage.slug; 2947 isValidLink = true; 2948 console.log('[RenderHorizontalMenu] Set non-preview href:', finalLinkHref); 2949 } else if (targetPage.url && targetPage.url.startsWith('/')) { 2950 finalLinkHref = targetPage.url; 2951 isValidLink = true; 2952 console.log('[RenderHorizontalMenu] Set non-preview href from url:', finalLinkHref); 2953 } 2954 } 2955 } else { 2956 console.warn('[RenderHorizontalMenu] Target page not found for linkPageId:', logoElementData.linkPageId); 2957 } 2958 } 2959 2960 if (isValidLink) { 2961 link.href = finalLinkHref; 2962 // Ensure link is clickable 2963 link.style.pointerEvents = 'auto'; 2964 link.style.cursor = 'pointer'; 2965 link.style.textDecoration = 'none'; 2966 link.style.display = 'flex'; 2967 link.style.alignItems = 'center'; 2968 link.style.height = 'auto'; 2969 link.style.maxHeight = '100%'; 2970 link.style.minHeight = '0'; 2971 link.style.lineHeight = '0'; 2972 console.log('[RenderHorizontalMenu] Link created with href:', finalLinkHref, 'target:', link.target, 'styles applied'); 2973 2974 // For internal links, add click handler to use site navigation 2975 if (logoElementData.linkType === 'internal' && logoElementData.linkPageId) { 2976 // Store linkPageId in a variable that won't be affected by closure 2977 const linkPageId = logoElementData.linkPageId; 2978 const linkHref = finalLinkHref; 2979 2980 // Add click handler with both capture and bubble phases to ensure it fires 2981 const clickHandler = (e) => { 2982 console.log('[RenderHorizontalMenu] Link clicked! linkPageId:', linkPageId, 'event:', e); 2983 e.preventDefault(); 2984 e.stopPropagation(); 2985 e.stopImmediatePropagation(); 2986 2987 const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []); 2988 const linkPageIdNum = parseInt(linkPageId); 2989 const targetPage = allGalleries.find(g => g.id === linkPageIdNum); 2990 2991 console.log('[RenderHorizontalMenu] Click handler - targetPage found:', targetPage ? { id: targetPage.id, title: targetPage.title, isPage: targetPage.isPage } : null); 2992 console.log('[RenderHorizontalMenu] Click handler - loadPage function:', typeof window.loadPage, 'loadGallery function:', typeof window.loadGallery); 2993 2994 if (targetPage) { 2995 if (isPageMenuItem(targetPage) && typeof window.loadPage === 'function') { 2996 console.log('[RenderHorizontalMenu] Calling loadPage with id:', targetPage.id); 2997 window.loadPage(targetPage.id); 2998 } else if (typeof window.loadGallery === 'function') { 2999 console.log('[RenderHorizontalMenu] Calling loadGallery with id:', targetPage.id); 3000 window.loadGallery(targetPage.id); 3001 } else { 3002 // Fallback to standard navigation 3003 console.log('[RenderHorizontalMenu] Fallback to window.location.href:', linkHref); 3004 window.location.href = linkHref; 3005 } 3006 } else { 3007 // Fallback to standard navigation if page not found 3008 console.warn('[RenderHorizontalMenu] Target page not found in click handler, using fallback href:', linkHref); 3009 window.location.href = linkHref; 3010 } 3011 3012 return false;
3013 }; 3014 3015 // Add handler in capture phase to catch event early 3016 link.addEventListener('click', clickHandler, true); 3017 // Also add in bubble phase as backup 3018 link.addEventListener('click', clickHandler, false); 3019 3020 // Also add mousedown as backup in case click is being prevented 3021 link.addEventListener('mousedown', (e) => { 3022 if (e.button === 0) { // Left click only 3023 console.log('[RenderHorizontalMenu] MouseDown detected on link, triggering click handler'); 3024 clickHandler(e); 3025 } 3026 }); 3027 3028 console.log('[RenderHorizontalMenu] Click handlers attached to link'); 3029 } 3030 3031 link.appendChild(logoDisplayNode); 3032 logoContainer.appendChild(link); 3033 console.log('[RenderHorizontalMenu] SUCCESSFULLY appended linked image to logoContainer. Link element:', link, 'href:', link.href, 'parent:', logoContainer); 3034 3035 // Verify link is in DOM and clickable 3036 setTimeout(() => { 3037 const verifyLink = logoContainer.querySelector('a'); 3038 if (verifyLink) { 3039 console.log('[RenderHorizontalMenu] Link verification - Found in DOM:', { 3040 href: verifyLink.href, 3041 pointerEvents: window.getComputedStyle(verifyLink).pointerEvents, 3042 cursor: window.getComputedStyle(verifyLink).cursor, 3043 zIndex: window.getComputedStyle(verifyLink).zIndex, 3044 position: window.getComputedStyle(verifyLink).position 3045 }); 3046 } else { 3047 console.warn('[RenderHorizontalMenu] Link verification - Link NOT found in DOM!'); 3048 } 3049 }, 100); 3050 } else { 3051 console.warn('[RenderHorizontalMenu] Link is not valid, appending image without link. isValidLink:', isValidLink); 3052 logoContainer.appendChild(logoDisplayNode); 3053 console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.'); 3054 } 3055 } else { 3056 logoContainer.appendChild(logoDisplayNode); 3057 console.log('[RenderHorizontalMenu] SUCCESSFULLY appended image to logoContainer.'); 3058 } 3059 } catch (e) { 3060 console.error('[RenderHorizontalMenu] Error during logoContainer.appendChild(img):', e); 3061 renderTextLogo(logoContainer, logoElementData.title || 'Logo'); 3062 } 3063 3064 } else if (logoElementData.type === 'text' && logoElementData.textContent) { 3065 console.log('[RenderHorizontalMenu] Attempting to render TEXT logo:', logoElementData.textContent); 3066 renderTextLogo(logoContainer, logoElementData.textContent); 3067 3068 } else { 3069 console.log('[RenderHorizontalMenu] Using title as fallback logo'); 3070 renderTextLogo(logoContainer, logoElementData.title || 'Menu'); 3071 } 3072 } else { 3073 console.log("[RenderHorizontalMenu] No logoElementData available. Using default text."); 3074 renderTextLogo(logoContainer, 'Menu'); 3075 } 3076 } 3077 3078 // Render menu items (only on desktop, not on mobile) 3079 const menuContainer = mobileHeader.querySelector('.horizontal-menu-container'); 3080 if (!menuContainer) { 3081 console.error("[RenderHorizontalMenu] .horizontal-menu-container NOT FOUND within .mobile-header."); 3082 } else { 3083 menuContainer.innerHTML = ''; 3084 3085 // Apply alignment class from settings 3086 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 3087 const rawAlign = window.MenuStyleCustomizer.settings.horizontalMenuAlignment || 'left'; 3088 const alignment = ['left', 'center', 'right'].indexOf(rawAlign) >= 0 ? rawAlign : 'left'; 3089 menuContainer.classList.remove('menu-align-left', 'menu-align-right', 'menu-align-center'); 3090 menuContainer.classList.add('menu-align-' + alignment); 3091 console.log('[RenderHorizontalMenu] Applied alignment class: menu-align-'
3091+ alignment); 3092 } 3093 3094 // Only populate menu items on desktop, not on mobile 3095 if (!isMobileView) { 3096 let galleryTree = []; 3097 if (typeof createGalleryTree === 'function' && typeof galleries !== 'undefined') { 3098 galleryTree = createGalleryTree(galleries); 3099 } else { 3100 console.warn("[RenderHorizontalMenu] createGalleryTree function or galleries array is undefined."); 3101 } 3102 3103 galleryTree.forEach(item => { 3104 const menuItemElement = createHorizontalMenuItem(item); 3105 if (menuItemElement) { 3106 menuContainer.appendChild(menuItemElement); 3107 } 3108 }); 3109 if (typeof syncSeoNavA11y === 'function') syncSeoNavA11y(); 3110 } else { 3111 console.log("[RenderHorizontalMenu] Mobile view - skipping horizontal menu item population"); 3112 } 3113 } 3114 3115 // Render social icons (only on desktop, not on mobile) 3116 let socialContainer = mobileHeader.querySelector('.horizontal-social-container'); 3117 if (!socialContainer) { 3118 // Fallback: create the social container if it doesn't exist 3119 console.log("[RenderHorizontalMenu] .horizontal-social-container not found, creating it..."); 3120 const mobileHeaderContent = mobileHeader.querySelector('.mobile-header-content'); 3121 if (mobileHeaderContent) { 3122 socialContainer = document.createElement('div'); 3123 socialContainer.className = 'horizontal-social-container'; 3124 mobileHeaderContent.appendChild(socialContainer); 3125 console.log("[RenderHorizontalMenu] Created .horizontal-social-container"); 3126 } else { 3127 console.error("[RenderHorizontalMenu] .mobile-header-content not found, cannot create social container"); 3128 } 3129 } 3130 3131 if (socialContainer) { 3132 socialContainer.innerHTML = ''; 3133 3134 // Only populate social icons on desktop, not on mobile 3135 if (!isMobileView) { 3136 const socialElement = getFirstSocialElementForHeader(); 3137 if (socialElement && socialElement.type === 'social') { 3138 console.log('[RenderHorizontalMenu] Found social element, rendering social icons'); 3139 renderSocialIcons(socialContainer, socialElement); 3140 } else { 3141 console.log('[RenderHorizontalMenu] No social element found or element is not social type'); 3142 // Container remains empty - no impact on layout 3143 } 3144 } else { 3145 console.log("[RenderHorizontalMenu] Mobile view - skipping social icons population"); 3146 } 3147 } 3148 3149 3150 3151 if (typeof updateActiveStatesHorizontal === 'function') { 3152 updateActiveStatesHorizontal(); 3153 } 3154 3155 if (!isMobileView && mobileHeaderContent) { 3156 mobileHeaderContent.querySelectorAll('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title').forEach(function(el) { 3157 if (!el.closest('.horizontal-header-logo')) { 3158 el.remove(); 3159 } 3160 }); 3161 } 3162 3163setTimeout(() => { 3164 ensureSidebarContentForMobile(); 3165}, 100); 3166 3167 if (typeof syncContentMarginToHorizontalHeader === 'function') { 3168 setTimeout(function() { syncContentMarginToHorizontalHeader(); }, 200); 3169 } 3170 3171 console.log("[RenderHorizontalMenu] Function COMPLETED."); 3172} 3173 3174/** 3175 * Renders social icons in the horizontal header container. 3176 * @param {HTMLElement} container - The container to render social icons in 3177 * @param {object} socialElement - The social element data from sidebar 3178 */
3179function renderSocialIcons(container, socialElement) { 3180 console.log('[RenderSocialIcons] Rendering social icons:', socialElement); 3181 3182 try { 3183 // Safety check: ensure we have a valid social element 3184 if (!socialElement || socialElement.type !== 'social') { 3185 console.log('[RenderSocialIcons] Invalid social element - skipping rendering'); 3186 return; 3187 } 3188 3189 /* Placeholders without URLs still need menu color (CSS also sets this). */ 3190 container.style.color = 'var(--menu-color, #333)'; 3191 3192 // Get icon size with proper fallbacks 3193 let iconSize = 30; 3194 if (typeof socialElement.socialIconSize === 'number' && !isNaN(socialElement.socialIconSize)) { 3195 iconSize = socialElement.socialIconSize; 3196 } else if (typeof socialElement.socialIconSize === 'string' && !isNaN(parseInt(socialElement.socialIconSize))) { 3197 iconSize = parseInt(socialElement.socialIconSize); 3198 } 3199 3200 // Render icons directly from element data 3201 if (socialElement.socialIcons && Array.isArray(socialElement.socialIcons)) { 3202 socialElement.socialIcons.forEach(icon => { 3203 // Create icon wrapper (color via CSS --menu-color so placeholders without URLs stay visible) 3204 const iconWrapper = document.createElement('div'); 3205 iconWrapper.className = 'social-icon-wrapper'; 3206 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;'; 3207 3208 const applySvgSizing = (host) => { 3209 const svg = host.querySelector('svg'); 3210 if (!svg) return; 3211 svg.style.width = '100%'; 3212 svg.style.height = '100%'; 3213 svg.setAttribute('width', '100%'); 3214 svg.setAttribute('height', '100%'); 3215 svg.setAttribute('preserveAspectRatio', 'xMidYMid meet'); 3216 svg.setAttribute('aria-hidden', 'true'); 3217 svg.setAttribute('focusable', 'false'); 3218 svg.style.color = 'inherit'; 3219 svg.style.fill = 'currentColor'; 3220 svg.style.stroke = 'currentColor'; 3221 }; 3222 3223 const hasUrl = !!(icon.url && String(icon.url).trim()); 3224 3225 // Add link if URL exists; otherwise still render the glyph (placeholder icons) 3226 if (hasUrl) { 3227 const link = document.createElement('a'); 3228 link.href = icon.url; 3229 link.target = '_blank'; 3230 link.rel = 'noopener noreferrer'; 3231 link.classList.add('horizontal-social-link'); 3232 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;'; 3233 const socialLabel = (icon.alt && String(icon.alt).trim()) 3234 || (window.SidebarManager && typeof window.SidebarManager._socialIconTypeLabel === 'function' 3235 ? window.SidebarManager._socialIconTypeLabel(icon.type) 3236 : (icon.type && icon.type !== 'custom' ? String(icon.type).replace(/_/g, ' ') : '')) 3237 || 'Social link'; 3238 const socialLabelPretty = socialLabel.charAt(0).toUpperCase() + socialLabel.slice(1); 3239 link.setAttribute('aria-label', socialLabelPretty + ' (opens in new tab)'); 3240 3241 if (icon.type === 'custom' && icon.iconUrl) { 3242 // Render uploaded image 3243 const img = document.createElement('img'); 3244 img.src = icon.iconUrl; 3245 img.alt = ''; 3246 img.setAttribute('aria-hidden', 'true'); 3247 img.style.width = '100%'; 3248 img.style.height = '100%'; 3249 img.style.objectFit = 'contain'; 3250 link.appendChild(img); 3251 } else { 3252 // Add SVG - use SidebarManager's method if available, otherwise create basic SVG 3253 if (window.SidebarManager && typeof window.SidebarManager._getSocialIconSvg === 'function') { 3254 link.innerHTML = window.SidebarManager._getSocialIconSvg(icon.type); 3255 } else { 3256 // Fallback: create a simple SVG placeholder 3257 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>'; 3258 } 3259 applySvgSizing(link); 3260 } 3261 iconWrapper.appendChild(link); 3262 } else {
3263 // Placeholder: no URL â still paint with menu color via container inheritance 3264 iconWrapper.setAttribute('aria-hidden', 'true'); 3265 if (icon.type === 'custom' && icon.iconUrl) { 3266 const img = document.createElement('img'); 3267 img.src = icon.iconUrl; 3268 img.alt = ''; 3269 img.style.width = '100%'; 3270 img.style.height = '100%'; 3271 img.style.objectFit = 'contain'; 3272 iconWrapper.appendChild(img); 3273 } else { 3274 if (window.SidebarManager && typeof window.SidebarManager._getSocialIconSvg === 'function') { 3275 iconWrapper.innerHTML = window.SidebarManager._getSocialIconSvg(icon.type); 3276 } else { 3277 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>'; 3278 } 3279 applySvgSizing(iconWrapper); 3280 } 3281 } 3282 3283 container.appendChild(iconWrapper); 3284 console.log('[RenderSocialIcons] Rendered social icon:', icon.type, icon.url); 3285 }); 3286 } else { 3287 console.log('[RenderSocialIcons] No social icons found in element'); 3288 } 3289 } catch (error) { 3290 console.error('[RenderSocialIcons] Error rendering social icons:', error); 3291 } 3292} 3293 3294/** 3295 * Gets the appropriate icon text or class for a social platform. 3296 * @param {string} platform - The social platform name 3297 * @returns {string} The icon text or class 3298 */ 3299function getSocialIconText(platform) { 3300 const iconMap = { 3301 'facebook': 'f', 3302 'twitter': 't', 3303 'instagram': 'i', 3304 'linkedin': 'in', 3305 'youtube': 'yt', 3306 'pinterest': 'p', 3307 'tiktok': 'tt', 3308 'snapchat': 'sc', 3309 'whatsapp': 'wa', 3310 'telegram': 'tg', 3311 'discord': 'd', 3312 'github': 'gh', 3313 'email': '@', 3314 'phone': 'ð', 3315 'website': 'ð' 3316 }; 3317 3318 return iconMap[platform.toLowerCase()] || 'â¢'; 3319} 3320 3321 3322 3323 3324/** 3325 * Creates a single top-level horizontal menu item and its submenu if it has children. 3326 * (UPDATED to handle async loadGallery and pass clickedItemId to a delayed updateActiveStatesHorizontal) 3327 * @param {object} galleryItem - The gallery item data. 3328 * @returns {HTMLElement | null} The created menu item element, or null if not rendered. 3329 */ 3330function createHorizontalMenuItem(galleryItem) { 3331 if (galleryItem.visible === false || galleryItem.isSpacer) { 3332 return null; 3333 } 3334 3335 const menuItem = document.createElement('div'); 3336 menuItem.className = 'horizontal-menu-item'; 3337 menuItem.dataset.id = String(galleryItem.id); 3338 menuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false'); 3339 3340 const hasVisibleChildren = !!(galleryItem.children && galleryItem.children.filter(c => c.visible !== false && !c.isSpacer).length); 3341 const isNavigableParent = isNavigableMenuParent(galleryItem); 3342 let titleEl; 3343 if (hasVisibleChildren && !isNavigableParent) { 3344 titleEl = document.createElement('span'); 3345 titleEl.className = 'horizontal-menu-label'; 3346 titleEl.textContent = galleryItem.title; 3347 menuItem.setAttribute('aria-haspopup', 'true'); 3348 menuItem.setAttribute('aria-expanded', 'false'); 3349 } else { 3350 titleEl = document.createElement('a'); 3351 titleEl.className = 'horizontal-menu-label'; 3352 titleEl.href = typeof getMenuItemHref === 'function' ? getMenuItemHref(galleryItem) : '#'; 3353 titleEl.textContent = galleryItem.title; 3354 if (galleryItem.isExternal) { 3355 titleEl.target = '_blank'; 3356 titleEl.rel = 'noopener noreferrer'; 3357 } else { 3358 titleEl.addEventListener('click', (e) => { e.preventDefault(); }); 3359 } 3360 } 3361 menuItem.appendChild(titleEl); 3362 3363 // Ensure activeGalleryId is treated as a number for comparison during initial render 3364 const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId)); 3365 if (galleryItem.id === currentGlobalActiveId) { 3366 menuItem.classList.add('active'); 3367 } 3368 3369 let leaveTimer; // For mouseleave timeout to close dropdown 3370 3371 if (galleryItem.children && galleryItem.children.length > 0) { 3372 const visibleChildren = galleryItem.children.filter(child => child.visible !== false && !child.isSpacer); 3373 3374 if (visibleChildren.length > 0) { 3375 menuItem.classList.add('has-children'); 3376 if (isNavigableParent) { 3377 menuItem.setAttribute('aria-haspopup', 'true'); 3378 menuItem.setAttribute('aria-expanded', 'false'); 3379 } 3380 3381 const toggleIconContainer = document.createElement(isNavigableParent ? 'button' : 'span'); 3382 toggleIconContainer.className = 'submenu-toggle-horizontal'; 3383 if (isNavigableParent) { 3384 toggleIconContainer.type = 'button'; 3385 toggleIconContainer.setAttribute('aria-label', 'Show submenu for ' + galleryItem.title); 3386 toggleIconContainer.setAttribute('aria-expanded', 'false'); 3387 toggleIconContainer.addEventListener('click', function(event) { 3388 event.preventDefault(); 3389 event.stopPropagation(); 3390 const willExpand = !menuItem.classList.contains('expanded'); 3391 menuItem.classList.toggle('expanded', willExpand); 3392 menuItem.setAttribute('aria-expanded', willExpand ? 'true' : 'false'); 3393 toggleIconContainer.setAttribute('aria-expanded', willExpand ? 'true' : 'false'); 3394 const icon = toggleIconContainer.querySelector('.icon.toggle-icon'); 3395 if (icon) icon.classList.toggle('rotated', willExpand); 3396 }); 3397 } 3398 const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 3399 svgIcon.setAttribute("class", "icon toggle-icon"); 3400 svgIcon.setAttribute("viewBox", "0 0 24 24"); 3401 svgIcon.setAttribute("fill", "none"); 3402 svgIcon.setAttribute("stroke", "currentColor"); 3403 svgIcon.setAttribute("stroke-width", "2"); 3404 const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); 3405 polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow
3406 svgIcon.appendChild(polyline); 3407 toggleIconContainer.appendChild(svgIcon); 3408 menuItem.appendChild(toggleIconContainer); 3409 3410 const subMenu = document.createElement('div'); 3411 subMenu.className = 'horizontal-dropdown'; 3412 const subMenuList = document.createElement('ul'); 3413 subMenuList.className = 'horizontal-dropdown-list'; 3414 3415 visibleChildren.forEach((childItem) => { 3416 const subMenuItemElement = createHorizontalSubMenuItem(childItem); // Recursive call 3417 if (subMenuItemElement) { 3418 subMenuList.appendChild(subMenuItemElement); 3419 } 3420 }); 3421 3422 if (subMenuList.hasChildNodes()) { 3423 subMenu.appendChild(subMenuList); 3424 menuItem.appendChild(subMenu); 3425 3426 menuItem.addEventListener('mouseenter', function() { 3427 clearTimeout(leaveTimer); 3428 document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => { 3429 if (openItem !== this) { 3430 openItem.classList.remove('expanded'); 3431 openItem.setAttribute('aria-expanded', 'false'); 3432 const otherIcon = openItem.querySelector('.icon.toggle-icon'); 3433 if (otherIcon) otherIcon.classList.remove('rotated'); 3434 } 3435 }); 3436 this.classList.add('expanded'); 3437 this.setAttribute('aria-expanded', 'true'); 3438 if (isNavigableParent) toggleIconContainer.setAttribute('aria-expanded', 'true'); 3439 const currentIcon = this.querySelector('.icon.toggle-icon'); 3440 if (currentIcon) currentIcon.classList.add('rotated'); 3441 }); 3442 3443 menuItem.addEventListener('mouseleave', function(event) { 3444 // Check if we're moving to the dropdown or a flyout 3445 const relatedTarget = event.relatedTarget; 3446 const isMovingToDropdown = relatedTarget && ( 3447 relatedTarget.closest('.horizontal-dropdown') || 3448 relatedTarget.classList.contains('horizontal-submenu-item') || 3449 relatedTarget.closest('.horizontal-submenu-item') 3450 ); 3451 const isMovingToFlyout = relatedTarget && ( 3452 relatedTarget.classList.contains('horizontal-nested-dropdown') || 3453 relatedTarget.closest('.horizontal-nested-dropdown') 3454 ); 3455 3456 if (!isMovingToDropdown && !isMovingToFlyout) { 3457 console.log('Main menu item mouseleave - starting timer'); 3458 leaveTimer = setTimeout(() => { 3459 // Check if there's an active flyout - if so, keep parent open 3460 // Use the same multiple detection methods as the submenu handler 3461 const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') || 3462 document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') || 3463 document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])'); 3464 console.log('Main menu timer fired - active flyout:', activeFlyout); 3465 if (!activeFlyout) { 3466 console.log('Main menu closing dropdown - no active flyout'); 3467 this.classList.remove('expanded'); 3468 this.setAttribute('aria-expanded', 'false'); 3469 if (isNavigableParent) toggleIconContainer.setAttribute('aria-expanded', 'false'); 3470 const currentIcon = this.querySelector('.icon.toggle-icon'); 3471 if (currentIcon) currentIcon.classList.remove('rotated'); 3472 } else { 3473 console.log('Main menu keeping dropdown open - flyout is active'); 3474 } 3475 }, 200); 3476 } else { 3477 console.log('Main menu item mouseleave - not starting timer (moving to dropdown or flyout)'); 3478 } 3479 }); 3480 3481 subMenu.addEventListener('mouseenter', function() { 3482 const parentMenuItem = this.closest('.horizontal-menu
3482-item'); 3483 if (parentMenuItem && parentMenuItem.leaveTimer) { 3484 clearTimeout(parentMenuItem.leaveTimer); 3485 parentMenuItem.leaveTimer = null; 3486 } 3487 }); 3488 3489 subMenu.addEventListener('mouseleave', function(event) { 3490 const parentMenuItem = this.closest('.horizontal-menu-item'); 3491 if (parentMenuItem) { 3492 // Check if we're moving to another element within the same dropdown 3493 const relatedTarget = event.relatedTarget; 3494 console.log('SubMenu mouseleave - relatedTarget:', relatedTarget); 3495 console.log('SubMenu mouseleave - relatedTarget classes:', relatedTarget ? relatedTarget.className : 'null'); 3496 3497 // Check if we're moving to another element within the same dropdown 3498 const currentDropdown = this.closest('.horizontal-dropdown') || this.closest('.horizontal-submenu')?.closest('.horizontal-dropdown'); 3499 console.log('Current dropdown found:', !!currentDropdown); 3500 console.log('Current dropdown element:', currentDropdown); 3501 console.log('RelatedTarget:', relatedTarget); 3502 console.log('RelatedTarget closest dropdown:', relatedTarget ? relatedTarget.closest('.horizontal-dropdown') : 'null'); 3503 3504 // Check if relatedTarget is within the same dropdown container 3505 const isMovingWithinDropdown = relatedTarget && currentDropdown && ( 3506 relatedTarget.closest('.horizontal-dropdown') === currentDropdown || 3507 relatedTarget.classList.contains('horizontal-submenu-item') || 3508 relatedTarget.closest('.horizontal-submenu-item') || 3509 currentDropdown.contains(relatedTarget) 3510 ); 3511 3512 // Alternative approach: check if relatedTarget is within the same parent menu item 3513 const parentMenuContainer = this.closest('.horizontal-menu-item'); 3514 const isMovingWithinParent = relatedTarget && parentMenuContainer && parentMenuContainer.contains(relatedTarget); 3515 console.log('Is moving within parent menu item:', isMovingWithinParent); 3516 console.log('Current dropdown contains relatedTarget:', currentDropdown ? currentDropdown.contains(relatedTarget) : 'no dropdown'); 3517 console.log('RelatedTarget closest dropdown === currentDropdown:', relatedTarget && currentDropdown ? relatedTarget.closest('.horizontal-dropdown') === currentDropdown : 'null'); 3518 3519 // Also check if mouse is still within the dropdown bounds (in case relatedTarget is null or outside) 3520 // Get the parent menu item's dropdown container 3521 const parentMenuItem = this.closest('.horizontal-menu-item'); 3522 console.log('Parent menu item found:', !!parentMenuItem); 3523 const dropdownContainer = parentMenuItem ? parentMenuItem.querySelector('.horizontal-dropdown') : null; 3524 console.log('Dropdown container found:', !!dropdownContainer); 3525 if (dropdownContainer) { 3526 console.log('Dropdown container classes:', dropdownContainer.className); 3527 console.log('Dropdown container display:', window.getComputedStyle(dropdownContainer).display); 3528 console.log('Dropdown container position:', window.getComputedStyle(dropdownContainer).position); 3529 } 3530 3531 // Get bounds from the visible dropdown (not the hidden one) 3532 let dropdownRect = { left: 0, right: 0, top: 0, bottom: 0 }; 3533 if (dropdownContainer) { 3534 const computedStyle = window.getComputedStyle(dropdownContainer); 3535 if (computedStyle.display !== 'none') { 3536 dropdownRect = dropdownContainer.getBoundingClientRect(); 3537 } else { 3538 // If dropdown is hidden, try to get bounds from the parent menu item 3539 const parentRect = parentMenuItem.getBoundingClientRect(); 3540 dropdownRect = parentRect; 3541 } 3542 } 3543 const mouseX = event.clientX; 3544 const mouseY = event.clientY; 3545 const isMouseInDropdownBounds = mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && 3546 mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom; 3547
3548 console.log('Dropdown bounds:', { 3549 left: dropdownRect.left, 3550 right: dropdownRect.right, 3551 top: dropdownRect.top, 3552 bottom: dropdownRect.bottom, 3553 mouseX, 3554 mouseY, 3555 mouseInBounds: mouseX >= dropdownRect.left && mouseX <= dropdownRect.right && mouseY >= dropdownRect.top && mouseY <= dropdownRect.bottom 3556 }); 3557 3558 // If there's an active flyout, always keep the parent dropdown open 3559 // Check multiple ways to detect active flyouts 3560 const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]') || 3561 document.querySelector('.horizontal-nested-dropdown[style*="visibility: visible"]') || 3562 document.querySelector('.horizontal-nested-dropdown:not([style*="display: none"])'); 3563 console.log('Active flyout found:', !!activeFlyout); 3564 if (activeFlyout) { 3565 console.log('Active flyout display style:', activeFlyout.style.display); 3566 console.log('Active flyout computed display:', window.getComputedStyle(activeFlyout).display); 3567 console.log('Active flyout visibility style:', activeFlyout.style.visibility); 3568 } 3569 const hasActiveFlyout = !!activeFlyout; 3570 3571 // If the dropdown is hidden (display: none), we can't get accurate bounds 3572 // So let's use a simpler approach - if we're moving to another submenu item, keep open 3573 const isMovingToSubmenuItem = relatedTarget && ( 3574 relatedTarget.classList.contains('horizontal-submenu-item') || 3575 relatedTarget.closest('.horizontal-submenu-item') 3576 ); 3577 console.log('Is moving to submenu item:', isMovingToSubmenuItem); 3578 console.log('RelatedTarget is submenu item:', relatedTarget ? relatedTarget.classList.contains('horizontal-submenu-item') : 'null'); 3579 console.log('RelatedTarget has submenu item ancestor:', relatedTarget ? !!relatedTarget.closest('.horizontal-submenu-item') : 'null'); 3580 3581 // If we're moving to another submenu item within the same dropdown, al
3581ways keep open 3582 // OR if there's an active flyout (regardless of where the mouse is), keep open 3583 // Simple approach: if mouse is within dropdown bounds OR there's an active flyout, keep open 3584 const shouldKeepOpen = isMouseInDropdownBounds || hasActiveFlyout || isMovingWithinDropdown || isMovingToSubmenuItem; 3585 3586 console.log('Is moving within dropdown:', isMovingWithinDropdown); 3587 console.log('Is mouse in dropdown bounds:', isMouseInDropdownBounds); 3588 console.log('Has active flyout:', hasActiveFlyout); 3589 console.log('Should keep open:', shouldKeepOpen); 3590 3591 if (!shouldKeepOpen) { 3592 console.log('SubMenu mouseleave - starting timer (moving outside dropdown)'); 3593 parentMenuItem.leaveTimer = setTimeout(() => { 3594 // Check if there's an active flyout - if so, keep parent open 3595 const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]'); 3596 console.log('Timer fired - active flyout:', activeFlyout); 3597 if (!activeFlyout) { 3598 console.log('Closing parent dropdown - no active flyout'); 3599 parentMenuItem.classList.remove('expanded'); 3600 const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon'); 3601 if (currentIcon) currentIcon.classList.remove('rotated'); 3602 } else { 3603 console.log('Keeping parent dropdown open - flyout is active'); 3604 } 3605 }, 200); 3606 } else { 3607 console.log('SubMenu mouseleave - not starting timer (moving within dropdown)'); 3608 } 3609 } 3610 }); 3611 } else { 3612 menuItem.classList.remove('has-children'); 3613 if (toggleIconContainer.parentNode) { 3614 toggleIconContainer.remove(); 3615 } 3616 } 3617 } 3618 } 3619 3620 // Click listener for items that are direct navigation links (not folders with children) 3621 if (!menuItem.classList.contains('has-children') || isNavigableParent) { 3622 menuItem.addEventListener('click', async (event) => { 3623 if (isNavigableParent && hasVisibleChildren && !titleEl.contains(event.target)) { 3624 return; 3625 } 3626 event.stopPropagation(); 3627 const clickedItemId = galleryItem.id; // Capture the ID of the clicked item 3628 3629 // Check if it's an external URL first 3630 if (galleryItem.isExternal && galleryItem.url) { 3631 window.open(galleryItem.url, '_blank', 'noopener,noreferrer'); 3632 return; 3633 } 3634 3635 if (isPageMenuItem(galleryItem)) { 3636 if (typeof loadPage === 'function') { 3637 loadPage(clickedItemId, event); // loadPage is synchronous 3638 if (typeof updateActiveStatesHorizontal === 'function') { 3639 console.log("Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId); 3640 // Use a minimal delay even for sync operations if page rendering has its own async microtasks 3641 setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50); 3642 } 3643 } 3644 } else { // It's a gallery 3645 if (typeof loadGallery === 'function') { 3646 console.log("Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId); 3647 await loadGallery(clickedItemId, event); // Await the asynchronous loadGallery 3648 console.log("Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay."); 3649 if (typeof updateActiveStatesHorizontal === 'function') { 3650 setTimeout(() => { 3651 console.log("[Delayed Update from Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId); 3652 updateActiveStatesHorizontal(clickedItemId); // Pass the captured clickedItemId 3653 }, 100); // Delay to allow gallery system to settle 3654 } 3655 } 3656 } 3657
3658 // Close any other open dropdowns and flyouts 3659 document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => { 3660 // Don't close the parent if a child within its dropdown was clicked (already handled by submenu click) 3661 if (openItem !== menuItem.closest('.horizontal-menu-item.expanded')) { 3662 openItem.classList.remove('expanded'); 3663 openItem.setAttribute('aria-expanded', 'false'); 3664 const icon = openItem.querySelector('.icon.toggle-icon'); 3665 if (icon) icon.classList.remove('rotated'); 3666 } 3667 }); 3668 3669 // Close all flyouts 3670 document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => { 3671 flyout.style.display = 'none'; 3672 flyout.style.opacity = '0'; 3673 flyout.style.visibility = 'hidden'; 3674 }); 3675 3676 // Remove expanded class from all submenu items 3677 document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((subItem) => { 3678 subItem.classList.remove('expanded'); 3679 }); 3680 }); 3681 } else { 3682 // Keyboard: Enter/Space toggles hover-style dropdown without changing mouse behavior 3683 menuItem.setAttribute('tabindex', '0'); 3684 menuItem.addEventListener('keydown', function(event) { 3685 if (event.key !== 'Enter' && event.key !== ' ') return; 3686 event.preventDefault(); 3687 const isOpen = this.classList.contains('expanded'); 3688 document.querySelectorAll('.horizontal-menu-item.expanded').forEach((openItem) => { 3689 if (openItem !== this) { 3690 openItem.classList.remove('expanded'); 3691 openItem.setAttribute('aria-expanded', 'false'); 3692 const otherIcon = openItem.querySelector('.icon.toggle-icon'); 3693 if (otherIcon) otherIcon.classList.remove('rotated'); 3694 } 3695 }); 3696 this.classList.toggle('expanded', !isOpen); 3697 this.setAttribute('aria-expanded', !isOpen ? 'true' : 'false'); 3698 const currentIcon = this.querySelector('.icon.toggle-icon'); 3699 if (currentIcon) currentIcon.classList.toggle('rotated', !isOpen); 3700 }); 3701 } 3702 return menuItem; 3703} 3704 3705/** 3706* Creates a single horizontal submenu item. 3707* (UPDATED to handle nested children and async loadGallery) 3708* @param {object} galleryItem - The gallery item data for the submenu. 3709* @returns {HTMLElement | null} The created submenu item element, or null if not rendered. 3710*/ 3711function createHorizontalSubMenuItem(galleryItem) { 3712 if (galleryItem.visible === false || galleryItem.isSpacer) { 3713 return null; 3714 } 3715 3716 const subMenuItem = document.createElement('li'); 3717 subMenuItem.className = 'horizontal-submenu-item'; 3718 subMenuItem.dataset.id = String(galleryItem.id); 3719 subMenuItem.setAttribute('data-visible', galleryItem.visible !== false ? 'true' : 'false'); 3720 3721 const subVisibleChildren = (galleryItem.children || []).filter(child => child.visible !== false && !child.isSpacer); 3722 const isNavigableParent = isNavigableMenuParent(galleryItem); 3723 let titleSpan; 3724 if (subVisibleChildren.length > 0 && !isNavigableParent) { 3725 titleSpan = document.createElement('span'); 3726 titleSpan.className = 'horizontal-menu-label'; 3727 titleSpan.textContent = galleryItem.title; 3728 } else { 3729 titleSpan = document.createElement('a'); 3730 titleSpan.className = 'horizontal-menu-label'; 3731 titleSpan.href = typeof getMenuItemHref === 'function' ? getMenuItemHref(galleryItem) : '#'; 3732 titleSpan.textContent = galleryItem.title; 3733 if (galleryItem.isExternal) { 3734 titleSpan.target = '_blank'; 3735 titleSpan.rel = 'noopener noreferrer'; 3736 } else { 3737 titleSpan.addEventListener('click', (e) => { e.preventDefault(); }); 3738 } 3739 } 3740 subMenuItem.appendChild(titleSpan); 3741 3742 const currentGlobalActiveId = parseInt(String(window.activeGalleryId || activeGalleryId)); 3743 if (galleryItem.id === currentGlobalActiveId) { 3744 subMenuItem.classList.add('active'); 3745 if (titleSpan.tagName === 'A') titleSpan.setAttribute('aria-current', 'page'); 3746 } 3747 3748 // Check if this submenu item has children (nested sub-sub items) 3749 if (galleryItem.children && galleryItem.children.length > 0) { 3750 const visibleChildren = subVisibleChildren; 3751 3752 if (visibleChildren.length > 0) { 3753 subMenuItem.classList.add('has-children'); 3754 3755 // Add arrow indicator for nested items (same as top-level menu) 3756 const arrowSpan = document.createElement(isNavigableParent ? 'button' : 'span'); 3757 arrowSpan.className = 'submenu-arrow'; 3758 if (isNavigableParent) { 3759 arrowSpan.type = 'button'; 3760 arrowSpan.setAttribute('aria-label', 'Show submenu for ' + galleryItem.title); 3761 arrowSpan.setAttribute('aria-expanded', 'false'); 3762 arrowSpan.addEventListener('click', function(event) { 3763 event.preventDefault(); 3764 event.stopPropagation(); 3765 const isOpen = subMenuItem.classList.contains('expanded'); 3766 if (isOpen) { 3767 closeHorizontalNestedFlyout(subMenuItem); 3768 } else { 3769 openHorizontalNestedFlyout(subMenuItem); 3770 } 3771 }); 3772 } 3773 const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 3774 svgIcon.setAttribute("class", "icon toggle-icon"); 3775 svgIcon.setAttribute("viewBox", "0 0 24 24"); 3776 svgIcon.setAttribute("fill", "none"); 3777 svgIcon.setAttribute("stroke", "currentColor"); 3778 svgIcon.setAttribute("stroke-width", "2"); 3779 const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline"); 3780 polyline.setAttribute("points", "9 6 15 12 9 18"); // Right-pointing arrow
3781 svgIcon.appendChild(polyline); 3782 arrowSpan.appendChild(svgIcon); 3783 subMenuItem.appendChild(arrowSpan); 3784 3785 // Create nested submenu as a true flyout 3786 const nestedSubMenu = document.createElement('div'); 3787 nestedSubMenu.className = 'horizontal-nested-dropdown'; 3788 3789 // Create a list container for the nested items 3790 const nestedList = document.createElement('ul'); 3791 nestedList.className = 'horizontal-nested-list'; 3792 3793 visibleChildren.forEach((childItem) => { 3794 const nestedSubMenuItem = createHorizontalSubMenuItem(childItem); // Recursive call 3795 if (nestedSubMenuItem) { 3796 nestedList.appendChild(nestedSubMenuItem); 3797 } 3798 }); 3799 3800 nestedSubMenu.appendChild(nestedList); 3801 3802 if (nestedSubMenu.hasChildNodes()) { 3803 // Append to document body for true flyout behavior 3804 document.body.appendChild(nestedSubMenu); 3805 // Store reference to the flyout for cleanup 3806 const flyoutId = 'flyout-' + Date.now() + '-' + Math.random(); 3807 subMenuItem.setAttribute('data-flyout-id', flyoutId); 3808 nestedSubMenu.setAttribute('data-flyout-id', flyoutId); 3809 3810 console.log('Created flyout with ID:', flyoutId, 'for item:', galleryItem.title); // Debug 3811 console.log('Flyout element:', nestedSubMenu); // Debug 3812 3813 // Add hover events for nested submenu 3814 let nestedLeaveTimer; 3815 3816 // Shared open/close used by hover AND keyboard â mouse path unchanged 3817 function openHorizontalNestedFlyout(hostItem) { 3818 if (!hostItem) return; 3819 if (hostItem.nestedLeaveTimer) { 3820 clearTimeout(hostItem.nestedLeaveTimer); 3821 } 3822 const parentMenuItem = hostItem.closest('.horizontal-menu-item'); 3823 if (parentMenuItem && parentMenuItem.leaveTimer) { 3824 clearTimeout(parentMenuItem.leaveTimer); 3825 } 3826 document.querySelectorAll('.horizontal-submenu-item.expanded').forEach((openItem) => { 3827 if (openItem !== hostItem) { 3828 openItem.classList.remove('expanded'); 3829 openItem.setAttribute('aria-expanded', 'false'); 3830 const otherFlyoutId = openItem.getAttribute('data-flyout-id'); 3831 if (otherFlyoutId) { 3832 const otherFlyout = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + otherFlyoutId + '"]'); 3833 if (otherFlyout) { 3834 otherFlyout.style.display = 'none'; 3835 otherFlyout.style.opacity = '0'; 3836 otherFlyout.style.visibility = 'hidden'; 3837 } 3838 } 3839 } 3840 }); 3841 hostItem.classList.add('expanded'); 3842 hostItem.setAttribute('aria-expanded', 'true'); 3843 const hostArrow = hostItem.querySelector('.submenu-arrow'); 3844 if (hostArrow && isNavigableParent) hostArrow.setAttribute('aria-expanded', 'true'); 3845 const hostFlyoutId = hostItem.getAttribute('data-flyout-id'); 3846 if (!hostFlyoutId) return; 3847 const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]'); 3848 if (!nestedDropdown) return; 3849 const rect = hostItem.getBoundingClientRect(); 3850 const parentDropdown = hostItem.closest('.horizontal-dropdown'); 3851 const parentRect = parentDropdown ? parentDropdown.getBoundingClientRect() : rect; 3852 const leftPos = parentRect.right - 1; 3853 const topPos = rect.top; 3854 nestedDropdown.style.left = leftPos + 'px'; 3855 nestedDropdown.style.top = topPos + 'px'; 3856 nestedDropdown.style.display = 'block'; 3857 nestedDropdown.style.opacity = '1'; 3858 nestedDropdown.style.visibility = 'visible'; 3859 nestedDropdown.style.position = 'fixed'; 3860 nestedDropdown.style.zIndex = '1251'; 3861 setTimeout(() => { 3862 const dropdownRect = nestedDropdown.getBoundingClientRect(); 3863 if (dropdownRect.right > window.innerWidth) { 3864 nestedDropdown.style.left = (parentRect.left - dropdownRect.width - 5) + 'px'; 3865 } 3866 }, 10); 3867 } 3868 3869 function closeHorizontalNestedFlyout(hostItem) { 3870 if (!hostItem) return; 3871 hostItem.classList.remove('expanded'); 3872 hostItem.setAttribute('aria-expanded', 'false'); 3873 const hostArrow = hostItem.querySelector('.submenu-arrow'); 3874 if (hostArrow && isNavigableParent) hostArrow.setAttribute('aria-expanded', 'false'); 3875 const hostFlyoutId = hostItem.getAttribute('data-flyout-id'); 3876 if (!hostFlyoutId) return; 3877 const nestedDropdown = document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]'); 3878 if (nestedDropdown) { 3879 nestedDropdown.style.display = 'none'; 3880 nestedDropdown.style.opacity = '0'; 3881 nestedDropdown.style.visibility = 'hidden'; 3882 } 3883 } 3884 3885 // Keyboard opens the same flyout hover shows â attrs on label so mouse click focus stays natural 3886 if (titleSpan && !isNavigableParent) { 3887 titleSpan.setAttribute('tabindex', '0'); 3888 titleSpan.setAttribute('role', 'button'); 3889 titleSpan.setAttribute('aria-haspopup', 'true'); 3890 titleSpan.setAttribute('aria-expanded', 'false'); 3891 titleSpan.addEventListener('keydown', function(event) { 3892 if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowRight') { 3893 event.preventDefault(); 3894 event.stopPropagation(); 3895 openHorizontalNestedFlyout(subMenuItem); 3896 titleSpan.setAttribute('aria-expanded', 'true'); 3897 const hostFlyoutId = subMenuItem.getAttribute('data-flyout-id'); 3898 const nestedDropdown = hostFlyoutId 3899 ? document.querySelector('.horizontal-nested-dropdown[data-flyout-id="' + hostFlyoutId + '"]') 3900 : null; 3901 const firstLink = nestedDropdown && nestedDropdown.querySelector('a.horizontal-menu
3901-label'); 3902 if (firstLink && typeof firstLink.focus === 'function') { 3903 try { firstLink.focus(); } catch (e) {} 3904 } 3905 } else if (event.key === 'Escape' || event.key === 'ArrowLeft') { 3906 event.preventDefault(); 3907 event.stopPropagation(); 3908 closeHorizontalNestedFlyout(subMenuItem); 3909 titleSpan.setAttribute('aria-expanded', 'false'); 3910 } 3911 }); 3912 } 3913 3914 subMenuItem.addEventListener('mouseenter', function() { 3915 openHorizontalNestedFlyout(this); 3916 if (titleSpan) titleSpan.setAttribute('aria-expanded', 'true'); 3917 }); 3918 3919 subMenuItem.addEventListener('mouseleave', function(event) { 3920 // Check if we're moving to the flyout 3921 const flyoutId = this.getAttribute('data-flyout-id'); 3922 const relatedTarget = event.relatedTarget; 3923 const isMovingToFlyout = relatedTarget && flyoutId && 3924 relatedTarget.closest('.horizontal-nested-dropdown[data-flyout-id="' + flyoutId + '"]'); 3925 3926 // Also check if we're moving within the dropdown 3927 const parentMenuItem = this.closest('.horizontal-menu-item'); 3928 const isMovingWithinDropdown = relatedTarget && parentMenuItem && parentMenuItem.contains(relatedTarget); 3929 3930 console.log('Submenu item mouseleave - moving to flyout:', isMovingToFlyout); 3931 console.log('Submenu item mouseleave - moving within dropdown:', isMovingWithinDropdown); 3932 3933 if (!isMovingToFlyout && !isMovingWithinDropdown) { 3934 this.nestedLeaveTimer = setTimeout(() => { 3935 closeHorizontalNestedFlyout(this); 3936 if (titleSpan) titleSpan.setAttribute('aria-expanded', 'false'); 3937 3938 // Now that the flyout is closed, allow the parent dropdown to close 3939 const parentMenuItem = this.closest('.horizontal-menu-item'); 3940 if (parentMenuItem) { 3941 setTimeout(() => { 3942 const activeFlyout = document.querySelector('.horizontal-nested-dropdown[style*="display: block"]'); 3943 if (!activeFlyout) { 3944 parentMenuItem.classList.remove('expanded'); 3945 const currentIcon = parentMenuItem.querySelector('.icon.toggle-icon'); 3946 if (currentIcon) currentIcon.classList.remove('rotated'); 3947 } 3948 }, 50); 3949 } 3950 }, 200); 3951 } else { 3952 console.log('Submenu item mouseleave - not starting timer (moving to flyout)'); 3953 } 3954 }); 3955 3956 nestedSubMenu.addEventListener('mouseenter', function() { 3957 console.log('Flyout mouseenter - clearing timers'); 3958 clearTimeout(nestedLeaveTimer); 3959 3960 // Clear the parent dropdown's leave timer to keep it open 3961 const flyoutId = this.getAttribute('data-flyout-id'); 3962 if (flyoutId) { 3963 const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]'); 3964 if (parentSubItem) { 3965 const parentMenuItem = parentSubItem.closest('.horizontal-menu-item'); 3966 if (parentMenuItem && parentMenuItem.leaveTimer) { 3967 console.log('Clearing parent menu item leave timer'); 3968 clearTimeout(parentMenuItem.leaveTimer); 3969 parentMenuItem.leaveTimer = null; 3970 } else { 3971 console.log('No parent menu item leave timer to clear'); 3972 } 3973 if (parentSubItem.nestedLeaveTimer) { 3974 console.log('Clearing submenu item nested leave timer'); 3975 clearTimeout(parentSubItem.nestedLeaveTimer); 3976 parentSubItem.nestedLeaveTimer = null; 3977 } 3978 } 3979 } 3980 }); 3981 3982 nestedSubMenu.addEventListener('mouseleave', function() { 3983 const flyoutId = this.getAttribute('data-flyout-id'); 3984 if (flyoutId) { 3985 const parentSubItem = document.querySelector('.horizontal-submenu-item[data-flyout-id="' + flyoutId + '"]'); 3986 if (parentSubItem) { 3987 nestedLeaveTimer = setTimeout(() => { 3988 closeHorizontalNestedFlyout(parentSubItem); 3989 const label = parentSubItem.querySelector('.horizontal-menu
3989-label'); 3990 if (label) label.setAttribute('aria-expanded', 'false'); 3991 }, 200); 3992 } 3993 } 3994 }); 3995 } 3996 } 3997 } 3998 3999 // Click listener for navigation (only if no children or if it's a direct link) 4000 if (!subMenuItem.classList.contains('has-children') || isPageMenuItem(galleryItem) || isNavigableParent) { 4001 subMenuItem.addEventListener('click', async (event) => { 4002 if (isNavigableParent && subVisibleChildren.length > 0 && !titleSpan.contains(event.target)) { 4003 return; 4004 } 4005 event.stopPropagation(); 4006 const clickedItemId = galleryItem.id; 4007 4008 // Check if it's an external URL first 4009 if (galleryItem.isExternal && galleryItem.url) { 4010 window.open(galleryItem.url, '_blank', 'noopener,noreferrer'); 4011 return; 4012 } 4013 4014 if (isPageMenuItem(galleryItem)) { 4015 if (typeof loadPage === 'function') { 4016 loadPage(clickedItemId, event); 4017 if (typeof updateActiveStatesHorizontal === 'function') { 4018 console.log("SubMenu Click Handler (Page): Calling updateActiveStatesHorizontal for ID:", clickedItemId); 4019 setTimeout(() => updateActiveStatesHorizontal(clickedItemId), 50); 4020 } 4021 } 4022 } else { // It's a gallery 4023 if (typeof loadGallery === 'function') { 4024 console.log("SubMenu Click Handler (Gallery): Awaiting loadGallery for ID:", clickedItemId); 4025 await loadGallery(clickedItemId, event); 4026 console.log("SubMenu Click Handler (Gallery): loadGallery completed for ID:", clickedItemId, ". Calling updateActiveStatesHorizontal with delay."); 4027 if (typeof updateActiveStatesHorizontal === 'function') { 4028 setTimeout(() => { 4029 console.log("[Delayed Update from SubMenu Click] Calling updateActiveStatesHorizontal for gallery ID:", clickedItemId); 4030 updateActiveStatesHorizontal(clickedItemId); 4031 }, 100); 4032 } 4033 } 4034 } 4035 4036 // Close all parent dropdowns and flyouts after navigation 4037 document.querySelectorAll('.horizontal-menu-item.expanded, .horizontal-submenu-item.expanded').forEach((openItem) => { 4038 openItem.classList.remove('expanded'); 4039 const icon = openItem.querySelector('.icon.toggle-icon'); 4040 if (icon) icon.classList.remove('rotated'); 4041 }); 4042 4043 // Close all flyouts 4044 document.querySelectorAll('.horizontal-nested-dropdown').forEach((flyout) => { 4045 flyout.style.display = 'none'; 4046 flyout.style.opacity = '0'; 4047 flyout.style.visibility = 'hidden'; 4048 }); 4049 }); 4050 } 4051 4052 return subMenuItem; 4053} 4054 4055/** 4056* Updates the 'active' class for items in the horizontal menu. 4057*/ 4058 4059function updateActiveStatesHorizontal(forcedActiveId = null) { 4060 // Prioritize forcedActiveId if provided, otherwise use global state. 4061 // Ensure the ID is treated as a number for comparison. 4062 const targetId = forcedActiveId !== null 4063 ? parseInt(String(forcedActiveId)) 4064 : parseInt(String(window.activeGalleryId || activeGalleryId)); 4065 4066 console.log("[Horizontal Menu Update] Initiated. Target Active ID:", targetId, 4067 (forcedActiveId !== null ? `(Forced: ${forcedActiveId})` : "(Global)"), 4068 "Type:", typeof targetId); 4069 4070 const menuItems = document.querySelectorAll('.horizontal-menu-item, .horizontal-submenu-item'); 4071 4072 if (menuItems.length === 0 && !isNaN(targetId)) { // Check if targetId is a valid number 4073 console.warn("[Horizontal Menu Update] No horizontal menu items found in DOM to update. Target ID was:", targetId); 4074 return; 4075 } 4076 4077 let activeItemSet = false; 4078 menuItems.forEach(item => { 4079 const itemIdStr = item.dataset.id; 4080 if (!itemIdStr) { 4081 // console.warn("[Horizontal Menu Update] Menu item found without a data-id attribute:", item); 4082 return; 4083 } 4084 const itemId = parseInt(itemIdStr); 4085 4086 if (itemId === targetId) { 4087 if (!item.classList.contains('active')) { 4088 item.classList.add('active'); 4089 console.log(`[Horizontal Menu Update] ADDED .active to item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`); 4090 } 4091 activeItemSet = true; 4092 } else { 4093 if (item.classList.contains('active')) { 4094 item.classList.remove('active'); 4095 console.log(`[Horizontal Menu Update] REMOVED .active from item ID ${itemId} ('${item.textContent.trim()}') using target ID ${targetId}`); 4096 } 4097 } 4098 }); 4099 4100 if (!isNaN(targetId) && !activeItemSet) { // Check if targetId is a valid number 4101 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.`); 4102 } 4103 4104 const activeSubItem = document.querySelector('.horizontal-submenu-item.active'); 4105 if (activeSubItem) { 4106 const parentTopItem = activeSubItem.closest('.horizontal-menu
4106-item.has-children'); 4107 if (parentTopItem && !parentTopItem.classList.contains('active')) { 4108 // parentTopItem.classList.add('active-parent'); // Optional class for styling parent 4109 } 4110 } 4111 console.log("[Horizontal Menu Update] Completed for Target ID:", targetId); 4112} 4113 4114 4115 // Listener for closing dropdowns when clicking outside 4116 document.addEventListener('click', function(event) { 4117 if (currentMenuLayout === 'horizontal') { 4118 const openSubmenus = document.querySelectorAll('.horizontal-menu-item.expanded'); 4119 let clickedInsideSubmenuOrParent = false; 4120 4121 openSubmenus.forEach(submenuContainer => { 4122 // Check if click is on the submenu container OR its direct parent menu item 4123 if (submenuContainer.contains(event.target) || 4124 (submenuContainer.parentElement && submenuContainer.parentElement.contains(event.target) && submenuContainer.parentElement.classList.contains('horizontal-menu-item'))) { 4125 clickedInsideSubmenuOrParent = true; 4126 } 4127 }); 4128 4129 // If click is on another top-level item that is NOT expanded, also don't close 4130 const targetMenuItem = event.target.closest('.horizontal-menu-item'); 4131 if (targetMenuItem && !targetMenuItem.classList.contains('expanded') && targetMenuItem.classList.contains('has-children')) { 4132 clickedInsideSubmenuOrParent = true; // Don't close if clicking another parent to open it 4133 } 4134 4135 4136 if (!clickedInsideSubmenuOrParent) { 4137 openSubmenus.forEach(submenuContainer => { 4138 submenuContainer.classList.remove('expanded'); 4139 }); 4140 } 4141 } 4142 }); 4143 4144document.addEventListener('menuLayoutChanged', function(e) { 4145 const newLayout = e.detail.layout; 4146 console.log('EVENT: menuLayoutChanged detected. New layout:', newLayout, 'Current isEditing state:', isEditing); 4147 4148 // Update the global currentMenuLayout variable if you have one 4149 if (typeof currentMenuLayout !== 'undefined') { 4150 currentMenuLayout = newLayout; 4151 } 4152 4153 const sidebar = document.querySelector('.sidebar'); // Ensure sidebar DOM element is selected 4154 4155 // Always clear and re-render the appropriate menu structure for the new layout. 4156 // This is important for style changes (like font size) that affect dimensions. 4157 const galleryTreeContainer = document.getElementById('galleryTree'); 4158 const mobileHeaderContent = document.querySelector('.mobile-header-content'); 4159 4160 if (galleryTreeContainer) { 4161 galleryTreeContainer.innerHTML = ''; // Clear existing tree 4162 } 4163 if (mobileHeaderContent) { 4164 const horizontalMenuContainer = mobileHeaderContent.querySelector('.horizontal-menu-container'); 4165 if (horizontalMenuContainer) { 4166 horizontalMenuContainer.innerHTML = ''; // Clear only items, not the container itself 4167 } 4168 } 4169 4170 // Render the correct menu structure based on the new layout 4171 if (newLayout === 'horizontal') { 4172 if (typeof renderHorizontalMenu === 'function') { 4173 renderHorizontalMenu(); 4174 } 4175 // CSS rules will handle sidebar visibility based on .edit-mode-active and .menu-layout-horizontal 4176 } else { // 'sidebar' or 'top' 4177 if (typeof renderGalleries === 'function') { 4178 renderGalleries(); 4179 } 4180 // CSS rules will handle sidebar visibility 4181 } 4182 4183 // If currently in edit mode, ensure the UI components for editing are correctly displayed 4184 // for the new layout, without fully toggling the mode off and on. 4185 if (isEditing && typeof window.updateGlobalEditState === 'function') { 4186 console.log('menuLayoutChanged: In edit mode. Ensuring edit UI is consistent for new layout.'); 4187 4188 // 1. Forcefully ensure global state and body/sidebar classes are correct for edit mode. 4189 // updateGlobalEditState should handle adding .edit-mode-active to body and .editing to sidebar. 4190 window.updateGlobalEditState(true); 4191 4192 // 2. Re-initialize sortables. It's often safer to destroy existing ones first. 4193 if (typeof initializeNestedSortables === 'function') { 4194 if (typeof destroyNestedSortables === 'function') { 4195 destroyNestedSortables(); 4196 } 4197 setTimeout(initializeNestedSortables, 100); // Delay for DOM updates 4198 } 4199 // Also reinitialize for SidebarManager if it exists and handles its own sortables 4200 if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') { 4201 setTimeout(() => window.SidebarManager._reinitializeNestedSortables(), 150); 4202 } 4203 4204 // 3. Ensure edit controls (the top bar of buttons in the sidebar) are visible
4205 const editControls = document.querySelector('.edit-controls'); 4206 if (editControls) { 4207 editControls.style.display = 'flex'; // Or your default display type for these controls 4208 editControls.classList.add('visible'); 4209 } 4210 4211 // 4. If the layout is now horizontal, the sidebar (for editing) should be visible. 4212 // The CSS rule `body.menu-layout-horizontal.edit-mode-active .sidebar { display: block !important; }` 4213 // should handle this. This log is for confirmation. 4214 if (newLayout === 'horizontal' && sidebar) { 4215 console.log('menuLayoutChanged: Horizontal layout in edit mode, sidebar should be visible via CSS.'); 4216 } 4217 4218 // 5. If PageManager is active and a page is loaded, ensure its edit mode is also set. 4219 if (window.PageManager && typeof window.PageManager.setEditMode === 'function' && 4220 window.PageManager.getCurrentPageId && window.PageManager.getCurrentPageId()) { 4221 window.PageManager.setEditMode(true); 4222 } 4223 4224 } else if (!isEditing) { 4225 // If not in edit mode, CSS should handle the view state. 4226 // This block can be used for any explicit view mode adjustments if CSS isn't sufficient. 4227 console.log('menuLayoutChanged: Not in edit mode. CSS will handle view state.'); 4228 } 4229 4230 // Update mobile title and close mobile menu if open, regardless of edit mode 4231 if (typeof updateMobileTitle === 'function') { 4232 updateMobileTitle(); 4233 } 4234 if (typeof closeMobileMenu === 'function') { 4235 closeMobileMenu(); 4236 } 4237 4238 // Re-render the active page so row-band insets and recipe layout match the new menu shell 4239 if (window.PageManager && typeof window.PageManager.renderElements === 'function' && window.PageManager.currentPageId) { 4240 console.log('menuLayoutChanged: Re-rendering page', window.PageManager.currentPageId); 4241 window.PageManager.renderElements(window.PageManager.currentPageId); 4242 } 4243 if (typeof ensureCorrectLayoutApplied === 'function') { 4244 setTimeout(ensureCorrectLayoutApplied, 0); 4245 } 4246}); 4247 4248 4249// Make sure getFirstSidebarElementForHeader is available if called from other scripts 4250if (typeof window.getFirstSidebarElementForHeader === 'undefined') { 4251 window.getFirstSidebarElementForHeader = getFirstSidebarElementForHeader; 4252} 4253 4254// Make sure getFirstSocialElementForHeader is available if called from other scripts 4255if (typeof window.getFirstSocialElementForHeader === 'undefined') { 4256 window.getFirstSocialElementForHeader = getFirstSocialElementForHeader; 4257} 4258 4259// Make sure renderSocialIcons is available if called from other scripts 4260if (typeof window.renderSocialIcons === 'undefined') { 4261 window.renderSocialIcons = renderSocialIcons; 4262} 4263if (typeof window.renderHorizontalMenu === 'undefined') { 4264 window.renderHorizontalMenu = renderHorizontalMenu; 4265} 4266if (typeof window.createHorizontalMenuItem === 'undefined') { 4267 window.createHorizontalMenuItem = createHorizontalMenuItem; 4268} 4269if (typeof window.createHorizontalSubMenuItem === 'undefined') { 4270 window.createHorizontalSubMenuItem = createHorizontalSubMenuItem; 4271} 4272if (typeof window.updateActiveStatesHorizontal === 'undefined') { 4273 window.updateActiveStatesHorizontal = updateActiveStatesHorizontal; 4274} 4275// Ensure toggleEditMode is globally available if it's the primary one. 4276if (typeof window.toggleEditMode === 'undefined' || window.toggleEditMode.toString().length < 100) { // Heuristic to check if it's a placeholder 4277 window.toggleEditMode = toggleEditMode; 4278} 4279 4280 4281// Helper function to toggle edit controls visibility 4282function toggleEditControlsVisibility(show) { 4283 const editControls = document.querySelector('.edit-controls'); 4284 if (editControls) { 4285 if (show) { 4286 editControls.classList.add('visible'); 4287 editControls.style.display = 'flex'; 4288 } else { 4289 editControls.classList.remove('visible'); 4290 editControls.style.display = 'none'; 4291 } 4292 } 4293} 4294 4295/** Reliable + shortcut across US/Intl layouts, numpad, and browsers (key/keyCode differs). */ 4296function isHydraPlusKey(e) { 4297 if (e.repeat) return false;
4298 if (e.ctrlKey || e.metaKey || e.altKey) return false; 4299 if (e.key === '+' || e.key === 'Add') return true; 4300 if (e.code === 'NumpadAdd') return true; 4301 if ((e.code === 'Equal' || e.key === '=') && e.shiftKey) return true; 4302 if (e.keyCode === 187 && e.shiftKey) return true; 4303 if (e.keyCode === 107) return true; 4304 return false; 4305} 4306 4307function handleHydraPlusShortcut() { 4308 try { 4309 const token = window.didToken || localStorage.getItem('hydra_auth_token'); 4310 const isAdmin = window.isAdmin || localStorage.getItem('hydra_is_admin') === 'true'; 4311 if (token && isAdmin) { 4312 console.log('User already logged in - toggling edit mode'); 4313 if (typeof window.toggleEditMode === 'function') { 4314 window.toggleEditMode(); 4315 } else if (typeof toggleEditMode === 'function') { 4316 toggleEditMode(); 4317 } else { 4318 console.warn('toggleEditMode function not found'); 4319 } 4320 } else { 4321 const p = startOTPLogin(); 4322 if (p && typeof p.then === 'function') { 4323 p.catch(function(err) { 4324 console.error('startOTPLogin failed:', err); 4325 }); 4326 } 4327 } 4328 } catch (err) { 4329 console.error('Hydra + key handler error:', err); 4330 } 4331} 4332 4333document.addEventListener('keydown', function(e) { 4334 if (!isHydraPlusKey(e)) return; 4335 e.preventDefault(); 4336 handleHydraPlusShortcut(); 4337}, true); 4338 4339// Custom HTML pages load in a sandboxed iframe; + is captured there unless forwarded via postMessage. 4340function isHydraCustomPageHtmlPlusMessage(e) { 4341 if (!e.data || e.data.source !== 'hydra-custom-page-html' || e.data.type !== 'plus-key') return false; 4342 if (e.origin === window.location.origin) return true; 4343 // Sandboxed iframes (no allow-same-origin) post with origin "null" â accept only from our iframe. 4344 if (e.origin !== 'null' || !e.source) return false; 4345 const iframes = document.querySelectorAll('iframe.page-custom-html-iframe'); 4346 for (let i = 0; i < iframes.length; i++) { 4347 if (iframes[i].contentWindow === e.source) return true; 4348 } 4349 return false; 4350} 4351 4352window.addEventListener('message', function(e) { 4353 if (!isHydraCustomPageHtmlPlusMessage(e)) return; 4354 handleHydraPlusShortcut(); 4355}); 4356 4357/** Option+/ (÷) opens Help â Pathways. Option+? opens Help â Ask (help admin only). */ 4358function isHydraHelpKey(e) { 4359 if (e.repeat) return false; 4360 if (e.ctrlKey || e.metaKey) return false; 4361 if (!e.altKey) return false; 4362 if (e.shiftKey) return false; 4363 if (e.key === '÷') return true; 4364 if (e.code === 'Slash' && e.altKey) return true; 4365 return false; 4366} 4367 4368function isHydraHelpAskKey(e) { 4369 if (e.repeat) return false; 4370 if (e.ctrlKey || e.metaKey) return false; 4371 if (!e.altKey || !e.shiftKey) return false; 4372 if (e.key === '?' || e.key === '¿') return true; 4373 if (e.code === 'Slash') return true; 4374 return false; 4375} 4376 4377document.addEventListener('keydown', function(e) { 4378 if (!window.HYDRA_HELP_ENABLED) return; 4379 var t = e.target; 4380 if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return; 4381 if (isHydraHelpAskKey(e)) { 4382 e.preventDefault(); 4383 e.stopPropagation(); 4384 openHydraHelp({ initialTab: 'ask' }); 4385 return; 4386 } 4387 if (!isHydraHelpKey(e)) return; 4388 e.preventDefault(); 4389 e.stopPropagation(); 4390 openHydraHelp(); 4391}, true); 4392 4393// Email OTP Authentication with Enhanced Debugging 4394 4395 4396// Add this function to the client-side code 4397// Improved client-side backup function with better token handling 4398async function createLoginBackup() { 4399 try { 4400 console.log('Attempting to create login backup...'); 4401 4402 // Get authentication info from multiple possible sources 4403 let token = window.didToken || localStorage.getItem('hydra_auth_token'); 4404 const email = localStorage.getItem('hydra_auth_email') || 4405 (window.userMetadata ? window.userMetadata.email : '') || 4406 ''; 4407 4408 // Check if we have authentication 4409 if (!token) { 4410 console.error('No auth token available for backup'); 4411 return; 4412 } 4413 4414 if (!email) { 4415 console.error('No email available for backup - make sure it was stored during login'); 4416 return; 4417 } 4418 4419 console.log(`Creating backup with token (${token.length} chars) for ${email}`); 4420 4421 // Format the token correctly - VERY IMPORTANT 4422 // If it's already a hydra token (starts with hydra:), use it as is 4423 // Otherwise, add the hydra: prefix 4424 const formattedToken = token.startsWith('hydra:') ? token : `hydra:${token}`; 4425 4426 console.log(`Using formatted token: ${formattedToken.substring(0, 15)}...`); 4427 4428 // Get the API URL with preview handling if needed 4429 const backupUrl = typeof getApiUrl === 'function' ? 4430 getApiUrl('/api/create-backup') : 4431 '/api/create-backup'; 4432 4433 console.log(`Sending backup request to: ${backupUrl}`); 4434 4435 // IMPORTANT: Make sure to add the Bearer prefix to the Authorization header 4436 const response = await fetch(backupUrl, { 4437 method: 'POST', 4438 headers: { 4439 'Authorization': `Bearer ${formattedToken}`, 4440 'Content-Type': 'application/json', 4441 'X-User-Email': email, // Include email as fallback 4442 'X-API-Request': 'true' // Mark as API request 4443 } 4444 }); 4445 4446 // Handle response 4447 if (!response.ok) { 4448 const errorText = await response.text(); 4449 let errorData;
4450 try { 4451 // Try to parse as JSON 4452 errorData = JSON.parse(errorText); 4453 console.error('Backup failed:', errorData.error, errorData); 4454 } catch { 4455 // Not JSON, log raw text 4456 console.error('Backup failed with status', response.status, errorText); 4457 } 4458 return; 4459 } 4460 4461 // Parse successful response 4462 const result = await response.json(); 4463 console.log(`â Backup created successfully: ${result.backup}`); 4464 } catch (error) { 4465 console.error('Error in backup process:', error); 4466 } 4467} 4468 4469// Initialize Magic with detailed logging 4470function initMagic() { 4471 try { 4472 // If Magic is already initialized, return it 4473 if (window.magic) { 4474 return window.magic; 4475 } 4476 4477 console.log('Initializing Magic SDK...'); 4478 4479 // Initialize with minimal options 4480 const magic = new Magic('pk_live_E0B68BA6DCA8C75F', { 4481 testMode: false 4482 }); 4483 4484 // Store reference globally 4485 window.magic = magic; 4486 4487 console.log('Magic SDK initialized successfully'); 4488 return magic; 4489 } catch (error) { 4490 console.error('Error initializing Magic:', error); 4491 throw error; 4492 } 4493} 4494 4495// Start OTP login flow with proper event handling 4496async function startOTPLogin() { 4497 try { 4498 // Initialize Magic 4499 const magic = initMagic(); 4500 4501 // Get email from user (styled overlay â same auth path as native prompt) 4502 const email = await showEmailLoginOverlay(); 4503 if (!email) return; // User cancelled 4504 4505 console.log('Starting Email OTP login for:', email); 4506 showLoadingOverlay('Sending verification code...'); 4507 4508 // Check if Magic's event enum types are available - for newer Magic SDK versions 4509 // If not available, fall back to string-based events 4510 const useEnums = typeof LoginWithEmailOTPEventOnReceived !== 'undefined' && 4511 typeof LoginWithEmailOTPEventEmit !== 'undefined'; 4512 4513 console.log('Using enum-based events:', useEnums); 4514 4515 // Create a timeout handler 4516 let emailSentTimeout = setTimeout(() => { 4517 console.log('No email-otp-sent event received, using fallback UI'); 4518 hideLoadingOverlay(); 4519 showOTPInputOverlay(email); 4520 }, 10000); 4521 4522 // Create OTP login handle with showUI: false 4523 const handle = magic.auth.loginWithEmailOTP({ 4524 email, 4525 showUI: false, 4526 deviceCheckUI: false 4527 }); 4528 4529 // Track authentication state 4530 window.pendingAuth = { 4531 email, 4532 handle 4533 }; 4534 4535 // Listen for events - using both enum-based and string-based event handling 4536 console.log('Setting up OTP event handlers...'); 4537 4538 // Email OTP sent event 4539 if (useEnums) { 4540 handle.on(LoginWithEmailOTPEventOnReceived.EmailOTPSent, () => { 4541 console.log('EVENT: EmailOTPSent (enum) - Email was sent successfully'); 4542 clearTimeout(emailSentTimeout); 4543 hideLoadingOverlay(); 4544 showOTPInputOverlay(email); 4545 }); 4546 } 4547 4548 // Also listen for string-based event (for backward compatibility) 4549 handle.on('email-otp-sent', () => { 4550 console.log('EVENT: email-otp-sent - Email was sent successfully'); 4551 clearTimeout(emailSentTimeout); 4552 hideLoadingOverlay(); 4553 showOTPInputOverlay(email); 4554 }); 4555 4556 // Invalid OTP event 4557 if (useEnums) { 4558 handle.on(LoginWithEmailOTPEventOnReceived.InvalidEmailOtp, () => { 4559 console.log('EVENT: InvalidEmailOtp (enum) - Invalid code entered'); 4560 showErrorMessage('Invalid verification code. Please try again.'); 4561 updateOTPInputForRetry(); 4562 }); 4563 } 4564 4565 // Also listen for string-based event 4566 handle.on('invalid-email-otp', () => { 4567 console.log('EVENT: invalid-email-otp - Invalid code entered'); 4568 showErrorMessage('Invalid verification code. Please try again.'); 4569 updateOTPInputForRetry(); 4570 }); 4571 4572 async function handleLoginSuccess(result) { 4573 console.log('OTP verification successful, checking admin status...'); 4574 hideOTPInputOverlay(); 4575 hideLoadingOverlay(); 4576 4577 try { 4578 window.didToken = result; 4579 console.log('DID Token received:', result ? (result.substring(0, 20) + '...') : 'none'); 4580 4581 const userInfo = await magic.user.getInfo(); 4582 window.userMetadata = userInfo; 4583 console.log('User info retrieved:', userInfo); 4584 4585 const isUserAdmin = await checkAdminStatus(result); 4586 4587 if (!isUserAdmin && !window.isAdmin) { 4588 console.log('User is not on admin list, login denied'); 4589 showErrorMessage('Login failed'); 4590 clearHydraClientAuthStorage(); 4591 resetAuthUI();
4592 try { 4593 await magic.user.logout(); 4594 } catch (logoutError) { 4595 console.error('Error logging out non-admin user from Magic:', logoutError); 4596 } 4597 window.pendingAuth = null; 4598 return; 4599 } 4600 4601 console.log('Admin status check completed'); 4602 window.isAuthenticated = true; 4603 isAuthenticated = true; 4604 showSuccessMessage('Login successful!'); 4605 4606 // Store the ID token (JWT) for API calls 4607 try { 4608 const idToken = await magic.user.getIdToken(); 4609 if (idToken && idToken.includes('.')) { 4610 console.log('Got proper JWT token from Magic.link'); 4611 localStorage.setItem('hydra_auth_token', idToken); 4612 } else if (userInfo && userInfo.email) { 4613 const userToken = { 4614 email: userInfo.email, 4615 iat: Math.floor(Date.now() / 1000), 4616 exp: Math.floor(Date.now() / 1000) + (24 * 60 * 60) 4617 }; 4618 const header = btoa(JSON.stringify({ typ: 'JWT', alg: 'none' })); 4619 const payload = btoa(JSON.stringify(userToken)); 4620 const jwtToken = header + '.' + payload + '.nosignature'; 4621 localStorage.setItem('hydra_auth_token', jwtToken); 4622 localStorage.setItem('hydra_auth_email', userInfo.email); 4623 } 4624 localStorage.setItem('hydra_did_token', result); 4625 } catch (tokenError) { 4626 console.error('Error getting ID token:', tokenError); 4627 localStorage.setItem('hydra_auth_token', result); 4628 } 4629 4630 // IMPROVED UI UPDATE APPROACH - Use multiple techniques for redundancy 4631 4632 // 1. Add state classes to body 4633 document.body.classList.add('hydra-authenticated'); 4634 if (window.isAdmin) { 4635 document.body.classList.add('hydra-admin'); 4636 } 4637 4638 // 2. Direct DOM manipulation 4639 const logoutButton = document.getElementById('logoutButton'); 4640 const editButton = document.getElementById('editButton'); 4641 4642 // Create new buttons if they don't exist 4643 if (!logoutButton) { 4644 console.log('Creating logout button'); 4645 const newLogoutButton = document.createElement('button'); 4646 newLogoutButton.id = 'logoutButton'; 4647 newLogoutButton.className = 'btn'; 4648 newLogoutButton.textContent = 'Logout'; 4649 newLogoutButton.onclick = logout; 4650 newLogoutButton.style.display = 'block'; 4651 4652 const sidebarHeader = document.querySelector('.sidebar-header'); 4653 if (sidebarHeader) { 4654 sidebarHeader.appendChild(newLogoutButton); 4655 } else { 4656 document.body.appendChild(newLogoutButton); 4657 } 4658 } else { 4659 logoutButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;'; 4660 } 4661 4662 if (!editButton && window.isAdmin) { 4663 console.log('Creating edit button'); 4664 const newEditButton = document.createElement('button'); 4665 newEditButton.id = 'editButton'; 4666 newEditButton.className = 'btn btn-primary'; 4667 newEditButton.innerHTML = ` 4668 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 4669 <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> 4670 <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path> 4671 </svg> 4672 Edit 4673 `; 4674 newEditButton.onclick = toggleEditMode; 4675 newEditButton.style.display = 'block'; 4676 4677 const sidebarHeader = document.querySelector('.sidebar-header'); 4678 if (sidebarHeader) { 4679 sidebarHeader.appendChild(newEditButton); 4680 } else { 4681 document.body.appendChild(newEditButton); 4682 } 4683 } else if (editButton && window.isAdmin) { 4684 editButton.style.cssText = 'display: block !important; visibility: visible !important; opacity: 1 !important;'; 4685 } 4686 4687 // 3. Dispatch event for other listeners 4688 document.dispatchEvent(new CustomEvent('login-complete', { 4689 detail: { 4690 isAdmin: window.isAdmin, 4691 email: window.userMetadata ? window.userMetadata.email : null 4692 } 4693 })); 4694 4695 window.pendingAuth = null; 4696 4697 if (typeof checkAuth === 'function') { 4698 console.log('Running existing checkAuth function'); 4699 setTimeout(checkAuth, 500); 4700 } 4701 } catch (error) { 4702 console.error('Error in login success handling:', error); 4703 showErrorMessage('Login failed'); 4704 clearHydraClientAuthStorage(); 4705 resetAuthUI(); 4706 window.pendingAuth = null; 4707 } 4708} 4709 4710 4711 // Completion event 4712 handle.on('done', async (result) => { 4713 await handleLoginSuccess(result); 4714 }); 4715 4716 // Error event 4717 handle.on('error', (error) => { 4718 console.error('EVENT: error - OTP login error:', error); 4719 hideLoadingOverlay(); 4720 hideOTPInputOverlay(); 4721 showErrorMessage('Login failed: ' + (error.message || 'Unknown error')); 4722 4723 // Clear timeout if still active 4724 clearTimeout(emailSentTimeout); 4725 4726 // Clean up 4727 window.pendingAuth = null; 4728 }); 4729 4730 console.log('OTP event handlers set up successfully'); 4731 4732 } catch (error) { 4733 console.error('Error starting OTP login:', error); 4734 hideLoadingOverlay(); 4735 hideOTPInputOverlay(); 4736 showErrorMessage('Error starting login: ' + error.message); 4737 } 4738} 4739
4740// Submit OTP code with support for enum-based events 4741function submitOTP(code) { 4742 try { 4743 // Validate code input 4744 if (!code || code.trim() === '') { 4745 showErrorMessage('Please enter the verification code.'); 4746 return; 4747 } 4748 4749 // Check if we have a pending authentication 4750 if (!window.pendingAuth || !window.pendingAuth.handle) { 4751 showErrorMessage('No active login session. Please try again.'); 4752 hideOTPInputOverlay(); 4753 return; 4754 } 4755 4756 console.log('Submitting OTP code...'); 4757 showLoadingOverlay('Verifying code...'); 4758 4759 // Check if enum types are available 4760 const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined'; 4761 4762 // Verify the OTP code 4763 if (useEnums) { 4764 window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.VerifyEmailOtp, code); 4765 } else { 4766 window.pendingAuth.handle.emit('verify-email-otp', code); 4767 } 4768 4769 } catch (error) { 4770 console.error('Error submitting OTP:', error); 4771 hideLoadingOverlay(); 4772 showErrorMessage('Error verifying code: ' + error.message); 4773 } 4774} 4775 4776// Cancel OTP login with support for enum-based events 4777function cancelOTPLogin() { 4778 try { 4779 // Check if we have a pending authentication 4780 if (window.pendingAuth && window.pendingAuth.handle) { 4781 // Check if enum types are available 4782 const useEnums = typeof LoginWithEmailOTPEventEmit !== 'undefined'; 4783 4784 // Emit cancel event 4785 if (useEnums) { 4786 window.pendingAuth.handle.emit(LoginWithEmailOTPEventEmit.Cancel); 4787 } else { 4788 window.pendingAuth.handle.emit('cancel'); 4789 } 4790 4791 console.log('OTP login cancelled'); 4792 } 4793 4794 // Clean up 4795 window.pendingAuth = null; 4796 4797 // Hide any overlays 4798 hideEmailLoginOverlay(); 4799 hideOTPInputOverlay(); 4800 hideLoadingOverlay(); 4801 } catch (error) { 4802 console.error('Error cancelling OTP login:', error); 4803 } 4804} 4805 4806// Update OTP input for retry 4807function updateOTPInputForRetry() {
4808 const otpInput = document.getElementById('otp-input'); 4809 if (otpInput) { 4810 otpInput.value = ''; 4811 otpInput.focus(); 4812 4813 // Add a shake animation for visual feedback 4814 otpInput.classList.add('shake'); 4815 4816 // Remove the animation class after it completes 4817 setTimeout(() => { 4818 otpInput.classList.remove('shake'); 4819 }, 500); 4820 } 4821 4822 // Hide loading overlay if it's visible 4823 hideLoadingOverlay(); 4824} 4825 4826function isValidLoginEmail(value) { 4827 const trimmed = String(value || '').trim(); 4828 if (!trimmed || trimmed.indexOf(' ') >= 0) return false; 4829 const at = trimmed.indexOf('@'); 4830 if (at <= 0 || at !== trimmed.lastIndexOf('@')) return false; 4831 const local = trimmed.slice(0, at); 4832 const domain = trimmed.slice(at + 1); 4833 if (!local || !domain || domain.indexOf('.') < 1) return false; 4834 if (domain.startsWith('.') || domain.endsWith('.')) return false; 4835 return true; 4836} 4837 4838function showEmailLoginOverlay() { 4839 hideEmailLoginOverlay(); 4840 4841 return new Promise((resolve) => { 4842 const overlay = document.createElement('div'); 4843 overlay.id = 'email-login-overlay'; 4844 overlay.className = 'pe-auth-overlay'; 4845 4846 const content = document.createElement('div'); 4847 content.className = 'pe-auth-shell'; 4848 4849 const logo = document.createElement('img'); 4850 logo.src = 'https://cdn.neonsky.app/neon-sky-logo.png'; 4851 logo.alt = 'Neon Sky Logo'; 4852 logo.className = 'pe-auth-shell__logo'; 4853 4854 const description = document.createElement('p'); 4855 description.className = 'pe-auth-shell__desc'; 4856 description.textContent = 'Enter your email address to receive a verification code.'; 4857 4858 const form = document.createElement('form'); 4859 form.noValidate = true; 4860 form.onsubmit = function(e) { 4861 e.preventDefault(); 4862 const raw = input.value.trim(); 4863 if (!isValidLoginEmail(raw)) { 4864 input.classList.add('shake'); 4865 setTimeout(() => input.classList.remove('shake'), 500); 4866 return; 4867 } 4868 hideEmailLoginOverlay(); 4869 resolve(raw); 4870 }; 4871 4872 const input = document.createElement('input'); 4873 input.id = 'email-login-input'; 4874 input.type = 'email'; 4875 input.name = 'email'; 4876 input.placeholder = '[email protected]'; 4877 input.autocomplete = 'email'; 4878 input.inputMode = 'email'; 4879 input.required = true; 4880 input.className = 'pe-auth-shell__input pe-auth-shell__input--email'; 4881 const savedEmail = localStorage.getItem('hydra_auth_email'); 4882 if (savedEmail && isValidLoginEmail(savedEmail)) { 4883 input.value = savedEmail.trim(); 4884 } 4885 form.appendChild(input); 4886 4887 const buttonContainer = document.createElement('div'); 4888 buttonContainer.className = 'pe-auth-actions'; 4889 4890 const continueButton = document.createElement('button'); 4891 continueButton.type = 'submit'; 4892 continueButton.textContent = 'Continue'; 4893 continueButton.className = 'pe-btn pe-btn--primary'; 4894 buttonContainer.appendChild(continueButton); 4895 4896 const cancelButton = document.createElement('button'); 4897 cancelButton.type = 'button'; 4898 cancelButton.textContent = 'Cancel'; 4899 cancelButton.className = 'pe-btn'; 4900 cancelButton.onclick = function() { 4901 hideEmailLoginOverlay(); 4902 resolve(null); 4903 }; 4904 buttonContainer.appendChild(cancelButton); 4905 form.appendChild(buttonContainer); 4906 4907 content.appendChild(logo); 4908 content.appendChild(description); 4909 content.appendChild(form); 4910 overlay.appendChild(content); 4911 document.body.appendChild(overlay); 4912 4913 setTimeout(() => { 4914 input.focus(); 4915 if (input.value) { 4916 input.select(); 4917 } 4918 }, 100); 4919 }); 4920} 4921 4922function hideEmailLoginOverlay() { 4923 const overlay = document.getElementById('email-login-overlay'); 4924 if (overlay && overlay.parentNode) { 4925 overlay.parentNode.removeChild(overlay); 4926 } 4927} 4928 4929// Show OTP input overlay - improved version 4930function showOTPInputOverlay(email) { 4931 hideOTPInputOverlay(); 4932 hideEmailLoginOverlay(); 4933 4934 const overlay = document.createElement('div'); 4935 overlay.id = 'otp-input-overlay'; 4936 overlay.className = 'pe-auth-overlay'; 4937 4938 const content = document.createElement('div'); 4939 content.className = 'pe-auth-shell'; 4940 4941 const logo = document.createElement('img'); 4942 logo.src = 'https://cdn.neonsky.app/neon-sky-logo.png'; 4943 logo.alt = 'Neon Sky Logo'; 4944 logo.className = 'pe-auth-shell__logo'; 4945 4946 const description = document.createElement('p'); 4947 description.className = 'pe-auth-shell__desc'; 4948 const descPrefix = document.createTextNode("We've sent a verification code to "); 4949 const descEmail = document.createElement('strong'); 4950 descEmail.textContent = String(email || ''); 4951 const descSuffix = document.createTextNode('. Please check your email and enter the code below:'); 4952 description.appendChild(descPrefix); 4953 description.appendChild(descEmail); 4954 description.appendChild(document.createElement('br')); 4955 description.appendChild(descSuffix); 4956 4957 const form = document.createElement('form'); 4958 form.onsubmit = function(e) { 4959 e.preventDefault();
4960 const otpInput = document.getElementById('otp-input'); 4961 if (otpInput && otpInput.value) { 4962 submitOTP(otpInput.value); 4963 } 4964 }; 4965 4966 const input = document.createElement('input'); 4967 input.id = 'otp-input'; 4968 input.type = 'text'; 4969 input.placeholder = 'Enter code'; 4970 input.pattern = '[0-9]*'; 4971 input.inputMode = 'numeric'; 4972 input.autocomplete = 'one-time-code'; 4973 input.className = 'pe-auth-shell__input'; 4974 form.appendChild(input); 4975 4976 const buttonContainer = document.createElement('div'); 4977 buttonContainer.className = 'pe-auth-actions'; 4978 4979 const verifyButton = document.createElement('button'); 4980 verifyButton.type = 'submit'; 4981 verifyButton.textContent = 'Verify'; 4982 verifyButton.className = 'pe-btn pe-btn--primary'; 4983 buttonContainer.appendChild(verifyButton); 4984 4985 const cancelButton = document.createElement('button'); 4986 cancelButton.type = 'button'; 4987 cancelButton.textContent = 'Cancel'; 4988 cancelButton.className = 'pe-btn'; 4989 cancelButton.onclick = function() { 4990 cancelOTPLogin(); 4991 }; 4992 buttonContainer.appendChild(cancelButton); 4993 form.appendChild(buttonContainer); 4994 4995 const resendContainer = document.createElement('div'); 4996 resendContainer.className = 'pe-auth-resend'; 4997 4998 const resendText = document.createElement('span'); 4999 resendText.textContent = "Didn't receive the code? "; 5000 5001 const resendLink = document.createElement('a'); 5002 resendLink.textContent = 'Resend'; 5003 resendLink.href = '#'; 5004 resendLink.onclick = function(e) { 5005 e.preventDefault(); 5006 cancelOTPLogin(); 5007 setTimeout(() => { 5008 startOTPLogin(); 5009 }, 500); 5010 }; 5011 5012 resendContainer.appendChild(resendText); 5013 resendContainer.appendChild(resendLink); 5014 form.appendChild(resendContainer); 5015 5016 content.appendChild(logo); 5017 content.appendChild(description); 5018 content.appendChild(form); 5019 overlay.appendChild(content); 5020 document.body.appendChild(overlay); 5021 5022 setTimeout(() => { 5023 input.focus(); 5024 }, 100); 5025} 5026 5027// Hide OTP input overlay 5028function hideOTPInputOverlay() {
5029 const overlay = document.getElementById('otp-input-overlay'); 5030 if (overlay && overlay.parentNode) { 5031 overlay.parentNode.removeChild(overlay); 5032 } 5033} 5034 5035// Utility function to show loading overlay 5036function showLoadingOverlay(message = 'Loading...') { 5037 hideLoadingOverlay(); 5038 5039 const overlay = document.createElement('div'); 5040 overlay.id = 'loading-overlay'; 5041 overlay.className = 'pe-auth-overlay'; 5042 5043 const content = document.createElement('div'); 5044 content.className = 'pe-auth-shell'; 5045 5046 const loaderContainer = document.createElement('div'); 5047 loaderContainer.className = 'pe-auth-shell__loader'; 5048 loaderContainer.innerHTML = ` 5049 <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24"> 5050 <defs> 5051 <filter id="svgSpinnersGooeyBalls20"> 5052 <feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/> 5053 <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"/> 5054 <feBlend in="SourceGraphic" in2="z"/> 5055 </filter> 5056 </defs> 5057 <g filter="url(#svgSpinnersGooeyBalls20)"> 5058 <circle cx="5" cy="12" r="4" fill="currentColor"> 5059 <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/> 5060 </circle> 5061 <circle cx="19" cy="12" r="4" fill="currentColor"> 5062 <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/> 5063 </circle> 5064 <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/> 5065 </g> 5066 </svg> 5067 `; 5068 content.appendChild(loaderContainer); 5069 5070 const messageEl = document.createElement('p'); 5071 messageEl.className = 'pe-auth-shell__message'; 5072 messageEl.textContent = message; 5073 content.appendChild(messageEl); 5074 5075 overlay.appendChild(content); 5076 document.body.appendChild(overlay); 5077} 5078 5079// Utility function to hide loading overlay 5080function hideLoadingOverlay() { 5081 const overlay = document.getElementById('loading-overlay'); 5082 if (overlay && overlay.parentNode) { 5083 overlay.parentNode.removeChild(overlay); 5084 } 5085} 5086 5087// Utility function to show success message 5088function showSuccessMessage(message, duration = 3000) { 5089 const existingToast = document.getElementById('success-toast'); 5090 if (existingToast && existingToast.parentNode) { 5091 existingToast.parentNode.removeChild(existingToast); 5092 } 5093 5094 const toast = document.createElement('div'); 5095 toast.id = 'success-toast'; 5096 toast.className = 'pe-auth-toast'; 5097 toast.textContent = message; 5098 5099 document.body.appendChild(toast); 5100 5101 setTimeout(() => { 5102 if (toast.parentNode) { 5103 toast.parentNode.removeChild(toast); 5104 } 5105 }, duration); 5106} 5107 5108// Utility function to show error message 5109function showErrorMessage(message, duration = 5000) { 5110 const existingToast = document.getElementById('error-toast'); 5111 if (existingToast && existingToast.parentNode) { 5112 existingToast.parentNode.removeChild(existingToast); 5113 } 5114 5115 const toast = document.createElement('div'); 5116 toast.id = 'error-toast'; 5117 toast.className = 'pe-auth-toast pe-auth-toast--error'; 5118 toast.textContent = message; 5119 5120 document.body.appendChild(toast); 5121 5122 setTimeout(() => { 5123 if (toast.parentNode) { 5124 toast.parentNode.removeChild(toast); 5125 } 5126 }, duration); 5127} 5128 5129// UPDATED: Check authentication status with class-based approach 5130async function checkAuth() { 5131 try { 5132 if (!magic) { 5133 console.log('Magic not initialized, skipping auth check'); 5134 return; 5135 } 5136 5137 // Prevent multiple simultaneous auth checks 5138 if (window._checkingAuth) return; 5139 window._checkingAuth = true; 5140 5141 console.log('Checking auth status...'); 5142 5143 isAuthenticated = await magic.user.isLoggedIn(); 5144 console.log('isLoggedIn check:', isAuthenticated); 5145 5146 if (isAuthenticated) { 5147 // Add the authenticated class 5148 document.body.classList.add('hydra-authenticated'); 5149 5150 // Get user metadata 5151 userMetadata = await magic.user.getInfo(); 5152 console.log('User info:', userMetadata); 5153 5154 // Get ID token for API calls (JWT format) 5155 const rawDidToken = await magic.user.getIdToken(); 5156 console.log('ID token acquired, length:', rawDidToken.length); 5157
5158 // Do NOT write the raw DID token to localStorage here â checkAdminStatus will 5159 // receive the server-issued hydra token and store that instead, which is what 5160 // the save function needs. Writing the DID token now would overwrite a valid 5161 // hydra token that may already be stored from the OTP login flow. 5162 5163 // Check if user is admin; on success this updates both localStorage and didToken 5164 // with a proper hydra token via checkAdminStatus â TokenManager.storeToken 5165 await checkAdminStatus(rawDidToken); 5166 } else { 5167 // Magic has no session â still try Hydra OTP token so /_manage and return visits work 5168 const hydraOnly = localStorage.getItem('hydra_auth_token'); 5169 if (hydraOnly) { 5170 try { 5171 await checkAdminStatus(hydraOnly); 5172 } catch (e) { 5173 console.error('Hydra token restore after Magic logout failed:', e); 5174 } 5175 } else { 5176 resetAuthUI(); 5177 } 5178 } 5179 5180 // Release lock 5181 window._checkingAuth = false; 5182 } catch (error) { 5183 console.error('Error checking authentication:', error); 5184 window._checkingAuth = false; 5185 resetAuthUI(); 5186 } 5187} 5188 5189 5190async function checkAdminStatus(token) { 5191 try { 5192 console.log('Checking admin status...'); 5193 5194 // Add user email to headers if available 5195 let userEmail = null; 5196 if (window.userMetadata && window.userMetadata.email) { 5197 userEmail = window.userMetadata.email; 5198 console.log('Using email from userMetadata:', userEmail); 5199 } else { 5200 userEmail = localStorage.getItem('hydra_auth_email'); // Fallback to localStorage 5201 if (userEmail) console.log('Using email from localStorage:', userEmail); 5202 } 5203 5204 if (!token) { 5205 console.log('No token provided to checkAdminStatus'); 5206 return false; // Indicate failure 5207 } 5208 5209 // Format the token correctly for the API call 5210 let formattedToken = token; 5211 // Ensure Bearer prefix is added for Magic tokens, or use hydra: prefix 5212 if (!token.startsWith('hydra:') && !token.startsWith('Bearer ')) { 5213 formattedToken = `Bearer ${token}`; // Assume Magic/JWT needs Bearer 5214 } else if (token.startsWith('hydra:')) { 5215 formattedToken = `Bearer ${token}`; // API expects Bearer even for hydra tokens 5216 } 5217 // If it already starts with Bearer, use as is 5218 5219 // Get the API URL, handling preview mode 5220 const apiUrl = typeof getApiUrl === 'function' ? 5221 getApiUrl('/api/check-admin') : 5222 '/api/check-admin'; 5223 5224 console.log('Sending admin check request to:', apiUrl); 5225 5226 // Create request headers 5227 const headers = { 5228 'Authorization': formattedToken, 5229 'X-Hydra-Request': 'true' // Indicate this might be part of Hydra flow 5230 }; 5231 5232 if (userEmail) { 5233 headers['X-User-Email'] = userEmail; 5234 } 5235 5236 console.log('Admin check headers:', { 5237 'Authorization': `${formattedToken.substring(0, 20)}...`, 5238 'X-User-Email': userEmail || 'Not available' 5239 }); 5240 5241 // Add timeout to the fetch request 5242 const controller = new AbortController(); 5243 const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout 5244 5245 let data; 5246 try { 5247 const response = await fetch(apiUrl, { 5248 method: 'GET', 5249 headers: headers, 5250 signal: controller.signal 5251 }); 5252 5253 clearTimeout(timeoutId); // Clear timeout if fetch succeeds 5254 5255 console.log('Admin check response status:', response.status); 5256 5257 // Get the response as text first for better debugging 5258 const responseText = await response.text(); 5259 console.log('Admin check response length:', responseText.length); 5260 console.log('Admin check response text (first 100 chars):', responseText.substring(0, 100) + '...'); 5261 5262 5263 try { 5264 data = JSON.parse(responseText); 5265 console.log('Admin check data:', data); 5266 } catch (parseError) { 5267 console.error('Error parsing admin check response:', parseError); 5268 // Handle cases where the response might not be JSON (e.g., HTML error page) 5269 // For preview URLs, let's still assume admin to allow editing flow 5270 if (window.location.hostname === 'preview.neonsky.app') { 5271 console.log('Preview URL detected, assuming admin status due to non-JSON response'); 5272 isAdmin = true; 5273 document.body.classList.add('hydra-admin'); 5274 localStorage.setItem('hydra_is_admin', 'true'); 5275 forceShowEditUI(); // Attempt to show UI 5276 return true; // Indicate potential success for preview 5277 } 5278 throw new Error(`Failed to parse admin check response: ${responseText.substring(0, 100)}...`); 5279 } 5280 5281 if (!response.ok) { 5282 // Throw an error with details from the parsed JSON if available 5283 throw new Error(`Admin check failed: ${response.status} - ${data?.error || responseText.substring(0, 50)}`); 5284 } 5285 5286 // Process the admin status 5287 isAdmin = data.isAdmin; 5288 window.isAdmin = isAdmin; // Update global flag 5289 5290 // Update classes based on admin status 5291 if (isAdmin) { 5292 document.body.classList.add('hydra-admin'); 5293 localStorage.setItem('hydra_is_admin', 'true'); 5294 // Store email if we got it from the server response 5295 if (data.email) { 5296 localStorage.setItem('hydra_auth_email', data.email); 5297 } else if (userEmail) { 5298 localStorage.setItem('hydra_auth_email', userEmail); // Store the email we used 5299 } 5300 } else { 5301 document.body.classList.remove('hydra-admin'); 5302 localStorage.removeItem('hydra_is_admin'); 5303 localStorage.removeItem('hydra_auth_email'); // Clear email if not admin 5304 } 5305 5306 // Update siteId if provided 5307 if (data.siteId) { 5308 siteId = data.siteId; 5309 window.siteId = siteId; 5310 if (window.Parameters) window.Parameters.siteId = siteId; 5311 } 5312 5313 // Store new token if provided (Hydra token) 5314 if (data.hydraToken) { 5315 console.log('Received hydra token:', data.hydraToken.substring(0, 10) + '...'); 5316 const cleanHydraToken = data.hydraToken.startsWith('hydra:') ? data.hydraToken.substring(6) : data.hydraToken; 5317 // Always update the local didToken variable so saves immediately use the hydra token 5318 didToken = cleanHydraToken; 5319 // Store token globally and in localStorage using TokenManager if available 5320 if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') { 5321 window.TokenManager.storeToken(data.hydraToken, data.email || userEmail || ''); 5322 } else { 5323 // Fallback storage 5324 localStorage.setItem('hydra_auth_token', cleanHydraToken); 5325 if (data.email || userEmail) { 5326 localStorage.setItem('hydra_auth_email', data.email || userEmail); 5327 } 5328 } 5329 console.log('Stored credentials in localStorage'); 5330 } 5331 5332 // Update UI based on admin status 5333 if (isAdmin) { 5334 console.log('Admin status confirmed, showing edit UI'); 5335 forceShowEditUI(); // Ensure UI elements are visible 5336 // --- MODIFICATION START --- 5337 // If layout is horizontal, re-render the menu to show controls 5338 let currentLayout = 'sidebar'; // Default 5339 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 5340 currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; 5341 } 5342 if (currentLayout === 'horizontal') { 5343 console.log('checkAdminStatus: Horizontal layout detected, re-rendering horizontal menu.'); 5344 if (typeof renderHorizontalMenu === 'function') { 5345 renderHorizontalMenu(); 5346 } else { 5347 console.warn("checkAdminStatus: renderHorizontalMenu function not found."); 5348 } 5349 } 5350 // --- MODIFICATION END --- 5351 createLoginBackup(); // Create backup after successful login/admin check 5352 } else { 5353 console.log('User is not admin, resetting UI.'); 5354 resetAuthUI(); // Ensure non-admin UI state 5355 } 5356 5357 return isAdmin; // Return the admin status 5358 5359 } catch (fetchError) { 5360 clearTimeout(timeoutId); // Clear timeout on error as well 5361 console.error('Fetch error during admin check:', fetchError); 5362 5363 // For preview URLs, still allow editing as a fallback 5364 if (window.location.hostname === 'preview.neonsky.app') { 5365 console.log('Preview URL + fetch error, still enabling editing'); 5366 isAdmin = true; 5367 window.isAdmin = true; 5368 document.body.classList.add('hydra-admin'); 5369 localStorage.setItem('hydra_is_admin', 'true'); 5370 forceShowEditUI(); 5371 return true; // Indicate potential success for preview 5372 } 5373 5374 // For non-preview, throw the error to indicate failure 5375 throw fetchError; 5376 } 5377 } catch (error) { 5378 console.error('Error in checkAdminStatus:', error); 5379 // Don't show alert here, let the calling function handle UI feedback 5380 // Reset UI to non-admin state on error 5381 resetAuthUI(); 5382 return false; // Indicate failure 5383 } 5384} 5385 5386// UPDATED: Force showing edit UI with class-based approach 5387function forceShowEditUI() { 5388 console.log('Forcing admin/auth state for UI'); 5389 5390 // Ensure global state flags are set 5391 isAuthenticated = true; 5392 isAdmin = true; 5393 window.isAuthenticated = true; 5394 window.isAdmin = true; 5395 5396 // Add admin/auth classes to body - CSS will handle button visibility 5397 document.body.classList.add('hydra-authenticated', 'hydra-admin'); 5398 5399 // Store admin status in localStorage 5400 localStorage.setItem('hydra_is_admin', 'true'); 5401 5402 // Show edit controls container ONLY if already in edit mode 5403 if (window.isInEditMode && window.isInEditMode()) {
5404 const editControls = document.querySelector('.edit-controls'); 5405 if (editControls) { 5406 editControls.classList.add('visible'); 5407 editControls.style.display = 'flex'; // Make sure container is flex 5408 } 5409 // Ensure controls inside items are visible if already editing 5410 document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => { 5411 if (el.closest('.sidebar.editing') || el.closest('.page-container.editing')) { 5412 el.style.display = 'flex'; 5413 } 5414 }); 5415 5416 // Show logout button in edit controls when in edit mode 5417 const logoutButton = document.getElementById('logoutButton'); 5418 if (logoutButton) { 5419 logoutButton.style.display = 'flex'; 5420 logoutButton.classList.add('visible'); 5421 } 5422 } else { 5423 // Ensure edit controls container is hidden if not in edit mode 5424 const editControls = document.querySelector('.edit-controls'); 5425 if (editControls) { 5426 editControls.classList.remove('visible'); 5427 editControls.style.display = 'none'; 5428 } 5429 5430 // Hide logout button when not in edit mode 5431 const logoutButton = document.getElementById('logoutButton'); 5432 if (logoutButton) { 5433 logoutButton.style.display = 'none'; 5434 logoutButton.classList.remove('visible'); 5435 } 5436 } 5437 5438 console.log('Admin/Auth classes set. CSS should handle button visibility.'); 5439} 5440 5441// UPDATED: Reset auth UI with class-based approach 5442function resetAuthUI() { 5443 document.body.classList.remove('hydra-authenticated'); 5444 document.body.classList.remove('hydra-admin'); 5445 5446 5447 const logoutButton = document.getElementById('logoutButton'); 5448 if (logoutButton) { 5449 logoutButton.style.display = 'none'; 5450 logoutButton.classList.remove('visible'); 5451 } 5452 5453 const editButton = document.getElementById('editButton'); 5454 if (editButton) { 5455 editButton.style.display = 'none'; 5456 editButton.classList.remove('visible'); 5457 } 5458 5459 // Remove admin status from localStorage 5460 localStorage.removeItem('hydra_is_admin'); 5461 localStorage.removeItem('hydra_auth_token'); 5462 5463 if (isEditing) { 5464 toggleEditMode(); // Exit edit mode if active 5465 } 5466} 5467 5468// Debug function - add to your code during testing 5469function debugUIClasses() { 5470 console.log('Current body classes:', document.body.className); 5471 console.log('hydra-initialized:', document.body.classList.contains('hydra-initialized')); 5472 console.log('hydra-authenticated:', document.body.classList.contains('hydra-authenticated')); 5473 console.log('hydra-admin:', document.body.classList.contains('hydra-admin')); 5474 console.log('edit-mode-active:', document.body.classList.contains('edit-mode-active')); 5475} 5476 5477 5478 5479async function logout() { 5480 console.log('Logout initiated'); 5481 5482 try { 5483 // Show loading indication (optional) 5484 showLoadingOverlay('Logging out...'); 5485 5486 // 1. Properly logout from Magic SDK 5487 if (window.magic) { 5488 try { 5489 await window.magic.user.logout(); 5490 console.log('Magic SDK logout successful'); 5491 } catch (magicError) { 5492 console.error('Error during Magic logout:', magicError); 5493 // Continue with other logout steps even if Magic SDK logout fails 5494 } 5495 } 5496 5497 // 2. Reset all authentication state variables 5498 clearHydraClientAuthStorage(); 5499 5500 // 3. Reset all UI state - remove authentication classes from body 5501 document.body.classList.remove('hydra-authenticated'); 5502 document.body.classList.remove('hydra-admin'); 5503 5504 // 5. If in edit mode, exit it first 5505 if (document.body.classList.contains('edit-mode-active')) { 5506 console.log('Exiting edit mode before logout'); 5507 // If toggleEditMode exists, call it to exit edit mode 5508 if (typeof toggleEditMode === 'function') { 5509 try { 5510 toggleEditMode(); 5511 } catch (editError) { 5512 console.error('Error exiting edit mode:', editError); 5513 } 5514 } 5515 5516 // Ensure edit mode class is removed regardless 5517 document.body.classList.remove('edit-mode-active'); 5518 } 5519 5520 // 6. Ensure all edit UI elements are hidden 5521 5522 // Hide edit and logout buttons 5523 const editButton = document.getElementById('editButton'); 5524 if (editButton) { 5525 editButton.classList.remove('visible'); 5526 editButton.style.display = 'none'; 5527 } 5528 5529 const logoutButton = document.getElementById('logoutButton'); 5530 if (logoutButton) { 5531 logoutButton.classList.remove('visible'); 5532 logoutButton.style.display = 'none'; 5533 } 5534 5535 // Hide edit controls
5536 const editControls = document.querySelector('.edit-controls'); 5537 if (editControls) { 5538 editControls.style.display = 'none'; 5539 editControls.classList.remove('visible'); 5540 } 5541 5542 // Hide sidebar header if it should be hidden when logged out 5543 const sidebarHeader = document.querySelector('.sidebar-header'); 5544 if (sidebarHeader) { 5545 sidebarHeader.style.display = 'none'; 5546 } 5547 5548 // Hide all element controls 5549 document.querySelectorAll('.controls, .element-controls, .edit-btn, .delete-btn, .visibility-toggle').forEach(el => { 5550 el.style.display = 'none'; 5551 }); 5552 5553 // 7. Reset editor state if applicable 5554 if (window.Parameters) { 5555 window.Parameters.isInEditor = false; 5556 } 5557 5558 // Hide any editing forms that might be open 5559 const forms = document.querySelectorAll('.add-form, .edit-form, .element-edit-form, #menuStyleEditor, #metadataEditor, #importClassicForm, #sidebarElementForm'); 5560 forms.forEach(form => { 5561 form.style.display = 'none'; 5562 if (form.classList.contains('visible')) { 5563 form.classList.remove('visible'); 5564 } 5565 }); 5566 5567 // Remove editing class from sidebar 5568 const sidebar = document.querySelector('.sidebar'); 5569 if (sidebar) { 5570 sidebar.classList.remove('editing'); 5571 } 5572 5573 // 8. Clear any page-specific state 5574 if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { 5575 window.PageManager.clearCurrentPage(); 5576 } 5577 5578 // 9. Notify any components that need to know about logout 5579 document.dispatchEvent(new CustomEvent('user-logout', { 5580 detail: { timestamp: Date.now() } 5581 })); 5582 5583 // 10. Show success message 5584 hideLoadingOverlay(); 5585 showSuccessMessage('Successfully logged out'); 5586 5587 console.log('Logout complete'); 5588 5589 // 11. Optional: Refresh the page after a short delay for a clean state 5590 // Uncomment the following lines if you want the page to refresh after logout 5591 /* 5592 setTimeout(() => { 5593 window.location.reload(); 5594 }, 1500); 5595 */ 5596 5597 } catch (error) { 5598 console.error('Error during logout process:', error); 5599 hideLoadingOverlay(); 5600 showErrorMessage('Error during logout. Please refresh the page.'); 5601 } 5602} 5603 5604// Ensure the logout button is properly connected to the logout function 5605document.addEventListener('DOMContentLoaded', function() { 5606 const logoutButton = document.getElementById('logoutButton'); 5607 if (logoutButton) { 5608 // Remove any existing event listeners to avoid duplicates 5609 const newButton = logoutButton.cloneNode(true); 5610 logoutButton.parentNode.replaceChild(newButton, logoutButton); 5611 5612 // Add fresh event listener 5613 newButton.addEventListener('click', function(e) { 5614 e.preventDefault(); 5615 e.stopPropagation(); 5616 logout(); 5617 }); 5618 } 5619}); 5620 5621function stopAutoAdvanceTimer() { 5622 if (window.currentAutoAdvanceTimerId) { 5623 clearTimeout(window.currentAutoAdvanceTimerId); 5624 window.currentAutoAdvanceTimerId = null; 5625 console.log('Auto-advance timer stopped.'); 5626 } 5627} 5628window.stopAutoAdvanceTimer = stopAutoAdvanceTimer; 5629 5630function beginHydraContentLoad(type, itemId, pageId) { 5631 const generation = (Number(window.__hydraContentLoadGeneration) || 0) + 1; 5632 window.__hydraContentLoadGeneration = generation; 5633 window.__neonGalleryInitEpoch = (Number(window.__neonGalleryInitEpoch) || 0) + 1; 5634 window.__hydraContentLoadOwner = { 5635 generation, 5636 type, 5637 itemId: itemId === undefined || itemId === null ? '' : String(itemId), 5638 pageId: pageId === undefined || pageId === null ? '' : String(pageId) 5639 }; 5640 return generation; 5641} 5642 5643function isHydraContentLoadCurrent(generation, type, itemId) { 5644 if (generation != null && Number(generation) !== Number(window.__hydraContentLoadGeneration || 0)) { 5645 return false; 5646 } 5647 if (type && window._activeContentType && window._activeContentType !== type) { 5648 return false; 5649 } 5650 if (itemId != null && itemId !== '' && window.activeGalleryId != null && 5651 String(window.activeGalleryId) !== String(itemId)) { 5652 return false; 5653 } 5654 return true; 5655} 5656 5657window.beginHydraContentLoad = beginHydraContentLoad; 5658window.isHydraContentLoadCurrent = isHydraContentLoadCurrent; 5659 5660function dismissGalleryPasswordOverlay() { 5661 if (window.PageManager && typeof window.PageManager.removeGalleryPasswordOverlay === 'function') { 5662 window.PageManager.removeGalleryPasswordOverlay(); 5663 return; 5664 } 5665 document.querySelectorAll('.gallery-password-protection-overlay').forEach(function(overlay) { 5666 if (overlay._hydraPasswordAbort && typeof overlay._hydraPasswordAbort.abort === 'function') { 5667 overlay._hydraPasswordAbort.abort(); 5668 } 5669 overlay.remove(); 5670 }); 5671} 5672window.dismissGalleryPasswordOverlay = dismissGalleryPasswordOverlay; 5673 5674function toggleEditMode() { 5675 // Prevent multiple simultaneous calls 5676 if (window._editModeToggleInProgress) { 5677 console.log('Edit mode toggle already in progress, skipping'); 5678 return; 5679 } 5680 window._editModeToggleInProgress = true; 5681 5682 console.log("toggleEditMode called. Current window.isEditing:", window.isEditing, "Current window.isInEditMode():", window.isInEditMode ? window.isInEditMode() : 'undefined'); 5683 5684 stopAutoAdvanceTimer(); // Assumes this function exists and stops any slideshow/auto-advance 5685 5686 // Authentication and Authorization Checks 5687 if (!window.isAuthenticated && !(localStorage.getItem('hydra_is_admin') === 'true')) { 5688 alert('You must be logged in to edit'); 5689 window._editModeToggleInProgress = false;
5690 return; 5691 } 5692 if (localStorage.getItem('hydra_is_admin') === 'true' && !window.isAdmin) { 5693 window.isAdmin = true; // Synchronize global flag 5694 } 5695 if (!window.isAdmin) { 5696 alert('You must be an admin to edit'); 5697 window._editModeToggleInProgress = false; 5698 return; 5699 } 5700 5701 const currentEditState = window.isInEditMode ? window.isInEditMode() : (typeof window.isEditing !== 'undefined' ? window.isEditing : false); 5702 const newEditState = !currentEditState; 5703 5704 console.log(`Changing edit mode from ${currentEditState} to: ${newEditState}`); 5705 5706 // Apply body class for edit mode styling 5707 if (newEditState) { 5708 console.log('Entering edit mode - setting body class and theme'); 5709 document.body.setAttribute('data-theme', 'pico'); // Optional: if Pico theme is used for edit mode 5710 document.body.classList.add('edit-mode-active'); 5711 } else { 5712 console.log('Exiting edit mode - removing body class and theme'); 5713 document.body.removeAttribute('data-theme'); 5714 document.body.classList.remove('edit-mode-active'); 5715 } 5716 5717 // Update the global state and dispatch event 5718 if (typeof window.updateGlobalEditState === 'function') { 5719 window.updateGlobalEditState(newEditState); 5720 } else { 5721 window.isEditing = newEditState; // Fallback 5722 document.dispatchEvent(new CustomEvent('edit-mode-changed', { detail: { editing: newEditState } })); 5723 if (!newEditState && typeof window.closeOptionsPanel === 'function') { 5724 window.closeOptionsPanel(); 5725 } 5726 } 5727 5728 if (document.body.classList.contains('menu-layout-horizontal') && typeof syncContentMarginToHorizontalHeader === 'function') { 5729 setTimeout(syncContentMarginToHorizontalHeader, 50); 5730 setTimeout(syncContentMarginToHorizontalHeader, 350); 5731 } 5732 5733 const sidebarEditControls = document.querySelector('.edit-controls'); 5734 5735 if (newEditState) { 5736 // Entering edit mode 5737 if (sidebarEditControls) { 5738 sidebarEditControls.style.display = 'flex'; 5739 sidebarEditControls.classList.add('visible'); 5740 } 5741 5742 // Show logout button in edit controls when entering edit mode 5743 const logoutButton = document.getElementById('logoutButton'); 5744 if (logoutButton) { 5745 logoutButton.style.display = 'flex'; 5746 logoutButton.classList.add('visible'); 5747 } 5748 5749 // Always render the tree-like menu in the sidebar for editing 5750 if (typeof window.renderGalleries === 'function') { 5751 console.log('toggleEditMode (Edit): Rendering sidebar menu (gallery tree) for editing.'); 5752 window.renderGalleries(); // This will show all items, including those with visible:false 5753 } 5754 5755 setTimeout(() => { 5756 if (typeof window.initializeNestedSortables === 'function') { 5757 console.log('Initializing nested sortables for edit mode'); 5758 window.initializeNestedSortables(); 5759 } 5760 if (window.SidebarManager && typeof window.SidebarManager._reinitializeNestedSortables === 'function') { 5761 window.SidebarManager._reinitializeNestedSortables(); 5762 } 5763 }, 150); 5764 5765 // If the currently active item was invisible, reload it now that we are in edit mode. 5766 if(window.galleries && window.activeGalleryId){ 5767 const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId); 5768 if (currentGalleryItem && currentGalleryItem.visible === false) { 5769 console.log('toggleEditMode (Edit): Active item was invisible, reloading it.'); 5770 if (currentGalleryItem.isPage && window.loadPage) { 5771 window.loadPage(window.activeGalleryId); 5772 } else if(window.loadGallery) { 5773 window.loadGallery(window.activeGalleryId); 5774 } 5775 } 5776 } 5777 if(typeof window.showStyleEditor === 'function' && 5778 !(window.MenuStyleCustomizer && window.MenuStyleCustomizer._overlayOpen)) { 5779 window.showStyleEditor(false); // Ensure style editor is hidden initially 5780 } 5781 5782 } else { 5783 // Exiting edit mode (going to Live view) 5784 if (sidebarEditControls) { 5785 sidebarEditControls.style.display = 'none'; 5786 sidebarEditControls.classList.remove('visible'); 5787 } 5788 5789 // Hide logout button when exiting edit mode 5790 const logoutButton = document.getElementById('logoutButton'); 5791 if (logoutButton) { 5792 logoutButton.style.display = 'none'; 5793 logoutButton.classList.remove('visible'); 5794 } 5795 if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer._prepareOverlayForLiveView === 'function') { 5796 window.MenuStyleCustomizer._prepareOverlayForLiveView(); 5797 } 5798 if(typeof window.closeAllForms === 'function') window.closeAllForms(); 5799 5800 if (typeof window.destroyNestedSortables === 'function') { 5801 window.destroyNestedSortables(); 5802 } else if (window.sortableInstances && Array.isArray(window.sortableInstances)) {
5803 window.sortableInstances.forEach(instance => instance.destroy()); 5804 window.sortableInstances = []; 5805 } 5806 5807 // Re-render the menu based on the actual view mode layout. 5808 // These functions should internally handle not showing items with visible:false. 5809 let currentLayout = 'sidebar'; 5810 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 5811 currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; 5812 } 5813 if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') { 5814 window.renderHorizontalMenu(); 5815 } else if (typeof window.renderGalleries === 'function') { 5816 window.renderGalleries(); 5817 } 5818 5819 // Re-render sidebar elements (social, text, etc.) so edit-only UI (grid, dashed slots) is replaced by live view 5820 if (window.SidebarManager && typeof window.SidebarManager._syncWithGlobalEditState === 'function' && typeof window.SidebarManager.renderElements === 'function') { 5821 window.SidebarManager._syncWithGlobalEditState(); 5822 window.SidebarManager.renderElements(); 5823 } 5824 5825 // Handle the currently active gallery/page. 5826 if (window.galleries && window.activeGalleryId) { 5827 const currentGalleryItem = window.galleries.find(g => g.id === window.activeGalleryId); 5828 if (currentGalleryItem) { 5829 // The content for currentGalleryItem should remain loaded, 5830 // even if currentGalleryItem.visible is false. 5831 // The menu rendering above will hide it from the list if it's invisible. 5832 5833 // Apply page-specific menu hiding rules. 5834 const bodyEl = document.body; 5835 // When exiting edit mode, isInEditMode() will effectively be false for this check. 5836 if (shouldApplyMenuHiddenForEmbedOrGallery(currentGalleryItem)) { 5837 bodyEl.classList.add('menu-hidden-on-page'); 5838 } else { 5839 bodyEl.classList.remove('menu-hidden-on-page'); 5840 } 5841 applyMenuOverlayClassForGallery(currentGalleryItem); 5842 // Ensure active states in the (potentially now filtered) menu are updated. 5843 // If the active item is invisible, it won't be marked active in the menu, which is fine. 5844 if(typeof window.updateActiveStates === 'function') window.updateActiveStates(); 5845 if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal(); 5846 5847 } else { 5848 // activeGalleryId points to a non-existent item, so clear it and the content. 5849 console.log('toggleEditMode (Live): activeGalleryId points to a non-existent item. Clearing content.'); 5850 window.activeGalleryId = null; 5851 const galleryContainer = document.querySelector('.gallery-container'); 5852 if (galleryContainer) galleryContainer.innerHTML = ''; 5853 if(typeof window.updateActiveStates === 'function') window.updateActiveStates(); 5854 if(typeof window.updateActiveStatesHorizontal === 'function') window.updateActiveStatesHorizontal(); 5855 } 5856 } 5857 5858 // If, after all that, no content is effectively active (e.g., initial load to root, 5859 // or activeGalleryId became null because the item was deleted) 5860 // then try to load a default page (home page or first visible gallery). 5861 const galleryContainer = document.querySelector('.gallery-container'); 5862 // Check if activeGalleryId is null OR if it's set but the container is empty (e.g. item was deleted) 5863 const noActiveContent = window.activeGalleryId === null || 5864 (galleryContainer && galleryContainer.innerHTML.trim() === ''); 5865 5866 if (noActiveContent && window.galleries) { 5867 console.log("toggleEditMode (Live): No active content, attempting to load default page."); 5868 const homePage = window.galleries.find(g => g.isHomePage === true); 5869 let loadedDefault = false;
5870 if (homePage) { 5871 // Load home page regardless of its visibility status for this default loading logic 5872 console.log(`toggleEditMode (Live): Loading home page: ${homePage.title}`); 5873 if (homePage.isPage && window.loadPage) { window.loadPage(homePage.id); loadedDefault = true; } 5874 else if (window.loadGallery) { window.loadGallery(homePage.id); loadedDefault = true; } 5875 } 5876 5877 if (!loadedDefault) { 5878 // If no home page, load the first *visible* gallery/page in live view. 5879 const firstVisibleGallery = window.galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu); 5880 if (firstVisibleGallery) { 5881 console.log(`toggleEditMode (Live): No home page, loading first visible item: ${firstVisibleGallery.title}`); 5882 if (firstVisibleGallery.isPage && window.loadPage) { 5883 window.loadPage(firstVisibleGallery.id); 5884 } else if(window.loadGallery) { 5885 window.loadGallery(firstVisibleGallery.id); 5886 } 5887 } else { 5888 console.log('toggleEditMode (Live): No home page and no visible items to load.'); 5889 } 5890 } 5891 } 5892 5893 if (window.MenuStyleCustomizer && 5894 window.MenuStyleCustomizer._overlayOpen && 5895 typeof window.MenuStyleCustomizer.applyStyles === 'function') { 5896 window.MenuStyleCustomizer.applyStyles(); 5897 } 5898 } 5899 window._editModeToggleInProgress = false; 5900} 5901 5902 5903 5904/** 5905 * Improved toggleSidebarElementForm - Closes other forms first 5906 */ 5907function toggleSidebarElementForm() { 5908 const formId = 'sidebarElementForm'; 5909 const form = document.getElementById(formId); 5910 5911 if (!form) return; 5912 5913 // If this form is already open, just close everything 5914 if (window.currentOpenForm === formId) { 5915 closeAllForms(); 5916 return; 5917 } 5918 5919 // Otherwise close all forms and open this one 5920 closeAllForms(); 5921 5922 // Show the form 5923 form.style.display = 'block'; 5924 form.classList.add('visible'); 5925 5926 // Set this as the current open form 5927 window.currentOpenForm = formId; 5928} 5929 5930// New function to add a sidebar element 5931function addSidebarElement() { 5932const typeInput = document.querySelector('input[name="sidebarElementType"]:checked'); 5933if (!typeInput) { 5934alert('Please select an element type'); 5935return; 5936} 5937 5938const type = typeInput.value; 5939 5940// Use the SidebarManager to add the element 5941if (window.SidebarManager) { 5942const newElementId = window.SidebarManager.addElement(type); 5943toggleSidebarElementForm(); 5944 5945if (newElementId) { 5946 // Save the changes to KV store 5947 saveGalleries().then(() => { 5948 console.log(`New ${type} element added successfully with ID: ${newElementId}`); 5949 }).catch(error => { 5950 console.error('Error saving new element:', error); 5951 }); 5952} 5953} else { 5954alert('SidebarManager not available'); 5955} 5956} 5957 5958 5959// Global variable to store metadata 5960window.siteMetadata = { 5961 title: '', 5962 description: '', 5963 googleAnalytics: '', 5964 noIndex: false, 5965 favicon: '', 5966 ogImageUrl: '', 5967 contentLanguage: 'en', // Default to English 5968 copyrightEnabled: false, 5969 copyrightText: '', 5970 customPublicSiteEnabled: false, 5971 customPublicSiteHtmlR2Key: '', 5972 customPublicSiteHtmlFilename: '', 5973 siteWideBasicAuth: undefined 5974}; 5975 5976// Global variable to store uploaded favicon URL temporarily 5977window.uploadedFaviconUrl = ''; 5978 5979function updateMetadataCopyrightFieldState() { 5980 const toggle = document.getElementById('metadataCopyrightEnabled'); 5981 const textInput = document.getElementById('metadataCopyrightText'); 5982 const isEnabled = !!(toggle && toggle.checked); 5983 5984 if (textInput) { 5985 textInput.disabled = !isEnabled; 5986 } 5987 5988 // Intentionally leave the surrounding form group in place; the disabled attribute on the input 5989 // provides the visual cue in PicoCSS. 5990} 5991 5992/** 5993 * Improved toggleMetadataEditor - Closes other forms first 5994 */ 5995function toggleMetadataEditor() { 5996 const formId = 'metadataEditor'; 5997 const editor = document.getElementById(formId); 5998 5999 if (!editor) return; 6000 6001 // If this form is already open, just close everything 6002 if (window.currentOpenForm === formId) { 6003 closeAllForms(); 6004 return; 6005 } 6006 6007 // Otherwise close all forms and open this one 6008 closeAllForms(); 6009 6010 // Load current values before showing 6011 loadMetadataValues(); 6012 editor.style.display = 'block'; 6013 6014 // Set this as the current open form 6015 window.currentOpenForm = formId; 6016 if (typeof syncCustomPublicSiteBypassVisibility === 'function') { 6017 syncCustomPublicSiteBypassVisibility(); 6018 } 6019} 6020 6021 6022// Load metadata values into the form 6023function loadMetadataValues() { 6024 // Get the current values from the global object 6025 const metadata = window.siteMetadata || {}; 6026 console.log('loadMetadataValues: Loading metadata:', JSON.stringify(metadata)); 6027 console.log('loadMetadataValues: Google Analytics from metadata:', metadata.googleAnalytics); 6028 6029 console.warn('=== FAVICON LOAD DEBUG ==='); 6030 console.warn('window.siteMetadata:', JSON.stringify(window.siteMetadata)); 6031 console.warn('metadata object:', JSON.stringify(metadata)); 6032 console.warn('metadata.favicon value:', metadata.favicon); 6033 console.warn('metadata.favicon type:', typeof metadata.favicon); 6034 6035 // Populate form fields 6036 document.getElementById('metaTitle').value = metadata.title || ''; 6037 document.getElementById('metaDescription').value = metadata.description || ''; 6038 document.getElementById('googleAnalytics').value = metadata.googleAnalytics || ''; 6039 document.getElementById('noIndexToggle').checked = metadata.noIndex || false; 6040 6041 // Schema / Identity fields 6042 const schemaType = metadata.schemaType || ''; 6043 const personRadio = document.getElementById('schemaTypePerson'); 6044 const orgRadio = document.getElementById('schemaTypeOrganization'); 6045 if (personRadio) personRadio.checked = schemaType === 'Person'; 6046 if (orgRadio) orgRadio.checked = schemaType === 'Organization'; 6047 const jobTitleEl = document.getElementById('schemaJobTitle'); 6048 if (jobTitleEl) jobTitleEl.value = metadata.schemaJobTitle || ''; 6049 const cityEl = document.getElementById('schemaCity'); 6050 if (cityEl) cityEl.value = metadata.schemaCity || ''; 6051 const emailEl = document.getElementById('schemaEmail'); 6052 if (emailEl) emailEl.value = metadata.schemaEmail || ''; 6053 const imageLicenseUrlEl = document.getElementById('imageLicenseUrl'); 6054 if (imageLicenseUrlEl) imageLicenseUrlEl.value = metadata.imageLicenseUrl || ''; 6055 const acquireLicensePageUrlEl = document.getElementById('acquireLicensePageUrl'); 6056 if (acquireLicensePageUrlEl) acquireLicensePageUrlEl.value = metadata.acquireLicensePageUrl || ''; 6057 if (typeof updateSchemaFields === 'function') updateSchemaFields(); 6058 6059 const copyrightToggle = document.getElementById('metadataCopyrightEnabled'); 6060 const copyrightInput = document.getElementById('metadataCopyrightText'); 6061 if (copyrightToggle) { 6062 const enabled = !!metadata.copyrightEnabled; 6063 copyrightToggle.checked = enabled; 6064 if (copyrightInput) { 6065 copyrightInput.value = metadata.copyrightText || ''; 6066 } 6067 } 6068 updateMetadataCopyrightFieldState(); 6069 6070 const customPubToggle = document.getElementById('customPublicSiteEnabled'); 6071 if (customPubToggle) { 6072 customPubToggle.checked = !!metadata.customPublicSiteEnabled; 6073 } 6074 const customPubKeyEl = document.getElementById('customPublicSiteHtmlR2Key'); 6075 if (customPubKeyEl) { 6076 customPubKeyEl.value = metadata.customPublicSiteHtmlR2Key || ''; 6077 } 6078 const customPubFileLabel = document.getElementById('customPublicSiteHtmlFilenameLabel'); 6079 if (customPubFileLabel) { 6080 customPubFileLabel.textContent = metadata.customPublicSiteHtmlFilename 6081 ? ('Current file: ' + metadata.customPublicSiteHtmlFilename) 6082 : 'No HTML file uploaded yet.'; 6083 } 6084 6085 const swAuth = metadata.siteWideBasicAuth; 6086 const swEnabled = !!(swAuth && swAuth.username && swAuth.password); 6087 const swEnableEl = document.getElementById('siteWideBasicAuthEnabled'); 6088 if (swEnableEl) swEnableEl.checked = swEnabled; 6089 const swUserEl = document.getElementById('siteWideBasicAuthUsername'); 6090 if (swUserEl) swUserEl.value = swAuth && swAuth.username ? swAuth.username : ''; 6091 const swPassEl = document.getElementById('siteWideBasicAuthPassword');
6092 if (swPassEl) swPassEl.value = swAuth && swAuth.password ? swAuth.password : ''; 6093 const swRealmEl = document.getElementById('siteWideBasicAuthRealm'); 6094 if (swRealmEl) swRealmEl.value = swAuth && swAuth.realm ? swAuth.realm : ''; 6095 if (typeof updateSiteWideBasicAuthFieldState === 'function') updateSiteWideBasicAuthFieldState(); 6096 6097 // Set content language dropdown (defaults to 'en') 6098 const contentLanguageSelect = document.getElementById('contentLanguage'); 6099 if (contentLanguageSelect) { 6100 contentLanguageSelect.value = metadata.contentLanguage || 'en'; 6101 console.log('Loaded content language:', contentLanguageSelect.value); 6102 } 6103 6104 const faviconInput = document.getElementById('faviconUrl'); 6105 if (faviconInput) { 6106 // Check if we have a recently uploaded favicon URL 6107 const faviconValue = metadata.favicon || window.uploadedFaviconUrl || ''; 6108 faviconInput.value = faviconValue; 6109 console.warn('Set favicon input value to:', faviconInput.value); 6110 console.warn('Used global uploaded favicon URL:', window.uploadedFaviconUrl); 6111 } else { 6112 console.error('Favicon input element not found!'); 6113 } 6114 6115 // Show favicon preview if there's an existing favicon 6116 if (metadata.favicon) { 6117 console.warn('Showing favicon preview for:', metadata.favicon); 6118 showFaviconPreview(metadata.favicon); 6119 } else { 6120 console.warn('No favicon to preview'); 6121 } 6122 6123 const ogInput = document.getElementById('ogImageUrl'); 6124 if (ogInput) { 6125 const ogVal = (metadata.ogImageUrl || '').trim(); 6126 ogInput.value = ogVal; 6127 } 6128 const ogBtnText = document.getElementById('uploadOgImageBtnText'); 6129 if (ogBtnText) { 6130 ogBtnText.textContent = (metadata.ogImageUrl || '').trim() ? 'Change image' : 'Upload image'; 6131 } 6132 if ((metadata.ogImageUrl || '').trim()) { 6133 showOgImagePreview(metadata.ogImageUrl.trim()); 6134 } else { 6135 const ogCont = document.getElementById('ogImagePreviewContainer'); 6136 if (ogCont) ogCont.innerHTML = ''; 6137 } 6138 6139 console.log('loadMetadataValues: Google Analytics field value after setting:', document.getElementById('googleAnalytics').value); 6140} 6141 6142// Save metadata values 6143function saveMetadata() { 6144 // Get values from form 6145 const googleAnalyticsElement = document.getElementById('googleAnalytics'); 6146 console.log('Google Analytics element found:', !!googleAnalyticsElement); 6147 console.log('Google Analytics element value:', googleAnalyticsElement ? googleAnalyticsElement.value : 'ELEMENT NOT FOUND'); 6148 6149 const faviconInput = document.getElementById('faviconUrl'); 6150 const faviconValue = faviconInput ? faviconInput.value : ''; 6151 const ogImageInput = document.getElementById('ogImageUrl'); 6152 const ogImageValue = ogImageInput ? (ogImageInput.value || '').trim() : ''; 6153 6154 const contentLanguageSelect = document.getElementById('contentLanguage'); 6155 const contentLanguageValue = contentLanguageSelect ? contentLanguageSelect.value : 'en'; 6156 6157 const copyrightToggle = document.getElementById('metadataCopyrightEnabled'); 6158 const copyrightInput = document.getElementById('metadataCopyrightText'); 6159 const copyrightEnabled = copyrightToggle ? copyrightToggle.checked : false; 6160 const copyrightText = copyrightInput ? copyrightInput.value.trim() : ''; 6161 6162 console.warn('=== FAVICON SAVE DEBUG ==='); 6163 console.warn('Favicon input element exists:', !!faviconInput); 6164 console.warn('Favicon input value:', faviconValue); 6165 console.warn('Favicon input value length:', faviconValue.length); 6166 6167 console.warn('=== CONTENT LANGUAGE SAVE ==='); 6168 console.warn('Content language selected:', contentLanguageValue); 6169 6170 const rawGa = googleAnalyticsElement ? googleAnalyticsElement.value : ''; 6171 // Normalize GA to ID only (client may not have normalizeGoogleAnalyticsId in scope) 6172 let gaId = (rawGa && typeof rawGa === 'string') ? rawGa.trim() : ''; 6173 if (gaId && !/^G-[A-Z0-9]+$/i.test(gaId) && !/^UA-d+-d+$/.test(gaId)) { 6174 const match = gaId.match(/(G-[A-Z0-9]+|UA-d+-d+)/i); 6175 gaId = match ? match[1] : ''; 6176 } 6177 const customPubKeyInput = document.getElementById('customPublicSiteHtmlR2Key'); 6178 const customPubFilename = document.getElementById('customPublicSiteHtmlFilenameStore'); 6179 const schemaPersonRadio = document.getElementById('schemaTypePerson'); 6180 const schemaOrgRadio = document.getElementById('schemaTypeOrganization'); 6181 const schemaType = schemaPersonRadio && schemaPersonRadio.checked ? 'Person' 6182 : schemaOrgRadio && schemaOrgRadio.checked ? 'Organization' 6183 : ''; 6184 const schemaJobTitleEl = document.getElementById('schemaJobTitle'); 6185 const schemaCityEl = document.getElementById('schemaCity'); 6186 const schemaEmailEl = document.getElementById('schemaEmail'); 6187 const imageLicenseUrlEl = document.getElementById('imageLicenseUrl'); 6188 const acquireLicensePageUrlEl = document.getElementById('acquireLicensePageUrl'); 6189 const imageLicenseUrlRaw = imageLicenseUrlEl ? imageLicenseUrlEl.value.trim() : ''; 6190 const acquireLicensePageUrlRaw = acquireLicensePageUrlEl ? acquireLicensePageUrlEl.value.trim() : ''; 6191 6192 const normalizePublicAbsoluteUrl = (value) => { 6193 if (!value) return ''; 6194 try { 6195 const parsed = new URL(value); 6196 if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') return ''; 6197 return parsed.href; 6198 } catch { 6199 return ''; 6200 } 6201 }; 6202 const imageLicenseUrl = normalizePublicAbsoluteUrl(imageLicenseUrlRaw); 6203 const acquireLicensePageUrl = normalizePublicAbsoluteUrl(acquireLicensePageUrlRaw); 6204 if ((imageLicenseUrlRaw || acquireLicensePageUrlRaw) && (!imageLicenseUrl || !acquireLicensePageUrl)) { 6205 alert('Image licensing: enter two public http(s) URLs, or leave both fields empty.'); 6206 return; 6207 } 6208 6209 const siteWideBasicAuthEnabled = !!(document.getElementById('siteWideBasicAuthEnabled') && document.getElementById('siteWideBasicAuthEnabled').checked); 6210 const swUser = document.getElementById('siteWideBasicAuthUsername') ? document.getElementById('siteWideBasicAuthUsername').value.trim() : ''; 6211 const swPassEl = document.getElementById('siteWideBasicAuthPassword');
6212 const swPass = swPassEl ? swPassEl.value : ''; 6213 const swRealm = document.getElementById('siteWideBasicAuthRealm') ? document.getElementById('siteWideBasicAuthRealm').value.trim() : ''; 6214 6215 if (siteWideBasicAuthEnabled && (!swUser || !swPass)) { 6216 alert('Site-wide password: enter both username and password, or turn off the option.'); 6217 return; 6218 } 6219 6220 const metadata = { 6221 title: document.getElementById('metaTitle').value, 6222 description: document.getElementById('metaDescription').value, 6223 googleAnalytics: gaId, 6224 noIndex: document.getElementById('noIndexToggle').checked, 6225 favicon: faviconValue, 6226 ogImageUrl: ogImageValue, 6227 contentLanguage: contentLanguageValue, 6228 copyrightEnabled, 6229 copyrightText, 6230 customPublicSiteEnabled: !!(document.getElementById('customPublicSiteEnabled') && document.getElementById('customPublicSiteEnabled').checked), 6231 customPublicSiteHtmlR2Key: customPubKeyInput ? customPubKeyInput.value.trim() : '', 6232 customPublicSiteHtmlFilename: customPubFilename ? customPubFilename.value.trim() : '', 6233 schemaType: schemaType || undefined, 6234 schemaJobTitle: schemaJobTitleEl ? schemaJobTitleEl.value.trim() : '', 6235 schemaCity: schemaCityEl ? schemaCityEl.value.trim() : '',
6236 schemaEmail: schemaEmailEl ? schemaEmailEl.value.trim() : '', 6237 ...(imageLicenseUrl && acquireLicensePageUrl ? { imageLicenseUrl, acquireLicensePageUrl } : {}) 6238 }; 6239 6240 if (siteWideBasicAuthEnabled && swUser && swPass) { 6241 metadata.siteWideBasicAuth = { 6242 username: swUser, 6243 password: swPass, 6244 ...(swRealm ? { realm: swRealm } : {}) 6245 }; 6246 } 6247 6248 console.warn('Metadata object being created:', JSON.stringify(metadata)); 6249 console.warn('Favicon in metadata object:', metadata.favicon); 6250 6251 // Show normalized GA ID in the form so user sees what was saved (e.g. after pasting full snippet) 6252 if (googleAnalyticsElement && metadata.googleAnalytics !== rawGa.trim()) { 6253 googleAnalyticsElement.value = metadata.googleAnalytics; 6254 } 6255 6256 // Update global metadata object immediately 6257 window.siteMetadata = metadata; 6258 console.log("Updated window.siteMetadata:", JSON.stringify(window.siteMetadata)); 6259 if (window.SidebarManager && typeof window.SidebarManager.updateMetadataFooter === 'function') { 6260 window.SidebarManager.updateMetadataFooter(); 6261 } 6262 6263 // Ensure we have the latest galleries, sidebar elements, and styles 6264 // before constructing the customData object. 6265 6266 // Synchronize galleries first if the function exists 6267 if (typeof synchronizeGalleries === 'function') { 6268 synchronizeGalleries(); 6269 console.log('Galleries synchronized within saveMetadata'); 6270 } 6271 6272 // Retrieve current galleries (use window.galleries as the source of truth after sync) 6273 const currentGalleries = window.galleries || galleries || []; 6274 console.log(`Current galleries count before constructing customData: ${currentGalleries.length}`); 6275 6276 // Retrieve current sidebar elements 6277 const currentSidebarElements = window.SidebarManager && typeof window.SidebarManager.getElementsForSave === 'function' 6278 ? window.SidebarManager.getElementsForSave() 6279 : null; 6280 6281 // Retrieve current menu styles 6282 const currentMenuStyles = window.MenuStyleCustomizer ? window.MenuStyleCustomizer.settings : {}; 6283 6284 // Now construct customData using the retrieved current state 6285 if (window.saveGalleries) { 6286 const customData = { 6287 galleries: currentGalleries, // Use the synchronized/retrieved galleries 6288 siteMetadata: metadata, // Use the metadata gathered from the form 6289 menuStyles: currentMenuStyles // Use the retrieved menu styles 6290 }; 6291 if (Array.isArray(currentSidebarElements) && currentSidebarElements.length > 0) { 6292 customData.sidebarElements = currentSidebarElements; 6293 } 6294 6295 6296 console.log(`Saving metadata with preserved galleries: ${customData.galleries.length}`); 6297 console.log("Metadata being saved:", JSON.stringify(metadata)); 6298 console.log("Google Analytics specifically:", metadata.googleAnalytics); 6299 console.log("Sidebar elements being saved:", Array.isArray(customData.sidebarElements) ? customData.sidebarElements.length : '(omitted)'); 6300 console.log("Menu styles being saved:", JSON.stringify(customData.menuStyles)); 6301 6302 // Save with our correctly constructed custom data object 6303 window.saveGalleries(customData) 6304 .then(() => { 6305 alert('Metadata saved successfully'); 6306 toggleMetadataEditor(); // Hide the editor 6307 }) 6308 .catch(error => { 6309 console.error('Error saving metadata:', error); 6310 alert('Error saving metadata. Please try again.'); 6311 }); 6312 } else { 6313 alert('Save function not available'); 6314 } 6315} 6316 6317 6318// Function to add new entries to the style editor 6319function showStyleEditor(display = false) { 6320 if (display && window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.openOverlay === 'function') { 6321 window.MenuStyleCustomizer.openOverlay(); 6322 window.currentOpenForm = 'menuStyleEditor'; 6323 return; 6324 } 6325 hideStyleEditor(); 6326} 6327 6328// New function to hide the style editor 6329function hideStyleEditor() { 6330 if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.closeOverlay === 'function') { 6331 window.MenuStyleCustomizer.closeOverlay(); 6332 } 6333 const styleEditor = document.getElementById('menuStyleEditor'); 6334 if (styleEditor) { 6335 styleEditor.style.display = 'none'; 6336 } 6337} 6338 6339 6340/** 6341 * Improved toggleAddForm - Closes other forms first and ensures proper sizing 6342 */ 6343function toggleAddForm() { 6344 const formId = 'addForm'; 6345 const form = document.getElementById(formId); 6346 6347 if (!form) return; 6348 6349 // If this form is already open, just close everything 6350 if (window.currentOpenForm === formId) { 6351 closeAllForms(); 6352 return; 6353 } 6354 6355 // Otherwise close all forms and open this one 6356 closeAllForms(); 6357 6358 // Determine the appropriate max-height based on content 6359 // First check if we're showing the page form by checking the radio button 6360 const isPageSelected = document.querySelector('input[name="galleryType"][value="page"]')?.checked; 6361 6362 // Allow extra space for the page form 6363 const maxHeight = isPageSelected ? '600px' : '500px'; 6364 6365 // Show the form with proper max height 6366 form.style.maxHeight = maxHeight; 6367 form.style.overflow = 'visible';
6367 // Ensure all content is visible 6368 form.classList.add('visible'); 6369 6370 // Also add inline styles to ensure visibility during animation 6371 form.style.padding = '15px'; 6372 6373 // Force recalculation of max-height after UI is visible 6374 setTimeout(() => { 6375 // Get actual content height + padding 6376 const contentHeight = form.scrollHeight + 30; // Add padding 6377 6378 // Set max-height based on content with minimum of 500px 6379 form.style.maxHeight = Math.max(contentHeight, 500) + 'px'; 6380 6381 console.log(`Setting add form max height to ${form.style.maxHeight}`); 6382 }, 50); 6383 6384 // Populate parent options 6385 const parentSelect = document.getElementById('galleryParent'); 6386 if (parentSelect) { 6387 parentSelect.innerHTML = '<option value="">None (Top Level)</option>'; 6388 6389 // Add all galleries that could be parents (including submenus) 6390 galleries.forEach(gallery => { 6391 parentSelect.innerHTML += `<option value="${gallery.id}">${gallery.title}</option>`; 6392 }); 6393 } 6394 6395 // Set this as the current open form 6396 window.currentOpenForm = formId; 6397} 6398 6399 6400// Store the currently open form ID 6401window.currentOpenForm = null; 6402 6403/** 6404 * Close a child panel opened on top of the Style Customizer (palette explorer, custom code, color mixer). 6405 * Returns true when a child was closed so callers can keep the Style Customizer open. 6406 */ 6407function closeTopmostStyleCustomizerChildOverlay() { 6408 if (document.querySelector('.pe-palette-explorer-overlay')) { 6409 if (window.PaletteExplorer && typeof window.PaletteExplorer.close === 'function') { 6410 const wasOpen = !!document.querySelector('.pe-palette-explorer-overlay'); 6411 window.PaletteExplorer.close(); 6412 if (wasOpen && document.querySelector('.pe-palette-explorer-overlay')) { 6413 return false; 6414 } 6415 return wasOpen; 6416 } 6417 } 6418 if (document.querySelector('.pe-style-history-overlay, .sc-style-history-overlay') && 6419 window.MenuStyleCustomizer && 6420 typeof window.MenuStyleCustomizer.closeStyleHistoryOverlay === 'function') { 6421 window.MenuStyleCustomizer.closeStyleHistoryOverlay(); 6422 return true; 6423 } 6424 if (document.querySelector('.pe-font-explorer-overlay') && 6425 window.MenuStyleCustomizer && 6426 typeof window.MenuStyleCustomizer.closeFontExplorer === 'function') { 6427 window.MenuStyleCustomizer.closeFontExplorer(); 6428 return true; 6429 } 6430 if (document.querySelector('.pe-custom-code-overlay') && 6431 window.MenuStyleCustomizer && 6432 typeof window.MenuStyleCustomizer.closeCustomCodeOverlay === 'function') { 6433 window.MenuStyleCustomizer.closeCustomCodeOverlay(); 6434 return true; 6435 } 6436 const colorMixer = document.getElementById('colorMixerPopup'); 6437 if (colorMixer && colorMixer.classList.contains('is-open')) { 6438 colorMixer.classList.remove('is-open'); 6439 return true; 6440 } 6441 return false; 6442} 6443 6444/** 6445 * Close all editor forms with improved handling for add form 6446 */ 6447function closeAllForms() { 6448 if (typeof closeTopmostStyleCustomizerChildOverlay === 'function') { 6449 while (closeTopmostStyleCustomizerChildOverlay()) { 6450 // Close nested Style Customizer panels (palette, custom code, color mixer) first. 6451 } 6452 } 6453 6454 const keepMinimizedStyleCustomizer = !!(window.MenuStyleCustomizer && 6455 window.MenuStyleCustomizer._overlayOpen && 6456 window.MenuStyleCustomizer._overlayMinimized); 6457 6458 if (!keepMinimizedStyleCustomizer && 6459 window.MenuStyleCustomizer && 6460 typeof window.MenuStyleCustomizer.closeOverlay === 'function') { 6461 const styleOverlayWasOpen = window.MenuStyleCustomizer._overlayOpen; 6462 window.MenuStyleCustomizer.closeOverlay(); 6463 if (styleOverlayWasOpen && window.MenuStyleCustomizer._overlayOpen) { 6464 return; 6465 } 6466 } 6467 6468 // Add Form - Special handling to ensure proper animation 6469 const addForm = document.getElementById('addForm'); 6470 if (addForm) { 6471 addForm.classList.remove('visible'); 6472 6473 // Also force style reset after animation completes 6474 setTimeout(() => { 6475 if (!addForm.classList.contains('visible')) { 6476 addForm.style.maxHeight = '0'; 6477 addForm.style.overflow = 'hidden'; 6478 addForm.style.padding = '0 15px'; 6479 } 6480 }, 350); // Set slightly longer than CSS transition 6481 } 6482 6483 // Style Editor (do not hide the overlay host while the minimized mutate bar is live) 6484 const styleEditor = document.getElementById('menuStyleEditor'); 6485 if (styleEditor && !keepMinimizedStyleCustomizer) { 6486 styleEditor.style.display = 'none'; 6487 } 6488 6489 // Metadata Editor 6490 const metadataEditor = document.getElementById('metadataEditor'); 6491 if (metadataEditor) { 6492 metadataEditor.style.display = 'none'; 6493 } 6494 if (typeof syncCustomPublicSiteBypassVisibility === 'function') { 6495 syncCustomPublicSiteBypassVisibility(); 6496 } 6497 6498 // Sidebar Element Form 6499 const sidebarElementForm = document.getElementById('sidebarElementForm'); 6500 if (sidebarElementForm) { 6501 sidebarElementForm.style.display = 'none'; 6502 sidebarElementForm.classList.remove('visible'); 6503 } 6504 6505 // Import Classic Form 6506 const importClassicForm = document.getElementById('importClassicForm'); 6507 if (importClassicForm) { 6508 importClassicForm.style.display = 'none'; 6509 importClassicForm.classList.remove('visible'); 6510 } 6511 6512 // Clear the current open form 6513 window.currentOpenForm = null; 6514} 6515function downloadJson() { 6516 const data = JSON.stringify(galleries, null, 2); 6517 const blob = new Blob([data], { type: 'application/json' }); 6518 const url = URL.createObjectURL(blob); 6519 const a = document.createElement('a'); 6520 a.href = url; 6521 a.download = 'galleries.json'; 6522 document.body.appendChild(a); 6523 a.click(); 6524 document.body.removeChild(a); 6525 URL.revokeObjectURL(url); 6526} 6527 6528let galleriesBackup = []; 6529 6530 6531 6532/** 6533 * Improved version of initializeNestedSortables with a better nesting approach 6534 */ 6535function initializeNestedSortables() { 6536 console.log('Initializing nested sortables with improved nesting logic...'); 6537 6538 // Destroy leftover instances before binding again. A second Sortable on the 6539 // same list makes items preview during drag and snap back on release. 6540 document.querySelectorAll('#galleryTree .nested-sortable').forEach((el) => { 6541 try { 6542 const existing = typeof Sortable !== 'undefined' && Sortable.get ? Sortable.get(el) : null; 6543 if (existing && typeof existing.destroy === 'function') existing.destroy(); 6544 } catch (error) { 6545 console.warn('Could not destroy existing nested sortable:', error); 6546 } 6547 }); 6548 6549 // Create a backup of galleries 6550 try { 6551 galleriesBackup = JSON.parse(JSON.stringify(galleries)); 6552 } catch (error) { 6553 console.error('Error creating galleries backup:', error); 6554 galleriesBackup = []; 6555 } 6556 6557 // Find all nested sortable lists 6558 const nestedSortables = document.querySelectorAll('.nested-sortable'); 6559 if (nestedSortables.length === 0) { 6560 console.warn('No nested sortable elements found! Check your HTML
6560structure.'); 6561 return; 6562 } 6563 6564 // Helper: compute depth based on ancestor nested lists 6565 function computeDepth(liEl) { 6566 let depth = 0; 6567 let node = liEl; 6568 while (node) { 6569 const parentOl = node.closest('ol.nested-sortable'); 6570 if (!parentOl) break; 6571 const parentLi = parentOl.closest('li'); 6572 if (parentLi) { 6573 depth += 1; 6574 node = parentLi; 6575 } else { 6576 break; 6577 } 6578 } 6579 // Top-level depth should be 0 6580 return Math.max(depth - 1, 0); 6581 } 6582 6583 // Helper: apply indentation styling to an li and its subtree 6584 function applyIndentationFromDepth(rootLi) { 6585 if (!rootLi) return; 6586 const applyToItem = (li) => { 6587 const depth = computeDepth(li); 6588 // Update data-nesting-level attribute 6589 li.setAttribute('data-nesting-level', depth); 6590 // Apply padding-left style 6591 const content = li.querySelector('.gallery-item-content'); 6592 if (content) { 6593 content.style.paddingLeft = (depth * 16) + 'px'; 6594 } 6595 }; 6596 applyToItem(rootLi); 6597 rootLi.querySelectorAll('li').forEach(applyToItem); 6598 } 6599 6600 // Helper: ensure parent has proper state and nested list 6601 function ensureParentChildState(parentLi) { 6602 if (!parentLi) return null; 6603 parentLi.classList.add('has-children'); 6604 let nestedList = parentLi.querySelector('ol.nested-sortable'); 6605 if (!nestedList) { 6606 nestedList = document.createElement('ol'); 6607 nestedList.className = 'nested-sortable'; 6608 parentLi.appendChild(nestedList); 6609 } 6610 return nestedList; 6611 } 6612 6613 // Helper: clean parent if it has no children 6614 function cleanupParentIfEmpty(parentLi) { 6615 if (!parentLi) return; 6616 const nestedList = parentLi.querySelector(':scope > ol.nested-sortable'); 6617 const parentId = parseInt(parentLi.dataset && parentLi.dataset.id, 10); 6618 const parentItem = !isNaN(parentId) ? galleries.find(g => g.id === parentId) : null; 6619 // Opted-in pages/galleries must keep an empty drop zone and chevron. 6620 if (isNavigableMenuParent(parentItem)) { 6621 parentLi.classList.add('has-children'); 6622 return; 6623 } 6624 if (nestedList && nestedList.children.length === 0) { 6625 nestedList.remove(); 6626 parentLi.classList.remove('has-children'); 6627 } 6628 } 6629 6630 // Initialize each sortable with improved nesting settings 6631 let sortableInstances = []; 6632 6633 for (let i = 0; i < nestedSortables.length; i++) { 6634 try { 6635 const container = nestedSortables[i]; 6636 6637 // Track where the placeholder indicates the item should be placed 6638 // This helps us detect when SortableJS incorrectly auto-nests based on mouse position 6639 let intendedPlacementContainer = null; 6640 let intendedRelatedElement = null; 6641 6642 // Track last move to prevent oscillation 6643 let lastMoveTime = 0; 6644 let lastMoveRelatedId = null; 6645 const MOVE_DEBOUNCE_MS = 50; // Minimum time between moves (in milliseconds) 6646 6647 // Track if we're dragging the last item in a folder (to prevent boundary oscillation) 6648 let isLastItemInFolder = false; 6649 let draggedItemOriginalContainer = null; 6650 6651 // Track container changes to prevent rapid switching (hysteresis) 6652 let firstPositionFixLoopActive = false; // Prevent multiple fix loops 6653 let lastContainerDecision = null; // 'inside' or 'outside' 6654 let lastContainerDecisionTime = 0; 6655 const CONTAINER_SWITCH_DEBOUNCE_MS = 150; // Longer debounce for container switches 6656 6657 const sortable = new Sortable(container, { 6658 group: 'nested', 6659 animation: 150, // Reduced animation during drag to reduce lag 6660 easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', // Smooth easing function 6661 fallbackOnBody: false, // Disable forced fallback on desktop for better performance 6662 forceFallback: false, // Use native drag on desktop 6663 6664 // CRITICAL: These settings make nesting much easier 6665 // Higher swap threshold makes it easier to drag over items 6666 swapThreshold: 0.65, // Increased to reduce oscillation within same folder 6667 6668 // IMPROVED NESTING SETTINGS 6669 // Used when determining if an item should be nested 6670 invertSwap: false, // Disable to reduce oscillation 6671 direction: 'vertical', // Lock direction to vertical to reduce oscillation 6672 6673 // Use handle for dragging 6674 handle: '.nested-sortable-handle', 6675 6676 // Filter out only the toggle button - this prevents SortableJS from intercepting clicks on it 6677 // The folder text (.menu-item.folder) is NOT filtered, so it can be used for both: 6678 // - Quick click â toggles folder (onclick handler fires before drag starts) 6679 // - Click and drag â drags the item (SortableJS handles the drag) 6680 // The delay helps distinguish between click and drag, but on desktop with delayOnTouchOnly: true, 6681 // the onclick should fire quickly enough before SortableJS intercepts 6682 filter: '.submenu-toggle, .submenu-toggle *, .toggle-icon', 6683 preventOnFilter: true, // Prevent drag when clicking filtered elements (toggle button only) 6684 6685 // Distinguish between click and drag 6686 delay: 120, 6687 delayOnTouchOnly: true, 6688 6689 // Empty container settings 6690 emptyInsertThreshold: 10, 6691 6692 // Add custom ghost class 6693 ghostClass: 'sortable-ghost', 6694 chosenClass: 'sortable-chosen', 6695 dragClass: 'sortable-drag', 6696 6697 // Make sure we listen to external move events 6698 dragoverBubble: true, 6699 6700 // Better drag preview stability 6701 scroll: true, 6702 scrollSensitivity: 100, 6703 scrollSpeed: 20, 6704 6705 // Reduce oscillation by making placeholder movement less sensitive 6706 forceFallback: false, // Use native drag for better performance 6707 fallbackTolerance: 0, // No tolerance - use exact position 6708 6709 /** 6710 * NESTING LOGIC: Prevent auto-nesting based on hover 6711 * Instead, rely on SortableJS's placeholder position which is accurate 6712 * We'll verify and correct placement in onEnd based on actual DOM position 6713 */ 6714 onMove: function(evt, originalEvent) { 6715 const dragged = evt.dragged; 6716 const related = evt.related; 6717 6718 if (!dragged || !related) return true; 6719 6720 // Safety check: Ensure items have data-id attributes 6721 // This can be missing if item was just added and DOM not fully updated 6722 if (!dragged.dataset.id || !related.dataset.id) { 6723 console.warn('â ï¸ [Drag] Item missing data-id attribute, aborting drag', { 6724 draggedHasId: !!dragged.dataset.id, 6725 relatedHasId: !!related.dataset.id 6726 }); 6727 return false;
6728 } 6729 6730 // Get the IDs to check for circular references 6731 const draggedId = parseInt(dragged.dataset.id); 6732 const relatedId = parseInt(related.dataset.id); 6733 6734 // Don't allow dropping inside itself or its descendants 6735 if (draggedId === relatedId || hasAncestor(relatedId, draggedId)) { 6736 return false; 6737 } 6738 6739 const currentTime = Date.now(); 6740 const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); 6741 const draggedParent = draggedItemOriginalContainer || dragged.parentElement; 6742 6743 // Determine if we're moving inside or outside a folder 6744 // IMPORTANT: Check if related element IS the folder item itself (parent li) 6745 // When targeting first position, SortableJS might use the folder item as related 6746 const isRelatedFolderItem = related.classList && related.classList.contains('has-children'); 6747 6748 // CRITICAL: Handle empty folder case FIRST - before any other logic 6749 // When dragging into an empty folder, related is the folder item itself 6750 // This is essentially the same as targeting first position, but for an empty folder 6751 // We need to allow this immediately, bypassing all debounce and boundary checks 6752 if (isRelatedFolderItem) { 6753 const folderNestedSortable = related.querySelector(':scope > ol.nested-sortable'); 6754 const relatedIsNavigableParent = related.classList.contains('navigable-parent-item'); 6755 // Navigable parents look like normal links. Only treat them as an 6756 // empty folder when the pointer is actually over the empty list. 6757 let allowEmptyFolderInsert = !!(folderNestedSortable && folderNestedSortable.children.length === 0); 6758 if (allowEmptyFolderInsert && relatedIsNavigableParent && originalEvent) { 6759 const listRect = folderNestedSortable.getBoundingClientRect(); 6760 const pointerY = originalEvent.clientY; 6761 allowEmptyFolderInsert = pointerY >= listRect.top - 6 && pointerY <= listRect.bottom + 10; 6762 } 6763 if (allowEmptyFolderInsert) { 6764 // Empty folder detected - this is a drop into empty folder (first position) 6765 // Always allow it, bypass all debounce and boundary logic 6766 lastMoveTime = currentTime; 6767 lastMoveRelatedId = relatedId; 6768 intendedPlacementContainer = folderNestedSortable; 6769 intendedRelatedElement = null; // No related element in empty folder 6770 // Mark as moving inside folder for tracking 6771 lastContainerDecision = 'inside'; 6772 lastContainerDecisionTime = currentTime; 6773 6774 // CRITICAL: Manually move placeholder into empty folder's nested-sortable 6775 // SortableJS might not do this automatically for empty containers 6776 requestAnimationFrame(() => { 6777 const placeholder = document.querySelector('.sortable-ghost'); 6778 if (placeholder && placeholder.parentElement !== folderNestedSortable) { 6779 // Move placeholder into the empty folder 6780 folderNestedSortable.appendChild(placeholder); 6781 } 6782 6783 // Update ghost indentation for empty folder 6784 const ghost = document.querySelector('.sortable-ghost'); 6785 if (ghost) { 6786 const ghostContent = ghost.querySelector('.gallery-item-content'); 6787 if (ghostContent) { 6788 // Calculate depth from parent folder 6789 const parentFolder = folderNestedSortable.closest('li.has-children'); 6790 if (parentFolder) { 6791 const targetDepth = computeDepth(parentFolder) + 1; 6792 ghostContent.style.paddingLeft = (targetDepth * 16) + 'px'; 6793 } 6794 } 6795 } 6796 }); 6797 6798 // Return true immediately to allow the move 6799 return true; 6800 } 6801 } 6802 6803 let actualRelatedParent = related.parentElement; 6804 6805 // If related IS the folder item, we need to get its nested sortable container 6806 if (isRelatedFolderItem) { 6807 const nestedSortable = related.querySelector('ol.nested-sortable'); 6808 if (nestedSortable) { 6809 actualRelatedParent = nestedSortable; 6810 } 6811 } 6812 6813 const relatedParent = actualRelatedParent; 6814 6815 const isMovingInsideFolder = relatedParent && 6816 relatedParent.classList.contains('nested-sortable') && 6817 relatedParent !== rootContainer; 6818 const isMovingOutsideFolder = !isRelatedFolderItem && relatedParent === rootContainer; 6819 const currentDecision = isMovingInsideFolder ? 'inside' : (isMovingOutsideFolder ? 'outside' : null); 6820 6821 // Determine if we're moving within the same folder - this is the key check 6822 // If related IS the folder item, we're still moving within the same folder 6823 let isMovingWithinSameFolder = (isMovingInsideFolder && 6824 draggedParent && 6825 draggedParent.classList.contains('nested-sortable') && 6826 draggedParent !== rootContainer && 6827 relatedParent === draggedParent) || 6828 // OR: if related is the folder item itself and we're dragging from that folder 6829 (isRelatedFolderItem && 6830 draggedParent && 6831 draggedParent.classList.contains('nested-sortable') && 6832 draggedParent !== rootContainer && 6833 related.querySelector('ol.nested-sortable') === draggedParent); 6834 6835 // Special case: If related is the folder item, we're definitely trying to move within that folder 6836 // This happens when targeting first position - SortableJS uses the folder item as related 6837 if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { 6838 const folderNestedSortable = related.querySelector('ol.nested-sortable'); 6839 if (folderNestedSortable === draggedParent) { 6840 // We're dragging within the folder, and related is the folder item 6841 // This means we're targeting first position - FORCE it to be treated as same-folder move 6842 // FORCE same-folder detection - override any previous calculation 6843 isMovingWithinSameFolder = true; 6844 } 6845 } 6846 6847 // DEBUG: Log placeholder position changes ONLY when dragging from within a folder AND something interesting happens 6848 // (Don't log every single move - too noisy) 6849 if (draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { 6850 const folderItems = Array.from(draggedParent.children); 6851 const draggedIndex = folderItems.indexOf(dragged); 6852 const relatedIndex = folderItems.indexOf(related); 6853 6854 // Get placeholder element to see where it actually is 6855 const placeholder = document.querySelector('.sortable-ghost'); 6856 const placeholderParent = placeholder ? placeholder.parentElement : null; 6857 const placeholderIndex = placeholderParent && placeholderParent.classList.contains('nested-sortable') 6858 ? Array.from(placeholderParent.children).indexOf(placeholder) 6859 : -1; 6860 6861 const folderItem = draggedParent.closest('li.has-children'); 6862 const folderItemId = folderItem ? folderItem.dataset.id : null; 6863 const firstItemInFolder = folderItems.length > 0 ? folderItems[0] : null; 6864 const firstItemInFolderId = firstItemInFolder ? firstItemInFolder.dataset.id : null; 6865 6866 // Check if we're trying to target first position 6867 // related could be the first item OR the folder item itself (when SortableJS uses folder as related) 6868 const isFirstPosition = folderItems.length > 0 && related === firstItemInFolder; 6869 const isRelatedFolderItemCheck = related === folderItem; 6870 const isTargetingFirstViaFolderItem = isRelatedFolderItemCheck && !!folderItem; // FIXED: was assigning folderItem instead of boolean 6871 6872 // When targeting first via folder item, placeholder should be at index 0 6873 // But we're seeing it at index 1 - this is the issue! 6874 const shouldBeAtFirstPosition = isTargetingFirstViaFolderItem && placeholderIndex === 1 && draggedIndex > 0; 6875 6876 // Check if placeholder is outside the folder when it should be inside 6877 const placeholderIsOutside = placeholderParent === rootContainer || (placeholderParent && !placeholderParent.classList.contains('nested-sortable')); 6878 const shouldBeInside = draggedIndex >= 0; // We're dragging from inside 6879 6880 // Only log when there's a potential issue or interesting state change 6881 const placeholderMismatch = placeholderIndex !== relatedIndex && placeholderIndex >= 0 && relatedIndex >= 0; 6882 const jumpingOut = !isMovingWithinSameFolder && draggedIndex >= 0 && relatedIndex < 0; 6883 const jumpingOutToRoot = placeholderIsOutside && shouldBeInside; 6884 6885 // First position issue: trying to target first but: 6886 // - placeholder is outside OR 6887 // - placeholder is at index 1 instead of 0 (when targeting first via folder item) 6888 const firstPositionIssue = (isFirstPosition || isTargetingFirstViaFolderItem) && (jumpingOut || jumpingOutToRoot || placeholderIsOutside || shouldBeAtFirstPosition); 6889 6890 // ALWAYS log when targeting first position via folder item (this is the key case we're debugging) 6891 // OR when placeholder jumps out when it should stay in 6892 // OR when placeholder should be at first position but isn't 6893 const shouldLog = placeholderMismatch || jumpingOut || jumpingOutToRoot || firstPositionIssue || isTargetingFirstViaFolderItem || shouldBeAtFirstPosition; 6894 6895 if (shouldLog) { 6896 console.error('ð [Placeholder Position]', { 6897 draggedId, 6898 draggedIndex, 6899 relatedId, 6900 relatedIndex, 6901 placeholderIndex, 6902 relatedElementTag: related.tagName, 6903 isRelatedFolderItem: isRelatedFolderItemCheck, 6904 isTargetingFirstViaFolderItem, 6905 isMovingWithinSameFolder, 6906 placeholderMismatch, 6907 jumpingOut, 6908 jumpingOutToRoot, 6909 isFirstPosition, 6910 placeholderIsOutside, 6911 placeholderParentType: placeholderParent === rootContainer ? 'ROOT' : (placeholderParent && placeholderParent.classList.contains('nested-sortable') ? 'FOLDER' : 'OTHER'), 6912 folderItemId, 6913 firstItemInFolderId, 6914 firstPositionIssue, 6915 shouldBeAtFirstPosition, 6916 expectedPlaceholderIndex: isTargetingFirstViaFolderItem ? 0 : placeholderIndex 6917 }); 6918 } 6919 } 6920 6921 // CRITICAL: For moves within the same folder, skip ALL the complex boundary logic 6922 // This allows smooth reordering just like items not in folders 6923 if (isMovingWithinSameFolder) { 6924 // Simple debounce only - same as root level items 6925 const timeSinceLastMove = currentTime - lastMoveTime; 6926 // Only prevent very rapid oscillation to same element 6927 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5 && relatedId === lastMoveRelatedId) { 6928 return false;
6929 } 6930 // Allow all moves within same folder - update tracking and continue 6931 6932 // SPECIAL FIX: When targeting first position via folder item, fix placeholder position 6933 // SortableJS places it at index 1 when related is the folder item, but we want it at index 0 6934 // BUT: Only apply this fix when actually near/at first position to avoid being too aggressive 6935 if (isRelatedFolderItem && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { 6936 const folderItems = Array.from(draggedParent.children); 6937 const firstItem = folderItems.length > 0 ? folderItems[0] : null; 6938 6939 // Only apply fix if we're actually near first position (draggedIndex is small, indicating we're moving up) 6940 // This prevents the fix from being too aggressive when moving to other positions 6941 const isNearFirstPosition = draggedIndex <= 2; // Allow fix when within first 3 positions 6942 6943 if (firstItem && firstItem !== dragged && isNearFirstPosition) { 6944 // Fix immediately (synchronously) first - don't wait for next frame 6945 const placeholder = document.querySelector('.sortable-ghost'); 6946 if (placeholder && placeholder.parentElement === draggedParent) { 6947 const currentIndex = Array.from(draggedParent.children).indexOf(placeholder); 6948 // Only fix if placeholder is at index 1 when we're targeting first position 6949 // Don't force it if it's at index 0 or beyond index 2 (user might be moving to position 2 or 3) 6950 if (currentIndex === 1 && draggedIndex <= 1) { 6951 draggedParent.insertBefore(placeholder, firstItem); 6952 } 6953 } 6954 } else { 6955 // Not near first position - stop any active fix loop 6956 firstPositionFixLoopActive = false; 6957 } 6958 } else { 6959 // Not targeting first position anymore - reset flag 6960 firstPositionFixLoopActive = false; 6961 } 6962 6963 lastMoveTime = currentTime; 6964 lastMoveRelatedId = relatedId; 6965 // Skip all the boundary case logic below - just continue to allow the move 6966 // Fall through to update intendedPlacementContainer and return true 6967 } else { 6968 // Only apply complex boundary logic for moves BETWEEN folders or to/from root 6969 6970 // Hysteresis: Prevent rapid switching between inside/outside folder 6971 const targetFolderItems = isMovingInsideFolder ? Array.from(relatedParent.children) : []; 6972 const isFirstPositionInFolder = isMovingInsideFolder && 6973 targetFolderItems.length > 0 && 6974 related === targetFolderItems[0]; 6975 // Special case: Empty folder - allow easy dropping into it 6976 const isEmptyFolder = isMovingInsideFolder && targetFolderItems.length === 0; 6977 6978 if (currentDecision && lastContainerDecision && currentDecision !== lastContainerDecision) { 6979 const timeSinceContainerSwitch = currentTime - lastContainerDecisionTime; 6980 6981 // Make first position easier to target from outside 6982 if (isFirstPositionInFolder) { 6983 // Moving to first position from outside - use shorter debounce 6984 const effectiveDebounce = CONTAINER_SWITCH_DEBOUNCE_MS * 0.3; 6985 if (timeSinceContainerSwitch < effectiveDebounce) {
6986 // Only block if it's very rapid (oscillation) 6987 if (timeSinceContainerSwitch < MOVE_DEBOUNCE_MS) { 6988 return false; 6989 } 6990 } 6991 } else if (isEmptyFolder) { 6992 // Empty folder - allow easy dropping, minimal debounce 6993 // Only block very rapid oscillation 6994 if (timeSinceContainerSwitch < MOVE_DEBOUNCE_MS * 0.2) { 6995 return false; 6996 } 6997 } else { 6998 // Normal container switch - apply full hysteresis 6999 if (timeSinceContainerSwitch < CONTAINER_SWITCH_DEBOUNCE_MS) { 7000 return false; 7001 } 7002 } 7003 } 7004 } 7005 7006 // Special handling for boundary cases (last item in folder, or dragging into folder) 7007 // BUT: Skip all this logic if we're moving within the same folder (already handled above) 7008 if (!isMovingWithinSameFolder && draggedParent && draggedParent.classList.contains('nested-sortable') && draggedParent !== rootContainer) { 7009 const folderItem = draggedParent.closest('li.has-children'); 7010 const currentFolderItems = Array.from(draggedParent.children); 7011 const isFirstPositionInCurrentFolder = currentFolderItems.length > 0 && 7012 currentFolderItems[0] !== dragged && 7013 (related === currentFolderItems[0] || 7014 (isMovingInsideFolder && relatedParent === draggedParent && 7015 related === currentFolderItems[0])); 7016 7017 // Case 0: This case is now handled earlier for same-folder moves 7018 // Only handle cross-folder moves here 7019 7020 // Case 1: Dragging last item out of folder 7021 if (isLastItemInFolder && isMovingOutsideFolder) { 7022 const timeSinceLastMove = currentTime - lastMoveTime; 7023 7024 // BUT: If we're trying to move to first position, don't block it 7025 // Check if the related element is the folder itself or right after it 7026 if (folderItem) { 7027 const folderNextSibling = folderItem.nextElementSibling; 7028 // If we're moving to position right after folder, it might be trying to go to first position 7029 // Allow it if it's been a reasonable time 7030 if (folderNextSibling && related === folderNextSibling) { 7031 // This is ambiguous - could be first position or outside 7032 // Require more time but not as much as pure outside move 7033 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) { 7034 if (relatedId === lastMoveRelatedId) { 7035 return false; 7036 } 7037 } 7038 } else { 7039 // Definitely moving outside - require longer debounce 7040 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) { 7041 if (relatedId === lastMoveRelatedId) { 7042 return false; // Reject if oscillating 7043 } 7044 } 7045 } 7046 } else { 7047 // No folder item found - apply standard debounce 7048 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2.5) { 7049 if (relatedId === lastMoveRelatedId) { 7050 return false; 7051 } 7052 } 7053 } 7054 } 7055 7056 // Case 2: Dragging item into folder from outside 7057 if (isMovingInsideFolder && relatedParent !== draggedParent) { 7058 const targetFolderItems = Array.from(relatedParent.children); 7059 const isEmptyFolder = targetFolderItems.length === 0; 7060 const isNearBottom = targetFolderItems.length > 0 && 7061 (related === targetFolderItems[targetFolderItems.length - 1] || 7062 targetFolderItems.indexOf(related) >= targetFolderItems.length - 1); 7063 const isFirstPos = targetFolderItems.length > 0 && related === targetFolderItems[0]; 7064 7065 // Special handling for empty folder - allow easy dropping 7066 if (isEmptyFolder) { 7067 const timeSinceLastMove = currentTime - lastMoveTime; 7068 // Very minimal debounce for empty folder - only prevent rapid oscillation 7069 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.2 && relatedId === lastMoveRelatedId) { 7070 return false;
7071 } 7072 // Allow move into empty folder - don't block it 7073 } else if (isFirstPos) { 7074 // Moving to first position - make it easy 7075 const timeSinceLastMove = currentTime - lastMoveTime; 7076 // Very short debounce for first position 7077 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.3 && relatedId === lastMoveRelatedId) { 7078 return false; 7079 } 7080 // Allow move to first position 7081 } else if (isNearBottom) { 7082 // For bottom position, still use some debounce to prevent oscillation 7083 const timeSinceLastMove = currentTime - lastMoveTime; 7084 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.5) { 7085 if (relatedId === lastMoveRelatedId) { 7086 return false; 7087 } 7088 } 7089 } 7090 } 7091 7092 // Case 2.5: Prevent jumping OUT of folder when trying to move to first/last position within 7093 // This is tricky - SortableJS might detect it as outside when we're actually targeting first/last 7094 if (isMovingOutsideFolder && folderItem && draggedParent === draggedItemOriginalContainer) { 7095 // We're dragging from within a folder but detected as moving outside 7096 // Check if the related element is adjacent to the folder (which might indicate targeting first/last) 7097 const folderIndex = Array.from(rootContainer.children).indexOf(folderItem); 7098 const relatedIndex = Array.from(rootContainer.children).indexOf(related); 7099 7100 // If related is immediately before or after folder, might be targeting first/last 7101 if (folderIndex >= 0 && relatedIndex >= 0) { 7102 const isAdjacentToFolder = (relatedIndex === folderIndex - 1) || (relatedIndex === folderIndex + 1); 7103 7104 if (isAdjacentToFolder) { 7105 // Might be targeting first/last position - be lenient 7106 const timeSinceLastMove = currentTime - lastMoveTime;
7107 // Only block if it's very rapid oscillation 7108 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.4 && relatedId === lastMoveRelatedId) { 7109 return false; 7110 } 7111 // Otherwise allow - user might be trying to target first/last position 7112 // Don't block based on this check 7113 } 7114 } 7115 } 7116 } 7117 7118 // Case 3: Dragging from root into folder 7119 if (draggedParent === rootContainer && isMovingInsideFolder) { 7120 const targetFolderItems = Array.from(relatedParent.children); 7121 const isEmptyFolder = targetFolderItems.length === 0; 7122 const isNearBottom = targetFolderItems.length > 0 && 7123 (related === targetFolderItems[targetFolderItems.length - 1] || 7124 targetFolderItems.indexOf(related) >= targetFolderItems.length - 1); 7125 const isFirstPosition = targetFolderItems.length > 0 && related === targetFolderItems[0]; 7126 7127 // Special handling for empty folder - allow easy dropping 7128 if (isEmptyFolder) { 7129 const timeSinceLastMove = currentTime - lastMoveTime; 7130 // Very minimal debounce for empty folder - only prevent rapid oscillation 7131 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.2) { 7132 if (relatedId === lastMoveRelatedId) { 7133 return false; 7134 } 7135 } 7136 // Allow move into empty folder - don't block it 7137 } else if (isFirstPosition) { 7138 // Special handling for first position - make it easier to target 7139 const timeSinceLastMove = currentTime - lastMoveTime; 7140 // Make first position easier - only prevent very rapid oscillation 7141 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.5) { 7142 if (relatedId === lastMoveRelatedId) { 7143 return false; 7144 } 7145 } 7146 // Allow move to first position - don't block it 7147 // Continue to track but don't apply strict debouncing 7148 } else if (isNearBottom) { 7149 // Keep strict debouncing for bottom position 7150 const timeSinceLastMove = currentTime - lastMoveTime; 7151 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 2) { 7152 if (relatedId === lastMoveRelatedId) { 7153 return false; 7154 } 7155 } 7156 } 7157 } 7158 7159 // Case 4: Prevent jumping FROM first position to outside folder (but allow easy access TO first position) 7160 // If we're at first position and trying to move outside, require deliberate movement 7161 if (isMovingInsideFolder) { 7162 const currentFolderItems = Array.from(relatedParent.children); 7163 const isFirstPos = currentFolderItems.length > 0 && related === currentFolderItems[0]; 7164 7165 // If we're moving FROM first position to outside (reverse hysteresis) 7166 if (isFirstPos && lastContainerDecision === 'outside') { 7167 const timeSinceLastMove = currentTime - lastMoveTime; 7168 // If we just moved outside, require more time before allowing back to first position 7169 // BUT: if it's been a reasonable time, allow it (don't make it too hard) 7170 if (timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.8 && relatedId === lastMoveRelatedId) { 7171 // Very rapid - might be oscillation 7172 return false; 7173 } 7174 } 7175 } 7176 7177 // Case 5: Prevent oscillation between first and second position within folder 7178 // BUT: Only apply if NOT moving within same folder (to allow smooth reordering) 7179 if (isMovingInsideFolder && relatedParent && !isMovingWithinSameFolder) { 7180 const folderItems = Array.from(relatedParent.children); 7181 const isFirstPos = folderItems.length > 0 && related === folderItems[0]; 7182 const isSecondPos = folderItems.length > 1 && related === folderItems[1]; 7183 7184 // If moving from first to second (or vice versa) very rapidly, prevent oscillation 7185 if ((isFirstPos || isSecondPos) && lastMoveRelatedId) { 7186 const lastRelated = document.querySelector('#galleryTree li[data-id="' + lastMoveRelatedId + '"]') || document.querySelector('li[data-id="' + lastMoveRelatedId + '"]'); 7187 if (lastRelated && lastRelated.parentElement === relatedParent) { 7188 const lastRelatedIndex = folderItems.indexOf(lastRelated); 7189 const isOscillating = (isFirstPos && lastRelatedIndex === 1) || (isSecondPos && lastRelatedIndex === 0); 7190 7191 if (isOscillating) { 7192 const timeSinceLastMove = currentTime - lastMoveTime; 7193 // Allow moving to first position easily, but prevent rapid oscillation between first/second 7194 if (isSecondPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 1.2) { 7195 return false; // Prevent jumping from first to second too easily 7196 } 7197 // But allow first position more easily 7198 if (isFirstPos && timeSinceLastMove < MOVE_DEBOUNCE_MS * 0.6 && relatedId === lastMoveRelatedId) { 7199 return false; // Only prevent very rapid oscillation 7200 } 7201 } 7202 } 7203 } 7204 } 7205 7206 // Prevent rapid oscillation when dragging within same container 7207 // NOTE: For same-folder moves, we already updated tracking above, so skip this
7207section 7208 if (!isMovingWithinSameFolder) { 7209 const timeSinceLastMove = currentTime - lastMoveTime; 7210 7211 // If moving to the same related element very quickly, reject to prevent oscillation 7212 if (timeSinceLastMove < MOVE_DEBOUNCE_MS && relatedId === lastMoveRelatedId) { 7213 return false; 7214 } 7215 7216 // Update tracking variables (only for non-same-folder moves) 7217 lastMoveTime = currentTime; 7218 lastMoveRelatedId = relatedId; 7219 } 7220 // For same-folder moves, tracking was already updated in the earlier block 7221 7222 // Update container decision tracking 7223 if (currentDecision) { 7224 if (currentDecision !== lastContainerDecision) { 7225 lastContainerDecisionTime = currentTime; 7226 } 7227 lastContainerDecision = currentDecision; 7228 } 7229 7230 // Track where the placeholder indicates the item should be placed 7231 // The related element is next to where the placeholder will be positioned 7232 // Check if it's at root level or nested level 7233 if (related) { 7234 intendedRelatedElement = related; 7235 const relatedParent = related.parentElement; 7236 const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); 7237 7238 // Determine if the placeholder is at root level or nested level 7239 if (relatedParent === rootContainer) { 7240 // Placeholder is at root level (not nested) 7241 intendedPlacementContainer = rootContainer; 7242 } else if (relatedParent && relatedParent.classList.contains('nested-sortable')) { 7243 // Placeholder is in a nested list (inside a folder) 7244 intendedPlacementContainer = relatedParent; 7245 } 7246 7247 // Update ghost element indentation to match the target position 7248 requestAnimationFrame(() => { 7249 const ghost = document.querySelector('.sortable-ghost'); 7250 if (ghost && intendedPlacementContainer) { 7251 const ghostContent = ghost.querySelector('.gallery-item-content'); 7252 if (ghostContent) { 7253 // Calculate the depth where the ghost will be placed 7254 // Use the related element to determine the depth (if it exists and is in the target container) 7255 let targetDepth = 0; 7256 if (intendedPlacementContainer === rootContainer) { 7257 targetDepth = 0; // Root level 7258 } else if (related && related.parentElement === intendedPlacementContainer) { 7259 // Use the related element's depth to determine target depth 7260 targetDepth = computeDepth(related); 7261 } else { 7262 // Fallback: find first child in the container or calculate from parent folder 7263 const firstChild = intendedPlacementContainer.querySelector('li'); 7264 if (firstChild) { 7265 targetDepth = computeDepth(firstChild); 7266 } else { 7267 // Empty container (like empty folder) - calculate from parent folder 7268 const parentFolder = intendedPlacementContainer.closest('li.has-children'); 7269 if (parentFolder) { 7270 targetDepth = computeDepth(parentFolder) + 1; 7271 } 7272 } 7273 } 7274 7275 // Special handling: If intendedPlacementContainer is empty and placeholder isn't in it yet, 7276 // ensure placeholder is moved there and indentation is correct 7277 if (intendedPlacementContainer.children.length === 0) { 7278 const placeholder = document.querySelector('.sortable-ghost'); 7279 if (placeholder && placeholder.parentElement !== intendedPlacementContainer) { 7280 // Move placeholder into empty container 7281 intendedPlacementContainer.appendChild(placeholder); 7282 } 7283 } 7284 ghostContent.style.paddingLeft = (targetDepth * 16) + 'px'; 7285 } 7286 } 7287 }); 7288 } 7289 7290 // Let SortableJS handle the placement based on placeholder position 7291 // We'll verify and correct if needed in onEnd 7292 return true; // Allow the move 7293 }, 7294 7295 // Capture indentation before drag starts (fires when item is chosen) 7296 onChoose: function(evt) { 7297 if (!evt.item) return; 7298 7299 const draggedItem = evt.item; 7300 const content = draggedItem.querySelector('.gallery-item-content'); 7301 7302 // Store the original padding-left for the ghost 7303 if (content) { 7304 const computedStyle = window.getComputedStyle(content); 7305 const originalPaddingLeft = computedStyle.paddingLeft; 7306 draggedItem.dataset.originalPaddingLeft = originalPaddingLeft; 7307 } 7308 }, 7309 7310 // Visual feedback on drag start 7311 onStart: function(evt) { 7312 if (!evt.item) return; 7313 7314 // Don't set dimensions on the dragged item - let it maintain natural size 7315 // Setting width/height can cause layout shifts and menu expansion 7316 const draggedItem = evt.item; 7317 7318 // Store original container and check if it's the last item in folder 7319 draggedItemOriginalContainer = draggedItem.parentElement; 7320 const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); 7321 if (draggedItemOriginalContainer && 7322 draggedItemOriginalContainer.classList.contains('nested-sortable') && 7323 draggedItemOriginalContainer !== rootContainer) { 7324 const siblings = Array.from(draggedItemOriginalContainer.children); 7325 isLastItemInFolder = siblings.length > 0 && siblings[siblings.length - 1] === draggedItem; 7326 } else { 7327 isLastItemInFolder = false;
7328 } 7329 7330 // Apply the stored padding-left to the ghost element 7331 // The ghost is created by SortableJS at this point 7332 requestAnimationFrame(() => { 7333 const ghost = document.querySelector('.sortable-ghost'); 7334 if (ghost) { 7335 const ghostContent = ghost.querySelector('.gallery-item-content'); 7336 const originalPaddingLeft = draggedItem.dataset.originalPaddingLeft; 7337 if (ghostContent && originalPaddingLeft) { 7338 ghostContent.style.paddingLeft = originalPaddingLeft; 7339 } 7340 } 7341 }); 7342 7343 // Reset oscillation tracking variables 7344 lastMoveTime = 0; 7345 lastMoveRelatedId = null; 7346 lastContainerDecision = null; 7347 lastContainerDecisionTime = 0; 7348 7349 // Create a backup 7350 try { 7351 window._dragStartElementIds = []; 7352 document.querySelectorAll('#galleryTree .nested-sortable li').forEach(li => { 7353 if (li.dataset.id) { 7354 window._dragStartElementIds.push(li.dataset.id); 7355 } 7356 }); 7357 7358 galleriesBackup = JSON.parse(JSON.stringify(galleries)); 7359 } catch (error) { 7360 console.error('Error creating backup:', error); 7361 } 7362 7363 // Add indicator classes 7364 document.body.classList.add('menu-item-dragging'); 7365 7366 // Add hover indicator class to submenu items 7367 document.querySelectorAll('.has-children').forEach(item => { 7368 item.classList.add('potential-parent'); 7369 }); 7370 7371 // Prevent layout recalculation during drag 7372 document.body.style.setProperty('--is-dragging', '1'); 7373 }, 7374 7375 // Cleanup on end 7376 onEnd: function(evt) { 7377 // Verify and correct placement based on placeholder's intended position 7378 // SortableJS may auto-nest incorrectly based on mouse position, even if placeholder shows root level 7379 const dragged = evt.item; 7380 const previousParentLi = evt.from ? evt.from.closest('li') : null; 7381 const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); 7382 7383 if (!dragged || !rootContainer) { 7384 intendedPlacementContainer = null; 7385 intendedRelatedElement = null; 7386 // Restore normal sizing 7387 dragged.style.width = ''; 7388 dragged.style.height = ''; 7389 dragged.style.minHeight = ''; 7390 document.body.style.removeProperty('--is-dragging'); 7391 return; 7392 } 7393 7394 // Get the actual parent container where SortableJS placed the item 7395 const actualParentList = dragged.parentElement; 7396 7397 // Restore normal sizing - use requestAnimationFrame to prevent layout thrashing 7398 requestAnimationFrame(() => { 7399 dragged.style.width = ''; 7400 dragged.style.height = ''; 7401 dragged.style.minHeight = ''; 7402 document.body.style.removeProperty('--is-dragging'); 7403 }); 7404 7405 if (intendedPlacementContainer && !document.body.contains(intendedPlacementContainer)) { 7406 intendedPlacementContainer = null; 7407 intendedRelatedElement = null; 7408 } 7409 7410 // Check if the intended placement (where placeholder showed) differs from actual placement 7411 // This happens when SortableJS auto-nests based on mouse position over a folder 7412 if (intendedPlacementContainer && actualParentList !== intendedPlacementContainer) { 7413 // The item ended up in a different container than the placeholder indicated 7414 // Move it to where the placeholder showed it should be (next to intendedRelatedElement) 7415 if (intendedPlacementContainer === rootContainer) { 7416 // Placeholder was at root level - move item to root, positioned next to related element 7417 if (intendedRelatedElement && intendedRelatedElement.parentElement === rootContainer) { 7418 // Insert before the related element (where placeholder was) 7419 rootContainer.insertBefore(dragged, intendedRelatedElement); 7420 console.log("Corrected placement: moved from folder to root level where placeholder indicated"); 7421 } else if (intendedRelatedElement) {
7422 // Related element exists but not at root - find its folder and insert before it at root 7423 const folderItem = intendedRelatedElement.closest('li.has-children'); 7424 if (folderItem && folderItem.parentElement === rootContainer) { 7425 rootContainer.insertBefore(dragged, folderItem); 7426 console.log("Corrected placement: moved to root level above folder where placeholder indicated"); 7427 } else { 7428 rootContainer.appendChild(dragged); 7429 } 7430 } else { 7431 rootContainer.appendChild(dragged); 7432 } 7433 // Update indentation since it's now at root 7434 applyIndentationFromDepth(dragged); 7435 } else { 7436 // Placeholder was in a nested list - move it there 7437 if (intendedRelatedElement && intendedRelatedElement.parentElement === intendedPlacementContainer) { 7438 // Insert before the related element in the nested list 7439 intendedPlacementContainer.insertBefore(dragged, intendedRelatedElement); 7440 } else { 7441 intendedPlacementContainer.appendChild(dragged); 7442 } 7443 console.log("Corrected placement: moved to nested list where placeholder indicated"); 7444 // Update indentation for nested item 7445 applyIndentationFromDepth(dragged); 7446 7447 // Ensure parent folder has proper structure 7448 const folderItem = intendedPlacementContainer.closest('li.has-children'); 7449 if (folderItem) { 7450 const nestedList = folderItem.querySelector('ol.nested-sortable'); 7451 if (nestedList && intendedPlacementContainer !== nestedList) { 7452 nestedList.appendChild(dragged); 7453 } 7454 7455 const parentDepth = computeDepth(folderItem); 7456 const nestedDepth = parentDepth + 1; 7457 7458 if (nestedList) { 7459 nestedList.className = 'nested-sortable nested-level-' + nestedDepth; 7460 } 7461 7462 folderItem.classList.add('has-children'); 7463 } 7464 } 7465 } else if (actualParentList && actualParentList.classList.contains('nested-sortable') && actualParentList !== rootContainer) { 7466 // Item is correctly in a nested list - ensure proper structure 7467 const folderItem = actualParentList.closest('li.has-children'); 7468 if (folderItem) { 7469 const nestedList = folderItem.querySelector('ol.nested-sortable'); 7470 if (nestedList && actualParentList !== nestedList) { 7471 nestedList.appendChild(dragged); 7472 } 7473 7474 const parentDepth = computeDepth(folderItem); 7475 const nestedDepth = parentDepth + 1; 7476 7477 if (nestedList) { 7478 nestedList.className = 'nested-sortable nested-level-' + nestedDepth; 7479 } 7480 7481 folderItem.classList.add('has-children'); 7482 } 7483 applyIndentationFromDepth(dragged); 7484 } else { 7485 // Item is correctly at root level 7486 applyIndentationFromDepth(dragged); 7487 } 7488 7489 // Reset tracking variables 7490 intendedPlacementContainer = null; 7491 intendedRelatedElement = null; 7492 isLastItemInFolder = false; 7493 draggedItemOriginalContainer = null; 7494 lastContainerDecision = null; 7495 lastContainerDecisionTime = 0; 7496 7497 // Apply indentation updates for ALL affected items after drag 7498 // Use requestAnimationFrame to batch DOM updates and prevent visual jumps 7499 requestAnimationFrame(() => { 7500 if (dragged) {
7501 // Update the dragged item and all its descendants 7502 applyIndentationFromDepth(dragged); 7503 7504 // Update all items in the new container (siblings of dragged item) 7505 const newContainer = dragged.parentElement; 7506 if (newContainer && newContainer.classList.contains('nested-sortable')) { 7507 const siblings = Array.from(newContainer.children); 7508 siblings.forEach(sibling => { 7509 if (sibling !== dragged && sibling.tagName === 'LI') { 7510 applyIndentationFromDepth(sibling); 7511 } 7512 }); 7513 } 7514 7515 // Update all items in the old container (if it still exists and has items) 7516 if (evt.from && evt.from.classList.contains('nested-sortable')) { 7517 const oldSiblings = Array.from(evt.from.children); 7518 oldSiblings.forEach(sibling => { 7519 if (sibling.tagName === 'LI') { 7520 applyIndentationFromDepth(sibling); 7521 } 7522 }); 7523 } 7524 7525 // Also update all items in the entire tree to ensure consistency 7526 // This handles edge cases where indentation might be off 7527 const allItems = document.querySelectorAll('#galleryTree .nested-sortable li'); 7528 allItems.forEach(li => { 7529 const depth = computeDepth(li); 7530 li.setAttribute('data-nesting-level', depth); 7531 const content = li.querySelector('.gallery-item-content'); 7532 if (content) { 7533 content.style.paddingLeft = (depth * 16) + 'px'; 7534 } 7535 }); 7536 } 7537 }); 7538 7539 // Clean up previous parent if it lost its last child 7540 if (previousParentLi) { 7541 cleanupParentIfEmpty(previousParentLi); 7542 } 7543 7544 // Remove all indicator classes 7545 document.body.classList.remove('menu-item-dragging'); 7546 document.querySelectorAll('.potential-parent, .will-accept-child, .will-be-nested').forEach(el => { 7547 el.classList.remove('potential-parent', 'will-accept-child', 'will-be-nested'); 7548 }); 7549 7550 // Standard cleanup for ghost elements 7551 document.querySelectorAll('.sortable-ghost, .sortable-chosen, .sortable-drag').forEach(item => { 7552 item.classList.remove('sortable-ghost', 'sortable-chosen', 'sortable-drag'); 7553 }); 7554 7555 // Check if drop was outside a valid container 7556 if (!evt.to || !evt.to.classList.contains('nested-sortable')) { 7557 console.warn('Item was dropped outside a valid container - reverting'); 7558 restoreFromBackup(); 7559 return; 7560 } 7561 7562 // Verify no elements were lost 7563 try { 7564 // Check DOM elements 7565 const endElementIds = []; 7566 document.querySelectorAll('#galleryTree .nested-sortable li').forEach(li => { 7567 if (li.dataset.id) { 7568 endElementIds.push(li.dataset.id); 7569 } 7570 }); 7571 7572 if (window._dragStartElementIds && window._dragStartElementIds.length > 0) { 7573 const startCount = window._dragStartElementIds.length; 7574 const endCount = endElementIds.length; 7575 7576 if (endCount < startCount) { 7577 console.error(`DOM elements were lost during drag! Original: ${startCount}, New: ${endCount}`); 7578 restoreFromBackup(); 7579 return; 7580 } 7581 } 7582 7583 // Update the data structure 7584 updateGalleryStructure(); 7585 7586 // Verify data model 7587 const originalCount = countGalleries(galleriesBackup); 7588 const newCount = countGalleries(galleries); 7589 7590 if (newCount < originalCount) { 7591 console.error(`Items were lost! Original: ${originalCount}, New: ${newCount}`); 7592 restoreFromBackup(); 7593 return; 7594 } 7595 7596 // updateGalleryStructure already persists; do not save or re-render again 7597 // here. A second save/render can rebind Sortable and snap the drop back. 7598 } catch (error) { 7599 console.error('Error updating gallery structure:', error); 7600 restoreFromBackup(); 7601 } 7602 } 7603 }); 7604 7605 sortableInstances.push(sortable); 7606 } catch (error) { 7607 console.error(`Error initializing sortable for container ${i}:`, error); 7608 } 7609 } 7610 7611 // Add event delegation for folder toggles (will be added by setupFolderToggleHandler) 7612 setupFolderToggleHandler(); 7613 7614 return sortableInstances; 7615} 7616 7617// Helper function to set up folder toggle event delegation 7618// This must be called after galleries are rendered, in both view and edit modes 7619function setupFolderToggleHandler() { 7620 const galleryTree = document.getElementById('galleryTree'); 7621 if (galleryTree && !galleryTree.dataset.folderToggleHandlerAdded) { 7622 galleryTree.addEventListener('click', function(e) { 7623 7624 const target = e.target; 7625 // Resolve the clicked row first â do not use target.closest('.menu-item.folder'), 7626 // which matches ancestor folder labels when clicking nested pages/galleries. 7627 const listItem = target.closest('li[data-id]'); 7628 if (!listItem) { 7629 return; 7630 } 7631 7632 const contentDiv = listItem.querySelector(':scope > .gallery-item-content'); 7633 if (!contentDiv) { 7634 return; 7635 } 7636 7637 const toggleButton = contentDiv.querySelector('.submenu-toggle'); 7638 const folderText = contentDiv.querySelector('.menu-item.folder'); 7639 const isToggleClick = toggleButton && toggleButton.contains(target); 7640 const isFolderTextClick = folderText && folderText.contains(target); 7641 7642 7643 if (isToggleClick || isFolderTextClick) { 7644 const folderId = parseInt(listItem.dataset.id, 10); 7645 7646 if (folderId && !isNaN(folderId)) { 7647 // Prevent SortableJS from intercepting this click 7648 e.stopPropagation(); 7649 e.stopImmediatePropagation(); 7650 toggleFolder(folderId, e); 7651 } 7652 } 7653 }, true); // Use capture phase to run before SortableJS 7654 7655 galleryTree.dataset.folderToggleHandlerAdded = 'true'; 7656 } 7657} 7658 7659// NEW HELPER FUNCTION: Gets the nesting level of an element 7660function getElementNestingLevel(element) { 7661 let level = 0; 7662 let current = element; 7663 7664 // Count how many nested-sortable parents this element has 7665 while (current && current.parentElement) { 7666 if (current.parentElement.classList && current.parentElement.classList.contains('nested-sortable')) { 7667 level++; 7668 } 7669 current = current.parentElement; 7670 } 7671 7672 return level; 7673} 7674 7675// NEW HELPER FUNCTION: Determines if an item would become nested 7676function wouldBeNestedItem(draggedEl, relatedEl, event) { 7677 if (!relatedEl || !draggedEl) return false;
7678 7679 // Get mouse position 7680 const mouseX = event.clientX; 7681 7682 // Get the bounding rect of the related element 7683 const rect = relatedEl.getBoundingClientRect(); 7684 7685 // Determine the nesting threshold - how far from the left edge 7686 // the mouse needs to be to consider it a nesting operation 7687 const nestingThreshold = 25; // pixels from left edge 7688 7689 // Check if mouse is within the nesting threshold from the left edge 7690 const distanceFromLeft = mouseX - rect.left; 7691 7692 // Check if we should nest (mouse is within threshold of left edge) 7693 const wouldNest = distanceFromLeft <= nestingThreshold; 7694 7695 // Log the calculation for debugging 7696 console.log(`Nesting check: distance=${distanceFromLeft}px, threshold=${nestingThreshold}px, would nest=${wouldNest}`); 7697 7698 return wouldNest; 7699} 7700 7701/** 7702 * Helper function to find gallery by ID in a tree structure 7703 * @param {Array} galleries - The gallery array to search 7704 * @param {number} id - The ID to find 7705 * @returns {Object|null} - The found gallery or null 7706 */ 7707function findGalleryById(galleries, id) { 7708 if (window.HelpSystem && typeof window.HelpSystem.isDemoId === 'function' && window.HelpSystem.isDemoId(id)) { 7709 return window.HelpSystem.getDemoRecord(id); 7710 } 7711 if (!Array.isArray(galleries)) return null; 7712 7713 // First check at the current level 7714 const directMatch = galleries.find(g => g.id == id); 7715 if (directMatch) return directMatch; 7716 7717 // Then check in children 7718 for (const gallery of galleries) { 7719 if (gallery.children && gallery.children.length > 0) { 7720 const childMatch = findGalleryById(gallery.children, id); 7721 if (childMatch) return childMatch; 7722 } 7723 } 7724 7725 return null; 7726} 7727 7728/** Detect page menu items even when isPage flag was not persisted */ 7729function isPageMenuItem(item) { 7730 if (!item) return false; 7731 if (item.isPage === true) return true; 7732 if (item.pageId && String(item.pageId).indexOf('page_') === 0) return true; 7733 if (Array.isArray(item.pageElements) && item.pageElements.length > 0) return true; 7734 return false; 7735} 7736 7737/** 7738 * Return true only for pages/galleries that explicitly opted into navigable-parent behavior. 7739 * Never infer this from children or parentId; legacy items must retain their existing behavior. 7740 */ 7741function isNavigableMenuParent(item) { 7742 if (!item || item.actsAsMenuParent !== true) return false; 7743 if (item.isFolder === true || item.isSubmenu === true || item.isSpacer === true || item.isExternal === true) { 7744 return false; 7745 } 7746 return isPageMenuItem(item) || item.isIntegrated === true; 7747} 7748 7749/** 7750 * Get all gallery IDs from a tree structure 7751 * @param {Array} galleries - Array of galleries 7752 * @returns {Array} - Flat array of all IDs 7753 */ 7754function getAllGalleryIds(galleries) { 7755 if (!Array.isArray(galleries)) return []; 7756 7757 let ids = []; 7758 7759 galleries.forEach(gallery => { 7760 if (gallery && gallery.id) { 7761 ids.push(gallery.id); 7762 } 7763 7764 // Add child IDs if any 7765 if (gallery.children && Array.isArray(gallery.children)) { 7766 ids = ids.concat(getAllGalleryIds(gallery.children)); 7767 } 7768 }); 7769 7770 return ids; 7771} 7772 7773/** 7774 * Enhanced count function that handles edge cases 7775 * @param {Array} galleryArray - The array to count 7776 * @returns {number} - The total count of galleries 7777 */ 7778function countGalleries(galleryArray) { 7779 if (!Array.isArray(galleryArray)) return 0; 7780 7781 // Menu items are stored flat and also get a derived children[] at render time. 7782 // Counting children recursively double-counts those items and falsely triggers 7783 // restoreFromBackup after a successful drop. 7784 const ids = new Set(); 7785 function walk(items) { 7786 if (!Array.isArray(items)) return; 7787 items.forEach(function(gallery) { 7788 if (!gallery) return; 7789 if (gallery.id != null) ids.add(gallery.id); 7790 if (gallery.children && Array.isArray(gallery.children)) walk(gallery.children); 7791 }); 7792 } 7793 walk(galleryArray); 7794 return ids.size; 7795} 7796 7797/** 7798 * Enhanced backup restoration function with better debugging and notification 7799 */ 7800function restoreFromBackup() { 7801 console.warn('Restoring galleries from backup due to invalid operation'); 7802 7803 // Verify we have a valid backup 7804 if (!galleriesBackup || !Array.isArray(galleriesBackup) || galleriesBackup.length === 0) { 7805 console.error('No valid backup available for restoration'); 7806 7807 // Create a backup from the current DOM as a last resort 7808 try { 7809 console.log('Attempting to rebuild from DOM structure...'); 7810 rebuildGalleriesFromDOM(); 7811 return; 7812 } catch (rebuildError) { 7813 console.error('Failed to rebuild from DOM:', rebuildError); 7814 // Continue with normal user notification 7815 } 7816 } else { 7817 // Restore from backup 7818 galleries = JSON.parse(JSON.stringify(galleriesBackup)); 7819 } 7820 7821 // Re-render the tree with the restored data 7822 renderGalleries(); 7823 7824 // Force reinitialize nested sortables after restoration 7825 setTimeout(() => { 7826 initializeNestedSortables(); 7827 }, 300); 7828} 7829 7830/** 7831 * Emergency recovery function that tries to rebuild galleries from DOM 7832 * This is a last resort when backup restoration fails 7833 */ 7834function rebuildGalleriesFromDOM() { 7835 console.log('EMERGENCY RECOVERY: Attempting to rebuild galleries from DOM structure'); 7836 7837 // Create a new galleries array 7838 const rebuiltGalleries = []; 7839 7840 // This will track items we've already processed 7841 const processedIds = new Set(); 7842 7843 // Function to process a container and its children 7844 function processContainer(container, parentId = null) { 7845 // Get all immediate list items 7846 const items = container.querySelectorAll(':scope > li'); 7847
7848 items.forEach(item => { 7849 // Get item ID from data attribute 7850 const id = parseInt(item.dataset.id); 7851 if (isNaN(id)) return; // Skip if no valid ID 7852 7853 // Skip already processed items to avoid duplicates 7854 if (processedIds.has(id)) return; 7855 processedIds.add(id); 7856 7857 // Try to find the original gallery data 7858 let galleryData = null; 7859 7860 // First check current galleries array 7861 const existingGallery = findGalleryById(galleries, id); 7862 if (existingGallery) { 7863 galleryData = { ...existingGallery }; 7864 delete galleryData.children; // We'll rebuild the hierarchy 7865 } 7866 // If not found, check if we have a backup 7867 else if (galleriesBackup && Array.isArray(galleriesBackup)) { 7868 const backupGallery = findGalleryById(galleriesBackup, id); 7869 if (backupGallery) { 7870 galleryData = { ...backupGallery }; 7871 delete galleryData.children; // We'll rebuild the hierarchy 7872 } 7873 } 7874 7875 // If we still don't have data, create minimal placeholder 7876 if (!galleryData) { 7877 // Try to extract title from DOM 7878 const titleElement = item.querySelector('.menu-item'); 7879 const title = titleElement ? titleElement.textContent.trim() : `Item ${id}`; 7880 7881 galleryData = { 7882 id: id, 7883 title: title, 7884 visible: !item.classList.contains('hidden-gallery') 7885 }; 7886 } 7887 7888 // Update parent ID to match DOM structure 7889 galleryData.parentId = parentId; 7890 7891 // Add to rebuilt galleries 7892 rebuiltGalleries.push(galleryData); 7893 7894 // Process children if any 7895 const childContainer = item.querySelector('ol.nested-sortable'); 7896 if (childContainer) { 7897 processContainer(childContainer, id); 7898 } 7899 }); 7900 } 7901 7902 // Start with root container 7903 const rootContainer = document.querySelector('#galleryTree > ol.nested-sortable'); 7904 if (rootContainer) { 7905 processContainer(rootContainer); 7906 7907 // Replace global galleries array with our rebuilt one 7908 galleries = rebuiltGalleries; 7909 7910 // Re-render with the rebuilt data 7911 renderGalleries(); 7912 7913 console.log(`RECOVERY COMPLETE: Rebuilt ${rebuiltGalleries.length} items from DOM`); 7914 return true; 7915 } else { 7916 console.error('RECOVERY FAILED: Root container not found'); 7917 return false; 7918 } 7919} 7920 7921// Make sure these functions are available to the global scope 7922window.initializeNestedSortables = initializeNestedSortables; 7923window.restoreFromBackup = restoreFromBackup; 7924window.getElementNestingLevel = getElementNestingLevel; 7925window.wouldBeNestedItem = wouldBeNestedItem; 7926 7927 7928 7929function hasDescendant(itemId, possibleDescendantId) { 7930// Check if possibleDescendantId is a descendant of itemId 7931const children = galleries.filter(g => g.parentId === itemId); 7932 7933if (children.some(child => child.id === possibleDescendantId)) { 7934return true; 7935} 7936 7937return children.some(child => hasDescendant(child.id, possibleDescendantId)); 7938} 7939 7940function updateGalleryStructure() { 7941// Create a new array to hold the updated gallery structure 7942const newGalleries = []; 7943 7944// Function to recursively process nested lists 7945function processNestedList(container, parentId = null) { 7946if (!container || !container.children) { 7947 console.warn('Invalid container in processNestedList', container); 7948 return; 7949} 7950 7951const items = container.children; 7952 7953Array.from(items).forEach(item => { 7954 if (item.tagName !== 'LI') return; // Skip non-list items 7955 7956 const id = parseInt(item.dataset.id); 7957 if (isNaN(id)) { 7958 console.warn('Invalid ID in list item:', item); 7959 return; 7960 } 7961 7962 const gallery = galleries.find(g => g.id === id); 7963 7964 if (gallery) { 7965 // Create a new gallery object without children 7966 const newGallery = { ...gallery }; 7967 if (newGallery.children) delete newGallery.children; 7968 7969 // Update parent ID 7970 newGallery.parentId = parentId; 7971 7972 // Add to new galleries array 7973 newGalleries.push(newGallery); 7974 7975 // Process any nested OL with class 'nested-sortable' within this item 7976 const nestedList = item.querySelector('ol.nested-sortable'); 7977 if (nestedList) { 7978 processNestedList(nestedList, id); 7979 } 7980 } else { 7981 console.warn(`Gallery with ID ${id} not found in galleries array`); 7982 } 7983}); 7984} 7985 7986// Start processing from the root list 7987const rootList = document.querySelector('#galleryTree > ol.nested-sortable'); 7988if (rootList) { 7989try { 7990 processNestedList(rootList); 7991 7992 // Verify that we haven't lost any galleries 7993 if (newGalleries.length < galleries.length) { 7994 console.error(`Gallery count mismatch! Original: ${galleries.length}, New: ${newGalleries.length}`); 7995 7996 // Find missing galleries 7997 const originalIds = galleries.map(g => g.id); 7998 const newIds = newGalleries.map(g => g.id); 7999 const missingIds = originalIds.filter(id => !newIds.includes(id)); 8000 8001 if (missingIds.length > 0) { 8002 console.error('Missing gallery IDs:', missingIds); 8003 8004 // Add missing galleries to the new structure 8005 missingIds.forEach(id => { 8006 const missingGallery = galleries.find(g => g.id === id); 8007 if (missingGallery) { 8008 // Add it as a top-level item 8009 const newGallery = { ...missingGallery, parentId: null }; 8010 if (newGallery.children) delete newGallery.children; 8011 newGalleries.push(newGallery); 8012 console.log(`Rescued gallery: ${newGallery.title} (ID: ${newGallery.id})`); 8013 } 8014 }); 8015 } 8016 } 8017 8018 // Update the galleries array with the new structure 8019 galleries = newGalleries; 8020 if (typeof window !== 'undefined') { 8021 window.galleries = galleries; 8022 } 8023 8024 // Save the updated structure 8025 saveGalleries(); 8026} catch (error) { 8027 console.error('Error in updateGalleryStructure:', error); 8028 throw error; // Re-throw to trigger the backup restore 8029} 8030} else { 8031console.warn('Root list not found'); 8032throw new Error('Root list not found'); // Throw error to trigger backup restore 8033} 8034} 8035 8036function toggleSubmenu(id, event) { 8037// Ensure the event doesn't interfere with drag operations 8038event.stopPropagation(); 8039 8040const listItem = getGalleryTreeListItem(id, event); 8041if (listItem) { 8042// Toggle the expanded class 8043listItem.classList.toggle('expanded'); 8044 8045// Find the submenu toggle icon and rotate it 8046const toggleIcon = listItem.querySelector('.toggle-icon'); 8047if (toggleIcon) { 8048 toggleIcon.classList.toggle('rotated'); 8049} 8050} 8051} 8052 8053/** 8054 * Improved slugify function with robust character handling 8055 * @param {string} text - The text to convert to a slug 8056 * @returns {string} A URL-friendly slug 8057 */ 8058function slugify(text) { 8059 // Guard against null or empty input 8060 if (!text || text.trim() === '') { 8061 return 'page-' + Date.now(); 8062 } 8063 8064 // First convert to lowercase 8065 let slug = text.toLowerCase(); 8066 8067 // Replace spaces with hyphens 8068 slug = slug.split(' ').join('-'); 8069 8070 // Filter out unwanted characters (keep only a-z, 0-9, and hyphens) 8071 let filtered = ''; 8072 for (let i = 0; i < slug.length; i++) { 8073 const char = slug.charAt(i); 8074 if ((char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char === '-') { 8075 filtered += char; 8076 } 8077 } 8078 slug = filtered; 8079 8080 /* 8081 // Clean up multiple hyphens 8082 while (slug.indexOf('--') !== -1) { 8083 slug = slug.replace('--', '-'); 8084 } 8085 */ 8086 8087 // Remove leading and trailing hyphens 8088 if (slug.startsWith('-')) { 8089 slug = slug.substring(1); 8090 } 8091 if (slug.endsWith('-')) { 8092 slug = slug.substring(0, slug.length - 1); 8093 } 8094 8095 // If slug is empty after processing, use a default 8096 if (!slug || slug === '-') { 8097 return 'page-' + Date.now(); 8098 } 8099 8100 return slug; 8101} 8102 8103// Update the addPage function to use our new robust slugify function 8104/**
8105 * Adds a new page to the galleries array 8106 * @returns {number|null} The new page ID or null if error 8107 */ 8108function addPage() { 8109 try { 8110 // Get the page title 8111 const titleInput = document.getElementById('galleryTitle'); 8112 if (!titleInput) { 8113 console.error('Page title input not found'); 8114 return; 8115 } 8116 const title = titleInput.value.trim(); 8117 8118 // Validate title is not empty 8119 if (!title) { 8120 alert('Please enter a page title'); 8121 return; 8122 } 8123 8124 // Get parent ID with error handling 8125 const parentSelect = document.getElementById('galleryParent'); 8126 const parentId = parentSelect && parentSelect.value ? parseInt(parentSelect.value) : null; 8127 8128 // Generate a unique ID for the page 8129 const id = Date.now(); 8130 const pageId = `page_${id}`; 8131 8132 // Calculate slug using our more robust slugify function 8133 const pageSlug = slugify(title); 8134 8135 // Create the page object with explicit properties 8136 const page = { 8137 id: id, 8138 title: title, 8139 isPage: true, 8140 isIntegrated: true, 8141 isSubmenu: false, 8142 pageId: pageId, 8143 visible: true, 8144 parentId: parentId, 8145 siteId: siteId, 8146 // Set slug and URL explicitly 8147 slug: pageSlug, 8148 url: '/' + pageSlug 8149 }; 8150 8151 console.log(`Creating new page with ID: ${id}, title: "${title}"`); 8152 8153 // Add to galleries array 8154 galleries.push(page); 8155 8156 // CRITICAL: Ensure window.galleries is synchronized 8157 if (typeof window.galleries === 'undefined') { 8158 window.galleries = galleries; 8159 } else { 8160 // Make sure the page is in window.galleries too 8161 const existingIndex = window.galleries.findIndex(g => g.id === id); 8162 if (existingIndex >= 0) { 8163 window.galleries[existingIndex] = page; 8164 } else { 8165 window.galleries.push(page); 8166 } 8167 } 8168 8169 // Initialize empty page elements array in PageManager 8170 if (window.PageManager) { 8171 window.PageManager.elements[pageId] = []; 8172 } 8173 8174 // Update UI first 8175 renderGalleries(); 8176 clearAddForm(); 8177 toggleAddForm(); 8178 8179 // Set as active page 8180 console.log(`Setting new page "${page.title}" as active`); 8181 activeGalleryId = id; 8182 window.activeGalleryId = id; 8183 8184 // Update active states in the menu 8185 updateActiveStates(); 8186 8187 // Save to server 8188 saveGalleries().then(() => { 8189 console.log(`Page "${page.title}" saved successfully`); 8190 8191 // Double check that our gallery is still in the array 8192 const pageExists = galleries.some(g => g.id === id); 8193 console.log(`Page exists in galleries array: ${pageExists}`); 8194 8195 if (pageExists) { 8196 // Load the new page after a slight delay to ensure DOM is updated 8197 setTimeout(() => { 8198 console.log(`Loading new page "${page.title}" with ID: ${id}`); 8199 loadPage(id); 8200 }, 50); 8201 } else { 8202 console.error(`Page with ID ${id} not found in galleries array after save`); 8203 } 8204 }).catch(error => { 8205 alert('Error saving page. Please try again.'); 8206 }); 8207 8208 return id; 8209 } catch (error) { 8210 console.error('Error adding page:', error); 8211 alert('Error adding page. Please try again.'); 8212 return null; 8213 } 8214} 8215 8216/** 8217 * Ensures a slug is unique among existing galleries 8218 * @param {string} slug - The base slug to check 8219 * @param {Array} existingGalleries - Array of existing gallery configs 8220 * @returns {string} A unique slug 8221 */ 8222function ensureUniqueSlug(slug, existingGalleries) { 8223 // Guard against invalid slugs 8224 if (!slug || slug === '-') { 8225 slug = 'page-' + Date.now(); 8226 } 8227 8228 let finalSlug = slug; 8229 let counter = 1; 8230 8231 // Check if slug already exists 8232 while (existingGalleries.some(g => g.slug === finalSlug)) { 8233 finalSlug = `${slug}-${counter}`; 8234 counter++; 8235 } 8236 8237 return finalSlug; 8238} 8239 8240/** 8241 * Adds a new gallery based on the selected type 8242 * @returns {number|null} The new gallery ID or null if error 8243 */ 8244function addGallery() { 8245 try { 8246 // Get the gallery title 8247 const titleInput = document.getElementById('galleryTitle'); 8248 if (!titleInput) { 8249 console.error('Gallery title input not found'); 8250 return; 8251 } 8252 const title = titleInput.value.trim(); 8253 8254 // Get the selected gallery type 8255 const galleryTypeRadio = document.querySelector('input[name="galleryType"]:checked'); 8256 if (!galleryTypeRadio) { 8257 console.error('No gallery type selected'); 8258 alert('Please select a gallery type'); 8259 return; 8260 } 8261 8262 const galleryType = galleryTypeRadio.value; 8263 console.log("Selected gallery type:", galleryType); 8264 8265 // If this is a page, use the addPage function 8266 if (galleryType === 'page') { 8267 console.log("Creating page instead of gallery"); 8268 return addPage(); 8269 } 8270 8271 // For spacer type, we don't require a title 8272 if (galleryType !== 'spacer' && !title) { 8273 alert('Please enter a gallery title'); 8274 return; 8275 } 8276 8277 // Continue with gallery creation logic 8278 // Generate unique ID 8279 const id = Date.now(); 8280 8281 // Get parent gallery if selected 8282 const parentSelect = document.getElementById('galleryParent'); 8283 const parentId = parentSelect.value ? parseInt(parentSelect.value) : null; 8284 8285 // Get URL if this is an external gallery 8286 const galleryUrl = document.getElementById('galleryUrl'); 8287 const url = galleryTypeRadio.value === 'external' && galleryUrl 8288 ? galleryUrl.value.trim() 8289 : ""; 8290 8291 // Set basic properties 8292 const gallery = { 8293 id: id, 8294 title: title || "Spacer", // Use "Spacer" as internal title if no title provided 8295 parentId: parentId, 8296 visible: true 8297 }; 8298 8299 // Set type-specific properties 8300 if (galleryTypeRadio.value === 'spacer') { 8301 // Spacer properties 8302 gallery.isSpacer = true; 8303 gallery.title = title || "Spacer"; // For internal reference 8304 } else if (galleryTypeRadio.value === 'integrated') { 8305 // Generate pageId for integrated galleries 8306 gallery.isIntegrated = true; 8307 gallery.pageId = generatePageId(); 8308 8309 // Generate and ensure unique slug 8310 const baseSlug = slugify(title); 8311 gallery.slug = ensureUniqueSlug(baseSlug, galleries); 8312 gallery.url = "/" + gallery.slug;
8313 8314 // Get the siteId from multiple possible sources 8315 const siteId = window.Parameters?.siteId || 8316 window.siteId || 8317 document.querySelector('meta[name="hydra-site-id"]')?.getAttribute('content') || 8318 ''; 8319 8320 // Add galleryOptions with GUID=siteId as the manualCollectionName 8321 gallery.galleryOptions = { 8322 manualCollectionName: `GUID=${siteId}`, 8323 isIntegratedGallery: true, 8324 pageId: gallery.pageId, // Store pageId for reference 8325 siteId: siteId 8326 }; 8327 } else if (galleryTypeRadio.value === 'folder') { 8328 gallery.isFolder = true; 8329 gallery.isCollapsed = true; // Make folders closed by default 8330 } else if (galleryTypeRadio.value === 'external') { 8331 // External URL 8332 gallery.url = url; 8333 gallery.isExternal = true; // Add this flag to identify external URLs 8334 } 8335 8336 console.log(`Creating new gallery with ID: ${id}, title: "${title}"`); 8337 8338 // Add to galleries array 8339 galleries.push(gallery); 8340 8341 // CRITICAL: Ensure window.galleries is synchronized 8342 if (typeof window.galleries === 'undefined') { 8343 window.galleries = galleries; 8344 } else { 8345 // Make sure the gallery is in window.galleries too 8346 const existingIndex = window.galleries.findIndex(g => g.id === id); 8347 if (existingIndex >= 0) { 8348 window.galleries[existingIndex] = gallery; 8349 } else { 8350 window.galleries.push(gallery); 8351 } 8352 } 8353 8354 // Update UI first 8355 renderGalleries(); 8356 clearAddForm(); 8357 toggleAddForm(); 8358 8359 // Save galleries after UI update 8360 saveGalleries().then(() => { 8361 console.log(`Gallery "${gallery.title}" saved successfully`); 8362 8363 // Double check that our gallery is still in the array 8364 const galleryExists = galleries.some(g => g.id === id); 8365 console.log(`Gallery exists in galleries array: ${galleryExists}`); 8366 8367 // For non-folder types, load the new gallery content after a slight delay 8368 if (!gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && gallery.visible !== false) { 8369 setTimeout(() => { 8370 console.log(`Loading new gallery "${gallery.title}" with ID: ${id}`); 8371 if (gallery.isIntegrated) { 8372 loadGallery(id); 8373 } else if (gallery.isExternal && gallery.url) { 8374 // For external URLs, set the iframe src and mark active after navigation target is ready 8375 activeGalleryId = id; 8376 window.activeGalleryId = id; 8377 updateActiveStates(); 8378 const frame = document.getElementById('galleryFrame'); 8379 if (frame) frame.src = gallery.url; 8380 } 8381 }, 50); 8382 } 8383 }).catch(error => { 8384 console.error(`Error saving gallery "${gallery.title}":`, error); 8385 alert('Error saving gallery. Please try again.'); 8386 }); 8387 8388 // Return the new gallery ID 8389 return id; 8390 } catch (error) { 8391 console.error('Error adding gallery:', error); 8392 alert('Error adding gallery. Please try again.'); 8393 } 8394} 8395 8396function debugDOMState(message) { 8397console.log(`[DEBUG] ${message}`); 8398const galleryContainer = document.querySelector('.gallery-container'); 8399const iframe = document.getElementById('galleryFrame'); 8400const pageContainers = document.querySelectorAll('.page-container'); 8401 8402console.log(`- Gallery container: ${galleryContainer ? 'found' : 'not found'}`); 8403console.log(`- iframe: ${iframe ? 'found' : 'not found'}, display: ${iframe?.style.display}`); 8404console.log(`- Page containers: ${pageContainers.length} found`); 8405 8406pageContainers.forEach((container, index) => { 8407console.log(` - Container ${index}: display: ${container.style.display}, visibility: ${container.style.visibility}`); 8408}); 8409} 8410 8411 8412/** 8413* Global helper: whether edit tools are explicitly toggled open. 8414* Do not use Parameters.isInEditor â that is also true for logged-in admins (cache bypass). 8415* Do not infer from sidebar/page .editing alone (can desync from the toggle). 8416*/ 8417window.isInEditMode = function() { 8418 if (document.body.classList.contains('edit-mode-active')) { 8419 return true; 8420 } 8421 if (typeof window.isEditing === 'boolean') { 8422 return window.isEditing; 8423 } 8424 return false;
8425}; 8426 8427/** 8428* This function synchronizes edit mode state across all components 8429*/ 8430window.updateGlobalEditState = function(isEditing) { 8431console.log('Updating global edit state:', isEditing); 8432 8433// Update global variable 8434window.isEditing = isEditing; 8435 8436// Update Parameters object if it exists 8437if (window.Parameters) { 8438window.Parameters.isInEditor = isEditing; 8439} 8440 8441// Update sidebar class 8442const sidebar = document.querySelector('.sidebar'); 8443if (sidebar) { 8444if (isEditing) { 8445 sidebar.classList.add('editing'); 8446} else { 8447 sidebar.classList.remove('editing'); 8448} 8449} 8450 8451// Update page containers 8452const pageContainers = document.querySelectorAll('.page-container'); 8453pageContainers.forEach(container => { 8454if (isEditing) { 8455 container.classList.add('editing'); 8456} else { 8457 container.classList.remove('editing'); 8458} 8459}); 8460 8461// Notify all components via event 8462document.dispatchEvent(new CustomEvent('edit-mode-changed', { 8463detail: { editing: isEditing } 8464})); 8465 8466if (document.body.classList.contains('menu-layout-horizontal') && typeof window.syncContentMarginToHorizontalHeader === 'function') { 8467 window.syncContentMarginToHorizontalHeader(); 8468} 8469 8470if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 8471 window.syncGalleryOptionsButtonVisibility(); 8472} 8473 8474// Gallery Options panel styling depends on body.edit-mode-active â hide it when leaving edit mode 8475if (!isEditing && typeof window.closeOptionsPanel === 'function') { 8476 window.closeOptionsPanel(); 8477} 8478}; 8479 8480/** Show or hide gallery options (#editorButton) based on edit mode and active content type */ 8481window.syncGalleryOptionsButtonVisibility = function() { 8482 const btn = document.getElementById('editorButton'); 8483 if (!btn) return; 8484 8485 const inEditMode = document.body.classList.contains('edit-mode-active'); 8486 // When content type is explicitly gallery, ignore leftover .page-container nodes 8487 // (common when Help sandbox switches from a page to Example Gallery). 8488 const onPageView = window._activeContentType === 'gallery' 8489 ? false 8490 : (window._activeContentType === 'page' || 8491 !!document.querySelector('.gallery-container .page-container')); 8492 let isActiveGallery = false; 8493 const activeId = window.activeGalleryId; 8494 const list = window.galleries || (typeof galleries !== 'undefined' ? galleries : null); 8495 const activeItem = activeId != null && list && typeof findGalleryById === 'function' 8496 ? findGalleryById(list, activeId) 8497 : (Array.isArray(list) ? list.find(function(g) { return g && g.id === activeId; }) : null); 8498 if (activeId != null && inEditMode && !onPageView) { 8499 isActiveGallery = !!(activeItem && !isPageMenuItem(activeItem) && !activeItem.isFolder && !activeItem.isSpacer && !activeItem.isExternal); 8500 } 8501 8502 const shouldShow = inEditMode && !onPageView && isActiveGallery; 8503 if (shouldShow) { 8504 if (window.Parameters) { 8505 window.Parameters.isInEditor = true; 8506 } 8507 btn.classList.remove('gallery-options-force-hidden'); 8508 btn.style.removeProperty('display'); 8509 btn.style.setProperty('display', 'flex', 'important'); 8510 btn.style.pointerEvents = 'auto'; 8511 } else { 8512 btn.classList.add('gallery-options-force-hidden'); 8513 btn.style.setProperty('display', 'none', 'important'); 8514 btn.style.pointerEvents = ''; 8515 } 8516}; 8517 8518document.addEventListener('gallery-loaded', function() { 8519 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 8520 window.syncGalleryOptionsButtonVisibility(); 8521 } 8522}); 8523 8524document.addEventListener('page-loaded', function() { 8525 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 8526 window.syncGalleryOptionsButtonVisibility(); 8527 } 8528}); 8529 8530/** 8531 * Wait for PageManager after CDNâproxy late script load (ISP/Firefox clients). 8532 * Home/page bootstrap can run before the proxy copy of page-manager-v260720-021.js executes. 8533 */ 8534function whenPageManagerReady(callback, options) { 8535 const maxAttempts = (options && options.maxAttempts) || 50; 8536 const intervalMs = (options && options.intervalMs) || 100; 8537 let attempts = 0; 8538 const run = function() { 8539 if (window.PageManager && typeof window.PageManager.loadPage === 'function') { 8540 callback(window.PageManager); 8541 return; 8542 } 8543 attempts += 1; 8544 if (attempts >= maxAttempts) { 8545 console.error('â [loadPage] Timed out waiting for PageManager after', attempts, 'attempts');
8546 if (options && typeof options.onTimeout === 'function') options.onTimeout(); 8547 return; 8548 } 8549 if (attempts === 1 || attempts % 10 === 0) { 8550 console.warn('â³ [loadPage] Waiting for PageManager (CDN proxy late load?) attempt', attempts + '/' + maxAttempts); 8551 } 8552 setTimeout(run, intervalMs); 8553 }; 8554 run(); 8555} 8556 8557/** 8558 * loadPage function that safely handles undefined galleries 8559 * @param {number} id - The gallery ID 8560 * @param {Event} event - Optional event object 8561 */ 8562function loadPage(id, event){ 8563 if (event && event.target && event.target.isConnected === false) { 8564 console.log('loadPage: Ignoring navigation from detached element'); 8565 return; 8566 } 8567 if (event) { 8568 event.preventDefault(); 8569 event.stopPropagation(); 8570 const now = Date.now(); 8571 const lastCallTime = window._lastPageLoadTime || 0; 8572 window._lastPageLoadTime = now; 8573 if (now - lastCallTime < 100) { 8574 console.log('Ignoring duplicate loadPage call'); 8575 return; 8576 } 8577 } 8578 console.log('loadPage: Loading page with gallery ID:', id); 8579 8580 stopAutoAdvanceTimer(); 8581 if (typeof dismissGalleryPasswordOverlay === 'function') { 8582 dismissGalleryPasswordOverlay(); 8583 } 8584 8585 let galleriesData = window.galleries || galleries; 8586 const gallery = findGalleryById(galleriesData, id); 8587 8588 if (!gallery) { 8589 console.warn('No gallery found with ID:', id, 'for page load.'); 8590 return; 8591 } 8592 8593 const contentLoadGeneration = beginHydraContentLoad('page', gallery.id, gallery.pageId || `page_${id}`); 8594 8595 window.activeGalleryId = id; 8596 activeGalleryId = id; 8597 window._activeContentType = 'page'; 8598 if (window.EnhancedGallerySystem) { 8599 window.EnhancedGallerySystem.currentGalleryId = null; 8600 } 8601 8602 if (typeof window.removeGalleryScriptsWithPause === 'function') { 8603 window.removeGalleryScriptsWithPause(); 8604 } 8605 8606 const galleryContainer = document.querySelector('.gallery-container'); 8607 if (galleryContainer) { 8608 galleryContainer.innerHTML = ''; 8609 } 8610 8611 if (!gallery.pageId) gallery.pageId = `page_${id}`; 8612 if (!gallery.isPage) gallery.isPage = true; 8613 8614 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 8615 window.syncGalleryOptionsButtonVisibility(); 8616 } 8617 8618 if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {}; 8619 window._pageIdToGalleryId[gallery.pageId] = id; 8620 8621 if (gallery.pageElements && Array.isArray(gallery.pageElements)) { 8622 if (window.PageManager && window.PageManager.elements) { 8623 window.PageManager.elements[gallery.pageId] = [...gallery.pageElements]; 8624 } 8625 } else if (window.PageManager && window.PageManager.elements && 8626 window.PageManager.elements[gallery.pageId] && 8627 window.PageManager.elements[gallery.pageId].length > 0) { 8628 gallery.pageElements = [...window.PageManager.elements[gallery.pageId]]; 8629 } 8630 8631 if (window.PageManager && typeof window.PageManager.loadPage === 'function') { 8632 try { 8633 window.PageManager.loadPage(gallery.pageId); 8634 const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false; 8635 if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') { 8636 window.PageManager.setEditMode(isCurrentlyEditing); 8637 } 8638 } catch (error) { 8639 console.error('Error loading page with PageManager:', error); 8640 } 8641 } else { 8642 console.error('PageManager not found yet â waiting for CDN proxy script'); 8643 whenPageManagerReady(function() { 8644 try { 8645 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) { 8646 console.log('loadPage: Skipping stale PageManager retry for', gallery.pageId); 8647 return; 8648 } 8649 if (gallery.pageElements && Array.isArray(gallery.pageElements) && window.PageManager.elements) { 8650 window.PageManager.elements[gallery.pageId] = [...gallery.pageElements]; 8651 } 8652 window.PageManager.loadPage(gallery.pageId); 8653 console.log('loadPage: PageManager became available â page render retried'); 8654 } catch (error) { 8655 console.error('Error loading page with PageManager after wait:', error); 8656 } 8657 }); 8658 } 8659 8660 // Apply menu visibility 8661 const bodyEl = document.body; 8662 if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 8663 bodyEl.classList.add('menu-hidden-on-page'); 8664 } else { 8665 bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden 8666 } 8667 applyMenuOverlayClassForGallery(gallery); 8668 8669 if (typeof updateActiveStates === 'function') updateActiveStates(); 8670 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 8671 if (typeof closeMobileMenu === 'function') closeMobileMenu(); 8672 // Close the gallery options panel when navigating to a different page 8673 if (typeof window.closeOptionsPanel === 'function') { 8674 window.closeOptionsPanel(); 8675 } 8676 if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery); 8677 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 8678 window.syncGalleryOptionsButtonVisibility(); 8679 } 8680 if (typeof window !== 'undefined' && typeof document !== 'undefined') { 8681 window.dispatchEvent(new CustomEvent('page-loaded', { 8682 detail: { pageId: gallery.pageId, galleryId: id } 8683 })); 8684 } 8685} 8686 8687 8688 8689 8690 8691function generatePageId() { 8692const characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; 8693let result = ''; 8694const charactersLength = characters.length; 8695for (let i = 0; i < 8; i++) { 8696 result += characters.charAt(Math.floor(Math.random() * charactersLength)); 8697} 8698return result; 8699}; 8700 8701 8702function normalizeNameForComparison(name) { 8703if (!name) {
8704console.log("Empty name passed to normalization"); 8705return ''; 8706} 8707 8708console.log(`Normalizing name: "${name}"`); 8709 8710// Step 1: Convert to lowercase 8711let normalized = name.toLowerCase(); 8712console.log(`After lowercase: "${normalized}"`); 8713 8714// Step 2: Replace spaces with hyphens (without regex) 8715normalized = normalized.split(' ').join('-'); 8716console.log(`After space replacement: "${normalized}"`); 8717 8718// Step 3: Filter out unwanted characters (without regex) 8719let filtered = ''; 8720for (let i = 0; i < normalized.length; i++) { 8721const char = normalized.charAt(i); 8722// Keep only a-z, 0-9, and hyphens 8723if ((char >= 'a' && char <= 'z') || 8724 (char >= '0' && char <= '9') || 8725 char === '-') { 8726 filtered += char; 8727} 8728} 8729normalized = filtered; 8730console.log(`After character filtering: "${normalized}"`); 8731 8732// Step 4: Clean up multiple hyphens (without regex) 8733while (normalized.indexOf('--') !== -1) { 8734normalized = normalized.split('--').join('-'); 8735} 8736 8737// Step 5: Remove leading and trailing hyphens (without regex) 8738if (normalized.charAt(0) === '-') { 8739normalized = normalized.substring(1); 8740} 8741if (normalized.charAt(normalized.length - 1) === '-') { 8742normalized = normalized.substring(0, normalized.length - 1); 8743} 8744 8745// Check if the result is valid 8746if (!normalized || normalized === '-') { 8747console.log(`WARNING: Normalization produced invalid result: "${normalized}"`); 8748} else { 8749console.log(`Final normalized slug: "${normalized}"`); 8750} 8751 8752return normalized; 8753} 8754 8755function ensureUniqueSlug(slug, existingGalleries) { 8756if (!slug) return 'gallery'; 8757 8758let finalSlug = slug; 8759let counter = 1; 8760 8761// Check if slug exists in any gallery 8762const slugExists = function(s) { 8763for (let i = 0; i < existingGalleries.length; i++) { 8764 if (existingGalleries[i].slug === s) { 8765 return true; 8766 } 8767} 8768return false; 8769}; 8770 8771// Keep incrementing counter until unique 8772while (slugExists(finalSlug)) { 8773finalSlug = slug + '-' + counter; 8774counter++; 8775} 8776 8777return finalSlug; 8778} 8779 8780/** 8781 * Gathers the current complete site configuration from the client-side state 8782 * and triggers a download of the data as a single JSON file. 8783 * This provides a simple way to back up or export the current site structure. 8784 */ 8785function downloadCurrentSiteJSON() { 8786 try { 8787 console.log('Gathering current site data for download...'); 8788 8789 // Get the current hostname to use in the filename. 8790 const hostname = window.location.hostname.replace('www.', ''); 8791 8792 // --- FIX: Ensure we get the most up-to-date menu styles --- 8793 // The MenuStyleCustomizer.settings object is the most reliable source 8794 // for the current styles being used on the client. 8795 const currentMenuStyles = window.MenuStyleCustomizer?.settings || {}; 8796 8797 // Get current galleries 8798 const currentGalleries = window.galleries || galleries || []; 8799 8800 // --- STREAMLINED: Process galleries without including image data --- 8801 const enhancedGalleries = currentGalleries.map(gallery => { 8802 // If this is a Classic collection gallery, add note about manual retrieval 8803 if (gallery.galleryOptions?.manualCollectionName?.startsWith('GUID=')) { 8804 const classicGuid = gallery.galleryOptions.manualCollectionName.substring(5); 8805 console.log(`Processing Classic collection gallery: ${gallery.title} (GUID: ${classicGuid})`); 8806 8807 return { 8808 ...gallery, 8809 _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`, 8810 _classicGuid: classicGuid, 8811 _requiresClassicData: true 8812 }; 8813 } 8814 8815 return gallery; 8816 }); 8817 8818 // Assemble the complete site configuration object from global state variables. 8819 // This structure mirrors what the `save-config` API endpoint expects, ensuring 8820 // the exported file is a complete and re-importable replica. 8821 const siteData = { 8822 // The core menu/page structure with enhanced Classic collection data. 8823 galleries: enhancedGalleries, 8824 8825 // Admin emails are managed server-side for security. We export the list if it's 8826 // available on the client from a previous check, but the server remains the 8827 // source of truth. A re-import will not overwrite the admin list. 8828 adminEmails: window.siteConfig?.adminEmails || [], 8829 8830 // The unique ID for the site. 8831 siteId: window.siteId || window.Parameters?.siteId || '', 8832 8833 // Custom styling rules for the menu. 8834 menuStyles: currentMenuStyles, 8835 8836 // All elements configured to appear in the sidebar. 8837 sidebarElements: window.SidebarManager?.elements || [], 8838 8839 // Global site metadata for SEO and analytics. 8840 siteMetadata: window.siteMetadata || {}, 8841 8842 // Add metadata about Classic collections that need manual data retrieval 8843 _classicCollections: enhancedGalleries 8844 .filter(g => g._requiresClassicData) 8845 .map(g => ({ 8846 title: g.title, 8847 guid: g._classicGuid, 8848 note: g._note 8849 })) 8850 }; 8851 8852 console.log('Enhanced site data collected for export:', siteData); 8853 8854 // Convert the JavaScript object to a formatted JSON string. 8855 const jsonString = JSON.stringify(siteData, null, 2); 8856 8857 // Create a Blob, which is a file-like object of immutable, raw data. 8858 const blob = new Blob([jsonString], { type: 'application/json' }); 8859 8860 // Create a temporary link element to trigger the browser's download functionality. 8861 const url = URL.createObjectURL(blob); 8862 const a = document.createElement('a'); 8863 a.href = url; 8864 a.download = `${hostname}-config.json`; // e.g., 'yoursite.com-config.json' 8865 document.body.appendChild(a); 8866 a.click(); 8867 8868 // Clean up by removing the temporary link and revoking the object URL to free up memory. 8869 document.body.removeChild(a); 8870 URL.revokeObjectURL(url); 8871 8872 console.log('Enhanced JSON download initiated successfully.'); 8873 8874 // Show a helpful message about Classic collections 8875 const classicCount = siteData._classicCollections?.length || 0; 8876 8877 if (classicCount > 0) { 8878 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
8878need to manually fetch the Classic collection JSON files.`); 8879 } 8880 8881 } catch (error) { 8882 console.error('Error creating JSON download:', error); 8883 alert('An error occurred while preparing the download. Please check the console.'); 8884 } 8885} 8886 8887// Build Hydra Classic-style NDJSON (first line: metadata; second line: imageMetadata array) 8888function buildHydraClassicNdjson(classicGuid, images) { 8889 const header = { 8890 title: 'Untitled', 8891 description: '', 8892 version: '1', 8893 isClassic: true, 8894 isClassicCollection: true, 8895 classicGuid: 'GUID=' + String(classicGuid || '') 8896 }; 8897 8898 const imageArray = Array.isArray(images) ? images.map(function(img, idx) { 8899 var path = ''; 8900 if (img) { 8901 path = img.image || img.link || img.originalUrl || (img.urls && img.urls.original) || ''; 8902 } 8903 // Derive filename base from any provided path/filename and normalize extension to .jpg 8904 var fnameBase = ''; 8905 if (typeof path === 'string' && path.length > 0) { 8906 var slash = path.lastIndexOf('/'); 8907 var raw = slash >= 0 ? path.substring(slash + 1) : path; 8908 // remove query/hash 8909 var q = raw.indexOf('?'); if (q >= 0) raw = raw.substring(0, q); 8910 var h = raw.indexOf('#'); if (h >= 0) raw = raw.substring(0, h); 8911 // strip extension if present 8912 var dot = raw.lastIndexOf('.'); 8913 fnameBase = dot > 0 ? raw.substring(0, dot) : raw; 8914 } else if (img && typeof img.filename === 'string') { 8915 var rf = img.filename; 8916 var q2 = rf.indexOf('?'); if (q2 >= 0) rf = rf.substring(0, q2); 8917 var h2 = rf.indexOf('#'); if (h2 >= 0) rf = rf.substring(0, h2); 8918 var d2 = rf.lastIndexOf('.'); 8919 fnameBase = d2 > 0 ? rf.substring(0, d2) : rf; 8920 } 8921 var ext = (img && img.metadata && typeof img.metadata.extension === 'string' && img.metadata.extension.startsWith('.')) 8922 ? img.metadata.extension : '.jpg'; 8923 var filename = fnameBase ? (fnameBase + ext) : ''; 8924 // Build CDN image URL using site GUID: https://cdn.neonsky.app/{siteGuid}/images/{filename} 8925 // Note: classicGuid is a site GUID, not an image GUID - it identifies the site, and images are stored under that site's path 8926 var guidRaw = String(classicGuid || '').replace(/^GUID=/, ''); 8927 var cdnImageUrl = filename && guidRaw ? ('https://cdn.neonsky.app/' + guidRaw + '/images/' + filename) : path; 8928 var rec = { 8929 caption: (img && (img.caption || img.description)) || '', 8930 description: (img && (img.description || img.caption)) || '', 8931 isPreview: idx === 1, 8932 title: (img && img.title) || '', 8933 'alt-text': (img && (img['alt-text'] || img.altText || '')) || '', 8934 image: cdnImageUrl || '', 8935 link: '', 8936 'link-text': '', 8937 filename: filename, 8938 isClassic: true, 8939 isClassicCollection: true, 8940 classicGuid: 'GUID=' + String(classicGuid || '') 8941 }; 8942 return rec; 8943 }) : []; 8944 8945 // Build NDJSON as: header line, imageMetadata array line, then one line per full image record 8946 var body = { imageMetadata: imageArray }; 8947 var out = [JSON.stringify(header), JSON.stringify(body)]; 8948 for (var k = 0; k < imageArray.length; k++) { 8949 out.push(JSON.stringify(imageArray[k])); 8950 } 8951 return out.join('\n') + '\n'; 8952} 8953 8954async function uploadNdjsonToTigris(siteId, pageId, ndjsonContent) { 8955 // Dual upload strategy: Fly.io (uncompressed) + Irys (compressed if <800KB) 8956 const fileName = String(siteId || '') + '_' + String(pageId || '') + '.ndjson'; 8957 const flyUrl = 'https://hydra-press-v2.fly.dev/publish'; 8958 const irysUrl = 'https://hydra-press-v2.fly.dev/upload-irys'; 8959 8960 console.error('Starting dual NDJSON upload for:', fileName); 8961 console.error('Content length:', ndjsonContent.length, 'characters'); 8962 8963 // COMPRESSION: Compress for Irys only (not Tigris) 8964 console.error('ð Compressing NDJSON with gzip for Irys...'); 8965 const encoder = new TextEncoder(); 8966 const ndjsonBytes = encoder.encode(ndjsonContent); 8967 const compressedStream = new Response(ndjsonBytes).body.pipeThrough(new CompressionStream('gzip')); 8968 const compressedBlob = await new Response(compressedStream).blob(); 8969 const compressedBytes = new Uint8Array(await compressedBlob.arrayBuffer()); 8970 8971 const originalSize = ndjsonBytes.length; 8972 const compressedSize = compressedBytes.length; 8973 const compressionRatio = ((1 - compressedSize / originalSize) * 100).toFixed(2); 8974 8975 console.error('ð Compression results:', { 8976 original: originalSize + ' bytes (' + (originalSize / 1024).toFixed(2) + ' KB)', 8977 compressed: compressedSize + ' bytes (' + (compressedSize / 1024).toFixed(2) + ' KB)', 8978 ratio: compressionRatio + '%' 8979 }); 8980 8981 const IRYS_SIZE_LIMIT = 800 * 1024; // 800KB limit for compressed Irys upload 8982 const shouldUploadToIrys = compressedSize < IRYS_SIZE_LIMIT; 8983 8984 console.error('Irys upload eligible: ' + shouldUploadToIrys + ' (compressed: ' + (compressedSize / 1024).toFixed(2) + ' KB)'); 8985 8986 // STEP 1: Upload to Irys (non-blocking, compressed if <800KB) 8987 let irysResult = null; 8988 if (shouldUploadToIrys) { 8989 try { 8990 console.error('ð Starting Irys upload (compressed, non-blocking)...'); 8991 8992 // Convert compressed bytes to base64 8993 const compressedBase64 = btoa(String.fromCharCode(...compressedBytes)); 8994 8995 const irysResp = await fetch(irysUrl, { 8996 method: 'POST', 8997 headers: { 'content-type': 'application/json' }, 8998 body: JSON.stringify({ 8999 compressedData: compressedBase64, 9000 originalSize: originalSize, 9001 compressedSize: compressedSize, 9002 siteId: siteId, 9003 pageId: pageId 9004 }) 9005 }); 9006 9007 if (irysResp.ok) { 9008 irysResult = await irysResp.json(); 9009 console.error('â Irys upload successful:', irysResult.txId); 9010 console.error(' Irys Gateway:', irysResult.urls?.irys); 9011 console.error(' ar.io Gateway:', irysResult.urls?.arweave); 9012 9013 // Log wallet balance and cost 9014 if (irysResult.walletBalance !== undefined) { 9015 console.error('ð° Wallet Balance:', irysResult.walletBalance.toFixed(6), 'AR'); 9016 } 9017 if (irysResult.uploadCost !== undefined) { 9018 const costUSD = (irysResult.uploadCost * 20).toFixed(4); 9019 if (irysResult.uploadCost === 0) { 9020 console.error('ð¸ Upload Cost: FREE (0 AR)'); 9021 } else { 9022 console.error('ð¸ Upload Cost:', irysResult.uploadCost.toFixed(6), 'AR (~$' + costUSD + ' USD at $20/AR)'); 9023 } 9024 } 9025 } else { 9026 const errorText = await irysResp.text().catch(() => 'Unknown error'); 9027 console.warn('â ï¸ Irys upload failed (non-critical):', irysResp.status, errorText); 9028 } 9029 } catch (irysError) { 9030 console.warn('â ï¸ Irys upload error (non-critical):', irysError.message); 9031 // Continue - Irys failure doesn't block Fly.io upload 9032 } 9033 } else { 9034 console.error('âï¸ Skipping Irys upload (compressed size exceeds 800KB limit)'); 9035 } 9036 9037 // STEP 2: Upload to Fly.io (CRITICAL - must succeed, uncompressed) 9038 // Hydra sites require Bearer auth - get token from TokenManager/localStorage (same as Edition Publisher) 9039 try { 9040 console.error('ð Starting Fly.io upload (uncompressed)...'); 9041 const flyHeaders = { 'content-type': 'application/json' }; 9042 let authToken = null; 9043 try { 9044 if (window.TokenManager) { 9045 if (typeof window.TokenManager.getHydraAuthToken === 'function') { 9046 try { authToken = window.TokenManager.getHydraAuthToken(); } catch (e) {} 9047 }
9048 if (!authToken && typeof window.TokenManager.getBestToken === 'function') { 9049 try { 9050 const tw = window.TokenManager.getBestToken(); 9051 authToken = tw && tw.startsWith('Bearer ') ? tw.substring(7) : tw; 9052 } catch (e) {} 9053 } 9054 } 9055 if (!authToken) authToken = localStorage.getItem('hydra_auth_token'); 9056 if (!authToken) authToken = window.authToken; 9057 if (authToken) { 9058 flyHeaders['Authorization'] = authToken.includes('.') || authToken.startsWith('hydra:') 9059 ? 'Bearer ' + authToken 9060 : (authToken.startsWith('Bearer ') ? authToken : 'Bearer hydra:' + authToken); 9061 console.error('ð Fly.io auth: token attached'); 9062 } else { 9063 console.warn('â ï¸ No auth token - Fly.io may reject Hydra site upload (401)'); 9064 } 9065 } catch (authErr) { 9066 console.warn('â ï¸ Auth token lookup failed:', authErr.message); 9067 } 9068 const flyResp = await fetch(flyUrl, { 9069 method: 'POST', 9070 headers: flyHeaders, 9071 body: JSON.stringify({ ndjsonContent, dataFileName: fileName }) 9072 }); 9073 9074 console.log('Server response status:', flyResp.status); 9075 9076 if (!flyResp.ok) { 9077 const errorText = await flyResp.text().catch(() => 'Unknown error'); 9078 console.error('â Fly.io upload failed', flyResp.status, 'for', fileName + ':', errorText); 9079 if (flyResp.status === 401) { 9080 console.error('ð¡ Tip: Open the Edition Publisher (or log in as admin) first to authenticate for Fly.io uploads.'); 9081 } 9082 return { success: false, irysResult: null }; 9083 } 9084 9085 const flyData = await flyResp.json().catch(() => null); 9086 if (flyData && flyData.urls) { 9087 console.log('â Fly.io upload successful:', flyData.urls); 9088 console.log(' Tigris URL:', flyData.urls.urlTigris); 9089 9090 // Return comprehensive result 9091 const result = { 9092 success: true, 9093 flyUrls: flyData.urls, 9094 irysResult: irysResult, 9095 uploadedTo: irysResult ? ['fly', 'irys'] : ['fly'], 9096 fileSize: compressedSize, // Irys compressed size 9097 originalFileSize: originalSize // Original uncompressed size 9098 }; 9099 9100 console.log('ð¦ Upload complete:', { 9101 destinations: result.uploadedTo.join(' + '), 9102 txId: irysResult?.txId || 'N/A', 9103 flyUrl: flyData.urls.urlTigris 9104 }); 9105 9106 return result; 9107 } else { 9108 console.warn('Fly.io upload response missing URLs:', flyData); 9109 return { success: false, irysResult: null }; 9110 } 9111 } catch (flyError) { 9112 console.error('â Fly.io upload error for', fileName + ':', flyError); 9113 return { success: false, irysResult: null }; 9114 } 9115} 9116 9117async function importClassicGalleries() { 9118 // Browser-scoped helper: mirrors server-side behavior with escaped patterns for template-safe JS 9119 function cleanDescriptionLeadingLines(text) { 9120 if (!text || typeof text !== 'string') return text; 9121 9122 let result = text; 9123 9124 // Remove leading empty TEXTFORMAT blocks: <TEXTFORMAT><P><FONT></FONT></P></TEXTFORMAT> 9125 result = result.replace( 9126 new RegExp( 9127 '^(\s*\<TEXTFORMAT[^\>]*\>\s*\<P[^\>]*\>\s*\<FONT[^\>]*\>\s*\<\/FONT\>\s*\<\/P\>\s*\<\/TEXTFORMAT\>\s*)+', 9128 'i' 9129 ), 9130 '' 9131 ); 9132 9133 // Remove leading empty P tags 9134 result = result.replace( 9135 new RegExp('^(\s*\<P[^\>]*\>\s*\<\/P\>\s*)+', 'i'), 9136 '' 9137 ); 9138 9139 // Remove leading empty FONT tags 9140 result = result.replace( 9141 new RegExp('^(\s*\<FONT[^\>]*\>\s*\<\/FONT\>\s*)+', 'i'), 9142 '' 9143 ); 9144 9145 // Remove leading BR tags 9146 result = result.replace( 9147 new RegExp('^(\s*\<br\s*\/\?>\s*)+', 'i'), 9148 '' 9149 ); 9150 9151 // Remove any remaining leading whitespace 9152 return result.replace(new RegExp('^\s+', 'i'), '').trim(); 9153 } 9154 9155 // Browser-scoped helper: fix multi-line links (template-safe escaped regex) 9156 function fixMultilineLinks(text) { 9157 if (!text || typeof text !== 'string') return text; 9158 return text.replace( 9159 new RegExp('<a[^>]*>.*?Click.*?<\/a><a[^>]*>.*?HERE.*?<\/a><a[^>]*>.*?to Add to Cart.*?<\/a>', 'gis'), 9160 function (match) { 9161 const first = match.match(new RegExp('<a href="([^"]*)" target="([^"]*)"', 'i')); 9162 if (first) { 9163 return '<a href="' + first[1] + '" target="' + first[2] + '">
9163Click HERE to Add to Cart</a>'; 9164 } 9165 return match; 9166 } 9167 ); 9168 } 9169 9170 // Browser-scoped helper: convert plain text link phrase to HTML link 9171 function convertPlainTextLinks(text) { 9172 if (!text || typeof text !== 'string') return text; 9173 return text.replace( 9174 new RegExp('Click HERE to Add to Cart', 'g'), 9175 '<a href="#" target="_blank">Click HERE to Add to Cart</a>' 9176 ); 9177 } 9178 // Get input elements, including the new checkbox 9179 const guidInputEl = document.getElementById('classicGuid'); 9180 const pastedJsonEl = document.getElementById('pastedJson'); 9181 const parentIdInputEl = document.getElementById('importParent'); 9182 const createSubmenuCheckboxEl = document.getElementById('createSubmenu'); 9183 const submenuTitleInputEl = document.getElementById('submenuTitle'); 9184 const replaceMenuCheckboxEl = document.getElementById('replaceMenuCheckbox'); 9185 9186 // Get values from inputs 9187 const guidInput = guidInputEl instanceof HTMLInputElement ? guidInputEl.value.trim() : ''; 9188 const pastedJson = pastedJsonEl instanceof HTMLTextAreaElement ? pastedJsonEl.value.trim() : ''; 9189 const replaceMenu = replaceMenuCheckboxEl instanceof HTMLInputElement ? replaceMenuCheckboxEl.checked : false; 9190 9191 let parentId = parentIdInputEl instanceof HTMLSelectElement && parentIdInputEl.value ? parseInt(parentIdInputEl.value) : null; 9192 let createSubmenu = createSubmenuCheckboxEl instanceof HTMLInputElement ? createSubmenuCheckboxEl.checked : false; 9193 let submenuTitle = submenuTitleInputEl instanceof HTMLInputElement ? submenuTitleInputEl.value.trim() : ''; 9194 9195 // If replacing the menu, ignore parent/submenu settings 9196 if (replaceMenu) { 9197 parentId = null; 9198 createSubmenu = false; 9199 } 9200 9201 // Validate submenu title if checkbox is checked 9202 if (createSubmenu && !submenuTitle) { 9203 alert('Please provide a title for the submenu'); 9204 return; 9205 } 9206 9207 // Show status display 9208 const statusEl = document.getElementById('importStatus'); 9209 const progressBarEl = statusEl ? statusEl.querySelector('.progress-bar') : null; 9210 const statusMessageEl = statusEl ? statusEl.querySelector('.status-message') : null; 9211 const progressBar = progressBarEl instanceof HTMLElement ? progressBarEl : null; 9212 const statusMessage = statusMessageEl instanceof HTMLElement ? statusMessageEl : null; 9213 9214 if (statusEl && progressBar && statusMessage) { 9215 statusEl.style.display = 'block'; 9216 statusEl.classList.add('importing'); 9217 progressBar.style.width = '10%'; 9218 statusMessage.textContent = 'Preparing to import...'; 9219 } 9220 9221 try {
9222 let classicData; 9223 let classicGuid; // Will hold the primary GUID for image path lookups 9224 9225 // --- LOGIC TO GET DATA: Prioritize Pasted JSON --- 9226 if (pastedJson) { 9227 if (statusMessage) statusMessage.textContent = 'Parsing pasted JSON...'; 9228 classicData = JSON.parse(pastedJson); 9229 9230 if (!classicData.galleries || !Array.isArray(classicData.galleries)) { 9231 throw new Error('Pasted JSON is invalid: "galleries" array not found.'); 9232 } 9233 // Use the first available GUID from the pasted data as the default for images 9234 classicGuid = classicData.galleries[0]?.classicGuid || classicData.siteId; 9235 9236 } else if (guidInput) { 9237 // Check if this is the new import_ format 9238 if (guidInput.startsWith('import_')) { 9239 if (statusMessage) statusMessage.textContent = 'Fetching data via import_ format...'; 9240 const importGuid = guidInput.substring(7); // Remove 'import_' prefix 9241 9242 const jsonUrl = `https://hydra.neonsky.app/json-imports/${guidInput}.json`; 9243 const response = await fetch(jsonUrl); 9244 if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`); 9245 9246 classicData = await response.json(); 9247 9248 if (!classicData.galleries || !Array.isArray(classicData.galleries)) {
9249 throw new Error('Fetched import JSON is invalid: "galleries" array not found.'); 9250 } 9251 9252 // HYBRID APPROACH: Also fetch the original GUID JSON for image metadata 9253 if (statusMessage) statusMessage.textContent = 'Fetching image metadata from original JSON...'; 9254 9255 const originalJsonUrl = `https://cdn.neonsky.app/sites:site_${importGuid}.json`; 9256 9257 try { 9258 const originalResponse = await fetch(originalJsonUrl); 9259 if (originalResponse.ok) { 9260 const originalData = await originalResponse.json(); 9261 9262 // Get the default language from site configuration 9263 let defaultLanguageID = 'en'; // fallback 9264 if (originalData.languages && Array.isArray(originalData.languages)) { 9265 const defaultLang = originalData.languages.find(lang => lang.isDefault === true); 9266 if (defaultLang) { 9267 defaultLanguageID = defaultLang.id; 9268 } 9269 } 9270 9271 // Merge image metadata from original JSON into import JSON galleries 9272 if (originalData.galleries && Array.isArray(originalData.galleries)) { 9273 classicData.galleries = classicData.galleries.map(importGallery => { 9274 // Find matching gallery in original data by name or categoryID 9275 9276 const originalGallery = originalData.galleries.find(orig => 9277 orig.title === importGallery.title || // Primary match by title 9278 orig.name === importGallery.title || // Fallback: orig.name matches import.title 9279 orig.categoryID === importGallery.categoryID // Fallback: categoryID match 9280 ); 9281 9282 9283 if (originalGallery && originalGallery.images && Array.isArray(originalGallery.images)) { 9284 9285 // Process all images from original gallery with metadata extraction 9286 const processedImages = originalGallery.images.map(originalImage => { 9287 // Apply EXACT same metadata processing logic as Edition Publisher 9288 let title = originalImage.title || ''; 9289 let dateline = originalImage.dateline || ''; 9290 let caption = originalImage.caption || ''; 9291 9292 // Check languageVersions array for metadata (this is where most data is stored) 9293 if (originalImage.languageVersions && originalImage.languageVersions.length > 0) { 9294 // Look for site's default language first, then fall back to first available 9295 let langData = originalImage.languageVersions.find(lang => lang.languageID === defaultLanguageID); 9296 if (!langData) { 9297 langData = originalImage.languageVersions[0]; 9298 } 9299 9300 // For language-enabled JSON files, prioritize languageVersions data 9301 // Only fall back to top-level if languageVersions data is empty 9302 if (!title || title.trim() === '') title = langData.title || ''; 9303 if (!dateline || dateline.trim() === '') dateline = langData.dateline || ''; 9304 if (!caption || caption.trim() === '') caption = langData.caption || ''; 9305 } 9306 9307 // Convert Flash TEXTFORMAT to HTML (properly escaped for template literals) 9308 let description = caption; 9309 if (caption && caption.includes('TEXTFORMAT')) { 9310 try { 9311 // Use properly escaped HTML characters for template literal context 9312 description = caption 9313 .replace(/\<TEXTFORMAT[^\>]*\>/g, '') 9314 .replace(/\<\/TEXTFORMAT\>/g, '') 9315 .replace(/\<P[^\>]*\>/g, '\<p\>') 9316 .replace(/\<\/P\>/g, '\<\/p\>') 9317 .replace(/\<FONT[^\>]*\>/g, '') 9318 .replace(/\<\/FONT\>/g, '') 9319 .replace(/\<A HREF="([^"]*)"[^\>]*\>/g, '\<a href="$1" target="_blank"\>') 9320 .replace(/\<\/A\>/g, '\<\/a\>') 9321 .replace(/\<U\>/g, '\<u\>') 9322 .replace(/\<\/U\>/g, '\<\/u\>') 9323 .replace(/\<B\>/g, '\<strong\>') 9324 .replace(/\<\/B\>/g, '\<\/strong\>') 9325 .replace(/\<I\>/g, '\<em\>') 9326 .replace(/\<\/I\>/g, '\<\/em\>') 9327 .replace(/"/g, '"') 9328 .replace(/'/g, "'") 9329 .replace(/\s+/g, ' ') 9330 .trim(); 9331 } catch (error) { 9332 console.warn('TEXTFORMAT conversion failed, using original caption:', error); 9333 description = caption; 9334 } 9335 } 9336 9337 // Prepend dateline to description if it exists (convert to uppercase) 9338 if (dateline) { 9339 const uppercaseDateline = dateline.toUpperCase(); 9340 description = `${uppercaseDateline} ${description}`.trim(); 9341 } 9342 9343 // Apply description fixes 9344 description = cleanDescriptionLeadingLines(description); 9345 description = fixMultilineLinks(description); 9346 description = convertPlainTextLinks(description); 9347 9348 const imageEntry = { 9349 // Match EXACT structure from Edition Publisher hydraImageEntry 9350 image: originalImage.image || originalImage.urls?.original || '', 9351 caption: description, // Use processed description as caption 9352 description: description, // ALSO set description field (like Edition Publisher) 9353 title: title, 9354 'alt-text': title, // Use title as alt-text like Edition Publisher 9355 link: originalImage.urls?.original || '', 9356 'link-text': originalImage.urls?.original || '', 9357 isPreview: false, 9358 filename: originalImage.filename || '', 9359 originalUrl: originalImage.image || originalImage.urls?.original || '', 9360 dateline: dateline 9361 }; 9362 9363 return imageEntry; 9364 }); 9365 9366 return { 9367 ...importGallery, // Keep import gallery structure (order, settings, etc.) 9368 images: processedImages // Replace with processed original images 9369 }; 9370 } 9371 return importGallery; 9372 }); 9373 } 9374 } else { 9375 console.warn('Could not fetch original JSON for metadata, using import data only'); 9376 } 9377 } catch (error) { 9378 console.warn('Error fetching original JSON for metadata:', error); 9379 } 9380 9381 // Extract original GUID for image path lookups 9382 // If importGuid has a suffix (e.g., "4bd5ebf7bda9d-all"), extract the base GUID 9383 // GUIDs are typically 13 characters, so split on common separators 9384 let baseGuid = importGuid; 9385 const guidMatch = importGuid.match(/^([a-f0-9]{13})(?:-.*)?$/i); 9386 if (guidMatch) { 9387 baseGuid = guidMatch[1]; 9388 } else { 9389 // Fallback: try to extract first 13-character hex string 9390 const hexMatch = importGuid.match(/^([a-f0-9]{13})/i); 9391 if (hexMatch) { 9392 baseGuid = hexMatch[1]; 9393 } 9394 } 9395 9396 // Use the base GUID for image path lookups (e.g., /images/filename.jpg) 9397 classicGuid = baseGuid; 9398 console.log('Successfully loaded hybrid import data with import GUID:', importGuid); 9399 console.log('Using base GUID for image paths:', classicGuid); 9400 9401 } else { 9402 // Standard GUID format - existing logic 9403 if (statusMessage) statusMessage.textContent = 'Fetching data via GUID...';
9404 classicGuid = guidInput.startsWith('GUID=') ? guidInput.substring(5) : guidInput; 9405 console.log('Detected standard GUID format, GUID:', classicGuid); 9406 9407 const jsonUrl = `https://storage.neonsky.app/sites:site_${classicGuid}.json`; 9408 console.log('Fetching classic JSON from:', jsonUrl); 9409 const response = await fetch(jsonUrl); 9410 9411 if (!response.ok) throw new Error(`HTTP error! status: ${response.status} for ${jsonUrl}`); 9412 classicData = await response.json(); 9413 9414 if (!classicData.galleries || !Array.isArray(classicData.galleries)) { 9415 throw new Error('Fetched JSON is invalid: "galleries" array not found.'); 9416 } 9417 9418 console.log('Successfully loaded classic data with GUID:', classicGuid); 9419 } 9420 9421 } else { 9422 alert('Please either paste site JSON or enter a Classic GUID to import.'); 9423 if (statusEl) statusEl.style.display = 'none'; 9424 return; 9425 } 9426 9427 const galleriesForImport = classicData.galleries; 9428 if (statusMessage) statusMessage.textContent = `Found ${galleriesForImport.length} items to import...`; 9429 9430 // import_ format: galleries come from DB/import JSON, not classic site - treat as new Hydra NDJSON galleries 9431 const isImportFormat = !!(guidInput && guidInput.startsWith('import_')); 9432 9433 // --- HIERARCHY RECONSTRUCTION LOGIC --- 9434 9435 const currentSiteId = window.Parameters?.siteId || siteId; 9436 let submenuParentId = parentId; 9437 const tempNewItems = []; 9438 const originalIdToNewIdMap = {}; 9439 9440 // Create the optional top-level submenu if requested 9441 if (createSubmenu) { 9442 const submenuItem = { 9443 id: Date.now(), 9444 title: submenuTitle, 9445 isSubmenu: true, 9446 isCollapsed: true, 9447 visible: true, 9448 parentId: parentId, 9449 siteId: currentSiteId, 9450 children: [] 9451 }; 9452 galleries.push(submenuItem); 9453 submenuParentId = submenuItem.id; 9454 } 9455 9456 // --- Pass 1: Create new items with correct data and map old IDs to new IDs --- 9457 for (let index = 0; index < galleriesForImport.length; index++) { 9458 const classicItem = galleriesForImport[index]; 9459 const newItemId = Date.now() + index + 100; 9460 originalIdToNewIdMap[classicItem.id] = newItemId; 9461 9462 if (progressBar) progressBar.style.width = `${40 + Math.round(index * 50 / galleriesForImport.length)}%`; 9463 if (statusMessage) statusMessage.textContent = `Processing item ${index + 1} of ${galleriesForImport.length}...`; 9464 9465 const title = classicItem.title || classicItem.languageVersions?.[0]?.title || classicItem.name || `Item ${classicItem.categoryID}`; 9466 9467 let slug; 9468 // If pasting from JSON, *always* regenerate the slug from the title to ensure it's clean. 9469 // Otherwise (for GUID import), trust the existing slug if it's there. 9470 if (pastedJson) { 9471 slug = normalizeNameForComparison(title); 9472 } else if (classicItem.slug && classicItem.slug.trim() !== '') { 9473 slug = classicItem.slug; 9474 } else { 9475 slug = normalizeNameForComparison(title); 9476 } 9477const finalSlug = ensureUniqueSlug(slug || 'gallery', [...galleries, ...tempNewItems]); // Ensure uniqueness 9478 9479 // Determine the GUID to use for this specific item's image paths 9480 const itemGuid = classicItem.classicGuid || classicGuid; 9481 9482 let newItem; 9483 9484 // --- Check if it's an External URL --- 9485 if (classicItem.isExternal === true && classicItem.url) { 9486 console.log(`Creating external URL menu item: "${title}" -> ${classicItem.url}`); 9487 newItem = { 9488 id: newItemId, 9489 title, 9490 url: classicItem.url, 9491 isExternal: true, 9492 visible: classicItem.visible !== false, 9493 parentId: null, 9494 siteId: currentSiteId, 9495 slug: finalSlug, 9496 importSource: 'external-url' 9497 }; 9498 } 9499 // --- Check if it's a Page and build its elements --- 9500 else if (classicItem.isPage === true || classicItem.type === 3 || classicItem.behaviorID === 3) { 9501 const pageUniqueId = `page_${newItemId}`; 9502 9503 let pageElements = []; 9504 9505 // ================================================================= 9506 // ENHANCED: Handle pageElements for both pasted JSON and import_guid.json format 9507 // ================================================================= 9508 if (classicItem.pageElements && Array.isArray(classicItem.pageElements)) { 9509 console.log(`Using existing pageElements for page: "${title}"`); 9510 9511 // Check if we have both pageElements and images - create two-column layout 9512 if (classicItem.images && classicItem.images.length > 0) { 9513 console.log(`Creating two-column layout for page: "${title}" with ${classicItem.images.length} images`); 9514 9515 // Create metadata element 9516 pageElements.push({ 9517 id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0, 9518 metaTitle: classicItem.meta?.title || title, 9519 metaDescription: classicItem.meta?.description || "", 9520 metaKeywords: classicItem.meta?.keywords || "" 9521 }); 9522 9523 // Create two-column container 9524 const leftColumnElements = []; 9525 const rightColumnElements = []; 9526 9527 // Add images to left column 9528 const slides = classicItem.images.map((img) => { 9529 const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg'); 9530 const imageKey = `${itemGuid}/images/${imageFilename}`; 9531 const imageUrl = `https://storage.neonsky.app/${imageKey}`; 9532 9533 return { 9534 imageUrl, 9535 imageKey, 9536 imageFilename, 9537 imageType: img.metadata?.mimetype || 'image/jpeg', 9538 caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))), 9539 title: img.title || '', 9540 byline: img.byline || '', 9541 dateline: img.dateline || '', 9542 altText: img.metadata?.alt_text || '' 9543 }; 9544 }); 9545 9546 leftColumnElements.push({ 9547 id: newItemId + 3100, type: "slideshow", title: "Page Images", visible: true, position: 0, 9548 slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true 9549 }); 9550 9551 // Add text content to right column
9552 classicItem.pageElements.forEach((el, elIndex) => { 9553 if (el.type === 'text' && el.content) { 9554 rightColumnElements.push({ 9555 id: newItemId + 3200 + elIndex, type: "text", title: "Text Block", visible: true, position: elIndex, 9556 textContent: el.content, textWidth: 70 9557 }); 9558 } 9559 }); 9560 9561 const columns = [ 9562 { id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements }, 9563 { id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements } 9564 ]; 9565 9566 pageElements.push({ 9567 id: newItemId + 3000, type: "column-container", title: "Two Column Layout", visible: true, position: 1, 9568 columns, 9569 backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false, showSingleRandomImage: false, randomStart: false }, 9570 backgroundVideo: { enabled: false, videoUrl: '', videoUrls: [], randomizeVideo: false, videoHeight: 50 } 9571 }); 9572 9573 } else { 9574 // No images, just transform the pageElements as before 9575 pageElements = classicItem.pageElements.map((el, elIndex) => { 9576 const elementId = Date.now() + index + 1000 + elIndex; 9577 9578 // Handle import_guid.json format (type + content) 9579 if (el.type && el.content) { 9580 if (el.type === 'text') { 9581 return { 9582 id: elementId, 9583 type: 'text', 9584 title: 'Text Block', 9585 visible: true, 9586 position: elIndex, 9587 textContent: el.content, 9588 textWidth: 100 9589 }; 9590 } else if (el.type === 'metadata') { 9591 return { 9592 id: elementId, 9593 type: 'metadata', 9594 title: 'Metadata', 9595 visible: true, 9596 position: elIndex, 9597 metaTitle: el.metaTitle || title, 9598 metaDescription: el.metaDescription || '', 9599 metaKeywords: el.metaKeywords || '' 9600 }; 9601 } 9602 } 9603 9604 // Handle existing format (already has id, position, etc.) 9605 return { 9606 ...el, 9607 id: elementId, 9608 position: el.position !== undefined ? el.position : elIndex 9609 }; 9610 }); 9611 } 9612 9613 } else { 9614 // Otherwise (for GUID-based import), generate the page structure from scratch. 9615 console.log(`Generating new pageElements for page: "${title}"`); 9616 pageElements.push({ 9617 id: newItemId + 1000, type: "metadata", title: "Metadata", visible: true, position: 0, 9618 metaTitle: classicItem.meta?.title || title, 9619 metaDescription: classicItem.meta?.description || "", 9620 metaKeywords: classicItem.meta?.keywords || "" 9621 }); 9622 9623 // Add images if they exist in the page 9624 if (classicItem.images && classicItem.images.length > 0) { 9625 const slides = classicItem.images.map((img) => { 9626 const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg'); 9627 const imageKey = `${itemGuid}/images/${imageFilename}`; 9628 const imageUrl = `https://storage.neonsky.app/${imageKey}`; 9629 9630 return { 9631 imageUrl, 9632 imageKey, 9633 imageFilename, 9634 imageType: img.metadata?.mimetype || 'image/jpeg', 9635 caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || ''))), 9636 title: img.title || '', 9637 byline: img.byline || '', 9638 dateline: img.dateline || '', 9639 altText: img.metadata?.alt_text || '' 9640 }; 9641 }); 9642 9643 pageElements.push({ 9644 id: newItemId + 1500, type: "slideshow", title: "Page Images", visible: true, position: 1, 9645 slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 100, slideshowHeight: 60, showFullImages: true 9646 }); 9647 } 9648 9649 pageElements.push({ 9650 id: newItemId + 2000, type: "text", title: "Text Block Spacer", visible: true, position: 2, 9651 textContent: "<p> </p><p><br></p>", textWidth: 100 9652 }); 9653 9654 const columns = []; 9655 const leftColumnElements = []; 9656 const rightColumnElements = []; 9657 9658 if (classicItem.images && classicItem.images.length > 0) { 9659 const slides = classicItem.images.map((img) => { 9660 const imageFilename = (img.filename || `image_${Date.now()}`) + (img.metadata?.extension || '.jpg'); 9661 const imageKey = `${itemGuid}/images/${imageFilename}`; 9662 const imageUrl = `https://storage.neonsky.app/${imageKey}`; 9663 9664 // Preserve all image metadata including captions, titles, etc. 9665 return { 9666 imageUrl, 9667 imageKey, 9668 imageFilename, 9669 imageType: img.metadata?.mimetype || 'image/jpeg', 9670 // Preserve caption information - handle both direct and language version storage patterns 9671 caption: cleanDescriptionLeadingLines(fixMultilineLinks(convertPlainTextLinks(img.caption || img.languageVersions?.[0]?.caption || ''))), 9672 title: img.title || img.languageVersions?.[0]?.title || '', 9673 byline: img.byline || img.languageVersions?.[0]?.byline || '', 9674 dateline: img.dateline || img.languageVersions?.[0]?.dateline || '', 9675 altText: img.metadata?.alt_text || '', 9676 // Preserve any additional metadata 9677 metaTitle: img.metadata?.meta_title || '', 9678 metaDescription: img.metadata?.meta_description || '', 9679 metaKeywords: img.metadata?.meta_keywords || '', 9680 // Preserve gallery-specific data 9681 sortOrder: img.galleryData?.sortOrder || 0, 9682 linkURL: img.galleryData?.linkURL || null, 9683 linkTarget: img.galleryData?.linkTarget || '_self' 9684 }; 9685 }); 9686 leftColumnElements.push({ 9687 id: newItemId + 3100, type: "slideshow", title: "Image Slideshow", visible: true, position: 0, 9688 slides, slideDuration: 5000, transitionDuration: 500, slideshowWidth: 88, slideshowHeight: 50, showFullImages: true 9689 }); 9690 } 9691 9692 let rawDetails = classicItem.details || classicItem.languageVersions?.[0]?.details || ""; 9693 if (rawDetails && typeof rawDetails === 'string') { 9694 // ================================================================= 9695 // CHANGE 2: Fix for "Invalid regular expression flags" error. 9696 // Changed from /.../gi literal to new RegExp('...', 'gi') constructor 9697 // to prevent syntax errors. 9698 // ================================================================= 9699 let processedText = rawDetails.replace(new RegExp('<TEXTFORMAT.*?>', 'gi'), '').replace(new RegExp('</TEXTFORMAT>', 'gi'), ''); 9700 processedText = processedText.replace(new RegExp('<P ALIGN=".*?"', 'gi'), '<p').replace(new RegExp('</P>', 'gi'), '</p>'); 9701 processedText = processedText.replace(new RegExp('<FONT.*?>', 'gi'), '').replace(new RegExp('</FONT>', 'gi'), ''); 9702 processedText = processedText.replace(new RegExp('<B>
9702', 'gi'), '<strong>').replace(new RegExp('</B>', 'gi'), '</strong>'); 9703 processedText = processedText.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&'); 9704 9705 // Apply description fixes to processed text 9706 processedText = cleanDescriptionLeadingLines(processedText); 9707 processedText = fixMultilineLinks(processedText); 9708 processedText = convertPlainTextLinks(processedText); 9709 9710 rightColumnElements.push({ 9711 id: newItemId + 3200, type: "text", title: "Text Block Content", visible: true, position: 0, 9712 textContent: processedText, textWidth: 70 9713 }); 9714 } 9715 9716 columns.push({ id: newItemId + 3300, hAlign: "right", vAlign: "middle", elements: leftColumnElements }); 9717 columns.push({ id: newItemId + 3400, hAlign: "left", vAlign: "middle", elements: rightColumnElements }); 9718 9719 pageElements.push({ 9720 id: newItemId + 3000, type: "column-container", title: "Imported Columns", visible: true, position: 2, 9721 columns, 9722 backgroundSlideshow: { enabled: false, slides: [], slideDuration: 5000, transitionDuration: 500, slideshowHeight: 50, showFullImages: false, showSingleRandomImage: false, randomStart: false }, 9723 backgroundVideo: { enabled: false, videoUrl: '', videoUrls: [], randomizeVideo: false, videoHeight: 50 } 9724 }); 9725 } 9726 9727 newItem = { 9728 id: newItemId, title, isPage: true, isIntegrated: true, isSubmenu: false, pageId: pageUniqueId, visible: classicItem.visible !== false, parentId: null, 9729 siteId: currentSiteId, slug: finalSlug, url: `/${finalSlug}`, pageElements, 9730 classicCategoryId: classicItem.categoryID, classicGuid: itemGuid, importSource: 'classic-page' 9731 }; 9732 } else if (classicItem.isFolder === true) { 9733 // --- Handle folders from import_guid.json format --- 9734 console.log(`Creating folder: "${title}"`); 9735 newItem = { 9736 id: newItemId, 9737 title, 9738 isFolder: true, 9739 isCollapsed: classicItem.isCollapsed !== false, 9740 visible: classicItem.visible !== false, 9741 parentId: null, 9742 siteId: currentSiteId, 9743 slug: finalSlug, 9744 importSource: 'folder' 9745 }; 9746 } else { 9747 // --- This is a Gallery --- 9748 const galleryPageId = generatePageId(); 9749 9750 // --- ENHANCED: Try to fetch and include actual image data with captions --- 9751 let enhancedGalleryOptions = { 9752 manualCollectionName: `GUID=${itemGuid}`, 9753 categoryId: classicItem.categoryID, 9754 importedFromClassic: true, 9755 siteId: currentSiteId, 9756 pageId: galleryPageId 9757 }; 9758 9759 // --- ENHANCED: Preserve gallery display settings from Classic JSON --- 9760 if (classicItem.galleryOptions) { 9761 console.log(`Preserving gallery settings for: ${title}`); 9762 9763 // Preserve layout and display settings 9764 const preservedSettings = [ 9765 'startInSingles', 'layoutType', 'columns', 'spacing', 9766 'showDescription', 'showFilename', 'displayAllInfo', 9767 'navigationMode', 'showTextBlock', 'fixedHeroImage', 9768 'zoomInLightbox', 'lightboxOnMobile', 'fadeDuration', 9769 'fadeInDuration', 'descriptionTextColor', 'gridImageOverlayColor', 9770 'gridImageOverlayOpacity', 'lightboxBgColor', 'lightboxBgOpacity', 9771 'lightboxCloseColor', 'lightboxArrowColor', 'useTitles', 9772 'useLinks', 'desktopTitleDisplayMode', 'titleTextAlign', 9773 'showDescriptionInOverlay', 'includeRolloverImageInOverlay', 9774 'filterMenuEnabled', 'filterMenuCollectionNames', 'filterMenuTitles', 9775 'filterMenuStyle', 'rolloverSwap', 'rolloverCollectionNames', 9776 'openMultipleLightboxes', 'batchSize', 'autoplaySingles', 9777 'autoplayDuration', 'autoplayTransition', 'autoplayRandomStart' 9778 ]; 9779
9780 preservedSettings.forEach(setting => { 9781 if (classicItem.galleryOptions[setting] !== undefined) { 9782 enhancedGalleryOptions[setting] = classicItem.galleryOptions[setting]; 9783 } 9784 }); 9785 9786 // Preserve text content 9787 if (classicItem.galleryOptions.horizontalScrollerText) { 9788 enhancedGalleryOptions.horizontalScrollerText = classicItem.galleryOptions.horizontalScrollerText; 9789 } 9790 9791 // Preserve JSON collections if present 9792 if (classicItem.galleryOptions.jsonCollections) { 9793 enhancedGalleryOptions.jsonCollections = classicItem.galleryOptions.jsonCollections; 9794 } else { 9795 // Create jsonCollections structure with processed image metadata 9796 const collectionKey = 'GUID=' + itemGuid; 9797 const processedImages = classicItem.images || []; 9798 9799 enhancedGalleryOptions.jsonCollections = { 9800 [collectionKey]: { 9801 isClassicCollection: true, 9802 guid: collectionKey, 9803 metadata: processedImages, 9804 totalItems: processedImages.length 9805 } 9806 }; 9807 9808 console.log('Created jsonCollections for ' + title + ' with ' + processedImages.length + ' images'); 9809 9810 // Build and try to upload NDJSON for this gallery. 9811 // If upload succeeds, switch to metadata = {} so frontend fetches NDJSON. 9812 // Skip NDJSON generation for galleries with no images to avoid creating empty files 9813 if (processedImages.length > 0) { 9814 try { 9815 console.log('Building and uploading NDJSON for gallery:', title, '(' + processedImages.length + ' images)'); 9816 const ndjsonContent = buildHydraClassicNdjson(itemGuid, processedImages); 9817 console.log('Generated NDJSON content length:', ndjsonContent.length, 'characters'); 9818 9819 const uploadResult = await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent); 9820 if (uploadResult && uploadResult.success) { 9821 try { 9822 // import_ format: treat as new Hydra NDJSON gallery (not classic-dependent) 9823 if (isImportFormat) { 9824 const hydraCollectionKey = 'GUID=' + currentSiteId; 9825 enhancedGalleryOptions.manualCollectionName = hydraCollectionKey; 9826 enhancedGalleryOptions.jsonCollections = { 9827 [hydraCollectionKey]: { 9828 isClassicCollection: false, 9829 guid: hydraCollectionKey, 9830 metadata: {}, 9831 totalItems: processedImages.length 9832 } 9833 }; 9834 enhancedGalleryOptions.siteAlias = window.Parameters?.SiteAlias || window.location?.hostname || currentSiteId; 9835 enhancedGalleryOptions.initialPageUuid = galleryPageId; 9836 enhancedGalleryOptions.isHydra = true; 9837 enhancedGalleryOptions.flyIoSaveTimestamp = Date.now(); 9838 console.log('NDJSON uploaded for import_ gallery "' + title + '" - configured as Hydra NDJSON (not classic)'); 9839 } else { 9840 enhancedGalleryOptions.jsonCollections[collectionKey].metadata = {}; 9841 enhancedGalleryOptions.jsonCollections[collectionKey].totalItems = processedImages.length; 9842 } 9843 9844 // Store dual upload metadata if Irys was successful 9845 if (uploadResult.irysResult && uploadResult.irysResult.txId) { 9846 enhancedGalleryOptions.txId = uploadResult.irysResult.txId; 9847 enhancedGalleryOptions.permaURL = uploadResult.irysResult.txId; // Plain txId for racing 9848 enhancedGalleryOptions.isPerma = true; 9849 enhancedGalleryOptions.uploadedTo = uploadResult.uploadedTo; 9850 enhancedGalleryOptions.fileSize = uploadResult.fileSize; 9851 enhancedGalleryOptions.siteId = currentSiteId; 9852 enhancedGalleryOptions.pageId = galleryPageId; 9853 console.log('â Dual upload complete - Irys txId:', uploadResult.irysResult.txId); 9854 console.log(' Uploaded to:', uploadResult.uploadedTo.join(' + ')); 9855 } else { 9856 console.log('â Single upload (Fly.io only) - No Irys data'); 9857 } 9858 } catch (applyErr) { 9859 console.warn('Failed to switch metadata to {} after NDJSON upload:', applyErr); 9860 } 9861 } else { 9862 console.warn('NDJSON upload failed for "' + title + '"; keeping inline metadata array'); 9863 } 9864 } catch (e) { 9865 console.warn('NDJSON build/upload skipped for "' + title + '" due to error:', e); 9866 } 9867 } else {
9868 console.log('Skipping NDJSON generation for "' + title + '" - no images to process'); 9869 } 9870 } 9871 9872 // Preserve additional metadata 9873 if (classicItem.galleryOptions.siteAlias) { 9874 enhancedGalleryOptions.siteAlias = classicItem.galleryOptions.siteAlias; 9875 } 9876 if (classicItem.galleryOptions.initialPageUuid) { 9877 enhancedGalleryOptions.initialPageUuid = classicItem.galleryOptions.initialPageUuid; 9878 } 9879 if (classicItem.galleryOptions.initialPageAlias) { 9880 enhancedGalleryOptions.initialPageAlias = classicItem.galleryOptions.initialPageAlias; 9881 } 9882 if (classicItem.galleryOptions.timestamp) { 9883 enhancedGalleryOptions.timestamp = classicItem.galleryOptions.timestamp; 9884 } 9885 } else if (isImportFormat && (classicItem.images || []).length > 0) { 9886 // import_ format without galleryOptions: still create NDJSON for new Hydra galleries 9887 const processedImages = classicItem.images || []; 9888 const collectionKey = 'GUID=' + itemGuid; 9889 enhancedGalleryOptions.jsonCollections = { 9890 [collectionKey]: { 9891 isClassicCollection: true, 9892 guid: collectionKey, 9893 metadata: processedImages, 9894 totalItems: processedImages.length 9895 } 9896 }; 9897 console.log('Created jsonCollections for import_ gallery (no galleryOptions):', title, '(' + processedImages.length + ' images)'); 9898 try { 9899 const ndjsonContent = buildHydraClassicNdjson(itemGuid, processedImages); 9900 const uploadResult = await uploadNdjsonToTigris(currentSiteId, galleryPageId, ndjsonContent); 9901 if (uploadResult && uploadResult.success) { 9902 const hydraCollectionKey = 'GUID=' + currentSiteId; 9903 enhancedGalleryOptions.manualCollectionName = hydraCollectionKey; 9904 enhancedGalleryOptions.jsonCollections = { 9905 [hydraCollectionKey]: { 9906 isClassicCollection: false, 9907 guid: hydraCollectionKey, 9908 metadata: {}, 9909 totalItems: processedImages.length 9910 } 9911 }; 9912 enhancedGalleryOptions.siteAlias = window.Parameters?.SiteAlias || window.location?.hostname || currentSiteId; 9913 enhancedGalleryOptions.initialPageUuid = galleryPageId; 9914 enhancedGalleryOptions.isHydra = true; 9915 enhancedGalleryOptions.flyIoSaveTimestamp = Date.now(); 9916 if (uploadResult.irysResult?.txId) { 9917 enhancedGalleryOptions.txId = uploadResult.irysResult.txId; 9918 enhancedGalleryOptions.permaURL = uploadResult.irysResult.txId; 9919 enhancedGalleryOptions.isPerma = true; 9920 enhancedGalleryOptions.uploadedTo = uploadResult.uploadedTo || ['fly']; 9921 } 9922 console.log('NDJSON uploaded for import_ gallery "' + title + '" (no galleryOptions) - configured as Hydra NDJSON'); 9923 } 9924 } catch (e) { 9925 console.warn('NDJSON build/upload skipped for import_ gallery "' + title + '":', e); 9926 } 9927 } 9928 9929 // Note: For import from pasted JSON, we don't include image data to avoid overwriting existing captions 9930 // Image data is only included when importing from Classic GUID directly 9931 console.log(`Gallery ${title} imported with structure and settings preserved (image data not included to avoid overwriting existing captions)`); 9932 9933 newItem = { 9934 id: newItemId, title, url: `/${finalSlug}`, isIntegrated: true, isPage: false, isSubmenu: false, visible: classicItem.visible !== false, parentId: null, 9935 siteId: currentSiteId, pageId: galleryPageId, classicCategoryId: classicItem.categoryID, classicGuid: itemGuid, 9936 importSource: 'classic-gallery', slug: finalSlug, normalizedName: finalSlug, 9937 galleryOptions: enhancedGalleryOptions 9938 }; 9939 } 9940 9941 tempNewItems.push(newItem); 9942 } 9943 9944 // --- Pass 2: Link children to parents using the ID map ---
9945 tempNewItems.forEach(newItem => { 9946 const originalId = Object.keys(originalIdToNewIdMap).find(key => originalIdToNewIdMap[key] === newItem.id); 9947 const originalItem = galleriesForImport.find(g => String(g.id) === String(originalId)); 9948 9949 if (originalItem && originalItem.parentId) { 9950 const newParentId = originalIdToNewIdMap[originalItem.parentId]; 9951 if (newParentId) { 9952 newItem.parentId = newParentId; 9953 const parentItem = tempNewItems.find(p => p.id === newParentId); 9954 if (parentItem) { 9955 parentItem.isSubmenu = true; 9956 parentItem.isCollapsed = true; 9957 } 9958 } else { 9959 newItem.parentId = submenuParentId; 9960 } 9961 } else { 9962 newItem.parentId = submenuParentId; 9963 } 9964 }); 9965 9966 if (replaceMenu) { 9967 console.log("Replacing existing menu structure."); 9968 // If replacing, overwrite the entire galleries array. 9969 galleries = tempNewItems; 9970 } else { 9971 console.log("Appending to existing menu structure."); 9972 // If not replacing, push the new items to the existing array. 9973 galleries.push(...tempNewItems); 9974 } 9975 9976 9977 9978 if (progressBar) progressBar.style.width = '90%'; 9979 if (statusMessage) statusMessage.textContent = 'Saving changes...'; 9980 9981 // Pass import flag to skip localStorage for large import operations 9982 await saveGalleries({ isImport: true }); 9983 9984 if (progressBar) progressBar.style.width = '100%'; 9985 if (statusMessage) statusMessage.textContent = `Successfully imported ${tempNewItems.length} items!`; 9986 if (statusEl) statusEl.classList.remove('importing'); 9987 9988 setTimeout(() => { 9989 renderGalleries(); 9990 toggleImportClassicForm(); 9991 if (progressBar) progressBar.style.width = '0'; 9992 if (statusEl) statusEl.style.display = 'none'; 9993 if (confirm('Import complete! Would you like to reload the page to ensure everything is displayed correctly?')) { 9994 window.location.reload(); 9995 } 9996 }, 1000); 9997 9998 } catch (error) { 9999 console.error('Error importing classic galleries:', error); 10000 if (statusMessage) statusMessage.textContent = `Error: ${error.message}`; 10001 if (progressBar) { 10002 progressBar.style.width = '100%'; 10003 progressBar.style.backgroundColor = '#dc3545'; 10004 } 10005 if (statusEl) statusEl.classList.remove('importing'); 10006 } 10007} 10008 10009 10010 10011/** 10012 * Improved toggleImportClassicForm - Closes other forms first 10013 */ 10014function toggleImportClassicForm() { 10015 // First check if user is logged in and in edit mode 10016 if (!isAuthenticated || !isAdmin || !window.isInEditMode()) { 10017 alert('You must be logged in as an admin and in edit mode to import galleries.'); 10018 return; 10019 } 10020 10021 const formId = 'importClassicForm'; 10022 const form = document.getElementById(formId); 10023 10024 if (!form) { 10025 console.error('Import form element not found'); 10026 return; 10027 } 10028 10029 // If this form is already open, just close everything 10030 if (window.currentOpenForm === formId) { 10031 closeAllForms(); 10032 return; 10033 } 10034 10035 // Otherwise close all forms and open this one 10036 closeAllForms(); 10037 10038 // Show the form 10039 form.style.display = 'block'; 10040 10041 // Use setTimeout to allow the display change to take effect before adding the class 10042 setTimeout(() => { 10043 form.classList.add('visible'); 10044 }, 10); 10045 10046 // Populate parent options 10047 const parentSelect = document.getElementById('importParent'); 10048 if (parentSelect) { 10049 parentSelect.innerHTML = '<option value="">Top Level (No Parent)</option>'; 10050 10051 // Add all galleries that could be parents (including submenus) 10052 galleries.forEach(gallery => { 10053 // Only include items that are not already marked as imported 10054 if (!gallery.importSource) { 10055 parentSelect.innerHTML += `<option value="${gallery.id}">${gallery.title}</option>`; 10056 } 10057 }); 10058 } 10059 10060 // Set default values 10061 const submenuTitle = document.getElementById('submenuTitle'); 10062 if (submenuTitle && !submenuTitle.value) { 10063 submenuTitle.value = 'Classic Galleries'; 10064 } 10065 10066 // Reset any previous import status 10067 const statusEl = document.getElementById('importStatus'); 10068 if (statusEl) { 10069 statusEl.style.display = 'none'; 10070 const progressBar = statusEl.querySelector('.progress-bar'); 10071 if (progressBar) { 10072 progressBar.style.width = '0'; 10073 progressBar.style.backgroundColor = '#4682B4'; // Reset to blue 10074 } 10075 } 10076 10077 // Set this as the current open form 10078 window.currentOpenForm = formId; 10079} 10080 10081// Add event listener for the radio buttons to adjust height when page is selected 10082document.addEventListener('DOMContentLoaded', function() { 10083 const galleryTypeRadios = document.querySelectorAll('input[name="galleryType"]'); 10084 galleryTypeRadios.forEach(radio => { 10085 radio.addEventListener('change', function() { 10086 const addForm = document.getElementById('addForm'); 10087 if (addForm && addForm.classList.contains('visible')) { 10088 const isPageSelected = this.value === 'page'; 10089 10090 // Give more height for page type 10091 if (isPageSelected) { 10092 addForm.style.maxHeight = '600px'; 10093 } else { 10094 // For other types, measure content height 10095 const contentHeight = addForm.scrollHeight + 30; 10096 addForm.style.maxHeight = Math.max(contentHeight, 500) + 'px'; 10097 } 10098 } 10099 }); 10100 }); 10101}); 10102 10103 10104/** 10105 * Improved toggleStyleEditor - Closes other forms first 10106 */ 10107function toggleStyleEditor(display = false, event) { 10108 const formId = 'menuStyleEditor'; 10109 const overlayOpen = window.MenuStyleCustomizer && window.MenuStyleCustomizer._overlayOpen; 10110 10111 if (overlayOpen && window.MenuStyleCustomizer._overlayMinimized && display !== false) { 10112 if (typeof window.MenuStyleCustomizer._toggleOverlayMinimized === 'function') { 10113 window.MenuStyleCustomizer._toggleOverlayMinimized(false); 10114 } 10115 return; 10116 } 10117 10118 if (display === false || window.currentOpenForm === formId || overlayOpen) { 10119 closeAllForms(); 10120 return; 10121 } 10122 10123 closeAllForms(); 10124 10125 if (window.MenuStyleCustomizer && typeof window.MenuStyleCustomizer.toggleOverlay === 'function') { 10126 window.MenuStyleCustomizer.toggleOverlay(); 10127 } 10128} 10129 10130 10131/** 10132 * This function closes all forms when escape key is pressed 10133 */ 10134document.addEventListener('keydown', function(e) { 10135 if (e.key === 'Escape' && window.currentOpenForm) { 10136 if (closeTopmostStyleCustomizerChildOverlay()) { 10137 return; 10138 } 10139 if (window.MenuStyleCustomizer && 10140 window.MenuStyleCustomizer._overlayOpen && 10141 window.MenuStyleCustomizer._overlayMinimized) { 10142 return; 10143 } 10144 closeAllForms(); 10145 } 10146}); 10147 10148/** 10149 * This function closes all forms when clicking outside any form 10150 */ 10151document.addEventListener('click', function(e) { 10152 if (!window.currentOpenForm) return; 10153 10154 if (window.MenuStyleCustomizer && 10155 window.MenuStyleCustomizer._overlayOpen && 10156 window.MenuStyleCustomizer._overlayMinimized) { 10157 return; 10158 } 10159 10160 // Keep Style Customizer and its nested panels open during in-overlay interactions. 10161 if (e.target.closest( 10162 '.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' 10163 )) { 10164 return; 10165 } 10166 10167 if (e.target.closest('.image-uploader-overlay')) { 10168 return; 10169 } 10170 10171 const currentForm = document.getElementById(window.currentOpenForm); 10172 if (!currentForm) { 10173 if (e.target.closest( 10174 '.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' 10175 )) { 10176 return; 10177 } 10178 closeAllForms(); 10179 return; 10180 } 10181 10182 const isInsideForm = currentForm.contains(e.target);
10183 const isFormToggleButton = e.target.closest('[onclick*="toggle"]') !== null; 10184 10185 if (isInsideForm || isFormToggleButton) { 10186 return; 10187 } 10188 10189 closeAllForms(); 10190}); 10191 10192// Override the original functions with our improved versions 10193window.toggleAddForm = toggleAddForm; 10194window.toggleStyleEditor = toggleStyleEditor; 10195window.toggleMetadataEditor = toggleMetadataEditor; 10196window.toggleSidebarElementForm = toggleSidebarElementForm; 10197window.toggleImportClassicForm = toggleImportClassicForm; 10198window.closeAllForms = closeAllForms; 10199window.closeTopmostStyleCustomizerChildOverlay = closeTopmostStyleCustomizerChildOverlay; 10200 10201 10202/** 10203 * Opens the edit form for a gallery item. 10204 * (UPDATED to include Meta Title and Meta Description fields, removed Parent Menu) 10205 * @param {number} id - The ID of the gallery item. 10206 * @param {Event} event - The click event. 10207 */ 10208function editGallery(id, event) { 10209 event.stopPropagation(); // Prevent event bubbling 10210 const gallery = findGalleryById(galleries, id); 10211 const li = getGalleryTreeListItem(id, event); 10212 10213 if (gallery && li) { 10214 const showUrlField = gallery.isPage || gallery.isIntegrated; 10215 const showExternalUrlField = gallery.isExternal; 10216 const showMenuParentOption = !gallery.isFolder && !gallery.isSubmenu && !gallery.isSpacer && !gallery.isExternal && 10217 (isPageMenuItem(gallery) || gallery.isIntegrated === true); 10218 /** Sidebar meta title/description apply to gallery routes only; pages use the page metadata element. */ 10219 const showSidebarSeoFields = !gallery.isPage; 10220 const currentSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : ''); 10221 const currentExternalUrl = gallery.url || ''; 10222 10223 // Clear existing content and rebuild form to ensure event listeners are fresh if needed 10224 li.innerHTML = ''; // Clear previous form if any 10225 10226 const formDiv = document.createElement('div'); 10227 formDiv.className = 'edit-form'; 10228 formDiv.innerHTML = ` 10229 <div class="form-group"> 10230 <label>Title:</label> 10231 <input type="text" value="${gallery.title || ''}" class="edit-title"> 10232 </div> 10233 10234 ${showUrlField ? ` 10235 <div class="form-group"> 10236 <label>URL Slug:</label> 10237 <input type="text" value="${currentSlug}" class="edit-slug" placeholder="e.g., my-gallery"> 10238 <small>Path after domain name (e.g., yoursite.com<b>/my-gallery</b>). Use lowercase letters, numbers, and hyphens.</small> 10239 </div> 10240 ` : ''} 10241 10242 ${showExternalUrlField ? ` 10243 <div class="form-group"> 10244 <label>External URL:</label> 10245 <input type="text" value="${currentExternalUrl}" class="edit-external-url" placeholder="https://example.com"> 10246 <small>Full URL for the external link (e.g., https://example.com/page).</small> 10247 </div> 10248 ` : ''} 10249 10250 ${showSidebarSeoFields ? ` 10251 <div class="form-group"> 10252 <label>Meta Title (Optional):</label> 10253 <input type="text" value="${gallery.metaTitle || ''}" class="edit-meta-title" placeholder="Title for search results (if different from main title)"> 10254 <small>Overrides site title for this item in search results/browser tabs.</small> 10255 </div> 10256 10257 <div class="form-group"> 10258 <label>Meta Description (Optional):</label> 10259 <textarea class="edit-meta-description" placeholder="Description for search results (150-160 chars recommended)">${gallery.metaDescription || ''}</textarea> 10260 <small>Overrides site description for this item in search results.</small> 10261 </div> 10262 ` : ` 10263 <div class="form-group"> 10264 <p class="text-muted" style="margin: 0 0 8px 0; font-size: 12px; line-height: 1.4;"> 10265 Search title and description for this page are set in <strong>Page â metadata</strong> (the metadata block on the page), not here. 10266 </p> 10267 </div> 10268 `} 10269 10270 <div class="form-group"> 10271 <label style="display: flex; align-items: center; gap: 8px;"> 10272 <input type="checkbox" class="edit-hide-menu" ${gallery.hideMenuOnPage ? 'checked' : ''} style="width: auto; margin-bottom: 0;" onchange="onPageMenuChromeToggle(this)"> 10273 Hide menu when viewing this page 10274 </label> 10275 <small>Allows for a full-screen page or gallery layout. Menu remains visible in edit mode.</small> 10276 </div> 10277 10278 <div class="form-group"> 10279 <label style="display: flex; align-items: center; gap: 8px;"> 10280 <input type="checkbox" class="edit-overlay-menu" ${gallery.overlayMenuOnPage ? 'checked' : ''} style="width: auto; margin-bottom: 0;" onchange="onPageMenuChromeToggle(this)"> 10281 Show menu as overlay on this page 10282 </label> 10283 <small>Keeps menu items visible over a full-bleed page. Menu background is hidden. Solid menu remains in edit mode.</small> 10284 </div> 10285 10286 ${showMenuParentOption ? ` 10287 <div class="form-group"> 10288 <label style="display: flex; align-items: center; gap: 8px;"> 10289 <input type="checkbox" class="edit-acts-as-menu-parent" ${gallery.actsAsMenuParent === true ? 'checked' : ''} style="width: auto; margin-bottom: 0;"> 10290 Allow nested menu items 10291 </label> 10292 <small>Keeps this ${isPageMenuItem(gallery) ? 'page' : 'gallery'} clickable while allowing it to expand and contain sub-items.</small> 10293 </div> 10294 ` : ''} 10295 10296 <div class="form-group"> 10297 <label style="display: flex; align-items: center; gap: 8px;"> 10298 <input type="checkbox" class="edit-password-protected" ${gallery.passwordProtected ? 'checked' : ''} style="width: auto; margin-bottom: 0;"> 10299 Password protect this page 10300 </label> 10301 <small>Visitors will need to enter a password to access this page.</small> 10302 </div> 10303 10304 <div class="form-group password-field" style="display: ${gallery.passwordProtected ? 'block' : 'none'};"> 10305 <label>Password:</label> 10306 <input type="password" value="${gallery.password || ''}" class="edit-password" placeholder="Enter password"> 10307 <small>Password visitors must enter to access this page.</small> 10308 </div> 10309 10310 <div class="form-group"> 10311 <label style="display: flex; align-items: center; gap: 8px;"> 10312 <input type="checkbox" class="edit-no-index" ${(() => { 10313 // Explicit setting takes priority;
10313 otherwise default to checked for hidden non-home pages 10314 if (gallery.noIndex === true) return 'checked'; 10315 if (gallery.noIndex === false) return ''; 10316 return (gallery.visible === false && !gallery.isHomePage) ? 'checked' : ''; 10317 })()} style="width: auto; margin-bottom: 0;"> 10318 Hide from search engines (noindex) 10319 </label> 10320 <small>When checked, search engines will not index this page. Automatically enabled for hidden pages â uncheck to allow indexing even when hidden.</small> 10321 </div> 10322 10323 <div class="edit-form-actions"> 10324 <button class="btn btn-primary" id="saveEditBtn-${id}">Save</button> 10325 <button class="btn" id="cancelEditBtn-${id}">Cancel</button> 10326 </div> 10327 <br> 10328 `; 10329 li.appendChild(formDiv); // Append the new form 10330 10331 // Add event listeners programmatically 10332 const saveButton = formDiv.querySelector(`#saveEditBtn-${id}`); 10333 if (saveButton) { 10334 saveButton.addEventListener('click', (e) => saveEdit(id, e)); 10335 } 10336 10337 const cancelButton = formDiv.querySelector(`#cancelEditBtn-${id}`); 10338 if (cancelButton) { 10339 cancelButton.addEventListener('click', () => renderGalleries()); 10340 } 10341 10342 // Add password protection toggle functionality 10343 const passwordProtectedCheckbox = formDiv.querySelector('.edit-password-protected'); 10344 const passwordField = formDiv.querySelector('.password-field'); 10345 if (passwordProtectedCheckbox && passwordField) { 10346 passwordProtectedCheckbox.addEventListener('change', function() { 10347 passwordField.style.display = this.checked ? 'block' : 'none'; 10348 }); 10349 } 10350 10351 li.classList.add('edit-mode'); 10352 } else { 10353 console.error("Could not find gallery or list item for ID:", id); 10354 } 10355} 10356 10357/** 10358 * Resolve the correct sidebar #galleryTree li for an id. Horizontal dropdowns also use 10359 * li[data-id] (.horizontal-submenu-item); document.querySelector would return that node first 10360 * and break nested folder toggles in edit mode while horizontal layout is on. 10361 * @param {number} id - Gallery ID 10362 * @param {Event|null} event - Optional click event (preferred: closest li inside #galleryTree) 10363 * @returns {HTMLElement|null} 10364 */ 10365function getGalleryTreeListItem(id, event) { 10366 var numericId = typeof id === 'number' ? id : parseInt(String(id), 10); 10367 if (isNaN(numericId)) return null; 10368 var galleryTreeEl = document.getElementById('galleryTree'); 10369 if (event && event.target && galleryTreeEl) { 10370 var clicked = event.target.closest('li[data-id]'); 10371 if (clicked && galleryTreeEl.contains(clicked)) { 10372 var cid = parseInt(clicked.getAttribute('data-id') || '', 10); 10373 if (cid === numericId) return clicked; 10374 } 10375 } 10376 if (galleryTreeEl) { 10377 var scoped = galleryTreeEl.querySelector('li[data-id="' + numericId + '"]'); 10378 if (scoped) return scoped; 10379 } 10380 return document.querySelector('li[data-id="' + numericId + '"]'); 10381} 10382 10383/** 10384 * Handles click on a folder item to show/hide its contents 10385 * @param {number} id - The gallery ID 10386 * @param {Event} event - The click event 10387 */ 10388function toggleFolder(id, event) { 10389 // Ensure the event doesn't interfere with drag operations 10390 if (event) { 10391 event.stopPropagation(); 10392 } 10393 10394 10395 const gallery = galleries.find(g => g.id === id); 10396 if (!gallery) { 10397 return; 10398 } 10399 10400 if (gallery.isFolder !== true && gallery.isSubmenu !== true && !isNavigableMenuParent(gallery)) { 10401 return; 10402 } 10403 10404 10405 // Toggle the collapsed state 10406 gallery.isCollapsed = !gallery.isCollapsed; 10407 10408 // Find the list item in the DOM (#galleryTree first â not horizontal li.horizontal-submenu-item) 10409 const listItem = getGalleryTreeListItem(id, event); 10410 if (!listItem) { 10411 return; 10412 } 10413 10414 10415 // Toggle the expanded class FIRST - this is important for CSS to work correctly 10416 listItem.classList.toggle('expanded', !gallery.isCollapsed); 10417 10418 // Keep ARIA in sync for folder label + chevron (screen readers / keyboard) 10419 const folderExpanded = !gallery.isCollapsed; 10420 listItem.querySelectorAll('.menu-item.folder, .submenu-toggle').forEach((el) => { 10421 el.setAttribute('aria-expanded', folderExpanded ? 'true' : 'false'); 10422 }); 10423 10424 // Force a reflow after toggling the class to ensure CSS recalculates 10425 void listItem.offsetHeight; 10426 10427 // Find the nested list - SIMPLIFIED: Check if we're in sidebar first, then horizontal 10428 // CRITICAL: Check #galleryTree first - this is the definitive indicator of sidebar layout 10429 // Use contains() instead of closest() because closest() only checks ancestors 10430 const galleryTree = document.getElementById('galleryTree'); 10431 const isInGalleryTree = galleryTree && galleryTree.contains(listItem); 10432 const isInSidebar = listItem.closest('.sidebar') || listItem.closest('#sidebar') || isInGalleryTree; 10433 // Only consider it horizontal menu if it's explicitly in .horizontal-menu
10433-container AND not in galleryTree 10434 const isInHorizontalMenu = listItem.closest('.horizontal-menu-container') && !isInGalleryTree && !isInSidebar; 10435 10436 let nestedList = null; 10437 10438 // If we're in sidebar (#galleryTree), ONLY look for ol.nested-sortable - ignore horizontal structures 10439 // CRITICAL: Prioritize sidebar detection - if we're in galleryTree, we're definitely in sidebar 10440 if (isInGalleryTree) { 10441 nestedList = listItem.querySelector('ol.nested-sortable'); 10442 10443 // Also check direct children as fallback 10444 if (!nestedList) { 10445 const directChildren = Array.from(listItem.children); 10446 nestedList = directChildren.find(child => child.tagName === 'OL' && child.classList.contains('nested-sortable')); 10447 } 10448 } else if (isInSidebar) { 10449 // Fallback: if we're in .sidebar but not in #galleryTree, still treat as sidebar 10450 nestedList = listItem.querySelector('ol.nested-sortable'); 10451 if (!nestedList) { 10452 const directChildren = Array.from(listItem.children); 10453 nestedList = directChildren.find(child => child.tagName === 'OL' && child.classList.contains('nested-sortable')); 10454 } 10455 } 10456 10457 // For horizontal layout, check horizontal structures (ONLY if NOT in sidebar/galleryTree) 10458 if (!nestedList && isInHorizontalMenu && !isInGalleryTree && !isInSidebar) { 10459 nestedList = listItem.querySelector('.horizontal-submenu'); 10460 if (!nestedList) { 10461 nestedList = listItem.querySelector('.horizontal-dropdown'); 10462 } 10463 if (!nestedList) { 10464 // For horizontal nested dropdowns, they're appended to document.body, not the list item 10465 // They're linked via data-flyout-id attribute 10466 const flyoutId = listItem.getAttribute('data-flyout-id'); 10467 if (flyoutId) { 10468 // Look specifically for div.horizontal-nested-dropdown, not the list item itself 10469 const allElementsWithId = document.querySelectorAll('[data-flyout-id="' + flyoutId + '"]'); 10470 10471 // Find the actual dropdown div, not the list item 10472 const flyout = Array.from(allElementsWithId).find(el => 10473 el.classList.contains('horizontal-nested-dropdown') && el !== listItem 10474 ); 10475 10476 10477 if (flyout) { 10478 nestedList = flyout.querySelector('.horizontal-nested-list') || flyout; 10479 } 10480 } 10481 } 10482 if (!nestedList) { 10483 nestedList = listItem.querySelector('.horizontal-nested-dropdown'); 10484 } 10485 } 10486 10487 // Fallback: try all selectors if not found yet 10488 if (!nestedList) { 10489 nestedList = listItem.querySelector('ol.nested-sortable'); 10490 } 10491 if (!nestedList) { 10492 // Debug: Log the actual DOM structure to understand what's there 10493 10494 // Try to find any nested structure at all 10495 const anyNested = listItem.querySelector('ol, ul, div[class*="submenu"], div[class*="nested"], div[class*="dropdown"]'); 10496 if (anyNested) { 10497 nestedList = anyNested; // Use it anyway 10498 } 10499 10500 // Last resort: Check if nested list exists as a direct child but wasn't found by querySelector 10501 // This can happen if the element was removed/re-added or if there's a timing issue 10502 const directChildren = Array.from(listItem.children); 10503 const olChild = directChildren.find(child => child.tagName === 'OL'); 10504 if (olChild) { 10505 nestedList = olChild; 10506 } 10507 } 10508 10509 if (!nestedList) { 10510 10511 // If the nested list doesn't exist, we need to create it 10512 // This can happen if children aren't rendered until the folder is opened 10513 // First try flat array 10514 let gallery = galleries.find(g => g.id === id); 10515 10516 // If gallery not found or has no children in flat array, build tree structure to find it 10517 if (!gallery || !gallery.children || gallery.children.length === 0) { 10518 10519 // Build tree structure from flat array 10520 const buildTree = (items, parentId = null) => { 10521 return items 10522 .filter(item => item.parentId === parentId) 10523 .map(item => ({ 10524 ...item, 10525 children: buildTree(items, item.id) 10526 })); 10527 }; 10528 10529 const galleryTree = buildTree(galleries); 10530 10531 // Recursive function to find gallery in tree 10532 const findInTree = (items) => { 10533 for (const item of items) { 10534 if (item.id === id) { 10535 return item; 10536 } 10537 if (item.children && item.children.length > 0) { 10538 const found = findInTree(item.children); 10539 if (found) return found; 10540 } 10541 } 10542 return null; 10543 }; 10544 10545 const treeGallery = findInTree(galleryTree); 10546 if (treeGallery) { 10547 gallery = treeGallery; 10548 } 10549 } 10550 10551 if (gallery && gallery.children && gallery.children.length > 0) { 10552 10553 // Determine nesting level from list item 10554 const currentLevel = parseInt(listItem.getAttribute('data-nesting-level') || '0', 10); 10555 const nextLevel = currentLevel + 1; 10556 10557 // Create the nested list element 10558 nestedList = document.createElement('ol'); 10559 nestedList.id = 'gallery-children-' + id; 10560 nestedList.className = 'nested-sortable'; 10561 if (nextLevel > 0) { 10562 nestedList.classList.add('nested-level-' + nextLevel); 10563 } 10564 10565 // Render the children - ensure we're in the right context (not in a horizontal dropdown wrapper)
10566 gallery.children.forEach(child => { 10567 const childHtml = renderGalleryItem(child, nextLevel, 20); 10568 const tempDiv = document.createElement('div'); 10569 tempDiv.innerHTML = childHtml; 10570 const childLi = tempDiv.querySelector('li'); 10571 if (childLi) { 10572 nestedList.appendChild(childLi); 10573 } 10574 }); 10575 10576 // CRITICAL: Only check for horizontal dropdowns if we're actually in a horizontal menu context 10577 // If we're in #galleryTree (sidebar), skip all horizontal menu logic 10578 const galleryTree = document.getElementById('galleryTree'); 10579 const isInGalleryTree = galleryTree && galleryTree.contains(listItem); 10580 const isInSidebar = listItem.closest('.sidebar') || listItem.closest('#sidebar') || isInGalleryTree; 10581 10582 if (!isInGalleryTree && !isInSidebar) { 10583 const isInHorizontalMenu = listItem.closest('.horizontal-menu-container'); 10584 if (isInHorizontalMenu) { 10585 const horizontalDropdown = listItem.closest('.horizontal-dropdown, .horizontal-nested-dropdown'); 10586 if (horizontalDropdown && horizontalDropdown !== listItem) { 10587 horizontalDropdown.style.setProperty('display', 'block', 'important'); 10588 horizontalDropdown.style.setProperty('visibility', 'visible', 'important'); 10589 horizontalDropdown.style.setProperty('opacity', '1', 'important'); 10590 } 10591 } 10592 } 10593 10594 // CRITICAL: Ensure list item itself is visible and has correct display 10595 listItem.style.setProperty('display', 'list-item', 'important'); 10596 listItem.style.setProperty('visibility', 'visible', 'important'); 10597 10598 // CRITICAL: Ensure ALL parent elements up to the sidebar are visible 10599 // BUT: Don't make .horizontal-menu-container visible - that's the entire horizontal menu! 10600 let parent = listItem.parentElement; 10601 let depth = 0; 10602 while (parent && depth < 10) { 10603 // Skip .horizontal-menu-container - we don't want to show the entire horizontal menu 10604 if (parent.classList.contains('horizontal-menu-container')) { 10605 parent = parent.parentElement; 10606 depth++; 10607 continue; 10608 } 10609 10610 const parentStyles = window.getComputedStyle(parent); 10611 if (parentStyles.display === 'none' || parentStyles.visibility === 'hidden' || parentStyles.opacity === '0') { 10612 if (parentStyles.display === 'none') { 10613 parent.style.setProperty('display', 'block', 'important'); 10614 } 10615 if (parentStyles.visibility === 'hidden') { 10616 parent.style.setProperty('visibility', 'visible', 'important'); 10617 } 10618 if (parentStyles.opacity === '0') { 10619 parent.style.setProperty('opacity', '1', 'important'); 10620 } 10621 } 10622 // Stop at sidebar or galleryTree 10623 if (parent.classList.contains('sidebar') || parent.id === 'galleryTree' || parent.id === 'sidebar') { 10624 break; 10625 } 10626 parent = parent.parentElement; 10627 depth++; 10628 } 10629 10630 // Append directly to the list item (not to any wrapper) 10631 // Make sure we're not appending to a hidden parent 10632 listItem.appendChild(nestedList); 10633 10634 // Immediately set the nested list to be visible since we're opening it 10635 nestedList.style.setProperty('display', 'block', 'important'); 10636 nestedList.style.setProperty('visibility', 'visible', 'important'); 10637 nestedList.style.setProperty('opacity', '1', 'important'); 10638 nestedList.style.setProperty('height', 'auto', 'important'); 10639 nestedList.style.setProperty('max-height', 'none', 'important'); 10640 10641 // Force immediate reflow after appending 10642 void nestedList.offsetHeight; 10643 void listItem.offsetHeight; 10644 } 10645 } 10646 10647 if (nestedList) { 10648 if (!gallery.isCollapsed) { 10649 // Show the nested list - ensure parent has expanded class FIRST 10650 // CRITICAL: Add expanded class BEFORE any style operations 10651 listItem.classList.add('expanded'); 10652 10653 // Verify expanded class was added 10654 if (!listItem.classList.contains('expanded')) { 10655 listItem.className = listItem.className + ' expanded'; 10656 } 10657 10658 // CRITICAL: Ensure parent li is visible (it might be hidden by CSS) 10659 const parentVisibility = window.getComputedStyle(listItem).visibility; 10660 if (parentVisibility === 'hidden') { 10661 listItem.style.setProperty('visibility', 'visible', 'important'); 10662 } 10663 10664 // Force a reflow to ensure CSS recalculates after adding expanded class 10665 void listItem.offsetHeight; 10666 void nestedList.offsetHeight; 10667 10668 // Show the nested list with !important to override CSS 10669 // Set these BEFORE removing any existing inline styles to avoid flicker 10670 nestedList.style.setProperty('display', 'block', 'important'); 10671 nestedList.style.setProperty('visibility', 'visible', 'important'); 10672 nestedList.style.setProperty('opacity', '1', 'important'); 10673 nestedList.style.setProperty('height', 'auto', 'important'); 10674 nestedList.style.setProperty('max-height', 'none', 'important'); 10675 nestedList.style.setProperty('overflow', 'visible', 'important'); 10676 10677 // Also ensure the nested list is not clipped by parent overflow
10678 // BUT: Don't modify .horizontal-menu-container overflow - that's the entire horizontal menu! 10679 let checkParent = nestedList.parentElement; 10680 let checkDepth = 0; 10681 while (checkParent && checkDepth < 5) { 10682 // Skip .horizontal-menu-container - we don't want to modify the entire horizontal menu 10683 if (checkParent.classList.contains('horizontal-menu-container')) { 10684 checkParent = checkParent.parentElement; 10685 checkDepth++; 10686 continue; 10687 } 10688 10689 const parentOverflow = window.getComputedStyle(checkParent).overflow; 10690 if (parentOverflow === 'hidden') { 10691 checkParent.style.setProperty('overflow', 'visible', 'important'); 10692 } 10693 if (checkParent.classList.contains('sidebar') || checkParent.id === 'galleryTree') { 10694 break; 10695 } 10696 checkParent = checkParent.parentElement; 10697 checkDepth++; 10698 } 10699 10700 // Force another reflow after setting styles 10701 void nestedList.offsetHeight; 10702 10703 const finalRect = nestedList.getBoundingClientRect(); 10704 10705 // If the nested list has zero dimensions, it's not actually visible 10706 if (finalRect.width === 0 || finalRect.height === 0) { 10707 // Force all parent elements to be visible 10708 // BUT: Don't modify .horizontal-menu-container - that's the entire horizontal menu! 10709 let forceParent = nestedList.parentElement; 10710 while (forceParent && forceParent !== document.body) { 10711 // Skip .horizontal-menu-container - we don't want to show the entire horizontal menu 10712 if (forceParent.classList.contains('horizontal-menu-container')) { 10713 forceParent = forceParent.parentElement; 10714 continue; 10715 } 10716 10717 forceParent.style.setProperty('display', 'block', 'important'); 10718 forceParent.style.setProperty('visibility', 'visible', 'important'); 10719 forceParent.style.setProperty('opacity', '1', 'important'); 10720 forceParent.style.setProperty('overflow', 'visible', 'important'); 10721 if (forceParent.classList.contains('sidebar') || forceParent.id === 'galleryTree') { 10722 break; 10723 } 10724 forceParent = forceParent.parentElement; 10725 } 10726 void nestedList.offsetHeight; 10727 } 10728 10729 // Check for any parent elements that might be hiding the nested list and fix them 10730 let parent = nestedList.parentElement; 10731 let depth = 0; 10732 while (parent && depth < 5) { 10733 const parentStyles = window.getComputedStyle(parent); 10734 if (parentStyles.display === 'none' || parentStyles.visibility === 'hidden' || parentStyles.opacity === '0') { 10735 10736 // Fix hidden parent elements 10737 if (parentStyles.display === 'none') { 10738 parent.style.setProperty('display', 'block', 'important'); 10739 } 10740 if (parentStyles.visibility === 'hidden') { 10741 parent.style.setProperty('visibility', 'visible', 'important'); 10742 } 10743 if (parentStyles.opacity === '0') { 10744 parent.style.setProperty('opacity', '1', 'important'); 10745 } 10746 } 10747 parent = parent.parentElement; 10748 depth++; 10749 } 10750 10751 } else { 10752 // Hide the nested list 10753 nestedList.style.setProperty('display', 'none', 'important'); 10754 nestedList.style.setProperty('visibility', 'hidden', 'important'); 10755 nestedList.style.setProperty('opacity', '0', 'important'); 10756 } 10757 10758 // Force multiple reflows to ensure the browser applies all changes 10759 void listItem.offsetHeight; 10760 void nestedList.offsetHeight; 10761 void listItem.offsetWidth; 10762 void nestedList.offsetWidth; 10763 10764 // Force a more aggressive repaint by temporarily toggling a class 10765 // This is especially important for sidebar layout where CSS rules might be interfer
10765ing 10766 listItem.classList.add('force-reflow'); 10767 void listItem.offsetHeight; 10768 listItem.classList.remove('force-reflow'); 10769 10770 // Force reflow again after class manipulation 10771 void listItem.offsetHeight; 10772 void nestedList.offsetHeight; 10773 10774 // Use requestAnimationFrame with multiple frames to ensure visual update 10775 // This ensures the browser has time to recalculate layout and repaint 10776 requestAnimationFrame(() => { 10777 void nestedList.offsetHeight; 10778 // Second frame to ensure all CSS has been recalculated 10779 requestAnimationFrame(() => { 10780 void nestedList.offsetHeight; 10781 // Verify the final state 10782 const finalDisplay = window.getComputedStyle(nestedList).display; 10783 const finalVisibility = window.getComputedStyle(nestedList).visibility; 10784 10785 // If still not visible, force one more repaint cycle 10786 if (!gallery.isCollapsed && finalDisplay === 'none') { 10787 nestedList.style.setProperty('display', 'block', 'important'); 10788 nestedList.style.setProperty('visibility', 'visible', 'important'); 10789 void nestedList.offsetHeight; 10790 requestAnimationFrame(() => { 10791 void nestedList.offsetHeight; 10792 }); 10793 } 10794 }); 10795 }); 10796 } 10797 10798 // Find the submenu toggle icon and rotate it 10799 const toggleIcon = listItem.querySelector('.toggle-icon'); 10800 if (toggleIcon) { 10801 toggleIcon.classList.toggle('rotated', !gallery.isCollapsed); 10802 } 10803 10804 10805 // Folder open/closed is a local UI preference â do not POST the full site config. 10806 try { 10807 const folderStates = JSON.parse(localStorage.getItem('folder_states') || '{}'); 10808 folderStates[id] = !gallery.isCollapsed; 10809 localStorage.setItem('folder_states', JSON.stringify(folderStates)); 10810 } catch (e) { 10811 console.error('Error saving folder state to localStorage:', e); 10812 } 10813} 10814 10815window.toggleFolder = toggleFolder; 10816 10817/** 10818 * Toggle password protection for a gallery/page 10819 * @param {number} id - The gallery ID 10820 * @param {Event} event - The click event 10821 */ 10822function toggleGalleryPasswordProtection(id, event) { 10823 event.stopPropagation(); 10824 10825 const gallery = findGalleryById(galleries, id); 10826 if (!gallery) { 10827 console.error("Could not find gallery for password protection toggle, ID:", id); 10828 return; 10829 } 10830 10831 if (gallery.passwordProtected) { 10832 // Remove password protection 10833 if (confirm('Remove password protection from this page?')) { 10834 gallery.passwordProtected = false; 10835 delete gallery.password; 10836 saveGalleries(); 10837 renderGalleries(); 10838 } 10839 } else { 10840 // Add password protection 10841 const password = prompt('Enter a password for this page:'); 10842 if (password && password.trim()) { 10843 gallery.passwordProtected = true; 10844 gallery.password = password.trim(); 10845 saveGalleries(); 10846 renderGalleries(); 10847 } 10848 } 10849} 10850 10851window.toggleGalleryPasswordProtection = toggleGalleryPasswordProtection; 10852 10853function onPageMenuChromeToggle(checkbox) { 10854 if (!checkbox || !checkbox.checked) return; 10855 const root = checkbox.closest ? checkbox.closest('li') : null; 10856 if (!root) return; 10857 const siblingClass = checkbox.classList.contains('edit-hide-menu') ? '.edit-overlay-menu' : '.edit-hide-menu'; 10858 const sibling = root.querySelector(siblingClass); 10859 if (sibling && sibling !== checkbox) sibling.checked = false; 10860} 10861window.onPageMenuChromeToggle = onPageMenuChromeToggle; 10862 10863/** 10864 * Saves the edited gallery item details. 10865 * (UPDATED to handle Meta Title and Meta Description, removed Parent Menu logic) 10866 * @param {number} id - The ID of the gallery item. 10867 * @param {Event} event - The click event. 10868 */ 10869function saveEdit(id, event) { 10870 event.stopPropagation(); 10871 const li = getGalleryTreeListItem(id, event); 10872 if (!li) { 10873 console.error("Could not find list item for saveEdit, ID:", id); 10874 return; 10875 } 10876 10877 const titleInput = li.querySelector('.edit-title'); 10878 const slugInput = li.querySelector('.edit-slug'); 10879 const externalUrlInput = li.querySelector('.edit-external-url'); 10880 const metaTitleInput = li.querySelector('.edit-meta-title'); 10881 const metaDescriptionInput = li.querySelector('.edit-meta-description'); 10882 const hideMenuInput = li.querySelector('.edit-hide-menu'); 10883 const overlayMenuInput = li.querySelector('.edit-overlay-menu'); 10884 const actsAsMenuParentInput = li.querySelector('.edit-acts-as-menu-parent'); 10885 const passwordProtectedInput = li.querySelector('.edit-password-protected'); 10886 const passwordInput = li.querySelector('.edit-password'); 10887 const noIndexInput = li.querySelector('.edit-no-index'); 10888 10889 const title = titleInput ? titleInput.value.trim() : ''; 10890 const newSlugRaw = slugInput ? slugInput.value.trim() : null; 10891 const externalUrl = externalUrlInput ? externalUrlInput.value.trim() : ''; 10892 const metaTitle = metaTitleInput ? metaTitleInput.value.trim() : ''; 10893 const metaDescription = metaDescriptionInput ? metaDescriptionInput.value.trim() : ''; 10894 const hideMenu = hideMenuInput ? hideMenuInput.checked : false;
10895 const overlayMenu = overlayMenuInput ? overlayMenuInput.checked : false; 10896 const passwordProtected = passwordProtectedInput ? passwordProtectedInput.checked : false; 10897 const password = passwordInput ? passwordInput.value.trim() : ''; 10898 // noIndex is stored explicitly so it survives visibility changes without reverting 10899 const noIndex = noIndexInput ? noIndexInput.checked : undefined; 10900 10901 const gallery = findGalleryById(galleries, id); 10902 if (!gallery) { 10903 console.error("Could not find gallery object for saveEdit, ID:", id); 10904 renderGalleries(); 10905 return; 10906 } 10907 10908 if (!title && !gallery.isSpacer) { 10909 alert('Please enter a title'); 10910 return; 10911 } 10912 10913 // Validate password protection 10914 if (passwordProtected && !password) { 10915 alert('Please enter a password when password protection is enabled'); 10916 return; 10917 } 10918 10919 // Only mutate the opt-in property when the user explicitly changes this checkbox. 10920 // This preserves the serialized shape and behavior of every existing item. 10921 if (actsAsMenuParentInput) { 10922 const wasNavigableMenuParent = gallery.actsAsMenuParent === true; 10923 const wantsNavigableMenuParent = actsAsMenuParentInput.checked === true; 10924 if (wasNavigableMenuParent && !wantsNavigableMenuParent) { 10925 const hasNestedItems = galleries.some(item => item.parentId === gallery.id); 10926 if (hasNestedItems) { 10927 alert('Move all nested menu items out of this item before turning off âAllow nested menu itemsâ.'); 10928 return; 10929 } 10930 delete gallery.actsAsMenuParent; 10931 } else if (!wasNavigableMenuParent && wantsNavigableMenuParent) { 10932 gallery.actsAsMenuParent = true; 10933 } 10934 } 10935 10936 gallery.title = title; 10937 // Sidebar SEO fields are only shown for galleries; do not overwrite stored values when saving a page. 10938 if (!gallery.isPage) { 10939 gallery.metaTitle = metaTitle; 10940 gallery.metaDescription = metaDescription; 10941 } 10942 // Hide-menu wins if both are somehow checked; overlay is opt-in and omitted when off. 10943 if (hideMenu) { 10944 gallery.hideMenuOnPage = true; 10945 delete gallery.overlayMenuOnPage; 10946 } else { 10947 gallery.hideMenuOnPage = hideMenu; 10948 if (overlayMenu) { 10949 gallery.overlayMenuOnPage = true; 10950 } else { 10951 delete gallery.overlayMenuOnPage; 10952 } 10953 } 10954 // Persist the explicit noIndex choice so it survives future visibility toggles 10955 if (noIndex !== undefined) gallery.noIndex = noIndex; 10956 10957 // Save password protection settings 10958 gallery.passwordProtected = passwordProtected; 10959 if (passwordProtected) { 10960 gallery.password = password; 10961 } else { 10962 delete gallery.password; // Remove password if protection is disabled 10963 } 10964 10965 if ((gallery.isPage || gallery.isIntegrated) && newSlugRaw !== null) { 10966 const originalSlug = gallery.slug || (gallery.url && gallery.url.startsWith('/') ? gallery.url.substring(1) : ''); 10967 let sanitizedSlug = slugify(newSlugRaw); 10968 10969 if (!sanitizedSlug) { 10970 sanitizedSlug = slugify(gallery.title); 10971 } 10972 10973 if (sanitizedSlug !== originalSlug) { 10974 const otherGalleries = galleries.filter(g => g.id !== id); 10975 gallery.slug = ensureUniqueSlug(sanitizedSlug, otherGalleries); 10976 gallery.url = `/${gallery.slug}`; 10977 } else { 10978 if (gallery.url !== `/${gallery.slug}`) { 10979 gallery.url = `/${gallery.slug}`; 10980 } 10981 } 10982 } 10983 10984 // Update external URL if this is an external link 10985 if (gallery.isExternal && externalUrl) { 10986 gallery.url = externalUrl; 10987 } 10988 10989 saveGalleries(); 10990 renderGalleries(); 10991 10992 // If the currently active gallery was just edited, re-apply menu visibility 10993 if (gallery.id === activeGalleryId) { 10994 const bodyEl = document.body; 10995 if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 10996 bodyEl.classList.add('menu-hidden-on-page'); 10997 } else { 10998 bodyEl.classList.remove('menu-hidden-on-page'); 10999 } 11000 applyMenuOverlayClassForGallery(gallery); 11001 } 11002} 11003// Helper function to check if a gallery is an ancestor of another 11004function hasAncestor(galleryId, ancestorId) { 11005if (galleryId === ancestorId) return true; 11006 11007const gallery = galleries.find(g => g.id === galleryId); 11008if (!gallery || !gallery.parentId) return false; 11009 11010if (gallery.parentId === ancestorId) return true; 11011 11012return hasAncestor(gallery.parentId, ancestorId); 11013} 11014 11015function deleteGallery(id, event) { 11016 event.stopPropagation(); 11017 const gallery = galleries.find(g => g.id === id); 11018 const title = gallery && (gallery.title || gallery.slug || 'this item') || 'this item'; 11019 if (!confirm('Are you sure you want to delete "' + title + '"?')) { 11020 return; 11021 }
11022 const overlay = showDeletionOverlay(); 11023 overlay.updateMessage('Saving...'); 11024 galleries = galleries.filter(g => g.id !== id); 11025 if (typeof window !== 'undefined') { 11026 window.galleries = galleries; 11027 } 11028 if (activeGalleryId === id) { 11029 activeGalleryId = null; 11030 const frame = document.getElementById('galleryFrame'); 11031 if (frame) frame.src = ''; 11032 } 11033 saveGalleries() 11034 .then(function(success) { 11035 renderGalleries(); 11036 overlay.hide(); 11037 if (success === false) { 11038 alert('Failed to delete. Please try again.'); 11039 } 11040 }) 11041 .catch(function(err) { 11042 overlay.hide(); 11043 console.error('Error deleting item:', err); 11044 alert('Failed to delete. Please try again.'); 11045 }); 11046} 11047 11048function debugNestedStructure() { 11049console.group('Current Gallery Structure'); 11050 11051function printGallery(gallery, level = 0) { 11052const indent = ' '.repeat(level); 11053console.log(`${indent}${gallery.title} (ID: ${gallery.id}, Parent: ${gallery.parentId || 'none'})`); 11054 11055if (gallery.children && gallery.children.length > 0) { 11056 gallery.children.forEach(child => printGallery(child, level + 1)); 11057} 11058} 11059 // Create a tree structure for debugging 11060const galleryTree = createGalleryTree(galleries); 11061galleryTree.forEach(gallery => printGallery(gallery)); 11062 11063console.groupEnd(); 11064} 11065 11066function toggleHome(id, event) { 11067 event.stopPropagation(); 11068 11069 console.log('Setting home page to gallery:', id); 11070 11071 // Find gallery 11072 const gallery = galleries.find(g => g.id === id); 11073 if (!gallery) return; 11074 11075 // Check if this gallery is already set as home 11076 const alreadyHome = gallery.isHomePage === true; 11077 11078 // Set all galleries to not be the home page 11079 galleries.forEach(g => { 11080 g.isHomePage = false; 11081 }); 11082 11083 // Toggle this gallery's home status - if it was already home, we're unsetting it 11084 gallery.isHomePage = !alreadyHome; 11085 11086 // Save changes 11087 saveGalleries(); 11088 11089 // Update UI 11090 renderGalleries(); 11091 11092 // If this gallery is now the home page, display a message 11093 if (gallery.isHomePage) { 11094 const title = gallery.title || 'This page'; 11095 showSuccessMessage(`${title} is now set as the Home page`); 11096 } 11097} 11098 11099// Function to toggle gallery visibility 11100function toggleVisibility(id, event) { 11101 event.stopPropagation(); 11102 11103 const gallery = galleries.find(g => g.id === id); 11104 if (gallery) { 11105 // Toggle the visibility 11106 gallery.visible = gallery.visible === false ? true : false; 11107 11108 // Save the change to server 11109 saveGalleries(); 11110 11111 // Update UI - important to toggle the class on the button itself 11112 const toggle = event.currentTarget; 11113 if (toggle) { 11114 if (gallery.visible === false) { 11115 toggle.classList.add('hidden'); 11116 11117 // Update the SVG icon 11118 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>'; 11119 } else { 11120 toggle.classList.remove('hidden'); 11121 11122 // Update the SVG icon 11123 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>'; 11124 } 11125 } 11126 11127 // Update parent row class (#galleryTree row, not horizontal submenu li) 11128 const listItem = getGalleryTreeListItem(id, event); 11129 if (listItem) { 11130 if (gallery.visible === false) { 11131 listItem.classList.add('hidden-gallery'); 11132 } else { 11133 listItem.classList.remove('hidden-gallery'); 11134 } 11135 } 11136 11137 // Only clear iframe if not in edit mode 11138 if (id === activeGalleryId && gallery.visible === false && !isEditing) { 11139 document.getElementById('galleryFrame').src = ''; 11140 } 11141 11142 // Re-render menus based on current layout 11143 const currentLayout = document.body.classList.contains('menu-layout-horizontal') ? 'horizontal' : 11144 document.body.classList.contains('menu-layout-top') ? 'top' : 'sidebar'; 11145 11146 if (currentLayout === 'horizontal' && typeof window.renderHorizontalMenu === 'function') { 11147 window.renderHorizontalMenu(); 11148 } 11149 11150 // Also re-render the sidebar gallery tree to ensure hidden-gallery class is properly applied 11151 // This is especially important when horizontal layout is active but we're viewing the sidebar in edit mode 11152 if (typeof window.renderGalleries === 'function') { 11153 window.renderGalleries(); 11154 } else if (typeof renderGalleries === 'function') { 11155 renderGalleries(); 11156 } 11157 } 11158} 11159 11160 11161 11162/** 11163* Simplified loadGallery function that follows the same content swap pattern 11164* @param {number} id - The gallery ID 11165* @param {Event} event - Optional event object 11166*/ 11167// Enhanced loadGallery function with script cleanup 11168async function loadGallery(id, event) { 11169 11170// Stop event propagation if provided; preventDefault keeps <a href> SPA navigation 11171if (event) { 11172 if (typeof event.preventDefault === 'function') event.preventDefault(); 11173 event.stopPropagation(); 11174} 11175 11176const galleryContainer = document.querySelector('.gallery-container'); 11177if (galleryContainer) { 11178 // Apply immediate hiding styles 11179 galleryContainer.style.opacity = '0'; 11180} 11181 11182console.log('Loading gallery with ID:', id); 11183
11184if (typeof dismissGalleryPasswordOverlay === 'function') { 11185 dismissGalleryPasswordOverlay(); 11186} 11187 11188// Find gallery by ID 11189const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible 11190 11191if (!gallery) { 11192 console.warn('No gallery found with ID:', id); 11193 if (galleryContainer) { 11194 galleryContainer.style.opacity = '1'; 11195 } 11196 return; 11197} 11198 11199// Check if this is a page, if so use loadPage instead 11200if (isPageMenuItem(gallery)) { 11201 // Use the loadPage function defined in this scope (or window.loadPage if you prefer) 11202 return loadPage(id, event); 11203} 11204 11205// Skip submenu items 11206if (gallery.isSubmenu) { 11207 console.log('Gallery is submenu, skipping URL update'); 11208 toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available 11209 if (galleryContainer) { 11210 galleryContainer.style.opacity = '1'; 11211 } // Show container 11212 return; 11213} 11214 11215console.log('Found gallery:', gallery.title); 11216 11217window._activeContentType = 'gallery'; 11218beginHydraContentLoad('gallery', gallery.id, gallery.pageId); 11219if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { 11220 window.PageManager.clearCurrentPage(); 11221} 11222 11223// CRITICAL: Set active gallery ID consistently in both global and window scope 11224console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')'); 11225window.activeGalleryId = id; 11226activeGalleryId = id; // Ensure both variables are synchronized 11227 11228// Save reference to which gallery we're loading (for later verification) 11229window.currentLoadingGalleryId = id; 11230 11231// CRITICAL: Thorough cleanup with await to ensure completion 11232if (typeof window.removeGalleryScriptsWithPause === 'function') { 11233 await window.removeGalleryScriptsWithPause(); 11234} 11235 11236 11237// Clear all existing content from gallery container 11238if (!galleryContainer) { 11239 console.error('Gallery container not found'); 11240 return; 11241} 11242 11243// Force clear all content 11244galleryContainer.innerHTML = ''; 11245 11246 11247 // CRITICAL: Double-check active gallery ID before updating UI 11248 if (window.activeGalleryId !== id || activeGalleryId !== id) { 11249 console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id); 11250 window.activeGalleryId = id; 11251 activeGalleryId = id; 11252 } 11253 11254// Apply menu visibility based on the gallery's setting and edit mode 11255const bodyEl = document.body; 11256if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 11257 bodyEl.classList.add('menu-hidden-on-page'); 11258} else { 11259 bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden 11260} 11261applyMenuOverlayClassForGallery(gallery); 11262 11263// Update UI state using explicit window function references or local fallbacks 11264if (typeof window.updateActiveStates === 'function') { 11265 window.updateActiveStates(); 11266} else if (typeof updateActiveStates === 'function') { 11267 updateActiveStates(); 11268} 11269 11270if (typeof window.updateMobileTitle === 'function') { 11271 window.updateMobileTitle(); 11272} else if (typeof updateMobileTitle === 'function') { 11273 updateMobileTitle(); 11274} 11275 11276if (typeof window.closeMobileMenu === 'function') { 11277 window.closeMobileMenu(); 11278} else if (typeof closeMobileMenu === 'function') { 11279 closeMobileMenu(); 11280} 11281 11282// Update URL with gallery slug 11283if (typeof window.updateURLWithGallerySlug === 'function') { 11284 window.updateURLWithGallerySlug(gallery); 11285} else if (typeof updateURLWithGallerySlug === 'function') { 11286 updateURLWithGallerySlug(gallery); 11287} 11288 11289 // Create gallery container and load content 11290const neonGalleryContainer = document.createElement('div'); 11291neonGalleryContainer.id = 'neon-gallery-container'; 11292neonGalleryContainer.className = 'gallery-direct-content'; 11293neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString()); 11294neonGalleryContainer.style.width = '100%'; 11295neonGalleryContainer.style.height = '100%'; 11296galleryContainer.appendChild(neonGalleryContainer); 11297 11298// Add a cache-busting parameter to ensure script is freshly loaded 11299const timestamp = Date.now(); 11300 11301// Set up gallery parameters 11302window.Parameters = { 11303 SiteAlias: window.location.hostname, 11304 InitialPageUuid: gallery.pageId, 11305 InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available 11306 isInEditor: window.isEditing || false, // Assuming window.isEditing is available 11307 siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available 11308 isHydra: true,
11309 galleryInstanceId: timestamp, 11310 // Added loadedGalleryId as per your original function 11311 loadedGalleryId: gallery.id 11312}; 11313 11314// Debug: Check what galleryOptions contains 11315console.error('ð INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL); 11316 11317// Set up gallery config 11318window.neonGalleryConfig = { 11319 useData: true, 11320 useCDN: true, 11321 version: 'live', 11322 manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod", 11323 layoutType: gallery.galleryOptions?.layoutType || "grid", 11324 // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence 11325 // This ensures that only properties from the current gallery are included, not from previous galleries 11326 ...(gallery.galleryOptions || {}), 11327 // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script 11328 // These fields are required for the gallery to determine if it should use Load Network racing 11329 loadTxId: gallery.galleryOptions?.loadTxId || null, 11330 loadPermaURL: gallery.galleryOptions?.loadPermaURL || null, 11331 // Cloudflare CDN NDJSON (unique id) â when present, gallery loads CDN-first (3s) then Fly 11332 cdnNdjsonId: gallery.galleryOptions?.cdnNdjsonId || null, 11333 cdnNdjsonUrl: gallery.galleryOptions?.cdnNdjsonUrl || null, 11334 cdnNdjsonStableUrl: gallery.galleryOptions?.cdnNdjsonStableUrl || gallery.galleryOptions?.embedPointerUrl || null, 11335 embedPointerUrl: gallery.galleryOptions?.embedPointerUrl || gallery.galleryOptions?.cdnNdjsonStableUrl || null, 11336 cdnSaveTimestamp: gallery.galleryOptions?.cdnSaveTimestamp || null, 11337 siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available 11338 // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread 11339 galleryId: gallery.id, 11340 galleryInstanceId: timestamp 11341}; 11342 11343console.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); 11344 11345// Create and add the gallery script with cache-busting 11346// Use Worker route (avoids CDN SSL issues) 11347const script = document.createElement('script'); 11348const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; 11349script.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; // Using versioned file 11350script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute 11351script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute 11352console.log('Loading gallery-main script from Worker:', script.src); 11353script.onerror = function() { 11354 console.error('â Gallery script failed, retrying via proxy:', script.src); 11355 const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 11356 script.src = proxyUrl; 11357 script.onerror = function() { 11358 console.error('â Proxy failed, trying storage:', proxyUrl); 11359 script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app'); 11360 }; 11361}; 11362 11363// Added script.onload from your original function 11364script.onload = function() { 11365 if (window.currentLoadingGalleryId !== gallery.id) { 11366 console.warn('Gallery ID mismatch! Expected:', gallery.id, 11367 'Current:', window.currentLoadingGalleryId); 11368 } 11369 console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`); 11370}; 11371 11372 11373if (galleryContainer) { 11374 setTimeout(() => { 11375 galleryContainer.style.opacity = '1'; 11376 }, 50); // Small delay to ensure content is ready 11377 } 11378 11379console.log(`Loading fresh gallery script for: ${gallery.title}`); 11380document.body.appendChild(script); 11381} 11382 11383 11384 11385function removeGalleryScriptsWithPause() { 11386 return new Promise(resolve => { 11387 console.log('Performing comprehensive gallery cleanup (v2 - refined selectors)'); 11388 11389 // 1. Capture and clear any gallery data in localStorage (remains the same) 11390 try { 11391 const localStorageKeys = Object.keys(localStorage); 11392 const galleryStorageKeys = localStorageKeys.filter(key => 11393 key.includes('gallery') || 11394 key.includes('neon') || 11395 key.includes('lightbox') 11396 ); 11397 galleryStorageKeys.forEach(key => { 11398 localStorage.removeItem(key); 11399 }); 11400 } catch (e) { 11401 console.warn('Error clearing localStorage:', e); 11402 } 11403 11404 // @ts-ignore 11405 window._galleryPageContext = null; // (remains the same) 11406 11407 // Keep HP gallery CSS. Page-element edit/view remounts skip HP reload; 11408 // stripping neon-gallery-hp-hydra.css zeros masonry-item / container margins. 11409 const galleryStyles = document.querySelectorAll('style[data-gallery], link[href*="neon-gallery"]');
11410 galleryStyles.forEach(style => { 11411 const href = (style && style.href) ? String(style.href) : ''; 11412 if (style && style.id === 'neon-gallery-css') return; 11413 if (href.indexOf('neon-gallery-hp-hydra') !== -1) return; 11414 if (style && style.parentNode) { 11415 style.parentNode.removeChild(style); 11416 } 11417 }); 11418 11419 // 4. Remove lightbox elements (more specific selectors now) 11420 // These are specific IDs and classes for lightbox components. 11421 const lightboxComponentSelectors = [ 11422 '#neon-lightbox', // Main lightbox container by ID 11423 '.neon-lightbox', // Alternative class for main container 11424 '.lightbox-container',// General container class if used 11425 // Add any other *specific* classes or IDs your lightbox system uses for its top-level elements 11426 ]; 11427 const lightboxElements = document.querySelectorAll(lightboxComponentSelectors.join(', ')); 11428 lightboxElements.forEach(lightboxEl => { 11429 if (lightboxEl && lightboxEl.parentNode && lightboxEl !== document.body && lightboxEl !== document.documentElement) { 11430 // console.log('Removing lightbox component:', lightboxEl.id || lightboxEl.className); 11431 lightboxEl.parentNode.removeChild(lightboxEl); 11432 } 11433 }); 11434 11435 // 5. Remove other gallery-specific DOM elements 11436 // REMOVED: '[class*="lightbox"]', '[id*="lightbox"]' from this list 11437 // The selector '[class*="neon-"]' is still broad but less likely to match body unless you add "neon-" class to body. 11438 const galleryElementSelectors = [ 11439 '.neon-gallery-wrapper', '.gallery-container .fullscreen-overlay', 11440 '.neon-gallery-modal', '.gallery-tooltip', '.neon-gallery-context-menu', 11441 '.neon-thumbnails', '.neon-image', '.neon-caption', 11442 '.neon-controls', '.neon-pagination', 11443 '[class*="neon-"]', // BE CAUTIOUS: If this matches body or critical layout elements, it can cause issues. 11444 '[data-gallery-id]', '[data-image-id]', '.image-grid', '.image-masonry' 11445 // Ensure none of these selectors inadvertently match document.body or critical layout containers 11446 ]; 11447 const galleryElements = document.querySelectorAll(galleryElementSelectors.join(', ')); 11448 11449 if (galleryElements.length > 0) { 11450 galleryElements.forEach(el => { 11451 // CRITICAL FIX: Add checks to ensure we don't remove body or html 11452 if (el && el.parentNode && el !== document.body && el !== document.documentElement) { 11453 // console.log('Removing gallery-specific DOM element:', el.id || el.className); 11454 el.parentNode.removeChild(el); 11455 } else if (el === document.body || el === document.documentElement) { 11456 console.warn(`[CRITICAL WARNING] Attempt to remove document.body or document.documentElement blocked by selector: ${el.id || el.className}. Review your selectors.`); 11457 } 11458 }); 11459 } 11460 11461 // 6. Find and remove ALL gallery scripts (remains the same) 11462 const scripts = document.querySelectorAll( 11463 'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' + 11464 'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]' 11465 ); 11466 scripts.forEach(script => { 11467 if (script && script.parentNode) { 11468 // console.log('Removing script:', script.src || script.getAttribute('data-gallery-id')); 11469 script.parentNode.removeChild(script); 11470 } 11471 }); 11472 11473 // 6b. Specifically remove gallery options scripts (but NOT CSS - CSS is needed across galleries) 11474 const optionsScripts = document.querySelectorAll( 11475 'script[src*="galleryOptions"], script[data-options-js]' 11476 ); 11477 optionsScripts.forEach(script => { 11478 if (script && script.parentNode) { 11479 console.log('Removing gallery options script:', script.src || script.getAttribute('data-options-js')); 11480 script.parentNode.removeChild(script); 11481 } 11482 }); 11483 11484 // NOTE: We intentionally do NOT remove gallery options CSS here 11485 // The CSS is needed across galleries and doesn't cause conflicts like scripts do 11486 11487 setTimeout(() => { 11488 const remainingScripts = document.querySelectorAll( 11489 'script[src*="neon-gallery-main-hydra"], script[src*="neon-gallery-main-dev.js"], ' + 11490 'script[src*="gallery"], script[data-gallery-id], script[data-gallery], script[data-timestamp]' 11491 ); 11492 if (remainingScripts.length > 0) { 11493 // console.warn(`[DEBUG] ${remainingScripts.length} gallery scripts still found after removal attempt.`); 11494 } 11495 }, 50); 11496 11497 // 7. Reset ALL known global state variables used by the gallery (remains the same) 11498 const resetGlobals = [ 11499 'neonGalleryInitComplete', 'neonGalleryInitInProgress', 11500 'neonGalleryLoaded', 'neonGalleryConfig', 'neonGalleryState', 11501 'neonGalleryCache', 'neonGalleryImages', 'neonGallerySettings', 11502 'neonLightbox', 'neonGalleryEventListeners', 'neonGalleryInstance', 11503 'currentGalleryId', 'galleryData', 'imageCache', 'thumbnailCache', 11504 ]; 11505
11506 resetGlobals.forEach(prop => { 11507 // @ts-ignore 11508 if (window[prop] !== undefined) { 11509 // @ts-ignore 11510 window[prop] = null; 11511 } 11512 }); 11513 // Keep the bootstrap wipe flag. Clearing it let a re-injected main script 11514 // replace PageManager embed config with the default "mod" collection. 11515 if (typeof window.resetNeonGalleryInitGates === 'function') { 11516 window.resetNeonGalleryInitGates('removeGalleryScriptsWithPause'); 11517 } 11518 11519 // 7b. Reset gallery options panel state 11520 if (window._editorButtonVisibilityInterval) { 11521 clearInterval(window._editorButtonVisibilityInterval); 11522 window._editorButtonVisibilityInterval = null; 11523 } 11524 const editorBtn = document.getElementById('editorButton'); 11525 if (editorBtn) { 11526 editorBtn.classList.add('gallery-options-force-hidden'); 11527 editorBtn.style.setProperty('display', 'none', 'important'); 11528 editorBtn.style.pointerEvents = ''; 11529 } 11530 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 11531 window.syncGalleryOptionsButtonVisibility(); 11532 } 11533 // Reset optionsVisible flag if it exists on window (for cross-module access) 11534 // @ts-ignore 11535 if (window.optionsVisible !== undefined) { 11536 // @ts-ignore 11537 window.optionsVisible = false; 11538 } 11539 11540 // Reset options-related globals 11541 const optionsGlobals = [ 11542 'generateOptionsUI', 'openOptionsOverlay', 'showOptionsPanel', 11543 'hideOptionsPanel', 'toggleOptionsOverlay', 'optionsVisible' 11544 ]; 11545 optionsGlobals.forEach(prop => { 11546 // @ts-ignore 11547 if (window[prop] !== undefined && typeof window[prop] !== 'function') { 11548 // Only reset non-function properties (like flags) 11549 // Functions should remain available 11550 // @ts-ignore 11551 if (prop === 'optionsVisible') { 11552 // @ts-ignore 11553 window[prop] = false; 11554 } 11555 } 11556 }); 11557 11558 // Reset EnhancedGallerySystem so a pending initPromise cannot hang forever 11559 // after scripts it was waiting on were just removed from the DOM. 11560 if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.resetInitializationState === 'function') { 11561 window.EnhancedGallerySystem.resetInitializationState(); 11562 } 11563 11564 // Reset PageManager page-embedded gallery tracking 11565 window.neonGalleryScriptLoaded = false; 11566 window.pageManagerGalleryInitBlocked = false; 11567 if (window.pageManagerGalleryContainers) { 11568 window.pageManagerGalleryContainers.clear(); 11569 } 11570 11571 // Remove orphaned lightbox keyboard listeners (survive script tag removal) 11572 if (typeof window.removeAllLightboxKeyboardListeners === 'function') { 11573 window.removeAllLightboxKeyboardListeners(); 11574 } else if (window.currentLightboxKeyboardListener) { 11575 document.removeEventListener('keydown', window.currentLightboxKeyboardListener); 11576 window.currentLightboxKeyboardListener = null; 11577 window.lightboxKeyboardListenerActive = false; 11578 } 11579 11580 // Short pause before resolving 11581 setTimeout(resolve, 150); 11582 }); 11583} 11584 11585 11586// Fixed clearAddForm function with proper variable names 11587function clearAddForm() { 11588// Clear the title input 11589const titleInput = document.getElementById('galleryTitle'); 11590if (titleInput) { 11591titleInput.value = ''; 11592} 11593 11594// Clear the URL input 11595const urlInput = document.getElementById('galleryUrl'); 11596if (urlInput) { 11597urlInput.value = ''; 11598} 11599 11600// Reset the parent selection 11601const parentSelect = document.getElementById('galleryParent'); 11602if (parentSelect) { 11603parentSelect.value = ''; 11604} 11605 11606// Reset radio buttons to Gallery (integrated) as default 11607const galleryRadio = document.querySelector('input[name="galleryType"][value="integrated"]'); 11608if (galleryRadio) { 11609galleryRadio.checked = true; 11610} 11611 11612// Hide URL input container when default is Gallery 11613const urlInputContainer = document.getElementById('urlInputContainer'); 11614if (urlInputContainer) { 11615urlInputContainer.style.display = 'none'; 11616} else { 11617console.log('URL input container not found'); 11618} 11619} 11620 11621/** 11622 * Ensures window.galleries is synchronized with the global galleries variable 11623 * Call this function after any modification to gallery settings 11624 * 11625 * @returns {boolean} True if synchronization was successful 11626 */ 11627/** 11628 * Enhanced synchronizeGalleries function that checks ALL galleries 11629 * not just imported ones, and ensures settings are preserved 11630 * @returns {boolean} Success indicator 11631 */ 11632function synchronizeGalleries() { 11633 console.log('Starting enhanced gallery synchronization...'); 11634 11635 if (typeof galleries !== 'undefined' && Array.isArray(galleries)) { 11636 // First check if window.galleries exists and is different from galleries 11637 const needsSync = !window.galleries || 11638 !Array.isArray(window.galleries) || 11639 window.galleries.length !== galleries.length; 11640 11641 // ENHANCED: Check ALL galleries for options preservation, not just imported ones 11642 if (!needsSync && window.galleries.length > 0) { 11643 // Find all galleries with galleryOptions 11644 const galleriesWithOptions = galleries.filter(g => 11645 g.galleryOptions && Object.keys(g.galleryOptions).length > 4 11646 ); 11647 11648 if (galleriesWithOptions.length > 0) { 11649 // Check ALL galleries with substantial options, not just a sample 11650 let syncNeeded = false;
11651 11652 // Use for loop so we can break early if we find a mismatch 11653 for (let i = 0; i < galleriesWithOptions.length; i++) { 11654 const gallery = galleriesWithOptions[i]; 11655 const windowGallery = window.galleries.find(g => g.id === gallery.id); 11656 11657 if (windowGallery) { 11658 // Get options depth 11659 const galleryOptionsDepth = gallery.galleryOptions ? 11660 Object.keys(gallery.galleryOptions).length : 0; 11661 11662 const windowOptionsDepth = windowGallery.galleryOptions ? 11663 Object.keys(windowGallery.galleryOptions).length : 0; 11664 11665 // If options depth differs significantly, we need to sync 11666 if (Math.abs(galleryOptionsDepth - windowOptionsDepth) > 2) { 11667 console.log(`Gallery sync needed: Options mismatch detected for ${gallery.title}`); 11668 console.log(`Options count: global=${galleryOptionsDepth}, window=${windowOptionsDepth}`); 11669 11670 syncNeeded = true; 11671 break; // No need to check more galleries 11672 } 11673 11674 // Additional check: Look for specific important gallery settings that should be preserved 11675 // Only do this check for galleries with substantial settings 11676 if (galleryOptionsDepth > 5 && windowOptionsDepth > 5) { 11677 // Critical keys that indicate gallery customization 11678 const criticalKeys = ['columns', 'spacing', 'layoutType', 'startInSingles', 11679 'autoplaySingles', 'autoplayRandomStart', 'lightboxBgColor']; 11680 11681 // Check if any critical keys are missing in window.galleries 11682 const missingKeys = criticalKeys.filter(key => 11683 gallery.galleryOptions[key] !== undefined && 11684 windowGallery.galleryOptions[key] === undefined 11685 ); 11686 11687 if (missingKeys.length > 0) { 11688 console.log(`Gallery sync needed: Critical settings missing in window.galleries for ${gallery.title}`); 11689 console.log(`Missing keys: ${missingKeys.join(', ')}`); 11690 11691 syncNeeded = true; 11692 break; // No need to check more galleries 11693 } 11694 } 11695 } 11696 } 11697 11698 // If we detected a need to sync, do it now 11699 if (syncNeeded) { 11700 // Ensure we preserve rich gallery settings and per-page custom HTML fields 11701 window.galleries = galleries.map(gallery => { 11702 const windowGallery = window.galleries.find(g => g.id === gallery.id); 11703 if (!windowGallery) return gallery; 11704 11705 let merged = { ...gallery }; 11706 11707 if (windowGallery.galleryOptions && gallery.galleryOptions) { 11708 const globalOptionsDepth = Object.keys(gallery.galleryOptions).length; 11709 const windowOptionsDepth = Object.keys(windowGallery.galleryOptions).length; 11710 if (windowOptionsDepth > globalOptionsDepth) { 11711 merged = { 11712 ...merged, 11713 galleryOptions: { 11714 ...windowGallery.galleryOptions, 11715 ...gallery.galleryOptions 11716 } 11717 }; 11718 } 11719 } 11720 11721 if (windowGallery.customPageHtmlR2Key || windowGallery.customPageHtmlEnabled) { 11722 merged = { 11723 ...merged, 11724 customPageHtmlEnabled: windowGallery.customPageHtmlEnabled, 11725 customPageHtmlR2Key: windowGallery.customPageHtmlR2Key, 11726 customPageHtmlFilename: windowGallery.customPageHtmlFilename 11727 }; 11728 } 11729 11730 return merged; 11731 }); 11732 11733 console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`); 11734 return true; 11735 } 11736 } 11737 } 11738 11739 // If we determined sync is needed (or as a precaution), update window.galleries 11740 if (needsSync || !window.galleries) { 11741 if (window.galleries && Array.isArray(window.galleries)) { 11742 galleries = galleries.map(function(gallery) { 11743 const windowGallery = window.galleries.find(function(g) { return g && g.id === gallery.id; }); 11744 if (!windowGallery) return gallery; 11745 if (windowGallery.customPageHtmlR2Key || windowGallery.customPageHtmlEnabled) { 11746 return { 11747 ...gallery, 11748 customPageHtmlEnabled: windowGallery.customPageHtmlEnabled, 11749 customPageHtmlR2Key: windowGallery.customPageHtmlR2Key, 11750 customPageHtmlFilename: windowGallery.customPageHtmlFilename 11751 }; 11752 } 11753 return gallery; 11754 }); 11755 } 11756 window.galleries = [...galleries]; // Create a shallow copy to ensure different reference 11757 console.log(`Synchronized window.galleries with global galleries (${galleries.length} items)`); 11758 } else { 11759 console.log('Gallery synchronization check: No sync needed'); 11760 } 11761 11762 // Verify that all galleries with rich settings have their full options preserved 11763 const richGalleries = galleries.filter(g => 11764 g.galleryOptions && Object.keys(g.galleryOptions).length > 5 11765 ); 11766 11767 console.log(`Status: Found ${richGalleries.length} galleries with rich settings`); 11768 11769 // Final verification - keep both arrays aligned without dropping page-level fields 11770 if (window.galleries !== galleries) { 11771 console.warn('References not identical after sync - merging page fields into global galleries'); 11772 if (window.galleries && Array.isArray(window.galleries)) { 11773 galleries = galleries.map(function(gallery) { 11774 const windowGallery = window.galleries.find(function(g) { return g && g.id === gallery.id; }); 11775 if (!windowGallery) return gallery; 11776 return { 11777 ...gallery, 11778 customPageHtmlEnabled: windowGallery.customPageHtmlEnabled ?? gallery.customPageHtmlEnabled, 11779 customPageHtmlR2Key: windowGallery.customPageHtmlR2Key ?? gallery.customPageHtmlR2Key, 11780 customPageHtmlFilename: windowGallery.customPageHtmlFilename ?? gallery.customPageHtmlFilename, 11781 hideMenuOnPage: windowGallery.hideMenuOnPage ?? gallery.hideMenuOnPage, 11782 overlayMenuOnPage: windowGallery.overlayMenuOnPage ?? gallery.overlayMenuOnPage 11783 }; 11784 }); 11785 } 11786 window.galleries = galleries; 11787 return true; 11788 } 11789 11790 return true; 11791 } else { 11792 console.warn('Cannot synchronize galleries: global galleries variable is undefined or not an array'); 11793 11794 // If we have window.galleries but no global galleries, try to reverse-sync 11795 if (window.galleries && Array.isArray(window.galleries) && window.galleries.length > 0) { 11796 console.log('Attempting reverse sync: updating global galleries from window.galleries');
11797 try { 11798 // This is risky and should only happen in unusual circumstances 11799 galleries = window.galleries; 11800 return true; 11801 } catch (e) { 11802 console.error('Could not reverse-sync galleries:', e); 11803 } 11804 } 11805 11806 return false; 11807 } 11808} 11809 11810 11811 11812/** 11813 * Loads NDJSON content from the original gallery 11814 * @param {string} siteId - The site ID 11815 * @param {string} pageId - The page ID of the original gallery 11816 * @returns {Promise<string|null>} - The NDJSON content or null if not found/error 11817 */ 11818async function loadGalleryNDJSON(siteId, pageId) { 11819 console.error('[NDJSON Duplication] loadGalleryNDJSON called with siteId:', siteId, 'pageId:', pageId); 11820 11821 if (!siteId || !pageId) { 11822 console.error('[NDJSON Duplication] ERROR: Cannot load NDJSON - missing siteId or pageId', { siteId: siteId, pageId: pageId }); 11823 return null; 11824 } 11825 11826 try { 11827 const ndjsonFilename = `${siteId}_${pageId}.ndjson`; 11828 const url = `https://fly.storage.tigris.dev/ns-bridge-pub/${ndjsonFilename}`; 11829 11830 console.error('[NDJSON Duplication] Attempting to load NDJSON from:', url); 11831 console.error('[NDJSON Duplication] Filename:', ndjsonFilename); 11832 11833 const response = await fetch(url); 11834 11835 console.error('[NDJSON Duplication] Fetch response status:', response.status, response.statusText); 11836 11837 if (!response.ok) { 11838 if (response.status === 404) { 11839 console.error('[NDJSON Duplication] ERROR: NDJSON file not found (404) - gallery may not have been published yet'); 11840 console.error('[NDJSON Duplication] Attempted URL:', url); 11841 return null; 11842 } 11843 const errorText = await response.text().catch(() => 'Could not read error text'); 11844 console.error('[NDJSON Duplication] ERROR: Failed to load NDJSON:', response.status, response.statusText, errorText); 11845 throw new Error(`Failed to load NDJSON: ${response.statusText}`); 11846 } 11847 11848 const ndjsonContent = await response.text(); 11849 console.error('[NDJSON Duplication] SUCCESS: Loaded NDJSON, length:', ndjsonContent.length, 'characters'); 11850 console.error('[NDJSON Duplication] First 200 chars:', ndjsonContent.substring(0, 200)); 11851 return ndjsonContent; 11852 } catch (error) { 11853 console.error('[NDJSON Duplication] ERROR: Exception loading NDJSON:', error); 11854 console.error('[NDJSON Duplication] Error message:', error.message); 11855 console.error('[NDJSON Duplication] Error stack:', error.stack); 11856 return null; 11857 } 11858} 11859 11860/** 11861 * Duplicates and publishes NDJSON for a gallery 11862 * @param {Object} originalGallery - The original gallery object 11863 * @param {Object} newGallery - The new gallery object with new pageId 11864 * @returns {Promise<boolean>} - Success indicator 11865 */ 11866async function duplicateGalleryNDJSON(originalGallery, newGallery) { 11867 console.error('[NDJSON Duplication] ===== STARTING duplicateGalleryNDJSON ====='); 11868 console.error('[NDJSON Duplication] Original gallery:', { 11869 id: originalGallery.id, 11870 title: originalGallery.title, 11871 pageId: originalGallery.pageId, 11872 classicGuid: originalGallery.classicGuid, 11873 hasGalleryOptions: !!originalGallery.galleryOptions, 11874 manualCollectionName: originalGallery.galleryOptions?.manualCollectionName, 11875 isClassicCollection: originalGallery.galleryOptions?.isClassicCollection 11876 }); 11877 console.error('[NDJSON Duplication] New gallery:', { 11878 id: newGallery.id, 11879 title: newGallery.title, 11880 pageId: newGallery.pageId 11881 }); 11882 11883 try { 11884 // Check if this is an NDJSON-based gallery (not classic collection) 11885 // IMPORTANT: A gallery can have a GUID-style manualCollectionName but still use NDJSON files 11886 // Only skip if it's explicitly marked as a classic collection OR has no pageId (which means no NDJSON) 11887 // Having a pageId means it uses NDJSON files, so we should duplicate them 11888 const isClassicCollection = originalGallery.galleryOptions?.isClassicCollection === true; 11889 11890 console.error('[NDJSON Duplication] Classic collection check:', { 11891 hasClassicGuid: !!originalGallery.classicGuid, 11892 manualCollectionName: originalGallery.galleryOptions?.manualCollectionName, 11893 startsWithGUID: originalGallery.galleryOptions?.manualCollectionName?.startsWith('G
11893UID='), 11894 isClassicCollectionFlag: originalGallery.galleryOptions?.isClassicCollection, 11895 hasPageId: !!originalGallery.pageId, 11896 isClassicCollection: isClassicCollection, 11897 willSkip: isClassicCollection && !originalGallery.pageId 11898 }); 11899 11900 // Only skip if explicitly marked as classic collection AND has no pageId 11901 // If it has a pageId, it uses NDJSON files and we should duplicate them 11902 if (isClassicCollection && !originalGallery.pageId) { 11903 console.error('[NDJSON Duplication] SKIP: Gallery is explicitly marked as classic collection with no pageId, skipping NDJSON duplication'); 11904 return true; // Not an error, just not applicable 11905 } 11906 11907 // Check if original gallery has a pageId 11908 if (!originalGallery.pageId) { 11909 console.error('[NDJSON Duplication] SKIP: Original gallery has no pageId, skipping NDJSON duplication'); 11910 console.error('[NDJSON Duplication] Original gallery keys:', Object.keys(originalGallery)); 11911 return true; // Not an error, just not applicable 11912 } 11913 11914 // Get siteId 11915 const siteId = window.siteId || originalGallery.siteId; 11916 console.error('[NDJSON Duplication] SiteId resolution:', { 11917 windowSiteId: window.siteId, 11918 gallerySiteId: originalGallery.siteId, 11919 resolvedSiteId: siteId 11920 }); 11921 11922 if (!siteId) { 11923 console.error('[NDJSON Duplication] ERROR: Cannot duplicate NDJSON - missing siteId'); 11924 console.error('[NDJSON Duplication] window.siteId:', window.siteId); 11925 console.error('[NDJSON Duplication] originalGallery.siteId:', originalGallery.siteId); 11926 return false; 11927 } 11928 11929 // Load original NDJSON 11930 console.error('[NDJSON Duplication] Loading original NDJSON for duplication...'); 11931 console.error('[NDJSON Duplication] Using siteId:', siteId, 'pageId:', originalGallery.pageId); 11932 const originalNdjson = await loadGalleryNDJSON(siteId, originalGallery.pageId); 11933 11934 if (!originalNdjson) { 11935 console.error('[NDJSON Duplication] ERROR: Could not load original NDJSON, gallery may not have been published yet'); 11936 console.error('[NDJSON Duplication] This means the original gallery\'s NDJSON file was not found'); 11937 return false; 11938 } 11939 11940 // Publish the duplicated NDJSON with new pageId using the same endpoint as edition publisher 11941 console.error('[NDJSON Duplication] Publishing duplicated NDJSON with new pageId:', newGallery.pageId); 11942 const dataFileName = `${siteId}_${newGallery.pageId}.ndjson`; 11943 console.error('[NDJSON Duplication] New filename:', dataFileName); 11944 console.error('[NDJSON Duplication] NDJSON content length:', originalNdjson.length); 11945 11946 try { 11947 // Hydra sites require Bearer auth (same token resolution as Edition Publisher) 11948 const publishHeaders = { 'Content-Type': 'application/json' }; 11949 let authToken = null; 11950 try { 11951 if (window.TokenManager) { 11952 if (typeof window.TokenManager.getHydraAuthToken === 'function') { 11953 try { authToken = window.TokenManager.getHydraAuthToken(); } catch (e) {} 11954 } 11955 if (!authToken && typeof window.TokenManager.getBestToken === 'function') { 11956 try { 11957 const tokenWithBearer = window.TokenManager.getBestToken(); 11958 authToken = tokenWithBearer && tokenWithBearer.startsWith('Bearer ') 11959 ? tokenWithBearer.substring(7) 11960 : tokenWithBearer; 11961 } catch (e) {} 11962 } 11963 } 11964 if (!authToken) authToken = localStorage.getItem('hydra_auth_token'); 11965 if (!authToken) authToken = window.authToken || window.didToken; 11966 if (authToken) { 11967 publishHeaders['Authorization'] = authToken.includes('.') || authToken.startsWith('hydra:') 11968 ? 'Bearer ' + authToken 11969 : (authToken.startsWith('Bearer ') ? authToken : 'Bearer hydra:' + authToken); 11970 console.error('[NDJSON Duplication] Auth token attached for publish'); 11971 } else { 11972 console.error('[NDJSON Duplication] WARNING: No auth token â publish will likely fail with 401'); 11973 } 11974 } catch (authErr) { 11975 console.error('[NDJSON Duplication] Auth token lookup failed:', authErr.message); 11976 } 11977 11978 // Call the publish endpoint (same as edition publisher) 11979 console.error('[NDJSON Duplication] Calling publish endpoint: https://hydra-press-v2.fly.dev/publish'); 11980 const response = await fetch('https://hydra-press-v2.fly.dev/publish', { 11981 method: 'POST', 11982 headers: publishHeaders, 11983 body: JSON.stringify({ 11984 ndjsonContent: originalNdjson, 11985 dataFileName: dataFileName 11986 }), 11987 }); 11988 11989 console.error('[NDJSON Duplication] Publish response status:', response.status, response.statusText); 11990 console.error('[NDJSON Duplication] Response headers:', Object.fromEntries(response.headers.entries())); 11991 11992 if (!response.ok) { 11993 const errorText = await response.text().catch(() => 'Unknown error'); 11994 console.error('[NDJSON Duplication] ERROR: Failed to publish duplicated NDJSON:', response.status, errorText); 11995 console.error('[NDJSON Duplication] Response status:', response.status); 11996 console.error('[NDJSON Duplication] Response statusText:', response.statusText); 11997 return false;
11998 } 11999 12000 const result = await response.json(); 12001 console.error('[NDJSON Duplication] SUCCESS: Published duplicated NDJSON'); 12002 console.error('[NDJSON Duplication] Publish result:', JSON.stringify(result, null, 2)); 12003 console.error('[NDJSON Duplication] Publish result URLs:', result.urls); 12004 12005 // Note: The publish endpoint doesn't return Irys txId, so we don't update galleryOptions 12006 // with txId/permaURL here. If Irys upload is needed, it would need to be done separately. 12007 12008 console.error('[NDJSON Duplication] ===== duplicateGalleryNDJSON COMPLETED SUCCESSFULLY ====='); 12009 return true; 12010 } catch (error) { 12011 console.error('[NDJSON Duplication] ERROR: Exception publishing duplicated NDJSON:', error); 12012 console.error('[NDJSON Duplication] Error message:', error.message); 12013 console.error('[NDJSON Duplication] Error stack:', error.stack); 12014 return false; 12015 } 12016 } catch (error) { 12017 console.error('[NDJSON Duplication] ERROR: Unexpected error in duplicateGalleryNDJSON:', error); 12018 console.error('[NDJSON Duplication] Error message:', error.message); 12019 console.error('[NDJSON Duplication] Error stack:', error.stack); 12020 return false; 12021 } 12022} 12023 12024/** 12025 * Creates and shows a loading overlay for gallery deletion (same pattern as duplication). 12026 * @returns {Object} Object with hide() and updateMessage() to control the overlay 12027 */ 12028function showDeletionOverlay() { 12029 const existingOverlay = document.getElementById('deletion-overlay'); 12030 if (existingOverlay) existingOverlay.remove(); 12031 if (!document.querySelector('#deletion-overlay-styles')) { 12032 const style = document.createElement('style'); 12033 style.id = 'deletion-overlay-styles'; 12034 style.textContent = ` 12035 #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; } 12036 #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; } 12037 #deletion-overlay .deletion-spinner { width: 80px; height: 80px; color: #444; } 12038 `; 12039 document.head.appendChild(style); 12040 } 12041 const overlayEl = document.createElement('div'); 12042 overlayEl.id = 'deletion-overlay'; 12043 const messageEl = document.createElement('div'); 12044 messageEl.className = 'deletion-message'; 12045 const spinnerEl = document.createElement('div'); 12046 spinnerEl.className = 'deletion-spinner'; 12047 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>`; 12048 const textEl = document.createElement('div'); 12049 textEl.textContent = 'Deleting...'; 12050 textEl.style.textAlign = 'center'; 12051 messageEl.appendChild(spinnerEl); 12052 messageEl.appendChild(textEl); 12053 overlayEl.appendChild(messageEl); 12054 document.body.appendChild(overlayEl); 12055 return { 12056 hide: function() { if (overlayEl && overlayEl.parentNode) overlayEl.parentNode.removeChild(overlayEl); }, 12057 updateMessage: function(msg) { textEl.textContent = msg; } 12058 }; 12059} 12060 12061/** 12062 * Creates and shows a loading overlay for gallery duplication 12063 * @returns {Object} Object with hide function to remove the overlay 12064 */ 12065function showDuplicationOverlay() { 12066 // Remove any existing overlay first 12067 const existingOverlay = document.getElementById('duplication-overlay'); 12068 if (existingOverlay) { 12069 existingOverlay.remove(); 12070 } 12071 12072 // Add styles if not already present 12073 if (!document.querySelector('#duplication-overlay-styles')) { 12074 const style = document.createElement('style'); 12075 style.id = 'duplication-overlay-styles'; 12076 style.textContent = ` 12077 #duplication-overlay { 12078 position: fixed; 12079 top: 0; 12080 left: 0; 12081 right: 0; 12082 bottom: 0; 12083 background: rgba(0, 0, 0, 0.5); 12084 z-index: 99999999; 12085 display: flex; 12086 align-items: center; 12087 justify-content: center;
12088 cursor: not-allowed; 12089 pointer-events: all; 12090 } 12091 #duplication-overlay .duplication-message { 12092 background: rgba(255, 255, 255, 0.95); 12093 padding: 30px 50px; 12094 font-family: Arial, sans-serif; 12095 font-size: 16px; 12096 box-shadow: 0 4px 12px rgba(0,0,0,0.15); 12097 display: flex; 12098 flex-direction: column; 12099 align-items: center; 12100 gap: 20px; 12101 } 12102 #duplication-overlay .duplication-spinner { 12103 width: 80px; 12104 height: 80px; 12105 color: #444444; 12106 } 12107 `; 12108 document.head.appendChild(style); 12109 } 12110 12111 const overlay = document.createElement('div'); 12112 overlay.id = 'duplication-overlay'; 12113 12114 const message = document.createElement('div'); 12115 message.className = 'duplication-message'; 12116 12117 const spinner = document.createElement('div'); 12118 spinner.className = 'duplication-spinner'; 12119 spinner.innerHTML = ` 12120 <svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24"> 12121 <defs> 12122 <filter id="svgSpinnersGooeyBalls20"> 12123 <feGaussianBlur in="SourceGraphic" result="y" stdDeviation="1"/> 12124 <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"/> 12125 <feBlend in="SourceGraphic" in2="z"/> 12126 </filter> 12127 </defs> 12128 <g filter="url(#svgSpinnersGooeyBalls20)"> 12129 <circle cx="5" cy="12" r="4" fill="currentColor"> 12130 <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="5;8;5"/> 12131 </circle> 12132 <circle cx="19" cy="12" r="4" fill="currentColor"> 12133 <animate attributeName="cx" calcMode="spline" dur="2s" keySplines=".36,.62,.43,.99;.79,0,.58,.57" repeatCount="indefinite" values="19;16;19"/> 12134 </circle> 12135 <animateTransform attributeName="transform" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/> 12136 </g> 12137 </svg> 12138 `; 12139 12140 const text = document.createElement('div'); 12141 text.textContent = 'Duplicating gallery...'; 12142 text.style.textAlign = 'center'; 12143 12144 message.appendChild(spinner); 12145 message.appendChild(text); 12146 overlay.appendChild(message); 12147 document.body.appendChild(overlay); 12148 12149 return { 12150 hide: function() { 12151 if (overlay && overlay.parentNode) { 12152 overlay.parentNode.removeChild(overlay); 12153 } 12154 }, 12155 updateMessage: function(newMessage) { 12156 text.textContent = newMessage; 12157 } 12158 }; 12159} 12160 12161/** 12162 * Duplicates a gallery or page item. 12163 * @param {number} id - The ID of the item to duplicate. 12164 * @param {Event} event - The click event. 12165 */ 12166async function duplicateGalleryItem(id, event) { 12167 event.stopPropagation(); 12168 console.log(`Duplicating item with ID: ${id}`); 12169 12170 // Show loading overlay 12171 const overlay = showDuplicationOverlay(); 12172 12173 // Find the original item in the global galleries array 12174 const originalItem = findGalleryById(galleries, id); // Use helper if available, otherwise simple find 12175 12176 if (!originalItem) { 12177 console.error(`Cannot duplicate: Item with ID ${id} not found.`); 12178 overlay.hide(); 12179 alert('Error: Could not find the item to duplicate.'); 12180 return; 12181 } 12182 12183 if (isPageMenuItem(originalItem)) { 12184 overlay.updateMessage('Duplicating page...'); 12185 } 12186 12187 try { 12188 // --- Create a Deep Copy --- 12189 // Using JSON.parse/stringify is a common way for simple object structures 12190 // Be cautious if your objects have Dates, functions, Maps, Sets, etc. 12191 const newItem = JSON.parse(JSON.stringify(originalItem)); 12192 12193 // --- Modify Copied Item --- 12194 newItem.id = Date.now() + Math.floor(Math.random() * 1000); // Generate a new unique ID 12195 newItem.title = `${originalItem.title}-copy`; // Append "-copy" to title 12196 12197 // Generate new pageId if it's a page or integrated gallery 12198 if (newItem.isPage || newItem.isIntegrated) { 12199 newItem.pageId = generatePageId(); // Assign a new unique pageId 12200 } 12201 12202 // Generate new slug and URL 12203 const newBaseSlug = slugify(newItem.title); // Use the new title 12204 newItem.slug = ensureUniqueSlug(newBaseSlug, galleries); // Ensure uniqueness 12205 newItem.url = `/${newItem.slug}`; // Update URL based on new slug 12206 12207 // Reset children (don't duplicate children for simplicity) 12208 newItem.children = []; 12209 12210 // Deep clone galleryOptions and pageElements, update pageId if needed 12211 if (newItem.galleryOptions) { 12212 newItem.galleryOptions = JSON.parse(JSON.stringify(newItem.galleryOptions)); 12213 // Update pageId within options if it exists and matches the old one 12214 if (newItem.galleryOptions.pageId && newItem.galleryOptions.pageId === originalItem.pageId) { 12215 newItem.galleryOptions.pageId = newItem.pageId; 12216 } 12217 // Clear permanent-storage pointers so duplicate loads its own Tigris NDJSON, not the original 12218 ['txId', 'permaURL', 'loadTxId', 'loadPermaURL', 'loadNetworkUrl', 'r2StorageUrl', 12219 'cdnNdjsonId', 'cdnNdjsonUrl', 'cdnNdjsonStableUrl', 'embedPointerUrl', 'cdnSaveTimestamp', 12220 'flyIoSaveTimestamp', 'loadNetworkSaveTimestamp', 'irysSaveTimestamp', 'timestampsMatch', 'isPerma'] 12221 .forEach(function(field) { delete newItem.galleryOptions[field]; }); 12222 } 12223 if (newItem.pageElements) { 12224 newItem.pageElements = JSON.parse(JSON.stringify(newItem.pageElements)); 12225 // Give new IDs to duplicated page elements
12226 newItem.pageElements.forEach(el => { 12227 el.id = Date.now() + Math.floor(Math.random() * 10000); 12228 }); 12229 } 12230 12231 // Reset home page status 12232 newItem.isHomePage = false; 12233 12234 // --- Duplicate NDJSON if applicable (galleries only; pages copy content via pageElements) --- 12235 const isDuplicatingPage = isPageMenuItem(originalItem); 12236 console.error('[Gallery Duplication] Checking if NDJSON duplication is needed...', { 12237 isDuplicatingPage, 12238 newItemPageId: newItem.pageId, 12239 originalItemPageId: originalItem.pageId 12240 }); 12241 12242 if (!isDuplicatingPage && newItem.pageId && originalItem.pageId) { 12243 overlay.updateMessage('Duplicating gallery files...'); 12244 console.error('[Gallery Duplication] Both galleries have pageIds, attempting to duplicate NDJSON file...'); 12245 const ndjsonDuplicated = await duplicateGalleryNDJSON(originalItem, newItem); 12246 if (!ndjsonDuplicated) { 12247 console.error('[Gallery Duplication] WARNING: NDJSON duplication failed, but continuing with gallery duplication'); 12248 console.error('[Gallery Duplication] User can republish the gallery later if needed'); 12249 alert('Gallery menu item was created, but gallery images could not be copied. Open Edition Publisher on this gallery and publish to load images.'); 12250 // Don't block the duplication if NDJSON fails - user can republish later 12251 } else { 12252 console.error('[Gallery Duplication] SUCCESS: NDJSON duplication completed successfully'); 12253 } 12254 } else { 12255 console.error('[Gallery Duplication] SKIP: Skipping NDJSON duplication:', { 12256 isDuplicatingPage, 12257 hasNewPageId: !!newItem.pageId, 12258 hasOriginalPageId: !!originalItem.pageId, 12259 reason: isDuplicatingPage ? 'Page items use pageElements, not NDJSON' : 12260 !newItem.pageId ? 'New gallery has no pageId' : 'Original gallery has no pageId' 12261 }); 12262 } 12263 12264 overlay.updateMessage(isDuplicatingPage ? 'Saving duplicated page...' : 'Saving duplicated gallery...'); 12265 12266 // --- Insert into Galleries Array --- 12267 // Find the index of the original item 12268 const originalIndex = galleries.findIndex(item => item.id === id); 12269 12270 if (originalIndex > -1) { 12271 // Insert the new item right after the original 12272 galleries.splice(originalIndex + 1, 0, newItem); 12273 console.log(`Inserted duplicate "${newItem.title}" after "${originalItem.title}"`); 12274 12275 // Update positions for all items after the insertion point 12276 for (let i = originalIndex + 1; i < galleries.length; i++) { 12277 galleries[i].position = i; // Re-assign position based on new index 12278 } 12279 12280 } else { 12281 // Fallback: Add to the end if original couldn't be found (shouldn't happen) 12282 console.warn(`Original item ${id} not found in array, adding duplicate to the end.`); 12283 newItem.position = galleries.length; 12284 galleries.push(newItem); 12285 } 12286 12287 console.log('Rendering galleries immediately after duplication...'); 12288 renderGalleries(); 12289 // Update active states if necessary (though unlikely needed here) 12290 updateActiveStates(); 12291 12292 // --- Save and Refresh --- 12293 console.log('Saving duplicated item...'); 12294 saveGalleries(null, { addedNewItem: true }) // Pass flag to trigger refresh 12295 .then(success => { 12296 overlay.hide(); // Hide overlay before page refresh 12297 if (success) { 12298 console.log('Duplicate saved successfully. Page will refresh.'); 12299 // No need to call renderGalleries() here because saveGalleries will trigger a reload. 12300 } else { 12301 console.error('Failed to save the duplicated item.'); 12302 alert('Error: Could not save the duplicated item.'); 12303 // Optionally try to revert the galleries array change here if save fails 12304 } 12305 }) 12306 .catch(error => { 12307 overlay.hide(); // Hide overlay on error 12308 console.error('Error saving duplicated item:', error); 12309 alert('Error saving duplicated item.'); 12310 }); 12311 12312 } catch (error) { 12313 overlay.hide(); // Hide overlay on error 12314 console.error('Error duplicating item:', error); 12315 alert('An error occurred while duplicating the item.'); 12316 } 12317} 12318 12319/** 12320 * saveGalleries function that always saves FULL data 12321 * Ensures gallery settings are preserved in both localStorage and server 12322 * 12323 * @param {Object} customData - Optional custom data to save instead of galleries 12324 * @returns {Promise<boolean>} - Success indicator 12325 */ 12326 12327function attachLiveSidebarElements(data) { 12328 if (!data || typeof data !== 'object') return data; 12329 var hasGetter = !!(window.SidebarManager && typeof window.SidebarManager.getElementsForSave === 'function'); 12330 if (hasGetter) { 12331 var live = window.SidebarManager.getElementsForSave(); 12332 if (Array.isArray(live) && live.length > 0) { 12333 data.sidebarElements = live; 12334 return data; 12335 } 12336 delete data.sidebarElements; 12337 return data; 12338 } 12339 if (Array.isArray(data.sidebarElements) && data.sidebarElements.length === 0) { 12340 delete data.sidebarElements; 12341 } 12342 return data; 12343} 12344 12345async function saveGalleries(customData) { 12346 if (!isAuthenticated && localStorage.getItem('hydra_is_admin') !== 'true') { 12347 alert('You must be logged in as an admin to save changes'); 12348 return false;
12349 } 12350 12351 if (window.HelpSystem && typeof window.HelpSystem.isSandboxActive === 'function' && window.HelpSystem.isSandboxActive()) { 12352 console.warn('Save blocked: help sandbox is active'); 12353 return false; 12354 } 12355 12356 function _hasHelpDemoInTree(items) { 12357 if (!Array.isArray(items)) return false; 12358 for (var i = 0; i < items.length; i++) { 12359 var g = items[i]; 12360 if (!g) continue; 12361 if (g.isHelpDemo || Number(g.id) < 0) return true; 12362 if (g.children && _hasHelpDemoInTree(g.children)) return true; 12363 } 12364 return false; 12365 } 12366 12367 if (window.activeGalleryId != null && Number(window.activeGalleryId) < 0) { 12368 console.warn('Save blocked: active help demo gallery'); 12369 return false; 12370 } 12371 12372 var savePayload = customData && customData.galleries ? customData.galleries : galleries; 12373 if (_hasHelpDemoInTree(savePayload)) { 12374 console.warn('Save blocked: payload contains help demo records'); 12375 return false; 12376 } 12377 12378 // Prevent multiple simultaneous saves â queue the latest payload instead of dropping it 12379 if (window._saveInProgress) { 12380 console.log('Save already in progress, queueing latest payload'); 12381 if (customData) { 12382 window._pendingSaveGalleriesData = attachLiveSidebarElements(customData); 12383 } 12384 return new Promise(function(resolve) { 12385 window._pendingSaveResolvers = window._pendingSaveResolvers || []; 12386 window._pendingSaveResolvers.push(resolve); 12387 }); 12388 } 12389 12390 window._saveInProgress = true; 12391 12392 try { 12393 // CRITICAL: Synchronize galleries before saving to ensure we use the latest data 12394 if (typeof synchronizeGalleries === 'function') { 12395 synchronizeGalleries(); 12396 console.log('Galleries synchronized before saving'); 12397 } 12398 12399 // Try to get a valid token from multiple sources 12400 let token = null; 12401 let tokenSource = 'none'; 12402 12403 // First try the global didToken (traditional approach) 12404 if (didToken) { 12405 console.log('Using global didToken for save operation'); 12406 token = didToken; 12407 tokenSource = 'didToken'; 12408 } 12409 // Next try TokenManager if available 12410 else if (window.TokenManager && typeof window.TokenManager.getToken === 'function') { 12411 token = window.TokenManager.getToken(); 12412 12413 // Remove Bearer prefix if present (we'll add it later) 12414 if (token && token.startsWith('Bearer ')) { 12415 token = token.substring(7); 12416 } 12417 12418 console.log('Using TokenManager token for save operation, length:', token ? token.length : 0); 12419 tokenSource = 'TokenManager'; 12420 } 12421 // Fallback to localStorage 12422 else if (localStorage.getItem('hydra_auth_token')) { 12423 token = localStorage.getItem('hydra_auth_token'); 12424 console.log('Using localStorage token for save operation, length:', token.length); 12425 tokenSource = 'localStorage'; 12426 } 12427 // Last resort: try to get a fresh token 12428 else if (magic && magic.user) { 12429 try { 12430 console.log('Attempting to get fresh token from Magic'); 12431 token = await magic.user.getIdToken(); 12432 console.log('Obtained fresh token from Magic:', token ? 'success' : 'failed'); 12433 tokenSource = token ? 'magic' : 'none'; 12434 } catch (e) { 12435 console.error('Error getting token from Magic:', e); 12436 tokenSource = 'magic_error'; 12437 } 12438 } 12439 12440 // [AUTH_SAVE] Log for S+J download - token state and logged-in email before request 12441 const loggedInEmail = (window.userMetadata && window.userMetadata.email) || 12442 localStorage.getItem('hydra_auth_email') || 12443 ''; 12444 console.log('[AUTH_SAVE] Token resolved:', { 12445 loggedInEmail: loggedInEmail || '(none)', 12446 tokenSource, 12447 tokenLength: token ? token.length : 0, 12448 tokenPrefix: token ? token.substring(0, 15) + '...' : '(none)', 12449 hasToken: !!token, 12450 isAuthenticated, 12451 hydraIsAdmin: localStorage.getItem('hydra_is_admin') === 'true', 12452 hasLocalStorageToken: !!localStorage.getItem('hydra_auth_token'), 12453 hasTokenManager: !!(window.TokenManager && typeof window.TokenManager.getToken === 'function') 12454 }); 12455 12456 if (!token) { 12457 console.error('[AUTH_SAVE] No token available - save will fail. Sources checked: didToken, TokenManager, localStorage, magic.'); 12458 throw new Error('Could not find a valid authentication token. Please log in again.'); 12459 } 12460 12461 // IMPORTANT: Try to save to localStorage with isolated error handling 12462 // This ensures localStorage failures don't prevent server saves 12463 if (!customData) { // Only try localStorage for regular gallery saves
12464 try { 12465 // FIXED: Always save the FULL galleries data to localStorage 12466 localStorage.setItem('galleries', JSON.stringify(galleries)); 12467 console.log(`Saved ${galleries.length} galleries to localStorage (FULL format with complete settings)`); 12468 } catch (storageError) { 12469 // Now storage errors are isolated and non-fatal 12470 console.warn('LocalStorage quota exceeded, proceeding without local backup', storageError); 12471 } 12472 } else if (customData && customData.isImport) { 12473 // Skip localStorage for import operations - data too large 12474 console.log('Skipping localStorage for import operation - data too large for local storage'); 12475 } 12476 12477 // IMPORTANT: Ensure all pages have proper URLs and slugs before saving 12478 galleries.forEach(gallery => { 12479 if (gallery.isPage === true) { 12480 const pageSlug = slugify(gallery.title); 12481 12482 // Make sure slug and URL are set correctly 12483 if (!gallery.slug || gallery.slug === '') { 12484 gallery.slug = pageSlug; 12485 } 12486 12487 // Make sure URL points to slug 12488 if (!gallery.url || gallery.url === '/' || !gallery.url.includes(gallery.slug)) { 12489 gallery.url = `/${pageSlug}`; 12490 } 12491 } 12492 }); 12493 12494 // Set up the data to save - ensure we're using the complete data 12495 let siteData; 12496 console.log('saveGalleries: customData received:', JSON.stringify(customData)); 12497 console.log('saveGalleries: siteMetadata in customData:', customData?.siteMetadata); 12498 console.log('saveGalleries: TITLE in customData:', customData?.siteMetadata?.title); 12499 console.log('saveGalleries: GOOGLE ANALYTICS in customData:', customData?.siteMetadata?.googleAnalytics); 12500 if (customData) { 12501 // CRITICAL FIX: Ensure gallery settings are preserved when saving from sidebar 12502 // If customData contains galleries (which it should for sidebar-initiated saves), 12503 // merge them with any existing settings to ensure nothing is lost 12504 if (customData.galleries && Array.isArray(customData.galleries)) { 12505 // First, ensure we preserve all current gallery settings from the existing galleries 12506 const existingGalleries = (typeof galleries !== 'undefined' ? galleries : window.galleries) || []; 12507 12508 // Create merged galleries array with preserved settings 12509 const mergedGalleries = customData.galleries.map(gallery => { 12510 const existingGallery = existingGalleries.find(g => g.id === gallery.id); 12511 12512 // If this gallery exists in the current dataset, ensure all galleryOptions are preserved 12513 if (existingGallery && existingGallery.galleryOptions && gallery.galleryOptions) { 12514 // Calculate settings depth 12515 const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length; 12516 const serverOptionsDepth = Object.keys(gallery.galleryOptions).length; 12517 12518 // If server has fewer settings, merge them with existing settings 12519 if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) { 12520 console.log(`Preserving rich settings for ${gallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`); 12521 12522 // Create a merged gallery with preserved settings 12523 return { 12524 ...gallery, 12525 galleryOptions: { 12526 ...existingGallery.galleryOptions, // Start with all existing settings 12527 ...gallery.galleryOptions // Override with any new server settings 12528 } 12529 }; 12530 } 12531 } 12532 12533 // If no preservation needed, use server gallery as is 12534 return gallery; 12535 }); 12536 12537 // Use the merged galleries in customData 12538 siteData = { 12539 ...customData, 12540 galleries: mergedGalleries 12541 }; 12542 12543 console.log(`Settings-preservation merge completed: ${mergedGalleries.length} galleries`); 12544 console.log('saveGalleries: siteData after merge:', JSON.stringify(siteData)); 12545 console.log('saveGalleries: siteMetadata after merge:', siteData.siteMetadata); 12546 console.log('saveGalleries: TITLE after merge:', siteData.siteMetadata?.title); 12547 console.log('saveGalleries: GOOGLE ANALYTICS after merge:', siteData.siteMetadata?.googleAnalytics); 12548 } else { 12549 siteData = customData; 12550 } 12551 } else { 12552 const galleriesForSave = (typeof window !== 'undefined' && window.galleries && Array.isArray(window.galleries)) 12553 ? window.galleries 12554 : galleries; 12555 siteData = { 12556 galleries: galleriesForSave 12557 }; 12558 12559 if (window.siteMetadata) { 12560 siteData.siteMetadata = window.siteMetadata; 12561 } 12562 12563 if (window.SidebarManager) { 12564 const liveSidebar = typeof window.SidebarManager.getElementsForSave === 'function' 12565 ? window.SidebarManager.getElementsForSave() 12566 : null; 12567 if (Array.isArray(liveSidebar) && liveSidebar.length > 0) { 12568 siteData.sidebarElements = liveSidebar; 12569 } 12570 } 12571 } 12572 12573 attachLiveSidebarElements(siteData); 12574 12575 // Ensure galleries are never saved as pages: any item with galleryOptions is a gallery and must persist with isPage false.
12576 // (loadPage can set isPage=true in memory; sidebar/full saves would otherwise persist that and convert TEAM etc. to pages.) 12577 if (siteData.galleries && Array.isArray(siteData.galleries)) { 12578 siteData.galleries = siteData.galleries.map(g => { 12579 if (g && g.galleryOptions && typeof g.galleryOptions === 'object') { 12580 return { ...g, isPage: false }; 12581 } 12582 return g; 12583 }); 12584 } 12585 12586 // Get the save URL 12587 const saveUrl = typeof getApiUrl === 'function' ? 12588 getApiUrl('/api/save-config') : 12589 '/api/save-config'; 12590 12591 // Get user email from various sources 12592 const userEmail = (window.userMetadata && window.userMetadata.email) || 12593 localStorage.getItem('hydra_auth_email') || 12594 ''; 12595 12596 // Ensure token has proper format 12597 const formattedToken = token.startsWith('hydra:') ? token : 12598 (token.includes('.') ? token : `hydra:${token}`); 12599 12600 console.log('Making server save request...'); 12601 console.log('saveGalleries: Final siteData being sent to server:', JSON.stringify(siteData)); 12602 console.log('saveGalleries: Final siteMetadata being sent:', siteData.siteMetadata); 12603 console.log('saveGalleries: FINAL TITLE being sent:', siteData.siteMetadata?.title); 12604 console.log('saveGalleries: FINAL GOOGLE ANALYTICS being sent:', siteData.siteMetadata?.googleAnalytics); 12605 12606 // [AUTH_SAVE] Log for S+J download - request details (no full token) 12607 console.log('[AUTH_SAVE] Sending save request:', { 12608 saveUrl, 12609 authHeaderPrefix: `Bearer ${formattedToken.substring(0, 20)}...`, 12610 userEmail: userEmail || '(empty)', 12611 tokenSource, 12612 formattedTokenLength: formattedToken.length 12613 }); 12614 12615 // Make the request 12616 let response = await fetch(saveUrl, { 12617 method: 'POST', 12618 headers: { 12619 'Content-Type': 'application/json', 12620 'Authorization': `Bearer ${formattedToken}`, 12621 'X-User-Email': userEmail, 12622 'X-API-Request': 'true' // Always include this header for API requests 12623 }, 12624 body: JSON.stringify(siteData) 12625 }); 12626 12627 console.log('Save response status:', response.status); 12628 12629 // Process the response 12630 let responseText = await response.text(); 12631 console.log('Server response text (first 500 chars):', responseText.substring(0, 500)); 12632 let responseData; 12633 12634 try { 12635 responseData = JSON.parse(responseText); 12636 console.log('Server response parsed successfully:', responseData); 12637 if (responseData.debug) { 12638 console.log('Server debug info:', responseData.debug); 12639 console.log('Server debug - requestBodyKeys:', responseData.debug.requestBodyKeys); 12640 console.log('Server debug - hasSiteMetadata:', responseData.debug.hasSiteMetadata); 12641 console.log('Server debug - siteMetadata:', responseData.debug.siteMetadata); 12642 console.log('Server debug - finalSiteMetadata:', responseData.debug.finalSiteMetadata); 12643 console.log('Server debug - googleAnalytics:', responseData.debug.googleAnalytics); 12644 } 12645 } catch (jsonError) { 12646 if (response.status === 200 && responseText.trim().startsWith('<!DOCTYPE html>')) { 12647 responseData = { success: true }; 12648 } else { 12649 throw new Error('Invalid response format: ' + responseText.substring(0, 100)); 12650 } 12651 } 12652 12653 if (!response.ok && !responseData.success) { 12654 // [AUTH_SAVE] Log for S+J download - capture full 401/auth failure details 12655 const authSaveFailure = { 12656 status: response.status, 12657 statusText: response.statusText, 12658 error: responseData && responseData.error, 12659 details: responseData && responseData.details, 12660 authMethod: responseData && responseData.authMethod, 12661 fullResponse: responseData, 12662 responseTextPreview: responseText.substring(0, 300) 12663 }; 12664 console.error('[AUTH_SAVE] Save request failed:', authSaveFailure); 12665 12666 // Retry once on 401: token may have expired - try refresh or fresh Magic token 12667 if (response.status === 401) {
12668 console.log('[AUTH_SAVE] 401 received - attempting token refresh before retry...'); 12669 let newToken = null; 12670 if (window.TokenManager && typeof window.TokenManager.refreshToken === 'function') { 12671 newToken = await window.TokenManager.refreshToken(); 12672 } 12673 if (!newToken && magic && magic.user) { 12674 try { 12675 newToken = await magic.user.getIdToken(); 12676 } catch (e) { 12677 console.warn('[AUTH_SAVE] Magic getIdToken failed:', e); 12678 } 12679 } 12680 if (newToken) { 12681 if (newToken.startsWith('Bearer ')) newToken = newToken.substring(7); 12682 token = newToken; 12683 didToken = newToken; 12684 localStorage.setItem('hydra_auth_token', newToken); 12685 if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') { 12686 window.TokenManager.storeToken(newToken.startsWith('hydra:') ? newToken : `hydra:${newToken}`, userEmail); 12687 } 12688 console.log('[AUTH_SAVE] Got fresh token, retrying save...'); 12689 const retryResponse = await fetch(saveUrl, { 12690 method: 'POST', 12691 headers: { 12692 'Content-Type': 'application/json', 12693 'Authorization': `Bearer ${token.startsWith('hydra:') ? token : (token.includes('.') ? token : `hydra:${token}`)}`, 12694 'X-User-Email': userEmail, 12695 'X-API-Request': 'true' 12696 }, 12697 body: JSON.stringify(siteData) 12698 }); 12699 const retryText = await retryResponse.text(); 12700 let retryData; 12701 try { retryData = JSON.parse(retryText); } catch (e) { 12702 if (retryResponse.status === 200 && retryText.startsWith('<!DOCTYPE')) retryData = { success: true }; 12703 else throw new Error('Invalid retry response'); 12704 } 12705 if (retryResponse.ok || retryData.success) { 12706 response = retryResponse; 12707 responseText = retryText; 12708 responseData = retryData; 12709 console.log('[AUTH_SAVE] Retry succeeded'); 12710 } else { 12711 console.error('[AUTH_SAVE] Retry also failed:', retryResponse.status, retryData); 12712 throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`); 12713 } 12714 } else { 12715 throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`); 12716 } 12717 } else { 12718 throw new Error(`Failed to save: ${response.status} - ${JSON.stringify(responseData)}`); 12719 } 12720 } 12721 12722 console.log('Configuration saved successfully'); 12723 12724 // Update local lastKnownConfigTimestamp after successful save 12725 if (responseData.lastModified) { 12726 window.lastKnownConfigTimestamp = responseData.lastModified; 12727 console.error('[CONFLICT DETECT] â Updated local config timestamp from save response:', window.lastKnownConfigTimestamp); 12728 } else { 12729 console.error('[CONFLICT DETECT] â ï¸ No lastModified in save response, fetching from server...'); 12730 // If server doesn't return timestamp, fetch it 12731 try { 12732 const timestampUrl = typeof getApiUrl === 'function' ? 12733 getApiUrl('/api/config-timestamp') : 12734 '/api/config-timestamp'; 12735 const timestampResponse = await fetch(timestampUrl); 12736 if (timestampResponse.ok) { 12737 const timestampData = await timestampResponse.json(); 12738 if (timestampData.lastModified) { 12739 window.lastKnownConfigTimestamp = timestampData.lastModified; 12740 console.error('[CONFLICT DETECT] â Fetched and updated local config timestamp:', window.lastKnownConfigTimestamp); 12741 } else { 12742 console.error('[CONFLICT DETECT] â ï¸ No lastModified in fetched timestamp data:', timestampData); 12743 } 12744 } else { 12745 console.error('[CONFLICT DETECT] â ï¸ Failed to fetch timestamp after save, status:', timestampResponse.status); 12746 } 12747 } catch (err) { 12748 console.error('[CONFLICT DETECT] â ï¸ Could not fetch timestamp after save:', err); 12749 } 12750 } 12751 12752 // Store new token if provided 12753 if (responseData.hydraToken) { 12754 console.log('Received new hydra token from server'); 12755 12756 // Update the global didToken 12757 didToken = responseData.hydraToken.startsWith('hydra:') ? 12758 responseData.hydraToken.substring(6) : responseData.hydraToken; 12759 12760 // Update localStorage 12761 localStorage.setItem('hydra_auth_token', didToken); 12762 12763 // Update TokenManager if available 12764 if (window.TokenManager && typeof window.TokenManager.storeToken === 'function') { 12765 window.TokenManager.storeToken(responseData.hydraToken, userEmail); 12766 } 12767 } 12768 12769 // Process server response data if needed 12770 if (responseData.galleries) { 12771 // If server returns updated galleries data, update the global galleries variable 12772 if (Array.isArray(responseData.galleries) && responseData.galleries.length > 0) { 12773 // CRITICAL FIX: Before replacing galleries with server response, 12774 // preserve any gallery settings that might be lost in the response 12775 const updatedGalleries = responseData.galleries.map(serverGallery => { 12776 // Find the matching gallery in our current data 12777 const existingGallery = galleries.find(g => g.id === serverGallery.id); 12778 12779 // Check if we need to preserve gallery settings 12780 if (existingGallery && existingGallery.galleryOptions && serverGallery.galleryOptions) { 12781 // Calculate settings depth 12782 const existingOptionsDepth = Object.keys(existingGallery.galleryOptions).length; 12783 const serverOptionsDepth = Object.keys(serverGallery.galleryOptions).length; 12784 12785 // If server has fewer settings, merge them with existing settings 12786 if (serverOptionsDepth < existingOptionsDepth && existingOptionsDepth > 5) { 12787 console.log(`Preserving rich settings for ${serverGallery.title} (${existingOptionsDepth} vs ${serverOptionsDepth})`); 12788 12789 // Create a merged gallery with preserved settings 12790 return { 12791 ...serverGallery, 12792 galleryOptions: { 12793 ...existingGallery.galleryOptions, // Start with all existing settings 12794 ...serverGallery.galleryOptions // Override with any new server settings 12795 } 12796 }; 12797 } 12798 } 12799 12800 // If no preservation needed, use server gallery as is 12801 return serverGallery; 12802 }); 12803 12804 // Update the galleries variable with our preserved data 12805 galleries = updatedGalleries; 12806 console.log(`Updated galleries from server response: ${galleries.length} galleries`); 12807 12808 // IMPORTANT: Synchronize again after receiving server response 12809 if (typeof synchronizeGalleries === 'function') { 12810 synchronizeGalleries(); 12811 console.log('Galleries re-synchronized after server response'); 12812 } 12813 12814 // Keep sidebar UI in sync with current galleries (e.g. after delete) 12815 if (typeof renderGalleries === 'function') { 12816 renderGalleries(); 12817 } 12818 12819 // Update localStorage with full data 12820 try { 12821 localStorage.setItem('galleries', JSON.stringify(galleries)); 12822 console.log('Updated localStorage with server gallery data (FULL format)'); 12823 } catch (e) { 12824 console.warn('Could not update localStorage with server gallery data', e); 12825 } 12826 } 12827 } 12828
12829 // CRITICAL FIX: Clear all sessionStorage caches after successful save 12830 // This ensures that when users open the site in a new tab, they get fresh data 12831 try { 12832 console.log('Clearing sessionStorage caches after successful save...'); 12833 const keysToRemove = []; 12834 for (let i = 0; i < sessionStorage.length; i++) { 12835 const key = sessionStorage.key(i); 12836 if (key && (key.includes('gallery_data_') || key.includes('neonGallery_'))) { 12837 keysToRemove.push(key); 12838 } 12839 } 12840 keysToRemove.forEach(key => { 12841 sessionStorage.removeItem(key); 12842 console.log('Cleared sessionStorage key: ' + key); 12843 }); 12844 console.log('Cleared ' + keysToRemove.length + ' sessionStorage cache entries'); 12845 } catch (cacheError) { 12846 console.warn('Error clearing sessionStorage cache:', cacheError); 12847 // Don't fail the save if cache clearing fails 12848 } 12849 12850 window._saveInProgress = false; 12851 12852 if (!customData || !Array.isArray(customData.sidebarElements)) { 12853 document.dispatchEvent(new CustomEvent('sidebar-save-requested')); 12854 } 12855 12856 // Flush any save queued while this request was in flight 12857 if (window._pendingSaveGalleriesData) { 12858 const pending = window._pendingSaveGalleriesData; 12859 window._pendingSaveGalleriesData = null; 12860 const resolvers = window._pendingSaveResolvers || []; 12861 window._pendingSaveResolvers = []; 12862 return saveGalleries(pending).then(function(result) { 12863 resolvers.forEach(function(r) { r(result); }); 12864 return result; 12865 }); 12866 } 12867 12868 const idleResolvers = window._pendingSaveResolvers || []; 12869 window._pendingSaveResolvers = []; 12870 idleResolvers.forEach(function(r) { r(true); }); 12871 12872 return true; 12873 } catch (error) { 12874 // [AUTH_SAVE] Log for S+J download - capture exception (network, parse, or thrown after 401) 12875 console.error('[AUTH_SAVE] Save threw exception:', { 12876 message: error.message, 12877 name: error.name, 12878 stack: error.stack 12879 }); 12880 console.error('Error saving configuration:', error); 12881 alert('Failed to save changes: ' + error.message); 12882 window._saveInProgress = false; 12883 const failResolvers = window._pendingSaveResolvers || []; 12884 window._pendingSaveResolvers = []; 12885 failResolvers.forEach(function(r) { r(false); }); 12886 return false; 12887 } 12888} 12889 12890// Helper function to get API URL for preview sites 12891function getApiUrl(endpoint) { 12892 // Check if we're on a preview URL 12893 const isPreviewUrl = window.location.hostname === 'preview.neonsky.app'; 12894 12895 if (isPreviewUrl) { 12896 // Extract the site GUID from the path (first segment after domain) 12897 const pathParts = window.location.pathname.split('/').filter(Boolean); 12898 if (pathParts.length > 0) { 12899 const siteGuid = pathParts[0]; 12900 // Include the site GUID in the API URL 12901 return `/${siteGuid}${endpoint}`; 12902 } 12903 } 12904 12905 // Regular case - return the endpoint as is 12906 return endpoint; 12907} 12908 12909function updateActiveStates() { 12910 // Get the most current active gallery ID, ensuring consistency between both variables 12911 const currentActiveId = window.activeGalleryId || activeGalleryId; 12912 12913 // Ensure both variables are synchronized 12914 window.activeGalleryId = currentActiveId; 12915 activeGalleryId = currentActiveId; 12916 12917 console.log("updateActiveStates: Using active gallery ID:", currentActiveId); 12918 12919 // Special case for invisible home page - don't try to highlight it in the menu 12920 const activeGallery = galleries.find(g => g.id === currentActiveId); 12921 if (activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true) { 12922 console.log("Active gallery is invisible home page - not highlighting in menu"); 12923 // We're not returning here, so it will still update the visibility classes 12924 // but won't try to find an element to mark as active 12925 } 12926 12927 // Fix selector issue - look for both .tree and .sidebar 12928 document.querySelectorAll('.tree li, .sidebar li').forEach(li => { 12929 if (!li.dataset.id) return; // Skip items without data-id attribute 12930 12931 const id = parseInt(li.dataset.id); 12932 if (isNaN(id)) return; // Skip if ID is not a number 12933 12934 const gallery = galleries.find(g => g.id === id); 12935 12936 // Update active state using the most current ID 12937 const isActive = id === currentActiveId; 12938 12939 // Add or remove the active class 12940 if (isActive) { 12941 li.classList.add('active'); 12942 console.log(`Setting active class for element: ${id} (${gallery ? gallery.title : 'Unknown'})`); 12943 } else { 12944 li.classList.remove('active'); 12945 } 12946 12947 // Update visibility class if gallery exists 12948 if (gallery) { 12949 li.classList.toggle('hidden-gallery', gallery.visible === false); 12950 12951 // Update visibility toggle icon if it exists 12952 const toggleButton = li.querySelector('.visibility-toggle'); 12953 if (toggleButton) { 12954 toggleButton.classList.toggle('hidden', gallery.visible === false); 12955 } 12956 } 12957 }); 12958 12959 // Verify that the active state was actually set for the current activeGalleryId 12960 const activeElements = document.querySelectorAll('.active'); 12961 if (activeElements.length === 0 && currentActiveId) { 12962 // Only log a warning if we're not dealing with an invisible home page 12963 const activeGallery = galleries.find(g => g.id === currentActiveId); 12964 if (!(activeGallery && activeGallery.visible === false && activeGallery.isHomePage === true)) { 12965 console.warn(`No active elements found after update for ID: ${currentActiveId}`); 12966 } 12967 } 12968 12969 // Log active items for debugging 12970 console.log("Active gallery ID after update:", currentActiveId);
12971 activeElements.forEach(el => 12972 console.log("Active element:", el.textContent.trim()) 12973 ); 12974} 12975 12976 12977function debugGalleryStructure() { 12978console.group('Gallery Structure Debug'); 12979 12980// Log the raw data structure 12981console.log('Raw galleries array:', JSON.parse(JSON.stringify(galleries))); 12982 12983// Log the tree structure 12984const treeStructure = createGalleryTree(galleries); 12985 12986// Helper to print the tree 12987function printTree(items, level = 0) { 12988const indent = ' '.repeat(level); 12989items.forEach(item => { 12990 console.log(`${indent}${item.title} (ID: ${item.id}, Parent: ${item.parentId || 'ROOT'})`); 12991 if (item.children && item.children.length > 0) { 12992 printTree(item.children, level + 1); 12993 } 12994}); 12995} 12996 12997console.log('Tree structure:'); 12998printTree(treeStructure); 12999 13000// Log the DOM structure 13001console.log('DOM structure:'); 13002const treeEl = document.getElementById('galleryTree'); 13003console.log(treeEl); 13004 13005console.groupEnd(); 13006} 13007 13008function createGalleryTree(galleries) { 13009console.log("Creating gallery tree with preservation safeguards..."); 13010 13011// First, create a map of galleries by id for quick lookup 13012const galleryMap = {}; 13013const processedIds = new Set(); 13014 13015// First pass: Create clean copies without children 13016galleries.forEach(gallery => { 13017if (!gallery || !gallery.id) return; // Skip invalid galleries 13018 13019// Create a copy of the gallery object without the children field 13020const cleanGallery = { ...gallery }; 13021delete cleanGallery.children; 13022 13023// Initialize an empty children array 13024cleanGallery.children = []; 13025 13026// Add to map - even if we've seen this ID before, keep the latest version 13027if (processedIds.has(gallery.id)) { 13028 console.warn(`Multiple galleries with ID ${gallery.id} - using latest version`); 13029} 13030 13031processedIds.add(gallery.id); 13032galleryMap[gallery.id] = cleanGallery; 13033}); 13034 13035// Second pass: Assign children to their parents 13036const rootGalleries = []; 13037 13038// To detect already-processed children 13039const childrenAssigned = new Set(); 13040 13041galleries.forEach(gallery => { 13042if (!gallery || !gallery.id) return; // Skip invalid galleries 13043 13044const galleryId = gallery.id; 13045 13046// Skip if this gallery isn't in our map (could be invalid) 13047if (!galleryMap[galleryId]) return; 13048 13049// Skip if we've already assigned this gallery as a child 13050if (childrenAssigned.has(galleryId)) return; 13051 13052if (gallery.parentId && galleryMap[gallery.parentId]) { 13053 // This is a child gallery, but first check it doesn't create a circular reference 13054 13055 // Find all parents in the chain up to the root 13056 let currentParent = galleryMap[gallery.parentId]; 13057 let safeToAdd = true; 13058 let chainIds = [galleryId, gallery.parentId]; 13059 13060 while (currentParent.parentId) { 13061 // If we've seen this ID in the chain, it would create a circular reference 13062 if (chainIds.includes(currentParent.parentId)) { 13063 console.warn(`Would create circular reference for gallery ${gallery.title} - making it a root gallery`); 13064 safeToAdd = false; 13065 break; 13066 } 13067 13068 chainIds.push(currentParent.parentId); 13069 currentParent = galleryMap[currentParent.parentId]; 13070 13071 // If parent doesn't exist in the map, break the chain 13072 if (!currentParent) break; 13073 } 13074 13075 if (safeToAdd) { 13076 // Safe to add as child 13077 galleryMap[gallery.parentId].children.push(galleryMap[galleryId]); 13078 childrenAssigned.add(galleryId); 13079 } else { 13080 // Not safe - make it a root gallery 13081 rootGalleries.push(galleryMap[galleryId]); 13082 } 13083} else { 13084 // This is a root gallery (no parent or parent doesn't exist) 13085 if (!childrenAssigned.has(galleryId)) { 13086 rootGalleries.push(galleryMap[galleryId]); 13087 } 13088} 13089}); 13090 13091// Final verification 13092const allGalleryIds = new Set(galleries.map(g => g.id)); 13093const treeGalleryIds = new Set(); 13094 13095// Function to collect all IDs in the tree 13096const collectIds = (items) => { 13097items.forEach(item => { 13098 treeGalleryIds.add(item.id); 13099 if (item.children && item.children.length > 0) { 13100 collectIds(item.children); 13101 } 13102}); 13103}; 13104 13105collectIds(rootGalleries); 13106 13107// Check if any galleries are missing from the tree 13108const missingIds = []; 13109allGalleryIds.forEach(id => { 13110if (!treeGalleryIds.has(id)) { 13111 missingIds.push(id); 13112} 13113}); 13114 13115if (missingIds.length > 0) { 13116console.warn(`${missingIds.length} galleries are missing from the tree structure`); 13117 13118// Add the missing galleries as root items 13119missingIds.forEach(id => { 13120 const gallery = galleryMap[id]; 13121 if (gallery && !childrenAssigned.has(id)) { 13122 console.log(`Adding missing gallery to root: ${gallery.title}`); 13123 rootGalleries.push(gallery); 13124 } 13125}); 13126} 13127 13128console.log(`Created tree with ${rootGalleries.length} root galleries`); 13129return rootGalleries; 13130} 13131 13132function alignFolderStates() { 13133 console.log("Aligning folder data states with collapsed visual appearance"); 13134 let changed = 0; 13135
13136 galleries.forEach(gallery => { 13137 if ((gallery.isFolder === true || gallery.isSubmenu === true) && gallery.isCollapsed === false) { 13138 gallery.isCollapsed = true; 13139 changed++; 13140 } 13141 }); 13142 13143} 13144 13145 13146 13147function renderGalleryItem(gallery, level = 0, maxLevel = 10) { 13148 // Prevent infinite recursion by limiting depth 13149 if (level >= maxLevel) { 13150 console.warn(`Maximum nesting level reached for gallery: ${gallery.title}`); 13151 return ''; 13152 } 13153 13154 // Add debug info to console 13155 console.log(`Rendering gallery item: ${gallery.title}, isEditing=${isEditing}, level=${level}`); 13156 13157 // Skip hidden items in view mode (but not spacers) 13158 if (!isEditing && gallery.visible === false && !gallery.isSpacer) { 13159 return ''; 13160 } 13161 13162const isPage = isPageMenuItem(gallery); 13163const isFolder = gallery.isFolder === true || gallery.isSubmenu === true; // Support both folder and legacy submenu 13164const isNavigableParent = isNavigableMenuParent(gallery); 13165const isSpacer = gallery.isSpacer === true; 13166const isExternal = gallery.isExternal === true; 13167 13168// CRITICAL FIX: Ensure children are always populated for folders 13169// If gallery.children is missing or empty, but this is a folder, build children from flat galleries array 13170// This ensures the nested <ol> is always rendered with children in the DOM, even in view mode 13171if (isFolder) { 13172 // Always ensure children array exists 13173 if (!gallery.children) { 13174 gallery.children = []; 13175 } 13176 // If children array is empty, build it from flat galleries array using parentId 13177 if (gallery.children.length === 0) { 13178 const childrenFromFlat = galleries.filter(g => g.parentId === gallery.id); 13179 if (childrenFromFlat.length > 0) { 13180 gallery.children = childrenFromFlat; 13181 console.log(`[renderGalleryItem] Built children for folder ${gallery.title}: ${gallery.children.length} children`); 13182 } 13183 } 13184} 13185 13186// Parent-capable items need their chevron and empty nested drop zone immediately, 13187// before the first child has been added. 13188const hasChildren = isFolder || isNavigableParent || gallery.children?.length > 0; 13189const hasChildrenClass = hasChildren ? 'has-children' : ''; 13190 13191// Standard collapsed state check 13192const isCollapsed = gallery.isCollapsed === true; 13193 13194// Simple expanded class logic - if the folder is not collapsed, add 'expanded' class 13195const isExpandedClass = (isFolder || isNavigableParent) && !isCollapsed ? 'expanded' : ''; 13196 13197// Add additional classes for special types 13198const spacerClass = isSpacer ? 'spacer-item' : ''; 13199const externalClass = isExternal ? 'external-link' : ''; 13200const folderClass = isFolder ? 'folder-item' : ''; 13201const navigableParentClass = isNavigableParent ? 'navigable-parent-item' : ''; 13202 13203 const duplicateIconSvg = ` 13204 <svg xmlns="[http://www.w3.org/2000/svg](http://www.w3.org/2000/svg)" width="24" height="24" viewBox="0 0 24 24"> 13205 <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"> 13206 <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"/> 13207 <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"/> 13208 </g> 13209 </svg> 13210 `; 13211 13212 13213 13214 // NEW: Add nesting level as a data attribute for styling 13215 const nestingLevelAttr = `data-nesting-level="${level}"`; 13216 13217 // Accessible href for SPA items (loadPage/loadGallery still preventDefault) 13218 const menuHref = gallery.isHomePage 13219 ? '/' 13220 : (gallery.url && String(gallery.url).startsWith('/') 13221 ? gallery.url 13222 : (gallery.slug ? ('/' + gallery.slug) : '#')); 13223 const childrenListId = 'gallery-children-' + gallery.id; 13224 const folderExpandedAttr = !isCollapsed ? 'true' : 'false'; 13225 const ariaCurrentAttr = (gallery.id === activeGalleryId && !isFolder && !isSpacer) 13226 ? ' aria-current="page"' 13227 : ''; 13228 13229 // Create the gallery item HTML with enhanced nesting indicators 13230 const html = ` 13231<li data-id="${gallery.id}" 13232 ${nestingLevelAttr} 13233 class="${gallery.id === activeGalleryId ? 'active' : ''} 13234 ${gallery.visible === false ? 'hidden-gallery' : ''} 13235 ${hasChildrenClass} ${isExpandedClass} ${spacerClass} ${externalClass} ${folderClass} ${navigableParentClass}">
13236 <div class="gallery-item-content nested-sortable-handle"> 13237 ${hasChildren ? ` 13238 <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, '"')}"> 13239 <svg class="icon toggle-icon ${!isCollapsed ? 'rotated' : ''}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> 13240 <polyline points="9 6 15 12 9 18"></polyline> 13241 </svg> 13242 </button> 13243 ` : ''} 13244 13245 <!-- Menu item with different rendering based on type --> 13246 ${isSpacer ? ` 13247 <!-- Spacer with non-breaking space to ensure height --> 13248 <span class="menu-item spacer" aria-hidden="true"> </span> 13249 ` : isExternal ? ` 13250 <!-- External URL uses an anchor tag with target="_blank" --> 13251 <a href="${gallery.url}" class="menu-item external-link" target="_blank" rel="noopener noreferrer"${ariaCurrentAttr}> 13252 ${gallery.title} 13253 </a> 13254 ` : isFolder ? ` 13255 <!-- Folder item with toggle functionality (button for keyboard / SR) --> 13256 <button type="button" class="menu-item folder" data-folder="true" 13257 onclick="toggleFolder(${gallery.id}, event)" aria-expanded="${folderExpandedAttr}" aria-controls="${childrenListId}"> 13258 ${gallery.title} 13259 </button> 13260 ` : ` 13261 <!-- Real link for SR/keyboard; SPA handlers keep click behavior --> 13262 <a href="${menuHref}" class="menu-item" onclick="${isPage ? `loadPage(${gallery.id}, event)` : `loadGallery(${gallery.id}, event)`}"${ariaCurrentAttr}> 13263 ${gallery.title} 13264 </a> 13265 `} 13266 13267<div class="controls"> 13268 ${!isSpacer ? ` 13269 <button class="btn visibility-toggle ${gallery.visible === false ? 'hidden' : ''}" onclick="toggleVisibility(${gallery.id}, event)"> 13270 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> 13271 ${gallery.visible !== false ? 13272 '<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>' : 13273 '<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>' 13274 } 13275 </svg> 13276 </button> 13277 13278 <button class="btn password-toggle ${gallery.passwordProtected ? 'protected' : ''}" onclick="toggleGalleryPasswordProtection(${gallery.id}, event)" title="${gallery.passwordProtected ? 'Remove Password Protection' : 'Add Password Protection'}"> 13279 <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24"> 13280 ${gallery.passwordProtected ? 13281 '<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>' : 13282 '<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"/>' 13283 } 13284 </svg> 13285 </button> 13286 13287 <button class="btn edit-btn" onclick="editGallery(${gallery.id}, event)">
13288 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> 13289 <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path> 13290 <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path> 13291 </svg> 13292 </button> 13293 ` : ''} 13294 13295 <button class="btn duplicate-btn" onclick="duplicateGalleryItem(${gallery.id}, event)" title="Duplicate"> 13296 ${duplicateIconSvg} 13297 </button> 13298 13299 13300 <button class="btn delete-btn" onclick="deleteGallery(${gallery.id}, event)"> 13301 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> 13302 <polyline points="3 6 5 6 21 6"></polyline> 13303 <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> 13304 </svg> 13305 </button> 13306${!isSpacer ? ` 13307<!-- Add home toggle button --> 13308<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'}"> 13309 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> 13310 ${gallery.isHomePage ? 13311 '<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"/>' : 13312 '<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>' 13313 } 13314 </svg> 13315</button> 13316` : ''} 13317 </div> 13318 </div> 13319 <ol id="${childrenListId}" class="nested-sortable ${level > 0 ? 'nested-level-' + level : ''}"> 13320 ${gallery.children && gallery.children.length > 0 ? 13321 gallery.children.map(child => renderGalleryItem(child, level + 1, maxLevel)).join('') : ''} 13322 </ol> 13323</li> 13324`; 13325 13326 return html; 13327} 13328 13329// Additional helper function to add visual indicators during drag operations 13330function addDragVisualFeedback() { 13331 // Add event listener for drag operations 13332 document.addEventListener('dragover', function(e) { 13333 // Find the closest nested sortable list 13334 const nearestList = e.target.closest('.nested-sortable'); 13335 if (!nearestList) return; 13336 13337 // Remove active-drop-target class from all lists 13338 document.querySelectorAll('.active-drop-target').forEach(el => { 13339 el.classList.remove('active-drop-target'); 13340 }); 13341 13342 // Add it to the current list 13343 nearestList.classList.add('active-drop-target'); 13344 13345 // Calculate if this would be a nested position 13346 // This is a simplified calculation - SortableJS has its own logic, 13347 // but this helps provide additional visual feedback 13348 const rect = nearestList.getBoundingClientRect(); 13349 const distanceFromLeft = e.clientX - rect.left; 13350 13351 // If we're close to the left edge of a nested list, add a class 13352 if (distanceFromLeft < 30) { 13353 nearestList.classList.add('potential-parent'); 13354 } else { 13355 nearestList.classList.remove('potential-parent'); 13356 } 13357 }); 13358 13359 // Remove classes when drag ends 13360 document.addEventListener('dragend', function() { 13361 document.querySelectorAll('.active-drop-target, .potential-parent').forEach(el => { 13362 el.classList.remove('active-drop-target', 'potential-parent'); 13363 }); 13364 }); 13365} 13366 13367// Make sure the addDragVisualFeedback function is called when the document is ready 13368document.addEventListener('DOMContentLoaded', function() { 13369 // Initialize the visual feedback helper 13370 addDragVisualFeedback(); 13371 13372 // If we're in edit mode, make sure the nested sortables are initialized 13373 if (document.body.classList.contains('edit-mode-active')) { 13374 setTimeout(function() { 13375 initializeNestedSortables(); 13376 }, 300); 13377 } 13378}); 13379 13380// Update the toggleEditMode function to ensure proper initialization of sortables 13381const originalToggleEditMode = window.toggleEditMode; 13382window.toggleEditMode = function() { 13383 originalToggleEditMode.apply(this, arguments); // Call original function 13384 13385 stopAutoAdvanceTimer(); 13386 13387 const sidebar = document.querySelector('.sidebar');
13388 const editControls = document.querySelector('.edit-controls'); 13389 13390 if (currentMenuLayout === 'horizontal') { 13391 if (document.body.classList.contains('edit-mode-active')) { 13392 // In horizontal layout and edit mode is ON: show sidebar and edit controls 13393 if(sidebar) sidebar.style.display = 'block'; // Or 'flex' if it's a flex container 13394 if(editControls) editControls.style.display = 'flex'; 13395 // Re-render the sidebar menu for editing 13396 renderGalleries(); 13397 } else { 13398 // In horizontal layout and edit mode is OFF: hide sidebar 13399 if(sidebar) sidebar.style.display = 'none'; 13400 // Re-render the horizontal menu for viewing 13401 renderHorizontalMenu(); 13402 } 13403 } else { 13404 // For other layouts (sidebar, top), ensure sidebar is visible 13405 if(sidebar) sidebar.style.display = 'block'; // Or 'flex' 13406 } 13407}; 13408 13409function verifyGalleryStructure() { 13410console.log("Verifying gallery structure integrity..."); 13411 13412// Count number of galleries 13413const galleryCount = galleries.length; 13414console.log(`Total galleries in flat array: ${galleryCount}`); 13415 13416// Create tree and count galleries in tree 13417const tree = createGalleryTree([...galleries]); 13418let treeCount = 0; 13419 13420function countGalleriesInTree(items) { 13421items.forEach(item => { 13422 treeCount++; 13423 if (item.children && item.children.length > 0) { 13424 countGalleriesInTree(item.children); 13425 } 13426}); 13427} 13428 13429countGalleriesInTree(tree); 13430console.log(`Total galleries in tree structure: ${treeCount}`); 13431 13432if (treeCount !== galleryCount) { 13433console.warn(`GALLERY COUNT MISMATCH: ${galleryCount} in array vs ${treeCount} in tree`); 13434return false; 13435} else { 13436console.log("Gallery structure is consistent"); 13437return true; 13438} 13439} 13440 13441function renderGalleries() { 13442console.log("Rendering galleries..."); 13443 13444 // Align with the explicit edit toggle only (not admin login / stale sidebar.editing) 13445 if (document.body.classList.contains('edit-mode-active')) { 13446 isEditing = true; 13447 } else if (typeof window.isEditing === 'boolean') { 13448 isEditing = window.isEditing; 13449 } 13450 13451 var galleriesSource = galleries; 13452 if (window.HelpSystem && typeof window.HelpSystem.getRenderGalleries === 'function') { 13453 var helpList = window.HelpSystem.getRenderGalleries(); 13454 if (helpList) galleriesSource = helpList; 13455 } 13456 13457try { 13458// Only fix circular references if absolutely necessary 13459const foundCircular = fixCircularReferences(); 13460 13461// Only create a new tree if we had to fix circular references 13462let galleryTree; 13463if (foundCircular) { 13464 console.log("Creating fresh gallery tree after fixing circular references"); 13465 galleryTree = createGalleryTree(galleriesSource); 13466} else { 13467 console.log("Using existing structure to create gallery tree"); 13468 galleryTree = createGalleryTree(galleriesSource); 13469} 13470 13471// Render the tree - IMPORTANT: Add null check 13472const tree = document.getElementById('galleryTree'); 13473if (!tree) { 13474 console.warn('galleryTree element not found, cannot render galleries'); 13475 return; // Exit the function if the element doesn't exist 13476} 13477 13478// Render the tree with maximum depth protection 13479tree.innerHTML = ` 13480 <ol class="nested-sortable"> 13481 ${galleryTree.map(gallery => renderGalleryItem(gallery, 0, 20)).join('')} 13482 </ol> 13483`; 13484 13485// Initialize sortable for all nested sortable elements 13486if (isEditing) { 13487 // Use requestAnimationFrame to ensure DOM is fully updated before initializing sortables 13488 // This prevents drag issues when items are added and immediately dragged 13489 requestAnimationFrame(() => { 13490 initializeNestedSortables(); 13491 }); 13492} 13493 13494console.log("Galleries rendered successfully"); 13495 13496// Set up folder toggle handler after rendering (works in both view and edit modes) 13497setupFolderToggleHandler(); 13498 13499// Verify structure integrity after rendering 13500verifyGalleryStructure(); 13501 13502if (typeof syncSeoNavA11y === 'function') syncSeoNavA11y(); 13503} catch (error) { 13504console.error("Error rendering galleries:", error); 13505} 13506} 13507 13508/** Prefer visible menu links for AT; keep SEO nav in DOM for crawlers. */ 13509function syncSeoNavA11y() { 13510 const seoNav = document.querySelector('.hydra-seo-nav'); 13511 if (!seoNav) return; 13512 const hasVisibleLinks = 13513 document.querySelectorAll('#galleryTree a.menu-item').length > 0 ||
13514 document.querySelectorAll('.horizontal-menu-container a.horizontal-menu-label').length > 0; 13515 const hideFromAt = hasVisibleLinks; 13516 seoNav.setAttribute('aria-hidden', hideFromAt ? 'true' : 'false'); 13517 // aria-hidden alone is not enough â focusable descendants must leave the tab order 13518 seoNav.querySelectorAll('a, button, [tabindex]').forEach((el) => { 13519 if (hideFromAt) { 13520 if (!el.hasAttribute('data-seo-prev-tabindex')) { 13521 el.setAttribute('data-seo-prev-tabindex', el.hasAttribute('tabindex') ? el.getAttribute('tabindex') : ''); 13522 } 13523 el.setAttribute('tabindex', '-1'); 13524 } else { 13525 const prev = el.getAttribute('data-seo-prev-tabindex'); 13526 if (prev === null) return; 13527 if (prev === '') el.removeAttribute('tabindex'); 13528 else el.setAttribute('tabindex', prev); 13529 el.removeAttribute('data-seo-prev-tabindex'); 13530 } 13531 }); 13532} 13533 13534function getMenuItemHref(galleryItem) { 13535 if (!galleryItem) return '#'; 13536 if (galleryItem.isExternal && galleryItem.url) return galleryItem.url; 13537 if (galleryItem.isHomePage) return '/'; 13538 if (galleryItem.url && String(galleryItem.url).startsWith('/')) return galleryItem.url; 13539 if (galleryItem.slug) return '/' + galleryItem.slug; 13540 return '#'; 13541} 13542 13543 13544function fixCircularReferences() { 13545console.log("Selectively checking for circular references..."); 13546 13547// Only process galleries that need checking: 13548// 1. Imported galleries (they're new and might cause issues) 13549// 2. Galleries with extremely deep nesting 13550// 3. Actual circular references 13551 13552const processed = new Set(); 13553 13554// First pass: Identify actual circular references without modifying structure 13555const circularIds = new Set(); 13556const suspiciousIds = new Set(); 13557 13558galleries.forEach(gallery => { 13559if (!gallery || !gallery.id) return; 13560 13561// Skip galleries we've already processed 13562if (processed.has(gallery.id)) return; 13563 13564// Check for self-reference - this is always wrong 13565if (gallery.parentId === gallery.id) { 13566 console.warn(`Self-reference detected: Gallery ${gallery.title} (${gallery.id}) is its own parent`); 13567 circularIds.add(gallery.id); 13568 return; 13569} 13570 13571// Skip galleries without parents 13572if (!gallery.parentId) { 13573 processed.add(gallery.id); 13574 return; 13575} 13576 13577// Follow the parent chain to detect circular references 13578const parentChain = [gallery.id]; 13579let currentId = gallery.parentId; 13580let loopCount = 0; 13581let foundCircular = false; 13582 13583while (currentId && loopCount < 20) { // Reasonable limit to prevent infinite loops 13584 loopCount++; 13585 13586 // If we've seen this ID before, it's a circular reference 13587 if (parentChain.includes(currentId)) { 13588 console.warn(`Circular reference detected in chain for gallery: ${gallery.title}`); 13589 circularIds.add(gallery.id); 13590 foundCircular = true; 13591 break; 13592 } 13593 13594 parentChain.push(currentId); 13595 13596 // Move up to the next parent 13597 const parent = galleries.find(g => g.id === currentId); 13598 currentId = parent?.parentId; 13599 13600 // If we can't find the parent, we can stop 13601 if (!parent) break; 13602} 13603 13604// If we hit our loop limit but didn't find a circular reference, 13605// it's suspicious but not definitely circular 13606if (loopCount >= 20 && !foundCircular) { 13607 console.warn(`Suspiciously deep parent chain for gallery ${gallery.title}, marking for review`); 13608 suspiciousIds.add(gallery.id); 13609} 13610 13611// Mark this gallery and all its ancestors as processed 13612parentChain.forEach(id => processed.add(id)); 13613}); 13614 13615// Second pass: Only fix the identified circular references 13616if (circularIds.size > 0) { 13617console.log(`Fixing ${circularIds.size} confirmed circular references`); 13618 13619galleries.forEach(gallery => { 13620 if (circularIds.has(gallery.id)) { 13621 console.log(`Breaking circular reference for gallery: ${gallery.title}`); 13622 gallery.parentId = null; 13623 } 13624}); 13625} else { 13626console.log("No circular references found"); 13627} 13628 13629// For suspicious galleries, don't automatically fix them 13630if (suspiciousIds.size > 0) { 13631console.warn(`Found ${suspiciousIds.size} galleries with unusually deep nesting - not fixing automatically`); 13632} 13633 13634return circularIds.size > 0; // Return true if we fixed anything 13635} 13636 13637// Deep linking implementation that uses the existing slug property 13638 13639// Function to get the slug for a gallery 13640function getGallerySlug(gallery) { 13641// First try to use the existing slug property 13642if (gallery.slug) { 13643console.log(`Using existing gallery slug: ${gallery.slug}`); 13644return gallery.slug;
13645} 13646 13647// If no slug exists, generate one from the title as fallback 13648if (gallery.title) { 13649const generatedSlug = gallery.title 13650 .toLowerCase() 13651 .replace(/[^ws-]/g, '') 13652 .replace(/s+/g, '-') 13653 .replace(/--+/g, '-') 13654 .trim(); 13655 13656console.log(`Generated slug from title: ${generatedSlug}`); 13657return generatedSlug || 'gallery'; 13658} 13659 13660// Default fallback 13661console.warn('Gallery has no slug or title, using default'); 13662return 'gallery'; 13663} 13664 13665// Function to update the URL when a gallery is loaded 13666// Update the URL when a gallery is loaded 13667function updateURLWithGallerySlug(gallery) { 13668 if (!gallery) return; 13669 13670 // Keep bookmarkable /_manage URL â do not rewrite to / or /slug when loading galleries/pages 13671 const __hydraPath = window.location.pathname; 13672 if (__hydraPath === '/_manage' || __hydraPath === '/_manage/') { 13673 console.log('[updateURLWithGallerySlug] On /_manage editor path; skipping slug URL update.'); 13674 return; 13675 } 13676 13677 // Get the slug for this gallery/page 13678 let slug = gallery.slug; 13679 13680 if (!slug && gallery.title) { 13681 slug = gallery.title.toLowerCase().replace(/s+/g, '-').replace(/[^w-]+/g, '').replace(/--+/g, '-').trim(); 13682 } 13683 13684 // Check if we're in preview mode (add this new code) 13685 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 13686 13687 // Check if current history state already has this galleryId 13688 const currentState = window.history.state; 13689 const currentPath = window.location.pathname; 13690 const isAlreadyCurrentPage = currentState && currentState.galleryId === gallery.id && 13691 ((isPreviewMode && currentPath.includes(slug)) || (!isPreviewMode && currentPath === '/' + slug)); 13692 13693 if (isPreviewMode) { 13694 // Extract GUID from current URL path - first segment after domain 13695 const pathParts = window.location.pathname.split('/').filter(Boolean); 13696 const siteGuid = pathParts[0]; 13697 13698 // Create history entry if we have a valid GUID, and it's not already the current state. 13699 // Home pages use /{guid}/ (canonical preview home), matching live's '/' special-case. 13700 if (siteGuid && (gallery.isHomePage || slug)) { 13701 const targetPath = gallery.isHomePage ? ('/' + siteGuid + '/') : ('/' + siteGuid + '/' + slug); 13702 const alreadyAtPreviewHome = gallery.isHomePage && ( 13703 currentPath === '/' + siteGuid || currentPath === '/' + siteGuid + '/' 13704 ); 13705 // Only push if URL is different OR if state doesn't match (allows multiple navigations) 13706 if ((!alreadyAtPreviewHome && currentPath !== targetPath) || !isAlreadyCurrentPage) { 13707 console.log('ð [updateURLWithGallerySlug] Updating URL to ' + targetPath + ' (preview mode), galleryId: ' + gallery.id); 13708 13709 // Update browser URL without reloading, preserving the GUID 13710 window.history.pushState( 13711 { galleryId: gallery.id }, 13712 gallery.title || 'Gallery', 13713 targetPath 13714 ); 13715 13716 // Update page title 13717 document.title = formatGalleryDocumentTitle(gallery.title); 13718 } else { 13719 console.log('ð [updateURLWithGallerySlug] URL and state already match, skipping history update (preview mode)'); 13720 } 13721 } 13722 13723 return; // Exit early, we've handled the preview case 13724 } 13725 13726 // Regular (non-preview) URL handling 13727 if (slug) { 13728 // If this gallery is the designated home page, keep the URL as '/' (canonical home URL) 13729 const targetPath = gallery.isHomePage ? '/' : '/' + slug; 13730 // Only push if URL is different OR if state doesn't match (allows multiple navigations) 13731 if (currentPath !== targetPath || !isAlreadyCurrentPage) { 13732 console.log('ð [updateURLWithGallerySlug] Updating URL to ' + targetPath + ', galleryId: ' + gallery.id); 13733 window.history.pushState( 13734 { galleryId: gallery.id }, 13735 gallery.title || 'Gallery', 13736 targetPath 13737 ); 13738 document.title = formatGalleryDocumentTitle(gallery.title); 13739 } else { 13740 console.log('ð [updateURLWithGallerySlug] URL and state already match, skipping history update'); 13741 } 13742 } 13743} 13744 13745 13746function loadHomePage() { 13747 // Find a gallery marked as home page (regardless of visibility) 13748 const homePage = galleries.find(gallery => gallery.isHomePage === true); 13749 13750 if (homePage) {
13751 console.log(`Loading home page: ${homePage.title} (Visible: ${homePage.visible !== false})`); 13752 13753 // Update active gallery ID 13754 activeGalleryId = homePage.id; 13755 13756 // Add a special flag to indicate we're loading the home page 13757 // This allows us to bypass visibility checks 13758 window._loadingHomePage = true; 13759 13760 // Determine if this is a page or gallery and load appropriately 13761 if (homePage.isPage) { 13762 loadPage(homePage.id); 13763 } else { 13764 loadGallery(homePage.id); 13765 } 13766 13767 // Clear the flag after loading 13768 setTimeout(() => { 13769 window._loadingHomePage = false; 13770 }, 100); 13771 13772 // Update UI state 13773 updateActiveStates(); 13774 updateMobileTitle(); 13775 if (typeof closeMobileMenu === 'function') closeMobileMenu(); 13776 13777 return true; 13778 } 13779 13780 console.log('No home page defined'); 13781 return false; 13782} 13783 13784// 4. Update the handleURLNavigation function to check for home page 13785// Find the existing handleURLNavigation function and modify as follows: 13786// In index.ts 13787function handleURLNavigation() { 13788 const path = window.location.pathname.substring(1); // Remove leading slash 13789 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 13790 let slug = path; 13791 let siteGuid = null; 13792 13793 // /_manage is the Hydra editor entry â treat like site root for slug resolution (not a gallery slug) 13794 if (!isPreviewMode && (path === '_manage' || path.startsWith('_manage/'))) { 13795 console.log('[handleURLNavigation] /_manage detected â using root navigation behavior'); 13796 slug = ''; 13797 } 13798 13799 if (isPreviewMode && path) { 13800 const pathParts = path.split('/'); 13801 if (pathParts.length >= 1) { 13802 siteGuid = pathParts[0]; // Path could be just GUID/ or GUID/slug 13803 } 13804 if (pathParts.length >= 2) { 13805 slug = pathParts[1]; // Slug is the part after GUID 13806 console.log(`Preview URL detected, using slug: "${slug}" under GUID: "${siteGuid}"`); 13807 } else { 13808 // Only GUID is present, or path is empty after GUID 13809 slug = ''; // Treat as root/home for the given GUID 13810 console.log(`Preview URL with only GUID: "${siteGuid}", checking for home page or default.`); 13811 } 13812 } 13813 13814 if (!slug) { // Handles root path ('/') or preview URL with only GUID ('/GUID/') 13815 console.log("No specific slug in path (root or GUID-only preview), checking for home page."); 13816 if (!loadHomePage() && galleries.length > 0) { 13817 const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu); 13818 if (firstVisibleGallery) { 13819 console.log("No home page, loading first visible item:", firstVisibleGallery.title); 13820 if (firstVisibleGallery.isPage) { 13821 loadPage(firstVisibleGallery.id); 13822 } else { 13823 loadGallery(firstVisibleGallery.id); 13824 } 13825 } else { 13826 console.log("No home page and no visible items to load for this path."); 13827 } 13828 } 13829 } else { 13830 console.log(`Handling URL navigation for slug: "${slug}"` + (isPreviewMode ? ` (Preview GUID: ${siteGuid})` : "")); 13831 // findGalleryByPath should be able to find the item using the slug, 13832 // handling preview mode internally if necessary or by being passed the correct slug. 13833 const matchingGallery = findGalleryByPath(slug, isPreviewMode, siteGuid); 13834 13835 if (matchingGallery) { 13836 console.log('Loading content from URL: "' + matchingGallery.title + '" (ID: ' + matchingGallery.id + ')'); 13837 console.log('ð [handleURLNavigation] Gallery properties - isPage:', matchingGallery.isPage, 'pageId:', matchingGallery.pageId, 'has pageElements:', !!(matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements))); 13838 13839 if (matchingGallery.isSubmenu) { 13840 console.log('Gallery is a submenu, skipping direct load. Parent expansion might be needed.'); 13841 // Potentially, you might want to find and expand its parent here if the UI supports it. 13842 } else { 13843 // Set activeGalleryId *before* calling loadPage/loadGallery 13844 activeGalleryId = matchingGallery.id; 13845 window.activeGalleryId = matchingGallery.id; 13846 13847 // Determine if this is a page by checking multiple indicators 13848 // Check isPage flag, pageId, or pageElements array 13849 const isPage = matchingGallery.isPage === true || 13850 (matchingGallery.pageId && matchingGallery.pageId.startsWith('page_')) || 13851 (matchingGallery.pageElements && Array.isArray(matchingGallery.pageElements) && matchingGallery.pageElements.length > 0); 13852 13853 if (isPage) { 13854 console.log('ð [handleURLNavigation] Detected as PAGE, loading page: "' + matchingGallery.title + '"'); 13855 const pageLoader = typeof window.loadPage === 'function' ? window.loadPage : loadPage; 13856 pageLoader(matchingGallery.id); 13857 } else { 13858 console.log('ð [handleURLNavigation] Detected as GALLERY, loading gallery: "' + matchingGallery.title + '"'); 13859 const galleryLoader = typeof window.loadGallery === 'function' ? window.loadGallery : loadGallery; 13860 galleryLoader(matchingGallery.id, null, { forceReload: true }); 13861 } 13862 } 13863 } else { 13864 console.log(`No matching gallery found for slug: "${slug}". Checking for home page as fallback.`); 13865 if (!loadHomePage()) { // Try loading home page 13866 const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer && !g.isFolder && !g.isSubmenu); 13867 if (firstVisibleGallery) { 13868 console.log(`Loading first visible gallery instead: "${firstVisibleGallery.title}"`); 13869 if (firstVisibleGallery.isPage) { 13870 loadPage(firstVisibleGallery.id); 13871 } else { 13872 loadGallery(firstVisibleGallery.id); 13873 } 13874 } else { 13875 console.log("No matching gallery, no home page, and no visible items to load."); 13876 // Optionally, display a 404 message in the gallery-container 13877 const galleryContainer = document.querySelector('.gallery-container'); 13878 if (galleryContainer) { 13879 galleryContainer.innerHTML = '<div>Page not found.</div>'; 13880 galleryContainer.style.opacity = '1'; 13881 } 13882 } 13883 } 13884 } 13885 } 13886 13887 // Update UI states after initiating load. 13888 // These functions should ideally use the now-set activeGalleryId. 13889 if (typeof updateActiveStates === 'function') updateActiveStates(); // For sidebar/tree 13890 if (typeof updateMobileTitle === 'function') updateMobileTitle(); // For mobile header 13891 13892 // **** ADDED FOR DEEP LINKING HORIZONTAL MENU **** 13893 // Determine current layout (it might not be set by MenuStyleCustomizer yet on direct load) 13894 let currentLayout = 'sidebar'; // Default assumption 13895 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) { 13896 currentLayout = window.MenuStyleCustomizer.settings.menuLayout; 13897 } else { 13898 // Fallback: check body class if MenuStyleCustomizer hasn't initialized settings 13899 if (document.body.classList.contains('menu-layout-horizontal')) { 13900 currentLayout = 'horizontal'; 13901 } 13902 } 13903 13904 console.log(`handleURLNavigation: Determined current layout as: ${currentLayout}`); 13905 13906 if (currentLayout === 'horizontal') { 13907 if (typeof updateActiveStatesHorizontal === 'function') { 13908 console.log("handleURLNavigation: Explicitly calling updateActiveStatesHorizontal for horizontal layout on direct navigation."); 13909 // It's possible menu items aren't rendered yet by renderHorizontalMenu if this is a very early call. 13910 // A small delay might be needed, or ensure renderHorizontalMenu has run. 13911 // For now, call it directly. If items aren't there, it won't do anything harmful. 13912 setTimeout(() => { // Add a slight delay to allow menu rendering 13913 updateActiveStatesHorizontal(); 13914 }, 100); // Adjust delay if needed, or find a more robust way to ensure menu is rendered. 13915 } else { 13916 console.warn("handleURLNavigation: updateActiveStatesHorizontal function not found for horizontal layout."); 13917 } 13918 } 13919} 13920 13921/** 13922* Enhanced loadPage function that properly cleans up gallery content 13923* @param {number} id - The page ID 13924* @param {Event} event - Optional event object 13925*/ 13926window.loadPage = function(id, event) { 13927 if (event && event.target && event.target.isConnected === false) { 13928 console.log('ð [loadPage] Ignoring navigation from detached element'); 13929 return; 13930 } 13931 if (event) { 13932 event.preventDefault(); 13933 event.stopPropagation(); 13934 const now = Date.now(); 13935 const lastCallTime = window._lastPageLoadTime || 0; 13936 window._lastPageLoadTime = now; 13937 if (now - lastCallTime < 100) {
13938 console.log('Ignoring duplicate loadPage call'); 13939 return; 13940 } 13941 } 13942 console.error('ð [loadPage] Loading page with gallery ID:', id); 13943 13944 stopAutoAdvanceTimer(); 13945 if (typeof dismissGalleryPasswordOverlay === 'function') { 13946 dismissGalleryPasswordOverlay(); 13947 } 13948 13949 let galleriesData = window.galleries || galleries; 13950 const gallery = findGalleryById(galleriesData, id); 13951 13952 if (!gallery) { 13953 console.warn('No gallery found with ID:', id, 'for page load.'); 13954 return; 13955 } 13956 13957 const contentLoadGeneration = beginHydraContentLoad('page', gallery.id, gallery.pageId || `page_${id}`); 13958 13959 window.activeGalleryId = id; 13960 activeGalleryId = id; 13961 window._activeContentType = 'page'; 13962 if (window.EnhancedGallerySystem) { 13963 window.EnhancedGallerySystem.currentGalleryId = null; 13964 } 13965 13966 if (typeof window.removeGalleryScriptsWithPause === 'function') { 13967 window.removeGalleryScriptsWithPause(); 13968 } 13969 13970 const galleryContainer = document.querySelector('.gallery-container'); 13971 if (galleryContainer) { 13972 // Clear the container but ensure it's visible 13973 galleryContainer.innerHTML = ''; 13974 galleryContainer.style.display = 'block'; 13975 galleryContainer.style.visibility = 'visible'; 13976 galleryContainer.style.opacity = '1'; 13977 console.log('ð [loadPage] Cleared and ensured gallery-container is visible'); 13978 } else { 13979 console.error('ð [loadPage] gallery-container not found!'); 13980 } 13981 13982 if (!gallery.pageId) gallery.pageId = `page_${id}`; 13983 if (!gallery.isPage) gallery.isPage = true; 13984 13985 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 13986 window.syncGalleryOptionsButtonVisibility(); 13987 } 13988 13989 if (!window._pageIdToGalleryId) window._pageIdToGalleryId = {}; 13990 window._pageIdToGalleryId[gallery.pageId] = id; 13991 13992 const seedPageElementsOntoManager = function() { 13993 if (gallery.pageElements && Array.isArray(gallery.pageElements)) { 13994 if (window.PageManager && window.PageManager.elements) { 13995 window.PageManager.elements[gallery.pageId] = [...gallery.pageElements]; 13996 } 13997 } else if (window.PageManager && window.PageManager.elements && 13998 window.PageManager.elements[gallery.pageId] && 13999 window.PageManager.elements[gallery.pageId].length > 0) { 14000 gallery.pageElements = [...window.PageManager.elements[gallery.pageId]]; 14001 } 14002 }; 14003 seedPageElementsOntoManager(); 14004 14005 const invokePageManagerLoad = function() { 14006 seedPageElementsOntoManager(); 14007 try { 14008 console.error('ð [loadPage] Calling PageManager.loadPage for pageId:', gallery.pageId); 14009 console.error('ð [loadPage] Gallery container exists:', !!galleryContainer, 'Container HTML length:', galleryContainer ? galleryContainer.innerHTML.length : 0); 14010 console.error('ð [loadPage] Page elements count:', gallery.pageElements ? gallery.pageElements.length : 0); 14011 console.error('ð [loadPage] PageManager.elements[' + gallery.pageId + '] exists:', !!(window.PageManager.elements && window.PageManager.elements[gallery.pageId])); 14012 14013 // Small delay to ensure DOM is ready, especially when navigating back via history 14014 setTimeout(() => { 14015 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) { 14016 console.log('ð [loadPage] Skipping stale delayed PageManager load for:', gallery.pageId); 14017 return; 14018 } 14019 14020 // Verify container still exists 14021 const containerCheck = document.querySelector('.gallery-container'); 14022 console.error('ð [loadPage] Container check before loadPage:', !!containerCheck, 'Same as original:', containerCheck === galleryContainer); 14023 14024 // PageManager.loadPage is async - we need to properly handle it 14025 try { 14026 const loadPromise = window.PageManager.loadPage(gallery.pageId); 14027 console.error('ð [loadPage] PageManager.loadPage called, returned:', typeof loadPromise, 'is promise:', loadPromise && typeof loadPromise.then === 'function'); 14028 14029 if (loadPromise && typeof loadPromise.then === 'function') { 14030 loadPromise.then(() => { 14031 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) { 14032 console.log('ð [loadPage] Ignoring stale PageManager completion for:', gallery.pageId); 14033 return; 14034 } 14035 14036 console.error('ð [loadPage] PageManager.loadPage promise resolved'); 14037 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 14038 window.syncGalleryOptionsButtonVisibility(); 14039 } 14040 // Ensure container visibility after PageManager has initialized 14041 if (galleryContainer) { 14042 const pageContainer = galleryContainer.querySelector('.page-container'); 14043 if (pageContainer) { 14044 pageContainer.style.display = 'block'; 14045 pageContainer.style.visibility = 'visible'; 14046 pageContainer.style.opacity = '1'; 14047 const elementCount = pageContainer.querySelectorAll('.page-element').length; 14048 console.error('ð [loadPage] Page container found and made visible, has', elementCount, 'elements'); 14049 if (elementCount === 0) { 14050 console.error('ð [loadPage] WARNING: Page container exists but has no elements!'); 14051 console.error('ð [loadPage] PageManager.elements[' + gallery.pageId + ']:', window.PageManager.elements[gallery.pageId]); 14052 } 14053 } else { 14054 console.error('â [loadPage] Page container not found after PageManager.loadPage'); 14055 console.error('â [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500)); 14056 console.error('â [loadPage] Gallery container children:', galleryContainer.children.length); 14057 } 14058 } else { 14059 console.error('â [loadPage] Gallery container is null after PageManager.loadPage'); 14060 } 14061 }).catch((error) => { 14062 console.error('â [loadPage] Error in PageManager.loadPage promise:', error); 14063 console.error('â [loadPage] Error stack:', error.stack); 14064 }); 14065 } else {
14066 // If loadPage doesn't return a promise, check after a delay 14067 console.error('ð [loadPage] loadPage did not return a promise, checking after delay'); 14068 setTimeout(() => { 14069 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) { 14070 console.log('ð [loadPage] Ignoring stale non-promise PageManager completion for:', gallery.pageId); 14071 return; 14072 } 14073 if (galleryContainer) { 14074 const pageContainer = galleryContainer.querySelector('.page-container'); 14075 if (pageContainer) { 14076 pageContainer.style.display = 'block'; 14077 pageContainer.style.visibility = 'visible'; 14078 pageContainer.style.opacity = '1'; 14079 console.error('ð [loadPage] Ensured page-container is visible (non-promise path)'); 14080 } else { 14081 console.error('â [loadPage] Page container not found (non-promise path)'); 14082 console.error('â [loadPage] Gallery container HTML:', galleryContainer.innerHTML.substring(0, 500)); 14083 } 14084 } 14085 }, 200); 14086 } 14087 } catch (loadError) { 14088 console.error('â [loadPage] Exception calling PageManager.loadPage:', loadError); 14089 console.error('â [loadPage] Error stack:', loadError.stack); 14090 } 14091 14092 const isCurrentlyEditing = typeof isInEditMode === 'function' ? isInEditMode() : false; 14093 if (isCurrentlyEditing && typeof window.PageManager.setEditMode === 'function') { 14094 window.PageManager.setEditMode(isCurrentlyEditing); 14095 } 14096 }, 50); // Small delay to ensure DOM is ready 14097 } catch (error) { 14098 console.error('â [loadPage] Error in loadPage function:', error); 14099 console.error('â [loadPage] Error stack:', error.stack); 14100 } 14101 }; 14102 14103 if (window.PageManager && typeof window.PageManager.loadPage === 'function') { 14104 invokePageManagerLoad(); 14105 } else { 14106 console.error('â [loadPage] PageManager not found yet â waiting for CDN proxy script'); 14107 console.error('â [loadPage] window.PageManager:', !!window.PageManager); 14108 console.error('â [loadPage] typeof loadPage:', window.PageManager ? typeof window.PageManager.loadPage : 'N/A'); 14109 whenPageManagerReady(function() { 14110 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'page', gallery.id)) { 14111 console.log('ð [loadPage] Skipping stale PageManager ready retry for:', gallery.pageId); 14112 return; 14113 } 14114 console.log('â [loadPage] PageManager became available â retrying page render'); 14115 invokePageManagerLoad(); 14116 }); 14117 } 14118 14119 // Apply menu visibility 14120 const bodyEl = document.body; 14121 if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 14122 bodyEl.classList.add('menu-hidden-on-page'); 14123 } else { 14124 bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden 14125 } 14126 applyMenuOverlayClassForGallery(gallery); 14127 14128 if (typeof updateActiveStates === 'function') updateActiveStates(); 14129 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 14130 if (typeof closeMobileMenu === 'function') closeMobileMenu(); 14131 // Close the gallery options panel when navigating to a different page 14132 if (typeof window.closeOptionsPanel === 'function') { 14133 window.closeOptionsPanel(); 14134 } 14135 if (typeof updateURLWithGallerySlug === 'function') updateURLWithGallerySlug(gallery); 14136 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 14137 window.syncGalleryOptionsButtonVisibility(); 14138 } 14139 if (typeof window !== 'undefined' && typeof document !== 'undefined') { 14140 window.dispatchEvent(new CustomEvent('page-loaded', { 14141 detail: { pageId: gallery.pageId, galleryId: id } 14142 })); 14143 } 14144} 14145 14146 14147 14148/** 14149* Ensures all required gallery styles are loaded 14150*/ 14151function ensureGalleryStylesLoaded() { 14152window.ensureGalleryStylesLoaded = ensureGalleryStylesLoaded; 14153// Use Worker route for gallery CSS (avoids CDN SSL issues) 14154const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; 14155const requiredStyles = [ 14156{ id: 'neon-gallery-css', href: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css' }, 14157{ id: 'quill-css', href: 'https://cdn.quilljs.com/1.3.6/quill.snow.css' } 14158]; 14159console.log('Loading gallery CSS from Worker:', galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css'); 14160
14161requiredStyles.forEach(style => { 14162if (!document.getElementById(style.id)) { 14163 const link = document.createElement('link'); 14164 link.id = style.id; 14165 link.rel = 'stylesheet'; 14166 link.href = style.href; 14167 if (style.href.includes('cdn.neonsky.app')) { 14168 link.onerror = function() { 14169 console.error('â Gallery CSS failed, retrying via proxy:', link.href); 14170 // Set flag if not already set 14171 if (!window.useCdnProxy) { 14172 window.useCdnProxy = true; 14173 console.error('ð© CDN_PROXY_FLAG SET: Gallery CSS failed, using proxy for all subsequent requests'); 14174 } 14175 const proxyUrl = link.href.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 14176 link.href = proxyUrl; 14177 link.onerror = function() { 14178 console.error('â Proxy failed, trying storage:', proxyUrl); 14179 link.href = link.href.replace('cdn.neonsky.app', 'storage.neonsky.app'); 14180 }; 14181 }; 14182 } 14183 document.head.appendChild(link); 14184 console.log(`Added gallery style: ${style.id}`); 14185} 14186}); 14187} 14188 14189 14190/** 14191* Ensures all required gallery scripts are loaded 14192* @returns {Promise} A promise that resolves when all scripts are loaded 14193*/ 14194function ensureGalleryScriptsLoaded() { 14195const requiredScripts = [ 14196{ id: 'crypto-js', src: 'https://cdn.jsdelivr.net/npm/[email protected]/crypto-js.min.js' }, 14197{ id: 'quill-js', src: 'https://cdn.quilljs.com/1.3.6/quill.min.js' }, 14198 { id: 'quill-integration', src: 'https://cdn.neonsky.app/quill-integration-v260923-002.js' } 14199]; 14200 14201const promises = requiredScripts.map(script => { 14202return new Promise((resolve, reject) => { 14203 // If script is already loaded, resolve immediately 14204 if (document.getElementById(script.id)) { 14205 resolve(); 14206 return; 14207 } 14208 14209 // Create and load the script 14210 const scriptElement = document.createElement('script'); 14211 scriptElement.id = script.id; 14212 scriptElement.src = script.src; 14213 scriptElement.onload = resolve; 14214 scriptElement.onerror = () => reject(new Error(`Failed to load script: ${script.src}`)); 14215 document.head.appendChild(scriptElement); 14216}); 14217}); 14218 14219return Promise.all(promises); 14220} 14221 14222/** 14223 * Creates a clean gallery context without using an iframe 14224 * @param {Object} gallery - The gallery object 14225 * @param {HTMLElement} container - The container to load the gallery into 14226 */ 14227function createGalleryContext(gallery, container) { 14228 console.log('Creating gallery context for:', gallery.title); 14229 14230 // CRITICAL: Ensure gallery CSS is loaded BEFORE loading the gallery script 14231 // This ensures styles are available even if NDJSON fails and falls back to classic JSON 14232 ensureGalleryStylesLoaded(); 14233 14234 // Generate the page alias safely 14235 const pageAlias = gallery.slug || gallery.title.toLowerCase().replace(/s+/g, '-'); 14236 14237 // Get our gallery options 14238 const galleryOptions = gallery.galleryOptions || {}; 14239 14240 // Get token data 14241 let authToken = null; 14242 // First try TokenManager if available 14243 if (window.TokenManager && typeof window.TokenManager.getToken === 'function') { 14244 authToken = window.TokenManager.getToken(); 14245 } 14246 // Then try global token 14247 else if (window.didToken) { 14248 authToken = window.didToken; 14249 } 14250 14251 // Set up Parameters for the gallery 14252 const galleryParameters = { 14253 SiteAlias: window.location.hostname, 14254 InitialPageUuid: gallery.pageId, 14255 InitialPageAlias: pageAlias, 14256 isInEditor: window.isEditing || false, 14257 siteId: window.siteId || '', 14258 isHydra: true 14259 }; 14260 14261 // If we have a token, add it 14262 if (authToken) { 14263 galleryParameters.hydraAuthToken = authToken; 14264 } 14265 14266 // Preserve original window.Parameters 14267 const originalParameters = window.Parameters; 14268 14269 // REMOVED: Loading indicator - no longer showing the loading animation 14270 14271 // Load the main gallery script - Use Worker route (avoids CDN SSL issues) 14272 const galleryScript = document.createElement('script'); 14273 const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; 14274 galleryScript.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; 14275 console.log('Loading gallery-main script from Worker:', galleryScript.src); 14276 galleryScript.onerror = function() { 14277 console.error('â Gallery script failed, retrying via proxy:', galleryScript.src); 14278 const proxyUrl = galleryScript.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 14279 galleryScript.src = proxyUrl; 14280 galleryScript.onerror = function() { 14281 console.error('â Proxy failed, trying storage:', proxyUrl); 14282 galleryScript.src = galleryScript.src.replace('cdn.neonsky.app', 'storage.neonsky.app'); 14283 }; 14284 }; 14285 14286 // Setup the gallery configuration 14287 const neonGalleryConfig = { 14288 useData: true, 14289 useCDN: true, 14290 version: 'live', 14291 manualCollectionName: "mod", 14292 layoutType: "grid", 14293 ...galleryOptions, 14294 siteId: window.siteId || '' 14295 }; 14296 14297 // Execute in a safe way that minimizes global namespace pollution 14298 try { 14299 // Set up the global variables needed by the gallery script 14300 window.Parameters = galleryParameters; 14301 window.neonGalleryConfig = neonGalleryConfig; 14302 14303 // Monitor for script load/error 14304 galleryScript.onload = () => { 14305 console.log('Gallery script loaded successfully for:', gallery.title); 14306 14307 // REMOVED: Don't need to remove the loading indicator since we don't add it 14308 }; 14309 14310 galleryScript.onerror = () => { 14311 console.error('Failed to load gallery script for:', gallery.title); 14312 //container.innerHTML = '<div class="gallery-error">Error loading gallery. Please try again later.</div>'; 14313 // Restore original parameters 14314 window.Parameters = originalParameters; 14315 }; 14316 14317 // Add the script to load the gallery 14318 document.body.appendChild(galleryScript); 14319 14320 // Set up a cleanup function that will be called when another gallery is loaded 14321 container.cleanup = () => { 14322 // Restore original parameters when cleaning up 14323 window.Parameters = originalParameters; 14324 14325 // Remove the gallery script to prevent conflicts 14326 galleryScript.remove(); 14327 14328 // Clear main gallery container 14329 if (container.parentNode) { 14330 container.innerHTML = ''; 14331 } 14332 }; 14333 } catch (error) { 14334 console.error('Error creating gallery context:', error); 14335 //container.innerHTML = '<div class="gallery-error">Error initializing gallery. Please try again later.</div>'; 14336 // Restore original parameters 14337 window.Parameters = originalParameters; 14338 } 14339} 14340 14341/** 14342* Only used for the PageManager's initializeDOM - ensures clean content swap 14343*/ 14344function cleanGalleryContainer() { 14345const galleryContainer = document.querySelector('.gallery-container'); 14346if (galleryContainer) { 14347galleryContainer.innerHTML = ''; 14348} 14349} 14350 14351 14352 14353/** 14354* Initializes the page using the PageManager 14355* To be used by PageManager.initializeDOM 14356*/ 14357function initPageContent(pageId) { 14358// First clear the container completely 14359cleanGalleryContainer(); 14360 14361// Then load the page content 14362if (window.PageManager && window.PageManager.loadPage) { 14363window.PageManager.loadPage(pageId); 14364} 14365} 14366 14367 14368/** 14369* Cleanup gallery content when changing between galleries or pages 14370*/ 14371function cleanupGalleryContent() { 14372// Get the gallery container 14373const galleryContainer = document.querySelector('.gallery-container'); 14374if (!galleryContainer) return; 14375 14376// Find the direct gallery content 14377const directContent = galleryContainer.querySelector('.gallery-direct-content'); 14378if (directContent && typeof directContent.cleanup === 'function') { 14379// Call the cleanup function 14380directContent.cleanup(); 14381} 14382 14383// Clear the container 14384galleryContainer.innerHTML = ''; 14385} 14386 14387 14388 14389 14390/** 14391* Simplified loadGallery function that follows the same content swap pattern 14392* @param {number} id - The gallery ID 14393* @param {Event} event - Optional event object 14394*/ 14395 14396// Override loadGallery to update URL 14397window.loadGallery = async function(id, event) { 14398 14399 14400// Stop event propagation if provided 14401if (event) { 14402 event.stopPropagation(); 14403} 14404 14405const galleryContainer = document.querySelector('.gallery-container'); 14406if (galleryContainer) { 14407 // Apply immediate hiding styles 14408 galleryContainer.style.opacity = '0'; 14409} 14410 14411console.log('Loading gallery with ID:', id); 14412 14413stopAutoAdvanceTimer();
14414if (typeof dismissGalleryPasswordOverlay === 'function') { 14415 dismissGalleryPasswordOverlay(); 14416} 14417 14418// Find gallery by ID 14419const gallery = findGalleryById(galleries, id); // Assuming `galleries` is accessible 14420 14421if (!gallery) { 14422 console.warn('No gallery found with ID:', id); 14423 if (galleryContainer) { 14424 galleryContainer.style.opacity = '1'; 14425 } // Show container if gallery not found 14426 return; 14427} 14428 14429// Check if this is a page, if so use loadPage instead 14430if (isPageMenuItem(gallery)) { 14431 // Use the loadPage function defined in this scope (or window.loadPage if you prefer) 14432 return loadPage(id, event); 14433} 14434 14435// Skip submenu items 14436if (gallery.isSubmenu) { 14437 console.log('Gallery is submenu, skipping URL update'); 14438 toggleSubmenu(id, event || { stopPropagation: () => {} }); // Assuming toggleSubmenu is available 14439 if (galleryContainer) { 14440 galleryContainer.style.opacity = '1'; 14441 } // Show container 14442 return; 14443} 14444 14445console.log('Found gallery:', gallery.title); 14446 14447window._activeContentType = 'gallery'; 14448beginHydraContentLoad('gallery', gallery.id, gallery.pageId); 14449if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { 14450 window.PageManager.clearCurrentPage(); 14451} 14452 14453// CRITICAL: Set active gallery ID consistently in both global and window scope 14454console.log('Setting active gallery ID to:', id, '(Previous value:', (window.activeGalleryId || activeGalleryId), ')'); 14455window.activeGalleryId = id; 14456activeGalleryId = id; // Ensure both variables are synchronized 14457 14458// Save reference to which gallery we're loading (for later verification) 14459window.currentLoadingGalleryId = id; 14460 14461// CRITICAL: Thorough cleanup with await to ensure completion 14462if (typeof window.removeGalleryScriptsWithPause === 'function') { 14463 await window.removeGalleryScriptsWithPause(); 14464} 14465 14466 14467// Clear all existing content from gallery container 14468if (!galleryContainer) { 14469 console.error('Gallery container not found'); 14470 return; 14471} 14472 14473// Force clear all content 14474galleryContainer.innerHTML = ''; 14475 14476 14477// CRITICAL: Double-check active gallery ID before updating UI 14478if (window.activeGalleryId !== id || activeGalleryId !== id) { 14479 console.warn('Active gallery ID changed unexpectedly before UI update, restoring to:', id); 14480 window.activeGalleryId = id; 14481 activeGalleryId = id; 14482} 14483 14484// Apply menu visibility based on the gallery's setting and edit mode 14485const bodyEl = document.body; 14486if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 14487 bodyEl.classList.add('menu-hidden-on-page'); 14488} else { 14489 bodyEl.classList.remove('menu-hidden-on-page'); // Ensure menu is visible if in edit mode or not hidden 14490} 14491applyMenuOverlayClassForGallery(gallery); 14492 14493// Update UI state using explicit window function references or local fallbacks 14494if (typeof window.updateActiveStates === 'function') { 14495 window.updateActiveStates(); 14496} else if (typeof updateActiveStates === 'function') { 14497 updateActiveStates(); 14498} 14499 14500if (typeof window.updateMobileTitle === 'function') { 14501 window.updateMobileTitle(); 14502} else if (typeof updateMobileTitle === 'function') { 14503 updateMobileTitle(); 14504} 14505 14506if (typeof window.closeMobileMenu === 'function') { 14507 window.closeMobileMenu(); 14508} else if (typeof closeMobileMenu === 'function') { 14509 closeMobileMenu(); 14510} 14511 14512// Update URL with gallery slug 14513if (typeof window.updateURLWithGallerySlug === 'function') { 14514 window.updateURLWithGallerySlug(gallery); 14515} else if (typeof updateURLWithGallerySlug === 'function') { 14516 updateURLWithGallerySlug(gallery); 14517} 14518 14519 // Create gallery container and load content 14520const neonGalleryContainer = document.createElement('div'); 14521neonGalleryContainer.id = 'neon-gallery-container'; 14522neonGalleryContainer.className = 'gallery-direct-content'; 14523neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString()); 14524neonGalleryContainer.style.width = '100%'; 14525neonGalleryContainer.style.height = '100%'; 14526galleryContainer.appendChild(neonGalleryContainer); 14527 14528// Add a cache-busting parameter to ensure script is freshly loaded 14529const timestamp = Date.now(); 14530 14531// Set up gallery parameters 14532window.Parameters = { 14533 SiteAlias: window.location.hostname, 14534 InitialPageUuid: gallery.pageId, 14535 InitialPageAlias: gallery.slug || slugify(gallery.title), // Assuming slugify is available 14536 isInEditor: window.isEditing || false, // Assuming window.isEditing is available 14537 siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available 14538 isHydra: true,
14539 galleryInstanceId: timestamp, 14540 // Added loadedGalleryId as per your original function 14541 loadedGalleryId: gallery.id 14542}; 14543 14544// Debug: Check what galleryOptions contains 14545console.error('ð INDEX.TS DEBUG - Gallery:', gallery.title, 'has isPerma:', gallery.galleryOptions?.isPerma, 'permaURL:', gallery.galleryOptions?.permaURL, 'loadTxId:', gallery.galleryOptions?.loadTxId, 'loadPermaURL:', gallery.galleryOptions?.loadPermaURL); 14546 14547// Set up gallery config 14548window.neonGalleryConfig = { 14549 useData: true, 14550 useCDN: true, 14551 version: 'live', 14552 manualCollectionName: gallery.galleryOptions?.manualCollectionName || "mod", 14553 layoutType: gallery.galleryOptions?.layoutType || "grid", 14554 // Spread gallery options AFTER setting defaults to ensure current gallery's settings take precedence 14555 // This ensures that only properties from the current gallery are included, not from previous galleries 14556 ...(gallery.galleryOptions || {}), 14557 // CRITICAL: Explicitly include Load Network fields to ensure they're passed to gallery script 14558 // These fields are required for the gallery to determine if it should use Load Network racing 14559 loadTxId: gallery.galleryOptions?.loadTxId || null, 14560 loadPermaURL: gallery.galleryOptions?.loadPermaURL || null, 14561 // Cloudflare CDN NDJSON (unique id) â when present, gallery loads CDN-first (3s) then Fly 14562 cdnNdjsonId: gallery.galleryOptions?.cdnNdjsonId || null, 14563 cdnNdjsonUrl: gallery.galleryOptions?.cdnNdjsonUrl || null, 14564 cdnNdjsonStableUrl: gallery.galleryOptions?.cdnNdjsonStableUrl || gallery.galleryOptions?.embedPointerUrl || null, 14565 embedPointerUrl: gallery.galleryOptions?.embedPointerUrl || gallery.galleryOptions?.cdnNdjsonStableUrl || null, 14566 cdnSaveTimestamp: gallery.galleryOptions?.cdnSaveTimestamp || null, 14567 siteId: window.siteId || gallery.siteId || '', // Assuming window.siteId is available 14568 // Added galleryId and galleryInstanceId as per your original function - these OVERRIDE spread 14569 galleryId: gallery.id, 14570 galleryInstanceId: timestamp 14571}; 14572 14573console.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); 14574 14575// Create and add the gallery script with cache-busting 14576// Use Worker route (avoids CDN SSL issues) 14577const script = document.createElement('script'); 14578const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; 14579script.src = galleryScriptsBase + 'neon-gallery-main-v260923-002.js'; // Using versioned file 14580script.setAttribute('data-gallery-id', gallery.id.toString()); // Keep data-gallery-id attribute 14581script.setAttribute('data-timestamp', timestamp.toString()); // Keep data-timestamp attribute 14582console.log('Loading gallery-main script from Worker:', script.src); 14583script.onerror = function() { 14584 console.error('â Gallery script failed, retrying via proxy:', script.src); 14585 const proxyUrl = script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 14586 script.src = proxyUrl; 14587 script.onerror = function() { 14588 console.error('â Proxy failed, trying storage:', proxyUrl); 14589 script.src = script.src.replace('cdn.neonsky.app', 'storage.neonsky.app'); 14590 }; 14591}; 14592 14593// Added script.onload from your original function 14594script.onload = function() { 14595 if (window.currentLoadingGalleryId !== gallery.id) { 14596 console.warn('Gallery ID mismatch! Expected:', gallery.id, 14597 'Current:', window.currentLoadingGalleryId); 14598 } 14599 console.log(`Gallery script loaded for: ${gallery.title} (ID: ${gallery.id})`); 14600}; 14601 14602 14603if (galleryContainer) { 14604 setTimeout(() => { 14605 galleryContainer.style.opacity = '1'; 14606 }, 50); // Small delay to ensure content is ready 14607 } 14608 14609console.log(`Loading fresh gallery script for: ${gallery.title}`); 14610document.body.appendChild(script); 14611} 14612 14613// Function to ensure gallery container is visible 14614function ensureGalleryVisibility() { 14615 const galleryContainer = document.querySelector('.gallery-container'); 14616 if (galleryContainer && galleryContainer.style.opacity === '0') { 14617 console.log('ensureGalleryVisibility: Setting gallery container opacity to 1'); 14618 galleryContainer.style.opacity = '1'; 14619 } 14620} 14621 14622// Handle browser back/forward navigation 14623window.addEventListener('popstate', async function(event) { // Make it async 14624 if (window.__hydraPopstateLock) { 14625 return; 14626 } 14627 window.__hydraPopstateLock = true; 14628 try { 14629 console.error('ð [Popstate] Event triggered. State:', event.state, 'Current URL:', window.location.pathname); 14630 let galleryIdToLoad = null; 14631 let galleryToLoad = null; 14632 14633 // First, try to get galleryId from event.state 14634 if (event.state && event.state.galleryId) { 14635 galleryIdToLoad = event.state.galleryId; 14636 console.error('ð [Popstate] Found galleryId in event.state:', galleryIdToLoad); 14637 // Ensure galleries array is available and use findGalleryById helper 14638 const currentGalleries = window.galleries || galleries || []; 14639 galleryToLoad = findGalleryById(currentGalleries, galleryIdToLoad); 14640 } 14641 14642 if (window.__closingNestedLightbox) { 14643 window.__closingNestedLightbox = false;
14644 if (typeof updateActiveStates === 'function') updateActiveStates(); 14645 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 14646 return; 14647 } 14648 14649 if (window.lightboxNavigationStack?.length > 0 && 14650 typeof window.closeNestedGalleryLightbox === 'function') { 14651 const stackTop = window.lightboxNavigationStack[window.lightboxNavigationStack.length - 1]; 14652 const parentId = stackTop?.parentGalleryId; 14653 if (parentId && (event.state?.galleryId === parentId || galleryToLoad?.id === parentId)) { 14654 window.closeNestedGalleryLightbox({ fromPopstate: true }); 14655 if (typeof updateActiveStates === 'function') updateActiveStates(); 14656 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 14657 return; 14658 } 14659 } 14660 14661 if (typeof dismissGalleryPasswordOverlay === 'function') { 14662 dismissGalleryPasswordOverlay(); 14663 } 14664 14665 // If no gallery found from state, resolve from URL slug (handles history entries with null state) 14666 if (!galleryToLoad) { 14667 console.error('ð [Popstate] No galleryId in event.state or gallery not found, reading from URL:', window.location.pathname); 14668 const path = window.location.pathname.substring(1); 14669 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 14670 let slug = path; 14671 let siteGuid = null; 14672 if (!isPreviewMode && (path === '_manage' || path.startsWith('_manage/'))) { 14673 slug = ''; 14674 } 14675 if (isPreviewMode && path) { 14676 const pathParts = path.split('/'); 14677 if (pathParts.length >= 1) siteGuid = pathParts[0]; 14678 if (pathParts.length >= 2) slug = pathParts[1]; 14679 else slug = ''; 14680 } 14681 if (slug && typeof findGalleryByPath === 'function') { 14682 galleryToLoad = findGalleryByPath(slug, isPreviewMode, siteGuid); 14683 if (galleryToLoad) { 14684 console.error('ð [Popstate] Resolved from URL slug: "' + galleryToLoad.title + '" (ID: ' + galleryToLoad.id + ')'); 14685 } 14686 } 14687 } 14688 14689 // If we have a gallery from state or URL, load it 14690 let contentLoaded = false; 14691 if (galleryToLoad) { 14692 console.error('ð [Popstate] Found gallery in history: "' + galleryToLoad.title + '" (ID: ' + galleryToLoad.id + ')'); 14693 console.error('ð [Popstate] Gallery properties - isPage:', galleryToLoad.isPage, 'pageId:', galleryToLoad.pageId, 'has pageElements:', !!(galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements))); 14694 14695 activeGalleryId = galleryToLoad.id; 14696 window.activeGalleryId = galleryToLoad.id; 14697 14698 // Browser history navigation is explicit user intent â load content regardless of menu visibility. 14699 // Determine if this is a page by checking multiple indicators 14700 const isPage = galleryToLoad.isPage === true || 14701 (galleryToLoad.pageId && galleryToLoad.pageId.startsWith('page_')) || 14702 (galleryToLoad.pageElements && Array.isArray(galleryToLoad.pageElements) && galleryToLoad.pageElements.length > 0); 14703 14704 if (isPage) { 14705 console.error('ð [Popstate] Detected as PAGE, loading page "' + galleryToLoad.title + '"'); 14706 const pageLoader = typeof window.loadPage === 'function' ? window.loadPage : loadPage; 14707 if (typeof pageLoader === 'function') { 14708 console.error('ð [Popstate] Calling loadPage for ID:', galleryToLoad.id); 14709 pageLoader(galleryToLoad.id); 14710 contentLoaded = true; 14711 } else { 14712 console.error('â [Popstate] loadPage function not available'); 14713 } 14714 } else { 14715 console.error('ð [Popstate] Detected as GALLERY, loading gallery "' + galleryToLoad.title + '"'); 14716 // Clear page state left over from prior page navigation 14717 if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { 14718 window.PageManager.clearCurrentPage(); 14719 } 14720 window._activeContentType = 'gallery'; 14721 const galleryLoader = typeof window.loadGallery === 'function' ? window.loadGallery : loadGallery; 14722 if (typeof galleryLoader === 'function') { 14723 await galleryLoader(galleryToLoad.id, null, { forceReload: true }); 14724 contentLoaded = true; 14725 } else { 14726 console.error('â [Popstate] loadGallery function not available'); 14727 } 14728 } 14729 } else { 14730 console.error('â [Popstate] No gallery found and handleURLNavigation not available'); 14731 } 14732 14733 // Update UI states after loading (or attempting to load) content 14734 if (typeof updateActiveStates === 'function') updateActiveStates(); 14735 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 14736 14737 // Ensure gallery visibility after popstate navigation 14738 setTimeout(ensureGalleryVisibility, 1000); 14739 14740 let currentLayout = 'sidebar'; 14741 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings && window.MenuStyleCustomizer.settings.menuLayout) { 14742 currentLayout = window.MenuStyleCustomizer.settings.menuLayout; 14743 } else if (document.body.classList.contains('menu-layout-horizontal')) { 14744 currentLayout = 'horizontal'; 14745 } 14746 14747 if (currentLayout === 'horizontal') { 14748 if (typeof updateActiveStatesHorizontal === 'function') { 14749 console.log("Popstate: Calling updateActiveStatesHorizontal for horizontal layout with a delay."); 14750 setTimeout(() => { 14751 console.log("[Delayed Update from Popstate] Calling updateActiveStatesHorizontal."); 14752 updateActiveStatesHorizontal(); 14753 }, 150); // Slightly longer delay for popstate, as full content might be re-initializing 14754 } 14755 } 14756 } finally { 14757 setTimeout(function() { window.__hydraPopstateLock = false; }, 300); 14758 } 14759}); 14760 14761// Check for direct URL access on page load (fallback when bootstrap did not route) 14762document.addEventListener('DOMContentLoaded', function() { 14763setTimeout(function() { 14764console.log('Checking for deep links after DOM content loaded'); 14765var __p = window.location.pathname; 14766if (__p !== '/' && __p !== '/_manage' && __p !== '/_manage/' && !window.__hydraBootstrapRouteAttempted) { 14767 handleURLNavigation(); 14768} 14769// Ensure gallery visibility after URL navigation 14770setTimeout(ensureGalleryVisibility, 1000); 14771}, 500); 14772}); 14773 14774document.addEventListener('page-save-requested', function(e) { 14775const data = e.detail; 14776if (data && data.pageId && data.elements) { 14777// Find the gallery/page that corresponds to this pageId 14778const page = galleries.find(g => g.pageId === data.pageId); 14779if (page) { 14780 // Store the page elements 14781 page.pageElements = data.elements; 14782 14783 // Save to server 14784 saveGalleries(); 14785} 14786} 14787}); 14788 14789document.addEventListener('DOMContentLoaded', function() { 14790function preloadRichTextEditor() { 14791 if (window.RichTextEditor && typeof window.RichTextEditor._ensureEditorDependencies === 'function') { 14792 console.log('Preloading Rich Text Editor dependencies'); 14793 return window.RichTextEditor._ensureEditorDependencies() 14794 .then(() => console.log('Rich Text Editor dependencies preloaded successfully')) 14795 .catch(error => console.warn('Failed to preload Rich Text Editor:', error)); 14796 } 14797 14798 if (window.PageManager && typeof window.PageManager._ensureRichTextEditor === 'function') { 14799 return window.PageManager._ensureRichTextEditor() 14800 .catch(error => console.warn('Failed to preload Rich Text Editor via PageManager:', error)); 14801 } 14802 14803 return null; 14804} 14805 14806preloadRichTextEditor(); 14807setTimeout(preloadRichTextEditor, 1000); 14808setTimeout(preloadRichTextEditor, 3000); 14809}); 14810 14811document.addEventListener('DOMContentLoaded', function() { 14812const submenuCheck = document.getElementById('createSubmenu'); 14813const submenuTitleField = document.getElementById('submenuTitle'); 14814const submenuTitleGroup = submenuTitleField?.parentElement; 14815 14816if (submenuCheck && submenuTitleGroup) { 14817submenuCheck.addEventListener('change', function() { 14818 submenuTitleGroup.style.display = this.checked ? 'block' : 'none'; 14819}); 14820} 14821}); 14822 14823// Also check immediately if document is already loaded (fallback only â avoid duplicate loadPage vs bootstrap) 14824if (document.readyState !== 'loading') { 14825setTimeout(function() { 14826console.log('Document already loaded, checking for deep links'); 14827var __p2 = window.location.pathname; 14828if (__p2 !== '/' && __p2 !== '/_manage' && __p2 !== '/_manage/' && !window.__hydraBootstrapRouteAttempted) { 14829 handleURLNavigation(); 14830} 14831}, 300); 14832} 14833 14834// ============================================ 14835// CONCURRENT EDIT CONFLICT DETECTION 14836// ============================================ 14837(function() { 14838 // Initialize local timestamp tracking 14839 window.lastKnownConfigTimestamp = null; 14840 let conflictPollInterval = null; 14841 14842 // Initialize timestamp on page load 14843 async function initializeConfigTimestamp() { 14844 try { 14845 const timestampUrl = typeof getApiUrl === 'function' ? 14846 getApiUrl('/api/config-timestamp') : 14847 '/api/config-timestamp'; 14848 console.error('[CONFLICT DETECT] Initializing config timestamp from:', timestampUrl); 14849 const response = await fetch(timestampUrl); 14850 if (response.ok) { 14851 const data = await response.json(); 14852 if (data.lastModified) { 14853 window.lastKnownConfigTimestamp = data.lastModified; 14854 console.error('[CONFLICT DETECT] Initialized config timestamp:', window.lastKnownConfigTimestamp); 14855 } else { 14856 console.error('[CONFLICT DETECT] No lastModified in response:', data); 14857 } 14858 } else { 14859 console.error('[CONFLICT DETECT] Failed to initialize timestamp, status:', response.status); 14860 } 14861 } catch (error) { 14862 console.error('[CONFLICT DETECT] Could not initialize config timestamp:', error); 14863 } 14864 } 14865
14866 // Check for conflicts by polling server 14867 async function checkForConflicts() { 14868 // Only poll when in edit mode 14869 if (!document.body.classList.contains('edit-mode-active')) { 14870 console.error('[CONFLICT DETECT] Skipping poll - not in edit mode'); 14871 return; 14872 } 14873 14874 try { 14875 const timestampUrl = typeof getApiUrl === 'function' ? 14876 getApiUrl('/api/config-timestamp') : 14877 '/api/config-timestamp'; 14878 console.error('[CONFLICT DETECT] Polling for conflicts from:', timestampUrl); 14879 console.error('[CONFLICT DETECT] Current local timestamp:', window.lastKnownConfigTimestamp); 14880 14881 const response = await fetch(timestampUrl); 14882 14883 if (response.ok) { 14884 const data = await response.json(); 14885 const serverTimestamp = data.lastModified; 14886 console.error('[CONFLICT DETECT] Server timestamp:', serverTimestamp); 14887 14888 // If we have a local timestamp and it differs from server, show conflict 14889 if (window.lastKnownConfigTimestamp !== null && 14890 serverTimestamp !== null && 14891 serverTimestamp !== window.lastKnownConfigTimestamp) { 14892 console.error('[CONFLICT DETECT] â ï¸ CONFLICT DETECTED! Local:', window.lastKnownConfigTimestamp, 'Server:', serverTimestamp); 14893 showConflictIndicator(); 14894 } else { 14895 console.error('[CONFLICT DETECT] No conflict - timestamps match or null'); 14896 hideConflictIndicator(); 14897 } 14898 } else { 14899 console.error('[CONFLICT DETECT] Poll failed with status:', response.status); 14900 } 14901 } catch (error) { 14902 console.error('[CONFLICT DETECT] Error checking for conflicts:', error); 14903 } 14904 } 14905 14906 // Show conflict indicator icon 14907 function showConflictIndicator() { 14908 const indicator = document.getElementById('conflictIndicator'); 14909 console.error('[CONFLICT DETECT] Showing conflict indicator, element found:', !!indicator); 14910 if (indicator) { 14911 indicator.style.display = 'flex'; 14912 indicator.style.visibility = 'visible'; 14913 indicator.style.opacity = '1'; 14914 indicator.style.background = '#d32f2f'; 14915 indicator.style.borderColor = '#d32f2f'; 14916 console.error('[CONFLICT DETECT] Conflict indicator displayed'); 14917 } else { 14918 console.error('[CONFLICT DETECT] â ï¸ Conflict indicator element not found in DOM!'); 14919 } 14920 } 14921 14922 // Hide conflict indicator icon 14923 function hideConflictIndicator() { 14924 const indicator = document.getElementById('conflictIndicator'); 14925 if (indicator) { 14926 indicator.style.display = 'none'; 14927 indicator.style.visibility = 'hidden'; 14928 indicator.style.opacity = '0'; 14929 } 14930 } 14931 14932 // Start/stop polling based on edit mode 14933 function updatePolling() { 14934 const isEditMode = document.body.classList.contains('edit-mode-active'); 14935 14936 // Always hide indicator when not in edit mode 14937 if (!isEditMode) { 14938 hideConflictIndicator(); 14939 } 14940 14941 if (isEditMode && !conflictPollInterval) { 14942 // Start polling every 10 seconds 14943 conflictPollInterval = setInterval(checkForConflicts, 10000); 14944 console.error('[CONFLICT DETECT] â Started conflict detection polling (every 10s)'); 14945 14946 // Also check immediately 14947 checkForConflicts(); 14948 } else if (!isEditMode && conflictPollInterval) { 14949 // Stop polling when not in edit mode 14950 clearInterval(conflictPollInterval); 14951 conflictPollInterval = null; 14952 hideConflictIndicator(); 14953 console.error('[CONFLICT DETECT] Stopped conflict detection polling'); 14954 } 14955 } 14956 14957 // Initialize on page load 14958 if (document.readyState === 'loading') { 14959 document.addEventListener('DOMContentLoaded', function() { 14960 initializeConfigTimestamp(); 14961 // Watch for edit mode changes 14962 const editModeObserver = new MutationObserver(updatePolling); 14963 editModeObserver.observe(document.body, { 14964 attributes: true, 14965 attributeFilter: ['class'] 14966 }); 14967 updatePolling(); 14968 }); 14969 } else { 14970 initializeConfigTimestamp(); 14971 // Watch for edit mode changes 14972 const editModeObserver = new MutationObserver(updatePolling); 14973 editModeObserver.observe(document.body, { 14974 attributes: true, 14975 attributeFilter: ['class'] 14976 }); 14977 updatePolling(); 14978 } 14979})(); 14980 14981</script>
14981 14982</head> 14983<body class="menu-layout-sidebar"> 14984 <a class="hydra-skip-link" href="#hydra-main-content">Skip to content</a> 14985<nav class="hydra-seo-nav" aria-label="Site navigation"> 14986 <ul> 14987 <li><a href="/">Intro</a></li> 14988 <li><a href="/ferguson-michael-brown">Ferguson Uprising, Michael Brown</a></li> 14989 <li><a href="/life">Life</a></li> 14990 <li><a href="/news">News</a></li> 14991 <li><a href="/sports">Sports</a></li> 14992 <li><a href="/portraits-&-illustrations">Portraits & Illustrations</a></li> 14993 <li><a href="/heroin-in-st.-louis">Heroin in St. Louis</a></li> 14994 <li><a href="/death-on-the-rails">Death on the rails</a></li> 14995 <li><a href="/st.-louis,-guns,-shootings-and-crimes">St. Louis, guns, shootings and crimes</a></li> 14996 <li><a href="/afghanistan,-october-2006">Afghanistan, October 2006</a></li> 14997 <li><a href="/iraq,-september-2004">Iraq, September 2004</a></li> 14998 <li><a href="/dominician-baseball">Dominician baseball</a></li> 14999 <li><a href="/oliver,-$6.50-an-hour-cop">Oliver, $6.50 an hour cop</a></li> 15000 <li><a href="/gephardt's-last-run">Gephardt's last run</a></li> 15001 <li><a href="/multimedia-copy">Multimedia</a></li> 15002 <li><a href="/page-1772690596779">Fur, Feathers and Friends</a></li> 15003 <li><a href="/view-from-above">View From Above</a></li> 15004 <li><a href="/sun-moon-and-stars">Sun, Moon and Stars</a></li> 15005 <li><a href="/bio">Bio</a></li> 15006 <li><a href="/contact">Contact</a></li> 15007 </ul> 15008</nav> 15009<noscript class="hydra-gallery-noscript"> 15010 <article> 15011 <h1>Life | Simple Image Page</h1> 15012 <div class="gallery-overview"><p style="line-height: 1; margin-bottom: 0.5em;">Scroll Horizontally--></p></div> 15013 <ul class="gallery-images"> 15014 <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/161113-DC-SupermoonSTL.jpg" alt="Supermoon St. Louis, Arch" loading="lazy"></li> 15015 <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/34-60-08-CarsD-04a.jpg" alt="Prayer Shot" loading="lazy"></li> 15016 <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-Scout-Flags-1a-copy.jpg" alt="Memorial Day scouts" loading="lazy"></li> 15017 <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/57-CD-DC-Tumble-Kids1.jpg" alt="Flipping out" loading="lazy"></li> 15018 <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/a04-CD-DC-Robins-1.jpg" alt="Baby Robbins" loading="lazy"></li> 15019 <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/54-CD-DC-New-Town-Kids.jpg" alt="Dock" loading="lazy"></li> 15020 <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-Frosty-December-2.jpg" alt="Frosty morning" loading="lazy"></li> 15021 <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/Portfolio---022.jpg" alt="Santa hatch" loading="lazy"></li> 15022 <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/070413-DC-Fireworks-2A.jpg" alt="Fair St. Louis fireworks light up the St. Louis skyline on Thursday, July 4, 2013. This picture was shot from the roof of the Pierre Chouteau building in 4400 Lindell Blvd." loading="lazy"></li> 15023 <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/Portfolio---018.jpg" alt="Memorial Day" loading="lazy"></li> 15024 <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/64-abes-.jpg" alt="00011184-GNN-004" loading="lazy"></li> 15025 <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/Dance-BWb.jpg" alt="Dance BWb" loading="lazy"></li> 15026 <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/fashion-show.jpg" alt="fashion show" loading="lazy"></li> 15027 <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/Big-Surf-waves.jpg" alt="Big Surf waves" loading="lazy"></li> 15028 <li><img src="https://cdn.neonsky.app/4bd5ec04c2f91/images/CD-DC-STL-Heat.jpg" alt="Rainbow hop" loading="lazy"></li> 15029 </ul> 15030 <p>
15030<a href="https://davidcarsonphotos.com/life">View full gallery</a></p> 15031 </article> 15032</noscript> 15033 15034 <div class="global-header-controls"> 15035 15036 <button id="editButton" class="btn-icon btn-primary" onclick="toggleEditMode()" style="display: none;" data-help-id="edit-toggle"> 15037 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 15038 <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"> 15039 <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"/> 15040 <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"/> 15041 </g> 15042 </svg> 15043 </button> 15044 <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"> 15045 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="color: white;"> 15046 <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"/> 15047 </svg> 15048 </button> 15049 </div> 15050 15051 <div class="container"> 15052 <div class="mobile-header"> 15053 <div class="mobile-header-content"> 15054 <div class="horizontal-header-logo"></div> 15055 <div class="horizontal-menu-container"></div> 15056 </div> 15057 <button id="hamburgerBtn" class="hamburger-btn" type="button" aria-label="Menu" aria-expanded="false" aria-controls="siteSidebar"> 15058 <span class="hamburger-icon" aria-hidden="true"> 15059 <span class="hamburger-line"></span> 15060 <span class="hamburger-line"></span> 15061 <span class="hamburger-line"></span> 15062 </span> 15063 </button> 15064 </div> 15065 <div id="mobileBackdrop" class="mobile-backdrop"></div> 15066 15067 <div class="sidebar" id="siteSidebar" role="navigation" aria-label="Site"> 15068 <div class="sidebar-header"> 15069</div> 15070 15071 15072 <div class="edit-controls" data-help-id="edit-toolbar"> 15073 <button type="button" class="btn-icon btn-primary" onclick="openHydraHelp()" data-custom-tooltip="Help" data-help-id="help-toolbar"> 15074 <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"> 15075 <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" /> 15076 </svg> 15077 </button> 15078 <button class="btn-icon btn-primary" onclick="toggleStyleEditor(undefined, event)" data-custom-tooltip="Edit Style" data-help-id="style-toolbar"> 15079 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 15080 <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"/> 15081 </svg> 15082 </button> 15083 <button class="btn-icon btn-primary" onclick="toggleAddForm()" data-custom-tooltip="Add"> 15084 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 15085 <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"> 15086 <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"/> 15087 <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"/> 15088 </g> 15089 </svg> 15090 </button> 15091 <button class="btn-icon btn-primary" onclick="toggleSidebarElementForm(
15091)" data-custom-tooltip="Add Element"> 15092 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 15093 <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"/> 15094 </svg> 15095 </button> 15096<button class="btn-icon btn-primary" onclick="toggleMetadataEditor()" data-custom-tooltip="Site Metadata"> 15097<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" color="currentColor"><path d="M7.5 4.945H16a1.5 1.5 0 0 1 1.5 1.5v1.5m-2.5 5H9m3 4H9"/><path d="M18.497 2H6.307c-.496 0-1.005.073-1.406.368c-1.274.935-2.256 3.02-.273 4.903c.556.528 1.334.72 2.099.72h11.557c.793 0 2.216.113 2.216 2.536v7.454c0 2.22-1.79 4.019-3.997 4.019h-9.03c-2.204 0-3.807-1.557-3.933-3.929L3.506 5.166"/></g></svg> 15098</button> 15099<button id="logoutButton" class="btn-icon btn-primary" onclick="logout()" data-custom-tooltip="Log Out" style="display: none;"> 15100 <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> 15101 <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"/> 15102 </svg> 15103 </button> 15104
15104<script> 15105 15106// Initialize mobile menu functionality 15107function initMobileMenu() { 15108 const hamburgerBtn = document.getElementById('hamburgerBtn'); 15109 const sidebar = document.querySelector('.sidebar'); 15110 const mobileBackdrop = document.getElementById('mobileBackdrop'); 15111 15112 // --- ADDED CHECK: Only update header if layout is NOT horizontal --- 15113 let currentLayout = 'sidebar'; // Default 15114 if (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings) { 15115 currentLayout = window.MenuStyleCustomizer.settings.menuLayout || 'sidebar'; 15116 } 15117 15118 // --- END ADDED CHECK --- 15119 15120 // Ensure backdrop is properly hidden for horizontal layout on page load 15121 if (mobileBackdrop && document.body.classList.contains('menu-layout-horizontal')) { 15122 mobileBackdrop.classList.remove('mobile-open'); 15123 mobileBackdrop.classList.remove('active'); 15124 // Clear any inline overrides - CSS controls backdrop visibility via .mobile-open 15125 mobileBackdrop.style.display = ''; 15126 mobileBackdrop.style.pointerEvents = ''; 15127 mobileBackdrop.style.opacity = ''; 15128 mobileBackdrop.style.visibility = ''; 15129 } 15130 15131 // Toggle mobile menu when hamburger is clicked 15132 hamburgerBtn.addEventListener('click', function(e) { 15133 e.stopPropagation(); 15134 toggleMobileMenu(); // Assumes this function exists 15135 }); 15136 15137 // Close mobile menu when backdrop is clicked 15138 if (mobileBackdrop) { 15139 mobileBackdrop.addEventListener('click', function() { 15140 closeMobileMenu(); // Assumes this function exists 15141 }); 15142 } 15143 15144 // Escape closes mobile menu (keyboard / SR) 15145 document.addEventListener('keydown', function(e) { 15146 if (e.key !== 'Escape') return; 15147 const sb = document.querySelector('.sidebar'); 15148 if (sb && sb.classList.contains('mobile-open') && typeof closeMobileMenu === 'function') { 15149 closeMobileMenu(); 15150 } 15151 }); 15152 15153 // Handle window resize events to ensure proper state 15154 window.addEventListener('resize', function() { 15155 // If we're not on mobile anymore, remove mobile-open class 15156 if (window.innerWidth > 768 && sidebar.classList.contains('mobile-open')) { 15157 closeMobileMenu(); // Assumes this function exists 15158 } 15159 }); 15160} 15161 15162document.addEventListener('keydown', function(e) { 15163 // Check for caret (Shift+6) 15164 if (e.key === '^' || (e.key === '6' && e.shiftKey)) { 15165 // Only work in edit mode 15166 if (window.isInEditMode && window.isInEditMode()) { 15167 console.log('Import shortcut detected'); 15168 toggleImportClassicForm(); 15169 e.preventDefault(); 15170 } 15171 } 15172}); 15173 15174// Enhanced tooltip management with automatic timeouts 15175 15176// Store active tooltip timeouts by ID 15177window.tooltipTimeouts = {}; 15178 15179function showTooltip(trigger) { 15180 const tooltipText = trigger.getAttribute('data-custom-tooltip'); 15181 if (!tooltipText) return; 15182 15183 // Get or create the main tooltip container 15184 let container = document.getElementById('tooltip-container'); 15185 if (!container) { 15186 console.log("Tooltip container not found, creating one."); 15187 container = document.createElement('div'); // Assign to 'container' 15188 container.id = 'tooltip-container'; 15189 // CSS for #tooltip-container should make it position:fixed, top:0, left:0, pointer-events:none, etc. 15190 document.body.appendChild(container); 15191 } 15192 15193 const tooltipId = `tooltip-${trigger.id || Math.random().toString(36).substr(2, 9)}`; 15194 15195 // Remove any existing tooltip for this trigger 15196 const existingTooltip = document.getElementById(tooltipId); 15197 if (existingTooltip) { 15198 existingTooltip.remove(); 15199 } 15200 15201 // Clear any existing timeout for this tooltip 15202 if (!window.tooltipTimeouts) { 15203 window.tooltipTimeouts = {}; // Initialize if it doesn't exist 15204 } 15205 15206 if (window.tooltipTimeouts[tooltipId]) { 15207 clearTimeout(window.tooltipTimeouts[tooltipId]); 15208 delete window.tooltipTimeouts[tooltipId]; 15209 } 15210 15211 // Create tooltip element 15212 const tooltip = document.createElement('div'); 15213 tooltip.className = 'tooltip'; // Ensure your CSS styles .tooltip 15214 tooltip.id = tooltipId; 15215 tooltip.textContent = tooltipText; 15216 15217 // Add to container (now 'container' is guaranteed to be an element) 15218 container.appendChild(tooltip); 15219 15220 // Position tooltip 15221 if (typeof positionTooltip === 'function') { 15222 positionTooltip(tooltip, trigger); 15223 } else { 15224 console.error("positionTooltip function is not defined."); 15225 tooltip.remove(); // Clean up if we can't position 15226 return; 15227 } 15228 15229 // Show with animation (slight delay for CSS transition) 15230 setTimeout(() => { 15231 tooltip.style.opacity = '1'; 15232 // The transform for animation should be handled by CSS transitions 15233 // e.g., .tooltip.visible { transform: translateX(-50%) translateY(-5px); } 15234 // Or ensure positionTooltip sets final transform if it needs to override CSS. 15235 // For now, assuming CSS handles the animated transform from its initial state. 15236 }, 10); 15237 15238 trigger.setAttribute('data-active-tooltip', tooltipId); 15239 15240 // Set automatic timeout to hide after 3 seconds 15241 window.tooltipTimeouts[tooltipId] = setTimeout(() => { 15242 hideTooltip(trigger); // Ensure hideTooltip function exists 15243 }, 3000); 15244 15245 // Also set up a global timer to ensure all tooltips are dismissed 15246 if (window.globalTooltipTimer) { 15247 clearTimeout(window.globalTooltipTimer); 15248 } 15249 window.globalTooltipTimer = setTimeout(() => { 15250 // Force hide all visible tooltips after 3 seconds 15251 const allTooltips = document.querySelectorAll('.tooltip'); 15252 allTooltips.forEach(tooltip => { 15253 if (tooltip.style.opacity !== '0') { 15254 tooltip.style.opacity = '0'; 15255 setTimeout(() => { 15256 if (tooltip.parentNode) { 15257 tooltip.parentNode.removeChild(tooltip); 15258 } 15259 }, 200); 15260 } 15261 }); 15262 // Clear all timeout references 15263 if (window.tooltipTimeouts) { 15264 for (const id in window.tooltipTimeouts) { 15265 clearTimeout(window.tooltipTimeouts[id]); 15266 } 15267 window.tooltipTimeouts = {}; 15268 } 15269 }, 3000); 15270} 15271 15272// Function to hide a tooltip 15273function hideTooltip(trigger) { 15274 const tooltipId = trigger.getAttribute('data-active-tooltip'); 15275 if (!tooltipId) return; 15276 15277 const tooltip = document.getElementById(tooltipId); 15278 if (tooltip) { 15279 // Fade out 15280 tooltip.style.opacity = '0'; 15281 tooltip.style.transform = 'translateX(-50%) translateY(0)'; 15282 15283 // Remove after animation 15284 setTimeout(() => { 15285 if (tooltip.parentNode) { 15286 tooltip.parentNode.removeChild(tooltip); 15287 } 15288 }, 200); 15289 } 15290 15291 // Clear any active timeout for this tooltip 15292 if (window.tooltipTimeouts && window.tooltipTimeouts[tooltipId]) { 15293 clearTimeout(window.tooltipTimeouts[tooltipId]); 15294 delete window.tooltipTimeouts[tooltipId]; 15295 } 15296 15297 // Clear reference 15298 trigger.removeAttribute('data-active-tooltip'); 15299 15300 // If this was the last tooltip, clear the global timer 15301 const remainingTooltips = document.querySelectorAll('.tooltip'); 15302 if (remainingTooltips.length === 0 && window.globalTooltipTimer) { 15303 clearTimeout(window.globalTooltipTimer); 15304 window.globalTooltipTimer = null; 15305 } 15306} 15307 15308// Function to initialize tooltips across the document 15309function initTooltipSystem() { 15310 // Check if container already exists 15311 let tooltipContainer = document.getElementById('tooltip-container'); 15312 15313 // Create it if it doesn't exist 15314 if (!tooltipContainer) { 15315 tooltipContainer = document.createElement('div'); 15316 tooltipContainer.id = 'tooltip-container'; 15317 document.body.appendChild(tooltipContainer); 15318 } 15319 15320 // Find all elements with data-custom-tooltip attribute 15321 const tooltipTriggers = document.querySelectorAll('[data-custom-tooltip]'); 15322 15323 // Add event listeners to each trigger
15324 tooltipTriggers.forEach(trigger => { 15325 // Skip if already initialized 15326 if (trigger.hasAttribute('data-custom-tooltip-initialized')) return; 15327 15328 // Mark as initialized 15329 trigger.setAttribute('data-custom-tooltip-initialized', 'true'); 15330 15331 // MouseEnter: Show tooltip 15332 trigger.addEventListener('mouseenter', () => { 15333 showTooltip(trigger); 15334 }); 15335 15336 // MouseLeave: Hide tooltip 15337 trigger.addEventListener('mouseleave', () => { 15338 hideTooltip(trigger); 15339 }); 15340 15341 // Click: Hide tooltip immediately 15342 trigger.addEventListener('click', () => { 15343 hideTooltip(trigger); 15344 }); 15345 15346 // Also handle focus/blur for accessibility 15347 trigger.addEventListener('focus', () => { 15348 showTooltip(trigger); 15349 }); 15350 15351 trigger.addEventListener('blur', () => { 15352 hideTooltip(trigger); 15353 }); 15354 }); 15355 15356 // Set up a failsafe function to clear any orphaned tooltips 15357 window.clearAllTooltips = function() { 15358 const container = document.getElementById('tooltip-container'); 15359 if (container) { 15360 container.innerHTML = ''; 15361 } 15362 15363 // Clear all timeouts 15364 if (window.tooltipTimeouts) { 15365 for (const id in window.tooltipTimeouts) { 15366 clearTimeout(window.tooltipTimeouts[id]); 15367 } 15368 window.tooltipTimeouts = {}; 15369 } 15370 15371 // Clear global timer 15372 if (window.globalTooltipTimer) { 15373 clearTimeout(window.globalTooltipTimer); 15374 window.globalTooltipTimer = null; 15375 } 15376 15377 // Clear all data-active-tooltip attributes 15378 document.querySelectorAll('[data-active-tooltip]').forEach(el => { 15379 el.removeAttribute('data-active-tooltip'); 15380 }); 15381 }; 15382 15383 // Initialize new tooltips when DOM changes 15384 setupMutationObserver(); 15385 15386 // Add a global function to manually dismiss all tooltips (for testing) 15387 window.dismissAllTooltips = function() { 15388 const allTooltips = document.querySelectorAll('.tooltip'); 15389 allTooltips.forEach(tooltip => { 15390 if (tooltip.style.opacity !== '0') { 15391 tooltip.style.opacity = '0'; 15392 setTimeout(() => { 15393 if (tooltip.parentNode) { 15394 tooltip.parentNode.removeChild(tooltip); 15395 } 15396 }, 200); 15397 } 15398 }); 15399 15400 // Clear all timeout references 15401 if (window.tooltipTimeouts) { 15402 for (const id in window.tooltipTimeouts) { 15403 clearTimeout(window.tooltipTimeouts[id]); 15404 } 15405 window.tooltipTimeouts = {}; 15406 } 15407 15408 // Clear global timer 15409 if (window.globalTooltipTimer) { 15410 clearTimeout(window.globalTooltipTimer); 15411 window.globalTooltipTimer = null; 15412 } 15413 15414 // Clear all data-active-tooltip attributes 15415 document.querySelectorAll('[data-active-tooltip]').forEach(el => { 15416 el.removeAttribute('data-active-tooltip'); 15417 }); 15418 }; 15419} 15420 15421function positionTooltip(tooltip, trigger) { 15422 const triggerRect = trigger.getBoundingClientRect(); // Viewport-relative coordinates 15423 15424 // To accurately get tooltip dimensions, temporarily make it visible but off-screen. 15425 // The #tooltip-container is position:fixed, so the tooltip's absolute positioning 15426 // will be relative to the viewport. 15427 tooltip.style.visibility = 'hidden'; 15428 tooltip.style.display = 'block'; // Or 'inline-block' if that's its intended final display 15429 // No need to change position to 'absolute' here if its CSS is already 'position: absolute;' 15430 // within the 'fixed' #tooltip-container. 15431 15432 const tooltipHeight = tooltip.offsetHeight; 15433 const tooltipWidth = tooltip.offsetWidth; 15434 15435 // Restore visibility; opacity transition will handle the fade-in. 15436 tooltip.style.visibility = ''; 15437 // tooltip.style.display = ''; // Let CSS manage display if opacity is used for showing/hiding 15438 15439 // Check if tooltip should be positioned to the right (for conflict indicator) 15440 const tooltipPosition = trigger.getAttribute('data-tooltip-position'); 15441 if (tooltipPosition === 'right') { 15442 // Position to the right of the trigger 15443 const viewportWidth = window.innerWidth; 15444 const viewportHeight = window.innerHeight; 15445 let left = triggerRect.right + 10; // 10px gap from the right edge of trigger 15446 let top = triggerRect.top + (triggerRect.height / 2) - (tooltipHeight / 2); // Vertically centered 15447 15448 // Recalculate tooltip dimensions after allowing text wrap 15449 tooltip.style.maxWidth = '200px'; 15450 tooltip.style.whiteSpace = 'normal'; 15451 tooltip.style.wordWrap = 'break-word'; 15452 const tooltipWidthAfterWrap = tooltip.offsetWidth; 15453 const tooltipHeightAfterWrap = tooltip.offsetHeight; 15454 15455 // If tooltip goes off-screen to the right, try positioning to the left of the button 15456 if (left + tooltipWidthAfterWrap > viewportWidth - 5) { 15457 left = triggerRect.left - tooltipWidthAfterWrap - 10; // Position to the left of button 15458 // If it still goes off the left edge, position it at the left edge with padding 15459 if (left < 5) { 15460 left = 5; // Keep it at least 5px from the left edge 15461 // The tooltip will wrap to fit within the available space 15462 tooltip.style.maxWidth = `${viewportWidth - left - 10}px`; 15463 } 15464 } else {
15465 // Tooltip fits on the right, but make sure it doesn't go off-screen 15466 if (left + tooltipWidthAfterWrap > viewportWidth - 5) { 15467 tooltip.style.maxWidth = `${viewportWidth - left - 10}px`; 15468 } 15469 } 15470 15471 // Recalculate after max-width adjustment 15472 const finalTooltipWidth = tooltip.offsetWidth; 15473 const finalTooltipHeight = tooltip.offsetHeight; 15474 15475 // Ensure tooltip doesn't go off-screen on the right side 15476 if (left + finalTooltipWidth > viewportWidth - 5) { 15477 left = viewportWidth - finalTooltipWidth - 5; 15478 } 15479 15480 // Adjust if tooltip goes off-screen vertically 15481 if (top < 5) { 15482 top = 5; 15483 } else if (top + finalTooltipHeight > viewportHeight - 5) { 15484 top = viewportHeight - finalTooltipHeight - 5; 15485 } 15486 15487 tooltip.style.left = `${left}px`; 15488 tooltip.style.top = `${top}px`; 15489 tooltip.classList.remove('tooltip-bottom'); // No arrow needed for side positioning 15490 return; 15491 } 15492 15493 // Calculate desired horizontal center point of the trigger 15494 const triggerCenter = triggerRect.left + triggerRect.width / 2; 15495 15496 // Calculate tooltip's left position to center it under/over trigger's center 15497 let left = triggerCenter - tooltipWidth / 2; 15498 15499 // Default position: above the trigger 15500 // Arrow height (8px) + desired gap (e.g., 2px) = 10px total offset 15501 let top = triggerRect.top - tooltipHeight - 10; 15502 tooltip.classList.remove('tooltip-bottom'); // Manages arrow direction via CSS 15503 15504 // Check if it goes off-screen at the top (add a small buffer, e.g., 5px) 15505 if (top < 5) { 15506 // Position below the trigger 15507 top = triggerRect.bottom + 10; // 10px gap (arrow height + small space) 15508 tooltip.classList.add('tooltip-bottom'); 15509 } 15510 15511 // Boundary checks for left position to prevent going off-screen horizontally 15512 const viewportWidth = window.innerWidth; 15513 if (left < 5) { // 5px buffer from left edge 15514 left = 5; 15515 } else if (left + tooltipWidth > viewportWidth - 5) { // 5px buffer from right edge 15516 left = viewportWidth - tooltipWidth - 5; 15517 } 15518 15519 tooltip.style.left = `${left}px`; 15520 tooltip.style.top = `${top}px`; 15521 15522 // The CSS for .tooltip might have `transform: translateX(-50%)` for initial centering. 15523 // Since JS now calculates the exact 'left' for centering, 15524 // we can clear any such transform that might interfere. 15525 // However, if your CSS animation relies on a transform, adjust accordingly. 15526 // If your CSS only uses opacity for transition, this is fine: 15527 tooltip.style.transform = ''; 15528} 15529 15530// Watch for new tooltip triggers added to the DOM 15531function setupMutationObserver() { 15532 // Check if MutationObserver is available 15533 if (!window.MutationObserver) return; 15534 15535 // Create observer instance 15536 const observer = new MutationObserver((mutations) => { 15537 let shouldInit = false; 15538 15539 // Check if relevant nodes were added 15540 mutations.forEach(mutation => { 15541 if (mutation.type === 'childList') { 15542 mutation.addedNodes.forEach(node => { 15543 // Check if it's an Element and has data-custom-tooltip or contains elements with data-custom-tooltip 15544 if (node.nodeType === 1) { // ELEMENT_NODE 15545 if (node.hasAttribute && node.hasAttribute('data-custom-tooltip')) { 15546 shouldInit = true; 15547 } else if (node.querySelectorAll) { 15548 const tooltips = node.querySelectorAll('[data-custom-tooltip]'); 15549 if (tooltips.length) shouldInit = true; 15550 } 15551 } 15552 }); 15553 } 15554 }); 15555 15556 // If relevant nodes found, reinitialize tooltips 15557 if (shouldInit) { 15558 initTooltipSystem(); 15559 } 15560 }); 15561 15562 // Start observing the entire document with the configured parameters 15563 observer.observe(document.body, { 15564 childList: true, 15565 subtree: true 15566 }); 15567} 15568 15569// Initialize when DOM is ready 15570document.addEventListener('DOMContentLoaded', initTooltipSystem); 15571 15572// Also initialize immediately if document already loaded 15573if (document.readyState === 'complete' || document.readyState === 'interactive') { 15574 initTooltipSystem(); 15575} 15576 15577// Add a global safety function that runs every minute to clear any tooltips that might have gotten stuck 15578setInterval(() => { 15579 // Get all visible tooltips 15580 const visibleTooltips = document.querySelectorAll('.tooltip[style*="opacity: 1"]'); 15581 15582 // If any are found, check how long they've been visible 15583 if (visibleTooltips.length > 0) { 15584 console.log('Found visible tooltips during cleanup check, clearing them'); 15585 window.clearAllTooltips(); 15586 } 15587}, 5000); // 15588 15589// Mobile drawer a11y: Tab cycle + restore focus. No visual/mouse behavior changes. 15590var _mobileMenuFocusTrapHandler = null; 15591 15592function getMobileMenuFocusables(sidebar) { 15593 if (!sidebar) return []; 15594 return Array.prototype.filter.call( 15595 sidebar.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not
15595([tabindex="-1"])'), 15596 function (el) { 15597 if (el.getAttribute('aria-hidden') === 'true') return false; 15598 var style = typeof getComputedStyle === 'function' ? getComputedStyle(el) : null; 15599 if (style && (style.display === 'none' || style.visibility === 'hidden')) return false; 15600 return el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement; 15601 } 15602 ); 15603} 15604 15605function releaseMobileMenuFocusTrap() { 15606 if (_mobileMenuFocusTrapHandler) { 15607 document.removeEventListener('keydown', _mobileMenuFocusTrapHandler, true); 15608 _mobileMenuFocusTrapHandler = null; 15609 } 15610} 15611 15612function installMobileMenuFocusTrap(sidebar) { 15613 releaseMobileMenuFocusTrap(); 15614 if (!sidebar) return; 15615 _mobileMenuFocusTrapHandler = function (event) { 15616 if (event.key !== 'Tab') return; 15617 var sb = document.querySelector('.sidebar'); 15618 if (!sb || !sb.classList.contains('mobile-open')) return; 15619 var focusables = getMobileMenuFocusables(sb); 15620 var hamburger = document.getElementById('hamburgerBtn'); 15621 // Include hamburger so Shift+Tab from first item returns to the control that opened the drawer 15622 var cycle = hamburger ? [hamburger].concat(focusables.filter(function (el) { return el !== hamburger; })) : focusables; 15623 if (!cycle.length) return; 15624 var first = cycle[0]; 15625 var last = cycle[cycle.length - 1]; 15626 if (event.shiftKey) { 15627 if (document.activeElement === first || (sb.contains(document.activeElement) === false && document.activeElement !== hamburger)) { 15628 event.preventDefault(); 15629 last.focus(); 15630 } 15631 } else if (document.activeElement === last) { 15632 event.preventDefault(); 15633 first.focus(); 15634 } 15635 }; 15636 document.addEventListener('keydown', _mobileMenuFocusTrapHandler, true); 15637} 15638 15639// Toggle mobile menu state 15640function toggleMobileMenu() { 15641const sidebar = document.querySelector('.sidebar'); 15642const hamburgerBtn = document.getElementById('hamburgerBtn'); 15643const mobileBackdrop = document.getElementById('mobileBackdrop'); 15644 15645// Check if we're in horizontal layout mode 15646const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal'); 15647 15648sidebar.classList.toggle('mobile-open'); 15649hamburgerBtn.classList.toggle('active'); 15650const isOpen = sidebar.classList.contains('mobile-open'); 15651hamburgerBtn.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 15652 15653if (mobileBackdrop) { 15654 if (isHorizontalLayout) { 15655 // For horizontal layout on mobile, use normal backdrop (tap outside to close) 15656 // Clear any inline overrides so CSS can control visibility 15657 mobileBackdrop.style.display = ''; 15658 mobileBackdrop.style.pointerEvents = ''; 15659 mobileBackdrop.style.opacity = ''; 15660 mobileBackdrop.style.visibility = ''; 15661 mobileBackdrop.classList.toggle('mobile-open', isOpen); 15662 setTimeout(() => { 15663 mobileBackdrop.classList.toggle('active', isOpen); 15664 }, 10); 15665 } else { 15666 // For other layouts, use normal backdrop behavior 15667 mobileBackdrop.classList.toggle('mobile-open'); 15668 // Slight delay to show opacity transition 15669 setTimeout(() => { 15670 mobileBackdrop.classList.toggle('active', isOpen); 15671 }, 10); 15672 } 15673} 15674 15675// Prevent body scrolling when menu is open 15676document.body.style.overflow = isOpen ? 'hidden' : ''; 15677 15678if (isOpen) { 15679 // Trap Tab only â do not steal focus on mouse/touch open (avoids focus rings / scroll jumps) 15680 installMobileMenuFocusTrap(sidebar); 15681} else { 15682 releaseMobileMenuFocusTrap(); 15683 // Restore focus only if keyboard focus was inside the drawer 15684 if (hamburgerBtn && sidebar && sidebar.contains(document.activeElement)) { 15685 try { hamburgerBtn.focus(); } catch (e) {} 15686 } 15687} 15688} 15689 15690// Close the mobile menu 15691function closeMobileMenu() { 15692const sidebar = document.querySelector('.sidebar'); 15693const hamburgerBtn = document.getElementById('hamburgerBtn'); 15694const mobileBackdrop = document.getElementById('mobileBackdrop'); 15695const wasOpen = sidebar && sidebar.classList.contains('mobile-open'); 15696 15697// Check if we're in horizontal layout mode 15698const isHorizontalLayout = document.body.classList.contains('menu-layout-horizontal'); 15699 15700sidebar.classList.remove('mobile-open'); 15701hamburgerBtn.classList.remove('active'); 15702if (hamburgerBtn) hamburgerBtn.setAttribute('aria-expanded', 'false'); 15703 15704if (mobileBackdrop) { 15705 if (isHorizontalLayout) { 15706 // For horizontal layout, clear inline overrides and remove mobile-open 15707 mobileBackdrop.style.display = ''; 15708 mobileBackdrop.style.pointerEvents = ''; 15709 mobileBackdrop.style.opacity = ''; 15710 mobileBackdrop.style.visibility = ''; 15711 mobileBackdrop.classList.remove('mobile-open'); 15712 mobileBackdrop.classList.remove('active'); 15713 } else { 15714 // For other layouts, use normal backdrop behavior 15715 mobileBackdrop.classList.remove('active'); 15716 setTimeout(() => { 15717 mobileBackdrop.classList.remove('mobile-open'); 15718 }, 300); // Match transition duration 15719 } 15720} 15721
15722// Re-enable body scrolling 15723document.body.style.overflow = ''; 15724releaseMobileMenuFocusTrap(); 15725// Return focus to hamburger only if focus was inside the drawer (keyboard/SR); mouse users unchanged 15726if (wasOpen && hamburgerBtn && sidebar && sidebar.contains(document.activeElement)) { 15727 try { hamburgerBtn.focus(); } catch (e) {} 15728} 15729} 15730 15731function updateMobileHeaderContent() { 15732 const currentLayout = typeof getEffectiveMenuLayout === 'function' 15733 ? getEffectiveMenuLayout() 15734 : (window.MenuStyleCustomizer && window.MenuStyleCustomizer.settings 15735 ? window.MenuStyleCustomizer.settings.menuLayout || 'sidebar' 15736 : (document.body.classList.contains('menu-layout-horizontal') ? 'horizontal' : 'sidebar')); 15737 15738 const isMobileView = window.innerWidth <= 768; // Assuming 768px is your mobile breakpoint 15739 15740 const mobileHeaderContent = document.querySelector('.mobile-header-content'); 15741 if (!mobileHeaderContent) { 15742 console.warn("updateMobileHeaderContent: .mobile-header-content element not found."); 15743 return; 15744 } 15745 15746 // On mobile, center the content unless Space Colony (left-aligned title) 15747 if (isMobileView) { 15748 const isSpaceColonyTheme = typeof isSpaceColonySiteTheme === 'function' && isSpaceColonySiteTheme(); 15749 mobileHeaderContent.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center'; 15750 // Also ensure logo-menu-container doesn't grow 15751 const logoMenuContainer = mobileHeaderContent.querySelector('.logo-menu-container'); 15752 if (logoMenuContainer) { 15753 logoMenuContainer.style.flexGrow = '0'; 15754 logoMenuContainer.style.justifyContent = isSpaceColonyTheme ? 'flex-start' : 'center'; 15755 } 15756 } 15757 15758 console.log(`updateMobileHeaderContent: Layout: ${currentLayout}, Mobile: ${isMobileView}`); 15759 15760 // Clear previous generic mobile content (like centered logo/title) 15761 const existingMobileImage = mobileHeaderContent.querySelector('.mobile-header-image'); 15762 const existingMobileText = mobileHeaderContent.querySelector('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title'); 15763 if (existingMobileImage) { 15764 // The img is always appended directly (never wrapped in an <a>). Remove it straight. 15765 // Also handle any legacy <a>-wrapped images left from older deployments. 15766 const imageParent = existingMobileImage.parentElement; 15767 if (imageParent && imageParent.tagName === 'A' && imageParent.parentElement === mobileHeaderContent) { 15768 imageParent.remove(); 15769 } else { 15770 existingMobileImage.remove(); 15771 } 15772 } 15773 // Sweep any stray empty <a> tags left by older deployments that used the wrapper approach 15774 Array.from(mobileHeaderContent.children).forEach(function(child) { 15775 if (child.tagName === 'A' && !child.querySelector('.mobile-header-image') && !(child.textContent || '').trim()) { 15776 child.remove(); 15777 } 15778 }); 15779 if (existingMobileText) existingMobileText.remove(); 15780 15781 if (currentLayout === 'horizontal' && !isMobileView) { 15782 // Desktop Horizontal Layout: 15783 // The logo is handled by renderHorizontalMenu placing it in .horizontal-header-logo. 15784 // The menu items are in .horizontal-menu-container. 15785 // Remove stray mobile-only title/image nodes (duplicate of horizontal-header-logo). 15786 mobileHeaderContent.querySelectorAll('.mobile-header-text, .mobile-header-menu-title, .mobile-header-title, .mobile-header-image').forEach(function(el) { 15787 el.remove(); 15788 }); 15789 console.log("updateMobileHeaderContent: Desktop horizontal layout, re-rendering horizontal menu to ensure sidebar elements are available."); 15790 if (typeof renderHorizontalMenu === 'function') { 15791 // Wait for sidebar elements to be available before rendering 15792 const waitForSidebarElements = (attempts = 0) => { 15793 const maxAttempts = 10; 15794 if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 15795 console.log("updateMobileHeaderContent: Sidebar elements available, rendering horizontal menu."); 15796 renderHorizontalMenu(); 15797 } else if (attempts < maxAttempts) { 15798 const attemptNum = attempts + 1; 15799 console.log("updateMobileHeaderContent: Waiting for sidebar elements (attempt " + attemptNum + "/" + maxAttempts + ")..."); 15800 setTimeout(() => waitForSidebarElements(attempts + 1), 100); 15801 } else { 15802 console.warn("updateMobileHeaderContent: Sidebar elements not available after waiting, rendering anyway."); 15803 renderHorizontalMenu(); 15804 } 15805 }; 15806 waitForSidebarElements(); 15807 } 15808 return; 15809 } 15810 15811 // For Horizontal layout on mobile, we need to clear the horizontal menu items 15812 // and show only the mobile header logo/title 15813 if (currentLayout === 'horizontal' && isMobileView) { 15814 console.log("updateMobileHeaderContent: Mobile horizontal layout - clearing horizontal menu items and showing mobile logo."); 15815 15816 // Clear horizontal menu items from mobile header 15817 const horizontalMenuContainer = mobileHeaderContent.querySelector('.horizontal-menu
15817-container'); 15818 if (horizontalMenuContainer) { 15819 console.log("updateMobileHeaderContent: Clearing horizontal menu container"); 15820 horizontalMenuContainer.innerHTML = ''; 15821 } else { 15822 console.log("updateMobileHeaderContent: No horizontal menu container found to clear"); 15823 } 15824 15825 // Clear horizontal header logo from mobile header 15826 const horizontalHeaderLogo = mobileHeaderContent.querySelector('.horizontal-header-logo'); 15827 if (horizontalHeaderLogo) { 15828 console.log("updateMobileHeaderContent: Clearing horizontal header logo"); 15829 horizontalHeaderLogo.innerHTML = ''; 15830 } else { 15831 console.log("updateMobileHeaderContent: No horizontal header logo found to clear"); 15832 } 15833 15834 // Also clear any existing horizontal menu items that might be directly in mobile-header-content 15835 const existingHorizontalMenuItems = mobileHeaderContent.querySelectorAll('.horizontal-menu-item'); 15836 if (existingHorizontalMenuItems.length > 0) { 15837 console.log('updateMobileHeaderContent: Removing ' + existingHorizontalMenuItems.length + ' existing horizontal menu items'); 15838 existingHorizontalMenuItems.forEach(item => item.remove()); 15839 } 15840 } 15841 15842 // For Sidebar, Top, OR Horizontal layout on mobile: 15843 // Populate .mobile-header-content with the first sidebar element (logo/title). 15844 // The CSS will hide .horizontal-menu-container and .horizontal-header-logo on mobile for horizontal layout. 15845 15846 console.log("updateMobileHeaderContent: Populating with first sidebar element."); 15847 15848 // For horizontal layout on mobile, use the header-specific function to get logo/title 15849 // For other layouts, use the general function 15850 let firstElement; 15851 if (currentLayout === 'horizontal' && isMobileView) { 15852 firstElement = getFirstSidebarElementForHeader(); // Gets logo/title specifically 15853 console.log("updateMobileHeaderContent: Using getFirstSidebarElementForHeader for mobile horizontal layout"); 15854 } else { 15855 firstElement = getFirstSidebarElement(); // Gets any first element 15856 console.log("updateMobileHeaderContent: Using getFirstSidebarElement for other layouts"); 15857 } 15858 15859 if (firstElement) { 15860 console.log('updateMobileHeaderContent: First element found:', JSON.stringify(firstElement)); 15861 15862 if (firstElement.type === 'image' && (firstElement.imageData || firstElement.imageUrl)) { 15863 console.log('updateMobileHeaderContent: Creating mobile header image'); 15864 const img = document.createElement('img'); 15865 img.src = firstElement.imageUrl || firstElement.imageData; 15866 img.alt = resolveBrandLogoAlt(firstElement); 15867 img.className = 'mobile-header-image sidebar-image'; // For specific styling if needed 15868 15869 let mobileDisplayNode = img; 15870 if (firstElement.colorizeWithSiteColors && 15871 window.SidebarManager && 15872 typeof window.SidebarManager._buildSidebarImageDisplayNode === 'function') { 15873 const buildData = Object.assign({}, firstElement); 15874 if (!buildData.imageWidth) { 15875 buildData.imageWidth = 'auto'; 15876 } 15877 mobileDisplayNode = window.SidebarManager._buildSidebarImageDisplayNode(img, buildData); 15878 } 15879 15880 // Opt-in per-image padding wrapper (no-op when unset) 15881 if (window.SidebarManager && 15882 typeof window.SidebarManager._wrapWithImagePadding === 'function') { 15883 mobileDisplayNode = window.SidebarManager._wrapWithImagePadding(mobileDisplayNode, firstElement); 15884 } 15885 15886 // Handle linking: always append the img directly so existing CSS centering is 15887 // not disrupted by an <a> wrapper (which the many !important positioning rules 15888 // were not written to handle). Navigation is wired via a click handler. 15889 if (firstElement.linkType && firstElement.linkType !== 'none') { 15890 let finalLinkHref = '#'; 15891 let isValidLink = false;
15892 const linkTarget = firstElement.linkTarget || '_self'; 15893 15894 if (firstElement.linkType === 'external' && firstElement.linkUrl) { 15895 finalLinkHref = firstElement.linkUrl; 15896 isValidLink = true; 15897 } else if (firstElement.linkType === 'internal' && firstElement.linkPageId) { 15898 const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []); 15899 const targetPage = allGalleries.find(g => g.id === parseInt(firstElement.linkPageId)); 15900 15901 if (targetPage) { 15902 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 15903 if (isPreviewMode) { 15904 const pathParts = window.location.pathname.split('/').filter(Boolean); 15905 const siteGuid = pathParts[0]; 15906 if (siteGuid && targetPage.slug) { 15907 finalLinkHref = '/' + siteGuid + '/' + targetPage.slug; 15908 isValidLink = true; 15909 } else if (targetPage.url && targetPage.url.startsWith('/')) { 15910 finalLinkHref = targetPage.url; 15911 isValidLink = true; 15912 } 15913 } else { 15914 if (targetPage.slug) { 15915 finalLinkHref = '/' + targetPage.slug; 15916 isValidLink = true; 15917 } else if (targetPage.url && targetPage.url.startsWith('/')) { 15918 finalLinkHref = targetPage.url; 15919 isValidLink = true; 15920 } 15921 } 15922 } 15923 } 15924 15925 if (isValidLink) { 15926 mobileDisplayNode.style.cursor = 'pointer'; 15927 const capturedHref = finalLinkHref; 15928 const capturedTarget = linkTarget; 15929 const capturedLinkType = firstElement.linkType; 15930 const capturedPageId = firstElement.linkPageId; 15931 15932 mobileDisplayNode.addEventListener('click', (e) => { 15933 e.preventDefault(); 15934 if (capturedLinkType === 'internal' && capturedPageId) { 15935 const allGalleries = window.galleries || (typeof galleries !== 'undefined' ? galleries : []); 15936 const targetPage = allGalleries.find(g => g.id === parseInt(capturedPageId)); 15937 if (targetPage) { 15938 if (isPageMenuItem(targetPage) && typeof window.loadPage === 'function') { 15939 window.loadPage(targetPage.id); 15940 } else if (typeof window.loadGallery === 'function') { 15941 window.loadGallery(targetPage.id); 15942 } else { 15943 window.location.href = capturedHref; 15944 } 15945 } else { 15946 window.location.href = capturedHref; 15947 } 15948 } else { 15949 if (capturedTarget === '_blank') { 15950 window.open(capturedHref, '_blank'); 15951 } else { 15952 window.location.href = capturedHref; 15953 } 15954 } 15955 }); 15956 } 15957 } 15958 mobileHeaderContent.appendChild(mobileDisplayNode); 15959 } else if (firstElement.type === 'text' && firstElement.textContent) { 15960 console.log('updateMobileHeaderContent: Creating mobile header text'); 15961 const textDiv = document.createElement('div'); 15962 textDiv.className = 'mobile-header-text'; // For specific styling 15963 textDiv.innerHTML = firstElement.textContent; // Use innerHTML if textContent can contain HTML 15964 const titleEl = textDiv.querySelector('.site-title-text, h1, h2, p:last-of-type, p:only-of-type'); 15965 if (titleEl && !titleEl.classList.contains('site-title-text')) { 15966 titleEl.classList.add('site-title-text'); 15967 } 15968 mobileHeaderContent.appendChild(textDiv); 15969 } else if (firstElement.type === 'menu' && galleries && galleries.length > 0) { 15970 console.log('updateMobileHeaderContent: Creating mobile header menu title'); 15971 // Fallback for 'menu' type, find the first visible gallery title 15972 const firstVisibleGallery = galleries.find(g => g.visible !== false && !g.isSpacer); 15973 const titleText = firstVisibleGallery ? firstVisibleGallery.title : (firstElement.title || 'Menu'); 15974 const titleDiv = document.createElement('div'); 15975 titleDiv.className = 'mobile-header-menu-title'; // For specific styling 15976 titleDiv.textContent = titleText; 15977 mobileHeaderContent.appendChild(titleDiv); 15978 } else { // Default fallback 15979 console.log('updateMobileHeaderContent: Creating mobile header title (fallback)'); 15980 const titleDiv = document.createElement('div'); 15981 titleDiv.className = 'mobile-header-title'; // For specific styling 15982 titleDiv.textContent = firstElement.title || (galleries && galleries.length > 0 && galleries[0].title) || 'Menu'; 15983 mobileHeaderContent.appendChild(titleDiv); 15984 } 15985 } else { 15986 // Fallback if no sidebar elements exist 15987 const defaultTitle = document.createElement('div'); 15988 defaultTitle.className = 'mobile-header-title'; 15989 defaultTitle.textContent = 'Menu'; 15990 mobileHeaderContent.appendChild(defaultTitle); 15991 console.log("updateMobileHeaderContent: No first sidebar element found, showing default title."); 15992 } 15993 15994 if (typeof syncContentMarginToHorizontalHeader === 'function') { 15995 syncContentMarginToHorizontalHeader(); 15996 } 15997} 15998 15999// Helper function to get the first element in the sidebar 16000function getFirstSidebarElement() { 16001// Check if window.SidebarManager exists and has elements 16002if (window.SidebarManager && window.SidebarManager.elements && window.SidebarManager.elements.length > 0) { 16003// Filter to only visible elements 16004const visibleElements = window.SidebarManager.elements.filter(el => el.visible !== false && el.type !== 'spacer'); 16005 16006if (visibleElements.length > 0) { 16007 // Sort by position and return the first one 16008 return visibleElements.sort((a, b) => a.position - b.position)[0]; 16009} 16010} 16011 16012// If no sidebar elements, return first gallery as fallback 16013if (galleries.length > 0) { 16014const firstVisibleGallery = galleries.find(g => g.visible !== false); 16015if (firstVisibleGallery) { 16016 return { 16017 type: 'menu', 16018 title: firstVisibleGallery.title 16019 }; 16020} 16021} 16022 16023// Default fallback 16024return { 16025type: 'text', 16026title: 'Menu' 16027}; 16028} 16029// KEEP YOUR ORIGINAL initMobileMenu, toggleMobileMenu, closeMobileMenu FUNCTIONS EXACTLY AS THEY ARE 16030// Just replace the updateMobileTitle function and add our initialization code 16031 16032// Replace the existing updateMobileTitle function with this one 16033function updateMobileTitle() { 16034// Always call our implementation that shows the top sidebar element 16035updateMobileHeaderContent(); 16036} 16037 16038// Add window resize listener to handle mobile header content updates 16039window.addEventListener('resize', () => { 16040 // Debounce the resize event to avoid excessive calls 16041 clearTimeout(window.resizeTimeout); 16042 window.resizeTimeout = setTimeout(() => { 16043 if (typeof updateMobileHeaderContent === 'function') { 16044 updateMobileHeaderContent(); 16045 } 16046 if (typeof applyHorizontalLogoMenuLayoutStyles === 'function') { 16047 applyHorizontalLogoMenuLayoutStyles(); 16048 } 16049 if (typeof syncContentMarginToHorizontalHeader === 'function') { 16050 syncContentMarginToHorizontalHeader(); 16051 } 16052 }, 250); 16053}); 16054 16055//separate initialization function that doesn't replace the original initMobileMenu 16056function initTopElementDisplay() { 16057console.log("Initializing top element display"); 16058 16059// Run immediately 16060updateMobileHeaderContent(); 16061 16062if (typeof syncContentMarginToHorizontalHeader === 'function') { 16063 setTimeout(syncContentMarginToHorizontalHeader, 150); 16064} 16065 16066// Also hook into loadGallery to ensure it runs whenever galleries change 16067const originalLoadGallery = window.loadGallery; 16068if (typeof originalLoadGallery === 'function') { 16069window.loadGallery = function(id, event, options) { 16070 // Forward all args (including post-save { forceReload: true }) to the underlying loader 16071 const result = originalLoadGallery(id, event, options); 16072 updateMobileHeaderContent(); 16073 return result; 16074}; 16075} 16076} 16077 16078 16079// Initialize our functionality after a short delay to ensure SidebarManager is ready 16080document.addEventListener('DOMContentLoaded', function() { 16081// Short delay to ensure SidebarManager is initialized 16082setTimeout(initTopElementDisplay, 100); 16083 16084// Also run a bit later to catch any late-loading elements 16085setTimeout(updateMobileHeaderContent, 500); 16086}); 16087 16088// Run immediately if document is already loaded 16089if (document.readyState === 'complete' || document.readyState === 'interactive') { 16090setTimeout(initTopElementDisplay, 100); 16091} 16092 16093// Add a final fallback to ensure gallery visibility on window load 16094window.addEventListener('load', function() { 16095 setTimeout(ensureGalleryVisibility, 2000); 16096}); 16097 16098 16099(function() { 16100 /** 16101 * EnhancedGallerySystem - Comprehensive gallery script and state management 16102 */ 16103 class EnhancedGallerySystem { 16104 constructor() { 16105 // Core state tracking 16106 this.initialized = false;
16107 this.initPromise = null; 16108 this.isLoading = false; 16109 this.currentGalleryId = null; 16110 this.version = '260923-002'; // Increment when gallery scripts change (matches current versioned filenames) 16111 16112 // Track loaded resources 16113 this.loadedScripts = new Set(); 16114 this.loadedStyles = new Set(); 16115 16116 // Resources to ensure are loaded - Use Worker route (avoids CDN SSL issues) 16117 const galleryScriptsBase = window.location.origin + '/gallery-scripts/'; 16118 this.essentialResources = { 16119 scripts: [ 16120 { id: 'neon-gallery-main', src: galleryScriptsBase + 'neon-gallery-main-v260923-002.js' }, 16121 { id: 'neon-gallery-hp', src: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.js' } 16122 ], 16123 styles: [ 16124 { id: 'neon-gallery-css', href: galleryScriptsBase + 'neon-gallery-hp-hydra-v260923-002.css' } 16125 ] 16126 }; 16127 16128 // Keep track of original initialization functions 16129 this.originalInitFunctions = {}; 16130 16131 // Keep reference to gallery state between changes 16132 this.previousGalleryState = null; 16133 16134 // Track last initialization time per gallery ID to prevent duplicates 16135 this.lastInitializationTime = new Map(); 16136 this._loadInProgress = null; 16137 this._loadingGalleryId = null; 16138 } 16139 16140 /** 16141 * Setup the correct gallery container structure 16142 * @param {HTMLElement} container - The main container element 16143 * @returns {boolean} - Success indicator 16144 */ 16145setupCorrectGalleryContainer(container) { 16146 console.log('EnhancedGallerySystem: Setting up correct gallery container structure'); 16147 16148 // Validate container 16149 if (!container) { 16150 console.error('EnhancedGallerySystem: Main gallery container is null'); 16151 return false; 16152 } 16153 16154 try { 16155 // Clear the container 16156 container.innerHTML = ''; 16157 16158 // Create the exact container structure expected by the gallery script 16159 const neonContainer = document.createElement('div'); 16160 neonContainer.id = 'neon-gallery-container'; 16161 neonContainer.style.width = '100%'; 16162 neonContainer.style.height = '100%'; 16163 neonContainer.style.position = 'relative'; 16164 16165 // Add the container to the DOM 16166 container.appendChild(neonContainer); 16167 16168 // Set visibility 16169 container.style.opacity = '1'; 16170 container.style.display = 'block'; 16171 16172 console.log('EnhancedGallerySystem: Gallery container structure created'); 16173 return true; 16174 } catch (error) { 16175 console.error('EnhancedGallerySystem: Error setting up gallery container:', error); 16176 return false; 16177 } 16178} 16179 16180 /** 16181 * Ensure #neon-gallery-container exists before gallery scripts execute. 16182 * neon-gallery-main bails early without it; hp may leave neonGalleryUtils as {}. 16183 */ 16184 ensureGalleryContainerPlaceholder(container) { 16185 if (document.getElementById('neon-gallery-container')) { 16186 return document.getElementById('neon-gallery-container'); 16187 } 16188 const host = container || document.querySelector('.gallery-container') || document.getElementById('galleryFrame')?.parentElement; 16189 if (!host) { 16190 return null; 16191 } 16192 const neonContainer = document.createElement('div'); 16193 neonContainer.id = 'neon-gallery-container'; 16194 neonContainer.className = 'gallery-direct-content'; 16195 neonContainer.style.width = '100%'; 16196 neonContainer.style.height = '100%'; 16197 neonContainer.style.position = 'relative'; 16198 host.appendChild(neonContainer); 16199 console.log('EnhancedGallerySystem: Created placeholder neon-gallery-container before script load'); 16200 return neonContainer; 16201 } 16202 16203 /** 16204 * Wait for neon-gallery-main to expose loadGallerySettings; reload main if needed. 16205 */ 16206 async ensureNeonGalleryUtilsReady(container) { 16207 const hasLoadGallerySettings = () => 16208 typeof window.neonGalleryUtils?.loadGallerySettings === 'function'; 16209 16210 if (hasLoadGallerySettings()) { 16211 return true; 16212 } 16213 16214 const mainScript = this.essentialResources.scripts.find(s => s.id === 'neon-gallery-main'); 16215 if (!mainScript) { 16216 return false; 16217 } 16218 16219 console.warn('EnhancedGallerySystem: neonGalleryUtils.loadGallerySettings missing â reloading main script'); 16220 this.ensureGalleryContainerPlaceholder(container); 16221 16222 document.querySelectorAll('script[src*="neon-gallery-main"]').forEach(el => { 16223 if (el.parentNode) el.parentNode.removeChild(el); 16224 }); 16225 this.loadedScripts.delete('neon-gallery-main');
16226 try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; } 16227 16228 await new Promise((resolve, reject) => { 16229 const scriptElement = document.createElement('script'); 16230 scriptElement.id = 'neon-gallery-main'; 16231 scriptElement.src = `${mainScript.src}?v=${this.version}&reload=${Date.now()}`; 16232 scriptElement.async = false; 16233 scriptElement.onload = () => { 16234 this.loadedScripts.add('neon-gallery-main'); 16235 resolve(); 16236 }; 16237 scriptElement.onerror = (err) => reject(err); 16238 document.body.appendChild(scriptElement); 16239 }); 16240 16241 for (let i = 0; i < 50; i++) { 16242 if (hasLoadGallerySettings()) { 16243 return true; 16244 } 16245 await new Promise(r => setTimeout(r, 100)); 16246 } 16247 16248 return hasLoadGallerySettings(); 16249 } 16250 16251 /** 16252 * Preload styles only â defer script loading until a gallery load provides a container. 16253 */ 16254 preloadStylesOnly() { 16255 return this.loadStyles(); 16256 } 16257 16258 /** 16259 * Reset initialization tracking when gallery scripts are removed from the DOM 16260 * (e.g. loadPage cleanup). Without this, a pending initPromise never resolves and 16261 * subsequent loadGallery calls hang with isLoading:true, initialized:false. 16262 */ 16263 resetInitializationState() { 16264 console.log('EnhancedGallerySystem: Resetting initialization state'); 16265 this.initPromise = null; 16266 this.initialized = false; 16267 this.isLoading = false; 16268 this.loadedScripts.clear(); 16269 this.loadedStyles.clear(); 16270 // Allow neon-gallery-main bootstrap wipe once again after scripts are re-injected 16271 try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; } 16272 } 16273 16274 /** 16275 * Initialize the gallery system 16276 * @returns {Promise} Resolves when all core dependencies are loaded 16277 */ 16278 initialize() { 16279 // Return existing promise if already initializing or complete 16280 if (this.initPromise) { 16281 return this.initPromise; 16282 } 16283 16284 console.log('EnhancedGallerySystem: Initializing gallery framework'); 16285 this.isLoading = true; 16286 16287 // Create a promise to track loading completion 16288 this.initPromise = (async () => { 16289 try { 16290 // CRITICAL: Initialize neonGalleryState before anything else 16291 // This prevents "Cannot set properties of undefined" errors 16292 if (!window.neonGalleryState) { 16293 window.neonGalleryState = { 16294 initialized: false, 16295 initializing: false, 16296 domInitialized: false, 16297 configApplied: false, 16298 initializationPromise: null 16299 }; 16300 console.log('EnhancedGallerySystem: Initialized neonGalleryState'); 16301 } 16302 16303 // 1. First ensure all essential styles are loaded 16304 await this.loadStyles(); 16305 16306 // 2. Preload all scripts to hint to the browser 16307 this.preloadScripts(); 16308 16309 // 3. Ensure gallery container exists before scripts that require it 16310 this.ensureGalleryContainerPlaceholder( 16311 document.querySelector('.gallery-container') 16312 ); 16313 16314 // 4. Load all essential scripts 16315 await this.loadScripts(); 16316 16317 // 5. Initialize the gallery events 16318 this.setupEvents(); 16319 16320 // Mark as fully initialized 16321 this.initialized = true; 16322 this.isLoading = false; 16323 16324 console.log('EnhancedGallerySystem: Successfully initialized'); 16325 return true; 16326 } catch (error) { 16327 console.error('EnhancedGallerySystem: Initialization failed:', error); 16328 this.isLoading = false; 16329 throw error; 16330 } 16331 })(); 16332 16333 return this.initPromise; 16334 } 16335 16336 /** 16337 * Load all necessary stylesheet resources 16338 */ 16339 async loadStyles() { 16340 console.log('EnhancedGallerySystem: Loading essential stylesheets'); 16341 16342 const stylePromises = this.essentialResources.styles.map(style => { 16343 // Skip if already loaded 16344 if (this.loadedStyles.has(style.id) || document.getElementById(style.id)) { 16345 console.log(`Style already loaded: ${style.id}`); 16346 this.loadedStyles.add(style.id); 16347 return Promise.resolve(); 16348 } 16349 16350 return new Promise((resolve, reject) => { 16351 const link = document.createElement('link'); 16352 link.id = style.id; 16353 link.rel = 'stylesheet'; 16354 // Use proxy URL if flag is set, otherwise use CDN URL 16355 let styleUrl = `${style.href}?v=${this.version}`; 16356 if (window.useCdnProxy && window.convertCdnToProxyUrl && style.href.includes('cdn.neonsky.app')) { 16357 styleUrl = window.convertCdnToProxyUrl(style.href) + '?v=' + this.version; 16358 console.log('ð Using proxy URL for style:', style.id, styleUrl); 16359 } 16360 16361 link.href = styleUrl; 16362 16363 link.onload = () => { 16364 console.log(`â Style loaded: ${style.id}`); 16365 this.loadedStyles.add(style.id); 16366 resolve(); 16367 }; 16368 16369 link.onerror = (err) => { 16370 console.error(`â Failed to load style: ${style.id}`, err); 16371 if (style.href.includes('cdn.neonsky.app') && !styleUrl.includes('/cdn-proxy/')) { 16372 console.error('ð Retrying via proxy:', style.href); 16373 const proxyUrl = window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(style.href) : style.href.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 16374 link.href = proxyUrl + '?v=' + this.version; 16375 link.onerror = function() { 16376 console.error('â Proxy failed, trying storage:', proxyUrl); 16377 link.href = link.href.replace('cdn.neonsky.app', 'storage.neonsky.app'); 16378 // Don't reject - let it try storage 16379 }; 16380 } else { 16381 reject(new Error(`Failed to load ${style.id}`)); 16382 } 16383 }; 16384 16385 document.head.appendChild(link); 16386 }); 16387 }); 16388 16389 // Wait for all styles to load 16390 await Promise.all(stylePromises); 16391 console.log('EnhancedGallerySystem: All stylesheets loaded'); 16392 } 16393 16394 /** 16395 * Preload scripts to improve browser loading priority 16396 */ 16397 preloadScripts() {
16398 this.essentialResources.scripts.forEach(script => { 16399 // Enhanced check for already preloaded scripts 16400 const scriptName = script.src.split('/').pop(); 16401 const alreadyPreloaded = document.querySelector(`link[rel="preload"][href*="${scriptName}"]`) || 16402 document.querySelector(`script[src*="${scriptName}"]`); 16403 16404 if (alreadyPreloaded) { 16405 console.log(`Script already preloaded or loaded: ${script.id}`); 16406 return; 16407 } 16408 16409 const preload = document.createElement('link'); 16410 preload.rel = 'preload'; 16411 preload.href = `${script.src}?v=${this.version}`; 16412 preload.as = 'script'; 16413 document.head.appendChild(preload); 16414 16415 console.log(`Script preloaded: ${script.id}`); 16416 }); 16417 } 16418 16419 /** 16420 * Load all necessary scripts 16421 */ 16422 async loadScripts() { 16423 console.log('EnhancedGallerySystem: Loading essential scripts'); 16424 16425 // Load sequentially (main before hp) so neon-gallery-main assigns window.neonGalleryUtils 16426 // before hp's initNeonGallery can run; parallel async scripts could execute hp before main finishes. 16427 const loadOne = (script) => { 16428 const alreadyLoaded = this.loadedScripts.has(script.id) || 16429 document.getElementById(script.id) || 16430 document.querySelector(`script[src*="${script.src.split('/').pop()}"]`); 16431 16432 if (alreadyLoaded) { 16433 const mainNeedsReload = 16434 script.id === 'neon-gallery-main' && 16435 typeof window.neonGalleryUtils?.loadGallerySettings !== 'function'; 16436 if (mainNeedsReload) { 16437 console.warn(`EnhancedGallerySystem: ${script.id} in DOM but neonGalleryUtils incomplete â reloading`); 16438 document.querySelectorAll('script[src*="neon-gallery-main"]').forEach(el => { 16439 if (el.parentNode) el.parentNode.removeChild(el); 16440 }); 16441 this.loadedScripts.delete(script.id); 16442 try { delete window.__neonGalleryMainBootstrapWipeDone; } catch (e) { window.__neonGalleryMainBootstrapWipeDone = false; } 16443 } else { 16444 console.log(`Script already loaded: ${script.id}`); 16445 this.loadedScripts.add(script.id); 16446 return Promise.resolve(); 16447 } 16448 } 16449 16450 return new Promise((resolve, reject) => { 16451 const scriptElement = document.createElement('script'); 16452 scriptElement.id = script.id; 16453 16454 let scriptUrl = `${script.src}?v=${this.version}`; 16455 if (window.useCdnProxy && window.convertCdnToProxyUrl && script.src.includes('cdn.neonsky.app')) { 16456 scriptUrl = window.convertCdnToProxyUrl(script.src) + '?v=' + this.version; 16457 console.log('ð Using proxy URL for script:', script.id, scriptUrl); 16458 } 16459 16460 scriptElement.src = scriptUrl; 16461 scriptElement.async = false; 16462 16463 scriptElement.onload = () => { 16464 console.log(`â Script loaded: ${script.id}`); 16465 this.loadedScripts.add(script.id); 16466 resolve(); 16467 }; 16468 16469 scriptElement.onerror = (err) => { 16470 console.error(`â Failed to load script: ${script.id}`, err); 16471 if (script.src.includes('cdn.neonsky.app') && !scriptUrl.includes('/cdn-proxy/')) { 16472 console.error('ð Retrying script via proxy:', script.id); 16473 const proxyUrl = window.convertCdnToProxyUrl ? window.convertCdnToProxyUrl(script.src) : script.src.replace('https://cdn.neonsky.app/', window.location.origin + '/cdn-proxy/'); 16474 scriptElement.src = proxyUrl + '?v=' + this.version; 16475 scriptElement.onerror = function() { 16476 console.error('â Proxy failed, trying storage:', proxyUrl); 16477 scriptElement.src = scriptElement.src.replace('cdn.neonsky.app', 'storage.neonsky.app'); 16478 }; 16479 } else { 16480 reject(new Error(`Failed to load ${script.id}`)); 16481 } 16482 }; 16483 16484 document.body.appendChild(scriptElement); 16485 }); 16486 }; 16487 16488 for (const script of this.essentialResources.scripts) { 16489 await loadOne(script); 16490 } 16491 console.log('EnhancedGallerySystem: All scripts loaded'); 16492 16493 // Save references to original initialization functions after scripts are loaded 16494 if (window.initNeonGallery && typeof window.initNeonGallery === 'function') { 16495 this.originalInitFunctions.initNeonGallery = window.initNeonGallery; 16496 } 16497 16498 if (window.reinitializeGalleryAfterSave && typeof window.reinitializeGalleryAfterSave === 'function') { 16499 this.originalInitFunctions.reinitializeGalleryAfterSave = window.reinitializeGalleryAfterSave; 16500 } 16501 } 16502 16503 /** 16504 * Set up enhanced event listeners 16505 */ 16506 setupEvents() { 16507 // Listen for gallery save events to capture latest state 16508 document.addEventListener('gallery-saved', (event) => { 16509 if (event.detail && event.detail.settings) { 16510 this.captureGalleryState(event.detail.settings); 16511 } 16512 }); 16513 16514 // Override existing functions to capture state changes 16515 this.enhanceExistingFunctions(); 16516 } 16517 16518 /** 16519 * Enhance existing gallery functions to work with our system 16520 */ 16521 enhanceExistingFunctions() { 16522 // Save original functions 16523 const originalInit = window.initNeonGallery; 16524 const originalReinit = window.reinitializeGalleryAfterSave; 16525 16526 // Only enhance if they exist 16527 if (typeof originalInit === 'function') { 16528 // Track if initNeonGallery is currently executing to prevent concurrent calls 16529 let isExecuting = false;
16530 let lastExecutionTime = 0; 16531 16532 window.initNeonGallery = async (...args) => { 16533 // Prevent duplicate initializations within 1 second 16534 const now = Date.now(); 16535 16536 // Check if already executing or called recently 16537 if (isExecuting) { 16538 console.log('EnhancedGallerySystem: initNeonGallery already executing, skipping duplicate call'); 16539 return Promise.resolve(); 16540 } 16541 16542 if (lastExecutionTime && (now - lastExecutionTime) < 1000) { 16543 console.log('EnhancedGallerySystem: Skipping duplicate initNeonGallery call (last call ' + (now - lastExecutionTime) + 'ms ago)'); 16544 return Promise.resolve(); 16545 } 16546 16547 // Set flag and timestamp immediately to prevent concurrent calls 16548 isExecuting = true; 16549 lastExecutionTime = now; 16550 this.lastInitializationTime.set('initNeonGallery', now); 16551 16552 try { 16553 // Call original with all arguments 16554 const result = await originalInit.apply(this, args); 16555 16556 // Capture state after initialization 16557 if (window.neonGalleryConfig) { 16558 this.captureGalleryState(window.neonGalleryConfig); 16559 } 16560 16561 return result; 16562 } finally { 16563 // Clear execution flag after a short delay to allow for async operations 16564 setTimeout(() => { 16565 isExecuting = false; 16566 }, 100); 16567 } 16568 }; 16569 } 16570 16571 if (typeof originalReinit === 'function') { 16572 window.reinitializeGalleryAfterSave = async (savedSettings) => { 16573 // Call original function 16574 const result = await originalReinit(savedSettings); 16575 16576 // Capture updated state 16577 this.captureGalleryState(savedSettings); 16578 16579 return result; 16580 }; 16581 } 16582 } 16583 16584 /** 16585 * Capture current gallery state for potential reuse 16586 */ 16587 captureGalleryState(settings) { 16588 if (!settings) return; 16589 16590 // Store a deep copy of the settings to avoid reference issues 16591 try { 16592 this.previousGalleryState = JSON.parse(JSON.stringify(settings)); 16593 console.log('EnhancedGallerySystem: Captured gallery state for possible reuse'); 16594 } catch (e) { 16595 console.error('EnhancedGallerySystem: Failed to capture gallery state:', e); 16596 } 16597 } 16598 16599 /** 16600 * Load a specific gallery using the enhanced system 16601 * @param {Object} gallery The gallery configuration 16602 * @param {HTMLElement} container The container to load the gallery into 16603 */ 16604 async loadGallery(gallery, container, options) { 16605 if (!gallery) { 16606 console.error('EnhancedGallerySystem: Invalid gallery configuration'); 16607 return false; 16608 } 16609 16610 const forceGalleryReload = options?.forceReload === true || window._forceGalleryReload === true; 16611 16612 // Coalesce concurrent loads for the same gallery â await instead of wiping mid-flight 16613 if (this._loadInProgress && this._loadingGalleryId === gallery.id && !forceGalleryReload) { 16614 const pageViewStillShowing = window._activeContentType === 'page' || 16615 !!document.querySelector('.gallery-container .page-container, .page-container'); 16616 if (!pageViewStillShowing) { 16617 console.log('EnhancedGallerySystem: Awaiting in-flight load for gallery ' + gallery.id); 16618 return this._loadInProgress; 16619 } 16620 console.log('EnhancedGallerySystem: Abandoning standalone in-flight load for page embed', gallery.id); 16621 this._loadInProgress = null; 16622 this._loadingGalleryId = null; 16623 } 16624 16625 const now = Date.now(); 16626 const lastInitTime = this.lastInitializationTime.get(gallery.id); 16627 if (!forceGalleryReload && lastInitTime && (now - lastInitTime) < 1000) { 16628 const hasContent = !!document.querySelector('.gallery-wrapper .masonry-item, .gallery-wrapper .horizontal-scroller, .gallery-wrapper .masonry'); 16629 const pageViewStillShowing = window._activeContentType === 'page' || 16630 !!document.querySelector('.gallery-container .page-container, .page-container'); 16631 if (hasContent && !pageViewStillShowing) { 16632 console.log('EnhancedGallerySystem: Gallery ' + gallery.id + ' recently loaded with content, skipping re-init'); 16633 if (container) container.style.opacity = '1'; 16634 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 16635 window.syncGalleryOptionsButtonVisibility(); 16636 } 16637 return true; 16638 } 16639 } 16640 16641 if (forceGalleryReload) { 16642 console.log('EnhancedGallerySystem: force reload requested for gallery ' + gallery.id); 16643 this.lastInitializationTime.delete(gallery.id); 16644 this._loadInProgress = null; 16645 this._loadingGalleryId = null; 16646 } 16647 16648 const loadTask = this._executeGalleryLoad(gallery, container, now, options); 16649 this._loadInProgress = loadTask; 16650 this._loadingGalleryId = gallery.id;
16651 try { 16652 return await loadTask; 16653 } finally { 16654 if (this._loadInProgress === loadTask) { 16655 this._loadInProgress = null; 16656 this._loadingGalleryId = null; 16657 } 16658 } 16659 } 16660 16661 async _executeGalleryLoad(gallery, container, startedAt, options) { 16662 this.lastInitializationTime.set(gallery.id, startedAt); 16663 const contentLoadGeneration = options && options.contentLoadGeneration != null 16664 ? options.contentLoadGeneration 16665 : window.__hydraContentLoadGeneration; 16666 const stillCurrent = () => typeof isHydraContentLoadCurrent === 'function' 16667 ? isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id) 16668 : contentLoadGeneration === window.__hydraContentLoadGeneration && 16669 (!window._activeContentType || window._activeContentType === 'gallery') && 16670 (window.activeGalleryId == null || String(window.activeGalleryId) === String(gallery.id)); 16671 16672 stopAutoAdvanceTimer(); 16673 16674 // CRITICAL: Verify container exists 16675 if (!container) { 16676 console.error('EnhancedGallerySystem: Gallery container is null or undefined'); 16677 return false; 16678 } 16679 16680 // Create a unique instance ID for this gallery load (needed before script init) 16681 const galleryInstanceId = Date.now(); 16682 16683 try { 16684 // Clean up previous gallery state and prepare container BEFORE loading scripts 16685 this.cleanupGalleryState(container); 16686 const containerSetup = this.setupCorrectGalleryContainer(container); 16687 if (!containerSetup) { 16688 throw new Error('Failed to set up gallery container'); 16689 } 16690 this.setupGalleryParameters(gallery, galleryInstanceId); 16691 } catch (prepError) { 16692 console.error('EnhancedGallerySystem: Failed to prepare gallery container:', prepError); 16693 return false; 16694 } 16695 16696 // Make sure the system is initialized (scripts load with container already in DOM) 16697 try { 16698 await this.initialize(); 16699 } catch (error) { 16700 console.error('EnhancedGallerySystem: Failed to initialize gallery framework:', error); 16701 //this.showErrorMessage(container, 'Failed to initialize gallery system. Please try again later.'); 16702 return false; 16703 } 16704 16705 if (!stillCurrent()) { 16706 console.log('EnhancedGallerySystem: Aborting stale load after framework initialize for', gallery.id); 16707 return false; 16708 } 16709 16710 if (!(await this.ensureNeonGalleryUtilsReady(container))) { 16711 console.error('EnhancedGallerySystem: neonGalleryUtils.loadGallerySettings unavailable after script load'); 16712 return false; 16713 } 16714 16715 if (!stillCurrent()) { 16716 console.log('EnhancedGallerySystem: Aborting stale load after utils ready for', gallery.id); 16717 return false; 16718 } 16719 16720 // Update current gallery tracking 16721 this.currentGalleryId = gallery.id; 16722 16723 console.log(`EnhancedGallerySystem: Loading gallery ${gallery.title} (ID: ${gallery.id}, Instance: ${galleryInstanceId})`); 16724 16725 try { 16726 // Determine which initialization method to use 16727 if (this.canUseReinitForGallery(gallery, options)) { 16728 const result = await this.reinitializeGallery(gallery, container, options); 16729 return stillCurrent() ? result : false;
16730 } else { 16731 const result = await this.initializeNewGallery(gallery, container, galleryInstanceId, options); 16732 return stillCurrent() ? result : false; 16733 } 16734 } catch (error) { 16735 console.error('EnhancedGallerySystem: Error loading gallery:', error); 16736 if (container) { 16737 console.error('EnhancedGallerySystem: Error loading gallery:', error); 16738 //this.showErrorMessage(container, 'There was a problem loading the gallery. Please try again later.'); 16739 } 16740 return false; 16741 } 16742} 16743 16744 normalizeGalleryIdentityValue(value) { 16745 if (value === undefined || value === null || value === '') return ''; 16746 return String(value); 16747 } 16748 16749 previousGalleryStateBelongsTo(gallery, previousState) { 16750 if (!gallery || !previousState) return false; 16751 16752 const targetGalleryId = this.normalizeGalleryIdentityValue(gallery.id); 16753 const targetPageId = this.normalizeGalleryIdentityValue(gallery.pageId); 16754 if (!targetGalleryId && !targetPageId) return false; 16755 16756 const previousGalleryId = this.normalizeGalleryIdentityValue( 16757 previousState.galleryId || previousState.loadedGalleryId 16758 ); 16759 const previousPageId = this.normalizeGalleryIdentityValue( 16760 previousState.pageId || previousState.initialPageUuid 16761 ); 16762 16763 if (!previousGalleryId && !previousPageId) return false; 16764 if (previousGalleryId && targetGalleryId && previousGalleryId !== targetGalleryId) { 16765 return false; 16766 } 16767 if (previousPageId && targetPageId && previousPageId !== targetPageId) { 16768 return false; 16769 } 16770 16771 return ( 16772 (previousGalleryId && targetGalleryId && previousGalleryId === targetGalleryId) || 16773 (previousPageId && targetPageId && previousPageId === targetPageId) 16774 ); 16775 } 16776 16777 /** 16778 * Determine if we can use reinitialization for this gallery 16779 */ 16780 canUseReinitForGallery(gallery, options) { 16781 if (options?.forceReload === true || window._forceGalleryReload === true) { 16782 return false; 16783 } 16784 // Check if reinitializeGalleryAfterSave function exists 16785 if (typeof window.reinitializeGalleryAfterSave !== 'function') { 16786 return false; 16787 } 16788 16789 // Check if we have a previous state to reuse 16790 if (!this.previousGalleryState) { 16791 return false; 16792 } 16793 16794 if (!this.previousGalleryStateBelongsTo(gallery, this.previousGalleryState)) { 16795 console.log('EnhancedGallerySystem: Cannot use reinit - previous gallery state does not match target gallery'); 16796 return false; 16797 } 16798 16799 // Check if the gallery has the necessary properties for reinit 16800 if (!gallery.pageId || !gallery.galleryOptions) { 16801 return false; 16802 } 16803 16804 // CRITICAL: Check if the gallery wrapper actually exists in the DOM 16805 // Reinitialization requires an existing gallery structure 16806 const galleryWrapper = document.querySelector('.gallery-wrapper'); 16807 const neonContainer = document.getElementById('neon-gallery-container'); 16808 if (!galleryWrapper || !neonContainer) { 16809 console.log('EnhancedGallerySystem: Cannot use reinit - gallery wrapper or container not found'); 16810 return false; 16811 } 16812 16813 return true; 16814 } 16815 16816 /** 16817 * Reinitialize a gallery using the reinitialization process 16818 */ 16819async reinitializeGallery(gallery, container, options) { 16820 console.log('EnhancedGallerySystem: Using reinitialization process for gallery', gallery.id); 16821 16822 try { 16823 // CRITICAL: Ensure CSS is loaded before attempting reinitialization 16824 ensureGalleryStylesLoaded(); 16825 16826 // We should already have the proper container, but just in case: 16827 if (!document.getElementById('neon-gallery-container')) { 16828 this.setupCorrectGalleryContainer(container); 16829 } 16830 16831 // CRITICAL: Ensure config is set up with all required fields before reinitialization 16832 const galleryOptions = gallery.galleryOptions || {}; 16833 window.neonGalleryConfig = { 16834 useData: true, 16835 useCDN: true, 16836 version: 'live', 16837 manualCollectionName: galleryOptions.manualCollectionName || "mod", 16838 layoutType: galleryOptions.layoutType || "grid", 16839 ...galleryOptions, 16840 siteId: window.siteId || gallery.siteId || '', 16841 galleryId: gallery.id, 16842 // Explicitly include all permanent storage fields 16843 isPerma: galleryOptions.isPerma, 16844 permaURL: galleryOptions.permaURL, 16845 loadTxId: galleryOptions.loadTxId, 16846 loadPermaURL: galleryOptions.loadPermaURL, 16847 containerSelector: '#neon-gallery-container' 16848 }; 16849 16850 // Prepare settings by merging previous state with new gallery options. 16851 // Identity fields always come from the menu target, never leftover snapshot ids. 16852 const settings = { 16853 ...this.previousGalleryState, 16854 ...gallery.galleryOptions, 16855 pageId: gallery.pageId, 16856 initialPageUuid: gallery.pageId, 16857 galleryId: gallery.id, 16858 loadedGalleryId: gallery.id, 16859 forceReload: true, 16860 containerSelector: '#neon-gallery-container' // Add this explicitly 16861 }; 16862 16863 // Call reinitializeGalleryAfterSave with the merged settings 16864 window._reinitializeGalleryFromSystem = true; 16865 let result; 16866 try { 16867 result = await window.reinitializeGalleryAfterSave(settings); 16868 } finally { 16869 window._reinitializeGalleryFromSystem = false;
16870 } 16871 16872 if (result) { 16873 console.log('EnhancedGallerySystem: Gallery reinitialization completed successfully'); 16874 // Fade in the container 16875 container.style.opacity = '1'; 16876 return true; 16877 } else { 16878 throw new Error('Reinitialization did not complete successfully'); 16879 } 16880 } catch (error) { 16881 console.error('EnhancedGallerySystem: Error reinitializing gallery:', error); 16882 // Fall back to standard initialization - ensure config is set up properly 16883 const galleryInstanceId = Date.now(); 16884 // Re-setup parameters and config for the fallback 16885 this.setupGalleryParameters(gallery, galleryInstanceId); 16886 return this.initializeNewGallery(gallery, container, galleryInstanceId, options); 16887 } 16888} 16889 16890 /** 16891 * Initialize a gallery from scratch (fallback method) 16892 */ 16893 async initializeNewGallery(gallery, container, galleryInstanceId, options) { 16894 console.log('EnhancedGallerySystem: Using standard initialization for gallery', gallery.id); 16895 const contentLoadGeneration = options && options.contentLoadGeneration != null 16896 ? options.contentLoadGeneration 16897 : window.__hydraContentLoadGeneration; 16898 const stillCurrent = () => typeof isHydraContentLoadCurrent === 'function' 16899 ? isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id) 16900 : (!window._activeContentType || window._activeContentType === 'gallery') && 16901 (window.activeGalleryId == null || String(window.activeGalleryId) === String(gallery.id)); 16902 16903 try { 16904 // CRITICAL: Ensure CSS is loaded BEFORE creating any gallery elements 16905 ensureGalleryStylesLoaded(); 16906 16907 // Clean up container 16908 if (container) { 16909 container.innerHTML = ''; 16910 } else { 16911 console.error('EnhancedGallerySystem: Container is null in initializeNewGallery'); 16912 return false; 16913 } 16914 16915 // Create gallery container with unique ID 16916 const galleryContainerId = `gallery-container-${gallery.id}-${galleryInstanceId}`; 16917 const neonGalleryContainer = document.createElement('div'); 16918 neonGalleryContainer.id = 'neon-gallery-container'; 16919 neonGalleryContainer.className = 'gallery-direct-content'; 16920 neonGalleryContainer.setAttribute('data-gallery-id', gallery.id.toString()); 16921 neonGalleryContainer.setAttribute('data-instance-id', galleryInstanceId.toString()); 16922 neonGalleryContainer.setAttribute('data-unique-id', galleryContainerId); 16923 neonGalleryContainer.style.width = '100%'; 16924 neonGalleryContainer.style.height = '100%'; 16925 container.appendChild(neonGalleryContainer); 16926 16927 // CRITICAL: Initialize neonGalleryState if it doesn't exist 16928 if (!window.neonGalleryState) { 16929 window.neonGalleryState = { 16930 initialized: false, 16931 initializing: false, 16932 domInitialized: false, 16933 configApplied: false, 16934 initializationPromise: null 16935 }; 16936 } 16937 16938 // CRITICAL: Always ensure neonGalleryConfig is properly set with ALL required fields 16939 // Don't rely on conditional logic - always set it correctly here 16940 const galleryOptions = gallery.galleryOptions || {}; 16941 window.neonGalleryConfig = { 16942 useData: true, 16943 useCDN: true, 16944 version: 'live', 16945 manualCollectionName: galleryOptions.manualCollectionName || "mod", 16946 layoutType: galleryOptions.layoutType || "grid", 16947 ...galleryOptions, 16948 siteId: window.siteId || gallery.siteId || '', 16949 galleryId: gallery.id, 16950 galleryInstanceId: galleryInstanceId, 16951 containerSelector: '#neon-gallery-container', 16952 // Explicitly include all permanent storage fields (critical for proper initialization) 16953 isPerma: galleryOptions.isPerma, 16954 permaURL: galleryOptions.permaURL, 16955 loadTxId: galleryOptions.loadTxId, 16956 loadPermaURL: galleryOptions.loadPermaURL 16957 }; 16958 16959 // Call the initialization function directly if available 16960 if (typeof window.initNeonGallery === 'function') { 16961 console.log('EnhancedGallerySystem: Calling direct initialization method'); 16962 16963 // Additional check: prevent duplicate initNeonGallery calls within 1 second 16964 const initKey = 'init_' + gallery.id + '_' + galleryInstanceId; 16965 const lastInitCall = this.lastInitializationTime.get(initKey); 16966 const now = Date.now(); 16967 if (lastInitCall && (now - lastInitCall) < 1000) { 16968 console.log('EnhancedGallerySystem: Skipping duplicate initNeonGallery call for gallery ' + gallery.id + '
16968(last call ' + (now - lastInitCall) + 'ms ago)'); 16969 container.style.opacity = '1'; 16970 return true; 16971 } 16972 this.lastInitializationTime.set(initKey, now); 16973 16974 try { 16975 // Wait a tiny bit to ensure DOM updates have propagated 16976 await new Promise(resolve => setTimeout(resolve, 10)); 16977 if (!stillCurrent()) { 16978 console.log('EnhancedGallerySystem: Aborting stale initializeNewGallery before initNeonGallery'); 16979 return false; 16980 } 16981 16982 const result = await window.initNeonGallery(); 16983 if (!stillCurrent()) { 16984 console.log('EnhancedGallerySystem: Ignoring stale initializeNewGallery completion'); 16985 return false; 16986 } 16987 console.log('EnhancedGallerySystem: Direct initialization successful'); 16988 16989 // Save state for future loads 16990 if (window.neonGalleryConfig) { 16991 this.captureGalleryState(window.neonGalleryConfig); 16992 } 16993 16994 // Fade in the container 16995 container.style.opacity = '1'; 16996 return true; 16997 } catch (e) { 16998 if (!stillCurrent()) { 16999 console.log('EnhancedGallerySystem: Ignoring stale direct initialization error'); 17000 return false; 17001 } 17002 console.error('EnhancedGallerySystem: Direct initialization failed:', e); 17003 throw e; 17004 } 17005 } else { 17006 // If initNeonGallery isn't available, use event-based initialization 17007 console.log('EnhancedGallerySystem: Using event-based initialization fallback'); 17008 17009 // Create an event to trigger initialization 17010 const initEvent = new CustomEvent('neon-gallery-init', { 17011 detail: { 17012 galleryId: gallery.id, 17013 instanceId: galleryInstanceId, 17014 containerId: galleryContainerId 17015 } 17016 }); 17017 document.dispatchEvent(initEvent); 17018 17019 // Wait a short time to see if initialization completes 17020 await new Promise(resolve => setTimeout(resolve, 500)); 17021 if (!stillCurrent()) { 17022 console.log('EnhancedGallerySystem: Ignoring stale event-based gallery initialization'); 17023 return false; 17024 } 17025 17026 // Fade in regardless of result - gallery script should handle displaying errors 17027 container.style.opacity = '1'; 17028 return true; 17029 } 17030 } catch (error) { 17031 console.error('EnhancedGallerySystem: Error in standard initialization:', error); 17032 if (!stillCurrent()) { 17033 console.log('EnhancedGallerySystem: Ignoring stale standard initialization error'); 17034 return false; 17035 } 17036 if (container) { 17037 this.showErrorMessage(container, 'Failed to load gallery content. Please try again later.'); 17038 } 17039 return false; 17040 } 17041 } 17042 17043 /** 17044 * Clean up gallery state without removing loaded scripts 17045 */ 17046 cleanupGalleryState(container) { 17047 console.log('EnhancedGallerySystem: Cleaning up gallery state'); 17048 17049 // Stop any ongoing singles autoplay from the previous gallery 17050 if (typeof window.stopSinglesAutoplay === 'function') { 17051 try { 17052 window.stopSinglesAutoplay(); 17053 console.log('EnhancedGallerySystem: Called stopSinglesAutoplay() during cleanup.'); 17054 } catch (error) { 17055 console.warn('EnhancedGallerySystem: stopSinglesAutoplay() failed during cleanup:', error); 17056 } 17057 } else { 17058 console.warn('EnhancedGallerySystem: stopSinglesAutoplay() function not found during cleanup.'); 17059 } 17060 17061 // 1. Reset global gallery state variables 17062 const resetGlobals = [ 17063 'neonGalleryInitComplete', 'neonGalleryInitInProgress', 17064 'neonGalleryLoaded', 'neonGalleryConfig', 'neonGalleryCache', 'neonGalleryImages', 17065 'neonGallerySettings', 'neonLightbox', 'neonGalleryEventListeners', 17066 'neonGalleryInstance', 'currentGalleryId', 'galleryData', 17067 'imageCache', 'thumbnailCache' 17068 ]; 17069
17070 resetGlobals.forEach(prop => { 17071 if (window[prop] !== undefined) { 17072 window[prop] = null; 17073 } 17074 }); 17075 17076 // 2. Reset neonGalleryState if it's a proper object with properties 17077 if (window.neonGalleryState && typeof window.neonGalleryState === 'object') { 17078 window.neonGalleryState = { 17079 initialized: false, 17080 initializing: false, 17081 domInitialized: false, 17082 configApplied: false, 17083 initializationPromise: null 17084 }; 17085 } 17086 17087 // 3. Remove lightbox elements and overlays 17088 document.querySelectorAll('#neon-lightbox, .neon-lightbox, .lightbox-container').forEach(el => { 17089 if (el && el.parentNode) { 17090 el.parentNode.removeChild(el); 17091 } 17092 }); 17093 17094 const staleOptionsOverlay = document.getElementById('optionsOverlay'); 17095 if (staleOptionsOverlay && staleOptionsOverlay.parentNode) { 17096 staleOptionsOverlay.parentNode.removeChild(staleOptionsOverlay); 17097 } 17098 window.optionsVisible = false; 17099 17100 // 4. Remove any gallery-specific DOM elements 17101 document.querySelectorAll( 17102 '.neon-gallery-wrapper, .fullscreen-overlay, ' + 17103 '.neon-gallery-modal, .gallery-tooltip, .neon-gallery-context-menu, ' + 17104 '.neon-thumbnails, .neon-image, .neon-caption, ' + 17105 '.neon-controls, .neon-pagination' 17106 ).forEach(el => { 17107 if (el && el.parentNode) { 17108 el.parentNode.removeChild(el); 17109 } 17110 }); 17111 17112 // 5. Clear container if provided and valid 17113 if (container) { 17114 container.innerHTML = ''; 17115 container.style.opacity = '0'; 17116 } 17117 17118 // 6. Clear keyboard listeners to prevent multiple listeners from different galleries 17119 if (typeof window.removeAllLightboxKeyboardListeners === 'function') { 17120 window.removeAllLightboxKeyboardListeners(); 17121 console.log('EnhancedGallerySystem: Cleared all lightbox keyboard listeners during cleanup'); 17122 } else if (window.currentLightboxKeyboardListener) { 17123 document.removeEventListener('keydown', window.currentLightboxKeyboardListener); 17124 window.currentLightboxKeyboardListener = null; 17125 console.log('EnhancedGallerySystem: Cleared keyboard listeners during cleanup'); 17126 } 17127 window.lightboxKeyboardListenerActive = false; 17128 } 17129 17130 /** 17131 * Prepare container for gallery loading 17132 */ 17133 prepareContainer(container, gallery, instanceId) { 17134 // CRITICAL: Add null check for container 17135 if (!container) { 17136 console.error('EnhancedGallerySystem: Container is null in prepareContainer'); 17137 return; 17138 } 17139 17140 try { 17141 // Add attributes and data 17142 container.setAttribute('data-gallery-id', gallery.id.toString()); 17143 container.setAttribute('data-instance-id', instanceId.toString()); 17144 17145 // Add loading indicator 17146 const loadingIndicator = document.createElement('div'); 17147 loadingIndicator.className = 'gallery-loading'; 17148 loadingIndicator.innerHTML = ` 17149 <div class="spinner"></div> 17150 <div class="loading-text">Loading Gallery...</div> 17151 `; 17152 17153 container.appendChild(loadingIndicator); 17154 } catch (error) { 17155 console.error('EnhancedGallerySystem: Error preparing container:', error); 17156 } 17157 } 17158 17159 17160 17161 17162setupGalleryParameters(gallery, galleryInstanceId) { 17163 // Set up gallery parameters with enhanced uniqueness 17164 window.Parameters = window.Parameters || {}; 17165 17166 // Clone and extend existing Parameters to avoid losing important values 17167 const existingParams = { ...window.Parameters }; 17168 17169 window.Parameters = { 17170 ...existingParams, 17171 SiteAlias: window.location.hostname, 17172 InitialPageUuid: gallery.pageId, 17173 InitialPageAlias: gallery.slug || this.slugify(gallery.title), 17174 // CRITICAL FIX: Set isInEditor to true if user is logged in as admin OR in edit mode 17175 // This ensures gallery files bypass cache for any logged-in admin, not just when edit mode is active 17176 isInEditor: window.isEditing || 17177 localStorage.getItem('hydra_is_admin') === 'true' || 17178 window.isAdmin === true || 17179 document.body.classList.contains('hydra-admin'), 17180 siteId: window.siteId || gallery.siteId || '', 17181 isHydra: true, 17182 17183 // CRITICAL: These properties help the gallery script find the container 17184 galleryContainerId: 'neon-gallery-container', 17185 containerSelector: '#neon-gallery-container', 17186 17187 // Additional properties 17188 galleryInstanceId: galleryInstanceId, 17189 galleryUniqueId: `gallery-container-${gallery.id}-${galleryInstanceId}`, 17190 loadTimestamp: galleryInstanceId, 17191 loadedGalleryId: gallery.id 17192 }; 17193 17194 // Set up gallery config - CRITICAL: Include all permanent storage fields 17195 const galleryOptions = gallery.galleryOptions || {}; 17196 window.neonGalleryConfig = { 17197 useData: true, 17198 useCDN: true, 17199 version: 'live', 17200 manualCollectionName: galleryOptions.manualCollectionName || "mod", 17201 layoutType: galleryOptions.layoutType || "grid", 17202 ...galleryOptions, 17203 siteId: window.siteId || gallery.siteId || '', 17204 galleryId: gallery.id, 17205 galleryInstanceId: galleryInstanceId, 17206 17207 // Add container selector here too 17208 containerSelector: '#neon-gallery-container', 17209 17210 // CRITICAL: Explicitly include all permanent storage fields to ensure they're available 17211 // These are needed for proper gallery initialization and data loading 17212 isPerma: galleryOptions.isPerma, 17213 permaURL: galleryOptions.permaURL, 17214 loadTxId: galleryOptions.loadTxId, 17215 loadPermaURL: galleryOptions.loadPermaURL 17216 }; 17217} 17218 17219 /** 17220 * Show error message in container 17221 */ 17222 showErrorMessage(container, message) {
17223 // CRITICAL: Add null check for container 17224 if (!container) { 17225 console.error('EnhancedGallerySystem: Container is null in showErrorMessage'); 17226 return; 17227 } 17228 17229 try { 17230 // Clear container 17231 container.innerHTML = ''; 17232 17233 // Create error message element 17234 const errorElement = document.createElement('div'); 17235 errorElement.className = 'gallery-error'; 17236 errorElement.innerHTML = ` 17237 <h3>Error Loading Gallery</h3> 17238 <p>${message}</p> 17239 `; 17240 container.appendChild(errorElement); 17241 container.style.opacity = '1'; 17242 } catch (error) { 17243 console.error('EnhancedGallerySystem: Error showing error message:', error); 17244 } 17245 } 17246 17247 /** 17248 * Utility function to convert title to slug 17249 */ 17250 slugify(text) { 17251 return text 17252 .toLowerCase() 17253 .replace(/s+/g, '-') 17254 .replace(/[^w-]+/g, '') 17255 .replace(/--+/g, '-') 17256 .trim(); 17257 } 17258 } 17259 17260 // Create a singleton instance 17261 window.EnhancedGallerySystem = window.EnhancedGallerySystem || new EnhancedGallerySystem(); 17262 17263 /** 17264 * Enhanced loadGallery function using the EnhancedGallerySystem 17265 * This replaces the existing loadGallery function 17266 */ 17267const enhancedLoadGallery = async function(id, event, options) { 17268 const originalLoadGallery = window.originalLoadGallery; 17269 const forceGalleryReload = options?.forceReload === true || window._forceGalleryReload === true; 17270 17271 stopAutoAdvanceTimer(); 17272 if (typeof dismissGalleryPasswordOverlay === 'function') { 17273 dismissGalleryPasswordOverlay(); 17274 } 17275 17276 if (event && event.target && event.target.isConnected === false) { 17277 console.log('enhancedLoadGallery: Ignoring navigation from detached element'); 17278 return; 17279 } 17280 17281 if (event) { 17282 event.preventDefault(); 17283 event.stopPropagation(); 17284 } 17285 17286 let galleryContainer = document.querySelector('.gallery-container'); 17287 17288 if (!galleryContainer) { 17289 galleryContainer = document.getElementById('galleryFrame')?.parentElement || 17290 document.getElementById('main-content') || 17291 document.getElementById('content'); 17292 17293 if (!galleryContainer) { 17294 console.error('Gallery container not found, falling back to original function'); 17295 if (typeof originalLoadGallery === 'function') { 17296 return originalLoadGallery(id, event, options); 17297 } else { 17298 console.error('Gallery container not found and no fallback available'); 17299 return; 17300 } 17301 } 17302 } 17303 17304 // Bail out before hiding/clearing when the same gallery is already loading or rendered. 17305 // Post-save passes { forceReload: true } to bypass this path without affecting navigation. 17306 const egEarly = window.EnhancedGallerySystem; 17307 if (egEarly && !forceGalleryReload) { 17308 if (egEarly._loadInProgress && egEarly._loadingGalleryId === id) { 17309 const pageViewStillShowing = window._activeContentType === 'page' || 17310 !!document.querySelector('.gallery-container .page-container, .page-container'); 17311 if (pageViewStillShowing) { 17312 console.log('enhancedLoadGallery: abandoning standalone in-flight load for page view', id); 17313 egEarly._loadInProgress = null; 17314 egEarly._loadingGalleryId = null; 17315 } else { 17316 console.log('enhancedLoadGallery: awaiting in-flight load for gallery', id); 17317 await egEarly._loadInProgress; 17318 if (galleryContainer) galleryContainer.style.opacity = '1'; 17319 return; 17320 } 17321 } 17322 const hasRenderedGallery = !!document.querySelector( 17323 '.gallery-wrapper .masonry-item, .gallery-wrapper .horizontal-scroller, .gallery-wrapper .masonry' 17324 ); 17325 // Embedded galleries on a page share currentGalleryId; that is not a standalone gallery view. 17326 const pageViewStillShowing = window._activeContentType === 'page' || 17327 !!document.querySelector('.gallery-container .page-container, .page-container'); 17328 if (hasRenderedGallery && egEarly.currentGalleryId === id && !pageViewStillShowing) { 17329 console.log('enhancedLoadGallery: gallery already rendered, skipping reload'); 17330 if (galleryContainer) galleryContainer.style.opacity = '1'; 17331 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 17332 window.syncGalleryOptionsButtonVisibility(); 17333 } 17334 return; 17335 } 17336 } else if (forceGalleryReload) { 17337 console.log('enhancedLoadGallery: force reload requested, bypassing already-rendered skip'); 17338 if (egEarly?.lastInitializationTime?.delete) { 17339 egEarly.lastInitializationTime.delete(id); 17340 } 17341 if (egEarly) { 17342 egEarly._loadInProgress = null; 17343 egEarly._loadingGalleryId = null; 17344 } 17345 } 17346 17347 if (galleryContainer) { 17348 galleryContainer.style.opacity = '0'; 17349 } 17350 17351 console.log('Loading gallery with ID (enhancedLoadGallery):', id); 17352 17353 const gallery = findGalleryById(galleries, id); 17354 17355 if (!gallery) { 17356 console.warn('No gallery found with ID:', id); 17357 if (galleryContainer) { 17358 galleryContainer.innerHTML = ` 17359 <div class="gallery-error"> 17360 <h3>Gallery Not Found</h3> 17361 <p>The requested gallery could not be found.</p> 17362 </div> 17363 `; 17364 galleryContainer.style.opacity = '1'; 17365 } 17366 return; 17367 } 17368 17369 if (isPageMenuItem(gallery)) { 17370 return typeof window.loadPage === 'function' ? window.loadPage(id, event) : loadPage(id, event); 17371 } 17372 17373 window._activeContentType = 'gallery'; 17374 17375 if (gallery.isSubmenu) { 17376 console.log('Gallery is submenu, skipping URL update'); 17377 toggleSubmenu(id, event || { stopPropagation: () => {} }); 17378 if (galleryContainer){ 17379 galleryContainer.style.opacity = '1'; 17380 } 17381 return; 17382 } 17383 17384 const contentLoadGeneration = beginHydraContentLoad('gallery', gallery.id, gallery.pageId); 17385 if (window.PageManager && typeof window.PageManager.clearCurrentPage === 'function') { 17386 window.PageManager.clearCurrentPage(); 17387 } 17388 17389 console.log('Found gallery:', gallery.title); 17390 17391 // Check for gallery-level password protection (only for visitors, not in edit mode) 17392 if (!isInEditMode() && gallery.passwordProtected) { 17393 // Use PageManager's gallery password protection methods 17394 if (window.PageManager && typeof window.PageManager.checkGalleryPasswordAccess === 'function' && 17395 typeof window.PageManager.createGalleryPasswordOverlay === 'function') { 17396 17397 if (!window.PageManager.checkGalleryPasswordAccess(gallery)) { 17398 console.log('Gallery is password protected, showing overlay'); 17399 window.PageManager.createGalleryPasswordOverlay(gallery, (accessGranted) => { 17400 if (accessGranted) { 17401 // Reload the gallery now that access is granted 17402 enhancedLoadGallery(id, event); 17403 } 17404 }); 17405 return; // Exit early, will reload after password is entered 17406 } 17407 } 17408 } 17409 17410 window.activeGalleryId = id; 17411 activeGalleryId = id; 17412 17413 // CRITICAL: Clean up page-embedded gallery scripts/DOM before loading a standalone gallery. 17414 // loadPage() calls removeGalleryScriptsWithPause which can interrupt EnhancedGallerySystem.initialize(), 17415 // leaving isLoading:true with a never-resolving initPromise. Re-run cleanup here (matching original 17416 // loadGallery) and clear the container so page content does not block gallery rendering. 17417 if (typeof window.removeGalleryScriptsWithPause === 'function') { 17418 await window.removeGalleryScriptsWithPause(); 17419 } 17420 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) { 17421 console.log('enhancedLoadGallery: Aborting stale load after cleanup for', gallery.id); 17422 return; 17423 } 17424 if (galleryContainer) { 17425 galleryContainer.innerHTML = ''; 17426 } 17427 17428 const bodyEl = document.body; 17429 if (shouldApplyMenuHiddenForEmbedOrGallery(gallery)) { 17430 bodyEl.classList.add('menu-hidden-on-page'); 17431 } else { 17432 bodyEl.classList.remove('menu-hidden-on-page'); 17433 } 17434 applyMenuOverlayClassForGallery(gallery); 17435 17436 try { 17437 if (typeof updateActiveStates === 'function') updateActiveStates(); 17438 if (typeof updateMobileTitle === 'function') updateMobileTitle(); 17439 if (typeof closeMobileMenu === 'function') closeMobileMenu(); 17440 // Close the gallery options panel when navigating to a different gallery 17441 if (typeof window.closeOptionsPanel === 'function' && !forceGalleryReload) { 17442 window.closeOptionsPanel(); 17443 } 17444 const staleOptionsOverlay = document.getElementById('optionsOverlay'); 17445 if (staleOptionsOverlay) { 17446 staleOptionsOverlay.remove(); 17447 } 17448 window.optionsVisible = false;
17449 if (typeof updateURLWithGallerySlug === 'function') { 17450 updateURLWithGallerySlug(gallery); 17451 } 17452 } catch (uiError) { 17453 console.error('Error updating UI state:', uiError); 17454 } 17455 17456 try { 17457 // Add detailed logging about EnhancedGallerySystem availability 17458 console.log('ð Checking EnhancedGallerySystem availability:', { 17459 exists: !!window.EnhancedGallerySystem, 17460 hasLoadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'), 17461 initialized: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.initialized), 17462 isLoading: !!(window.EnhancedGallerySystem && window.EnhancedGallerySystem.isLoading) 17463 }); 17464 17465 if (window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function') { 17466 console.log('â EnhancedGallerySystem available, calling loadGallery'); 17467 const result = await window.EnhancedGallerySystem.loadGallery( 17468 gallery, 17469 galleryContainer, 17470 { 17471 ...(options || {}), 17472 ...(forceGalleryReload ? { forceReload: true } : {}), 17473 contentLoadGeneration 17474 } 17475 ); 17476 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) { 17477 console.log('enhancedLoadGallery: Ignoring stale gallery completion for', gallery.id); 17478 return; 17479 } 17480 console.log('ð EnhancedGallerySystem.loadGallery result:', result); 17481 17482 if (!result && typeof originalLoadGallery === 'function') { 17483 console.log('â ï¸ Enhanced gallery loading failed, falling back to original function'); 17484 return originalLoadGallery(id, event, options); 17485 } 17486 if (galleryContainer) { 17487 galleryContainer.style.opacity = '1'; 17488 } 17489 if (typeof window.syncGalleryOptionsButtonVisibility === 'function') { 17490 window.syncGalleryOptionsButtonVisibility(); 17491 setTimeout(window.syncGalleryOptionsButtonVisibility, 350); 17492 } 17493 } else { 17494 console.error('â EnhancedGallerySystem or its loadGallery method is not available.', { 17495 EnhancedGallerySystem: !!window.EnhancedGallerySystem, 17496 loadGallery: !!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function'), 17497 useCdnProxy: window.useCdnProxy || false 17498 }); 17499 17500 // Fallback: Try using createGalleryContext directly 17501 console.log('ð Attempting fallback: createGalleryContext'); 17502 if (typeof createGalleryContext === 'function') { 17503 createGalleryContext(gallery, galleryContainer); 17504 if (galleryContainer) { 17505 galleryContainer.style.opacity = '1'; 17506 } 17507 return; 17508 } 17509 17510 if (galleryContainer) { 17511 galleryContainer.innerHTML = `<div class="gallery-error">Gallery system not available. EnhancedGallerySystem: ${!!window.EnhancedGallerySystem}, loadGallery: ${!!(window.EnhancedGallerySystem && typeof window.EnhancedGallerySystem.loadGallery === 'function')}</div>`; 17512 galleryContainer.style.opacity = '1'; 17513 } 17514 return; 17515 } 17516 } catch (error) { 17517 console.error('Error loading gallery:', error); 17518 if (!isHydraContentLoadCurrent(contentLoadGeneration, 'gallery', gallery.id)) { 17519 console.log('enhancedLoadGallery: Ignoring stale gallery error for', gallery.id); 17520 return; 17521 } 17522 if (galleryContainer) { 17523 try { 17524 galleryContainer.innerHTML = ` 17525 <div class="gallery-error"> 17526 <h3>Error Loading Gallery</h3> 17527 <p>There was a problem loading the gallery. Please try again later.</p> 17528 <p class="error-details">${error.message}</p> 17529 </div> 17530 `; 17531 galleryContainer.style.opacity = '1'; 17532 } catch (containerError) { 17533 console.error('Error showing error message in container:', containerError); 17534 } 17535 } 17536 if (typeof originalLoadGallery === 'function') { 17537 console.log('Falling back to original loadGallery after error'); 17538 return originalLoadGallery(id, event, options); 17539 } 17540 } 17541 }; 17542 17543 /** 17544 * Initialization function to set up the gallery system 17545 */ 17546 function initializeGallerySystem() { 17547 console.log('Initializing enhanced gallery system'); 17548 17549 // Store reference to original function before replacing 17550 if (typeof window.loadGallery === 'function') { 17551 window.originalLoadGallery = window.loadGallery; 17552 } 17553 17554 // Replace the existing loadGallery function with our optimized version 17555 window.loadGallery = enhancedLoadGallery; 17556 17557 // Add this CSS for loading/error states 17558 addGallerySystemStyles(); 17559 17560 // Preload styles only at boot â scripts load when first gallery opens (with container ready) 17561 window.EnhancedGallerySystem.preloadStylesOnly().catch(error => { 17562 console.warn('Gallery styles preloading failed:', error); 17563 }); 17564 } 17565 17566 /** 17567 * Add CSS styles for loading indicators and error messages 17568 */ 17569 function addGallerySystemStyles() { 17570 const styleId = 'enhanced-gallery-system-styles'; 17571 17572 // Skip if already added 17573 if (document.getElementById(styleId)) { 17574 return; 17575 } 17576 17577 const style = document.createElement('style'); 17578 style.id = styleId; 17579 style.textContent = ` 17580 /* Loading indicator */ 17581 .gallery-loading { 17582 display: flex; 17583 flex-direction: column; 17584 justify-content: center; 17585 align-items: center; 17586 width: 100%; 17587 height: 100%; 17588 min-height: 200px; 17589 opacity: 0 !important; 17590 } 17591 17592 .gallery-loading .spinner { 17593 width: 50px; 17594 height: 50px; 17595 border: 3px solid rgba(0, 0, 0, 0.1); 17596 border-radius: 50%; 17597 border-top-color: #4682B4; 17598 animation: spin 1s ease-in-out infinite; 17599 margin-bottom: 20px; 17600 opacity: 0 !important; 17601 } 17602 17603 @keyframes spin { 17604 to { transform: rotate(360deg); } 17605 } 17606 17607 .gallery-loading .loading-text { 17608 font-family: Arial, sans-serif; 17609 font-size: 16px; 17610 color: #555; 17611 opacity: 0; 17612 } 17613 17614 /* Error message */ 17615 .gallery-error { 17616 display: flex; 17617 flex-direction: column; 17618 justify-content: center; 17619 align-items: center; 17620 width: 100%; 17621 height: 100%; 17622 min-height: 200px; 17623 padding: 30px; 17624 text-align: center; 17625 } 17626 17627 .gallery-error h3 { 17628 font-family: Arial, sans-serif; 17629 font-size: 24px; 17630 color:rgba(211, 47, 47, 0.25); 17631 margin-bottom: 20px; 17632 } 17633 17634 .gallery-error p { 17635 font-family: Arial, sans-serif; 17636 font-size: 16px; 17637 color: #555; 17638 max-width: 600px; 17639 margin-bottom: 10px; 17640 } 17641 17642 .gallery-error .error-details { 17643 font-size: 14px; 17644 color: #888; 17645 font-style: italic; 17646 } 17647 `; 17648 17649 document.head.appendChild(style); 17650 } 17651 17652 // Initialize gallery system immediately if possible 17653 initializeGallerySystem(); 17654})(); 17655 17656 17657/** 17658 * Enhanced Gallery Link Interceptor 17659 * Handles clicks on gallery links to prevent full page reloads 17660 */ 17661(function() { 17662 // Store any loaded gallery data for quick reference 17663 const GALLERY_DATA_CACHE = { 17664 initialized: false, 17665 galleries: [], 17666 lastUpdate: 0 17667 }; 17668 17669 // Initialize when DOM is ready to catch early clicks 17670 document.addEventListener('DOMContentLoaded', initGalleryLinkInterceptor); 17671 17672 // Also initialize when window loads to ensure complete gallery data 17673 window.addEventListener('load', function() { 17674 // Use a small delay to ensure galleries have fully initialized 17675 setTimeout(initGalleryLinkInterceptor, 200); 17676 }); 17677 17678 // Additional init on first user interaction 17679 document.addEventListener('click', function initOnFirstClick() { 17680 initGalleryLinkInterceptor(); 17681 // Remove this listener after first click 17682 document.removeEventListener('click', initOnFirstClick); 17683 }, { once: true }); 17684 17685 // Main initialization function 17686 function initGalleryLinkInterceptor() { 17687 // Prevent multiple initializations 17688 if (window._galleryLinkInterceptorInitialized) { 17689 // If already initialized, just refresh the gallery data 17690 refreshGalleryCache(); 17691 return; 17692 } 17693 17694 window._galleryLinkInterceptorInitialized = true; 17695 console.log('Initializing enhanced gallery link interceptor'); 17696 17697 // Update our cache with current galleries data 17698 refreshGalleryCache(); 17699 17700 // Get primary containers that might contain links 17701 const galleryContainer = document.querySelector('.gallery-container'); 17702 const mainContainer = document.querySelector('.container') || document.body; 17703 17704 if (!galleryContainer && !mainContainer) { 17705 console.warn('No gallery containers found, link interceptor partially initialized'); 17706 } 17707 17708 // Add global click handler to catch all link clicks 17709 document.addEventListener('click', handleGlobalLinkClick, true); 17710 17711 // Try to intercept links from iframes - critical for gallery content 17712 interceptAllIframeLinks(); 17713 17714 // Set up a mutation observer to handle dynamically added content 17715 setupMutationObserver(); 17716 17717 // Log initialization with site details 17718 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 17719 console.log(`Gallery link interceptor initialized. Preview mode: ${isPreviewMode}`); 17720 17721 // Create global helper for debugging 17722 window.debugGalleryLinks = function() { 17723 return { 17724 inPreviewMode: isPreviewMode, 17725 interceptorInitialized: window._galleryLinkInterceptorInitialized, 17726 cachedGalleries: GALLERY_DATA_CACHE.galleries.length, 17727 urlPath: window.location.pathname, 17728 siteGuid: getSiteGuidFromPath() 17729 }; 17730 }; 17731 } 17732 17733 // Update the gallery cache when gallery data changes 17734function refreshGalleryCache() { 17735 // Initialize GALLERY_DATA_CACHE if it doesn't exist 17736 if (!window.GALLERY_DATA_CACHE) { 17737 window.GALLERY_DATA_CACHE = { 17738 initialized: false, 17739 galleries: [], 17740 lastUpdate: 0 17741 }; 17742 } 17743 17744 // Check for global galleries variable first 17745 if (typeof galleries !== 'undefined' && Array.isArray(galleries)) { 17746 // Use the global galleries array 17747 window.GALLERY_DATA_CACHE.galleries = galleries; 17748 window.GALLERY_DATA_CACHE.initialized = true; 17749 window.GALLERY_DATA_CACHE.lastUpdate = Date.now(); 17750 window.galleries = galleries; 17751 17752 console.log(`Gallery cache refreshed with ${galleries.length} galleries from global variable`); 17753 return; 17754 } 17755 17756 // Debug log 17757 console.log(`Gallery cache refreshed with ${GALLERY_DATA_CACHE.galleries.length} galleries`); 17758 } 17759 17760 // Global click handler - catches all link clicks 17761 function handleGlobalLinkClick(e) {
17762 // Find if a link was clicked 17763 const link = e.target.closest('a'); 17764 if (!link || !link.href) return; 17765 17766 // Skip if the link uses special targets or modifiers 17767 if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return; 17768 17769 // Skip for hash links or javascript: links 17770 if (link.href.includes('#') || link.href.startsWith('javascript:')) return; 17771 17772 try { 17773 // Parse the URL to work with it 17774 const url = new URL(link.href); 17775 17776 // Only intercept links to the same domain 17777 if (url.hostname !== window.location.hostname) return; 17778 17779 // Skip API and resource URLs 17780 if (url.pathname.includes('/api/') || isResourceUrl(url.pathname)) return; 17781 17782 console.log('Link intercepted:', url.pathname); 17783 17784 // For preview URLs, we need to handle the path specially 17785 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 17786 const siteGuid = getSiteGuidFromPath(); 17787 17788 console.log('Link interceptor - isPreviewMode:', isPreviewMode, 'siteGuid:', siteGuid); 17789 17790 // Site Graph card links carry data-sg-target-id â use SPA navigation (not preview iframe fallback) 17791 if (link.classList.contains('sg-card-link') && link.closest('.site-graph-element') && link.dataset.sgTargetId) { 17792 let galleriesData = (typeof galleries !== 'undefined' && Array.isArray(galleries)) 17793 ? galleries 17794 : window.galleries; 17795 const sgItem = Array.isArray(galleriesData) 17796 ? findGalleryById(galleriesData, link.dataset.sgTargetId) 17797 : null; 17798 if (sgItem) { 17799 e.preventDefault(); 17800 e.stopPropagation(); 17801 if (sgItem.isPage && window.loadPage) { 17802 window.loadPage(sgItem.id); 17803 } else if (window.loadGallery) { 17804 window.loadGallery(sgItem.id); 17805 } 17806 let targetUrl = url.href; 17807 if (isPreviewMode && siteGuid) { 17808 const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 17809 url.pathname : 17810 `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; 17811 const newUrl = new URL(url); 17812 newUrl.pathname = pathname; 17813 targetUrl = newUrl.href; 17814 } 17815 window.history.pushState( 17816 { galleryId: sgItem.id }, 17817 sgItem.title || document.title, 17818 targetUrl 17819 ); 17820 document.title = formatGalleryDocumentTitle(sgItem.title); 17821 window.scrollTo(0, 0); 17822 return; 17823 } 17824 } 17825 17826 // Check if this is internal navigation first 17827 let matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid); 17828 17829 console.log('Link interceptor - matchingItem found:', !!matchingItem, matchingItem ? matchingItem.title : 'none'); 17830 17831 if (matchingItem) { 17832 // Prevent default navigation (full page reload) 17833 e.preventDefault(); 17834 e.stopPropagation(); 17835 17836 console.log('Found matching gallery for link:', matchingItem.title); 17837 17838 const nestedLinkModeActive = typeof window.isNestedGalleryLinkModeEnabled === 'function' 17839 ? window.isNestedGalleryLinkModeEnabled() 17840 : !!(window.neonGalleryConfig?.useLinks && window.neonGalleryConfig?.openMultipleLightboxes); 17841 const isGridGalleryLink = !!(link.closest('.masonry-item') || 17842 (link.closest('.gallery-wrapper, #neon-gallery-container, .gallery-direct-content, .masonry') && 17843 link.querySelector('img') && 17844 !link.closest('#neon-lightbox, .neon-lightbox'))); 17845 17846 if (!matchingItem.isPage && 17847 nestedLinkModeActive && 17848 isGridGalleryLink && 17849 typeof window.openNestedGalleryLightbox === 'function') { 17850 void window.openNestedGalleryLightbox(matchingItem, window.activeGalleryId).then((opened) => { 17851 if (!opened) { 17852 console.warn('Nested gallery lightbox failed â staying on parent gallery (no loadGallery fallback)'); 17853 } 17854 }); 17855 return; 17856 } 17857 17858 // Use the existing navigation functions 17859 if (matchingItem.isPage && window.loadPage) { 17860 window.loadPage(matchingItem.id); 17861 } else if (window.loadGallery) { 17862 window.loadGallery(matchingItem.id); 17863 } 17864 17865 // Construct proper URL for preview mode 17866 let targetUrl = url.href; 17867 if (isPreviewMode && siteGuid) { 17868 // Ensure we keep the GUID in the URL path 17869 const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 17870 url.pathname : 17871 `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; 17872 17873 const newUrl = new URL(url); 17874 newUrl.pathname = pathname; 17875 targetUrl = newUrl.href; 17876 } 17877 17878 // Update URL without reload 17879 window.history.pushState( 17880 { galleryId: matchingItem.id }, 17881 matchingItem.title || document.title, 17882 targetUrl 17883 ); 17884 17885 // Update page title 17886 document.title = formatGalleryDocumentTitle(matchingItem.title); 17887 17888 // Scroll to top 17889 window.scrollTo(0, 0); 17890 17891 return; 17892 } 17893 // Special case for preview mode - even if no matching gallery found 17894 else if (isPreviewMode && siteGuid) { 17895 // We're in preview mode with a GUID - intercept the navigation 17896 // to maintain the GUID in the URL. Only do this when #galleryFrame 17897 // can load content; otherwise allow SPA onclick handlers to run. 17898 17899 console.log('No direct match but in preview mode - handling navigation'); 17900 17901 const frame = document.getElementById('galleryFrame'); 17902 17903 if (!frame) { 17904 console.warn('Gallery frame not found - allowing default/SPA handlers'); 17905 } else { 17906 // Prevent default navigation 17907 e.preventDefault(); 17908 e.stopPropagation(); 17909 17910 // Construct a URL that preserves the GUID 17911 let newPathname = url.pathname; 17912 if (!newPathname.startsWith(`/${siteGuid}/`)) { 17913 // Add the GUID to the path if not already there 17914 newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`; 17915 } 17916 17917 // Create the preview-friendly URL for browser history 17918 const browserUrl = new URL(window.location.origin); 17919 browserUrl.pathname = newPathname; 17920 17921 console.log('Constructing preview URL with GUID:', browserUrl.href); 17922 17923 // IMPORTANT: For gallery navigation, we need to create a special URL 17924 // that tells the gallery system to load the content directly 17925 console.log('Loading preview path in iframe:', newPathname); 17926 17927 // First update browser URL to show the correct path 17928 window.history.pushState(
17929 { customPath: newPathname }, 17930 document.title, 17931 browserUrl.href 17932 ); 17933 17934 // Create direct path to the gallery content 17935 const cleanPath = newPathname.replace(`/${siteGuid}/`, '/'); 17936 17937 // For direct gallery paths, we need to load the gallery with special parameters 17938 // This tells the gallery system to load content from this path 17939 const galleryUrl = `https://storage.neonsky.app/sites:site_${siteGuid}.json`; 17940 17941 // Two approaches to loading the content: 17942 17943 // 1. Use the gallery wrapper approach 17944 const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 17945 `?siteId=${siteGuid}` + 17946 `&path=${encodeURIComponent(cleanPath)}` + 17947 `&directPath=true` + 17948 `&t=${Date.now()}`; // Cache buster 17949 17950 console.log('Loading gallery via wrapper URL:', wrapperUrl); 17951 frame.src = wrapperUrl; 17952 17953 // Clear any active gallery ID since we're navigating directly 17954 if (window.activeGalleryId) { 17955 console.log('Clearing active gallery ID during direct navigation'); 17956 window.activeGalleryId = null; 17957 } 17958 17959 return; // Critical: prevent further navigation 17960 } 17961 } 17962 17963 console.log('No matching gallery or special handling - allow normal navigation'); 17964 } catch (error) { 17965 console.error('Error processing link click:', error); 17966 } 17967 } 17968 17969 // Handle all iframes on the page 17970 function interceptAllIframeLinks() { 17971 // Look for any iframe that might contain gallery content 17972 const iframes = document.querySelectorAll('iframe'); 17973 17974 iframes.forEach(iframe => { 17975 if (iframe.id === 'galleryFrame' || iframe.classList.contains('gallery-iframe')) { 17976 // Primary gallery iframe - high priority 17977 setupIframeIntercept(iframe, true); 17978 } else { 17979 // Any other iframe - still try to intercept 17980 setupIframeIntercept(iframe, false); 17981 } 17982 }); 17983 17984 console.log(`Attempted to intercept links in ${iframes.length} iframes`); 17985 } 17986 17987 // Setup link interception for a specific iframe 17988 function setupIframeIntercept(iframe, isPrimary) { 17989 try { 17990 // Skip if iframe is not fully loaded yet 17991 if (!iframe.contentWindow || !iframe.contentDocument) { 17992 // Add load event to try again when loaded 17993 iframe.addEventListener('load', function() { 17994 setupIframeIntercept(iframe, isPrimary); 17995 }); 17996 return; 17997 } 17998 17999 // Try to access the iframe content - might fail due to same-origin policy 18000 try { 18001 const iframeDoc = iframe.contentDocument; 18002 const iframeWin = iframe.contentWindow; 18003 18004 // Add event listener for all link clicks in the iframe 18005 iframeDoc.addEventListener('click', function(e) { 18006 const link = e.target.closest('a'); 18007 if (!link || !link.href) return; 18008 18009 // Skip special cases 18010 if (link.target === '_blank' || e.ctrlKey || e.metaKey || e.shiftKey) return; 18011 if (link.href.includes('#') || link.href.startsWith('javascript:')) return; 18012 18013 try { 18014 const url = new URL(link.href); 18015 18016 // Check if same domain or relative URL 18017 const isSameDomain = url.hostname === window.location.hostname || 18018 url.hostname === iframe.src.hostname || 18019 !url.hostname; 18020 18021 if (isSameDomain) { 18022 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 18023 const siteGuid = getSiteGuidFromPath(); 18024 18025 // First try to find a matching gallery/page 18026 const matchingItem = parent.findGalleryByPath ? 18027 parent.findGalleryByPath(url.pathname, isPreviewMode, siteGuid) : 18028 findGalleryByPath(url.pathname, isPreviewMode, siteGuid); 18029 18030 if (matchingItem) { 18031 // We found a match - prevent default and navigate 18032 e.preventDefault(); 18033 e.stopPropagation(); 18034 18035 console.log('Iframe: Found matching content for link:', matchingItem.title); 18036 18037 // Access parent window functions 18038 if (matchingItem.isPage && parent.loadPage) { 18039 parent.loadPage(matchingItem.id); 18040 } else if (parent.loadGallery) { 18041 parent.loadGallery(matchingItem.id); 18042 } 18043 18044 // Update URL in parent window 18045 let targetUrl = url.href; 18046 if (isPreviewMode && siteGuid) { 18047 // Ensure GUID is preserved in URL 18048 const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 18049 url.pathname : 18050 `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; 18051 18052 const newUrl = new URL(window.location.origin + pathname); 18053 targetUrl = newUrl.href; 18054 } 18055 18056 // Update browser URL 18057 parent.history.pushState( 18058 { galleryId: matchingItem.id }, 18059 matchingItem.title || document.title, 18060 targetUrl 18061 ); 18062 18063 // Update parent window title 18064 parent.document.title = formatGalleryDocumentTitle(matchingItem.title, parent); 18065 18066 // Scroll parent to top 18067 parent.scrollTo(0, 0); 18068 return; 18069 } 18070 // Special handling for preview mode - critical fix here! 18071 else if (isPreviewMode && siteGuid) { 18072 // Special handling for preview URLs 18073 e.preventDefault(); 18074 e.stopPropagation(); 18075 18076 console.log('Iframe: No direct match but in preview mode - handling navigation'); 18077 18078 // Construct a URL that preserves the GUID 18079 let newPathname = url.pathname; 18080 if (!newPathname.startsWith(`/${siteGuid}/`)) { 18081 newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`; 18082 } 18083 18084 // Create a URL for the browser history 18085 const browserUrl = new URL(window.location.origin); 18086 browserUrl.pathname = newPathname; 18087 18088 console.log('Iframe: Constructing preview URL with GUID:', browserUrl.href); 18089 18090 // Update browser URL in parent 18091 parent.history.pushState(
18092 { customPath: newPathname }, 18093 document.title, 18094 browserUrl.href 18095 ); 18096 18097 // Handle the navigation in the parent window 18098 if (isPrimary && parent.document.getElementById('galleryFrame')) { 18099 const galleryFrame = parent.document.getElementById('galleryFrame'); 18100 18101 // Create clean path for the gallery system 18102 const cleanPath = newPathname.replace(`/${siteGuid}/`, '/'); 18103 18104 // Create wrapper URL for gallery content 18105 const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 18106 `?siteId=${siteGuid}` + 18107 `&path=${encodeURIComponent(cleanPath)}` + 18108 `&directPath=true` + 18109 `&t=${Date.now()}`; // Cache buster 18110 18111 console.log('Iframe: Loading gallery via wrapper URL:', wrapperUrl); 18112 galleryFrame.src = wrapperUrl; 18113 18114 // Clear any active gallery ID since we're navigating directly 18115 if (parent.activeGalleryId) { 18116 console.log('Iframe: Clearing active gallery ID during direct navigation'); 18117 parent.activeGalleryId = null; 18118 } 18119 } else { 18120 // Fallback - navigate normally in case we can't handle it 18121 console.log('Iframe: Gallery frame not found or not primary, navigating normally'); 18122 window.location.href = browserUrl.href; 18123 } 18124 return; 18125 } 18126 } 18127 } catch (iframeError) { 18128 console.error('Error handling iframe link:', iframeError); 18129 } 18130 }); 18131 18132 if (isPrimary) { 18133 console.log('Successfully intercepted links in primary gallery iframe'); 18134 } 18135 } catch (sameOriginError) { 18136 // This is expected for cross-origin iframes 18137 if (isPrimary) { 18138 console.warn('Cannot access iframe content due to same-origin policy. Using fallback method.'); 18139 18140 // Apply a fallback method - add postMessage listener 18141 setupPostMessageListener(iframe); 18142 } 18143 } 18144 } catch (error) { 18145 console.warn('Error setting up iframe link interception:', error); 18146 } 18147 } 18148 18149 // Fallback: use postMessage for cross-origin iframes 18150 function setupPostMessageListener(iframe) { 18151 // Listen for messages from the iframe 18152 window.addEventListener('message', function(event) { 18153 try { 18154 // Verify the message is from our iframe 18155 if (event.source !== iframe.contentWindow) return; 18156 18157 const data = event.data; 18158 18159 // Check if this is a link click event 18160 if (data && data.type === 'linkClick' && data.href) { 18161 console.log('Received link click message from iframe:', data.href); 18162 18163 // Create a URL object to work with the link 18164 const url = new URL(data.href); 18165 18166 // Check if it's a local navigation 18167 if (url.hostname === window.location.hostname) { 18168 const isPreviewMode = window.location.hostname === 'preview.neonsky.app'; 18169 const siteGuid = getSiteGuidFromPath(); 18170 18171 // Try to find a matching gallery 18172 const matchingItem = findGalleryByPath(url.pathname, isPreviewMode, siteGuid); 18173 18174 if (matchingItem) { 18175 console.log('Found matching gallery for postMessage link:', matchingItem.title); 18176 18177 // Navigate using the appropriate function 18178 if (matchingItem.isPage && window.loadPage) { 18179 window.loadPage(matchingItem.id); 18180 } else if (window.loadGallery) { 18181 window.loadGallery(matchingItem.id); 18182 } 18183 18184 // Update URL without reload 18185 let targetUrl = url.href; 18186 if (isPreviewMode && siteGuid) { 18187 // Ensure GUID is preserved 18188 const pathname = url.pathname.startsWith(`/${siteGuid}/`) ? 18189 url.pathname : 18190 `/${siteGuid}${url.pathname.startsWith('/') ? url.pathname : '/' + url.pathname}`; 18191 18192 const newUrl = new URL(window.location.origin + pathname); 18193 targetUrl = newUrl.href; 18194 } 18195 18196 window.history.pushState( 18197 { galleryId: matchingItem.id }, 18198 matchingItem.title || document.title, 18199 targetUrl 18200 ); 18201 18202 // Update page title 18203 document.title = formatGalleryDocumentTitle(matchingItem.title); 18204 18205 return; 18206 } 18207 // Special handling for preview mode - Fixed! 18208 else if (isPreviewMode && siteGuid) { 18209 console.log('No direct match but in preview mode - handling navigation via postMessage'); 18210 18211 // Construct URL that preserves the GUID 18212 let newPathname = url.pathname; 18213 if (!newPathname.startsWith(`/${siteGuid}/`)) { 18214 newPathname = `/${siteGuid}${newPathname.startsWith('/') ? newPathname : '/' + newPathname}`; 18215 } 18216 18217 // Create browser URL 18218 const browserUrl = new URL(window.location.origin); 18219 browserUrl.pathname = newPathname; 18220 18221 // Update browser URL 18222 window.history.pushState(
18223 { customPath: newPathname }, 18224 document.title, 18225 browserUrl.href 18226 ); 18227 18228 // Get the main gallery frame - NOT the iframe that sent the message 18229 const galleryFrame = document.getElementById('galleryFrame'); 18230 if (galleryFrame) { 18231 // Create clean path without GUID 18232 const cleanPath = newPathname.replace(`/${siteGuid}/`, '/'); 18233 18234 // Create wrapper URL 18235 const wrapperUrl = `https://storage.neonsky.app/standalone/gallery-wrapper.html` + 18236 `?siteId=${siteGuid}` + 18237 `&path=${encodeURIComponent(cleanPath)}` + 18238 `&directPath=true` + 18239 `&t=${Date.now()}`; // Cache buster 18240 18241 console.log('Loading gallery via wrapper URL (postMessage):', wrapperUrl); 18242 galleryFrame.src = wrapperUrl; 18243 18244 // Clear any active gallery ID 18245 if (window.activeGalleryId) { 18246 window.activeGalleryId = null; 18247 } 18248 18249 return; 18250 } else { 18251 console.warn('Gallery frame not found - cannot load content via postMessage'); 18252 } 18253 } 18254 } 18255 } 18256 } catch (error) { 18257 console.error('Error processing postMessage from iframe:', error); 18258 } 18259 }); 18260 18261 // Inject a script into the iframe if possible to send messages back 18262 try { 18263 // We might be able to inject a script tag even in cross-origin scenarios 18264 // if the iframe is not yet loaded or using document.write 18265 iframe.addEventListener('load', function() { 18266 try { 18267 const script = iframe.contentDocument.createElement('script'); 18268 script.textContent = ` 18269 // Link interception script injected from parent 18270 document.addEventListener('click', function(e) { 18271 const link = e.target.closest('a'); 18272 if (link && link.href && !link.target && !e.ctrlKey && !e.metaKey && !e.shiftKey) { 18273 // Send message to parent window 18274 window.parent.postMessage({ 18275 type: 'linkClick', 18276 href: link.href, 18277 pathname: link.pathname 18278 }, '*'); 18279 18280 // Prevent default navigation 18281 e.preventDefault(); 18282 } 18283 }); 18284 console.log('Gallery link interceptor injected from parent'); 18285 `; 18286 iframe.contentDocument.head.appendChild(script); 18287 } catch (e) { 18288 // This will likely fail for cross-origin iframes 18289 console.log('Could not inject script into iframe - expected for cross-origin frames'); 18290 } 18291 }); 18292 } catch (error) { 18293 console.warn('Error trying to inject script into iframe:', error); 18294 } 18295 } 18296 18297 // Observer for new content/iframes 18298 function setupMutationObserver() { 18299 if (!window.MutationObserver) return; 18300 18301 const observer = new MutationObserver(function(mutations) { 18302 let newIframesFound = false; 18303 18304 mutations.forEach(function(mutation) { 18305 if (mutation.type === 'childList' && mutation.addedNodes.length) { 18306 // Check if any new iframes were added 18307 mutation.addedNodes.forEach(function(node) { 18308 // Check if the node is an iframe or contains iframes 18309 if (node.nodeName === 'IFRAME') { 18310 newIframesFound = true; 18311 setupIframeIntercept(node, node.id === 'galleryFrame'); 18312 } else if (node.querySelectorAll) { 18313 const iframes = node.querySelectorAll('iframe'); 18314 if (iframes.length > 0) { 18315 newIframesFound = true; 18316 iframes.forEach(function(iframe) { 18317 setupIframeIntercept(iframe, iframe.id === 'galleryFrame'); 18318 }); 18319 } 18320 } 18321 }); 18322 } 18323 }); 18324 18325 // If new iframes were found, update our cache 18326 if (newIframesFound) { 18327 refreshGalleryCache(); 18328 } 18329 }); 18330 18331 // Observe the entire document 18332 observer.observe(document.body, { 18333 childList: true, 18334 subtree: true 18335 }); 18336 } 18337 18338 // Helper to check if a URL is a resource rather than a page 18339 function isResourceUrl(path) { 18340 const resourceExtensions = [ 18341 '.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', 18342 '.css', '.js', '.pdf', '.ico', '.ttf', '.woff', '.woff2' 18343 ]; 18344 return resourceExtensions.some(ext => path.toLowerCase().endsWith(ext)); 18345 } 18346 18347 // Get the site GUID from the path in preview mode 18348 function getSiteGuidFromPath() { 18349 if (window.location.hostname === 'preview.neonsky.app') { 18350 const pathParts = window.location.pathname.split('/').filter(
18350Boolean); 18351 if (pathParts.length > 0) { 18352 return pathParts[0]; 18353 } 18354 } 18355 return null; 18356 } 18357 18358 18359/** 18360 * Enhanced gallery path matching with improved accuracy: 18361 * 18362 * This function finds the appropriate gallery based on URL path with better 18363 * prioritization and more precise matching criteria. 18364 * 18365 * @param {string} path - The URL path to match 18366 * @param {boolean} isPreviewMode - Whether we're in preview mode 18367 * @param {string} siteGuid - The site GUID (for preview mode) 18368 * @returns {Object|null} The matching gallery or null if none found 18369 */ 18370function findGalleryByPath(path, isPreviewMode, siteGuid) { 18371 // Ensure path is a string 18372 if (typeof path !== 'string') { 18373 console.warn('Invalid path provided to findGalleryByPath:', path); 18374 return null; 18375 } 18376 18377 // Determine which galleries array to use - check global variable first 18378 let galleriesData; 18379 let galleryCount = 0; 18380 18381 if (typeof galleries !== 'undefined' && Array.isArray(galleries)) { 18382 galleriesData = galleries; 18383 galleryCount = galleries.length; 18384 } else if (typeof window.galleries !== 'undefined' && Array.isArray(window.galleries)) { 18385 galleriesData = window.galleries; 18386 galleryCount = window.galleries.length; 18387 } else if (window.GALLERY_DATA_CACHE && 18388 window.GALLERY_DATA_CACHE.initialized && 18389 Array.isArray(window.GALLERY_DATA_CACHE.galleries)) { 18390 galleriesData = window.GALLERY_DATA_CACHE.galleries; 18391 galleryCount = window.GALLERY_DATA_CACHE.galleries.length; 18392 } else { 18393 console.warn('No galleries array available'); 18394 return null; 18395 } 18396 18397 console.log(`Searching for match among ${galleryCount} galleries`); 18398 18399 // Strip leading/trailing slashes without regex â template-literal embedding mangles / escapes 18400 // (e.g. /^/|/$/g becomes /^/|/$/g â 0), which left "/" unnormalized and broke home matching. 18401 function stripEdgeSlashes(value) { 18402 return String(value || '').split('/').filter(Boolean).join('/'); 18403 } 18404 18405 // Normalize the path for comparison (handle trailing/leading slashes) 18406 let normalizedPath = stripEdgeSlashes(path).toLowerCase(); 18407 const originalNormalizedPath = normalizedPath; // Keep original for logging 18408 18409 // If this is a preview URL, we need to handle the path differently 18410 if (isPreviewMode && siteGuid) { 18411 // Check if the path starts with the GUID 18412 if (normalizedPath.startsWith(siteGuid.toLowerCase() + '/')) { 18413 // Remove the GUID prefix 18414 normalizedPath = normalizedPath.substring(siteGuid.length + 1); 18415 } else if (normalizedPath === siteGuid.toLowerCase()) { 18416 // This is just the GUID with no additional path 18417 normalizedPath = ''; 18418 } 18419 } 18420 18421 // Create a dash-normalized version (all sequences of dashes become single dashes) 18422 // This handles both double-dash patterns and any other dash inconsistencies 18423 const normalizedPathWithSingleDashes = normalizedPath.replace(/-+/g, '-'); 18424 18425 console.log(`Normalized path: "${normalizedPath}" (dash-normalized: "${normalizedPathWithSingleDashes}")`); 18426 18427 // For empty paths, try to find the homepage 18428 if (!normalizedPath) { 18429 // Look for a gallery marked as home page 18430 const homePage = galleriesData.find(g => g.isHomePage === true); 18431 if (homePage) { 18432 console.log('Empty path - returning homepage:', homePage.title); 18433 return homePage; 18434 } 18435 } 18436 18437 // Array to store potential matches with their confidence levels 18438 const potentialMatches = []; 18439 18440 // Process each gallery to find potential matches with confidence levels 18441 galleriesData.forEach(gallery => { 18442 // Skip invalid galleries 18443 if (!gallery || !gallery.id) return; 18444 18445 // Store matching details for debugging 18446 const matchInfo = { 18447 gallery, 18448 confidence: 0, 18449 matchType: null, 18450 usedNormalizedDashes: false 18451 }; 18452 18453 // 1. EXACT SLUG MATCH (highest confidence) 18454 if (gallery.slug) { 18455 const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase(); 18456 18457 // Try original path 18458 if (gallerySlug === normalizedPath) { 18459 matchInfo.confidence = 100; // Max confidence 18460 matchInfo.matchType = 'exact_slug'; 18461 potentialMatches.push({...matchInfo}); 18462 return; // Continue to next gallery 18463 } 18464 18465 // Try dash-normalized path 18466 const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-'); 18467 if (normalizedPathWithSingleDashes === gallerySlugWithSingleDashes) { 18468 matchInfo.confidence = 95; // Very high confidence 18469 matchInfo.matchType = 'exact_slug_normalized_dashes'; 18470 matchInfo.usedNormalizedDashes = true; 18471 potentialMatches.push({...matchInfo}); 18472 return; // Continue to next gallery 18473 } 18474 } 18475 18476 // 2. EXACT URL PATH MATCH (very high confidence) 18477 if (gallery.url) { 18478 let galleryPath = gallery.url.toLowerCase(); 18479 18480 if (galleryPath.startsWith('http')) { 18481 try { 18482 const urlObj = new URL(galleryPath); 18483 galleryPath = urlObj.pathname; 18484 } catch (e) { 18485 // If URL parsing fails, use the string as is 18486 } 18487 } 18488 18489 galleryPath = stripEdgeSlashes(galleryPath); 18490 18491 // Try original path 18492 if (galleryPath === normalizedPath) { 18493 matchInfo.confidence = 90; // Very high confidence 18494 matchInfo.matchType = 'exact_url_path'; 18495 potentialMatches.push({...matchInfo}); 18496 return; // Continue to next gallery 18497 } 18498 18499 // Try dash-normalized path 18500 const galleryPathWithSingleDashes = galleryPath.replace(/-+/g, '-'); 18501 if (normalizedPathWithSingleDashes === galleryPathWithSingleDashes) { 18502 matchInfo.confidence = 85; // High confidence 18503 matchInfo.matchType = 'exact_url_path_normalized_dashes'; 18504 matchInfo.usedNormalizedDashes = true; 18505 potentialMatches.push({...matchInfo}); 18506 return; // Continue to next gallery 18507 } 18508 } 18509 18510 // 3. TITLE SLUG MATCH (high confidence) 18511 if (gallery.title) { 18512 const titleSlug = gallery.title.toLowerCase() 18513 .replace(/s+/g, '-') 18514 .replace(/[^w-]+/g, '') 18515 .replace(/--+/g, '-') 18516 .trim(); 18517 18518 // Try original path 18519 if (titleSlug === normalizedPath) { 18520 matchInfo.confidence = 80; // High confidence 18521 matchInfo.matchType = 'title_slug_match'; 18522 potentialMatches.push({...matchInfo}); 18523 return; // Continue to next gallery 18524 } 18525 18526 // Try dash-normalized path 18527 const titleSlugWithSingleDashes = titleSlug.replace(/-+/g, '-'); 18528 if (normalizedPathWithSingleDashes === titleSlugWithSingleDashes) { 18529 matchInfo.confidence = 75; // Good confidence 18530 matchInfo.matchType = 'title_slug_match_normalized_dashes'; 18531 matchInfo.usedNormalizedDashes = true; 18532 potentialMatches.push({...matchInfo}); 18533 return; // Continue to next gallery 18534 } 18535 } 18536 18537 // 4. PARTIAL SLUG MATCH - BUT WITH MUCH STRICTER CRITERIA 18538 if (gallery.slug && normalizedPath.length > 5) { 18539 const gallerySlug = stripEdgeSlashes(gallery.slug).toLowerCase(); 18540 const gallerySlugWithSingleDashes = gallerySlug.replace(/-+/g, '-'); 18541 18542 // Only if paths are substantial (to avoid false positives) 18543 if (normalizedPath.length >= 5 && gallerySlug.length >= 5) { 18544 // Try matching significant portions with dash normalization 18545 18546 // If the normalized paths contain each other with substantial overlap 18547 if (normalizedPathWithSingleDashes.includes(gallerySlugWithSingleDashes) || 18548 gallerySlugWithSingleDashes.includes(normalizedPathWithSingleDashes)) { 18549 18550 // Calculate overlap ratio based on the shorter string 18551 const overlapRatio = Math.min(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length) / 18552 Math.max(normalizedPathWithSingleDashes.length, gallerySlugWithSingleDashes.length); 18553 18554 // Only consider matches with substantial overlap (over 70%) 18555 if (overlapRatio >= 0.7) { 18556 const score = Math.round(60 + (overlapRatio * 20)); // 60-80 based on overlap 18557 18558 matchInfo.confidence = score; 18559 matchInfo.matchType = 'partial_match_normalized'; 18560 matchInfo.usedNormalizedDashes = true; 18561 matchInfo.overlapRatio = overlapRatio; 18562 potentialMatches.push({...matchInfo}); 18563 } 18564 } 18565 } 18566 } 18567 }); 18568 18569 // No potential matches found 18570 if (potentialMatches.length === 0) { 18571 console.log('No gallery match found for path:', path); 18572 return null; 18573 } 18574 18575 // Sort potential matches by confidence (highest first) 18576 potentialMatches.sort((a, b) => b.confidence - a.confidence); 18577 18578 // Get the best match 18579 const bestMatch = potentialMatches[0]; 18580 18581 // Log detailed debugging info about the match 18582 console.log(`Best match for "${normalizedPath}":`, { 18583 title: bestMatch.gallery.title, 18584 id: bestMatch.gallery.id, 18585 matchType: bestMatch.matchType, 18586 confidence: bestMatch.confidence, 18587 usedNormalizedDashes: bestMatch.usedNormalizedDashes, 18588 totalMatches: potentialMatches.length 18589 }); 18590 18591 // If we have multiple close matches, log them for debugging 18592 if (potentialMatches.length > 1 && 18593 potentialMatches[1].confidence >= bestMatch.confidence - 10) { 18594 console.log('Close alternative matches:', potentialMatches.slice(1, 3).map(m => ({ 18595 title: m.gallery.title, 18596 id: m.gallery.id, 18597 matchType: m.matchType, 18598 confidence: m.confidence 18599 }
18599))); 18600 } 18601 18602 // Return the best matching gallery 18603 return bestMatch.gallery; 18604} 18605 18606 // Make helpful functions available globally 18607 window.findGalleryByPath = findGalleryByPath; 18608 window.findGalleryById = findGalleryById; 18609 window.refreshGalleryCache = refreshGalleryCache; 18610 window.getSiteGuidFromPath = getSiteGuidFromPath; 18611 if (typeof getSiteGalleriesArray === 'function') { 18612 window.getSiteGalleriesArray = getSiteGalleriesArray; 18613 } 18614})(); 18615 18616 18617 18618 </script>
18618 18619 </div> 18620 18621<!-- Sidebar element form for adding new elements --> 18622<div class="sidebar-element-form pe-edit-shell" id="sidebarElementForm" style="display: none;" data-nosnippet> 18623<p class="pe-edit-shell__title">Add sidebar element</p> 18624<p class="pe-hint">Select the type of element to add:</p> 18625 18626<div class="radio-group"> 18627 <label> 18628 <input type="radio" name="sidebarElementType" value="text" checked> Text Block 18629 </label> 18630 <label> 18631 <input type="radio" name="sidebarElementType" value="image"> Image 18632 </label> 18633 <label> 18634 <input type="radio" name="sidebarElementType" value="social"> Social Icons 18635 </label> 18636 <label> 18637 <input type="radio" name="sidebarElementType" value="spacer"> Spacer 18638 </label> 18639</div> 18640 18641<div class="pe-footer-actions"> 18642<button type="button" class="pe-btn pe-btn--primary" onclick="addSidebarElement()">Add element</button> 18643<button type="button" class="pe-btn" onclick="toggleSidebarElementForm()">Cancel</button> 18644</div> 18645</div> 18646 18647 <div class="add-form pe-edit-shell" id="addForm" data-nosnippet> 18648 <p class="pe-edit-shell__title">Add to menu</p> 18649 <input type="text" id="galleryTitle" placeholder="Gallery Title"> 18650 <div class="radio-group"> 18651 <label> 18652 <input type="radio" name="galleryType" value="integrated" checked> Gallery 18653 </label> 18654 <label> 18655 <input type="radio" name="galleryType" value="page"> Page 18656 </label> 18657 <label> 18658 <input type="radio" name="galleryType" value="folder"> Folder 18659 </label> 18660 <label> 18661 <input type="radio" name="galleryType" value="spacer"> Spacer 18662 </label> 18663 <label> 18664 <input type="radio" name="galleryType" value="external"> External URL 18665 </label> 18666</div> 18667 <div id="urlInputContainer"> 18668 <input type="text" id="galleryUrl" placeholder="Gallery URL"> 18669 </div> 18670 <div id="galleryParentContainer" style="display: none;" aria-hidden="true"> 18671 <label for="galleryParent">Parent Menu:</label> 18672 <select id="galleryParent"> 18673 <option value="">None (Top Level)</option> 18674 </select> 18675 </div> 18676 <div class="pe-footer-actions"> 18677 <button type="button" class="pe-btn pe-btn--primary" onclick="addGallery()">Save</button> 18678 </div> 18679
18679<script> 18680 18681 18682 // Add event listeners to the radio buttons 18683 document.addEventListener('DOMContentLoaded', () => { 18684 const radioButtons = document.querySelectorAll('input[name="galleryType"]'); 18685 radioButtons.forEach(radio => { 18686 radio.addEventListener('change', function() { 18687 const urlContainer = document.getElementById('urlInputContainer'); 18688 if (this.value === 'external') { 18689 urlContainer.style.display = 'block'; 18690 } else { 18691 urlContainer.style.display = 'none'; 18692 } 18693 }); 18694 }); 18695 }); 18696 </script>
18696 18697 </div> 18698 18699<div class="metadata-editor pe-edit-shell" id="metadataEditor" style="display: none; max-height: 90vh; overflow-y: auto;" data-nosnippet> 18700 <h3 class="pe-edit-shell__title">Site Metadata</h3> 18701 18702 <div class="form-group"> 18703 <label for="metaTitle">Page Title (Search Results & Browsers)</label> 18704 <input type="text" id="metaTitle" class="form-control" placeholder="Enter site title for search results"> 18705 <small>Recommended length: 50-60 characters</small> 18706 </div> 18707 18708 <div class="form-group"> 18709 <label for="metaDescription">Meta Description</label> 18710 <textarea id="metaDescription" class="form-control" rows="3" placeholder="Enter site description for search results"></textarea> 18711 <small>Recommended length: 150-160 characters</small> 18712 </div> 18713 18714 <div class="form-group"> 18715 <label for="ogImageUrl">Social preview image (Open Graph / X / iMessage)</label> 18716 <input type="hidden" id="ogImageUrl" class="form-control" value=""> 18717 <button type="button" id="uploadOgImageBtn" class="pe-btn upload-image-btn"> 18718 <svg style="width: 16px; height: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 18719 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> 18720 <polyline points="17 8 12 3 7 8"></polyline> 18721 <line x1="12" y1="3" x2="12" y2="15"></line> 18722 </svg> 18723 <span id="uploadOgImageBtnText">Upload image</span> 18724 </button> 18725 <div id="ogImagePreviewContainer"></div> 18726 <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> 18727 </div> 18728 18729 <div class="form-group"> 18730 <label for="googleAnalytics">Google Analytics Code</label> 18731 <input type="text" id="googleAnalytics" class="form-control" placeholder="Enter GA-XXXXXXXXXX"> 18732 <small>Example: G-XXXXXXXXXX or UA-XXXXXXXX-X</small> 18733 </div> 18734 18735 <div class="form-group"> 18736 <label for="contentLanguage">Content Language</label> 18737 <select id="contentLanguage" class="form-control"> 18738 <option value="en">English (en)</option> 18739 <option value="es">Spanish / Español (es)</option> 18740 <option value="fr">French / Français (fr)</option> 18741 <option value="ne">Nepali / नà¥à¤ªà¤¾à¤²à¥ (ne)</option> 18742 <option value="de">German / Deutsch (de)</option> 18743 <option value="it">Italian / Italiano (it)</option> 18744 <option value="pt">Portuguese / Português (pt)</option> 18745 <option value="ru">Russian / Ð ÑÑÑкий (ru)</option> 18746 <option value="zh">Chinese / 䏿 (zh)</option> 18747 <option value="ja">Japanese / æ¥æ¬èª (ja)</option> 18748 <option value="ko">Korean / íêµì´ (ko)</option> 18749 <option value="ar">Arabic / Ø§ÙØ¹Ø±Ø¨ÙØ© (ar)</option> 18750 <option value="hi">Hindi / हिनà¥à¤¦à¥ (hi)</option> 18751 </select> 18752 <small>Set the primary language of your site content. Chrome will offer to translate if visitor's language is different.</small> 18753 </div> 18754 18755 <div class="form-group"> 18756 <label> 18757 <input type="checkbox" id="noIndexToggle"> 18758 Hide site from search engines (noindex) 18759 </label> 18760 <small>Use this if the site is under development or shouldn't appear in search results</small> 18761 </div> 18762 18763 <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;"> 18764 <h4 style="margin-bottom: 8px;">Schema / Identity</h4> 18765 <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> 18766 <label style="display: block; margin-bottom: 6px;">This site represents:</label> 18767 <div style="display: flex; gap: 20px; margin-bottom: 4px;"> 18768 <label style="display: flex; align-items: center; gap: 6px; font-weight: normal;"> 18769 <input type="radio" name="schemaType" id="schemaTypePerson" value="Person" onchange="updateSchemaFields()"> Person / Individual 18770 </label> 18771 <label style="display: flex; align-items: center; gap: 6px; font-weight: normal;"> 18772 <input type="radio" name="schemaType" id="schemaTypeOrganization" value="Organization" onchange="updateSchemaFields()"> Organization / Business 18773 </label> 18774 </div> 18775 <small>Select "Person" for individual photographers, artists, consultants. Select "Organization" for studios, agencies, companies.</small> 18776 </div> 18777 18778 <div class="form-group" id="schemaJobTitleGroup" style="display: none;"> 18779 <label for="schemaJobTitle">Job Title</label> 18780 <input type="text" id="schemaJobTitle" class="form-control" placeholder=""> 18781 <small>Used in Google Knowledge Panels and local search (e.g. "photojournalist Minneapolis").</small> 18782 </div> 18783 18784 <div class="form-group" id="schemaCityGroup" style="display: none;"> 18785 <label for="schemaCity">City / Location</label> 18786 <input type="text" id="schemaCity" class="form-control" placeholder=""> 18787 <small>Improves visibility in location-based searches.</small> 18788 </div> 18789 18790 <div class="form-group" id="schemaEmailGroup" style="display: none;"> 18791 <label for="schemaEmail">Contact Email (for structured data)</label> 18792 <input type="text" id="schemaEmail" class="form-control" placeholder="e.g. [email protected]"> 18793 <small>Embedded in structured data only â not displayed on the page.</small> 18794 </div> 18795 18796 <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;"> 18797 <h4 style="margin-bottom: 8px;">
18797Image licensing (optional)</h4> 18798 <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> 18799 <label for="imageLicenseUrl">License page URL</label> 18800 <input type="url" id="imageLicenseUrl" class="form-control" placeholder="https://example.com/licensing"> 18801 <small>A public page that states how images may be used (your terms, or a Creative Commons license).</small> 18802 </div> 18803 18804 <div class="form-group"> 18805 <label for="acquireLicensePageUrl">Acquire license page URL</label> 18806 <input type="url" id="acquireLicensePageUrl" class="form-control" placeholder="https://example.com/contact"> 18807 <small>A public page where someone can request or buy a license. Both URLs are required, or leave both empty.</small> 18808 </div> 18809 18810 <div class="form-group"> 18811 <label> 18812 <input type="checkbox" id="metadataCopyrightEnabled"> 18813 Enable sidebar copyright footer 18814 </label> 18815 <small>When enabled, the copyright text shows at the bottom of the sidebar.</small> 18816 </div> 18817 18818 <div class="form-group" id="metadataCopyrightTextGroup"> 18819 <label for="metadataCopyrightText">Copyright Text</label> 18820 <input type="text" id="metadataCopyrightText" class="form-control" placeholder="© 2025 Your Name"> 18821 <small>This text appears in the sidebar footer and inside the copyright overlay.</small> 18822 </div> 18823 18824 <div class="form-group"> 18825 <label for="faviconUrl">Favicon</label> 18826 <input type="hidden" id="faviconUrl" class="form-control"> 18827 <button type="button" id="uploadFaviconBtn" class="pe-btn upload-image-btn"> 18828 <svg style="width: 16px; height: 16px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 18829 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> 18830 <polyline points="17 8 12 3 7 8"></polyline> 18831 <line x1="12" y1="3" x2="12" y2="15"></line> 18832 </svg> 18833 <span id="uploadFaviconBtnText">Upload Favicon</span> 18834 </button> 18835 <div id="faviconPreviewContainer"></div> 18836 <small>Upload an image file for your site favicon (recommended: 32x32px or 16x16px, PNG or ICO format)</small> 18837 </div> 18838 18839 <div class="form-group" id="customPublicSiteBypassGroup" hidden style="display: none; border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;"> 18840 <h4 style="margin-bottom: 12px;">Custom HTML site (full bypass)</h4> 18841 <p style="font-size: 13px; color: #555; margin-bottom: 12px;"> 18842 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. 18843 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. 18844 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). 18845 </p> 18846 <label> 18847 <input type="checkbox" id="customPublicSiteEnabled" /> 18848 Serve uploaded HTML as the public site (bypass Hydra shell on <code>/</code>) 18849 </label> 18850 <input type="hidden" id="customPublicSiteHtmlR2Key" value="" /> 18851 <input type="hidden" id="customPublicSiteHtmlFilenameStore" value="" /> 18852 <div class="form-group" style="margin-top: 10px;"> 18853 <button type="button" id="uploadCustomPublicHtmlBtn" class="pe-btn">Upload HTML file</button> 18854 <button type="button" id="removeCustomPublicHtmlBtn" class="pe-btn">Clear file</button> 18855 </div> 18856 <p id="customPublicSiteHtmlFilenameLabel" style="font-size: 13px; color: #666; margin-top: 8px;">No HTML file uploaded yet.</p> 18857 </div> 18858 18859 <div class="form-group" style="border-top: 1px solid #ddd; padding-top: 16px; margin-top: 16px;"> 18860 <h4 style="margin-bottom: 8px;">Site-wide password (browser login)</h4> 18861 <p style="font-size: 13px; color: #555; margin-bottom: 12px;"> 18862 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 18863 <code>preview.neonsky.app/<your-site-id>/</code>). Editors still use Magic login to edit. Turn off to make the site fully public again. 18864 </p> 18865 <label> 18866 <input type="checkbox" id="siteWideBasicAuthEnabled" onchange="updateSiteWideBasicAuthFieldState()" /> 18867 Require HTTP Basic password for the entire site 18868 </label> 18869 <div id="siteWideBasicAuthFields" style="display: none; margin-top: 12px;"> 18870 <div class="form-group"> 18871 <label for="siteWideBasicAuthUsername">Username</label> 18872 <input type="text" id="siteWideBasicAuthUsername" class="form-control" autocomplete="username" /> 18873 </div> 18874 <div class="form-group"> 18875 <label for="siteWideBasicAuthPassword">Password</label> 18876 <input type="password" id="siteWideBasicAuthPassword" class="form-control" autocomplete="new-password" /> 18877 <small>Stored in your site configuration. Use a strong password; anyone with it can view the site.</small> 18878 </div> 18879 <div class="form-group"> 18880 <label for="siteWideBasicAuthRealm">Realm (optional)</label> 18881 <input type="text" id="siteWideBasicAuthRealm" class="form-control" placeholder="Site" /> 18882 <small>Label shown in the browser login dialog.</small> 18883 </div> 18884 </div> 18885 </div> 18886 18887 <div class="form-actions pe-footer-actions"> 18888 <button type="button" class="pe-btn pe-btn--primary" onclick="saveMetadata()">Save Metadata</button> 18889 <button type="button" class="pe-btn" onclick="toggleMetadataEditor()">Cancel</button> 18890 </div> 18891</div> 18892
18893<script> 18894var customPublicSiteOptionHeld = false; 18895var customPublicSiteOptionKeyBound = false; 18896 18897function isMetadataEditorOpen() { 18898 var editor = document.getElementById('metadataEditor'); 18899 if (!editor) return false; 18900 if (window.currentOpenForm === 'metadataEditor') return true; 18901 return editor.style.display === 'block'; 18902} 18903 18904function syncCustomPublicSiteBypassVisibility() { 18905 var group = document.getElementById('customPublicSiteBypassGroup'); 18906 if (!group) return; 18907 var show = isMetadataEditorOpen() && customPublicSiteOptionHeld; 18908 group.hidden = !show; 18909 group.style.display = show ? 'block' : 'none'; 18910} 18911 18912function bindCustomPublicSiteOptionKeyListeners() { 18913 if (customPublicSiteOptionKeyBound) return; 18914 customPublicSiteOptionKeyBound = true; 18915 18916 document.addEventListener('keydown', function(e) { 18917 if (!e.altKey) return; 18918 customPublicSiteOptionHeld = true; 18919 syncCustomPublicSiteBypassVisibility(); 18920 }, true); 18921 18922 document.addEventListener('keyup', function(e) { 18923 var isOptionRelease = 18924 e.key === 'Alt' || 18925 e.key === 'Option' || 18926 e.code === 'AltLeft' || 18927 e.code === 'AltRight'; 18928 if (!isOptionRelease) return; 18929 customPublicSiteOptionHeld = false; 18930 syncCustomPublicSiteBypassVisibility(); 18931 }, true); 18932} 18933 18934window.syncCustomPublicSiteBypassVisibility = syncCustomPublicSiteBypassVisibility; 18935window.bindCustomPublicSiteOptionKeyListeners = bindCustomPublicSiteOptionKeyListeners; 18936 18937function updateSiteWideBasicAuthFieldState() { 18938 const en = document.getElementById('siteWideBasicAuthEnabled'); 18939 const fields = document.getElementById('siteWideBasicAuthFields'); 18940 if (fields) fields.style.display = en && en.checked ? 'block' : 'none'; 18941} 18942 18943function updateSchemaFields() { 18944 const isPerson = document.getElementById('schemaTypePerson') && document.getElementById('schemaTypePerson').checked; 18945 const isOrg = document.getElementById('schemaTypeOrganization') && document.getElementById('schemaTypeOrganization').checked; 18946 const anySelected = isPerson || isOrg; 18947 const jobTitleGroup = document.getElementById('schemaJobTitleGroup'); 18948 const cityGroup = document.getElementById('schemaCityGroup'); 18949 const emailGroup = document.getElementById('schemaEmailGroup'); 18950 if (jobTitleGroup) jobTitleGroup.style.display = isPerson ? 'block' : 'none'; 18951 if (cityGroup) cityGroup.style.display = anySelected ? 'block' : 'none'; 18952 if (emailGroup) emailGroup.style.display = anySelected ? 'block' : 'none'; 18953} 18954 18955function uploadCustomPublicHtmlFile() { 18956 const siteId = window.Parameters && window.Parameters.siteId ? window.Parameters.siteId : (window.siteId || ''); 18957 if (!siteId) { 18958 alert('Site ID not found. Refresh the page and try again.'); 18959 return; 18960 } 18961 const authToken = window.ImageUploader && window.ImageUploader.getAuthToken ? window.ImageUploader.getAuthToken() : null; 18962 if (!authToken) { 18963 alert('Please log in first (press + on the page).'); 18964 return; 18965 } 18966 const input = document.createElement('input'); 18967 input.type = 'file'; 18968 input.accept = '.html,.htm,text/html'; 18969 input.addEventListener('change', function() { 18970 const file = input.files && input.files[0]; 18971 if (!file) return; 18972 const formData = new FormData(); 18973 formData.append('file', file); 18974 formData.append('guid', siteId); 18975 formData.append('purpose', 'custom-public-site'); 18976 formData.append('pageId', '_site'); 18977 formData.append('elementId', 'custom-public-html'); 18978 formData.append('folderPath', ''); 18979 var relUpload = typeof window.getApiUrl === 'function' ? window.getApiUrl('/api/upload-file') : '/api/upload-file'; 18980 var uploadUrl = window.location && window.location.origin ? new URL(relUpload, window.location.origin).href : relUpload; 18981 fetch(uploadUrl, { 18982 method: 'POST', 18983 headers: { 'Authorization': authToken, 'X-User-Email': localStorage.getItem('hydra_auth_email') || '' }, 18984 body: formData 18985 }).then(function(r) { 18986 return r.text().then(function(text) { 18987 var ct = (r.headers.get('content-type') || '').toLowerCase(); 18988 if (ct.indexOf('application/json') === -1 && /<!DOCTYPE/i.test(text)) { 18989 throw new Error( 18990 'Server returned HTML instead of JSON â POST /api/upload-file is not reaching the Hydra worker. ' + 18991 'In Cloudflare, ensure this hostname has a Worker route (e.g. neonsky.app/*) so API traffic is not sent to another host or Pages.' 18992 ); 18993 } 18994 if (r.status === 403 && ct.indexOf('application/json') === -1) { 18995 throw new Error( 18996 '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.' 18997 ); 18998 } 18999 try { 19000 return JSON.parse(text); 19001 } catch (parseErr) { 19002 throw new Error('Server did not return JSON: ' + text.slice(0, 120) + (text.length > 120 ? '...' : '')); 19003 } 19004 }); 19005 }).then(function(res) { 19006 if (res.success && res.key) { 19007 var keyEl = document.getElementById('customPublicSiteHtmlR2Key'); 19008 var fnEl = document.getElementById('customPublicSiteHtmlFilenameStore'); 19009 var lbl = document.getElementById('customPublicSiteHtmlFilenameLabel'); 19010 if (keyEl) keyEl.value = res.key; 19011 if (fnEl) fnEl.value = file.name; 19012 if (lbl) lbl.textContent = 'Current file: ' + file.name + ' â click Save Metadata below to apply.'; 19013 window.siteMetadata = window.siteMetadata || {}; 19014 window.siteMetadata.customPublicSiteHtmlR2Key = res.key; 19015 window.siteMetadata.customPublicSiteHtmlFilename = file.name; 19016 } else { 19017 alert(res.error || res.details || 'Upload failed'); 19018 } 19019 }).catch(function(e) { 19020 alert('Upload failed: ' + (e && e.message ? e.message : 'Unknown error')); 19021 }); 19022 }); 19023 input.click(); 19024} 19025 19026function clearCustomPublicHtmlFile() { 19027 if (!confirm('Clear the uploaded HTML file reference? (You can upload again later.)')) return; 19028 var k = document.getElementById('customPublicSiteHtmlR2Key'); 19029 var fn = document.getElementById('customPublicSiteHtmlFilenameStore'); 19030 var lbl = document.getElementById('customPublicSiteHtmlFilenameLabel'); 19031 if (k) k.value = ''; 19032 if (fn) fn.value = ''; 19033 if (lbl) lbl.textContent = 'No HTML file uploaded yet.'; 19034 if (window.siteMetadata) { 19035 window.siteMetadata.customPublicSiteHtmlR2Key = ''; 19036 window.siteMetadata.customPublicSiteHtmlFilename = ''; 19037 } 19038} 19039 19040// Handle favicon upload button click 19041document.addEventListener('DOMContentLoaded', function() { 19042 const uploadBtn = document.getElementById('uploadFaviconBtn'); 19043 if (uploadBtn) { 19044 uploadBtn.addEventListener('click', function() { 19045 uploadFavicon(); 19046 }); 19047 } 19048 const uploadOgBtn = document.getElementById('uploadOgImageBtn'); 19049 if (uploadOgBtn) { 19050 uploadOgBtn.addEventListener('click', function() { 19051 uploadSiteOgImage(); 19052 }); 19053 } 19054 var upHtml = document.getElementById('uploadCustomPublicHtmlBtn'); 19055 if (upHtml) upHtml.addEventListener('click', uploadCustomPublicHtmlFile); 19056 var rmHtml = document.getElementById('removeCustomPublicHtmlBtn'); 19057 if (rmHtml) rmHtml.addEventListener('click', clearCustomPublicHtmlFile); 19058 bindCustomPublicSiteOptionKeyListeners(); 19059 syncCustomPublicSiteBypassVisibility(); 19060 19061 // Handle content language dropdown change 19062 const contentLanguageSelect = document.getElementById('contentLanguage'); 19063 if (contentLanguageSelect) { 19064 contentLanguageSelect.addEventListener('change', function() { 19065 console.warn('Content language changed to:', this.value); 19066 // Update global variable immediately 19067 window.siteMetadata.contentLanguage = this.value; 19068 console.warn('Updated window.siteMetadata.contentLanguage:', window.siteMetadata.contentLanguage); 19069 }); 19070 } 19071 19072 const copyrightToggle = document.getElementById('metadataCopyrightEnabled'); 19073 if (copyrightToggle) { 19074 copyrightToggle.addEventListener('change', updateMetadataCopyrightFieldState); 19075 } 19076 19077 updateMetadataCopyrightFieldState(); 19078}); 19079 19080// Function to handle favicon upload using ImageUploader 19081function uploadFavicon() { 19082 console.log('Starting favicon upload...'); 19083 19084 // Check if ImageUploader is available 19085 if (!window.ImageUploader || typeof window.ImageUploader.open !== 'function') { 19086 alert('Image uploader not available. Please enter a favicon URL manually.'); 19087 return; 19088 } 19089 19090 try { 19091 // Create options for the ImageUploader 19092 const options = { 19093 title: 'Upload Favicon', 19094 containerType: 'favicon', 19095 maxWidth: 64, // Favicons are typically small 19096 maxHeight: 64, 19097 quality: 0.9 19098 }; 19099 19100 // Call the ImageUploader 19101 window.ImageUploader.open(options, function(imageResult, imageInfo) { 19102 console.log('Favicon upload callback received:', imageInfo); 19103 19104 // Get the favicon URL input field 19105 let faviconInput = document.getElementById('faviconUrl'); 19106 console.warn('=== UPLOAD CALLBACK DEBUG ==='); 19107 console.warn('Favicon input element found:', !!faviconInput); 19108 console.warn('Favicon input element type:', faviconInput ? faviconInput.type : 'N/A'); 19109 console.warn('Favicon input element id:', faviconInput ? faviconInput.id : 'N/A'); 19110 19111 // If element not found, wait a bit and try again (metadata editor might not be open yet) 19112 if (!faviconInput) { 19113 console.warn('Favicon input not found, waiting 100ms and trying again...'); 19114 setTimeout(() => { 19115 faviconInput = document.getElementById('faviconUrl'); 19116 console.warn('Retry - Favicon input element found:', !!faviconInput); 19117 if (faviconInput) { 19118 console.warn('Retry - Setting favicon input value to:', imageInfo.url); 19119 faviconInput.value = imageInfo.url; 19120 console.warn('Retry - Favicon input value after setting:', faviconInput.value); 19121 } else { 19122 console.error('Retry - Favicon input field still not found'); 19123 } 19124 }, 100); 19125 return; 19126 } 19127 19128 // Set the uploaded image URL 19129 if (imageInfo && imageInfo.type === 'r2' && imageInfo.url) { 19130 // R2 storage - use the URL 19131 console.warn('Setting favicon input value to:', imageInfo.url); 19132 faviconInput.value = imageInfo.url; 19133 console.warn('Favicon input value after setting:', faviconInput.value); 19134 19135 // Also store in global variable as backup 19136 window.uploadedFaviconUrl = imageInfo.url; 19137 console.warn('Stored favicon URL in global variable:', window.uploadedFaviconUrl); 19138 19139 console.log('Favicon URL set to:', imageInfo.url); 19140 19141 // Update button text 19142 const btnText = document.getElementById('uploadFaviconBtnText'); 19143 if (btnText) { 19144 btnText.textContent = 'Change Favicon'; 19145 } 19146 19147 // Show preview 19148 showFaviconPreview(imageInfo.url); 19149 } else if (imageResult) { 19150 // Legacy base64 storage 19151 faviconInput.value = imageResult; 19152 console.log('Favicon set to base64 data'); 19153 19154 // Update button text 19155 const btnText = document.getElementById('uploadFaviconBtnText'); 19156 if (btnText) { 19157 btnText.textContent = 'Change Favicon'; 19158 } 19159 19160 // Show preview 19161 showFaviconPreview(imageResult); 19162 } else { 19163 console.error('No image data received from uploader'); 19164 alert('Upload failed. Please try again.'); 19165 return; 19166 } 19167 19168 // Show success message 19169 console.log('Favicon uploaded successfully'); 19170 }); 19171 19172 } catch (error) { 19173 console.error('Error opening favicon uploader:', error); 19174 alert('Error opening uploader: ' + (error.message || 'Unknown error')); 19175 } 19176} 19177 19178// Function to show a preview of the uploaded favicon 19179function showFaviconPreview(imageUrl) { 19180 const container = document.getElementById('faviconPreviewContainer'); 19181 if (!container) { 19182 console.error('Favicon preview container not found'); 19183 return; 19184 } 19185 19186 container.innerHTML = 19187 '<div style="margin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; display: flex; align-items: center; gap: 15px;">' + 19188 '<img src="' + imageUrl + '" alt="Favicon Preview" style="width: 32px; height: 32px; border: 1px solid #ccc; background: white; padding: 4px;">' + 19189 '<div style="flex: 1;">' + 19190 '<div style="font-size: 14px; color: #333; font-weight: 500;">Current Favicon</div>' + 19191 '<div style="font-size: 12px; color: #666; margin-top: 2px;">Preview (32x32px)</div>' + 19192 '</div>' + 19193 '<button type="button" onclick="removeFavicon()" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' + 19194 'Remove' + 19195 '</button>' + 19196 '</div>'; 19197} 19198 19199// Function to remove the favicon 19200function removeFavicon() { 19201 const faviconInput = document.getElementById('faviconUrl'); 19202 if (faviconInput) { 19203 faviconInput.value = ''; 19204 } 19205 19206 const container = document.getElementById('faviconPreviewContainer'); 19207 if (container) { 19208 container.innerHTML = ''; 19209 } 19210 19211 const btnText = document.getElementById('uploadFaviconBtnText'); 19212 if (btnText) { 19213 btnText.textContent = 'Upload Favicon'; 19214 } 19215 19216 console.log('Favicon removed'); 19217} 19218 19219function uploadSiteOgImage() { 19220 if (!window.ImageUploader || typeof window.ImageUploader.open !== 'function') { 19221 alert('Image uploader not available. Refresh the page and try again.'); 19222 return; 19223 } 19224 try { 19225 window.ImageUploader.open({ 19226 title: 'Upload social preview image', 19227 containerType: 'page', 19228 quality: 0.92 19229 }, function(imageResult, imageInfo) { 19230 const inp = document.getElementById('ogImageUrl'); 19231 const setUrl = function(url) { 19232 if (inp) inp.value = url; 19233 const btnText = document.getElementById('uploadOgImageBtnText'); 19234 if (btnText) btnText.textContent = 'Change image'; 19235 showOgImagePreview(url); 19236 window.siteMetadata = window.siteMetadata || {}; 19237 window.siteMetadata.ogImageUrl = url; 19238 }; 19239 if (imageInfo && imageInfo.url) { 19240 setUrl(imageInfo.url); 19241 } else if (imageResult && typeof imageResult === 'string' && imageResult.indexOf('data:') === 0) { 19242 setUrl(imageResult); 19243 } else { 19244 alert('Upload failed. Please try again.'); 19245 } 19246 }); 19247 } catch (error) { 19248 console.error('Error opening OG image uploader:', error); 19249 alert('Error opening uploader: ' + (error.message || 'Unknown error')); 19250 } 19251} 19252 19253function showOgImagePreview(imageUrl) { 19254 const container = document.getElementById('ogImagePreviewContainer'); 19255 if (!container) return; 19256 container.innerHTML = 19257 '<div style="margin-top: 15px; padding: 15px; border: 1px solid #ddd; border-radius: 4px; background: #f9f9f9; display: flex; align-items: center; gap: 15px;">' + 19258 '<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;">' + 19259 '<div style="flex: 1;">' + 19260 '<div style="font-size: 14px; color: #333; font-weight: 500;">Current social preview image</div>' + 19261 '<div style="font-size: 12px; color: #666; margin-top: 2px;">This image overrides the first page/gallery image for social previews</div>' + 19262 '</div>' + 19263 '<button type="button" onclick="removeOgImage()" style="background: #dc3545; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' + 19264 'Remove' + 19265 '</button>' + 19266 '</div>'; 19267} 19268 19269function removeOgImage() { 19270 const inp = document.getElementById('ogImageUrl'); 19271 if (inp) inp.value = ''; 19272 if (window.siteMetadata) window.siteMetadata.ogImageUrl = ''; 19273 const container = document.getElementById('ogImagePreviewContainer'); 19274 if (container) container.innerHTML = ''; 19275 const btnText = document.getElementById('uploadOgImageBtnText'); 19276 if (btnText) btnText.textContent = 'Upload image'; 19277} 19278</script>
19278 19279 19280 <!-- Import Classic Form --> 19281<div class="import-classic-form" id="importClassicForm" style="display: none;" data-nosnippet> 19282 <h3>Import Galleries from JSON or Classic GUID</h3> 19283 <p>Paste the full site JSON into the text area below, <strong>OR</strong> enter the GUID of a classic gallery collection.</p> 19284 19285 <div class="form-group"> 19286 <label for="pastedJson">Pasted Site JSON:</label> 19287 <textarea id="pastedJson" rows="6" placeholder="Paste the entire site JSON here..."></textarea> 19288 </div> 19289 19290 <div class="import-separator">OR</div> 19291 19292 <div class="form-group"> 19293 <label for="classicGuid">Classic GUID or Import ID:</label> 19294 <input type="text" id="classicGuid" placeholder="Enter GUID (e.g., 4bd5ebf1b1418) or import ID (e.g., import_4c86452fb530b)"> 19295 <small>Use a GUID to fetch from classic site, or use import_ format to fetch from R2 storage.</small> 19296 </div> 19297 19298 <div class="form-group"> 19299 <label> 19300 <input type="checkbox" id="replaceMenuCheckbox" onchange="document.getElementById('importParent').disabled = this.checked; document.getElementById('createSubmenu').disabled = this.checked;"> 19301 Replace existing menu structure 19302 </label> 19303 <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> 19304 </div> 19305 19306 <div class="form-group"> 19307 <label for="importParent">Import under:</label> 19308 <select id="importParent"> 19309 <option value="">Top Level (No Parent)</option> 19310 </select> 19311 </div> 19312 19313 <div class="form-group"> 19314 <label> 19315 <input type="checkbox" id="createSubmenu" checked> 19316 Create submenu for all imported galleries 19317 </label> 19318 </div> 19319 19320 <div class="form-group" id="submenuTitleContainer"> 19321 <label for="submenuTitle">Submenu title:</label> 19322 <input type="text" id="submenuTitle" placeholder="e.g., Classic Galleries"> 19323 </div> 19324 19325 <div id="importStatus" class="import-status" style="display: none;"> 19326 <div class="import-progress"><div class="progress-bar"></div></div> 19327 <div class="status-message">Processing...</div> 19328 </div> 19329 19330 <div class="form-actions"> 19331 <button class="btn btn-primary" onclick="importClassicGalleries()">Import from Paste/GUID</button> 19332 <button class="btn" onclick="downloadCurrentSiteJSON()">Download Current Site JSON</button> 19333 <button class="btn" onclick="toggleImportClassicForm()">Cancel</button> 19334 </div> 19335</div> 19336 <div class="tree-container"> 19337 <div id="galleryTree" data-help-id="menu-tree"></div> 19338 </div> 19339 19340 <!-- Sidebar Elements Container will be inserted here by the sidebar-manager.js --> 19341 <div class="sidebar-elements-container"></div> 19342</div> 19343<div class="gallery-container" id="hydra-main-content" role="main" tabindex="-1"> 19344 <iframe class="gallery-iframe" id="galleryFrame" title="Life | Simple Image Page"></iframe> 19345</div> 19346</div> 19347 19348<!-- Cookie Notice (compliance mode â only when Display Cookie Notice is enabled) --> 19349 19350
19351<script> 19352// Reveal content once layout is applied, CSS is loaded, AND gallery is initialized 19353(function() { 19354 let hasRevealed = false; // Ensure we only reveal once per page load 19355 19356 function revealContent() { 19357 if (hasRevealed) return; // Prevent multiple calls 19358 hasRevealed = true; 19359 document.documentElement.classList.add('hydra-ready'); 19360 document.body.classList.add('hydra-ready'); 19361 console.log('â Content revealed - hydra-ready class added'); 19362 } 19363 19364 // Check if this page has a gallery container 19365 function hasGalleryContainer() { 19366 return !!document.getElementById('neon-gallery-container') || 19367 !!document.querySelector('.gallery-wrapper') || 19368 !!document.querySelector('.masonry-container'); 19369 } 19370 19371 // Check if gallery is already initialized 19372 function isGalleryInitialized() { 19373 return window.neonGalleryState?.initialized || 19374 window.neonGalleryState?.domInitialized || 19375 (window.neonGalleryUtils && window.neonGalleryUtils.isInitialized && window.neonGalleryUtils.isInitialized()); 19376 } 19377 19378 // Main reveal logic 19379 function attemptReveal() { 19380 const hasGallery = hasGalleryContainer(); 19381 const galleryInitialized = isGalleryInitialized(); 19382 19383 // If no gallery, reveal immediately after CSS loads 19384 if (!hasGallery) { 19385 if (document.readyState === 'complete') { 19386 revealContent(); 19387 } else { 19388 window.addEventListener('load', revealContent, { once: true }); 19389 } 19390 return; 19391 } 19392 19393 // If gallery exists and is initialized, reveal 19394 if (hasGallery && galleryInitialized) { 19395 revealContent(); 19396 return; 19397 } 19398 19399 // If gallery exists but not initialized, wait for initialization 19400 if (hasGallery && !galleryInitialized) { 19401 console.log('â³ Waiting for gallery initialization before revealing content...'); 19402 19403 // Listen for gallery-loaded event 19404 window.addEventListener('gallery-loaded', function() { 19405 console.log('â Gallery-loaded event received'); 19406 setTimeout(revealContent, 100); // Small delay to ensure DOM is updated 19407 }, { once: true }); 19408 19409 // Listen for page-loaded event 19410 window.addEventListener('page-loaded', function() { 19411 console.log('â Page-loaded event received'); 19412 setTimeout(revealContent, 100); 19413 }, { once: true }); 19414 19415 // Fallback: Poll for gallery initialization (max 10 seconds) 19416 let pollAttempts = 0; 19417 const maxPollAttempts = 100; // 10 seconds (100 * 100ms) 19418 const pollInterval = setInterval(function() { 19419 pollAttempts++; 19420 if (isGalleryInitialized()) { 19421 console.log('â Gallery initialized detected via polling'); 19422 clearInterval(pollInterval); 19423 revealContent(); 19424 } else if (pollAttempts >= maxPollAttempts) { 19425 console.warn('â ï¸ Gallery initialization timeout - revealing content anyway'); 19426 clearInterval(pollInterval); 19427 revealContent(); // Reveal anyway to prevent blank screen 19428 } 19429 }, 100); 19430 19431 // Safety fallback: Always reveal after 8 seconds max (prevents permanent blank screen) 19432 setTimeout(function() { 19433 if (!hasRevealed) { 19434 console.warn('â ï¸ Safety timeout reached - revealing content to prevent blank screen'); 19435 revealContent(); 19436 } 19437 }, 8000); 19438 } 19439 } 19440 19441 // Start the reveal process 19442 if (document.readyState === 'loading') { 19443 document.addEventListener('DOMContentLoaded', function() { 19444 // Wait a bit for scripts to initialize 19445 setTimeout(attemptReveal, 100); 19446 }); 19447 } else if (document.readyState === 'interactive' || document.readyState === 'complete') { 19448 // DOM already ready, wait a bit for scripts 19449 setTimeout(attemptReveal, 100); 19450 } else { 19451 // Fallback 19452 window.addEventListener('load', attemptReveal, { once: true }); 19453 } 19454})(); 19455</script>
19455 19456
19457<script> 19458// ============================================ 19459// LANGUAGE TRANSLATION SYSTEM 19460// ============================================ 19461 19462(function() { 19463 // CRITICAL: Only initialize translation system if a languages element exists 19464 // This ensures sites without languages element have ZERO overhead 19465 const checkForLanguagesElement = function() { 19466 // Wait for SidebarManager to be initialized 19467 if (!window.SidebarManager || !window.SidebarManager.elements) { 19468 return false; 19469 } 19470 19471 // Check if any languages element exists 19472 const hasLanguagesElement = window.SidebarManager.elements.some(el => el.type === 'languages'); 19473 19474 if (!hasLanguagesElement) { 19475 console.warn('No languages element found - translation system disabled'); 19476 return false; 19477 } 19478 19479 return true; 19480 }; 19481 19482 // Check for languages element, retry if SidebarManager not ready yet 19483 let initAttempts = 0; 19484 const maxAttempts = 10; 19485 19486 function tryInitialize() { 19487 if (checkForLanguagesElement()) { 19488 console.warn('â Languages element found - initializing translation system'); 19489 initializeTranslationSystem(); 19490 } else { 19491 initAttempts++; 19492 if (initAttempts < maxAttempts) { 19493 // Retry in 500ms (total max wait: 5 seconds) 19494 setTimeout(tryInitialize, 500); 19495 } else { 19496 console.warn('Translation system not needed for this site - no languages element found after 5 seconds'); 19497 } 19498 } 19499 } 19500 19501 // Start checking after initial delay 19502 setTimeout(tryInitialize, 500); 19503 19504 function initializeTranslationSystem() { 19505 console.warn('â Translation system initialized successfully'); 19506 19507 let currentLanguage = localStorage.getItem('hydra_language') || 'en'; 19508 const originalTexts = new Map(); 19509 19510 // Expose test function for debugging 19511 window.testTranslation = function(langCode) { 19512 console.warn('𧪠Manual translation test triggered for:', langCode); 19513 console.warn('ð Dispatching test event...'); 19514 window.dispatchEvent(new CustomEvent('hydra:language-change', { 19515 detail: { code: langCode || 'es', name: 'Test' } 19516 })); 19517 console.warn('â Test event dispatched - check if listener caught it above'); 19518 }; 19519 19520 console.warn('â Event listener for "hydra:language-change" is now active'); 19521 19522 // Watch for edit mode changes and restore default language 19523 const editModeObserver = new MutationObserver(function(mutations) { 19524 mutations.forEach(function(mutation) { 19525 if (mutation.attributeName === 'class') { 19526 const isEditMode = document.body.classList.contains('edit-mode-active'); 19527 if (isEditMode) { 19528 console.warn('Edit mode activated - restoring default language'); 19529 // Restore original text 19530 restoreOriginalText(); 19531 19532 // Update language buttons to show default language 19533 const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages'); 19534 const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en'; 19535 19536 document.querySelectorAll('.language-button-live').forEach(btn => { 19537 if (btn.getAttribute('data-lang-code') === defaultLang) { 19538 btn.style.background = 'var(--menu-active-color, #4682B4)'; 19539 } else { 19540 btn.style.background = 'transparent'; 19541 } 19542 }); 19543 } 19544 } 19545 }); 19546 }); 19547 19548 // Start observing body for class changes 19549 if (document.body) { 19550 editModeObserver.observe(document.body, { attributes: true, attributeFilter: ['class'] }); 19551 } 19552 19553 // Watch for dynamically loaded content (galleries, pages, etc.) and translate it 19554 const contentObserver = new MutationObserver(function(mutations) { 19555 // Only process if we're not in default language and not in edit mode 19556 const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages'); 19557 const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en'; 19558 const isEditMode = document.body.classList.contains('edit-mode-active'); 19559 19560 if (currentLanguage === defaultLang || isEditMode) { 19561 return; // Don't translate if in default language or edit mode 19562 } 19563 19564 // Check if new text content was added 19565 let hasNewTextContent = false;
19566 mutations.forEach(function(mutation) { 19567 if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { 19568 mutation.addedNodes.forEach(function(node) { 19569 if (node.nodeType === Node.ELEMENT_NODE) { 19570 // Check if the added element contains text we should translate 19571 const textElements = node.querySelectorAll('p, h1, h2, h3, .page-text'); 19572 if (textElements.length > 0 || (node.textContent && node.textContent.trim().length > 0)) { 19573 hasNewTextContent = true; 19574 } 19575 } 19576 }); 19577 } 19578 }); 19579 19580 if (hasNewTextContent) { 19581 console.warn('New content detected - translating to:', currentLanguage); 19582 // Debounce to avoid translating multiple times rapidly 19583 clearTimeout(window._translationDebounce); 19584 window._translationDebounce = setTimeout(function() { 19585 translatePage(currentLanguage, defaultLang); 19586 }, 300); 19587 } 19588 }); 19589 19590 // Observe the gallery container and page content areas for new content 19591 const galleryContainer = document.querySelector('.gallery-container'); 19592 if (galleryContainer) { 19593 contentObserver.observe(galleryContainer, { 19594 childList: true, 19595 subtree: true 19596 }); 19597 } 19598 19599 // Also observe the main content area 19600 const mainContent = document.querySelector('.container'); 19601 if (mainContent) { 19602 contentObserver.observe(mainContent, { 19603 childList: true, 19604 subtree: true 19605 }); 19606 } 19607 19608 // Listen for language change events 19609 window.addEventListener('hydra:language-change', async function(e) { 19610 const { code, name } = e.detail; 19611 console.warn('ð Language changed to:', code, name); 19612 19613 // CRITICAL: Ignore language changes in edit mode 19614 if (document.body.classList.contains('edit-mode-active')) { 19615 console.warn('â ï¸ Edit mode active - ignoring language change'); 19616 // Reset to default language 19617 const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages'); 19618 const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en'; 19619 19620 // Update button states to show default language 19621 document.querySelectorAll('.language-button-live').forEach(btn => { 19622 if (btn.getAttribute('data-lang-code') === defaultLang) { 19623 btn.style.background = 'var(--menu-active-color, #4682B4)'; 19624 } else { 19625 btn.style.background = 'transparent'; 19626 } 19627 }); 19628 19629 // Show message to user 19630 console.warn('Language switching is disabled in edit mode'); 19631 return; 19632 } 19633 19634 // Update all language buttons 19635 document.querySelectorAll('.language-button-live').forEach(btn => { 19636 if (btn.getAttribute('data-lang-code') === code) { 19637 btn.style.background = 'var(--menu-active-color, #4682B4)'; 19638 } else { 19639 btn.style.background = 'transparent'; 19640 } 19641 }); 19642 19643 // Save preference 19644 localStorage.setItem('hydra_language', code); 19645 currentLanguage = code; 19646 19647 // Get default language from languages element 19648 const languagesElement = window.SidebarManager?.elements?.find(el => el.type === 'languages'); 19649 const defaultLang = languagesElement?.languages?.find(l => l.isDefault)?.code || 'en'; 19650 19651 if (code === defaultLang) { 19652 // Restore original language 19653 restoreOriginalText(); 19654 } else { 19655 // Translate page 19656 await translatePage(code, defaultLang); 19657 } 19658 }); 19659 19660 async function translatePage(targetLang, sourceLang) { 19661 console.warn(`ð Translating page from ${sourceLang} to ${targetLang}`); 19662 19663 // Elements to translate 19664 const selectors = [ 19665 '.sidebar li a:not(.edit-mode-active .sidebar li a)', // Menu items (not in edit mode) 19666 '.page-text:not(.edit-mode-active .page-text)', // Page content (not in edit mode) 19667 'h1:not(.edit-mode-active h1)', 19668 'h2:not(.edit-mode-active h2)', 19669 'h3:not(.edit-mode-active h3)', 19670 'p:not(.edit-mode-active p)' 19671 ]; 19672 19673 const elements = document.querySelectorAll(selectors.join(', ')); 19674 19675 for (const element of elements) { 19676 // Skip if already translated to this language 19677 if (element.dataset.translatedTo === targetLang) continue; 19678 19679 // Skip if element is empty 19680 const text = element.textContent.trim(); 19681 if (!text || text.length === 0) continue; 19682 19683 // Store original text 19684 if (!originalTexts.has(element)) { 19685 originalTexts.set(element, text); 19686 } 19687 19688 // Check translation cache 19689 const cacheKey = `translate_${text}_${targetLang}`; 19690 let translated = getFromCache(cacheKey); 19691 19692 if (translated) { 19693 element.textContent = translated; 19694 element.dataset.translatedTo = targetLang; 19695 } else { 19696 // Translate 19697 try { 19698 translated = await translate(text, sourceLang, targetLang); 19699 if (translated && translated !== text) { 19700 element.textContent = translated; 19701 element.dataset.translatedTo = targetLang; 19702 saveToCache(cacheKey, translated); 19703 } 19704 } catch (error) { 19705 console.error('Translation error:', error); 19706 } 19707 } 19708 } 19709 } 19710 19711 function restoreOriginalText() {
19712 originalTexts.forEach((originalText, element) => { 19713 if (element && element.isConnected) { 19714 element.textContent = originalText; 19715 delete element.dataset.translatedTo; 19716 } 19717 }); 19718 } 19719 19720 async function translate(text, sourceLang, targetLang) { 19721 if (!text || text.trim().length === 0) return text; 19722 19723 // Try Browser Translation API (Free, Chrome 130+) 19724 if ('translation' in self && 'createTranslator' in self.translation) { 19725 try { 19726 console.warn('ð Using Browser Translation API'); 19727 const translator = await self.translation.createTranslator({ 19728 sourceLanguage: sourceLang, 19729 targetLanguage: targetLang 19730 }); 19731 const result = await translator.translate(text); 19732 console.warn('â Translation successful via Browser API'); 19733 return result; 19734 } catch (error) { 19735 console.warn('â Browser translation failed:', error); 19736 } 19737 } else { 19738 console.warn('â ï¸ Browser Translation API not available'); 19739 console.warn(' To enable in Chrome: chrome://flags/#translation-api'); 19740 console.warn(' OR add Google Translate API key to use paid translation'); 19741 } 19742 19743 // TODO: Add Google Translate API fallback here 19744 // For now, return original text 19745 console.warn('â ï¸ No translation provider configured - returning original text'); 19746 return text; 19747 } 19748 19749 // Cache management 19750 function getFromCache(key) { 19751 try { 19752 const item = localStorage.getItem(`hydra_${key}`); 19753 if (!item) return null; 19754 19755 const { value, expiry } = JSON.parse(item); 19756 if (expiry && Date.now() > expiry) { 19757 localStorage.removeItem(`hydra_${key}`); 19758 return null; 19759 } 19760 return value; 19761 } catch { 19762 return null; 19763 } 19764 } 19765 19766 function saveToCache(key, value) { 19767 try { 19768 const item = { 19769 value, 19770 expiry: Date.now() + (30 * 24 * 60 * 60 * 1000) // 30 days 19771 }; 19772 localStorage.setItem(`hydra_${key}`, JSON.stringify(item)); 19773 } catch (error) { 19774 console.warn('Cache save failed:', error); 19775 } 19776 } 19777 19778 } // End of initializeTranslationSystem 19779})(); 19780</script>
19780 19781 19782 19783
19784<script> 19785window.HYDRA_HELP_ENABLED = true; 19786function loadHelpSystemScript() { 19787 if (window.HelpSystem && window.HelpSystem.openHub) return Promise.resolve(); 19788 return new Promise(function(resolve, reject) { 19789 var existing = document.querySelector('script[data-hydra-help-system]'); 19790 if (existing) { 19791 existing.addEventListener('load', function() { resolve(); }); 19792 existing.addEventListener('error', reject); 19793 return; 19794 } 19795 var s = document.createElement('script'); 19796 s.src = '/api/help-system.js?v=686c85345749'; 19797 s.async = true; 19798 s.setAttribute('data-hydra-help-system', '1'); 19799 s.onload = function() { resolve(); }; 19800 s.onerror = reject; 19801 document.head.appendChild(s); 19802 }); 19803} 19804function openHydraHelp(opts) { 19805 opts = opts || {}; 19806 if (!window.HYDRA_HELP_ENABLED) return; 19807 var token = window.didToken || localStorage.getItem('hydra_auth_token'); 19808 var isAdmin = window.isAdmin || localStorage.getItem('hydra_is_admin') === 'true'; 19809 if (!token || !isAdmin) return; 19810 var p = loadHelpSystemScript(); 19811 Promise.resolve(p).then(function() { 19812 if (window.HelpSystem && window.HelpSystem.init) return window.HelpSystem.init(); 19813 }).then(function() { 19814 if (window.HelpSystem && window.HelpSystem.openHub) { 19815 window.HelpSystem.openHub(opts.initialTab ? { initialTab: opts.initialTab } : undefined); 19816 } 19817 }).catch(function(err) { 19818 console.error('HelpSystem init failed:', err); 19819 }); 19820} 19821window.openHydraHelp = openHydraHelp; 19822</script>
19822 19823
19824<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>
19824 19825</body> 19826</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.