((document) => { // Attribute names const ATTRIBUTES = { VARIANT: 'data-variant', PARAMS: 'data-params', ENDPOINT: 'data-endpoint', IS_AUTHOR: 'data-is-author', IS_ENV_PROD: 'data-is-env-prod', LOCALE: 'data-locale', SOURCE: 'data-source', RESULTS_TEXT: 'data-results-text' } // Constants const MAX_CARDS = 9; // cards per page const NUM_OF_COLS = 3; // num of cols in card grid /** Mock Search API Functions **/ // Mock function that should be returned by the search API that will // return the MAX_CARDS items that we care about. function getItemsForPage(data, pageNumber) { const cards = data[0].cards; const startIndex = (pageNumber - 1) * MAX_CARDS; const endIndex = startIndex + MAX_CARDS; return [{ cards: cards.slice(startIndex, endIndex), numOfResults: cards.length }]; } // Set mapping function template selector const TEMPLATE_SELECTOR = { HEADER_HEADING: '.card-grid__heading .card-grid__heading-text', CARDS: '.card-grid__cards', LAYOUT: '.card-grid__cards .layout', LAYOUT_COL: '.card-grid__cards .layout__col', CARD_LABEL: '.block-feature__label', CARD_EYEBROW: '.block-feature__eyebrow h5', CARD_TITLE: '.block-feature__title', CARD_PARAGRAPH: '.block-feature__paragraph', CARD_MEDIA: '.media__asset', CARD_MEDIA_SLOT: '.media__slot', CARD_ACTION_GROUP: '.card__content .button-group', CARD_ACTION: '.card__content .link', LOAD_MORE_CONTAINER: '.dynamic-content__load-more', LOAD_MORE_BTN: '.dynamic-content__load-more .btn', CARD_RELATED_PRODUCTS: '.related-products', CARD_RELATED_PRODUCT: '.related-products__product', RELEVANCE_DROPDOWN: '.search-results__main-panel__header__right__button-group', MULTISELECT_BUTTON: '.button-group .btn' }; /** * Builds an API URL based on the filter selections array. * @param {Array} filterSelections - Array of filters used to construct the query parameters in the URL. * @param {string} locale - Locale segment of URL. * @param {string} params - String of extraneous parameters that shouldb e passed into the API call. * @returns {string} - Constructed endpoint for the API call. */ function buildApiUrl(filterSelections, locale, params, requestUri) { let baseParams = `locale=${locale}`; // If there are added params, add them to the endpoint if (params && params !== '') { baseParams += params; } if (filterSelections.length === 0) { return `${requestUri}?onload=true&${baseParams}`; } const filters = filterSelections.join(','); return `${requestUri}?for=${filters}&${baseParams}`; } /** Search API Functions **/ /** * Filters an array of card objects based on the selected filters. * @param {Array} data - Array of objects to be filtered * @param {Array} filterSelections - Each object must include all these IDs in its 'parents' array. * @returns {Array} - Filtered array of objects. */ function filterCardsByParent(data, filterSelections) { return data.filter((obj) => { const parentIds = obj.parents.map((parent) => parent.id); return filterSelections.every((selection) => parentIds.includes(selection)); }); } /** * Fetch data from the API, update the number of stories, and process the cards. * * @param {string} apiUrl - The URL to fetch data from. * @param {boolean} isEnvProd - Boolean value indicating whether or not this is prod environment. * @param {boolean} isAuthor - Boolean value indicating whether or not this is authoring environment. * @param {function} processCardsCallback - A callback function to process the fetched cards. */ async function fetchAndProcessCards(apiUrl, isEnvProd, isAuthor, processCardsCallback) { ajaxFilterCardGrid = new AjaxUtil({ isProd: isEnvProd === 'true', isAuthor: isAuthor === 'true' }); // Fetch, Parse, and extract card array from the API data ajaxFilterCardGrid.invokeRequest({ url: apiUrl, method: 'GET' }).then(response => response.json()) .then(data => { const { card } = data; // Call the callback function to process the fetched cards processCardsCallback(card); }).catch(err => { console.log(err); }); } async function fetchCustomerCards(dynamicContent, locale, isEnvProd, sortBy, skip, activeFilters, processDataCallback) { // Toggle spinner dynamicContent.toggleSpinner(true); // Add -ww to the locale if it's not hyphenated if (!locale.includes('-')) { locale = `${locale}-ww`; } // Create the URL const CUSTOMERS_BASE_URL = isEnvProd === "true" ? "https://msstoreapiprod.microsoft.com" : "https://msstoreapippe.microsoft.com"; let CUSTOMERS_URL = CUSTOMERS_BASE_URL + `/api/customerstoriessearch`; let requestBody = { "locale": locale, "top": MAX_CARDS, "orderBy": `PublishedDate ${sortBy}` } // Add skip if it's provided if (skip) { requestBody.skip = skip; } // Add active filters if provided if (activeFilters) { requestBody = {...activeFilters, ...requestBody}; } await fetch(CUSTOMERS_URL, { method: 'POST', headers: { 'Content-Type': 'applicationhttps://www.microsoft.com/json' }, body: JSON.stringify(requestBody) }) .then(response => response.json()) .then(data => { dynamicContent.toggleSpinner(false); // Call the callback functions to process the fetched cards processDataCallback(data); }).catch(err => { dynamicContent.toggleSpinner(false); console.log(err); }); } // Get the data from the API and update the Filter search results component on content loaded. document.addEventListener('DOMContentLoaded', () => { // Handles all instances of filter card carousel on the page ocrReimagine.SolutionCenter.getInstances().filter((instance) => instance?.el?.classList?.contains('search-results')).forEach(async (searchResultsInstance) => { // Single Select or Multi Select variant const VARIANT = searchResultsInstance.el.getAttribute(ATTRIBUTES.VARIANT); const isSingleSelect = (VARIANT === 'single-select'); const isMultiSelect = (VARIANT === 'multi-select'); // Filter search results component data (from model). const IS_AUTHOR = searchResultsInstance.el.getAttribute(ATTRIBUTES.IS_AUTHOR); const IS_ENV_PROD = searchResultsInstance.el.getAttribute(ATTRIBUTES.IS_ENV_PROD); const PARAMS = searchResultsInstance.el.getAttribute(ATTRIBUTES.PARAMS); const LOCALE = searchResultsInstance.el.getAttribute(ATTRIBUTES.LOCALE); const SOURCE = searchResultsInstance.el.getAttribute(ATTRIBUTES.SOURCE); const RESULTS_TEXT = searchResultsInstance.el.getAttribute(ATTRIBUTES.RESULTS_TEXT); const REQUEST_URI = "/msonecloudapi/" + SOURCE + "/cards"; // Keep track of the sort by - default is descending let sortBy = 'desc'; let activeFiltersQueryParam = {}; // Show relevance dropdown for multiselect if (isMultiSelect) { // Show the relevance dropdown const relevanceDropdown = searchResultsInstance.el.querySelector(TEMPLATE_SELECTOR.RELEVANCE_DROPDOWN); relevanceDropdown.classList.remove('d-none'); } // Dynamically Added Filters const dynamicallyAddedFiltersInstance = searchResultsInstance.getDynamicallyAddedFiltersInstance(); paginationInstance = dynamicallyAddedFiltersInstance.paginationInstance; // Hide the description and load more container const dynamicContent = searchResultsInstance.dynamicContent; dynamicContent.hideDescriptionContainer(); dynamicContent.hideLoadMoreContainer(); /** Functions **/ function update(data) { searchResultsInstance.update({ filteredData: data, pageType: null, // No page type, setting to null showAllCards: false, pageMaxItems: MAX_CARDS, filteredDataLimit: 0, // show all card data mappingObj: { pageType: 'category', onLoad: false, currentPaginationPage: paginationInstance.activeNumber }, mappingFunc: searchResultsMappingFunction, templateSelector: TEMPLATE_SELECTOR, dynamicContentPostTextOpt1: RESULTS_TEXT }); searchResultsInstance.dynamicContent.renderWithDataSubset({ iterableData: false }); } // Initial load if (isSingleSelect) { // Single Select variant fetchAndProcessCards(buildApiUrl([], LOCALE, PARAMS, REQUEST_URI), IS_ENV_PROD, IS_AUTHOR, (cards) => { // Pagination and number of results data model currentData = [{ numOfResults: cards.length, cards: cards }] // Update the card display with the fetched data update(cards); dynamicallyAddedFiltersInstance.updatePaginationAndResults(cards.length); }); } else if (isMultiSelect) { fetchCustomerCards(dynamicContent, LOCALE, IS_ENV_PROD, sortBy, 0, activeFiltersQueryParam, (data) => { update(data.cards); dynamicallyAddedFiltersInstance.updatePaginationAndResults(data.totalCount); }); } // If dynamicallyAddedFilterInstance is not null, then bind click events for the pill bar items if (dynamicallyAddedFiltersInstance) { // Clicking on a pill bar item removes a filter // Unchecking/checking a checkbox also all removes/add filters dynamicallyAddedFiltersInstance.el.addEventListener('onDynamicallyAddedFilterIsFiltered', function (e) { if (e.detail) { // This is where you would update the api string based on filters // e.detail is the filterSelection in filters if (isSingleSelect) { // Build the new API URL based on the filter selections const updatedApiUrl = buildApiUrl(e.detail, LOCALE, PARAMS, REQUEST_URI); // Fetch data with new endpoint fetchAndProcessCards(updatedApiUrl, IS_ENV_PROD, IS_AUTHOR, (cards) => { // Filter the fetched cards based on filterSelections const filteredCards = filterCardsByParent(cards, e.detail); currentData = [{ numOfResults: filteredCards.length, cards: filteredCards }] // Update the UI with the filtered data update(filteredCards); dynamicallyAddedFiltersInstance.updatePaginationAndResults(filteredCards.length); }); } else if (isMultiSelect) { // Create the query params if (e.detail.length > 0) { // Extract query parameters const prefixMatchings = [ { prefix: 'product:', param: 'products' }, { prefix: 'organization-size:', param: 'organizationSize' }, { prefix: 'industry:', param: 'industries' }, { prefix: 'service:', param: 'services' }, { prefix: 'region:', param: 'regions' }, { prefix: 'business-need:', param: 'businessneeds' }, { prefix: 'language:', param: 'language' } ] // Use the prefixes to map the filters to the correct query param activeFiltersQueryParam = {}; prefixMatchings.forEach(({ prefix, param }) => { // Filter the list of filters by the current prefix const filtered = e.detail.filter(filter => filter.startsWith(prefix)); if (filtered.length > 0) { activeFiltersQueryParam[param] = filtered.join(','); } }); } else { // If no filters, reset the query params activeFiltersQueryParam = {}; } // Fetch data with new endpoint fetchCustomerCards(dynamicContent, LOCALE, IS_ENV_PROD, sortBy, 0, activeFiltersQueryParam, (data) => { update(data.cards); dynamicallyAddedFiltersInstance.updatePaginationAndResults(data.totalCount); }); } } }) // Relevance Dropdown Filter Event Listener dynamicallyAddedFiltersInstance.el.addEventListener('onRelevanceChanged', function (e) { if (e.detail) { if (isMultiSelect) { // If publish date, sort by ascending if (e.detail.includes('published-date')) { sortBy = 'asc'; } else { sortBy = 'desc'; } fetchCustomerCards(dynamicContent, LOCALE, IS_ENV_PROD, sortBy, 0, activeFiltersQueryParam, (data) => { update(data.cards); dynamicallyAddedFiltersInstance.updatePaginationAndResults(data.totalCount); }); } else { if (paginationInstance) { // Reset pagination to 1 dynamicallyAddedFiltersInstance.updatePaginationAndResults(currentData[0].numOfResults); } // When published-date is added or removed, we will reverse the order of the current data currentData = [{ numOfResults: currentData[0].numOfResults, cards: currentData[0].cards.slice().reverse() }] update(currentData); } } }) } // Clicking a pagination page updates the data returned if (paginationInstance) { dynamicallyAddedFiltersInstance.el.addEventListener('paginationActivePageChanged', async function (e) { if (e.detail) { // get the new active page const activePage = e.detail.activePage; if (isMultiSelect) { // fetch new data from skipping 9 per page const skip = (activePage - 1) * MAX_CARDS; fetchCustomerCards(dynamicContent, LOCALE, IS_ENV_PROD, sortBy, skip, activeFiltersQueryParam, (data) => { update(data.cards); }); } else { // simulate search api returning spliced data const splicedData = getItemsForPage(currentData, activePage); update(splicedData); } // Set focus on card container after pagination clicked const cardsContainer = searchResultsInstance.el.querySelectorAll(TEMPLATE_SELECTOR.LAYOUT); if (cardsContainer.length > 0) { cardsContainer[1].setAttribute('tabindex', '-1'); cardsContainer[1].focus(); cardsContainer[1].scrollIntoView(); } } }) } if (dynamicallyAddedFiltersInstance && isMultiSelect) { // After all the event listeners are added, check the URL's query params // And simulate a search query based on the query params const queryParams = new URLSearchParams(window.location.search); // Extract values from 'text=' and 'q=' let filterTagsArray = (queryParams.get('q') || '').split(','); // Filter out all duplicates filterTagsArray = [...new Set(filterTagsArray)]; // For each filter tag, simulate a click filterTagsArray.forEach((filterTag) => { dynamicallyAddedFiltersInstance.el.querySelector(`[data-filter-tag="${filterTag}"]`)?.click(); }); } }) }); // MAPPING FUNCTION function searchResultsMappingFunction(obj) { if (!obj.data || (obj.data && obj.data.length === 0)) { return; } const { htmlTemplateClone, singleItem } = obj; let { cards } = singleItem; // cards is not available in single item for Single Select variant if (!cards) { cards = obj.data; } const setCard = (cardData, htmlTemplateClone) => { const { label, title, image, paragraph, eyebrow, footer } = cardData.content; const { text, href, isOpenNewTab, ariaLabel, attributes } = cardData.content.action; const { alt, src, slot } = image; const layoutCol = htmlTemplateClone .querySelectorAll(TEMPLATE_SELECTOR.LAYOUT_COL)[0] .cloneNode(true); const card = layoutCol.querySelector('.card'); const media = card.querySelector(TEMPLATE_SELECTOR.CARD_MEDIA); const mediaSlot = card.querySelector(TEMPLATE_SELECTOR.CARD_MEDIA_SLOT); const action = card.querySelector(TEMPLATE_SELECTOR.CARD_ACTION); const relatedProductsNode = card.querySelector(TEMPLATE_SELECTOR.CARD_RELATED_PRODUCTS); this.setHTML(layoutCol, TEMPLATE_SELECTOR.CARD_LABEL, label ?? ''); this.setHTML(layoutCol, TEMPLATE_SELECTOR.CARD_EYEBROW, eyebrow ?? ''); this.setHTML(layoutCol, TEMPLATE_SELECTOR.CARD_TITLE, title); this.setHTML(layoutCol, TEMPLATE_SELECTOR.CARD_PARAGRAPH, paragraph ?? ''); // remove existing picture element if (card.querySelector('picture')) { card.querySelector('picture').remove(); } // set image element if (media) { const imgNode = document.createElement('img'); imgNode.src = src ?? DefaultImage.src; // Only set alt text if it exists if (alt) { imgNode.alt = alt; } media.append(imgNode); // set the badge if (slot) { const { badge } = slot; const { icon, size } = badge; const mediaSlotImgNode = mediaSlot.querySelector('img'); mediaSlotImgNode.src = icon.src; mediaSlotImgNode.alt = icon.alt || ''; const badgeNode = mediaSlot.querySelector(TEMPLATE_SELECTOR.CARD_BADGE); // m is the default size so if size isn't m then update if (size !== 'm') { badgeNode.classList.remove('badge--size-m', 'badge-logo--m'); badgeNode.classList.add('badge', `badge--size-${size}`, `badge-logo--${size}`); } } else { mediaSlot.remove(); } } // set link if (cardData.content.action) { if (action) { action.href = href; action.setAttribute('aria-label', ariaLabel); action.querySelector('.link__text').innerHTML = text; if (isOpenNewTab) { action.setAttribute('target', '_blank'); } // set link attributes for (const property in attributes) { if (Object.hasOwn(attributes, property)) { action.setAttribute(property, attributes[property]); } } } else { // if it's multiselect, button is used instead of action const button = card.querySelector(TEMPLATE_SELECTOR.MULTISELECT_BUTTON); button.setAttribute('aria-label', ariaLabel); button.querySelector('.btn__text').innerHTML = text; if (isOpenNewTab) { button.setAttribute('target', '_blank'); } } } // set action group actions if (cardData.content.actions) { cardData.content.actions.forEach((cardAction) => { if (cardAction) { const actionTemplate = card.querySelector(TEMPLATE_SELECTOR.CARD_ACTION).cloneNode(true); actionTemplate.href = cardAction.href; actionTemplate.setAttribute('aria-label', cardAction.ariaLabel); actionTemplate.querySelector('.link__text').innerHTML = cardAction.text; if (cardAction.isOpenNewTab) { actionTemplate.setAttribute('target', '_blank'); } // set link attributes for (const property in attributes) { if (Object.hasOwn(attributes, property)) { actionTemplate.setAttribute(property, attributes[property]); } } layoutCol.querySelector(TEMPLATE_SELECTOR.CARD_ACTION_GROUP).append(actionTemplate); } }); } // set footer's related products if (footer?.relatedProducts) { const { products } = footer.relatedProducts; const uniqueLabels = new Set(); const uniqueProducts = products.filter(item => { if (!uniqueLabels.has(item.label)) { uniqueLabels.add(item.label); return true; } return false; }); // Use top 3 products uniqueProducts.slice(0, 3).forEach((product) => { const relatedProduct = relatedProductsNode .querySelector(TEMPLATE_SELECTOR.CARD_RELATED_PRODUCT) .cloneNode(true); const rpBadge = relatedProduct.querySelector('.badge'); const rpLabel = relatedProduct.querySelector('.label'); // update the related product's label rpLabel.innerHTML = product.label; if (product.badge) { const classes = rpBadge.classList; // update the badge size for (const className of classes) { if (className.startsWith('badge--size')) { classes.replace(className, `badge--size-${product.badge.size}`); } } // update the badge icon if (product.badge.icon?.image) { const icon = rpBadge.querySelector('.ocr-icon'); // remove the icon's current icon icon.innerHTML = ''; // create a new icon and append it const newIcon = document.createElement('div'); newIcon.classList.add('ocr-img', 'media__asset'); // create a new img and append it const newImg = document.createElement('img'); newImg.src = product.badge.icon.image.src; newImg.alt = product.badge.icon.image.altText || ''; newIcon.append(newImg); icon.append(newIcon); } } else { rpBadge.remove(); } // append the new related produtct relatedProductsNode.append(relatedProduct); }); // remove the templated related products // currently there are 3 Array.from(relatedProductsNode.querySelectorAll(TEMPLATE_SELECTOR.CARD_RELATED_PRODUCT)).slice(0, 3).forEach(item => item.remove()); } else if (relatedProductsNode){ // if no related products, remove it relatedProductsNode.remove(); } htmlTemplateClone.querySelector(TEMPLATE_SELECTOR.LAYOUT).append(layoutCol); }; let i = 0; for (const cardData of cards) { i++; if (i > MAX_CARDS) { break; } setCard(cardData, htmlTemplateClone); } htmlTemplateClone.querySelectorAll(TEMPLATE_SELECTOR.LAYOUT_COL)[0].remove(); const layoutContainer = htmlTemplateClone.querySelector(TEMPLATE_SELECTOR.LAYOUT); // Remove the existing class that starts with 'layout--cols-' layoutContainer.classList.remove('layout--cols-'); // Add the new class based on the numOfCols value layoutContainer.classList.add(`layout--cols-${NUM_OF_COLS}`); } })(document);