1import { Controller } from "@hotwired/stimulus" 2 3const APP_SHELL_STATE_KEY = "snapnsell:app-shell-state" 4 5export default class extends Controller { 6 static targets = [ 7 "startPage", 8 "imagesPage", 9 "sellPane", 10 "sellFrame", 11 "sellTab", 12 "snapTab", 13 "snapBackground", 14 "menu", 15 "collectionsMenu", 16 "overlay", 17 "card", 18 "cardFrame", 19 "chats", 20 "chatsFrame", 21 "chatsBackdrop" 22 ] 23 static values = { 24 initialPane: { type: String, default: "snap" }, 25 initialCardOpen: { type: Boolean, default: false }, 26 rootUrl: String, 27 chatsDeeplink: { type: Object, default: {} } 28 } 29 30 connect() { 31 this.boundShowSnap = () => this.activatePane("snap", { explicit: true }) 32 this.boundShowSell = () => this.activatePane("sell", { explicit: true }) 33 this.boundShowImages = () => this.activatePane("images", { explicit: true }) 34 this.boundOpenCard = (event) => this.openCardForEvent(event) 35 this.boundCloseCard = () => this.closeCard() 36 this.boundCloseMenus = () => this.closeMenus() 37 38 document.addEventListener("ss:show-snap", this.boundShowSnap) 39 document.addEventListener("ss:show-sell", this.boundShowSell) 40 document.addEventListener("ss:show-images", this.boundShowImages) 41 document.addEventListener("ss:open-card", this.boundOpenCard) 42 document.addEventListener("ss:close-card", this.boundCloseCard) 43 document.addEventListener("ss:close-menus", this.boundCloseMenus) 44 45 this.normalizeBrowserUrl() 46 this.restoreShellState() 47 this.maybeOpenChatsFromDeeplink() 48 } 49 50 disconnect() { 51 document.removeEventListener("ss:show-snap", this.boundShowSnap) 52 document.removeEventListener("ss:show-sell", this.boundShowSell) 53 document.removeEventListener("ss:show-images", this.boundShowImages) 54 document.removeEventListener("ss:open-card", this.boundOpenCard) 55 document.removeEventListener("ss:close-card", this.boundCloseCard) 56 document.removeEventListener("ss:close-menus", this.boundCloseMenus) 57 } 58 59 resetToSnap(event) { 60 event.preventDefault() 61 this.closeCard() 62 if (window.ssApp && typeof window.ssApp.resetApp === "function") { 63 window.ssApp.resetApp() 64 return 65 } 66 67 this.activatePane("snap", { explicit: true }) 68 } 69 70 showSell(event) { 71 if (event) { 72 event.preventDefault() 73 } 74 75 this.activatePane("sell", { explicit: true }) 76 } 77 78 showSellFromLink(event) { 79 this.activatePane("sell", { explicit: true }) 80 } 81 82 openCardFromLink(event) { 83 event.preventDefault() 84 85 const url = event.currentTarget?.href 86 if (!url) return 87 88 this.openCardAtUrl(url) 89 } 90 91 openChatsFromLink(event) { 92 event.preventDefault() 93 const url = event.currentTarget?.href 94 if (!url) return 95 this.openChatsAtUrl(url) 96 } 97 98 handleSellFrameLoad() { 99 } 100 101 handleCardFrameLoad() { 102 this.activatePane("sell") 103 this.openCard() 104 } 105 106 handleChatsFrameLoad() { 107 this.activatePane("sell") 108 this.openChats() 109 } 110 111 handleBeforeStreamRender(event) { 112 const streamElement = event.target 113 if (!(streamElement instanceof HTMLElement)) return 114 115 const target = streamElement.getAttribute("target") 116 if (!["sellPage", "sellCard", "chatsCard"].includes(target)) return 117 118 const originalRender = event.detail.render 119 event.detail.render = (currentStreamElement) => { 120 originalRender(currentStreamElement) 121 if (target === "sellCard") { 122 this.activatePane("sell") 123 this.openCard() 124 } 125 if (target === "chatsCard") { 126 this.activatePane("sell") 127 this.openChats() 128 } 129 } 130 } 131 132 toggleMenu(event) { 133 event.preventDefault() 134 event.stopPropagation() 135 this.toggleDrawer(this.menuTarget) 136 } 137 138 closeMenu() { 139 if (!this.hasMenuTarget) return 140 141 this.menuTarget.classList.remove("open") 142 this.updateOverlayVisibility() 143 } 144 145 toggleCollectionsMenu(event) { 146 event.preventDefault() 147 event.stopPropagation() 148 if (!this.hasCollectionsMenuTarget) return 149 150 this.toggleDrawer(this.collectionsMenuTarget) 151 } 152 153 closeCollectionsMenu() { 154 if (!this.hasCollectionsMenuTarget) return 155 156 this.collectionsMenuTarget.classList.remove("open") 157 this.updateOverlayVisibility() 158 } 159 160 closeMenus() { 161 if (this.hasMenuTarget) { 162 this.menuTarget.classList.remove("open") 163 } 164 165 if (this.hasCollectionsMenuTarget) { 166 this.collectionsMenuTarget.classList.remove("open") 167 } 168 169 this.updateOverlayVisibility() 170 } 171 172 handleDocumentClick(event) { 173 if (!this.anyMenuOpen()) return 174 if (this.hasMenuTarget && this.menuTarget.contains(event.target)) return 175 if (this.hasCollectionsMenuTarget && this.collectionsMenuTarget.contains(event.target)) return 176 177 this.closeMenus() 178 } 179 180 dismissCard(event) { 181 event.preventDefault() 182 this.closeCard() 183 } 184 185 dismissChats(event) { 186 if (event) { 187 event.preventDefault() 188 } 189 this.closeChats() 190 } 191 192 closeCardAfterDelete() { 193 this.closeCard() 194 } 195 196 showImages() { 197 this.activatePane("images", { explicit: true }) 198 } 199 200 openCardForEvent(event) { 201 const url = event.detail?.url 202 const showLoading = event.detail?.showLoading !== false 203 const loadingText = event.detail?.loadingText 204 205 this.activatePane("sell", { explicit: true }) 206 this.openCardAtUrl(url, { showLoading, loadingText }) 207 } 208 209 activatePane(pane, options = {}) { 210 const normalizedPane = this.normalizePane(pane) 211 const snapActive = normalizedPane !== "sell" 212 const imagesActive = normalizedPane === "images" 213 const sellActive = normalizedPane === "sell" 214 215 this.element.classList.toggle("is-sell-active", sellActive) 216 this.element.classList.toggle("is-snap-active", snapActive) 217 this.element.classList.toggle("is-images-active", imagesActive) 218 219 if (this.hasSnapTabTarget) { 220 this.snapTabTarget.classList.toggle("active", !sellActive) 221 this.snapTabTarget.classList.toggle("inactive", sellActive) 222 } 223 224 if (this.hasSellTabTarget) { 225 this.sellTabTarget.classList.toggle("active", sellActive) 226 this.sellTabTarget.classList.toggle("inactive", !sellActive) 227 } 228 229 if (this.hasStartPageTarget) { 230 this.startPageTarget.classList.toggle("hidden", imagesActive) 231 this.startPageTarget.classList.toggle("visible", !imagesActive) 232 } 233 234 if (this.hasImagesPageTarget) { 235 this.imagesPageTarget.classList.toggle("hidden", !imagesActive) 236 this.imagesPageTarget.classList.toggle("visible", imagesActive) 237 } 238
239 if (this.hasSnapBackgroundTarget) { 240 this.snapBackgroundTarget.classList.toggle("hidden", sellActive || imagesActive) 241 } 242 243 if (sellActive) { 244 this.closeMenus() 245 } 246 247 const nextState = { pane: normalizedPane } 248 if (options.explicit) { 249 nextState.lastExplicitPane = normalizedPane 250 } 251 252 this.persistState(nextState) 253 } 254 255 openCard() { 256 if (!this.hasCardTarget) return 257 258 this.element.classList.add("is-card-open") 259 this.cardTarget.classList.add("is-open") 260 this.persistState({ cardOpen: true }) 261 } 262 263 closeCard() { 264 if (!this.hasCardTarget) return 265 266 this.element.classList.remove("is-card-open") 267 this.cardTarget.classList.remove("is-open") 268 this.persistState({ cardOpen: false }) 269 } 270 271 openChats() { 272 if (!this.hasChatsTarget) return 273 this.element.classList.add("is-chats-open") 274 this.chatsTarget.classList.add("is-open") 275 } 276 277 closeChats() { 278 if (!this.hasChatsTarget) return 279 this.element.classList.remove("is-chats-open") 280 this.chatsTarget.classList.remove("is-open") 281 this.persistState({}) 282 } 283 284 openChatsAtUrl(url, options = {}) { 285 const showLoading = options.showLoading !== false 286 const loadingText = options.loadingText || "Loadingâ¦" 287 if (showLoading) { 288 this.showChatsLoading(loadingText) 289 } 290 this.openChats() 291 this.loadChatsFrame(url) 292 } 293 294 showChatsLoading(message = "Loadingâ¦") { 295 if (!this.hasChatsFrameTarget) return 296 this.chatsFrameTarget.innerHTML = ` 297 <div class="sell-card-loading"> 298 <div class="sell-card-loading__spinner"></div> 299 <div class="sell-card-loading__text">${message}</div> 300 </div> 301 ` 302 } 303 304 loadChatsFrame(url) { 305 if (!this.hasChatsFrameTarget) return 306 if (!url) return 307 const requestUrl = new URL(url, window.location.origin) 308 requestUrl.searchParams.set("_chats_request_id", Date.now().toString()) 309 this.chatsFrameTarget.setAttribute("src", requestUrl.toString()) 310 } 311 312 showCardLoading(message = "Loading item details...") { 313 if (!this.hasCardFrameTarget) return 314 315 this.cardFrameTarget.innerHTML = ` 316 <div class="sell-card-loading"> 317 <div class="sell-card-loading__spinner"></div> 318 <div class="sell-card-loading__text">${message}</div> 319 </div> 320 ` 321 } 322 323 restoreShellState() { 324 const savedState = this.readState() 325 const hasServerChatsDeeplink = this.hasChatsDeeplinkValue && this.chatsDeeplinkValue && this.chatsDeeplinkValue.itemId 326 const restoredPane = this.shouldRestoreSavedState() && !hasServerChatsDeeplink 327 ? (savedState.cardOpen && savedState.cardUrl ? "sell" : savedState.lastExplicitPane) 328 : null 329 const initialPane = this.normalizePane(restoredPane || this.initialPaneValue) 330 331 this.activatePane(initialPane) 332 333 if (initialPane === "sell" && savedState.cardOpen && savedState.cardUrl && !hasServerChatsDeeplink) { 334 this.openCardAtUrl(savedState.cardUrl) 335 return 336 } 337 338 if (this.initialCardOpenValue) { 339 this.openCard() 340 } 341 } 342 343 maybeOpenChatsFromDeeplink() { 344 if (!this.hasChatsDeeplinkValue) { 345 return 346 } 347 const d = this.chatsDeeplinkValue 348 if (!d || !d.itemId) { 349 return 350 } 351 352 setTimeout(() => { 353 this.closeCard() 354 this.activatePane("sell", { explicit: true }) 355 const u = new URL("/app_v2/items/" + encodeURIComponent(d.itemId) + "/chats", window.location.origin) 356 if (d.threadId) { 357 u.searchParams.set("thread_id", d.threadId) 358 } 359 u.searchParams.set("_chats_request_id", String(Date.now())) 360 this.openChatsAtUrl(u.toString(), { showLoading: true }) 361 this.clearOpenChatsQueryParams() 362 }, 0) 363 } 364 365 clearOpenChatsQueryParams() { 366 const sp = new URLSearchParams(window.location.search) 367 if (!sp.has("open_chats")) { 368 return 369 } 370 const c = new URL(window.location) 371 c.searchParams.delete("open_chats") 372 c.searchParams.delete("item_id") 373 c.searchParams.delete("thread_id") 374 c.searchParams.delete("collection_id") 375 c.searchParams.delete("restore_pane") 376 const q = c.searchParams.toString() 377 const next = c.pathname + (q ? "?" + q : "") 378 window.history.replaceState(window.history.state, "", next) 379 } 380 381 openCardAtUrl(url, options = {}) { 382 const showLoading = options.showLoading !== false 383 const loadingText = options.loadingText || "Loading item details..." 384 385 if (showLoading) { 386 this.showCardLoading(loadingText) 387 } 388 389 this.openCard() 390 391 if (!url) return 392 393 this.loadCardFrame(url) 394 } 395 396 loadCardFrame(url) { 397 if (!this.hasCardFrameTarget) return 398 399 this.persistState({ cardUrl: url, cardOpen: true }) 400 401 // Always force a fresh request when opening a sell card. 402 // This avoids stale frame reuse when rapidly opening different items. 403 const requestUrl = new URL(url, window.location.origin) 404 requestUrl.searchParams.set("_card_request_id", Date.now().toString()) 405 this.cardFrameTarget.setAttribute("src", requestUrl.toString()) 406 } 407 408 toggleDrawer(target) { 409 if (!target) return 410 411 const shouldOpen = !target.classList.contains("open") 412 this.closeMenus() 413 414 if (shouldOpen) { 415 target.classList.add("open") 416 } 417 418 this.updateOverlayVisibility() 419 } 420 421 anyMenuOpen() { 422 const leftOpen = this.hasMenuTarget && this.menuTarget.classList.contains("open") 423 const rightOpen = this.hasCollectionsMenuTarget && this.collectionsMenuTarget.classList.contains("open") 424 425 return leftOpen || rightOpen 426 } 427 428 updateOverlayVisibility() { 429 if (!this.hasOverlayTarget) return 430 431 this.overlayTarget.classList.toggle("is-visible", this.anyMenuOpen()) 432 } 433 434 normalizePane(pane) { 435 return ["snap", "sell", "images"].includes(pane) ? pane : "snap" 436 } 437 438 normalizeBrowserUrl() { 439 if (!this.hasRootUrlValue) return 440 if (window.location.pathname === this.rootUrlValue) return 441 442 window.history.replaceState(window.history.state, "", this.rootUrlValue) 443 } 444 445 shouldRestoreSavedState() { 446 const navigationEntry = window.performance?.getEntriesByType?.("navigation")?.[0] 447 const navigationType = navigationEntry?.type 448 449 return navigationType === "reload" || navigationType === "back_forward" 450 } 451 452 readState() { 453 try { 454 const rawState = window.sessionStorage.getItem(APP_SHELL_STATE_KEY) 455 return rawState ? JSON.parse(rawState) : {} 456 } catch (error) { 457 console.warn("Unable to restore app shell state", error) 458 return {} 459 } 460 } 461 462 persistState(attributes = {}) { 463 try { 464 const nextState = { 465 ...this.readState(), 466 ...attributes 467 } 468 469 window.sessionStorage.setItem(APP_SHELL_STATE_KEY, JSON.stringify(nextState)) 470 } catch (error) { 471 console.warn("Unable to persist app shell state", error) 472 } 473 } 474}
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.