diff options
author | Hsieh Chin Fan <pham@topo.tw> | 2024-09-10 19:46:20 +0800 |
---|---|---|
committer | Hsieh Chin Fan <pham@topo.tw> | 2024-09-11 14:10:19 +0800 |
commit | aded35c97a57eeb5eabff8d9a6853b01bbfa197e (patch) | |
tree | 8481990e83404dd8aa83026f061c7640621167ff /src/editor.mjs | |
parent | 7ba3a70473b0a1748ca4d1dbb657fd43683094eb (diff) |
refactor: Switch to EasyMDE
EasyMDE is based on codemirror, so completion and more features maybe
applied later.
* Now suggestions also works in EasyMDE
* For preview, change return value of markdown2HTML()
Diffstat (limited to 'src/editor.mjs')
-rw-r--r-- | src/editor.mjs | 301 |
1 files changed, 144 insertions, 157 deletions
diff --git a/src/editor.mjs b/src/editor.mjs index dd18a31..b283a90 100644 --- a/src/editor.mjs +++ b/src/editor.mjs | |||
@@ -1,12 +1,22 @@ | |||
1 | import TinyMDE from 'tiny-markdown-editor' | ||
2 | import { markdown2HTML, generateMaps } from './dumbymap' | 1 | import { markdown2HTML, generateMaps } from './dumbymap' |
3 | import { defaultAliasesForRenderer, parseConfigsFromYaml } from 'mapclay' | 2 | import { defaultAliasesForRenderer, parseConfigsFromYaml } from 'mapclay' |
4 | 3 | ||
5 | // Set up Editor {{{ | 4 | // Set up Editor {{{ |
6 | 5 | ||
7 | const HtmlContainer = document.querySelector(".result-html") | 6 | const HtmlContainer = document.querySelector(".result-html") |
8 | const mdeElement = document.querySelector("#tinymde") | 7 | const textArea = document.querySelector(".editor textarea") |
9 | 8 | ||
9 | const toggleMaps = (container) => { | ||
10 | if (!container.querySelector('.Showcase')) { | ||
11 | generateMaps(container) | ||
12 | document.activeElement.blur(); | ||
13 | } else { | ||
14 | markdown2HTML(HtmlContainer, editor.value()) | ||
15 | container.setAttribute('data-layout', 'none') | ||
16 | } | ||
17 | } | ||
18 | |||
19 | // Content values for editor | ||
10 | const getContentFromHash = (cleanHash = false) => { | 20 | const getContentFromHash = (cleanHash = false) => { |
11 | const hashValue = location.hash.substring(1); | 21 | const hashValue = location.hash.substring(1); |
12 | if (cleanHash) window.location.hash = '' | 22 | if (cleanHash) window.location.hash = '' |
@@ -14,58 +24,60 @@ const getContentFromHash = (cleanHash = false) => { | |||
14 | ? decodeURIComponent(hashValue.substring(5)) | 24 | ? decodeURIComponent(hashValue.substring(5)) |
15 | : null | 25 | : null |
16 | } | 26 | } |
17 | |||
18 | // Add Editor | ||
19 | const contentFromHash = getContentFromHash(true) | 27 | const contentFromHash = getContentFromHash(true) |
20 | const lastContent = localStorage.getItem('editorContent') | 28 | const lastContent = localStorage.getItem('editorContent') |
21 | const defaultContent = '## Links\n\n- [Go to marker](geo:24,121?id=foo,leaflet&text=normal "Link Test")\n\n```map\nid: foo\nuse: Maplibre\n```\n' | 29 | const defaultContent = '## Links\n\n- [Go to marker](geo:24,121?id=foo,leaflet&text=normal "Link Test")\n\n```map\nid: foo\nuse: Maplibre\n```\n' |
22 | 30 | ||
23 | const tinyEditor = new TinyMDE.Editor({ | 31 | // Set up EasyMDE {{{ |
24 | element: 'tinymde', | 32 | const editor = new EasyMDE({ |
25 | content: contentFromHash ?? lastContent ?? defaultContent | 33 | element: textArea, |
34 | indentWithTabs: false, | ||
35 | initialValue: contentFromHash ?? lastContent ?? defaultContent, | ||
36 | lineNumbers: true, | ||
37 | promptURLs: true, | ||
38 | uploadImage: true, | ||
39 | spellChecker: false, | ||
40 | toolbarButtonClassPrefix: 'mde', | ||
41 | status: false, | ||
42 | shortcuts: { | ||
43 | "map": "Ctrl-Alt-M", | ||
44 | "debug": "Ctrl-Alt-D", | ||
45 | "toggleUnorderedList": "Ctrl-Shift-L", | ||
46 | }, | ||
47 | toolbar: [ | ||
48 | { | ||
49 | name: 'map', | ||
50 | title: 'Toggle Map Generation', | ||
51 | text: "🌏", | ||
52 | action: toggleMaps(HtmlContainer), | ||
53 | }, | ||
54 | { | ||
55 | name: 'debug', | ||
56 | title: 'Save content as URL', | ||
57 | text: "🤔", | ||
58 | action: () => { | ||
59 | window.location.hash = '#text=' + encodeURIComponent(editor.value()) | ||
60 | navigator.clipboard.writeText(window.location.href) | ||
61 | alert('URL copied to clipboard') | ||
62 | }, | ||
63 | }, 'undo', 'redo', '|', 'heading-1', 'heading-2', '|', 'link', 'image', '|', 'bold', 'italic', 'strikethrough', 'code', 'clean-block', '|', 'unordered-list', 'ordered-list', 'quote', 'table', '|', 'fullscreen' | ||
64 | ], | ||
26 | }); | 65 | }); |
27 | mdeElement.querySelectorAll('span').forEach(e => e.setAttribute('spellcheck', 'false')) | ||
28 | 66 | ||
67 | const cm = editor.codemirror | ||
68 | markdown2HTML(HtmlContainer, editor.value()) | ||
69 | |||
70 | cm.on("change", () => { | ||
71 | markdown2HTML(HtmlContainer, editor.value()) | ||
72 | }) | ||
73 | // }}} | ||
74 | |||
75 | // Reload editor content by hash value | ||
29 | onhashchange = () => { | 76 | onhashchange = () => { |
30 | const contentFromHash = getContentFromHash() | 77 | const contentFromHash = getContentFromHash() |
31 | if (contentFromHash) tinyEditor.setContent(contentFromHash) | 78 | if (contentFromHash) editor.value(contentFromHash) |
32 | } | 79 | } |
33 | 80 | ||
34 | // Add command bar for editor | ||
35 | // Use this command to render maps and geoLinks | ||
36 | const mapCommand = { | ||
37 | name: 'map', | ||
38 | title: 'Switch Map Generation', | ||
39 | innerHTML: `<div style="font-size: 16px; line-height: 1.1;">🌏</div>`, | ||
40 | action: () => { | ||
41 | if (!HtmlContainer.querySelector('.map-container')) { | ||
42 | generateMaps(HtmlContainer) | ||
43 | document.activeElement.blur(); | ||
44 | } else { | ||
45 | markdown2HTML(HtmlContainer, tinyEditor.getContent()) | ||
46 | HtmlContainer.setAttribute('data-layout', 'none') | ||
47 | } | ||
48 | }, | ||
49 | hotkey: 'Ctrl-m' | ||
50 | } | ||
51 | const debugCommand = { | ||
52 | name: 'debug', | ||
53 | title: 'show debug message', | ||
54 | innerHTML: `<div style="font-size: 16px; line-height: 1.1;">🤔</div>`, | ||
55 | action: () => { | ||
56 | window.location.hash = '#text=' + encodeURIComponent(tinyEditor.getContent()) | ||
57 | }, | ||
58 | hotkey: 'Ctrl-i' | ||
59 | } | ||
60 | // Set up command bar | ||
61 | new TinyMDE.CommandBar({ | ||
62 | element: 'tinymde_commandbar', editor: tinyEditor, | ||
63 | commands: [mapCommand, debugCommand, '|', 'h1', 'h2', '|', 'insertLink', 'insertImage', '|', 'bold', 'italic', 'strikethrough', 'code', '|', 'ul', 'ol', '|', 'blockquote'] | ||
64 | }); | ||
65 | |||
66 | // Render HTML to result container | ||
67 | markdown2HTML(HtmlContainer, tinyEditor.getContent()) | ||
68 | |||
69 | // FIXME DEBUGONLY | 81 | // FIXME DEBUGONLY |
70 | // generateMaps(HtmlContainer) | 82 | // generateMaps(HtmlContainer) |
71 | // setTimeout(() => { | 83 | // setTimeout(() => { |
@@ -73,34 +85,10 @@ markdown2HTML(HtmlContainer, tinyEditor.getContent()) | |||
73 | // }, 500) | 85 | // }, 500) |
74 | 86 | ||
75 | // }}} | 87 | // }}} |
76 | // Event Listener: change {{{ | ||
77 | |||
78 | // Save editor content to local storage, set timeout for 3 seconds | ||
79 | let cancelLastSave | ||
80 | const saveContent = (content) => { | ||
81 | new Promise((resolve, reject) => { | ||
82 | // If user is typing, the last change cancel previous ones | ||
83 | if (cancelLastSave) cancelLastSave(content.length) | ||
84 | cancelLastSave = reject | ||
85 | |||
86 | setTimeout(() => { | ||
87 | localStorage.setItem('editorContent', content) | ||
88 | resolve('Content Saved') | ||
89 | }, 3000) | ||
90 | }).catch(() => null) | ||
91 | } | ||
92 | |||
93 | // Render HTML to result container and save current content | ||
94 | tinyEditor.addEventListener('change', e => { | ||
95 | markdown2HTML(HtmlContainer, e.content) | ||
96 | saveContent(e.content) | ||
97 | }); | ||
98 | // }}} | ||
99 | // Completion in Code Blok {{{ | 88 | // Completion in Code Blok {{{ |
100 | // Elements about suggestions {{{ | 89 | // Elements about suggestions {{{ |
101 | const suggestionsEle = document.createElement('div') | 90 | const suggestionsEle = document.createElement('div') |
102 | suggestionsEle.classList.add('container__suggestions'); | 91 | suggestionsEle.classList.add('container__suggestions'); |
103 | mdeElement.appendChild(suggestionsEle) | ||
104 | 92 | ||
105 | const rendererOptions = {} | 93 | const rendererOptions = {} |
106 | 94 | ||
@@ -110,7 +98,6 @@ class Suggestion { | |||
110 | this.replace = replace | 98 | this.replace = replace |
111 | } | 99 | } |
112 | } | 100 | } |
113 | |||
114 | // }}} | 101 | // }}} |
115 | // {{{ Aliases for map options | 102 | // {{{ Aliases for map options |
116 | const aliasesForMapOptions = {} | 103 | const aliasesForMapOptions = {} |
@@ -124,50 +111,54 @@ fetch(defaultApply) | |||
124 | .catch(err => console.warn(`Fail to get aliases from ${defaultApply}`, err)) | 111 | .catch(err => console.warn(`Fail to get aliases from ${defaultApply}`, err)) |
125 | // }}} | 112 | // }}} |
126 | // FUNCTION: Check cursor is inside map code block {{{ | 113 | // FUNCTION: Check cursor is inside map code block {{{ |
127 | const insideCodeblockForMap = (element) => { | 114 | // const insideCodeblockForMap = (currentLine) => { |
128 | const code = element.closest('.TMFencedCodeBacktick') | 115 | // let tokens = cm.getLineTokens(currentLine) |
129 | if (!code) return false | 116 | // |
130 | 117 | // if (!tokens.includes("comment") || tokens.includes('formatting-code-block')) return false | |
131 | let ps = code.previousSibling | 118 | // |
132 | if (!ps) return false | 119 | // do { |
133 | 120 | // line = line - 1 | |
134 | // Look backward to find pattern of code block: /```map/ | 121 | // if (line < 0) return false |
135 | while (!ps.classList.contains('TMCodeFenceBacktickOpen')) { | 122 | // tokens = cm.getLineTokens(line) |
136 | ps = ps.previousSibling | 123 | // } while (!tokens.includes('formatting-code-block')) |
137 | if (!ps) return false | 124 | // |
138 | if (ps.classList.contains('TMCodeFenceBacktickClose')) return false | 125 | // return true |
139 | } | 126 | // } |
140 | 127 | // }}} | |
141 | return ps.querySelector('.TMInfoString')?.textContent === 'map' | 128 | // Check if current token is inside code block {{{ |
142 | } | 129 | const insideCodeblockForMap = (token) => |
130 | token.state.overlay.codeBlock && !token.string.match(/^````*/) | ||
143 | // }}} | 131 | // }}} |
144 | // FUNCTION: Get renderer by cursor position in code block {{{ | 132 | // FUNCTION: Get renderer by cursor position in code block {{{ |
145 | const getLineWithRenderer = (element) => { | 133 | const getLineWithRenderer = (anchor) => { |
146 | const currentLine = element.closest('.TMFencedCodeBacktick') | 134 | const currentLine = anchor.line |
147 | if (!currentLine) return null | 135 | const match = (line) => cm.getLine(line).match(/^use: /) |
136 | if (match(currentLine)) return currentLine | ||
137 | |||
138 | const getToken = (line) => cm.getTokenAt({ line: line, ch: 1 }) | ||
148 | 139 | ||
149 | // Look backward/forward for pattern of used renderer: /use: .+/ | 140 | // Look backward/forward for pattern of used renderer: /use: .+/ |
150 | let ps = currentLine | 141 | let ps = currentLine - 1 |
151 | do { | 142 | while (ps > 0 && insideCodeblockForMap(getToken(ps))) { |
152 | ps = ps.previousSibling | 143 | if (match(ps)) { |
153 | if (ps.textContent.match(/^use: /)) { | ||
154 | return ps | 144 | return ps |
155 | } else if (ps.textContent.match(/^---/)) { | 145 | } else if (cm.getLine(ps).match(/^---/)) { |
156 | // If yaml doc separator is found | 146 | // If yaml doc separator is found |
157 | break | 147 | break |
158 | } | 148 | } |
159 | } while (ps && ps.classList.contains('TMFencedCodeBacktick')) | 149 | ps = ps - 1 |
150 | } | ||
160 | 151 | ||
161 | let ns = currentLine | 152 | let ns = currentLine + 1 |
162 | do { | 153 | while (insideCodeblockForMap(getToken(ns))) { |
163 | ns = ns.nextSibling | 154 | if (match(ns)) { |
164 | if (ns.textContent.match(/^use: /)) { | ||
165 | return ns | 155 | return ns |
166 | } else if (ns.textContent.match(/^---/)) { | 156 | } else if (cm.getLine(ns).match(/^---/)) { |
167 | // If yaml doc separator is found | 157 | // If yaml doc separator is found |
168 | return null | 158 | return null |
169 | } | 159 | } |
170 | } while (ns && ns.classList.contains('TMFencedCodeBacktick')) | 160 | ns = ns + 1 |
161 | } | ||
171 | 162 | ||
172 | return null | 163 | return null |
173 | } | 164 | } |
@@ -218,29 +209,35 @@ const getSuggestionsFromAliases = (option) => Object.entries(aliasesForMapOption | |||
218 | }) | 209 | }) |
219 | ?? [] | 210 | ?? [] |
220 | // }}} | 211 | // }}} |
221 | const handleTypingInCodeBlock = (currentLine, selection) => { | 212 | // FUCNTION: Handler for map codeblock {{{ |
222 | const text = currentLine.textContent | 213 | const handleTypingInCodeBlock = (anchor) => { |
214 | const text = cm.getLine(anchor.line) | ||
223 | if (text.match(/^\s\+$/) && text.length % 2 !== 0) { | 215 | if (text.match(/^\s\+$/) && text.length % 2 !== 0) { |
224 | // TODO Completion for even number of spaces | 216 | // TODO Completion for even number of spaces |
225 | } else if (text.match(/^-/)){ | 217 | } else if (text.match(/^-/)) { |
226 | // TODO Completion for YAML doc separator | 218 | // TODO Completion for YAML doc separator |
227 | } else { | 219 | } else { |
228 | addSuggestions(currentLine, selection) | 220 | const suggestions = getSuggestions(anchor) |
221 | addSuggestions(anchor, suggestions) | ||
229 | } | 222 | } |
230 | } | 223 | } |
231 | // FUNCTION: Add HTML element for List of suggestions {{{ | 224 | // }}} |
232 | const addSuggestions = (currentLine, selection) => { | 225 | // FUNCTION: get suggestions by current input {{{ |
233 | const text = currentLine.textContent | 226 | const getSuggestions = (anchor) => { |
234 | const markInputIsInvalid = (ele) => (ele ?? currentLine).classList.add('invalid-input') | 227 | const text = cm.getLine(anchor.line) |
228 | const markInputIsInvalid = () => cm.getDoc().markText( | ||
229 | { ...anchor, ch: 0 }, | ||
230 | { ...anchor, ch: -1 }, | ||
231 | { className: 'invalid-input' }, | ||
232 | ) | ||
235 | let suggestions = [] | 233 | let suggestions = [] |
236 | 234 | ||
237 | // Check if "use: <renderer>" is set | 235 | // Check if "use: <renderer>" is set |
238 | const lineWithRenderer = getLineWithRenderer(currentLine) | 236 | const lineWithRenderer = getLineWithRenderer(anchor) |
239 | const renderer = lineWithRenderer?.textContent.split(' ')[1] | 237 | const renderer = cm.getLine(lineWithRenderer).split(' ')[1] |
240 | if (renderer) { | 238 | if (renderer) { |
241 | |||
242 | // Do not check properties | 239 | // Do not check properties |
243 | if (text.startsWith(' ')) return | 240 | if (text.startsWith(' ')) return [] |
244 | 241 | ||
245 | // If no valid options for current used renderer, go get it! | 242 | // If no valid options for current used renderer, go get it! |
246 | const validOptions = rendererOptions[renderer] | 243 | const validOptions = rendererOptions[renderer] |
@@ -253,9 +250,9 @@ const addSuggestions = (currentLine, selection) => { | |||
253 | }) | 250 | }) |
254 | .catch(() => { | 251 | .catch(() => { |
255 | markInputIsInvalid(lineWithRenderer) | 252 | markInputIsInvalid(lineWithRenderer) |
256 | console.error('Fail to get valid options from renderer, URL is', rendererUrl) | 253 | console.warn(`Fail to get valid options from renderer with URL ${rendererUrl}` ) |
257 | }) | 254 | }) |
258 | return | 255 | return [] |
259 | } | 256 | } |
260 | 257 | ||
261 | // If input is "key:value" (no space left after colon), then it is invalid | 258 | // If input is "key:value" (no space left after colon), then it is invalid |
@@ -263,7 +260,7 @@ const addSuggestions = (currentLine, selection) => { | |||
263 | const isValidKeyValue = text.match(/^[^:]+:\s+/) | 260 | const isValidKeyValue = text.match(/^[^:]+:\s+/) |
264 | if (isKeyFinished && !isValidKeyValue) { | 261 | if (isKeyFinished && !isValidKeyValue) { |
265 | markInputIsInvalid() | 262 | markInputIsInvalid() |
266 | return | 263 | return [] |
267 | } | 264 | } |
268 | 265 | ||
269 | // If user is typing option | 266 | // If user is typing option |
@@ -311,6 +308,11 @@ const addSuggestions = (currentLine, selection) => { | |||
311 | ) | 308 | ) |
312 | suggestions = rendererSuggestions.length > 0 ? rendererSuggestions : [] | 309 | suggestions = rendererSuggestions.length > 0 ? rendererSuggestions : [] |
313 | } | 310 | } |
311 | return suggestions | ||
312 | } | ||
313 | // }}} | ||
314 | // {{{ FUNCTION: Show element about suggestions | ||
315 | const addSuggestions = (anchor, suggestions) => { | ||
314 | 316 | ||
315 | if (suggestions.length === 0) { | 317 | if (suggestions.length === 0) { |
316 | suggestionsEle.style.display = 'none'; | 318 | suggestionsEle.style.display = 'none'; |
@@ -336,55 +338,35 @@ const addSuggestions = (currentLine, selection) => { | |||
336 | option.classList.remove('focus') | 338 | option.classList.remove('focus') |
337 | } | 339 | } |
338 | option.onclick = () => { | 340 | option.onclick = () => { |
339 | const newFocus = { ...selection.focus, col: 0 } | 341 | cm.setSelection(anchor, { ...anchor, ch: 0 }) |
340 | const newAnchor = { ...selection.anchor, col: text.length } | 342 | cm.replaceSelection(suggestion.replace) |
341 | tinyEditor.paste(suggestion.replace, newFocus, newAnchor) | 343 | cm.focus(); |
342 | suggestionsEle.style.display = 'none'; | 344 | const newAnchor = { ...anchor, ch: suggestion.replace.length } |
343 | option.classList.remove('focus') | 345 | cm.setCursor(newAnchor); |
344 | }; | 346 | }; |
345 | suggestionsEle.appendChild(option); | 347 | suggestionsEle.appendChild(option); |
346 | }); | 348 | }); |
347 | 349 | ||
348 | const rect = currentLine.getBoundingClientRect(); | 350 | cm.addWidget(anchor, suggestionsEle, true) |
349 | suggestionsEle.style.top = `${rect.top + rect.height + 12}px`; | 351 | const rect = suggestionsEle.getBoundingClientRect() |
350 | suggestionsEle.style.left = `${rect.right}px`; | 352 | suggestionsEle.style.maxWidth = `calc(${window.innerWidth}px - ${rect.x}px - 2rem)`; |
351 | suggestionsEle.style.maxWidth = `calc(${window.innerWidth}px - ${rect.right}px - 2rem)`; | 353 | suggestionsEle.style.display = 'block' |
352 | } | 354 | } |
353 | // }}} | 355 | // }}} |
354 | // EVENT: suggests for current selection {{{ | 356 | // EVENT: suggests for current selection {{{ |
355 | tinyEditor.addEventListener('selection', selection => { | 357 | // FIXME Dont show suggestion when selecting multiple chars |
356 | // Check selection is inside editor contents | 358 | cm.on("beforeSelectionChange", (_, obj) => { |
357 | const node = selection?.anchor?.node | 359 | const anchor = (obj.ranges[0].anchor) |
358 | if (!node) return | 360 | const token = cm.getTokenAt(anchor) |
359 | |||
360 | // FIXME Better way to prevent spellcheck across editor | ||
361 | // Get HTML element for current selection | ||
362 | const element = node instanceof HTMLElement | ||
363 | ? node | ||
364 | : node.parentNode | ||
365 | element.setAttribute('spellcheck', 'false') | ||
366 | |||
367 | // To trigger click event on suggestions list, don't set suggestion list invisible | ||
368 | if (suggestionsEle.querySelector('.container__suggestion.focus:hover') !== null) { | ||
369 | return | ||
370 | } else { | ||
371 | suggestionsEle.style.display = 'none'; | ||
372 | } | ||
373 | |||
374 | // Do not show suggestion by attribute | ||
375 | if (suggestionsEle.getAttribute('data-keep-close') === 'true') { | ||
376 | suggestionsEle.setAttribute('data-keep-close', 'false') | ||
377 | return | ||
378 | } | ||
379 | 361 | ||
380 | // Show suggestions for map code block | 362 | if (insideCodeblockForMap(token)) { |
381 | if (insideCodeblockForMap(element)) { | 363 | handleTypingInCodeBlock(anchor) |
382 | handleTypingInCodeBlock(element, selection) | ||
383 | } | 364 | } |
384 | }); | 365 | }); |
385 | // }}} | 366 | // }}} |
386 | // EVENT: keydown for suggestions {{{ | 367 | // EVENT: keydown for suggestions {{{ |
387 | mdeElement.addEventListener('keydown', (e) => { | 368 | cm.on('keydown', (_, e) => { |
369 | |||
388 | // Only the following keys are used | 370 | // Only the following keys are used |
389 | const keyForSuggestions = ['Tab', 'Enter', 'Escape'].includes(e.key) | 371 | const keyForSuggestions = ['Tab', 'Enter', 'Escape'].includes(e.key) |
390 | if (!keyForSuggestions || suggestionsEle.style.display === 'none') return; | 372 | if (!keyForSuggestions || suggestionsEle.style.display === 'none') return; |
@@ -402,7 +384,6 @@ mdeElement.addEventListener('keydown', (e) => { | |||
402 | const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion | 384 | const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion |
403 | 385 | ||
404 | // Current editor selection state | 386 | // Current editor selection state |
405 | const selection = tinyEditor.getSelection(true) | ||
406 | switch (e.key) { | 387 | switch (e.key) { |
407 | case 'Tab': | 388 | case 'Tab': |
408 | Array.from(suggestionsEle.children).forEach(s => s.classList.remove('focus')) | 389 | Array.from(suggestionsEle.children).forEach(s => s.classList.remove('focus')) |
@@ -411,16 +392,22 @@ mdeElement.addEventListener('keydown', (e) => { | |||
411 | break; | 392 | break; |
412 | case 'Enter': | 393 | case 'Enter': |
413 | currentSuggestion.onclick() | 394 | currentSuggestion.onclick() |
414 | suggestionsEle.style.display = 'none'; | ||
415 | break; | 395 | break; |
416 | case 'Escape': | 396 | case 'Escape': |
397 | const anchor = cm.getCursor() | ||
417 | suggestionsEle.style.display = 'none'; | 398 | suggestionsEle.style.display = 'none'; |
418 | // Prevent trigger selection event again | 399 | // Focus editor again |
419 | suggestionsEle.setAttribute('data-keep-close', 'true') | 400 | setTimeout(() => cm.focus() && cm.setCursor(anchor), 100) |
420 | setTimeout(() => tinyEditor.setSelection(selection), 100) | ||
421 | break; | 401 | break; |
422 | } | 402 | } |
423 | }); | 403 | }); |
404 | |||
405 | document.onkeydown = (e) => { | ||
406 | if (e.altKey && e.ctrlKey && e.key === 'm') { | ||
407 | toggleMaps(HtmlContainer) | ||
408 | } | ||
409 | } | ||
410 | |||
424 | // }}} | 411 | // }}} |
425 | // }}} | 412 | // }}} |
426 | 413 | ||