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);