@salesforcedevs/docs-components 1.33.2 → 1.34.0-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 +1 -0
- package/package.json +28 -28
- package/src/modules/doc/contentActionToolbar/contentActionToolbar.css +1 -0
- package/src/modules/doc/redocReference/redocReference.css +2 -2
- package/src/modules/doc/redocReference/redocReference.ts +336 -42
- package/src/modules/doc/unifiedContentLayout/unifiedContentLayout.css +19 -0
- package/src/modules/doc/unifiedContentLayout/unifiedContentLayout.html +29 -0
- package/src/modules/doc/unifiedContentLayout/unifiedContentLayout.ts +94 -0
- package/src/modules/doc/versionPicker/versionPicker.css +154 -16
- package/src/modules/doc/versionPicker/versionPicker.html +54 -21
- package/src/modules/doc/versionPicker/versionPicker.ts +86 -0
- package/LICENSE +0 -12
package/lwc.config.json
CHANGED
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.0-alpha",
|
|
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
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
:host {
|
|
2
2
|
--dx-footer-margin-top: 142px;
|
|
3
3
|
--doc-c-redoc-sidebar-top: calc(
|
|
4
|
-
var(--dx-g-global-header-height) + var(--dx-g-doc-header-height)
|
|
5
|
-
var(--dx-g-spacing-xl)
|
|
4
|
+
var(--dx-g-global-header-height) + var(--dx-g-doc-header-height)
|
|
6
5
|
);
|
|
6
|
+
--doc-c-redoc-sidebar-bg: white;
|
|
7
7
|
}
|
|
@@ -3,9 +3,13 @@ import { createElement, LightningElement, api } from "lwc";
|
|
|
3
3
|
import DocPhase from "doc/phase";
|
|
4
4
|
import DxFooter from "dx/footer";
|
|
5
5
|
import DxIcon from "dx/icon";
|
|
6
|
+
import SidebarFooterNav from "dx/sidebarFooterNav";
|
|
7
|
+
import VersionPicker from "doc/versionPicker";
|
|
6
8
|
import SprigSurvey from "doc/sprigSurvey";
|
|
7
9
|
import { throttle } from "throttle-debounce";
|
|
8
10
|
import { pollUntil } from "dxUtils/async";
|
|
11
|
+
import { toJson } from "dxUtils/normalizers";
|
|
12
|
+
import type { OptionWithLink } from "typings/custom";
|
|
9
13
|
|
|
10
14
|
declare global {
|
|
11
15
|
interface Window {
|
|
@@ -30,14 +34,28 @@ type ReferenceItem = {
|
|
|
30
34
|
topic?: ReferenceTopic;
|
|
31
35
|
};
|
|
32
36
|
|
|
37
|
+
/** A selectable spec version. Mirrors `ReferenceVersion` in `doc/amfReference`. */
|
|
38
|
+
type ReferenceVersion = {
|
|
39
|
+
id: string;
|
|
40
|
+
label: string;
|
|
41
|
+
deprecated?: boolean;
|
|
42
|
+
selected?: boolean;
|
|
43
|
+
link: { href: string };
|
|
44
|
+
};
|
|
45
|
+
|
|
33
46
|
type ReferenceConfig = {
|
|
34
47
|
refList: ReferenceItem[];
|
|
48
|
+
versions?: ReferenceVersion[];
|
|
35
49
|
};
|
|
36
50
|
|
|
37
51
|
const SCROLL_THROTTLE_DELAY = 50;
|
|
38
52
|
const ELEMENT_TIMEOUT = 10000;
|
|
39
53
|
const ELEMENT_CHECK_INTERVAL = 100;
|
|
40
|
-
const
|
|
54
|
+
const DEFAULT_PROJECT_TITLE = "All Reference";
|
|
55
|
+
const BACK_TARGET_STORAGE_KEY = "redoc-back-target";
|
|
56
|
+
const SELECTED_VIEW_STORAGE_KEY = "redoc-selected-view";
|
|
57
|
+
const DOCS_PATH_SEGMENT = "docs";
|
|
58
|
+
const DEFAULT_LOCALE = "en-us";
|
|
41
59
|
|
|
42
60
|
export default class RedocReference extends LightningElement {
|
|
43
61
|
private _referenceConfig: ReferenceConfig = { refList: [] };
|
|
@@ -47,6 +65,7 @@ export default class RedocReference extends LightningElement {
|
|
|
47
65
|
private docHeaderElement: Element | null = null;
|
|
48
66
|
private docPhaseWrapperElement: Element | null = null;
|
|
49
67
|
private lastSidebarTop = 0;
|
|
68
|
+
private _languages: OptionWithLink[] = [];
|
|
50
69
|
|
|
51
70
|
/**
|
|
52
71
|
* History length captured at mount (pre-Redoc), used by `onBackClick` to
|
|
@@ -91,19 +110,112 @@ export default class RedocReference extends LightningElement {
|
|
|
91
110
|
* Project title (same value passed to `<doc-header>` as `subtitle`). Used
|
|
92
111
|
* inside the Redoc-rendered UI to label the parent project.
|
|
93
112
|
*/
|
|
94
|
-
@api
|
|
113
|
+
@api
|
|
114
|
+
get projectTitle(): string | null {
|
|
115
|
+
return this.isDocContentType
|
|
116
|
+
? this._projectTitle
|
|
117
|
+
: DEFAULT_PROJECT_TITLE;
|
|
118
|
+
}
|
|
119
|
+
set projectTitle(value: string | null) {
|
|
120
|
+
this._projectTitle = value;
|
|
121
|
+
}
|
|
122
|
+
private _projectTitle: string | null = null;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Href to navigate to when the back link is clicked AND there is no
|
|
126
|
+
* usable referrer (e.g. the user opened the page directly in a fresh
|
|
127
|
+
* tab).
|
|
128
|
+
*/
|
|
129
|
+
@api headerHref: string | null = null;
|
|
130
|
+
|
|
131
|
+
@api
|
|
132
|
+
get contentType(): string {
|
|
133
|
+
return this._contentType;
|
|
134
|
+
}
|
|
135
|
+
set contentType(value: string) {
|
|
136
|
+
this._contentType = value;
|
|
137
|
+
}
|
|
138
|
+
private _contentType: string = "";
|
|
139
|
+
|
|
140
|
+
@api
|
|
141
|
+
get languages(): OptionWithLink[] {
|
|
142
|
+
return this._languages;
|
|
143
|
+
}
|
|
144
|
+
set languages(value: string | OptionWithLink[]) {
|
|
145
|
+
this._languages = toJson(value) || [];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@api language: string | null = null;
|
|
95
149
|
|
|
96
150
|
get specTitle(): string | null {
|
|
97
151
|
return this.getSelectedReference()?.title ?? null;
|
|
98
152
|
}
|
|
99
153
|
|
|
154
|
+
get isDocContentType(): boolean {
|
|
155
|
+
return this.contentType === "docs";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get isReferenceContentType(): boolean {
|
|
159
|
+
return this.contentType === "references";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get isMultiSpecSet(): boolean {
|
|
163
|
+
const refCount = this.referenceConfig?.refList?.length ?? 0;
|
|
164
|
+
return refCount > 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get versions(): ReferenceVersion[] {
|
|
168
|
+
return this._referenceConfig?.versions ?? [];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The version flagged `selected`, else the first (the latest/GA version). */
|
|
172
|
+
get selectedVersion(): ReferenceVersion | null {
|
|
173
|
+
const versions = this.versions;
|
|
174
|
+
return versions.find((v) => v.selected) ?? versions[0] ?? null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** True when the selected version is the latest/GA one (index 0). */
|
|
178
|
+
get latestVersion(): boolean {
|
|
179
|
+
return this.versions.length
|
|
180
|
+
? this.selectedVersion?.id === this.versions[0].id
|
|
181
|
+
: false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
get hasVersionPicker(): boolean {
|
|
185
|
+
return this.versions.length > 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** A single version has nothing to choose, so it renders read-only. */
|
|
189
|
+
get isVersionReadOnly(): boolean {
|
|
190
|
+
return this.versions.length === 1;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Reads stored back target
|
|
194
|
+
private getBackTargetFromSession(): string | null {
|
|
195
|
+
return sessionStorage.getItem(BACK_TARGET_STORAGE_KEY);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private setBackTargetInSession(href: string): void {
|
|
199
|
+
sessionStorage.setItem(BACK_TARGET_STORAGE_KEY, href);
|
|
200
|
+
}
|
|
201
|
+
|
|
100
202
|
/**
|
|
101
|
-
*
|
|
203
|
+
* when more than one localized spec is available
|
|
204
|
+
*/
|
|
205
|
+
get hasLocalePicker(): boolean {
|
|
206
|
+
return this.languages.length > 1;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Whether to show the project header. Shown for multi-spec reference
|
|
211
|
+
* sets, and for any docs-content spec topic so it always has a back
|
|
212
|
+
* link.
|
|
102
213
|
*/
|
|
103
214
|
get showRedocHeader(): boolean {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
215
|
+
return (
|
|
216
|
+
(this.isReferenceContentType && this.isMultiSpecSet) ||
|
|
217
|
+
this.isDocContentType
|
|
218
|
+
);
|
|
107
219
|
}
|
|
108
220
|
|
|
109
221
|
/**
|
|
@@ -111,16 +223,73 @@ export default class RedocReference extends LightningElement {
|
|
|
111
223
|
*/
|
|
112
224
|
private onBackClick = (event: Event): void => {
|
|
113
225
|
event.preventDefault();
|
|
226
|
+
|
|
227
|
+
const target = this.getTranslatedBackTarget() ?? this.headerHref;
|
|
228
|
+
|
|
229
|
+
if (target) {
|
|
230
|
+
window.location.assign(target);
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
private getTranslatedBackTarget(): string | null {
|
|
235
|
+
const stored = this.getBackTargetFromSession();
|
|
236
|
+
if (stored) {
|
|
237
|
+
return this.language
|
|
238
|
+
? this.translateToCurrentLocale(stored)
|
|
239
|
+
: stored;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const referrer = this.getSameOriginReferrerHref();
|
|
243
|
+
return referrer &&
|
|
244
|
+
!this.isLocaleHref(new URL(referrer).pathname) &&
|
|
245
|
+
!this.isVersionHref(new URL(referrer).pathname)
|
|
246
|
+
? referrer
|
|
247
|
+
: null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* The locale segment sits after "docs" for docs pages,
|
|
251
|
+
* and after the product segment for references.
|
|
252
|
+
*/
|
|
253
|
+
private translateToCurrentLocale(storedHref: string): string {
|
|
254
|
+
const url = new URL(storedHref, window.location.origin);
|
|
255
|
+
const localeIds = this.languages.map((loc) => loc.id);
|
|
256
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
257
|
+
|
|
258
|
+
// docs -> right after "docs"; references -> after the product segment.
|
|
259
|
+
const localeIndex = this.isDocContentType ? 1 : 2;
|
|
260
|
+
|
|
261
|
+
if (segments[0] === DOCS_PATH_SEGMENT) {
|
|
262
|
+
// Drop the existing locale segment, if any
|
|
263
|
+
if (localeIds.includes(segments[localeIndex])) {
|
|
264
|
+
segments.splice(localeIndex, 1);
|
|
265
|
+
}
|
|
266
|
+
// Add the current locale
|
|
267
|
+
if (this.language && this.language !== DEFAULT_LOCALE) {
|
|
268
|
+
segments.splice(localeIndex, 0, this.language);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
url.pathname = "/" + segments.join("/");
|
|
273
|
+
return url.href;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Preserves target across locale switches, updates on normal navigation
|
|
277
|
+
private trackBackTarget(): void {
|
|
114
278
|
const referrerHref = this.getSameOriginReferrerHref();
|
|
115
|
-
if (referrerHref) {
|
|
116
|
-
window.location.href = referrerHref;
|
|
279
|
+
if (!referrerHref) {
|
|
117
280
|
return;
|
|
118
281
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
282
|
+
|
|
283
|
+
const referrerUrl = new URL(referrerHref);
|
|
284
|
+
if (
|
|
285
|
+
this.isLocaleHref(referrerUrl.pathname) ||
|
|
286
|
+
this.isVersionHref(referrerUrl.pathname)
|
|
287
|
+
) {
|
|
288
|
+
return;
|
|
122
289
|
}
|
|
123
|
-
|
|
290
|
+
|
|
291
|
+
this.setBackTargetInSession(referrerHref);
|
|
292
|
+
}
|
|
124
293
|
|
|
125
294
|
/**
|
|
126
295
|
* Returns the referrer URL when the page was reached via in-tab navigation
|
|
@@ -144,16 +313,29 @@ export default class RedocReference extends LightningElement {
|
|
|
144
313
|
}
|
|
145
314
|
|
|
146
315
|
/**
|
|
147
|
-
*
|
|
148
|
-
* trimming any trailing reference id (and deeper segments). Returns null
|
|
149
|
-
* when the URL doesn't contain a `/references` segment.
|
|
316
|
+
* switching locale on this same page.
|
|
150
317
|
*/
|
|
151
|
-
private
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
318
|
+
private isLocaleHref(pathname: string): boolean {
|
|
319
|
+
return this.languages.some((locale) => {
|
|
320
|
+
const href = locale?.link?.href;
|
|
321
|
+
return (
|
|
322
|
+
!!href &&
|
|
323
|
+
new URL(href, window.location.origin).pathname === pathname
|
|
324
|
+
);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* switching version on this same page.
|
|
330
|
+
*/
|
|
331
|
+
private isVersionHref(pathname: string): boolean {
|
|
332
|
+
return this.versions.some((version) => {
|
|
333
|
+
const href = version?.link?.href;
|
|
334
|
+
return (
|
|
335
|
+
!!href &&
|
|
336
|
+
new URL(href, window.location.origin).pathname === pathname
|
|
337
|
+
);
|
|
338
|
+
});
|
|
157
339
|
}
|
|
158
340
|
|
|
159
341
|
/** When origin is provided, pass it to the footer; otherwise use dx-footer's default. */
|
|
@@ -168,6 +350,9 @@ export default class RedocReference extends LightningElement {
|
|
|
168
350
|
// so it reflects real in-tab navigation rather than Redoc's churn.
|
|
169
351
|
this.initialHistoryLength = window.history.length;
|
|
170
352
|
|
|
353
|
+
// Track the back target, preserving it across locale switches
|
|
354
|
+
this.trackBackTarget();
|
|
355
|
+
|
|
171
356
|
window.addEventListener("scroll", this.handleScrollAndResize);
|
|
172
357
|
window.addEventListener("resize", this.handleScrollAndResize);
|
|
173
358
|
}
|
|
@@ -225,6 +410,14 @@ export default class RedocReference extends LightningElement {
|
|
|
225
410
|
return parseInt(value, 10) || 0;
|
|
226
411
|
}
|
|
227
412
|
|
|
413
|
+
/** Redoc's LNB background color, themeable via `--doc-c-redoc-sidebar-bg`. */
|
|
414
|
+
private get sidebarBackgroundColor(): string {
|
|
415
|
+
const value = getComputedStyle(this.template.host).getPropertyValue(
|
|
416
|
+
"--doc-c-redoc-sidebar-bg"
|
|
417
|
+
);
|
|
418
|
+
return value.trim() || "white";
|
|
419
|
+
}
|
|
420
|
+
|
|
228
421
|
/*
|
|
229
422
|
** Since we could not use --dx-g-global-header-height as getPropertyValue returns a calc expression,
|
|
230
423
|
** we are using the respective CSS variables to calculate the height.
|
|
@@ -340,7 +533,14 @@ export default class RedocReference extends LightningElement {
|
|
|
340
533
|
specUrl,
|
|
341
534
|
{
|
|
342
535
|
// Dynamic scroll offset to account for headers
|
|
343
|
-
scrollYOffset: this.calculateScrollYOffset
|
|
536
|
+
scrollYOffset: this.calculateScrollYOffset,
|
|
537
|
+
// Redoc's own styled background outranks injected
|
|
538
|
+
// CSS, so the LNB background must be set via theme.
|
|
539
|
+
theme: {
|
|
540
|
+
sidebar: {
|
|
541
|
+
backgroundColor: this.sidebarBackgroundColor
|
|
542
|
+
}
|
|
543
|
+
}
|
|
344
544
|
},
|
|
345
545
|
redocContainer,
|
|
346
546
|
(error: any) => {
|
|
@@ -394,14 +594,26 @@ export default class RedocReference extends LightningElement {
|
|
|
394
594
|
this.appendFooterItems(apiContentDiv);
|
|
395
595
|
|
|
396
596
|
// Inject the multi-spec project header into Redoc's left menu only.
|
|
397
|
-
this.
|
|
597
|
+
this.insertSidebarNav(redocContainer);
|
|
398
598
|
|
|
399
599
|
// Wait for footer to be rendered before updating styles
|
|
400
600
|
requestAnimationFrame(() => {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
601
|
+
try {
|
|
602
|
+
this.updateRedocThirdColumnStyle(redocContainer);
|
|
603
|
+
|
|
604
|
+
// Restore the view selected before a version switch only
|
|
605
|
+
// after all layout mutations (footer, header) are
|
|
606
|
+
// complete, so the scroll lands on the correct element.
|
|
607
|
+
this.restoreSelectedView();
|
|
608
|
+
|
|
609
|
+
// Fix initial hash scroll after doc phase insertion
|
|
610
|
+
this.handleInitialHashScrollFix();
|
|
611
|
+
} catch (error) {
|
|
612
|
+
this.showErrorUI(
|
|
613
|
+
"Failed to integrate custom components:",
|
|
614
|
+
error
|
|
615
|
+
);
|
|
616
|
+
}
|
|
405
617
|
});
|
|
406
618
|
} catch (error) {
|
|
407
619
|
this.showErrorUI("Failed to integrate custom components:", error);
|
|
@@ -409,31 +621,107 @@ export default class RedocReference extends LightningElement {
|
|
|
409
621
|
}
|
|
410
622
|
|
|
411
623
|
/**
|
|
412
|
-
* Inserts the project header
|
|
624
|
+
* Inserts the project header and LNB footer into Redoc.
|
|
625
|
+
*/
|
|
626
|
+
private insertSidebarNav(redocContainer: HTMLElement): void {
|
|
627
|
+
// Select the LNB and content area of Redoc and insert the requried header.
|
|
628
|
+
if (this.showRedocHeader) {
|
|
629
|
+
redocContainer
|
|
630
|
+
.querySelectorAll<HTMLElement>(".menu-content, .api-content")
|
|
631
|
+
.forEach((target) => {
|
|
632
|
+
target.insertBefore(
|
|
633
|
+
this.buildProjectHeaderDom(),
|
|
634
|
+
target.firstChild
|
|
635
|
+
);
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Locale picker
|
|
640
|
+
if (this.hasLocalePicker) {
|
|
641
|
+
const menuContent = redocContainer.querySelector(".menu-content");
|
|
642
|
+
menuContent?.appendChild(this.buildLocalePickerDom());
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** Builds the version picker DOM by reusing `doc-version-picker`. */
|
|
647
|
+
private buildVersionPickerDom(): HTMLElement {
|
|
648
|
+
const wrapper = document.createElement("div");
|
|
649
|
+
wrapper.className = "redoc-version-picker";
|
|
650
|
+
|
|
651
|
+
const picker = createElement("doc-version-picker", {
|
|
652
|
+
is: VersionPicker
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
Object.assign(picker, {
|
|
656
|
+
versions: this.versions,
|
|
657
|
+
selectedVersion: this.selectedVersion,
|
|
658
|
+
latestVersion: this.latestVersion,
|
|
659
|
+
readOnly: this.isVersionReadOnly
|
|
660
|
+
});
|
|
661
|
+
picker.addEventListener("change", this.onVersionChange);
|
|
662
|
+
wrapper.appendChild(picker);
|
|
663
|
+
|
|
664
|
+
return wrapper;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** Stashes the current view (URL hash) before the version link navigates. */
|
|
668
|
+
private onVersionChange = (): void => {
|
|
669
|
+
sessionStorage.setItem(
|
|
670
|
+
SELECTED_VIEW_STORAGE_KEY,
|
|
671
|
+
window.location.hash || ""
|
|
672
|
+
);
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Re-applies the view stashed before a version switch, if its anchor exists
|
|
677
|
+
* in the new spec; otherwise a no-op (lands on the spec root).
|
|
413
678
|
*/
|
|
414
|
-
private
|
|
415
|
-
|
|
679
|
+
private restoreSelectedView(): void {
|
|
680
|
+
const storedHash = sessionStorage.getItem(SELECTED_VIEW_STORAGE_KEY);
|
|
681
|
+
sessionStorage.removeItem(SELECTED_VIEW_STORAGE_KEY);
|
|
682
|
+
|
|
683
|
+
if (!storedHash || window.location.hash) {
|
|
416
684
|
return;
|
|
417
685
|
}
|
|
418
686
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
.
|
|
422
|
-
|
|
423
|
-
target.insertBefore(
|
|
424
|
-
this.buildProjectHeaderDom(),
|
|
425
|
-
target.firstChild
|
|
426
|
-
);
|
|
427
|
-
});
|
|
687
|
+
const targetId = storedHash.replace(/^#/, "");
|
|
688
|
+
if (targetId && document.getElementById(targetId)) {
|
|
689
|
+
window.location.hash = storedHash;
|
|
690
|
+
}
|
|
428
691
|
}
|
|
429
692
|
|
|
430
693
|
/**
|
|
431
|
-
* Builds
|
|
694
|
+
* Builds the locale picker DOM by reusing `dx-sidebar-footer-nav`
|
|
695
|
+
*/
|
|
696
|
+
private buildLocalePickerDom(): HTMLElement {
|
|
697
|
+
const wrapper = document.createElement("div");
|
|
698
|
+
wrapper.className = "redoc-footer-nav";
|
|
699
|
+
|
|
700
|
+
const picker = createElement("dx-sidebar-footer-nav", {
|
|
701
|
+
is: SidebarFooterNav
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
Object.assign(picker, {
|
|
705
|
+
languages: this.languages,
|
|
706
|
+
language: this.language
|
|
707
|
+
});
|
|
708
|
+
wrapper.appendChild(picker);
|
|
709
|
+
|
|
710
|
+
return wrapper;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Builds the doc header: a title group (back link + spec title) and, when
|
|
715
|
+
* versions are available, the version picker as a sibling row. Layout/gaps
|
|
716
|
+
* are styled by the developer-website's redoc CSS.
|
|
432
717
|
*/
|
|
433
718
|
private buildProjectHeaderDom(): HTMLElement {
|
|
434
719
|
const wrapper = document.createElement("div");
|
|
435
720
|
wrapper.className = "redoc-project-header";
|
|
436
721
|
|
|
722
|
+
const main = document.createElement("div");
|
|
723
|
+
main.className = "redoc-project-header-main";
|
|
724
|
+
|
|
437
725
|
if (this.projectTitle) {
|
|
438
726
|
const backLink = document.createElement("a");
|
|
439
727
|
backLink.className = "redoc-project-back";
|
|
@@ -454,14 +742,20 @@ export default class RedocReference extends LightningElement {
|
|
|
454
742
|
|
|
455
743
|
backLink.appendChild(icon);
|
|
456
744
|
backLink.appendChild(label);
|
|
457
|
-
|
|
745
|
+
main.appendChild(backLink);
|
|
458
746
|
}
|
|
459
747
|
|
|
460
748
|
if (this.specTitle) {
|
|
461
749
|
const specEl = document.createElement("h2");
|
|
462
750
|
specEl.className = "redoc-spec-title dx-text-display-7";
|
|
463
751
|
specEl.textContent = this.specTitle;
|
|
464
|
-
|
|
752
|
+
main.appendChild(specEl);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
wrapper.appendChild(main);
|
|
756
|
+
|
|
757
|
+
if (this.hasVersionPicker) {
|
|
758
|
+
wrapper.appendChild(this.buildVersionPickerDom());
|
|
465
759
|
}
|
|
466
760
|
|
|
467
761
|
return wrapper;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
display: block;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.content-type-docs doc-phase {
|
|
6
|
+
--doc-c-phase-top: calc(
|
|
7
|
+
var(--dx-g-global-header-height) + var(--dx-g-doc-header-height) +
|
|
8
|
+
var(--dx-g-spacing-xl)
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@media screen and (max-width: 768px) {
|
|
13
|
+
.content-type-docs doc-phase {
|
|
14
|
+
--doc-c-phase-top: calc(
|
|
15
|
+
var(--dx-g-global-header-height) + var(--dx-g-doc-header-height) +
|
|
16
|
+
40px
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<doc-content-layout
|
|
3
|
+
class="content-type content-type-markdown content-type-docs"
|
|
4
|
+
breadcrumbs={breadcrumbs}
|
|
5
|
+
share-title={shareTitle}
|
|
6
|
+
share-twitter-via={twitterVia}
|
|
7
|
+
sidebar-header={sidebarHeader}
|
|
8
|
+
sidebar-value={sidebarValue}
|
|
9
|
+
sidebar-content={sidebarContent}
|
|
10
|
+
toc-title={tocTitle}
|
|
11
|
+
toc-options={tocOptions}
|
|
12
|
+
toc-aria-level={tocAriaLevel}
|
|
13
|
+
enable-slot-change="true"
|
|
14
|
+
languages={languages}
|
|
15
|
+
language={language}
|
|
16
|
+
show-footer={enableFooter}
|
|
17
|
+
show-content-action-toolbar={showContentActionToolbar}
|
|
18
|
+
origin={origin}
|
|
19
|
+
dev-center={devCenter}
|
|
20
|
+
brand={brand}
|
|
21
|
+
>
|
|
22
|
+
<doc-phase
|
|
23
|
+
slot="doc-phase"
|
|
24
|
+
lwc:if={docPhaseInfo}
|
|
25
|
+
doc-phase-info={docPhaseInfo}
|
|
26
|
+
></doc-phase>
|
|
27
|
+
<slot></slot>
|
|
28
|
+
</doc-content-layout>
|
|
29
|
+
</template>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { LightningElement, api } from "lwc";
|
|
2
|
+
import { toJson } from "dxUtils/normalizers";
|
|
3
|
+
import type { OptionWithLink, TreeNode } from "typings/custom";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Per-topic type emitted by the docs content-type parser
|
|
7
|
+
* (see @salesforcedevs/sfdocs-doc-framework: `TopicTypeEnum`). Only `spec`
|
|
8
|
+
* is meaningful inside this component; everything else renders as a plain
|
|
9
|
+
* markdown-style tile.
|
|
10
|
+
*/
|
|
11
|
+
const TOPIC_TYPE_SPEC = "spec";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Translates per-topic `topicType` into the generic `showForwardArrow` flag
|
|
15
|
+
* that `dx-tree-tile` reads (spec topics get the forward arrow icon for Redoc).
|
|
16
|
+
*/
|
|
17
|
+
function decorateTopicsWithForwardArrow(
|
|
18
|
+
topics:
|
|
19
|
+
| Array<TreeNode & { topicType?: string; versions?: unknown }>
|
|
20
|
+
| undefined
|
|
21
|
+
): TreeNode[] | undefined {
|
|
22
|
+
return topics?.map((topic) => {
|
|
23
|
+
const { topicType, versions, ...decorated } = topic;
|
|
24
|
+
if (topicType === TOPIC_TYPE_SPEC) {
|
|
25
|
+
decorated.showForwardArrow = true;
|
|
26
|
+
// Nested and standalone specs never get a sidebar picker; only the
|
|
27
|
+
// parent MD topic (when versioned) shows one.
|
|
28
|
+
} else if (versions) {
|
|
29
|
+
decorated.versions = versions;
|
|
30
|
+
}
|
|
31
|
+
if (decorated.children) {
|
|
32
|
+
decorated.children = decorateTopicsWithForwardArrow(
|
|
33
|
+
decorated.children
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return decorated;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Wrapper around `doc-content-layout` for the "docs" content type emitted by
|
|
42
|
+
* the `DocsContentTypeParser`.
|
|
43
|
+
*/
|
|
44
|
+
export default class UnifiedContentLayout extends LightningElement {
|
|
45
|
+
@api breadcrumbs: string | null = null;
|
|
46
|
+
@api sidebarHeader?: string;
|
|
47
|
+
@api sidebarValue?: string;
|
|
48
|
+
@api tocTitle?: string;
|
|
49
|
+
@api tocOptions?: string;
|
|
50
|
+
@api tocAriaLevel?: string;
|
|
51
|
+
@api languages?: OptionWithLink[];
|
|
52
|
+
@api language?: string;
|
|
53
|
+
@api devCenter: any = null;
|
|
54
|
+
@api brand: any = null;
|
|
55
|
+
@api showContentActionToolbar = false;
|
|
56
|
+
|
|
57
|
+
/** Optional origin URL for the footer MFE (e.g. wp-json endpoint). */
|
|
58
|
+
@api origin: string | null = null;
|
|
59
|
+
|
|
60
|
+
/** Article name from breadcrumbs, used as share title (e.g. for social share). */
|
|
61
|
+
@api shareTitle: string | null = null;
|
|
62
|
+
|
|
63
|
+
/** Optional Twitter "via" handle (e.g. SalesforceDevs) for social share. */
|
|
64
|
+
@api twitterVia: string | null = null;
|
|
65
|
+
|
|
66
|
+
@api hideFooter = false;
|
|
67
|
+
|
|
68
|
+
private _docPhaseInfo: string | null = null;
|
|
69
|
+
private _sidebarContent: unknown = null;
|
|
70
|
+
|
|
71
|
+
@api
|
|
72
|
+
get docPhaseInfo(): string | null {
|
|
73
|
+
return this._docPhaseInfo;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
set docPhaseInfo(value: string | null) {
|
|
77
|
+
this._docPhaseInfo = value || null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@api
|
|
81
|
+
get sidebarContent(): unknown {
|
|
82
|
+
return this._sidebarContent;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
set sidebarContent(value: string) {
|
|
86
|
+
this._sidebarContent = decorateTopicsWithForwardArrow(
|
|
87
|
+
toJson(value)?.topics
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private get enableFooter(): boolean {
|
|
92
|
+
return !this.hideFooter;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -4,26 +4,59 @@
|
|
|
4
4
|
:host {
|
|
5
5
|
--dx-c-dropdown-option-font-weight: normal;
|
|
6
6
|
--dx-c-dropdown-option-label-color: var(--dx-g-gray-10);
|
|
7
|
+
--dx-c-dropdown-option-padding: var(--dx-g-spacing-sm)
|
|
8
|
+
var(--dx-g-spacing-lg);
|
|
9
|
+
--dx-c-popover-padding: var(--dx-g-spacing-sm) 0;
|
|
7
10
|
--popover-container-open-transform: translateY(4px);
|
|
8
11
|
}
|
|
9
12
|
|
|
13
|
+
/* Open menus must stack above later sibling pickers in the sidebar. */
|
|
14
|
+
:host(:has([aria-expanded="true"])) {
|
|
15
|
+
position: relative;
|
|
16
|
+
z-index: 2;
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
.version-picker-container {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
20
|
+
/* Override --doc-version-picker-padding to change the inset (e.g. when the
|
|
21
|
+
picker sits inside an already-padded container). */
|
|
22
|
+
padding: var(
|
|
23
|
+
--doc-version-picker-padding,
|
|
24
|
+
var(--dx-g-spacing-sm) var(--dx-g-spacing-lg) var(--dx-g-spacing-sm)
|
|
25
|
+
var(--dx-g-global-header-padding-horizontal)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
/* Override --doc-version-picker-divider (e.g. `none`) to restyle dividers. */
|
|
29
|
+
border-top: var(
|
|
30
|
+
--doc-version-picker-divider,
|
|
31
|
+
1px solid var(--dx-g-gray-90)
|
|
32
|
+
);
|
|
33
|
+
border-bottom: var(
|
|
34
|
+
--doc-version-picker-divider,
|
|
35
|
+
1px solid var(--dx-g-gray-90)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Inline sidebar variant: drop the header-slot chrome so the picker sits
|
|
40
|
+
beside a tree node without extra padding or divider lines. */
|
|
41
|
+
.version-picker-container-small {
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
padding: 0;
|
|
45
|
+
border: none;
|
|
15
46
|
}
|
|
16
47
|
|
|
17
48
|
.version-picker-button {
|
|
18
49
|
display: flex;
|
|
19
50
|
width: var(--doc-version-picker-width, 296px);
|
|
51
|
+
|
|
52
|
+
--dx-c-button-horizontal-spacing: var(--dx-g-spacing-sm);
|
|
53
|
+
--dx-g-button-icon-color: var(--dx-g-gray-50);
|
|
20
54
|
}
|
|
21
55
|
|
|
22
56
|
.version-picker-button:hover,
|
|
23
|
-
.version-picker-button:
|
|
24
|
-
.version-picker-button
|
|
25
|
-
--dx-
|
|
26
|
-
--dx-c-button-primary-color: var(--dx-g-blue-vibrant-40);
|
|
57
|
+
.version-picker-button:focus-within,
|
|
58
|
+
.version-picker-button[aria-expanded="true"] {
|
|
59
|
+
--dx-g-button-icon-color: var(--dx-g-blue-vibrant-20);
|
|
27
60
|
}
|
|
28
61
|
|
|
29
62
|
/**
|
|
@@ -35,6 +68,27 @@ dx-button::part(content) {
|
|
|
35
68
|
overflow: hidden;
|
|
36
69
|
}
|
|
37
70
|
|
|
71
|
+
/* The border is an inset box-shadow so thickening it on hover/focus doesn't
|
|
72
|
+
resize the small variant's fit-content box. */
|
|
73
|
+
.version-picker-button::part(container) {
|
|
74
|
+
background: white;
|
|
75
|
+
box-shadow: inset 0 0 0 1px var(--dx-g-gray-50);
|
|
76
|
+
color: var(--dx-g-gray-10);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.version-picker-button::part(container):hover {
|
|
80
|
+
background: var(--dx-g-blue-vibrant-95);
|
|
81
|
+
box-shadow: inset 0 0 0 2px var(--dx-g-blue-vibrant-20);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* aria-expanded keeps the focus ring while the menu is open. */
|
|
85
|
+
.version-picker-button:focus-within::part(container),
|
|
86
|
+
.version-picker-button[aria-expanded="true"]::part(container) {
|
|
87
|
+
background: var(--dx-g-blue-vibrant-95);
|
|
88
|
+
box-shadow: inset 0 0 0 2px var(--dx-g-blue-vibrant-20), 0 0 0 2px white,
|
|
89
|
+
0 0 0 4px var(--dx-g-blue-vibrant-60);
|
|
90
|
+
}
|
|
91
|
+
|
|
38
92
|
.selected-version {
|
|
39
93
|
display: flex;
|
|
40
94
|
flex-direction: row;
|
|
@@ -49,16 +103,100 @@ dx-button::part(content) {
|
|
|
49
103
|
white-space: nowrap;
|
|
50
104
|
}
|
|
51
105
|
|
|
52
|
-
dx-type-badge.latest-badge
|
|
53
|
-
|
|
54
|
-
--dx-c-type-badge-background: var(--dx-g-green-vibrant-95);
|
|
55
|
-
|
|
106
|
+
dx-type-badge.latest-badge,
|
|
107
|
+
dx-type-badge.not-latest-badge {
|
|
56
108
|
margin-left: var(--dx-g-spacing-sm);
|
|
57
109
|
}
|
|
58
110
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
111
|
+
/* ------------------------------------------------------------------ *
|
|
112
|
+
* Small "dot" variant (opt-in via `small`). Additive
|
|
113
|
+
* ------------------------------------------------------------------ */
|
|
62
114
|
|
|
63
|
-
|
|
115
|
+
/* Option padding is sm both axes: the small menu tracks the ~104px trigger, so
|
|
116
|
+
the default 24px horizontal would crowd the labels. */
|
|
117
|
+
.version-picker-dropdown-small {
|
|
118
|
+
--dx-c-dropdown-option-font-size: var(--dx-g-text-xs);
|
|
119
|
+
--dx-c-dropdown-option-padding: var(--dx-g-spacing-sm)
|
|
120
|
+
var(--dx-g-spacing-sm);
|
|
121
|
+
--dx-c-dropdown-option-border-radius: 0;
|
|
122
|
+
--dx-c-popover-border: none;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.version-picker-button-small {
|
|
126
|
+
width: fit-content;
|
|
127
|
+
max-width: 104px;
|
|
128
|
+
|
|
129
|
+
--dx-c-button-font-size: var(--dx-g-text-xs);
|
|
130
|
+
--dx-c-button-font-weight: var(--dx-g-font-normal);
|
|
131
|
+
--dx-c-button-line-height: var(--dx-g-spacing-lg);
|
|
132
|
+
--dx-c-button-icon-gap: var(--dx-g-spacing-2xs);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* min-width: 0 on each flex ancestor lets the label truncate: a flex item's
|
|
136
|
+
default min-width: auto refuses to shrink below its content and overrides the
|
|
137
|
+
host's max-width, so without this the trigger overflows instead. */
|
|
138
|
+
.version-picker-button-small::part(content) {
|
|
139
|
+
display: flex;
|
|
140
|
+
flex: 1;
|
|
141
|
+
min-width: 0;
|
|
142
|
+
width: auto;
|
|
143
|
+
overflow: hidden;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* width:100% re-ties to the 104px-capped host (container is width:inherit,
|
|
147
|
+
which copies width but not max-width). Border/bg/states are shared above. */
|
|
148
|
+
.version-picker-button-small::part(container) {
|
|
149
|
+
width: 100%;
|
|
150
|
+
height: var(--dx-g-spacing-lg);
|
|
151
|
+
padding: 0 var(--dx-g-spacing-xs);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.selected-version-small {
|
|
155
|
+
flex: 1;
|
|
156
|
+
min-width: 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.version-picker-dot {
|
|
160
|
+
flex: 0 0 auto;
|
|
161
|
+
width: var(--dx-g-spacing-sm);
|
|
162
|
+
height: var(--dx-g-spacing-sm);
|
|
163
|
+
margin-right: var(--dx-g-spacing-xs);
|
|
164
|
+
border-radius: 50%;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.version-picker-dot-latest {
|
|
168
|
+
background: var(--dx-g-green-vibrant-60);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.version-picker-dot-not-latest {
|
|
172
|
+
background: var(--dx-g-yellow-vibrant-80);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* inline-block (not flex) so the label can truncate; line-height centers it. */
|
|
176
|
+
.version-picker-readonly {
|
|
177
|
+
box-sizing: border-box;
|
|
178
|
+
display: inline-block;
|
|
179
|
+
width: var(--doc-version-picker-width, 296px);
|
|
180
|
+
height: var(--dx-g-spacing-xl);
|
|
181
|
+
margin: 0;
|
|
182
|
+
padding: 0 var(--dx-g-spacing-sm);
|
|
183
|
+
border: 1px solid var(--dx-g-gray-80);
|
|
184
|
+
border-radius: var(--dx-g-spacing-xs);
|
|
185
|
+
overflow: hidden;
|
|
186
|
+
color: var(--dx-g-gray-10);
|
|
187
|
+
text-overflow: ellipsis;
|
|
188
|
+
white-space: nowrap;
|
|
189
|
+
font: var(--dx-g-font-normal) var(--dx-g-text-sm) / var(--dx-g-spacing-xl)
|
|
190
|
+
var(--dx-g-font-sans),
|
|
191
|
+
sans-serif;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.version-picker-readonly-small {
|
|
195
|
+
width: fit-content;
|
|
196
|
+
max-width: 104px;
|
|
197
|
+
height: var(--dx-g-spacing-lg);
|
|
198
|
+
padding: 0 var(--dx-g-spacing-xs);
|
|
199
|
+
font: var(--dx-g-font-normal) var(--dx-g-text-xs) / var(--dx-g-spacing-lg)
|
|
200
|
+
var(--dx-g-font-sans),
|
|
201
|
+
sans-serif;
|
|
64
202
|
}
|
|
@@ -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
|
+
full-width="true"
|
|
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,97 @@ 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
|
+
private get triggerClass(): string {
|
|
128
|
+
return cx(
|
|
129
|
+
"version-picker-button",
|
|
130
|
+
this.small && "version-picker-button-small"
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private get triggerIconSize(): string {
|
|
135
|
+
return this.small ? "xsmall" : "medium";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Default returns "" (dx-button's own default) so the trigger's rendered
|
|
139
|
+
// aria-label is unchanged for the default variant.
|
|
140
|
+
private get triggerAriaLabel(): string {
|
|
141
|
+
return this.small ? this.smallTriggerAriaLabel : "";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private get selectedVersionClass(): string {
|
|
145
|
+
return cx("selected-version", this.small && "selected-version-small");
|
|
146
|
+
}
|
|
147
|
+
|
|
62
148
|
private onVersionChange(e: CustomEvent) {
|
|
63
149
|
this.dispatchEvent(new CustomEvent("change", { detail: e.detail }));
|
|
64
150
|
}
|
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.
|