DigitalConsumer.ISS/src/helpers/cms-content-helper.js

636 lines
21 KiB
JavaScript

import dynamicStrings from '@/constants/dynamic-strings';
import { useMainStore } from '@/store';
/**
* @function getStringWithCustomValues
* @summary Returns str with all custom values replaced in accordance with the provided customValueMap
* @param {string} str
* @param {Dictionary} customValueMap
* @returns {string}
*/
export function getStringWithCustomValues(str, customValueMap) {
let newString = str ?? '';
if (customValueMap != null) {
Object.keys(customValueMap).forEach((key) => {
newString = newString.replaceAll(`{custom:${key}}`, customValueMap[key]);
});
}
return newString;
}
// This function will process the widget item and replace any global state variables with their values.
// This is a recursive function, it will call itself until it runs out of items to iterate on given the object.
/**
* @function processWidgetItemForReplacement
* @param widgetModel
* @param key
*/
function processWidgetItemForReplacement(widgetModel, key) {
// If we have a string, and it needs to be replaced.
if (typeof widgetModel[key] === 'string') {
if (widgetModel[key].includes('{if:')) {
widgetModel[key] = processIfStatements(
widgetModel[key],
dynamicStrings.GLOBAL_STATE,
getStoreValueFromString
);
}
if (widgetModel[key].includes(dynamicStrings.MODAL_LINK)) {
widgetModel[key] = mapStringToModal(widgetModel[key]);
}
if (widgetModel[key].includes(dynamicStrings.EXTERNAL_LINK)) {
widgetModel[key] = mapStringToLink(widgetModel[key]);
}
if (widgetModel[key].includes(dynamicStrings.GLOBAL_STATE)) {
widgetModel[key] = mapStringToState(widgetModel[key]);
}
return widgetModel[key];
}
// If we have an object. array, etc
if (typeof widgetModel[key] === 'object' && Object.keys(widgetModel[key]).length) {
Object.keys(widgetModel[key]).forEach((item) => {
processWidgetItemForReplacement(widgetModel[key], item);
});
return widgetModel[key];
}
// If we have something else like a number, boolean, etc. just return it
return widgetModel[key];
}
// Parent function for processWidgetItemForReplacement. This will loop through the parent
// object and pass any objects that need additional processing to the processWidgetItemForReplacement function.
/**
* @function processWidgetItemForfindAndReplaceGlobalStateValuesReplacement
* @param widgetModel
* @param widgetName
*/
function findAndReplaceGlobalStateValues(widgetModel, widgetName) {
const objWithReplacements = {
Name: widgetName,
Model: {}
};
Object.keys(widgetModel).forEach((key) => {
const modelWithReplacements = processWidgetItemForReplacement(widgetModel, key);
objWithReplacements.Model[key] = modelWithReplacements;
});
return objWithReplacements;
}
// Support method for processing the page data from the CMS call.
// widgets = current widget collection used by page.
// baseResponse = contains the widgets from the base page.
// clientResponse = contains the widgets from the client override page. (null if none)
/**
* @function processPageData
* @param baseResponse
* @param clientResponse
*/
function processPageData(baseResponse, clientResponse) {
const pageDataFromCms = {};
let widgets = [];
if (!baseResponse?.data?.Result) {
console.error('No result data found'); // Something has gone terribly wrong.
return {};
}
if (!clientResponse?.data?.Result) {
widgets = baseResponse.data.Result;
} else {
// Override any base widgets with the client override widgets if found.
baseResponse.data.Result.forEach((baseWidget) => {
let found = false;
clientResponse.data.Result.forEach((clientWidget) => {
if (clientWidget.Name === baseWidget.Name) {
widgets.push(clientWidget);
found = true;
}
});
if (!found) {
widgets.push(baseWidget);
}
});
// Add any unique client widgets that does not exist in the current main widgets collection.
clientResponse.data.Result.forEach((clientWidget) => {
let found = false;
widgets.forEach((currentWidget) => {
if (clientWidget.Name === currentWidget.Name) {
found = true;
}
});
if (!found) {
widgets.push(clientWidget);
}
});
}
widgets.forEach((widget) => {
// Global state value replacement.
const widgetWithReplacements = findAndReplaceGlobalStateValues(widget.Model, widget.Name);
// If we already have this widget, push it on the collection
if (widgetWithReplacements.Name in pageDataFromCms) {
pageDataFromCms[widgetWithReplacements.Name].push(widgetWithReplacements.Model);
return;
}
pageDataFromCms[widgetWithReplacements.Name] = [widgetWithReplacements.Model];
});
Object.keys(pageDataFromCms).forEach((key) => {
if (pageDataFromCms[key].length === 1) {
pageDataFromCms[key] = pageDataFromCms[key][0];
}
});
return pageDataFromCms;
}
/**
*
* @param issPage
*/
export function fetchCmsContentForPage(issPage) {
const store = useMainStore();
const { clientName } = store.issConfig;
const { parentAccountNumber } = store.issConfig;
const clientOverride = clientName.length > 0 && parentAccountNumber > 0;
return (
store
.getPageData(issPage)
// Get the base/default page first.
.then((baseResponse) => {
if (!clientOverride) {
// Return the base page if there are no client override.
return processPageData(baseResponse, null);
}
// Else get the client override page.
const pageName = `${issPage}_${clientName.toLowerCase().replace(/ /g, '')}`;
return store.getPageData(pageName).then(
(clientResponse) =>
// Process the client override if it exists.
processPageData(baseResponse, clientResponse),
(error) => {
console.error(error);
// Process the just the base if no client override exists.
return processPageData(baseResponse, null);
}
);
})
);
}
export function fetchGlobalCmsContent(issComponent) {
const store = useMainStore();
return (
store.getPageData(issComponent)
.then((baseResponse) => processPageData(baseResponse, null)));
}
export function updateCmsSiteHeader(cmsResponse) {
const clientCmsSiteHeader = getCmsSiteHeader(cmsResponse);
useMainStore().issConfig.clientHeader = clientCmsSiteHeader;
}
function getCmsSiteHeader(cmsResponse) {
if (cmsResponse?.SiteHeaderWidget) {
const siteHeader = cmsResponse?.SiteHeaderWidget;
if (siteHeader.Answers) {
const { parentAccountNumber } = useMainStore().issConfig;
let headerAnswersToUse = siteHeader.Answers
.find((answer) => answer.AccountNumber === parentAccountNumber.toString());
if (headerAnswersToUse == null) {
headerAnswersToUse = siteHeader.Answers
.find((answer) => answer.AccountNumber === '0');
}
if (headerAnswersToUse) {
return {
altText: headerAnswersToUse.AltText,
clientName: headerAnswersToUse.ClientName,
hexCode: headerAnswersToUse.HexCode,
imageId: headerAnswersToUse.ImageId,
imageUrl: headerAnswersToUse.AnswerImageUrl
};
}
}
}
return {};
}
/**
* @function mapStringToModal
* @param str
*/
function mapStringToModal(str) {
const startIndex = str.indexOf(`{${dynamicStrings.MODAL_LINK}`);
let linkToReplace = str.substring(startIndex, str.length);
linkToReplace = linkToReplace.substring(0, linkToReplace.indexOf('}') + 1);
const params = linkToReplace.substring(
dynamicStrings.MODAL_LINK.length + 2,
linkToReplace.length - 1
);
const splitParams = params.split(',');
const bodyText =
`<a href="#" modalTarget="${splitParams[0]}" class="modal-text"><span class="sr-only">Link:</span>${splitParams[1]}</a>`;
let returnVal = str.replace(linkToReplace, bodyText);
if (returnVal.includes(dynamicStrings.MODAL_LINK)) {
returnVal = mapStringToModal(returnVal);
}
return returnVal;
}
/**
* @function mapStringToLink
* @param str
*/
function mapStringToLink(str) {
const startIndex = str.indexOf(`{${dynamicStrings.EXTERNAL_LINK}`);
let linkToReplace = str.substring(startIndex, str.length);
linkToReplace = linkToReplace.substring(0, linkToReplace.indexOf('}') + 1);
const params = linkToReplace.substring(
dynamicStrings.EXTERNAL_LINK.length + 2,
linkToReplace.length - 1
);
const splitParams = params.split(',');
const bodyText = `<a href="${splitParams[0]}" class="external-text" target="_blank">${splitParams[1]}</a>`;
let returnVal = str.replace(linkToReplace, bodyText);
if (returnVal.includes(dynamicStrings.EXTERNAL_LINK)) {
returnVal = mapStringToLink(returnVal);
}
return returnVal;
}
// Function to convert a string, into a matching global state item.
/**
* @function mapStringToState
* @param str
*/
function mapStringToState(str) {
// Pull all matches out of the string.
const regexExp = /{([^{}]*?):([^{}]*?)}/g;
const regexMatches = [...str.matchAll(regexExp)];
const globalStateMatches = regexMatches.filter((match) => match[1] === dynamicStrings.GLOBAL_STATE);
// Our final string value that will be built from the matches.
const stringBuilder = '';
for (const match of globalStateMatches) {
// Reset store state for each match.
const valueFromStore = getStoreValueFromString(match[2]);
if (!valueFromStore) {
console.warn('Unable to resolve global state data.');
console.warn(`Tried to resolve: ${match[2]}`);
return ''; // if we can't map our string to state data, return an empty string.
}
const stringWithReplacement = str.replace(match[0], valueFromStore);
// If we still have values we need to substitute, call this function again.
if (stringWithReplacement.includes(dynamicStrings.GLOBAL_STATE)) {
return mapStringToState(stringWithReplacement);
}
// Concatenate the string.
str = `${stringBuilder} ${stringWithReplacement}`;
}
return str.trimStart();
}
/**
* @function getStoreValueFromString
* @param str
*/
function getStoreValueFromString(str) {
if (!str) return '';
let storeOrStateObject = useMainStore();
for (const s of str.split('.')) {
if (s === 'getters') continue; // For backward compatibility
// TODO: I don't think this next line is doing what they think it's doing.
if (typeof storeOrStateObject[s] !== "undefined") {
storeOrStateObject = storeOrStateObject[s];
} else {
break;
}
}
return storeOrStateObject ?? '';
}
/// ////////////////////////////////
// If Statement Processing Logic //
/// ////////////////////////////////
/**
* Recursive function - replaces all instances of if statements from the CMS that utilize the specified ifConditionKeyword
* @param {*} str string - Input string to be processed
* @param {*} ifConditionKeyword string - Defines which if statements to process ex: 'globalState'
* @param {*} replacePlaceholderCallback function - Callback to replace CMS placeholder values
* @returns The processed string
*/
export function processIfStatements(str, ifConditionKeyword, replacePlaceholderCallback) {
const containsRelevantIfStatement = new RegExp(`{if:${ifConditionKeyword}:.+?}`, 'g').test(str);
if (!containsRelevantIfStatement) {
return str;
}
const ifStatementRegexExpression = getIfStatementRegexExpression();
const ifStatementRegexMatches = [...str.matchAll(ifStatementRegexExpression)];
const completeIfStatementArray = getAndFlagFirstNonNestedIfStatementWithKeyword(
ifStatementRegexMatches,
ifConditionKeyword
);
executeIfStatementAndSetProcessedStrings(completeIfStatementArray, replacePlaceholderCallback);
const reconstructedPostProcessedString = joinProcessedRegexArray(ifStatementRegexMatches);
return processIfStatements(
reconstructedPostProcessedString,
ifConditionKeyword,
replacePlaceholderCallback
);
}
/**
*
* @param matches
* @param ifConditionKeyword
*/
function getAndFlagFirstNonNestedIfStatementWithKeyword(matches, ifConditionKeyword) {
let index = 0;
for (const match of matches) {
if (match.groups.isIfStatement && match.groups.ifConditionType === ifConditionKeyword) {
let interiorIndex = 0;
let nestedLevel = 0;
let elseStatementIndex = null;
for (const interiorMatch of matches.slice(index + 1)) {
if (interiorMatch.groups.isIfStatement) {
if (interiorMatch.groups.ifConditionType === ifConditionKeyword) {
break;
} else {
nestedLevel++;
}
} else if (interiorMatch.groups.isElseStatement) {
if (!nestedLevel) {
elseStatementIndex = interiorIndex + 1;
}
} else if (interiorMatch.groups.isEndStatement) {
if (nestedLevel) {
nestedLevel--;
} else {
const ifStatementArray = matches.slice(index, index + interiorIndex + 2);
flagMatchesForProcessing(ifStatementArray, elseStatementIndex);
return ifStatementArray;
}
}
interiorIndex++;
}
}
index++;
}
console.error('Did not find end in conditional logic');
return matches;
}
/**
*
* @param matches
* @param elseStatementIndex
*/
function flagMatchesForProcessing(matches, elseStatementIndex) {
matches[0].isFlaggedForProcessing = true;
matches[matches.length - 1].isFlaggedForProcessing = true;
if (elseStatementIndex) {
matches[elseStatementIndex].isFlaggedForProcessing = true;
}
}
/**
*
* @param regexMatches
*/
function joinProcessedRegexArray(regexMatches) {
let processedString = '';
regexMatches.forEach((match) => {
const rawString = match[0];
processedString += match.groups.processedString ?? rawString;
});
return processedString;
}
/**
*
* @param ifStatementArray
* @param replacePlaceholderCallback
*/
function executeIfStatementAndSetProcessedStrings(ifStatementArray, replacePlaceholderCallback) {
const ifCondition = replacePlaceholderCallback(ifStatementArray[0].groups.ifCondition);
let isInsideDesiredBlock = ifCondition;
ifStatementArray.forEach((entry) => {
if (entry.groups.isElseStatement && entry.isFlaggedForProcessing) {
isInsideDesiredBlock = !ifCondition;
} else if (entry.groups.isEndStatement && entry.isFlaggedForProcessing) {
isInsideDesiredBlock = true;
}
setProcessedStringOnEntry(entry, isInsideDesiredBlock);
});
}
/**
*
* @param entry
* @param isInsideDesiredBlock
*/
function setProcessedStringOnEntry(entry, isInsideDesiredBlock) {
if (!isInsideDesiredBlock) {
entry.groups.processedString = '';
} else if (entry.groups.isIfStatement) {
entry.groups.processedString = entry.isFlaggedForProcessing
? entry.groups.ifTrailingString
: entry[0];
} else if (entry.groups.isElseStatement) {
entry.groups.processedString = entry.isFlaggedForProcessing
? entry.groups.elseTrailingString
: entry[0];
} else {
entry.groups.processedString = entry.isFlaggedForProcessing
? entry.groups.endTrailingString
: entry[0];
}
}
/**
*
*/
function getIfStatementRegexExpression() {
// Matches but does not capture:
// {if:...} or {else} or {end}
const anyLogicOperatorNonCapture = '(?:{(?:end|else|if:.*?)})';
// NOTE: ?<variableName> syntax stores the captured match like so: match.groups.variableName
const matchStartOfString =
'(?<processedString>^.+?)' // Match and Capture all characters (lazy), cannot be empty
+ '(?=(?:{if))'; // Looks ahead but does not capture {if
const matchIfOperator =
'(?<isIfStatement>{if:)' // Match & Capture {if:
+ '(?<ifConditionType>.*?):' // Match all chars up to and including next ':' - Capture all chars up to ':'
+ '(?<ifCondition>.*?)}' // Match all chars up to and including next '}' - Capture all chars up to '}'
+ '(?<ifTrailingString>.*?)' // Match and Capture all characters (lazy), can be empty
+ `(?=${anyLogicOperatorNonCapture})`; // Looks ahead but does not capture the next logic operator
const matchElseOperator =
'(?<isElseStatement>{else})' // Match & Capture {else}
+ '(?<elseTrailingString>.*?)' // Match & Capture all characters (lazy), can be empty
+ `(?=${anyLogicOperatorNonCapture})`; // Looks ahead but does not capture the next logic operator
const matchEndOperator =
'(?<isEndStatement>{end})' // Match & Capture {end}
+ '(?<endTrailingString>.*?)' // Match & Capture all characters (lazy), can be empty
+ `(?=${anyLogicOperatorNonCapture}|$)`; // Looks ahead but does not capture the next logic operator
// Combine all matching patterns, separated by 'or' pipes
return new RegExp(
`${matchStartOfString}|${matchIfOperator}|${matchElseOperator}|${matchEndOperator}`,
'g'
);
}
/// ///////////////////////////////////////
// End of If Statement Processing Logic //
/// ///////////////////////////////////////
/**
*
* @param copy
*/
export function doesCopyContainTextLink(copy) {
return copy.includes(dynamicStrings.TEXT_LINK);
}
/**
*
* @param context
*/
export function setupModalLinks(context) {
context.$nextTick(() => {
const elements = document.getElementsByClassName('modal-text');
for (const element of elements) {
const target = element.getAttribute('modalTarget');
if (target) {
element.addEventListener('click', () => context.$refs[target].openModal());
}
}
});
}
/**
*
* @param context
* @param desiredTarget
*/
export function setupModalLink(context, desiredTarget) {
context.$nextTick(() => {
const element = document.querySelector(`[modalTarget=${desiredTarget}]`);
if (element) {
element.addEventListener('click', () => context.$refs[desiredTarget].openModal());
}
});
}
/**
*
* @param copy
*/
export function doesCopyContainRouterLink(copy) {
return copy.includes(this.dynamicStrings.ROUTER_LINK);
}
/**
* splits copy on { ... } such as {routerlink: ...}
* @param copy
* @returns array of strings
*/
export function splitCopyOnCMSPlaceHolder(copy) {
// splits copy on { ... } such as {routerlink: ...}
return copy.split(/{(.*?)}/g);
}
export function getExternalLink(copy) {
const url = copy.split(':')[1].split(',')[0];
if (url.includes('https-')) {
const prefixAdded = url.replace('https-', 'https://');
return prefixAdded;
}
return '#!';
}
/**
* Returns string2 of input following this pattern: {string1:string2,string3}
* @param copy
* @returns string
*/
export function getRouterLinkRouteFromCopy(copy) {
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'estimate'
return copy.split(':')[1].split(',')[0];
}
/**
* Returns string3 of input following this pattern: { string1: string2, string3 }
* @param copy
* @returns string
*/
export function getRouterLinkDisplayTextFromCopy(copy) {
// sample input: {routerLink:estimate,provide your VIN}
// first split would return 'estimate,provide your VIN'
// second split would return 'provide your VIN'
return copy.split(':')[1].split(',')[1];
}
/**
* Returns a router link as an 'a' tag element
* @param copy
* @returns string
*/
export function getRouterLinkHtmlStringFromCopy(copy) {
return `<a href='/?issPage=${getRouterLinkRouteFromCopy(copy)}' class='router-link'>${getRouterLinkDisplayTextFromCopy(copy)}</a>`;
}
/**
* Returns a phone link as an 'a' tag element
* @param phoneNumber
* @returns string
*/
export function getPhoneLinkHtmlStringFromPhoneNumber(phoneNumber) {
return `<a href='tel:${phoneNumber}' class='phone-link'>${phoneNumber}.</a>`;
}
// Copy returned from the CMS that has newlines will return blocks wrapped in
// <p ... >...</p>
// This function returns an array of each paragraph, works with or without html
// attributes present
/**
*
* @param copy
*/
export function splitCMSCopyOnParagraphTag(copy) {
// filter removes empty strings that are a result of string.split with regex
return copy.split(/(?:<p(?:.*?)>)|(?:<\/p>)/g).filter((paragraph) => paragraph !== '');
}