@inappstory/slide-api 0.1.40 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1989,6 +1989,65 @@ class Products {
1989
1989
  static get [Symbol.for("___CTOR_ARGS___")]() { return [`HTMLElement`, `Layer`, `typeof WidgetProducts.api`, `WidgetCallbacks`, `WidgetDeps`]; }
1990
1990
  }
1991
1991
 
1992
+ class ProductCarousel {
1993
+ _elementNodeRef;
1994
+ _layer;
1995
+ _widgetApi;
1996
+ _widgetCallbacks;
1997
+ _widgetDeps;
1998
+ static _className = "narrative-element-product-carousel";
1999
+ static className() {
2000
+ return ProductCarousel._className;
2001
+ }
2002
+ static isTypeOf(element) {
2003
+ return element instanceof ProductCarousel;
2004
+ }
2005
+ constructor(_elementNodeRef, _layer, _widgetApi, _widgetCallbacks, _widgetDeps) {
2006
+ this._elementNodeRef = _elementNodeRef;
2007
+ this._layer = _layer;
2008
+ this._widgetApi = _widgetApi;
2009
+ this._widgetCallbacks = _widgetCallbacks;
2010
+ this._widgetDeps = _widgetDeps;
2011
+ }
2012
+ mediaElementsLoadingPromises = [];
2013
+ get nodeRef() {
2014
+ return this._elementNodeRef;
2015
+ }
2016
+ init(localData) {
2017
+ try {
2018
+ this._widgetApi.init(this._elementNodeRef, localData, this._widgetCallbacks, this._widgetDeps);
2019
+ }
2020
+ catch (e) {
2021
+ console.error(e);
2022
+ }
2023
+ return Promise.resolve(true);
2024
+ }
2025
+ onPause() { }
2026
+ onResume() { }
2027
+ onStart() {
2028
+ this._widgetApi.onStart(this._elementNodeRef);
2029
+ }
2030
+ onStop() {
2031
+ this._widgetApi.onStop(this._elementNodeRef);
2032
+ }
2033
+ onBeforeUnmount() {
2034
+ return Promise.resolve();
2035
+ }
2036
+ handleClick() {
2037
+ return false;
2038
+ }
2039
+ get isClickCapturedByWidget() {
2040
+ return this._widgetApi.isClickCapturedByWidget(this._elementNodeRef);
2041
+ }
2042
+ get isLayerForcePaused() {
2043
+ return this._widgetApi.isForcePaused(this._elementNodeRef);
2044
+ }
2045
+ get elementNodeRef() {
2046
+ return this._elementNodeRef;
2047
+ }
2048
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`HTMLElement`, `Layer`, `typeof WidgetProductCarousel.api`, `WidgetCallbacks`, `WidgetDeps`]; }
2049
+ }
2050
+
1992
2051
  class Quest {
1993
2052
  _elementNodeRef;
1994
2053
  _layer;
@@ -3468,6 +3527,10 @@ const tryCreateFromHtmlElement = (nodeRef, layer, widgetCallbacks, widgetDeps) =
3468
3527
  return layoutApi.widgetVoteApi ? new Vote(nodeRef, layer, layoutApi.widgetVoteApi, widgetCallbacks, widgetDeps) : null;
3469
3528
  case Products.className():
3470
3529
  return layoutApi.widgetProductsApi ? new Products(nodeRef, layer, layoutApi.widgetProductsApi, widgetCallbacks, widgetDeps) : null;
3530
+ case ProductCarousel.className():
3531
+ return layoutApi.widgetProductCarouselApi
3532
+ ? new ProductCarousel(nodeRef, layer, layoutApi.widgetProductCarouselApi, widgetCallbacks, widgetDeps)
3533
+ : null;
3471
3534
  case Tooltip.className():
3472
3535
  return layoutApi.widgetTooltipApi ? new Tooltip(nodeRef, layer, layoutApi.widgetTooltipApi, widgetCallbacks, widgetDeps) : null;
3473
3536
  case Timer.className():
@@ -4021,6 +4084,14 @@ class Layer {
4021
4084
  }
4022
4085
  return null;
4023
4086
  }
4087
+ get productCarouselElement() {
4088
+ for (const element of this._elements) {
4089
+ if (ProductCarousel.isTypeOf(element)) {
4090
+ return element;
4091
+ }
4092
+ }
4093
+ return null;
4094
+ }
4024
4095
  isClickCapturedBySlider() {
4025
4096
  return (this.rangeSliderElement?.isClickCapturedBySlider ?? false) || (this.productsElement?.isClickCapturedByWidget ?? false);
4026
4097
  }
@@ -5440,6 +5511,12 @@ let SlideApi$1 = class SlideApi {
5440
5511
  return result; // disable all clicks if event captured by products widget (slider or swipe down)
5441
5512
  }
5442
5513
  }
5514
+ if (this.activeLayer.productCarouselElement) {
5515
+ if (this.activeLayer.productCarouselElement.isClickCapturedByWidget) {
5516
+ result.canClickNext = false;
5517
+ return result; // disable all clicks if event captured by product carousel widget
5518
+ }
5519
+ }
5443
5520
  if (this.activeLayer.questElement) {
5444
5521
  const slideIndex = this.slide.slideIndex;
5445
5522
  const slideCount = this.slide.slideCount;
@@ -19951,6 +20028,10 @@ class BottomSheet extends RenderableComponent {
19951
20028
  }), h("div", {
19952
20029
  class: "ias-bottom-sheet__container",
19953
20030
  style: this.props.minHeight != null ? `min-height: ${this.props.minHeight + borderRadius}px` : "",
20031
+ onclick: e => {
20032
+ e.preventDefault();
20033
+ e.stopPropagation();
20034
+ },
19954
20035
  }, h("div", { class: "ias-bottom-sheet__content" },
19955
20036
  /* h("div", { class: "ias-bottom-sheet__header" }), */
19956
20037
  h("div", { class: "ias-bottom-sheet__body" }, ...(this.props?.children?.map(c => c.render()) ?? [])))));
@@ -20015,7 +20096,7 @@ class ProductDetailsGallery extends RenderableComponent {
20015
20096
  static get [Symbol.for("___CTOR_ARGS___")]() { return [`ProductDetailsGalleryProps`]; }
20016
20097
  }
20017
20098
 
20018
- const ADD_TO_CART_TIMEOUT = 60000;
20099
+ const ADD_TO_CART_TIMEOUT$1 = 60000;
20019
20100
 
