1import { block, unblock } from "./helpers.js"; 2 3let cartRequestInFlight = false; 4let basketLoadingCount = 0; 5 6const stockErrorTimers = new WeakMap(); 7 8const getBasketRoot = () => document.getElementById("photohuman-basket"); 9 10const getWcAjaxUrl = () => getBasketRoot()?.dataset.wcAjaxUrl || ""; 11 12const getStockErrorFromCounter = (counter) => { 13 const host = counter?.closest(".product-footer__box, .basket-product__box"); 14 15 return host?.querySelector(".photohuman-catalog-stock-error") || null; 16}; 17 18const numDecline = (number, titles) => { 19 const forms = Array.isArray(titles) ? titles : String(titles).split(/,\s*/); 20 21 if (!forms[2]) { 22 forms[2] = forms[1]; 23 } 24 25 const intnum = Math.abs(Number(number) || 0); 26 const cases = [2, 0, 1, 1, 1, 2]; 27 const titleIndex = intnum % 100 > 4 && intnum % 100 < 20 ? 2 : cases[Math.min(intnum % 10, 5)]; 28 29 return `${intnum} ${forms[titleIndex]}`; 30}; 31 32const getStockLimitMessage = (counter) => { 33 const template = getBasketRoot()?.dataset.stockLimitMessage || "ÐоÑÑÑпнÑй Ð»Ð¸Ð¼Ð¸Ñ Ð½Ð° ÑÑÐ¾Ñ ÑÐ¾Ð²Ð°Ñ â %s. ÐолÑÑе добавиÑÑ Ð½ÐµÐ»ÑзÑ."; 34 const stockQty = counter?.dataset.stockQty ? Number(counter.dataset.stockQty) : 0;
35 const qtyLabel = numDecline(stockQty, "ÑкземплÑÑ, ÑкземплÑÑа, ÑкземплÑÑов"); 36 37 return template.replace("%s", qtyLabel); 38}; 39 40const hideStockError = (element, immediate = false) => { 41 if (!element) { 42 return; 43 } 44 45 const timer = stockErrorTimers.get(element); 46 47 if (timer) { 48 window.clearTimeout(timer); 49 stockErrorTimers.delete(element); 50 } 51 52 element.classList.remove("_is-visible"); 53 54 if (immediate) { 55 element.classList.add("_hide"); 56 element.textContent = ""; 57 return; 58 } 59 60 const finishHide = () => { 61 element.classList.add("_hide"); 62 element.textContent = ""; 63 }; 64 65 let finished = false; 66 const onTransitionEnd = (event) => { 67 if (finished || event.target !== element || event.propertyName !== "transform") { 68 return; 69 } 70 71 finished = true; 72 element.removeEventListener("transitionend", onTransitionEnd); 73 finishHide(); 74 }; 75 76 element.addEventListener("transitionend", onTransitionEnd); 77 window.setTimeout(() => { 78 if (!finished) { 79 finished = true; 80 element.removeEventListener("transitionend", onTransitionEnd); 81 finishHide(); 82 } 83 }, 400); 84}; 85 86const clearOtherStockErrors = (activeTarget) => { 87 getBasketRoot()?.querySelectorAll(".photohuman-catalog-stock-error").forEach((element) => { 88 if (element !== activeTarget) { 89 hideStockError(element, true); 90 } 91 }); 92}; 93 94const showStockError = (counter) => { 95 const element = getStockErrorFromCounter(counter); 96 97 if (!element) { 98 return; 99 } 100 101 clearOtherStockErrors(element); 102 hideStockError(element, true); 103 element.textContent = getStockLimitMessage(counter); 104 element.classList.remove("_hide"); 105 106 window.requestAnimationFrame(() => { 107 window.requestAnimationFrame(() => { 108 element.classList.add("_is-visible"); 109 }); 110 }); 111 112 const timer = window.setTimeout(() => { 113 hideStockError(element); 114 }, 4000); 115 116 stockErrorTimers.set(element, timer); 117}; 118 119const isStockLimitResponse = (data) => { 120 if (!data?.message) { 121 return false; 122 } 123 124 const message = String(data.message).toLowerCase(); 125 126 return message.includes("доÑÑÑп") || message.includes("лимиÑ") || message.includes("stock") || message.includes("оÑÑаÑ"); 127}; 128 129const applyFragments = (fragments = {}) => { 130 if (fragments.photohuman_checkout_empty) { 131 window.location.href = window.photohumanCheckout?.cartUrl || "/cart/"; 132 return; 133 } 134 135 Object.entries(fragments).forEach(([selector, html]) => { 136 if (!html) { 137 return; 138 } 139 140 document.querySelectorAll(selector).forEach((node) => { 141 const template = document.createElement("template"); 142 template.innerHTML = html.trim(); 143 const next = template.content.firstElementChild; 144 145 if (next) { 146 node.replaceWith(next); 147 } 148 }); 149 }); 150}; 151 152const notifyCartPageUpdated = (productId, data = {}) => { 153 document.dispatchEvent( 154 new CustomEvent("photohuman:cart-page-updated", { 155 detail: { 156 productId: Number(productId) || 0, 157 quantity: data.quantity, 158 cartLines: Array.isArray(data.cart_lines) ? data.cart_lines : null, 159 fragments: data.fragments || null, 160 }, 161 }) 162 ); 163}; 164 165const isBasketLoading = () => basketLoadingCount > 0; 166 167const setBasketLoading = (loading) => { 168 const basket = getBasketRoot(); 169 170 if (!basket) { 171 return; 172 } 173 174 if (loading) { 175 basketLoadingCount += 1; 176 177 if (basketLoadingCount === 1) { 178 block(basket); 179 basket.setAttribute("aria-busy", "true"); 180 } 181 182 return; 183 } 184 185 basketLoadingCount = Math.max(0, basketLoadingCount - 1); 186 187 if (basketLoadingCount === 0) { 188 unblock(basket); 189 basket.removeAttribute("aria-busy"); 190 } 191}; 192 193const updateCounterUi = (counter, quantity) => { 194 const qty = Math.max(0, Number(quantity) || 0); 195 counter.dataset.qty = String(qty); 196 197 const input = counter.querySelector(".counter-input"); 198 const num = counter.querySelector(".counter-num"); 199 200 if (input) { 201 input.value = String(qty); 202 } 203 204 if (num) { 205 num.textContent = String(qty); 206 } 207}; 208 209const isCheckoutPage = () => getBasketRoot()?.dataset.checkoutPage === "1"; 210 211const setCartLineQty = async (counter, quantity) => { 212 const wcAjaxUrl = getWcAjaxUrl(); 213 const cartItemKey = counter.dataset.cartItemKey || counter.closest(".product, .basket-product__item")?.dataset.cartItemKey || ""; 214 const productId = counter.dataset.productId || counter.closest(".product, .basket-product__item")?.dataset.productId || ""; 215 const onCheckout = isCheckoutPage(); 216 217 if (!wcAjaxUrl || !cartItemKey || cartRequestInFlight) { 218 return null; 219 } 220 221 cartRequestInFlight = true; 222 setBasketLoading(true); 223 224 const body = new FormData(); 225 body.append("cart_item_key", cartItemKey); 226 body.append("quantity", String(quantity)); 227 body.append("product_id", productId); 228 body.append("ui_scope", onCheckout ? "checkout" : "both"); 229 230 if (onCheckout) { 231 body.append("checkout_page", "1"); 232 } else { 233 body.append("cart_page", "1"); 234 } 235 236 const url = wcAjaxUrl.replace("%%endpoint%%", "photohuman_set_cart_line_qty"); 237 238 try { 239 const response = await fetch(url, { 240 method: "POST", 241 credentials: "same-origin", 242 body, 243 }); 244 245 const data = await response.json(); 246 247 if (data?.error) { 248 if (isStockLimitResponse(data)) { 249 showStockError(counter); 250 } else if (data.message) { 251 window.alert(data.message); 252 } 253 return null; 254 } 255 256 if (data?.cart_empty) { 257 if (onCheckout) { 258 window.location.href = window.photohumanCheckout?.cartUrl || "/cart/"; 259 } else { 260 window.location.reload(); 261 } 262 return data; 263 } 264 265 if (data?.fragments) { 266 applyFragments(data.fragments); 267 notifyCartPageUpdated(productId, { ...data, fragments: data.fragments }); 268 } 269 270 return data; 271 } catch (error) { 272 return null; 273 } finally { 274 cartRequestInFlight = false; 275 setBasketLoading(false); 276 } 277}; 278 279const handleCounterClick = async (event) => { 280 const control = event.target.closest(".counter-minus, .counter-plus"); 281 282 if (!control || isBasketLoading()) { 283 return; 284 } 285 286 const counter = control.closest("[data-cart-counter]"); 287 288 if (!counter) { 289 return; 290 } 291 292 event.preventDefault(); 293 294 const current = Number(counter.dataset.qty || counter.querySelector(".counter-num")?.textContent || 0); 295 const maxQty = counter.dataset.maxQty ? Number(counter.dataset.maxQty) : null; 296 let next = control.classList.contains("counter-plus") ? current + 1 : current - 1; 297 298 if (control.classList.contains("counter-minus")) { 299 hideStockError(getStockErrorFromCounter(counter), true); 300 } else if (maxQty !== null && next > maxQty) { 301 showStockError(counter); 302 return; 303 } 304 305 const data = await setCartLineQty(counter, next); 306 307 if (data && !data.cart_empty) { 308 const refreshed = document.querySelector(`[data-cart-counter][data-cart-item-key="${CSS.escape(counter.dataset.cartItemKey)}"]`); 309 310 if (refreshed) { 311 updateCounterUi(refreshed, data.quantity); 312 } 313 } 314}; 315 316const handleRemoveClick = async (event) => { 317 const button = event.target.closest("[data-cart-remove]"); 318 319 if (!button || button.disabled) { 320 return; 321 } 322 323 const product = button.closest(".product, .basket-product__item"); 324 const counter = product?.querySelector("[data-cart-counter]"); 325 326 if (!counter) { 327 return; 328 } 329 330 event.preventDefault(); 331 await setCartLineQty(counter, 0); 332}; 333
334export const initCartPage = () => { 335 const basket = getBasketRoot(); 336 337 if (!basket || basket.dataset.cartPageBound === "1") { 338 return; 339 } 340 341 basket.dataset.cartPageBound = "1"; 342 343 document.addEventListener("photohuman:basket-loading", (event) => { 344 setBasketLoading(!!event.detail?.loading); 345 }); 346 347 basket.addEventListener("click", (event) => { 348 if (event.target.closest("[data-cart-remove]")) { 349 handleRemoveClick(event); 350 return; 351 } 352 353 if (event.target.closest(".counter-minus, .counter-plus")) { 354 handleCounterClick(event); 355 } 356 }); 357};
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.