@salesforcedevs/docs-components 0.53.3 → 0.54.0-alpha02

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 (39) hide show
  1. package/lwc.config.json +6 -2
  2. package/package.json +16 -2
  3. package/src/modules/doc/amfReference/amfReference.css +5 -0
  4. package/src/modules/doc/amfReference/amfReference.html +39 -0
  5. package/src/modules/doc/amfReference/amfReference.ts +874 -0
  6. package/src/modules/doc/amfReference/route-meta.ts +22 -0
  7. package/src/modules/doc/amfReference/types.ts +88 -0
  8. package/src/modules/doc/amfReference/utils.ts +669 -0
  9. package/src/modules/doc/amfTopic/amfTopic.css +1 -0
  10. package/src/modules/doc/amfTopic/amfTopic.html +3 -0
  11. package/src/modules/doc/amfTopic/amfTopic.ts +94 -0
  12. package/src/modules/doc/amfTopic/types.ts +54 -0
  13. package/src/modules/doc/amfTopic/utils.ts +117 -0
  14. package/src/modules/doc/breadcrumbItem/breadcrumbItem.css +2 -2
  15. package/src/modules/doc/breadcrumbs/breadcrumbs.css +2 -2
  16. package/src/modules/doc/breadcrumbs/breadcrumbs.ts +1 -1
  17. package/src/modules/doc/content/content.css +4 -4
  18. package/src/modules/doc/content/content.ts +1 -1
  19. package/src/modules/doc/contentCallout/contentCallout.css +3 -3
  20. package/src/modules/doc/contentLayout/contentLayout.css +100 -0
  21. package/src/modules/doc/contentLayout/contentLayout.html +55 -0
  22. package/src/modules/doc/contentLayout/contentLayout.ts +242 -0
  23. package/src/modules/doc/header/header.css +1 -1
  24. package/src/modules/doc/header/header.ts +2 -2
  25. package/src/modules/doc/headingAnchor/headingAnchor.css +1 -1
  26. package/src/modules/doc/headingContent/headingContent.css +1 -1
  27. package/src/modules/doc/phase/phase.css +3 -3
  28. package/src/modules/doc/phase/phase.ts +1 -1
  29. package/src/modules/doc/xmlContent/types.ts +119 -0
  30. package/src/modules/doc/xmlContent/utils.ts +163 -0
  31. package/src/modules/doc/xmlContent/xmlContent.css +25 -0
  32. package/src/modules/doc/xmlContent/xmlContent.html +34 -0
  33. package/src/modules/doc/xmlContent/xmlContent.ts +553 -0
  34. package/src/modules/docBaseElements/lightningElementWithState/lightningElementWithState.ts +93 -0
  35. package/src/modules/docHelpers/amfStyle/amfStyle.css +390 -0
  36. package/src/modules/docHelpers/phaseContentLayout/phaseContentLayout.css +37 -0
  37. package/src/modules/{helpers → docHelpers}/status/status.css +0 -0
  38. package/src/modules/docUtils/SearchSyncer/SearchSyncer.ts +80 -0
  39. package/LICENSE +0 -12
