@salesforcedevs/docs-components 0.7.0 → 0.7.59-sppage-alpha2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/lwc.config.json +17 -3
  2. package/package.json +16 -6
  3. package/src/modules/README.md +41 -0
  4. package/src/modules/doc/amfModelParser/amfModelParser.ts +674 -0
  5. package/src/modules/doc/amfReference/amfReference.css +25 -0
  6. package/src/modules/doc/amfReference/amfReference.html +60 -0
  7. package/src/modules/doc/amfReference/amfReference.ts +1494 -0
  8. package/src/modules/doc/amfReference/constants.ts +76 -0
  9. package/src/modules/doc/amfReference/types.ts +125 -0
  10. package/src/modules/doc/amfTopic/amfTopic.css +21 -0
  11. package/src/modules/doc/amfTopic/amfTopic.html +3 -0
  12. package/src/modules/doc/amfTopic/amfTopic.ts +111 -0
  13. package/src/modules/doc/amfTopic/types.ts +56 -0
  14. package/src/modules/doc/amfTopic/utils.ts +136 -0
  15. package/src/modules/doc/breadcrumbItem/breadcrumbItem.css +51 -0
  16. package/src/modules/doc/breadcrumbItem/breadcrumbItem.html +5 -0
  17. package/src/modules/doc/breadcrumbItem/breadcrumbItem.ts +71 -0
  18. package/src/modules/doc/breadcrumbs/breadcrumbs.css +27 -0
  19. package/src/modules/doc/breadcrumbs/breadcrumbs.html +58 -0
  20. package/src/modules/doc/breadcrumbs/breadcrumbs.ts +192 -0
  21. package/src/modules/doc/content/content.css +94 -70
  22. package/src/modules/doc/content/content.ts +233 -169
  23. package/src/modules/doc/contentCallout/contentCallout.css +17 -23
  24. package/src/modules/doc/contentCallout/contentCallout.html +13 -4
  25. package/src/modules/doc/contentCallout/contentCallout.ts +16 -3
  26. package/src/modules/doc/contentLayout/contentLayout.css +131 -0
  27. package/src/modules/doc/contentLayout/contentLayout.html +64 -0
  28. package/src/modules/doc/contentLayout/contentLayout.ts +610 -0
  29. package/src/modules/doc/doDont/doDont.css +47 -0
  30. package/src/modules/doc/doDont/doDont.html +27 -0
  31. package/src/modules/doc/doDont/doDont.ts +17 -0
  32. package/src/modules/doc/header/header.css +70 -37
  33. package/src/modules/doc/header/header.html +40 -135
  34. package/src/modules/doc/header/header.ts +29 -78
  35. package/src/modules/doc/heading/heading.css +33 -0
  36. package/src/modules/doc/heading/heading.html +14 -0
  37. package/src/modules/doc/heading/heading.ts +67 -0
  38. package/src/modules/doc/headingAnchor/headingAnchor.css +33 -0
  39. package/src/modules/doc/headingAnchor/headingAnchor.html +19 -0
  40. package/src/modules/doc/headingAnchor/headingAnchor.ts +43 -0
  41. package/src/modules/doc/headingContent/headingContent.css +53 -0
  42. package/src/modules/doc/headingContent/headingContent.html +13 -0
  43. package/src/modules/doc/headingContent/headingContent.ts +30 -0
  44. package/src/modules/doc/overview/overview.css +40 -0
  45. package/src/modules/doc/overview/overview.html +34 -0
  46. package/src/modules/doc/overview/overview.ts +12 -0
  47. package/src/modules/doc/phase/phase.css +70 -0
  48. package/src/modules/doc/phase/phase.html +38 -0
  49. package/src/modules/doc/phase/phase.ts +93 -0
  50. package/src/modules/doc/specificationContent/specificationContent.css +3 -0
  51. package/src/modules/doc/specificationContent/specificationContent.html +99 -0
  52. package/src/modules/doc/specificationContent/specificationContent.ts +56 -0
  53. package/src/modules/doc/sprigSurvey/sprigSurvey.html +20 -0
  54. package/src/modules/doc/sprigSurvey/sprigSurvey.scoped.css +16 -0
  55. package/src/modules/doc/sprigSurvey/sprigSurvey.ts +16 -0
  56. package/src/modules/doc/toc/toc.ts +1 -1
  57. package/src/modules/doc/versionPicker/versionPicker.css +64 -0
  58. package/src/modules/doc/versionPicker/versionPicker.html +38 -0
  59. package/src/modules/doc/versionPicker/versionPicker.ts +65 -0
  60. package/src/modules/doc/xmlContent/types.ts +120 -0
  61. package/src/modules/doc/xmlContent/utils.ts +163 -0
  62. package/src/modules/doc/xmlContent/xmlContent.css +54 -0
  63. package/src/modules/doc/xmlContent/xmlContent.html +52 -0
  64. package/src/modules/doc/xmlContent/xmlContent.ts +780 -0
  65. package/src/modules/docHelpers/amfStyle/amfStyle.css +355 -0
  66. package/src/modules/docHelpers/imgStyle/imgStyle.css +59 -0
  67. package/src/modules/docHelpers/status/status.css +22 -0
  68. package/src/modules/docUtils/searchSyncer/searchSyncer.ts +86 -0
  69. package/src/modules/docUtils/utils/utils.ts +32 -0
  70. package/LICENSE +0 -12
  71. package/src/modules/doc/content/__tests__/content.test.ts +0 -120
  72. package/src/modules/doc/content/__tests__/mockDocContent.ts +0 -292
  73. package/src/modules/doc/content/__tests__/mockPageReference.ts +0 -8
  74. package/src/modules/doc/content/content.stories.ts +0 -108
  75. package/src/modules/doc/contentCallout/__tests__/contentCallout.test.ts +0 -80
  76. package/src/modules/doc/contentCallout/__tests__/mockProps.ts +0 -14
  77. package/src/modules/doc/contentCallout/contentCallout.stories.ts +0 -29
  78. package/src/modules/doc/contentMedia/__tests__/contentMedia.test.ts +0 -97
  79. package/src/modules/doc/contentMedia/contentMedia.stories.ts +0 -113
  80. package/src/modules/doc/header/__tests__/coveoConfig.ts +0 -12
  81. package/src/modules/doc/header/__tests__/header.test.ts +0 -434
  82. package/src/modules/doc/header/__tests__/mockNavDevelopers.ts +0 -427
  83. package/src/modules/doc/header/__tests__/mockNavs.ts +0 -115
  84. package/src/modules/doc/header/__tests__/mockProps.ts +0 -149
  85. package/src/modules/doc/header/header.stories.ts +0 -160
  86. package/src/modules/doc/nav/__tests__/mockAvailableLanguages.ts +0 -8
  87. package/src/modules/doc/nav/__tests__/mockAvailableVersions.ts +0 -122
  88. package/src/modules/doc/nav/__tests__/mockPageReference.ts +0 -8
  89. package/src/modules/doc/nav/__tests__/mockPdfUrl.ts +0 -1
  90. package/src/modules/doc/nav/__tests__/mockSelectedLanguage.ts +0 -8
  91. package/src/modules/doc/nav/__tests__/mockSelectedVersion.ts +0 -8
  92. package/src/modules/doc/nav/__tests__/mockToc.ts +0 -146
  93. package/src/modules/doc/nav/__tests__/nav.test.ts +0 -58
  94. package/src/modules/doc/toc/__tests__/mockPageReference.ts +0 -8
  95. package/src/modules/doc/toc/__tests__/mockToc.ts +0 -146
  96. package/src/modules/doc/toc/__tests__/toc.test.ts +0 -29
  97. package/src/modules/doc/toolbar/__tests__/mockAvailableLanguages.ts +0 -8
  98. package/src/modules/doc/toolbar/__tests__/mockAvailableVersions.ts +0 -122
  99. package/src/modules/doc/toolbar/__tests__/mockPdfUrl.ts +0 -1
  100. package/src/modules/doc/toolbar/__tests__/mockSelectedLanguage.ts +0 -8
  101. package/src/modules/doc/toolbar/__tests__/mockSelectedVersion.ts +0 -8
  102. package/src/modules/doc/toolbar/__tests__/toolbar.test.ts +0 -44
