PageSourceSearch

https://www.theengineeringprojects.com/res/js/content/base/product.wi0j9e9pnd.js

js theengineeringprojects.com collected 2026-10-02 03:56:37 UTC 14,208 bytes, 360 lines download raw bytes

1/* Product interactions share the locked Shop's browser cart and saved products. */
2(() => {
3  "use strict";
4  const $ = (selector) => document.querySelector(selector);
5  const $$ = (selector) => [...document.querySelectorAll(selector)];
6  const core = globalThis.TEPProductCore,
7    shop = globalThis.TEPShopCore;
8  const data = JSON.parse($("#product-data").textContent);
9  const previewProducts = [...data.catalog.products];
10  const catalogNode = $("#shop-catalog-server-data");
11  if (catalogNode?.textContent.trim()) {
12    try { data.catalog = globalThis.TEPShopCatalog.merge(data.catalog, JSON.parse(catalogNode.textContent)); }
13    catch { /* Retain the design catalog if server data is unavailable. */ }
14  }
15  const serverDataNode = $("#product-server-data");
16  if (serverDataNode?.textContent.trim()) {
17    try {
18      const serverData = JSON.parse(serverDataNode.textContent);
19      const live = serverData?.product;
20      if (live?.id) {
21        const templates = previewProducts;
22        const sum = Array.from(String(live.id)).reduce(
23          (total, character) => total + character.codePointAt(0),
24          0,
25        );
26        const template =
27          templates.find(item => item.id === live.id) ||
28          templates[sum % templates.length] ||
29          templates.find((item) => item.id === data.defaultProduct) ||
30          templates[0];
31        const templateDetails = data.details[template.id] || {};
32        const category = {
33          id: live.category || template.category,
34          name: live.categoryName || live.category || "Uncategorized",
35          icon: "box",
36          tone: "cyan",
37        };
38        const existingCategory = data.catalog.categories.find(
39          (item) => item.id === category.id,
40        );
41        if (existingCategory) existingCategory.name = category.name;
42        else data.catalog.categories.push(category);
43
44        const liveProduct = {
45          ...template,
46          ...live,
47          ...live.commerce,
48          category: category.id,
49          description: live.description || template.description,
50          tags:
51            Array.isArray(live.tags) && live.tags.length ? live.tags : template.tags,
52        };
53        const serverDetails = serverData.details || {};
54        data.catalog.products = [
55          liveProduct,
56          ...data.catalog.products.filter((item) => item.id !== liveProduct.id),
57        ];
58        data.defaultProduct = liveProduct.id;
59        data.details[liveProduct.id] = {
60          ...templateDetails,
61          ...serverDetails,
62          intro: serverDetails.intro || templateDetails.intro,
63          subtitle: serverDetails.subtitle || templateDetails.subtitle,
64          sku: serverDetails.sku ?? templateDetails.sku,
65          specs:
66            Array.isArray(serverDetails.specs)
67              ? serverDetails.specs
68              : templateDetails.specs,
69          included:
70            Array.isArray(serverDetails.included) && serverDetails.included.length
71              ? serverDetails.included
72              : templateDetails.included,
73          gallery:
74            serverDetails.gallery?.length > 0
75              ? serverDetails.gallery
76              : templateDetails.gallery,
77        };
78      }
79    } catch {
80      // Keep the approved preview data if a server payload is unavailable.
81    }
82  }
83  const pageUrl = new URL(location.href),
84    params = pageUrl.searchParams,
85    pathProduct = pageUrl.pathname.match(/^\/shop\/product\/([^/]+)\/?$/i)?.[1];
86  const product = core.get(
87    data,
88    pathProduct
89      ? decodeURIComponent(pathProduct)
90      : params.has("id")
91        ? params.get("id")
92        : data.defaultProduct,
93  );
94  const cartKey = "tep.shop.cart.v1",
95    savedKey = "tep.shop.saved.v1";
96  function read(key) {
97    try {
98      return JSON.parse(localStorage.getItem(key));
99    } catch {
100      return null;
101    }
102  }
103  function store(key, value) {
104    try {
105      localStorage.setItem(key, JSON.stringify(value));
106      return true;
107    } catch {
108      return false;
109    }
110  }
111  let cart = shop.cartFromLocation(data.catalog, read(cartKey));
112  let saved = read(savedKey);
113  saved = Array.isArray(saved) ? [...new Set(saved.filter((id) => core.get(data, id)))] : [];
114  $("#product-page").innerHTML = core.pageHtml(data, product, saved);
115  if (!product) return;
116  if (!serverDataNode) document.title = `${product.title} — TEP Shop`;
117
118  const images = core.gallery(product, data.details[product.id]);
119  const quantity = $("#product-quantity"),
120    dialog = $("#product-dialog");
121  let imageIndex = 0,
122    mode = "",
123    returnFocus = null,
124    timer;
125  function toast(message) {
126    clearTimeout(timer);
127    $("#product-toast").textContent = message;
128    $("#product-toast").hidden = false;
129    timer = setTimeout(() => {
130      $("#product-toast").hidden = true;
131    }, 4500);
132  }
133  function status(message) {
134    $("#product-buy-status").textContent = message;
135    $("#product-buy-status").hidden = !message;
136  }
137  function validQuantity() {
138    const value = Number(quantity.value);
139    return Number.isInteger(value) && value >= 1 && value <= shop.maxQuantity(product) ? value : null;
140  }
141  function updateQuantity() {
142    if (!product.stock) {
143      quantity.setAttribute("aria-invalid", "false");
144      status("This product is out of stock.");
145      return;
146    }
147    const value = validQuantity();
148    quantity.setAttribute("aria-invalid", String(value === null));
149    $("#product-total").textContent =
150      value === null ? "—" : shop.money((Math.round(product.price * 100) * value) / 100);
151    $("#product-decrease").disabled = !product.stock || value === null || value <= 1;
152    $("#product-increase").disabled = !product.stock || value === null || value >= shop.maxQuantity(product);
153    status(value === null ? `Enter a whole quantity from 1 to ${shop.maxQuantity(product)}.` : "");
154  }
155  function counts() {
156    $("#product-cart-count").textContent = cart.reduce((n, item) => n + item.qty, 0);
157  }
158  function open(modeName, title, content) {
159    mode = modeName;
160    returnFocus = document.activeElement;
161    $("#product-dialog-title").textContent = title;
162    $("#product-dialog-content").innerHTML = content;
163    dialog.classList.toggle("shop-dialog--cart", mode === "cart");
164    dialog.classList.toggle("product-dialog--image", mode === "image");
165    dialog.showModal();
166    $("#product-dialog-close").focus();
167  }
168  function close() {
169    dialog.close();
170  }
171  dialog.addEventListener("close", () => {
172    mode = "";
173    if (returnFocus?.isConnected) returnFocus.focus();
174    else $("#product-cart-open").focus();
175  });
176  dialog.addEventListener("click", (event) => {
177    if (event.target === dialog) close();
178  });
179  $("#product-dialog-close").addEventListener("click", close);
180  $("#product-cart-open").addEventListener("click", () =>
181    open("cart", "Your cart", shop.cartHtml(data.catalog, cart)),
182  );
183  function persistCart(focusSelector) {
184    const stored = store(cartKey, cart);
185    counts();
186    if (mode === "cart") {
187      $("#product-dialog-content").innerHTML = shop.cartHtml(data.catalog, cart);
188      ($(focusSelector || "#product-dialog-close") || $("#product-dialog-close")).focus();
189    }
190    if (!stored) toast("Cart updated for this visit. Browser saving is unavailable.");
191    return stored;
192  }
193  function add(id, amount) {
194    const itemProduct = core.get(data, id);
195    if (!itemProduct?.stock) return;
196    const existing = cart.find((item) => item.id === id),
197      previous = existing?.qty || 0;
198    if (previous + amount > shop.maxQuantity(itemProduct)) {
199      const message = `Your cart already has ${previous}. The available limit is ${shop.maxQuantity(itemProduct)}.`;
200      if (id === product.id) status(message);
201      toast(message);
202      return;
203    }
204    if (existing) existing.qty += amount;
205    else cart.push({ id, qty: amount });
206    const stored = persistCart();
207    const message =
208      `${amount} × ${itemProduct.title} added to cart.` +
209      (stored ? "" : " Saved for this visit only.");
210    if (id === product.id) status(message);
211    toast(message);
212  }
213  quantity.addEventListener("input", updateQuantity);
214  $("#product-decrease").addEventListener("click", () => {
215    const value = validQuantity();
216    if (value !== null) {
217      quantity.value = Math.max(1, value - 1);
218      updateQuantity();
219    }
220  });
221  $("#product-increase").addEventListener("click", () => {
222    const value = validQuantity();
223    if (value !== null) {
224      quantity.value = Math.min(shop.maxQuantity(product), value + 1);
225      updateQuantity();
226    }
227  });
228  $("#product-buy-form").addEventListener("submit", (event) => {
229    event.preventDefault();
230    if (!product.stock) return;
231    const value = validQuantity();
232    if (value === null) {
233      updateQuantity();
234      quantity.focus();
235      return;
236    }
237    add(product.id, value);
238  });
239  function selectImage(index) {
240    if (!Number.isInteger(index) || index < 0 || index >= images.length) return;
241    imageIndex = index;
242    const selected = images[index];
243    $("#product-image").closest("picture").outerHTML = core.picture(
244      data,
245      selected.link ? selected : selected.image,
246      product.title + " — " + selected.label,
247    );
248    $("#product-image-caption").textContent = selected.label + " · Product illustration";
249    $$("[data-gallery]").forEach((button) =>
250      button.setAttribute("aria-pressed", String(Number(button.dataset.gallery) === index)),
251    );
252  }
253  $("#product-zoom").addEventListener("click", () => {
254    const selected = images[imageIndex];
255    open(
256      "image",
257      product.title,
258      `<figure class="product-zoom-view">${core.picture(data, selected.link ? selected : selected.image, product.title + " — " + selected.label, "zoom")}<figcaption>${core.esc(selected.label)} · Product illustration</figcaption></figure>`,
259    );
260  });
261  function toggleSaved(id) {
262    if (!core.get(data, id)) return;
263    const wasSaved = saved.includes(id);
264    saved = wasSaved ? saved.filter((value) => value !== id) : [...saved, id];
265    const stored = store(savedKey, saved);
266    $("#product-save").setAttribute("aria-pressed", String(saved.includes(product.id)));
267    $("#product-save span").textContent = saved.includes(product.id) ? "Saved" : "Save product";
268    $$("[data-save]").forEach((button) => {
269      const active = saved.includes(button.dataset.save);
270      button.setAttribute("aria-pressed", String(active));
271      button.setAttribute(
272        "aria-label",
273        (active ? "Unsave " : "Save ") + core.get(data, button.dataset.save).title,
274      );
275    });
276    toast(
277      (wasSaved ? "Removed from saved products." : "Product saved.") +
278        (stored ? "" : " Saved for this visit only."),
279    );
280  }
281  $("#product-save").addEventListener("click", () => toggleSaved(product.id));
282  function selectTab(name, focus = false) {
283    const selected = $(`[data-tab="${name}"]`);
284    if (!selected) return;
285    $$("[data-tab]").forEach((tab) => {
286      const active = tab === selected;
287      tab.setAttribute("aria-selected", String(active));
288      tab.tabIndex = active ? 0 : -1;
289      $("#" + tab.getAttribute("aria-controls")).hidden = !active;
290    });
291    if (focus) selected.focus();
292  }
293  $(".product-tabs").addEventListener("keydown", (event) => {
294    const tabs = $$("[data-tab]"),
295      current = tabs.indexOf(event.target);
296    if (current < 0) return;
297    let index;
298    if (event.key === "ArrowRight") index = (current + 1) % tabs.length;
299    else if (event.key === "ArrowLeft") index = (current + tabs.length - 1) % tabs.length;
300    else if (event.key === "Home") index = 0;
301    else if (event.key === "End") index = tabs.length - 1;
302    else return;
303    event.preventDefault();
304    selectTab(tabs[index].dataset.tab, true);
305  });
306  $("#product-share").addEventListener("click", async () => {
307    const link = new URL(location.href);
308    link.searchParams.set("id", product.id);
309    link.hash = "";
310    if (link.protocol === "https:" || link.protocol === "http:") {
311      try {
312        await navigator.clipboard.writeText(link.href);
313        toast("Product link copied.");
314        return;
315      } catch {
316        /* Show a manual copy field. */
317      }
318    }
319    const local = link.protocol === "file:";
320    open(
321      "share",
322      "Share this product",
323      `<div class="product-share-panel"><p>${local ? "This preview is a local file. Share the preview ZIP so someone else can open the product." : "Copy the product link below."}</p><label for="product-share-link">${local ? "Local preview address" : "Product link"}</label><input id="product-share-link" type="text" readonly value="${core.esc(link.href)}"></div>`,
324    );
325    $("#product-share-link").focus();
326    $("#product-share-link").select();
327  });
328  $("#product-main").addEventListener("click", (event) => {
329    const reviewLink = event.target.closest("[data-select-tab]");
330    if (reviewLink) {
331      event.preventDefault();
332      selectTab(reviewLink.dataset.selectTab, true);
333      $("#product-information").scrollIntoView({
334        block: "start",
335        behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth",
336      });
337      return;
338    }
339    const button = event.target.closest("button");
340    if (!button || button.disabled) return;
341    if (button.hasAttribute("data-gallery")) selectImage(Number(button.dataset.gallery));
342    if (button.hasAttribute("data-tab")) selectTab(button.dataset.tab);
343    if (button.hasAttribute("data-save")) toggleSaved(button.dataset.save);
344    if (button.hasAttribute("data-add")) add(button.dataset.add, 1);
345    if (button.hasAttribute("data-qty")) {
346      const item = cart.find((value) => value.id === button.dataset.qty);
347      if (!item) return;
348      item.qty = Math.max(1, Math.min(shop.maxQuantity(core.get(data, item.id)), item.qty + Number(button.dataset.delta)));
349      persistCart(`[data-qty="${item.id}"][data-delta="${button.dataset.delta}"]:not(:disabled)`);
350    }
351    if (button.hasAttribute("data-remove")) {
352      cart = cart.filter((item) => item.id !== button.dataset.remove);
353      persistCart("[data-qty]");
354    }
355    if (button.hasAttribute("data-close")) close();
356  });
357  shop.bindCartLinks(() => cart);
358  updateQuantity();
359  counts();
360})();

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.