1import { Modal } from 'bootstrap.native'; 2/* eslint-disable-next-line */ 3import { event, loader } from '../../Foundation'; 4/* eslint-disable-next-line */ 5import throttle from 'lodash.throttle'; 6import { AmendPrices } from './components/amend-prices'; 7import domready from 'domready'; 8import { NumberInput } from '../../Feature'; 9 10export class CartLine { 11 /** 12 * Creates a new instance of the Video class for handling HTML5 Video, Youtube Videos and 23Videos 13 * @param {HTMLElement} element Video DOM element 14 */ 15 constructor(element) { 16 // Name, reference 17 element.CartLine = this; 18 this.el = element; 19 20 // Data 21 this._data = JSON.parse(element.dataset.cartline); 22 23 // Props 24 this._props = { 25 isFavorite: this.el.classList.contains('is-favorite'), 26 }; 27 28 // Elements 29 this._refs = { 30 el: element, 31 numberInput: element.querySelector('.number-input'), 32 accessoriesNumberInputs: element.getElementsByClassName('accessories-number-input'), 33 flexpoints: element.querySelector('.cart-line__flex-points'), 34 editButton: element.querySelector('.is-editable'), 35 configuratorSide: element.querySelector('.configurator__selector__side'), 36 buttons: { 37 delete: element.querySelector('.js-cartline-remove'), 38 amendPrice: element.querySelector('.change-price'), 39 }, 40 dialogs: { 41 delete: element.querySelector('.js-cartline-remove-dialog'), 42 quantity: element.querySelector('.js-cartline-quantity-update-dialog'), 43 quantityError: element.querySelector('.js-cartline-quantity-error-dialog'), 44 }, 45 config: { 46 links: Array.from(element.querySelectorAll('.cart-line__controls-start a')), 47 contentarea: element.querySelector('.cart-line__configuration'), 48 }, 49 table: { 50 columns: Array.from(element.querySelectorAll('td.hover')), 51 }, 52 }; 53 54 this._refs.editButton?.addEventListener('click', this._onItemClick.bind(this)); 55 56 // Load 57 if ( 58 this._refs.config.contentarea.classList.contains('is-active') && 59 this._refs.config.show != null && 60 this._refs.config.hide != null 61 ) { 62 this._refs.config.show.classList.add('is-hidden'); 63 this._refs.config.hide.classList.remove('is-hidden'); 64 } 65 66 // Toggle configuration 67 if (this._refs.buttons.delete != null && !element.classList.contains('constructorFinished')) { 68 this._refs.buttons.delete.addEventListener('click', this._onRemoveItemClick.bind(this)); 69 } 70 71 // Number input 72 if (this._refs.numberInput != null && !element.classList.contains('constructorFinished')) { 73 this._refs.numberInput.addEventListener( 74 'numberinput.updated', 75 this._onNumberinputUpdate.bind(this), 76 ); 77 } 78 79 // Accessories number input 80 if (this._refs.accessoriesNumberInputs.length > 0) { 81 for (var i = 0; i < this._refs.accessoriesNumberInputs.length; i++) { 82 var accessoryNumberInput = this._refs.accessoriesNumberInputs[i]; 83 accessoryNumberInput.addEventListener( 84 'accessoriesnumberinput.updated', 85 this._onAccessoriesNumberInputUpdate.bind(this, accessoryNumberInput), 86 ); 87 var accessoryCartLineId = accessoryNumberInput.querySelector( 88 '#AccessoryLineIdHiddenField', 89 ).value; 90 var accessoryItemNo = accessoryNumberInput.querySelector( 91 '#AccessoryItemNoHiddenField', 92 ).value; 93 new NumberInput(accessoryNumberInput, true, accessoryCartLineId, accessoryItemNo); 94 } 95 } 96 97 if (this._refs.buttons.amendPrice != null) { 98 this._refs.buttons.amendPrice.addEventListener( 99 'click', 100 this._onAmendPriceLinkClick.bind(this), 101 ); 102 } 103 104 // Checking if page has loaded 105 if (this._refs.buttons.amendPrice != null) { 106 if (document.readyState === 'complete') { 107 this._refs.buttons.amendPrice.classList.remove('disableClick'); 108 } else { 109 this._refs.buttons.amendPrice.classList.add('disableClick'); 110 domready(() => { 111 this._refs.buttons.amendPrice.classList.remove('disableClick'); 112 }); 113 } 114 } 115 116 this._addLoaderForRemoveAccessories(); 117 this.hideEmptyModelText(); 118 119 element.classList.add('constructorFinished'); 120 } 121 122 /** 123 * Calculator for flexpoints in case a flexpoint-item has been added as cartLine 124 * @param {Event} detail - Details from updated number-input 125 */ 126 /* eslint-disable-next-line */ 127 _onNumberInputUpdate({ detail }) {} 128 129 _addLoaderForRemoveAccessories() { 130 const accessoriesBinIcons = document.querySelectorAll('.remove-accessory'); 131 132 accessoriesBinIcons.forEach((accessoriesBinIcon) => { 133 accessoriesBinIcon.addEventListener('click', () => loader.show(), { once: true });
134 }); 135 } 136 137 /** 138 * onItemClick 139 * When a user clicks an editable field inside product-table 140 * @param {Event} e - event interface 141 */ 142 /* eslint-disable-next-line */ 143 _onItemClick(e) { 144 const currentPropertyIdHiddenField = document.getElementById('CurrentPropertyIdHiddenField'), 145 /* eslint-disable-next-line */ 146 selectedPropertyIdHiddenField = document.getElementById('SelectedPropertyIdHiddenField'), 147 //propertyIdHiddenField = e.target.closest('td').querySelector('#PropertyIdHiddenField'), 148 selectedEditLineIdHiddenField = document.getElementById('SelectedEditLineIdHiddenField'), 149 lineIdHiddenField = this._refs.el.querySelector('#LineIdHiddenField'), 150 isEditClickedHiddenField = document.getElementById('IsEditClickedHiddenField'); 151 152 currentPropertyIdHiddenField.value = selectedEditLineIdHiddenField.value; 153 //selectedPropertyIdHiddenField.value = propertyIdHiddenField.value; 154 155 selectedEditLineIdHiddenField.value = lineIdHiddenField.value; 156 isEditClickedHiddenField.value = 'true'; 157 158 loader.show(); 159 160 __doPostBack('OverviewUpdatePanel', ''); 161 } 162 163 /** 164 * _onRemoveItemClick 165 * Remove an item from cart 166 * @param {Event} e - event interface 167 */ 168 _onRemoveItemClick(e) { 169 e.preventDefault(); 170 171 let id = this._refs.buttons.delete.dataset.target, 172 el = this.el.querySelector(id), 173 modal = el.Modal || new Modal(el), 174 handler = () => { 175 modal.hide(); 176 177 let lineId = this.el.querySelector('#LineIdHiddenField'); 178 if (lineId != null) { 179 let oLineId = document.getElementById('SelectedDeleteLineIdHiddenField'); 180 if (oLineId != null) { 181 oLineId.value = lineId.value; 182 } 183 } 184 185 const calendarFields = document.querySelectorAll('[data-datepicker]'); 186 const calendarFieldsAsElements = [...calendarFields]; 187 188 calendarFieldsAsElements.forEach((cf) => { 189 cf.disabled = true; 190 }); 191 192 // Execute postback 193 __doPostBack(this._data.RemoveCartItemLinkButtonId, ''); 194 }; 195 196 // Display 197 modal.show(); 198 199 // Events 200 if (el) { 201 el.addEventListener( 202 'shown.bs.modal', 203 (event) => { 204 let x = event.target, 205 trigger = x.querySelector('.btn-primary'); 206 207 trigger.addEventListener('click', handler); 208 }, 209 false, 210 ); 211 212 el.addEventListener( 213 'hide.bs.modal', 214 (event) => { 215 let x = event.target, 216 trigger = x.querySelector('.btn-primary'); 217 218 trigger.removeEventListener('click', handler); 219 }, 220 false, 221 ); 222 } 223 } 224 225 /** 226 * _onConfigurationToggle 227 * @param {Event} e - Event Interface 228 */ 229 _onConfigurationToggle(e) { 230 let target = e.target; 231 232 e.preventDefault(); 233 234 if (target.classList.contains('js-hide-configurator')) { 235 this._refs.config.show.classList.remove('is-hidden'); 236 237 this._refs.config.contentarea.classList.remove('is-active'); 238 239 this._refs.config.hide.classList.add('is-hidden'); 240 } else { 241 this._refs.config.show.classList.add('is-hidden'); 242 243 this._refs.config.contentarea.classList.add('is-active'); 244 245 this._refs.config.hide.classList.remove('is-hidden'); 246 } 247 } 248 249 /** 250 * _onNumberinputUpdate 251 * Update quantity for cartLine once user has changed input 252 * @param {HTMLEvent} e - event interface 253 */ 254 _onNumberinputUpdate({ detail }) { 255 const updateFavoritesApi = '/api/favorites/updatequantity'; 256 const updateCartlineApi = '/api/cart/cartline/updatequantity'; 257 const quantity = detail.value, 258 lineId = this.el.querySelector('#LineIdHiddenField'), 259 api = this._props.isFavorite ? updateFavoritesApi : updateCartlineApi; 260 261 // Flexpoints calculation 262 if (this._refs.flexpoints !== null) { 263 // Flexpoint calculation 264 const oItemUnitValue = parseInt(this._refs.flexpoints.dataset.point), 265 oItemPointCalculation = this._refs.flexpoints.querySelector('div'); 266 267 if (oItemUnitValue === null || oItemPointCalculation === null) return; 268 269 oItemPointCalculation.innerHTML = (oItemUnitValue * detail.value).toLocaleStr
269ing('en-US', { 270 maximumFractionDigits: 2, 271 }); 272 } 273 loader.show(); 274 fetch(api, { 275 method: 'PATCH', 276 headers: { 277 'Content-Type': 'application/json', 278 }, 279 body: JSON.stringify({ 280 Id: lineId.value, 281 Quantity: quantity, 282 }), 283 }) 284 .then((response) => { 285 if (response.status === 200) { 286 var oldValue = this.el.getAttribute('oldvalue') ?? detail.initial; 287 //var currentCartTotalItemsCount = +document.querySelector('.main-navigation__basket>span').innerHTML; 288 289 //if (detail.value != oldValue) 290 //currentCartTotalItemsCount = currentCartTotalItemsCount - oldValue + detail.value; 291 292 //this._setNewTotalCartItemsCount(currentCartTotalItemsCount); 293 this.el.setAttribute('oldvalue', detail.value); 294 295 // Send to google analytics 296 if (oldValue != detail.value) { 297 var numberChange = Math.abs(detail.value - oldValue); 298 var gaEventName = oldValue > detail.value ? 'remove_from_cart' : 'add_to_cart'; 299 var productNumber = this.el.querySelector('.product_number').innerText; 300 var productName = this.el.querySelector('.product-item__modelText').innerText; 301 302 dataLayer.push({ ecommerce: null }); 303 dataLayer.push({ 304 event: gaEventName, 305 ecommerce: { 306 currency: 'EUR', 307 items: [ 308 { 309 item_id: productNumber, 310 item_name: productName, 311 item_category: 'n/a', 312 item_category2: 'n/a', 313 item_category3: 'n/a', 314 item_category4: 'n/a', 315 price: 1.0, 316 quantity: numberChange, 317 }, 318 ], 319 }, 320 }); 321 322 response.json().then((body) => { 323 const cartData = JSON.parse(body); 324 if (cartData !== undefined) { 325 if (api === updateCartlineApi) { 326 this.syncReceiversAmount(cartData); 327 } 328 cartData.forEach((product) => { 329 cartItems.push({ 330 item_id: product.ProductId, 331 item_name: product.ModelName, 332 item_brand: product.Brand, 333 item_category: product.PricePoint, 334 item_category2: product.FamilyCode, 335 item_category3: product.ShellType, 336 item_category4: product.Color, 337 item_variant: product.ModelNumber, 338 price: 1, 339 quantity: product.Quantity, 340 }); 341 }); 342 dataLayer.push({ 343 event: 'cart_state', 344 ecommerce: { 345 currency: 'EUR', 346 items: cartItems, 347 }, 348 }); 349 350 this._updatePackagingText(lineId.value, productNumber, cartData); 351 this._pushDataLayerCartState(cartData); 352 } 353 }); 354 } 355 } 356 // Minimum display time for the loader 357 setTimeout(() => loader.hide(), 250); 358 /* eslint-disable-next-line */ 359 }) 360 .catch((response) => { 361 console.log(response); 362 loader.hide(); 363 }); 364 } 365 366 _updatePackagingText(lineId, productNumber, cartData) { 367 const tableEl = this._refs.el.querySelector('.productDetailTable'); 368 const rowsEl = tableEl?.getElementsByTagName('tr') ?? []; 369 370 for (let i = 0; i < rowsEl.length; i++) { 371 const inputsEl = [...rowsEl[i].getElementsByTagName('input')]; 372 const packagingInput = !!inputsEl.find((inputEl) => inputEl.value === 'Packaging'); 373 374 if (packagingInput) { 375 const rowLiElements = rowsEl[i].getElementsByTagName('li'); 376 const cartDataItem = cartData.find( 377 (d) => d.LineId === lineId && productNumber.includes(d.ProductId), 378 ); 379 380 if (rowLiElements.length && cartDataItem) { 381 rowLiElements[0].textContent = cartDataItem.Packaging; 382 } 383 break; 384 } 385 } 386 } 387 388 _pushDataLayerCartState(cartData) { 389 const cartItems = []; 390 391 cartData.forEach((product) => { 392 cartItems.push({ 393 item_id: product.ProductId, 394 item_name: product.ModelName, 395 item_brand: product.Brand, 396 item_category: product.PricePoint, 397 item_category2: product.FamilyCode,
398 item_category3: product.ShellType, 399 item_category4: product.Color, 400 item_variant: product.ModelNumber, 401 price: 1, 402 quantity: product.Quantity, 403 }); 404 }); 405 406 dataLayer.push({ 407 event: 'cart_state', 408 ecommerce: { 409 currency: 'EUR', 410 items: cartItems, 411 }, 412 }); 413 } 414 415 /** 416 * syncReceiversAmount 417 * Synchronization receivers amount on the UI based on the response from the backend once user has changed quantity 418 * @param {HTMLEvent} cartData - cart data from the API call 419 */ 420 syncReceiversAmount(cartData) { 421 const productIds = cartData.map((d) => d.ProductId); 422 const receiverNumberSpans = [...this.el.querySelectorAll('span.product_number')]; 423 receiverNumberSpans.forEach((span) => { 424 if (!productIds.includes(span.innerText)) { 425 const liElement = span.closest('li'); 426 if (liElement) { 427 liElement.remove(); 428 } 429 } 430 }); 431 } 432 433 /** 434 * __onAccessoriesNumberInputUpdate 435 * Update quantity for accessories cartLine once user has changed input 436 * @param {HTMLEvent} e - event interface 437 */ 438 _onAccessoriesNumberInputUpdate(accessoryNumberInputElement, e) { 439 const quantity = e.detail.value, 440 lineId = accessoryNumberInputElement.querySelector('#AccessoryLineIdHiddenField'), 441 itemNo = accessoryNumberInputElement.querySelector('#AccessoryItemNoHiddenField'), 442 api = this._props.isFavorite 443 ? '/api/favorites/updatequantity' 444 : '/api/cart/accessorycartline/updatequantity'; 445 446 // Flexpoints calculation 447 if (this._refs.flexpoints !== null) { 448 // Flexpoint calculation 449 const oItemUnitValue = parseInt(this._refs.flexpoints.dataset.point), 450 oItemPointCalculation = this._refs.flexpoints.querySelector('div'); 451 452 if (oItemUnitValue === null || oItemPointCalculation === null) return; 453 454 oItemPointCalculation.innerHTML = (oItemUnitValue * detail.value).toLocaleString('en-US', { 455 maximumFractionDigits: 2, 456 }); 457 } 458 459 loader.show(); 460 fetch(api, { 461 method: 'PATCH', 462 headers: { 463 'Content-Type': 'application/json', 464 }, 465 body: JSON.stringify({ 466 Id: lineId.value, 467 Quantity: quantity, 468 ItemNo: itemNo.value, 469 }), 470 }) 471 .then((response) => { 472 if (response.status === 200) { 473 var oldValue = accessoryNumberInputElement.getAttribute('oldvalue') ?? e.detail.initial; 474 var currentCartTotalItemsCount = +document.querySelector('.main-navigation__basket>span') 475 .innerHTML; 476 477 if (e.srcElement.classList.contains('accessories-number-input')) { 478 if (e.detail.value != oldValue) 479 currentCartTotalItemsCount = currentCartTotalItemsCount - oldValue + e.detail.value; 480 } 481 482 this._setNewTotalCartItemsCount(currentCartTotalItemsCount); 483 accessoryNumberInputElement.setAttribute('oldvalue', e.detail.value); 484 } 485 // Minimum display time for the loader 486 setTimeout(() => loader.hide(), 250); 487 /* eslint-disable-next-line */ 488 }) 489 .catch((response) => { 490 console.log(response); 491 loader.hide(); 492 }); 493 } 494 /** 495 * _onChangePriceItemClick 496 * Method for abort the amend prices flow 497 * @param {Event} e - event interface 498 */ 499 _onAmendPriceLinkClick(e) { 500 e.preventDefault(); 501 502 let id = this._refs.buttons.amendPrice.dataset.target, 503 el = this.el.querySelector(id), 504 modal = el.Modal || new Modal(el); 505 506 // Display 507 modal.show(); 508 509 /* eslint-disable-next-line */ 510 const oAmendPrices = new AmendPrices(el); 511 } 512 513 removeOrderTypeRadioButton() { 514 const stockQuantity = document.querySelector('.cart-line__quantity'); 515 document.querySelector('.cart-line__delivery-date .form-group')?.appendChild(stockQuantity); 516 let radioButtons = document.getElementsByClassName('RadiobuttonSelection'); 517 [...radioButtons].forEach((radio) => { 518 if (!radio.firstChild.getAttribute('checked')) { 519 radio.remove(); 520 } 521 });
522 } 523 524 appendProductQuantity() { 525 [...document.querySelectorAll('.cart-line:not(.is-favorite)')].forEach((cartLine) => { 526 let quantity = cartLine.querySelector('.cart-line__quantity'); 527 let quantityTable = cartLine.querySelector('.cart-line__quantity table'); 528 if (quantity) { 529 cartLine.querySelector('.cart-line__delivery-date .form-group')?.appendChild(quantity); 530 if (quantityTable) { 531 quantityTable.classList.add('is-hidden'); 532 } 533 } 534 }); 535 } 536 537 getConfiguratorSideText() { 538 [...document.querySelectorAll('.cart-line')].forEach((cartLineSection) => { 539 const sideValue = cartLineSection.querySelector('.configurator__selector__side label'); 540 let orderTypeText = cartLineSection.querySelector( 541 '.productDetailTable .RadiobuttonSelection label', 542 ); 543 if (sideValue !== null) { 544 orderTypeText.innerHTML += `, ${sideValue.textContent}`; 545 } 546 }); 547 } 548 549 addMarginBusinessWrapperDescription() { 550 if (document.querySelector('.top-free-text') !== null) { 551 document.querySelector('.top-free-text').classList.add('col-md-10'); 552 document.querySelector('.top-free-text').classList.add('cart-top-free-text'); 553 } 554 } 555 556 //_setNewTotalCartItemsCount(newTotalCartItemsCount) { 557 // const topMainTitle = document.querySelector('.top-main-title').textContent.substring(0, document.querySelector('.top-main-title').textContent.indexOf('(')); 558 559 // document.querySelector('.main-navigation__basket>span').innerHTML = newTotalCartItemsCount; 560 // document.querySelector('.products-counter').innerHTML = newTotalCartItemsCount; 561 // document.getElementById('cssCart')?.setAttribute('cart-title-overwrite-counter', newTotalCartItemsCount); 562 // document.querySelector('.top-main-title').textContent = `${topMainTitle} (${newTotalCartItemsCount} ${parseInt(newTotalCartItemsCount) > 1 ? document.getElementById('cssCart')?.getAttribute('cart-title-overwrite-text-plural') : document.getElementById('cssCart')?.getAttribute('cart-title-overwrite-text-singular')})` 563 //} 564 565 adjustProductColorRow() { 566 [...document.querySelectorAll('.configurator__controller-color')].forEach((colorDivNode) => { 567 if (colorDivNode.nextSibling?.textContent.trim() === '') { 568 colorDivNode.nextSibling.remove(); 569 } 570 571 let parentNode = colorDivNode.parentNode; 572 let colorText = parentNode.querySelector('span'); 573 colorText.classList.add('capitalize-letters'); 574 575 if (parentNode) { 576 parentNode.insertBefore(colorText, colorDivNode); 577 } 578 }); 579 } 580 581 adjustEarmoldTitleStyling() { 582 [...document.querySelectorAll('.product-item__familyCode')].forEach((productCode) => { 583 if (productCode.textContent === '') { 584 productCode.parentNode.classList.add('hide-dots'); 585 } 586 }); 587 } 588 589 adjustAccessoriesTitleStyling() { 590 [...document.querySelectorAll('.product-item__familyCode')].forEach((productCode) => { 591 if (productCode.textContent === '') { 592 const productItemName = productCode.parentNode; 593 productItemName.previousElementSibling.style.display = 'none'; 594 595 const accessoryNameElement = productItemName.nextElementSibling; 596 597 if (!accessoryNameElement) return; 598 599 const headingContainer = productItemName.parentNode; 600 const cartLineHeadStart = headingContainer.parentNode; 601 const accessoryName = accessoryNameElement.textContent; 602 603 let PKUNumber = document.createElement('p'); 604 let accessoryDetails = document.createElement('span'); 605 accessoryDetails.textContent = cartLineHeadStart 606 .querySelector('h3.cart-line__sku > span') 607 ?.textContent.split('- ')[1]; 608 accessoryDetails.classList.add('product_number'); 609 headingContainer.classList.add('product-details-line-height'); 610 PKUNumber.textContent = accessoryName.split('- ')[0]; 611 PKUNumber.style.fontSize = '1.2rem'; 612 PKUNumber.style.marginBottom = '2px'; 613 if (!/^\d/.test(PKUNumber.textContent)) PKUNumber.style.fontSize = '2rem'; 614 const splitAccesoryArray = accessoryName.split('- '); 615 const accesoryText = splitAccesoryArray[splitAccesoryArray.length - 1] || accessoryName; 616 accessoryNameElement.textContent = accesoryText; 617 if (productCode.nextElementSibling.innerText === '') { 618 headingContainer.appendChild(PKUNumber); 619 headingContainer.appendChild(accessoryDetails); 620 } 621 } 622 });
623 } 624 625 hideEmptyModelText() { 626 [...document.querySelectorAll('.product-item__modelText')].forEach((modelText) => { 627 if (modelText.textContent === '' && modelText.previousElementSibling.textContent !== '') { 628 modelText.parentNode.removeChild(modelText); 629 } 630 }); 631 } 632 633 addEarmoldSideToTitle() { 634 [...document.querySelectorAll('h3.cart-line__sku > span')].forEach((productInfo) => { 635 if (productInfo.textContent !== '') { 636 const side = productInfo.textContent.split(' - ', 1)[0]; 637 const productDetails = productInfo.parentNode.nextElementSibling; 638 if (!/^\d/.test(side)) 639 productDetails.lastElementChild.innerText = `${side}, ${productDetails.lastElementChild.innerText}`; 640 } 641 }); 642 } 643 644 updateDeleteButtonAlignment() { 645 [...document.querySelectorAll('.cart-line__configuration')].forEach((removeIcon, index) => { 646 removeIcon?.appendChild([...document.querySelectorAll('.js-cartline-remove')][index]); 647 }); 648 } 649}
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.