import React, { memo, useCallback, useEffect, useMemo, useState } from "react"; import Tooltip from "./Tooltip"; import { useSwatchSettings } from "../hooks/useSwatchSettings"; interface SwatchData { type: string; color: string; label: string; hide_label?: boolean; } type SwatchOption = { label: string; value: string; disabled: boolean; }; interface SwatchGroupProps { attribute: string; select: HTMLSelectElement; swatches: Record; settingsOverride?: Record; } const SHAPE_CLASS = "b3-wvs-rounded-none"; const SwatchGroup: React.FC = ({ select, swatches, settingsOverride, }) => { const [options, setOptions] = useState([]); const [selectedValue, setSelectedValue] = useState(select.value || ""); const settings: Record = useMemo(() => { if (settingsOverride && typeof settingsOverride === "object") { return settingsOverride; } return typeof b3WvsData !== "undefined" && b3WvsData?.settings ? b3WvsData.settings : {}; }, [settingsOverride]); const { tooltipsEnabled, layoutPaddingTop, layoutPaddingRight, layoutPaddingBottom, layoutPaddingLeft, externalPaddingTop, externalPaddingRight, externalPaddingBottom, externalPaddingLeft, paddingTop, paddingRight, paddingBottom, paddingLeft, showLabelsColors, selectedColor, swatchWidth, swatchHeight, buttonSwatchWidth, buttonSwatchHeight, swatchGap, labelFontSize, } = useSwatchSettings(settings); useEffect(() => { let observer: MutationObserver | null = null; const form = select.closest( "form.variations_form", ) as HTMLFormElement | null; const jq = typeof window !== "undefined" ? (window as any).jQuery : undefined; const $form = form && typeof jq === "function" ? jq(form) : null; const triggerChange = () => { try { select.dispatchEvent(new Event("change", { bubbles: true })); } catch { const ev = document.createEvent("Event"); ev.initEvent("change", true, true); select.dispatchEvent(ev); } }; const updateOptions = () => { const nextOptions = Array.from(select.options).reduce( (acc, option) => { const value = option.value || ""; if (!value) { return acc; } acc.push({ label: ( option.getAttribute("data-label") || option.textContent || value ).trim(), value, disabled: Boolean(option.disabled), }); return acc; }, [], ); setOptions(nextOptions); const currentValue = select.value || ""; if ( currentValue !== "" && !nextOptions.find((item) => item.value === currentValue) ) { select.value = ""; triggerChange(); } setSelectedValue(select.value || ""); }; const syncFromWooEvent = () => { window.setTimeout(() => { updateOptions(); }, 0); }; updateOptions(); const handleChange = () => { setSelectedValue(select.value || ""); updateOptions(); }; const handleResetClick = (event: Event) => { const target = event.target as HTMLElement | null; if (!target || !form) { return; } const resetLink = target.closest(".reset_variations"); if (!resetLink || !form.contains(resetLink)) { return; } syncFromWooEvent(); }; select.addEventListener("change", handleChange); document.addEventListener("click", handleResetClick); if ($form && typeof $form.on === "function") { $form.on( "update_variation_values.b3wvs woocommerce_update_variation_values.b3wvs found_variation.b3wvs reset_data.b3wvs hide_variation.b3wvs", syncFromWooEvent, ); } observer = new MutationObserver(updateOptions); observer.observe(select, { childList: true, subtree: true, attributes: true, attributeFilter: ["disabled", "value"], }); return () => { select.removeEventListener("change", handleChange); document.removeEventListener("click", handleResetClick); if ($form && typeof $form.off === "function") { $form.off( "update_variation_values.b3wvs woocommerce_update_variation_values.b3wvs found_variation.b3wvs reset_data.b3wvs hide_variation.b3wvs", syncFromWooEvent, ); } observer?.disconnect(); }; }, [select]); const layoutPadding = useMemo( () => `${layoutPaddingTop}px ${layoutPaddingRight}px ${layoutPaddingBottom}px ${layoutPaddingLeft}px`, [ layoutPaddingBottom, layoutPaddingLeft, layoutPaddingRight, layoutPaddingTop, ], ); const outerPadding = useMemo( () => `${externalPaddingTop}px ${externalPaddingRight}px ${externalPaddingBottom}px ${externalPaddingLeft}px`, [ externalPaddingBottom, externalPaddingLeft, externalPaddingRight, externalPaddingTop, ], ); const swatchPadding = useMemo( () => `${paddingTop}px ${paddingRight}px ${paddingBottom}px ${paddingLeft}px`, [paddingBottom, paddingLeft, paddingRight, paddingTop], ); const gapStyle = useMemo(() => ({ gap: `${swatchGap}px` }), [swatchGap]); const labelTextStyle = useMemo(() => { const labelTextColorOverride = typeof settings.label_text_color === "string" ? settings.label_text_color.trim() : ""; return { fontSize: `${labelFontSize}px`, lineHeight: 1, ...(labelTextColorOverride ? { color: labelTextColorOverride } : {}), } as React.CSSProperties; }, [labelFontSize, settings.label_text_color]); const getTextColorForBackground = useCallback((bg: string | undefined) => { if (!bg) { return undefined; } const hex = bg.trim(); const hexMatch = hex.match(/^#?([0-9a-fA-F]{6})$/); if (hexMatch) { const value = hexMatch[1]; const r = parseInt(value.slice(0, 2), 16); const g = parseInt(value.slice(2, 4), 16); const b = parseInt(value.slice(4, 6), 16); const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; return luminance < 0.55 ? "#ffffff" : "#111827"; } return undefined; }, []); const handleSwatchClick = useCallback( (value: string, isDisabled: boolean) => { if (isDisabled) { return; } select.value = value; const jq = typeof window !== "undefined" ? (window as any).jQuery : undefined; if (typeof jq === "function") { jq(select).trigger("change"); } else { try { select.dispatchEvent(new Event("change", { bubbles: true })); } catch { const event = document.createEvent("Event"); event.initEvent("change", true, true); select.dispatchEvent(event); } } setSelectedValue(value); }, [select], ); const renderTooltip = useCallback( (text: string | undefined, child: React.ReactElement, key: string) => ( {child} ), [tooltipsEnabled], ); const renderSwatch = useCallback( (option: SwatchOption) => { const data = swatches[option.value]; const isDisabled = option.disabled; const isSelected = selectedValue === option.value; const isColor = data?.type === "color"; const shouldShowLabel = isColor ? Boolean(showLabelsColors && !data?.hide_label) : true; const label = data?.label || option.label; const hasColor = typeof data?.color === "string" && data.color.trim(); const showSelectedIndicator = isSelected; if (isColor) { return renderTooltip( label,
, option.value, ); } return renderTooltip( label,
{showSelectedIndicator ? (
) : null}
, option.value, ); }, [ buttonSwatchHeight, buttonSwatchWidth, getTextColorForBackground, handleSwatchClick, labelTextStyle, renderTooltip, selectedColor, selectedValue, showLabelsColors, swatchHeight, swatchPadding, swatchWidth, swatches, ], ); return (
{options.map((option) => (
{renderSwatch(option)}
))}
); }; export default memo(SwatchGroup);