356 lines
13 KiB
JavaScript
356 lines
13 KiB
JavaScript
import { storeActions } from "@/constants/store-actions.js";
|
|
import store from "@/store";
|
|
import { dynamicStrings } from "@/constants/dynamic-strings";
|
|
|
|
export function fetchCmsContentForPage(fmgPage) {
|
|
return store.dispatch(storeActions.GET_PAGE_DATA, { pageName: fmgPage }).then((response) => {
|
|
const pageDataFromCms = {};
|
|
|
|
response.data.Result.forEach((widget) => {
|
|
let 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;
|
|
});
|
|
}
|
|
|
|
// Function to convert a string, into a matching global state item.
|
|
function mapStringToState(str) {
|
|
// Pull all matches out of the string.
|
|
const regexExp = new RegExp("{([^{}]*?):([^{}]*?)}", "g");
|
|
const regexMatches = [...str.matchAll(regexExp)];
|
|
const globalStateMatches = regexMatches.filter((match) => {
|
|
return match[1] === dynamicStrings.GLOBAL_STATE;
|
|
});
|
|
|
|
// Our final string value that will be built from the matches.
|
|
let stringBuilder = "";
|
|
for (const match of globalStateMatches) {
|
|
const valueFromStore = getStoreValueFromString(match[2]);
|
|
if (!valueFromStore) {
|
|
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.
|
|
stringBuilder = `${stringBuilder} ${stringWithReplacement}`;
|
|
}
|
|
return stringBuilder.trimStart();
|
|
}
|
|
|
|
// Parent function for processWidgetItemForReplacement. This will loop through the parent
|
|
// object and pass any objects that need additional processing to the processWidgetItemForReplacement function.
|
|
function findAndReplaceGlobalStateValues(widgetModel, widgetName) {
|
|
const objWithReplacements = {
|
|
Name: widgetName,
|
|
Model: {},
|
|
};
|
|
|
|
Object.keys(widgetModel).forEach((key) => {
|
|
let modelWithReplacements = processWidgetItemForReplacement(widgetModel, key);
|
|
|
|
objWithReplacements.Model[key] = modelWithReplacements;
|
|
});
|
|
|
|
return objWithReplacements;
|
|
}
|
|
|
|
// 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(widgetModel, key) {
|
|
// If we have a string, and it needs to be replaced.
|
|
if (typeof widgetModel[key] === "string") {
|
|
widgetModel[key] = processIfStatements(
|
|
widgetModel[key],
|
|
dynamicStrings.GLOBAL_STATE,
|
|
getStoreValueFromString
|
|
);
|
|
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];
|
|
}
|
|
|
|
function getStoreValueFromString(str) {
|
|
let storeOrStateObject = str.includes("getters") ? store : store.state;
|
|
for (const s of str.split(".")) {
|
|
if (storeOrStateObject[s] != undefined) {
|
|
storeOrStateObject = storeOrStateObject[s];
|
|
} else {
|
|
return ""; // if we can't map our string to state data, return an empty string.
|
|
}
|
|
}
|
|
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;
|
|
} else {
|
|
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
|
|
);
|
|
}
|
|
}
|
|
|
|
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++;
|
|
}
|
|
}
|
|
|
|
function flagMatchesForProcessing(matches, elseStatementIndex) {
|
|
matches[0].isFlaggedForProcessing = true;
|
|
matches[matches.length - 1].isFlaggedForProcessing = true;
|
|
if (elseStatementIndex) {
|
|
matches[elseStatementIndex].isFlaggedForProcessing = true;
|
|
}
|
|
}
|
|
|
|
function joinProcessedRegexArray(regexMatches) {
|
|
let processedString = "";
|
|
regexMatches.forEach((match) => {
|
|
const rawString = match[0];
|
|
processedString += match.groups.processedString ?? rawString;
|
|
});
|
|
return processedString;
|
|
}
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
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 chracters (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 //
|
|
//////////////////////////////////////////
|
|
|
|
export function doesCopyContainRouterLink(copy) {
|
|
return copy.includes(dynamicStrings.ROUTER_LINK);
|
|
}
|
|
|
|
export function doesCopyContainTextLink(copy) {
|
|
return copy.includes(dynamicStrings.TEXT_LINK);
|
|
}
|
|
|
|
/**
|
|
* splits copy on { ... } such as {routerlink: ...}
|
|
* @returns array of strings
|
|
*/
|
|
export function splitCopyOnCMSPlaceHolder(copy) {
|
|
return copy.split(/{(.*?)}/g).filter((str) => str.length > 0);
|
|
}
|
|
|
|
/**
|
|
* Returns string2 of input following this pattern: {string1:string2,string3}
|
|
* @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];
|
|
}
|
|
|
|
export function getExternalLink(copy) {
|
|
let url = copy.split(":")[1].split(",")[0];
|
|
if (url.includes("https-")) {
|
|
const prefixAdded = url.replace("https-", "https://");
|
|
return prefixAdded;
|
|
}
|
|
|
|
return "#!";
|
|
}
|
|
|
|
/**
|
|
* Returns string3 of input following this pattern: {string1:string2,string3}
|
|
* @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];
|
|
}
|
|
|
|
// 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
|
|
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 !== "");
|
|
}
|
|
|
|
export function splitCMSCopyOnBR(copy) {
|
|
return copy.split("<br>");
|
|
}
|
|
|
|
// This function takes in a number and returns it formatted as USD currency, with or without cents depending on if the number is an integer or not.
|
|
export function formatToUSDollar(amount) {
|
|
const isInteger = amount % 1 === 0;
|
|
return new Intl.NumberFormat("en-US", {
|
|
style: "currency",
|
|
currency: "USD",
|
|
minimumFractionDigits: isInteger ? 0 : 2,
|
|
maximumFractionDigits: 2,
|
|
}).format(amount);
|
|
}
|