@salesforcedevs/docs-components 1.17.5 → 1.17.6-redoc1

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.
package/lwc.config.json CHANGED
@@ -20,6 +20,7 @@
20
20
  "doc/headingAnchor",
21
21
  "doc/overview",
22
22
  "doc/phase",
23
+ "doc/docReference",
23
24
  "doc/specificationContent",
24
25
  "doc/versionPicker",
25
26
  "doc/xmlContent",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/docs-components",
3
- "version": "1.17.5",
3
+ "version": "1.17.6-redoc1",
4
4
  "description": "Docs Lightning web components for DSC",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -25,5 +25,5 @@
25
25
  "@types/lodash.orderby": "4.6.9",
26
26
  "@types/lodash.uniqby": "4.7.9"
27
27
  },
28
- "gitHead": "5aaacff2c6b494563ccf0d8a08ee809f5c1226b7"
28
+ "gitHead": "4629fdd9ca18a13480044ad43515b91945d16aad"
29
29
  }
@@ -0,0 +1,52 @@
1
+ @import "docHelpers/amfStyle";
2
+
3
+ :host {
4
+ --reference-container-margin-top: var(--dx-g-spacing-sm);
5
+ --api-documentation-margin-top: var(--dx-g-spacing-3xl);
6
+ }
7
+
8
+ /**
9
+ * 1. We need to scroll to top from the tablet size as side nav bar and content in side by side from tablet size
10
+ * 2. Consider global nav height, doc header height and content margins to scroll to the right position
11
+ */
12
+
13
+ @media screen and (min-width: 769px) {
14
+ .redoc-container {
15
+ scroll-margin-top: calc(
16
+ var(--dx-g-global-header-height) + var(--dx-g-doc-header-height) +
17
+ var(--reference-container-margin-top) +
18
+ var(--api-documentation-margin-top)
19
+ );
20
+ }
21
+ }
22
+
23
+ .redoc-container {
24
+ width: 100%;
25
+ height: 100%;
26
+ /* Ensure Redoc content is scrollable within the content layout */
27
+ overflow-y: auto;
28
+ }
29
+
30
+ /* Loading state styles */
31
+ .loading-container {
32
+ display: flex;
33
+ justify-content: center;
34
+ align-items: center;
35
+ height: 200px;
36
+ font-size: 16px;
37
+ color: var(--dx-g-color-text-secondary);
38
+ }
39
+
40
+ /* Error state styles */
41
+ .error-container {
42
+ padding: 20px;
43
+ background-color: var(--dx-g-color-background-error);
44
+ border: 1px solid var(--dx-g-color-border-error);
45
+ border-radius: 4px;
46
+ margin: 20px 0;
47
+ }
48
+
49
+ .error-message {
50
+ color: var(--dx-g-color-text-error);
51
+ font-weight: 600;
52
+ }
@@ -0,0 +1,62 @@
1
+ <template>
2
+ <doc-content-layout
3
+ lwc:if={isVersionFetched}
4
+ use-old-sidebar={useOldSidebar}
5
+ class="content-type content-type-reference"
6
+ coveo-organization-id={coveoOrganizationId}
7
+ coveo-public-access-token={coveoPublicAccessToken}
8
+ coveo-analytics-token={coveoAnalyticsToken}
9
+ coveo-search-hub={coveoSearchHub}
10
+ coveo-advanced-query-config={coveoAdvancedQueryConfig}
11
+ breadcrumbs={breadcrumbs}
12
+ sidebar-header={sidebarHeader}
13
+ sidebar-value={selectedSidebarValue}
14
+ sidebar-content={navigation}
15
+ onselect={onNavSelect}
16
+ onexpandcollapse={onExpandCollapse}
17
+ toc-title={tocTitle}
18
+ toc-options={tocOptions}
19
+ enable-slot-change="true"
20
+ languages={languages}
21
+ language={language}
22
+ show-footer={enableFooter}
23
+ >
24
+ <doc-phase
25
+ slot="doc-phase"
26
+ lwc:if={docPhaseInfo}
27
+ doc-phase-info={docPhaseInfo}
28
+ ></doc-phase>
29
+ <doc-phase
30
+ slot="version-banner"
31
+ lwc:if={showVersionBanner}
32
+ doc-phase-info={oldVersionInfo}
33
+ icon-name="warning"
34
+ dismissible="true"
35
+ ondismissphase={handleDismissVersionBanner}
36
+ ></doc-phase>
37
+ <div lwc:if={isVersionEnabled} slot="sidebar-header">
38
+ <doc-version-picker
39
+ onchange={handleVersionChange}
40
+ data-type="version"
41
+ versions={versions}
42
+ selected-version={selectedVersion}
43
+ latest-version={latestVersion}
44
+ ></doc-version-picker>
45
+ </div>
46
+ <template lwc:if={showSpecBasedReference}>
47
+ <div class="container">
48
+ <div class="api-documentation">
49
+ <!-- Replace doc-amf-topic with Redoc container -->
50
+ <div
51
+ class="redoc-container"
52
+ lwc:dom="manual"
53
+ data-redoc-container
54
+ ></div>
55
+ </div>
56
+ </div>
57
+ </template>
58
+ <template lwc:else>
59
+ <slot></slot>
60
+ </template>
61
+ </doc-content-layout>
62
+ </template>
@@ -0,0 +1,1090 @@
1
+ import { LightningElement, api, track } from "lwc";
2
+ import { normalizeBoolean, toJson } from "dxUtils/normalizers";
3
+ import type { OptionWithLink } from "typings/custom";
4
+ import type {
5
+ AmfConfig,
6
+ ReferenceVersion,
7
+ ReferenceSetConfig,
8
+ ParsedMarkdownTopic
9
+ } from "../amfReference/types";
10
+
11
+ import {
12
+ REFERENCE_TYPES,
13
+ oldReferenceIdNewReferenceIdMap
14
+ } from "../amfReference/constants";
15
+ import { restoreScroll } from "dx/scrollManager";
16
+ import { DocPhaseInfo } from "typings/custom";
17
+ import { logCoveoPageView, oldVersionDocInfo } from "docUtils/utils";
18
+ import type { RedocNavigationItem, RedocSidebarData } from "./types";
19
+ import "./types"; // Import types to ensure global declarations are loaded
20
+
21
+ type NavigationItem = {
22
+ label: string;
23
+ name: string;
24
+ isExpanded: boolean;
25
+ children: RedocNavigationItem[];
26
+ isChildrenLoading: boolean;
27
+ };
28
+
29
+ export default class DocReference extends LightningElement {
30
+ @api breadcrumbs: string | null = null;
31
+ @api sidebarHeader!: string;
32
+ @api coveoOrganizationId!: string;
33
+ @api coveoPublicAccessToken!: string;
34
+ @api coveoAnalyticsToken!: string;
35
+ @api coveoSearchHub!: string;
36
+ @api useOldSidebar: boolean = false;
37
+ @api tocTitle?: string;
38
+ @api tocOptions?: string;
39
+ @api languages!: OptionWithLink[];
40
+ @api language!: string;
41
+ @api hideFooter = false;
42
+ @track navigation = [] as NavigationItem[];
43
+ @track versions: Array<ReferenceVersion> = [];
44
+ @track showVersionBanner = false;
45
+ @track _coveoAdvancedQueryConfig!: { [key: string]: any };
46
+
47
+ get isVersionEnabled(): boolean {
48
+ return !!this._referenceSetConfig?.versions?.length;
49
+ }
50
+
51
+ /**
52
+ * Gives if the currently selected reference is spec based or not
53
+ */
54
+ get showSpecBasedReference(): boolean {
55
+ return this.isSpecBasedReference(this._currentReferenceId);
56
+ }
57
+
58
+ @api
59
+ get referenceSetConfig(): ReferenceSetConfig {
60
+ return this._referenceSetConfig;
61
+ }
62
+
63
+ set referenceSetConfig(value: ReferenceSetConfig) {
64
+ // No change, do nothing.
65
+ if (value === this._referenceSetConfig) {
66
+ return;
67
+ }
68
+
69
+ try {
70
+ const refConfig =
71
+ typeof value === "string" ? JSON.parse(value) : value;
72
+ if (!(<ReferenceSetConfig>refConfig).versions) {
73
+ return;
74
+ }
75
+ this._referenceSetConfig = refConfig;
76
+ } catch (e) {
77
+ this._referenceSetConfig = {
78
+ refList: [],
79
+ versions: []
80
+ };
81
+ }
82
+
83
+ this._amfConfigList = this._referenceSetConfig.refList || [];
84
+
85
+ this._amfConfigList.forEach((amfConfig) => {
86
+ this._amfConfigMap.set(amfConfig.id, amfConfig);
87
+ });
88
+
89
+ if (this._amfConfigList.length > 0) {
90
+ this._currentReferenceId =
91
+ this._referenceSetConfig.refId || this._amfConfigList[0].id;
92
+ }
93
+
94
+ if (this.isVersionEnabled) {
95
+ const selectedVersion = this.getSelectedVersion();
96
+
97
+ if (this.isSpecBasedReference(this._currentReferenceId)) {
98
+ this.versions = this.getVersions();
99
+ }
100
+ this.selectedVersion = selectedVersion;
101
+ if (this.isSpecBasedReference(this._currentReferenceId)) {
102
+ this.isVersionFetched = true;
103
+ if (this.oldVersionInfo) {
104
+ this.showVersionBanner = true;
105
+ } else {
106
+ this.latestVersion = true;
107
+ }
108
+ }
109
+ } else {
110
+ this.isVersionFetched = true;
111
+ }
112
+
113
+ // This is to check if the url is hash based and redirect if needed
114
+ const redirectUrl = this.getHashBasedRedirectUrl();
115
+ if (redirectUrl) {
116
+ window.location.href = redirectUrl;
117
+ } else {
118
+ this.updateConfigInView();
119
+ }
120
+ }
121
+
122
+ @api
123
+ get docPhaseInfo(): string | null {
124
+ return this.selectedReferenceDocPhase || null;
125
+ }
126
+
127
+ set docPhaseInfo(value: string) {
128
+ if (value) {
129
+ this.isParentLevelDocPhaseEnabled = true;
130
+ this.selectedReferenceDocPhase = value;
131
+ }
132
+ }
133
+
134
+ @api
135
+ get expandChildren() {
136
+ return this._expandChildren;
137
+ }
138
+
139
+ set expandChildren(value) {
140
+ this._expandChildren = normalizeBoolean(value);
141
+ }
142
+
143
+ @api
144
+ get coveoAdvancedQueryConfig(): { [key: string]: any } {
145
+ const coveoConfig = this._coveoAdvancedQueryConfig;
146
+ if (this.versions.length > 1 && this.selectedVersion) {
147
+ const currentGAVersionRef = this.versions[0];
148
+ if (this.selectedVersion.id !== currentGAVersionRef.id) {
149
+ const version = this.selectedVersion.id.replace("v", "");
150
+ coveoConfig.version = version;
151
+ this._coveoAdvancedQueryConfig = coveoConfig;
152
+ }
153
+ }
154
+ return this._coveoAdvancedQueryConfig;
155
+ }
156
+
157
+ set coveoAdvancedQueryConfig(config) {
158
+ this._coveoAdvancedQueryConfig = toJson(config);
159
+ }
160
+
161
+ private get enableFooter(): boolean {
162
+ return !this.hideFooter;
163
+ }
164
+
165
+ // model
166
+ protected _amfConfigList: AmfConfig[] = [];
167
+ protected _amfConfigMap: Map<string, AmfConfig> = new Map();
168
+ protected _referenceSetConfig!: ReferenceSetConfig;
169
+ protected _currentReferenceId = "";
170
+
171
+ protected parentReferenceUrls = [] as string[];
172
+ protected redocFetchPromiseMap = {} as any;
173
+ protected selectedSidebarValue: string | undefined = undefined;
174
+ protected selectedVersion: ReferenceVersion | null = null;
175
+
176
+ // Redoc-specific properties
177
+ private redocContainers: Map<string, Element> = new Map();
178
+ private redocSidebarData: Map<string, RedocSidebarData> = new Map();
179
+ private currentActiveSection: string | null = null;
180
+ private isProgrammaticScroll = false;
181
+ private scrollTimeout: number | null = null;
182
+
183
+ private hasRendered = false;
184
+ private isParentLevelDocPhaseEnabled = false;
185
+ private selectedReferenceDocPhase?: string | null = null;
186
+ private _expandChildren?: boolean = false;
187
+ private isVersionFetched = false;
188
+ private latestVersion = false;
189
+
190
+ private readonly docsReferenceUrlSessionKey: string = "docsReferenceUrl";
191
+
192
+ _boundOnApiNavigationChanged;
193
+ _boundUpdateSelectedItemFromUrlQuery;
194
+ _boundHandleRedocScroll;
195
+
196
+ constructor() {
197
+ super();
198
+
199
+ this._boundOnApiNavigationChanged =
200
+ this.onApiNavigationChanged.bind(this);
201
+ this._boundUpdateSelectedItemFromUrlQuery =
202
+ this.updateSelectedItemFromUrlQuery.bind(this);
203
+ this._boundHandleRedocScroll =
204
+ this.handleRedocScroll.bind(this);
205
+ }
206
+
207
+ connectedCallback(): void {
208
+ this.addEventListener(
209
+ "api-navigation-selection-changed",
210
+ this._boundOnApiNavigationChanged
211
+ );
212
+ window.addEventListener(
213
+ "popstate",
214
+ this._boundUpdateSelectedItemFromUrlQuery
215
+ );
216
+ window.addEventListener(
217
+ "hashchange",
218
+ this.handleHashNavigation.bind(this)
219
+ );
220
+ }
221
+
222
+ disconnectedCallback(): void {
223
+ this.removeEventListener(
224
+ "api-navigation-selection-changed",
225
+ this._boundOnApiNavigationChanged
226
+ );
227
+ window.removeEventListener(
228
+ "popstate",
229
+ this._boundUpdateSelectedItemFromUrlQuery
230
+ );
231
+ window.removeEventListener(
232
+ "hashchange",
233
+ this.handleHashNavigation.bind(this)
234
+ );
235
+ if (this.scrollTimeout) {
236
+ clearTimeout(this.scrollTimeout);
237
+ }
238
+ }
239
+
240
+ renderedCallback(): void {
241
+ if (!this.hasRendered) {
242
+ this.hasRendered = true;
243
+ if (this._amfConfigList && this._amfConfigList.length) {
244
+ this.updateView();
245
+ }
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Check if the URL hash to see whether this is one we want to redirect
251
+ */
252
+ private getHashBasedRedirectUrl(): string | undefined {
253
+ const { hash } = window.location;
254
+ let hashBasedRedirectUrl = "";
255
+ if (hash) {
256
+ const strippedHash = hash.startsWith("#") ? hash.slice(1) : hash;
257
+ const strippedHashItems = strippedHash
258
+ ? strippedHash.split(":")
259
+ : [];
260
+ if (strippedHashItems.length) {
261
+ const referenceId = strippedHashItems[0];
262
+ const section = strippedHashItems[1];
263
+ const updatedReferenceId =
264
+ oldReferenceIdNewReferenceIdMap[referenceId];
265
+ const newReferenceId = updatedReferenceId || referenceId;
266
+ const referenceItemConfig =
267
+ this.getAmfConfigWithId(newReferenceId);
268
+ if (referenceItemConfig) {
269
+ hashBasedRedirectUrl = `${referenceItemConfig.href}#${section}`;
270
+ }
271
+ }
272
+ }
273
+ return hashBasedRedirectUrl;
274
+ }
275
+
276
+ /**
277
+ * @param referenceId
278
+ * @returns AMFConfig with given reference Id
279
+ */
280
+ private getAmfConfigWithId(referenceId: string): AmfConfig | undefined {
281
+ return this._amfConfigMap.get(referenceId);
282
+ }
283
+
284
+ /**
285
+ * @param referenceId
286
+ * @returns if the reference is spec based one or not with given referenceId.
287
+ */
288
+ private isSpecBasedReference(referenceId: string): boolean {
289
+ const selectedReference = this.getAmfConfigWithId(referenceId);
290
+ return selectedReference
291
+ ? selectedReference.referenceType !== REFERENCE_TYPES.markdown
292
+ : false;
293
+ }
294
+
295
+ /**
296
+ * @param url
297
+ * @returns reference Id from url path / selected sidebar item.
298
+ */
299
+ private getReferenceIdFromUrl(url: string): string {
300
+ let referenceId = "";
301
+ const urlItems = url.split("/references/");
302
+ if (urlItems.length > 1) {
303
+ const rightSidePart = urlItems[1];
304
+ const slashSeparatorItems = rightSidePart.split("/");
305
+ const querySeparatorItems = slashSeparatorItems[0].split("?");
306
+ referenceId = querySeparatorItems[0];
307
+ }
308
+ return referenceId;
309
+ }
310
+
311
+ private get oldVersionInfo(): DocPhaseInfo | null {
312
+ let info = null;
313
+ if (this.versions.length > 1 && this.selectedVersion) {
314
+ const currentGAVersionRef = this.versions[0];
315
+ if (this.selectedVersion.id !== currentGAVersionRef.id) {
316
+ info = oldVersionDocInfo(currentGAVersionRef.link.href);
317
+ }
318
+ }
319
+ return info;
320
+ }
321
+
322
+ /**
323
+ * @returns versions to be shown in the dropdown
324
+ */
325
+ private getVersions(): Array<ReferenceVersion> {
326
+ const allVersions = this._referenceSetConfig.versions;
327
+ if (!this.isSpecBasedReference(this._currentReferenceId)) {
328
+ // For markdown references, handle differently if needed
329
+ // For now, return as-is since we're focusing on spec-based
330
+ }
331
+ return allVersions;
332
+ }
333
+
334
+ /**
335
+ * Returns the selected version or the first available version.
336
+ */
337
+ private getSelectedVersion(): ReferenceVersion | null {
338
+ const versions = this._referenceSetConfig?.versions || [];
339
+ const selectedVersion = versions.find(
340
+ (v: ReferenceVersion) => v.selected
341
+ );
342
+ return selectedVersion || (versions.length && versions[0]) || null;
343
+ }
344
+
345
+ private updateConfigInView(): void {
346
+ if (this._amfConfigList && this._amfConfigList.length) {
347
+ this.populateReferenceItems();
348
+ if (this.hasRendered) {
349
+ this.updateView();
350
+ }
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Fetch spec and generate Redoc structure
356
+ */
357
+ private async fetchSpec(amfConfig: AmfConfig): Promise<RedocSidebarData | null> {
358
+ const { amf } = amfConfig;
359
+ try {
360
+ // Load Redoc sidebar structure
361
+ if (window.Redoc?.getSpecStructure && amf) {
362
+ return await window.Redoc.getSpecStructure(amf);
363
+ }
364
+ return null;
365
+ } catch (error) {
366
+ console.error('Failed to fetch spec structure:', error);
367
+ return null;
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Populates reference Items and assigns it to navigation for sidebar
373
+ */
374
+ private populateReferenceItems(): void {
375
+ const navOrder = [] as NavigationItem[];
376
+ for (const [index, amfConfig] of this._amfConfigList.entries()) {
377
+ let navItemChildren = [] as RedocNavigationItem[];
378
+ let isChildrenLoading = false;
379
+
380
+ if (amfConfig.referenceType !== REFERENCE_TYPES.markdown) {
381
+ if (amfConfig.isSelected) {
382
+ const redocPromise = this.fetchSpec(amfConfig).then(
383
+ (redocData) => {
384
+ if (redocData) {
385
+ this.redocSidebarData.set(amfConfig.id, redocData);
386
+ this.assignNavigationItemsFromRedoc(amfConfig, index);
387
+ }
388
+ }
389
+ );
390
+ this.redocFetchPromiseMap[amfConfig.id] = redocPromise;
391
+ }
392
+ isChildrenLoading = true;
393
+ } else {
394
+ const isExpandChildrenEnabled = this.isExpandChildrenEnabled(
395
+ amfConfig.id
396
+ );
397
+ // check whether we should expand all the child nodes, this is required for Coveo to crawl.
398
+ if (isExpandChildrenEnabled && amfConfig.topic) {
399
+ this.expandChildrenForMarkdownReferences(
400
+ amfConfig.topic.children
401
+ );
402
+ }
403
+ // Convert markdown topics to RedocNavigationItem format
404
+ navItemChildren = this.convertMarkdownTopicsToNavItems(
405
+ amfConfig.topic?.children || []
406
+ );
407
+ }
408
+
409
+ navOrder[index] = {
410
+ label: amfConfig.title,
411
+ name: amfConfig.href,
412
+ isExpanded:
413
+ amfConfig.isSelected ||
414
+ this.isExpandChildrenEnabled(amfConfig.id),
415
+ children: navItemChildren,
416
+ isChildrenLoading
417
+ };
418
+ this.parentReferenceUrls.push(amfConfig.href);
419
+ }
420
+ this.navigation = navOrder;
421
+ }
422
+
423
+ /**
424
+ * Returns a boolean indicating whether the children should be expanded or not.
425
+ */
426
+ private isExpandChildrenEnabled(referenceId: string): boolean {
427
+ return (
428
+ !!this.expandChildren && this._currentReferenceId === referenceId
429
+ );
430
+ }
431
+
432
+ /**
433
+ * Assigns Navigation Items to dx-sidebar from the Redoc structure.
434
+ */
435
+ private assignNavigationItemsFromRedoc(
436
+ amfConfig: AmfConfig,
437
+ amfIdx: number
438
+ ): void {
439
+ const referenceId = amfConfig.id;
440
+ const redocData = this.redocSidebarData.get(referenceId);
441
+
442
+ if (!redocData) {
443
+ return;
444
+ }
445
+
446
+ const children: RedocNavigationItem[] = this.convertRedocItemsToNavItems(
447
+ redocData.items,
448
+ amfConfig.href
449
+ );
450
+
451
+ this.navigation[amfIdx] = {
452
+ ...this.navigation[amfIdx],
453
+ children,
454
+ isChildrenLoading: false
455
+ };
456
+ this.navigation = [...this.navigation];
457
+ }
458
+
459
+ /**
460
+ * Convert Redoc sidebar items to navigation items with proper hash URLs
461
+ */
462
+ private convertRedocItemsToNavItems(
463
+ redocItems: RedocNavigationItem[],
464
+ baseHref: string
465
+ ): RedocNavigationItem[] {
466
+ return redocItems.map(item => ({
467
+ label: item.label,
468
+ name: `${baseHref}#${item.id}`, // Hash-based navigation
469
+ id: item.id,
470
+ children: item.children ?
471
+ this.convertRedocItemsToNavItems(item.children, baseHref) :
472
+ undefined
473
+ }));
474
+ }
475
+
476
+ /**
477
+ * Find a Redoc navigation item by its ID
478
+ */
479
+ private findRedocItemById(
480
+ items: RedocNavigationItem[],
481
+ targetId: string
482
+ ): RedocNavigationItem | null {
483
+ for (const item of items) {
484
+ if (item.id === targetId) {
485
+ return item;
486
+ }
487
+ if (item.children) {
488
+ const found = this.findRedocItemById(item.children, targetId);
489
+ if (found) {
490
+ return found;
491
+ }
492
+ }
493
+ }
494
+ return null;
495
+ }
496
+
497
+ /**
498
+ * Convert markdown topics to RedocNavigationItem format for unified sidebar handling
499
+ */
500
+ private convertMarkdownTopicsToNavItems(
501
+ markdownTopics: ParsedMarkdownTopic[]
502
+ ): RedocNavigationItem[] {
503
+ return markdownTopics.map(topic => ({
504
+ label: topic.label,
505
+ name: topic.link?.href || '',
506
+ id: topic.label.toLowerCase().replace(/\s+/g, '-'),
507
+ children: topic.children ?
508
+ this.convertMarkdownTopicsToNavItems(topic.children) :
509
+ undefined
510
+ }));
511
+ }
512
+
513
+ /**
514
+ * Update the DOM on the first load
515
+ */
516
+ updateView(): void {
517
+ const referenceId = this._currentReferenceId;
518
+ const specBasedReference = this.isSpecBasedReference(referenceId);
519
+
520
+ if (specBasedReference) {
521
+ // Wait till the spec is loaded.
522
+ this.redocFetchPromiseMap[referenceId].then(() => {
523
+ this.loadSpecReferenceContent(referenceId);
524
+ this.updateDocPhase();
525
+
526
+ // Handle hash navigation after content is loaded
527
+ setTimeout(() => {
528
+ this.handleHashNavigation();
529
+ }, 1000);
530
+ });
531
+ } else {
532
+ // Handle markdown references if needed
533
+ this.loadMarkdownBasedReference();
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Load and render Redoc content for the selected reference
539
+ */
540
+ private async loadSpecReferenceContent(referenceId: string): Promise<void> {
541
+ const amfConfig = this.getAmfConfigWithId(referenceId);
542
+ if (!amfConfig) {
543
+ return;
544
+ }
545
+
546
+ const redocContainer = this.template.querySelector('.redoc-container');
547
+ if (!redocContainer) {
548
+ return;
549
+ }
550
+
551
+ try {
552
+ // Clear existing content
553
+ while (redocContainer.firstChild) {
554
+ redocContainer.firstChild.remove();
555
+ }
556
+
557
+ // Initialize Redoc
558
+ if (window.Redoc && amfConfig.amf) {
559
+ await window.Redoc.init(amfConfig.amf, {
560
+ expandResponses: '200,400',
561
+ scrollYOffset: 73,
562
+ sidebar: false
563
+ }, redocContainer);
564
+
565
+ // Set up scroll sync after Redoc is loaded
566
+ setTimeout(() => {
567
+ this.setupScrollSync();
568
+ }, 2000);
569
+ }
570
+ } catch (error) {
571
+ console.error('Failed to load Redoc:', error);
572
+
573
+ // Create error elements properly without innerHTML
574
+ const errorContainer = document.createElement('div');
575
+ errorContainer.className = 'error-container';
576
+
577
+ const errorMessage = document.createElement('div');
578
+ errorMessage.className = 'error-message';
579
+ errorMessage.textContent = 'Failed to load API documentation';
580
+
581
+ errorContainer.appendChild(errorMessage);
582
+ redocContainer.appendChild(errorContainer);
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Set up scroll synchronization between sidebar and Redoc content
588
+ */
589
+ private setupScrollSync(): void {
590
+ const contentArea = this.template.querySelector('.api-documentation');
591
+ if (!contentArea) {
592
+ return;
593
+ }
594
+
595
+ contentArea.addEventListener('scroll', this._boundHandleRedocScroll);
596
+ }
597
+
598
+ /**
599
+ * Handle scroll events in Redoc content to update active sidebar item
600
+ */
601
+ private handleRedocScroll(): void {
602
+ if (this.isProgrammaticScroll) {
603
+ return;
604
+ }
605
+
606
+ if (this.scrollTimeout) {
607
+ clearTimeout(this.scrollTimeout);
608
+ }
609
+
610
+ this.scrollTimeout = window.setTimeout(() => {
611
+ this.updateActiveSidebarItemFromScroll();
612
+ }, 50);
613
+ }
614
+
615
+ /**
616
+ * Update active sidebar item based on scroll position
617
+ */
618
+ private updateActiveSidebarItemFromScroll(): void {
619
+ const redocSections = this.template.querySelectorAll('[data-section-id]');
620
+ if (redocSections.length === 0) {
621
+ return;
622
+ }
623
+
624
+ let activeSection: string | null = null;
625
+ let bestScore = -Infinity;
626
+ const viewportHeight = window.innerHeight;
627
+ const scrollThreshold = viewportHeight * 0.3;
628
+
629
+ redocSections.forEach(section => {
630
+ const sectionId = section.getAttribute('data-section-id');
631
+ if (!sectionId) {
632
+ return;
633
+ }
634
+
635
+ const rect = section.getBoundingClientRect();
636
+ const visibleHeight = Math.max(0, Math.min(viewportHeight, rect.bottom) - Math.max(0, rect.top));
637
+ const visibilityPercentage = visibleHeight / section.clientHeight;
638
+
639
+ let score = 0;
640
+ if (visibilityPercentage > 0.05) {
641
+ score += visibilityPercentage * 100;
642
+
643
+ if (rect.top <= scrollThreshold && rect.top >= -scrollThreshold) {
644
+ score += 100;
645
+ }
646
+ }
647
+
648
+ if (score > bestScore) {
649
+ bestScore = score;
650
+ activeSection = sectionId;
651
+ }
652
+ });
653
+
654
+ if (activeSection && activeSection !== this.currentActiveSection) {
655
+ this.currentActiveSection = activeSection;
656
+ this.updateSelectedSidebarItem(activeSection);
657
+
658
+ // Update URL hash
659
+ if (window.location.hash !== `#${activeSection}`) {
660
+ window.history.replaceState(null, '', `#${activeSection}`);
661
+ }
662
+ }
663
+ }
664
+
665
+ /**
666
+ * Update the selected sidebar item based on section ID
667
+ */
668
+ private updateSelectedSidebarItem(sectionId: string): void {
669
+ const currentRef = this.getAmfConfigWithId(this._currentReferenceId);
670
+ if (currentRef) {
671
+ this.selectedSidebarValue = `${currentRef.href}#${sectionId}`;
672
+ }
673
+ }
674
+
675
+ /**
676
+ * Handle hash navigation from URL changes
677
+ */
678
+ private handleHashNavigation(): void {
679
+ const hash = window.location.hash;
680
+ if (hash) {
681
+ const sectionId = hash.substring(1);
682
+ this.scrollToSection(sectionId);
683
+ this.updateSelectedSidebarItem(sectionId);
684
+ }
685
+ }
686
+
687
+ /**
688
+ * Scroll to a specific section in Redoc content
689
+ */
690
+ private scrollToSection(sectionId: string): void {
691
+ this.isProgrammaticScroll = true;
692
+
693
+ const targetSection = this.template.querySelector(`[data-section-id="${sectionId}"]`);
694
+ if (targetSection) {
695
+ targetSection.scrollIntoView({
696
+ behavior: 'smooth',
697
+ block: 'start'
698
+ });
699
+
700
+ // Reset flag after scroll animation
701
+ setTimeout(() => {
702
+ this.isProgrammaticScroll = false;
703
+ }, 1000);
704
+ } else {
705
+ this.isProgrammaticScroll = false;
706
+ }
707
+ }
708
+
709
+ /**
710
+ * Handle navigation selection from sidebar
711
+ */
712
+ onNavSelect(event: CustomEvent): void {
713
+ const name = event.detail.name;
714
+ if (name) {
715
+ const urlReferenceId = this.getReferenceIdFromUrl(name);
716
+ const specBasedReference = this.isSpecBasedReference(urlReferenceId);
717
+
718
+ if (specBasedReference) {
719
+ // Extract hash from the name (URL)
720
+ const hashIndex = name.indexOf('#');
721
+ if (hashIndex > -1) {
722
+ const sectionId = name.substring(hashIndex + 1);
723
+
724
+ // Update URL hash and scroll to section
725
+ window.location.hash = sectionId;
726
+
727
+ // Update page title with section name
728
+ const redocData = this.redocSidebarData.get(urlReferenceId);
729
+ if (redocData) {
730
+ const sectionItem = this.findRedocItemById(redocData.items, sectionId);
731
+ if (sectionItem) {
732
+ this.updateTags(sectionItem.label);
733
+ }
734
+ }
735
+
736
+ logCoveoPageView(
737
+ this.coveoOrganizationId,
738
+ this.coveoAnalyticsToken
739
+ );
740
+ } else {
741
+ // This is a parent reference selection
742
+ if (this.isParentReferencePath(name)) {
743
+ this.loadNewReferenceItem(name);
744
+ }
745
+ }
746
+ } else {
747
+ this.loadMarkdownBasedReference(name);
748
+ }
749
+ }
750
+ }
751
+
752
+ onExpandCollapse(event: CustomEvent): void {
753
+ const { name, isSelectAction, isExpanded } = event.detail;
754
+ if (!isSelectAction && isExpanded) {
755
+ const referenceId = this.getReferenceIdFromUrl(name);
756
+ const currentUrl = window.location.href;
757
+ const currentReferenceId = this.getReferenceIdFromUrl(currentUrl);
758
+
759
+ if (referenceId !== currentReferenceId) {
760
+ const isSpecBasedReference = this.isSpecBasedReference(referenceId);
761
+ if (isSpecBasedReference) {
762
+ this.onNavSelect(event);
763
+ }
764
+ }
765
+ }
766
+ }
767
+
768
+ /**
769
+ * The API Navigation event will always intend to navigate within the current reference
770
+ */
771
+ protected onApiNavigationChanged(): void {
772
+ // Handle API navigation changes if needed
773
+ restoreScroll();
774
+ }
775
+
776
+ /**
777
+ * This method gets called when the user navigates back and forth using browser arrows
778
+ */
779
+ protected updateSelectedItemFromUrlQuery(): void {
780
+ const specBasedReference = this.isSpecBasedReference(this._currentReferenceId);
781
+ if (specBasedReference) {
782
+ this.handleHashNavigation();
783
+ } else {
784
+ this.loadMarkdownBasedReference();
785
+ }
786
+ restoreScroll();
787
+ }
788
+
789
+ /**
790
+ * Returns whether given url is parent reference path
791
+ */
792
+ private isParentReferencePath(urlPath?: string | null): boolean {
793
+ if (!urlPath) {
794
+ return false;
795
+ }
796
+ const parentReferenceIndex = this.parentReferenceUrls.findIndex(
797
+ (referenceUrl: string) => {
798
+ return urlPath.endsWith(referenceUrl);
799
+ }
800
+ );
801
+ return parentReferenceIndex !== -1;
802
+ }
803
+
804
+ /**
805
+ * Navigates to reference of the given URL
806
+ */
807
+ private loadNewReferenceItem(url: string): void {
808
+ const referenceId = this.getReferenceIdFromUrl(url);
809
+ const referenceItem = this.getAmfConfigWithId(referenceId);
810
+ if (referenceItem) {
811
+ window.location.href = referenceItem.href;
812
+ }
813
+ }
814
+
815
+ /**
816
+ * Load markdown-based reference
817
+ */
818
+ private loadMarkdownBasedReference(referenceUrl?: string | null): void {
819
+ let referenceId = "";
820
+ const currentUrl = window.location.href;
821
+
822
+ if (this.isProjectRootPath()) {
823
+ /**
824
+ * CASE1: This case is to consider when the user navigates to references by clicking a project card
825
+ * Ex: /docs/example-project/references should navigate to the first topic in the first reference
826
+ */
827
+ referenceId = this._currentReferenceId;
828
+ } else if (this.isParentReferencePath(referenceUrl)) {
829
+ /**
830
+ * CASE2: This case is to navigate to respective reference when the user clicked on root item
831
+ * Ex: .../references/markdown-ref should navigate to first topic.
832
+ */
833
+ referenceId = this.getReferenceIdFromUrl(referenceUrl!);
834
+ } else if (this.isParentReferencePath(currentUrl)) {
835
+ /**
836
+ * CASE3: This case is to navigate to respective reference when the user entered url with reference id
837
+ * Ex: .../references/markdown-ref should navigate to first topic.
838
+ */
839
+ referenceId = this.getReferenceIdFromUrl(currentUrl);
840
+ } else if (referenceUrl) {
841
+ /**
842
+ * CASE4: This case is to navigate to first item when we don't have topic in the selected version
843
+ * Ex: .../references/markdown-ref/not-existed-topic-url should navigate to first topic.
844
+ */
845
+ referenceId = this.getReferenceIdFromUrl(referenceUrl);
846
+ }
847
+
848
+ let isRedirecting = false;
849
+ if (referenceId) {
850
+ const amfConfig = this.getAmfConfigWithId(referenceId);
851
+ let redirectReferenceUrl = "";
852
+ if (amfConfig && amfConfig.topic) {
853
+ const childrenItems = amfConfig.topic.children;
854
+ if (childrenItems && childrenItems.length > 0) {
855
+ redirectReferenceUrl = childrenItems[0].link!.href;
856
+ }
857
+ }
858
+ if (redirectReferenceUrl) {
859
+ if (this.isParentReferencePath(referenceUrl)) {
860
+ // This is for CASE2 mentioned above, Where we need to navigate user to respective href
861
+ isRedirecting = true;
862
+ window.location.href = redirectReferenceUrl;
863
+ } else {
864
+ // This is for CASE 1,3 and 4 mentioned above, Where we need to update the browser history
865
+ window.history.replaceState(
866
+ window.history.state,
867
+ "",
868
+ redirectReferenceUrl
869
+ );
870
+ }
871
+ }
872
+ }
873
+
874
+ if (!isRedirecting) {
875
+ // Update title for markdown references
876
+ // For markdown, we could extract title from the page or use a default
877
+ this.updateTags("Reference Documentation");
878
+
879
+ this.versions = this.getVersions();
880
+ if (this.oldVersionInfo) {
881
+ this.showVersionBanner = true;
882
+ } else {
883
+ this.latestVersion = true;
884
+ }
885
+
886
+ this.isVersionFetched = true;
887
+ this.updateDocPhase();
888
+ this.selectedSidebarValue = window.location.pathname;
889
+ }
890
+ }
891
+
892
+ /**
893
+ * Handle version change
894
+ */
895
+ handleVersionChange(): void {
896
+ const currentUrl = window.location.href;
897
+ window.sessionStorage.setItem(
898
+ this.docsReferenceUrlSessionKey,
899
+ currentUrl
900
+ );
901
+ }
902
+
903
+ handleDismissVersionBanner() {
904
+ this.showVersionBanner = false;
905
+ }
906
+
907
+ /**
908
+ * Updates doc phase of selected reference
909
+ */
910
+ updateDocPhase(): void {
911
+ /* If parent level doc phase is enabled, Individual reference level doc phase should not be considered */
912
+
913
+ if (!this.isParentLevelDocPhaseEnabled) {
914
+ const selectedReference = this._amfConfigList.find(
915
+ (referenceItem: AmfConfig) => {
916
+ return referenceItem.id === this._currentReferenceId;
917
+ }
918
+ );
919
+ if (selectedReference) {
920
+ this.selectedReferenceDocPhase = JSON.stringify(
921
+ selectedReference.docPhase
922
+ );
923
+ }
924
+ }
925
+ }
926
+
927
+ /**
928
+ * Normalizes topic identifier by replacing spaces with '+'
929
+ * and running encodeURI() on it
930
+ * @param identifier raw identifier for a topic as parsed from the spec file
931
+ * @returns normalized and encoded identifier
932
+ */
933
+ protected encodeIdentifier(identifier: string): string {
934
+ let result = identifier.trim();
935
+ result = result.replace(new RegExp(/\s+/, "g"), "+");
936
+ return encodeURI(result);
937
+ }
938
+
939
+ /**
940
+ * Gets the encoded url.
941
+ * This method will return the encoded url for 2 cases,
942
+ * 1. If the url is encoded already
943
+ * 2. If the url is decoded
944
+ */
945
+ getUrlEncoded(url: string): string {
946
+ // if url matches, then return the encoded url.
947
+ if (decodeURIComponent(url) === url) {
948
+ return encodeURIComponent(url);
949
+ }
950
+ // return the encoded url.
951
+ return this.getUrlEncoded(decodeURIComponent(url));
952
+ }
953
+
954
+ /**
955
+ * Constructs a Reference Topic ID
956
+ */
957
+ protected getFormattedIdentifier(referenceId: string, id: string): string {
958
+ return `${referenceId}:${id}`;
959
+ }
960
+
961
+ /**
962
+ * Update browser URL with hash-based navigation
963
+ */
964
+ protected updateUrlWithHash(hash: string): void {
965
+ if (hash) {
966
+ window.history.pushState({}, "", `#${hash}`);
967
+ }
968
+ }
969
+
970
+ /**
971
+ * Replace browser URL with hash-based navigation (no history entry)
972
+ */
973
+ protected replaceUrlWithHash(hash: string): void {
974
+ if (hash) {
975
+ window.history.replaceState(null, '', `#${hash}`);
976
+ }
977
+ }
978
+
979
+ /**
980
+ * Update page title and meta tags
981
+ */
982
+ private updateTags(navTitle = ""): void {
983
+ if (!navTitle) {
984
+ return;
985
+ }
986
+
987
+ // this is required to update the nav title meta tag.
988
+ // eslint-disable-next-line @lwc/lwc/no-document-query
989
+ const metaNavTitle = document.querySelector('meta[name="nav-title"]');
990
+ // eslint-disable-next-line @lwc/lwc/no-document-query
991
+ const titleTag = document.querySelector("title");
992
+ const TITLE_SEPARATOR = " | ";
993
+
994
+ if (metaNavTitle) {
995
+ metaNavTitle.setAttribute("content", navTitle);
996
+ }
997
+
998
+ /**
999
+ * Right now, the title tag only changes when you pick a Ref spec,
1000
+ * not every time you choose a subsection of the Ref spec.
1001
+ * This update aims to refresh the title tag with each selection.
1002
+ * If a Ref spec is chosen, we add the value of the <selected topic> to the title.
1003
+ * If a subsection is selected, we update the first part of the current
1004
+ * title with the new <selected topic>.
1005
+ * Example: Following is a sample project structure.
1006
+ * - Project Name
1007
+ * - Ref Spec1
1008
+ * - Summary
1009
+ * - Endpoints
1010
+ * - E1
1011
+ * - E2
1012
+ * - Ref Spec2
1013
+ * - Summary
1014
+ * - Endpoints
1015
+ * - E1 (Selected)
1016
+ * - E2
1017
+ * Previous Title: Ref Spec2 | Project Name | Salesforce Developer
1018
+ * New Title: E1 | Ref Spec2 | Project Name | Salesforce Developer
1019
+ *
1020
+ */
1021
+ if (titleTag) {
1022
+ let titleTagValue = titleTag.textContent;
1023
+ const titleTagSectionValues: string[] =
1024
+ titleTagValue?.split(TITLE_SEPARATOR) || [];
1025
+ if (titleTagSectionValues.length > 0) {
1026
+ if (titleTagSectionValues.length <= 3) {
1027
+ titleTagValue = navTitle + TITLE_SEPARATOR + titleTagValue;
1028
+ } else {
1029
+ titleTagSectionValues[0] = navTitle;
1030
+ titleTagValue = titleTagSectionValues.join(TITLE_SEPARATOR);
1031
+ }
1032
+ }
1033
+ titleTag.textContent = titleTagValue;
1034
+ }
1035
+ }
1036
+
1037
+ /**
1038
+ * Expands the children of Markdown-based References.
1039
+ */
1040
+ private expandChildrenForMarkdownReferences(
1041
+ children: ParsedMarkdownTopic[]
1042
+ ): void {
1043
+ if (!children) {
1044
+ return;
1045
+ }
1046
+ for (const childNode of children) {
1047
+ childNode.isExpanded = true;
1048
+ this.expandChildrenForMarkdownReferences(childNode.children);
1049
+ }
1050
+ }
1051
+
1052
+ /**
1053
+ * Returns whether current location is project root path like ../example-project/references
1054
+ */
1055
+ private isProjectRootPath(): boolean {
1056
+ return this.getReferenceIdFromUrl(window.location.href) === "";
1057
+ }
1058
+
1059
+ /**
1060
+ * Returns the current hash from URL (for hash-based navigation)
1061
+ * Note: This replaces the old meta query param logic
1062
+ */
1063
+ getMetaFromUrl(): string {
1064
+ // For hash-based navigation, extract from hash instead
1065
+ const hash = window.location.hash;
1066
+ if (hash) {
1067
+ return hash.substring(1); // Remove '#' prefix
1068
+ }
1069
+
1070
+ return "";
1071
+ }
1072
+
1073
+ /**
1074
+ * Convert any case to a readable Title
1075
+ * ex: snake_case => Snake case
1076
+ * ex: camelCase => Camel case
1077
+ * ex: PascalCase => Pascal case
1078
+ * @param label
1079
+ * @returns string
1080
+ */
1081
+ private getTitleForLabel(label: string): string {
1082
+ // Simple sentence case conversion without external dependencies
1083
+ return label
1084
+ .replace(/([A-Z])/g, ' $1') // Add space before capitals
1085
+ .replace(/[-_]/g, ' ') // Replace dashes and underscores with spaces
1086
+ .trim()
1087
+ .toLowerCase()
1088
+ .replace(/^\w/, c => c.toUpperCase()); // Capitalize first letter
1089
+ }
1090
+ }
@@ -0,0 +1,24 @@
1
+ // Redoc type declarations for window object
2
+ export interface RedocNavigationItem {
3
+ label: string;
4
+ name: string; // Hash-based URL (#section-id)
5
+ id: string; // Section ID for scroll targeting
6
+ children?: RedocNavigationItem[];
7
+ }
8
+
9
+ export interface RedocSidebarData {
10
+ spec: {
11
+ title: string;
12
+ version: string;
13
+ };
14
+ items: RedocNavigationItem[];
15
+ }
16
+
17
+ declare global {
18
+ interface Window {
19
+ Redoc?: {
20
+ init: (specUrl: string, options: any, container: Element) => Promise<void>;
21
+ getSpecStructure?: (specUrl: string) => Promise<RedocSidebarData>;
22
+ };
23
+ }
24
+ }
package/LICENSE DELETED
@@ -1,12 +0,0 @@
1
- Copyright (c) 2020, Salesforce.com, Inc.
2
- All rights reserved.
3
-
4
- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
-
6
- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
-
8
- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
-
10
- * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11
-
12
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.