20020
20101
  class ProductDetailsPurchaseAction extends RenderableComponent {
20021
20102
  button;
@@ -20040,7 +20121,13 @@ class ProductDetailsPurchaseAction extends RenderableComponent {
20040
20121
  await this.addToCart();
20041
20122
  }
20042
20123
  catch (error) {
20043
- this.handleAddToCartError(error);
20124
+ if (error instanceof Error) {
20125
+ this.handleAddToCartError(error);
20126
+ }
20127
+ else if (typeof error === "string") {
20128
+ // Native SDKs return a string
20129
+ this.handleAddToCartError(new Error(error));
20130
+ }
20044
20131
  }
20045
20132
  finally {
20046
20133
  this.hideLoader();
@@ -20057,7 +20144,7 @@ class ProductDetailsPurchaseAction extends RenderableComponent {
20057
20144
  this.props.onAddToCartError({ error });
20058
20145
  }
20059
20146
  async addToCart() {
20060
- await Promise.race([this.props.onAddToCart(), new Promise((resolve, reject) => setTimeout(reject, ADD_TO_CART_TIMEOUT))]);
20147
+ await Promise.race([this.props.onAddToCart(), new Promise((resolve, reject) => setTimeout(reject, ADD_TO_CART_TIMEOUT$1))]);
20061
20148
  }
20062
20149
  static get [Symbol.for("___CTOR_ARGS___")]() { return [`ProductDetailsPurchaseActionProps`]; }
20063
20150
  }
@@ -20460,13 +20547,15 @@ class ProductCheckout extends RenderableComponent {
20460
20547
  }, h("button", {
20461
20548
  class: "ias-product-checkout__button ias-product-checkout__button--close",
20462
20549
  textContent: this.props.translations.continueBtn,
20463
- onclick: () => {
20550
+ onclick: e => {
20551
+ e.stopPropagation();
20464
20552
  this.props.onClose();
20465
20553
  },
20466
20554
  }), h("button", {
20467
20555
  class: "ias-product-checkout__button ias-product-checkout__button--checkout",
20468
20556
  textContent: this.props.translations.goToCartBtn,
20469
- onclick: () => {
20557
+ onclick: e => {
20558
+ e.stopPropagation();
20470
20559
  this.props.onCheckout();
20471
20560
  },
20472
20561
  })));
@@ -20523,6 +20612,7 @@ class ProductComponentsFactory {
20523
20612
  const bs = new BottomSheet({
20524
20613
  onClose: () => {
20525
20614
  bs.destroy();
20615
+ props.onClose?.();
20526
20616
  },
20527
20617
  children: [],
20528
20618
  minHeight: props.minHeight,
@@ -20545,10 +20635,7 @@ class ProductComponentsFactory {
20545
20635
  },
20546
20636
  });
20547
20637
  }
20548
- createProductCheckout(bottomSheet, offers) {
20549
- const offersByIds = {};
20550
- for (const offer of offers)
20551
- offersByIds[offer.offerId] = offer;
20638
+ createProductCheckout(bottomSheet) {
20552
20639
  return new ProductCheckout({
20553
20640
  onClose: () => bottomSheet.close(),
20554
20641
  onCheckout: () => {
@@ -20562,7 +20649,6 @@ class ProductComponentsFactory {
20562
20649
  onClick: async () => {
20563
20650
  try {
20564
20651
  const state = await this.widgetDeps.slideApiDeps.productCartGetState();
20565
- console.log({ cart: state });
20566
20652
  this.widgetDeps.slideApiDeps.showToast(JSON.stringify(state.offers.map(({ offerId, name, quantity }) => ({
20567
20653
  offerId,
20568
20654
  name,
@@ -20632,10 +20718,16 @@ class ProductDetailsBottomSheet extends RenderableComponent {
20632
20718
  super(props);
20633
20719
  this.widgetDeps = widgetDeps;
20634
20720
  this.factory = new ProductComponentsFactory(this.widgetDeps, this.props);
20635
- this.bottomSheet = this.factory.createBottomSheet({ minHeight: props.height });
20721
+ this.bottomSheet = this.factory.createBottomSheet({
20722
+ minHeight: props.height,
20723
+ onClose: () => {
20724
+ this._root?.remove();
20725
+ this.props.onClose?.();
20726
+ },
20727
+ });
20636
20728
  }
20637
20729
  renderTemplate() {
20638
- return this.bottomSheet.render();
20730
+ return h("div", { class: "ias-products-container-view" }, this.bottomSheet.render());
20639
20731
  }
20640
20732
  open(params) {
20641
20733
  this.showProductDetails(params);
@@ -20654,17 +20746,17 @@ class ProductDetailsBottomSheet extends RenderableComponent {
20654
20746
  onAddToCart: async (payload) => {
20655
20747
  await this.productCartUpdate({
20656
20748
  offerDtos,
20657
- product: payload.product,
20749
+ offerId: payload.product.offerId,
20658
20750
  quantity: payload.quantity,
20659
20751
  });
20660
- this.showProductCheckout({ offerDtos });
20752
+ this.showProductCheckout();
20661
20753
  },
20662
20754
  isCartSupported,
20663
20755
  });
20664
20756
  this.updateBottomSheetContent(productDetails);
20665
20757
  }
20666
- showProductCheckout({ offerDtos }) {
20667
- const productCheckout = this.factory.createProductCheckout(this.bottomSheet, offerDtos);
20758
+ showProductCheckout() {
20759
+ const productCheckout = this.factory.createProductCheckout(this.bottomSheet);
20668
20760
  this.updateBottomSheetContent(productCheckout);
20669
20761
  }
20670
20762
  updateBottomSheetContent(content) {
@@ -20674,10 +20766,10 @@ class ProductDetailsBottomSheet extends RenderableComponent {
20674
20766
  children,
20675
20767
  });
20676
20768
  }
20677
- async productCartUpdate({ offerDtos, product, quantity }) {
20678
- const offerDto = offerDtos.find(offerDto => product.offerId === offerDto.offerId);
20769
+ async productCartUpdate({ offerDtos, offerId, quantity }) {
20770
+ const offerDto = offerDtos.find(offerDto => offerId === offerDto.offerId);
20679
20771
  if (!offerDto)
20680
- throw new Error(`[IAS]: Not found offer for ID ${product.offerId}`);
20772
+ throw new Error(`[IAS]: Not found offer for ID ${offerId}`);
20681
20773
  const cartOffer = ProductOfferMapper.fromOfferDtoToProductCartOffer(offerDto, quantity);
20682
20774
  const delay = async () => new Promise(resolve => setTimeout(resolve, 300));
20683
20775
  const [productCart] = await Promise.all([
@@ -20697,6 +20789,271 @@ class ProductDetailsBottomSheet extends RenderableComponent {
20697
20789
  static get [Symbol.for("___CTOR_ARGS___")]() { return [`WidgetDeps`, `ProductDetailsBottomSheetProps`]; }
20698
20790
  }
20699
20791
 
20792
+ class ProductCheckoutBottomSheet extends RenderableComponent {
20793
+ widgetDeps;
20794
+ factory;
20795
+ bottomSheet;
20796
+ constructor(widgetDeps, props) {
20797
+ super(props);
20798
+ this.widgetDeps = widgetDeps;
20799
+ this.factory = new ProductComponentsFactory(this.widgetDeps, this.props);
20800
+ this.bottomSheet = this.factory.createBottomSheet({
20801
+ onClose: () => {
20802
+ this._root?.remove();
20803
+ this.props.onClose?.();
20804
+ },
20805
+ });
20806
+ }
20807
+ renderTemplate() {
20808
+ return h("div", { class: "ias-products-container-view" }, this.bottomSheet.render());
20809
+ }
20810
+ open() {
20811
+ const productCheckout = this.factory.createProductCheckout(this.bottomSheet);
20812
+ this.updateBottomSheetContent(productCheckout);
20813
+ this.bottomSheet.open();
20814
+ }
20815
+ async productCartUpdate({ offerDtos, offerId, quantity }) {
20816
+ const offerDto = offerDtos.find(offerDto => offerId === offerDto.offerId);
20817
+ if (!offerDto)
20818
+ throw new Error(`[IAS]: Not found offer for ID ${offerId}`);
20819
+ const cartOffer = ProductOfferMapper.fromOfferDtoToProductCartOffer(offerDto, quantity);
20820
+ const delay = async () => new Promise(resolve => setTimeout(resolve, 300));
20821
+ const [productCart] = await Promise.all([
20822
+ this.widgetDeps.slideApiDeps.productCartUpdate({
20823
+ offer: cartOffer,
20824
+ }),
20825
+ delay(),
20826
+ ]);
20827
+ return productCart.offers;
20828
+ }
20829
+ updateBottomSheetContent(content) {
20830
+ const cartButton = this.factory.createProductCartButton();
20831
+ const children = process.env.NODE_ENV === "staging" ? [cartButton, content] : [content];
20832
+ this.bottomSheet.updateProps({
20833
+ children,
20834
+ });
20835
+ }
20836
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`WidgetDeps`, `ProductCheckoutBottomSheetProps`]; }
20837
+ }
20838
+
20839
+ class ProductOffersRequestError extends Error {
20840
+ status;
20841
+ code;
20842
+ cause;
20843
+ constructor(message, params) {
20844
+ super(message);
20845
+ this.name = this.constructor.name;
20846
+ this.status = params?.status;
20847
+ this.code = params?.code;
20848
+ this.cause = params?.cause;
20849
+ }
20850
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`string`, `{ status?: number; code?: string; cause?: unknown }`]; }
20851
+ }
20852
+ class ProductOffersNetworkError extends ProductOffersRequestError {
20853
+ }
20854
+ class ProductOffersServiceError extends ProductOffersRequestError {
20855
+ }
20856
+ class ProductOfferRepository {
20857
+ widgetDeps;
20858
+ getLinkTarget;
20859
+ getMsgNetworkError;
20860
+ getMsgServiceError;
20861
+ static rawOffersCache = new Map();
20862
+ static pendingFetches = new Map();
20863
+ static RAW_CACHE_TTL_MS = 60_000;
20864
+ offers = [];
20865
+ constructor(widgetDeps, getLinkTarget, getMsgNetworkError, getMsgServiceError) {
20866
+ this.widgetDeps = widgetDeps;
20867
+ this.getLinkTarget = getLinkTarget;
20868
+ this.getMsgNetworkError = getMsgNetworkError;
20869
+ this.getMsgServiceError = getMsgServiceError;
20870
+ }
20871
+ getCurrentModels() {
20872
+ return this.offers;
20873
+ }
20874
+ clear() {
20875
+ this.offers = [];
20876
+ }
20877
+ async getOrFetchProducts() {
20878
+ if (this.offers.length > 0)
20879
+ return this.offers;
20880
+ const cacheKey = this.getCacheKey();
20881
+ const cached = ProductOfferRepository.getRawFromCache(cacheKey);
20882
+ if (cached != null && cached.length > 0) {
20883
+ const cloned = cached.slice();
20884
+ this.offers = cloned;
20885
+ return this.offers;
20886
+ }
20887
+ const result = await this.fetchProductsRaw(cacheKey);
20888
+ if (result.models.length > 0) {
20889
+ const offers = result.models.slice();
20890
+ const { objectUrls } = await ProductOfferRepository.hydrateOfferImageBlobs(offers, this.getMsgNetworkError());
20891
+ ProductOfferRepository.rawOffersCache.set(cacheKey, {
20892
+ cachedAtMs: Date.now(),
20893
+ offers,
20894
+ objectUrls,
20895
+ });
20896
+ this.offers = offers;
20897
+ return this.offers;
20898
+ }
20899
+ throw new ProductOffersServiceError(result.message);
20900
+ }
20901
+ async fetchProductsRaw(cacheKey) {
20902
+ const existing = ProductOfferRepository.pendingFetches.get(cacheKey);
20903
+ if (existing)
20904
+ return existing;
20905
+ const request = (async () => {
20906
+ try {
20907
+ const linkTarget = this.getOfferIdsOrThrow();
20908
+ const path = this.buildOffersPath(linkTarget);
20909
+ const headers = this.getOffersHeaders();
20910
+ const profileKey = "fetch-products";
20911
+ const response = await this.widgetDeps.slideApiDeps.sendApiRequest(path, "GET", null, headers, null, profileKey);
20912
+ return this.parseOffersResponse(response.status, response.data);
20913
+ }
20914
+ catch (error) {
20915
+ console.error(error);
20916
+ throw error;
20917
+ }
20918
+ finally {
20919
+ ProductOfferRepository.pendingFetches.delete(cacheKey);
20920
+ }
20921
+ })();
20922
+ const delayed = Promise.all([request, new Promise(t => setTimeout(t, 200))]).then(([result]) => result);
20923
+ ProductOfferRepository.pendingFetches.set(cacheKey, delayed);
20924
+ return delayed;
20925
+ }
20926
+ getOfferIdsOrThrow() {
20927
+ const linkTarget = this.getLinkTarget();
20928
+ if (!linkTarget.length) {
20929
+ throw new ProductOffersServiceError(this.getMsgServiceError() ?? "", { code: "EMPTY_OFFER_IDS" });
20930
+ }
20931
+ return linkTarget;
20932
+ }
20933
+ buildOffersPath(offerIds) {
20934
+ const qs = this.buildOffersQueryString(offerIds);
20935
+ return `product/offer?${qs}`;
20936
+ }
20937
+ buildOffersQueryString(offerIds) {
20938
+ let qs = `id=${offerIds.join(",")}&expand=images,subOffersApi`;
20939
+ const sdkClientVariables = this.widgetDeps.getSdkClientVariables();
20940
+ if (sdkClientVariables?.pos != null) {
20941
+ qs += `&pos=${String(sdkClientVariables.pos)}`;
20942
+ }
20943
+ return qs;
20944
+ }
20945
+ getOffersHeaders() {
20946
+ return {
20947
+ accept: "application/json",
20948
+ "Content-Type": "application/json",
20949
+ };
20950
+ }
20951
+ parseOffersResponse(status, data) {
20952
+ if (status === 200 || status === 201) {
20953
+ if (Array.isArray(data) && data.length > 0) {
20954
+ return { message: "", models: data };
20955
+ }
20956
+ throw new ProductOffersServiceError(this.getMsgServiceError() ?? "", { status, code: "EMPTY_RESPONSE" });
20957
+ }
20958
+ // WinInet-like network errors sometimes bubble up as "status"
20959
+ if (status === 12163 || status === 12002) {
20960
+ throw new ProductOffersNetworkError(this.getMsgNetworkError() ?? "", { status, code: "NETWORK" });
20961
+ }
20962
+ // All other statuses are treated as a general service error.
20963
+ throw new ProductOffersServiceError(this.getMsgServiceError() ?? "", { status, code: "SERVICE_ERROR" });
20964
+ }
20965
+ getCacheKey() {
20966
+ const ids = [...this.getLinkTarget()].sort((a, b) => a - b).join(",");
20967
+ const sdkClientVariables = this.widgetDeps.getSdkClientVariables();
20968
+ const pos = sdkClientVariables?.pos != null ? String(sdkClientVariables.pos) : "";
20969
+ return `offers:${ids}:pos:${pos}`;
20970
+ }
20971
+ static revokeObjectUrls(urls) {
20972
+ for (const u of urls) {
20973
+ try {
20974
+ URL.revokeObjectURL(u);
20975
+ }
20976
+ catch {
20977
+ // ignore
20978
+ }
20979
+ }
20980
+ }
20981
+ static collectDistinctImageUrls(offers, into) {
20982
+ for (const o of offers) {
20983
+ if (o.coverUrl) {
20984
+ into.add(o.coverUrl);
20985
+ }
20986
+ if (o.images?.length) {
20987
+ for (const img of o.images) {
20988
+ if (img.url) {
20989
+ into.add(img.url);
20990
+ }
20991
+ }
20992
+ }
20993
+ if (o.subOffersApi?.length) {
20994
+ this.collectDistinctImageUrls(o.subOffersApi, into);
20995
+ }
20996
+ }
20997
+ }
20998
+ static applyImageBlobMap(offers, originalToBlob) {
20999
+ for (const o of offers) {
21000
+ const coverOrig = o.coverUrl;
21001
+ if (coverOrig) {
21002
+ const blobUrl = originalToBlob.get(coverOrig);
21003
+ if (blobUrl) {
21004
+ o.coverUrl = blobUrl;
21005
+ }
21006
+ }
21007
+ if (o.images?.length) {
21008
+ for (const img of o.images) {
21009
+ const urlOrig = img.url;
21010
+ const blobUrl = originalToBlob.get(urlOrig);
21011
+ if (blobUrl) {
21012
+ img.url = blobUrl;
21013
+ }
21014
+ }
21015
+ }
21016
+ if (o.subOffersApi?.length) {
21017
+ this.applyImageBlobMap(o.subOffersApi, originalToBlob);
21018
+ }
21019
+ }
21020
+ }
21021
+ /**
21022
+ * Fetches `coverUrl` and `images[].url` as blobs and replaces them with `blob:` URLs.
21023
+ * Products are considered unresolved until this completes successfully.
21024
+ */
21025
+ static async hydrateOfferImageBlobs(offers, networkErrorMessage) {
21026
+ const distinct = new Set();
21027
+ ProductOfferRepository.collectDistinctImageUrls(offers, distinct);
21028
+ const objectUrls = [];
21029
+ const originalToBlob = new Map();
21030
+ await Promise.all([...distinct].map(async (original) => {
21031
+ const response = await fetch(original, { mode: "cors" });
21032
+ if (!response.ok) {
21033
+ throw new ProductOffersNetworkError(networkErrorMessage ?? "", { status: response.status, code: "IMAGE_BLOB_FETCH" });
21034
+ }
21035
+ const blob = await response.blob();
21036
+ const blobUrl = URL.createObjectURL(blob);
21037
+ objectUrls.push(blobUrl);
21038
+ originalToBlob.set(original, blobUrl);
21039
+ }));
21040
+ ProductOfferRepository.applyImageBlobMap(offers, originalToBlob);
21041
+ return { objectUrls };
21042
+ }
21043
+ static getRawFromCache(cacheKey) {
21044
+ const item = this.rawOffersCache.get(cacheKey);
21045
+ if (!item)
21046
+ return null;
21047
+ if (Date.now() - item.cachedAtMs > this.RAW_CACHE_TTL_MS) {
21048
+ ProductOfferRepository.revokeObjectUrls(item.objectUrls);
21049
+ this.rawOffersCache.delete(cacheKey);
21050
+ return null;
21051
+ }
21052
+ return item.offers;
21053
+ }
21054
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`WidgetDeps`, `() => Array<number>`, `() => string | undefined`, `() => string | undefined`]; }
21055
+ }
21056
+
20700
21057
  /**
20701
21058
  * adult: null
20702
21059
  * availability: 1
@@ -20752,6 +21109,7 @@ class WidgetProducts extends WidgetBase {
20752
21109
  this.isScreenSupportsTouch = isScreenSupportsTouch(this.env);
20753
21110
  this.msgNetworkError = getTagData(this.element, "msgNetworkError");
20754
21111
  this.msgServiceError = getTagData(this.element, "msgServiceError");
21112
+ this.repository = new ProductOfferRepository(this.widgetDeps, () => this.linkTarget, () => this.msgNetworkError, () => this.msgServiceError);
20755
21113
  }
20756
21114
  /**
20757
21115
  * Start or restart widget
@@ -20847,107 +21205,6 @@ class WidgetProducts extends WidgetBase {
20847
21205
  console.error(error);
20848
21206
  }
20849
21207
  }
20850
- async getOrFetchProducts() {
20851
- if (this.currentModels.length > 0)
20852
- return this.currentModels;
20853
- const result = await this.fetchProducts();
20854
- if (result.models.length > 0) {
20855
- this.currentModels = result.models;
20856
- return result.models;
20857
- }
20858
- throw new Error(result.message);
20859
- }
20860
- async fetchProducts() {
20861
- const fetchAndCacheMedia = async () => {
20862
- if (!this.linkTarget.length) {
20863
- return { message: this.msgServiceError ?? "", models: [] };
20864
- }
20865
- let qs = `id=${this.linkTarget.join(",")}&expand=images,subOffersApi`;
20866
- const sdkClientVariables = this.widgetDeps.getSdkClientVariables();
20867
- if (sdkClientVariables != null && sdkClientVariables.pos != null) {
20868
- qs += `&pos=${String(sdkClientVariables.pos)}`;
20869
- }
20870
- const path = `product/offer?${qs}`;
20871
- const headers = {
20872
- accept: "application/json",
20873
- "Content-Type": "application/json",
20874
- };
20875
- const profileKey = "fetch-products";
20876
- try {
20877
- const response = await this.widgetDeps.slideApiDeps.sendApiRequest(path, "GET", null, headers, null, profileKey);
20878
- // console.log({response});
20879
- const status = response.status;
20880
- if (status === 200 || status === 201) {
20881
- if (response.data && Array.isArray(response.data) && response.data.length > 0) {
20882
- try {
20883
- await this.cacheOffersMediaResources(response.data);
20884
- }
20885
- catch (error) {
20886
- console.error(error);
20887
- }
20888
- return { message: "", models: response.data };
20889
- }
20890
- else {
20891
- return { message: this.msgServiceError ?? "", models: [] };
20892
- }
20893
- }
20894
- else if (status === 12163 || status === 12002) {
20895
- return { message: this.msgNetworkError ?? "", models: [] };
20896
- }
20897
- else {
20898
- return { message: this.msgServiceError ?? "", models: [] };
20899
- }
20900
- }
20901
- catch (error) {
20902
- console.error(error);
20903
- return { message: this.msgServiceError ?? "", models: [] };
20904
- }
20905
- };
20906
- return Promise.all([
20907
- fetchAndCacheMedia(),
20908
- new Promise(t => {
20909
- return setTimeout(t, 200);
20910
- }),
20911
- ]).then(([result]) => result);
20912
- }
20913
- cacheOffersMediaResources(offers) {
20914
- const cacheItem = (offer) => {
20915
- return new Promise(resolve => {
20916
- if (offer.coverUrl != null) {
20917
- this.env
20918
- .fetch(offer.coverUrl)
20919
- .then(response => {
20920
- if (response != null && response.ok) {
20921
- response
20922
- .blob()
20923
- .then(blob => {
20924
- offer.coverUrl = URL.createObjectURL(blob);
20925
- resolve();
20926
- })
20927
- .catch(resolve);
20928
- }
20929
- else {
20930
- resolve();
20931
- }
20932
- })
20933
- .catch(resolve);
20934
- }
20935
- else {
20936
- resolve();
20937
- }
20938
- });
20939
- };
20940
- const promises = offers.map(cacheItem);
20941
- return Promise.all(promises);
20942
- }
20943
- revokeOffersMediaResources(offers) {
20944
- offers.forEach(offer => {
20945
- if (offer.coverUrl != null) {
20946
- // todo check if coverUrl really is object URL
20947
- URL.revokeObjectURL(offer.coverUrl);
20948
- }
20949
- });
20950
- }
20951
21208
  initSwipeGestureDetector() {
20952
21209
  if (this.isOpen) {
20953
21210
  if (this.swipeGestureDetector == null) {
@@ -20975,7 +21232,7 @@ class WidgetProducts extends WidgetBase {
20975
21232
  get isForcePaused() {
20976
21233
  return this.isOpen;
20977
21234
  }
20978
- currentModels = [];
21235
+ repository;
20979
21236
  async openProductsView() {
20980
21237
  if (this.isOpen || this.isLoading) {
20981
21238
  return;
@@ -20989,7 +21246,7 @@ class WidgetProducts extends WidgetBase {
20989
21246
  }
20990
21247
  try {
20991
21248
  this.isLoading = true;
20992
- const models = await this.getOrFetchProducts();
21249
+ const models = await this.repository.getOrFetchProducts();
20993
21250
  this.productsView = this.createProductsView(models, this.closeProductsView.bind(this));
20994
21251
  this.productsView.classList.add("ias-products-container-view--visible");
20995
21252
  this.slide.appendChild(this.productsView);
@@ -21028,12 +21285,11 @@ class WidgetProducts extends WidgetBase {
21028
21285
  const onClosed = () => {
21029
21286
  this.productsView?.removeEventListener("animationend", onClosed);
21030
21287
  this.productsView?.parentElement?.removeChild(this.productsView);
21031
- this.revokeOffersMediaResources(this.currentModels);
21032
21288
  if (!this.disableTimer) {
21033
21289
  this.onWidgetRequireResumeUI();
21034
21290
  }
21035
21291
  this.isOpen = false;
21036
- this.currentModels = [];
21292
+ this.repository.clear();
21037
21293
  };
21038
21294
  this.productsView?.addEventListener("animationend", onClosed);
21039
21295
  }
@@ -21297,6 +21553,446 @@ class WidgetProducts extends WidgetBase {
21297
21553
  static get [Symbol.for("___CTOR_ARGS___")]() { return [`HTMLElement`, `Partial`, `WidgetCallbacks`, `WidgetDeps`]; }
21298
21554
  }
21299
21555
 
21556
+ const ADD_TO_CART_TIMEOUT = 60000;
21557
+ class BottomSheetMountingError extends Error {
21558
+ constructor() {
21559
+ super("[IAS]: products bottom sheet mounting error");
21560
+ this.name = "BottomSheetMountingError";
21561
+ }
21562
+ static get [Symbol.for("___CTOR_ARGS___")]() { return []; }
21563
+ }
21564
+ class WidgetProductCarousel extends WidgetBase {
21565
+ static DEFAULTS = {
21566
+ slide: null,
21567
+ activateAfterCreate: false,
21568
+ create: false,
21569
+ localData: {},
21570
+ };
21571
+ static widgetClassName = "narrative-element-product-carousel";
21572
+ captionView;
21573
+ offerIds = [];
21574
+ msgNetworkError;
21575
+ msgServiceError;
21576
+ isScreenSupportsTouch;
21577
+ isLoading = false;
21578
+ repository;
21579
+ canClick = false;
21580
+ isBottomSheetOpened = false;
21581
+ isClickCapturedByScroll = false;
21582
+ $carousel = null;
21583
+ $track = null;
21584
+ isTouchListenersInit = false;
21585
+ constructor(element, options, widgetCallbacks, widgetDeps) {
21586
+ super(element, options, widgetCallbacks, widgetDeps);
21587
+ this.captionView = this.element.querySelector(".narrative-element-text-lines");
21588
+ this.isScreenSupportsTouch = isScreenSupportsTouch(this.env);
21589
+ this.ajustGeometryMargin();
21590
+ const offerIds = decodeURIComponent(getTagData(element, "offerIds") ?? "[]");
21591
+ try {
21592
+ const parsed = JSON.parse(offerIds);
21593
+ if (Array.isArray(parsed)) {
21594
+ this.offerIds = parsed.filter(FilterNumber);
21595
+ }
21596
+ }
21597
+ catch (e) {
21598
+ console.error(e);
21599
+ }
21600
+ this.msgNetworkError = getTagData(this.element, "msgNetworkError");
21601
+ this.msgServiceError = getTagData(this.element, "msgServiceError");
21602
+ this.repository = new ProductOfferRepository(this.widgetDeps, () => this.offerIds, () => this.msgNetworkError, () => this.msgServiceError);
21603
+ }
21604
+ onRefreshUserData(localData) {
21605
+ super.onRefreshUserData(localData);
21606
+ this.isLoading = false;
21607
+ this.renderCarousel();
21608
+ }
21609
+ onStart() {
21610
+ super.onStart();
21611
+ this.canClick = true;
21612
+ this.initTouchListeners();
21613
+ }
21614
+ onStop() {
21615
+ super.onStop();
21616
+ this.isClickCapturedByScroll = false;
21617
+ this.isBottomSheetOpened = false;
21618
+ this.canClick = false;
21619
+ this.isTouchListenersInit = false;
21620
+ this.enableHostUIInteraction();
21621
+ this.widgetDeps.slideRoot.removeEventListener("touchstart", this.handleMouseDown);
21622
+ this.widgetDeps.slideRoot.removeEventListener("mousedown", this.handleMouseDown);
21623
+ this.widgetDeps.slideRoot.removeEventListener("touchend", this.handleMouseUp);
21624
+ this.widgetDeps.slideRoot.removeEventListener("mouseup", this.handleMouseUp);
21625
+ }
21626
+ getIsClickCapturedByWidget() {
21627
+ return this.isClickCapturedByScroll || this.isBottomSheetOpened;
21628
+ }
21629
+ get isForcePaused() {
21630
+ return this.isBottomSheetOpened;
21631
+ }
21632
+ /** Offset compensation for an elongated viewport */
21633
+ ajustGeometryMargin() {
21634
+ this.element.parentElement.style.padding = "0 var(--x-offset, 0px)";
21635
+ }
21636
+ isTransparentElement() {
21637
+ if (!this.element)
21638
+ return false;
21639
+ try {
21640
+ const color = window.getComputedStyle(this.element).color;
21641
+ return color === "transparent" || color === "rgba(0, 0, 0, 0)" || color === "rgba(0,0,0,0)";
21642
+ }
21643
+ catch (err) {
21644
+ console.error(err);
21645
+ }
21646
+ return false;
21647
+ }
21648
+ async renderCarousel() {
21649
+ if (this.isLoading)
21650
+ return;
21651
+ if (!this.offerIds.length)
21652
+ throw new Error("[IAS]: empty offer ids list");
21653
+ this.isLoading = true;
21654
+ if (!this.isTransparentElement()) {
21655
+ this.element.classList.add("loader");
21656
+ }
21657
+ try {
21658
+ const models = await this.repository.getOrFetchProducts();
21659
+ this.renderInlineCarousel(models);
21660
+ }
21661
+ catch (error) {
21662
+ if (error instanceof Error) {
21663
+ this.widgetDeps.slideApiDeps.showToast(error.message);
21664
+ }
21665
+ }
21666
+ finally {
21667
+ this.isLoading = false;
21668
+ this.element.classList.remove("loader");
21669
+ }
21670
+ }
21671
+ initTouchListeners() {
21672
+ if (!this.isTouchListenersInit) {
21673
+ this.widgetDeps.slideRoot.addEventListener("touchstart", this.handleMouseDown);
21674
+ this.widgetDeps.slideRoot.addEventListener("mousedown", this.handleMouseDown);
21675
+ this.isTouchListenersInit = true;
21676
+ }
21677
+ }
21678
+ handleMouseDown = (e) => {
21679
+ let scrollView = e.target;
21680
+ if (!scrollView.classList.contains("ias-products-inline-carousel-scroll-view")) {
21681
+ scrollView = scrollView.closest(".ias-products-inline-carousel-scroll-view");
21682
+ }
21683
+ if (!scrollView) {
21684
+ return;
21685
+ }
21686
+ this.isClickCapturedByScroll = true;
21687
+ this.widgetDeps.slideApiDeps.disableHorizontalSwipeGesture();
21688
+ this.widgetDeps.slideApiDeps.disableVerticalSwipeGesture();
21689
+ this.widgetDeps.slideRoot.addEventListener("touchend", this.handleMouseUp);
21690
+ this.widgetDeps.slideRoot.addEventListener("mouseup", this.handleMouseUp);
21691
+ };
21692
+ handleMouseUp = () => {
21693
+ this.widgetDeps.slideRoot.removeEventListener("touchend", this.handleMouseUp);
21694
+ this.widgetDeps.slideRoot.removeEventListener("mouseup", this.handleMouseUp);
21695
+ this.env.requestAnimationFrame(() => {
21696
+ this.widgetDeps.slideApiDeps.enableHorizontalSwipeGesture();
21697
+ this.widgetDeps.slideApiDeps.enableVerticalSwipeGesture();
21698
+ this.isClickCapturedByScroll = false;
21699
+ });
21700
+ };
21701
+ createCarousel() {
21702
+ this.$carousel = h("div", { class: "ias-products-inline-carousel" });
21703
+ this.element.appendChild(this.$carousel);
21704
+ this.$carousel.dir = this.layoutDirection;
21705
+ this.$track = h("div", { class: "ias-products-inline-carousel-scroll-view", ontouchstart: () => { }, onmousedown: () => { } });
21706
+ this.$carousel.appendChild(this.$track);
21707
+ return { carousel: this.$carousel, track: this.$track };
21708
+ }
21709
+ renderInlineCarousel(offers) {
21710
+ if (this.$carousel)
21711
+ return;
21712
+ const { carousel, track } = this.createCarousel();
21713
+ track.innerHTML = "";
21714
+ offers.forEach(offer => {
21715
+ track.appendChild(this.renderCard(offer));
21716
+ });
21717
+ const controls = this.renderCarouselControls();
21718
+ this.env.requestAnimationFrame(() => {
21719
+ if (track.scrollWidth > track.clientWidth && controls) {
21720
+ carousel.appendChild(controls);
21721
+ }
21722
+ });
21723
+ }
21724
+ renderCarouselControls() {
21725
+ if (this.isScreenSupportsTouch)
21726
+ return null;
21727
+ const scrollControlStartIconView = h("div", { class: "ias-product-carousel-scroll-control-start-icon-view" });
21728
+ const scrollControlEndIconView = h("div", { class: "ias-product-carousel-scroll-control-end-icon-view" });
21729
+ const scrollControlStartView = h("div", {
21730
+ class: "ias-product-carousel-scroll-control-start-view",
21731
+ onclick: (e) => {
21732
+ if (!this.canClick)
21733
+ return;
21734
+ e.preventDefault();
21735
+ e.stopPropagation();
21736
+ if (this.layoutDirection === "ltr") {
21737
+ this.navigatePrev();
21738
+ }
21739
+ else {
21740
+ this.navigateNext();
21741
+ }
21742
+ },
21743
+ }, scrollControlStartIconView);
21744
+ const scrollControlEndView = h("div", {
21745
+ class: "ias-product-carousel-scroll-control-end-view",
21746
+ onclick: (e) => {
21747
+ if (!this.canClick)
21748
+ return;
21749
+ e.preventDefault();
21750
+ e.stopPropagation();
21751
+ if (this.layoutDirection === "ltr") {
21752
+ this.navigateNext();
21753
+ }
21754
+ else {
21755
+ this.navigatePrev();
21756
+ }
21757
+ },
21758
+ }, scrollControlEndIconView);
21759
+ return h("div", { class: "ias-product-carousel-scroll-controls-view" }, scrollControlStartView, scrollControlEndView);
21760
+ }
21761
+ navigatePrev() {
21762
+ this.setScrollLeft(this.getScrollLeft() - this.getScrollViewportWidth());
21763
+ }
21764
+ navigateNext() {
21765
+ this.setScrollLeft(this.getScrollLeft() + this.getScrollViewportWidth());
21766
+ }
21767
+ setScrollLeft(value) {
21768
+ if (this.$track != null) {
21769
+ this.$track.scrollLeft = Math.round(value);
21770
+ }
21771
+ }
21772
+ getScrollLeft() {
21773
+ if (this.$track != null) {
21774
+ return this.$track.scrollLeft;
21775
+ }
21776
+ return 0;
21777
+ }
21778
+ getScrollViewportWidth() {
21779
+ if (this.$track != null) {
21780
+ return this.$track.clientWidth;
21781
+ }
21782
+ return 0;
21783
+ }
21784
+ renderCard(offer) {
21785
+ const contentEl = this.renderCardContent(offer);
21786
+ const card = h("div", { class: "ias-product-card" }, this.renderCardImage(offer), contentEl);
21787
+ card.onclick = e => {
21788
+ if (this.isBottomSheetOpened || !this.canClick)
21789
+ return;
21790
+ e.stopPropagation();
21791
+ e.preventDefault();
21792
+ if (!this.disableTimer) {
21793
+ this.onWidgetRequirePauseUI();
21794
+ }
21795
+ this.statEventWidgetCardClick(offer);
21796
+ this.showProductDetails({ offer, card });
21797
+ };
21798
+ return card;
21799
+ }
21800
+ showProductDetails = ({ offer, card }) => {
21801
+ const bs = new ProductDetailsBottomSheet(this.widgetDeps, this.getBottomSheetParams());
21802
+ const bsNode = bs.render();
21803
+ if (!bsNode)
21804
+ throw new BottomSheetMountingError();
21805
+ this.slide.appendChild(bsNode);
21806
+ bs.open({ offer, isCartSupported: this.isCartSupported() });
21807
+ this.isBottomSheetOpened = true;
21808
+ this.disableHostUIInteraction();
21809
+ };
21810
+ renderCardImage(offer) {
21811
+ const coverUrl = offer.coverUrl ?? "";
21812
+ const title = offer.name ?? "";
21813
+ const img = coverUrl
21814
+ ? h("img", {
21815
+ src: coverUrl,
21816
+ alt: title,
21817
+ /* loading: "lazy", */
21818
+ })
21819
+ : null;
21820
+ return h("div", { class: "ias-product-card__image" }, img, h("div", {
21821
+ class: "ias-product-card__image-mask",
21822
+ }));
21823
+ }
21824
+ renderCardContent(offer) {
21825
+ const titleEl = this.renderCardTitle(offer);
21826
+ const pricesEl = this.renderCardPrices(offer);
21827
+ const purchaseButtonEl = this.renderPurchaseButton(offer);
21828
+ const rowEl = h("div", { class: "ias-product-card__row" }, pricesEl, purchaseButtonEl);
21829
+ return h("div", { class: "ias-product-card__content" }, titleEl, rowEl);
21830
+ }
21831
+ renderCardTitle(offer) {
21832
+ const title = offer.name ?? "";
21833
+ return h("h3", { class: "ias-product-card__title", textContent: title });
21834
+ }
21835
+ renderCardPrices(offer) {
21836
+ const priceEl = offer.price != null && offer.currency != null
21837
+ ? h("span", {
21838
+ class: "ias-product-card__price",
21839
+ innerHTML: formatter.asCurrency(offer.price, offer.currency),
21840
+ })
21841
+ : null;
21842
+ const oldPriceEl = offer.oldPrice != null && offer.currency != null
21843
+ ? h("span", {
21844
+ class: "ias-product-card__price ias-product-card__price--old",
21845
+ innerHTML: formatter.asCurrency(offer.oldPrice, offer.currency),
21846
+ })
21847
+ : null;
21848
+ return h("div", { class: "ias-product-card__prices" }, priceEl, oldPriceEl);
21849
+ }
21850
+ renderPurchaseButton(offer) {
21851
+ const isCartSupported = this.isCartSupported();
21852
+ if (!isCartSupported && !offer.url)
21853
+ return null;
21854
+ const button = h("button", {
21855
+ class: "ias-product-card__purchase-button",
21856
+ type: "button",
21857
+ textContent: getTagData(this.element, "msgBuyNow") ?? "Buy now",
21858
+ onclick: e => {
21859
+ if (!this.canClick)
21860
+ return;
21861
+ if (!this.disableTimer) {
21862
+ this.onWidgetRequirePauseUI();
21863
+ }
21864
+ e.stopPropagation();
21865
+ this.statEventWidgetCardClick(offer);
21866
+ if (isCartSupported) {
21867
+ this.addToCart(offer, button);
21868
+ }
21869
+ else if (offer.url) {
21870
+ this.widgetDeps.slideApiDeps.openUrl({ type: "link", link: { type: "url", target: offer.url } });
21871
+ }
21872
+ },
21873
+ });
21874
+ return button;
21875
+ }
21876
+ async withTimeout(promise, timeout) {
21877
+ await Promise.race([promise, new Promise((resolve, reject) => setTimeout(reject, timeout))]);
21878
+ }
21879
+ async addToCart(offerDto, button) {
21880
+ if (this.isLoading || this.isBottomSheetOpened)
21881
+ return;
21882
+ try {
21883
+ this.isLoading = true;
21884
+ button.classList.add("loader");
21885
+ const bs = new ProductCheckoutBottomSheet(this.widgetDeps, this.getBottomSheetParams());
21886
+ await this.withTimeout(bs.productCartUpdate({
21887
+ offerDtos: this.repository.getCurrentModels(),
21888
+ offerId: offerDto.offerId,
21889
+ quantity: 1,
21890
+ }), ADD_TO_CART_TIMEOUT);
21891
+ const bsNode = bs.render();
21892
+ if (!bsNode)
21893
+ throw new BottomSheetMountingError();
21894
+ this.slide.appendChild(bsNode);
21895
+ bs.open();
21896
+ this.isBottomSheetOpened = true;
21897
+ this.disableHostUIInteraction();
21898
+ }
21899
+ catch (error) {
21900
+ if (error instanceof Error) {
21901
+ this.widgetDeps.slideApiDeps.showToast(error.message);
21902
+ }
21903
+ else if (typeof error === "string") {
21904
+ // Native SDKs return a string
21905
+ this.widgetDeps.slideApiDeps.showToast(error);
21906
+ }
21907
+ }
21908
+ finally {
21909
+ button.classList.remove("loader");
21910
+ this.isLoading = false;
21911
+ }
21912
+ }
21913
+ getBottomSheetParams() {
21914
+ return {
21915
+ translations: {
21916
+ color: getTagData(this.element, "msgColor") ?? "",
21917
+ size: getTagData(this.element, "msgSize") ?? "",
21918
+ addToCart: getTagData(this.element, "msgAddToCart") ?? "",
21919
+ successAddToCart: getTagData(this.element, "msgSuccess") ?? "",
21920
+ successSubAddToCart: getTagData(this.element, "msgSuccessSub") ?? "",
21921
+ errorAddToCart: getTagData(this.element, "msgError") ?? "",
21922
+ goToCartBtn: getTagData(this.element, "msgGoToCart") ?? "",
21923
+ continueBtn: getTagData(this.element, "msgContinue") ?? "",
21924
+ openUrl: getTagData(this.element, "msgOpenUrl") ?? "",
21925
+ },
21926
+ onClose: () => {
21927
+ if (!this.disableTimer) {
21928
+ this.onWidgetRequireResumeUI();
21929
+ }
21930
+ this.isBottomSheetOpened = false;
21931
+ this.enableHostUIInteraction();
21932
+ },
21933
+ };
21934
+ }
21935
+ statEventWidgetCardClick(offer) {
21936
+ try {
21937
+ const captionViewText = this.captionView?.textContent ?? "";
21938
+ this.sendStatisticEventToApp("w-product-carousel-card-click", {
21939
+ ...this.statisticEventBaseFieldsShortForm,
21940
+ wi: this.elementId,
21941
+ wl: captionViewText,
21942
+ wv: offer.offerId,
21943
+ wvi: offer.id,
21944
+ }, {
21945
+ ...this.statisticEventBaseFieldsFullForm,
21946
+ widget_id: this.elementId,
21947
+ widget_label: captionViewText,
21948
+ widget_value: offer.offerId,
21949
+ widget_value_id: offer.id,
21950
+ }, {
21951
+ forceEnableStatisticV2: true,
21952
+ });
21953
+ }
21954
+ catch (error) {
21955
+ console.error(error);
21956
+ }
21957
+ }
21958
+ isCartSupported() {
21959
+ const isCartEnabled = getTagData(this.element, "isCartEnabled") === "true";
21960
+ return this.widgetDeps.slideApiDeps.isSdkSupportProductCart && isCartEnabled;
21961
+ }
21962
+ disableHostUIInteraction() {
21963
+ this.widgetDeps.slideApiDeps.disableHorizontalSwipeGesture();
21964
+ this.widgetDeps.slideApiDeps.disableVerticalSwipeGesture();
21965
+ this.widgetDeps.slideApiDeps.disableBackpress();
21966
+ }
21967
+ enableHostUIInteraction() {
21968
+ this.widgetDeps.slideApiDeps.enableHorizontalSwipeGesture();
21969
+ this.widgetDeps.slideApiDeps.enableVerticalSwipeGesture();
21970
+ this.widgetDeps.slideApiDeps.enableBackpress();
21971
+ }
21972
+ static api = {
21973
+ widgetClassName: WidgetProductCarousel.widgetClassName,
21974
+ onRefreshUserData: WidgetProductCarousel.onRefreshUserData,
21975
+ init: function (element, localData, widgetCallbacks, widgetDeps) {
21976
+ WidgetProductCarousel.initWidget(element, localData, (el, options) => new WidgetProductCarousel(el, options, widgetCallbacks, widgetDeps));
21977
+ },
21978
+ onStart: function (element) {
21979
+ WidgetProductCarousel.getInstance(element)?.onStart();
21980
+ },
21981
+ onStop: function (element) {
21982
+ WidgetProductCarousel.getInstance(element)?.onStop();
21983
+ },
21984
+ isClickCapturedByWidget: function (element) {
21985
+ const widget = WidgetProductCarousel.getInstance(element);
21986
+ return widget?.getIsClickCapturedByWidget() ?? false;
21987
+ },
21988
+ isForcePaused: function (element) {
21989
+ const widget = WidgetProductCarousel.getInstance(element);
21990
+ return widget?.isForcePaused ?? false;
21991
+ },
21992
+ };
21993
+ static get [Symbol.for("___CTOR_ARGS___")]() { return [`HTMLElement`, `Partial`, `WidgetCallbacks`, `WidgetDeps`]; }
21994
+ }
21995
+
21300
21996
  class WidgetQuest extends WidgetBase {
21301
21997
  static DEFAULTS = {
21302
21998
  slide: null,
@@ -24232,48 +24928,130 @@ var ResultDisplayFormat;
24232
24928
  ResultDisplayFormat["Percentage"] = "percentage";
24233
24929
  })(ResultDisplayFormat || (ResultDisplayFormat = {}));
24234
24930
 
24235
- const MIN_DURATION = 2000;
24236
- const MAX_EXTRA_DURATION = 1500;
24237
- const MIN_SCALE = 0.8;
24238
- const SCALE_VARIATION = 0.4;
24239
- const MAX_DRIFT_X = 40; // [-40, 40]
24240
- const MAX_DELAY = 300;
24931
+ /**
24932
+ * Tuning for {@link ReactionFloatEffect}: layout, motion, and styling.
24933
+ *
24934
+ * - `minScale` + `scaleVariations` → CSS `--scale` (emoji size). The largest variation is used with `minScale` to size slots in `getSlotWidth`.
24935
+ * - `maxDelay` upper bound (ms) for random stagger before each spawn in `animate`.
24936
+ * - `yThreshold` → fraction of `container.clientHeight` for `--y-threshold` (vertical float threshold).
24937
+ * - `jitterFactor` → scales horizontal random offset in `getStartX` (jitter is forced to 0 on the first and last column).
24938
+ * - `fastReactionProbability` → chance to pick the fast branch in `getDuration`.
24939
+ * - `fastMinDuration` / `fastExtraDuration` → fast path duration is `fastMinDuration + random * fastExtraDuration` (ms).
24940
+ * - `slowMinDuration` / `slowExtraDuration` → slow path duration (same pattern, ms).
24941
+ * - `reactionClassName` → class on the measurement probe and on each reaction node (metrics + CSS).
24942
+ */
24943
+ const config = {
24944
+ minScale: 0.75,
24945
+ scaleVariations: [0, 0.12, 0.25],
24946
+ maxDelay: 400,
24947
+ yThreshold: 0.6,
24948
+ jitterFactor: 0.6,
24949
+ fastReactionProbability: 0.6,
24950
+ fastMinDuration: 910,
24951
+ fastExtraDuration: 325,
24952
+ slowMinDuration: 1560,
24953
+ slowExtraDuration: 520,
24954
+ reactionClassName: "narrative-element-reaction--float-effect-v2",
24955
+ };
24241
24956
  class ReactionFloatEffect {
24242
24957
  container;
24243
24958
  emoji;
24244
24959
  count;
24960
+ reactionWidth;
24961
+ slotWidth;
24962
+ slotsCount;
24245
24963
  constructor(options) {
24246
24964
  this.container = options.container;
24247
24965
  this.emoji = options.emoji;
24248
24966
  this.count = options.count ?? 6;
24967
+ this.reactionWidth = this.getReactionWidth();
24968
+ this.slotWidth = this.getSlotWidth();
24969
+ this.slotsCount = this.getSlotsCount(this.slotWidth);
24970
+ }
24971
+ getReactionWidth() {
24972
+ const el = document.createElement("span");
24973
+ el.textContent = this.emoji;
24974
+ el.style.position = "absolute";
24975
+ el.style.visibility = "hidden";
24976
+ el.classList.add(config.reactionClassName);
24977
+ this.container.appendChild(el);
24978
+ const rect = el.getBoundingClientRect();
24979
+ el.remove();
24980
+ return rect.width;
24981
+ }
24982
+ getSlotsCount(slotWidth) {
24983
+ return Math.max(1, Math.floor(this.container.clientWidth / slotWidth));
24984
+ }
24985
+ getSlotWidth() {
24986
+ const scale = config.minScale + Math.max(...config.scaleVariations);
24987
+ return this.reactionWidth * scale;
24249
24988
  }
24250
24989
  animate() {
24251
24990
  for (let i = 0; i < this.count; i++) {
24252
- const delay = Math.random() * MAX_DELAY;
24991
+ const delay = Math.random() * config.maxDelay;
24992
+ const slotIndex = i % this.slotsCount;
24253
24993
  setTimeout(() => {
24254
- this.spawnReaction();
24994
+ this.spawnReaction(slotIndex);
24255
24995
  }, delay);
24256
24996
  }
24257
24997
  }
24258
- spawnReaction() {
24259
- const el = document.createElement("span");
24260
- el.className = "narrative-element-reaction--float-effect";
24261
- el.textContent = this.emoji;
24262
- const containerWidth = this.container.clientWidth;
24263
- const thresholdY = this.container.clientHeight / 2;
24264
- const startX = Math.random() * containerWidth;
24265
- const driftX = Math.random() * MAX_DRIFT_X * 2 - MAX_DRIFT_X;
24266
- const duration = MIN_DURATION + Math.random() * MAX_EXTRA_DURATION;
24267
- const randomScale = MIN_SCALE + Math.random() * SCALE_VARIATION;
24268
- el.style.left = `${startX}px`;
24269
- el.style.setProperty("--drift", `${driftX}px`);
24270
- el.style.setProperty("--y-threshold", `${thresholdY}px`);
24271
- el.style.setProperty("--duration", `${duration}ms`);
24272
- el.style.setProperty("--scale", `${randomScale}`);
24273
- this.container.appendChild(el);
24998
+ spawnReaction(slotIndex) {
24999
+ const reaction = this.createReaction();
25000
+ const options = this.getOptions(slotIndex);
25001
+ this.setStyle(reaction, options);
25002
+ this.container.appendChild(reaction);
25003
+ this.removeReactionWithDelay(reaction, options.duration);
25004
+ return reaction;
25005
+ }
25006
+ createReaction() {
25007
+ const reaction = document.createElement("span");
25008
+ reaction.className = config.reactionClassName;
25009
+ reaction.textContent = this.emoji;
25010
+ return reaction;
25011
+ }
25012
+ getOptions(slotIndex) {
25013
+ const thresholdY = this.getThresholdY();
25014
+ const scale = this.getScale();
25015
+ const startX = this.getStartX(slotIndex);
25016
+ const duration = this.getDuration();
25017
+ return { thresholdY, scale, startX, duration };
25018
+ }
25019
+ getStartX(slotIndex) {
25020
+ const slotsRowCenteringOffset = (this.container.clientWidth - this.slotsCount * this.slotWidth) / 2;
25021
+ const baseX = slotIndex * this.slotWidth + slotsRowCenteringOffset + this.slotWidth / 2;
25022
+ return baseX + this.getJitter(slotIndex);
25023
+ }
25024
+ getJitter(slotIndex) {
25025
+ const jitter = (Math.random() - 0.5) * this.slotWidth * config.jitterFactor;
25026
+ if (slotIndex === 0 && jitter < 0) {
25027
+ return -jitter;
25028
+ }
25029
+ if (slotIndex === this.slotsCount - 1 && jitter > 0) {
25030
+ return -jitter;
25031
+ }
25032
+ return jitter;
25033
+ }
25034
+ getDuration() {
25035
+ const isFast = Math.random() < config.fastReactionProbability;
25036
+ return isFast ? config.fastMinDuration + Math.random() * config.fastExtraDuration : config.slowMinDuration + Math.random() * config.slowExtraDuration;
25037
+ }
25038
+ getThresholdY() {
25039
+ return config.yThreshold * this.container.clientHeight;
25040
+ }
25041
+ getScale() {
25042
+ const scaleVariation = config.scaleVariations[Math.floor(Math.random() * (config.scaleVariations.length - 1))];
25043
+ return config.minScale + scaleVariation;
25044
+ }
25045
+ setStyle(reaction, options) {
25046
+ reaction.style.left = `${options.startX}px`;
25047
+ reaction.style.setProperty("--y-threshold", `${options.thresholdY}px`);
25048
+ reaction.style.setProperty("--duration", `${options.duration}ms`);
25049
+ reaction.style.setProperty("--scale", `${options.scale}`);
25050
+ }
25051
+ removeReactionWithDelay(reaction, delay) {
24274
25052
  setTimeout(() => {
24275
- el.remove();
24276
- }, duration);
25053
+ reaction.remove();
25054
+ }, delay);
24277
25055
  }
24278
25056
  static get [Symbol.for("___CTOR_ARGS___")]() { return [`ReactionFloatEffectOptions`]; }
24279
25057
  }
@@ -24340,7 +25118,7 @@ class WidgetReactions extends WidgetBase {
24340
25118
  const geometryParent = this.element.closest(".narrative-slide-elements");
24341
25119
  if (!geometryParent)
24342
25120
  return;
24343
- new ReactionFloatEffect({ container: geometryParent, emoji, count: 15 /* fontSize: getComputedStyle(this.reactions[0]).fontSize */ }).animate();
25121
+ new ReactionFloatEffect({ container: geometryParent, emoji, count: 20 /* fontSize: getComputedStyle(this.reactions[0]).fontSize */ }).animate();
24344
25122
  }
24345
25123
  initFromLocalData({ selectedIndex, widgetDoneAt, reaction }) {
24346
25124
  try {
@@ -24582,6 +25360,9 @@ class EsModuleLayoutApi {
24582
25360
  get widgetProductsApi() {
24583
25361
  return WidgetProducts.api;
24584
25362
  }
25363
+ get widgetProductCarouselApi() {
25364
+ return WidgetProductCarousel.api;
25365
+ }
24585
25366
  get widgetTooltipApi() {
24586
25367
  return WidgetTooltip.api;
24587
25368
  }