import { mergeDeepRight } from "ramda";
import { catchAndLogClientError, deref } from "@stylitics/core";
import * as core from "./core";
import Classic from "../ui/bases/Classic.svelte";
const defaultResponsiveConfig = [
[0, { columns: 1 }],
[640, { columns: 2 }],
[1024, { columns: 3 }],
[1280, { columns: 4 }],
[1536, { columns: 5 }],
];
//OMITTED
//@param {boolean} [params.customer.doNotTrack=false] - Set to true if you do not want the Stylitics Widget to record tracking events of customer interactions with the widget
/**
*/
class StyliticsClassicWidget {
/**
* Initialize a Classic widget object attached to a DOM element on your page. Afterward, when you are ready, call .start() on the object.
*
* @param {string} client - The username given to you by Stylitics
* @param {(HTMLElement|string)} target - The HTML element within which you would like to render the Stylitics Widget. You can pass either the HTMLElement directly or a string that matches the id attribute of the element on your page.
* @param {Object} params
* @param {Object} params.api
* @param {string} [params.api.item_number] - Identifier of the item corresponding to the current PDP page, typically a colorway-level ID. This item_number should be dynamically referenced in your code to reflect the item_number of the PDP/swatch the user has selected. If you do not supply a value for this field you must provide api.tags.
* @param {string} [params.api.tags] - A comma separated tag string. Bundles will be returned that have been given those tags. If you do not supply a value for this field you must provide api.item_number.
* @param {number} [params.api.max=10] - Maximum number of bundles to include in the widget
* @param {number} [params.api.min=1] - Use this to change the bundle count threshold under which the widget is hidden completely. For instance, pass 3 if you never want to display the widget with bundle counts of two or one at any screen size.
* @param {string} [params.api.from_accounts] - Filter bundle results to only return results from a specified list of Stylitics accounts. If you would like to supply multiple accounts please separate the account names with commas like: "some-account-women,some-account-children".
* @param {Array[]} [params.responsive] - See example
* @param {Object} [params.customer]
* @param {string} [params.customer.locale="en-US"] - If en-US is not correct, pass a different Unicode BCP 47 locale identifier string.
* @param {Object} [params.text]
* @param {string} [params.text.viewDetailsCTA] - Override the default text "View details"
* @param {string} [params.text.backToLookCTA] - Override the default "Back to Look"
* @param {string} [params.text.itemLinkCTA] - Override the default text "SHOP"
* @param {string} [params.text.seeMoreOptionsCTA] - Override the default Mix and Match text "See more options"
* @param {string} [params.text.itemDetailsModalHeader] - Override the default text for the mobile item details modal
* @param {string} [params.text.mnmClose] - Override the default text for the Mix & Match Expandable Shelf Close button
* @param {Object} [params.price]
* @param {"floor" | "ceiling" | "round" | "none"} [params.price.roundingStyle = "none"] - Default style is "none". "floor" would round down in every case no matter the decimal count. "ceiling" would round up in every case no matter the decimal count. "round" will round up when equal or greater than .50, and round down when equal or lesser than .49
* @param {boolean} [params.price.hideDoubleZeroCents = false] - Hide decimals that are .00
* @param {"strikethrough" | "sales-price-only"} [params.price.salesPriceStyle="strikethrough"]
* @param {Object} [params.navigation]
* @param {"new-tab" | "same-tab"} [params.navigation.clickItemLinkDesktop="new-tab"] Click on the shop button opens PDP in a new tab or same tab
* @param {"new-tab" | "same-tab"} [params.navigation.clickItemLinkMobile="same-tab"] Tap on the shop button opens PDP in a new tab or same tab
* @param {Object} [params.display]
* @param {boolean} [params.display.clickableCarouselDots=false] - Display carousel-advancement progress indicators (dots) on desktop
* @param {number} [params.display.clickableCarouselAdvancementRate=1] - Set the amount of cards to scroll by: min = 1, max = columnCount
* @param {number} [params.display.clickableCarouselNextItemPeek=0] - Set "peeking" visibility amount of the next (offscreen) carousel-item in pixels
* @param {number} [params.display.clickableCarouselPreviousItemPeek=0] - Set "peeking" visibility amount of the previous (offscreen) carousel-item in pixels
* @param {number} [params.display.swipeableCarouselNextItemPeek=0.275] - Set "peeking" visibility factor of the next (offscreen) mobile swipe gallery item from 0 to 1
* @param {number} [params.display.clickableCarouselGutterWidth=16] - Set gutterWidth for the desktop.
* @param {number} [params.display.swipeableCarouselGutterWidth=16] - Set gutterWidth for the mobile carousel.
* @param {number} [params.display.swipeableCarouselLeftPadding=0] - Set the left-most item margin on mobile swipe gallery
* @param {string} [params.display.bundleBackgroundColor="#fff"] - Set the bundle background color.
* @param {"slideout-on-click" | "product-list-on-click" | "product-list-on-hover"} [params.display.bundleProductList="slideout-on-click"] - Set the product list appearance. Default - slideout-on-click
* @param {boolean} [params.display.disableMnM] - Turns off Mix & Match functionality
* @param {string} [params.trackingEnv] - Send tracking events to "production" or "staging." Defaults to "production" for backwards compatibility
* @param {boolean} [params.display.swipeableCarouselDots = false] -Allows for swipeable carousel dots on mobile
* @param {boolean} [params.display.swipeableCarouselArrows = false] -Allows for swipeable carousel arrows on mobile
* @param {boolean} [params.display.clickableImageMnM = false] -When MnM is enabled allows for bundle item images to click through replacements
* @param {boolean} [params.display.productListMnM = false] -When MnM is enabled allows displaying MnM as a product list
* @param {boolean} [params.display.hideAnchorItem = false] - Hide the anchor item from the product list
*
* @example
* let widgetInstance = new StyliticsClassicWidget("account-name", "stylitics-widget-container", {
* api: {
* item_number: "1234567",
* // other optional API Params
* // tags: "glam,preppy",
* // from_accounts: "demo-men",
* min: 3,
* max: 6,
* },
* display: {
* disableMnM: true,
* }
* // optional config to change default text labels
* // text: {
* // viewDetailsCTA: "Shop Now",
* // itemLinkCTA: "SHOP THIS",
* // example of locale-based text override:
* // itemDetailsModalHeader: {
* // "fr-CA": "Liste de produits"
* // }
* // },
*
* // IMPORTANT information about the following responsive breakpoints param:
* // These breakpoints are NOT based on viewport width but rather the width of the widget container.
* // Only uncomment this section of the config if you plan to change the responsive breakpoint defaults, otherwise you can remove this section.
* // responsive: [
* // [0, { columns: 1 }],
* // [640, { columns: 2 }],
* // [1024, { columns: 3 }],
* // [1280, { columns: 4 }],
* // [1536, { columns: 5 }],
* // ]
* })
* // put calls to widgetInstance.on(...), widgetInstance.override(...) etc. here
* widgetInstance.start()
*
*
*
*/
constructor(client, target, params) {
this.type = "classic";
this.params = { ...params };
this.params.responsive = params.responsive
? core.validatedResponsiveConfig(params.responsive)
: defaultResponsiveConfig;
this.ComponentConstructor = Classic;
core.initializeState({ client, target, widget: this });
}
/**
* Call after initialization and after any calls to .on or .override Starts
* the process of fetching outfits from Stylitics in order to populate the
* widget.
* @example
* widgetInstance.start();
*/
start() {
core.scrollToStylitics(this.target, window.location.hash === "#scroll-to-stylitics");
core.gatherData(this);
}
/**
* Reload the widget passing in new params. Often useful if you would like to switch to a new colorway ID.
* @example
* let newItemNumber = "426088032";
* widgetInstance.refresh({ api: { item_number: newItemNumber } });
*/
refresh(params) {
this.params = mergeDeepRight(this.params, params);
this.start();
}
/**
* Remove the widget completely from the page, should you need to.
*/
destroy() {
this.component?.$destroy?.();
}
bundleByID(id) {
return core.bundleByID(this.DB, id);
}
itemByStyliticsID(id) {
return core.itemByStyliticsID(this.DB, id);
}
itemByRemoteID(id) {
return core.itemByRemoteID(this.DB, id);
}
/**
* Send a tracking event when a user clicks a jumplink
*/
trackClickJumpLink() {
core.trackClickJumpLink(this.engagements);
}
/**
* Send a tracking event when a user adds an item to the cart.
* This method supports two ways of being called.
*
* The first is to provide a context object with an itemId property,
* and optional contextual data, like price, and uiComponent.
* This is the recommended way to use this function.
*
* @deprecated The second way is to provide the item id as the first argument and an optional price as the second argument.
*
* @param {object} event
* @param {string | number} event.itemId
* @param {"main" | "widget"} [event.placement]
* @param {number} [event.price]
* @param {number} [price] Deprecated. Optionally add the price the user sees. Defaults to the sale_price or price of the item. The recommended approach is to pass price as an attribute on the AddToCartEvent argument.
*
* @example
* // recommended
* widgetInstance.trackAddItemToCart({ itemId: "a1b2c3" })
* widgetInstance.trackAddItemToCart({ itemId: "a1b2c3", price: 29.99 })
* widgetInstance.trackAddItemToCart({ itemId: "a1b2c3", price: 29.99, uiComponent: "pdp" })
*
* // deprecated
* widgetInstance.trackAddItemToCart("a1b2c3");
* widgetInstance.trackAddItemToCart("a1b2c3", 29.99);
*/
trackAddItemToCart(event, price /** @deprecated */) {
core.trackAddItemToCart(this.DB, this.engagements, event, price);
}
/**
* Send a tracking event when a user clicks on an item outside of the Stylitics Widget, for instance, in a "quickview".
* @param {string} remoteId The item, identifier typically a colorway-level ID. In our data this the field named `remote_id`
* @example widgetInstance.trackClickItem("13093805");
*/
trackClickItem(remoteId) {
return core.trackItemClick(this.DB, this.engagements, remoteId);
}
/**
* Send a tracking event when a user views an item outside of the Stylitics Widget, for instance, in a "quickview".
* @param {string} remoteId The item, identifier typically a colorway-level ID. In our data this is the field named `remote_id`
* @example widgetInstance.trackViewItem("13093805");
*/
trackViewItem(remoteId) {
return core.trackItemView(this.DB, this.engagements, remoteId);
}
/**
* IMPORTANT: Clients should consult with their account managers before toggling these tracking functions, as it may impact analytics results.
*/
doNotTrack() {
core.doNotTrack(this.DB);
}
doTrack() {
core.doTrack(this.DB);
}
disableLocalStorageAndCookies() {
this.isStorageEnabled = false;
}
enableLocalStorageAndCookies() {
this.isStorageEnabled = true;
}
get isStorageEnabled() {
const isDisabled = deref(this.DB).user?.disableLocalStorageAndCookies ?? false;
return !isDisabled;
}
/** @private */
set isStorageEnabled(newValue) {
newValue ? core.enableLocalStorageAndCookies(this.DB) : core.disableLocalStorageAndCookies(this.DB);
}
/**
* Add a callback to be called for an event type, likely for customization or metrics purposes
* @param {"click" | "view" | "swap" | "mount"} action - Choose one
* @param {"bundle" | "bundles" | "item" | "replacement" | "replacements"} subject - Choose one
* @param {function} f - callback function that will be called with relevant arguments when the event occurs
*
* @example
* // Valid action/subject pairs:
* // "click", "bundle"
* // "click", "item"
* // "view", "bundle"
* // "view", "item"
* // "swap", "item"
* // "mount", "item"
* // "mount", "bundle"
* // "mount", "bundles"
* // "mount", "replacement"
* // "mount", "replacements"
* // "load", "widget" // Deprecated, use "mount", "bundles" instead
* // "load", "replacements" // Deprecated, use "mount", "replacements" instead
*
* widgetInstance.on("click", "item", function (data) {
* console.log("user clicked item:", data)
* });
*
* widgetInstance.on("mount", "bundles", function(data) {
* console.log("bundles response:", data.bundles)
* });
*
* * // Important details regarding Stylitics callbacks
* // All stylitics callbacks must be placed in your code before the .start() method is called.
*
* // To avoid unexpected behavior, do not use multiple instances of the same action-subject pair. For example, you can not have multiple instances of .on("mount", "bundles",) in the code. You can, however, place multiple actions underneath one callback.
* // For example, if you need multiple actions to occur when the widget loads, such as showing the widget title, the jumplink, and checking for any Shop the Model bundles. You can nest all those actions underneath one .on("mount", "bundles",) callback.
* widgetInstance.on("mount", "bundles", function (data) {
* //Checking if outfits are returned and unhiding title and jumplink if so
* if (data.bundles && data.bundles.length) {
* let jumplinkElement = document.getElementById("stylitics-jumplink");
* let titleElement = document.getElementById("widget-title");
* // unhide jumplink after widget has loaded
* jumplinkElement.style.visibility = "visible";
* // unhide widget title after widget has loaded
* titleElement.style.visibility = "visible";
*
* // Checking for bundles that should be tagged shop the model and applying the badge
* let bundle = data.bundle;
* let element = data.element;
* // add if statement to make sure badge is added to the correct bundles
* if (bundle["on-model-image"]) {
* //create image element
* let badge = document.createElement("img");
* // add relative path to the img
* badge.setAttribute("src", "../img/shop_the_look.jpg");
* //append img to the "stylitics-bundle-badge" div
* element.querySelector(".stylitics-bundle-badge").appendChild(badge);
* }
* });
*
* // All callback methods must be added before the .start() method
* widgetInstance.start();
*
*/
on(action, subject, f) {
core.storedEventCallbacks(this.DB, action, subject, catchAndLogClientError(f));
}
/**
* Override the default behavior for an event type. For instance, use this to open a "quickview" on click.
* @param {"click"} action - Only click behaviors can be overriden for now
* @param {"bundle" | "item"} subject - Choose one
* @param {function} f - function that will will be called instead of the default behavior for that action/subject trigger
*
* @example
* widgetInstance.override("click", "item", function ({ item, bundle }) {
* openQuickView(item);
* })
*/
override(action, subject, f) {
core.storedEventOverrides(this.DB, action, subject, catchAndLogClientError(f));
}
/**
* Print information about the widget to the browser console
*
* @example
* StyliticsClassicWidget.info()
*/
static info() {
core.info();
}
}
window.StyliticsClassicWidget = StyliticsClassicWidget;
export default StyliticsClassicWidget;