PageSourceSearch

https://www.bitrix24.uk/bitrix/templates/landing/js/include_areas/…/bxw_block_price_calculator_short.js

js bitrix24.uk collected 2026-09-30 09:59:58 UTC 12,085 bytes, 424 lines download raw bytes

1const initPriceCalculatorItem = (calculator) => {
2	const features = calculator.querySelectorAll('.bxw-block-price-calculator-short__feature-input');
3	const userSlider = calculator.querySelector('.bxw-block-price-calculator-short__counter-input');
4	const userCountDisplay = calculator.querySelector('.bxw-block-price-calculator-short__counter-thumb');
5	const userCountMobileInput = calculator.querySelector('.bxw-block-price-calculator-short__counter-mobile-input');
6	const bitrixPriseDisplay = calculator.querySelector('.bxw-block-price-calculator-short__box-item_price-bitrix .bxw-block-price-calculator-short__box-title');
7	const yourBenefitDisplaYear = calculator.querySelector('.bxw-block-price-calculator-short__box_save .bxw-block-price-calculator-short__box-title');
8
9	if (calculator.dataset.priceCalculatorInitialized === '1') return;
10	if (!userSlider || !userCountDisplay || !bitrixPriseDisplay || !yourBenefitDisplaYear) return;
11
12	calculator.dataset.priceCalculatorInitialized = '1';
13
14	const tariffs = JSON.parse(calculator.dataset.tariffs);
15
16	const decimalSeparator = calculator.dataset.decimalSeparator || '.';
17	const thousandsSeparator = calculator.dataset.thousandsSeparator || ',';
18	const pricePattern = calculator.dataset.pricePattern || '#PRICE#';
19
20	const minValue = 1;
21	const centerValue = 50;
22	const maxValue = 250;
23
24	function positionToValue(pos) {
25		if (pos < 50) {
26			return Math.round(minValue + (pos / 50) * (centerValue - minValue));
27		} else {
28			return Math.round(centerValue + ((pos - 50) / 50) * (maxValue - centerValue));
29		}
30	}
31
32	function formatNumber(value) {
33		const parts = value.toFixed(0).toString().split('.');
34		parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandsSeparator);
35		return parts.join(decimalSeparator);
36	}
37
38	function formatPrice(value) {
39	const formatted = formatNumber(value);
40	const pricePatternParts = pricePattern.split('#PRICE#');
41		if (pricePatternParts.length < 2) {
42			return formatted;
43		}
44		const currencyPrefix = pricePatternParts[0].trim();
45		const currencySuffix = pricePatternParts[1]
46			.replace(/\/.*$/, '')
47			.replace(/[a-z]+\.?/gi, '')
48			.trim();
49		if (currencyPrefix) {
50			return currencyPrefix + ' ' + formatted + currencySuffix;
51		}
52		if (currencySuffix) {
53			return formatted + ' ' + currencySuffix;
54		}
55		return formatted;
56	}
57
58	function clampUsersCount(value) {
59		return Math.min(Math.max(value, minValue), maxValue);
60	}
61
62	const monthPriceBasic = parseInt(tariffs.BASIC, 10);
63	const monthPriceStandard = parseInt(tariffs.STANDART, 10);
64	const monthPriceProfessional = parseInt(tariffs.PROFESSIONAL, 10);
65	const monthPriceEnterprise250 = parseInt(tariffs.ENTERPRISE_250, 10);
66
67	let numberOfUsers = positionToValue(50);
68	let userPrice = 0;
69	let planPrice = 0;
70	let totalSumApps;
71
72	userSlider.value = 50;
73
74	function getFeaturePrice(feature) {
75		return parseInt(feature.getAttribute('data-price'), 10) || 0;
76	}
77
78	function updateUserPrice() {
79		userPrice = 0;
80
81		features.forEach(feature => {
82			if (feature.checked) {
83				userPrice += getFeaturePrice(feature);
84			}
85		});
86	}
87
88	features.forEach(feature => {
89		feature.addEventListener('change', () => {
90			updateUserPrice();
91			updateTotalSum();
92		});
93	});
94
95	function updateThumbPosition() {
96		const min = Number(userSlider.min) || 0;
97		const max = Number(userSlider.max) || 100;
98		const value = Number(userSlider.value);
99		const range = max - min;
100		const percentage = range === 0 ? 0 : (value - min) / range;
101
102		userCountDisplay.style.left = (percentage * 100) + '%';
103		userSlider.style.setProperty('--slider-value', (percentage * 100) + '%');
104	}
105
106	function updatePlanPrice() {
107		if (numberOfUsers > 0 && numberOfUsers <= 5) {
108			planPrice = monthPriceBasic;
109		} else if (numberOfUsers > 5 && numberOfUsers <= 50) {
110			planPrice = monthPriceStandard;
111		} else if (numberOfUsers > 50 && numberOfUsers <= 100) {
112			planPrice = monthPriceProfessional;
113		} else if (numberOfUsers > 100 && numberOfUsers <= 250) {
114			planPrice = monthPriceEnterprise250;
115		}
116
117		bitrixPriseDisplay.textContent = formatPrice(planPrice);
118		updateUsersCount(numberOfUsers);
119		updateYourBenefit(planPrice);
120	}
121
122	function updateTotalSum() {
123		totalSumApps = userPrice * numberOfUsers;
124		updateYourBenefit(planPrice);
125	}
126
127	function updateCalculatorByUsersCount(usersCount) {
128		numberOfUsers = clampUsersCount(usersCount);
129		userSlider.value = valueToPosition(numberOfUsers);
130		updateUsersCount(numberOfUsers);
131		updateThumbPosition();
132		updateTotalSum();
133		updatePlanPrice();
134	}
135
136	userSlider.addEventListener('input', () => {
137		numberOfUsers = positionToValue(parseFloat(userSlider.value));
138		updateUsersCount(numberOfUsers);
139		updateThumbPosition();
140		updateTotalSum();
141		updatePlanPrice();
142	});
143
144	function updateUsersCount(number) {
145		const formatted = formatNumber(number);
146		userCountDisplay.textContent = formatted;
147		if (userCountMobileInput) {
148			userCountMobileInput.value = formatted;
149		}
150	}
151
152	function updateYourBenefit(planPrice) {
153		const benefitMonth = totalSumApps - planPrice;
154		const benefitYear = benefitMonth * 12;
155
156		const safeBenefit = benefitYear >
156 0 ? benefitYear : 0;
157		yourBenefitDisplaYear.textContent = formatPrice(safeBenefit);
158	}
159
160	updateUserPrice();
161	updateThumbPosition();
162	updateTotalSum();
163	updatePlanPrice();
164	updateUsersCount(numberOfUsers);
165
166	//mobile
167	const mobileSteps = [1, 5, 50, 100, 250];
168	const counterMobileMinus = calculator.querySelector('.bxw-block-price-calculator-short__counter-mobile-minus');
169	const counterMobilePlus = calculator.querySelector('.bxw-block-price-calculator-short__counter-mobile-plus');
170
171	function valueToPosition(value) {
172		if (value <= centerValue) {
173			return Math.round(((value - minValue) / (centerValue - minValue)) * 50);
174		} else {
175			return Math.round(50 + ((value - centerValue) / (maxValue - centerValue)) * 50);
176		}
177	}
178
179	function getCurrentMobileStepIndex() {
180		var idx = mobileSteps.findIndex(function(step) { return step >= numberOfUsers; });
181		if (idx < 0) idx = mobileSteps.length - 1;
182		if (idx > 0 && mobileSteps[idx] !== numberOfUsers && (numberOfUsers - mobileSteps[idx - 1]) < (mobileSteps[idx] - numberOfUsers)) {
183			idx--;
184		}
185		return idx;
186	}
187
188	function applyMobileStep(stepValue) {
189		updateCalculatorByUsersCount(stepValue);
190	}
191
192	if (userCountMobileInput) {
193		userCountMobileInput.addEventListener('focus', function() {
194			this.select();
195		});
196
197		userCountMobileInput.addEventListener('input', function() {
198			const numericValue = this.value.replace(/\D/g, '');
199			this.value = numericValue;
200
201			if (numericValue === '') return;
202
203			updateCalculatorByUsersCount(parseInt(numericValue, 10));
204		});
205
206		userCountMobileInput.addEventListener('blur', function() {
207			const numericValue = parseInt(this.value.replace(/\D/g, ''), 10);
208			updateCalculatorByUsersCount(Number.isNaN(numericValue) ? minValue : numericValue);
209		});
210	}
211
212	if (counterMobileMinus) {
213		counterMobileMinus.addEventListener('click', function() {
214			var idx = getCurrentMobileStepIndex();
215			if (idx > 0) {
216				applyMobileStep(mobileSteps[idx - 1]);
217			}
218		});
219	}
220
221	if (counterMobilePlus) {
222		counterMobilePlus.addEventListener('click', function() {
223			var idx = getCurrentMobileStepIndex();
224			if (idx < mobileSteps.length - 1) {
225				applyMobileStep(mobileSteps[idx + 1]);
226			}
227		});
228	}
229};
230
231const initPriceCalculator = () => {
232	document.querySelectorAll('.bxw-block-price-calculator-short__content').forEach(initPriceCalculatorItem);
233};
234
235const initPriceCalculatorMarquee = () => {
236	const MARQUEE_SPEED = 36;
237	const MARQUEE_MOBILE_SPEED = 44;
238	const MARQUEE_MOBILE_MEDIA = '(max-width: 767.98px)';
239	const boxes = document.querySelectorAll('.bxw-block-price-calculator-short__apps-marquee-box');
240
241	function getMediaQuery(query) {
242		if (typeof window.matchMedia === 'function') {
243			return window.matchMedia(query);
244		}
245
246		return {
247			matches: false,
248			addEventListener: null,
249			removeEventListener: null,
250			addListener: null,
251			removeListener: null
252		};
253	}
254
255	function addMediaListener(mediaQuery, callback) {
256		if (typeof mediaQuery.addEventListener === 'function') {
257			mediaQuery.addEventListener('change', callback);
258			return;
259		}
260
261		if (typeof mediaQuery.addListener === 'function') {
262			mediaQuery.addListener(callback);
263		}
264	}
265
266	boxes.forEach((box) => {
267		const track = box.querySelector('.bxw-block-price-calculator-short__apps-marquee-track');
268		const reducedMotion = getMediaQuery('(prefers-reduced-motion: reduce)');
269		const mobileMq = getMediaQuery(MARQUEE_MOBILE_MEDIA);
270		const originalItems = track ? Array.prototype.slice.call(track.querySelectorAll('.bxw-block-price-calculator-short__apps-marquee-icon')) : [];
271		let animationFrame = 0;
272		let resizeFrame = 0;
273		let lastTimestamp = 0;
274		let position = 0;
275		let loopWidth = 0;
276		let speed = 0;
277
278		if (!track || box.dataset.priceCalculatorMarqueeInitialized === '1' || originalItems.length < 2) {
279			return;
280		}
281
282		box.dataset.priceCalculatorMarqueeInitialized = '1';
283
284		function getSpeed() {
285			return mobileMq.matches ? MARQUEE_MOBILE_SPEED : MARQUEE_SPEED;
286		}
287
288		function stop() {
289			if (animationFrame) {
290				window.cancelAnimationFrame(animationFrame);
291				animationFrame = 0;
292			}
293		}
294
295		function removeClones() {
296			Array.prototype.slice.call(track.querySelectorAll('[data-bxw-price-calculator-marquee-clone="true"]')).forEac
296h((clone) => {
297				if (clone.parentNode) {
298					clone.parentNode.removeChild(clone);
299				}
300			});
301		}
302
303		function cloneOriginalItems() {
304			const fragment = document.createDocumentFragment();
305
306			originalItems.forEach((item) => {
307				const clone = item.cloneNode(true);
308
309				clone.setAttribute('aria-hidden', 'true');
310				clone.setAttribute('data-bxw-price-calculator-marquee-clone', 'true');
311				fragment.appendChild(clone);
312			});
313
314			track.appendChild(fragment);
315		}
316
317		function tick(timestamp) {
318			let delta;
319
320			if (reducedMotion.matches || document.hidden || loopWidth <= 0) {
321				stop();
322				return;
323			}
324
325			if (!lastTimestamp) {
326				lastTimestamp = timestamp;
327			}
328
329			delta = Math.min(timestamp - lastTimestamp, 64);
330			lastTimestamp = timestamp;
331			position = (position + speed * delta / 1000) % loopWidth;
332			track.style.transform = 'translate3d(' + (-position) + 'px, 0, 0)';
333
334			animationFrame = window.requestAnimationFrame(tick);
335		}
336
337		function start() {
338			if (animationFrame || reducedMotion.matches || document.hidden || loopWidth <= 0) {
339				return;
340			}
341
342			lastTimestamp = 0;
343			animationFrame = window.requestAnimationFrame(tick);
344		}
345
346		function buildTrack() {
347			const firstItem = originalItems[0];
348			let firstClone;
349			let minTrackWidth;
350			let guard = 0;
351
352			stop();
353			removeClones();
354			track.style.transform = 'translate3d(0, 0, 0)';
355			position = 0;
356			loopWidth = 0;
357			speed = getSpeed();
358
359			cloneOriginalItems();
360			firstClone = track.querySelector('[data-bxw-price-calculator-marquee-clone="true"]');
361
362			if (!firstItem || !firstClone) {
363				return;
364			}
365
366			loopWidth = firstClone.offsetLeft - firstItem.offsetLeft;
367
368			if (loopWidth <= 0) {
369				return;
370			}
371
372			minTrackWidth = loopWidth + box.clientWidth;
373
374			while (track.scrollWidth < minTrackWidth && guard < 10) {
375				cloneOriginalItems();
376				guard += 1;
377			}
378
379			start();
380		}
381
382		function scheduleRebuild() {
383			if (resizeFrame) {
384				window.cancelAnimationFrame(resizeFrame);
385			}
386
387			resizeFrame = window.requestAnimationFrame(() => {
388				resizeFrame = 0;
389				buildTrack();
390			});
391		}
392
393		window.addEventListener('resize', scheduleRebuild);
394		document.addEventListener('visibilitychange', () => {
395			if (document.hidden) {
396				stop();
397				return;
398			}
399
400			start();
401		});
402		addMediaListener(reducedMotion, scheduleRebuild);
403		addMediaListener(mobileMq, scheduleRebuild);
404
405		originalItems.forEach((item) => {
406			if ('complete' in item && !item.complete) {
407				item.addEventListener('load', scheduleRebuild, {once: true});
408			}
409		});
410
411		buildTrack();
412	});
413};
414
415const initPriceCalculatorBlock = () => {
416	initPriceCalculator();
417	initPriceCalculatorMarquee();
418};
419
420BX.ready(initPriceCalculatorBlock);
421
422if (window.BX && typeof BX.addCustomEvent === 'function') {
423	BX.addCustomEvent('onFrameDataReceived', initPriceCalculatorBlock);
424}

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.