@salesforcedevs/docs-components 0.17.0 → 0.17.12-search-alpha

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