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