markdown-text-editor 0.1.0 → 0.1.1
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/LICENSE +20 -20
- package/README.md +277 -277
- package/dist/index.css +35 -0
- package/dist/index.css.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/markdown-text-editor.css +35 -0
- package/dist/markdown-text-editor.css.map +1 -1
- package/dist/markdown-text-editor.js +1 -1
- package/dist/markdown-text-editor.js.map +1 -1
- package/package.json +72 -67
- package/src/plugins/index.js +2 -2
- package/src/plugins/markdown/Toolbar/MakeTool.js +66 -63
- package/src/plugins/markdown/Toolbar/index.js +50 -42
- package/src/plugins/markdown/Toolbar/tools/BoldTool.js +14 -14
- package/src/plugins/markdown/Toolbar/tools/CheckListTool.js +17 -17
- package/src/plugins/markdown/Toolbar/tools/ItalicTool.js +16 -16
- package/src/plugins/markdown/Toolbar/tools/OLTool.js +19 -19
- package/src/plugins/markdown/Toolbar/tools/PreviewTool.js +110 -98
- package/src/plugins/markdown/Toolbar/tools/StrikethroughTool.js +15 -15
- package/src/plugins/markdown/Toolbar/tools/ULTool.js +17 -17
- package/src/plugins/markdown/editor.js +168 -141
- package/src/plugins/markdown/preview.js +16 -16
- package/src/plugins/markdown/styles/main.css +2 -2
- package/src/plugins/markdown/utils/markdownUtils.js +7 -7
|
@@ -1,98 +1,110 @@
|
|
|
1
|
-
// #components/Toolbar/tools/PreviewToggleTool.js
|
|
2
|
-
import MakeTool from '../MakeTool.js';
|
|
3
|
-
|
|
4
|
-
class PreviewTool extends MakeTool {
|
|
5
|
-
constructor(editor) {
|
|
6
|
-
// No markdown syntax for preview toggle, so we call the parent constructor with empty values
|
|
7
|
-
super(editor, '', 'Preview');
|
|
8
|
-
this.button = this.createButton(`
|
|
9
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
10
|
-
<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M12 3v18"/>
|
|
11
|
-
</svg>
|
|
12
|
-
`);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Override applySyntax to handle preview toggling
|
|
16
|
-
applySyntax() {
|
|
17
|
-
const previewWrapper = this.editor.previewContent?.parentNode;
|
|
18
|
-
const editorDiv = this.editor.markdownEditorDiv;
|
|
19
|
-
|
|
20
|
-
if (!previewWrapper || !editorDiv) return;
|
|
21
|
-
|
|
22
|
-
// Toggle the preview's visibility by switching between 'block' and 'hidden' classes
|
|
23
|
-
if (this.editor.preview) {
|
|
24
|
-
this.enablePreview(previewWrapper, editorDiv);
|
|
25
|
-
} else {
|
|
26
|
-
this.disablePreview(previewWrapper, editorDiv);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Method to hide the preview (disable it)
|
|
31
|
-
disablePreview(previewWrapper, editorDiv) {
|
|
32
|
-
|
|
33
|
-
this.editor.preview = true;
|
|
34
|
-
|
|
35
|
-
editorDiv.parentNode.classList.toggle('fixed');
|
|
36
|
-
editorDiv.parentNode.classList.toggle('top-0');
|
|
37
|
-
editorDiv.parentNode.classList.toggle('inset-x-0');
|
|
38
|
-
editorDiv.parentNode.classList.toggle('rounded-md');
|
|
39
|
-
editorDiv.parentNode.classList.toggle('z-[999]');
|
|
40
|
-
|
|
41
|
-
previewWrapper.classList.toggle('hidden');
|
|
42
|
-
|
|
43
|
-
// Add grid layout and divide classes to the editor div
|
|
44
|
-
editorDiv.classList.remove(
|
|
45
|
-
'md:grid',
|
|
46
|
-
'md:grid-cols-2',
|
|
47
|
-
'md:divide-x',
|
|
48
|
-
'rtl:md:divide-x-reverse',
|
|
49
|
-
'md:divide-stone-300',
|
|
50
|
-
'dark:md:divide-stone-700'
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
editorDiv.querySelector(".textarea-wrapper").classList.remove(
|
|
54
|
-
'h-[90lvh]',
|
|
55
|
-
'hidden',
|
|
56
|
-
'md:block'
|
|
57
|
-
);
|
|
58
|
-
this.editor.render(); // Re-render content in the preview
|
|
59
|
-
|
|
60
|
-
editorDiv.querySelector(".textarea-wrapper").querySelector("textarea").classList.remove("!h-[90lvh]");
|
|
61
|
-
|
|
62
|
-
document.querySelector("body").classList.remove('overflow-hidden');
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
editorDiv.classList.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
'md:
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
1
|
+
// #components/Toolbar/tools/PreviewToggleTool.js
|
|
2
|
+
import MakeTool from '../MakeTool.js';
|
|
3
|
+
|
|
4
|
+
class PreviewTool extends MakeTool {
|
|
5
|
+
constructor(editor) {
|
|
6
|
+
// No markdown syntax for preview toggle, so we call the parent constructor with empty values
|
|
7
|
+
super(editor, '', 'Preview');
|
|
8
|
+
this.button = this.createButton(`
|
|
9
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
10
|
+
<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M12 3v18"/>
|
|
11
|
+
</svg>
|
|
12
|
+
`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Override applySyntax to handle preview toggling
|
|
16
|
+
applySyntax() {
|
|
17
|
+
const previewWrapper = this.editor.previewContent?.parentNode;
|
|
18
|
+
const editorDiv = this.editor.markdownEditorDiv;
|
|
19
|
+
|
|
20
|
+
if (!previewWrapper || !editorDiv) return;
|
|
21
|
+
|
|
22
|
+
// Toggle the preview's visibility by switching between 'block' and 'hidden' classes
|
|
23
|
+
if (this.editor.preview) {
|
|
24
|
+
this.enablePreview(previewWrapper, editorDiv);
|
|
25
|
+
} else {
|
|
26
|
+
this.disablePreview(previewWrapper, editorDiv);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Method to hide the preview (disable it)
|
|
31
|
+
disablePreview(previewWrapper, editorDiv) {
|
|
32
|
+
|
|
33
|
+
this.editor.preview = true;
|
|
34
|
+
|
|
35
|
+
editorDiv.parentNode.classList.toggle('fixed');
|
|
36
|
+
editorDiv.parentNode.classList.toggle('top-0');
|
|
37
|
+
editorDiv.parentNode.classList.toggle('inset-x-0');
|
|
38
|
+
editorDiv.parentNode.classList.toggle('rounded-md');
|
|
39
|
+
editorDiv.parentNode.classList.toggle('z-[999]');
|
|
40
|
+
|
|
41
|
+
previewWrapper.classList.toggle('hidden');
|
|
42
|
+
|
|
43
|
+
// Add grid layout and divide classes to the editor div
|
|
44
|
+
editorDiv.classList.remove(
|
|
45
|
+
'md:grid',
|
|
46
|
+
'md:grid-cols-2',
|
|
47
|
+
'md:divide-x',
|
|
48
|
+
'rtl:md:divide-x-reverse',
|
|
49
|
+
'md:divide-stone-300',
|
|
50
|
+
'dark:md:divide-stone-700'
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
editorDiv.querySelector(".textarea-wrapper").classList.remove(
|
|
54
|
+
'h-[90lvh]',
|
|
55
|
+
'hidden',
|
|
56
|
+
'md:block'
|
|
57
|
+
);
|
|
58
|
+
this.editor.render(); // Re-render content in the preview
|
|
59
|
+
|
|
60
|
+
editorDiv.querySelector(".textarea-wrapper").querySelector("textarea").classList.remove("!h-[90lvh]");
|
|
61
|
+
|
|
62
|
+
document.querySelector("body").classList.remove('overflow-hidden');
|
|
63
|
+
|
|
64
|
+
document.querySelectorAll('.markdown-btn').forEach(button => {
|
|
65
|
+
if (!button.classList.contains('preview-btn')) {
|
|
66
|
+
button.classList.remove('pointer-events-none', 'md:pointer-events-auto', 'opacity-25', 'md:opacity-100');
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Method to show the preview (enable it)
|
|
72
|
+
enablePreview(previewWrapper, editorDiv) {
|
|
73
|
+
|
|
74
|
+
this.editor.preview = false;
|
|
75
|
+
|
|
76
|
+
editorDiv.parentNode.classList.toggle('fixed');
|
|
77
|
+
editorDiv.parentNode.classList.toggle('top-0');
|
|
78
|
+
editorDiv.parentNode.classList.toggle('inset-x-0');
|
|
79
|
+
editorDiv.parentNode.classList.toggle('rounded-md');
|
|
80
|
+
editorDiv.parentNode.classList.toggle('z-[999]');
|
|
81
|
+
|
|
82
|
+
previewWrapper.classList.toggle('hidden');
|
|
83
|
+
// Remove grid layout and divide classes from the editor div
|
|
84
|
+
editorDiv.classList.add(
|
|
85
|
+
'md:grid',
|
|
86
|
+
'md:grid-cols-2',
|
|
87
|
+
'md:divide-x',
|
|
88
|
+
'rtl:md:divide-x-reverse',
|
|
89
|
+
'md:divide-stone-300',
|
|
90
|
+
'dark:md:divide-stone-700'
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
editorDiv.querySelector(".textarea-wrapper").classList.add(
|
|
94
|
+
'h-[90lvh]',
|
|
95
|
+
'hidden',
|
|
96
|
+
'md:block'
|
|
97
|
+
);
|
|
98
|
+
editorDiv.querySelector(".textarea-wrapper").querySelector("textarea").classList.add("!h-[90lvh]");
|
|
99
|
+
|
|
100
|
+
document.querySelector("body").classList.add('overflow-hidden');
|
|
101
|
+
|
|
102
|
+
document.querySelectorAll('.markdown-btn').forEach(button => {
|
|
103
|
+
if (!button.classList.contains('preview-btn')) {
|
|
104
|
+
button.classList.add('pointer-events-none', 'md:pointer-events-auto', 'opacity-25', 'md:opacity-100');
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default PreviewTool;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import MakeTool from '../MakeTool.js';
|
|
2
|
-
|
|
3
|
-
class StrikethroughTool extends MakeTool {
|
|
4
|
-
constructor(editor) {
|
|
5
|
-
// Call the parent constructor with the markdown syntax for strikethrough (~~)
|
|
6
|
-
super(editor, '~~', '
|
|
7
|
-
this.button = this.createButton(`
|
|
8
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
9
|
-
<path d="M16 4H9a3 3 0 0 0-2.83 4"/>
|
|
10
|
-
<path d="M14 12a4 4 0 0 1 0 8H6"/><line x1="4" x2="20" y1="12" y2="12"/>
|
|
11
|
-
</svg>
|
|
12
|
-
`);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
1
|
+
import MakeTool from '../MakeTool.js';
|
|
2
|
+
|
|
3
|
+
class StrikethroughTool extends MakeTool {
|
|
4
|
+
constructor(editor) {
|
|
5
|
+
// Call the parent constructor with the markdown syntax for strikethrough (~~)
|
|
6
|
+
super(editor, '~~', 'Strikethrough');
|
|
7
|
+
this.button = this.createButton(`
|
|
8
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
9
|
+
<path d="M16 4H9a3 3 0 0 0-2.83 4"/>
|
|
10
|
+
<path d="M14 12a4 4 0 0 1 0 8H6"/><line x1="4" x2="20" y1="12" y2="12"/>
|
|
11
|
+
</svg>
|
|
12
|
+
`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
16
|
export default StrikethroughTool;
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import MakeTool from '../MakeTool.js';
|
|
2
|
-
|
|
3
|
-
class ULTool extends MakeTool {
|
|
4
|
-
constructor(editor) {
|
|
5
|
-
super(editor, '-', 'Unordered list');
|
|
6
|
-
this.button = this.createButton(`
|
|
7
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="
|
|
8
|
-
`);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// You can change how the syntax is applied for this specific tool:
|
|
12
|
-
applySyntax() {
|
|
13
|
-
super.applySyntax('start'); // Only apply strikethrough at the start
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
1
|
+
import MakeTool from '../MakeTool.js';
|
|
2
|
+
|
|
3
|
+
class ULTool extends MakeTool {
|
|
4
|
+
constructor(editor) {
|
|
5
|
+
super(editor, '-', 'Unordered list');
|
|
6
|
+
this.button = this.createButton(`
|
|
7
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h.01"/><path d="M3 18h.01"/><path d="M3 6h.01"/><path d="M8 12h13"/><path d="M8 18h13"/><path d="M8 6h13"/></svg>
|
|
8
|
+
`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// You can change how the syntax is applied for this specific tool:
|
|
12
|
+
applySyntax() {
|
|
13
|
+
super.applySyntax('start'); // Only apply strikethrough at the start
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
18
|
export default ULTool;
|
|
@@ -1,142 +1,169 @@
|
|
|
1
|
-
// #markdown\editor.js
|
|
2
|
-
import './styles/main.css';
|
|
3
|
-
import { marked } from 'marked';
|
|
4
|
-
import Toolbar from './Toolbar/index.js';
|
|
5
|
-
import Preview from './preview.js';
|
|
6
|
-
|
|
7
|
-
marked.setOptions({
|
|
8
|
-
breaks: true
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
class MarkdownEditor {
|
|
12
|
-
constructor(selector, options = {}) {
|
|
13
|
-
this.usertextarea = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
|
14
|
-
this.options = options;
|
|
15
|
-
this.preview = this.options.toolbar.includes('preview');
|
|
16
|
-
this.init();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
init() {
|
|
20
|
-
this.createEditor();
|
|
21
|
-
if (this.options.toolbar) this.addToolbar();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
createEditor() {
|
|
25
|
-
if (!this.isTextareaValid()) return;
|
|
26
|
-
|
|
27
|
-
this.applyDefaultAttributes();
|
|
28
|
-
this.buildEditorLayout();
|
|
29
|
-
this.addInputListener();
|
|
30
|
-
this.render();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
isTextareaValid() {
|
|
34
|
-
return this.usertextarea.tagName === 'TEXTAREA';
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
applyDefaultAttributes() {
|
|
38
|
-
this.usertextarea.classList.add(
|
|
39
|
-
"focus:outline-0",
|
|
40
|
-
"border-0",
|
|
41
|
-
"p-1.5",
|
|
42
|
-
"max-w-full",
|
|
43
|
-
"size-full",
|
|
44
|
-
"bg-transparent",
|
|
45
|
-
"outline-0",
|
|
46
|
-
"appearance-none",
|
|
47
|
-
"prose",
|
|
48
|
-
"prose-sm",
|
|
49
|
-
"md:prose-base",
|
|
50
|
-
"dark:prose-invert",
|
|
51
|
-
"text-stone-700",
|
|
52
|
-
"dark:text-stone-200",
|
|
53
|
-
"overflow-y-auto"
|
|
54
|
-
);
|
|
55
|
-
if (!this.usertextarea.hasAttribute('placeholder')) {
|
|
56
|
-
this.usertextarea.placeholder = this.options.placeholder || 'Write your markdown...';
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
buildEditorLayout() {
|
|
61
|
-
this.editorContainer = document.createElement('div');
|
|
62
|
-
this.editorContainer.className = `
|
|
63
|
-
markdown-editor-wrapper
|
|
64
|
-
border border-stone-200
|
|
65
|
-
dark:border-stone-700
|
|
66
|
-
rounded-md
|
|
67
|
-
overflow-hidden
|
|
68
|
-
`;
|
|
69
|
-
this.usertextarea.parentNode.insertBefore(this.editorContainer, this.usertextarea);
|
|
70
|
-
|
|
71
|
-
this.markdownEditorDiv = document.createElement('div');
|
|
72
|
-
this.markdownEditorDiv.className = `editor-layout`;
|
|
73
|
-
this.editorContainer.appendChild(this.markdownEditorDiv);
|
|
74
|
-
|
|
75
|
-
this.addTextareaWrapper();
|
|
76
|
-
if (this.preview) this.addPreviewWrapper();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
addTextareaWrapper() {
|
|
80
|
-
const textareaContainer = document.createElement('div');
|
|
81
|
-
textareaContainer.className = `
|
|
82
|
-
textarea-wrapper
|
|
83
|
-
p-2
|
|
84
|
-
bg-white
|
|
85
|
-
dark:bg-stone-800
|
|
86
|
-
grid
|
|
87
|
-
after:px-3.5
|
|
88
|
-
after:py-2.5
|
|
89
|
-
after:text-inherit
|
|
90
|
-
[&>textarea]:resize-none
|
|
91
|
-
[&>textarea]:[grid-area:1/1/2/2]
|
|
92
|
-
after:[grid-area:1/1/2/2]
|
|
93
|
-
after:whitespace-pre-wrap
|
|
94
|
-
after:invisible
|
|
95
|
-
after:content-[attr(data-cloned-val)_'_']
|
|
96
|
-
after:border
|
|
97
|
-
`;
|
|
98
|
-
textareaContainer.appendChild(this.usertextarea);
|
|
99
|
-
this.markdownEditorDiv.appendChild(textareaContainer);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
addPreviewWrapper() {
|
|
105
|
-
const preview = new Preview(this.markdownEditorDiv);
|
|
106
|
-
this.previewContent = preview.getPreviewContent();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
addInputListener() {
|
|
110
|
-
this.usertextarea.addEventListener('input', () => this.render());
|
|
111
|
-
this.usertextarea.addEventListener('scroll', () => {
|
|
112
|
-
const textarea = this.usertextarea;
|
|
113
|
-
const previewPane = this.previewContent;
|
|
114
|
-
|
|
115
|
-
// Calculate the proportion of the textarea that has been scrolled
|
|
116
|
-
const textareaScrollRatio = textarea.scrollTop / (textarea.scrollHeight - textarea.clientHeight);
|
|
117
|
-
|
|
118
|
-
// Apply the same scroll ratio to the preview pane
|
|
119
|
-
previewPane.scrollTop = textareaScrollRatio * (previewPane.scrollHeight - previewPane.clientHeight);
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
addToolbar() {
|
|
124
|
-
new Toolbar(this, this.options.toolbar || ['bold', 'italic', 'strikethrough']);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
insertText(text) {
|
|
128
|
-
const { selectionStart, selectionEnd } = this.usertextarea;
|
|
129
|
-
const value = this.usertextarea.value;
|
|
130
|
-
this.usertextarea.value = `${value.substring(0, selectionStart)}${text}${value.substring(selectionEnd)}`;
|
|
131
|
-
this.usertextarea.focus();
|
|
132
|
-
this.usertextarea.setSelectionRange(selectionStart, selectionStart + text.length);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
1
|
+
// #markdown\editor.js
|
|
2
|
+
import './styles/main.css';
|
|
3
|
+
import { marked } from 'marked';
|
|
4
|
+
import Toolbar from './Toolbar/index.js';
|
|
5
|
+
import Preview from './preview.js';
|
|
6
|
+
|
|
7
|
+
marked.setOptions({
|
|
8
|
+
breaks: true
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
class MarkdownEditor {
|
|
12
|
+
constructor(selector, options = {}) {
|
|
13
|
+
this.usertextarea = typeof selector === 'string' ? document.querySelector(selector) : selector;
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.preview = this.options.toolbar.includes('preview');
|
|
16
|
+
this.init();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
init() {
|
|
20
|
+
this.createEditor();
|
|
21
|
+
if (this.options.toolbar) this.addToolbar();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
createEditor() {
|
|
25
|
+
if (!this.isTextareaValid()) return;
|
|
26
|
+
|
|
27
|
+
this.applyDefaultAttributes();
|
|
28
|
+
this.buildEditorLayout();
|
|
29
|
+
this.addInputListener();
|
|
30
|
+
this.render();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
isTextareaValid() {
|
|
34
|
+
return this.usertextarea.tagName === 'TEXTAREA';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
applyDefaultAttributes() {
|
|
38
|
+
this.usertextarea.classList.add(
|
|
39
|
+
"focus:outline-0",
|
|
40
|
+
"border-0",
|
|
41
|
+
"p-1.5",
|
|
42
|
+
"max-w-full",
|
|
43
|
+
"size-full",
|
|
44
|
+
"bg-transparent",
|
|
45
|
+
"outline-0",
|
|
46
|
+
"appearance-none",
|
|
47
|
+
"prose",
|
|
48
|
+
"prose-sm",
|
|
49
|
+
"md:prose-base",
|
|
50
|
+
"dark:prose-invert",
|
|
51
|
+
"text-stone-700",
|
|
52
|
+
"dark:text-stone-200",
|
|
53
|
+
"overflow-y-auto"
|
|
54
|
+
);
|
|
55
|
+
if (!this.usertextarea.hasAttribute('placeholder')) {
|
|
56
|
+
this.usertextarea.placeholder = this.options.placeholder || 'Write your markdown...';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
buildEditorLayout() {
|
|
61
|
+
this.editorContainer = document.createElement('div');
|
|
62
|
+
this.editorContainer.className = `
|
|
63
|
+
markdown-editor-wrapper
|
|
64
|
+
border border-stone-200
|
|
65
|
+
dark:border-stone-700
|
|
66
|
+
rounded-md
|
|
67
|
+
overflow-hidden
|
|
68
|
+
`;
|
|
69
|
+
this.usertextarea.parentNode.insertBefore(this.editorContainer, this.usertextarea);
|
|
70
|
+
|
|
71
|
+
this.markdownEditorDiv = document.createElement('div');
|
|
72
|
+
this.markdownEditorDiv.className = `editor-layout`;
|
|
73
|
+
this.editorContainer.appendChild(this.markdownEditorDiv);
|
|
74
|
+
|
|
75
|
+
this.addTextareaWrapper();
|
|
76
|
+
if (this.preview) this.addPreviewWrapper();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
addTextareaWrapper() {
|
|
80
|
+
const textareaContainer = document.createElement('div');
|
|
81
|
+
textareaContainer.className = `
|
|
82
|
+
textarea-wrapper
|
|
83
|
+
p-2
|
|
84
|
+
bg-white
|
|
85
|
+
dark:bg-stone-800
|
|
86
|
+
grid
|
|
87
|
+
after:px-3.5
|
|
88
|
+
after:py-2.5
|
|
89
|
+
after:text-inherit
|
|
90
|
+
[&>textarea]:resize-none
|
|
91
|
+
[&>textarea]:[grid-area:1/1/2/2]
|
|
92
|
+
after:[grid-area:1/1/2/2]
|
|
93
|
+
after:whitespace-pre-wrap
|
|
94
|
+
after:invisible
|
|
95
|
+
after:content-[attr(data-cloned-val)_'_']
|
|
96
|
+
after:border
|
|
97
|
+
`;
|
|
98
|
+
textareaContainer.appendChild(this.usertextarea);
|
|
99
|
+
this.markdownEditorDiv.appendChild(textareaContainer);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
addPreviewWrapper() {
|
|
105
|
+
const preview = new Preview(this.markdownEditorDiv);
|
|
106
|
+
this.previewContent = preview.getPreviewContent();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
addInputListener() {
|
|
110
|
+
this.usertextarea.addEventListener('input', () => this.render());
|
|
111
|
+
this.usertextarea.addEventListener('scroll', () => {
|
|
112
|
+
const textarea = this.usertextarea;
|
|
113
|
+
const previewPane = this.previewContent;
|
|
114
|
+
|
|
115
|
+
// Calculate the proportion of the textarea that has been scrolled
|
|
116
|
+
const textareaScrollRatio = textarea.scrollTop / (textarea.scrollHeight - textarea.clientHeight);
|
|
117
|
+
|
|
118
|
+
// Apply the same scroll ratio to the preview pane
|
|
119
|
+
previewPane.scrollTop = textareaScrollRatio * (previewPane.scrollHeight - previewPane.clientHeight);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
addToolbar() {
|
|
124
|
+
new Toolbar(this, this.options.toolbar || ['bold', 'italic', 'strikethrough']);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
insertText(text) {
|
|
128
|
+
const { selectionStart, selectionEnd } = this.usertextarea;
|
|
129
|
+
const value = this.usertextarea.value;
|
|
130
|
+
this.usertextarea.value = `${value.substring(0, selectionStart)}${text}${value.substring(selectionEnd)}`;
|
|
131
|
+
this.usertextarea.focus();
|
|
132
|
+
this.usertextarea.setSelectionRange(selectionStart, selectionStart + text.length);
|
|
133
|
+
|
|
134
|
+
// Scroll the textarea to the inserted text
|
|
135
|
+
this.scrollToView();
|
|
136
|
+
|
|
137
|
+
this.render();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
scrollToView() {
|
|
141
|
+
const textarea = this.usertextarea;
|
|
142
|
+
|
|
143
|
+
// Calculate the position of the inserted text
|
|
144
|
+
const selectionStart = textarea.selectionStart;
|
|
145
|
+
|
|
146
|
+
// Get the line height (height of each row of text)
|
|
147
|
+
const lineHeight = parseInt(window.getComputedStyle(textarea).lineHeight);
|
|
148
|
+
|
|
149
|
+
// Get how many rows fit into the visible area of the textarea
|
|
150
|
+
const rowsInView = Math.floor(textarea.clientHeight / lineHeight);
|
|
151
|
+
|
|
152
|
+
// Calculate the current line number of the selectionStart
|
|
153
|
+
const currentLine = Math.floor(selectionStart / textarea.cols);
|
|
154
|
+
|
|
155
|
+
// Scroll to the line number that places the inserted text in the center
|
|
156
|
+
const targetScrollTop = (currentLine - Math.floor(rowsInView / 2)) * lineHeight;
|
|
157
|
+
|
|
158
|
+
// Adjust scrollTop to center the cursor's line in the view
|
|
159
|
+
textarea.scrollTop = targetScrollTop;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
render() {
|
|
164
|
+
const html = marked(this.usertextarea.value);
|
|
165
|
+
this.previewContent.innerHTML = html;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
142
169
|
export default MarkdownEditor;
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
// markdown\preview.js
|
|
2
|
-
class Preview {
|
|
3
|
-
constructor(container) {
|
|
4
|
-
this.previewContainer = document.createElement('div');
|
|
5
|
-
this.previewContainer.className = 'preview-wrapper bg-white dark:bg-stone-800 p-2 hidden';
|
|
6
|
-
this.previewContent = document.createElement('div');
|
|
7
|
-
this.previewContent.className = 'preview-content prose prose-sm md:prose-base dark:prose-invert p-1.5 overflow-y-auto h-[90lvh] max-w-max';
|
|
8
|
-
this.previewContainer.appendChild(this.previewContent);
|
|
9
|
-
container.appendChild(this.previewContainer);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
getPreviewContent() {
|
|
13
|
-
return this.previewContent;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
1
|
+
// markdown\preview.js
|
|
2
|
+
class Preview {
|
|
3
|
+
constructor(container) {
|
|
4
|
+
this.previewContainer = document.createElement('div');
|
|
5
|
+
this.previewContainer.className = 'preview-wrapper bg-white dark:bg-stone-800 p-2 hidden';
|
|
6
|
+
this.previewContent = document.createElement('div');
|
|
7
|
+
this.previewContent.className = 'preview-content prose prose-sm md:prose-base dark:prose-invert p-1.5 overflow-y-auto h-[90lvh] max-w-max';
|
|
8
|
+
this.previewContainer.appendChild(this.previewContent);
|
|
9
|
+
container.appendChild(this.previewContainer);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getPreviewContent() {
|
|
13
|
+
return this.previewContent;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
17
|
export default Preview;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
@tailwind base;
|
|
2
|
-
@tailwind components;
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
3
|
@tailwind utilities;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// utils/markdownUtils.js
|
|
2
|
-
|
|
3
|
-
import { marked } from 'marked';
|
|
4
|
-
|
|
5
|
-
export function renderMarkdown(content) {
|
|
6
|
-
return marked(content);
|
|
7
|
-
}
|
|
1
|
+
// utils/markdownUtils.js
|
|
2
|
+
|
|
3
|
+
import { marked } from 'marked';
|
|
4
|
+
|
|
5
|
+
export function renderMarkdown(content) {
|
|
6
|
+
return marked(content);
|
|
7
|
+
}
|