@salesforcedevs/docs-components
Advanced tools
Comparing version
{ | ||
"modules": [ | ||
{ "dir": "src/modules" }, | ||
{ "npm": "@salesforcedevs/dx-components" } | ||
{ "npm": "@salesforcedevs/dx-components" }, | ||
{ "npm": "@salesforcedevs/dw-components" } | ||
], | ||
@@ -9,13 +10,21 @@ "expose": [ | ||
"doc/breadcrumbs", | ||
"doc/componentPlayground", | ||
"doc/content", | ||
"doc/contentCallout", | ||
"doc/chat", | ||
"doc/doDont", | ||
"doc/contentLayout", | ||
"doc/contentMedia", | ||
"doc/docXmlContent", | ||
"doc/lwcContentLayout", | ||
"doc/header", | ||
"doc/heading", | ||
"doc/headingAnchor", | ||
"doc/overview", | ||
"doc/phase", | ||
"doc/xmlContent" | ||
"doc/specificationContent", | ||
"doc/versionPicker", | ||
"doc/xmlContent", | ||
"docUtils/utils" | ||
] | ||
} |
{ | ||
"name": "@salesforcedevs/docs-components", | ||
"version": "0.0.1", | ||
"version": "0.0.2-edit", | ||
"description": "Docs Lightning web components for DSC", | ||
@@ -8,3 +8,3 @@ "license": "MIT", | ||
"engines": { | ||
"node": ">= 14.x" | ||
"node": "20.x" | ||
}, | ||
@@ -15,16 +15,17 @@ "publishConfig": { | ||
"dependencies": { | ||
"@api-components/amf-helper-mixin": "^4.5.17", | ||
"classnames": "^2.2.6", | ||
"kagekiri": "^1.4.1", | ||
"lodash.orderby": "^4.6.0", | ||
"lodash.uniqby": "^4.7.0", | ||
"query-string": "^7.1.1", | ||
"sentence-case": "^3.0.4" | ||
"@api-components/amf-helper-mixin": "4.5.29", | ||
"classnames": "2.5.1", | ||
"dompurify": "3.2.4", | ||
"kagekiri": "1.4.2", | ||
"lodash.orderby": "4.6.0", | ||
"lodash.uniqby": "4.7.0", | ||
"query-string": "7.1.3", | ||
"sentence-case": "3.0.4" | ||
}, | ||
"devDependencies": { | ||
"@types/classnames": "^2.2.10", | ||
"@types/lodash.orderby": "^4.6.7", | ||
"@types/lodash.uniqby": "^4.7.7" | ||
"@types/classnames": "2.3.1", | ||
"@types/lodash.orderby": "4.6.9", | ||
"@types/lodash.uniqby": "4.7.9" | ||
}, | ||
"gitHead": "4629fdd9ca18a13480044ad43515b91945d16aad" | ||
} |
@@ -5,4 +5,5 @@ import { LightningElement, api, track } from "lwc"; | ||
import qs from "query-string"; | ||
import { AmfModelParser } from "./utils"; | ||
import { normalizeBoolean } from "dxUtils/normalizers"; | ||
import { AmfModelParser } from "doc/amfModelParser"; | ||
import { normalizeBoolean, toJson } from "dxUtils/normalizers"; | ||
import type { OptionWithLink } from "typings/custom"; | ||
import type { | ||
@@ -29,15 +30,31 @@ AmfConfig, | ||
} from "./constants"; | ||
import { restoreScroll } from "dx/scrollManager"; | ||
import { DocPhaseInfo } from "typings/custom"; | ||
import { logCoveoPageView, oldVersionDocInfo } from "docUtils/utils"; | ||
type NavigationItem = { | ||
label: string; | ||
name: string; | ||
isExpanded: boolean; | ||
children: ParsedMarkdownTopic[]; | ||
isChildrenLoading: boolean; | ||
}; | ||
export default class AmfReference extends LightningElement { | ||
@api breadcrumbs?: string | null | undefined = null; | ||
@api breadcrumbs: string | null = null; | ||
@api sidebarHeader!: string; | ||
@api coveoOrganizationId!: string; | ||
@api coveoPublicAccessToken!: string; | ||
@api coveoAdvancedQueryConfig!: string; | ||
@api coveoAnalyticsToken!: string; | ||
@api coveoSearchHub!: string; | ||
@api useOldSidebar?: boolean = false; | ||
@api useOldSidebar: boolean = false; | ||
@api tocTitle?: string; | ||
@api tocOptions?: string; | ||
@track navigation = []; | ||
@api languages!: OptionWithLink[]; | ||
@api language!: string; | ||
@api hideFooter = false; | ||
@track navigation = [] as NavigationItem[]; | ||
@track versions: Array<ReferenceVersion> = []; | ||
@track showVersionBanner = false; | ||
@track _coveoAdvancedQueryConfig!: { [key: string]: any }; | ||
@@ -106,2 +123,12 @@ // Update this to update what component gets rendered in the content block | ||
this.selectedVersion = selectedVersion; | ||
if (this.isSpecBasedReference(this._currentReferenceId)) { | ||
this.isVersionFetched = true; | ||
if (this.oldVersionInfo) { | ||
this.showVersionBanner = true; | ||
} else { | ||
this.latestVersion = true; | ||
} | ||
} | ||
} else { | ||
this.isVersionFetched = true; | ||
} | ||
@@ -119,4 +146,4 @@ | ||
@api | ||
get docPhaseInfo() { | ||
return this.selectedReferenceDocPhase; | ||
get docPhaseInfo(): string | null { | ||
return this.selectedReferenceDocPhase || null; | ||
} | ||
@@ -140,2 +167,31 @@ | ||
/* | ||
* The get coveoAdvancedQueryConfig() method returns this._coveoAdvancedQueryConfig, | ||
* but before returning it, it checks if there are multiple versions (this.versions.length > 1) | ||
* and if a version is selected (this.selectedVersion). If both conditions are met, | ||
* it updates the version property of this._coveoAdvancedQueryConfig with the selected version. | ||
*/ | ||
@api | ||
get coveoAdvancedQueryConfig(): { [key: string]: any } { | ||
const coveoConfig = this._coveoAdvancedQueryConfig; | ||
if (this.versions.length > 1 && this.selectedVersion) { | ||
const currentGAVersionRef = this.versions[0]; | ||
if (this.selectedVersion.id !== currentGAVersionRef.id) { | ||
// Currently Coveo only supports query without "v" | ||
const version = this.selectedVersion.id.replace("v", ""); | ||
coveoConfig.version = version; | ||
this._coveoAdvancedQueryConfig = coveoConfig; | ||
} | ||
} | ||
return this._coveoAdvancedQueryConfig; | ||
} | ||
set coveoAdvancedQueryConfig(config) { | ||
this._coveoAdvancedQueryConfig = toJson(config); | ||
} | ||
private get enableFooter(): boolean { | ||
return !this.hideFooter; | ||
} | ||
// model | ||
@@ -146,10 +202,9 @@ protected _amfConfigList: AmfConfig[] = []; | ||
protected _currentReferenceId = ""; | ||
protected _scrollInterval = 0; | ||
protected parentReferenceUrls = []; | ||
protected parentReferenceUrls = [] as string[]; | ||
protected amfMap: Record<string, AmfModelRecord> = {}; | ||
protected amfFetchPromiseMap = {}; | ||
protected amfFetchPromiseMap = {} as any; | ||
protected metadata: { [key: string]: AmfMetadataTopic } = {}; | ||
protected selectedTopic?: AmfMetaTopicType = undefined; | ||
protected selectedSidebarValue = undefined; | ||
protected selectedSidebarValue: string | undefined = undefined; | ||
@@ -163,2 +218,4 @@ protected selectedVersion: ReferenceVersion | null = null; | ||
private _expandChildren?: boolean = false; | ||
private isVersionFetched = false; | ||
private latestVersion = false; | ||
@@ -192,5 +249,2 @@ /** | ||
); | ||
this._scrollInterval = window.setInterval(() => { | ||
this.saveScroll(); | ||
}, 1000); | ||
} | ||
@@ -207,18 +261,4 @@ | ||
); | ||
window.clearInterval(this._scrollInterval); | ||
} | ||
saveScroll() { | ||
window.history.replaceState( | ||
{ scrollValue: document.body.scrollTop }, | ||
"", | ||
window.location.href | ||
); | ||
} | ||
restoreScroll() { | ||
document.body.scrollTop = document.documentElement.scrollTop = | ||
window.history.state?.scrollValue; | ||
} | ||
renderedCallback(): void { | ||
@@ -311,2 +351,13 @@ if (!this.hasRendered) { | ||
private get oldVersionInfo(): DocPhaseInfo | null { | ||
let info = null; | ||
if (this.versions.length > 1 && this.selectedVersion) { | ||
const currentGAVersionRef = this.versions[0]; | ||
if (this.selectedVersion.id !== currentGAVersionRef.id) { | ||
info = oldVersionDocInfo(currentGAVersionRef.link.href); | ||
} | ||
} | ||
return info; | ||
} | ||
/** | ||
@@ -342,3 +393,3 @@ * @returns versions to be shown in the dropdown | ||
*/ | ||
private getSelectedVersion(): ReferenceVersion { | ||
private getSelectedVersion(): ReferenceVersion | null { | ||
const versions = this._referenceSetConfig?.versions || []; | ||
@@ -349,3 +400,3 @@ const selectedVersion = versions.find( | ||
// return a selected version if there is one, else return the first one. | ||
return selectedVersion || (versions.length && versions[0]); | ||
return selectedVersion || (versions.length && versions[0]) || null; | ||
} | ||
@@ -364,5 +415,7 @@ | ||
private async fetchAmf(amfConfig): Promise<AmfModel | AmfModel[]> { | ||
private async fetchAmf( | ||
amfConfig: AmfConfig | ||
): Promise<AmfModel | AmfModel[]> { | ||
const { amf } = amfConfig; | ||
const response = await fetch(amf, { | ||
const response = await fetch(amf!, { | ||
headers: { | ||
@@ -386,3 +439,3 @@ "Cache-Control": `max-age=86400` | ||
*/ | ||
private isParentReferencePath(urlPath: string): boolean { | ||
private isParentReferencePath(urlPath?: string | null): boolean { | ||
if (!urlPath) { | ||
@@ -418,5 +471,5 @@ return false; | ||
private populateReferenceItems(): void { | ||
const navAmfOrder = []; | ||
const navAmfOrder = [] as NavigationItem[]; | ||
for (const [index, amfConfig] of this._amfConfigList.entries()) { | ||
let navItemChildren = []; | ||
let navItemChildren = [] as ParsedMarkdownTopic[]; | ||
let isChildrenLoading = false; | ||
@@ -441,6 +494,6 @@ if (amfConfig.referenceType !== REFERENCE_TYPES.markdown) { | ||
this.expandChildrenForMarkdownReferences( | ||
amfConfig.topic.children | ||
amfConfig.topic!.children | ||
); | ||
} | ||
navItemChildren = amfConfig.topic.children; | ||
navItemChildren = amfConfig.topic!.children; | ||
} | ||
@@ -466,3 +519,5 @@ // store nav items for each spec in order | ||
private isExpandChildrenEnabled(referenceId: string): boolean { | ||
return this.expandChildren && this._currentReferenceId === referenceId; | ||
return ( | ||
!!this.expandChildren && this._currentReferenceId === referenceId | ||
); | ||
} | ||
@@ -511,3 +566,3 @@ | ||
): NavItem[] { | ||
const methodList = []; | ||
const methodList = [] as NavItem[]; | ||
@@ -517,3 +572,3 @@ items.forEach((item) => { | ||
const title = | ||
this.getTitleForLabel(method.label) || method.method; | ||
this.getTitleForLabel(method.label!) || method.method; | ||
const meta = this.addToMetadata( | ||
@@ -566,3 +621,3 @@ parentReferencePath, | ||
const children = []; | ||
const children: any[] = []; | ||
const expandChildren = this.isExpandChildrenEnabled(referenceId); | ||
@@ -594,4 +649,4 @@ | ||
if ( | ||
model[childrenPropertyName] && | ||
model[childrenPropertyName].length | ||
model[childrenPropertyName!] && | ||
model[childrenPropertyName!].length | ||
) { | ||
@@ -605,3 +660,3 @@ const amfTopicId = this.getFormattedIdentifier( | ||
referenceId, | ||
model[childrenPropertyName] | ||
model[childrenPropertyName!] | ||
); | ||
@@ -621,13 +676,15 @@ children.push({ | ||
case "type": | ||
if (model[childrenPropertyName]?.length) { | ||
if (model[childrenPropertyName!]?.length) { | ||
// Sorting the types alphabetically | ||
model[childrenPropertyName].sort((typeA, typeB) => { | ||
const typeALbl = typeA.label.toLowerCase(); | ||
const typeBLbl = typeB.label.toLowerCase(); | ||
return typeALbl < typeBLbl | ||
? -1 | ||
: typeALbl > typeBLbl | ||
? 1 | ||
: 0; | ||
}); | ||
model[childrenPropertyName!].sort( | ||
(typeA: any, typeB: any) => { | ||
const typeALbl = typeA.label.toLowerCase(); | ||
const typeBLbl = typeB.label.toLowerCase(); | ||
return typeALbl < typeBLbl | ||
? -1 | ||
: typeALbl > typeBLbl | ||
? 1 | ||
: 0; | ||
} | ||
); | ||
} | ||
@@ -637,4 +694,4 @@ // eslint-disable-next-line no-fallthrough | ||
if ( | ||
model[childrenPropertyName] && | ||
model[childrenPropertyName].length | ||
model[childrenPropertyName!] && | ||
model[childrenPropertyName!].length | ||
) { | ||
@@ -652,4 +709,4 @@ const amfTopicId = this.getFormattedIdentifier( | ||
isExpanded: expandChildren, | ||
children: model[childrenPropertyName].map( | ||
(topic) => { | ||
children: model[childrenPropertyName!].map( | ||
(topic: any) => { | ||
const meta = this.addToMetadata( | ||
@@ -691,8 +748,14 @@ parentReferencePath, | ||
navTitle: string | ||
): string | undefined { | ||
const { urlIdentifer, prefix } = URL_CONFIG[type]; | ||
): string { | ||
const config = URL_CONFIG[type as keyof typeof URL_CONFIG]; | ||
const urlIdentifer = config.urlIdentifer; | ||
let prefix = null; | ||
if ("prefix" in config) { | ||
prefix = config.prefix; | ||
} | ||
// encodeURI to avoid special characters in the URL meta. | ||
const identifier = | ||
topic[urlIdentifer] && this.encodeIdentifier(topic[urlIdentifer]); | ||
urlIdentifer in topic && | ||
this.encodeIdentifier(topic[urlIdentifer as keyof typeof topic]); | ||
let meta; | ||
@@ -713,3 +776,3 @@ // Assuming that there will be an identifier always | ||
} | ||
return meta; | ||
return meta!; | ||
} | ||
@@ -725,3 +788,3 @@ | ||
} | ||
); | ||
)!; | ||
} | ||
@@ -740,3 +803,3 @@ | ||
referenceId === metadata.referenceId && amfId === metadata.amfId | ||
); | ||
)!; | ||
} | ||
@@ -756,3 +819,3 @@ | ||
identifier === metadata.identifier | ||
); | ||
)!; | ||
} | ||
@@ -771,3 +834,3 @@ | ||
referenceId === metadata.referenceId && type === metadata.type | ||
); | ||
)!; | ||
} | ||
@@ -832,2 +895,3 @@ | ||
const encodedMeta = this.getUrlEncoded(meta); | ||
window.history.replaceState( | ||
@@ -850,5 +914,4 @@ window.history.state, | ||
if (specBasedReference) { | ||
const currentMeta: RouteMeta | null = this.getReferenceMetaInfo( | ||
window.location.href | ||
); | ||
const currentMeta: RouteMeta | undefined = | ||
this.getReferenceMetaInfo(window.location.href); | ||
const metadata = | ||
@@ -877,3 +940,3 @@ currentMeta && this.getMetadataByUrlQuery(currentMeta); | ||
this.restoreScroll(); | ||
restoreScroll(); // don't try this at home kids | ||
} | ||
@@ -886,4 +949,2 @@ | ||
protected onApiNavigationChanged(): void { | ||
this.saveScroll(); | ||
const specBasedReference = this.isSpecBasedReference( | ||
@@ -893,3 +954,3 @@ this._currentReferenceId | ||
if (specBasedReference) { | ||
const { meta } = this.selectedTopic; | ||
const { meta } = this.selectedTopic!; | ||
const metadata = this.metadata[meta]; | ||
@@ -1009,3 +1070,3 @@ if (metadata) { | ||
*/ | ||
getUrlEncoded(url: string) { | ||
getUrlEncoded(url: string): string { | ||
// if url matches, then return the encoded url. | ||
@@ -1026,3 +1087,3 @@ if (decodeURIComponent(url) === url) { | ||
*/ | ||
getReferenceMetaInfo(referenceUrl: string): RouteMeta | undefined { | ||
getReferenceMetaInfo(referenceUrl: string | null): RouteMeta | undefined { | ||
let metaReferenceInfo; | ||
@@ -1069,6 +1130,6 @@ if (referenceUrl) { | ||
const topic = topics[i]; | ||
const meta = this.getMarkdownReferenceMeta(topic.link.href); | ||
const meta = this.getMarkdownReferenceMeta(topic.link!.href); | ||
const childTopics = topic.children; | ||
if (meta === topicMeta) { | ||
referenceUrl = topic.link.href; | ||
referenceUrl = topic.link!.href; | ||
topicTitle = topic.label; | ||
@@ -1176,2 +1237,3 @@ } else if (childTopics && childTopics.length) { | ||
); | ||
this.updateUrlWithSelected( | ||
@@ -1181,7 +1243,7 @@ selectedItemMetaData.parentReferencePath, | ||
); | ||
this.updateNavTitleMetaTag(selectedItemMetaData.navTitle); | ||
this.updateTags(selectedItemMetaData.navTitle); | ||
} | ||
}); | ||
} else { | ||
let invalidTopicReferenceUrl = ""; | ||
let invalidTopicReferenceUrl: string | null = ""; | ||
if (topicId) { | ||
@@ -1220,3 +1282,6 @@ const referenceDetails = this.getRefDetailsForGivenTopicMeta( | ||
private loadMarkdownBasedReference(referenceUrl?: string): void { | ||
private loadMarkdownBasedReference(referenceUrl?: string | null): void { | ||
// MILES TODO: figure out if we ever need to log a coveo page view in here | ||
// this would be the case if at some point we 'load' a new 'markdown based reference' | ||
// without actually triggering a page load | ||
let referenceId = ""; | ||
@@ -1235,3 +1300,3 @@ const currentUrl = window.location.href; | ||
*/ | ||
referenceId = this.getReferenceIdFromUrl(referenceUrl); | ||
referenceId = this.getReferenceIdFromUrl(referenceUrl!); | ||
} else if (this.isParentReferencePath(currentUrl)) { | ||
@@ -1265,5 +1330,5 @@ /** | ||
if (amfConfig) { | ||
const childrenItems = amfConfig.topic.children; | ||
const childrenItems = amfConfig.topic!.children; | ||
if (childrenItems.length > 0) { | ||
redirectReferenceUrl = childrenItems[0].link.href; | ||
redirectReferenceUrl = childrenItems[0].link!.href; | ||
} | ||
@@ -1297,6 +1362,13 @@ } | ||
if (referenceDetails) { | ||
this.updateNavTitleMetaTag(referenceDetails.topicTitle); | ||
this.updateTags(referenceDetails.topicTitle); | ||
} | ||
this.versions = this.getVersions(); | ||
if (this.oldVersionInfo) { | ||
this.showVersionBanner = true; | ||
} else { | ||
this.latestVersion = true; | ||
} | ||
this.isVersionFetched = true; | ||
this.updateDocPhase(); | ||
@@ -1318,14 +1390,62 @@ this.selectedSidebarValue = window.location.pathname; | ||
private updateNavTitleMetaTag(navTitle = ""): void { | ||
handleDismissVersionBanner() { | ||
this.showVersionBanner = false; | ||
} | ||
private updateTags(navTitle = ""): void { | ||
if (!navTitle) { | ||
return; | ||
} | ||
// this is required to update the nav title meta tag. | ||
// eslint-disable-next-line @lwc/lwc/no-document-query | ||
const metaNavTitle = document.querySelector('meta[name="nav-title"]'); | ||
if (metaNavTitle && navTitle) { | ||
// eslint-disable-next-line @lwc/lwc/no-document-query | ||
const titleTag = document.querySelector("title"); | ||
const TITLE_SEPARATOR = " | "; | ||
if (metaNavTitle) { | ||
metaNavTitle.setAttribute("content", navTitle); | ||
} | ||
/** | ||
* Right now, the title tag only changes when you pick a Ref spec, | ||
* not every time you choose a subsection of the Ref spec. | ||
* This update aims to refresh the title tag with each selection. | ||
* If a Ref spec is chosen, we add the value of the <selected topic> to the title. | ||
* If a subsection is selected, we update the first part of the current | ||
* title with the new <selected topic>. | ||
* Example: Following is a sample project structure. | ||
* - Project Name | ||
* - Ref Spec1 | ||
* - Summary | ||
* - Endpoints | ||
* - E1 | ||
* - E2 | ||
* - Ref Spec2 | ||
* - Summary | ||
* - Endpoints | ||
* - E1 (Selected) | ||
* - E2 | ||
* Previous Title: Ref Spec2 | Project Name | Salesforce Developer | ||
* New Title: E1 | Ref Spec2 | Project Name | Salesforce Developer | ||
* | ||
*/ | ||
if (titleTag) { | ||
let titleTagValue = titleTag.textContent; | ||
const titleTagSectionValues: string[] = | ||
titleTagValue?.split(TITLE_SEPARATOR); | ||
if (titleTagSectionValues) { | ||
if (titleTagSectionValues.length <= 3) { | ||
titleTagValue = navTitle + TITLE_SEPARATOR + titleTagValue; | ||
} else { | ||
titleTagSectionValues[0] = navTitle; | ||
titleTagValue = titleTagSectionValues.join(TITLE_SEPARATOR); | ||
} | ||
} | ||
titleTag.textContent = titleTagValue; | ||
} | ||
} | ||
onNavSelect(event: CustomEvent): void { | ||
this.saveScroll(); | ||
const name = event.detail.name; | ||
@@ -1364,4 +1484,9 @@ if (name) { | ||
); | ||
logCoveoPageView( | ||
this.coveoOrganizationId, | ||
this.coveoAnalyticsToken | ||
); | ||
this.updateUrlWithSelected(parentReferencePath, metaVal); | ||
this.updateNavTitleMetaTag(metadata.navTitle); | ||
this.updateTags(metadata.navTitle); | ||
} else { | ||
@@ -1398,8 +1523,16 @@ if (this.isParentReferencePath(name)) { | ||
// update topic view | ||
const { referenceId, amfId, type } = this.selectedTopic; | ||
const { referenceId, amfId, type } = this.selectedTopic!; | ||
// Adding stringify inside try/catch | ||
let amfModelString = ""; | ||
try { | ||
amfModelString = JSON.stringify(this.amfMap[referenceId].model); | ||
} catch (error) { | ||
console.error(`Error stringifying amf model: ${error}`); | ||
} | ||
// This updates the component in the content section. | ||
this.topicModel = { | ||
type, | ||
amf: this.amfMap[referenceId].model, | ||
amf: amfModelString, | ||
parser: this.amfMap[referenceId].parser, | ||
@@ -1406,0 +1539,0 @@ id: amfId |
@@ -1,2 +0,2 @@ | ||
import { Json } from "typings/custom"; | ||
import { Json, DocPhaseInfo } from "typings/custom"; | ||
@@ -24,3 +24,2 @@ export interface AmfTopicType { | ||
label: string; | ||
// TODO: Better type here | ||
children?: ({ | ||
@@ -51,10 +50,2 @@ id: string; | ||
export type DocPhase = "pilot" | "dev-preview" | "beta"; | ||
export type DocPhaseEntry = { | ||
phase: DocPhase; | ||
title: string; | ||
body: string; | ||
}; | ||
export type ReferenceType = "markdown" | "rest-raml" | "rest-oa2" | "rest-oa3"; | ||
@@ -79,3 +70,3 @@ | ||
version?: string; | ||
docPhase?: DocPhaseEntry; | ||
docPhase?: DocPhaseInfo; | ||
title: string; | ||
@@ -110,3 +101,3 @@ href: string; | ||
type: string; | ||
amf: AmfModel; | ||
amf: string; | ||
parser: AmfParser; | ||
@@ -113,0 +104,0 @@ } |
@@ -11,10 +11,13 @@ import { LightningElement, api } from "lwc"; | ||
import type { TopicModel } from "./types"; | ||
import { Json } from "typings/custom"; | ||
const TABLE_SIZE_MATCH = "769px"; | ||
export default class AmfTopic extends LightningElement { | ||
private _model; | ||
private amf; | ||
private type; | ||
private _model: TopicModel | undefined; | ||
private amf: Json; | ||
private type: string | undefined; | ||
@api | ||
get model(): TopicModel { | ||
get model(): TopicModel | undefined { | ||
return this._model; | ||
@@ -28,3 +31,7 @@ } | ||
) { | ||
this.amf = value && clone(value.amf); | ||
try { | ||
this.amf = value && JSON.parse(value.amf); | ||
} catch (error) { | ||
console.error(`Error parsing amf model: ${error}`); | ||
} | ||
} | ||
@@ -35,3 +42,3 @@ if ( | ||
) { | ||
this.type = value && clone(value.type); | ||
this.type = value && value.type; | ||
} | ||
@@ -47,3 +54,7 @@ | ||
update(): void { | ||
const container = this.template.querySelector("div.topic-container"); | ||
if (!this.model) { | ||
throw new Error("Amf TopicModel undefined when trying to update"); | ||
} | ||
const container = this.template.querySelector(".topic-container")!; | ||
const { id } = this.model; | ||
@@ -83,17 +94,23 @@ const type = this.type; | ||
if (container.firstChild) { | ||
if (container?.firstChild) { | ||
container.firstChild.remove(); | ||
} | ||
container.appendChild(element); | ||
container?.appendChild(element as Node); | ||
const isTabletOrDesktop = window.matchMedia( | ||
`(min-width: ${TABLE_SIZE_MATCH})` | ||
).matches; | ||
if (isTabletOrDesktop) { | ||
window.scrollTo(0, 0); | ||
} | ||
} | ||
} | ||
/** | ||
* The underlying web components we use from api-console mutate their models we pass in. | ||
* Since LWC makes them Read Only, we need to copy them before passing to the Web Component. | ||
* @param value JSON Serializable object to clone. | ||
* @returns A copy of Value. One that has been serialized and parsed via JSON. (Functions, Regex, etc are not preserved.) | ||
*/ | ||
function clone(value): object { | ||
return JSON.parse(JSON.stringify(value)); | ||
renderedCallback(): void { | ||
try { | ||
this.update(); | ||
} catch (error) { | ||
console.error(error); | ||
} | ||
} | ||
} |
import { Json } from "typings/custom"; | ||
import { AmfModelParser } from "doc/amfModelParser"; | ||
import { | ||
DomainElement, | ||
EndPoint, | ||
Operation, | ||
Shape | ||
} from "@api-components/amf-helper-mixin"; | ||
@@ -14,3 +21,3 @@ export type ApiSummaryElement = HTMLElement & { | ||
selected: string; | ||
endpoint: Json; | ||
endpoint: EndPoint; | ||
noTryIt: boolean; | ||
@@ -22,4 +29,4 @@ }; | ||
noNavigation: boolean; | ||
endpoint: Json; | ||
method: Json; | ||
endpoint: EndPoint | undefined; | ||
method: Operation | undefined; | ||
noTryIt: boolean; | ||
@@ -30,3 +37,3 @@ }; | ||
amf: Json; | ||
security: Json; | ||
security: DomainElement | undefined; | ||
}; | ||
@@ -36,3 +43,3 @@ | ||
amf: Json; | ||
type: Json; | ||
type?: Shape; | ||
mediaTypes: Json; | ||
@@ -43,3 +50,3 @@ }; | ||
amf: Json; | ||
shape: Json; | ||
shape: DomainElement | undefined; | ||
}; | ||
@@ -49,12 +56,7 @@ | ||
export interface AmfParser { | ||
parse(): void; | ||
parsedModel: any; | ||
} | ||
export interface TopicModel { | ||
id: string; | ||
type: string; | ||
amf: AmfModel; | ||
parser: AmfParser; | ||
amf: string; | ||
parser: AmfModelParser; | ||
} |
@@ -0,1 +1,7 @@ | ||
import { | ||
DomainElement, | ||
EndPoint, | ||
Operation, | ||
Shape | ||
} from "@api-components/amf-helper-mixin"; | ||
import type { | ||
@@ -40,3 +46,3 @@ ApiDocElement, | ||
amf: Json, | ||
endpointModel: Json, | ||
endpointModel: EndPoint, | ||
selected: string | ||
@@ -61,4 +67,4 @@ ): HTMLElement { | ||
amf: Json, | ||
endpointMethodModel: Json, | ||
methodModel: Json | ||
endpointMethodModel: EndPoint | undefined, | ||
methodModel: Operation | undefined | ||
): HTMLElement { | ||
@@ -84,3 +90,3 @@ const el: ApiMethodElement = document.createElement( | ||
amf: Json, | ||
securityModel: Json | ||
securityModel: DomainElement | undefined | ||
): HTMLElement { | ||
@@ -104,3 +110,3 @@ const el: ApiSecurityElement = document.createElement( | ||
amf: Json, | ||
typeModel: Json, | ||
typeModel: Shape | undefined, | ||
mediaTypes: Json | ||
@@ -126,3 +132,3 @@ ): HTMLElement { | ||
amf: Json, | ||
docsModel: Json | ||
docsModel: DomainElement | undefined | ||
): HTMLElement { | ||
@@ -129,0 +135,0 @@ const el: ApiDocElement = document.createElement( |
import { track } from "dxUtils/analytics"; | ||
import { LightningElement, api } from "lwc"; | ||
import { AnalyticsPayload, BreadcrumbItemVariant } from "typings/custom"; | ||
import { BreadcrumbItemVariant } from "typings/custom"; | ||
@@ -11,4 +11,4 @@ const BREADCRUMB_LONG = "breadcrumb_long"; | ||
@api href?: string; | ||
@api analyticsEvent!: string; | ||
@api analyticsBasePayload!: AnalyticsPayload; | ||
@api level?: string; | ||
@api breadcrumbLabels?: string; | ||
@@ -55,12 +55,19 @@ @api | ||
private onLinkClick(event: Event): void { | ||
if (!this.analyticsEvent) { | ||
return; | ||
} | ||
track(event.target!, "custEv_breadcrumbClick", { | ||
click_text: this.label, | ||
click_url: `${window.location.origin}${this.href}`, | ||
element_type: "link", | ||
nav_type: "breadcrumb", | ||
nav_level: this.level ? this.level + 1 : 1, | ||
nav_item: this.breadcrumbLabels | ||
}); | ||
track(event.target!, this.analyticsEvent, { | ||
...this.analyticsBasePayload, | ||
clickText: this.label, | ||
clickUrl: this.href | ||
track(event.target!, "custEv_linkClick", { | ||
click_text: this.label, | ||
click_url: `${window.location.origin}${this.href}`, | ||
element_title: this.label, | ||
element_type: "link", | ||
content_category: "cta" | ||
}); | ||
} | ||
} |
@@ -20,9 +20,2 @@ import { LightningElement, api } from "lwc"; | ||
export const ANALYTICS_EVENT_NAME = "custEv_breadcrumbClick"; | ||
export const ANALYTICS_BASE_PAYLOAD = { | ||
elementType: "breadcrumb", | ||
locationOnPage: "breadcrumb", | ||
ctaClick: true | ||
}; | ||
export default class Breadcrumbs extends LightningElement { | ||
@@ -39,4 +32,4 @@ @api ariaLabel: string = "Documentation Breadcrumbs"; | ||
this.calculateBreadcrumbsConfigs(); | ||
if (this.observer) { | ||
this.updateDropdownOptionAmount(); | ||
if (this.isRendered) { | ||
this.onWidthOrContentChange(); | ||
} | ||
@@ -57,5 +50,5 @@ } | ||
private navWidth = 0; | ||
private observer: ResizeObserver | null = null; | ||
private breadcrumbConfigs: BreadcrumbConfig[] = []; | ||
private dropdownOptionAmount? = 0; | ||
private isRendered = false; | ||
@@ -106,2 +99,6 @@ private get renderSmallVariant(): boolean { | ||
private get breadcrumbLabels(): string { | ||
return this.breadcrumbs.map((crumb) => crumb.label).join(":"); | ||
} | ||
private get lastCrumb(): Breadcrumb { | ||
@@ -111,30 +108,26 @@ return this.breadcrumbs[this.breadcrumbs.length - 1]; | ||
private get analyticsEventName() { | ||
return ANALYTICS_EVENT_NAME; | ||
} | ||
private get analyticsBasePayload() { | ||
// this payload is only used for breadcrumb dropdown | ||
private get ANALYTICS_PAYLOAD() { | ||
return { | ||
...ANALYTICS_BASE_PAYLOAD, | ||
itemTitle: this.breadcrumbs.map((crumb) => crumb.label).join("/") | ||
element_type: "link", | ||
nav_type: "breadcrumb", | ||
nav_level: 1 | ||
}; | ||
} | ||
private onWidthOrContentChange = () => { | ||
this.navWidth = this.template | ||
.querySelector("nav")! | ||
.getBoundingClientRect().width; | ||
this.updateDropdownOptionAmount(); | ||
}; | ||
renderedCallback(): void { | ||
if (!this.observer) { | ||
this.observer = new ResizeObserver((entries) => { | ||
const [nav] = entries; | ||
if (this.navWidth === nav.contentRect.width) { | ||
return; | ||
} | ||
this.navWidth = nav.contentRect.width; | ||
this.updateDropdownOptionAmount(); | ||
}); | ||
this.observer.observe(this.template.querySelector("nav")!); | ||
} | ||
this.isRendered = true; | ||
this.onWidthOrContentChange(); | ||
window.addEventListener("resize", this.onWidthOrContentChange); | ||
} | ||
disconnectedCallback(): void { | ||
this.observer?.disconnect(); | ||
window.removeEventListener("resize", this.onWidthOrContentChange); | ||
} | ||
@@ -147,6 +140,9 @@ | ||
this._breadcrumbs = toJson(breadcrumbs).map((crumb: Breadcrumb) => ({ | ||
...crumb, | ||
id: crumb.id || crumb.href | ||
})); | ||
this._breadcrumbs = toJson(breadcrumbs).map( | ||
(crumb: Breadcrumb, index: number) => ({ | ||
...crumb, | ||
id: crumb.id || crumb.href, | ||
level: index | ||
}) | ||
); | ||
} | ||
@@ -153,0 +149,0 @@ |
/* eslint-disable @lwc/lwc/no-inner-html */ | ||
import { createElement, LightningElement, api, track } from "lwc"; | ||
import { DocContent, PageReference } from "typings/custom"; | ||
import ContentCallout from "doc/contentCallout"; | ||
import CodeBlock from "dx/codeBlock"; | ||
import ContentMedia from "doc/contentMedia"; | ||
import Button from "dx/button"; | ||
import { highlightTerms } from "dxUtils/highlight"; | ||
import ContentCallout from "doc/contentCallout"; | ||
import ContentMedia from "doc/contentMedia"; | ||
@@ -46,22 +46,6 @@ const HIGHLIGHTABLE_SELECTOR = [ | ||
@api | ||
set codeBlockTheme(value) { | ||
this._codeBlockTheme = value; | ||
} | ||
get codeBlockTheme() { | ||
return this._codeBlockTheme; | ||
} | ||
@track docContent: DocContent = ""; | ||
_codeBlockTheme: string = "dark"; | ||
_docRendered: boolean = false; | ||
originalCodeBlockThemeValue: String = ""; | ||
connectedCallback() { | ||
this.template.addEventListener( | ||
"themechange", | ||
this.updateTheme.bind(this) // eslint-disableline no-use-before-define | ||
); | ||
window.addEventListener( | ||
@@ -80,13 +64,4 @@ "highlightedtermchange", | ||
updateTheme() { | ||
this._codeBlockTheme = | ||
this._codeBlockTheme === "dark" ? "light" : "dark"; | ||
const codeBlockEls = this.template.querySelectorAll("dx-code-block"); | ||
codeBlockEls.forEach((codeBlockEl) => { | ||
codeBlockEl.setAttribute("theme", this._codeBlockTheme); | ||
}); | ||
} | ||
renderPaginationButton(anchorEl: HTMLElement) { | ||
const isNext = anchorEl.textContent.includes("Next →"); | ||
const isNext = anchorEl.textContent!.includes("Next →"); | ||
anchorEl.innerHTML = ""; | ||
@@ -121,7 +96,7 @@ const buttonEl = createElement("dx-button", { is: Button }); | ||
codeBlockEl.setAttribute("lwc:dom", "manual"); | ||
const classList = codeBlockEl.firstChild.classList; | ||
const classList = codeBlockEl.firstElementChild?.classList; | ||
let language = ""; | ||
for (const key in classList) { | ||
if (typeof classList[key] === "string") { | ||
const className = classList[key]; | ||
if (classList) { | ||
for (let i = 0; i < classList.length; i++) { | ||
const className = classList[i]; | ||
if (className.startsWith("brush:")) { | ||
@@ -139,4 +114,3 @@ language = className.split(":")[1]; | ||
language: LANGUAGE_MAP[language] || language, | ||
theme: this.codeBlockTheme, | ||
title: "", // Default no title. | ||
header: "", // Default no title. | ||
variant: this.codeBlockType, | ||
@@ -165,5 +139,6 @@ isEncoded: true | ||
// Set a flag to 1 (true) if and only if all detailEls have no content. | ||
let flag = 1; | ||
for (let i: number = 0; i < detailEls.length; i++) { | ||
flag &= detailEls[i].innerHTML.trim() === ""; | ||
flag &= (detailEls[i].innerHTML.trim() === "") as any; // Dark Magic TM | ||
} | ||
@@ -178,6 +153,6 @@ | ||
const type = calloutEl.querySelector("h4").textContent; | ||
const type = calloutEl.querySelector("h4")!.textContent!; | ||
const typeLower = type.toLowerCase(); | ||
Object.assign(calloutCompEl, { | ||
title: type, | ||
header: type, | ||
variant: typeLower | ||
@@ -193,6 +168,6 @@ }); | ||
anchorEls.forEach((anchorEl) => { | ||
anchorEls.forEach((anchorEl: any) => { | ||
if ( | ||
anchorEl.textContent.includes("Next →") || | ||
anchorEl.textContent.includes("← Previous") | ||
anchorEl.textContent!.includes("Next →") || | ||
anchorEl.textContent!.includes("← Previous") | ||
) { | ||
@@ -263,2 +238,3 @@ if (this.showPaginationButtons) { | ||
const height = mediaEl.getAttribute("height"); | ||
const className = mediaEl.getAttribute("class"); | ||
@@ -275,2 +251,3 @@ if (isImage) { | ||
img.src = src; | ||
img.alt = ""; | ||
if (alt) { | ||
@@ -288,4 +265,7 @@ img.alt = alt; | ||
} | ||
if (className) { | ||
img.className = className; | ||
} | ||
img.className = "content-image"; | ||
img.className = `content-image ${img.className}`; | ||
mediaEl.parentNode!.insertBefore(img, mediaEl); | ||
@@ -334,4 +314,3 @@ } else { | ||
event.preventDefault(); | ||
// eslint-disable-next-line no-use-before-define | ||
const target = event.currentTarget.dataset.id; | ||
const target = (event.currentTarget! as any).dataset.id; | ||
const [page, docId, deliverable, tempContentDocumentId] = | ||
@@ -369,3 +348,3 @@ target.split("/"); | ||
@api | ||
public navigateToHash(hash: String) { | ||
public navigateToHash = (hash: String) => { | ||
const splitHash = hash.split("#"); | ||
@@ -381,3 +360,3 @@ if (splitHash.length === 2) { | ||
} | ||
} | ||
}; | ||
@@ -384,0 +363,0 @@ renderedCallback() { |
@@ -6,3 +6,3 @@ import { LightningElement, api } from "lwc"; | ||
export default class ContentCallout extends LightningElement { | ||
@api title!: string; | ||
@api header!: string; | ||
@api variant!: CalloutVariant; | ||
@@ -15,2 +15,5 @@ cardVariant?: string; | ||
switch (this.variant) { | ||
case "plain": | ||
this.cardVariant = "dx-callout-plain"; | ||
break; | ||
case "tip": | ||
@@ -54,2 +57,6 @@ this.cardVariant = "dx-callout-tip"; | ||
get hideIcon() { | ||
return this.variant === "plain"; | ||
} | ||
private isSlotEmpty: boolean = true; | ||
@@ -56,0 +63,0 @@ private onSlotChange(e: LightningSlotElement) { |
@@ -0,1 +1,2 @@ | ||
/* eslint-disable @lwc/lwc/no-document-query */ | ||
import { LightningElement, api, track } from "lwc"; | ||
@@ -5,7 +6,11 @@ import { closest } from "kagekiri"; | ||
import { highlightTerms } from "dxUtils/highlight"; | ||
import { SearchSyncer } from "docUtils/SearchSyncer"; | ||
import { SearchSyncer } from "docUtils/searchSyncer"; | ||
import type { OptionWithLink } from "typings/custom"; | ||
type AnchorMap = { [key: string]: { intersect: boolean; id: string } }; | ||
const TOC_HEADER_TAG = "DOC-HEADING"; | ||
declare const Sprig: (eventType: string, eventNme: string) => void; | ||
const TOC_HEADER_TAG = "doc-heading"; | ||
const HIGHLIGHTABLE_SELECTOR = [ | ||
@@ -24,15 +29,25 @@ "p", | ||
].join(","); | ||
const OBSERVER_ATTACH_WAIT_TIME = 500; | ||
export const OBSERVER_ATTACH_WAIT_TIME = 500; | ||
export default class ContentLayout extends LightningElement { | ||
@api sidebarValue: string; | ||
@api sidebarHeader: string; | ||
@api tocTitle: string; | ||
@api sidebarValue!: string; | ||
@api sidebarHeader!: string; | ||
@api tocTitle!: string; | ||
@api enableSlotChange = false; | ||
@api coveoOrganizationId!: string; | ||
@api coveoPublicAccessToken!: string; | ||
@api coveoAnalyticsToken!: string; | ||
@api coveoSearchHub!: string; | ||
@api coveoAdvancedQueryConfig!: string; | ||
@api useOldSidebar?: boolean = false; | ||
@api languages!: OptionWithLink[]; | ||
@api language!: string; | ||
@api bailHref!: string; | ||
@api bailLabel!: string; | ||
@api devCenter: any; | ||
@api brand: any; | ||
// This is needed for now to prevent failing snapshot tests due to links in the footer | ||
@api showFooter = false; | ||
@api | ||
@@ -43,3 +58,3 @@ get breadcrumbs() { | ||
set breadcrumbs(value): [] { | ||
set breadcrumbs(value) { | ||
if (value) { | ||
@@ -55,3 +70,3 @@ this._breadcrumbs = toJson(value); | ||
set sidebarContent(value) { | ||
set sidebarContent(value: any) { | ||
this._sidebarContent = toJson(value); | ||
@@ -71,23 +86,29 @@ } | ||
setSidebarInputValue(searchTerm: string): void { | ||
this.template.querySelector("dx-sidebar")?.setInputValue(searchTerm); | ||
(this.template.querySelector("dx-sidebar") as any)?.setInputValue( | ||
searchTerm | ||
); | ||
} | ||
@track | ||
private _sidebarContent: unknown; | ||
protected _sidebarContent: unknown; | ||
private _breadcrumbs = null; | ||
protected _breadcrumbs = null; | ||
@track | ||
private _tocOptions: Array<unknown>; | ||
protected _tocOptions!: Array<unknown>; | ||
private anchoredElements: AnchorMap = {}; | ||
private lastScrollPosition: number; | ||
private observer?: IntersectionObserver; | ||
private hasRendered: boolean = false; | ||
protected tocOptionIdsSet = new Set(); | ||
protected anchoredElements: AnchorMap = {}; | ||
protected lastScrollPosition!: number; | ||
protected observer?: IntersectionObserver; | ||
protected hasRendered: boolean = false; | ||
protected contentLoaded: boolean = false; | ||
protected sidebarOpen: boolean = false; | ||
private searchSyncer = new SearchSyncer({ | ||
get shouldDisplayFeedback() { | ||
return this.contentLoaded && typeof Sprig !== "undefined"; | ||
} | ||
protected searchSyncer = new SearchSyncer({ | ||
callbacks: { | ||
onUrlChange: (nextSearchString: string): void => { | ||
this.updateHighlightsAndSearch(nextSearchString); | ||
}, | ||
onSearchChange: (nextSearchString: string): void => { | ||
@@ -105,6 +126,7 @@ this.dispatchHighlightChange( | ||
}); | ||
private tocValue?: string = undefined; | ||
private observerTimerId = null; | ||
private didScrollToSelectedHash = false; | ||
private _scrollInterval = 0; | ||
protected tocValue?: string = undefined; | ||
// eslint-disable-next-line no-undef | ||
protected observerTimerId?: NodeJS.Timeout; | ||
protected didScrollToSelectedHash = false; | ||
protected _scrollInterval = 0; | ||
@@ -126,6 +148,2 @@ get showToc(): boolean { | ||
get docContentStyle(): string { | ||
return this.showBreadcrumbs ? "" : "margin-top: 48px"; | ||
} | ||
connectedCallback(): void { | ||
@@ -143,8 +161,7 @@ const hasParentHighlightListener = closest( | ||
} | ||
this._scrollInterval = window.setInterval(() => { | ||
this.saveScroll(); | ||
}, 1000); | ||
} | ||
// Placeholder for childs renderedCallback | ||
protected postRenderedCallback?(): void; | ||
renderedCallback(): void { | ||
@@ -160,5 +177,13 @@ /** | ||
); | ||
this.adjustNavPosition(); | ||
window.addEventListener("scroll", this.adjustNavPosition); | ||
window.addEventListener("resize", this.adjustNavPosition); | ||
if (!this.hasRendered) { | ||
this.hasRendered = true; | ||
this.restoreScroll(); | ||
// Dynamically call `renderedCallbackForLwcContentLayout` if it exists | ||
this.postRenderedCallback?.(); | ||
} | ||
@@ -173,2 +198,4 @@ } | ||
); | ||
window.removeEventListener("scroll", this.adjustNavPosition); | ||
window.removeEventListener("resize", this.adjustNavPosition); | ||
this.searchSyncer.dispose(); | ||
@@ -180,10 +207,2 @@ this.clearRenderObserverTimer(); | ||
saveScroll() { | ||
window.history.replaceState( | ||
{ scrollValue: document.body.scrollTop }, | ||
"", | ||
window.location.href | ||
); | ||
} | ||
restoreScroll() { | ||
@@ -200,2 +219,98 @@ document.body.scrollTop = document.documentElement.scrollTop = | ||
/* | ||
This is a workaround for the global nav sticky header being decoupled from the doc header & doc phase. | ||
We have to account for the global nav changing height due to animations. | ||
*/ | ||
adjustNavPosition = () => { | ||
const sidebarType = this.useOldSidebar | ||
? "dx-sidebar-old" | ||
: "dx-sidebar"; | ||
const sidebarEl = this.template.querySelector(sidebarType); | ||
const globalNavEl = document.querySelector( | ||
"hgf-c360nav" | ||
) as HTMLElement; | ||
const contextNavEl = document.querySelector( | ||
"hgf-c360contextnav" | ||
) as HTMLElement; | ||
const docHeaderEl = document.querySelector( | ||
".sticky-doc-header" | ||
) as HTMLElement; | ||
let docPhaseEl = ( | ||
this.template.querySelector("[name=doc-phase]")! as any | ||
).assignedElements()[0] as HTMLSlotElement; | ||
if (!docPhaseEl) { | ||
docPhaseEl = ( | ||
this.template.querySelector("[name=version-banner]")! as any | ||
).assignedElements()[0] as HTMLSlotElement; | ||
} | ||
if (!sidebarEl || !globalNavEl || !contextNavEl || !docHeaderEl) { | ||
console.warn("One or more required elements are missing."); | ||
return; | ||
} | ||
// sync with the browser to account for any reflows that may have happened | ||
requestAnimationFrame(() => { | ||
// ternary is a temporary fix for the global nav height reporting incorrectly on some browsers | ||
const globalNavHeight = | ||
(globalNavEl.getBoundingClientRect().height !== 72 ? 0 : 72) + | ||
contextNavEl.getBoundingClientRect().height; | ||
const docHeaderHeight = docHeaderEl.getBoundingClientRect().height; | ||
const totalHeaderHeight = globalNavHeight + docHeaderHeight; | ||
// Selecting the doc section heading and RNB here. | ||
const docHeadingEls = Array.from( | ||
document.querySelectorAll("doc-heading") | ||
); | ||
const rightNavBarEl = this.template.querySelector(".right-nav-bar"); | ||
sidebarEl.style.setProperty( | ||
"--dx-c-content-sidebar-sticky-top", | ||
`${globalNavHeight + docHeaderHeight}px` | ||
); | ||
docHeaderEl.style.setProperty( | ||
"--dx-g-global-header-height", | ||
`${globalNavHeight}px` | ||
); | ||
// Adjusting the doc section heading on scroll. | ||
docHeadingEls.forEach((docHeadingEl) => { | ||
(docHeadingEl as any).style.scrollMarginTop = docPhaseEl | ||
? `${ | ||
totalHeaderHeight + | ||
docPhaseEl.getBoundingClientRect().height + | ||
40 | ||
}px` | ||
: `${totalHeaderHeight + 40}px`; | ||
}); | ||
// Adjusting the right nav bar on scroll. | ||
if (rightNavBarEl) { | ||
rightNavBarEl.style.top = docPhaseEl | ||
? `${ | ||
totalHeaderHeight + | ||
docPhaseEl.getBoundingClientRect().height | ||
}px` | ||
: `${totalHeaderHeight}px`; | ||
} | ||
// If doc phase element exists, we need to account for its sticky position. Mobile should include the sidebar height (since it becomes sticky aswell). | ||
if (docPhaseEl) { | ||
docPhaseEl.style.setProperty( | ||
"--doc-c-phase-top", | ||
`${ | ||
window.innerWidth < 769 | ||
? globalNavHeight + | ||
docHeaderHeight + | ||
sidebarEl.getBoundingClientRect().height | ||
: globalNavHeight + docHeaderHeight | ||
}px` | ||
); | ||
} | ||
}); | ||
}; | ||
updateHighlighted = (event: Event): void => | ||
@@ -207,22 +322,17 @@ highlightTerms( | ||
attachInteractionObserver = (): void => { | ||
if (!this.enableSlotChange) { | ||
return; | ||
} | ||
this.disconnectObserver(); | ||
this.observer = new IntersectionObserver((entries) => { | ||
entries.forEach( | ||
(entry) => | ||
(this.anchoredElements[ | ||
entry.target.getAttribute("id") | ||
].intersect = entry.isIntersecting) | ||
); | ||
this.calculateActualSection(); | ||
}); | ||
protected getHeadingElements() { | ||
// Note: We are doing document.querySelectorAll as a quick fix as we are not getting heading elements reference this.querySelectorAll | ||
const headingElements = document.querySelectorAll(TOC_HEADER_TAG); | ||
for (const headingElement of headingElements) { | ||
return headingElements; | ||
} | ||
updateHeadingForRNB(): void { | ||
const headingElements = this.getHeadingElements(); | ||
this.addObserverAndScroll(headingElements); | ||
} | ||
addObserverAndScroll(headingElements: any) { | ||
for (const headingElement of headingElements as any) { | ||
// Add headingElements to intersectionObserver for highlighting respective RNB item when user scroll | ||
const id = headingElement.getAttribute("id"); | ||
const id = headingElement.getAttribute("id")!; | ||
this.anchoredElements[id] = { | ||
@@ -232,4 +342,5 @@ id, | ||
}; | ||
this.observer.observe(headingElement); | ||
this.observer?.observe(headingElement); | ||
} | ||
if (!this.didScrollToSelectedHash) { | ||
@@ -239,40 +350,87 @@ this.didScrollToSelectedHash = true; | ||
} | ||
}; | ||
} | ||
onSlotChange(event: Event): void { | ||
const slotElements = ( | ||
event.target as HTMLSlotElement | ||
).assignedElements(); | ||
attachInteractionObserver = (): void => { | ||
if (!this.enableSlotChange) { | ||
return; | ||
} | ||
this.disconnectObserver(); | ||
if (slotElements.length) { | ||
const slotContentElement = slotElements[0]; | ||
const headingElements = | ||
slotContentElement.ownerDocument?.getElementsByTagName( | ||
TOC_HEADER_TAG | ||
const globalNavOffset = `-${getComputedStyle( | ||
document.documentElement | ||
).getPropertyValue("--dx-g-doc-header-main-nav-height")}`; | ||
this.observer = new IntersectionObserver( | ||
(entries) => { | ||
entries.forEach( | ||
(entry) => | ||
(this.anchoredElements[ | ||
entry.target.getAttribute("id")! | ||
].intersect = entry.isIntersecting) | ||
); | ||
for (const headingElement of headingElements) { | ||
// Sometimes elements hash is not being set when slot content is wrapped with div | ||
headingElement.hash = headingElement.attributes.hash?.nodeValue; | ||
this.calculateActualSection(); | ||
}, | ||
{ | ||
rootMargin: globalNavOffset.trim() | ||
} | ||
const tocOptions = []; | ||
for (const headingElement of headingElements) { | ||
headingElement.id = headingElement.hash; | ||
); | ||
this.updateHeadingForRNB(); | ||
}; | ||
// Update tocOptions from anchorTags | ||
// eslint-disable-next-line no-undef | ||
updateTocItems(headingElements: any): void { | ||
const tocOptions = []; | ||
for (const headingElement of headingElements as any) { | ||
headingElement.id = headingElement.hash; | ||
// Update tocOptions from anchorTags only for H2, consider default as 2 as per component | ||
const headingAriaLevel = | ||
headingElement.attributes["aria-level"]?.nodeValue || "2"; | ||
const isH2 = headingAriaLevel === "2"; | ||
if (isH2) { | ||
const tocItem = { | ||
anchor: `#${headingElement.hash}`, | ||
id: headingElement.id, | ||
label: headingElement.title | ||
label: headingElement.header | ||
}; | ||
tocOptions.push(tocItem); | ||
this.tocOptionIdsSet.add(headingElement.id); | ||
} | ||
} | ||
this._tocOptions = tocOptions; | ||
this._tocOptions = tocOptions; | ||
} | ||
setHashAndHeaderForDocHeading(headingElements: any) { | ||
for (const headingElement of headingElements as any) { | ||
// Sometimes elements hash and header is not being set when slot content is wrapped with div | ||
if (!headingElement.hash) { | ||
headingElement.hash = headingElement.attributes.hash?.nodeValue; | ||
} | ||
if (!headingElement.header) { | ||
headingElement.header = | ||
headingElement.attributes.header?.nodeValue; | ||
} | ||
} | ||
this.updateTocItems(headingElements); | ||
} | ||
private disconnectObserver(): void { | ||
updateRNB = () => { | ||
const headingElements = this.getHeadingElements(); | ||
this.setHashAndHeaderForDocHeading(headingElements); | ||
}; | ||
onSlotChange(): void { | ||
this.updateRNB(); | ||
this.contentLoaded = true; | ||
} | ||
protected disconnectObserver(): void { | ||
if (this.observer) { | ||
this.observer.disconnect(); | ||
this.observer = null; | ||
this.observer = undefined; | ||
} | ||
@@ -282,9 +440,33 @@ } | ||
// eslint-disable-next-line no-undef | ||
private scrollToHash(headingElements: NodeListOf<Element>): void { | ||
protected scrollToHash(headingElements: NodeListOf<Element>): void { | ||
let { hash } = window.location; | ||
if (hash) { | ||
hash = hash.substr(1); | ||
for (const headingElement of headingElements) { | ||
const docHeaderEl = document.querySelector( | ||
".sticky-doc-header" | ||
) as HTMLElement; | ||
const globalNavEl = document.querySelector( | ||
"hgf-c360nav" | ||
) as HTMLElement; | ||
const contextNavEl = document.querySelector( | ||
"hgf-c360contextnav" | ||
) as HTMLElement; | ||
const headerHeight = | ||
docHeaderEl?.offsetHeight + | ||
globalNavEl?.offsetHeight + | ||
contextNavEl?.offsetHeight; | ||
const docPhaseEl = ( | ||
this.template.querySelector("[name=doc-phase]")! as any | ||
).assignedElements()[0] as HTMLSlotElement; | ||
const offset = docPhaseEl | ||
? headerHeight + docPhaseEl.offsetHeight | ||
: headerHeight; | ||
for (const headingElement of headingElements as any) { | ||
if (headingElement.getAttribute("id") === hash) { | ||
headingElement.scrollIntoView({ behavior: "auto" }); | ||
this.scrollIntoViewWithOffset(headingElement, offset); | ||
break; | ||
@@ -296,3 +478,16 @@ } | ||
private calculateActualSection(): void { | ||
protected scrollIntoViewWithOffset( | ||
headingElement: HTMLElement, | ||
offset: number | ||
) { | ||
window.scrollTo({ | ||
behavior: "auto", | ||
top: | ||
headingElement.getBoundingClientRect().top - | ||
document.body.getBoundingClientRect().top - | ||
offset | ||
}); | ||
} | ||
protected calculateActualSection(): void { | ||
const currentScrollPosition = document.documentElement.scrollTop; | ||
@@ -312,3 +507,3 @@ const id = Object.keys(this.anchoredElements).find( | ||
private calculatePreviousElementId(): string { | ||
protected calculatePreviousElementId(): string | undefined { | ||
const keys = Object.keys(this.anchoredElements); | ||
@@ -320,7 +515,10 @@ const currentIndex = keys.findIndex((id) => this.tocValue === id); | ||
private assignElementId(id: string): void { | ||
this.tocValue = id; | ||
protected assignElementId(id: string | undefined): void { | ||
// Change toc(RNB) highlight only for H2 | ||
if (this.tocOptionIdsSet.has(id)) { | ||
this.tocValue = id; | ||
} | ||
} | ||
private dispatchHighlightChange(term: string): void { | ||
protected dispatchHighlightChange(term: string): void { | ||
this.dispatchEvent( | ||
@@ -335,3 +533,3 @@ new CustomEvent("highlightedtermchange", { | ||
private updateHighlightsAndSearch(nextSearchString: string): void { | ||
protected updateHighlightsAndSearch(nextSearchString: string): void { | ||
const nextSearchParam = | ||
@@ -342,2 +540,13 @@ new URLSearchParams(nextSearchString).get("q") || ""; | ||
} | ||
protected onToggleSidebar(e: CustomEvent): void { | ||
this.sidebarOpen = e.detail.open; | ||
// eslint-disable-next-line @lwc/lwc/no-document-query | ||
const footer = document.querySelector("dx-footer") as HTMLElement; | ||
if (footer) { | ||
footer.style.display = this.sidebarOpen ? "none" : "block"; | ||
} | ||
} | ||
} |
import { api } from "lwc"; | ||
import cx from "classnames"; | ||
import type { OptionWithNested, OptionWithLink } from "typings/custom"; | ||
import type { OptionWithNested, DevCenterConfig } from "typings/custom"; | ||
import { HeaderBase } from "dxBaseElements/headerBase"; | ||
import { toJson } from "dxUtils/normalizers"; | ||
import get from "lodash.get"; | ||
import { toJson, normalizeBoolean } from "dxUtils/normalizers"; | ||
const TABLET_MATCH = "980px"; | ||
const MOBILE_MATCH = "880px"; | ||
const SMALL_MOBILE_MATCH = "740px"; | ||
const MOBILE_MATCH = "768px"; | ||
const isStorybook = () => { | ||
const { host } = window.location; | ||
return ( | ||
host === "localhost:6006" || /^dsc-comp.*\.herokuapp\.com$/.test(host) | ||
); | ||
}; | ||
export default class Header extends HeaderBase { | ||
@@ -17,37 +22,35 @@ @api langValuePath: string = "id"; // allows to override how language property is interpreted, follows valuePath dropdown api. | ||
@api | ||
get scopedNavItems() { | ||
return this._scopedNavItems; | ||
get hideDocHeader() { | ||
return this._hideDocHeader; | ||
} | ||
set scopedNavItems(value) { | ||
this._scopedNavItems = toJson(value); | ||
set hideDocHeader(value) { | ||
this._hideDocHeader = normalizeBoolean(value); | ||
} | ||
@api | ||
get languages() { | ||
return this._languages; | ||
get scopedNavItems() { | ||
return this._scopedNavItems; | ||
} | ||
set languages(value) { | ||
this._languages = toJson(value); | ||
set scopedNavItems(value) { | ||
this._scopedNavItems = toJson(value); | ||
} | ||
@api | ||
get language() { | ||
return this._language; | ||
get devCenter(): DevCenterConfig { | ||
return this._devCenter; | ||
} | ||
set language(value) { | ||
if (this._language !== value) { | ||
this._language = value; | ||
} | ||
set devCenter(value) { | ||
this._devCenter = toJson(value); | ||
} | ||
private _language: string | null = null; | ||
private _languages!: OptionWithLink[]; | ||
private _scopedNavItems!: OptionWithNested[]; | ||
private smallMobile = false; | ||
private smallMobileMatchMedia!: MediaQueryList; | ||
private tablet = false; | ||
private tabletMatchMedia!: MediaQueryList; | ||
private shouldRender: boolean = false; | ||
private showDocDivider: boolean = false; | ||
private _devCenter!: DevCenterConfig; | ||
private _hideDocHeader: boolean = false; | ||
@@ -58,36 +61,16 @@ protected mobileBreakpoint(): string { | ||
private get hasScopedNavItems(): boolean { | ||
return this.scopedNavItems && this.scopedNavItems.length > 0; | ||
} | ||
private get showDesktopNavItems(): boolean { | ||
return !this.mobile && this.hasNavItems; | ||
} | ||
private get showSignup(): boolean { | ||
return this.signupLink | ||
? (this.tablet && !this.isSearchOpen) || !this.tablet | ||
: false; | ||
} | ||
private get hasLanguages(): boolean { | ||
return !!(this.languages && this.languages.length); | ||
} | ||
private get showMobileLanguages(): boolean { | ||
return this.smallMobile && this.hasLanguages; | ||
} | ||
private get languageLabel(): string { | ||
private get showScopedNavItems(): boolean { | ||
return ( | ||
(this.language && | ||
this.languages.find( | ||
(lang) => get(lang, this.langValuePath) === this.language | ||
)?.label) || | ||
this.languages[0].label | ||
this.scopedNavItems && | ||
this.scopedNavItems.length > 0 && | ||
!this.hideDocHeader | ||
); | ||
} | ||
private get showMenuButton(): boolean { | ||
return this.mobile && this.hasNavItems; | ||
/** | ||
* This function returns true if the hideDocHeader is true and the view is not mobile. | ||
* Also we need to show the header border in case the doc is hidden or if the brand information doesn't exists. | ||
*/ | ||
private get shouldHideHeader(): boolean { | ||
return (this.hideDocHeader && !this.mobile) || this.showDocDivider; | ||
} | ||
@@ -103,10 +86,19 @@ | ||
this.smallMobileMatchMedia = window.matchMedia( | ||
`(max-width: ${SMALL_MOBILE_MATCH})` | ||
); | ||
this.onSmallMobileChange(this.smallMobileMatchMedia); | ||
this.smallMobileMatchMedia.addEventListener( | ||
"change", | ||
this.onSmallMobileChange | ||
); | ||
if ( | ||
(!this.shouldHideHeader && | ||
window.location.pathname.includes("/docs/") && | ||
window.location.pathname !== "/docs/apis") || | ||
window.location.pathname === | ||
"/tableau/embedding-playground/overview" || | ||
isStorybook() | ||
) { | ||
this.shouldRender = true; | ||
} | ||
if (this.shouldRender && window.location.pathname.includes("/docs/")) { | ||
if (!this.brand && !this.mobile) { | ||
this.shouldRender = false; | ||
this.showDocDivider = true; | ||
} | ||
} | ||
} | ||
@@ -120,7 +112,2 @@ | ||
); | ||
this.smallMobileMatchMedia.removeEventListener( | ||
"change", | ||
this.onSmallMobileChange | ||
); | ||
} | ||
@@ -131,17 +118,8 @@ | ||
private onSmallMobileChange = (e: MediaQueryListEvent | MediaQueryList) => | ||
(this.smallMobile = e.matches); | ||
protected additionalClasses(): string { | ||
return cx( | ||
this.brand && "has-brand", | ||
this.hasScopedNavItems && "has-scoped-nav-items" | ||
this.showScopedNavItems && "has-scoped-nav-items" | ||
); | ||
} | ||
private onLangChange(event: CustomEvent<string>): void { | ||
const { detail } = event; | ||
this._language = detail; | ||
this.dispatchEvent(new CustomEvent("langchange", { detail })); | ||
} | ||
} |
import { LightningElement, api } from "lwc"; | ||
export const displayLevels = ["3", "4", "5", "6"]; | ||
export const ariaLevels = ["1", "2", "3", "4"]; | ||
export const ariaDisplayLevels: { [key: string]: string } = { | ||
"1": "3", | ||
"2": "4", | ||
"3": "5", | ||
"4": "6" | ||
"1": "4", | ||
"2": "5", | ||
"3": "6", | ||
"4": "8" | ||
}; | ||
export const ariaLevels = Object.keys(ariaDisplayLevels); | ||
export const displayLevels = Object.values(ariaDisplayLevels); | ||
// @ts-ignore: Really Dark Magic (TM) to do with ariaLevel needing explicit getter/setters | ||
export default class Heading extends LightningElement { | ||
@api title: string = ""; | ||
@api header: string = ""; | ||
@api hash: string | null = null; | ||
@@ -20,2 +21,3 @@ | ||
private get ariaLevel(): string { | ||
// Really Dark Magic (TM) | ||
return this._ariaLevel || "2"; | ||
@@ -64,4 +66,4 @@ } | ||
private get className(): string { | ||
return `display-${this.displayLevel}`; | ||
return `dx-text-display-${this.displayLevel}`; | ||
} | ||
} |
@@ -9,3 +9,3 @@ import { LightningElement, api } from "lwc"; | ||
@api iconSymbol?: IconSymbol; | ||
@api title: string = ""; | ||
@api header: string = ""; | ||
@api urlText: string = ""; | ||
@@ -26,3 +26,3 @@ | ||
try { | ||
if (this.title && this.urlText) { | ||
if (this.header && this.urlText) { | ||
const [hostUrl] = window.location.href.split("#"); | ||
@@ -29,0 +29,0 @@ const url = `${hostUrl}#${this.urlText}`; |
import { LightningElement, api } from "lwc"; | ||
export default class HeadingContent extends LightningElement { | ||
@api title: string = ""; | ||
@api header: string = ""; | ||
@api hash: string | null = null; | ||
@@ -21,3 +21,3 @@ | ||
try { | ||
if (this.title && this.hash) { | ||
if (this.header && this.hash) { | ||
const [hostUrl] = window.location.href.split("#"); | ||
@@ -24,0 +24,0 @@ const url = `${hostUrl}#${this.hash}`; |
@@ -5,13 +5,9 @@ import { LightningElement, api } from "lwc"; | ||
import { DocPhaseInfo } from "typings/custom"; | ||
import { toJson } from "dxUtils/normalizers"; | ||
import { toJson, normalizeBoolean } from "dxUtils/normalizers"; | ||
export default class Phase extends LightningElement { | ||
_docPhaseInfo: DocPhaseInfo | null = null; | ||
_dismissible = false; | ||
_iconName = "recipe"; | ||
isBodyHidden = false; | ||
get docPhaseTitle() { | ||
return this.docPhaseInfo?.title; | ||
} | ||
@api | ||
@@ -26,2 +22,30 @@ get docPhaseInfo(): DocPhaseInfo | null { | ||
@api | ||
get dismissible(): boolean { | ||
return this._dismissible; | ||
} | ||
set dismissible(value) { | ||
if (value) { | ||
this._dismissible = normalizeBoolean(value); | ||
} | ||
} | ||
@api | ||
get iconName(): string { | ||
return this._iconName; | ||
} | ||
set iconName(value) { | ||
if (value) { | ||
this._iconName = value; | ||
} | ||
} | ||
isBodyHidden = false; | ||
get docPhaseTitle() { | ||
return this.docPhaseInfo?.title; | ||
} | ||
get hideBodyText() { | ||
@@ -56,5 +80,17 @@ return this.isBodyHidden ? "Show" : "Hide"; | ||
onButtonClick() { | ||
onShowHide() { | ||
this.isBodyHidden = !this.isBodyHidden; | ||
} | ||
onDismiss() { | ||
this.dispatchEvent( | ||
new CustomEvent("dismissphase", { | ||
detail: { | ||
docPhaseInfo: this.docPhaseInfo | ||
}, | ||
composed: true, | ||
bubbles: true | ||
}) | ||
); | ||
} | ||
} |
@@ -14,3 +14,3 @@ import { LightningElement, api } from "lwc"; | ||
//const target = event.detail.name.split('-') | ||
const target = event.currentTarget.dataset.id.split("-"); | ||
const target = (event.currentTarget as any).dataset.id.split("-"); | ||
newPageReference.contentDocumentId = target[0] + ".htm"; | ||
@@ -17,0 +17,0 @@ newPageReference.hash = target[1]; |
@@ -43,5 +43,5 @@ import { LightningElement, api } from "lwc"; | ||
if (languageEl) { | ||
const languageValue = ( | ||
languageEl[languageEl.selectedIndex] as HTMLOptionElement | ||
).value; | ||
const languageValue = (languageEl[ | ||
languageEl.selectedIndex | ||
] as HTMLOptionElement).value; | ||
this.dispatchEvent( | ||
@@ -65,5 +65,5 @@ new CustomEvent("languageselected", { | ||
if (versionEl) { | ||
const versionValue = ( | ||
versionEl[versionEl.selectedIndex] as HTMLOptionElement | ||
).value; | ||
const versionValue = (versionEl[ | ||
versionEl.selectedIndex | ||
] as HTMLOptionElement).value; | ||
this.dispatchEvent( | ||
@@ -70,0 +70,0 @@ new CustomEvent("versionselected", { |
@@ -38,2 +38,5 @@ export type CoveoAdvancedQueryXMLConfig = { | ||
url: string; | ||
link?: { | ||
href: string; | ||
}; | ||
}; | ||
@@ -62,8 +65,11 @@ | ||
subtitle: string; | ||
headerHref: string; | ||
} | ||
export type SiderbarFooter = { | ||
bailHref: string; | ||
bailLabel: string; | ||
languages: Array<DocLanguage>; | ||
language: string; | ||
headerHref: string; | ||
} | ||
language?: string; | ||
}; | ||
@@ -70,0 +76,0 @@ export type ApiNavItem = { |
@@ -90,3 +90,5 @@ import { | ||
private normalizeToc(apiToc: Array<ApiNavItem>): { | ||
private normalizeToc( | ||
apiToc: Array<ApiNavItem> | ||
): { | ||
tocMap: { [key: string]: TreeNode }; | ||
@@ -93,0 +95,0 @@ normalizedToc: Array<TreeNode>; |
@@ -0,1 +1,2 @@ | ||
/* eslint-disable @lwc/lwc/no-document-query */ | ||
import { api, track } from "lwc"; | ||
@@ -10,2 +11,3 @@ import { normalizeBoolean } from "dxUtils/normalizers"; | ||
Header, | ||
SiderbarFooter, | ||
HistoryState, | ||
@@ -15,8 +17,11 @@ PageReference, | ||
} from "./types"; | ||
import { SearchSyncer } from "docUtils/SearchSyncer"; | ||
import { LightningElementWithState } from "docBaseElements/lightningElementWithState"; | ||
import { Breadcrumb, Language } from "typings/custom"; | ||
import { SearchSyncer } from "docUtils/searchSyncer"; | ||
import { LightningElementWithState } from "dxBaseElements/lightningElementWithState"; | ||
import { logCoveoPageView, oldVersionDocInfo } from "docUtils/utils"; | ||
import { Breadcrumb, DocPhaseInfo, Language } from "typings/custom"; | ||
import { track as trackGTM } from "dxUtils/analytics"; | ||
import DOMPurify from "dompurify"; | ||
// TODO: Imitating from actual implementation as doc-content use it like this. We should refactor it later. | ||
const handleContentError = (error): void => console.log(error); | ||
const handleContentError = (error: any): void => console.log(error); | ||
@@ -28,2 +33,8 @@ const PIXEL_PER_CHARACTER_MAP: { [key: string]: number } = { | ||
const defaultSidebarFooter: SiderbarFooter = { | ||
bailHref: "", | ||
bailLabel: "", | ||
languages: [], | ||
language: "" | ||
}; | ||
export default class DocXmlContent extends LightningElementWithState<{ | ||
@@ -33,2 +44,3 @@ isFetchingDocument: boolean; | ||
lastHighlightedSearch: string; | ||
internalLinkClicked: boolean; | ||
}> { | ||
@@ -38,2 +50,5 @@ @api apiDomain = "https://developer.salesforce.com"; | ||
@api coveoPublicAccessToken!: string; | ||
@api coveoAnalyticsToken!: string; | ||
@api coveoSearchHub!: string; | ||
@api hideFooter = false; | ||
@@ -61,18 +76,24 @@ @api | ||
private availableLanguages: Array<DocLanguage> = []; | ||
private availableVersions: Array<DocVersion> = []; | ||
private contentProvider: FetchContent; | ||
@track private availableVersions: Array<DocVersion> = []; | ||
private contentProvider?: FetchContent; | ||
private docContent = ""; | ||
private language: DocLanguage = null; | ||
private language?: DocLanguage | null = null; | ||
private loaded = false; | ||
private _pageHeader?: Header; | ||
private pdfUrl = ""; | ||
private tocMap: TocMap = {}; | ||
private sidebarContent: Array<TreeNode> = null; | ||
private version: DocVersion = null; | ||
private sidebarContent: Array<TreeNode> | null = null; | ||
private version: DocVersion | null = null; | ||
private docTitle = ""; | ||
private analyticsEvent = "custEv_ctaLinkClick"; | ||
private _pathName = ""; | ||
private _pageHeader?: Header; | ||
private listenerAttached = false; | ||
private _enableCoveo?: boolean = false; | ||
private sidebarFooterContent: SiderbarFooter = { ...defaultSidebarFooter }; | ||
private latestVersion = false; | ||
private previewVersion = false; | ||
private get enableFooter(): boolean { | ||
return !this.hideFooter; | ||
} | ||
private searchSyncer = new SearchSyncer({ | ||
@@ -116,2 +137,33 @@ callbacks: { | ||
private get oldVersionInfo(): DocPhaseInfo | null { | ||
let info = null; | ||
if (!this.disableVersion) { | ||
const currentGAVersion = this.versionOptions.find( | ||
(version) => !version.url.includes(version.id) | ||
); | ||
if (currentGAVersion?.link?.href && this.version?.id) { | ||
const versionNo = currentGAVersion.id; | ||
/** | ||
* Need to show old version doc banner only if the version is less than the current ga version | ||
* We should not show it to the preview version whose version is more than ga | ||
**/ | ||
try { | ||
if (parseFloat(this.version.id) < parseFloat(versionNo)) { | ||
info = oldVersionDocInfo(currentGAVersion.link.href); | ||
} else if ( | ||
parseFloat(this.version.id) > parseFloat(versionNo) | ||
) { | ||
this.previewVersion = true; | ||
} | ||
} catch (exception) { | ||
/* Ideally this use case should not happen, but just added to not to break the page*/ | ||
console.warn(exception); | ||
} | ||
} | ||
} | ||
return info; | ||
} | ||
@track showVersionBanner = false; | ||
@track private pageReference: PageReference = {}; | ||
@@ -152,4 +204,4 @@ @track breadcrumbs: Array<Breadcrumb> = []; | ||
const urlSectionLink = | ||
this.pageReference?.hash?.split("#").length > 1 | ||
? this.pageReference.hash.split("#")[1] | ||
this.pageReference?.hash?.split("#").length! > 1 | ||
? this.pageReference.hash!.split("#")[1] | ||
: this.pageReference?.hash; | ||
@@ -167,2 +219,3 @@ | ||
anchorEl.scrollIntoView(); | ||
this.setState({ internalLinkClicked: false }); | ||
@@ -193,20 +246,13 @@ } | ||
this.searchSyncer.dispose(); | ||
if (this.listenerAttached) { | ||
this.pageHeader.removeEventListener( | ||
"langchange", | ||
this.handleLanguageChange | ||
); | ||
this.listenerAttached = false; | ||
} | ||
} | ||
private get languageId(): string { | ||
return this.language.id.replace("-", "_"); | ||
private get languageId(): string | undefined { | ||
return this.language?.id.replace("-", "_"); | ||
} | ||
private get releaseVersionId(): string { | ||
return this.version.id; | ||
private get releaseVersionId(): string | undefined { | ||
return this.version?.id; | ||
} | ||
private get deliverable(): string { | ||
private get deliverable(): string | undefined { | ||
return this.pageReference.deliverable; | ||
@@ -232,3 +278,7 @@ } | ||
private get coveoAdvancedQueryConfig(): CoveoAdvancedQueryXMLConfig { | ||
const config: { locale: string; topicid: string; version?: string } = { | ||
const config: { | ||
locale?: string; | ||
topicid?: string; | ||
version?: string; | ||
} = { | ||
locale: this.languageId, | ||
@@ -247,3 +297,3 @@ topicid: this.deliverable | ||
if (!this._pageHeader) { | ||
this._pageHeader = document.querySelector("doc-header"); | ||
this._pageHeader = document.querySelector("doc-header")!; | ||
} | ||
@@ -291,3 +341,3 @@ | ||
return ( | ||
PIXEL_PER_CHARACTER_MAP[this.language.id] || | ||
PIXEL_PER_CHARACTER_MAP[this.language!.id] || | ||
PIXEL_PER_CHARACTER_MAP.default | ||
@@ -297,5 +347,16 @@ ); | ||
private handlePopState = (): void => | ||
this.updatePageReference(this.getReferenceFromUrl()); | ||
private get ANALYTICS_PAYLOAD() { | ||
return { | ||
element_title: "version picker", | ||
content_category: "cta" | ||
}; | ||
} | ||
private handlePopState = (event: PopStateEvent): void => | ||
this.updatePageReference(this.getReferenceFromUrl(), event); | ||
handleDismissVersionBanner() { | ||
this.showVersionBanner = false; | ||
} | ||
handleSelect(event: CustomEvent<{ name: string }>): void { | ||
@@ -331,3 +392,3 @@ event.stopPropagation(); | ||
handleLanguageChange = (event: CustomEvent<string>): Promise<void> => { | ||
handleLanguageChange = (event: any) => { | ||
if (this.language && this.language.id === event.detail) { | ||
@@ -340,3 +401,14 @@ return; | ||
); | ||
this.pageReference.docId = this.language.url; | ||
this.pageReference.docId = this.language!.url; | ||
trackGTM(event.target!, "custEv_ctaLinkClick", { | ||
click_text: event.detail, | ||
element_title: "language selector", | ||
click_url: `${window.location.origin}${this.pageReferenceToString( | ||
this.pageReference | ||
)}`, | ||
element_type: "link", | ||
content_category: "cta" | ||
}); | ||
this.updateUrl(); | ||
@@ -346,3 +418,6 @@ this.fetchDocument(); | ||
updatePageReference(newPageReference: PageReference): void { | ||
updatePageReference( | ||
newPageReference: PageReference, | ||
event: PopStateEvent | undefined = undefined | ||
): void { | ||
this.pageReference.hash = newPageReference.hash; | ||
@@ -364,9 +439,22 @@ this.pageReference.search = newPageReference.search; | ||
this.fetchContent() | ||
.then(() => this.buildBreadcrumbs()) | ||
.then(() => { | ||
this.buildBreadcrumbs(); | ||
document.body.scrollTop = event?.state?.scroll?.value || 0; | ||
}) | ||
.catch(handleContentError); | ||
} | ||
private sanitizeUrlPart(part: string | undefined): string | undefined { | ||
if (!part) { | ||
return part; | ||
} | ||
return DOMPurify.sanitize(part); | ||
} | ||
getReferenceFromUrl(): PageReference { | ||
const [page, docId, deliverable, contentDocumentId] = | ||
window.location.pathname.substr(1).split("/"); | ||
window.location.pathname | ||
.substr(1) | ||
.split("/") | ||
.map(this.sanitizeUrlPart); | ||
@@ -380,5 +468,5 @@ const { origin: domain, hash, search } = window.location; | ||
domain, | ||
hash, | ||
hash: this.sanitizeUrlPart(hash), | ||
page, | ||
search | ||
search: this.sanitizeUrlPart(search) | ||
}; | ||
@@ -401,4 +489,5 @@ } | ||
}); | ||
const data = await this.contentProvider.fetchDocumentData( | ||
this.pageReference.docId | ||
const data = await this.contentProvider!.fetchDocumentData( | ||
this.pageReference.docId! | ||
); | ||
@@ -423,3 +512,3 @@ | ||
this.updateHeader(); | ||
this.updateHeaderAndSidebarFooter(); | ||
@@ -433,2 +522,8 @@ this.buildBreadcrumbs(); | ||
if (this.oldVersionInfo) { | ||
this.showVersionBanner = true; | ||
} else { | ||
this.latestVersion = true; | ||
} | ||
if ( | ||
@@ -463,8 +558,8 @@ this.pageReference?.contentDocumentId?.replace(/\.htm$/, "") !== | ||
}); | ||
const data = await this.contentProvider.fetchContent( | ||
this.pageReference.deliverable, | ||
this.pageReference.contentDocumentId, | ||
const data = await this.contentProvider!.fetchContent( | ||
this.pageReference.deliverable!, | ||
this.pageReference.contentDocumentId!, | ||
{ | ||
language: this.language.id, | ||
version: this.version.id | ||
language: this.language!.id, | ||
version: this.version!.id | ||
} | ||
@@ -478,7 +573,3 @@ ); | ||
if (!this.pageReference.hash) { | ||
document.querySelector("main")?.scrollIntoView({ | ||
behavior: "smooth", | ||
block: "start", | ||
inline: "nearest" | ||
}); | ||
document.body.scrollIntoView(); | ||
} | ||
@@ -491,3 +582,3 @@ } | ||
updateHeader(): void { | ||
updateHeaderAndSidebarFooter(): void { | ||
if (!this.pageHeader) { | ||
@@ -502,17 +593,9 @@ return; | ||
if (this.pdfUrl) { | ||
this.pageHeader.bailHref = this.pdfUrl; | ||
this.pageHeader.bailLabel = "PDF"; | ||
this.sidebarFooterContent.bailHref = this.pdfUrl; | ||
this.sidebarFooterContent.bailLabel = "PDF"; | ||
} | ||
if (!this.listenerAttached) { | ||
this.pageHeader.addEventListener( | ||
"langchange", | ||
this.handleLanguageChange | ||
); | ||
this.listenerAttached = true; | ||
} | ||
this.sidebarFooterContent.languages = this.availableLanguages; | ||
this.sidebarFooterContent.language = this.language?.id; | ||
this.pageHeader.languages = this.availableLanguages; | ||
this.pageHeader.language = this.language?.id; | ||
if (this.pageReference) { | ||
@@ -525,2 +608,3 @@ const { docId, deliverable, page } = this.pageReference; | ||
updateUrl(method = HistoryState.PUSH_STATE): void { | ||
logCoveoPageView(this.coveoOrganizationId, this.coveoAnalyticsToken); | ||
window.history[method]( | ||
@@ -541,5 +625,3 @@ {}, | ||
private updateSearchInput(searchParam: string): void { | ||
this.template | ||
.querySelector("doc-content-layout") | ||
?.setSidebarInputValue(searchParam); | ||
(this.refs.docContentLayout as any)?.setSidebarInputValue(searchParam); | ||
} | ||
@@ -551,10 +633,13 @@ | ||
return `/${page}/${docId}/${deliverable}/${contentDocumentId}${this.normalizeSearch( | ||
search | ||
search! | ||
)}${this.normalizeHash(hash)}`; | ||
} | ||
private normalizeUrlPart(part: string, sentinel: string): string { | ||
private normalizeUrlPart( | ||
part: string | undefined, | ||
sentinel: string | ||
): string { | ||
return ( | ||
(part && | ||
(part.startsWith(sentinel) ? part : `${sentinel}${part}`)) || | ||
(part.startsWith(sentinel!) ? part : `${sentinel}${part}`)) || | ||
"" | ||
@@ -568,3 +653,3 @@ ); | ||
private normalizeHash(hash: string): string { | ||
private normalizeHash(hash?: string): string { | ||
return this.normalizeUrlPart(hash, "#"); | ||
@@ -574,7 +659,7 @@ } | ||
private getComposedTitle( | ||
topicTitle: string | undefined, | ||
topicTitle: string | null | undefined, | ||
docTitle: string | undefined | ||
): string { | ||
// map to avoid duplicates | ||
const titleMap = {}; | ||
const titleMap: { [key: string]: any } = {}; | ||
if (topicTitle) { | ||
@@ -614,6 +699,2 @@ // sometimes the h1 tag text (which is docSubTitle) contains text with new line character. For e.g, "Bulk API 2.0 Older\n Documentation", | ||
get docContentStyle(): string { | ||
return this.showBreadcrumbs ? "" : "margin-top: 48px"; | ||
} | ||
private buildBreadcrumbs(): void { | ||
@@ -650,5 +731,19 @@ const { contentDocumentId } = this.pageReference; | ||
// This method take docId and drops the version from the docId. | ||
// Example: | ||
// Takes input string: docId = "atlas.en-us.238.0.b2b_b2c_comm_dev.meta" | ||
// Output string: filteredDocId = "atlas.en-us.b2b_b2c_comm_dev.meta" | ||
dropVersionFromDocId(docId: string): string { | ||
if (!this.version?.id) { | ||
return docId; | ||
} | ||
const curVersion = this.version.id + "."; | ||
const filteredDocId = docId.replace(curVersion, ""); | ||
return filteredDocId; | ||
} | ||
addMetatags(): void { | ||
const div = document.createElement("div"); | ||
div.innerHTML = this.docContent; | ||
div.innerHTML = DOMPurify.sanitize(this.docContent); | ||
const docDescription = div.querySelector(".shortdesc")?.textContent; | ||
@@ -660,3 +755,3 @@ const topicTitle = div.querySelector("h1")?.textContent; | ||
if (title) { | ||
if (title && title.textContent) { | ||
title.textContent = composedTitle; | ||
@@ -683,2 +778,6 @@ } | ||
if (metadescription) { | ||
const copyPageReference = { ...this.pageReference }; | ||
copyPageReference.docId = copyPageReference.docId | ||
? this.dropVersionFromDocId(copyPageReference.docId) | ||
: copyPageReference.docId; | ||
metadescription.setAttribute( | ||
@@ -689,7 +788,41 @@ "href", | ||
window.location.host + | ||
this.pageReferenceToString(this.pageReference) | ||
this.pageReferenceToString(copyPageReference) | ||
); | ||
} | ||
} | ||
this.addNoIndexMetaForOlderDocVersions(); | ||
} | ||
/** | ||
* Method adds noindex, follow meta tag to the older Couch DB doc pages. | ||
* Fixes W-12547462. | ||
*/ | ||
private addNoIndexMetaForOlderDocVersions() { | ||
// eslint-disable-next-line @lwc/lwc/no-document-query | ||
const headTag = document.getElementsByTagName("head"); | ||
// this checks if the selected version is not the latest version, | ||
// then it adds the noindex, follow meta tag to the older version pages. | ||
const versionId = this.version!.id; | ||
const docId = this.pageReference.docId; | ||
// SEO fix: | ||
// Doc id without version id is always considered latest and should be used for SEO. | ||
// Condition is to find a docId which includes version id, | ||
// these docs are always considered as old and should not be indexed including the preview docs. | ||
if ( | ||
headTag.length && | ||
docId?.includes(versionId) && | ||
!document.querySelector('meta[name="robots"]') | ||
) { | ||
const robotsMeta = document.createElement("meta"); | ||
robotsMeta.setAttribute("name", "robots"); | ||
robotsMeta.setAttribute("content", "noindex, follow"); | ||
headTag[0].appendChild(robotsMeta); | ||
} | ||
} | ||
private get showVersionPicker(): boolean { | ||
return !this.disableVersion; | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
354091
54.68%97
46.97%8800
49.63%8
14.29%+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
Updated
Updated
Updated
Updated
Updated
Updated