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.