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.
Files changed (3) hide show
  1. package/index.html +50 -14
  2. package/package.json +1 -1
  3. 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 - 4rem);
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
- # cm-md-editor
24
+ # Heading 1
25
25
 
26
- This is a minimal **markdown editor** which is used in chessmail.de.
26
+ ## Heading 2
27
27
 
28
- ## Key features
28
+ ### Heading 3
29
29
 
30
- - Vanilla JavaScript module, without dependencies
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
- ☝️ [Check out the Demo page](https://shaack.com/projekte/cm-md-editor/)</textarea>
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
+ ![An example image](https://example.com/image.png)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cm-md-editor",
3
- "version": "1.0.4",
3
+ "version": "2.1.0",
4
4
  "description": "a simple markdown editor",
5
5
  "main": "index.js",
6
6
  "scripts": {
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 ![alt](url) 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(/(&lt;)(\/?[a-zA-Z]\w*)(.*?)(&gt;)/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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
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
- if (selected) {
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
- if (selected) {
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 matchEmpty = currentLine.match(/^(\s*- )$/)
403
+ const matchEmptyUl = currentLine.match(/^(\s*- )$/)
404
+ const matchEmptyOl = currentLine.match(/^(\s*)\d+\. $/)
66
405
  const matchHyphen = currentLine.match(/^(\s*- )/)
67
- if (matchEmpty) {
68
- const pre = matchEmpty[1]
69
- this.element.selectionStart = this.element.selectionEnd - pre.length - 1
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
- const pre = matchHyphen[1]
73
- this.insertTextAtCursor('\n' + pre)
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)