PageSourceSearch

https://www.bullardfurniture.com/components/roomplanner/v3/init.js…wXnVsp6kt6meHNtoFsYMwHE5yzMg3lKKNkk5wehrwxU

js bullardfurniture.com collected 2026-09-26 11:41:17 UTC 16,140 bytes, 468 lines download raw bytes

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.