PageSourceSearch

https://photohuman.space/wp-content/themes/photohuman/source/js/modules/cartPage.js?v=1.6.284

js photohuman.space collected 2026-09-28 08:54:36 UTC 9,143 bytes, 357 lines download raw bytes

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.