/** * Gets the template markup for sku component. * @param {PricingClientConfig} pricingConfig config object. * @param {ProductPricingResponse} pricingResponse pricing response object. * @returns {string} The template markup for sku component. */ function getSkuMarkup(pricingConfig, pricingResponse) { if(!pricingConfig || !pricingResponse) { return null; } switch (pricingResponse.responseCode) { case "Success": return getSkuAvailableMarkup(pricingConfig, pricingResponse); case "DisabledMarket": return getSkuDisabledMarketTemplate(pricingConfig); default: return getSkuUnavailableTemplate(pricingConfig); } } /** * Gets the template markup for available sku component. * @param {PricingClientConfig} pricingConfig config object. * @param {ProductPricingResponse} pricingResponse pricing response object. * @returns {string} The template markup for sku component. */ function getSkuAvailableMarkup(pricingConfig, pricingResponse) { const screenReaderText = window.ocReimagine.ProductPriceModule.ProductPricingTemplates.createScreenReaderText(pricingConfig, pricingResponse); const screenReaderMarkup = `

${screenReaderText}

`; const msrpMarkup = `
${pricingResponse.sku.displayMSRPPrice}
`; const displayUnitMarkup = `
${pricingConfig.displayUnit}
`; return `

${pricingConfig.renderTitle}

${pricingConfig.isDiscounted ? screenReaderMarkup : ""}
${pricingConfig.isDiscounted ? msrpMarkup : ""}
${pricingResponse.sku.displayListPrice}
${pricingConfig.displayUnit ? displayUnitMarkup : ""}
`; } /** * Gets unavailable pricing component markup. * @param {PricingClientConfig} pricingConfig config object. * @returns HTML string for unavailable pricing component. */ function getSkuUnavailableTemplate(pricingConfig) { const unavailableMarkup = `
${pricingConfig.renderTitle ? `

${pricingConfig.renderTitle}

` : "" }
`; return unavailableMarkup; } /** * Gets disabled pricing component markup. * @param {PricingClientConfig} pricingConfig config object. * @returns HTML string for disabled pricing component. */ function getSkuDisabledMarketTemplate(pricingConfig) { const disabledMarkup = `
${pricingConfig.renderTitle ? `

${pricingConfig.renderTitle}

` : "" }
`; return disabledMarkup; } /** * Type definition for the sku web component action link config. * @typedef {Object} SkuWebComponentActionLinkConfig * @property {string} text - The text to be rendered for the action link. * @property {string} href - The action link URL. * @property {string} target - The target of the action link. * @property {string} ariaLabel - The aria-label for the action link. */ /** * Type definition for the sku web component actions. * @typedef {Object} SkuWebComponentActionsConfig * @property {SkuWebComponentActionLinkConfig?} primary - The primary action link configuration. * @property {SkuWebComponentActionLinkConfig?} secondary - The secondary action link configuration. */ /** * Type definition for the sku web component config. * @typedef {Object} SkuWebComponentConfig * @property {string} title - The product title for the sku web component to render. * @property {string} discountPrice - The discount price value for the sku web component to render. * @property {string} currentPrice - Current price value for the sku web component to render. * @property {string} recurrence - Recurrence policy for the sku web component to render. * @property {string} label - Label for the sku web component to render. * @property {string} srText - Screen reader text for the sku web component to render. * @property {string} note - The product note for the sku web component to render. *Note contains the Commitment and Tax Disclaimer * @property {string} embargoMarketMessage - Localized message for disabled markets. * @property {string} priceUnavailableText - Message to be rendered by sku web component when the product is unavailable. * @property {string} unavailableMessage - Localized message for unavailable products. * @property {SkuWebComponentActionsConfig} buttonGroup - Action link buttons for SKU web component. */ /** * Gets the template markup for available sku component. * @param {HTMLTemplateElement} pricingTemplateElement pricing component element * @param {PricingClientConfig} pricingConfig config object. * @param {ProductPricingResponse?} pricingResponse pricing response object. * @returns {DocumentFragment?} The template markup for sku component. */ function getSkuWebComponentMarkup(pricingTemplateElement, pricingConfig, pricingResponse) { // Get the SKU web component config from the template element const encodedString = pricingTemplateElement.getAttribute(window.ocReimagine.ProductPriceModule.ProductPricingConstants.SkuWebComponentAttributes.Data.SkuWebComponentConfig); if (!encodedString) { return null; } const originalSkuWebComponentConfigString = decodeURIComponent(encodedString); if (!originalSkuWebComponentConfigString) { return null; } /** * @type {SkuWebComponentConfig} * @description The original SKU web component config object. */ const originalSkuWebComponentConfig = JSON.parse(originalSkuWebComponentConfigString); // Build SKU web component config const updatedSkuWebComponentConfig = this.buildSkuWebComponentConfig(originalSkuWebComponentConfig, pricingConfig, pricingResponse); // Build SKU web component markup fragment const skuWebComponentFragment = this.buildSkuWebComponentMarkupFragment(updatedSkuWebComponentConfig, pricingConfig, pricingResponse); return skuWebComponentFragment; } /** * Builds the SKU web component config object based on the original config and pricing response. * @param {SkuWebComponentConfig} originalConfig * @param {PricingClientConfig} pricingConfig * @param {ProductPricingResponse?} pricingResponse * @returns {SkuWebComponentConfig} Updated SKU web component config object. */ function buildSkuWebComponentConfig(originalConfig, pricingConfig, pricingResponse) { /** * @type {SkuWebComponentConfig} */ let skuWebComponentConfig = {}; skuWebComponentConfig.title = pricingConfig.renderTitle; if (pricingResponse === null) { skuWebComponentConfig.priceUnavailableText = originalConfig.unavailableMessage; return skuWebComponentConfig; } if (pricingResponse.responseCode === window.ocReimagine.ProductPriceModule.ProductPricingConstants.Enumerables.Response.Success) { skuWebComponentConfig = { ...originalConfig, ...skuWebComponentConfig }; skuWebComponentConfig.recurrence = pricingConfig.displayUnit; if (pricingResponse.catalogType == 4) { skuWebComponentConfig.taxDisclaimer = originalConfig.commercialTaxDisclaimer; } else { skuWebComponentConfig.taxDisclaimer = originalConfig.consumerTaxDisclaimer; } if (pricingResponse.sku.discountPrice > 0) { skuWebComponentConfig.currentPrice = pricingResponse.sku.displayMSRPPrice; skuWebComponentConfig.discountPrice = pricingResponse.sku.displayDiscountPrice; skuWebComponentConfig.srText = window.ocReimagine.ProductPriceModule.ProductPricingTemplates.createScreenReaderText(pricingConfig, pricingResponse); } else { skuWebComponentConfig.currentPrice = pricingResponse.sku.displayListPrice; } } else if (pricingResponse.responseCode === window.ocReimagine.ProductPriceModule.ProductPricingConstants.Enumerables.Response.DisabledMarket) { skuWebComponentConfig.priceUnavailableText = originalConfig.embargoMarketMessage; } else { skuWebComponentConfig.priceUnavailableText = originalConfig.unavailableMessage; } return skuWebComponentConfig; } /** * Builds the SKU web component markup fragment based on the updated SKU web component config and pricing config. * @param {SkuWebComponentConfig} updatedSkuWebComponentConfig * @param {PricingClientConfig} pricingConfig * @param {ProductPricingResponse?} pricingResponse * @returns {DocumentFragment} The SKU web component markup fragment. */ function buildSkuWebComponentMarkupFragment(updatedSkuWebComponentConfig, pricingConfig, pricingResponse) { const skuWebComponentSlotMarkup = pricingResponse && pricingResponse.responseCode === window.ocReimagine.ProductPriceModule.ProductPricingConstants.Enumerables.Response.Success ? this.buildAvailableSlotsMarkup(updatedSkuWebComponentConfig) : this.buildUnavailableSlotsMarkup(updatedSkuWebComponentConfig); const skuWebComponentMarkup = ` ${skuWebComponentSlotMarkup} ` const skuWebComponentFragment = document .createRange() .createContextualFragment(skuWebComponentMarkup); return skuWebComponentFragment; } /** * Builds the SKU web component available slot elements markup. * @param {SkuWebComponentConfig} updatedSkuWebComponentConfig * @returns {String} HTML markup for available slots. */ function buildAvailableSlotsMarkup(updatedSkuWebComponentConfig) { const markup = ` ${updatedSkuWebComponentConfig.title ? `${updatedSkuWebComponentConfig.title}` : ""} ${updatedSkuWebComponentConfig.label ? `
${updatedSkuWebComponentConfig.label}
` : ""} ${updatedSkuWebComponentConfig.srText ? `
${updatedSkuWebComponentConfig.srText}
` : ""} ${updatedSkuWebComponentConfig.displayDiscountPrice ? `${updatedSkuWebComponentConfig.displayDiscountPrice}` : ""} ${updatedSkuWebComponentConfig.currentPrice ? `${updatedSkuWebComponentConfig.currentPrice}` : ""} ${updatedSkuWebComponentConfig.recurrence ? `
${updatedSkuWebComponentConfig.recurrence}
` : ""} ${updatedSkuWebComponentConfig.note || updatedSkuWebComponentConfig.taxDisclaimer ? `
${updatedSkuWebComponentConfig.note}${updatedSkuWebComponentConfig.taxDisclaimer}
` : ""} ${updatedSkuWebComponentConfig.buttonGroup ? this.buildButtonGroupMarkup(updatedSkuWebComponentConfig) : ""} `; return markup; } /** * Builds the SKU web component unavailable slot elements markup. * @param {SkuWebComponentConfig} updatedSkuWebComponentConfig * @returns {String} HTML markup for unavailable slots. */ function buildUnavailableSlotsMarkup(updatedSkuWebComponentConfig) { const markup = ` ${updatedSkuWebComponentConfig.title ? `${updatedSkuWebComponentConfig.title}` : ""}

${updatedSkuWebComponentConfig.priceUnavailableText}

`; return markup; } /** * Builds the button group web component markup. * @param {SkuWebComponentConfig} updatedSkuWebComponentConfig * @returns {String} HTML markup for the button group slots. */ function buildButtonGroupMarkup(updatedSkuWebComponentConfig) { const markup = ` ${updatedSkuWebComponentConfig.buttonGroup.primary ? this.buildButtonMarkup(updatedSkuWebComponentConfig.buttonGroup.primary) : ""} ${updatedSkuWebComponentConfig.buttonGroup.secondary ? this.buildButtonMarkup(updatedSkuWebComponentConfig.buttonGroup.secondary) : ""} `; return markup; } /** * Builds the button group web component markup. * @param {SkuWebComponentActionLinkConfig} buttonConfig * @returns {String} HTML markup for the button. */ function buildButtonMarkup(buttonConfig) { const markup = ` ${buttonConfig.text} `; return markup; } /** * Queries the document and shadowRoot to find element matching the selector. * @param {string} selector - The selector to match element against. * @param {HTMLElement?} rootNode - The root node to start the search from. * @param {boolean?} deep - Whether to search deep into shadow DOM (default is false). * @returns {Element | null} - The matching elements or null if none found. */ function querySelectorDeep(selector, rootNode = document.body, deep = false) { const results = querySelectorAllDeep(selector, rootNode, deep, false); return Array.isArray(results) && results.length ? results[0] : results; } /** * Queries the document and shadowRoot for elements matching the selector. * @param {string} selector - The selector to match elements against. * @param {HTMLElement?} rootNode - The root node to start the search from. * @param {boolean?} deep - Whether to search deep into shadow DOM (default is false). * @param {boolean?} all - Whether to return all matching elements or just the first one. * @returns {Element[] | Element | null} - The matching elements or null if none found. */ function querySelectorAllDeep(selector, rootNode = document.body, deep = false, all = true) { if (!selector) { return null; } if (!deep) { if (all) { return Array.from(rootNode.querySelectorAll(selector)); } else { return rootNode.querySelector(selector); } } /** * @type {Element[]} * @description Array to store the matching elements */ const results = []; /** * @description Traverses the DOM and shadow DOM to find matching elements * @param {Element} node * @returns {void} */ const traverser = (node) => { if (!all && results.length) return; // 1. decline all nodes that are not elements if (node.nodeType !== Node.ELEMENT_NODE) return; // 2. add the node to the array, if it matches the selector if (node.matches(selector)) { results.push(node); if (!all) return; } // 3. loop through the children const children = node.children; if (children.length) { for (const child of children) { traverser(child); } } // 4. check for shadow DOM, and loop through it's children const shadowRoot = node.shadowRoot; if (shadowRoot) { const shadowChildren = shadowRoot.children; for (const shadowChild of shadowChildren) { traverser(shadowChild); } } } traverser(rootNode); return all ? results : (results.length ? results[0] : null); } /** * Checks if an element exists in the document, including shadow DOMs. * @param {Element} element - The element to check. * @param {Element | ShadowRoot} root - The root node to start the search from (default is document.body). * @param {boolean?} deep - Whether to search deep into shadow DOM (default is false). * @returns {boolean} True if the element exists in the document or shadow DOM, false otherwise. */ function isElementInDocument(element, root = document.body, deep = false) { if (!element || !root) return false; // Check if the element exists in the current root if (root.contains(element)) return true; // If deep traversal is not required, return false if(!deep) return false; // Traverse shadow DOMs recursively const shadowHosts = root.querySelectorAll('*'); for (const host of shadowHosts) { if (host.shadowRoot && isElementInDocument(element, host.shadowRoot, deep)) { return true; } } return false; } /** * Update the pricing manager instance when SKU web components updates on DOM * @summary this custom event is dispatched by external pricing-components whenever the product pricing elements changes on DOM */ document.addEventListener("onOcrSkuWebComponentUpdate", (event) => { // Check if the event has a detail object and if it has the traversable property // If traversable is true, then we need to traverse the shadow DOM // If traversable is false, then we need to traverse the light DOM const enableShadowTraversal = event.detail && event.detail.traversable; // Get all the sku web components const skuWebComponents = querySelectorAllDeep("[data-ocr-pricing-component='sku-web-component']", document.body, enableShadowTraversal); skuWebComponents.forEach((skuWebComponent) => { // If the sku web component has already been initialized, then return if ( skuWebComponent.hasAttribute("data-ocr-pricing-status") ) { return; } skuWebComponent.setAttribute("data-ocr-pricing-status", "0"); }); // Dispatch the custom event to update the pricing manager instance document.dispatchEvent(new CustomEvent("onOcrClientPricingChange", { detail: event.detail })); }); /** * Gets the template markup for pricing-token component. * @param {HTMLSpanElement} pricingTemplateElement authored pricing component element * @param {PricingClientConfig} pricingConfig config object. * @param {ProductPricingResponse} pricingResponse pricing response object. * @returns {string} The template markup for sku component. */ function getPricingTokenMarkup(pricingTemplateElement, pricingConfig, pricingResponse) { if (!pricingConfig || !pricingResponse) { return null; } // Set product price response code to the token element. pricingTemplateElement.previousElementSibling.setAttribute("data-oc-product", `purchase ${pricingResponse.responseCode}`); switch (pricingResponse.responseCode) { case "Success": return getAvailableTokenMarkup(pricingTemplateElement, pricingConfig, pricingResponse); case "DisabledMarket": return getDisabledTokenMarkup(pricingTemplateElement, pricingConfig); default: return getUnavailableTokenMarkup(pricingTemplateElement, pricingConfig); } } /** * Gets the template markup for available token component. * @param {HTMLSpanElement} pricingTemplateElement authored pricing component element * @param {PricingClientConfig} pricingConfig config object. * @param {ProductPricingResponse} pricingResponse pricing response object. * @returns {string} The template markup for pricing token component. */ function getAvailableTokenMarkup(pricingTemplateElement, pricingConfig, pricingResponse) { if (!pricingConfig || !pricingResponse) { return null; } switch (pricingConfig.renderType) { case "list-price": return pricingResponse.sku.displayListPrice; case "title": return pricingResponse.title; case "sku-title": return pricingResponse.sku.title; case "msrp": return pricingResponse.sku.displayMSRPPrice; case "discount": return pricingResponse.sku.displayDiscountPrice; default: //Non catalog products will not have any ajax class. return ""; } } /** * Gets unavailable pricing component markup. * @param {HTMLSpanElement} pricingTemplateElement authored pricing component element * @param {PricingClientConfig} pricingConfig config object. * @returns HTML string for unavailable pricing component. */ function getUnavailableTokenMarkup(pricingTemplateElement, pricingConfig) { const unavailableMarkup = ""; // Set product price response code to the token element. pricingTemplateElement.previousElementSibling.setAttribute("data-oc-product", "purchase NotFound"); return unavailableMarkup; } /** * Gets disabled pricing component markup. * @param {HTMLSpanElement} pricingTemplateElement authored pricing component element * @param {PricingClientConfig} pricingConfig config object. * @returns HTML string for disabled pricing component. */ function getDisabledTokenMarkup(pricingTemplateElement, pricingConfig) { const disabledMarkup = ""; // Set product price response code to the token element. pricingTemplateElement.previousElementSibling.setAttribute("data-oc-product", "purchase DisabledMarket"); return disabledMarkup; } (function () { const pricingEvents = Object.freeze({ MarketSelector: Object.freeze({ OnInit: "onInit", }), Pricing: Object.freeze({ OnChange: "onOcrClientPricingChange", }), }); /** Check if OneCloud Reimagine namespace exists */ if (!window.ocReimagine) { window.ocReimagine = {}; } /** Create product price module namespace */ if (!window.ocReimagine.ProductPriceModule) { window.ocReimagine.ProductPriceModule = {}; } /** * Initializes the reimagine product pricing manager and services * @param {string | undefined | null} market * @returns {void} */ function initializeProductPriceModule(market) { try { // Check if product pricing manager instance exists if ( window.ocReimagine && window.ocReimagine.ProductPriceModule && window.ocReimagine.ProductPriceModule.PricingManagerInstance ) { // If it exists, that means it has already been initialized so no need to re-initialize return; } window.ocReimagine.ProductPriceModule.PricingManagerInstance = new window.ocReimagine.ProductPriceModule.ProductPricingManager({ market }); } catch (error) { console.warn("[OCR][ProductPricingModule] module failure: ", error); } } /** * By default the reimagine pricing-scripts are initialized on page load complete */ document.addEventListener("DOMContentLoaded", () => { initializeProductPriceModule(); }); /** * Initialize the reimagine pricing-scripts with market-selector init event * @summary since there is a race condition on page load event between market-selector's init event and pricing-module's init event, * try to initialize the pricing scripts after the market-selector is initialized and dispatches the init event */ document.addEventListener(pricingEvents.MarketSelector.OnInit, (event) => { if (!event.detail || !event.detail.value) { return; } initializeProductPriceModule(event.detail.value); }); /** * Update the pricing manager instance when the product pricing changes on DOM * @summary this custom event is dispatched by external pricing-components whenever the product pricing elements changes on DOM */ document.addEventListener(pricingEvents.Pricing.OnChange, (event) => { // Check if product pricing manager instance has been initialized if ( window.ocReimagine && window.ocReimagine.ProductPriceModule && window.ocReimagine.ProductPriceModule.PricingManagerInstance ) { // If it exists, then call the pricing manager update method window.ocReimagine.ProductPriceModule.PricingManagerInstance.updatePricingManager(event.detail); } }); })(); /** * Constants for the product pricing configuration options and services. * @class {ProductPricingConstants} - configuration parameters for the product pricing module. */ window.ocReimagine.ProductPriceModule.ProductPricingConstants = class ProductPricingConstants { /** * Default values for the product pricing module. * @readonly */ static Defaults = Object.freeze({ Locale: "en-us", }); /** * Cache policy for the product pricing module. * @readonly */ static CachePolicy = Object.freeze({ IsCachingEnabled: true, StorageLocation: "memory", }); /** * Request settings for the product pricing module. * @readonly */ static Request = Object.freeze({ Method: "GET", RelativeUri: "/m365/product/price", Headers: Object.freeze({ "Content-Type": "application/json", }), QueryParameters: Object.freeze({ v: "4", r: "json", }), MaxQueryCount: 5, }); /** * Pricing component template names for the product pricing module. */ static Templates = Object.freeze({ Sku: "sku", PricingToken: "pricing-token", SkuWebComponent: "sku-web-component", }); /** * HTML Attributes for the product pricing module. * @readonly */ static Attributes = Object.freeze({ Data: Object.freeze({ Component: "data-ocr-pricing-component", PricingIdentifier: "data-ocr-pricing-identifier", LegacyProductMain: "data-oc-product", ProductMain: "data-ocr-product" }) }); /** * HTML Selectors for the product pricing module. * @readonly */ static Selectors = Object.freeze({ Dataset: Object.freeze({ Component: `[${this.Attributes.Data.Component}]`, RenderSection: "[data-ocr-pricing-section='render']", SkuRequestQuery: "[data-ocr-sku-request]", TemplateContent: "[data-ocr-pricing-content]", PricingConfig: "[data-ocr-pricing-config]", MarketSelector: "[data-mount='market-selector']", PricingIdentifier: `[${this.Attributes.Data.PricingIdentifier}]`, PricingStatus: "[data-ocr-pricing-status]", PricingToken: `[${this.Attributes.Data.Component}='${this.Templates.PricingToken}']` }), }); /** * Enumerables for the product pricing module. * @readonly */ static Enumerables = Object.freeze({ Response: Object.freeze({ Undefined: "Undefined", Success: "Success", NotFound: "NotFound", NoAvailableSku: "NoAvailableSku", DisabledMarket: "DisabledMarket", }), TitleType: Object.freeze({ SKU: "SKU", PRODUCT: "PRODUCT", OVERRIDE: "OVERRIDE", }), }); /** * Parameters for the product pricing module. */ static Parameters = Object.freeze({ ScreenReader: Object.freeze({ ListPriceKey: "%{listPrice}", MsrpKey: "%{msrpPrice}", }), MarketSelector: Object.freeze({ Query: Object.freeze({ Market: "market", }), RefreshMode: Object.freeze({ AJAX: "ajax", }), }), }); /** * Constants for custom event listener names * @readonly */ static Events = Object.freeze({ MarketSelector: Object.freeze({ OnRefreshed: "onRefreshed", OnInit: "onInit", }), Pricing: Object.freeze({ OnChange: "onOcrClientPricingChange", OnComplete: "onComplete", // Legacy event name for onOcrClientPricingRenderComplete OnRenderComplete: "onOcrClientPricingRenderComplete", }), SkuWebComponent: Object.freeze({ OnUpdate: "onOcrSkuWebComponentUpdate", }), }); static SkuWebComponentAttributes = Object.freeze({ Data: Object.freeze({ SkuWebComponentConfig: "data-ocr-sku-web-component-config", SkuWebComponentStatus: "data-ocr-pricing-status", }), Selectors: Object.freeze({ SkuWebComponent: "[data-ocr-pricing-component='sku-web-component']", }), }); }; //#region Reimagine Product Pricing Templates Class /** * @class ProductPricingTemplates - Helper Class for getting product pricing component templates. */ window.ocReimagine.ProductPriceModule.ProductPricingTemplates = class ProductPricingTemplates { /** * Gets the constants for the product pricing module. */ static pricingConstants = window.ocReimagine.ProductPriceModule.ProductPricingConstants; /** * Gets the template markup for the given template name. * @param {HTMLTemplateElement | HTMLSpanElement} pricingTemplateElement pricing component element * @param {string} templateName template name * @param {PricingClientConfig} pricingConfig config object * @param {ProductPricingResponse} pricingResponse pricing response object. * @returns {DocumentFragment | string | null} The template markup for the given template name. */ static getPricingTemplate( pricingTemplateElement, templateName, pricingConfig, pricingResponse ) { /** * Get the template markup for the given template name. * @type {DocumentFragment | string | null} */ let updatedTemplateFragment = null; switch (templateName) { case this.pricingConstants.Templates.Sku: const skuTemplateMarkupText = getSkuMarkup( pricingConfig, pricingResponse ); updatedTemplateFragment = this.replaceConfigContent( skuTemplateMarkupText, pricingTemplateElement, pricingConfig ); break; case this.pricingConstants.Templates.SkuWebComponent: updatedTemplateFragment = getSkuWebComponentMarkup( pricingTemplateElement, pricingConfig, pricingResponse ); break; case this.pricingConstants.Templates.PricingToken: updatedTemplateFragment = getPricingTokenMarkup(pricingTemplateElement, pricingConfig, pricingResponse); break; default: updatedTemplateFragment = null; break; } return updatedTemplateFragment; } /** * Creates a screen reader text for the given screen reader template string. * @param {PricingClientConfig} pricingConfig config object * @param {ProductPricingResponse} pricingResponse pricing response object. * @returns {string} The sr text for the given template format. */ static createScreenReaderText(pricingConfig, pricingResponse) { const screenReaderTemplate = pricingConfig.discountedTextTemplate; let screenReaderText = ""; if (pricingConfig.isDiscounted && screenReaderTemplate) { // Replace the current/list price key with the current/list price value. screenReaderText = screenReaderTemplate.replace( window.ocReimagine.ProductPriceModule.ProductPricingConstants.Parameters .ScreenReader.ListPriceKey, pricingResponse.sku.displayListPrice ); // Replace the msrp price key with the msrp price value. screenReaderText = screenReaderText.replace( window.ocReimagine.ProductPriceModule.ProductPricingConstants.Parameters .ScreenReader.MsrpKey, pricingResponse.sku.displayMSRPPrice ); } return screenReaderText; } /** * Replaces the config content from the pricing component element in the template markup. * @param {string} templateMarkup template markup. * @param {HTMLTemplateElement | HTMLSpanElement} pricingTemplateElement authored pricing template element. * @param {PricingClientConfig} pricingConfig config object * @returns {DocumentFragment} The template markup for the given template name. */ static replaceConfigContent( templateMarkup, pricingTemplateElement, pricingConfig ) { /** * @type {Node} * Cloned node of the authored template element. */ let authoredTemplateFragment; if (pricingTemplateElement instanceof HTMLTemplateElement) { authoredTemplateFragment = pricingTemplateElement.content.cloneNode(true); } else { authoredTemplateFragment = pricingTemplateElement.cloneNode(true); } // Create a range to parse the template markup string into a DocumentFragment. const templateFragment = document .createRange() .createContextualFragment(templateMarkup); // Iterate over configured template fragment content elements. const templateContentElements = templateFragment.querySelectorAll( `${this.pricingConstants.Selectors.Dataset.TemplateContent}[data-ocr-pricing-identifier="${pricingConfig.pricingIdentifier}"]` ); templateContentElements.forEach((templateContentElement) => { const attributeValue = templateContentElement.dataset.ocrPricingContent; const configElement = authoredTemplateFragment.querySelector( `[data-ocr-pricing-content="${attributeValue}"][data-ocr-pricing-identifier="${pricingConfig.pricingIdentifier}"]` ); // Remove the content from template if match does not exist in authored template. if (!configElement) { templateContentElement.remove(); return; } // Replace the config content from the pricing component element in the template markup. templateContentElement.replaceWith(configElement); }); return templateFragment; } /** * Gets unavailable pricing component markup. * @param {string} templateName template name * @param {PricingConfig} pricingConfig config object. * @param {HTMLTemplateElement | HTMLSpanElement} pricingTemplateElement authored pricing component element * @returns {DocumentFragment | string | null} HTML string for unavailable pricing component. */ static getUnavailableTemplate(templateName, pricingConfig, pricingTemplateElement) { let unavailableTemplateFragment = null; switch (templateName) { case this.pricingConstants.Templates.Sku: const skuTemplateMarkupText = getSkuUnavailableTemplate( pricingConfig ); unavailableTemplateFragment = this.replaceConfigContent( skuTemplateMarkupText, pricingTemplateElement, pricingConfig ); break; case this.pricingConstants.Templates.SkuWebComponent: unavailableTemplateFragment = getSkuWebComponentMarkup( pricingTemplateElement, pricingConfig ); break; case this.pricingConstants.Templates.PricingToken: unavailableTemplateFragment = getUnavailableTokenMarkup(pricingTemplateElement, pricingConfig); break; default: unavailableTemplateFragment = null; break; } return unavailableTemplateFragment; } /** * Gets disabled pricing component markup. * @param {string} templateName template name * @param {PricingConfig} pricingConfig config object. * @param {HTMLTemplateElement | HTMLSpanElement} pricingTemplateElement authored pricing component element * @returns HTML string for disabled pricing component. */ static getDisabledMarketTemplate(templateName, pricingConfig, pricingTemplateElement) { let disabledTemplateFragment = null; switch (templateName) { case this.pricingConstants.Templates.Sku: const skuTemplateMarkupText = getSkuDisabledMarketTemplate( pricingConfig ); disabledTemplateFragment = this.replaceConfigContent( skuTemplateMarkupText, pricingTemplateElement, pricingConfig ); break; case this.pricingConstants.Templates.SkuWebComponent: disabledTemplateFragment = getSkuWebComponentMarkup( pricingTemplateElement, pricingConfig ); break; case this.pricingConstants.Templates.PricingToken: disabledTemplateFragment = getDisabledTokenMarkup(pricingTemplateElement, pricingConfig); break; default: disabledTemplateFragment = null; break; } return disabledTemplateFragment; } /*============================ Component Templates ============================*/ }; //#endregion Reimagine Product Pricing Template Class /** * @class ProductPricingRequest - Helper Class for making product pricing requests. */ window.ocReimagine.ProductPriceModule.ProductPricingRequest = class ProductPricingRequest { /** * Gets the constants for the product pricing module. */ pricingConstants = window.ocReimagine.ProductPriceModule.ProductPricingConstants; /** * Gets or sets the current locale for the page. * @type {string} */ locale; /** * Gets or sets the country for the page. * @type {string} */ country; /** * Gets or sets the market for the page. * @type {string | undefined | null} */ #market; /** * Gets the current selected market for the page. */ get market() { return this.#market; } /** * Sets the current selected market for the page. */ set market(value) { this.#market = value; } /** * Gets or sets the flag to indicate if caching is enabled for product pricing requests. * @type {boolean} */ isCachingEnabled; /** * Gets or sets the instances of rendered pricing component instances for the current page. * This is the source array for the rendered instances of the pricing component. * @type {ocReimagine.ProductPriceModule.ProductPricingRendering[]} */ renderedInstances; /** * Gets or sets the query from each instance of rendered pricing component for the current page. * @type {Set} */ uniqueQuerySet; /** * Gets or sets the map of pricing component request and response for the current page. * @description * Key - request query string, * Value - product pricing response for the request query * @summary this map is used to cache in-memory the product pricing response for the current page and avoid duplicate requests for the same request query. * @type {Map} */ responseCacheMap; /** * Gets or sets the instances of XHR request controllers for each in-progress requests. * @type {Array} */ xhrRequestControllers; /** * Initializes instance of the product pricing request helper class. * @param {string} locale - The locale 'll-cc' of the page. * @param {string} country - Current country of the page. * @param {string | undefined | null} market - Current selected market of the page. */ constructor(locale, country, market) { this.locale = locale; this.country = country; this.market = market; this.renderedInstances = []; this.uniqueQuerySet = new Set(); this.responseCacheMap = new Map(); this.xhrRequestControllers = []; this.isCachingEnabled = this.pricingConstants.CachePolicy.IsCachingEnabled; } /** * Clears the render instances queue for the current page. */ clearRequestManager() { this.abortPendingRequests(); this.renderedInstances = []; this.uniqueQuerySet.clear(); this.responseCacheMap.clear(); } /** * Enqueues the rendering instance the requests queue for the current page. * @param {ocReimagine.ProductPriceModule.ProductPricingRendering} renderInstance - The pricing component rendering instance to enqueue. */ enqueueRequest(renderInstance) { // Set the request query key map for the current instance of the rendering pricing component product price manager class // Store the render instance in the queue this.renderedInstances.push(renderInstance); // Adds the request query to the queue if it does not exist this.uniqueQuerySet.add(renderInstance.pricingConfig.requestQuery); } /** * Dequeues the rendering instance from the requests queue for the current page. * @param {string} instanceIdentifier * @param {boolean?} enableShadowTraversal - Flag to enable shadow DOM traversal. */ dequeueRequest(instanceIdentifier, enableShadowTraversal = false) { // Check if the instance identifier is not provided if (!instanceIdentifier) { return; } // Get the removed element render instance from the existing rendered instances const removedInstance = this.renderedInstances.find( (renderInstance) => !isElementInDocument(renderInstance.pricingComponentElement, document.body, enableShadowTraversal) && renderInstance.instanceIdentifier === instanceIdentifier ); // Check if the removed instance is not found if (!removedInstance) { return; } // Remove the instance from the existing rendered instances this.renderedInstances = this.renderedInstances.filter( (renderInstance) => !isElementInDocument(renderInstance.pricingComponentElement, document.body, enableShadowTraversal) && renderInstance.instanceIdentifier !== instanceIdentifier ); // Check if the request query is still in use by any other render instance const isRequestQueryInUse = this.renderedInstances.some( (renderInstance) => renderInstance.pricingConfig.requestQuery === removedInstance.pricingConfig.requestQuery ); // If the request query is still in use, do not remove it from the queue if (isRequestQueryInUse) { return; } // Remove the request query from the queue this.uniqueQuerySet.delete(removedInstance.pricingConfig.requestQuery); } /** * Gets the unique cache key for the current request query. * @param {string} requestQuery - The request query string. * @returns {string} requestQueryKey - The unique key for the request query, llcc and market. */ getRequestCacheKey(requestQuery) { // Create unique key for the request query by combining the locale, product ID, recurrence and payment cadence. const keyParameters = new URLSearchParams(); keyParameters.set("q", requestQuery); // Append llcc to the request query with market as cc if available. if (this.market) { keyParameters.set("llcc", `${this.country}-${this.market}`); } else { keyParameters.set("llcc", this.locale); } // Sort the query parameters to create the unique cache key for the request query keyParameters.sort(); // Create the unique cache key for the request query const requestQueryKey = keyParameters.toString(); return requestQueryKey; } /** * Updates the pricing request manager for the current page. * @param {string} market - The current selected market for the page. */ updateRequestManager(market) { if (this.market !== market) { this.market = market; } } /** * Combines each request query into group of maximum allowed requests per call. * @see {@link ProductPricingConstants.Request.MaxQueryCount} * @param {string[]} fetchQueries - The request queries to combine. * @returns {string[]} combinedQueries - The combined request queries. */ combineRequestQueries(fetchQueries) { // Return empty array if no fetch queries are available if (!fetchQueries.length) { return []; } /** * @example ["query1,query2,query3,query4,query5", "query6,query7,query8,query9", ...] * @type {string[]} */ const combinedQueries = []; // Split the request queries into batches of maximum allowed requests per call // Since the product pricing API only allows only certain number of queries per request for ( let i = 0; i < fetchQueries.length; i += this.pricingConstants.Request.MaxQueryCount ) { const requestQueries = fetchQueries.slice( i, i + this.pricingConstants.Request.MaxQueryCount ); const combinedRequestQuery = requestQueries.join(","); combinedQueries.push(combinedRequestQuery); } return combinedQueries; } /** * Starts processing the pricing component requests from queue for the current page. * @param {(productsResponse: ProductPricingResponse[], relatedRenderedInstances: ocReimagine.ProductPriceModule.ProductPricingRendering[]) => void} onFulfilledCallback - The callback function to execute when the fetch request promise is fulfilled. * @param {(error: Error, relatedRenderedInstances: ocReimagine.ProductPriceModule.ProductPricingRendering[]) => void} onRejectedCallback - The callback function to execute when the fetch request promise is rejected. * @param {((this: XMLHttpRequest, ev: Event, relatedRenderedInstances: ocReimagine.ProductPriceModule.ProductPricingRendering[]) => void) | null} onStatusChangeCallback - Optional - callback function to execute when the ready state changes. * @param {() => void} onRequestsProcessedCallback - Optional - callback function to execute when all the XHR requests are complete and processed. */ processRequests( onFulfilledCallback, onRejectedCallback, onStatusChangeCallback, onRequestsProcessedCallback ) { /** * Pending request queries to be fetched * @type {string[]} */ const fetchQueries = []; try { // Send the product pricing requests for each unique request key i.e., query, llcc and market this.uniqueQuerySet.forEach((requestQuery) => { // Get the render pricing component instances for the current group of request queries const renderingManagerInstances = this.renderedInstances.filter( (renderInstance) => requestQuery === renderInstance.pricingConfig.requestQuery ); /** * Get the cached product pricing response data * @type {ProductPricingResponse[]} */ let cachedResponse = []; // Check if the current request query is already cached if (this.isCachingEnabled) { // Get the cached response for the current request query // Get the cache key const requestQueryKey = this.getRequestCacheKey(requestQuery); // Get the cached response by the current request query key if (this.responseCacheMap.has(requestQueryKey)) { cachedResponse = this.responseCacheMap.get(requestQueryKey); } } if (cachedResponse.length) { // Return the cached product pricing response to each associated rendering manager instance onFulfilledCallback(cachedResponse, renderingManagerInstances); } else { // Add the current request query to the pending request queries fetchQueries.push(requestQuery); } }); // Combine the pending request queries into batches of maximum allowed requests per call const combinedQueries = this.combineRequestQueries(fetchQueries); // Fetch pending requests if any if (combinedQueries.length) { // Send the product pricing request for the current request query this.fetchRequest( combinedQueries, onFulfilledCallback, onRejectedCallback, onStatusChangeCallback, onRequestsProcessedCallback ); } else if (onRequestsProcessedCallback) { // Execute the callback function when all the requests are processed and completed onRequestsProcessedCallback(); } } catch (error) { // Handle any errors that occur during the request processing console.warn("[OCR][ProductPricingRequest] Error processing requests:", error); if (onRejectedCallback) { onRejectedCallback(error, this.renderedInstances); this.abortPendingRequests(); this.manageRequestsProgress(onRequestsProcessedCallback); } } } /** * Fetches the product pricing requests for the provided queries. * @param {string[]} combinedQueries - Array of combined request queries to be fetched using pricing service. * @param {ocReimagine.ProductPriceModule.ProductPricingRendering[]} relatedPricingInstances - The associated instances of the SKU rendering manager for the current request query. * @param {(productsResponse: ProductPricingResponse[], relatedPricingInstances: ocReimagine.ProductPriceModule.ProductPricingRendering[]) => void} onFulfilledCallback - The callback function to execute when the fetch request promise is fulfilled. * @param {(error: Error, relatedPricingInstances: ocReimagine.ProductPriceModule.ProductPricingRendering[]) => void} onRejectedCallback - The callback function to execute when the fetch request promise is rejected. * @param {((this: XMLHttpRequest, ev: Event, relatedPricingInstances: ocReimagine.ProductPriceModule.ProductPricingRendering[]) => void) | null} onStatusChangeCallback - Optional - callback function to execute when the ready state changes. * @param {() => void} onRequestsProcessedCallback - Optional - callback function to execute when all the XHR requests are complete and processed. */ fetchRequest( combinedQueries, onFulfilledCallback, onRejectedCallback, onStatusChangeCallback, onRequestsProcessedCallback ) { combinedQueries.forEach((combinedRequestQuery) => { // Get the render pricing component instances for the current group of request queries const relatedPricingInstances = this.renderedInstances.filter( (renderInstance) => combinedRequestQuery.includes( renderInstance.pricingConfig.requestQuery ) ); const requestResponse = this.sendRequest( this.pricingConstants.Request.Method, combinedRequestQuery, null, function (event) { if (onStatusChangeCallback) { onStatusChangeCallback(this, event, relatedPricingInstances); } } ); // Create the product pricing request for the current request query requestResponse .then((productsResponse) => { this.processSuccessResponse( combinedRequestQuery, productsResponse, onFulfilledCallback ); this.manageRequestsProgress(onRequestsProcessedCallback); }) .catch((errorResponse) => { onRejectedCallback(errorResponse, relatedPricingInstances); this.manageRequestsProgress(onRequestsProcessedCallback); }); }); } /** * Manages and tracks the progress status of the product pricing requests. * * @param {() => void} onRequestsProcessedCallback - Optional - callback function to execute when all the XHR requests are complete and processed. */ manageRequestsProgress(onRequestsProcessedCallback) { // Check if all product-price requests are complete and processed. const areAllRequestsComplete = this.xhrRequestControllers.every( (xhrRequestController) => xhrRequestController.readyState === XMLHttpRequest.DONE ); if (areAllRequestsComplete && onRequestsProcessedCallback) { // Execute the callback function when all the requests are processed and completed onRequestsProcessedCallback(); } // If all the requests are complete, clear the XHR requests controllers if (areAllRequestsComplete) { // Clear the XHR requests controller this.xhrRequestControllers = []; } } /** * Processes the product pricing response for the current combined request query. * @param {string} combinedQueries - Combined request queries grouped by comma separated values. * @param {ProductPricingResponse[]} productsResponse - The product pricing response for the associated SKU instances. * @param {(productsResponse: ProductPricingResponse[], relatedPricingInstances: ocReimagine.ProductPriceModule.ProductPricingRendering[]) => void} onFulfilledCallback - The callback function to execute when the fetch request promise is fulfilled. */ processSuccessResponse( combinedQueries, productsResponse, onFulfilledCallback ) { // Separate the combined queries into individual request queries const requestQueries = combinedQueries.split(","); requestQueries.forEach((requestQuery) => { // Get the product pricing response for the current request query // Filter the product pricing response data by component request query and PUID from response const responseData = productsResponse.filter( (productResponse) => requestQuery === productResponse.puid ); // Get the render pricing component instances for the current request query const relatedPricingInstances = this.renderedInstances.filter( (renderInstance) => requestQuery === renderInstance.pricingConfig.requestQuery ); // Cache the product pricing response for the current request query if caching is enabled if (this.isCachingEnabled) { // Get the cache key const requestQueryKey = this.getRequestCacheKey(requestQuery); // Cache the product pricing response for the current request query this.responseCacheMap.set(requestQueryKey, responseData); } // Return the product pricing response data to each associated rendering manager instance onFulfilledCallback(responseData, relatedPricingInstances); }); } /** * Gets the request URI for the product pricing request. * @param {string} query - The query string for the request. * @returns {string} requestUri - The request URI for the product pricing request. */ getRequestUri(query) { // Get catalog product price relative URI const relativeUri = this.pricingConstants.Request.RelativeUri; // Set query parameters const queryParameters = new URLSearchParams(); queryParameters.set("q", query); // Append llcc to the request query with market as cc if available. if (this.market) { queryParameters.set("llcc", `${this.country}-${this.market}`); } else { queryParameters.set("llcc", this.locale); } // Add default query parameters for (const parameterKey in this.pricingConstants.Request.QueryParameters) { queryParameters.set( parameterKey, this.pricingConstants.Request.QueryParameters[parameterKey] ); } return OneCloudUtil.getMsocapiurl(relativeUri, queryParameters.toString()); } /** * Aborts any pending or in-progress XHR requests. */ abortPendingRequests() { // Abort any pending or in-progress XHR requests this.xhrRequestControllers.forEach((xhr) => { xhr.abort(); }); // Clear the XHR requests controller this.xhrRequestControllers = []; } /** * Sends the product pricing request. * @param {string} method - The HTTP request method type. * @param {string} query - The query string for the request. * @param {Record | null} requestHeaders - Optional - additional request headers to add to the xhr request. * @param {((this: XMLHttpRequest, ev: Event) => void) | null} onReadyStateChange - Optional - callback function to execute when the ready state changes. * @returns {Promise | Promise} requestResult - Either the promise resolve or reject for the product pricing request. */ sendRequest(method, query, requestHeaders = null, onReadyStateChange = null) { const requestResult = new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); // Set the xhr request controller for the current request query this.xhrRequestControllers.push(xhr); const requestUri = this.getRequestUri(query); xhr.onreadystatechange = onReadyStateChange; xhr.open(method, requestUri); this.addRequestHeaders(xhr, requestHeaders); xhr.onload = function (ev) { if (this.readyState === XMLHttpRequest.DONE) { if (this.status === 200) { const parsedResponse = JSON.parse(this.response); resolve(parsedResponse); } else { const errorResponse = window.ocReimagine.ProductPriceModule.ProductPricingRequest.createRejectResponse( this, ev ); reject(errorResponse); } } }; xhr.onerror = function (ev) { const errorResponse = window.ocReimagine.ProductPriceModule.ProductPricingRequest.createRejectResponse( this, ev ); reject(errorResponse); }; xhr.send(); }); return requestResult; } /** * Adds request headers to the xhr request. * @param {XMLHttpRequest} xhr - The xhr request instance. * @param {Record | null} requestHeaders - Optional - additional request headers to add to the xhr request. */ addRequestHeaders(xhr, requestHeaders = null) { const defaultHeaders = this.pricingConstants.Request.Headers; for (const defaultHeader in defaultHeaders) { xhr.setRequestHeader(defaultHeader, defaultHeaders[defaultHeader]); } if (requestHeaders) { for (const additionalHeader in requestHeaders) { xhr.setRequestHeader( additionalHeader, requestHeaders[additionalHeader] ); } } } /** * Creates the error response for the product pricing request. * @param {XMLHttpRequest} xhr - The instance of current XMLHttpRequest request. * @param {ProgressEvent} ev - The progress event for the current XMLHttpRequest request. * @returns {Error} errorResponse - The error response for the product pricing request. */ static createRejectResponse(xhr, ev) { const errorMessage = xhr.response ? xhr.response : xhr.responseText ? xhr.responseText : xhr.statusText ? xhr.statusText : "Unknown error"; const errorResponse = new Error(errorMessage); errorResponse.name = xhr.status.toString(); return errorResponse; } }; new (function (document, $) { "use strict"; const SUCCESS_RESPONSE_CODE = "Success"; const SHARED_DATA_SELECTOR = ".oc-shared-pricing-data"; const PURCHASE_MAIN_SELECTOR = "[data-oc-product~='purchase']"; const NOT_AVAILABLE_SELECTOR = "[data-oc-product~='not-available'] p" const COMMERCIAL_TAX_DISCLAIMER_SELECTOR = "[data-oc-shared-data='oc-tax-disclaimer'] p"; const CONSUMER_TAX_DISCLAIMER_SELECTOR = "[data-oc-shared-data='oc-consumer-tax-disclmr'] p"; const DATA_OC_PRODUCT_ATTRIBUTE = "data-oc-product"; const OC_COMMERCIAL_TAX_DISCLAIMER_ATTRIBUTE = "oc-tax-disclaimer"; const OC_CONSUMER_TAX_DISCLAIMER_ATTRIBUTE = "oc-consumer-tax-disclmr"; /** * On page load. */ $(document).on("DOMContentLoaded", () => { replaceSharedData(); updateAccessibilityAttributes(); }); /** * On market selector complete. */ $(document).on("onComplete", (event) => { const enableShadowTraversal = event.detail?.traversable; updateTokenTextMainResponseCode(enableShadowTraversal); updateAccessibilityAttributes(enableShadowTraversal); replaceSharedData(enableShadowTraversal); }); /** * Replaces the shared data elements inner html with the data from the shared data element depending on key. * @param {boolean} [enableShadowTraversal=false] - Flag to enable shadow DOM traversal. * @returns {void} */ function replaceSharedData(enableShadowTraversal = false) { const PURCHASE_MAIN_ELEMENTS = querySelectorAllDeep(PURCHASE_MAIN_SELECTOR, document.body, enableShadowTraversal); if (!PURCHASE_MAIN_ELEMENTS) return; PURCHASE_MAIN_ELEMENTS.forEach((elem) => { const RESPONSE_CODE = elem.getAttribute(DATA_OC_PRODUCT_ATTRIBUTE).split(" ")[2]; const SHARED_DATA_ELEM = querySelectorDeep(SHARED_DATA_SELECTOR, document.body, enableShadowTraversal); let commercialTaxDisclaimerPlaceholder = querySelectorDeep(COMMERCIAL_TAX_DISCLAIMER_SELECTOR, elem, enableShadowTraversal); let consumerTaxDisclaimerPlaceholder = querySelectorDeep(CONSUMER_TAX_DISCLAIMER_SELECTOR, elem, enableShadowTraversal); let notAvailablePlaceholder = querySelectorDeep(NOT_AVAILABLE_SELECTOR, elem, enableShadowTraversal); if (!SHARED_DATA_ELEM) return; if (notAvailablePlaceholder && RESPONSE_CODE) { notAvailablePlaceholder.innerHTML = SHARED_DATA_ELEM.getAttribute(RESPONSE_CODE); } if (commercialTaxDisclaimerPlaceholder) { commercialTaxDisclaimerPlaceholder.innerHTML = SHARED_DATA_ELEM.getAttribute(OC_COMMERCIAL_TAX_DISCLAIMER_ATTRIBUTE); } if (consumerTaxDisclaimerPlaceholder) { consumerTaxDisclaimerPlaceholder.innerHTML = SHARED_DATA_ELEM.getAttribute(OC_CONSUMER_TAX_DISCLAIMER_ATTRIBUTE); } }); } /** * Updates the accessibility attributes for the product pricing hidden and visible elements. * @param {boolean} [enableShadowTraversal=false] - Flag to enable shadow DOM traversal. * @returns {void} */ function updateAccessibilityAttributes(enableShadowTraversal = false) { const PURCHASE_MAIN_ELEMENTS = querySelectorAllDeep(PURCHASE_MAIN_SELECTOR, document.body, enableShadowTraversal); if (!PURCHASE_MAIN_ELEMENTS) return; PURCHASE_MAIN_ELEMENTS.forEach((elem) => { const RESPONSE_CODE = elem.getAttribute(DATA_OC_PRODUCT_ATTRIBUTE).split(" ")[2]; let commercialTaxDisclaimerPlaceholder = querySelectorDeep(COMMERCIAL_TAX_DISCLAIMER_SELECTOR, elem, enableShadowTraversal); let consumerTaxDisclaimerPlaceholder = querySelectorDeep(CONSUMER_TAX_DISCLAIMER_SELECTOR, elem, enableShadowTraversal); let notAvailablePlaceholder = querySelectorDeep(NOT_AVAILABLE_SELECTOR, elem, enableShadowTraversal); if (RESPONSE_CODE === SUCCESS_RESPONSE_CODE) { if (commercialTaxDisclaimerPlaceholder) { commercialTaxDisclaimerPlaceholder.removeAttribute("aria-hidden"); } if (consumerTaxDisclaimerPlaceholder) { consumerTaxDisclaimerPlaceholder.removeAttribute("aria-hidden"); } if (notAvailablePlaceholder) { notAvailablePlaceholder.setAttribute("aria-hidden", "true"); } } else { if (commercialTaxDisclaimerPlaceholder) { commercialTaxDisclaimerPlaceholder.setAttribute("aria-hidden", "true"); } if (consumerTaxDisclaimerPlaceholder) { consumerTaxDisclaimerPlaceholder.setAttribute("aria-hidden", "true"); } if (notAvailablePlaceholder) { notAvailablePlaceholder.removeAttribute("aria-hidden"); } } }); } /** * Iterates through all token text elements and updates the main response code to the first non-success response code. * @param {boolean} [enableShadowTraversal=false] - Flag to enable shadow DOM traversal. * @returns {void} */ function updateTokenTextMainResponseCode(enableShadowTraversal = false) { const TOKEN_TEXT_ELEMENTS = querySelectorAllDeep("[data-token-text]", document.body, enableShadowTraversal); TOKEN_TEXT_ELEMENTS.forEach((tokenTextElem) => { const purchaseMainElement = querySelectorDeep("[data-oc-product*=purchase][data-oc-product*=main]", tokenTextElem, enableShadowTraversal); if (!purchaseMainElement) return; const PRICING_TOKEN_ELEMENTS = querySelectorAllDeep("[data-oc-product*=purchase]:not([data-oc-product*=main])[data-token=m365ProductPrice]", tokenTextElem, enableShadowTraversal); if (!PRICING_TOKEN_ELEMENTS || !PRICING_TOKEN_ELEMENTS.length) return; let responseCode = SUCCESS_RESPONSE_CODE; for (const pricingTokenElem of PRICING_TOKEN_ELEMENTS) { let curResponseCode = pricingTokenElem.getAttribute(DATA_OC_PRODUCT_ATTRIBUTE).split(" ")[1]; if (curResponseCode !== SUCCESS_RESPONSE_CODE) { responseCode = curResponseCode; break; } } let currentMainProductAttribute = purchaseMainElement.getAttribute(DATA_OC_PRODUCT_ATTRIBUTE); let currentMainResponseCode = currentMainProductAttribute.split(" ")[2]; purchaseMainElement.setAttribute(DATA_OC_PRODUCT_ATTRIBUTE, currentMainProductAttribute.replace(currentMainResponseCode, responseCode)); }); } })(document, $); //#region Reimagine Product Pricing Rendering class. /** * Manages rendering of product pricing component. * @note This class is not related with above script and is managed by product-pricing-manager script. */ window.ocReimagine.ProductPriceModule.ProductPricingRendering = class ProductPricingRendering { /** * Gets the unique parent product pricing element instance identifier. * @type {string | null | undefined} */ parentIdentifier; /** * Gets the unique product pricing element instance identifier. * @type {string} */ instanceIdentifier; /** * Gets the unique child product pricing element instance identifiers. * @type {string[] | null | undefined} */ childIdentifiers; /** * Gets the product pricing component name. * @type {string} * @note This is used to identify the product pricing component template. */ pricingComponentName; /** * Gets the constants for the product pricing module. */ pricingConstants = window.ocReimagine.ProductPriceModule.ProductPricingConstants; /** * Gets the pricing component templates. */ pricingTemplates = window.ocReimagine.ProductPriceModule.ProductPricingTemplates; /** * Gets or sets the target pricing component element. * @type {HTMLDivElement | HTMLSpanElement} */ pricingComponentElement; /** * Gets or sets the render section element. * @type {HTMLDivElement | HTMLSpanElement} */ renderSectionElement; /** * Gets or sets the pricing template fragment. * @type {HTMLTemplateElement | HTMLSpanElement} */ pricingTemplateElement; /** * Gets or sets the pricing config attributes for the current product sku request. * @type {PricingClientConfig} */ pricingConfig; /** * Gets or sets the product pricing response for the current product sku request. * @type {ProductPricingResponse[]} */ #productPriceResponse; /** * Gets the product pricing response data for the current product pricing config. * @returns {ProductPricingResponse[]} product pricing response data */ get productPriceResponse() { return this.#productPriceResponse; } /** * Sets the product pricing response data for the current product pricing request. * @param {ProductPricingResponse[]} value product pricing response data */ set productPriceResponse(value) { this.#productPriceResponse = value; } /** * Initializes new instance of the product pricing rendering handler class. * @param {HTMLDivElement | HTMLSpanElement} pricingComponentElement target pricing component element * @param {boolean} [enableShadowTraversal=false] - Flag to enable shadow DOM traversal. */ constructor(pricingComponentElement, enableShadowTraversal = false) { if (!pricingComponentElement) { throw new Error("[OCR][ProductPricingRenderer] Invalid pricing component element. Element is null or undefined."); } // Get pricing component element. this.pricingComponentElement = pricingComponentElement; // Find the first child element of the pricing component element from direct children of the current pricing element. const firstChildElement = window.ocReimagine.ProductPriceModule.ProductPricingManager.findPricingRequestElement(this.pricingComponentElement); // Set the unique identifier for the pricing component element render class instance this.instanceIdentifier = firstChildElement.dataset.ocrPricingIdentifier ?? OneCloudUtil.generateUniqueIdentifier(); // Get render section element. this.renderSectionElement = this.pricingComponentElement.querySelector( `${this.pricingConstants.Selectors.Dataset.RenderSection}[data-ocr-pricing-identifier="${this.instanceIdentifier}"]` ); // Get pricing component server-side config-template. this.pricingTemplateElement = this.pricingComponentElement.querySelector( `${this.pricingConstants.Selectors.Dataset.PricingConfig}[data-ocr-pricing-identifier="${this.instanceIdentifier}"]` ); const configData = this.pricingTemplateElement?.dataset.ocrPricingConfig; if (configData) { this.pricingConfig = JSON.parse(configData); } // Set the unique identifier for the pricing component element this.pricingComponentElement.dataset.instanceIdentifier = this.instanceIdentifier; // Get the product pricing component name from the element dataset. this.pricingComponentName = this.pricingComponentElement.dataset.ocrPricingComponent; // Set the pricing component element status. this.pricingComponentElement.dataset.ocrPricingStatus = "1"; // Assign child identifiers and parent identifier for the current product pricing component instance. this.assignChildIdentifiers(enableShadowTraversal); this.assignParentIdentifier(enableShadowTraversal); } /** * Assigns the parent identifier for the current product pricing component instance. * @param {boolean?} enableShadowTraversal - Flag to enable shadow DOM traversal. * @note This is used to track the parent identifier for the pricing token component. * @returns {void} */ assignParentIdentifier(enableShadowTraversal = false) { // Check if the pricing component is a pricing token // As of now, pricing tokens are the only components that can have a parent identifier. // Since the pricing token can be nested within another pricing component element, we need track the parent element. if (this.pricingComponentName === this.pricingConstants.Templates.PricingToken) { this.parentIdentifier = this.pricingComponentElement.dataset.ocrPricingParentIdentifier; } } /** * Assigns the child identifiers for the current product pricing component instance. * @param {boolean?} enableShadowTraversal - Flag to enable shadow DOM traversal. * @note This is used to track the child identifiers for the pricing token component. * @returns {void} */ assignChildIdentifiers(enableShadowTraversal = false) { // As of now, pricing tokens are the only components that can be nested within other pricing components. // Since the pricing token can be nested within another pricing component element, we need track the child elements. // Check if the pricing component is not a pricing token and has child pricing token elements if (this.pricingComponentName !== this.pricingConstants.Templates.PricingToken) { // Check if the current pricing component has any child pricing token elements present. /** * @type {NodeListOf} */ const childPricingTokenElements = querySelectorAllDeep( this.pricingConstants.Selectors.Dataset.PricingToken, this.pricingComponentElement, enableShadowTraversal ); // If child pricing token elements are present, assign the parent identifier to child pricing token elements. // Also assign the child identifiers to the current product pricing component instance. if (childPricingTokenElements && childPricingTokenElements.length) { // Initialize the child identifiers to an empty array. this.childIdentifiers = []; // Get the child identifiers from the child pricing token elements. childPricingTokenElements.forEach((childPricingTokenElement) => { // Set the parent identifier for the child pricing token element. childPricingTokenElement.dataset.ocrPricingParentIdentifier = this.instanceIdentifier; // Get the child pricing token sku request element const childSkuRequestElement = window.ocReimagine.ProductPriceModule.ProductPricingManager.findPricingRequestElement(childPricingTokenElement); // Get the child identifier from the child pricing token element. const childIdentifier = childSkuRequestElement.dataset.ocrPricingIdentifier; if (!childIdentifier) { return; } this.childIdentifiers.push(childIdentifier); }); } } } /** * Handles the product pricing manager instance to update the nested pricing components. * * @summary This is used to update the nested pricing components within the current product pricing component instance. * Can be called recursively for nested pricing components. * * @returns {void} */ handleNestedPricingElements() { try { // Check if the child identifiers are present and has any child pricing token elements. // If child pricing token elements are present, then update the nested pricing tokens within the current pricing component. if (!this.childIdentifiers || !this.childIdentifiers.length) { return; } // Check if the pricing manager instance is initialized if (!window.ocReimagine.ProductPriceModule.PricingManagerInstance) { return; } // Get the product pricing manager instance. const productPricingManager = window.ocReimagine.ProductPriceModule.PricingManagerInstance; // Update the nested - child pricing component elements. // The {updateNestedPricingComponents} method can be called recursively for nested pricing components. // The {updateNestedPricingComponents} method calls the {updateInstanceElements} method for each child pricing component element. productPricingManager.updateNestedPricingComponents(this.childIdentifiers); } catch (error) { console.warn("[OCR][ProductPricingRenderer] Unable to update nested pricing components: ", error); } } /** * Updates the product pricing component instance elements with the updated pricing component element. * @summary This updates the current instance with newly rendered same pricing element on DOM. * Since the old pricing component element is removed from DOM due to re-rendering of the parent element template, * we need to update the child instance with the new pricing component element. Only applicable for nested/child pricing components. * For example, if the parent pricing component is re-rendered, then the child pricing component element is removed from DOM. * In this case, we need to update the instance with the new pricing component element. * @note This method is called by the {ProductPricingManager.updateNestedPricingComponents}. Can be called recursively for nested pricing components. * * @param {HTMLDivElement | HTMLSpanElement} updatedPricingComponentElement * @returns */ updateInstanceElements(updatedPricingComponentElement) { // Check if the pricing component render instance is initialized if (!updatedPricingComponentElement) { return; } // Check if the pricing component element is rendered on the page if (isElementInDocument(this.pricingComponentElement)) { // No need to update the instance if the element is already rendered on the page. return; } // Get the first child element with the pricing request of the updated pricing component element. const firstChildElement = window.ocReimagine.ProductPriceModule.ProductPricingManager.findPricingRequestElement(updatedPricingComponentElement); // Check if the first child element matches with the current instance identifier and contains correct data-ocr-pricing-identifier and data-ocr-sku-request // Safety check to avoid updating the instance with incorrect element. if (firstChildElement.dataset.ocrSkuRequest !== this.pricingConfig.requestQuery || this.instanceIdentifier !== firstChildElement.dataset.ocrPricingIdentifier) { throw new Error("[OCR][ProductPricingRenderer] Unable to update pricing component element. Element does not match with current instance."); } // Replace the current instance pricing component element with the updated pricing component element from DOM. this.pricingComponentElement = updatedPricingComponentElement; // Get the updated render section element. this.renderSectionElement = this.pricingComponentElement.querySelector( `${this.pricingConstants.Selectors.Dataset.RenderSection}[data-ocr-pricing-identifier="${this.instanceIdentifier}"]` ); // Get the updated pricing component server-side config-template element. this.pricingTemplateElement = this.pricingComponentElement.querySelector( `${this.pricingConstants.Selectors.Dataset.PricingConfig}[data-ocr-pricing-identifier="${this.instanceIdentifier}"]` ); // Set the unique identifier for the pricing component element this.pricingComponentElement.dataset.instanceIdentifier = this.instanceIdentifier; // Set the pricing component element status. this.pricingComponentElement.dataset.ocrPricingStatus = "1"; // Render the updated pricing component element on the page. // This can be called recursively for nested pricing components if current instance has child elements. this.handleProductPricingResponse(); } /** * Handles the product pricing request response for the current reiamgine SKU request. * @returns {void} */ handleProductPricingResponse() { if (!this.productPriceResponse || !this.productPriceResponse.length) { this.displayUnavailable(); return; } try { const pricingResponse = this.productPriceResponse[0]; // Update the pricing config. this.updatePricingConfig(pricingResponse); this.displayPricingTemplate(pricingResponse); } catch (error) { console.warn("[OCR][ProductPricingRenderer] rendering failure: ", error); if (pricingResponse.responseCode === this.pricingConstants.Enumerables.Response.DisabledMarket) { this.displayDisabledMarket(); } else { this.displayUnavailable(); } } // Update the product pricing manager instance to update the nested pricing components. // This is used to update the nested pricing components within the current product pricing component instance. // This will be called only if the current product pricing component instance has child identifiers. // Can be called recursively for nested pricing components. this.handleNestedPricingElements(); } /** * Updates the config for the current product pricing rendering instance. * @param {ProductPricingResponse} pricingResponse pricing response object. */ updatePricingConfig(pricingResponse) { // Update edit mode flag this.pricingConfig.isEdit = this.pricingTemplateElement.hasAttribute("data-render-mode"); this.updateRenderTitle(pricingResponse); if (!pricingResponse || !pricingResponse.sku) { return; } // Get product price has discount or not. this.pricingConfig.isDiscounted = pricingResponse.sku.discountPrice > 0; } /** * Updates the config for the current product title rendering. * @param {ProductPricingResponse} pricingResponse pricing response object. */ updateRenderTitle(pricingResponse) { if (this.pricingConfig.titleOverride) { // Assign the title override to the render title. this.pricingConfig.renderTitle = this.pricingConfig.titleOverride; } else if (this.pricingConfig.isUsingProductTitle && pricingResponse) { // Assign the product title to the render title. this.pricingConfig.renderTitle = pricingResponse.title; } else if (pricingResponse && pricingResponse.sku) { // Assign the sku title to the render title. this.pricingConfig.renderTitle = pricingResponse.sku.title; } else if (this.pricingConfig.fallbackTitle) { // Assign the fallback title to the render title. this.pricingConfig.renderTitle = this.pricingConfig.fallbackTitle; } else { // Assign the empty string to the render title. // This is to avoid the title from being displayed as "undefined" in the template. this.pricingConfig.renderTitle = ""; } } /** * Assigns the product price response code to the product main - render element. * @param {ProductResponseCode | string} responseCode */ assignProductStatus(responseCode) { // For supporting legacy pricing token elements only, // Check if the render section element has the data-oc-product attribute. if (this.renderSectionElement.hasAttribute(this.pricingConstants.Attributes.Data.LegacyProductMain)) { this.renderSectionElement.setAttribute(this.pricingConstants.Attributes.Data.LegacyProductMain, `purchase ${responseCode}`); } // For reimagine pricing component elements, // Check if the render section element has the data-oc-product attribute. if (this.renderSectionElement.hasAttribute(this.pricingConstants.Attributes.Data.ProductMain)) { this.renderSectionElement.setAttribute(this.pricingConstants.Attributes.Data.ProductMain, `product main ${responseCode}`); } } /** * Renders the template fragment for the current pricing component instance. * @param {DocumentFragment | string} templateFragment template fragment to be rendered. */ renderTemplateFragment(templateFragment) { if (this.pricingConfig.isEdit && templateFragment instanceof DocumentFragment) { // Keep the authored content in the rendered content section and only replace configured sections from template fragment. this.replaceRenderedContent(templateFragment); } else { // Replace entire render section element with content from template fragment. this.renderSectionElement.replaceChildren(templateFragment); } } /** * Saves the authored content for the current product pricing rendering instance. * @param {DocumentFragment} templateFragment configured template fragment to be used for rendered. */ replaceRenderedContent(templateFragment) { // Replace the placeholder sections within rendered content element by replacing with content from configured template. for (const renderedElement of this.renderSectionElement.children) { const attributeValue = renderedElement.dataset.ocrPricingRender; const templateElement = templateFragment.querySelector( `[data-ocr-pricing-render="${attributeValue}"][data-ocr-pricing-identifier="${this.pricingConfig.pricingIdentifier}"]` ); if (templateElement) { renderedElement.replaceWith(templateElement); } } } /** * Prepares and displays pricing component template in the current product sku element on the page. * @param {ProductPricingResponse} pricingResponse pricing response object. */ displayPricingTemplate(pricingResponse) { const templateFragment = this.pricingTemplates.getPricingTemplate( this.pricingTemplateElement, this.pricingComponentName, this.pricingConfig, pricingResponse ); // Set the product price response code to the product main - render element. this.assignProductStatus(pricingResponse.responseCode); // Render the template fragment in the render section element. this.renderTemplateFragment(templateFragment); } /** * Hides the pricing section and displays the unavailable section. * @param {HTMLDivElement} skuElement target pricing component element */ displayUnavailable() { try { const templateFragment = this.pricingTemplates.getUnavailableTemplate( this.pricingComponentName, this.pricingConfig, this.pricingTemplateElement ); // Set the product price response code to the product main - render element. this.assignProductStatus(this.pricingConstants.Enumerables.Response.NotFound); // Render the template fragment in the render section element. this.renderTemplateFragment(templateFragment); } catch (error) { console.warn("[OCR][ProductPricingRenderer] rendering failure: ", error); } } /** * Hides the pricing section and displays the disabled market section. * @param {HTMLDivElement} skuElement target pricing component element */ displayDisabledMarket() { try { const templateFragment = this.pricingTemplates.getDisabledMarketTemplate( this.pricingComponentName, this.pricingConfig, this.pricingTemplateElement ); // Set the product price response code to the product main - render element. this.assignProductStatus(this.pricingConstants.Enumerables.Response.DisabledMarket); // Render the template fragment in the render section element. this.renderTemplateFragment(templateFragment); } catch (error) { console.warn("[OCR][ProductPricingRenderer] rendering failure: ", error); } } }; //#endregion Reimagine Product Pricing Rendered Instance Manager /** * @class ProductPricingManager - Manages the product pricing data and updates the UI */ window.ocReimagine.ProductPriceModule.ProductPricingManager = class ProductPricingManager { /** * Gets the constants for the product pricing module. */ pricingConstants = window.ocReimagine.ProductPriceModule.ProductPricingConstants; /** * Gets the pricing manager options / settings. * @type {PricingManagerOptions} */ pricingManagerOptions = {}; /** * Gets or sets the pricing component elements on the current page. * @type {Array} */ pricingComponentElements; /** * Gets or sets the market selector element on the current page. * @type {HTMLDivElement} */ marketSelector; /** * Gets or sets the current locale for the page. * @type {string} */ locale; /** * Gets or sets the country for the page. * @type {string} */ country; /** * Gets or sets the market for the page. * @type {string | null} */ market = null; /** * Gets or sets the market selector config options from data-layer attributes. * @type {MarketSelectorConfig} */ marketSelectorOptions; /** * Gets or sets the instance of product pricing request manager class. * @type {ocReimagine.ProductPriceModule.ProductPricingRequest} */ productPricingRequestManager; /** * Gets or sets the instances of product pricing rendering class for each pricing component on page. * @type {Array} * @summary This is only for reference and not used in the product pricing request manager. */ pricingComponentRenderInstances; /** * Gets or sets the flag to indicate if the pricing request mode is ajax. * @type {boolean} */ isPricingRequestModeAjax; /** * Gets or sets the flag indicating whether the product pricing manager is initialized. * @type {boolean} */ isPricingManagerInitialized = false; /** * Creates instance of the product pricing manager and associated services * @param {PricingManagerOptions?} options - The options/settings for the pricing manager. */ constructor(options) { // Initial market value can be set after the on init event is dispatched by market-selector script // Set the market value from the market selector or from the URL this.market = options?.market; // this.market property gets updated during on refreshed event at onMarketSelectorChange // Set the pricing manager options if(options) { this.pricingManagerOptions = options; } // Initialize product pricing manager this.initializePricingManager(options); } /** * Initializes the pricing manager and request manager * @param {PricingManagerOptions?} options - The options/settings for the pricing manager. * @summary Creates list of product price rendering instances of all elements with data-ocr-pricing-component * Request collection is managed by pricing request script, contains all the unique queries from data-sku-request/data-ocr-sku-request */ initializePricingManager(options) { // Get current locale, country and selected market from the URL const documentLang = document.documentElement.lang || this.pricingConstants.Defaults.Locale; this.locale = documentLang; this.country = documentLang.split("-")[0]; // Initial market value must be set after the on refreshed event is dispatched by market-selector script // this.market property is set during onMarketSelectorChange // Get pricing components. this.pricingComponentElements = this.getPricingComponents(options?.traversable); // Initialize instance of product pricing request manager class this.productPricingRequestManager = new window.ocReimagine.ProductPriceModule.ProductPricingRequest( documentLang, this.country, this.market ); this.marketSelector = this.getMarketSelector(); // Set the market selector config options from data-layer attributes this.setMarketSelectorConfig(); // Creates and enqueues requests for all the reimagine pricing component instances on the current page this.createProductPricingRequests(); if (this.isPricingManagerInitialized) { // if market selector is available on the page, set the market selector config and bind events if (this.marketSelector) { // Bind the market selector events this.bindEvents(); // When this.marketSelectorOptions.isRefreshModeAjax is true, // Assign the market-selector page refresh mode value to the request mode ajax value // When this.marketSelectorOptions.isRefreshModeAjax is false, // Retain the server-side feature switch value of this.isPricingRequestModeAjax - for initial page load rendering this.isPricingRequestModeAjax = !this.marketSelectorOptions.isRefreshModeAjax ? this.isPricingRequestModeAjax : this.marketSelectorOptions.isRefreshModeAjax; } // if request-mode ajax feature-switch is enabled then send client-side product pricing requests on page load // otherwise on initial page load, the product pricing requests are made from the server-side by OSGi/ESI service if (this.isPricingRequestModeAjax) { // Send all pricing requests this.sendPricingRequests(); } } } /** * Sends the product pricing requests for all the reimagine pricing component instances on the current page. * @summary This method is called on initialization and/or when the market selector is changed or when the product pricing elements changes on DOM. */ sendPricingRequests() { // Start processing the requests this.productPricingRequestManager.processRequests( (productsResponse, relatedRenderedInstances) => this.handleRequestSuccess(productsResponse, relatedRenderedInstances), (error, relatedRenderedInstances) => this.handleRequestFailure(error, relatedRenderedInstances), this.handleRequestStatusChange, () => this.handleRequestsComplete() ); } /** * Updates the pricing manager when the product pricing elements changes on DOM * @param {PricingManagerOptions?} options - The options/settings for the pricing manager. * @summary This custom event is dispatched by external pricing-components whenever the product pricing elements changes on DOM. This method checks if the pricing component elements have changed and updates the product pricing request instances accordingly * @returns {void} * @memberof ProductPricingManager */ updatePricingManager(options) { // Update the pricing manager options if (options) { // Merge the new options with the existing options this.pricingManagerOptions = { ...this.pricingManagerOptions, ...options, }; } // Check if the pricing manager is initialized if (!this.isPricingManagerInitialized) { // Initialize product pricing manager this.initializePricingManager(options); return; } // Remove the pricing component element from the request manager if it is not present on the page for (const currentPricingComponent of this.pricingComponentElements) { // Get the identifier of the current pricing component // The identifier is set in the data-ocr-pricing-identifier attribute of the pricing component element const currentPricingComponentIdentifier = currentPricingComponent.dataset.instanceIdentifier; // Check if the current pricing component element is present on the page if (!isElementInDocument(currentPricingComponent, document.body, options?.traversable)) { // Remove the pricing component element from the request manager this.productPricingRequestManager.dequeueRequest( currentPricingComponentIdentifier, options?.traversable ); } } // Get the pricing component elements on the current page const updatedPricingComponentElements = this.getPricingComponents(options?.traversable); let hasPricingComponentsChanged = false; // Check if the pricing component elements have changed for (const updatedPricingComponent of updatedPricingComponentElements) { // Get the identifier of the updated pricing component const updatedPricingComponentIdentifier = updatedPricingComponent.dataset.instanceIdentifier; // Get the status of the updated pricing component // The status is set to 0 when the pricing component is not rendered const updatedPricingComponentStatus = updatedPricingComponent.dataset.ocrPricingStatus; // Check if the updated pricing component status is not set or is 0 // If the status is not set or is 0, it means the pricing component is not rendered if (!updatedPricingComponentIdentifier || !updatedPricingComponentStatus || updatedPricingComponentStatus === "0") { hasPricingComponentsChanged = true; continue; } let isPricingComponentPresent = false; // Get the identifier of the current pricing component let currentPricingComponentIdentifier = ""; for (const currentPricingComponent of this.pricingComponentElements) { // Get the identifier of the current pricing component // The identifier is set in the data-ocr-pricing-identifier attribute of the pricing component element currentPricingComponentIdentifier = currentPricingComponent.dataset.instanceIdentifier; // Check if the current pricing component element exists in the updated list isPricingComponentPresent = currentPricingComponentIdentifier === updatedPricingComponentIdentifier; // If the pricing component element exists in the updated list, break the loop if (isPricingComponentPresent) { break; } } // If the pricing component element does not exist in the updated list, remove it from the request manager if (!isPricingComponentPresent) { // Remove the pricing component element from the request manager this.productPricingRequestManager.dequeueRequest( currentPricingComponentIdentifier, options?.traversable ); } } // Check if the updated pricing component elements have changed if (!hasPricingComponentsChanged) { return; } // Update the pricing component elements this.pricingComponentElements = updatedPricingComponentElements; // Update the product pricing request instances on page edit when pricing elements changes this.updateProductPricingRequests(); // Send the pricing requests this.sendPricingRequests(); } /** * Gets the pricing component elements on the current page. * @param {boolean?} enableShadowTraversal - If true, traverses shadow DOM to find the pricing component elements. * @summary Finds the pricing component elements by the [data-ocr-pricing-component] attribute selector. * @returns {Array} The pricing component elements on the current page. */ getPricingComponents(enableShadowTraversal) { return querySelectorAllDeep(this.pricingConstants.Selectors.Dataset.Component, document.body, enableShadowTraversal); } /** * Finds the first data-ocr-sku-request element for the provided pricing component element by searching direct children of the provided pricing element. * Descendants are ignored to avoid capturing nested pricing components. * @param {HTMLDivElement | HTMLSpanElement} pricingComponentElement target pricing component element * @returns {HTMLDivElement | HTMLSpanElement} the first pricing/sku request element of the pricing component element. * @throws {Error} if the pricing component element is invalid or does not have the required attributes. */ static findPricingRequestElement(pricingComponentElement) { // Get the first child element of the pricing component element from direct children of the current pricing element. const firstChildElement = pricingComponentElement.querySelector(`:scope > ${window.ocReimagine.ProductPriceModule.ProductPricingConstants.Selectors.Dataset.SkuRequestQuery}`); // Check if the first child element is present and has the data-ocr-pricing-identifier and data-ocr-sku-request if (!firstChildElement || !firstChildElement.dataset.ocrPricingIdentifier || !firstChildElement.dataset.ocrSkuRequest) { throw new Error("[OCR][ProductPricingManager] Invalid pricing component element. Missing data-ocr-pricing-identifier or data-ocr-sku-request attribute."); } return firstChildElement; } /** * Updates the nested pricing components on the current page. * @summary This method updates the nested pricing components on the current page based on the provided nested component identifiers. * It fires the pricing component rendered instance update event based on the instance identifiers. * Used as a callback function by product price rendered instance. Can be called recursively to update nested pricing components. * @param {string[] | null | undefined} componentIdentifiers - The identifiers of the pricing components to update. * @returns {void} */ updateNestedPricingComponents(componentIdentifiers) { if (!componentIdentifiers || componentIdentifiers.length === 0) { return; } // Update the pricing component elements on the current page this.pricingComponentElements = this.getPricingComponents(); // Fire the pricing component rendered instance update event based on the instance identifiers this.pricingComponentElements.forEach((pricingComponentElement) => { try { const pricingInstanceIdentifier = pricingComponentElement.dataset.instanceIdentifier; const pricingComponentStatus = pricingComponentElement.dataset.ocrPricingStatus; if (pricingComponentStatus === "1" || pricingInstanceIdentifier) { return; } const pricingComponentRequestElement = ProductPricingManager.findPricingRequestElement(pricingComponentElement); const pricingComponentIdentifier = pricingComponentRequestElement.getAttribute(this.pricingConstants.Attributes.Data.PricingIdentifier); if (!pricingComponentIdentifier || !componentIdentifiers.includes(pricingComponentIdentifier)) { return; } // Get the pricing component instance from the list of pricing component render instances const pricingComponentInstance = this.pricingComponentRenderInstances.find((existingInstance) => existingInstance.instanceIdentifier === pricingComponentIdentifier); if (!pricingComponentInstance) { return; } // Fire the update the pricing component instance event // updates the pricing component instance with the new pricing component element // The pricing component instance can callback the {updateNestedPricingComponents} method recursively to update nested pricing components pricingComponentInstance.updateInstanceElements(pricingComponentElement); } catch (e) { console.warn("[OCR][ProductPricingManager] update nested pricing components failure: ", e); } }); } /** * Gets the market selector element on the current page. * @returns {HTMLDivElement} marketSelector - The market selector element on the current page. */ getMarketSelector() { return document.querySelector( this.pricingConstants.Selectors.Dataset.MarketSelector ); } /** * Sets the market selector configuration options from data-layer attributes. */ setMarketSelectorConfig() { if (!this.marketSelector) { // If the market selector is not present on the page, set the default market selector options this.marketSelectorOptions = { isRefreshModeAjax: false }; return; } this.marketSelectorOptions = { refreshMode: this.marketSelector?.dataset?.refreshMode, isRefreshModeAjax: this.marketSelector?.dataset?.refreshMode === this.pricingConstants.Parameters.MarketSelector.RefreshMode.AJAX, }; } /** * Binds the market selector events for the product pricing manager. */ bindEvents() { if ( this.marketSelectorOptions.isRefreshModeAjax && oc.event.marketSelector ) { // This runs on first/initial page load event on document ready state and sub-sequent market selection changes document.body.addEventListener( this.pricingConstants.Events.MarketSelector.OnRefreshed, (ev) => { this.onMarketSelectorChange(ev); } ); } } /** * Handles the market selector change event. * @param {CustomEvent} event the on select event of market selector dropdown. */ onMarketSelectorChange(event) { if (event.detail.value && event.detail.value !== this.market) { this.market = event.detail.value; this.productPricingRequestManager.market = event.detail.value; // Update the product pricing request manager this.productPricingRequestManager.updateRequestManager( event.detail.value ); // Cancel all existing pending or in-progress requests this.productPricingRequestManager.abortPendingRequests(); // Send the pricing requests this.sendPricingRequests(); } } /** * Creates and enqueues product pricing requests for all the reimagine pricing components on the current page. */ createProductPricingRequests() { if (this.pricingComponentElements.length) { try { // Create the product pricing requests for each pricing component element for (const pricingComponentElement of this.pricingComponentElements) { try { // Create an instance of product price rendering class for the pricing component element this.createProductPriceRenderInstance(pricingComponentElement); } catch (e) { console.warn("[OCR][ProductPricingManager] creation error: ", e); } } // Get the instances of product pricing rendering class for each pricing component on page this.isPricingManagerInitialized = this.productPricingRequestManager.renderedInstances.length > 0; // Assign the product pricing request manager instances for each pricing component on page // Only for reference this.pricingComponentRenderInstances = this.productPricingRequestManager.renderedInstances; } catch (e) { this.isPricingManagerInitialized = false; console.warn("[OCR][ProductPricingManager] initialization failure: ", e); } } } /** * Updates the product pricing request instances on page edit. * @returns */ updateProductPricingRequests() { // update the product pricing request instances on pricing element changes. if (!this.pricingComponentElements.length) { return; } for (const pricingComponentElement of this.pricingComponentElements) { try { // Check if the pricing component element is already initialized // Check for instance identifier to avoid re-initializing the same instance of product pricing rendering class const instanceExists = this.productPricingRequestManager.renderedInstances.some( (existingInstance) => pricingComponentElement.dataset.instanceIdentifier && existingInstance.instanceIdentifier === pricingComponentElement.dataset.instanceIdentifier ); if (instanceExists) { // If the instance already exists, continue to the next pricing component element iteration continue; } // Create a new instance of product price rendering class for the pricing component element this.createProductPriceRenderInstance(pricingComponentElement); } catch (e) { console.warn("[OCR][ProductPricingManager] update error: ", e); } } } /** * Creates an instance of product price rendering class for the pricing component element. * @summary This method creates an instance of product price rendering class for the pricing component element. * It also checks if the request mode is ajax and if the market selector refresh mode is ajax. * If both are true, it sends the product pricing request. * @param {HTMLDivElement} pricingComponentElement * @returns {void} * @memberof ocReimagine.ProductPriceModule.ProductPricingManager */ createProductPriceRenderInstance(pricingComponentElement) { // Initialize instance of product price rendering class for the pricing component. // This rendering class is what updates the UI for the pricing component depending on the product pricing response. const productPricingRender = new window.ocReimagine.ProductPriceModule.ProductPricingRendering( pricingComponentElement, this.pricingManagerOptions.traversable ); const requestQuery = productPricingRender.pricingConfig.requestQuery; const isProductPriceOverridden = productPricingRender.pricingConfig.isProductPriceOverridden; // Get pricing request mode from the pricing component element this.isPricingRequestModeAjax = productPricingRender.pricingConfig.isRequestModeAjax; // if request mode is not ajax and market selector refresh mode is not ajax, then break the loop if ( !this.isPricingRequestModeAjax && !this.marketSelectorOptions.isRefreshModeAjax ) { return; } // if sku request query is available and product price is not overridden, only then send the product pricing request // otherwise we ignore the current instance of reimagine pricing component if (requestQuery && !isProductPriceOverridden) { this.productPricingRequestManager.enqueueRequest( productPricingRender ); } } /** * Handles the product pricing request status change for provided pricing component instances. * @param {XMLHttpRequest} xhr - The XMLHttpRequest object for the product pricing request. * @param {Event} event - The event object for the product pricing request. * @param {ocReimagine.ProductPriceModule.ProductPricingRendering[]} renderManagerInstances - Grouped instances of product pricing rendering class associated with same requests. */ handleRequestStatusChange(xhr, event, renderManagerInstances) { // TODO: add in-progress status handling - loading animation or spinner } /** * Manages the product pricing request success for provided pricing component instances. * @param {ProductPricingResponse[]} productsResponse - The product pricing response for the associated SKU instances. * @param {ocReimagine.ProductPriceModule.ProductPricingRendering[]} renderManagerInstances - Grouped instances of product pricing rendering class associated with same requests. */ handleRequestSuccess(productsResponse, renderManagerInstances) { // Assign and hanlde the product pricing response to each pricing component rendering class instance renderManagerInstances.forEach((renderingClassInstance) => { renderingClassInstance.productPriceResponse = productsResponse; renderingClassInstance.handleProductPricingResponse(); // update the sku telemetry attributes for the action components in each page const renderElement = renderingClassInstance.renderSectionElement; const priceResponse = renderingClassInstance.productPriceResponse[0]; this.updateSkuTelemetryAttributes(renderElement, priceResponse); }); } /** * Handles the product pricing request failure for provided pricing component instances. * @param {Error} error - The error object for the failed product pricing request. * @param {Array} renderManagerInstances - Grouped instances of product pricing rendering class associated with same requests. */ handleRequestFailure(error, renderManagerInstances) { // Display the product pricing unavailable for each SKU rendering class instance renderManagerInstances.forEach((renderingClassInstance) => renderingClassInstance.displayUnavailable() ); } /** * Handles the callback when all the product pricing requests are complete and processed. */ handleRequestsComplete() { if (!window.ocrReimagine) { return; } // Re-initialize popover component and/or pricing-tokens inside rich-text after rendering the template content. this.reinitializeComponents(); // Regenerates FWLinks after the product pricing is rendered from a 'static' template. this.regenerateFWLinkParams(); // Dispatch the render complete events for the product pricing module. this.dispatchRenderCompleteEvents(); } /** * Re-initializes the popover component and/or pricing-tokens inside rich-text for the all the product pricing instances. * * @summary Since the product pricing component content is rendered dynamically from authored template and configured template, * the popover component and/or pricing-tokens inside rich-text will not be initialized as they are within the template section * and not actually rendered on the page. This method re-initializes the popover component and/or pricing-tokens inside rich-text. */ reinitializeComponents() { // Try to re-initialize popover content and/or pricing-tokens scripts to update content functionality after it is rendered from template. try { if (window.ocrReimagine.PopoverRichTextPlugin) { // Re-initialize popover rich text plugin script. window.ocrReimagine.PopoverRichTextPlugin.initializePopoverRichTextPlugin(); } } catch (e) { } } /* * Regenerates FWLinks by calling the Market Selector's function that handles setting the FWLinkParams. */ regenerateFWLinkParams() { if (!window.ocrReimagine.MarketSelector) { return; } try { // Regenerate the FWLinkParams after the product pricing is rendered. window.ocrReimagine.MarketSelector.setFWLinksQueryParams( window.ocrReimagine.MarketSelector.instance.fwlinkParams ); } catch (e) { // No error handling. } } /** * Update sku telemetry attributes for all the action components in the page. * @param {HTMLDivElement | HTMLSpanElement} renderElement * @param {ProductPricingResponse} priceResponse * @returns {void} */ updateSkuTelemetryAttributes(renderElement, priceResponse) { if (!window.ocrReimagine.SkuTelemetry) return; try { window.ocrReimagine.SkuTelemetry.adjustActionTelemetryAjax(renderElement, priceResponse); } catch (e) { // No error handling. } } /** * Dispatches the render complete events for the product pricing module. * @summary This method dispatches the render complete events for the product pricing module. */ dispatchRenderCompleteEvents() { // Legacy event render complete event for updating the pricing-tokens inside rich-text. const legacyCompleteEvent = new CustomEvent(this.pricingConstants.Events.Pricing.OnComplete, { detail: { traversable: this.pricingManagerOptions.traversable } }); const renderCompleteEvent = new CustomEvent(this.pricingConstants.Events.Pricing.OnRenderComplete); document.dispatchEvent(legacyCompleteEvent); document.dispatchEvent(renderCompleteEvent); } }; new (function () { // Check if edit mode is enabled and page is loaded const isEditing = window.Granite && window.CQ && document.querySelector("[data-render-mode]") && document.readyState === "complete"; if (isEditing) { // Update the product pricing module updateProductPriceModule(); } /** * Updates the reimagine product pricing manager and services on page edit when dialog is submitted. * * @returns void */ function updateProductPriceModule() { try { // Check if product pricing manager instance exists if ( window.ocReimagine && window.ocReimagine.ProductPriceModule && window.ocReimagine.ProductPriceModule.PricingManagerInstance ) { // on page edit mode update product pricing manager instance on SKU component cq dialog submit event window.ocReimagine.ProductPriceModule.PricingManagerInstance.updatePricingManager(); } } catch (error) { console.warn( "[OCR][ProductPricingEditor] update failure: ", error ); } } })();