@@ -0,0 +1,1494 @@
1
+ import { LightningElement, api, track } from "lwc";
2
+ import { noCase } from "no-case";
3
+ import { sentenceCase } from "sentence-case";
4
+ import qs from "query-string";
5
+ import { AmfModelParser } from "doc/amfModelParser";
6
+ import { normalizeBoolean, toJson } from "dxUtils/normalizers";
7
+ import type { OptionWithLink } from "typings/custom";
8
+ import type {
9
+ AmfConfig,
10
+ AmfMetadataTopic,
11
+ AmfModel,
12
+ AmfModelRecord,
13
+ NavItem,
14
+ ParsedTopicModel,
15
+ TopicModel,
16
+ ReferenceVersion,
17
+ ReferenceSetConfig,
18
+ AmfMetaTopicType,
19
+ RouteMeta,
20
+ ParsedMarkdownTopic
21
+ } from "./types";
22
+
23
+ import {
24
+ NAVIGATION_ITEMS,
25
+ URL_CONFIG,
26
+ REFERENCE_TYPES,
27
+ oldReferenceIdNewReferenceIdMap
28
+ } from "./constants";
29
+ import { restoreScroll } from "dx/scrollManager";
30
+ import { DocPhaseInfo } from "typings/custom";
31
+ import { logCoveoPageView, oldVersionDocInfo } from "docUtils/utils";
32
+
33
+ type NavigationItem = {
34
+ label: string;
35
+ name: string;
36
+ isExpanded: boolean;
37
+ children: ParsedMarkdownTopic[];
38
+ isChildrenLoading: boolean;
39
+ };
40
+
41
+ export default class AmfReference extends LightningElement {
42
+ @api breadcrumbs: string | null = null;
43
+ @api sidebarHeader!: string;
44
+ @api coveoOrganizationId!: string;
45
+ @api coveoPublicAccessToken!: string;
46
+ @api coveoAnalyticsToken!: string;
47
+ @api coveoSearchHub!: string;
48
+ @api useOldSidebar: boolean = false;
49
+ @api tocTitle?: string;
50
+ @api tocOptions?: string;
51
+ @api languages!: OptionWithLink[];
52
+ @api language!: string;
53
+ @api hideFooter = false;
54
+ @track navigation = [] as NavigationItem[];
55
+ @track versions: Array<ReferenceVersion> = [];
56
+ @track showVersionBanner = false;
57
+ @track _coveoAdvancedQueryConfig!: { [key: string]: any };
58
+
59
+ // Update this to update what component gets rendered in the content block
60
+ @track
61
+ protected topicModel!: TopicModel;
62
+
63
+ get isVersionEnabled(): boolean {
64
+ return !!this._referenceSetConfig?.versions?.length;
65
+ }
66
+
67
+ /**
68
+ * Gives if the currently selected reference is spec based or not
69
+ */
70
+ get showSpecBasedReference(): boolean {
71
+ return this.isSpecBasedReference(this._currentReferenceId);
72
+ }
73
+
74
+ @api
75
+ get referenceSetConfig(): ReferenceSetConfig {
76
+ return this._referenceSetConfig;
77
+ }
78
+
79
+ set referenceSetConfig(value: ReferenceSetConfig) {
80
+ // No change, do nothing.
81
+ if (value === this._referenceSetConfig) {
82
+ return;
83
+ }
84
+
85
+ try {
86
+ const refConfig =
87
+ typeof value === "string" ? JSON.parse(value) : value;
88
+ if (!(<ReferenceSetConfig>refConfig).versions) {
89
+ return;
90
+ }
91
+ this._referenceSetConfig = refConfig;
92
+ } catch (e) {
93
+ this._referenceSetConfig = {
94
+ refList: [],
95
+ versions: []
96
+ };
97
+ }
98
+
99
+ this._amfConfigList = this._referenceSetConfig.refList || [];
100
+
101
+ this._amfConfigList.forEach((amfConfig) => {
102
+ this._amfConfigMap.set(amfConfig.id, amfConfig);
103
+ });
104
+
105
+ if (this._amfConfigList.length > 0) {
106
+ this._currentReferenceId =
107
+ this._referenceSetConfig.refId || this._amfConfigList[0].id;
108
+ }
109
+
110
+ if (this.isVersionEnabled) {
111
+ const selectedVersion = this.getSelectedVersion();
112
+
113
+ /**
114
+ * If current selected is markdown based reference,
115
+ * We will assign versions once the selected item url is updated
116
+ */
117
+ if (this.isSpecBasedReference(this._currentReferenceId)) {
118
+ this.versions = this.getVersions();
119
+ }
120
+ this.selectedVersion = selectedVersion;
121
+ if (this.isSpecBasedReference(this._currentReferenceId)) {
122
+ this.isVersionFetched = true;
123
+ if (this.oldVersionInfo) {
124
+ this.showVersionBanner = true;
125
+ } else {
126
+ this.latestVersion = true;
127
+ }
128
+ }
129
+ } else {
130
+ this.isVersionFetched = true;
131
+ }
132
+
133
+ // This is to check if the url is hash based and redirect if needed
134
+ const redirectUrl = this.getHashBasedRedirectUrl();
135
+ if (redirectUrl) {
136
+ window.location.href = redirectUrl;
137
+ } else {
138
+ this.updateAmfConfigInView();
139
+ }
140
+ }
141
+
142
+ @api
143
+ get docPhaseInfo(): string | null {
144
+ return this.selectedReferenceDocPhase || null;
145
+ }
146
+
147
+ set docPhaseInfo(value: string) {
148
+ if (value) {
149
+ this.isParentLevelDocPhaseEnabled = true;
150
+ this.selectedReferenceDocPhase = value;
151
+ }
152
+ }
153
+
154
+ @api
155
+ get expandChildren() {
156
+ return this._expandChildren;
157
+ }
158
+
159
+ set expandChildren(value) {
160
+ this._expandChildren = normalizeBoolean(value);
161
+ }
162
+
163
+ /*
164
+ * The get coveoAdvancedQueryConfig() method returns this._coveoAdvancedQueryConfig,
165
+ * but before returning it, it checks if there are multiple versions (this.versions.length > 1)
166
+ * and if a version is selected (this.selectedVersion). If both conditions are met,
167
+ * it updates the version property of this._coveoAdvancedQueryConfig with the selected version.
168
+ */
169
+ @api
170
+ get coveoAdvancedQueryConfig(): { [key: string]: any } {
171
+ const coveoConfig = this._coveoAdvancedQueryConfig;
172
+ if (this.versions.length > 1 && this.selectedVersion) {
173
+ const currentGAVersionRef = this.versions[0];
174
+ if (this.selectedVersion.id !== currentGAVersionRef.id) {
175
+ // Currently Coveo only supports query without "v"
176
+ const version = this.selectedVersion.id.replace("v", "");
177
+ coveoConfig.version = version;
178
+ this._coveoAdvancedQueryConfig = coveoConfig;
179
+ }
180
+ }
181
+ return this._coveoAdvancedQueryConfig;
182
+ }
183
+
184
+ set coveoAdvancedQueryConfig(config) {
185
+ this._coveoAdvancedQueryConfig = toJson(config);
186
+ }
187
+
188
+ private get enableFooter(): boolean {
189
+ return !this.hideFooter;
190
+ }
191
+
192
+ // model
193
+ protected _amfConfigList: AmfConfig[] = [];
194
+ protected _amfConfigMap: Map<string, AmfConfig> = new Map();
195
+ protected _referenceSetConfig!: ReferenceSetConfig;
196
+ protected _currentReferenceId = "";
197
+
198
+ protected parentReferenceUrls = [] as string[];
199
+ protected amfMap: Record<string, AmfModelRecord> = {};
200
+ protected amfFetchPromiseMap = {} as any;
201
+ protected metadata: { [key: string]: AmfMetadataTopic } = {};
202
+ protected selectedTopic?: AmfMetaTopicType = undefined;
203
+ protected selectedSidebarValue: string | undefined = undefined;
204
+
205
+ protected selectedVersion: ReferenceVersion | null = null;
206
+
207
+ private hasRendered = false;
208
+
209
+ private isParentLevelDocPhaseEnabled = false;
210
+ private selectedReferenceDocPhase?: string | null = null;
211
+ private _expandChildren?: boolean = false;
212
+ private isVersionFetched = false;
213
+ private latestVersion = false;
214
+
215
+ /**
216
+ * Key for storing the currently selected reference url. This will be used to save the
217
+ * previously selected reference url and restoring it when changing between reference versions.
218
+ */
219
+ private readonly docsReferenceUrlSessionKey: string = "docsReferenceUrl";
220
+
221
+ _boundOnApiNavigationChanged;
222
+ _boundUpdateSelectedItemFromUrlQuery;
223
+
224
+ constructor() {
225
+ super();
226
+
227
+ this._boundOnApiNavigationChanged =
228
+ this.onApiNavigationChanged.bind(this);
229
+ this._boundUpdateSelectedItemFromUrlQuery =
230
+ this.updateSelectedItemFromUrlQuery.bind(this);
231
+ }
232
+
233
+ connectedCallback(): void {
234
+ this.addEventListener(
235
+ "api-navigation-selection-changed",
236
+ this._boundOnApiNavigationChanged
237
+ );
238
+ window.addEventListener(
239
+ "popstate",
240
+ this._boundUpdateSelectedItemFromUrlQuery
241
+ );
242
+ }
243
+
244
+ disconnectedCallback(): void {
245
+ this.removeEventListener(
246
+ "api-navigation-selection-changed",
247
+ this._boundOnApiNavigationChanged
248
+ );
249
+ window.removeEventListener(
250
+ "popstate",
251
+ this._boundUpdateSelectedItemFromUrlQuery
252
+ );
253
+ }
254
+
255
+ renderedCallback(): void {
256
+ if (!this.hasRendered) {
257
+ this.hasRendered = true;
258
+ if (this._amfConfigList && this._amfConfigList.length) {
259
+ // If amfConfig has a value and length, it is assumed that fetch
260
+ // has already been called and promises stored.
261
+ this.updateView();
262
+ }
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Check if the URL hash to see whether this is one we want to redirect
268
+ * See GUS W-10718771 for references where we want hash-based redirects
269
+ * Return if we needs to redirect url to updated url
270
+ */
271
+ private getHashBasedRedirectUrl(): string | undefined {
272
+ const { hash } = window.location;
273
+ let hashBasedRedirectUrl = "";
274
+ if (hash) {
275
+ const strippedHash = hash.startsWith("#") ? hash.slice(1) : hash;
276
+ const strippedHashItems = strippedHash
277
+ ? strippedHash.split(":")
278
+ : [];
279
+ if (strippedHashItems.length) {
280
+ const referenceId = strippedHashItems[0];
281
+ const meta = strippedHashItems[1];
282
+ const encodedMeta = this.getUrlEncoded(meta);
283
+ const updatedReferenceId =
284
+ oldReferenceIdNewReferenceIdMap[referenceId];
285
+ const newReferenceId = updatedReferenceId || referenceId;
286
+ const referenceItemConfig =
287
+ this.getAmfConfigWithId(newReferenceId);
288
+ if (referenceItemConfig) {
289
+ hashBasedRedirectUrl = `${referenceItemConfig.href}?meta=${encodedMeta}`;
290
+ }
291
+ }
292
+ }
293
+ return hashBasedRedirectUrl;
294
+ }
295
+
296
+ /**
297
+ * @param referenceId
298
+ * @returns AMFConfig with given reference Id
299
+ */
300
+ private getAmfConfigWithId(referenceId: string): AmfConfig | undefined {
301
+ return this._amfConfigMap.get(referenceId);
302
+ }
303
+
304
+ /**
305
+ * @param referenceId
306
+ * @returns if the reference is spec based one or not with given referenceId.
307
+ */
308
+ private isSpecBasedReference(referenceId: string): boolean {
309
+ const selectedReference = this.getAmfConfigWithId(referenceId);
310
+ return selectedReference
311
+ ? selectedReference.referenceType !== REFERENCE_TYPES.markdown
312
+ : false;
313
+ }
314
+
315
+ /*
316
+ * Refactor below method when sidebar allows sending extraData along with the name for each item.
317
+ * See if we can refactor the below method using regex.
318
+ */
319
+
320
+ /**
321
+ * @param url
322
+ * @returns reference Id from url path / selected sidebar item.
323
+ */
324
+ private getReferenceIdFromUrl(url: string): string {
325
+ let referenceId = "";
326
+ const urlItems = url.split("/references/");
327
+ if (urlItems.length > 1) {
328
+ const rightSidePart = urlItems[1];
329
+
330
+ //This covers urls like "/project-name/references/reference-id/..."
331
+ const slashSeparatorItems = rightSidePart.split("/");
332
+
333
+ //This covers urls like "/project-name/references/reference-id?meta=Summary"
334
+ const querySeparatorItems = slashSeparatorItems[0].split("?");
335
+
336
+ referenceId = querySeparatorItems[0];
337
+ }
338
+
339
+ return referenceId;
340
+ }
341
+
342
+ private get oldVersionInfo(): DocPhaseInfo | null {
343
+ let info = null;
344
+ if (this.versions.length > 1 && this.selectedVersion) {
345
+ const currentGAVersionRef = this.versions[0];
346
+ if (this.selectedVersion.id !== currentGAVersionRef.id) {
347
+ info = oldVersionDocInfo(currentGAVersionRef.link.href);
348
+ }
349
+ }
350
+ return info;
351
+ }
352
+
353
+ /**
354
+ * @returns versions to be shown in the dropdown
355
+ * For markdown based specs, Adds selected markdown topic url to same references
356
+ */
357
+ private getVersions(): Array<ReferenceVersion> {
358
+ const allVersions = this._referenceSetConfig.versions;
359
+ if (!this.isSpecBasedReference(this._currentReferenceId)) {
360
+ const currentRefMeta = this.getMarkdownReferenceMeta(
361
+ window.location.href
362
+ );
363
+ if (currentRefMeta) {
364
+ for (let i = 0; i < allVersions.length; i++) {
365
+ const versionItem = allVersions[i];
366
+ const referenceLink = versionItem.link.href;
367
+ const referenceId =
368
+ this.getReferenceIdFromUrl(referenceLink);
369
+ if (this._currentReferenceId === referenceId) {
370
+ // This is to navigate to respective topic in the changed version
371
+ versionItem.link.href = `${referenceLink}/${currentRefMeta}`;
372
+ allVersions[i] = versionItem;
373
+ }
374
+ }
375
+ }
376
+ }
377
+ return allVersions;
378
+ }
379
+
380
+ /**
381
+ * Returns the selected version or the first available version.
382
+ */
383
+ private getSelectedVersion(): ReferenceVersion | null {
384
+ const versions = this._referenceSetConfig?.versions || [];
385
+ const selectedVersion = versions.find(
386
+ (v: ReferenceVersion) => v.selected
387
+ );
388
+ // return a selected version if there is one, else return the first one.
389
+ return selectedVersion || (versions.length && versions[0]) || null;
390
+ }
391
+
392
+ private updateAmfConfigInView(): void {
393
+ if (this._amfConfigList && this._amfConfigList.length) {
394
+ // fetch AMF Json as soon as config is set
395
+ this.populateReferenceItems();
396
+ // update() must be called after renderedCallback.
397
+ if (this.hasRendered) {
398
+ this.updateView();
399
+ }
400
+ }
401
+ }
402
+
403
+ private async fetchAmf(
404
+ amfConfig: AmfConfig
405
+ ): Promise<AmfModel | AmfModel[]> {
406
+ const { amf } = amfConfig;
407
+ const response = await fetch(amf!, {
408
+ headers: {
409
+ "Cache-Control": `max-age=86400`
410
+ }
411
+ });
412
+ const json = await response.json();
413
+ return json;
414
+ }
415
+
416
+ /**
417
+ * Returns whether current location is project root path like ../example-project/references
418
+ */
419
+ private isProjectRootPath(): boolean {
420
+ return this.getReferenceIdFromUrl(window.location.href) === "";
421
+ }
422
+
423
+ /**
424
+ * Returns whether given url is parent reference path like ../example-project/references/reference-id
425
+ */
426
+ private isParentReferencePath(urlPath?: string | null): boolean {
427
+ if (!urlPath) {
428
+ return false;
429
+ }
430
+ const parentReferenceIndex = this.parentReferenceUrls.findIndex(
431
+ (referenceUrl: string) => {
432
+ return urlPath.endsWith(referenceUrl);
433
+ }
434
+ );
435
+ return parentReferenceIndex !== -1;
436
+ }
437
+
438
+ /**
439
+ * Expands the children of Markdown-based References.
440
+ */
441
+ private expandChildrenForMarkdownReferences(
442
+ children: ParsedMarkdownTopic[]
443
+ ): void {
444
+ if (!children) {
445
+ return;
446
+ }
447
+ for (const childNode of children) {
448
+ childNode.isExpanded = true;
449
+ this.expandChildrenForMarkdownReferences(childNode.children);
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Populates reference Items from amfConfigList and assigns it to navigation for sidebar
455
+ */
456
+ private populateReferenceItems(): void {
457
+ const navAmfOrder = [] as NavigationItem[];
458
+ for (const [index, amfConfig] of this._amfConfigList.entries()) {
459
+ let navItemChildren = [] as ParsedMarkdownTopic[];
460
+ let isChildrenLoading = false;
461
+ if (amfConfig.referenceType !== REFERENCE_TYPES.markdown) {
462
+ if (amfConfig.isSelected) {
463
+ const amfPromise = this.fetchAmf(amfConfig).then(
464
+ (amfJson) => {
465
+ this.updateModel(amfConfig.id, amfJson);
466
+ this.assignNavigationItemsFromAmf(amfConfig, index);
467
+ }
468
+ );
469
+ this.amfFetchPromiseMap[amfConfig.id] = amfPromise;
470
+ }
471
+ isChildrenLoading = true;
472
+ } else {
473
+ const isExpandChildrenEnabled = this.isExpandChildrenEnabled(
474
+ amfConfig.id
475
+ );
476
+ // check whether we should expand all the child nodes, this is required for Coveo to crawl.
477
+ if (isExpandChildrenEnabled) {
478
+ this.expandChildrenForMarkdownReferences(
479
+ amfConfig.topic!.children
480
+ );
481
+ }
482
+ navItemChildren = amfConfig.topic!.children;
483
+ }
484
+ // store nav items for each spec in order
485
+ navAmfOrder[index] = {
486
+ label: amfConfig.title,
487
+ name: amfConfig.href,
488
+ isExpanded:
489
+ amfConfig.isSelected ||
490
+ this.isExpandChildrenEnabled(amfConfig.id),
491
+ children: navItemChildren,
492
+ isChildrenLoading
493
+ };
494
+ this.parentReferenceUrls.push(amfConfig.href);
495
+ }
496
+ this.navigation = navAmfOrder;
497
+ }
498
+
499
+ /**
500
+ * Returns a boolean indicating whether the children should be expanded or not.
501
+ */
502
+ private isExpandChildrenEnabled(referenceId: string): boolean {
503
+ return (
504
+ !!this.expandChildren && this._currentReferenceId === referenceId
505
+ );
506
+ }
507
+
508
+ /**
509
+ * Stores fetched AMF JSON value.
510
+ * Creates and stores a new AmfModelParser instance for the AMF spec.
511
+ * @param {*} referenceId
512
+ * @param {*} amf
513
+ */
514
+ private updateModel(referenceId: string, amf: AmfModel | AmfModel[]): void {
515
+ const parser = new AmfModelParser(amf);
516
+ this.amfMap[referenceId] = {
517
+ model: amf,
518
+ parser: parser,
519
+ parsedModel: parser.parsedModel
520
+ };
521
+ }
522
+
523
+ /**
524
+ * Convert any case to a readable Title
525
+ * ex: snake_case => Snake case
526
+ * ex: camelCase => Camel case
527
+ * ex: PascalCase => Pascal case
528
+ * @param label
529
+ * @returns string
530
+ */
531
+ private getTitleForLabel(label: string): string {
532
+ return sentenceCase(noCase(label));
533
+ }
534
+
535
+ /**
536
+ * Transforms a list of model data for endpoints into corresponding
537
+ * navigation list items that are compatible with dx-sidebar.
538
+ * Compatible with transforming AMF data parsed from both RAML and OAS spec.
539
+ * Transforms a flat list of endpoints into a nested list based on indentation level
540
+ * for RAML spec.
541
+ * @param {Array<Object>} items An array of endpoints.
542
+ * @returns {array<Object>} List of navigation items
543
+ */
544
+ private assignEndpointNavItems(
545
+ parentReferencePath: string,
546
+ referenceId: string,
547
+ items: ParsedTopicModel[]
548
+ ): NavItem[] {
549
+ const methodList = [] as NavItem[];
550
+
551
+ items.forEach((item) => {
552
+ item.methods?.forEach((method) => {
553
+ const title =
554
+ this.getTitleForLabel(method.label!) || method.method;
555
+ const meta = this.addToMetadata(
556
+ parentReferencePath,
557
+ referenceId,
558
+ "method",
559
+ method,
560
+ title
561
+ );
562
+ methodList.push(
563
+ Object.assign(method, {
564
+ name: this.getReferencePathWithMeta(
565
+ parentReferencePath,
566
+ meta
567
+ ),
568
+ label: title
569
+ })
570
+ );
571
+ });
572
+ });
573
+ return methodList;
574
+ }
575
+
576
+ /**
577
+ * Gives URL path for reference items, Which can be used to push to the history
578
+ */
579
+ private getReferencePathWithMeta(
580
+ parentReferencePath: string,
581
+ meta: string
582
+ ): string {
583
+ // update the encoded url meta param
584
+ const encodedMeta = meta ? this.getUrlEncoded(meta) : "";
585
+ return encodedMeta ? `${parentReferencePath}?meta=${encodedMeta}` : "";
586
+ }
587
+
588
+ /**
589
+ * Assigns Navigation Items to dx-sidebar from the parsed AMF model.
590
+ * Adds each nav item to the Metadata by type.
591
+ * The 'summary' nav item has no children.
592
+ * The 'endpoint' nav item may have nested children.
593
+ */
594
+ private assignNavigationItemsFromAmf(
595
+ amfConfig: AmfConfig,
596
+ amfIdx: number
597
+ ): void {
598
+ const referenceId = amfConfig.id;
599
+ const parentReferencePath = amfConfig.href;
600
+ const model = this.amfMap[referenceId].parser.parsedModel;
601
+
602
+ const children: any[] = [];
603
+ const expandChildren = this.isExpandChildrenEnabled(referenceId);
604
+
605
+ NAVIGATION_ITEMS.forEach(
606
+ ({ label, name, childrenPropertyName, type }) => {
607
+ const indexedName = `${name}-${amfIdx}`;
608
+ switch (type) {
609
+ case "summary": {
610
+ const summary = model[type];
611
+ const meta = this.addToMetadata(
612
+ parentReferencePath,
613
+ referenceId,
614
+ type,
615
+ summary,
616
+ label
617
+ );
618
+ children.push({
619
+ label,
620
+ name: this.getReferencePathWithMeta(
621
+ parentReferencePath,
622
+ meta
623
+ )
624
+ });
625
+ break;
626
+ }
627
+ case "endpoint":
628
+ if (
629
+ model[childrenPropertyName!] &&
630
+ model[childrenPropertyName!].length
631
+ ) {
632
+ const amfTopicId = this.getFormattedIdentifier(
633
+ referenceId,
634
+ indexedName
635
+ );
636
+ const childTopics = this.assignEndpointNavItems(
637
+ parentReferencePath,
638
+ referenceId,
639
+ model[childrenPropertyName!]
640
+ );
641
+ children.push({
642
+ label,
643
+ name: this.getReferencePathWithMeta(
644
+ parentReferencePath,
645
+ this.metadata[amfTopicId]?.meta
646
+ ),
647
+ isExpanded: expandChildren,
648
+ children: childTopics
649
+ });
650
+ }
651
+ break;
652
+ case "security":
653
+ case "type":
654
+ if (model[childrenPropertyName!]?.length) {
655
+ // Sorting the types alphabetically
656
+ model[childrenPropertyName!].sort(
657
+ (typeA: any, typeB: any) => {
658
+ const typeALbl = typeA.label.toLowerCase();
659
+ const typeBLbl = typeB.label.toLowerCase();
660
+ return typeALbl < typeBLbl
661
+ ? -1
662
+ : typeALbl > typeBLbl
663
+ ? 1
664
+ : 0;
665
+ }
666
+ );
667
+ }
668
+ // eslint-disable-next-line no-fallthrough
669
+ default:
670
+ if (
671
+ model[childrenPropertyName!] &&
672
+ model[childrenPropertyName!].length
673
+ ) {
674
+ const amfTopicId = this.getFormattedIdentifier(
675
+ referenceId,
676
+ indexedName
677
+ );
678
+ children.push({
679
+ label,
680
+ name: this.getReferencePathWithMeta(
681
+ parentReferencePath,
682
+ this.metadata[amfTopicId]?.meta
683
+ ),
684
+ isExpanded: expandChildren,
685
+ children: model[childrenPropertyName!].map(
686
+ (topic: any) => {
687
+ const meta = this.addToMetadata(
688
+ parentReferencePath,
689
+ referenceId,
690
+ type,
691
+ topic,
692
+ topic.label
693
+ );
694
+ return {
695
+ label: topic.label,
696
+ name: this.getReferencePathWithMeta(
697
+ parentReferencePath,
698
+ meta
699
+ )
700
+ };
701
+ }
702
+ )
703
+ });
704
+ }
705
+ }
706
+ }
707
+ );
708
+
709
+ this.navigation[amfIdx] = {
710
+ ...this.navigation[amfIdx],
711
+ children,
712
+ isChildrenLoading: false
713
+ };
714
+ this.navigation = [...this.navigation];
715
+ }
716
+
717
+ protected addToMetadata(
718
+ parentReferencePath: string,
719
+ referenceId: string,
720
+ type: string,
721
+ topic: { id: string; domId: string },
722
+ navTitle: string
723
+ ): string {
724
+ const config = URL_CONFIG[type as keyof typeof URL_CONFIG];
725
+ const urlIdentifer = config.urlIdentifer;
726
+ let prefix = null;
727
+ if ("prefix" in config) {
728
+ prefix = config.prefix;
729
+ }
730
+
731
+ // encodeURI to avoid special characters in the URL meta.
732
+ const identifier =
733
+ urlIdentifer in topic &&
734
+ this.encodeIdentifier(topic[urlIdentifer as keyof typeof topic]);
735
+ let meta;
736
+ // Assuming that there will be an identifier always
737
+ if (identifier) {
738
+ meta = prefix ? `${prefix}${identifier}` : `${identifier}`;
739
+ this.metadata[meta] = {
740
+ parentReferencePath,
741
+ meta,
742
+ referenceId,
743
+ amfId: topic.id,
744
+ elementId: topic.domId,
745
+ identifier,
746
+ type,
747
+ navTitle
748
+ };
749
+ }
750
+ return meta!;
751
+ }
752
+
753
+ /**
754
+ * Returns metadata given route meta
755
+ */
756
+ protected getMetadataByUrlQuery(routeMeta: RouteMeta): AmfMetadataTopic {
757
+ return Object.values(this.metadata).find(
758
+ (metadata: AmfMetadataTopic) => {
759
+ return routeMeta.meta === metadata.meta;
760
+ }
761
+ )!;
762
+ }
763
+
764
+ /**
765
+ * Returns metadata given reference ID and topic amf ID
766
+ */
767
+ protected getMetadataByAmfId(
768
+ referenceId: string,
769
+ amfId: string
770
+ ): AmfMetadataTopic {
771
+ // Lets make a map based on the hash values so we don't need to loop like this.
772
+ return Object.values(this.metadata).find(
773
+ (metadata: AmfMetadataTopic) =>
774
+ referenceId === metadata.referenceId && amfId === metadata.amfId
775
+ )!;
776
+ }
777
+
778
+ /**
779
+ * Returns metadata given reference ID and topic identifier
780
+ */
781
+ protected getMetadataByIdentifier(
782
+ referenceId: string,
783
+ identifier: string
784
+ ): AmfMetadataTopic {
785
+ // Lets make a map based on the hash values so we don't need to loop like this.
786
+ return Object.values(this.metadata).find(
787
+ (metadata: AmfMetadataTopic) =>
788
+ referenceId === metadata.referenceId &&
789
+ identifier === metadata.identifier
790
+ )!;
791
+ }
792
+
793
+ /**
794
+ * Returns metadata given reference ID and topic type
795
+ */
796
+ protected getMetadataByType(
797
+ referenceId: string,
798
+ type: string
799
+ ): AmfMetadataTopic {
800
+ // Lets make a map based on the hash values so we don't need to loop like this.
801
+ return Object.values(this.metadata).find(
802
+ (metadata: AmfMetadataTopic) =>
803
+ referenceId === metadata.referenceId && type === metadata.type
804
+ )!;
805
+ }
806
+
807
+ /**
808
+ * Parses URL query params without decoding of params
809
+ */
810
+ private parseParams(params: string): qs.ParsedQuery<string> {
811
+ if (!params) {
812
+ return {};
813
+ }
814
+ return qs.parse(params, {
815
+ decode: false
816
+ });
817
+ }
818
+
819
+ /**
820
+ * Normalizes topic identifier by replacing spaces with '+'
821
+ * and running encodeURI() on it
822
+ * @param identifier raw identifier for a topic as parsed from the spec file
823
+ * @returns normalized and encoded identifier
824
+ */
825
+ protected encodeIdentifier(identifier: string): string {
826
+ let result = identifier.trim();
827
+ result = result.replace(new RegExp(/\s+/, "g"), "+");
828
+ return encodeURI(result);
829
+ }
830
+
831
+ /**
832
+ * Constructs a Reference Topic ID
833
+ */
834
+ protected getFormattedIdentifier(referenceId: string, id: string): string {
835
+ return `${referenceId}:${id}`;
836
+ }
837
+
838
+ protected updateUrlWithSelected(
839
+ parentReferencePath: string,
840
+ meta?: string
841
+ ): void {
842
+ if (meta) {
843
+ // update the encoded url meta param
844
+ const encodedMeta = this.getUrlEncoded(meta);
845
+
846
+ window.history.pushState(
847
+ {},
848
+ "",
849
+ `${parentReferencePath}?meta=${encodedMeta}`
850
+ );
851
+ }
852
+ }
853
+
854
+ /**
855
+ * Does a replace on the URL meta, so it does not create a history entry.
856
+ */
857
+ protected replaceUrlWithSelected(
858
+ parentReferencePath: string,
859
+ meta?: string
860
+ ): void {
861
+ if (meta) {
862
+ // update the encoded url meta param
863
+ const encodedMeta = this.getUrlEncoded(meta);
864
+
865
+ window.history.replaceState(
866
+ window.history.state,
867
+ "",
868
+ `${parentReferencePath}?meta=${encodedMeta}`
869
+ );
870
+ }
871
+ }
872
+
873
+ /**
874
+ * This method gets called when the user navigates back and forth using browser arrows
875
+ * Updates content depending on the type of reference - spec based or markdown
876
+ */
877
+ protected updateSelectedItemFromUrlQuery(): void {
878
+ const specBasedReference = this.isSpecBasedReference(
879
+ this._currentReferenceId
880
+ );
881
+ if (specBasedReference) {
882
+ const currentMeta: RouteMeta | undefined =
883
+ this.getReferenceMetaInfo(window.location.href);
884
+ const metadata =
885
+ currentMeta && this.getMetadataByUrlQuery(currentMeta);
886
+ if (metadata) {
887
+ const {
888
+ parentReferencePath,
889
+ referenceId,
890
+ amfId,
891
+ type,
892
+ elementId
893
+ }: AmfMetadataTopic = metadata;
894
+ this.loadSpecReferenceContent(
895
+ parentReferencePath,
896
+ referenceId,
897
+ amfId,
898
+ type,
899
+ elementId,
900
+ currentMeta.meta
901
+ );
902
+ }
903
+ } else {
904
+ this.loadMarkdownBasedReference();
905
+ }
906
+
907
+ restoreScroll(); // don't try this at home kids
908
+ }
909
+
910
+ /**
911
+ * The API Navigation event will always intend to navigate within the current reference
912
+ * @param event
913
+ */
914
+ protected onApiNavigationChanged(): void {
915
+ const specBasedReference = this.isSpecBasedReference(
916
+ this._currentReferenceId
917
+ );
918
+ if (specBasedReference) {
919
+ const { meta } = this.selectedTopic!;
920
+ const metadata = this.metadata[meta];
921
+ if (metadata) {
922
+ const {
923
+ parentReferencePath,
924
+ referenceId,
925
+ amfId,
926
+ type,
927
+ elementId
928
+ }: AmfMetadataTopic = metadata;
929
+ this.loadSpecReferenceContent(
930
+ parentReferencePath,
931
+ referenceId,
932
+ amfId,
933
+ type,
934
+ elementId,
935
+ metadata.meta
936
+ );
937
+ }
938
+ } else {
939
+ this.loadMarkdownBasedReference();
940
+ }
941
+ }
942
+
943
+ /**
944
+ * Updates the currently selected amf and topic
945
+ */
946
+ protected loadSpecReferenceContent(
947
+ parentReferencePath: string,
948
+ referenceId: string,
949
+ amfId: string,
950
+ type: string,
951
+ elementId: string,
952
+ meta: string
953
+ ): void {
954
+ this.selectedTopic = {
955
+ referenceId,
956
+ parentReferencePath,
957
+ amfId,
958
+ elementId,
959
+ type,
960
+ meta
961
+ };
962
+ this.selectedSidebarValue = this.getReferencePathWithMeta(
963
+ parentReferencePath,
964
+ meta
965
+ );
966
+
967
+ this.handleSelectedItem();
968
+
969
+ this.updateDocPhase();
970
+ }
971
+
972
+ /**
973
+ * Updates doc phase of selected reference
974
+ */
975
+ updateDocPhase(): void {
976
+ /* If parent level doc phase is enabled, Individual reference level doc phase should not be considered */
977
+
978
+ if (!this.isParentLevelDocPhaseEnabled) {
979
+ const selectedReference = this._amfConfigList.find(
980
+ (referenceItem: AmfConfig) => {
981
+ return referenceItem.id === this._currentReferenceId;
982
+ }
983
+ );
984
+ if (selectedReference) {
985
+ this.selectedReferenceDocPhase = JSON.stringify(
986
+ selectedReference.docPhase
987
+ );
988
+ }
989
+ }
990
+ }
991
+
992
+ /**
993
+ * Returns the decoded meta query param from given Url as it is being used internally.
994
+ */
995
+ getMetaFromUrl(referenceUrl: string): string {
996
+ const indexOfQueryParam = referenceUrl.indexOf("?");
997
+ const urlPath = referenceUrl.substring(
998
+ indexOfQueryParam >= 0 ? indexOfQueryParam : referenceUrl.length
999
+ );
1000
+ const meta = this.parseParams(urlPath).meta as string;
1001
+ // Always get the meta query param encoded and decode it and store it for internal use
1002
+ // This has 2 advantages,
1003
+ // 1. Supports backward compatible meta query param, so there is no need for redirects.
1004
+ // 2. Supports Prerender and Coveo for their crawling.
1005
+ const encodedMeta = meta && this.getUrlEncoded(meta);
1006
+ const decodedMeta = encodedMeta && decodeURIComponent(encodedMeta);
1007
+ return decodedMeta || "";
1008
+ }
1009
+
1010
+ /**
1011
+ *
1012
+ * @returns meta for given markdown based referenceUrl
1013
+ * Consider last topic url in ../references/reference-name/example.html
1014
+ */
1015
+ getMarkdownReferenceMeta(referenceUrl: string): string {
1016
+ let meta = "";
1017
+ if (referenceUrl) {
1018
+ const slashSeparatorItems = referenceUrl.split("/");
1019
+ const lastItem =
1020
+ slashSeparatorItems[slashSeparatorItems.length - 1];
1021
+ if (lastItem.endsWith(".html")) {
1022
+ meta = lastItem;
1023
+ }
1024
+ }
1025
+ return meta;
1026
+ }
1027
+
1028
+ /**
1029
+ * Gets the encoded url.
1030
+ * This method will return the encoded url for 2 cases,
1031
+ * 1. If the url is encoded already
1032
+ * 2. If the url is decoded
1033
+ */
1034
+ getUrlEncoded(url: string): string {
1035
+ // if url matches, then return the encoded url.
1036
+ if (decodeURIComponent(url) === url) {
1037
+ return encodeURIComponent(url);
1038
+ }
1039
+ // return the encoded url.
1040
+ return this.getUrlEncoded(decodeURIComponent(url));
1041
+ }
1042
+
1043
+ /**
1044
+ *
1045
+ * @returns RouteMeta object for given referenceUrl
1046
+ * referenceId - gets referenceId from url
1047
+ * For spec based references gets meta parm from url and then topicId & type from meta
1048
+ * For markdown based references gets topicId as last html path in the name, meta & type will be empty
1049
+ */
1050
+ getReferenceMetaInfo(referenceUrl: string | null): RouteMeta | undefined {
1051
+ let metaReferenceInfo;
1052
+ if (referenceUrl) {
1053
+ const referenceId = this.getReferenceIdFromUrl(referenceUrl);
1054
+ let meta = "";
1055
+ let topicId = "";
1056
+ let type = "";
1057
+ if (this.isSpecBasedReference(referenceId)) {
1058
+ meta = this.getMetaFromUrl(referenceUrl);
1059
+ if (meta) {
1060
+ if (meta.includes(":")) {
1061
+ const metaInfo = meta.split(":");
1062
+ type = metaInfo[0];
1063
+ topicId = metaInfo[1] || type;
1064
+ } else {
1065
+ topicId = meta;
1066
+ }
1067
+ }
1068
+ } else {
1069
+ topicId = this.getMarkdownReferenceMeta(referenceUrl);
1070
+ }
1071
+ metaReferenceInfo = {
1072
+ referenceId,
1073
+ meta,
1074
+ topicId,
1075
+ type
1076
+ };
1077
+ }
1078
+ return metaReferenceInfo;
1079
+ }
1080
+
1081
+ /**
1082
+ * Finds and returns referenceUrl and topicTitle if given topic url matches
1083
+ */
1084
+ getReferenceDetailsInGivenTopics(
1085
+ topics: ParsedMarkdownTopic[],
1086
+ topicMeta: string
1087
+ ): { referenceUrl: string; topicTitle: string } {
1088
+ let referenceUrl = "";
1089
+ let topicTitle = "";
1090
+ for (let i = 0; i < topics.length; i++) {
1091
+ const topic = topics[i];
1092
+ const meta = this.getMarkdownReferenceMeta(topic.link!.href);
1093
+ const childTopics = topic.children;
1094
+ if (meta === topicMeta) {
1095
+ referenceUrl = topic.link!.href;
1096
+ topicTitle = topic.label;
1097
+ } else if (childTopics && childTopics.length) {
1098
+ const referenceDetails = this.getReferenceDetailsInGivenTopics(
1099
+ childTopics,
1100
+ topicMeta
1101
+ );
1102
+ referenceUrl = referenceDetails.referenceUrl;
1103
+ topicTitle = referenceDetails.topicTitle;
1104
+ }
1105
+ if (referenceUrl && topicTitle) {
1106
+ break;
1107
+ }
1108
+ }
1109
+ return {
1110
+ referenceUrl,
1111
+ topicTitle
1112
+ };
1113
+ }
1114
+
1115
+ /**
1116
+ * Gives referenceUrl and topicTitle for given markdown topic url
1117
+ */
1118
+ getRefDetailsForGivenTopicMeta(
1119
+ referenceId: string,
1120
+ topicMeta: string
1121
+ ): { referenceUrl: string; topicTitle: string } | undefined {
1122
+ const amfConfig = this.getAmfConfigWithId(referenceId);
1123
+ let referenceDetails;
1124
+ if (amfConfig) {
1125
+ const topics = amfConfig.topic?.children || [];
1126
+ referenceDetails = this.getReferenceDetailsInGivenTopics(
1127
+ topics,
1128
+ topicMeta
1129
+ );
1130
+ }
1131
+ return referenceDetails;
1132
+ }
1133
+
1134
+ /**
1135
+ * Updates the DOM on the first load
1136
+ */
1137
+ updateView(): void {
1138
+ const previousRefUrlInSession = window.sessionStorage.getItem(
1139
+ this.docsReferenceUrlSessionKey
1140
+ );
1141
+ window.sessionStorage.removeItem(this.docsReferenceUrlSessionKey);
1142
+ let previousRefInfo = this.getReferenceMetaInfo(
1143
+ previousRefUrlInSession
1144
+ );
1145
+
1146
+ // For spec based reference, We should consider urlData to show same topic when user reloads after navigating to specific topic
1147
+ if (!previousRefInfo) {
1148
+ const currentUrl = window.location.href;
1149
+ const urlReferenceId = this.getReferenceIdFromUrl(currentUrl);
1150
+ if (urlReferenceId && this.isSpecBasedReference(urlReferenceId)) {
1151
+ if (
1152
+ !this.isProjectRootPath() &&
1153
+ !this.isParentReferencePath(currentUrl)
1154
+ ) {
1155
+ previousRefInfo = this.getReferenceMetaInfo(currentUrl);
1156
+ }
1157
+ }
1158
+ }
1159
+
1160
+ let referenceId: string;
1161
+ let topicId = "";
1162
+
1163
+ if (
1164
+ previousRefInfo &&
1165
+ this._amfConfigMap.has(previousRefInfo.referenceId)
1166
+ ) {
1167
+ referenceId = previousRefInfo.referenceId;
1168
+ topicId = previousRefInfo.topicId;
1169
+ } else {
1170
+ referenceId = this._currentReferenceId;
1171
+ }
1172
+
1173
+ const specBasedReference = this.isSpecBasedReference(referenceId);
1174
+ if (specBasedReference) {
1175
+ // Wait till the AMF is loaded.
1176
+ this.amfFetchPromiseMap[referenceId].then(() => {
1177
+ let selectedItemMetaData = this.getMetadataByIdentifier(
1178
+ referenceId,
1179
+ topicId
1180
+ );
1181
+ if (!selectedItemMetaData) {
1182
+ // Doesn't exist, let's use the summary.
1183
+ selectedItemMetaData = this.getMetadataByType(
1184
+ referenceId,
1185
+ "summary"
1186
+ );
1187
+ }
1188
+
1189
+ if (selectedItemMetaData) {
1190
+ this.loadSpecReferenceContent(
1191
+ selectedItemMetaData.parentReferencePath,
1192
+ selectedItemMetaData.referenceId,
1193
+ selectedItemMetaData.amfId,
1194
+ selectedItemMetaData.type,
1195
+ "",
1196
+ selectedItemMetaData.meta
1197
+ );
1198
+
1199
+ this.updateUrlWithSelected(
1200
+ selectedItemMetaData.parentReferencePath,
1201
+ selectedItemMetaData.meta
1202
+ );
1203
+ this.updateTags(selectedItemMetaData.navTitle);
1204
+ }
1205
+ });
1206
+ } else {
1207
+ let invalidTopicReferenceUrl: string | null = "";
1208
+ if (topicId) {
1209
+ const referenceDetails = this.getRefDetailsForGivenTopicMeta(
1210
+ referenceId,
1211
+ topicId
1212
+ );
1213
+ const selectedItemUrl = referenceDetails?.referenceUrl;
1214
+ if (!selectedItemUrl) {
1215
+ invalidTopicReferenceUrl = previousRefUrlInSession;
1216
+ }
1217
+ }
1218
+ this.loadMarkdownBasedReference(invalidTopicReferenceUrl);
1219
+ }
1220
+ }
1221
+
1222
+ /**
1223
+ * Navigates to reference of the given URL
1224
+ * @param url
1225
+ */
1226
+ private loadNewReferenceItem(url: string): void {
1227
+ const referenceId = this.getReferenceIdFromUrl(url);
1228
+ const referenceItem = this.getAmfConfigWithId(referenceId);
1229
+ if (referenceItem) {
1230
+ window.location.href = referenceItem.href;
1231
+ }
1232
+ }
1233
+
1234
+ /**
1235
+ * @param referenceUrl to which user wants to navigate
1236
+ * Redirect to first sub item if it's root level item, otherwise content will be loaded
1237
+ * Push the history as a first child item
1238
+ * set selected sidebar value as a pathname
1239
+ */
1240
+
1241
+ private loadMarkdownBasedReference(referenceUrl?: string | null): void {
1242
+ // MILES TODO: figure out if we ever need to log a coveo page view in here
1243
+ // this would be the case if at some point we 'load' a new 'markdown based reference'
1244
+ // without actually triggering a page load
1245
+ let referenceId = "";
1246
+ const currentUrl = window.location.href;
1247
+ if (this.isProjectRootPath()) {
1248
+ /**
1249
+ * CASE1: This case is to consider when the user navigates to references by clicking a project card
1250
+ * Ex: /docs/example-project/references should navigate to the first topic in the first reference
1251
+ */
1252
+ referenceId = this._currentReferenceId;
1253
+ } else if (this.isParentReferencePath(referenceUrl)) {
1254
+ /**
1255
+ * CASE2: This case is to navigate to respective reference when the user clicked on root item
1256
+ * Ex: .../references/markdown-ref should navigate to first topic.
1257
+ */
1258
+ referenceId = this.getReferenceIdFromUrl(referenceUrl!);
1259
+ } else if (this.isParentReferencePath(currentUrl)) {
1260
+ /**
1261
+ * CASE3: This case is to navigate to respective reference when the user entered url with reference id
1262
+ * Ex: .../references/markdown-ref should navigate to first topic.
1263
+ */
1264
+ referenceId = this.getReferenceIdFromUrl(currentUrl);
1265
+ } else if (referenceUrl) {
1266
+ /**
1267
+ * CASE4: This case is to navigate to first item when we don't have topic in the selected version
1268
+ * Ex: .../references/markdown-ref/not-existed-topic-url should navigate to first topic.
1269
+ */
1270
+ const referenceMeta = this.getMarkdownReferenceMeta(referenceUrl);
1271
+ const selectedItemRefId = this.getReferenceIdFromUrl(referenceUrl);
1272
+ const referenceDetails = this.getRefDetailsForGivenTopicMeta(
1273
+ selectedItemRefId,
1274
+ referenceMeta
1275
+ );
1276
+ const selectedItemUrl = referenceDetails?.referenceUrl;
1277
+ if (!selectedItemUrl) {
1278
+ referenceId = this.getReferenceIdFromUrl(referenceUrl);
1279
+ }
1280
+ }
1281
+
1282
+ let isRedirecting = false;
1283
+ if (referenceId) {
1284
+ const amfConfig = this.getAmfConfigWithId(referenceId);
1285
+ let redirectReferenceUrl = "";
1286
+ if (amfConfig) {
1287
+ const childrenItems = amfConfig.topic!.children;
1288
+ if (childrenItems.length > 0) {
1289
+ redirectReferenceUrl = childrenItems[0].link!.href;
1290
+ }
1291
+ }
1292
+ if (redirectReferenceUrl) {
1293
+ if (this.isParentReferencePath(referenceUrl)) {
1294
+ // This is for CASE2 mentioned above, Where we need to navigate user to respective href
1295
+ isRedirecting = true;
1296
+ window.location.href = redirectReferenceUrl;
1297
+ } else {
1298
+ // This is for CASE 1,3 and 4 mentioned above, Where we need to update the browser history
1299
+ window.history.replaceState(
1300
+ window.history.state,
1301
+ "",
1302
+ redirectReferenceUrl
1303
+ );
1304
+ }
1305
+ }
1306
+ }
1307
+ if (!isRedirecting) {
1308
+ const currentReferenceUrl = window.location.href;
1309
+ const referenceMeta =
1310
+ this.getMarkdownReferenceMeta(currentReferenceUrl);
1311
+ const selectedItemRefId =
1312
+ this.getReferenceIdFromUrl(currentReferenceUrl);
1313
+ const referenceDetails = this.getRefDetailsForGivenTopicMeta(
1314
+ selectedItemRefId,
1315
+ referenceMeta
1316
+ );
1317
+ if (referenceDetails) {
1318
+ this.updateTags(referenceDetails.topicTitle);
1319
+ }
1320
+
1321
+ this.versions = this.getVersions();
1322
+ if (this.oldVersionInfo) {
1323
+ this.showVersionBanner = true;
1324
+ } else {
1325
+ this.latestVersion = true;
1326
+ }
1327
+
1328
+ this.isVersionFetched = true;
1329
+ this.updateDocPhase();
1330
+ this.selectedSidebarValue = window.location.pathname;
1331
+ }
1332
+ }
1333
+
1334
+ /**
1335
+ * Currently, used to handle the version change and store the current reference Url.
1336
+ */
1337
+ handleVersionChange(): void {
1338
+ const currentUrl = window.location.href;
1339
+ window.sessionStorage.setItem(
1340
+ this.docsReferenceUrlSessionKey,
1341
+ currentUrl
1342
+ );
1343
+ }
1344
+
1345
+ handleDismissVersionBanner() {
1346
+ this.showVersionBanner = false;
1347
+ }
1348
+
1349
+ private updateTags(navTitle = ""): void {
1350
+ if (!navTitle) {
1351
+ return;
1352
+ }
1353
+
1354
+ // this is required to update the nav title meta tag.
1355
+ // eslint-disable-next-line @lwc/lwc/no-document-query
1356
+ const metaNavTitle = document.querySelector('meta[name="nav-title"]');
1357
+ // eslint-disable-next-line @lwc/lwc/no-document-query
1358
+ const titleTag = document.querySelector("title");
1359
+ const TITLE_SEPARATOR = " | ";
1360
+
1361
+ if (metaNavTitle) {
1362
+ metaNavTitle.setAttribute("content", navTitle);
1363
+ }
1364
+
1365
+ /**
1366
+ * Right now, the title tag only changes when you pick a Ref spec,
1367
+ * not every time you choose a subsection of the Ref spec.
1368
+ * This update aims to refresh the title tag with each selection.
1369
+ * If a Ref spec is chosen, we add the value of the <selected topic> to the title.
1370
+ * If a subsection is selected, we update the first part of the current
1371
+ * title with the new <selected topic>.
1372
+ * Example: Following is a sample project structure.
1373
+ * - Project Name
1374
+ * - Ref Spec1
1375
+ * - Summary
1376
+ * - Endpoints
1377
+ * - E1
1378
+ * - E2
1379
+ * - Ref Spec2
1380
+ * - Summary
1381
+ * - Endpoints
1382
+ * - E1 (Selected)
1383
+ * - E2
1384
+ * Previous Title: Ref Spec2 | Project Name | Salesforce Developer
1385
+ * New Title: E1 | Ref Spec2 | Project Name | Salesforce Developer
1386
+ *
1387
+ */
1388
+ if (titleTag) {
1389
+ let titleTagValue = titleTag.textContent;
1390
+ const titleTagSectionValues: string[] =
1391
+ titleTagValue?.split(TITLE_SEPARATOR);
1392
+ if (titleTagSectionValues) {
1393
+ if (titleTagSectionValues.length <= 3) {
1394
+ titleTagValue = navTitle + TITLE_SEPARATOR + titleTagValue;
1395
+ } else {
1396
+ titleTagSectionValues[0] = navTitle;
1397
+ titleTagValue = titleTagSectionValues.join(TITLE_SEPARATOR);
1398
+ }
1399
+ }
1400
+ titleTag.textContent = titleTagValue;
1401
+ }
1402
+ }
1403
+
1404
+ onNavSelect(event: CustomEvent): void {
1405
+ const name = event.detail.name;
1406
+ if (name) {
1407
+ const urlReferenceId = this.getReferenceIdFromUrl(name);
1408
+ const specBasedReference =
1409
+ this.isSpecBasedReference(urlReferenceId);
1410
+ if (specBasedReference) {
1411
+ const metaVal = this.getMetaFromUrl(name);
1412
+ const currentSelectedMeta = this.selectedTopic
1413
+ ? this.selectedTopic.meta
1414
+ : "";
1415
+
1416
+ if (metaVal && metaVal === currentSelectedMeta) {
1417
+ // selecting the same nav item, skip update
1418
+ return;
1419
+ }
1420
+
1421
+ const metadata = this.metadata[metaVal];
1422
+ if (metadata) {
1423
+ const {
1424
+ parentReferencePath,
1425
+ referenceId,
1426
+ amfId,
1427
+ type,
1428
+ elementId
1429
+ } = metadata;
1430
+ this.loadSpecReferenceContent(
1431
+ parentReferencePath,
1432
+ referenceId,
1433
+ amfId,
1434
+ type,
1435
+ elementId,
1436
+ metaVal
1437
+ );
1438
+
1439
+ logCoveoPageView(
1440
+ this.coveoOrganizationId,
1441
+ this.coveoAnalyticsToken
1442
+ );
1443
+ this.updateUrlWithSelected(parentReferencePath, metaVal);
1444
+ this.updateTags(metadata.navTitle);
1445
+ } else {
1446
+ if (this.isParentReferencePath(name)) {
1447
+ this.loadNewReferenceItem(name);
1448
+ }
1449
+ }
1450
+ } else {
1451
+ this.loadMarkdownBasedReference(name);
1452
+ }
1453
+ }
1454
+ }
1455
+
1456
+ onExpandCollapse(event: CustomEvent): void {
1457
+ const { name, isSelectAction, isExpanded } = event.detail;
1458
+ if (!isSelectAction && isExpanded) {
1459
+ const referenceId = this.getReferenceIdFromUrl(name);
1460
+ const currentUrl = window.location.href;
1461
+ const currentReferenceId = this.getReferenceIdFromUrl(currentUrl);
1462
+ //No need to do anything if user is expanding currently selected reference
1463
+ if (referenceId !== currentReferenceId) {
1464
+ const isSpecBasedReference =
1465
+ this.isSpecBasedReference(referenceId);
1466
+ if (isSpecBasedReference) {
1467
+ // Perform functionality same as item selection
1468
+ this.onNavSelect(event);
1469
+ }
1470
+ }
1471
+ }
1472
+ }
1473
+
1474
+ handleSelectedItem(): void {
1475
+ // update topic view
1476
+ const { referenceId, amfId, type } = this.selectedTopic!;
1477
+
1478
+ // Adding stringify inside try/catch
1479
+ let amfModelString = "";
1480
+ try {
1481
+ amfModelString = JSON.stringify(this.amfMap[referenceId].model);
1482
+ } catch (error) {
1483
+ console.error(`Error stringifying amf model: ${error}`);
1484
+ }
1485
+
1486
+ // This updates the component in the content section.
1487
+ this.topicModel = {
1488
+ type,
1489
+ amf: amfModelString,
1490
+ parser: this.amfMap[referenceId].parser,
1491
+ id: amfId
1492
+ };
1493
+ }
1494
+ }