PageSourceSearch

https://www.cookshack.com/mm5/json.mvc?Store_Code=CS&Function=Java…JavaScriptResource_Code=mmx-card&T=63739f74

js cookshack.com collected 2026-09-26 02:32:00 UTC 36,274 bytes, 1,305 lines download raw bytes

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.