@salesforcedevs/docs-components 1.34.3 → 1.34.4-final-alpha1
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/package.json +28 -28
- package/src/modules/doc/banner/banner.css +41 -2
- package/src/modules/doc/banner/banner.html +6 -5
- package/src/modules/doc/banner/banner.ts +28 -0
- package/src/modules/doc/redocReference/redocReference.css +1 -0
- package/src/modules/doc/redocReference/redocReference.ts +297 -21
- package/src/modules/doc/unifiedContentLayout/unifiedContentLayout.html +8 -0
- package/src/modules/doc/unifiedContentLayout/unifiedContentLayout.ts +65 -46
- package/src/modules/doc/versionPicker/versionPicker.css +204 -17
- package/src/modules/doc/versionPicker/versionPicker.html +54 -21
- package/src/modules/doc/versionPicker/versionPicker.ts +92 -0
- package/src/modules/docUtils/utils/utils.ts +20 -2
- package/LICENSE +0 -12
package/package.json
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
2
|
+
"name": "@salesforcedevs/docs-components",
|
|
3
|
+
"version": "1.34.4-final-alpha1",
|
|
4
|
+
"description": "Docs Lightning web components for DSC",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": "22.x"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@api-components/amf-helper-mixin": "4.5.29",
|
|
15
|
+
"classnames": "2.5.1",
|
|
16
|
+
"dompurify": "3.2.4",
|
|
17
|
+
"kagekiri": "1.4.2",
|
|
18
|
+
"lodash.orderby": "4.6.0",
|
|
19
|
+
"lodash.uniqby": "4.7.0",
|
|
20
|
+
"query-string": "7.1.3",
|
|
21
|
+
"sentence-case": "3.0.4"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/classnames": "2.3.1",
|
|
25
|
+
"@types/lodash.orderby": "4.6.9",
|
|
26
|
+
"@types/lodash.uniqby": "4.7.9"
|
|
27
|
+
},
|
|
28
|
+
"gitHead": "4629fdd9ca18a13480044ad43515b91945d16aad"
|
|
29
|
+
}
|
|
@@ -16,8 +16,6 @@
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
.icon {
|
|
19
|
-
--dx-c-icon-size: var(--dx-g-icon-size-lg);
|
|
20
|
-
|
|
21
19
|
flex-shrink: 0;
|
|
22
20
|
margin-top: var(--dx-g-spacing-smd);
|
|
23
21
|
margin-right: var(--dx-g-spacing-sm);
|
|
@@ -73,6 +71,47 @@
|
|
|
73
71
|
cursor: pointer;
|
|
74
72
|
}
|
|
75
73
|
|
|
74
|
+
/* Centered variant: the message is centered in the banner and the icon hangs
|
|
75
|
+
to its left. The close button is taken out of the flow so it doesn't offset
|
|
76
|
+
the centering. */
|
|
77
|
+
.container_centered {
|
|
78
|
+
position: relative;
|
|
79
|
+
display: grid;
|
|
80
|
+
grid-template-columns: 1fr auto 1fr;
|
|
81
|
+
align-items: center;
|
|
82
|
+
|
|
83
|
+
/* Reserve the close button's width plus its right inset on both sides */
|
|
84
|
+
padding: 0
|
|
85
|
+
calc(
|
|
86
|
+
var(--dx-g-spacing-3xl) + var(--dx-g-spacing-xs) +
|
|
87
|
+
var(--doc-banner-padding-right)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.container_centered .icon {
|
|
92
|
+
grid-column: 1;
|
|
93
|
+
justify-self: end;
|
|
94
|
+
margin-top: 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.container_centered .main {
|
|
98
|
+
grid-column: 2;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.container_centered .message {
|
|
104
|
+
text-align: center;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.container_centered .close {
|
|
108
|
+
position: absolute;
|
|
109
|
+
top: 50%;
|
|
110
|
+
right: var(--doc-banner-padding-right);
|
|
111
|
+
margin-left: 0;
|
|
112
|
+
transform: translateY(-50%);
|
|
113
|
+
}
|
|
114
|
+
|
|
76
115
|
@media (max-width: 1279px) {
|
|
77
116
|
:host {
|
|
78
117
|
--doc-banner-padding-left: var(--dx-g-spacing-xl);
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<template lwc:if={showBanner}>
|
|
3
|
-
<div class=
|
|
3
|
+
<div class={containerClass} part="container">
|
|
4
4
|
<dx-icon
|
|
5
5
|
class="icon"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
sprite={iconSprite}
|
|
7
|
+
symbol={iconSymbol}
|
|
8
|
+
size={iconSize}
|
|
9
|
+
color={iconColor}
|
|
9
10
|
part="icon"
|
|
10
11
|
></dx-icon>
|
|
11
12
|
<div class="main">
|
|
12
13
|
<div class="message" part="message" lwc:dom="manual"></div>
|
|
13
|
-
<div class="actions" part="actions">
|
|
14
|
+
<div lwc:if={hasActions} class="actions" part="actions">
|
|
14
15
|
<template lwc:if={hasPrimaryButton}>
|
|
15
16
|
<dx-button
|
|
16
17
|
href={buttonHref}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { LightningElement, api } from "lwc";
|
|
2
|
+
import { IconSize, IconSprite, IconSymbol } from "typings/custom";
|
|
3
|
+
import { normalizeBoolean } from "dxUtils/normalizers";
|
|
2
4
|
|
|
3
5
|
const BANNER_STORAGE_PREFIX = "doc-banner-";
|
|
4
6
|
|
|
@@ -13,6 +15,24 @@ export default class Banner extends LightningElement {
|
|
|
13
15
|
|
|
14
16
|
@api dismissStorageKey = "";
|
|
15
17
|
|
|
18
|
+
@api
|
|
19
|
+
get centered(): boolean {
|
|
20
|
+
return this._centered;
|
|
21
|
+
}
|
|
22
|
+
set centered(value: boolean | string) {
|
|
23
|
+
this._centered = normalizeBoolean(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@api iconSprite: IconSprite = "utility";
|
|
27
|
+
|
|
28
|
+
@api iconSymbol: IconSymbol = "info";
|
|
29
|
+
|
|
30
|
+
@api iconColor = "gray-50";
|
|
31
|
+
|
|
32
|
+
@api iconSize: IconSize = "large";
|
|
33
|
+
|
|
34
|
+
private _centered = false;
|
|
35
|
+
|
|
16
36
|
private _dismissed = false;
|
|
17
37
|
|
|
18
38
|
connectedCallback() {
|
|
@@ -33,10 +53,18 @@ export default class Banner extends LightningElement {
|
|
|
33
53
|
}
|
|
34
54
|
}
|
|
35
55
|
|
|
56
|
+
get containerClass(): string {
|
|
57
|
+
return this._centered ? "container container_centered" : "container";
|
|
58
|
+
}
|
|
59
|
+
|
|
36
60
|
get showBanner(): boolean {
|
|
37
61
|
return !this._dismissed;
|
|
38
62
|
}
|
|
39
63
|
|
|
64
|
+
get hasActions(): boolean {
|
|
65
|
+
return this.hasPrimaryButton || this.hasSecondaryAction;
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
get hasPrimaryButton(): boolean {
|
|
41
69
|
return !!(this.buttonLabel && this.buttonHref);
|
|
42
70
|
}
|
|
@@ -4,11 +4,22 @@ import DocPhase from "doc/phase";
|
|
|
4
4
|
import DxFooter from "dx/footer";
|
|
5
5
|
import DxIcon from "dx/icon";
|
|
6
6
|
import SidebarFooterNav from "dx/sidebarFooterNav";
|
|
7
|
+
import VersionPicker from "doc/versionPicker";
|
|
7
8
|
import SprigSurvey from "doc/sprigSurvey";
|
|
8
9
|
import { throttle } from "throttle-debounce";
|
|
9
10
|
import { pollUntil } from "dxUtils/async";
|
|
10
11
|
import { toJson } from "dxUtils/normalizers";
|
|
11
|
-
import
|
|
12
|
+
import { oldVersionDocInfo } from "docUtils/utils";
|
|
13
|
+
import {
|
|
14
|
+
applyVersionToHref,
|
|
15
|
+
collectSubtreeCanonicalHrefs,
|
|
16
|
+
normalizePath,
|
|
17
|
+
getTopicVersionKey,
|
|
18
|
+
persistTopicVersion,
|
|
19
|
+
toCanonicalHref
|
|
20
|
+
} from "dxUtils/topicVersion";
|
|
21
|
+
import type { VersionedNode } from "dxUtils/topicVersion";
|
|
22
|
+
import type { DocPhaseInfo, OptionWithLink } from "typings/custom";
|
|
12
23
|
|
|
13
24
|
declare global {
|
|
14
25
|
interface Window {
|
|
@@ -33,8 +44,20 @@ type ReferenceItem = {
|
|
|
33
44
|
topic?: ReferenceTopic;
|
|
34
45
|
};
|
|
35
46
|
|
|
47
|
+
/** A selectable spec version. Mirrors `ReferenceVersion` in `doc/amfReference`. */
|
|
48
|
+
type ReferenceVersion = {
|
|
49
|
+
id: string;
|
|
50
|
+
label: string;
|
|
51
|
+
deprecated?: boolean;
|
|
52
|
+
selected?: boolean;
|
|
53
|
+
link: { href: string };
|
|
54
|
+
};
|
|
55
|
+
|
|
36
56
|
type ReferenceConfig = {
|
|
37
57
|
refList: ReferenceItem[];
|
|
58
|
+
versions?: ReferenceVersion[];
|
|
59
|
+
/** Sidebar topic name. Sidebar stores the selected version under this key. */
|
|
60
|
+
versionTopicKey?: string;
|
|
38
61
|
};
|
|
39
62
|
|
|
40
63
|
const SCROLL_THROTTLE_DELAY = 50;
|
|
@@ -42,6 +65,7 @@ const ELEMENT_TIMEOUT = 10000;
|
|
|
42
65
|
const ELEMENT_CHECK_INTERVAL = 100;
|
|
43
66
|
const DEFAULT_PROJECT_TITLE = "All Reference";
|
|
44
67
|
const BACK_TARGET_STORAGE_KEY = "redoc-back-target";
|
|
68
|
+
const SELECTED_VIEW_STORAGE_KEY = "redoc-selected-view";
|
|
45
69
|
const DOCS_PATH_SEGMENT = "docs";
|
|
46
70
|
const DEFAULT_LOCALE = "en-us";
|
|
47
71
|
|
|
@@ -116,6 +140,29 @@ export default class RedocReference extends LightningElement {
|
|
|
116
140
|
*/
|
|
117
141
|
@api headerHref: string | null = null;
|
|
118
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Versioned topic that owns this spec, including its children.
|
|
145
|
+
* Href matches this page, so an older version still has its version
|
|
146
|
+
* in the path.
|
|
147
|
+
*/
|
|
148
|
+
@api
|
|
149
|
+
set parentTopicFamily(value: string | { topics?: VersionedNode[] } | null) {
|
|
150
|
+
try {
|
|
151
|
+
const parsed = toJson(value) as { topics?: VersionedNode[] } | null;
|
|
152
|
+
this._parentTopicFamilyNode = parsed?.topics?.[0] ?? null;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
this._parentTopicFamilyNode = null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get parentTopicFamily(): { topics?: VersionedNode[] } | null {
|
|
159
|
+
return this._parentTopicFamilyNode
|
|
160
|
+
? { topics: [this._parentTopicFamilyNode] }
|
|
161
|
+
: null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private _parentTopicFamilyNode: VersionedNode | null = null;
|
|
165
|
+
|
|
119
166
|
@api
|
|
120
167
|
get contentType(): string {
|
|
121
168
|
return this._contentType;
|
|
@@ -152,6 +199,40 @@ export default class RedocReference extends LightningElement {
|
|
|
152
199
|
return refCount > 1;
|
|
153
200
|
}
|
|
154
201
|
|
|
202
|
+
get versions(): ReferenceVersion[] {
|
|
203
|
+
return this._referenceConfig?.versions ?? [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The version flagged `selected`, else the first (the latest/GA version). */
|
|
207
|
+
get selectedVersion(): ReferenceVersion | null {
|
|
208
|
+
const versions = this.versions;
|
|
209
|
+
return versions.find((v) => v.selected) ?? versions[0] ?? null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** True when the selected version is the latest/GA one (index 0). */
|
|
213
|
+
get latestVersion(): boolean {
|
|
214
|
+
return this.versions.length
|
|
215
|
+
? this.selectedVersion?.id === this.versions[0].id
|
|
216
|
+
: false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
get hasVersionPicker(): boolean {
|
|
220
|
+
return this.versions.length > 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** "Newer version available" banner info when on a non-latest version, else null. */
|
|
224
|
+
private get oldVersionInfo(): DocPhaseInfo | null {
|
|
225
|
+
if (this.versions.length > 1 && !this.latestVersion) {
|
|
226
|
+
return oldVersionDocInfo(this.versions[0].link.href);
|
|
227
|
+
}
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** A single version has nothing to choose, so it renders read-only. */
|
|
232
|
+
get isVersionReadOnly(): boolean {
|
|
233
|
+
return this.versions.length === 1;
|
|
234
|
+
}
|
|
235
|
+
|
|
155
236
|
// Reads stored back target
|
|
156
237
|
private getBackTargetFromSession(): string | null {
|
|
157
238
|
return sessionStorage.getItem(BACK_TARGET_STORAGE_KEY);
|
|
@@ -186,13 +267,64 @@ export default class RedocReference extends LightningElement {
|
|
|
186
267
|
private onBackClick = (event: Event): void => {
|
|
187
268
|
event.preventDefault();
|
|
188
269
|
|
|
189
|
-
const target = this.
|
|
270
|
+
const target = this.getVersionedBackTarget() ?? this.headerHref;
|
|
190
271
|
|
|
191
272
|
if (target) {
|
|
192
273
|
window.location.assign(target);
|
|
193
274
|
}
|
|
194
275
|
};
|
|
195
276
|
|
|
277
|
+
/**
|
|
278
|
+
* When the back target is in this spec's topic, return it on the
|
|
279
|
+
* selected version. With no back target, do the same for the header
|
|
280
|
+
* link when that link is the versioned parent.
|
|
281
|
+
*/
|
|
282
|
+
private getVersionedBackTarget(): string | null {
|
|
283
|
+
const stored = this.getTranslatedBackTarget();
|
|
284
|
+
const selectedVersion = this.selectedVersion;
|
|
285
|
+
const target = stored ?? this.headerHref;
|
|
286
|
+
if (!target || !selectedVersion) {
|
|
287
|
+
return stored;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let targetUrl: URL;
|
|
291
|
+
try {
|
|
292
|
+
targetUrl = new URL(target, window.location.origin);
|
|
293
|
+
} catch {
|
|
294
|
+
return target;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Family hrefs match this page's version. Compare them without
|
|
298
|
+
// the version segment. Rewrite only the pathname so the target's
|
|
299
|
+
// query and hash survive.
|
|
300
|
+
const familyHrefs = this._parentTopicFamilyNode
|
|
301
|
+
? collectSubtreeCanonicalHrefs(this._parentTopicFamilyNode).map(
|
|
302
|
+
(href) => toCanonicalHref(href, this.versions)
|
|
303
|
+
)
|
|
304
|
+
: [];
|
|
305
|
+
|
|
306
|
+
const canonicalTarget = toCanonicalHref(
|
|
307
|
+
targetUrl.pathname,
|
|
308
|
+
this.versions
|
|
309
|
+
);
|
|
310
|
+
const inFamily = familyHrefs.some(
|
|
311
|
+
(href) => normalizePath(href) === normalizePath(canonicalTarget)
|
|
312
|
+
);
|
|
313
|
+
if (!inFamily) {
|
|
314
|
+
return target;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const versioned = applyVersionToHref(
|
|
318
|
+
canonicalTarget,
|
|
319
|
+
selectedVersion,
|
|
320
|
+
this.versions
|
|
321
|
+
);
|
|
322
|
+
if (!versioned) {
|
|
323
|
+
return target;
|
|
324
|
+
}
|
|
325
|
+
return `${versioned}${targetUrl.search}${targetUrl.hash}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
196
328
|
private getTranslatedBackTarget(): string | null {
|
|
197
329
|
const stored = this.getBackTargetFromSession();
|
|
198
330
|
if (stored) {
|
|
@@ -202,7 +334,9 @@ export default class RedocReference extends LightningElement {
|
|
|
202
334
|
}
|
|
203
335
|
|
|
204
336
|
const referrer = this.getSameOriginReferrerHref();
|
|
205
|
-
return referrer &&
|
|
337
|
+
return referrer &&
|
|
338
|
+
!this.isLocaleHref(new URL(referrer).pathname) &&
|
|
339
|
+
!this.isVersionHref(new URL(referrer).pathname)
|
|
206
340
|
? referrer
|
|
207
341
|
: null;
|
|
208
342
|
}
|
|
@@ -241,7 +375,10 @@ export default class RedocReference extends LightningElement {
|
|
|
241
375
|
}
|
|
242
376
|
|
|
243
377
|
const referrerUrl = new URL(referrerHref);
|
|
244
|
-
if (
|
|
378
|
+
if (
|
|
379
|
+
this.isLocaleHref(referrerUrl.pathname) ||
|
|
380
|
+
this.isVersionHref(referrerUrl.pathname)
|
|
381
|
+
) {
|
|
245
382
|
return;
|
|
246
383
|
}
|
|
247
384
|
|
|
@@ -282,6 +419,19 @@ export default class RedocReference extends LightningElement {
|
|
|
282
419
|
});
|
|
283
420
|
}
|
|
284
421
|
|
|
422
|
+
/**
|
|
423
|
+
* switching version on this same page.
|
|
424
|
+
*/
|
|
425
|
+
private isVersionHref(pathname: string): boolean {
|
|
426
|
+
return this.versions.some((version) => {
|
|
427
|
+
const href = version?.link?.href;
|
|
428
|
+
return (
|
|
429
|
+
!!href &&
|
|
430
|
+
new URL(href, window.location.origin).pathname === pathname
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
285
435
|
/** When origin is provided, pass it to the footer; otherwise use dx-footer's default. */
|
|
286
436
|
get effectiveFooterOrigin(): string {
|
|
287
437
|
return (
|
|
@@ -297,6 +447,13 @@ export default class RedocReference extends LightningElement {
|
|
|
297
447
|
// Track the back target, preserving it across locale switches
|
|
298
448
|
this.trackBackTarget();
|
|
299
449
|
|
|
450
|
+
// Remember the selected version for the sidebar. Changing version
|
|
451
|
+
// reloads this page.
|
|
452
|
+
const topicKey = this.getParentTopicVersionKey();
|
|
453
|
+
if (topicKey && this.selectedVersion) {
|
|
454
|
+
persistTopicVersion(topicKey, this.selectedVersion.id);
|
|
455
|
+
}
|
|
456
|
+
|
|
300
457
|
window.addEventListener("scroll", this.handleScrollAndResize);
|
|
301
458
|
window.addEventListener("resize", this.handleScrollAndResize);
|
|
302
459
|
}
|
|
@@ -354,6 +511,14 @@ export default class RedocReference extends LightningElement {
|
|
|
354
511
|
return parseInt(value, 10) || 0;
|
|
355
512
|
}
|
|
356
513
|
|
|
514
|
+
/** Redoc's LNB background color, themeable via `--doc-c-redoc-sidebar-bg`. */
|
|
515
|
+
private get sidebarBackgroundColor(): string {
|
|
516
|
+
const value = getComputedStyle(this.template.host).getPropertyValue(
|
|
517
|
+
"--doc-c-redoc-sidebar-bg"
|
|
518
|
+
);
|
|
519
|
+
return value.trim() || "white";
|
|
520
|
+
}
|
|
521
|
+
|
|
357
522
|
/*
|
|
358
523
|
** Since we could not use --dx-g-global-header-height as getPropertyValue returns a calc expression,
|
|
359
524
|
** we are using the respective CSS variables to calculate the height.
|
|
@@ -469,7 +634,14 @@ export default class RedocReference extends LightningElement {
|
|
|
469
634
|
specUrl,
|
|
470
635
|
{
|
|
471
636
|
// Dynamic scroll offset to account for headers
|
|
472
|
-
scrollYOffset: this.calculateScrollYOffset
|
|
637
|
+
scrollYOffset: this.calculateScrollYOffset,
|
|
638
|
+
// Redoc's own styled background outranks injected
|
|
639
|
+
// CSS, so the LNB background must be set via theme.
|
|
640
|
+
theme: {
|
|
641
|
+
sidebar: {
|
|
642
|
+
backgroundColor: this.sidebarBackgroundColor
|
|
643
|
+
}
|
|
644
|
+
}
|
|
473
645
|
},
|
|
474
646
|
redocContainer,
|
|
475
647
|
(error: any) => {
|
|
@@ -515,10 +687,7 @@ export default class RedocReference extends LightningElement {
|
|
|
515
687
|
const apiContentDiv = await this.waitForApiContent(redocContainer);
|
|
516
688
|
apiContentDiv.setAttribute("lwc:dom", "manual");
|
|
517
689
|
|
|
518
|
-
|
|
519
|
-
if (docPhaseInfo) {
|
|
520
|
-
this.insertDocPhase(apiContentDiv, docPhaseInfo);
|
|
521
|
-
}
|
|
690
|
+
this.insertStatusItems(apiContentDiv);
|
|
522
691
|
|
|
523
692
|
this.appendFooterItems(apiContentDiv);
|
|
524
693
|
|
|
@@ -527,10 +696,22 @@ export default class RedocReference extends LightningElement {
|
|
|
527
696
|
|
|
528
697
|
// Wait for footer to be rendered before updating styles
|
|
529
698
|
requestAnimationFrame(() => {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
699
|
+
try {
|
|
700
|
+
this.updateRedocThirdColumnStyle(redocContainer);
|
|
701
|
+
|
|
702
|
+
// Restore the view selected before a version switch only
|
|
703
|
+
// after all layout mutations (footer, header) are
|
|
704
|
+
// complete, so the scroll lands on the correct element.
|
|
705
|
+
this.restoreSelectedView();
|
|
706
|
+
|
|
707
|
+
// Fix initial hash scroll after doc phase insertion
|
|
708
|
+
this.handleInitialHashScrollFix();
|
|
709
|
+
} catch (error) {
|
|
710
|
+
this.showErrorUI(
|
|
711
|
+
"Failed to integrate custom components:",
|
|
712
|
+
error
|
|
713
|
+
);
|
|
714
|
+
}
|
|
534
715
|
});
|
|
535
716
|
} catch (error) {
|
|
536
717
|
this.showErrorUI("Failed to integrate custom components:", error);
|
|
@@ -560,6 +741,65 @@ export default class RedocReference extends LightningElement {
|
|
|
560
741
|
}
|
|
561
742
|
}
|
|
562
743
|
|
|
744
|
+
/** Builds the version picker DOM by reusing `doc-version-picker`. */
|
|
745
|
+
private buildVersionPickerDom(): HTMLElement {
|
|
746
|
+
const wrapper = document.createElement("div");
|
|
747
|
+
wrapper.className = "redoc-version-picker";
|
|
748
|
+
|
|
749
|
+
const picker = createElement("doc-version-picker", {
|
|
750
|
+
is: VersionPicker
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
Object.assign(picker, {
|
|
754
|
+
versions: this.versions,
|
|
755
|
+
selectedVersion: this.selectedVersion,
|
|
756
|
+
latestVersion: this.latestVersion,
|
|
757
|
+
readOnly: this.isVersionReadOnly
|
|
758
|
+
});
|
|
759
|
+
picker.addEventListener("change", this.onVersionChange);
|
|
760
|
+
wrapper.appendChild(picker);
|
|
761
|
+
|
|
762
|
+
return wrapper;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Key the sidebar uses for this topic's selected version.
|
|
767
|
+
* Missing `versionTopicKey` returns null so nothing is written.
|
|
768
|
+
*/
|
|
769
|
+
private getParentTopicVersionKey(): string | null {
|
|
770
|
+
const versionTopicKey = this._referenceConfig?.versionTopicKey;
|
|
771
|
+
if (!versionTopicKey) {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
return getTopicVersionKey({ name: versionTopicKey }) || null;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** Stashes the current view (URL hash) before the version link navigates. */
|
|
778
|
+
private onVersionChange = (): void => {
|
|
779
|
+
sessionStorage.setItem(
|
|
780
|
+
SELECTED_VIEW_STORAGE_KEY,
|
|
781
|
+
window.location.hash || ""
|
|
782
|
+
);
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Re-applies the view stashed before a version switch, if its anchor exists
|
|
787
|
+
* in the new spec; otherwise a no-op (lands on the spec root).
|
|
788
|
+
*/
|
|
789
|
+
private restoreSelectedView(): void {
|
|
790
|
+
const storedHash = sessionStorage.getItem(SELECTED_VIEW_STORAGE_KEY);
|
|
791
|
+
sessionStorage.removeItem(SELECTED_VIEW_STORAGE_KEY);
|
|
792
|
+
|
|
793
|
+
if (!storedHash || window.location.hash) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const targetId = storedHash.replace(/^#/, "");
|
|
798
|
+
if (targetId && document.getElementById(targetId)) {
|
|
799
|
+
window.location.hash = storedHash;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
563
803
|
/**
|
|
564
804
|
* Builds the locale picker DOM by reusing `dx-sidebar-footer-nav`
|
|
565
805
|
*/
|
|
@@ -581,12 +821,17 @@ export default class RedocReference extends LightningElement {
|
|
|
581
821
|
}
|
|
582
822
|
|
|
583
823
|
/**
|
|
584
|
-
* Builds a
|
|
824
|
+
* Builds the doc header: a title group (back link + spec title) and, when
|
|
825
|
+
* versions are available, the version picker as a sibling row. Layout/gaps
|
|
826
|
+
* are styled by the developer-website's redoc CSS.
|
|
585
827
|
*/
|
|
586
828
|
private buildProjectHeaderDom(): HTMLElement {
|
|
587
829
|
const wrapper = document.createElement("div");
|
|
588
830
|
wrapper.className = "redoc-project-header";
|
|
589
831
|
|
|
832
|
+
const main = document.createElement("div");
|
|
833
|
+
main.className = "redoc-project-header-main";
|
|
834
|
+
|
|
590
835
|
if (this.projectTitle) {
|
|
591
836
|
const backLink = document.createElement("a");
|
|
592
837
|
backLink.className = "redoc-project-back";
|
|
@@ -607,14 +852,20 @@ export default class RedocReference extends LightningElement {
|
|
|
607
852
|
|
|
608
853
|
backLink.appendChild(icon);
|
|
609
854
|
backLink.appendChild(label);
|
|
610
|
-
|
|
855
|
+
main.appendChild(backLink);
|
|
611
856
|
}
|
|
612
857
|
|
|
613
858
|
if (this.specTitle) {
|
|
614
859
|
const specEl = document.createElement("h2");
|
|
615
860
|
specEl.className = "redoc-spec-title dx-text-display-7";
|
|
616
861
|
specEl.textContent = this.specTitle;
|
|
617
|
-
|
|
862
|
+
main.appendChild(specEl);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
wrapper.appendChild(main);
|
|
866
|
+
|
|
867
|
+
if (this.hasVersionPicker) {
|
|
868
|
+
wrapper.appendChild(this.buildVersionPickerDom());
|
|
618
869
|
}
|
|
619
870
|
|
|
620
871
|
return wrapper;
|
|
@@ -639,15 +890,40 @@ export default class RedocReference extends LightningElement {
|
|
|
639
890
|
return container.querySelector<HTMLElement>(".api-content")!;
|
|
640
891
|
}
|
|
641
892
|
|
|
642
|
-
//
|
|
643
|
-
|
|
893
|
+
// Inserts the doc phase and, on a non-latest version, a dismissible
|
|
894
|
+
// "newer version" banner into a shared doc-phase-wrapper at container start
|
|
895
|
+
private insertStatusItems(container: HTMLElement): void {
|
|
896
|
+
const docPhaseInfo = this.getDocPhaseInfo();
|
|
897
|
+
const oldVersionInfo = this.oldVersionInfo;
|
|
898
|
+
if (!docPhaseInfo && !oldVersionInfo) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
|
|
644
902
|
const wrapper = document.createElement("div");
|
|
645
903
|
wrapper.className = "doc-phase-wrapper";
|
|
646
904
|
container.insertBefore(wrapper, container.firstChild);
|
|
647
905
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
906
|
+
if (docPhaseInfo) {
|
|
907
|
+
const docPhaseElement = createElement("doc-phase", {
|
|
908
|
+
is: DocPhase
|
|
909
|
+
});
|
|
910
|
+
Object.assign(docPhaseElement, { docPhaseInfo });
|
|
911
|
+
wrapper.appendChild(docPhaseElement);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (oldVersionInfo) {
|
|
915
|
+
const versionBanner = createElement("doc-phase", { is: DocPhase });
|
|
916
|
+
Object.assign(versionBanner, {
|
|
917
|
+
docPhaseInfo: oldVersionInfo,
|
|
918
|
+
dismissible: true,
|
|
919
|
+
iconName: "warning"
|
|
920
|
+
});
|
|
921
|
+
versionBanner.addEventListener("dismissphase", () => {
|
|
922
|
+
versionBanner.remove();
|
|
923
|
+
this.updateSidebarPosition();
|
|
924
|
+
});
|
|
925
|
+
wrapper.appendChild(versionBanner);
|
|
926
|
+
}
|
|
651
927
|
}
|
|
652
928
|
|
|
653
929
|
// Appends footer component to container
|
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
lwc:if={docPhaseInfo}
|
|
25
25
|
doc-phase-info={docPhaseInfo}
|
|
26
26
|
></doc-phase>
|
|
27
|
+
<doc-phase
|
|
28
|
+
slot="version-banner"
|
|
29
|
+
lwc:if={showVersionBanner}
|
|
30
|
+
doc-phase-info={oldVersionInfo}
|
|
31
|
+
icon-name="warning"
|
|
32
|
+
dismissible="true"
|
|
33
|
+
ondismissphase={handleDismissVersionBanner}
|
|
34
|
+
></doc-phase>
|
|
27
35
|
<slot></slot>
|
|
28
36
|
</doc-content-layout>
|
|
29
37
|
</template>
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { LightningElement, api } from "lwc";
|
|
1
|
+
import { LightningElement, api, track } from "lwc";
|
|
2
2
|
import { toJson } from "dxUtils/normalizers";
|
|
3
|
-
import type {
|
|
3
|
+
import type { VersionedNode } from "dxUtils/topicVersion";
|
|
4
|
+
import {
|
|
5
|
+
findTopicVersionForCurrentPage,
|
|
6
|
+
latestHrefForCurrentPage
|
|
7
|
+
} from "dxUtils/topicVersion";
|
|
8
|
+
import { oldVersionDocInfo } from "docUtils/utils";
|
|
9
|
+
import type { DocPhaseInfo, OptionWithLink, TreeNode } from "typings/custom";
|
|
4
10
|
|
|
5
11
|
/**
|
|
6
12
|
* Per-topic type emitted by the docs content-type parser
|
|
@@ -10,61 +16,38 @@ import type { OptionWithLink, TreeNode } from "typings/custom";
|
|
|
10
16
|
*/
|
|
11
17
|
const TOPIC_TYPE_SPEC = "spec";
|
|
12
18
|
|
|
13
|
-
/**
|
|
14
|
-
* Walks down the first-child chain and returns the first markdown content
|
|
15
|
-
* page found, descending through nested content-less parents. Returns
|
|
16
|
-
* undefined if the chain breaks on a spec or external link before reaching a
|
|
17
|
-
* content page.
|
|
18
|
-
*/
|
|
19
|
-
function firstChildContentPage(node: TreeNode): TreeNode | undefined {
|
|
20
|
-
const firstChild = node.children?.[0];
|
|
21
|
-
if (!firstChild) {
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const isSpec =
|
|
26
|
-
(firstChild as { topicType?: string }).topicType === TOPIC_TYPE_SPEC;
|
|
27
|
-
const isExternal = firstChild.link?.target === "_blank";
|
|
28
|
-
if (isSpec || isExternal) {
|
|
29
|
-
return undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (firstChild.link?.href) {
|
|
33
|
-
return firstChild;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return firstChildContentPage(firstChild);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
19
|
/**
|
|
40
20
|
* Translates per-topic `topicType` into the generic `showForwardArrow` flag
|
|
41
|
-
* that `dx-tree-tile` reads (spec topics get the forward arrow icon for Redoc)
|
|
42
|
-
*
|
|
21
|
+
* that `dx-tree-tile` reads (spec topics get the forward arrow icon for Redoc).
|
|
22
|
+
*
|
|
23
|
+
* Content-less parents are flagged with redirectToChild and left without a
|
|
24
|
+
* link. The tree selects the first markdown descendant. Copying that child's
|
|
25
|
+
* href onto the parent makes the parent win selection.
|
|
43
26
|
*/
|
|
44
|
-
function
|
|
45
|
-
topics:
|
|
27
|
+
function decorateTopicsWithForwardArrow(
|
|
28
|
+
topics:
|
|
29
|
+
| Array<TreeNode & { topicType?: string; versions?: unknown }>
|
|
30
|
+
| undefined
|
|
46
31
|
): TreeNode[] | undefined {
|
|
47
32
|
return topics?.map((topic) => {
|
|
48
|
-
const { topicType, ...decorated } = topic;
|
|
33
|
+
const { topicType, versions, ...decorated } = topic;
|
|
49
34
|
if (topicType === TOPIC_TYPE_SPEC) {
|
|
50
35
|
decorated.showForwardArrow = true;
|
|
36
|
+
// Nested and standalone specs never get a sidebar picker; only the
|
|
37
|
+
// parent MD topic (when versioned) shows one.
|
|
38
|
+
} else if (versions) {
|
|
39
|
+
decorated.versions = versions;
|
|
51
40
|
}
|
|
52
41
|
if (decorated.children) {
|
|
53
|
-
decorated.children =
|
|
42
|
+
decorated.children = decorateTopicsWithForwardArrow(
|
|
43
|
+
decorated.children
|
|
44
|
+
);
|
|
54
45
|
}
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const isContentlessParent =
|
|
60
|
-
!!decorated.children?.length && !decorated.link?.href;
|
|
61
|
-
|
|
62
|
-
if (isContentlessParent) {
|
|
47
|
+
// Flag a parent with no page of its own. Leave its link empty so the
|
|
48
|
+
// child's URL stays indexed to the child.
|
|
49
|
+
if (!!decorated.children?.length && !decorated.link?.href) {
|
|
63
50
|
decorated.redirectToChild = true;
|
|
64
|
-
const firstChild = firstChildContentPage(topic);
|
|
65
|
-
if (firstChild?.link) {
|
|
66
|
-
decorated.link = { ...firstChild.link };
|
|
67
|
-
}
|
|
68
51
|
}
|
|
69
52
|
|
|
70
53
|
return decorated;
|
|
@@ -101,6 +84,9 @@ export default class UnifiedContentLayout extends LightningElement {
|
|
|
101
84
|
|
|
102
85
|
private _docPhaseInfo: string | null = null;
|
|
103
86
|
private _sidebarContent: unknown = null;
|
|
87
|
+
private _oldVersionInfo: DocPhaseInfo | null = null;
|
|
88
|
+
|
|
89
|
+
@track showVersionBanner = false;
|
|
104
90
|
|
|
105
91
|
@api
|
|
106
92
|
get docPhaseInfo(): string | null {
|
|
@@ -117,7 +103,40 @@ export default class UnifiedContentLayout extends LightningElement {
|
|
|
117
103
|
}
|
|
118
104
|
|
|
119
105
|
set sidebarContent(value: string) {
|
|
120
|
-
|
|
106
|
+
const topics = decorateTopicsWithForwardArrow(toJson(value)?.topics);
|
|
107
|
+
this._sidebarContent = topics;
|
|
108
|
+
this.updateTopicVersionBanner(topics as VersionedNode[] | undefined);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get oldVersionInfo(): DocPhaseInfo | null {
|
|
112
|
+
return this._oldVersionInfo;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
handleDismissVersionBanner(): void {
|
|
116
|
+
this.showVersionBanner = false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Same banner references use when the reader is on an older version.
|
|
121
|
+
* Topic versions are not a separate page property: the sidebar tree
|
|
122
|
+
* carries each version's switching URL, and the current pathname says
|
|
123
|
+
* which one is open.
|
|
124
|
+
*/
|
|
125
|
+
private updateTopicVersionBanner(
|
|
126
|
+
topics: VersionedNode[] | undefined
|
|
127
|
+
): void {
|
|
128
|
+
const match = findTopicVersionForCurrentPage(topics);
|
|
129
|
+
const latest = match?.node.versions?.[0];
|
|
130
|
+
const latestHref = latestHrefForCurrentPage(match?.node.versions);
|
|
131
|
+
|
|
132
|
+
if (match && latest && match.version.id !== latest.id && latestHref) {
|
|
133
|
+
this._oldVersionInfo = oldVersionDocInfo(latestHref);
|
|
134
|
+
this.showVersionBanner = true;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this._oldVersionInfo = null;
|
|
139
|
+
this.showVersionBanner = false;
|
|
121
140
|
}
|
|
122
141
|
|
|
123
142
|
private get enableFooter(): boolean {
|
|
@@ -2,28 +2,69 @@
|
|
|
2
2
|
|
|
3
3
|
/* NOTE: doc-version-picker-width width variable is used by both dx-button and dx-dropdown to maintain a consistent width. */
|
|
4
4
|
:host {
|
|
5
|
+
/* Always positioned so the popover's containing block stays the picker.
|
|
6
|
+
Toggling position with aria-expanded lets the menu jump to the sidebar
|
|
7
|
+
(top of the ToC) for a frame before LWC hides it. */
|
|
8
|
+
position: relative;
|
|
9
|
+
|
|
5
10
|
--dx-c-dropdown-option-font-weight: normal;
|
|
11
|
+
--dx-c-dropdown-option-font-size: var(--dx-g-text-base);
|
|
6
12
|
--dx-c-dropdown-option-label-color: var(--dx-g-gray-10);
|
|
13
|
+
--dx-c-dropdown-option-active-label-color: var(--dx-g-gray-10);
|
|
14
|
+
--dx-c-dropdown-option-padding: var(--dx-g-spacing-sm)
|
|
15
|
+
var(--dx-g-spacing-lg);
|
|
16
|
+
--dx-c-popover-padding: var(--dx-g-spacing-sm) 0;
|
|
7
17
|
--popover-container-open-transform: translateY(4px);
|
|
18
|
+
--dx-c-version-picker-focus-ring: 0 0 0 2px white,
|
|
19
|
+
0 0 0 4px var(--dx-g-blue-vibrant-60);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Open menus must stack above later sibling pickers in the sidebar. */
|
|
23
|
+
:host(:has([aria-expanded="true"])) {
|
|
24
|
+
z-index: 2;
|
|
8
25
|
}
|
|
9
26
|
|
|
10
27
|
.version-picker-container {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
28
|
+
/* Override --doc-version-picker-padding to change the inset (e.g. when the
|
|
29
|
+
picker sits inside an already-padded container). */
|
|
30
|
+
padding: var(
|
|
31
|
+
--doc-version-picker-padding,
|
|
32
|
+
var(--dx-g-spacing-sm) var(--dx-g-spacing-lg) var(--dx-g-spacing-sm)
|
|
33
|
+
var(--dx-g-global-header-padding-horizontal)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
/* Override --doc-version-picker-divider (e.g. `none`) to restyle dividers. */
|
|
37
|
+
border-top: var(
|
|
38
|
+
--doc-version-picker-divider,
|
|
39
|
+
1px solid var(--dx-g-gray-90)
|
|
40
|
+
);
|
|
41
|
+
border-bottom: var(
|
|
42
|
+
--doc-version-picker-divider,
|
|
43
|
+
1px solid var(--dx-g-gray-90)
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Inline sidebar variant: drop the header-slot chrome so the picker sits
|
|
48
|
+
beside a tree node without extra padding or divider lines. */
|
|
49
|
+
.version-picker-container-small {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
padding: 0;
|
|
53
|
+
border: none;
|
|
15
54
|
}
|
|
16
55
|
|
|
17
56
|
.version-picker-button {
|
|
18
57
|
display: flex;
|
|
19
|
-
width:
|
|
58
|
+
width: 100%;
|
|
59
|
+
overflow: visible;
|
|
60
|
+
|
|
61
|
+
--dx-c-button-horizontal-spacing: var(--dx-g-spacing-sm);
|
|
62
|
+
--dx-g-button-icon-color: var(--dx-g-gray-50);
|
|
20
63
|
}
|
|
21
64
|
|
|
22
65
|
.version-picker-button:hover,
|
|
23
|
-
.version-picker-button
|
|
24
|
-
|
|
25
|
-
--dx-c-button-secondary-color-hover: var(--dx-g-cloud-blue-vibrant-95);
|
|
26
|
-
--dx-c-button-primary-color: var(--dx-g-blue-vibrant-40);
|
|
66
|
+
.version-picker-button[aria-expanded="true"] {
|
|
67
|
+
--dx-g-button-icon-color: var(--dx-g-blue-vibrant-20);
|
|
27
68
|
}
|
|
28
69
|
|
|
29
70
|
/**
|
|
@@ -35,6 +76,57 @@ dx-button::part(content) {
|
|
|
35
76
|
overflow: hidden;
|
|
36
77
|
}
|
|
37
78
|
|
|
79
|
+
/* Border width stays 1px in every state. Hover, open, and focus paint the
|
|
80
|
+
2px blue stroke with an inset shadow so the box does not grow.
|
|
81
|
+
Tertiary's hover border is overridden. */
|
|
82
|
+
.version-picker-button::part(container) {
|
|
83
|
+
box-sizing: border-box;
|
|
84
|
+
background: white;
|
|
85
|
+
border: 1px solid var(--dx-g-gray-50);
|
|
86
|
+
border-radius: var(--dx-g-spacing-xs);
|
|
87
|
+
box-shadow: none;
|
|
88
|
+
color: var(--dx-g-gray-10);
|
|
89
|
+
outline: none;
|
|
90
|
+
transition: var(--dx-g-transition-hue-1x), box-shadow 0.15s linear;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Hover only (not open): 2px #032D60 on #EEF4FF, no ring.
|
|
94
|
+
Border color matches the fill so the 1px border does not add a third
|
|
95
|
+
pixel around the inset stroke.
|
|
96
|
+
:focus-visible is included so click-open + Escape (which upgrades the
|
|
97
|
+
trigger to keyboard focus) still looks like hover while the pointer is on
|
|
98
|
+
the picker. */
|
|
99
|
+
.version-picker-button:hover:not([aria-expanded="true"])::part(container),
|
|
100
|
+
.version-picker-button:hover:not([aria-expanded="true"])::part(
|
|
101
|
+
container
|
|
102
|
+
):focus-visible {
|
|
103
|
+
background: var(--dx-g-blue-vibrant-95);
|
|
104
|
+
border: 1px solid var(--dx-g-blue-vibrant-95);
|
|
105
|
+
box-shadow: inset 0 0 0 2px var(--dx-g-blue-vibrant-20);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/* Open keeps the fill and the 2px stroke. The focus ring is keyboard-only,
|
|
109
|
+
on the closed picker below. */
|
|
110
|
+
.version-picker-button[aria-expanded="true"]::part(container) {
|
|
111
|
+
background: var(--dx-g-blue-vibrant-95);
|
|
112
|
+
border: 1px solid var(--dx-g-blue-vibrant-95);
|
|
113
|
+
border-radius: var(--dx-g-spacing-xs);
|
|
114
|
+
box-shadow: inset 0 0 0 2px var(--dx-g-blue-vibrant-20);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Keyboard focus while closed. */
|
|
118
|
+
.version-picker-button:not([aria-expanded="true"])::part(
|
|
119
|
+
container
|
|
120
|
+
):focus-visible {
|
|
121
|
+
--dx-g-button-icon-color: var(--dx-g-blue-vibrant-20);
|
|
122
|
+
|
|
123
|
+
background: var(--dx-g-blue-vibrant-95);
|
|
124
|
+
border: 1px solid var(--dx-g-blue-vibrant-95);
|
|
125
|
+
border-radius: var(--dx-g-spacing-xs);
|
|
126
|
+
box-shadow: inset 0 0 0 2px var(--dx-g-blue-vibrant-20),
|
|
127
|
+
var(--dx-c-version-picker-focus-ring);
|
|
128
|
+
}
|
|
129
|
+
|
|
38
130
|
.selected-version {
|
|
39
131
|
display: flex;
|
|
40
132
|
flex-direction: row;
|
|
@@ -49,16 +141,111 @@ dx-button::part(content) {
|
|
|
49
141
|
white-space: nowrap;
|
|
50
142
|
}
|
|
51
143
|
|
|
52
|
-
dx-type-badge.latest-badge
|
|
53
|
-
|
|
54
|
-
--dx-c-type-badge-background: var(--dx-g-green-vibrant-95);
|
|
55
|
-
|
|
144
|
+
dx-type-badge.latest-badge,
|
|
145
|
+
dx-type-badge.not-latest-badge {
|
|
56
146
|
margin-left: var(--dx-g-spacing-sm);
|
|
57
147
|
}
|
|
58
148
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
149
|
+
/* ------------------------------------------------------------------ *
|
|
150
|
+
* Small "dot" variant (opt-in via `small`). Additive
|
|
151
|
+
* ------------------------------------------------------------------ */
|
|
62
152
|
|
|
63
|
-
|
|
153
|
+
/* Menu is capped at the ToC width (296px), not the 104px trigger, so options
|
|
154
|
+
keep the same 16px / 8px 24px / gray-10 type as the default picker. */
|
|
155
|
+
.version-picker-dropdown-small {
|
|
156
|
+
--dx-c-dropdown-option-font-size: var(--dx-g-text-base);
|
|
157
|
+
--dx-c-dropdown-option-padding: var(--dx-g-spacing-sm)
|
|
158
|
+
var(--dx-g-spacing-lg);
|
|
159
|
+
--dx-c-dropdown-option-label-color: var(--dx-g-gray-10);
|
|
160
|
+
--dx-c-dropdown-option-border-radius: 0;
|
|
161
|
+
--dx-c-popover-border: none;
|
|
162
|
+
|
|
163
|
+
/* Hug the labels (down to the shortest) but never wider than the ToC
|
|
164
|
+
content: 296px on desktop, and on the smallest breakpoint the sidebar
|
|
165
|
+
variable grows with the viewport up to 720px. Long labels wrap inside
|
|
166
|
+
that cap. */
|
|
167
|
+
--dx-c-dropdown-menu-max-width: var(--doc-version-picker-width, 296px);
|
|
168
|
+
--dx-c-dropdown-fit-white-space: normal;
|
|
169
|
+
--dx-c-dropdown-fit-overflow: hidden;
|
|
170
|
+
--dx-c-dropdown-fit-text-overflow: clip;
|
|
171
|
+
--dx-c-dropdown-option-overflow-wrap: anywhere;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.version-picker-button-small {
|
|
175
|
+
width: fit-content;
|
|
176
|
+
max-width: 104px;
|
|
177
|
+
|
|
178
|
+
--dx-c-button-font-size: var(--dx-g-text-xs);
|
|
179
|
+
--dx-c-button-font-weight: var(--dx-g-font-normal);
|
|
180
|
+
--dx-c-button-line-height: var(--dx-g-spacing-lg);
|
|
181
|
+
--dx-c-button-icon-gap: var(--dx-g-spacing-2xs);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* min-width: 0 on each flex ancestor lets the label truncate: a flex item's
|
|
185
|
+
default min-width: auto refuses to shrink below its content and overrides the
|
|
186
|
+
host's max-width, so without this the trigger overflows instead. */
|
|
187
|
+
.version-picker-button-small::part(content) {
|
|
188
|
+
display: flex;
|
|
189
|
+
flex: 1;
|
|
190
|
+
min-width: 0;
|
|
191
|
+
width: auto;
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* width:100% re-ties to the 104px-capped host (container is width:inherit,
|
|
196
|
+
which copies width but not max-width). Border/bg/states are shared above. */
|
|
197
|
+
.version-picker-button-small::part(container) {
|
|
198
|
+
width: 100%;
|
|
199
|
+
height: var(--dx-g-spacing-lg);
|
|
200
|
+
padding: 0 var(--dx-g-spacing-xs);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.selected-version-small {
|
|
204
|
+
flex: 1;
|
|
205
|
+
min-width: 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.version-picker-dot {
|
|
209
|
+
flex: 0 0 auto;
|
|
210
|
+
width: var(--dx-g-spacing-sm);
|
|
211
|
+
height: var(--dx-g-spacing-sm);
|
|
212
|
+
margin-right: var(--dx-g-spacing-xs);
|
|
213
|
+
border-radius: 50%;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.version-picker-dot-latest {
|
|
217
|
+
background: var(--dx-g-green-vibrant-60);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.version-picker-dot-not-latest {
|
|
221
|
+
background: var(--dx-g-yellow-vibrant-80);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* inline-block (not flex) so the label can truncate; line-height centers it. */
|
|
225
|
+
.version-picker-readonly {
|
|
226
|
+
box-sizing: border-box;
|
|
227
|
+
display: inline-block;
|
|
228
|
+
width: 100%;
|
|
229
|
+
height: var(--dx-g-spacing-xl);
|
|
230
|
+
margin: 0;
|
|
231
|
+
padding: 0 var(--dx-g-spacing-sm);
|
|
232
|
+
border: 1px solid var(--dx-g-gray-80);
|
|
233
|
+
border-radius: var(--dx-g-spacing-xs);
|
|
234
|
+
overflow: hidden;
|
|
235
|
+
color: var(--dx-g-gray-10);
|
|
236
|
+
text-overflow: ellipsis;
|
|
237
|
+
white-space: nowrap;
|
|
238
|
+
font: var(--dx-g-font-normal) var(--dx-g-text-sm) / var(--dx-g-spacing-xl)
|
|
239
|
+
var(--dx-g-font-sans),
|
|
240
|
+
sans-serif;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.version-picker-readonly-small {
|
|
244
|
+
width: fit-content;
|
|
245
|
+
max-width: 104px;
|
|
246
|
+
height: var(--dx-g-spacing-lg);
|
|
247
|
+
padding: 0 var(--dx-g-spacing-xs);
|
|
248
|
+
font: var(--dx-g-font-normal) var(--dx-g-text-xs) / var(--dx-g-spacing-lg)
|
|
249
|
+
var(--dx-g-font-sans),
|
|
250
|
+
sans-serif;
|
|
64
251
|
}
|
|
@@ -1,37 +1,70 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div lwc:if={showVersionPicker} class=
|
|
2
|
+
<div lwc:if={showVersionPicker} class={containerClass}>
|
|
3
|
+
<!-- Small read-only: plain value, no dropdown -->
|
|
4
|
+
<p
|
|
5
|
+
lwc:if={readOnly}
|
|
6
|
+
class={readOnlyClass}
|
|
7
|
+
title={selectedVersion.label}
|
|
8
|
+
>
|
|
9
|
+
{selectedVersion.label}
|
|
10
|
+
</p>
|
|
11
|
+
|
|
3
12
|
<dx-dropdown
|
|
13
|
+
lwc:else
|
|
14
|
+
class={dropdownClass}
|
|
4
15
|
options={versions}
|
|
5
16
|
analytics-event="custEv_docVersionSelect"
|
|
6
17
|
analytics-payload={analyticsPayload}
|
|
7
18
|
value={selectedVersion.id}
|
|
8
|
-
width=
|
|
19
|
+
width-mode={dropdownWidthMode}
|
|
9
20
|
onchange={onVersionChange}
|
|
10
21
|
>
|
|
11
22
|
<dx-button
|
|
12
|
-
class=
|
|
23
|
+
class={triggerClass}
|
|
13
24
|
variant="tertiary"
|
|
14
25
|
size="small"
|
|
26
|
+
font="sans"
|
|
15
27
|
icon-symbol="chevrondown"
|
|
16
|
-
icon-size=
|
|
28
|
+
icon-size={triggerIconSize}
|
|
29
|
+
aria-label={triggerAriaLabel}
|
|
17
30
|
>
|
|
18
|
-
<div class=
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
<div class={selectedVersionClass}>
|
|
32
|
+
<!-- Small: leading colored dot -->
|
|
33
|
+
<template lwc:if={small}>
|
|
34
|
+
<template lwc:if={showLatestTag}>
|
|
35
|
+
<span class={dotClass} aria-hidden="true"></span>
|
|
36
|
+
</template>
|
|
37
|
+
<p
|
|
38
|
+
class="selected-version-label"
|
|
39
|
+
title={selectedVersion.label}
|
|
40
|
+
>
|
|
41
|
+
{selectedVersion.label}
|
|
42
|
+
</p>
|
|
43
|
+
</template>
|
|
44
|
+
<!-- Default: trailing Latest/Not-Latest badge -->
|
|
45
|
+
<template lwc:else>
|
|
46
|
+
<p
|
|
47
|
+
class="selected-version-label"
|
|
48
|
+
title={selectedVersion.label}
|
|
49
|
+
>
|
|
50
|
+
{selectedVersion.label}
|
|
51
|
+
</p>
|
|
52
|
+
<template lwc:if={showLatestTag}>
|
|
53
|
+
<dx-type-badge
|
|
54
|
+
class="latest-badge"
|
|
55
|
+
lwc:if={latestVersion}
|
|
56
|
+
variant="status-success"
|
|
57
|
+
value="Latest"
|
|
58
|
+
size="small"
|
|
59
|
+
></dx-type-badge>
|
|
60
|
+
<dx-type-badge
|
|
61
|
+
class="not-latest-badge"
|
|
62
|
+
lwc:else
|
|
63
|
+
variant="status-warning"
|
|
64
|
+
value="Not Latest"
|
|
65
|
+
size="small"
|
|
66
|
+
></dx-type-badge>
|
|
67
|
+
</template>
|
|
35
68
|
</template>
|
|
36
69
|
</div>
|
|
37
70
|
</dx-button>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LightningElement, api, track } from "lwc";
|
|
2
|
+
import cx from "classnames";
|
|
2
3
|
|
|
3
4
|
import { AnalyticsPayload, OptionWithNested } from "typings/custom";
|
|
4
5
|
|
|
@@ -12,6 +13,8 @@ export default class VersionPicker extends LightningElement {
|
|
|
12
13
|
private _selectedVersion?: OptionWithNested;
|
|
13
14
|
private _latestVersion: boolean = false;
|
|
14
15
|
private _hideBadge: boolean = false;
|
|
16
|
+
private _small: boolean = false;
|
|
17
|
+
private _readOnly: boolean = false;
|
|
15
18
|
|
|
16
19
|
@api
|
|
17
20
|
get versions() {
|
|
@@ -51,14 +54,103 @@ export default class VersionPicker extends LightningElement {
|
|
|
51
54
|
this._hideBadge = normalizeBoolean(value);
|
|
52
55
|
}
|
|
53
56
|
|
|
57
|
+
// Opt-in compact "dot" variant (24px, abbreviated label). Off by default so
|
|
58
|
+
// existing consumers render the default trigger unchanged.
|
|
59
|
+
@api
|
|
60
|
+
get small() {
|
|
61
|
+
return this._small;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
set small(value) {
|
|
65
|
+
this._small = normalizeBoolean(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Opt-in read-only state: plain value, no dropdown menu.
|
|
69
|
+
@api
|
|
70
|
+
get readOnly() {
|
|
71
|
+
return this._readOnly;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
set readOnly(value) {
|
|
75
|
+
this._readOnly = normalizeBoolean(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
private get showVersionPicker() {
|
|
55
79
|
return this._versions && this._versions.length !== 0;
|
|
56
80
|
}
|
|
57
81
|
|
|
82
|
+
private get containerClass(): string {
|
|
83
|
+
return this.small
|
|
84
|
+
? "version-picker-container-small"
|
|
85
|
+
: "version-picker-container";
|
|
86
|
+
}
|
|
87
|
+
|
|
58
88
|
private get showLatestTag(): boolean {
|
|
59
89
|
return !this.hideBadge;
|
|
60
90
|
}
|
|
61
91
|
|
|
92
|
+
private get readOnlyClass(): string {
|
|
93
|
+
return cx(
|
|
94
|
+
"version-picker-readonly",
|
|
95
|
+
this.small && "version-picker-readonly-small"
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private get dotClass(): string {
|
|
100
|
+
return `version-picker-dot ${
|
|
101
|
+
this.latestVersion
|
|
102
|
+
? "version-picker-dot-latest"
|
|
103
|
+
: "version-picker-dot-not-latest"
|
|
104
|
+
}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Accessible name for the small trigger: includes the latest state so it
|
|
108
|
+
// isn't conveyed by the dot's color alone (WCAG 1.4.1). Omits the state
|
|
109
|
+
// when the indicator is hidden.
|
|
110
|
+
private get smallTriggerAriaLabel(): string {
|
|
111
|
+
const label = this.selectedVersion?.label ?? "";
|
|
112
|
+
if (!this.showLatestTag) {
|
|
113
|
+
return label;
|
|
114
|
+
}
|
|
115
|
+
return `${label}, ${this.latestVersion ? "Latest" : "Not Latest"}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- Per-variant values for the shared dropdown/button markup below. The
|
|
119
|
+
// small variant opts into extra classes and a compact chevron.
|
|
120
|
+
|
|
121
|
+
// Not cx(): the default must have NO class attribute (cx returns "", which
|
|
122
|
+
// would render class="" and break the frozen consumer snapshots).
|
|
123
|
+
private get dropdownClass(): string | undefined {
|
|
124
|
+
return this.small ? "version-picker-dropdown-small" : undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// "full" matches the trigger (ToC width). "fit-end" sizes to the labels,
|
|
128
|
+
// caps at the ToC width, and opens inward.
|
|
129
|
+
private get dropdownWidthMode(): "full" | "fit-end" {
|
|
130
|
+
return this.small ? "fit-end" : "full";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private get triggerClass(): string {
|
|
134
|
+
return cx(
|
|
135
|
+
"version-picker-button",
|
|
136
|
+
this.small && "version-picker-button-small"
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private get triggerIconSize(): string {
|
|
141
|
+
return this.small ? "xsmall" : "medium";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Default returns "" (dx-button's own default) so the trigger's rendered
|
|
145
|
+
// aria-label is unchanged for the default variant.
|
|
146
|
+
private get triggerAriaLabel(): string {
|
|
147
|
+
return this.small ? this.smallTriggerAriaLabel : "";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private get selectedVersionClass(): string {
|
|
151
|
+
return cx("selected-version", this.small && "selected-version-small");
|
|
152
|
+
}
|
|
153
|
+
|
|
62
154
|
private onVersionChange(e: CustomEvent) {
|
|
63
155
|
this.dispatchEvent(new CustomEvent("change", { detail: e.detail }));
|
|
64
156
|
}
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import { CoveoAnalyticsClient } from "coveo.analytics/dist/browser.mjs"; // This fix is required for Node 20 upgrade, so that Coveo analytics is loaded properly by LWR.
|
|
2
2
|
|
|
3
|
+
// Resolve against the current origin (version links are usually relative) and
|
|
4
|
+
// keep only http/https. Blocks executable schemes like javascript:/data: that
|
|
5
|
+
// would run when the innerHTML-rendered link is clicked
|
|
6
|
+
// encodeURI then keeps the value from breaking out of the href attribute.
|
|
7
|
+
const toSafeHref = (link: string): string => {
|
|
8
|
+
try {
|
|
9
|
+
const { protocol } = new URL(link, window.location.origin);
|
|
10
|
+
if (protocol === "http:" || protocol === "https:") {
|
|
11
|
+
return encodeURI(link);
|
|
12
|
+
}
|
|
13
|
+
} catch {
|
|
14
|
+
// Fall through to the rejection below.
|
|
15
|
+
}
|
|
16
|
+
console.warn(`Blocked unsafe version link: ${link}`);
|
|
17
|
+
return "#";
|
|
18
|
+
};
|
|
19
|
+
|
|
3
20
|
export const oldVersionDocInfo = (latestVersionLink: string) => {
|
|
21
|
+
const safeLink = toSafeHref(latestVersionLink);
|
|
4
22
|
return {
|
|
5
23
|
title: "Newer Version Available",
|
|
6
|
-
body: `This content describes an older version of this product.
|
|
7
|
-
<a style="font-weight: bold;" href="${
|
|
24
|
+
body: `This content describes an older version of this product.
|
|
25
|
+
<a style="font-weight: bold;" href="${safeLink}">View Latest</a>`
|
|
8
26
|
};
|
|
9
27
|
};
|
|
10
28
|
|
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.
|