import { LightningElement, api } from "lwc";
import cx from "classnames";
import { FooterVariant, LightningSlotElement } from "typings/custom";
import { track } from "dxUtils/analytics";
import { isSlotEmpty } from "dxUtils/slot";
class Footer extends LightningElement {
private _variant: FooterVariant = "small-signup";
private isSlotEmpty = true;
private signupUrl =
"https://www.salesforce.com/company/newsletter-subscribe/?topics=Developer&d=pb";
@api
mfeHomeHref: string = `/${window.location.host}`; // ugly hack: ideally this wouldn't be necessary, but the only way to remove the "See all ways to contact us" link from the footer MFE is to set this to a non-empty value other than "us"; and given the way that the footer works, the non-empty value needs to be something that can be appended to `/` and work correctly
@api
mfeConfigOrigin: string = `${window.location.origin}/developer/en-us/wp-json`;
@api
useMfe: boolean = false; // TODO: Probably just remove this once the footer MFE supports rendering in non-full-width contexts
@api
set variant(value: FooterVariant) {
if (!this.isFooterVariant(value)) {
console.error(
"Value provided for variant is invalid and should be of type FooterVariant"
);
return;
}
this._variant = value;
}
get variant() {
return this._variant;
}
get showLegalOnly() {
return this.variant === "terms-only";
}
get showLargeSignup() {
return this.variant === "large-signup";
}
get showSmallSignup() {
return this.variant === "small-signup";
}
get showLargeNoSignup() {
return this.variant === "large-no-signup";
}
get currentYear(): number {
const date = new Date();
return date.getFullYear();
}
get signupLabel(): string {
return this.showLargeSignup
? "Get timely developer updates
delivered to your inbox."
: "Sign up for developer updates";
}
get className() {
return cx(`signup-variant-${this.variant}`);
}
get largeNoSignupClassName() {
return cx(
"content-container",
"content-container_top_large",
"content-container_top_large_no_signup",
!this.isSlotEmpty && "custom-content"
);
}
private onSlotChange(e: LightningSlotElement): void {
this.isSlotEmpty = isSlotEmpty(e);
}
private isFooterVariant(value: string): value is FooterVariant {
return [
"no-signup",
"small-signup",
"large-signup",
"large-no-signup",
"terms-only"
].includes(value);
}
private handleSignup(e: any) {
track(e.currentTarget, "custEv_ctaButtonClick", {
click_text: this.signupLabel,
click_url: this.signupUrl,
element_type: "button",
element_title: this.showSmallSignup
? "Get Developer Updates"
: "get the latest developer updates.",
content_category: "cta"
});
}
}
// This encapsulates the old non-MFE footer functionality that should be removed once the footer MFE supports rendering in non-full-width contexts.
// It's encapsulated like this solely for ease of deletion later.
function augmentWithNonMFEFooterFunctionality(FooterClass: typeof Footer) {
const baseSocialIconUrl = `/assets/icons/brand-sprite/svg/symbols.svg`;
return class NonMFEFooter extends FooterClass {
private didRequestFooterConfig = false;
private footerConfig: any = []; // NOTE: See mockProps.ts for the expected format of the footer config
private configItemTitleToItemLookup: Map = new Map();
private configItemParentToChildrenLookup: Map =
new Map();
private generalLinks: any = [];
private socialLinks: any = [];
private termsLinks: any = [];
async renderedCallback() {
super.renderedCallback?.();
if (this.didRequestFooterConfig || this.useMfe) {
return;
}
this.didRequestFooterConfig = true;
this.footerConfig = await this.requestFooterConfig();
this.buildFooterConfigLookupTables(this.footerConfig);
this.setSocialLinks();
this.setGeneralLinks();
this.setLegalFooter();
}
get showContainerMiddle() {
return !this.showLegalOnly;
}
private async requestFooterConfig() {
let config: any = [];
try {
const response = await fetch(
`${this.mfeConfigOrigin}/c360/experience/v1/navigation`
);
const data = await response.json();
config = data["footer-navigation"] || [];
} catch (error) {
console.error("Error requesting footer config:", error);
}
return config;
}
private buildFooterConfigLookupTables(config: any[]) {
config.forEach((item: any) => {
// attr_title is preferable to title because it is not language-specific
this.configItemTitleToItemLookup.set(
item.attr_title || item.title,
item
);
if (item.menu_item_parent) {
const parentId = parseInt(item.menu_item_parent, 10);
const children =
this.configItemParentToChildrenLookup.get(parentId) ||
[];
children.push(item);
this.configItemParentToChildrenLookup.set(
parentId,
children
);
}
});
}
private setSocialLinks() {
const socialLinksParentItem =
this.configItemTitleToItemLookup.get("social-icons");
if (!socialLinksParentItem || !socialLinksParentItem.ID) {
console.error(
"Social links parent item not found in footer config"
);
return;
}
const socialLinksItems = this.configItemParentToChildrenLookup.get(
socialLinksParentItem.ID
);
if (!socialLinksItems) {
console.error(
"Social links children items not found in footer config"
);
return;
}
this.socialLinks = socialLinksItems.map((child: any) => {
const childTitle: string =
child.attr_title || child.title || ""; // attr_title is preferable to title because it is not language-specific
const iconSymbol =
childTitle === "LinkedIn"
? "linked-in"
: childTitle.toLocaleLowerCase();
const iconUrlHash =
iconSymbol === "twitter"
? "#twitter-x"
: `#themed-${iconSymbol}`;
return {
href: child.url,
iconSprite: "brand",
iconURL: `${baseSocialIconUrl}${iconUrlHash}`,
label: child.title,
iconSymbol
};
});
}
private setGeneralLinks() {
const topFooterItem =
this.configItemTitleToItemLookup.get("top-footer");
const mediaItem = this.configItemTitleToItemLookup.get("media");
if (!topFooterItem || !topFooterItem.ID) {
console.error("Top footer item not found in footer config");
return;
}
const generalLinksHeadingsItems =
this.configItemParentToChildrenLookup
.get(topFooterItem.ID)
?.filter((item: any) => item.ID !== mediaItem?.ID);
if (!generalLinksHeadingsItems) {
console.error(
"General links children items not found in footer config"
);
return;
}
const generalLinks: any[] = [];
generalLinksHeadingsItems.forEach((item: any) => {
const childrenItems = this.configItemParentToChildrenLookup.get(
item.ID
);
if (!childrenItems) {
console.error(
`General links children items not found for item with title "${item.title}"`
);
return;
}
generalLinks.push({
id: item.title,
label: item.title,
options: childrenItems.map((option: any) => {
return {
id: option.title,
label: option.title,
link: {
href: option.url,
target: option.target
}
};
})
});
});
this.generalLinks = generalLinks;
}
private setLegalFooter() {
const copyrightNoticeItem = this.configItemTitleToItemLookup.get(
"All rights reserved"
);
if (!copyrightNoticeItem) {
console.error(
"All rights reserved item not found in footer config"
);
return;
}
const { url, description } = copyrightNoticeItem;
const copyrightNoticeInnerHtml = description.replace(
"{{All rights reserved}}",
`All rights reserved`
);
const copyrightNoticeEl =
this.template.querySelector(".copyright-notice")!;
copyrightNoticeEl.innerHTML = copyrightNoticeInnerHtml;
const copyrightLink = copyrightNoticeEl.querySelector("a");
if (copyrightLink) {
copyrightLink.addEventListener("click", (e) =>
this.handleCopyrightLinkClick(e as Event)
);
}
const legalLinksItems = this.configItemParentToChildrenLookup.get(
copyrightNoticeItem.ID
);
if (!legalLinksItems) {
console.error(
"Legal links children items not found in footer config"
);
return;
}
this.termsLinks = legalLinksItems.map((item: any) => {
const link: any = {
label: item.title,
href: item.url,
target: item.target,
rel: item.rel
};
const itemTitle: string = item.attr_title || item.title || ""; // attr_title is preferable to title because it is not language-specific
if (itemTitle === "Cookie Preferences") {
link.href = "#";
link.onclick = this.handleCookiePreferencesClick;
} else if (itemTitle === "Your Privacy Choices") {
link.img =
"https://developer.salesforce.com/ns-assets/privacyoptions.svg";
}
return link;
});
}
private openOneTrustInfoDisplay() {
if (
(window as any).OneTrust &&
(window as any).OneTrust.ToggleInfoDisplay
) {
(window as any).OneTrust.ToggleInfoDisplay();
}
}
private trackFooterLinkClick(
e: Event,
options?: {
label?: string;
elementTitle?: string;
clickUrl?: string;
}
) {
const anchor = e.currentTarget as HTMLAnchorElement;
const clickText =
options?.label ??
anchor.textContent?.trim() ??
anchor.getAttribute("aria-label") ??
anchor.href;
const elementTitle = options?.elementTitle ?? clickText;
const clickUrl = options?.clickUrl ?? anchor.href;
track(anchor, "custEv_linkClick", {
click_text: clickText,
click_url: clickUrl,
element_type: "link",
element_title: elementTitle,
content_category: "footer",
nav_item: clickText,
nav_level: "1",
nav_type: "footer"
});
}
private handleLogoClick(e: Event) {
this.trackFooterLinkClick(e, {
label: "Salesforce",
elementTitle: "Salesforce logo",
clickUrl: `${window.location.origin}/`
});
}
private handleSocialLinkClick(e: Event) {
this.trackFooterLinkClick(e);
}
private handleCookiePreferencesClick(e: Event) {
this.trackFooterLinkClick(e, {
label: "Cookie Preferences",
clickUrl: (e.currentTarget as HTMLAnchorElement).href || "#"
});
this.openOneTrustInfoDisplay();
}
private handleTermsClick(e: Event) {
this.trackFooterLinkClick(e);
}
private handleCopyrightLinkClick(e: Event) {
this.trackFooterLinkClick(e, { label: "All rights reserved" });
}
};
}
export default augmentWithNonMFEFooterFunctionality(Footer);