1/** 2 * MMX / CARD 3 */ 4class MMX_Card extends MMX_Element { 5 6 static get props() { 7 return { 8 href: { 9 allowAny: true 10 }, 11 target: { 12 allowAny: true 13 } 14 }; 15 } 16 17 styleResourceCodes = ['mmx-base', 'mmx-card']; 18 renderUniquely = true; 19 20 constructor() { 21 super(); 22 this.makeComponent(); 23 } 24 25 render() { 26 return /*html*/` 27 <a 28 part="wrapper" 29 class="mmx-card" 30 ${this.#renderHref()} 31 ${this.#renderTarget()} 32 ${this.inheritAttrs()} 33 > 34 <slot name="flag" part="flag" class="mmx-card__flag"></slot> 35 <slot name="header" part="header" class="mmx-card__header"></slot> 36 <slot name="main" part="main" class="mmx-card__main"></slot> 37 <slot name="footer" part="footer" class="mmx-card__footer"></slot> 38 </a> 39 `; 40 } 41 42 #renderHref() { 43 const href = this.getPropValue('href'); 44 return typeof href === 'string' ? `href="${MMX.encodeEntities(href)}"` : ''; 45 } 46 47 #renderTarget() { 48 const target = this.getPropValue('target'); 49 return typeof target === 'string' ? `target="${MMX.encodeEntities(target)}"` : ''; 50 } 51 52 getCustomFieldValueFromRecord(customField = '', record = {}) { 53 return MMX.getCustomFieldValueFromRecord(customField, record); 54 } 55} 56 57if (!customElements.get('mmx-card')) { 58 customElements.define('mmx-card', MMX_Card); 59} 60 61/** 62 * MMX / PRODUCT CARD 63 */ 64class MMX_ProductCard extends MMX_Card { 65 66 static get props() { 67 return MMX.assign(MMX_ProductCard.productCardProps, MMX_Card.props); 68 } 69 70 static productCardProps = { 71 'fallback-image': { 72 allowAny: true, 73 default: 'graphics/en-US/cssui/blank.gif' 74 }, 75 'adpr-url': { 76 allowAny: true, 77 default: MMX.longMerchantUrl({Screen: 'BASK'}) 78 }, 79 'atwl-url': { 80 allowAny: true, 81 default: MMX.longMerchantUrl({Screen: 'WISH'}) 82 }, 83 'show-image': { 84 isBoolean: true, 85 default: true 86 }, 87 'image-type': { 88 allowAny: true, 89 default: 'main' 90 }, 91 'image-dimensions': { 92 allowAny: true, 93 default: '350x350' 94 }, 95 'image-fit': { 96 allowAny: true, 97 options: [ 98 'cover', 99 'contain', 100 'fill', 101 'none', 102 'scale-down' 103 ], 104 default: 'contain' 105 } 106 }; 107 108 styleResourceCodes = ['mmx-base', 'mmx-text', 'mmx-card']; 109 renderUniquely = true; 110 111 #product; 112 #card; 113 #details = [ 114 { 115 type: { value: 'name' } 116 }, 117 { 118 type: { value: 'price' }, 119 price: { 120 displayed: { 121 value: 'sale' 122 }, 123 additional: { 124 value: 'base' 125 } 126 } 127 }, 128 ]; 129 130 constructor() { 131 super(); 132 } 133 134 static create({product, card, details, ...options} = {}) { 135 return MMX.createElement({ 136 type: 'mmx-product-card', 137 data: { 138 product, 139 card, 140 details 141 }, 142 ...options 143 }); 144 } 145 146 #swatchAttribute = {}; 147 148 onDataChange() { 149 this.#product = this.data?.product ?? this.#product; 150 this.#card = this.data?.card ?? this.#card; 151 this.#details = this.data?.details ?? this.#card?.details?.children ?? this.#details; 152 this.#swatchAttribute = this.#getSwatchAttribute(); 153 } 154 155 render() { 156 if (!this.#product) { 157 return ''; 158 } 159 160 return /*html*/` 161 <mmx-card 162 part="wrapper" 163 exportparts="wrapper:card-wrapper, flag:card-flag, header:card-header, main:card-main, footer:card-footer" 164 data-href="${MMX.encodeEntities(this.getPropValue('href') ?? this.#product?.url)}" 165 data-target="${MMX.encodeEntities(this.getPropValue('target'))}" 166 > 167 ${this.#renderImage()} 168 ${this.#details?.map?.(detail => this.#renderProductDetail(detail)).join('')} 169 ${this.#renderAddToWishlist()} 170 </mmx-card> 171 `; 172 } 173 174 afterRender() { 175 this.#bindEvents(); 176 } 177 178 #bindEvents() { 179 this.#listenForSwatchButtonInteractions(); 180 this.#listenForHover(); 181 } 182 183 #listenForHover() { 184 if (!this.#isHoverEnabled()) { 185 return; 186 } 187 188 const imageContainer = '[part~="image-container"]'; 189 190 this.on('mouseover, touchstart', imageContainer, () => { 191 this.#setHovered(true); 192 }); 193 194 this.on('mouseout, touchend, touchcancel', imageContainer, () => { 195 this.#setHovered(false); 196 }); 197 198 this.#maintainHoverImageOnDrag(); 199 } 200 201 #isHoverEnabled() { 202 const mainImage = this.#getProductResizedImage(); 203 204 return !!this.#getHoverImage(mainImage)?.url; 205 } 206 207 #setHovered(isHovered) { 208 this.classList.toggle('mmx-product-card__is-hovered', isHovered); 209 210 if (isHovered) { 211 this.#loadHoverImage(); 212 } 213 } 214 215 #maintainHoverImageOnDrag() {
216 this.style.touchAction = 'none'; 217 218 const card = this.shadowRoot?.querySelector?.('mmx-card'); 219 220 if (card) { 221 card.style.touchAction = 'none'; 222 } 223 } 224 225 #renderProductDetail(detail) { 226 const type = detail?.type?.value; 227 228 if (type === 'price') { 229 return this.#renderPrice(detail); 230 } 231 232 if (type === 'rating') { 233 return this.#renderRating(detail); 234 } 235 236 if (['button__view-product', 'button__add-to-cart', 'button__add-to-wishlist'].includes(type)) { 237 return this.#renderButton(detail); 238 } 239 240 if (type === 'customfield') { 241 return this.#renderCustomField(detail); 242 } 243 244 if (type === 'fragment') { 245 return this.#renderFragmentDetail(detail); 246 } 247 248 if (type === 'discounts') { 249 return this.#renderDiscounts(detail); 250 } 251 252 if (type === 'inv_available') { 253 return this.#renderInventoryAvailable(detail); 254 } 255 256 if (type === 'weight') { 257 return this.#renderWeight(detail); 258 } 259 260 return this.#renderCoreDetail(detail); 261 } 262 263 // Render: Core Detail 264 #renderCoreDetail(detail = {}) { 265 const type = detail?.type?.value; 266 267 return this.#renderTextDetail({ 268 value: this.#product?.[type], 269 detail 270 }); 271 } 272 273 // Render: Text Detail 274 #renderTextDetail({value, detail} = {}) { 275 const prefix = detail?.prefix?.value; 276 const suffix = detail?.suffix?.value; 277 278 if (MMX.valueIsEmpty(value)) { 279 return this.#renderEmptyDetail(); 280 } 281 282 const theme_available = detail?.text_styles?.typography_theme?.theme_available ?? false; 283 284 return /*html*/` 285 <mmx-text 286 slot="main" 287 part="detail ${MMX.encodeEntities(detail?.type?.value)}" 288 data-theme="${MMX.encodeEntities(theme_available)}" 289 data-theme-class="${MMX.encodeEntities(detail?.text_styles?.typography_theme?.classname ?? '')}" 290 data-style="${MMX.encodeEntities(detail?.text_styles?.style?.value)}" 291 data-align="${MMX.encodeEntities(detail?.text_styles?.text_align?.value)}" 292 > 293 ${this.renderLegacyStylesTemplate(theme_available, this.getStylesFromGroup(detail?.text_styles))} 294 ${this.renderThemeStylesheetTemplate(theme_available)} 295 ${this.#renderProductDetailSpan('prefix', prefix)} 296 ${MMX.encodeEntities(value)}
297 ${this.#renderProductDetailSpan('suffix', suffix)} 298 </mmx-text> 299 `; 300 } 301 302 #renderProductDetailSpan(type, value) { 303 if (MMX.valueIsEmpty(value)) { 304 return ''; 305 } 306 307 return /*html*/` 308 <span part="detail-${MMX.encodeEntities(type)}"> 309 ${MMX.encodeEntities(value)} 310 </span> 311 `; 312 } 313 314 // Render: Empty Detail (keeps index-based preview_property_selectors working) 315 #renderEmptyDetail() { 316 return /*html*/` 317 <div slot="main" part="detail empty" class="mmx-product-card__detail--empty"></div> 318 `; 319 } 320 321 // Render: Rating 322 #renderRating(detail = {}) { 323 const rating = this.getCustomFieldValueFromRecord(detail?.rating_settings?.rating_custom_field?.value, this.#product); 324 const count = this.getCustomFieldValueFromRecord(detail?.rating_settings?.count_custom_field?.value, this.#product); 325 const iconSet = detail?.rating_settings?.icon?.value; 326 const maxRating = detail?.rating_settings?.max_rating?.value; 327 const activeColor = detail?.rating_settings?.active_color?.value; 328 const inactiveColor = detail?.rating_settings?.inactive_color?.value; 329 const textAlign = detail?.rating_settings?.align?.value; 330 331 return /*html*/` 332 <div 333 slot="main" 334 part="detail reviews-rating" 335 class="mmx-product-card__reviews-rating" 336 style="${textAlign ? `text-align: ${MMX.encodeEntities(textAlign)};` : ''}" 337 > 338 <mmx-rating 339 data-rating="${MMX.encodeEntities(rating)}" 340 data-count="${MMX.encodeEntities(count)}" 341 data-max-rating="${MMX.encodeEntities(maxRating)}" 342 data-icon-set="${MMX.encodeEntities(iconSet)}" 343 data-active-color="${MMX.encodeEntities(activeColor)}" 344 data-inactive-color="${MMX.encodeEntities(inactiveColor)}" 345 ></mmx-rating> 346 </div> 347 `; 348 } 349 350 #hasNameDetail() { 351 return Boolean(this.#details?.some?.(detail => detail?.type?.value === 'name')); 352 } 353 354 #mainImageAlt() { 355 return this.#hasNameDetail() ? '' : (this.#product?.name ?? ''); 356 } 357 358 // Render: Image 359 #renderImage() { 360 if (!this.getPropValue('show-image')) { 361 return ''; 362 } 363 364 const mainImage = this.#getProductResizedImage(); 365 366 if (typeof mainImage?.url !== 'string') { 367 return ''; 368 } 369 370 const hoverImage = this.#getHoverImage(mainImage); 371 372 return /*html*/` 373 <div 374 slot="main" 375 part="image-container" 376 class="mmx-product-card__image-container ${this.#getHoverTransitionClass()}" 377 style="${this.#getImageContainerStyle()}" 378 > 379 <img 380 slot="main" 381 part="image main-image" 382 src="${MMX.encodeEntitiesURI(mainImage.url)}" 383 alt="${MMX.encodeEntities(this.#mainImageAlt())}" 384 width="${MMX.encodeEntities(mainImage.width)}" 385 height="${MMX.encodeEntities(mainImage.height)}" 386 > 387 ${hoverImage?.url ? /*html*/` 388 <img 389 part="image hover-image" 390 data-src="${MMX.encodeEntitiesURI(hoverImage.url)}" 391 alt="" 392 width="${MMX.encodeEntities(hoverImage.width)}" 393 height="${MMX.encodeEntities(hoverImage.height)}" 394 > 395 ` : ''} 396 </div> 397 ${this.#renderAttributeSwatches()} 398 `; 399 } 400 401 #getHoverImage(mainImage) { 402 const hoverType = this.#card?.image?.hover_image?.type?.value; 403 404 if (MMX.valueIsEmpty(hoverType)) { 405 return undefined; 406 } 407 408 const imageType = this.#product?.imagetypes?.[hoverType]; 409 const dimensions = this.getPropValue('image-dimensions'); 410 411 const hasImage = imageType?.sizes?.[dimensions]?.url || imageType?.sizes?.original?.url; 412 413 if (!hasImage) { 414 return undefined; 415 } 416 417 const hoverImage = this.#getProductResizedImage(hoverType); 418 419 if (hoverImage?.url && hoverImage.url === mainImage?.url) { 420 return undefined; 421 } 422 423 return hoverImage; 424 } 425 426 #getHoverTransitionClass() { 427 const transition = (this.#card?.image?.hover_image?.transition?.value ?? 'swap').toLowerCase(); 428 429 return transition === 'fade' ? 'mmx-product-card__hover--fade' : ''; 430 } 431 432 #getImageContainerStyle() { 433 const fit = this.getPropValue('image-fit'); 434 const dimensions = this.getPropValue('image-dimensions'); 435 const [width, height] = dimensions?.split?.('x') ?? [1, 1]; 436 437 return ` 438 --mmx-card__object-fit: ${MMX.encodeEntities(fit)}; 439 --mmx-card__aspect-ratio: ${MMX.encodeEntities(`${width} / ${height}`)}; 440 `.trim(); 441 } 442 443 #loadHoverImage() { 444 const hoverImg = this.#hoverImage(); 445 446 if (!hoverImg) { 447 return; 448 } 449 450 if (hoverImg.getAttribute('src')) { 451 return; 452 } 453 454 const dataSrc = hoverImg.getAttribute('data-src'); 455 456 if (!dataSrc) { 457 return; 458 } 459 460 hoverImg.setAttribute('src', dataSrc); 461 hoverImg.removeAttribute('data-src'); 462 } 463 464 #getProductResizedImage(type = this.getPropValue('image-type')) { 465 const dimensions = this.getPropValue('image-dimensions'); 466 467 const image = this.#product?.imagetypes?.[type]; 468 return image?.sizes?.[dimensions] ?? image?.sizes?.original ?? { 469 url: this.getPropValue('fallback-image'), 470 height: '', 471 width: '' 472 }; 473 } 474 475 // Attribute Swatches 476 #renderAttributeSwatches() { 477 if (!this.#swatchAttribute) { 478 return ''; 479 } 480 481 return /*html*/` 482 <div 483 slot="main" 484 part="attribute-swatches" 485 class="mmx-product-card__attribute-swatches" 486 data-avoid-navigation="true" 487 > 488 ${this.#swatchAttribute?.options?.map(this.#renderAttributeSwatch.bind(this)).join('')} 489 ${this.#renderMoreSwatches()} 490 </div> 491 ${this.#renderAttributeSwatchStyles()} 492 `; 493 } 494 495 #renderAttributeSwatchStyles() { 496 const swatchSize = MMX.composeUnitValue(this.#card?.image?.swatches?.size) ?? '20px'; 497 const swatchBorderRadius = MMX.objectToCssShorthand(this.#card?.image?.swatches?.border_radius, {key: 'border_%corner%_radius'}) ?? '50%'; 498 const swatchBorderWidth = MMX.objectToCssShorthand(this.#card?.image?.swatches?.border_width, {key: 'border_%side%_width'}) ?? '1px'; 499 const swatchBorderColor = this.#card?.image?.swatches?.border_color?.value ?? 'var(--mmx-color-primary-text)'; 500 501 return /*html*/` 502 <style> 503 .mmx-product-card__attribute-swatches { 504 --mmx-product-card__swatch-size: ${MMX.encodeEntities(swatchSize)}; 505 --mmx-product-card__swatch-border-radius: ${MMX.encodeEntities(swatchBorderRadius)}; 506 --mmx-product-card__swatch-border-width: ${MMX.encodeEntities(swatchBorderWidth)}; 507 --mmx-product-card__swatch-border-color: ${MMX.encodeEntities(swatchBorderColor)}; 508 } 509 </style> 510 `; 511 } 512 513 #getSwatchAttribute() { 514 if (MMX.isFalsy(this.#card?.image?.swatches?.settings?.enabled) || MMX.arrayIsEmpty(this.#product?.attributes)) { 515 return undefined; 516 } 517 518 return this.#getFlatAttributes().find(attribute => this.#isSwatchAttribute(attribute)); 519 } 520 521 #getFlatAttributes() { 522 const flatAttributes = []; 523 524 for (const attribute of this.#product.attributes) { 525 if (attribute.type === 'template') { 526 for (const templateAttribute of attribute.attributes) { 527 flatAttributes.push({ 528 ...templateAttribute, 529 id: attribute.id, 530 templateAttributeId: templateAttribute.id 531 }); 532 } 533 } 534 else { 535 flatAttributes.push({ 536 ...attribute 537 }); 538 } 539 } 540 541 return flatAttributes; 542 } 543 544 #isSwatchAttribute(attribute = {}) { 545 if (MMX.isFalsy(attribute?.inventory)) { 546 return false;
547 } 548 549 if (!MMX.valueIsEmpty(this.#card.image?.swatches?.attribute_codes?.value)) { 550 const includedAttributeCodes = MMX.splitString(this.#card.image.swatches.attribute_codes.value); 551 if (!includedAttributeCodes.includes(attribute.code)) { 552 return false; 553 } 554 } 555 556 if (!MMX.valueIsEmpty(this.#card.image?.swatches?.attribute_types?.value)) { 557 const includedAttributeTypes = MMX.splitString(this.#card.image.swatches.attribute_types.value); 558 if (!includedAttributeTypes.includes(attribute.type)) { 559 return false; 560 } 561 } 562 563 return attribute.options?.some?.(option => option.image); 564 } 565 566 #renderAttributeSwatch(option, index) { 567 if (index >= this.#getMaxSwatchCount()) { 568 return ''; 569 } 570 571 return /*html*/` 572 <button 573 type="button" 574 part="attribute-swatch-button" 575 class="mmx-product-card__attribute-swatch-button" 576 data-option-code="${MMX.encodeEntities(option.code)}" 577 title="${MMX.encodeEntities(option.prompt)}" 578 aria-label="Select ${MMX.encodeEntities(option.prompt)}" 579 > 580 <img 581 part="attribute-swatch-image" 582 class="mmx-product-card__attribute-swatch-image" 583 src="${MMX.encodeEntitiesURI(option.image)}" 584 alt="" 585 loading="lazy" 586 > 587 </button> 588 `; 589 } 590 591 #renderMoreSwatches() { 592 const remainingCount = this.#swatchAttribute.options.length - this.#getMaxSwatchCount(); 593 594 if (remainingCount <= 0) { 595 return ''; 596 } 597 598 return /*html*/` 599 <span part="attribute-swatch-more" class="mmx-product-card__attribute-swatch-more">+${remainingCount} more</span> 600 `; 601 } 602 603 #getMaxSwatchCount() { 604 return MMX.coerceNumber(this.#card?.image?.swatches?.max_swatches?.value, 3); 605 } 606 607 #listenForSwatchButtonInteractions() { 608 const eventName = this.#getSwatchChangeEventName(); 609 610 this.on(eventName, '.mmx-product-card__attribute-swatch-button', (e, button) => { 611 this.#onSwatchButtonInteraction(e, button); 612 }); 613 } 614 615 #getSwatchChangeEventName() { 616 return this.#card?.image?.swatches?.change_swatches_on?.value?.toLowerCase?.() === 'hover' 617 ? 'pointerover, click' 618 : 'click'; 619 } 620 621 #onSwatchButtonInteraction(e, button) { 622 if (e.type === 'click') { 623 e.preventDefault(); 624 e.stopPropagation(); 625 } 626 else if (e.type === 'pointerover') { 627 const previousElement = e.relatedTarget; 628 629 if (previousElement && button.contains(previousElement)) { 630 return; 631 } 632 } 633 634 this.#loadVariantFromSelectedSwatchButton(button); 635 } 636 637 #loadVariantFromSelectedSwatchButton(button) { 638 const selections = this.#getSelections(button); 639 640 MMX.Runtime_JSON_API_Call({ 641 cached: true, 642 params: { 643 Function: 'Runtime_AttributeList_Load_ProductVariant_Possible', 644 Product_Code: this.#product.code, 645 Selected_Attribute_IDs: selections.attributeIds, 646 Selected_AttributeTemplateAttribute_IDs: selections.templateAttributeIds, 647 Selected_Option_IDs: selections.optionIds, 648 Selected_Attribute_Types: selections.attributeTypes 649 } 650 }) 651 .then(response => { 652 this.#handleLoadedVariant(response); 653 }); 654 } 655 656 #getSelections(button) { 657 const selections = { 658 attributeTypes: [], 659 attributeIds: [], 660 templateAttributeIds: [], 661 optionIds: [] 662 }; 663 664 this.#getFlatAttributes().forEach(attribute => { 665 if (MMX.isFalsy(attribute?.inventory)) { 666 return; 667 } 668 669 selections.attributeTypes.push(attribute.type); 670 selections.attributeIds.push(attribute.id); 671 672 if (attribute.templateAttributeId) { 673 selections.templateAttributeIds.push(attribute.templateAttributeId); 674 } 675 676 if (attribute.options) { 677 const isSwatchAttribute = attribute.disp_order === this.#swatchAttribute.disp_order; 678 const option = isSwatchAttribute ? this.#getOptionFromSwatchButton(button) : this.#getOptionFromAttribute(attribute); 679 680 selections.optionIds.push(option?.id); 681 } 682 }); 683 684 return selections; 685 } 686 687 #getOptionFromAttribute(attribute) { 688 if (attribute.default_id > 0) { 689 return attribute.options.find(option => option.id === attribute.default_id); 690 } else { 691 return attribute.options.at(0); 692 } 693 } 694 695 #getOptionFromSwatchButton(button) { 696 return this.#swatchAttribute?.options.find(option => option.code === button.dataset.optionCode); 697 } 698 699 #handleLoadedVariant(response) { 700 this.#loadVariantImages(response?.data?.variant?.variant_id); 701 } 702 703 #loadVariantImages(Variant_ID) { 704 MMX.Runtime_JSON_API_Call({ 705 cached: true, 706 params: { 707 Function: 'Runtime_ProductImageList_Load_Product_Variant', 708 Product_Code: this.#product.code, 709 Variant_ID, 710 Image_Sizes: this.getPropValue('image-dimensions') 711 } 712 }) 713 .then(response => { 714 this.#handleLoadedVariantImages(response); 715 }); 716 } 717 718 #handleLoadedVariantImages(response) { 719 const mainImage = this.#findImageByType(response?.data, this.getPropValue('image-type')); 720 this.#updateImageSrc(mainImage?.image_data?.[0]); 721 722 const hoverType = this.#card?.image?.hover_image?.type?.value; 723 const hoverImage = this.#findImageByType(response?.data, hoverType)?.image_data?.[0]; 724 725 if (hoverImage && this.#hoverImage()) { 726 this.#hoverImage().src = hoverImage; 727 } 728 } 729 730 #findImageByType(images = [], type = 'main') { 731 return images?.find?.(image => image?.type_code === type); 732 } 733 734 #updateImageSrc(src) { 735 this.#image().src = src ?? this.#getProductResizedImage().url; 736 } 737 738 #image() { 739 return this.shadowRoot.querySelector('[part~="main-image"]'); 740 } 741 742 #hoverImage() { 743 return this.shadowRoot.querySelector('[part~="hover-image"]'); 744 } 745 746 // Render: Add to Wishlist 747 #renderAddToWishlist() { 748 if (!MMX.isTruthy(this.#card?.add_to_wishlist?.settings?.enabled)) { 749 return ''; 750 } 751 752 if (this.#card?.add_to_wishlist?.login?.method?.value === 'none' && MMX.isTruthy(this.#card?.add_to_wishlist?.needs_login)) { 753 return ''; 754 } 755 756 const added = this.#product?.quantity_in_wishlists > 0 ? true : false;
757 758 return /*html*/` 759 <mmx-atwl-button 760 slot="flag" 761 part="wishlist-button" 762 data-product-code="${MMX.encodeEntities(this.#product.code)}" 763 data-icon-set="${MMX.encodeEntities(this.#card?.add_to_wishlist?.icon?.set?.value)}" 764 data-color="${MMX.encodeEntities(this.#card?.add_to_wishlist?.icon?.color?.value)}" 765 data-added="${added}" 766 > 767 </mmx-atwl-button> 768 `; 769 } 770 771 // Render: Price 772 #renderPrice(detail = {}) { 773 // Price Display 774 this.#product.price_display = this.#product.formatted_sale_price; 775 if (detail.price?.displayed?.value === 'base') { 776 this.#product.price_display = this.#product.formatted_base_price; 777 } 778 else if (detail.price?.displayed?.value === 'retail') { 779 this.#product.price_display = this.#product.formatted_retail; 780 } 781 782 // Additional Price Display 783 this.#product.additional_price_display = ''; 784 if (detail.price?.additional?.value === 'base') { 785 this.#product.additional_price_display = this.#product.formatted_base_price; 786 } 787 else if (detail.price?.additional?.value === 'retail') { 788 this.#product.additional_price_display = this.#product.formatted_retail; 789 } 790 791 const displayed_theme_available = detail?.price_text_styles?.displayed?.typography_theme?.theme_available ?? false; 792 const additional_theme_available = detail?.price_text_styles?.additional?.typography_theme?.theme_available ?? false; 793 794 return /*html*/` 795 <div 796 slot="main" 797 part="detail prices" 798 class="mmx-product-card__prices" 799 style="text-align: ${detail?.price_text_styles?.text_align?.value ?? ''}" 800 > 801 <mmx-text 802 part="price" 803 class="mmx-product-card__price" 804 data-theme="${MMX.encodeEntities(displayed_theme_available)}" 805 data-theme-class="${MMX.encodeEntities(detail?.price_text_styles?.displayed?.typography_theme?.classname ?? '')}" 806 data-style="${MMX.encodeEntities(detail?.price_text_styles?.displayed?.style?.value || 'product-price')}" 807 > 808 ${this.renderLegacyStylesTemplate(displayed_theme_available, this.getStylesFromGroup(detail?.price_text_styles?.displayed))} 809 ${this.renderThemeStylesheetTemplate(displayed_theme_available)} 810 ${this.#product.price_display} 811 </mmx-text> 812 ${this.#product.additional_price_display.length && this.#product.additional_price_display !== this.#product.price_display ? /*html*/` 813 <mmx-text 814 part="additional-price" 815 class="mmx-product-card__additional-price" 816 data-theme="${MMX.encodeEntities(additional_theme_available)}" 817 data-theme-class="${MMX.encodeEntities(detail?.price_text_styles?.additional?.typography_theme?.classname ?? '')}" 818 data-style="${MMX.encodeEntities(detail?.price_text_styles?.additional?.style?.value || 'product-additional-price')}" 819 > 820 ${this.renderLegacyStylesTemplate(additional_theme_available, this.getStylesFromGroup(detail?.price_text_styles?.additional))} 821 ${this.renderThemeStylesheetTemplate(additional_theme_available)} 822 ${this.#product.additional_price_display} 823 </mmx-text> 824 ` : ''} 825 </div> 826 `; 827 } 828 829 // Render: Button 830 #renderButton(detail = {}) { 831 const type = detail?.type?.value ?? 'button__view-product'; 832 833 let link; 834 let text = detail.button?.text?.value; 835 836 if (type === 'button__add-to-cart') { 837 link = this.#addActionToUrl({ 838 url: detail?.ADPR_url?.url ?? this.getPropValue('adpr-url'), 839 action: 'ADPR', 840 product: this.#product 841 }); 842 text = MMX.valueIsEmpty(text) ? 'Add to Cart' : text; 843 } else if (type === 'button__add-to-wishlist') { 844 link = this.#addActionToUrl({ 845 url: detail?.ATWL_url?.url ?? this.getPropValue('atwl-url'), 846 action: 'ATWL', 847 product: this.#product 848 }); 849 text = MMX.valueIsEmpty(text) ? 'Add to Wishlist' : text; 850 } else { 851 link = this.#product.url; 852 text = MMX.valueIsEmpty(text) ? 'View Details' : text; 853 } 854 855 const theme_available = detail.button?.text?.textsettings?.fields?.normal?.button_theme?.theme_available ?? false; 856 857 return /*html*/` 858 <mmx-button 859 slot="main" 860 part="detail button button-${MMX.encodeEntities(type)}" 861 exportparts="button: button-wrapper" 862 href="${MMX.encodeEntities(link)}" 863 data-style="${detail.button?.text?.textsettings?.fields?.normal?.button_style?.value || 'display-link'}" 864 data-size="${detail.button?.text?.textsettings?.fields?.normal?.button_size?.value || 'm'}" 865 data-theme="${theme_available}" 866 data-theme-class="${MMX.encodeEntities(detail.button?.text?.textsettings?.fields?.normal?.button_theme?.classname ?? '')}" 867 data-width="${!theme_available ? 'full' : MMX.encodeEntities(detail.button?.text?.textsettings?.fields?.normal?.button_width?.value ?? 'auto')}" 868 > 869 ${this.renderThemeStylesheetTemplate(theme_available)} 870 ${MMX.encodeEntities(text)} 871 </mmx-button>`; 872 } 873 874 #addActionToUrl({url, action = 'ADPR', quantity = 1}) { 875 let actionUrl = new URL(url, document.baseURI); 876 877 actionUrl.searchParams.append('Action', action); 878 actionUrl.searchParams.append('Product_Code', this.#product.code); 879 actionUrl.searchParams.append('Quantity', quantity); 880 881 return actionUrl.toString(); 882 } 883 884 // Render: Custom Field 885 #renderCustomField(detail = {}) { 886 return this.#renderTextDetail({ 887 value: this.getCustomFieldValueFromRecord(detail?.customfield?.value, this.#product), 888 detail 889 }); 890 } 891 892 // Render: Fragment 893 #renderFragmentDetail(detail = {}) { 894 const fragmentCode = detail?.fragment?.value; 895 const fragmentContent = this.renderProductFragment({ 896 product: this.#product, 897 fragmentCode 898 }); 899 900 if (MMX.valueIsEmpty(fragmentContent)) { 901 return this.#renderEmptyDetail(); 902 } 903 904 let classname = ''; 905 const theme_available = detail?.text_styles?.typography_theme?.theme_available ?? false; 906 907 if (!theme_available) { 908 classname = `class="type-${MMX.encodeEntities(detail?.text_styles?.style?.value)}"`; 909 } 910 911 return /*html*/` 912 <mmx-text 913 slot="main" 914 part="detail fragment fragment__${MMX.encodeEntities(fragmentCode)}" 915 ${classname} 916 data-hide-on-empty="false" 917 data-spacing="none" 918 data-theme="${MMX.encodeEntities(theme_available)}" 919 data-theme-class="${MMX.encodeEntities(detail?.text_styles?.typography_theme?.classname ?? '')}" 920 data-style="${MMX.encodeEntities(detail?.text_styles?.style?.value)}" 921 data-align="${MMX.encodeEntities(detail?.text_styles?.text_align?.value)}" 922 >
923 ${this.renderLegacyStylesTemplate(theme_available, this.getStylesFromGroup(detail?.text_styles))} 924 ${this.renderThemeStylesheetTemplate(theme_available)} 925 ${fragmentContent} 926 </mmx-text> 927 `; 928 } 929 930 // Render: Discounts 931 #renderDiscounts(detail) { 932 if (!Array.isArray(this.#product.discounts) || !this.#product.discounts.length) { 933 return this.#renderEmptyDetail(); 934 } 935 936 return /*html*/` 937 <div 938 slot="main" 939 part="detail discounts" 940 > 941 ${this.#product.discounts.map(discount => this.#renderDiscount({discount, detail})).join('')} 942 </div> 943 `; 944 } 945 946 #renderDiscount({discount = {}, detail = {}} = {}) { 947 if (typeof discount?.descrip !== 'string') { 948 return ''; 949 } 950 951 let classname = ''; 952 const theme_available = detail?.text_styles?.typography_theme?.theme_available ?? false; 953 954 if (!theme_available) { 955 classname = `class="type-${MMX.encodeEntities(detail?.text_styles?.style?.value)}"`; 956 } 957 958 return /*html*/` 959 <mmx-text 960 part="discount" 961 ${classname} 962 data-theme="${MMX.encodeEntities(theme_available)}" 963 data-theme-class="${MMX.encodeEntities(detail?.text_styles?.typography_theme?.classname ?? '')}" 964 data-style="${MMX.encodeEntities(detail?.text_styles?.style?.value)}" 965 data-align="${MMX.encodeEntities(detail?.text_styles?.text_align?.value)}" 966 > 967 ${this.renderLegacyStylesTemplate(theme_available, this.getStylesFromGroup(detail?.text_styles))} 968 ${this.renderThemeStylesheetTemplate(theme_available)} 969 ${discount.descrip}: ${discount.formatted_discount} 970 </mmx-text> 971 `; 972 } 973 974 // Render: Inventory Available 975 #renderInventoryAvailable(detail = {}) { 976 if (!this.#product?.inv_active) { 977 return this.#renderEmptyDetail(); 978 } 979 980 return this.#renderTextDetail({ 981 value: this.#product?.inv_available, 982 detail 983 }); 984 } 985 986 // Render: Weight 987 #renderWeight(detail = {}) { 988 return this.#renderTextDetail({ 989 value: this.#product?.formatted_weight, 990 detail 991 }); 992 } 993} 994 995if (!customElements.get('mmx-product-card')) { 996 customElements.define('mmx-product-card', MMX_ProductCard); 997} 998 999/** 1000 * MMX / CATEGORY CARD 1001 */ 1002class MMX_CategoryCard extends MMX_Card { 1003 1004 static get props() { 1005 return MMX.assign(MMX_CategoryCard.categoryCardProps, MMX_Card.props); 1006 } 1007 1008 static categoryCardProps = { 1009 'fallback-image': { 1010 allowAny: true, 1011 default: 'graphics/en-US/cssui/blank.gif' 1012 }, 1013 'image-type': { 1014 allowAny: true, 1015 options: [ 1016 'cattree', 1017 'cattitle', 1018 'customfield' 1019 ], 1020 default: 'cattree' 1021 }, 1022 'image-fit': { 1023 allowAny: true, 1024 options: [ 1025 'cover', 1026 'contain', 1027 'fill', 1028 'none', 1029 'scale-down' 1030 ], 1031 default: 'contain' 1032 } 1033 }; 1034 1035 styleResourceCodes = ['mmx-base', 'mmx-text', 'mmx-card']; 1036 renderUniquely = true; 1037 1038 #category; 1039 #details = [ 1040 { 1041 type: { value: 'image' } 1042 }, 1043 { 1044 type: { value: 'name' } 1045 } 1046 ]; 1047 #styles = {}; 1048 1049 constructor() { 1050 super(); 1051 } 1052 1053 static create({category, details, styles, ...options}) { 1054 return MMX.createElement({ 1055 type: 'mmx-category-card', 1056 data: { 1057 category, 1058 details, 1059 styles 1060 }, 1061 ...options 1062 }); 1063 } 1064 1065 render() { 1066 if (!this.#category) { 1067 return ''; 1068 } 1069 1070 return /*html*/` 1071 <mmx-card 1072 part="wrapper" 1073 exportparts="wrapper:card-wrapper, flag:card-flag, header:card-header, main:card-main, footer:card-footer" 1074 data-href="${MMX.encodeEntities(this.getPropValue('href') ?? this.#category?.url)}" 1075 data-target="${MMX.encodeEntities(this.getPropValue('target'))}" 1076 style="${MMX.encodeEntities(this.#getStyles())}" 1077 > 1078 ${this.#renderCategoryDetails()} 1079 </mmx-card> 1080 `; 1081 } 1082 1083 onDataChange() { 1084 this.#category = this.data?.category ?? this.#category; 1085 this.#details = this.data?.details ?? this.#details; 1086 this.#styles = this.data?.styles ?? this.#styles; 1087 } 1088 1089 // Details 1090 #renderCategoryDetails() { 1091 return this.#details?.map?.(detail => { 1092 return this.#renderCategoryDetail(detail); 1093 }).join(''); 1094 } 1095 1096 #renderCategoryDetail(detail = {}) { 1097 switch (detail?.type?.value) { 1098 case 'button__view-category': 1099 return this.#renderButton(detail); 1100 case 'customfield': 1101 return this.#renderCustomField(detail); 1102 case 'fragment': 1103 return this.#renderFragmentDetail(detail); 1104 case 'image': 1105 return this.#renderImage(detail); 1106 default: 1107 return this.#renderCoreDetail(detail); 1108 } 1109 } 1110 1111 // Render: Button 1112 #renderButton(detail = {}) { 1113 const className = MMX.coerceString(detail.button?.text?.textsettings?.fields?.normal?.button_theme?.classname); 1114 const link = this.#category.url; 1115 const size = MMX.coerceString(detail.button?.text?.textsettings?.fields?.normal?.button_size?.value, {fallback: 'm'}); 1116 const style = MMX.coerceString(detail.button?.text?.textsettings?.fields?.normal?.button_style?.value, {fallback: 'display-link'}); 1117 const text = MMX.coerceString(detail.button?.text?.value, {fallback: 'Shop Now'}); 1118 const theme_available = detail.button?.text?.textsettings?.fields?.normal?.button_theme?.theme_available ?? false; 1119 const width = MMX.coerceString(detail.button?.text?.textsettings?.fields?.normal?.button_width?.value, {fallback: 'auto'}); 1120 1121 return /*html*/` 1122 <mmx-button 1123 slot="main" 1124 part="detail button ${MMX.encodeEntities(detail?.type?.value)}" 1125 exportparts="button: button-wrapper" 1126 href="${MMX.encodeEntities(link)}" 1127 data-style="${MMX.encodeEntities(style)}" 1128 data-size="${MMX.encodeEntities(size)}" 1129 data-theme="${MMX.encodeEntities(theme_available)}" 1130 data-theme-class="${MMX.encodeEntities(className)}" 1131 data-width="${!theme_available ? 'full' : MMX.encodeEntities(width)}" 1132 > 1133 ${this.renderThemeStylesheetTemplate(theme_available)} 1134 ${MMX.encodeEntities(text)} 1135 </mmx-button>`; 1136 } 1137 1138 // Render: Core Detail 1139 #renderCoreDetail(detail = {}) { 1140 const type = detail?.type?.value; 1141 const value = this.#category?.[type]; 1142 1143 return this.#renderTextDetail({value, detail}); 1144 } 1145 1146 // Render: Custom Field 1147 #renderCustomField(detail = {}) { 1148 return this.#renderTextDetail({ 1149 value: this.getCustomFieldValueFromRecord(detail?.customfield?.value, this.#category), 1150 detail 1151 }); 1152 } 1153 1154 // Render: Empty Detail (keeps index-based preview_property_selectors working) 1155 #renderEmptyDetail() { 1156 return /*html*/` 1157 <div slot="main" part="detail empty"></div> 1158 `; 1159 } 1160 1161 // Render: Fragment 1162 #renderFragmentDetail(detail = {}) { 1163 const fragmentCode = detail?.fragment?.value; 1164 const fragmentContent = this.renderProductFragment({ 1165 product: this.#category, 1166 fragmentCode 1167 }); 1168 1169 if (MMX.valueIsEmpty(fragmentContent)) { 1170 return this.#renderEmptyDetail(); 1171 } 1172 1173 let classname = ''; 1174 const theme_available = detail?.text_styles?.typography_theme?.theme_available ?? false; 1175 1176 if (!theme_available) { 1177 classname = `class="type-${MMX.encodeEntities(detail?.text_styles?.style?.value)}"`; 1178 } 1179 1180 return /*html*/` 1181 <mmx-text 1182 slot="main" 1183 part="detail fragment fragment__${MMX.encodeEntities(fragmentCode)}" 1184 ${classname} 1185 data-hide-on-empty="false" 1186 data-theme="${MMX.encodeEntities(theme_available)}" 1187 data-theme-class="${MMX.encodeEntities(detail?.text_styles?.typography_theme?.classname ?? '')}" 1188 data-style="${MMX.encodeEntities(detail?.text_styles?.style?.value)}" 1189 data-align="${MMX.encodeEntities(detail?.text_styles?.text_align?.value)}" 1190 > 1191 ${this.renderThemeStylesheetTemplate(theme_available)} 1192 ${fragmentContent} 1193 </mmx-text> 1194 `; 1195 } 1196 1197 #hasNameDetail() { 1198 return Boolean(this.#details?.some?.(detail => detail?.type?.value === 'name')); 1199 } 1200 1201 #imageAlt() { 1202 return this.#hasNameDetail() ? '' : (this.#category?.name ?? ''); 1203 } 1204 1205 // Render: Image 1206 #renderImage(detail) { 1207 const imageType = detail?.image_settings?.type?.value ?? 'cattree'; 1208 const imageFit = detail?.image_settings?.fit?.value ?? 'cover'; 1209 const dimensions = detail?.image_settings?.dimensions?.value; 1210 const [width, height] = dimensions?.split?.('x') ?? []; 1211 const hasDimensions = MMX.coerceNumber(width) && MMX.coerceNumber(height); 1212 1213 let image = ''; 1214 1215 if (imageType === 'cattree') { 1216 image = this.#category?.CustomField_Values?.['cmp-cssui-cattree'].category_tree_image; 1217 } else if (imageType === 'cattitle') { 1218 image = this.#category?.CustomField_Values?.['cmp-cssui-cattitle'].category_title_image; 1219 } 1220 1221 if (MMX.valueIsEmpty(image)) { 1222 image = this.getPropValue('fallback-image'); 1223 } 1224 1225 const content = /*html*/` 1226 <img 1227 slot="main" 1228 part="detail ${MMX.encodeEntities(detail?.type?.value)}" 1229 src="${MMX.encodeEntitiesURI(image)}" 1230 alt="${MMX.encodeEntities(this.#imageAlt())}" 1231 ${hasDimensions ? `width="${MMX.encodeEntities(width)}" height="${MMX.encodeEntities(height)}"` : ''} 1232 style=" 1233 object-fit: ${MMX.encodeEntities(imageFit)}; 1234 ${hasDimensions ? `aspect-ratio: ${MMX.encodeEntities(width)} / ${MMX.encodeEntities(height)}; width: 100%; height: auto;` : ''} 1235 " 1236 > 1237 `; 1238 1239 return content; 1240 } 1241 1242 // Render: Text Detail 1243 #renderTextDetail({value, detail} = {}) { 1244 const prefix = detail?.prefix?.value; 1245 const suffix = detail?.suffix?.value; 1246 1247 if (MMX.valueIsEmpty(value)) { 1248 return this.#renderEmptyDetail(); 1249 } 1250 1251 const theme_available = detail?.text_styles?.typography_theme?.theme_available ?? false; 1252 1253 return /*html*/` 1254 <mmx-text 1255 slot="main" 1256 part="detail ${MMX.encodeEntities(detail?.type?.value)}" 1257 data-theme="${MMX.encodeEntities(theme_available)}" 1258 data-theme-class="${MMX.encodeEntities(detail?.text_styles?.typography_theme?.classname ?? '')}" 1259 data-style="${MMX.encodeEntities(detail?.text_styles?.style?.value)}" 1260 data-align="${MMX.encodeEntities(detail?.text_styles?.text_align?.value)}" 1261 > 1262 ${this.renderThemeStylesheetTemplate(theme_available)} 1263 ${this.#renderTextDetailSpan('prefix', prefix)} 1264 ${value}
1265 ${this.#renderTextDetailSpan('suffix', suffix)} 1266 </mmx-text> 1267 `; 1268 } 1269 1270 #renderTextDetailSpan(type, value) { 1271 if (MMX.valueIsEmpty(value)) { 1272 return ''; 1273 } 1274 1275 return /*html*/` 1276 <span part="detail-${MMX.encodeEntities(type)}"> 1277 ${MMX.encodeEntities(value)} 1278 </span> 1279 `; 1280 } 1281 1282 // Styles 1283 #getStyles() { 1284 return MMX.objectToInlineStyles(this.#getStylesObject()); 1285 } 1286 1287 #getStylesObject() { 1288 const borderWidth = MMX.objectToCssShorthand(this.#styles?.border_width, {key: 'border_%side%_width'}); 1289 1290 return { 1291 margin: MMX.objectToCssShorthand(this.#styles?.margin), 1292 padding: MMX.objectToCssShorthand(this.#styles?.padding), 1293 backgroundColor: MMX.coerceString(this.#styles?.background_color?.value), 1294 borderRadius: MMX.objectToCssShorthand(this.#styles?.border_radius, {key: 'border_%corner%_radius'}), 1295 borderColor: MMX.coerceString(this.#styles?.border_color?.value), 1296 boxShadow: MMX.boxShadowObjectToCssValue(this.#styles?.box_shadow), 1297 borderStyle: borderWidth ? 'solid' : undefined, 1298 borderWidth 1299 }; 1300 } 1301} 1302 1303if (!customElements.get('mmx-category-card')) { 1304 customElements.define('mmx-category-card', MMX_CategoryCard); 1305}
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.