@salesforcedevs/docs-components 1.29.0 → 1.30.0
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 +2 -2
- package/src/modules/doc/amfReference/amfReference.html +1 -0
- package/src/modules/doc/amfReference/amfReference.ts +11 -0
- package/src/modules/doc/contentActionToolbar/contentActionToolbar.css +31 -0
- package/src/modules/doc/contentActionToolbar/contentActionToolbar.html +53 -0
- package/src/modules/doc/contentActionToolbar/contentActionToolbar.ts +151 -0
- package/src/modules/doc/contentActionToolbar/contentActionToolbarMocks.ts +48 -0
- package/src/modules/doc/contentLayout/contentLayout.ts +61 -2
package/lwc.config.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforcedevs/docs-components",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "Docs Lightning web components for DSC",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "index.js",
|
|
@@ -25,5 +25,5 @@
|
|
|
25
25
|
"@types/lodash.orderby": "4.6.9",
|
|
26
26
|
"@types/lodash.uniqby": "4.7.9"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "f3e8e2c6ae6c8792b316e41021344648d4a07bec"
|
|
29
29
|
}
|
|
@@ -73,6 +73,17 @@ export default class AmfReference extends LightningElement {
|
|
|
73
73
|
return this.isSpecBasedReference(this._currentReferenceId);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Content action toolbar is only enabled for markdown-based references in the en-us locale.
|
|
78
|
+
*/
|
|
79
|
+
get showContentActionToolbar(): boolean {
|
|
80
|
+
if (this.showSpecBasedReference) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const locale = this.language?.toLowerCase() ?? "en-us";
|
|
84
|
+
return locale === "en-us";
|
|
85
|
+
}
|
|
86
|
+
|
|
76
87
|
@api
|
|
77
88
|
get referenceSetConfig(): ReferenceSetConfig {
|
|
78
89
|
return this._referenceSetConfig;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@import "dxHelpers/reset";
|
|
2
|
+
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.toolbar {
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: var(--dx-g-spacing-smd);
|
|
11
|
+
margin-bottom: var(--dx-g-spacing-lg);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.toolbar-button {
|
|
15
|
+
--dx-c-button-font-weight: var(--dx-g-font-demi);
|
|
16
|
+
--dx-c-button-line-height: var(--dx-g-spacing-mlg);
|
|
17
|
+
--dx-c-button-letter-spacing: 0.005em;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.divider {
|
|
21
|
+
width: 1px;
|
|
22
|
+
height: var(--dx-g-spacing-md);
|
|
23
|
+
background-color: var(--dx-g-gray-70);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@media screen and (max-width: 480px) {
|
|
27
|
+
.toolbar-button_copy-url,
|
|
28
|
+
.divider_copy-url {
|
|
29
|
+
display: none;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="toolbar" lwc:if={markdownUrl}>
|
|
3
|
+
<dx-tooltip placement="top-right" label={copyMarkdownLabel}>
|
|
4
|
+
<dx-button
|
|
5
|
+
class="toolbar-button"
|
|
6
|
+
variant="inline"
|
|
7
|
+
size="small"
|
|
8
|
+
icon-sprite="utility"
|
|
9
|
+
icon-symbol="copy"
|
|
10
|
+
icon-size="medium"
|
|
11
|
+
icon-position="right"
|
|
12
|
+
aria-label={copyMarkdownButtonText}
|
|
13
|
+
onclick={handleCopyMarkdown}
|
|
14
|
+
>
|
|
15
|
+
{copyMarkdownButtonText}
|
|
16
|
+
</dx-button>
|
|
17
|
+
</dx-tooltip>
|
|
18
|
+
|
|
19
|
+
<div class="divider"></div>
|
|
20
|
+
|
|
21
|
+
<dx-button
|
|
22
|
+
class="toolbar-button"
|
|
23
|
+
variant="inline"
|
|
24
|
+
size="small"
|
|
25
|
+
icon-sprite="utility"
|
|
26
|
+
icon-symbol="new_window"
|
|
27
|
+
icon-size="medium"
|
|
28
|
+
icon-position="right"
|
|
29
|
+
aria-label={viewMarkdownButtonText}
|
|
30
|
+
onclick={handleViewMarkdown}
|
|
31
|
+
>
|
|
32
|
+
{viewMarkdownButtonText}
|
|
33
|
+
</dx-button>
|
|
34
|
+
|
|
35
|
+
<div class="divider divider_copy-url"></div>
|
|
36
|
+
|
|
37
|
+
<dx-tooltip placement="top-right" label={copyUrlLabel}>
|
|
38
|
+
<dx-button
|
|
39
|
+
class="toolbar-button toolbar-button_copy-url"
|
|
40
|
+
variant="inline"
|
|
41
|
+
size="small"
|
|
42
|
+
icon-sprite="utility"
|
|
43
|
+
icon-symbol="link"
|
|
44
|
+
icon-size="medium"
|
|
45
|
+
icon-position="right"
|
|
46
|
+
aria-label={copyUrlButtonText}
|
|
47
|
+
onclick={handleCopyUrl}
|
|
48
|
+
>
|
|
49
|
+
{copyUrlButtonText}
|
|
50
|
+
</dx-button>
|
|
51
|
+
</dx-tooltip>
|
|
52
|
+
</div>
|
|
53
|
+
</template>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { LightningElement, api } from "lwc";
|
|
2
|
+
import { track } from "dxUtils/analytics";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_COPY_TOOLTIP_LABEL = "Click to copy";
|
|
5
|
+
const COPIED_TOOLTIP_LABEL = "Copied!";
|
|
6
|
+
const COPIED_TOOLTIP_RESET_MS = 1000;
|
|
7
|
+
|
|
8
|
+
const ANALYTICS_CONTENT_CATEGORY = "content action toolbar";
|
|
9
|
+
const COPY_MARKDOWN_LABEL = "Copy as Markdown";
|
|
10
|
+
const VIEW_MARKDOWN_LABEL = "View as Markdown";
|
|
11
|
+
const COPY_URL_LABEL = "Copy URL to Markdown";
|
|
12
|
+
|
|
13
|
+
export default class ContentActionToolbar extends LightningElement {
|
|
14
|
+
@api
|
|
15
|
+
get pageUrl(): string | undefined {
|
|
16
|
+
return this._pageUrl;
|
|
17
|
+
}
|
|
18
|
+
set pageUrl(value: string | undefined) {
|
|
19
|
+
if (this._pageUrl === value) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
this._pageUrl = value;
|
|
23
|
+
}
|
|
24
|
+
private _pageUrl?: string;
|
|
25
|
+
|
|
26
|
+
copyMarkdownLabel: string = DEFAULT_COPY_TOOLTIP_LABEL;
|
|
27
|
+
copyUrlLabel: string = DEFAULT_COPY_TOOLTIP_LABEL;
|
|
28
|
+
|
|
29
|
+
private copyTooltipResetTimeout: number | null = null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns the `.md` equivalent of `pageUrl` with any hash and query
|
|
33
|
+
* string stripped, or `null` when `pageUrl` is not set or does not end
|
|
34
|
+
* with `.html`.
|
|
35
|
+
*/
|
|
36
|
+
get markdownUrl(): string | null {
|
|
37
|
+
if (!this.pageUrl) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const url = new URL(this.pageUrl, window.location.href);
|
|
42
|
+
url.hash = "";
|
|
43
|
+
url.search = "";
|
|
44
|
+
|
|
45
|
+
if (!url.pathname.endsWith(".html")) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
url.pathname = url.pathname.replace(/\.html$/, ".md");
|
|
50
|
+
return url.toString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get copyMarkdownButtonText(): string {
|
|
54
|
+
return COPY_MARKDOWN_LABEL;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get viewMarkdownButtonText(): string {
|
|
58
|
+
return VIEW_MARKDOWN_LABEL;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get copyUrlButtonText(): string {
|
|
62
|
+
return COPY_URL_LABEL;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async handleCopyMarkdown(event: Event) {
|
|
66
|
+
if (!this.markdownUrl) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.trackToolbarEvent(
|
|
71
|
+
event,
|
|
72
|
+
"custEv_linkClick",
|
|
73
|
+
COPY_MARKDOWN_LABEL,
|
|
74
|
+
this.markdownUrl
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(this.markdownUrl);
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const markdown = await response.text();
|
|
83
|
+
await navigator.clipboard.writeText(markdown);
|
|
84
|
+
this.flashCopied("copyMarkdownLabel");
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(error);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
handleViewMarkdown(event: Event) {
|
|
91
|
+
if (!this.markdownUrl) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.trackToolbarEvent(
|
|
96
|
+
event,
|
|
97
|
+
"custEv_linkClick",
|
|
98
|
+
VIEW_MARKDOWN_LABEL,
|
|
99
|
+
this.markdownUrl
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
window.open(this.markdownUrl, "_blank", "noopener,noreferrer");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async handleCopyUrl(event: Event) {
|
|
106
|
+
if (!this.markdownUrl) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.trackToolbarEvent(
|
|
111
|
+
event,
|
|
112
|
+
"custEv_linkClick",
|
|
113
|
+
COPY_URL_LABEL,
|
|
114
|
+
this.markdownUrl
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await navigator.clipboard.writeText(this.markdownUrl);
|
|
119
|
+
this.flashCopied("copyUrlLabel");
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private trackToolbarEvent(
|
|
126
|
+
event: Event,
|
|
127
|
+
eventName: "custEv_linkClick",
|
|
128
|
+
label: string,
|
|
129
|
+
url: string
|
|
130
|
+
): void {
|
|
131
|
+
track(event.currentTarget!, eventName, {
|
|
132
|
+
click_text: label,
|
|
133
|
+
click_url: url,
|
|
134
|
+
element_type: "button_link",
|
|
135
|
+
element_title: label,
|
|
136
|
+
content_category: ANALYTICS_CONTENT_CATEGORY
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private flashCopied(labelKey: "copyMarkdownLabel" | "copyUrlLabel") {
|
|
141
|
+
if (this.copyTooltipResetTimeout !== null) {
|
|
142
|
+
window.clearTimeout(this.copyTooltipResetTimeout);
|
|
143
|
+
}
|
|
144
|
+
this[labelKey] = COPIED_TOOLTIP_LABEL;
|
|
145
|
+
this.copyTooltipResetTimeout = window.setTimeout(() => {
|
|
146
|
+
this.copyMarkdownLabel = DEFAULT_COPY_TOOLTIP_LABEL;
|
|
147
|
+
this.copyUrlLabel = DEFAULT_COPY_TOOLTIP_LABEL;
|
|
148
|
+
this.copyTooltipResetTimeout = null;
|
|
149
|
+
}, COPIED_TOOLTIP_RESET_MS);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { http, HttpResponse } from "msw";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mocks for the content action toolbar so stories never hit the real
|
|
5
|
+
* docs backend. Any story rendering `doc-content-action-toolbar` (directly or
|
|
6
|
+
* via `doc-content-layout`) must register `contentActionToolbarMswHandlers`
|
|
7
|
+
* and call `interceptWindowOpenForContentActionToolbar()`.
|
|
8
|
+
*/
|
|
9
|
+
export const DUMMY_MARKDOWN_CONTENT = `# Dummy Markdown
|
|
10
|
+
|
|
11
|
+
Storybook serves this placeholder content in place of the real markdown
|
|
12
|
+
that the docs backend would return for the current page. It exists so
|
|
13
|
+
the "Copied" tooltip, the "View as Markdown" new tab, and the
|
|
14
|
+
"Copy URL to Markdown" clipboard behavior are all exercisable in
|
|
15
|
+
storybook without hitting the live backend.
|
|
16
|
+
|
|
17
|
+
- Item 1
|
|
18
|
+
- Item 2
|
|
19
|
+
- Item 3
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
export const DUMMY_MARKDOWN_DATA_URL = `data:text/markdown;charset=utf-8,${encodeURIComponent(
|
|
23
|
+
DUMMY_MARKDOWN_CONTENT
|
|
24
|
+
)}`;
|
|
25
|
+
|
|
26
|
+
/** Intercepts any `.md` GET request and returns the dummy markdown. */
|
|
27
|
+
export const contentActionToolbarMswHandlers = [
|
|
28
|
+
http.get(/\.md(\?.*)?$/, () => HttpResponse.text(DUMMY_MARKDOWN_CONTENT))
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
let windowOpenIntercepted = false;
|
|
32
|
+
|
|
33
|
+
/** Redirects `window.open` calls for `.md` URLs to the dummy markdown data URL (MSW does not cover new tabs). */
|
|
34
|
+
export function interceptWindowOpenForContentActionToolbar() {
|
|
35
|
+
if (windowOpenIntercepted) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
windowOpenIntercepted = true;
|
|
39
|
+
|
|
40
|
+
const originalOpen = window.open.bind(window);
|
|
41
|
+
window.open = ((url?: string | URL, target?: string, features?: string) => {
|
|
42
|
+
const stringUrl = url?.toString() ?? "";
|
|
43
|
+
if (stringUrl.endsWith(".md")) {
|
|
44
|
+
return originalOpen(DUMMY_MARKDOWN_DATA_URL, target, features);
|
|
45
|
+
}
|
|
46
|
+
return originalOpen(url ?? "", target, features);
|
|
47
|
+
}) as typeof window.open;
|
|
48
|
+
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/* eslint-disable @lwc/lwc/no-document-query */
|
|
2
|
-
import { LightningElement, api, track } from "lwc";
|
|
3
|
-
import { closest } from "kagekiri";
|
|
2
|
+
import { LightningElement, api, createElement, track } from "lwc";
|
|
3
|
+
import { closest, querySelector } from "kagekiri";
|
|
4
4
|
import { toJson, normalizeBoolean } from "dxUtils/normalizers";
|
|
5
5
|
import { highlightTerms } from "dxUtils/highlight";
|
|
6
6
|
import { SearchSyncer } from "docUtils/searchSyncer";
|
|
7
7
|
import type { OptionWithLink } from "typings/custom";
|
|
8
8
|
import { buildDocLinkClickHandler } from "dxUtils/analytics";
|
|
9
|
+
import ContentActionToolbar from "doc/contentActionToolbar";
|
|
10
|
+
|
|
11
|
+
const CONTENT_ACTION_TOOLBAR_TAG = "doc-content-action-toolbar";
|
|
12
|
+
const PAGE_HEADING_SELECTOR = "h1";
|
|
9
13
|
|
|
10
14
|
type AnchorMap = { [key: string]: { intersect: boolean; id: string } };
|
|
11
15
|
|
|
@@ -95,6 +99,17 @@ export default class ContentLayout extends LightningElement {
|
|
|
95
99
|
/** Optional origin URL for the footer MFE (e.g. wp-json endpoint). Passed through to dx-footer. */
|
|
96
100
|
@api origin: string | null = null;
|
|
97
101
|
|
|
102
|
+
/** Controls whether the content action toolbar is displayed. */
|
|
103
|
+
@api
|
|
104
|
+
get showContentActionToolbar() {
|
|
105
|
+
return this._showContentActionToolbar;
|
|
106
|
+
}
|
|
107
|
+
set showContentActionToolbar(value) {
|
|
108
|
+
this._showContentActionToolbar = normalizeBoolean(value);
|
|
109
|
+
this.updateContentActionToolbar();
|
|
110
|
+
}
|
|
111
|
+
private _showContentActionToolbar = false;
|
|
112
|
+
|
|
98
113
|
@api
|
|
99
114
|
get breadcrumbs() {
|
|
100
115
|
return this._breadcrumbs;
|
|
@@ -152,6 +167,7 @@ export default class ContentLayout extends LightningElement {
|
|
|
152
167
|
protected hasRendered: boolean = false;
|
|
153
168
|
protected contentLoaded: boolean = false;
|
|
154
169
|
protected sidebarOpen: boolean = false;
|
|
170
|
+
protected contentActionToolbarElement: HTMLElement | null = null;
|
|
155
171
|
|
|
156
172
|
get shouldDisplayFeedback() {
|
|
157
173
|
return this.contentLoaded && typeof Sprig !== "undefined";
|
|
@@ -257,6 +273,8 @@ export default class ContentLayout extends LightningElement {
|
|
|
257
273
|
window.addEventListener("scroll", this.adjustNavPosition);
|
|
258
274
|
window.addEventListener("resize", this.adjustNavPosition);
|
|
259
275
|
|
|
276
|
+
this.updateContentActionToolbar();
|
|
277
|
+
|
|
260
278
|
if (!this.hasRendered) {
|
|
261
279
|
this.hasRendered = true;
|
|
262
280
|
this.restoreScroll();
|
|
@@ -266,6 +284,43 @@ export default class ContentLayout extends LightningElement {
|
|
|
266
284
|
}
|
|
267
285
|
}
|
|
268
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Inserts the content action toolbar into the slotted content immediately
|
|
289
|
+
* after the first H1 found inside this layout.
|
|
290
|
+
*/
|
|
291
|
+
protected updateContentActionToolbar(): void {
|
|
292
|
+
if (!this.showContentActionToolbar || !this.sidebarValue) {
|
|
293
|
+
this.removeContentActionToolbar();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const heading = querySelector(
|
|
298
|
+
PAGE_HEADING_SELECTOR,
|
|
299
|
+
this.template.host
|
|
300
|
+
) as HTMLElement | null;
|
|
301
|
+
|
|
302
|
+
if (!heading) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const toolbar = (this.contentActionToolbarElement ??
|
|
307
|
+
createElement(CONTENT_ACTION_TOOLBAR_TAG, {
|
|
308
|
+
is: ContentActionToolbar
|
|
309
|
+
})) as HTMLElement & { pageUrl?: string };
|
|
310
|
+
|
|
311
|
+
toolbar.pageUrl = this.sidebarValue;
|
|
312
|
+
|
|
313
|
+
if (toolbar.previousElementSibling !== heading) {
|
|
314
|
+
heading.parentNode?.insertBefore(toolbar, heading.nextSibling);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.contentActionToolbarElement = toolbar;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
protected removeContentActionToolbar(): void {
|
|
321
|
+
this.contentActionToolbarElement?.remove();
|
|
322
|
+
}
|
|
323
|
+
|
|
269
324
|
disconnectedCallback(): void {
|
|
270
325
|
this.disconnectObserver();
|
|
271
326
|
window.removeEventListener(
|
|
@@ -281,6 +336,9 @@ export default class ContentLayout extends LightningElement {
|
|
|
281
336
|
|
|
282
337
|
// Remove link click handler
|
|
283
338
|
this.template.removeEventListener("click", this.handleLinkClick);
|
|
339
|
+
|
|
340
|
+
this.removeContentActionToolbar();
|
|
341
|
+
this.contentActionToolbarElement = null;
|
|
284
342
|
}
|
|
285
343
|
|
|
286
344
|
restoreScroll() {
|
|
@@ -523,6 +581,7 @@ export default class ContentLayout extends LightningElement {
|
|
|
523
581
|
|
|
524
582
|
onSlotChange(): void {
|
|
525
583
|
this.updateRNB();
|
|
584
|
+
this.updateContentActionToolbar();
|
|
526
585
|
this.contentLoaded = true;
|
|
527
586
|
}
|
|
528
587
|
|