@salesforcedevs/docs-components 0.7.59-sppage-alpha1 → 0.7.76-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lwc.config.json +3 -0
- package/package.json +9 -4
- package/src/modules/doc/amfReference/amfReference.css +0 -12
- package/src/modules/doc/amfReference/amfReference.html +1 -6
- package/src/modules/doc/amfReference/amfReference.ts +10 -37
- package/src/modules/doc/amfTopic/amfTopic.ts +24 -0
- package/src/modules/doc/breadcrumbs/breadcrumbs.html +0 -1
- package/src/modules/doc/breadcrumbs/breadcrumbs.ts +14 -23
- package/src/modules/doc/componentPlayground/componentPlayground.css +30 -0
- package/src/modules/doc/componentPlayground/componentPlayground.html +20 -0
- package/src/modules/doc/componentPlayground/componentPlayground.ts +97 -0
- package/src/modules/doc/content/content.html +1 -0
- package/src/modules/doc/content/content.ts +7 -33
- package/src/modules/doc/contentCallout/contentCallout.css +1 -0
- package/src/modules/doc/contentLayout/contentLayout.css +27 -123
- package/src/modules/doc/contentLayout/contentLayout.html +42 -36
- package/src/modules/doc/contentLayout/contentLayout.ts +152 -204
- package/src/modules/doc/contentMedia/contentMedia.css +1 -1
- package/src/modules/doc/header/header.html +8 -3
- package/src/modules/doc/header/header.ts +49 -10
- package/src/modules/doc/lwcContentLayout/lwcContentLayout.css +9 -0
- package/src/modules/doc/lwcContentLayout/lwcContentLayout.html +64 -0
- package/src/modules/doc/lwcContentLayout/lwcContentLayout.ts +269 -0
- package/src/modules/doc/phase/phase.css +0 -7
- package/src/modules/doc/redocReference/redocReference.css +7 -0
- package/src/modules/doc/redocReference/redocReference.html +13 -0
- package/src/modules/doc/redocReference/redocReference.ts +427 -0
- package/src/modules/doc/specificationContent/specificationContent.css +33 -0
- package/src/modules/doc/specificationContent/specificationContent.html +94 -16
- package/src/modules/doc/specificationContent/specificationContent.ts +131 -21
- package/src/modules/doc/versionPicker/versionPicker.html +2 -0
- package/src/modules/doc/xmlContent/xmlContent.css +0 -10
- package/src/modules/doc/xmlContent/xmlContent.html +11 -8
- package/src/modules/doc/xmlContent/xmlContent.ts +76 -57
- package/src/modules/docHelpers/amfStyle/amfStyle.css +0 -2
- package/src/modules/docHelpers/contentLayoutStyle/contentLayoutStyle.css +160 -0
- package/src/modules/docUtils/utils/__mocks__/coveo.analytics.ts +16 -0
- package/src/modules/docUtils/utils/coveo.analytics.d.ts +10 -0
- package/src/modules/docUtils/utils/utils.ts +1 -1
|
@@ -1,56 +1,166 @@
|
|
|
1
1
|
import { LightningElement, track, api } from "lwc";
|
|
2
|
+
import { Method, Specification } from "typings/custom";
|
|
3
|
+
import debounce from "debounce";
|
|
2
4
|
|
|
5
|
+
const CX_ROUTER_API: string = "/cx-router/components";
|
|
3
6
|
export default class SpecificationContent extends LightningElement {
|
|
4
7
|
@track data: any;
|
|
5
|
-
@api component: string = "button";
|
|
6
|
-
@api type: string = "lightning";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
@api component!: string;
|
|
10
|
+
@api model!: string;
|
|
11
|
+
@api namespace!: string;
|
|
12
|
+
|
|
13
|
+
isLoading: boolean = true;
|
|
14
|
+
showError: boolean = false;
|
|
15
|
+
|
|
16
|
+
private attributes: Specification[] = [];
|
|
17
|
+
private methods: Method[] = [];
|
|
18
|
+
private slots: Specification[] = [];
|
|
19
|
+
private events: Specification[] = [];
|
|
20
|
+
private _processedEvents: Specification[] = [];
|
|
21
|
+
|
|
22
|
+
/* TODO: For now setting the timeout to 300ms,
|
|
23
|
+
* post integration with CX-Router API will test and change if required.
|
|
24
|
+
*/
|
|
25
|
+
private debouncedNotifyDataRendered = debounce(() => {
|
|
26
|
+
this.notifySpecificationDataRendered();
|
|
27
|
+
}, 300);
|
|
12
28
|
|
|
13
29
|
connectedCallback() {
|
|
14
|
-
this.
|
|
30
|
+
this.fetchComponentMetadata();
|
|
15
31
|
}
|
|
16
32
|
|
|
17
|
-
async
|
|
18
|
-
const
|
|
33
|
+
async fetchComponentMetadata() {
|
|
34
|
+
const componentQueryParams = {
|
|
35
|
+
model: this.model,
|
|
36
|
+
namespace: this.namespace,
|
|
37
|
+
component: this.component
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const queryString = new URLSearchParams(
|
|
41
|
+
componentQueryParams
|
|
42
|
+
).toString();
|
|
43
|
+
const url = `${CX_ROUTER_API}?${queryString}`;
|
|
19
44
|
|
|
20
45
|
try {
|
|
21
46
|
const response = await fetch(url);
|
|
22
47
|
|
|
23
48
|
if (!response.ok) {
|
|
49
|
+
// TODO: Will add loader and show error as follow-up
|
|
24
50
|
throw new Error(`Failed to fetch: ${response.statusText}`);
|
|
25
51
|
}
|
|
26
52
|
|
|
27
53
|
const result = await response.json();
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
54
|
+
|
|
55
|
+
// Due to middleware the data is sent as part of response now.
|
|
56
|
+
this.data = result?.response;
|
|
57
|
+
if (this.data) {
|
|
58
|
+
({
|
|
59
|
+
attributes: this.attributes,
|
|
60
|
+
methods: this.methods,
|
|
61
|
+
slots: this.slots,
|
|
62
|
+
events: this.events
|
|
63
|
+
} = this.data);
|
|
64
|
+
|
|
65
|
+
// Process events once when data is set
|
|
66
|
+
this._processedEvents = this.events
|
|
67
|
+
? this.events.map((event) => {
|
|
68
|
+
const capitalizedName = event.name
|
|
69
|
+
? event.name.charAt(0).toUpperCase() +
|
|
70
|
+
event.name.slice(1)
|
|
71
|
+
: "";
|
|
72
|
+
return {
|
|
73
|
+
...event,
|
|
74
|
+
nameCapitalized: capitalizedName
|
|
75
|
+
};
|
|
76
|
+
})
|
|
77
|
+
: [];
|
|
78
|
+
}
|
|
35
79
|
} catch (error) {
|
|
36
80
|
this.data = {};
|
|
37
|
-
|
|
81
|
+
this.showError = true;
|
|
82
|
+
console.error("fetchComponentMetadata() failed:", error);
|
|
83
|
+
} finally {
|
|
84
|
+
this.isLoading = false;
|
|
38
85
|
}
|
|
39
86
|
}
|
|
40
87
|
|
|
88
|
+
/**
|
|
89
|
+
* This getter is to preprocess the methods for easier rendering in the template.
|
|
90
|
+
* Each method is augmented with additional properties:
|
|
91
|
+
* - `firstArgument`: The first argument (if any).
|
|
92
|
+
* - `remainingArguments`: All other arguments (if any).
|
|
93
|
+
* - `hasArguments`: A boolean indicating whether the method has arguments or not.
|
|
94
|
+
*/
|
|
95
|
+
get processedMethods(): Method[] {
|
|
96
|
+
return this.methods.map((method) => {
|
|
97
|
+
const [firstArgument, ...remainingArguments] =
|
|
98
|
+
method.arguments || [];
|
|
99
|
+
return {
|
|
100
|
+
...method,
|
|
101
|
+
firstArgument,
|
|
102
|
+
remainingArguments,
|
|
103
|
+
hasArguments: method.arguments && method.arguments.length > 0,
|
|
104
|
+
cssForMultipleArguments:
|
|
105
|
+
remainingArguments.length > 0 ? "left-border" : ""
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Returns the preprocessed events for easier rendering in the template.
|
|
112
|
+
* Each event is augmented with a capitalized name property that capitalizes the first letter
|
|
113
|
+
* while maintaining camelCase format.
|
|
114
|
+
*/
|
|
115
|
+
get processedEvents(): Specification[] {
|
|
116
|
+
return this._processedEvents;
|
|
117
|
+
}
|
|
118
|
+
|
|
41
119
|
get hasAttributes() {
|
|
42
|
-
return this.attributes;
|
|
120
|
+
return this.attributes?.length > 0;
|
|
43
121
|
}
|
|
44
122
|
|
|
45
123
|
get hasMethods() {
|
|
46
|
-
return this.methods;
|
|
124
|
+
return this.methods?.length > 0;
|
|
47
125
|
}
|
|
48
126
|
|
|
49
127
|
get hasSlots() {
|
|
50
|
-
return this.slots;
|
|
128
|
+
return this.slots?.length > 0;
|
|
51
129
|
}
|
|
52
130
|
|
|
53
131
|
get hasEvents() {
|
|
54
|
-
return this.events;
|
|
132
|
+
return this.events?.length > 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
get showNoSpecifications() {
|
|
136
|
+
return (
|
|
137
|
+
!this.showError &&
|
|
138
|
+
!this.isLoading &&
|
|
139
|
+
!this.hasAttributes &&
|
|
140
|
+
!this.hasMethods &&
|
|
141
|
+
!this.hasSlots &&
|
|
142
|
+
!this.hasEvents
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get isModelAura() {
|
|
147
|
+
return this.model === "aura";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
renderedCallback(): void {
|
|
151
|
+
if (this.data) {
|
|
152
|
+
this.debouncedNotifyDataRendered();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
notifySpecificationDataRendered() {
|
|
157
|
+
// Dispatch a custom event to notify the specification tab has rendered.
|
|
158
|
+
this.dispatchEvent(
|
|
159
|
+
new CustomEvent("specificationdatarendered", {
|
|
160
|
+
detail: { name: "doc-specification-content" },
|
|
161
|
+
bubbles: true,
|
|
162
|
+
composed: true
|
|
163
|
+
})
|
|
164
|
+
);
|
|
55
165
|
}
|
|
56
166
|
}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
<div lwc:if={showVersionPicker} class="version-picker-container">
|
|
3
3
|
<dx-dropdown
|
|
4
4
|
options={versions}
|
|
5
|
+
analytics-event="custEv_docVersionSelect"
|
|
6
|
+
analytics-payload={analyticsPayload}
|
|
5
7
|
value={selectedVersion.id}
|
|
6
8
|
width="var(--doc-version-picker-width)"
|
|
7
9
|
onchange={onVersionChange}
|
|
@@ -31,10 +31,6 @@ dx-dropdown > dx-button:hover {
|
|
|
31
31
|
--border-color: var(--button-primary-color-hover);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
doc-phase {
|
|
35
|
-
--doc-c-phase-top: var(--dx-g-global-header-height);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
34
|
@media screen and (max-width: 768px) {
|
|
39
35
|
doc-content-layout {
|
|
40
36
|
--dx-g-doc-header-main-nav-height: 41px;
|
|
@@ -45,10 +41,4 @@ doc-phase {
|
|
|
45
41
|
var(--dx-g-global-header-height) + var(--dx-g-doc-header-height)
|
|
46
42
|
);
|
|
47
43
|
}
|
|
48
|
-
|
|
49
|
-
doc-phase {
|
|
50
|
-
--doc-c-phase-top: calc(
|
|
51
|
-
var(--dx-g-global-header-height) + var(--dx-g-doc-header-height)
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
44
|
}
|
|
@@ -1,23 +1,18 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<doc-content-layout
|
|
3
|
-
lwc:if={
|
|
3
|
+
lwc:if={displayContent}
|
|
4
4
|
lwc:ref="docContentLayout"
|
|
5
|
-
coveo-organization-id={coveoOrganizationId}
|
|
6
|
-
coveo-public-access-token={coveoPublicAccessToken}
|
|
7
|
-
coveo-analytics-token={coveoAnalyticsToken}
|
|
8
|
-
coveo-search-hub={coveoSearchHub}
|
|
9
|
-
coveo-advanced-query-config={coveoAdvancedQueryConfig}
|
|
10
5
|
sidebar-header={docTitle}
|
|
11
6
|
sidebar-content={sidebarContent}
|
|
12
7
|
sidebar-value={sidebarValue}
|
|
13
8
|
onselect={handleSelect}
|
|
14
|
-
use-old-sidebar={useOldSidebar}
|
|
15
9
|
onlangchange={handleLanguageChange}
|
|
16
10
|
languages={sidebarFooterContent.languages}
|
|
17
11
|
language={sidebarFooterContent.language}
|
|
18
12
|
bail-href={sidebarFooterContent.bailHref}
|
|
19
13
|
bail-label={sidebarFooterContent.bailLabel}
|
|
20
14
|
show-footer={enableFooter}
|
|
15
|
+
reading-time={computedReadingTime}
|
|
21
16
|
>
|
|
22
17
|
<doc-phase
|
|
23
18
|
slot="version-banner"
|
|
@@ -30,7 +25,7 @@
|
|
|
30
25
|
<div lwc:if={showVersionPicker} slot="sidebar-header">
|
|
31
26
|
<doc-version-picker
|
|
32
27
|
data-type="version"
|
|
33
|
-
analytics-event="
|
|
28
|
+
analytics-event="custEv_linkClick"
|
|
34
29
|
analytics-payload={ANALYTICS_PAYLOAD}
|
|
35
30
|
versions={versionOptions}
|
|
36
31
|
selected-version={version}
|
|
@@ -49,4 +44,12 @@
|
|
|
49
44
|
onnavclick={handleNavClick}
|
|
50
45
|
></doc-content>
|
|
51
46
|
</doc-content-layout>
|
|
47
|
+
<div lwc:if={display404}>
|
|
48
|
+
<dx-error
|
|
49
|
+
image="https://a.sfdcstatic.com/developer-website/prod/images/404.svg"
|
|
50
|
+
code="404"
|
|
51
|
+
header="Beep boop. That did not compute."
|
|
52
|
+
subtitle="The document you're looking for doesn't seem to exist."
|
|
53
|
+
></dx-error>
|
|
54
|
+
</div>
|
|
52
55
|
</template>
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @lwc/lwc/no-document-query */
|
|
2
2
|
import { api, track } from "lwc";
|
|
3
|
-
import { normalizeBoolean } from "dxUtils/normalizers";
|
|
4
3
|
import { FetchContent } from "./utils";
|
|
5
4
|
import {
|
|
6
|
-
CoveoAdvancedQueryXMLConfig,
|
|
7
5
|
DocLanguage,
|
|
8
6
|
DocVersion,
|
|
9
7
|
TreeNode,
|
|
@@ -11,13 +9,15 @@ import {
|
|
|
11
9
|
SiderbarFooter,
|
|
12
10
|
HistoryState,
|
|
13
11
|
PageReference,
|
|
14
|
-
TocMap
|
|
12
|
+
TocMap,
|
|
13
|
+
ContentData
|
|
15
14
|
} from "./types";
|
|
16
15
|
import { SearchSyncer } from "docUtils/searchSyncer";
|
|
17
16
|
import { LightningElementWithState } from "dxBaseElements/lightningElementWithState";
|
|
18
|
-
import {
|
|
17
|
+
import { oldVersionDocInfo } from "docUtils/utils";
|
|
19
18
|
import { Breadcrumb, DocPhaseInfo, Language } from "typings/custom";
|
|
20
19
|
import { track as trackGTM } from "dxUtils/analytics";
|
|
20
|
+
import DOMPurify from "dompurify";
|
|
21
21
|
|
|
22
22
|
// TODO: Imitating from actual implementation as doc-content use it like this. We should refactor it later.
|
|
23
23
|
const handleContentError = (error: any): void => console.log(error);
|
|
@@ -40,11 +40,8 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
40
40
|
internalLinkClicked: boolean;
|
|
41
41
|
}> {
|
|
42
42
|
@api apiDomain = "https://developer.salesforce.com";
|
|
43
|
-
@api coveoOrganizationId!: string;
|
|
44
|
-
@api coveoPublicAccessToken!: string;
|
|
45
|
-
@api coveoAnalyticsToken!: string;
|
|
46
|
-
@api coveoSearchHub!: string;
|
|
47
43
|
@api hideFooter = false;
|
|
44
|
+
@api displayReadingTime = false;
|
|
48
45
|
|
|
49
46
|
@api
|
|
50
47
|
get allLanguages(): Array<Language> {
|
|
@@ -57,21 +54,13 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
57
54
|
}
|
|
58
55
|
}
|
|
59
56
|
|
|
60
|
-
@api
|
|
61
|
-
get enableCoveo() {
|
|
62
|
-
return this._enableCoveo;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
set enableCoveo(value) {
|
|
66
|
-
this._enableCoveo = normalizeBoolean(value);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
57
|
private availableLanguages: Array<DocLanguage> = [];
|
|
70
58
|
@track private availableVersions: Array<DocVersion> = [];
|
|
71
59
|
private contentProvider?: FetchContent;
|
|
72
60
|
private docContent = "";
|
|
73
61
|
private language?: DocLanguage | null = null;
|
|
74
|
-
private
|
|
62
|
+
private displayContent = false;
|
|
63
|
+
private display404 = false;
|
|
75
64
|
private _pageHeader?: Header;
|
|
76
65
|
private pdfUrl = "";
|
|
77
66
|
private tocMap: TocMap = {};
|
|
@@ -80,7 +69,6 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
80
69
|
private docTitle = "";
|
|
81
70
|
private _pathName = "";
|
|
82
71
|
private listenerAttached = false;
|
|
83
|
-
private _enableCoveo?: boolean = false;
|
|
84
72
|
private sidebarFooterContent: SiderbarFooter = { ...defaultSidebarFooter };
|
|
85
73
|
private latestVersion = false;
|
|
86
74
|
private previewVersion = false;
|
|
@@ -89,6 +77,10 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
89
77
|
return !this.hideFooter;
|
|
90
78
|
}
|
|
91
79
|
|
|
80
|
+
private get computedReadingTime(): number | undefined {
|
|
81
|
+
return this.displayReadingTime ? this.readingTime : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
92
84
|
private searchSyncer = new SearchSyncer({
|
|
93
85
|
callbacks: {
|
|
94
86
|
onSearchChange: (nextSearchString: string): void => {
|
|
@@ -160,6 +152,7 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
160
152
|
|
|
161
153
|
@track private pageReference: PageReference = {};
|
|
162
154
|
@track breadcrumbs: Array<Breadcrumb> = [];
|
|
155
|
+
@track readingTime?: number;
|
|
163
156
|
|
|
164
157
|
constructor() {
|
|
165
158
|
super();
|
|
@@ -184,7 +177,13 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
184
177
|
this.apiDomain,
|
|
185
178
|
this.allLanguages
|
|
186
179
|
);
|
|
187
|
-
this.fetchDocument().then(() =>
|
|
180
|
+
this.fetchDocument().then((content: any) => {
|
|
181
|
+
if (content) {
|
|
182
|
+
this.displayContent = true;
|
|
183
|
+
} else {
|
|
184
|
+
this.display404 = true;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
188
187
|
window.addEventListener("popstate", this.handlePopState);
|
|
189
188
|
|
|
190
189
|
this.searchSyncer.init();
|
|
@@ -251,35 +250,13 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
251
250
|
// Coveo is enabled and the version is greater than 51 (within the latest 3 versions)
|
|
252
251
|
// TODO: we need a better fix for version number check
|
|
253
252
|
return !(
|
|
254
|
-
this.
|
|
255
|
-
this.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
(this.version?.releaseVersion &&
|
|
259
|
-
parseInt(
|
|
260
|
-
this.version.releaseVersion.replace("v", ""),
|
|
261
|
-
10
|
|
262
|
-
) >= 53))
|
|
253
|
+
!this.version?.releaseVersion ||
|
|
254
|
+
(this.version?.releaseVersion &&
|
|
255
|
+
parseInt(this.version.releaseVersion.replace("v", ""), 10) >=
|
|
256
|
+
53)
|
|
263
257
|
);
|
|
264
258
|
}
|
|
265
259
|
|
|
266
|
-
private get coveoAdvancedQueryConfig(): CoveoAdvancedQueryXMLConfig {
|
|
267
|
-
const config: {
|
|
268
|
-
locale?: string;
|
|
269
|
-
topicid?: string;
|
|
270
|
-
version?: string;
|
|
271
|
-
} = {
|
|
272
|
-
locale: this.languageId,
|
|
273
|
-
topicid: this.deliverable
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
if (this.releaseVersionId && this.releaseVersionId !== "noversion") {
|
|
277
|
-
config.version = this.releaseVersionId;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return config;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
260
|
private get pageHeader(): Header {
|
|
284
261
|
if (!this._pageHeader) {
|
|
285
262
|
this._pageHeader = document.querySelector("doc-header")!;
|
|
@@ -384,7 +361,7 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
384
361
|
);
|
|
385
362
|
this.pageReference.docId = this.language!.url;
|
|
386
363
|
|
|
387
|
-
trackGTM(event.target!, "
|
|
364
|
+
trackGTM(event.target!, "custEv_linkClick", {
|
|
388
365
|
click_text: event.detail,
|
|
389
366
|
element_title: "language selector",
|
|
390
367
|
click_url: `${window.location.origin}${this.pageReferenceToString(
|
|
@@ -425,9 +402,19 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
425
402
|
.catch(handleContentError);
|
|
426
403
|
}
|
|
427
404
|
|
|
405
|
+
private sanitizeUrlPart(part: string | undefined): string | undefined {
|
|
406
|
+
if (!part) {
|
|
407
|
+
return part;
|
|
408
|
+
}
|
|
409
|
+
return DOMPurify.sanitize(part);
|
|
410
|
+
}
|
|
411
|
+
|
|
428
412
|
getReferenceFromUrl(): PageReference {
|
|
429
413
|
const [page, docId, deliverable, contentDocumentId] =
|
|
430
|
-
window.location.pathname
|
|
414
|
+
window.location.pathname
|
|
415
|
+
.substr(1)
|
|
416
|
+
.split("/")
|
|
417
|
+
.map(this.sanitizeUrlPart);
|
|
431
418
|
|
|
432
419
|
const { origin: domain, hash, search } = window.location;
|
|
433
420
|
|
|
@@ -436,9 +423,9 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
436
423
|
deliverable,
|
|
437
424
|
docId,
|
|
438
425
|
domain,
|
|
439
|
-
hash,
|
|
426
|
+
hash: this.sanitizeUrlPart(hash),
|
|
440
427
|
page,
|
|
441
|
-
search
|
|
428
|
+
search: this.sanitizeUrlPart(search)
|
|
442
429
|
};
|
|
443
430
|
}
|
|
444
431
|
|
|
@@ -452,10 +439,11 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
452
439
|
);
|
|
453
440
|
}
|
|
454
441
|
|
|
455
|
-
async fetchDocument(): Promise<
|
|
442
|
+
async fetchDocument(): Promise<string> {
|
|
456
443
|
this.setState({
|
|
457
444
|
isFetchingDocument: true
|
|
458
445
|
});
|
|
446
|
+
|
|
459
447
|
const data = await this.contentProvider!.fetchDocumentData(
|
|
460
448
|
this.pageReference.docId!
|
|
461
449
|
);
|
|
@@ -465,7 +453,7 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
465
453
|
this.setState({
|
|
466
454
|
isFetchingDocument: false
|
|
467
455
|
});
|
|
468
|
-
return;
|
|
456
|
+
return "";
|
|
469
457
|
}
|
|
470
458
|
|
|
471
459
|
this.docTitle = data.docTitle;
|
|
@@ -497,11 +485,11 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
497
485
|
data.id
|
|
498
486
|
) {
|
|
499
487
|
try {
|
|
500
|
-
await this.fetchContent();
|
|
488
|
+
const moreData = await this.fetchContent();
|
|
501
489
|
this.setState({
|
|
502
490
|
isFetchingDocument: false
|
|
503
491
|
});
|
|
504
|
-
return;
|
|
492
|
+
return moreData.content;
|
|
505
493
|
} catch (error) {
|
|
506
494
|
this.pageReference.contentDocumentId = `${data.id}.htm`;
|
|
507
495
|
this.pageReference.hash = "";
|
|
@@ -511,13 +499,15 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
511
499
|
}
|
|
512
500
|
|
|
513
501
|
this.docContent = data.content;
|
|
502
|
+
this.calculateReadingTime(data.content);
|
|
514
503
|
this.addMetatags();
|
|
515
504
|
this.setState({
|
|
516
505
|
isFetchingDocument: false
|
|
517
506
|
});
|
|
507
|
+
return data.content;
|
|
518
508
|
}
|
|
519
509
|
|
|
520
|
-
async fetchContent(): Promise<
|
|
510
|
+
async fetchContent(): Promise<ContentData> {
|
|
521
511
|
this.setState({
|
|
522
512
|
isFetchingContent: true
|
|
523
513
|
});
|
|
@@ -532,6 +522,7 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
532
522
|
|
|
533
523
|
if (data) {
|
|
534
524
|
this.docContent = data.content;
|
|
525
|
+
this.calculateReadingTime(data.content);
|
|
535
526
|
this.addMetatags();
|
|
536
527
|
|
|
537
528
|
if (!this.pageReference.hash) {
|
|
@@ -541,6 +532,7 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
541
532
|
this.setState({
|
|
542
533
|
isFetchingContent: false
|
|
543
534
|
});
|
|
535
|
+
return data;
|
|
544
536
|
}
|
|
545
537
|
|
|
546
538
|
updateHeaderAndSidebarFooter(): void {
|
|
@@ -567,7 +559,6 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
567
559
|
}
|
|
568
560
|
|
|
569
561
|
updateUrl(method = HistoryState.PUSH_STATE): void {
|
|
570
|
-
logCoveoPageView(this.coveoOrganizationId, this.coveoAnalyticsToken);
|
|
571
562
|
window.history[method](
|
|
572
563
|
{},
|
|
573
564
|
"docs",
|
|
@@ -700,7 +691,7 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
700
691
|
|
|
701
692
|
addMetatags(): void {
|
|
702
693
|
const div = document.createElement("div");
|
|
703
|
-
div.innerHTML = this.docContent;
|
|
694
|
+
div.innerHTML = DOMPurify.sanitize(this.docContent);
|
|
704
695
|
const docDescription = div.querySelector(".shortdesc")?.textContent;
|
|
705
696
|
const topicTitle = div.querySelector("h1")?.textContent;
|
|
706
697
|
|
|
@@ -777,4 +768,32 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
777
768
|
private get showVersionPicker(): boolean {
|
|
778
769
|
return !this.disableVersion;
|
|
779
770
|
}
|
|
771
|
+
|
|
772
|
+
private calculateReadingTime(content: string): void {
|
|
773
|
+
if (!this.displayReadingTime) {
|
|
774
|
+
this.readingTime = undefined;
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (!content) {
|
|
779
|
+
this.readingTime = undefined;
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Strip HTML tags and get text content
|
|
784
|
+
const div = document.createElement("div");
|
|
785
|
+
div.innerHTML = DOMPurify.sanitize(content);
|
|
786
|
+
const textContent = div.textContent || div.innerText || "";
|
|
787
|
+
|
|
788
|
+
// Count words (split by whitespace and filter out empty strings)
|
|
789
|
+
const wordCount = textContent
|
|
790
|
+
.trim()
|
|
791
|
+
.split(/\s+/)
|
|
792
|
+
.filter((word) => word.length > 0).length;
|
|
793
|
+
|
|
794
|
+
// Calculate reading time: (wordCount / 240) + 1, rounded
|
|
795
|
+
const readingTimeMinutes = Math.round(wordCount / 240 + 1);
|
|
796
|
+
this.readingTime =
|
|
797
|
+
readingTimeMinutes > 0 ? readingTimeMinutes : undefined;
|
|
798
|
+
}
|
|
780
799
|
}
|