cm-md-editor 1.0.4 → 2.1.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/index.html +50 -14
- package/package.json +1 -1
- package/src/MdEditor.js +363 -17
package/index.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<style>
|
|
10
10
|
#editor {
|
|
11
11
|
width: 100%;
|
|
12
|
-
height: calc(100vh -
|
|
12
|
+
height: calc(100vh - 6rem);
|
|
13
13
|
padding: 0.7rem;
|
|
14
14
|
font-family: monospace;
|
|
15
15
|
tab-size: 4;
|
|
@@ -21,23 +21,59 @@
|
|
|
21
21
|
<div class="container-fluid">
|
|
22
22
|
<label for="editor">cm-md-editor</label>
|
|
23
23
|
<textarea id="editor">
|
|
24
|
-
#
|
|
24
|
+
# Heading 1
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
## Heading 2
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
### Heading 3
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
- Supports outlines with tab and shift-tab to indent and outdent
|
|
32
|
-
- Supports the **bold** syntax with command-b/ctrl-b
|
|
33
|
-
- Supports the _italic_ syntax with command-i/ctrl-i
|
|
34
|
-
- Supports undo and redo with command-z/ctrl-z and command-shift-z/ctrl-shift-z
|
|
35
|
-
- It is…
|
|
36
|
-
- lightweight
|
|
37
|
-
- easy to use
|
|
38
|
-
- fast
|
|
30
|
+
This is a minimal **markdown editor** with _italic_ and **_bold italic_** text. You can also use ~~strikethrough~~ for deleted text.
|
|
39
31
|
|
|
40
|
-
|
|
32
|
+
Inline `code spans` are highlighted and protect their content: `**not bold**`.
|
|
33
|
+
|
|
34
|
+
> Blockquotes are highlighted with a green prefix.
|
|
35
|
+
|
|
36
|
+
### Links and images
|
|
37
|
+
|
|
38
|
+
Visit [the demo page](https://shaack.com/projekte/cm-md-editor/) or use a [reference link][ref].
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
[ref]: https://shaack.com
|
|
43
|
+
|
|
44
|
+
### Lists
|
|
45
|
+
|
|
46
|
+
- Unordered list item
|
|
47
|
+
- Another item
|
|
48
|
+
- Nested item
|
|
49
|
+
- Deeper nesting
|
|
50
|
+
|
|
51
|
+
1. Ordered list item
|
|
52
|
+
2. Second item
|
|
53
|
+
|
|
54
|
+
### Task lists
|
|
55
|
+
|
|
56
|
+
- [ ] Unchecked task
|
|
57
|
+
- [x] Completed task
|
|
58
|
+
|
|
59
|
+
### Code block
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
const editor = new MdEditor(element)
|
|
63
|
+
console.log("syntax highlighted")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Horizontal rules
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### Escape sequences
|
|
71
|
+
|
|
72
|
+
Literal stars: \*not bold\* and backslash: \\
|
|
73
|
+
|
|
74
|
+
### HTML tags
|
|
75
|
+
|
|
76
|
+
This has <mark>inline HTML</mark> and a <br> tag.</textarea>
|
|
41
77
|
</div>
|
|
42
78
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
|
|
43
79
|
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
|
package/package.json
CHANGED
package/src/MdEditor.js
CHANGED
|
@@ -7,6 +7,352 @@ export class MdEditor {
|
|
|
7
7
|
constructor(element) {
|
|
8
8
|
this.element = element
|
|
9
9
|
this.element.addEventListener('keydown', (e) => this.handleKeyDown(e))
|
|
10
|
+
this.createToolbar()
|
|
11
|
+
this.createHighlightBackdrop()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
createToolbar() {
|
|
15
|
+
const wrapper = document.createElement('div')
|
|
16
|
+
this.element.parentNode.insertBefore(wrapper, this.element)
|
|
17
|
+
wrapper.appendChild(this.element)
|
|
18
|
+
const toolbar = document.createElement('div')
|
|
19
|
+
toolbar.style.cssText = 'display:flex;gap:1px;padding:2px;flex-wrap:wrap;background:rgba(128,128,128,0.15);border:1px solid rgba(128,128,128,0.3);border-bottom:none;border-radius:4px 4px 0 0;box-sizing:border-box;width:100%;'
|
|
20
|
+
wrapper.insertBefore(toolbar, this.element)
|
|
21
|
+
this.element.style.borderRadius = '0 0 4px 4px'
|
|
22
|
+
const buttons = [
|
|
23
|
+
{title: 'Heading 1', icon: '<text x="12" y="17.5" font-size="16" font-family="system-ui,sans-serif" font-weight="700" text-anchor="middle">H1</text>', action: () => this.toggleHeading(1)},
|
|
24
|
+
{title: 'Heading 2', icon: '<text x="12" y="17.5" font-size="16" font-family="system-ui,sans-serif" font-weight="700" text-anchor="middle">H2</text>', action: () => this.toggleHeading(2)},
|
|
25
|
+
{title: 'Heading 3', icon: '<text x="12" y="17.5" font-size="16" font-family="system-ui,sans-serif" font-weight="700" text-anchor="middle">H3</text>', action: () => this.toggleHeading(3)},
|
|
26
|
+
{title: 'Bold', icon: '<text x="12" y="18" font-size="18" font-family="system-ui,sans-serif" font-weight="800" text-anchor="middle">B</text>', action: () => this.toggleBold()},
|
|
27
|
+
{title: 'Italic', icon: '<text x="12" y="18" font-size="18" font-family="system-ui,sans-serif" font-weight="600" font-style="italic" text-anchor="middle">I</text>', action: () => this.toggleItalic()},
|
|
28
|
+
{title: 'Unordered List', icon: '<circle cx="5" cy="7" r="1.8"/><circle cx="5" cy="17" r="1.8"/><rect x="9.5" y="5.5" width="11" height="3" rx="1"/><rect x="9.5" y="15.5" width="11" height="3" rx="1"/>', action: () => this.insertUnorderedList()},
|
|
29
|
+
{title: 'Ordered List', icon: '<text x="3" y="9.5" font-size="8.5" font-family="system-ui,sans-serif" font-weight="700">1</text><text x="3" y="19.5" font-size="8.5" font-family="system-ui,sans-serif" font-weight="700">2</text><rect x="9.5" y="5.5" width="11" height="3" rx="1"/><rect x="9.5" y="15.5" width="11" height="3" rx="1"/>', action: () => this.insertOrderedList()},
|
|
30
|
+
]
|
|
31
|
+
buttons.forEach(btn => {
|
|
32
|
+
const button = document.createElement('button')
|
|
33
|
+
button.type = 'button'
|
|
34
|
+
button.title = btn.title
|
|
35
|
+
button.style.cssText = 'background:none;border:none;border-radius:3px;cursor:pointer;padding:4px 6px;display:flex;align-items:center;justify-content:center;color:inherit;opacity:0.6;transition:opacity 0.15s,background 0.15s;'
|
|
36
|
+
button.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="currentColor">${btn.icon}</svg>`
|
|
37
|
+
button.addEventListener('mouseenter', () => { button.style.opacity = '1'; button.style.background = 'rgba(128,128,128,0.2)' })
|
|
38
|
+
button.addEventListener('mouseleave', () => { button.style.opacity = '0.6'; button.style.background = 'none' })
|
|
39
|
+
button.addEventListener('mousedown', (e) => {
|
|
40
|
+
e.preventDefault()
|
|
41
|
+
})
|
|
42
|
+
button.addEventListener('click', (e) => {
|
|
43
|
+
e.preventDefault()
|
|
44
|
+
btn.action()
|
|
45
|
+
})
|
|
46
|
+
toolbar.appendChild(button)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Spacer to push wrap toggle to the right
|
|
50
|
+
const spacer = document.createElement('div')
|
|
51
|
+
spacer.style.cssText = 'flex:1;'
|
|
52
|
+
toolbar.appendChild(spacer)
|
|
53
|
+
|
|
54
|
+
// Wrap toggle button
|
|
55
|
+
this.wrapEnabled = true
|
|
56
|
+
this.wrapButton = document.createElement('button')
|
|
57
|
+
this.wrapButton.type = 'button'
|
|
58
|
+
this.wrapButton.title = 'Toggle word wrap'
|
|
59
|
+
this.wrapButton.style.cssText = 'background:none;border:none;border-radius:3px;cursor:pointer;padding:4px 6px;display:flex;align-items:center;justify-content:center;color:inherit;opacity:0.6;transition:opacity 0.15s,background 0.15s;'
|
|
60
|
+
this.wrapButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M4 6h16v2H4zm0 10h6v2H4zm0-5h16c0 0 0 0 0 0v0c0 2.2-1.8 4-4 4h-2l2-2h-1l-3 3 3 3h1l-2-2h2c3.3 0 6-2.7 6-6H4z"/></svg>`
|
|
61
|
+
this.wrapButton.addEventListener('mouseenter', () => { this.wrapButton.style.opacity = '1'; this.wrapButton.style.background = 'rgba(128,128,128,0.2)' })
|
|
62
|
+
this.wrapButton.addEventListener('mouseleave', () => { this.wrapButton.style.opacity = this.wrapEnabled ? '0.9' : '0.4'; this.wrapButton.style.background = 'none' })
|
|
63
|
+
this.wrapButton.addEventListener('mousedown', (e) => e.preventDefault())
|
|
64
|
+
this.wrapButton.addEventListener('click', (e) => {
|
|
65
|
+
e.preventDefault()
|
|
66
|
+
this.toggleWrapMode()
|
|
67
|
+
})
|
|
68
|
+
this.wrapButton.style.opacity = '0.9'
|
|
69
|
+
toolbar.appendChild(this.wrapButton)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
createHighlightBackdrop() {
|
|
73
|
+
const container = this.element.parentNode
|
|
74
|
+
container.style.position = 'relative'
|
|
75
|
+
this.backdrop = document.createElement('div')
|
|
76
|
+
this.highlightLayer = document.createElement('div')
|
|
77
|
+
this.backdrop.appendChild(this.highlightLayer)
|
|
78
|
+
container.appendChild(this.backdrop)
|
|
79
|
+
|
|
80
|
+
// Copy textarea computed styles to backdrop
|
|
81
|
+
const cs = window.getComputedStyle(this.element)
|
|
82
|
+
this.backdrop.style.cssText = `position:absolute;overflow:hidden;pointer-events:none;z-index:1;`
|
|
83
|
+
this.highlightLayer.style.cssText = `white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;color:transparent;`
|
|
84
|
+
|
|
85
|
+
const syncStyles = () => {
|
|
86
|
+
const cs = window.getComputedStyle(this.element)
|
|
87
|
+
const props = ['font-family', 'font-size', 'font-weight', 'line-height', 'letter-spacing',
|
|
88
|
+
'tab-size', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left']
|
|
89
|
+
props.forEach(p => this.highlightLayer.style[p.replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = cs.getPropertyValue(p))
|
|
90
|
+
this.highlightLayer.style.boxSizing = 'border-box'
|
|
91
|
+
// Use clientWidth to match the textarea's content area (excludes scrollbar)
|
|
92
|
+
this.highlightLayer.style.width = this.element.clientWidth + 'px'
|
|
93
|
+
const borderTop = parseInt(cs.getPropertyValue('border-top-width')) || 0
|
|
94
|
+
const borderLeft = parseInt(cs.getPropertyValue('border-left-width')) || 0
|
|
95
|
+
this.backdrop.style.top = (this.element.offsetTop + borderTop) + 'px'
|
|
96
|
+
this.backdrop.style.left = (this.element.offsetLeft + borderLeft) + 'px'
|
|
97
|
+
this.backdrop.style.width = this.element.clientWidth + 'px'
|
|
98
|
+
this.backdrop.style.height = this.element.clientHeight + 'px'
|
|
99
|
+
}
|
|
100
|
+
syncStyles()
|
|
101
|
+
|
|
102
|
+
// Make textarea background transparent so backdrop shows through
|
|
103
|
+
this.element.style.overscrollBehavior = 'none'
|
|
104
|
+
this.element.style.background = 'transparent'
|
|
105
|
+
this.element.style.position = 'relative'
|
|
106
|
+
this.element.style.caretColor = cs.color
|
|
107
|
+
|
|
108
|
+
// Sync scroll
|
|
109
|
+
this.element.addEventListener('scroll', () => {
|
|
110
|
+
this.backdrop.scrollTop = this.element.scrollTop
|
|
111
|
+
this.backdrop.scrollLeft = this.element.scrollLeft
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Update on input
|
|
115
|
+
this.element.addEventListener('input', () => this.updateHighlight())
|
|
116
|
+
new ResizeObserver(() => { syncStyles(); this.updateHighlight() }).observe(this.element)
|
|
117
|
+
this.updateHighlight()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
updateHighlight() {
|
|
121
|
+
const text = this.element.value
|
|
122
|
+
const lines = text.split('\n')
|
|
123
|
+
let html = ''
|
|
124
|
+
let inCodeBlock = false
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < lines.length; i++) {
|
|
127
|
+
if (i > 0) html += '\n'
|
|
128
|
+
const line = lines[i]
|
|
129
|
+
|
|
130
|
+
// Fenced code block delimiter
|
|
131
|
+
if (/^`{3,}/.test(line)) {
|
|
132
|
+
inCodeBlock = !inCodeBlock
|
|
133
|
+
html += '<span style="color:rgba(130,170,200,0.7)">' + this.escapeHtml(line) + '</span>'
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
if (inCodeBlock) {
|
|
137
|
+
html += '<span style="color:rgba(130,170,200,0.5)">' + this.escapeHtml(line) + '</span>'
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Horizontal rule (3+ of same -, *, or _ with optional spaces)
|
|
142
|
+
if (/^\s{0,3}([-*_])\s*(\1\s*){2,}$/.test(line)) {
|
|
143
|
+
html += '<span style="color:rgba(128,128,128,0.6)">' + this.escapeHtml(line) + '</span>'
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Headings
|
|
148
|
+
const headingMatch = line.match(/^(#{1,6}) /)
|
|
149
|
+
if (headingMatch) {
|
|
150
|
+
const opacity = Math.max(0.3, 0.8 - (headingMatch[1].length - 1) * 0.1)
|
|
151
|
+
html += '<span style="color:rgba(100,160,255,' + opacity + ')">' + this.escapeHtml(line) + '</span>'
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Reference link definition [ref]: url
|
|
156
|
+
const refMatch = line.match(/^(\s{0,3}\[)([^\]]+)(\]:\s+)(.+)$/)
|
|
157
|
+
if (refMatch) {
|
|
158
|
+
html += '<span style="color:rgba(100,180,220,0.5)">' + this.escapeHtml(refMatch[1]) + '</span>'
|
|
159
|
+
+ '<span style="color:rgba(100,180,220,0.8)">' + this.escapeHtml(refMatch[2]) + '</span>'
|
|
160
|
+
+ '<span style="color:rgba(100,180,220,0.5)">' + this.escapeHtml(refMatch[3]) + '</span>'
|
|
161
|
+
+ '<span style="color:rgba(100,180,220,0.6)">' + this.escapeHtml(refMatch[4]) + '</span>'
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Blockquote prefix
|
|
166
|
+
let prefix = ''
|
|
167
|
+
let rest = line
|
|
168
|
+
const bqMatch = line.match(/^(\s*>+\s?)/)
|
|
169
|
+
if (bqMatch) {
|
|
170
|
+
prefix = '<span style="color:rgba(128,180,128,0.7)">' + this.escapeHtml(bqMatch[0]) + '</span>'
|
|
171
|
+
rest = line.substring(bqMatch[0].length)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
html += prefix + this.highlightInline(rest)
|
|
175
|
+
}
|
|
176
|
+
// Trailing newline so the backdrop height matches the textarea
|
|
177
|
+
this.highlightLayer.innerHTML = html + '\n'
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
highlightInline(line) {
|
|
181
|
+
// Split by inline code to protect code content from other highlighting
|
|
182
|
+
const segments = []
|
|
183
|
+
let lastIndex = 0
|
|
184
|
+
const codeRegex = /`([^`]+)`/g
|
|
185
|
+
let match
|
|
186
|
+
|
|
187
|
+
while ((match = codeRegex.exec(line)) !== null) {
|
|
188
|
+
if (match.index > lastIndex) {
|
|
189
|
+
segments.push({type: 'text', content: line.substring(lastIndex, match.index)})
|
|
190
|
+
}
|
|
191
|
+
segments.push({type: 'code', content: match[0]})
|
|
192
|
+
lastIndex = codeRegex.lastIndex
|
|
193
|
+
}
|
|
194
|
+
if (lastIndex < line.length) {
|
|
195
|
+
segments.push({type: 'text', content: line.substring(lastIndex)})
|
|
196
|
+
}
|
|
197
|
+
if (segments.length === 0) {
|
|
198
|
+
segments.push({type: 'text', content: ''})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let result = ''
|
|
202
|
+
for (const seg of segments) {
|
|
203
|
+
if (seg.type === 'code') {
|
|
204
|
+
result += '<span style="color:rgba(130,170,200,0.7)">' + this.escapeHtml(seg.content) + '</span>'
|
|
205
|
+
} else {
|
|
206
|
+
result += this.highlightTextSegment(this.escapeHtml(seg.content))
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return result
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
highlightTextSegment(escaped) {
|
|
213
|
+
let result = escaped
|
|
214
|
+
|
|
215
|
+
// Escape sequences: dim the backslash before markdown punctuation
|
|
216
|
+
result = result.replace(/\\([\\`*_{}[\]()#+\-.!~|])/g,
|
|
217
|
+
'<span style="color:rgba(128,128,128,0.4)">\\</span>$1')
|
|
218
|
+
|
|
219
|
+
// Unordered list markers with optional task list checkbox
|
|
220
|
+
result = result.replace(/^(\t*)(- )(\[[ xX]\] )?/, (_, tabs, marker, task) => {
|
|
221
|
+
let r = tabs + '<span style="color:rgba(100,200,150,0.7)">' + marker + '</span>'
|
|
222
|
+
if (task) {
|
|
223
|
+
r += '<span style="color:rgba(100,200,150,0.7)">' + task + '</span>'
|
|
224
|
+
}
|
|
225
|
+
return r
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
// Ordered list markers
|
|
229
|
+
result = result.replace(/^(\t*)(\d+\. )/, (_, tabs, marker) =>
|
|
230
|
+
tabs + '<span style="color:rgba(100,200,150,0.7)">' + marker + '</span>')
|
|
231
|
+
|
|
232
|
+
// Images  and Links [text](url)
|
|
233
|
+
result = result.replace(/(!?\[)(.*?)(\]\()(.+?)(\))/g,
|
|
234
|
+
'<span style="color:rgba(100,180,220,0.5)">$1</span><span style="color:rgba(100,180,220,0.8)">$2</span><span style="color:rgba(100,180,220,0.5)">$3</span><span style="color:rgba(100,180,220,0.6)">$4</span><span style="color:rgba(100,180,220,0.5)">$5</span>')
|
|
235
|
+
|
|
236
|
+
// Reference links [text][ref]
|
|
237
|
+
result = result.replace(/(\[)(.*?)(\]\[)(.*?)(\])/g,
|
|
238
|
+
'<span style="color:rgba(100,180,220,0.5)">$1</span><span style="color:rgba(100,180,220,0.8)">$2</span><span style="color:rgba(100,180,220,0.5)">$3</span><span style="color:rgba(100,180,220,0.6)">$4</span><span style="color:rgba(100,180,220,0.5)">$5</span>')
|
|
239
|
+
|
|
240
|
+
// Strikethrough ~~text~~
|
|
241
|
+
result = result.replace(/(~~)(.*?)(~~)/g,
|
|
242
|
+
'<span style="color:rgba(255,100,100,0.5)">$1</span><span style="color:rgba(255,100,100,0.7)">$2</span><span style="color:rgba(255,100,100,0.5)">$3</span>')
|
|
243
|
+
|
|
244
|
+
// Bold **text**
|
|
245
|
+
result = result.replace(/(\*\*)(.*?)(\*\*)/g,
|
|
246
|
+
'<span style="color:rgba(255,180,80,0.5)">$1</span><span style="color:rgba(255,180,80,0.8)">$2</span><span style="color:rgba(255,180,80,0.5)">$3</span>')
|
|
247
|
+
|
|
248
|
+
// Italic _text_
|
|
249
|
+
result = result.replace(/((?:^|[^\\]))(\_)(.*?[^\\])(\_)/g,
|
|
250
|
+
'$1<span style="color:rgba(180,130,255,0.5)">$2</span><span style="color:rgba(180,130,255,0.8)">$3</span><span style="color:rgba(180,130,255,0.5)">$4</span>')
|
|
251
|
+
|
|
252
|
+
// HTML tags
|
|
253
|
+
result = result.replace(/(<)(\/?[a-zA-Z]\w*)(.*?)(>)/g,
|
|
254
|
+
'<span style="color:rgba(200,120,120,0.5)">$1$2$3$4</span>')
|
|
255
|
+
|
|
256
|
+
return result
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
toggleWrapMode() {
|
|
260
|
+
this.wrapEnabled = !this.wrapEnabled
|
|
261
|
+
const wrap = this.wrapEnabled
|
|
262
|
+
this.element.style.whiteSpace = wrap ? 'pre-wrap' : 'pre'
|
|
263
|
+
this.element.style.overflowX = wrap ? 'hidden' : 'auto'
|
|
264
|
+
this.highlightLayer.style.whiteSpace = wrap ? 'pre-wrap' : 'pre'
|
|
265
|
+
this.highlightLayer.style.overflowWrap = wrap ? 'break-word' : 'normal'
|
|
266
|
+
this.wrapButton.style.opacity = wrap ? '0.9' : '0.4'
|
|
267
|
+
this.updateHighlight()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
escapeHtml(str) {
|
|
271
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
getCurrentLineInfo() {
|
|
275
|
+
const start = this.element.selectionStart
|
|
276
|
+
const text = this.element.value
|
|
277
|
+
const lineStart = text.lastIndexOf('\n', start - 1) + 1
|
|
278
|
+
let lineEnd = text.indexOf('\n', start)
|
|
279
|
+
if (lineEnd === -1) lineEnd = text.length
|
|
280
|
+
const line = text.substring(lineStart, lineEnd)
|
|
281
|
+
return {lineStart, lineEnd, line}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
selectLineRange(lineStart, lineEnd) {
|
|
285
|
+
this.element.selectionStart = lineStart
|
|
286
|
+
this.element.selectionEnd = lineEnd
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
toggleHeading(level) {
|
|
290
|
+
const {lineStart, lineEnd, line} = this.getCurrentLineInfo()
|
|
291
|
+
const prefix = '#'.repeat(level) + ' '
|
|
292
|
+
const headingMatch = line.match(/^(#{1,6}) /)
|
|
293
|
+
this.selectLineRange(lineStart, lineEnd)
|
|
294
|
+
if (headingMatch && headingMatch[1].length === level) {
|
|
295
|
+
this.insertTextAtCursor(line.substring(prefix.length))
|
|
296
|
+
} else if (headingMatch) {
|
|
297
|
+
this.insertTextAtCursor(prefix + line.substring(headingMatch[0].length))
|
|
298
|
+
} else {
|
|
299
|
+
this.insertTextAtCursor(prefix + line)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
toggleWrap(marker) {
|
|
304
|
+
const start = this.element.selectionStart
|
|
305
|
+
const end = this.element.selectionEnd
|
|
306
|
+
const text = this.element.value
|
|
307
|
+
const len = marker.length
|
|
308
|
+
const before = text.substring(start - len, start)
|
|
309
|
+
const after = text.substring(end, end + len)
|
|
310
|
+
if (before === marker && after === marker) {
|
|
311
|
+
// Remove markers, keep selection on the inner text
|
|
312
|
+
this.element.selectionStart = start - len
|
|
313
|
+
this.element.selectionEnd = end + len
|
|
314
|
+
const selected = text.substring(start, end)
|
|
315
|
+
this.insertTextAtCursor(selected)
|
|
316
|
+
this.element.selectionStart = start - len
|
|
317
|
+
this.element.selectionEnd = end - len
|
|
318
|
+
} else if (start !== end) {
|
|
319
|
+
// Wrap selection, keep selection on the inner text
|
|
320
|
+
this.insertTextAtCursor(marker + text.substring(start, end) + marker)
|
|
321
|
+
this.element.selectionStart = start + len
|
|
322
|
+
this.element.selectionEnd = end + len
|
|
323
|
+
} else {
|
|
324
|
+
this.insertTextAtCursor(marker + marker)
|
|
325
|
+
this.element.selectionStart = this.element.selectionEnd = start + len
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
toggleBold() {
|
|
330
|
+
this.toggleWrap('**')
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
toggleItalic() {
|
|
334
|
+
this.toggleWrap('_')
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
insertUnorderedList() {
|
|
338
|
+
const {lineStart, lineEnd, line} = this.getCurrentLineInfo()
|
|
339
|
+
this.selectLineRange(lineStart, lineEnd)
|
|
340
|
+
if (line.startsWith('- ')) {
|
|
341
|
+
this.insertTextAtCursor(line.substring(2))
|
|
342
|
+
} else {
|
|
343
|
+
this.insertTextAtCursor('- ' + line)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
insertOrderedList() {
|
|
348
|
+
const {lineStart, lineEnd, line} = this.getCurrentLineInfo()
|
|
349
|
+
this.selectLineRange(lineStart, lineEnd)
|
|
350
|
+
const olMatch = line.match(/^\d+\. /)
|
|
351
|
+
if (olMatch) {
|
|
352
|
+
this.insertTextAtCursor(line.substring(olMatch[0].length))
|
|
353
|
+
} else {
|
|
354
|
+
this.insertTextAtCursor('1. ' + line)
|
|
355
|
+
}
|
|
10
356
|
}
|
|
11
357
|
|
|
12
358
|
insertTextAtCursor(text) {
|
|
@@ -20,7 +366,7 @@ export class MdEditor {
|
|
|
20
366
|
const before = this.element.value.substring(0, start)
|
|
21
367
|
const selected = this.element.value.substring(start, end)
|
|
22
368
|
const currentLine = before.substring(before.lastIndexOf('\n') + 1)
|
|
23
|
-
const isListMode = currentLine.match(/^\t*- /)
|
|
369
|
+
const isListMode = currentLine.match(/^\t*- /) || currentLine.match(/^\t*\d+\. /)
|
|
24
370
|
if (e.key === 'Tab') {
|
|
25
371
|
e.preventDefault()
|
|
26
372
|
if (isListMode) {
|
|
@@ -37,18 +383,10 @@ export class MdEditor {
|
|
|
37
383
|
} else if (e.ctrlKey || e.metaKey) {
|
|
38
384
|
if (e.key === 'b') { // bold
|
|
39
385
|
e.preventDefault()
|
|
40
|
-
|
|
41
|
-
this.insertTextAtCursor('**' + selected + '**')
|
|
42
|
-
} else {
|
|
43
|
-
this.insertTextAtCursor('**')
|
|
44
|
-
}
|
|
386
|
+
this.toggleBold()
|
|
45
387
|
} else if (e.key === 'i') { // italic
|
|
46
388
|
e.preventDefault()
|
|
47
|
-
|
|
48
|
-
this.insertTextAtCursor('_' + selected + '_')
|
|
49
|
-
} else {
|
|
50
|
-
this.insertTextAtCursor('_')
|
|
51
|
-
}
|
|
389
|
+
this.toggleItalic()
|
|
52
390
|
} else if (e.key === 'e') { // game todo this could be an extension
|
|
53
391
|
e.preventDefault()
|
|
54
392
|
this.insertTextAtCursor('[game id="' + selected + '"]')
|
|
@@ -62,15 +400,21 @@ export class MdEditor {
|
|
|
62
400
|
const start = this.element.selectionStart
|
|
63
401
|
const before = this.element.value.substring(0, start)
|
|
64
402
|
const currentLine = before.substring(before.lastIndexOf('\n') + 1)
|
|
65
|
-
const
|
|
403
|
+
const matchEmptyUl = currentLine.match(/^(\s*- )$/)
|
|
404
|
+
const matchEmptyOl = currentLine.match(/^(\s*)\d+\. $/)
|
|
66
405
|
const matchHyphen = currentLine.match(/^(\s*- )/)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.element.selectionStart = this.element.selectionEnd -
|
|
406
|
+
const matchOl = currentLine.match(/^(\s*)(\d+)\. ./)
|
|
407
|
+
if (matchEmptyUl) {
|
|
408
|
+
this.element.selectionStart = this.element.selectionEnd - matchEmptyUl[1].length - 1
|
|
409
|
+
} else if (matchEmptyOl) {
|
|
410
|
+
this.element.selectionStart = this.element.selectionEnd - matchEmptyOl[0].length - 1
|
|
70
411
|
} else if (matchHyphen) {
|
|
71
412
|
e.preventDefault()
|
|
72
|
-
|
|
73
|
-
|
|
413
|
+
this.insertTextAtCursor('\n' + matchHyphen[1])
|
|
414
|
+
} else if (matchOl) {
|
|
415
|
+
e.preventDefault()
|
|
416
|
+
const nextNum = parseInt(matchOl[2]) + 1
|
|
417
|
+
this.insertTextAtCursor('\n' + matchOl[1] + nextNum + '. ')
|
|
74
418
|
}
|
|
75
419
|
}
|
|
76
420
|
|
|
@@ -87,6 +431,8 @@ export class MdEditor {
|
|
|
87
431
|
this.element.selectionStart = this.element.selectionEnd = start + 1
|
|
88
432
|
}
|
|
89
433
|
|
|
434
|
+
// test
|
|
435
|
+
|
|
90
436
|
removeTab() {
|
|
91
437
|
const start = this.element.selectionStart
|
|
92
438
|
const before = this.element.value.substring(0, start)
|