@salesforcedevs/docs-components 1.14.3-alpha → 1.14.4-edit
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 +2 -1
- package/src/modules/doc/markdownEditor/README.md +70 -0
- package/src/modules/doc/markdownEditor/markdownEditor.css +169 -0
- package/src/modules/doc/markdownEditor/markdownEditor.html +64 -0
- package/src/modules/doc/markdownEditor/markdownEditor.ts +111 -0
- package/src/modules/doc/xmlContent/xmlContent.ts +15 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforcedevs/docs-components",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.4-edit",
|
|
4
4
|
"description": "Docs Lightning web components for DSC",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "index.js",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@api-components/amf-helper-mixin": "4.5.29",
|
|
15
15
|
"classnames": "2.5.1",
|
|
16
|
+
"dompurify": "3.2.4",
|
|
16
17
|
"kagekiri": "1.4.2",
|
|
17
18
|
"lodash.orderby": "4.6.0",
|
|
18
19
|
"lodash.uniqby": "4.7.0",
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Markdown Editor Component
|
|
2
|
+
|
|
3
|
+
A Lightning Web Component that provides an HTML-to-Markdown editor with Monaco Editor integration for documentation editing.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **HTML to Markdown Conversion**: Automatically converts HTML content to Markdown format
|
|
8
|
+
- **Monaco Editor Integration**: Rich code editing experience with syntax highlighting
|
|
9
|
+
- **Basic Save Functionality**: Save edited content to a configured endpoint
|
|
10
|
+
- **Responsive Design**: Works on desktop and mobile devices
|
|
11
|
+
- **Loading States**: Visual feedback during save operations
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<doc-markdown-editor
|
|
17
|
+
content="<h1>Your HTML content here</h1>"
|
|
18
|
+
file-path="/docs/your-file.md"
|
|
19
|
+
save-endpoint="/api/save"
|
|
20
|
+
auth-token="your-auth-token">
|
|
21
|
+
</doc-markdown-editor>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## API Properties
|
|
25
|
+
|
|
26
|
+
| Property | Type | Required | Description |
|
|
27
|
+
|----------|------|----------|-------------|
|
|
28
|
+
| `content` | string | Yes | The HTML content to display and edit |
|
|
29
|
+
| `filePath` | string | No | The file path for saving (used in save requests) |
|
|
30
|
+
| `saveEndpoint` | string | No | The API endpoint for saving content |
|
|
31
|
+
| `authToken` | string | No | Authentication token for API calls |
|
|
32
|
+
|
|
33
|
+
## Events
|
|
34
|
+
|
|
35
|
+
| Event | Detail | Description |
|
|
36
|
+
|-------|--------|-------------|
|
|
37
|
+
| `saved` | `{success: boolean, content: string, filePath: string}` | Fired when content is successfully saved |
|
|
38
|
+
|
|
39
|
+
## Dependencies
|
|
40
|
+
|
|
41
|
+
- **Monaco Editor**: Loaded dynamically from CDN
|
|
42
|
+
- **Turndown.js**: Loaded dynamically from CDN for HTML-to-Markdown conversion
|
|
43
|
+
|
|
44
|
+
## Browser Support
|
|
45
|
+
|
|
46
|
+
- Modern browsers with ES6+ support
|
|
47
|
+
- Requires JavaScript enabled
|
|
48
|
+
- Monaco Editor requires modern browser features
|
|
49
|
+
|
|
50
|
+
## Development
|
|
51
|
+
|
|
52
|
+
To test the component locally:
|
|
53
|
+
|
|
54
|
+
1. Start Storybook: `yarn storybook`
|
|
55
|
+
2. Navigate to "Doc/MarkdownEditor" story
|
|
56
|
+
3. Test the edit functionality
|
|
57
|
+
|
|
58
|
+
## Save Endpoint Format
|
|
59
|
+
|
|
60
|
+
The save endpoint should accept POST requests with the following JSON payload:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"content": "markdown content",
|
|
65
|
+
"filePath": "/path/to/file.md",
|
|
66
|
+
"originalContent": "original html content"
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Expected response: HTTP 200 for success, any other status for failure.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
.markdown-editor {
|
|
2
|
+
border: 1px solid #ddd;
|
|
3
|
+
border-radius: 4px;
|
|
4
|
+
margin: 1rem 0;
|
|
5
|
+
background: #fff;
|
|
6
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.editor-toolbar {
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
gap: 0.5rem;
|
|
13
|
+
padding: 0.75rem;
|
|
14
|
+
border-bottom: 1px solid #ddd;
|
|
15
|
+
background: #f8f9fa;
|
|
16
|
+
border-radius: 4px 4px 0 0;
|
|
17
|
+
justify-content: space-between;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.edit-button {
|
|
21
|
+
margin-left: auto;
|
|
22
|
+
min-width: 80px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.button-group {
|
|
26
|
+
display: flex;
|
|
27
|
+
gap: 0.5rem;
|
|
28
|
+
margin-left: auto;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.cancel-button,
|
|
32
|
+
.save-button {
|
|
33
|
+
min-width: 80px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.status-indicator {
|
|
37
|
+
font-size: 0.875rem;
|
|
38
|
+
font-weight: 500;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.status-saving {
|
|
42
|
+
color: #007bff;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.status-success {
|
|
46
|
+
color: #28a745;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.status-error {
|
|
50
|
+
color: #dc3545;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.content-viewer {
|
|
54
|
+
padding: 1rem;
|
|
55
|
+
min-height: 100px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.content-display {
|
|
59
|
+
line-height: 1.6;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.content-display h1,
|
|
63
|
+
.content-display h2,
|
|
64
|
+
.content-display h3,
|
|
65
|
+
.content-display h4,
|
|
66
|
+
.content-display h5,
|
|
67
|
+
.content-display h6 {
|
|
68
|
+
margin-top: 1.5rem;
|
|
69
|
+
margin-bottom: 0.5rem;
|
|
70
|
+
font-weight: 600;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.content-display p {
|
|
74
|
+
margin-bottom: 1rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.content-display code {
|
|
78
|
+
background: #f1f3f4;
|
|
79
|
+
padding: 0.125rem 0.25rem;
|
|
80
|
+
border-radius: 3px;
|
|
81
|
+
font-size: 0.875em;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.content-display pre {
|
|
85
|
+
background: #f8f9fa;
|
|
86
|
+
padding: 1rem;
|
|
87
|
+
border-radius: 4px;
|
|
88
|
+
overflow-x: auto;
|
|
89
|
+
margin: 1rem 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.content-display pre code {
|
|
93
|
+
background: none;
|
|
94
|
+
padding: 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.editor-wrapper {
|
|
98
|
+
position: relative;
|
|
99
|
+
min-height: 400px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.editor-container {
|
|
103
|
+
height: 400px;
|
|
104
|
+
min-height: 200px;
|
|
105
|
+
border-radius: 0 0 4px 4px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.simple-editor {
|
|
109
|
+
width: 100%;
|
|
110
|
+
height: 400px;
|
|
111
|
+
min-height: 200px;
|
|
112
|
+
padding: 1rem;
|
|
113
|
+
border: none;
|
|
114
|
+
border-radius: 0 0 4px 4px;
|
|
115
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
116
|
+
font-size: 14px;
|
|
117
|
+
line-height: 1.5;
|
|
118
|
+
resize: vertical;
|
|
119
|
+
background: #f8f9fa;
|
|
120
|
+
color: #333;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.simple-editor:focus {
|
|
124
|
+
outline: none;
|
|
125
|
+
background: #fff;
|
|
126
|
+
box-shadow: inset 0 0 0 2px #007bff;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.loading-overlay {
|
|
130
|
+
position: absolute;
|
|
131
|
+
top: 0;
|
|
132
|
+
left: 0;
|
|
133
|
+
right: 0;
|
|
134
|
+
bottom: 0;
|
|
135
|
+
background: rgba(255, 255, 255, 0.8);
|
|
136
|
+
display: flex;
|
|
137
|
+
align-items: center;
|
|
138
|
+
justify-content: center;
|
|
139
|
+
z-index: 1000;
|
|
140
|
+
border-radius: 0 0 4px 4px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Monaco Editor customizations */
|
|
144
|
+
.monaco-editor {
|
|
145
|
+
border-radius: 0 0 4px 4px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.monaco-editor .margin {
|
|
149
|
+
background: #f8f9fa;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Responsive design */
|
|
153
|
+
@media (max-width: 768px) {
|
|
154
|
+
.editor-toolbar {
|
|
155
|
+
flex-wrap: wrap;
|
|
156
|
+
gap: 0.25rem;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.status-indicator {
|
|
160
|
+
margin-left: 0;
|
|
161
|
+
width: 100%;
|
|
162
|
+
text-align: center;
|
|
163
|
+
margin-top: 0.5rem;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.editor-container {
|
|
167
|
+
height: 300px;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="markdown-editor">
|
|
3
|
+
<!-- Editor Toolbar -->
|
|
4
|
+
<div class="editor-toolbar">
|
|
5
|
+
<template if:true={showViewer}>
|
|
6
|
+
<dx-button
|
|
7
|
+
onclick={handleEdit}
|
|
8
|
+
variant="tertiary"
|
|
9
|
+
class="edit-button">
|
|
10
|
+
Edit Content
|
|
11
|
+
</dx-button>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<template if:true={showEditor}>
|
|
15
|
+
<div class="button-group">
|
|
16
|
+
<dx-button
|
|
17
|
+
onclick={handleCancel}
|
|
18
|
+
variant="secondary"
|
|
19
|
+
class="cancel-button">
|
|
20
|
+
Cancel
|
|
21
|
+
</dx-button>
|
|
22
|
+
|
|
23
|
+
<dx-button
|
|
24
|
+
onclick={handleSave}
|
|
25
|
+
variant="primary"
|
|
26
|
+
disabled={saveButtonDisabled}
|
|
27
|
+
class="save-button">
|
|
28
|
+
Save
|
|
29
|
+
</dx-button>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="status-indicator">
|
|
33
|
+
<span class={statusClass}>{statusMessage}</span>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<!-- Content Viewer (Read-only) -->
|
|
39
|
+
<template if:true={showViewer}>
|
|
40
|
+
<div class="content-viewer">
|
|
41
|
+
<div class="content-display" innerhtml={content}></div>
|
|
42
|
+
</div>
|
|
43
|
+
</template>
|
|
44
|
+
|
|
45
|
+
<!-- Simple Editor -->
|
|
46
|
+
<template if:true={showEditor}>
|
|
47
|
+
<div class="editor-wrapper">
|
|
48
|
+
<textarea
|
|
49
|
+
class="simple-editor"
|
|
50
|
+
value={markdownContent}
|
|
51
|
+
onchange={handleContentChange}
|
|
52
|
+
placeholder="Enter your markdown content here...">
|
|
53
|
+
</textarea>
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
56
|
+
|
|
57
|
+
<!-- Loading Overlay -->
|
|
58
|
+
<template if:true={isLoading}>
|
|
59
|
+
<div class="loading-overlay">
|
|
60
|
+
<dx-spinner size="medium" alternative-text="Loading"></dx-spinner>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { LightningElement, api, track } from 'lwc';
|
|
2
|
+
|
|
3
|
+
export default class MarkdownEditor extends LightningElement {
|
|
4
|
+
@api content: string = '';
|
|
5
|
+
@api filePath: string = '';
|
|
6
|
+
@api saveEndpoint: string = '';
|
|
7
|
+
@api authToken: string = '';
|
|
8
|
+
|
|
9
|
+
@track isEditing: boolean = false;
|
|
10
|
+
@track markdownContent: string = '';
|
|
11
|
+
@track isLoading: boolean = false;
|
|
12
|
+
@track saveStatus: 'idle' | 'saving' | 'success' | 'error' = 'idle';
|
|
13
|
+
|
|
14
|
+
connectedCallback() {
|
|
15
|
+
this.markdownContent = this.content; // Simple fallback for now
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Event Handlers
|
|
19
|
+
handleEdit() {
|
|
20
|
+
this.isEditing = true;
|
|
21
|
+
this.saveStatus = 'idle';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
handleCancel() {
|
|
25
|
+
this.isEditing = false;
|
|
26
|
+
this.saveStatus = 'idle';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
handleContentChange(event: Event) {
|
|
30
|
+
const target = event.target as HTMLTextAreaElement;
|
|
31
|
+
this.markdownContent = target.value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async handleSave() {
|
|
35
|
+
if (!this.saveEndpoint) {
|
|
36
|
+
console.error('Save endpoint not configured');
|
|
37
|
+
this.saveStatus = 'error';
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.saveStatus = 'saving';
|
|
42
|
+
this.isLoading = true;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(this.saveEndpoint, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: {
|
|
48
|
+
'Content-Type': 'application/json',
|
|
49
|
+
'Authorization': this.authToken ? `Bearer ${this.authToken}` : ''
|
|
50
|
+
},
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
content: this.markdownContent,
|
|
53
|
+
filePath: this.filePath,
|
|
54
|
+
originalContent: this.content
|
|
55
|
+
})
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (response.ok) {
|
|
59
|
+
this.saveStatus = 'success';
|
|
60
|
+
this.isEditing = false;
|
|
61
|
+
|
|
62
|
+
// Dispatch save success event
|
|
63
|
+
this.dispatchEvent(new CustomEvent('saved', {
|
|
64
|
+
detail: {
|
|
65
|
+
success: true,
|
|
66
|
+
content: this.markdownContent,
|
|
67
|
+
filePath: this.filePath
|
|
68
|
+
}
|
|
69
|
+
}));
|
|
70
|
+
} else {
|
|
71
|
+
throw new Error(`Save failed: ${response.status}`);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Save failed:', error);
|
|
75
|
+
this.saveStatus = 'error';
|
|
76
|
+
} finally {
|
|
77
|
+
this.isLoading = false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Getters for UI state
|
|
82
|
+
get showEditor() {
|
|
83
|
+
return this.isEditing;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get showViewer() {
|
|
87
|
+
return !this.isEditing;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
get saveButtonDisabled() {
|
|
91
|
+
return this.saveStatus === 'saving' || this.isLoading;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get statusMessage() {
|
|
95
|
+
switch (this.saveStatus) {
|
|
96
|
+
case 'saving': return 'Saving...';
|
|
97
|
+
case 'success': return 'Saved successfully!';
|
|
98
|
+
case 'error': return 'Save failed. Please try again.';
|
|
99
|
+
default: return '';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
get statusClass() {
|
|
104
|
+
switch (this.saveStatus) {
|
|
105
|
+
case 'saving': return 'status-saving';
|
|
106
|
+
case 'success': return 'status-success';
|
|
107
|
+
case 'error': return 'status-error';
|
|
108
|
+
default: return '';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -18,6 +18,7 @@ import { LightningElementWithState } from "dxBaseElements/lightningElementWithSt
|
|
|
18
18
|
import { logCoveoPageView, oldVersionDocInfo } from "docUtils/utils";
|
|
19
19
|
import { Breadcrumb, DocPhaseInfo, Language } from "typings/custom";
|
|
20
20
|
import { track as trackGTM } from "dxUtils/analytics";
|
|
21
|
+
import DOMPurify from "dompurify";
|
|
21
22
|
|
|
22
23
|
// TODO: Imitating from actual implementation as doc-content use it like this. We should refactor it later.
|
|
23
24
|
const handleContentError = (error: any): void => console.log(error);
|
|
@@ -425,9 +426,19 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
425
426
|
.catch(handleContentError);
|
|
426
427
|
}
|
|
427
428
|
|
|
429
|
+
private sanitizeUrlPart(part: string | undefined): string | undefined {
|
|
430
|
+
if (!part) {
|
|
431
|
+
return part;
|
|
432
|
+
}
|
|
433
|
+
return DOMPurify.sanitize(part);
|
|
434
|
+
}
|
|
435
|
+
|
|
428
436
|
getReferenceFromUrl(): PageReference {
|
|
429
437
|
const [page, docId, deliverable, contentDocumentId] =
|
|
430
|
-
window.location.pathname
|
|
438
|
+
window.location.pathname
|
|
439
|
+
.substr(1)
|
|
440
|
+
.split("/")
|
|
441
|
+
.map(this.sanitizeUrlPart);
|
|
431
442
|
|
|
432
443
|
const { origin: domain, hash, search } = window.location;
|
|
433
444
|
|
|
@@ -436,9 +447,9 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
436
447
|
deliverable,
|
|
437
448
|
docId,
|
|
438
449
|
domain,
|
|
439
|
-
hash,
|
|
450
|
+
hash: this.sanitizeUrlPart(hash),
|
|
440
451
|
page,
|
|
441
|
-
search
|
|
452
|
+
search: this.sanitizeUrlPart(search)
|
|
442
453
|
};
|
|
443
454
|
}
|
|
444
455
|
|
|
@@ -701,7 +712,7 @@ export default class DocXmlContent extends LightningElementWithState<{
|
|
|
701
712
|
|
|
702
713
|
addMetatags(): void {
|
|
703
714
|
const div = document.createElement("div");
|
|
704
|
-
div.innerHTML = this.docContent;
|
|
715
|
+
div.innerHTML = DOMPurify.sanitize(this.docContent);
|
|
705
716
|
const docDescription = div.querySelector(".shortdesc")?.textContent;
|
|
706
717
|
const topicTitle = div.querySelector("h1")?.textContent;
|
|
707
718
|
|