1/** 2 * Clientsites V3 room planner bootloader. 3 * Lazy-loads the Vue/Konva UMD and exposes window.RoomPlannerV3Launcher. 4 * V3 is the default entry; the in-app banner lets a user switch back to classic V2. 5 */ 6(function () { 7 var PLAN_YOUR_ROOM_DEALER_ID = 28592; 8 var UMD_PATH_SUFFIX = "/js/apps/room-planner/room-planner.umd.js"; 9 var CSS_PATH_SUFFIX = "/js/apps/room-planner/room-planner.css"; 10 var ASSET_VERSION = "202609171410"; 11 var ENGINE_PREF_KEY = "fdn-room-planner-engine"; 12 var scriptsLoaded = false; 13 var isCurrentlyLoading = false; 14 var initPromise = null; 15 var openPromise = null; 16 // In-memory only so dismiss sticks for this page load (timers won't re-show), 17 // but the prompt returns on the next navigation. 18 var classicBannerDismissed = false; 19 var classicBannerTimer = null; 20 21 function isV3Enabled() { 22 return !(window.__FDN_ROOM_PLANNER__ && window.__FDN_ROOM_PLANNER__.v3Enabled === false); 23 } 24 25 function isEngineSwitchBannerEnabled() { 26 var cfg = window.__FDN_ROOM_PLANNER__ || {}; 27 if (typeof cfg.tryV3BannerEnabled === "boolean") { 28 return cfg.tryV3BannerEnabled; 29 } 30 return isV3Enabled(); 31 } 32 33 function getBaseUrl() { 34 return (window.Utils && (Utils.baseUrl || Utils.BaseURL)) || "/"; 35 } 36 37 function getApiBaseUrl() { 38 var fromUtils = window.Utils && Utils.roomPlannerApiBaseUrl; 39 if (fromUtils) { 40 return String(fromUtils).replace(/\/$/, ""); 41 } 42 return "https://api.furnituredealer.net/v2"; 43 } 44 45 function getDealerId() { 46 var fromUtils = window.Utils && Utils.dealerId; 47 if (fromUtils != null && fromUtils !== "") { 48 var parsed = Number(fromUtils); 49 if (Number.isFinite(parsed)) { 50 return parsed; 51 } 52 } 53 if (window.udata && udata.DealerId != null) { 54 var fromUdata = Number(udata.DealerId); 55 if (Number.isFinite(fromUdata)) { 56 return fromUdata; 57 } 58 } 59 return 0; 60 } 61 62 function isPlanYourRoom(dealerId) { 63 return dealerId === PLAN_YOUR_ROOM_DEALER_ID; 64 } 65 66 function readEnginePreference() { 67 try { 68 var value = (localStorage.getItem(ENGINE_PREF_KEY) || "").toLowerCase(); 69 return value === "v2" || value === "v3" ? value : ""; 70 } catch (e) { 71 return ""; 72 } 73 } 74 75 function writeEnginePreference(engine) { 76 try { 77 if (engine) { 78 localStorage.setItem(ENGINE_PREF_KEY, engine); 79 } else { 80 localStorage.removeItem(ENGINE_PREF_KEY); 81 } 82 } catch (e) { 83 // ignore 84 } 85 } 86 87 // Shared with v2/init.js, which owns the site triggers and routes to an engine. 88 window.FdnRoomPlannerEngine = { 89 defaultEngine: "v3", 90 get: readEnginePreference, 91 set: writeEnginePreference 92 }; 93 94 function buildLoginUrl() { 95 var base = getBaseUrl(); 96 if (base.slice(-1) !== "/") { 97 base += "/"; 98 } 99 100 // Match V2: return from login should reopen the planner on V3. 101 try { 102 sessionStorage.setItem("fdn-room-planner-reopen-v3", "1"); 103 } catch (e) { 104 // ignore 105 } 106 107 var returnUrl = new URL(window.location.href); 108 returnUrl.searchParams.delete("autoOpenRoomPlanner"); 109 returnUrl.searchParams.delete("rp"); 110 returnUrl.searchParams.delete("rpEngine"); 111 returnUrl.searchParams.set("autoOpenRoomPlanner", "true"); 112 returnUrl.searchParams.set("rpEngine", "v3"); 113 114 return ( 115 base + 116 "login?returnUrl=" + 117 encodeURIComponent(returnUrl.pathname + returnUrl.search) 118 ); 119 } 120 121 function updateRoomPlannerHeaderOffset() { 122 var header = document.querySelector("header"); 123 var headerHeight = header ? Math.ceil(header.getBoundingClientRect().height) : 0; 124 if (headerHeight > 0) { 125 document.documentElement.style.setProperty("--room-planner-header-height", headerHeight + "px"); 126 } 127 } 128 129 function buildHostConfig() { 130 var dealerId = getDealerId();
131 var pyr = isPlanYourRoom(dealerId); 132 var isLoggedIn = !!(window.udata && udata.IsAuthenticated); 133 var accountUserId = window.udata && Number(udata.UserId) > 0 ? Number(udata.UserId) : 0; 134 var globalUserId = (window.udata && udata.GlobalUserId) ? String(udata.GlobalUserId) : ""; 135 136 if (pyr) { 137 updateRoomPlannerHeaderOffset(); 138 } 139 140 return { 141 platform: "clientsites", 142 apiBaseUrl: getApiBaseUrl(), 143 dealerId: dealerId, 144 auth: { 145 isLoggedIn: isLoggedIn, 146 globalUserId: globalUserId || undefined, 147 accountUserId: accountUserId > 0 ? accountUserId : undefined, 148 loginUrl: buildLoginUrl() 149 }, 150 features: { 151 wishlist: !pyr, 152 closeButton: !pyr 153 }, 154 layout: pyr 155 ? { 156 topOffset: "var(--room-planner-header-height, 0px)", 157 rightOffset: "var(--room-planner-right-ad-width, 0px)" 158 } 159 : undefined 160 }; 161 } 162 163 function getApi() { 164 return window.RoomPlanner && window.RoomPlanner.RoomPlanner; 165 } 166 167 function findAssetBySuffix(tagName, attr, suffix) { 168 var nodes = document.getElementsByTagName(tagName); 169 for (var i = 0; i < nodes.length; i++) { 170 var value = nodes[i].getAttribute(attr) || nodes[i][attr] || ""; 171 var path = String(value).split("?")[0]; 172 if (path.indexOf(suffix) !== -1) { 173 return nodes[i]; 174 } 175 } 176 return null; 177 } 178 179 function loadCss(href) { 180 return new Promise(function (resolve, reject) { 181 if (findAssetBySuffix("link", "href", CSS_PATH_SUFFIX)) { 182 resolve(href); 183 return; 184 } 185 186 var link = document.createElement("link"); 187 link.rel = "stylesheet"; 188 link.href = href; 189 link.onload = function () { resolve(href); }; 190 link.onerror = function () { reject(new Error("Failed to load CSS " + href)); }; 191 document.head.appendChild(link); 192 }); 193 } 194 195 function loadScript(src) { 196 return new Promise(function (resolve, reject) { 197 // Page may have already loaded the UMD (e.g. test host with asp-append-version). 198 if (getApi()) { 199 resolve(src); 200 return; 201 } 202 203 var existing = findAssetBySuffix("script", "src", UMD_PATH_SUFFIX); 204 if (existing) { 205 if (getApi()) { 206 resolve(src); 207 return; 208 } 209 existing.addEventListener("load", function () { resolve(src); }); 210 existing.addEventListener("error", function () { 211 reject(new Error("Failed to load " + src)); 212 }); 213 return; 214 } 215 216 var script = document.createElement("script"); 217 script.src = src; 218 script.async = false; 219 script.onload = function () { resolve(src); }; 220 script.onerror = function () { reject(new Error("Failed to load " + src)); }; 221 document.head.appendChild(script); 222 }); 223 } 224 225 function ensureInitialized() { 226 if (!isV3Enabled()) { 227 return Promise.reject(new Error("Room Planner V3 is disabled on this page.")); 228 } 229 230 if (initPromise) { 231 return initPromise; 232 } 233 234 initPromise = (async function () { 235 var existingApi = getApi(); 236 if (!existingApi && !scriptsLoaded) { 237 isCurrentlyLoading = true; 238 try { 239 var baseUrl = getBaseUrl(); 240 await loadCss(baseUrl + "js/apps/room-planner/room-planner.css?v=" + ASSET_VERSION); 241 await loadScript(baseUrl + "js/apps/room-planner/room-planner.umd.js?v=" + ASSET_VERSION); 242 scriptsLoaded = true; 243 } finally { 244 isCurrentlyLoading = false; 245 } 246 } else { 247 scriptsLoaded = true; 248 } 249 250 var api = getApi(); 251 if (!api) { 252 throw new Error("RoomPlanner V3 bundle did not expose RoomPlanner.RoomPlanner"); 253 } 254 255 if (!api.__clientsitesHostInit) { 256 api.init(buildHostConfig()); 257 api.__clientsitesHostInit = true; 258 } 259 260 return api; 261 })(); 262 263 return initPromise; 264 } 265 266 function prepareHostChrome() { 267 if (isPlanYourRoom(getDealerId())) { 268 updateRoomPlannerHeaderOffset(); 269 if (window.PyrRoomPlannerAdRail && typeof PyrRoomPlannerAdRail.sync === "function") { 270 PyrRoomPlannerAdRail.sync(); 271 } 272 } 273 } 274 275 function dismissClassicBanner(banner) { 276 classicBannerDismissed = true; 277 if (classicBannerTimer) { 278 clearTimeout(classicBannerTimer); 279 classicBannerTimer = null; 280 } 281 if (banner && banner.parentNode) { 282 banner.remove(); 283 } 284 } 285 286 function ensureClassicBannerStyles() { 287 if (document.getElementById("fdn-use-classic-banner-styles")) { 288 return; 289 } 290 var style = document.createElement("style"); 291 style.id = "fdn-use-classic-banner-styles"; 292 style.textContent = 293 ".fdn-use-classic-banner{position:absolute;
293left:50%;bottom:12px;transform:translateX(-50%);z-index:200;" + 294 "display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;max-width:min(560px,calc(100% - 24px));" + 295 "padding:.65rem .9rem;background:#1f2937;color:#f8fafc;border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,.28);" + 296 "font:500 13px/1.35 system-ui,-apple-system,Segoe UI,sans-serif}" + 297 ".fdn-use-classic-banner__text{flex:1 1 180px}" + 298 ".fdn-use-classic-banner__actions{display:flex;gap:.4rem;align-items:center}" + 299 ".fdn-use-classic-banner button{border:0;border-radius:6px;cursor:pointer;padding:.35rem .7rem;font:600 12px/1.2 inherit}" + 300 ".fdn-use-classic-banner__switch{background:#4b89dc;color:#fff}" + 301 ".fdn-use-classic-banner__dismiss{background:transparent;color:#cbd5e1}"; 302 document.head.appendChild(style); 303 } 304 305 async function switchToV2() { 306 writeEnginePreference("v2"); 307 await close(); 308 309 if (window.RoomPlannerV2Launcher && typeof RoomPlannerV2Launcher.openClassic === "function") { 310 RoomPlannerV2Launcher.openClassic(); 311 return; 312 } 313 314 if (window.RoomPlannerV2Launcher && typeof RoomPlannerV2Launcher.open === "function") { 315 RoomPlannerV2Launcher.open(); 316 return; 317 } 318 319 console.error("RoomPlannerV2Launcher is not available."); 320 } 321 322 function ensureClassicBanner() { 323 if (!isEngineSwitchBannerEnabled() || classicBannerDismissed) { 324 return; 325 } 326 327 var host = document.querySelector(".fdn-room-planner-host"); 328 if (!host || host.querySelector(".fdn-use-classic-banner")) { 329 return; 330 } 331 332 ensureClassicBannerStyles(); 333 334 var banner = document.createElement("div"); 335 banner.className = "fdn-use-classic-banner"; 336 banner.setAttribute("role", "status"); 337 banner.innerHTML = 338 '<div class="fdn-use-classic-banner__text">This is the new room planner. Prefer the classic version?</div>' + 339 '<div class="fdn-use-classic-banner__actions">' + 340 '<button type="button" class="fdn-use-classic-banner__switch">Use classic planner</button>' + 341 '<button type="button" class="fdn-use-classic-banner__dismiss">Dismiss</button>' + 342 "</div>"; 343 344 banner.querySelector(".fdn-use-classic-banner__switch").addEventListener("click", function () { 345 switchToV2().catch(function (err) { 346 console.error(err); 347 }); 348 }); 349 banner.querySelector(".fdn-use-classic-banner__dismiss").addEventListener("click", function () { 350 dismissClassicBanner(banner); 351 }); 352 353 host.appendChild(banner); 354 } 355 356 function scheduleClassicBanner() { 357 if (classicBannerDismissed) { 358 return; 359 } 360 361 if (classicBannerTimer) { 362 clearTimeout(classicBannerTimer); 363 } 364 365 classicBannerTimer = setTimeout(function () { 366 classicBannerTimer = null; 367 ensureClassicBanner(); 368 }, 150); 369 } 370 371 /** Match V2 launcher options vs add-item: only add when a product id is present. */ 372 function isAddItemPayload(data) { 373 if (!data || typeof data !== "object") { 374 return false; 375 } 376 377 var gpid = Number(data.gpid || data.Gpid || data.GPID || 0); 378 if (Number.isFinite(gpid) && gpid > 0) { 379 return true; 380 } 381 382 var masterId = data.masterProductRecordId || data.MasterProductRecordId || data.mprId || ""; 383 return Boolean(String(masterId).trim()); 384 } 385 386 async function open(data) { 387 if (openPromise) { 388 await openPromise; 389 if (!data) { 390 return; 391 } 392 } 393 394 openPromise = (async function () { 395 var api = await ensureInitialized(); 396 397 prepareHostChrome(); 398 399 if (isAddItemPayload(data)) { 400 await api.addItem(data); 401 scheduleClassicBanner(); 402 return; 403 } 404 405 api.open(); 406 scheduleClassicBanner(); 407 })(); 408 409 try { 410 await openPromise; 411 } finally { 412 openPromise = null; 413 } 414 } 415 416 async function openShared(shareId) { 417 var api = await ensureInitialized(); 418 prepareHostChrome(); 419 await api.openShared(shareId); 420 scheduleClassicBanner(); 421 } 422 423 /** 424 * Opens an account plan by id. Legacy (v2) ids are converted and saved as a brand-new 425 * v3 plan, leaving the original plan untouched in the classic planner. 426 */ 427 async function openPlan(planId) { 428 var api = await ensureInitialized(); 429 prepareHostChrome();
430 await api.loadPlan(String(planId)); 431 scheduleClassicBanner(); 432 } 433 434 async function close() { 435 var api = getApi(); 436 if (api && typeof api.close === "function") { 437 api.close(); 438 } 439 } 440 441 window.RoomPlannerV3Launcher = { 442 open: function (data) { 443 open(data).catch(function (err) { 444 console.error(err); 445 }); 446 }, 447 openShared: function (shareId) { 448 openShared(shareId).catch(function (err) { 449 console.error(err); 450 }); 451 }, 452 openPlan: function (planId) { 453 return openPlan(planId).catch(function (err) { 454 console.error(err); 455 window.alert("Unable to open this plan in the new room planner."); 456 }); 457 }, 458 close: function () { 459 close().catch(function (err) { 460 console.error(err); 461 }); 462 }, 463 ensureInitialized: ensureInitialized, 464 isLoading: function () { 465 return isCurrentlyLoading; 466 } 467 }; 468})();
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.