@salesforcedevs/docs-components 1.17.6-hover-edit → 1.17.6-redoc2
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 +1 -2
- package/package.json +5 -1
- package/src/modules/doc/componentPlayground/componentPlayground.css +1 -1
- package/src/modules/doc/componentPlayground/componentPlayground.html +2 -2
- package/src/modules/doc/componentPlayground/componentPlayground.ts +13 -0
- package/src/modules/doc/flexReference/constants.ts +20 -0
- package/src/modules/doc/flexReference/flexReference.css +52 -0
- package/src/modules/doc/flexReference/flexReference.html +62 -0
- package/src/modules/doc/flexReference/flexReference.ts +1090 -0
- package/src/modules/doc/flexReference/types.ts +80 -0
- package/src/modules/doc/xmlContent/xmlContent.html +9 -1
- package/src/modules/doc/xmlContent/xmlContent.ts +18 -8
- package/src/modules/doc/markdownEditor/markdownEditor.css +0 -225
- package/src/modules/doc/markdownEditor/markdownEditor.html +0 -80
- package/src/modules/doc/markdownEditor/markdownEditor.ts +0 -148
- package/src/modules/doc/textSelectionSearch/README.md +0 -185
- package/src/modules/doc/textSelectionSearch/textSelectionSearch.css +0 -286
- package/src/modules/doc/textSelectionSearch/textSelectionSearch.html +0 -90
- package/src/modules/doc/textSelectionSearch/textSelectionSearch.js +0 -236
- package/src/modules/doc/textSelectionSearch/textSelectionSearch.ts +0 -257
|
@@ -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 "./types";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
REFERENCE_TYPES,
|
|
13
|
+
oldReferenceIdNewReferenceIdMap
|
|
14
|
+
} from "./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 FlexReference 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
|
+
}
|