@@ -0,0 +1,874 @@
1
+ import { LightningElement, api, track } from "lwc";
2
+ import qs from "query-string";
3
+ import type {
4
+ AmfConfig,
5
+ AmfMetadataTopic,
6
+ AmfModel,
7
+ AmfModelRecord,
8
+ NavItem,
9
+ ParsedTopicModel,
10
+ TopicModel,
11
+ ReferenceVersion,
12
+ ReferenceSetConfig,
13
+ AmfMetaTopicType
14
+ } from "./types";
15
+ import { AmfModelParser } from "./utils";
16
+ import { RouteMeta } from "./route-meta";
17
+
18
+ const NAVIGATION_ITEMS = [
19
+ {
20
+ label: "Summary",
21
+ name: "summary",
22
+ childrenPropertyName: undefined,
23
+ type: "summary"
24
+ },
25
+ {
26
+ label: "Endpoints",
27
+ name: "endpoints",
28
+ childrenPropertyName: "endpoints",
29
+ type: "endpoint"
30
+ },
31
+ {
32
+ label: "Documentation",
33
+ name: "documentation",
34
+ childrenPropertyName: "docs",
35
+ type: "documentation"
36
+ },
37
+ {
38
+ label: "Types",
39
+ name: "types",
40
+ childrenPropertyName: "types",
41
+ type: "type"
42
+ },
43
+ {
44
+ label: "Security",
45
+ name: "security",
46
+ childrenPropertyName: "security",
47
+ type: "security"
48
+ }
49
+ ];
50
+
51
+ const URL_CONFIG = {
52
+ summary: {
53
+ urlIdentifer: "label"
54
+ },
55
+ endpoint: {
56
+ urlIdentifer: "path"
57
+ },
58
+ method: {
59
+ urlIdentifer: "label"
60
+ },
61
+ documentation: {
62
+ urlIdentifer: "label"
63
+ },
64
+ type: {
65
+ urlIdentifer: "label",
66
+ prefix: "type:"
67
+ },
68
+ security: {
69
+ urlIdentifer: "label",
70
+ prefix: "security:"
71
+ }
72
+ };
73
+
74
+ const urlHashToMetaRedirectMap = {
75
+ "commerce-api-assignments:Summary": "assignments:Summary",
76
+ "commerce-api-campaigns:Summary": "campaigns:Summary",
77
+ "commerce-api-catalogs:Summary": "catalogs:Summary",
78
+ "cdn-zones:Summary": "cdn-api-process-apis:Summary",
79
+ "inventory-impex:Summary": "impex:Summary",
80
+ "inventory-reservations:Summary": "inventory-reservation-service:Summary",
81
+ "shopper-login-and-api-access-service:Summary": "shopper-login:Summary",
82
+ "shopper-login-and-api-access-service-admin:Summary": "slas-admin:Summary",
83
+ "einstein-recommendations:Summary": "einstein-api-quick-start-guide:Summary"
84
+ };
85
+
86
+ export default class AmfReference extends LightningElement {
87
+ @api breadcrumbs?: string = null;
88
+ @api sidebarHeader: string;
89
+ @api coveoOrganizationId!: string;
90
+ @api coveoPublicAccessToken!: string;
91
+ @api coveoAdvancedQueryConfig!: string;
92
+ @api coveoSearchHub!: string;
93
+ @api useOldSidebar?: boolean = false;
94
+
95
+ // Update this to update what component gets rendered in the content block
96
+ @track
97
+ protected topicModel: TopicModel;
98
+
99
+ get isVersionEnabled(): boolean {
100
+ return !!this._referenceSetConfig?.versions?.length;
101
+ }
102
+
103
+ @api
104
+ get referenceSetConfig(): ReferenceSetConfig {
105
+ return this._referenceSetConfig;
106
+ }
107
+
108
+ set referenceSetConfig(value: ReferenceSetConfig) {
109
+ // No change, do nothing.
110
+ if (value === this._referenceSetConfig) {
111
+ return;
112
+ }
113
+
114
+ try {
115
+ const refConfig =
116
+ typeof value === "string" ? JSON.parse(value) : value;
117
+ if (!(<ReferenceSetConfig>refConfig).versions) {
118
+ return;
119
+ }
120
+ this._referenceSetConfig = refConfig;
121
+ } catch (e) {
122
+ this._referenceSetConfig = {
123
+ refList: []
124
+ };
125
+ }
126
+
127
+ if (this.isVersionEnabled) {
128
+ const selectedVersion = this.getSelectedVersion();
129
+ this.versionToRefMap = this._referenceSetConfig.versionToRefMap;
130
+ // if version is not available, then show empty - this will be the default behaviour
131
+ this._amfConfig = this.versionToRefMap[selectedVersion.id] || [];
132
+ this.versions = this._referenceSetConfig.versions;
133
+ this.selectedVersion = selectedVersion;
134
+ } else {
135
+ this._amfConfig = this._referenceSetConfig.refList;
136
+ }
137
+
138
+ this.updateAmfConfigInView();
139
+ }
140
+
141
+ @api
142
+ get docPhaseInfo() {
143
+ return this.selectedReferenceDocPhase;
144
+ }
145
+
146
+ set docPhaseInfo(value: string) {
147
+ if (value) {
148
+ this.isParentLevelDocPhaseEnabled = true;
149
+ this.selectedReferenceDocPhase = value;
150
+ }
151
+ }
152
+
153
+ // model
154
+ protected _amfConfig: AmfConfig[] = [];
155
+ protected _referenceSetConfig: ReferenceSetConfig;
156
+ protected _currentReferenceId = "";
157
+
158
+ protected amfMap: Record<string, AmfModelRecord> = {};
159
+ protected amfFetchPromiseMap = {};
160
+ protected metadata: { [key: string]: AmfMetadataTopic } = {};
161
+ protected selectedTopic: AmfMetaTopicType = undefined;
162
+ protected navigation = [];
163
+ protected selectedSidebarValue = undefined;
164
+ protected versions: Array<ReferenceVersion> = [];
165
+ protected selectedVersion: ReferenceVersion = null;
166
+
167
+ private hasRendered = false;
168
+ private navAmfOrder = [];
169
+
170
+ private versionToRefMap: Map<string, Array<AmfConfig>>;
171
+
172
+ private isParentLevelDocPhaseEnabled = false;
173
+ private selectedReferenceDocPhase?: string = null;
174
+
175
+ /**
176
+ * Key for storing the currently selected reference meta query param. This will be used to save the
177
+ * previously selected reference meta and restoring it when changing between reference versions.
178
+ */
179
+ private readonly docsReferenceMetaSessionKey: string = "docsReferenceMeta";
180
+
181
+ _boundOnApiNavigationChanged;
182
+ _boundUpdateSelectedItemFromUrlQuery;
183
+
184
+ get amfConfigMapped(): AmfConfig[] {
185
+ return this._amfConfig.map((config) => {
186
+ return config.id in this.amfMap
187
+ ? Object.assign(config, this.amfMap[config.id])
188
+ : config;
189
+ });
190
+ }
191
+
192
+ constructor() {
193
+ super();
194
+ this._boundOnApiNavigationChanged =
195
+ this.onApiNavigationChanged.bind(this);
196
+ this._boundUpdateSelectedItemFromUrlQuery =
197
+ this.updateSelectedItemFromUrlQuery.bind(this);
198
+ }
199
+
200
+ connectedCallback(): void {
201
+ this.addEventListener(
202
+ "api-navigation-selection-changed",
203
+ this._boundOnApiNavigationChanged
204
+ );
205
+ window.addEventListener(
206
+ "popstate",
207
+ this._boundUpdateSelectedItemFromUrlQuery
208
+ );
209
+ }
210
+
211
+ disconnectedCallback(): void {
212
+ this.removeEventListener(
213
+ "api-navigation-selection-changed",
214
+ this._boundOnApiNavigationChanged
215
+ );
216
+ window.removeEventListener(
217
+ "popstate",
218
+ this._boundUpdateSelectedItemFromUrlQuery
219
+ );
220
+ }
221
+
222
+ renderedCallback(): void {
223
+ if (!this.hasRendered) {
224
+ this.hasRendered = true;
225
+ if (this._amfConfig && this._amfConfig.length) {
226
+ // If amfConfig has a value and length, it is assumed that fetch
227
+ // has already been called and promises stored.
228
+ this.updateView();
229
+ }
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Returns the selected version or the first available version.
235
+ */
236
+ private getSelectedVersion(): ReferenceVersion {
237
+ const versions = this._referenceSetConfig?.versions || [];
238
+ const selectedVersion = versions.find(
239
+ (v: ReferenceVersion) => v.selected
240
+ );
241
+ // return a selected version if there is one, else return the first one.
242
+ return selectedVersion || (versions.length && versions[0]);
243
+ }
244
+
245
+ private updateAmfConfigInView(): void {
246
+ if (this._amfConfig && this._amfConfig.length) {
247
+ // fetch AMF Json as soon as config is set
248
+ this.fetchAllAmf();
249
+ // update() must be called after renderedCallback.
250
+ if (this.hasRendered) {
251
+ this.updateView();
252
+ }
253
+ }
254
+ }
255
+
256
+ private async fetchAmf(amfConfig): Promise<AmfModel | AmfModel[]> {
257
+ const { amf } = amfConfig;
258
+ const response = await fetch(amf, {
259
+ headers: {
260
+ "Cache-Control": `max-age=86400`
261
+ }
262
+ });
263
+ const json = await response.json();
264
+ return json;
265
+ }
266
+
267
+ /**
268
+ * Calls the fetch for each AMF in the config.
269
+ * Stores each fetch promise for handling after renderCallback.
270
+ */
271
+ private fetchAllAmf(): void {
272
+ for (const [i, amfConfig] of this._amfConfig.entries()) {
273
+ const p = this.fetchAmf(amfConfig).then((amfJson) => {
274
+ this.updateModel(amfConfig.id, amfJson);
275
+ this.assignNavigationItemsFromAmf(amfConfig.id, i);
276
+ });
277
+ this.amfFetchPromiseMap[amfConfig.id] = p;
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Stores fetched AMF JSON value.
283
+ * Creates and stores a new AmfModelParser instance for the AMF spec.
284
+ * @param {*} referenceId
285
+ * @param {*} amf
286
+ */
287
+ private updateModel(referenceId: string, amf: AmfModel | AmfModel[]): void {
288
+ const parser = new AmfModelParser(amf);
289
+ this.amfMap[referenceId] = {
290
+ model: amf,
291
+ parser: parser,
292
+ parsedModel: parser.parsedModel
293
+ };
294
+ }
295
+
296
+ /**
297
+ * Maps a single endpoint model to a nav item.
298
+ * Populates the children of this endpoints nav item
299
+ * with the list of methods if they exist.
300
+ * Adds the endpoint and any associated methods to the metadata.
301
+ * @param {Object} model Endpoint Model
302
+ * @returns {Object} Navigation item object model for dx-sidebar
303
+ */
304
+ private createEndpointNavItem(
305
+ referenceId: string,
306
+ model: ParsedTopicModel
307
+ ): NavItem {
308
+ const { label, methods = [] } = model;
309
+ const meta = this.addToMetadata(referenceId, "endpoint", model);
310
+
311
+ const navItem: NavItem = {
312
+ name: this.getReferencePathWithMeta(meta),
313
+ label
314
+ };
315
+
316
+ if (methods.length) {
317
+ // assign the endpoint methods as its children
318
+ navItem.children = methods.map((method) => {
319
+ const _meta = this.addToMetadata(referenceId, "method", method);
320
+ return Object.assign(method, {
321
+ name: this.getReferencePathWithMeta(_meta),
322
+ label: method.label || method.method
323
+ });
324
+ });
325
+ navItem.isExpanded = false;
326
+ }
327
+
328
+ return navItem;
329
+ }
330
+
331
+ /**
332
+ * Transforms a list of model data for endpoints into corresponding
333
+ * navigation list items that is compatible with dx-sidebar.
334
+ * Compatible with transforming AMF data parsed from both RAML and OAS spec.
335
+ * Transforms a flat list of endpoints into a nested list based on indentation level
336
+ * for RAML spec.
337
+ * @param {Array<Object>} items An array of endpoints.
338
+ * @returns {array<Object>} List of navigation items
339
+ */
340
+ private assignEndpointNavItems(
341
+ referenceId: string,
342
+ items: ParsedTopicModel[]
343
+ ): NavItem[] {
344
+ const indentStack = [];
345
+ let prevIndent = -1;
346
+
347
+ items.forEach((item) => {
348
+ // 'indent' only present for AMF data parsed from RAML spec.
349
+ // 'indent' is not present for AMF data parsed from OAS spec where endpoints cannot be nested
350
+ // thus we default indent at 0 to reflect the flat structure for OAS spec
351
+ const { indent = 0 } = item;
352
+ const endpoint = this.createEndpointNavItem(referenceId, item);
353
+
354
+ if (indent > prevIndent) {
355
+ // new or equivalent endpoint indentation level
356
+ // create new indent group
357
+ const indentGroup = {
358
+ indent,
359
+ children: [endpoint]
360
+ };
361
+ // push to stack
362
+ indentStack.push(indentGroup);
363
+ } else if (indent === prevIndent) {
364
+ // no change in indentation level
365
+ const lastIndentGroup = indentStack[indentStack.length - 1];
366
+ lastIndentGroup.children.push(endpoint);
367
+ } else {
368
+ // indent < prevIndent: back out to last matching indentation level
369
+ let indentGroup = indentStack.pop();
370
+ // hold on to the last indent group's children
371
+ let lastIndentGroupChildren = indentGroup.children;
372
+
373
+ while (indent < indentGroup.indent) {
374
+ // back out another indentation level
375
+ indentGroup = indentStack.pop();
376
+ // append children to next level's last child
377
+ const lastChild =
378
+ indentGroup.children[indentGroup.children.length - 1];
379
+ lastChild.children = (lastChild.children || []).concat(
380
+ lastIndentGroupChildren
381
+ );
382
+ lastIndentGroupChildren = indentGroup.children;
383
+ }
384
+ indentGroup.children.push(endpoint);
385
+ indentStack.push(indentGroup);
386
+ }
387
+
388
+ // update prevIndent
389
+ prevIndent = indent;
390
+ });
391
+
392
+ // fold remaining indent levels up to the root
393
+ if (prevIndent > 0) {
394
+ let indentGroup = indentStack.pop();
395
+ let lastIndentGroupChildren = indentGroup.children;
396
+ while (indentGroup.indent > 0) {
397
+ indentGroup = indentStack.pop();
398
+ const lastChild =
399
+ indentGroup.children[indentGroup.children.length - 1];
400
+ lastChild.children = (lastChild.children || []).concat(
401
+ lastIndentGroupChildren
402
+ );
403
+ lastIndentGroupChildren = indentGroup.children;
404
+ }
405
+ indentStack.push(indentGroup);
406
+ }
407
+
408
+ return indentStack[0].children;
409
+ }
410
+
411
+ private getReferencePathWithMeta(meta: string): string {
412
+ return meta ? `${window.location.pathname}?meta=${meta}` : "";
413
+ }
414
+
415
+ /**
416
+ * Assigns Navigation Items to dx-sidebar from the parsed AMF model.
417
+ * Adds each nav item to the Metadata by type.
418
+ * The 'summary' nav item has no children.
419
+ * The 'endpoint' nav item may have nested children.
420
+ */
421
+ private assignNavigationItemsFromAmf(
422
+ referenceId: string,
423
+ amfIdx: number
424
+ ): void {
425
+ const model = this.amfMap[referenceId].parser.parsedModel;
426
+
427
+ const children = [];
428
+
429
+ NAVIGATION_ITEMS.forEach(
430
+ ({ label, name, childrenPropertyName, type }) => {
431
+ const indexedName = `${name}-${amfIdx}`;
432
+ switch (type) {
433
+ case "summary": {
434
+ const summary = model[type];
435
+ const meta = this.addToMetadata(
436
+ referenceId,
437
+ type,
438
+ summary
439
+ );
440
+ children.push({
441
+ label,
442
+ name: this.getReferencePathWithMeta(meta)
443
+ });
444
+ break;
445
+ }
446
+ case "endpoint":
447
+ if (
448
+ model[childrenPropertyName] &&
449
+ model[childrenPropertyName].length
450
+ ) {
451
+ const amfTopicId = this.getFormattedIdentifier(
452
+ referenceId,
453
+ indexedName
454
+ );
455
+ const childTopics = this.assignEndpointNavItems(
456
+ referenceId,
457
+ model[childrenPropertyName]
458
+ );
459
+ children.push({
460
+ label,
461
+ name: this.getReferencePathWithMeta(
462
+ this.metadata[amfTopicId]?.meta
463
+ ),
464
+ isExpanded: false,
465
+ children: childTopics
466
+ });
467
+ }
468
+ break;
469
+ case "security":
470
+ case "type":
471
+ if (model[childrenPropertyName]?.length) {
472
+ // Sorting the types alphabetically
473
+ model[childrenPropertyName].sort((typeA, typeB) => {
474
+ const typeALbl = typeA.label.toLowerCase();
475
+ const typeBLbl = typeB.label.toLowerCase();
476
+ return typeALbl < typeBLbl
477
+ ? -1
478
+ : typeALbl > typeBLbl
479
+ ? 1
480
+ : 0;
481
+ });
482
+ }
483
+ // eslint-disable-next-line no-fallthrough
484
+ default:
485
+ if (
486
+ model[childrenPropertyName] &&
487
+ model[childrenPropertyName].length
488
+ ) {
489
+ const amfTopicId = this.getFormattedIdentifier(
490
+ referenceId,
491
+ indexedName
492
+ );
493
+ children.push({
494
+ label,
495
+ name: this.getReferencePathWithMeta(
496
+ this.metadata[amfTopicId]?.meta
497
+ ),
498
+ isExpanded: false,
499
+ children: model[childrenPropertyName].map(
500
+ (topic) => {
501
+ const meta = this.addToMetadata(
502
+ referenceId,
503
+ type,
504
+ topic
505
+ );
506
+ return {
507
+ label: topic.label,
508
+ name: this.getReferencePathWithMeta(
509
+ meta
510
+ )
511
+ };
512
+ }
513
+ )
514
+ });
515
+ }
516
+ }
517
+ }
518
+ );
519
+
520
+ // store nav items for each spec in order
521
+ this.navAmfOrder[amfIdx] = {
522
+ label: model.title,
523
+ name: this.getReferencePathWithMeta(`${referenceId}-root`),
524
+ isExpanded: amfIdx === 0, // only expand the first spec
525
+ children
526
+ };
527
+
528
+ // update navigation with each specs nav items as they become available
529
+ // navigation has to be an array because dx-sidebar expects an array.
530
+ const navigation = [];
531
+ for (const navAmf of this.navAmfOrder) {
532
+ if (navAmf) {
533
+ navigation.push(navAmf);
534
+ }
535
+ }
536
+ this.navigation = navigation;
537
+ }
538
+
539
+ protected addToMetadata(
540
+ referenceId: string,
541
+ type: string,
542
+ topic: { id: string; domId: string }
543
+ ): string {
544
+ const { urlIdentifer, prefix } = URL_CONFIG[type];
545
+
546
+ // encodeURI to avoid special characters in the URL meta.
547
+ const identifier =
548
+ topic[urlIdentifer] && this.encodeIdentifier(topic[urlIdentifer]);
549
+ const meta = prefix
550
+ ? `${referenceId}:${prefix}${identifier}`
551
+ : `${referenceId}:${identifier}`;
552
+ this.metadata[meta] = {
553
+ meta: meta,
554
+ referenceId: referenceId,
555
+ amfId: topic.id,
556
+ elementId: topic.domId,
557
+ identifier,
558
+ type
559
+ };
560
+ return meta;
561
+ }
562
+
563
+ /**
564
+ * Returns metadata given route meta
565
+ */
566
+ protected getMetadataByUrlQuery(routeMeta: RouteMeta): AmfMetadataTopic {
567
+ return Object.values(this.metadata).find(
568
+ (metadata: AmfMetadataTopic) => {
569
+ return routeMeta.meta === metadata.meta;
570
+ }
571
+ );
572
+ }
573
+
574
+ /**
575
+ * Returns metadata given reference ID and topic amf ID
576
+ */
577
+ protected getMetadataByAmfId(
578
+ referenceId: string,
579
+ amfId: string
580
+ ): AmfMetadataTopic {
581
+ // Lets make a map based on the hash values so we don't need to loop like this.
582
+ return Object.values(this.metadata).find(
583
+ (metadata: AmfMetadataTopic) =>
584
+ referenceId === metadata.referenceId && amfId === metadata.amfId
585
+ );
586
+ }
587
+
588
+ /**
589
+ * Returns metadata given reference ID and topic identifier
590
+ */
591
+ protected getMetadataByIdentifier(
592
+ referenceId: string,
593
+ identifier: string
594
+ ): AmfMetadataTopic {
595
+ // Lets make a map based on the hash values so we don't need to loop like this.
596
+ return Object.values(this.metadata).find(
597
+ (metadata: AmfMetadataTopic) =>
598
+ referenceId === metadata.referenceId &&
599
+ identifier === metadata.identifier
600
+ );
601
+ }
602
+
603
+ /**
604
+ * Returns metadata given reference ID and topic type
605
+ */
606
+ protected getMetadataByType(
607
+ referenceId: string,
608
+ type: string
609
+ ): AmfMetadataTopic {
610
+ // Lets make a map based on the hash values so we don't need to loop like this.
611
+ return Object.values(this.metadata).find(
612
+ (metadata: AmfMetadataTopic) =>
613
+ referenceId === metadata.referenceId && type === metadata.type
614
+ );
615
+ }
616
+
617
+ /**
618
+ * Parses url query params without decoding of params
619
+ */
620
+ private parseParams(path: string): qs.ParsedQuery<string> {
621
+ if (!path) {
622
+ return {};
623
+ }
624
+ return qs.parse(path, {
625
+ decode: false
626
+ });
627
+ }
628
+
629
+ /**
630
+ * Gets the portion from the URL query param 'meta'.
631
+ */
632
+ protected getCurrentRefMeta(
633
+ previousRefMetaInSession?: string
634
+ ): RouteMeta | null {
635
+ const path = window.location.search;
636
+ const urlParams = this.parseParams(path);
637
+ let meta = urlParams.meta as string;
638
+ let routeMeta = null;
639
+ if (previousRefMetaInSession) {
640
+ const refParts = previousRefMetaInSession.split(":");
641
+ const newRefId = refParts.length > 0 ? refParts[0] : null;
642
+ const [, type, topicId] = previousRefMetaInSession.split(":");
643
+ meta = newRefId ? [newRefId, type, topicId].join(":") : null;
644
+ } else if (!meta) {
645
+ // If no `meta` explicitly exists, check the URL hash to see whether this is one we
646
+ // want to redirect (see GUS W-10718771 for one reference where we want hash-based
647
+ // redirects)
648
+ const { hash } = window.location;
649
+ const strippedHash = hash.startsWith("#") ? hash.slice(1) : hash;
650
+ meta = urlHashToMetaRedirectMap[strippedHash];
651
+ }
652
+ if (meta) {
653
+ routeMeta = new RouteMeta(meta);
654
+ }
655
+ return routeMeta;
656
+ }
657
+
658
+ /**
659
+ * Normalizes topic identifier by replacing spaces with '+'
660
+ * and running encodeURI() on it
661
+ * @param identifer raw identifer for a topic as parsed from the spec file
662
+ * @returns normalized and encoded identifier
663
+ */
664
+ protected encodeIdentifier(identifer: string): string {
665
+ let result = identifer.trim();
666
+ result = result.replace(new RegExp(/\s+/, "g"), "+");
667
+ return encodeURI(result);
668
+ }
669
+
670
+ /**
671
+ * Constructs a Reference Topic ID
672
+ */
673
+ protected getFormattedIdentifier(referenceId: string, id: string): string {
674
+ return `${referenceId}:${id}`;
675
+ }
676
+
677
+ protected updateSelectedItemFromUrlQuery(): void {
678
+ const currentMeta: RouteMeta | null = this.getCurrentRefMeta();
679
+ const metadata = currentMeta && this.getMetadataByUrlQuery(currentMeta);
680
+ if (metadata) {
681
+ const { referenceId, amfId, type, elementId }: AmfMetadataTopic =
682
+ metadata;
683
+ this.loadContent(
684
+ referenceId,
685
+ amfId,
686
+ type,
687
+ elementId,
688
+ currentMeta.meta
689
+ );
690
+ }
691
+ }
692
+ protected onApiNavigationChanged(): void {
693
+ // The API Navigation event will always intend to navigate within the current reference
694
+ const metadata =
695
+ this.metadata[
696
+ this.getReferencePathWithMeta(this.selectedTopic.meta)
697
+ ];
698
+ const { referenceId, amfId, type, elementId }: AmfMetadataTopic =
699
+ metadata;
700
+ this.loadContent(referenceId, amfId, type, elementId, metadata.meta);
701
+ this.updateUrlWithSelected();
702
+ }
703
+
704
+ protected updateUrlWithSelected(meta?: string): void {
705
+ if (meta) {
706
+ window.history.pushState(
707
+ {},
708
+ "",
709
+ `${window.location.pathname}?meta=${meta}`
710
+ );
711
+ }
712
+ }
713
+
714
+ /**
715
+ * Does a replace on the url meta, so it does not create a history entry.
716
+ */
717
+ protected replaceUrlWithSelected(meta?: string): void {
718
+ if (meta) {
719
+ window.history.replaceState(
720
+ {},
721
+ "",
722
+ `${window.location.pathname}?meta=${meta}`
723
+ );
724
+ }
725
+ }
726
+
727
+ /**
728
+ * Updates the currently selected amf and topic
729
+ */
730
+ protected loadContent(
731
+ referenceId: string,
732
+ amfId: string,
733
+ type: string,
734
+ elementId = "",
735
+ meta = ""
736
+ ): void {
737
+ this.selectedTopic = {
738
+ referenceId,
739
+ amfId,
740
+ elementId,
741
+ type,
742
+ meta
743
+ };
744
+ this.selectedSidebarValue = this.getReferencePathWithMeta(meta);
745
+
746
+ this.handleSelectedItem();
747
+
748
+ // Ensures that the URL always has the meta, that way we don't get two history entries for summary
749
+ this.replaceUrlWithSelected(meta);
750
+ this.updateDocPhase();
751
+ }
752
+
753
+ /**
754
+ * Updates doc phase of selected reference
755
+ */
756
+ updateDocPhase(): void {
757
+ /* If parent level doc phase is enabled, Individual reference level doc phase should not be considered */
758
+
759
+ if (!this.isParentLevelDocPhaseEnabled) {
760
+ const referenceId = this.selectedTopic?.referenceId;
761
+ const selectedReference = this._amfConfig.find(
762
+ (referenceItem: AmfConfig) => {
763
+ return referenceItem.id === referenceId;
764
+ }
765
+ );
766
+ if (selectedReference) {
767
+ this.selectedReferenceDocPhase = JSON.stringify(
768
+ selectedReference.docPhase
769
+ );
770
+ }
771
+ }
772
+ }
773
+
774
+ /**
775
+ * Updates the DOM on first load
776
+ */
777
+ updateView(): void {
778
+ let referenceId: string;
779
+ let topicId = "";
780
+ const previousRefMetaInSession = window.sessionStorage.getItem(
781
+ this.docsReferenceMetaSessionKey
782
+ );
783
+ window.sessionStorage.removeItem(this.docsReferenceMetaSessionKey);
784
+ const currentMeta = this.getCurrentRefMeta(previousRefMetaInSession);
785
+ if (currentMeta) {
786
+ referenceId = currentMeta.referenceId;
787
+ topicId = currentMeta.topicId;
788
+ if (!this.amfFetchPromiseMap[referenceId]) {
789
+ // This could happen if they specify a bad query value.
790
+ // In this case, we'll do the logic below to use the default amf
791
+ referenceId = null;
792
+ }
793
+ }
794
+
795
+ if (!referenceId) {
796
+ referenceId = this._amfConfig[0].id;
797
+ if (!this.amfFetchPromiseMap[referenceId]) {
798
+ // This should never happen.
799
+ referenceId = Object.keys(this.amfFetchPromiseMap)[0];
800
+ }
801
+ }
802
+
803
+ // Wait till the AMF is loaded.
804
+ this.amfFetchPromiseMap[referenceId].then(() => {
805
+ let topic = this.getMetadataByIdentifier(referenceId, topicId);
806
+ if (!topic) {
807
+ // Doesn't exist, let's use the summary.
808
+ topic = this.getMetadataByType(referenceId, "summary");
809
+ }
810
+
811
+ if (topic) {
812
+ this.loadContent(
813
+ topic.referenceId,
814
+ topic.amfId,
815
+ topic.type,
816
+ "",
817
+ topic.meta
818
+ );
819
+ }
820
+ });
821
+ }
822
+
823
+ /**
824
+ * Currently, used to handle the version change and storing the current meta query param to the session storage.
825
+ */
826
+ handleVersionChange(): void {
827
+ const path = window.location.search;
828
+ const urlParams = this.parseParams(path);
829
+ const meta = urlParams.meta as string;
830
+ if (meta) {
831
+ window.sessionStorage.setItem(
832
+ this.docsReferenceMetaSessionKey,
833
+ meta
834
+ );
835
+ }
836
+ }
837
+
838
+ onNavSelect(event: CustomEvent): void {
839
+ const name = event.detail.name;
840
+ const indexOfQueryParam = name.indexOf("?");
841
+ const urlPath = name.substring(
842
+ indexOfQueryParam >= 0 ? indexOfQueryParam : name.length
843
+ );
844
+ const metaVal = this.parseParams(urlPath).meta as string;
845
+ const currentSelectedMeta = this.selectedTopic.meta;
846
+
847
+ if (metaVal === currentSelectedMeta) {
848
+ // selecting the same nav item, skip update
849
+ return;
850
+ }
851
+
852
+ const metadata = this.metadata[metaVal];
853
+ if (metadata) {
854
+ const { referenceId, amfId, type, elementId } = metadata;
855
+ this.loadContent(referenceId, amfId, type, elementId, metaVal);
856
+ this.updateUrlWithSelected(metaVal);
857
+ }
858
+ }
859
+
860
+ handleSelectedItem(): void {
861
+ // update topic view
862
+ const { referenceId, amfId, type } = this.selectedTopic;
863
+
864
+ // This updates the component in the content section.
865
+ this.topicModel = {
866
+ type,
867
+ amf: this.amfMap[referenceId].model,
868
+ parser: this.amfMap[referenceId].parser,
869
+ id: amfId
870
+ };
871
+
872
+ window.scrollTo({ top: 0, behavior: "smooth" });
873
+ }
874
+ }