diff options
Diffstat (limited to 'src/editor.mjs')
-rw-r--r-- | src/editor.mjs | 655 |
1 files changed, 348 insertions, 307 deletions
diff --git a/src/editor.mjs b/src/editor.mjs index c04ba24..73177b6 100644 --- a/src/editor.mjs +++ b/src/editor.mjs | |||
@@ -1,143 +1,170 @@ | |||
1 | /*global EasyMDE*/ | 1 | /*global EasyMDE*/ |
2 | /*eslint no-undef: "error"*/ | 2 | /*eslint no-undef: "error"*/ |
3 | import { markdown2HTML, generateMaps } from './dumbymap' | 3 | import { markdown2HTML, generateMaps } from "./dumbymap"; |
4 | import { defaultAliases, parseConfigsFromYaml } from 'mapclay' | 4 | import { defaultAliases, parseConfigsFromYaml } from "mapclay"; |
5 | import * as menuItem from './MenuItem' | 5 | import * as menuItem from "./MenuItem"; |
6 | 6 | ||
7 | // Set up Containers {{{ | 7 | // Set up Containers {{{ |
8 | 8 | ||
9 | const HtmlContainer = document.querySelector(".DumbyMap") | 9 | const HtmlContainer = document.querySelector(".DumbyMap"); |
10 | const textArea = document.querySelector(".editor textarea") | 10 | const textArea = document.querySelector(".editor textarea"); |
11 | let dumbymap | 11 | let dumbymap; |
12 | 12 | ||
13 | const toggleEditing = () => { | 13 | const toggleEditing = () => { |
14 | if (document.body.getAttribute("data-mode") === "editing") { | 14 | if (document.body.getAttribute("data-mode") === "editing") { |
15 | document.body.removeAttribute("data-mode") | 15 | document.body.removeAttribute("data-mode"); |
16 | } else { | 16 | } else { |
17 | document.body.setAttribute("data-mode", "editing") | 17 | document.body.setAttribute("data-mode", "editing"); |
18 | } | 18 | } |
19 | HtmlContainer.setAttribute("data-layout", "normal") | 19 | HtmlContainer.setAttribute("data-layout", "normal"); |
20 | } | 20 | }; |
21 | // }}} | 21 | // }}} |
22 | // Set up EasyMDE {{{ | 22 | // Set up EasyMDE {{{ |
23 | 23 | ||
24 | // Content values for editor | 24 | // Content values for editor |
25 | 25 | ||
26 | 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' | 26 | const defaultContent = |
27 | '## 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'; | ||
27 | const editor = new EasyMDE({ | 28 | const editor = new EasyMDE({ |
28 | element: textArea, | 29 | element: textArea, |
29 | initialValue: defaultContent, | 30 | initialValue: defaultContent, |
30 | autosave: { | 31 | autosave: { |
31 | enabled: true, | 32 | enabled: true, |
32 | uniqueId: 'dumbymap', | 33 | uniqueId: "dumbymap", |
33 | }, | 34 | }, |
34 | indentWithTabs: false, | 35 | indentWithTabs: false, |
35 | lineNumbers: true, | 36 | lineNumbers: true, |
36 | promptURLs: true, | 37 | promptURLs: true, |
37 | uploadImage: true, | 38 | uploadImage: true, |
38 | spellChecker: false, | 39 | spellChecker: false, |
39 | toolbarButtonClassPrefix: 'mde', | 40 | toolbarButtonClassPrefix: "mde", |
40 | status: false, | 41 | status: false, |
41 | shortcuts: { | 42 | shortcuts: { |
42 | "map": "Ctrl-Alt-M", | 43 | map: "Ctrl-Alt-M", |
43 | "debug": "Ctrl-Alt-D", | 44 | debug: "Ctrl-Alt-D", |
44 | "toggleUnorderedList": null, | 45 | toggleUnorderedList: null, |
45 | "toggleOrderedList": null, | 46 | toggleOrderedList: null, |
46 | }, | 47 | }, |
47 | toolbar: [ | 48 | toolbar: [ |
48 | { | 49 | { |
49 | name: 'map', | 50 | name: "map", |
50 | title: 'Toggle Map Generation', | 51 | title: "Toggle Map Generation", |
51 | text: "🌏", | 52 | text: "🌏", |
52 | action: () => toggleEditing(), | 53 | action: () => toggleEditing(), |
53 | }, | 54 | }, |
54 | { | 55 | { |
55 | name: 'debug', | 56 | name: "debug", |
56 | title: 'Save content as URL', | 57 | title: "Save content as URL", |
57 | text: "🤔", | 58 | text: "🤔", |
58 | action: () => { | 59 | action: () => { |
59 | const state = { content: editor.value() } | 60 | const state = { content: editor.value() }; |
60 | window.location.hash = encodeURIComponent(JSON.stringify(state)) | 61 | window.location.hash = encodeURIComponent(JSON.stringify(state)); |
61 | navigator.clipboard.writeText(window.location.href) | 62 | navigator.clipboard.writeText(window.location.href); |
62 | alert('URL copied to clipboard') | 63 | alert("URL copied to clipboard"); |
63 | }, | 64 | }, |
64 | }, 'undo', 'redo', '|', 'heading-1', 'heading-2', '|', 'link', 'image', '|', 'bold', 'italic', 'strikethrough', 'code', 'clean-block', '|', 'unordered-list', 'ordered-list', 'quote', 'table' | 65 | }, |
66 | "undo", | ||
67 | "redo", | ||
68 | "|", | ||
69 | "heading-1", | ||
70 | "heading-2", | ||
71 | "|", | ||
72 | "link", | ||
73 | "image", | ||
74 | "|", | ||
75 | "bold", | ||
76 | "italic", | ||
77 | "strikethrough", | ||
78 | "code", | ||
79 | "clean-block", | ||
80 | "|", | ||
81 | "unordered-list", | ||
82 | "ordered-list", | ||
83 | "quote", | ||
84 | "table", | ||
65 | ], | 85 | ], |
66 | }); | 86 | }); |
67 | 87 | ||
68 | const cm = editor.codemirror | 88 | const cm = editor.codemirror; |
69 | 89 | ||
70 | const getStateFromHash = (hash) => { | 90 | const getStateFromHash = hash => { |
71 | const hashValue = hash.substring(1); | 91 | const hashValue = hash.substring(1); |
72 | const stateString = decodeURIComponent(hashValue) | 92 | const stateString = decodeURIComponent(hashValue); |
73 | try { return JSON.parse(stateString) ?? {} } | 93 | try { |
74 | catch (_) { return {} } | 94 | return JSON.parse(stateString) ?? {}; |
75 | } | 95 | } catch (_) { |
96 | return {}; | ||
97 | } | ||
98 | }; | ||
76 | 99 | ||
77 | const getContentFromHash = (hash) => { | 100 | const getContentFromHash = hash => { |
78 | const state = getStateFromHash(hash) | 101 | const state = getStateFromHash(hash); |
79 | return state.content | 102 | return state.content; |
80 | } | 103 | }; |
81 | 104 | ||
82 | const initialState = getStateFromHash(window.location.hash) | 105 | const initialState = getStateFromHash(window.location.hash); |
83 | window.location.hash = '' | 106 | window.location.hash = ""; |
84 | const contentFromHash = initialState.content | 107 | const contentFromHash = initialState.content; |
85 | 108 | ||
86 | // Seems like autosave would overwrite initialValue, set content from hash here | 109 | // Seems like autosave would overwrite initialValue, set content from hash here |
87 | if (contentFromHash) { | 110 | if (contentFromHash) { |
88 | editor.cleanup() | 111 | editor.cleanup(); |
89 | editor.value(contentFromHash) | 112 | editor.value(contentFromHash); |
90 | } | 113 | } |
91 | // }}} | 114 | // }}} |
92 | // Set up logic about editor content {{{ | 115 | // Set up logic about editor content {{{ |
93 | const afterMapRendered = (_) => { | 116 | const afterMapRendered = _ => { |
94 | // mapHolder.oncontextmenu = (event) => { | 117 | // mapHolder.oncontextmenu = (event) => { |
95 | // event.preventDefault() | 118 | // event.preventDefault() |
96 | // const lonLat = mapHolder.renderer.unproject([event.x, event.y]) | 119 | // const lonLat = mapHolder.renderer.unproject([event.x, event.y]) |
97 | // // TODO... | 120 | // // TODO... |
98 | // } | 121 | // } |
99 | } | 122 | }; |
100 | markdown2HTML(HtmlContainer, editor.value()) | 123 | markdown2HTML(HtmlContainer, editor.value()); |
101 | dumbymap = generateMaps(HtmlContainer, afterMapRendered) | 124 | dumbymap = generateMaps(HtmlContainer, afterMapRendered); |
102 | 125 | ||
103 | // Quick hack to style lines inside code block | 126 | // Quick hack to style lines inside code block |
104 | const addClassToCodeLines = () => { | 127 | const addClassToCodeLines = () => { |
105 | const lines = cm.getLineHandle(0).parent.lines | 128 | const lines = cm.getLineHandle(0).parent.lines; |
106 | let insideCodeBlock = false | 129 | let insideCodeBlock = false; |
107 | lines.forEach((line, index) => { | 130 | lines.forEach((line, index) => { |
108 | if (line.text.match(/^[\u0060]{3}/)) { | 131 | if (line.text.match(/^[\u0060]{3}/)) { |
109 | insideCodeBlock = !insideCodeBlock | 132 | insideCodeBlock = !insideCodeBlock; |
110 | } else if (insideCodeBlock) { | 133 | } else if (insideCodeBlock) { |
111 | cm.addLineClass(index, "text", "inside-code-block") | 134 | cm.addLineClass(index, "text", "inside-code-block"); |
112 | } else { | 135 | } else { |
113 | cm.removeLineClass(index, "text", "inside-code-block") | 136 | cm.removeLineClass(index, "text", "inside-code-block"); |
114 | } | 137 | } |
115 | }) | 138 | }); |
116 | } | 139 | }; |
117 | addClassToCodeLines() | 140 | addClassToCodeLines(); |
118 | 141 | ||
119 | const completeForCodeBlock = (change) => { | 142 | const completeForCodeBlock = change => { |
120 | const line = change.to.line | 143 | const line = change.to.line; |
121 | if (change.origin === "+input") { | 144 | if (change.origin === "+input") { |
122 | const text = change.text[0] | 145 | const text = change.text[0]; |
123 | 146 | ||
124 | // Completion for YAML doc separator | 147 | // Completion for YAML doc separator |
125 | if (text === "-" && change.to.ch === 0 && insideCodeblockForMap(cm.getCursor())) { | 148 | if ( |
126 | cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }) | 149 | text === "-" && |
127 | cm.replaceSelection(text.repeat(3) + '\n') | 150 | change.to.ch === 0 && |
151 | insideCodeblockForMap(cm.getCursor()) | ||
152 | ) { | ||
153 | cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }); | ||
154 | cm.replaceSelection(text.repeat(3) + "\n"); | ||
128 | } | 155 | } |
129 | 156 | ||
130 | // Completion for Code fence | 157 | // Completion for Code fence |
131 | if (text === "`" && change.to.ch === 0) { | 158 | if (text === "`" && change.to.ch === 0) { |
132 | cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }) | 159 | cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 1 }); |
133 | cm.replaceSelection(text.repeat(3)) | 160 | cm.replaceSelection(text.repeat(3)); |
134 | const numberOfFences = cm.getValue() | 161 | const numberOfFences = cm |
135 | .split('\n') | 162 | .getValue() |
136 | .filter(line => line.match(/[\u0060]{3}/)) | 163 | .split("\n") |
137 | .length | 164 | .filter(line => line.match(/[\u0060]{3}/)).length; |
138 | if (numberOfFences % 2 === 1) { | 165 | if (numberOfFences % 2 === 1) { |
139 | cm.replaceSelection('map\n\n```') | 166 | cm.replaceSelection("map\n\n```"); |
140 | cm.setCursor({ line: line + 1 }) | 167 | cm.setCursor({ line: line + 1 }); |
141 | } | 168 | } |
142 | } | 169 | } |
143 | } | 170 | } |
@@ -145,63 +172,63 @@ const completeForCodeBlock = (change) => { | |||
145 | // For YAML doc separator, <hr> and code fence | 172 | // For YAML doc separator, <hr> and code fence |
146 | // Auto delete to start of line | 173 | // Auto delete to start of line |
147 | if (change.origin === "+delete") { | 174 | if (change.origin === "+delete") { |
148 | const match = change.removed[0].match(/^[-\u0060]$/)?.at(0) | 175 | const match = change.removed[0].match(/^[-\u0060]$/)?.at(0); |
149 | if (match && cm.getLine(line) === match.repeat(2) && match) { | 176 | if (match && cm.getLine(line) === match.repeat(2) && match) { |
150 | cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 2 }) | 177 | cm.setSelection({ line: line, ch: 0 }, { line: line, ch: 2 }); |
151 | cm.replaceSelection('') | 178 | cm.replaceSelection(""); |
152 | } | 179 | } |
153 | } | 180 | } |
154 | } | 181 | }; |
155 | 182 | ||
156 | const debounceForMap = (() => { | 183 | const debounceForMap = (() => { |
157 | let timer = null; | 184 | let timer = null; |
158 | 185 | ||
159 | return function(...args) { | 186 | return function (...args) { |
160 | dumbymap = generateMaps.apply(this, args) | 187 | dumbymap = generateMaps.apply(this, args); |
161 | // clearTimeout(timer); | 188 | // clearTimeout(timer); |
162 | // timer = setTimeout(() => { | 189 | // timer = setTimeout(() => { |
163 | // dumbymap = generateMaps.apply(this, args) | 190 | // dumbymap = generateMaps.apply(this, args) |
164 | // }, 10); | 191 | // }, 10); |
165 | } | 192 | }; |
166 | })() | 193 | })(); |
167 | 194 | ||
168 | const updateDumbyMap = () => { | 195 | const updateDumbyMap = () => { |
169 | markdown2HTML(HtmlContainer, editor.value()) | 196 | markdown2HTML(HtmlContainer, editor.value()); |
170 | // TODO Test if generate maps intantly is OK with map cache | 197 | // TODO Test if generate maps intantly is OK with map cache |
171 | // debounceForMap(HtmlContainer, afterMapRendered) | 198 | // debounceForMap(HtmlContainer, afterMapRendered) |
172 | dumbymap = generateMaps(HtmlContainer, afterMapRendered) | 199 | dumbymap = generateMaps(HtmlContainer, afterMapRendered); |
173 | } | 200 | }; |
174 | 201 | ||
175 | updateDumbyMap() | 202 | updateDumbyMap(); |
176 | 203 | ||
177 | // Re-render HTML by editor content | 204 | // Re-render HTML by editor content |
178 | cm.on("change", (_, change) => { | 205 | cm.on("change", (_, change) => { |
179 | updateDumbyMap() | 206 | updateDumbyMap(); |
180 | addClassToCodeLines() | 207 | addClassToCodeLines(); |
181 | completeForCodeBlock(change) | 208 | completeForCodeBlock(change); |
182 | }) | 209 | }); |
183 | 210 | ||
184 | // Set class for focus | 211 | // Set class for focus |
185 | cm.on("focus", () => { | 212 | cm.on("focus", () => { |
186 | cm.getWrapperElement().classList.add('focus') | 213 | cm.getWrapperElement().classList.add("focus"); |
187 | HtmlContainer.classList.remove('focus') | 214 | HtmlContainer.classList.remove("focus"); |
188 | }) | 215 | }); |
189 | 216 | ||
190 | cm.on("beforeChange", (_, change) => { | 217 | cm.on("beforeChange", (_, change) => { |
191 | const line = change.to.line | 218 | const line = change.to.line; |
192 | // Don't allow more content after YAML doc separator | 219 | // Don't allow more content after YAML doc separator |
193 | if (change.origin.match(/^(\+input|paste)$/)) { | 220 | if (change.origin.match(/^(\+input|paste)$/)) { |
194 | if (cm.getLine(line) === "---" && change.text[0] !== "") { | 221 | if (cm.getLine(line) === "---" && change.text[0] !== "") { |
195 | change.cancel() | 222 | change.cancel(); |
196 | } | 223 | } |
197 | } | 224 | } |
198 | }) | 225 | }); |
199 | 226 | ||
200 | // Reload editor content by hash value | 227 | // Reload editor content by hash value |
201 | window.onhashchange = () => { | 228 | window.onhashchange = () => { |
202 | const content = getContentFromHash(window.location.hash) | 229 | const content = getContentFromHash(window.location.hash); |
203 | if (content) editor.value(content) | 230 | if (content) editor.value(content); |
204 | } | 231 | }; |
205 | 232 | ||
206 | // FIXME DEBUGONLY | 233 | // FIXME DEBUGONLY |
207 | // generateMaps(HtmlContainer) | 234 | // generateMaps(HtmlContainer) |
@@ -212,401 +239,415 @@ window.onhashchange = () => { | |||
212 | // }}} | 239 | // }}} |
213 | // Completion in Code Blok {{{ | 240 | // Completion in Code Blok {{{ |
214 | // Elements about suggestions {{{ | 241 | // Elements about suggestions {{{ |
215 | const menu = document.createElement('div') | 242 | const menu = document.createElement("div"); |
216 | menu.id = 'menu' | 243 | menu.id = "menu"; |
217 | menu.onclick = () => menu.style.display = 'none' | 244 | menu.onclick = () => (menu.style.display = "none"); |
218 | document.body.append(menu) | 245 | document.body.append(menu); |
219 | 246 | ||
220 | const rendererOptions = {} | 247 | const rendererOptions = {}; |
221 | 248 | ||
222 | // }}} | 249 | // }}} |
223 | // Aliases for map options {{{ | 250 | // Aliases for map options {{{ |
224 | const aliasesForMapOptions = {} | 251 | const aliasesForMapOptions = {}; |
225 | const defaultApply = './dist/default.yml' | 252 | const defaultApply = "./dist/default.yml"; |
226 | fetch(defaultApply) | 253 | fetch(defaultApply) |
227 | .then(res => res.text()) | 254 | .then(res => res.text()) |
228 | .then(rawText => { | 255 | .then(rawText => { |
229 | const config = parseConfigsFromYaml(rawText)?.at(0) | 256 | const config = parseConfigsFromYaml(rawText)?.at(0); |
230 | Object.assign(aliasesForMapOptions, config.aliases ?? {}) | 257 | Object.assign(aliasesForMapOptions, config.aliases ?? {}); |
231 | }) | 258 | }) |
232 | .catch(err => console.warn(`Fail to get aliases from ${defaultApply}`, err)) | 259 | .catch(err => console.warn(`Fail to get aliases from ${defaultApply}`, err)); |
233 | // }}} | 260 | // }}} |
234 | // FUNCTION: Check if current token is inside code block {{{ | 261 | // FUNCTION: Check if current token is inside code block {{{ |
235 | const insideCodeblockForMap = (anchor) => { | 262 | const insideCodeblockForMap = anchor => { |
236 | const token = cm.getTokenAt(anchor) | 263 | const token = cm.getTokenAt(anchor); |
237 | const insideCodeBlock = token.state.overlay.codeBlock && !cm.getLine(anchor.line).match(/^[\u0060]{3}/) | 264 | const insideCodeBlock = |
238 | if (!insideCodeBlock) return false | 265 | token.state.overlay.codeBlock && |
239 | 266 | !cm.getLine(anchor.line).match(/^[\u0060]{3}/); | |
240 | let line = anchor.line - 1 | 267 | if (!insideCodeBlock) return false; |
268 | |||
269 | let line = anchor.line - 1; | ||
241 | while (line >= 0) { | 270 | while (line >= 0) { |
242 | const content = cm.getLine(line) | 271 | const content = cm.getLine(line); |
243 | if (content === '```map') { | 272 | if (content === "```map") { |
244 | return true | 273 | return true; |
245 | } else if (content === '```') { | 274 | } else if (content === "```") { |
246 | return false | 275 | return false; |
247 | } | 276 | } |
248 | line = line - 1 | 277 | line = line - 1; |
249 | } | 278 | } |
250 | return false | 279 | return false; |
251 | } | 280 | }; |
252 | // }}} | 281 | // }}} |
253 | // FUNCTION: Get Renderer by cursor position in code block {{{ | 282 | // FUNCTION: Get Renderer by cursor position in code block {{{ |
254 | const getLineWithRenderer = (anchor) => { | 283 | const getLineWithRenderer = anchor => { |
255 | const currentLine = anchor.line | 284 | const currentLine = anchor.line; |
256 | if (!cm.getLine) return null | 285 | if (!cm.getLine) return null; |
257 | 286 | ||
258 | const match = (line) => cm.getLine(line).match(/^use: /) | 287 | const match = line => cm.getLine(line).match(/^use: /); |
259 | |||
260 | if (match(currentLine)) return currentLine | ||
261 | 288 | ||
289 | if (match(currentLine)) return currentLine; | ||
262 | 290 | ||
263 | // Look backward/forward for pattern of used renderer: /use: .+/ | 291 | // Look backward/forward for pattern of used renderer: /use: .+/ |
264 | let pl = currentLine - 1 | 292 | let pl = currentLine - 1; |
265 | while (pl > 0 && insideCodeblockForMap(anchor)) { | 293 | while (pl > 0 && insideCodeblockForMap(anchor)) { |
266 | const text = cm.getLine(pl) | 294 | const text = cm.getLine(pl); |
267 | if (match(pl)) { | 295 | if (match(pl)) { |
268 | return pl | 296 | return pl; |
269 | } else if (text.match(/^---|^[\u0060]{3}/)) { | 297 | } else if (text.match(/^---|^[\u0060]{3}/)) { |
270 | break | 298 | break; |
271 | } | 299 | } |
272 | pl = pl - 1 | 300 | pl = pl - 1; |
273 | } | 301 | } |
274 | 302 | ||
275 | let nl = currentLine + 1 | 303 | let nl = currentLine + 1; |
276 | while (insideCodeblockForMap(anchor)) { | 304 | while (insideCodeblockForMap(anchor)) { |
277 | const text = cm.getLine(nl) | 305 | const text = cm.getLine(nl); |
278 | if (match(nl)) { | 306 | if (match(nl)) { |
279 | return nl | 307 | return nl; |
280 | } else if (text.match(/^---|^[\u0060]{3}/)) { | 308 | } else if (text.match(/^---|^[\u0060]{3}/)) { |
281 | return null | 309 | return null; |
282 | } | 310 | } |
283 | nl = nl + 1 | 311 | nl = nl + 1; |
284 | } | 312 | } |
285 | 313 | ||
286 | return null | 314 | return null; |
287 | } | 315 | }; |
288 | // }}} | 316 | // }}} |
289 | // FUNCTION: Return suggestions for valid options {{{ | 317 | // FUNCTION: Return suggestions for valid options {{{ |
290 | const getSuggestionsForOptions = (optionTyped, validOptions) => { | 318 | const getSuggestionsForOptions = (optionTyped, validOptions) => { |
291 | let suggestOptions = [] | 319 | let suggestOptions = []; |
292 | 320 | ||
293 | const matchedOptions = validOptions | 321 | const matchedOptions = validOptions.filter(o => |
294 | .filter(o => o.valueOf().toLowerCase().includes(optionTyped.toLowerCase())) | 322 | o.valueOf().toLowerCase().includes(optionTyped.toLowerCase()), |
323 | ); | ||
295 | 324 | ||
296 | if (matchedOptions.length > 0) { | 325 | if (matchedOptions.length > 0) { |
297 | suggestOptions = matchedOptions | 326 | suggestOptions = matchedOptions; |
298 | } else { | 327 | } else { |
299 | suggestOptions = validOptions | 328 | suggestOptions = validOptions; |
300 | } | 329 | } |
301 | 330 | ||
302 | return suggestOptions | 331 | return suggestOptions.map( |
303 | .map(o => new menuItem.Suggestion({ | 332 | o => |
304 | text: `<span>${o.valueOf()}</span><span class='info' title="${o.desc ?? ''}">ⓘ</span>`, | 333 | new menuItem.Suggestion({ |
305 | replace: `${o.valueOf()}: `, | 334 | text: `<span>${o.valueOf()}</span><span class='info' title="${o.desc ?? ""}">ⓘ</span>`, |
306 | })) | 335 | replace: `${o.valueOf()}: `, |
307 | } | 336 | }), |
337 | ); | ||
338 | }; | ||
308 | // }}} | 339 | // }}} |
309 | // FUNCTION: Return suggestion for example of option value {{{ | 340 | // FUNCTION: Return suggestion for example of option value {{{ |
310 | const getSuggestionFromMapOption = (option) => { | 341 | const getSuggestionFromMapOption = option => { |
311 | if (!option.example) return null | 342 | if (!option.example) return null; |
312 | 343 | ||
313 | const text = option.example_desc | 344 | const text = option.example_desc |
314 | ? `<span>${option.example_desc}</span><span class="truncate"style="color: gray">${option.example}</span>` | 345 | ? `<span>${option.example_desc}</span><span class="truncate"style="color: gray">${option.example}</span>` |
315 | : `<span>${option.example}</span>` | 346 | : `<span>${option.example}</span>`; |
316 | 347 | ||
317 | return new menuItem.Suggestion({ | 348 | return new menuItem.Suggestion({ |
318 | text: text, | 349 | text: text, |
319 | replace: `${option.valueOf()}: ${option.example ?? ""}`, | 350 | replace: `${option.valueOf()}: ${option.example ?? ""}`, |
320 | }) | 351 | }); |
321 | } | 352 | }; |
322 | // }}} | 353 | // }}} |
323 | // FUNCTION: Return suggestions from aliases {{{ | 354 | // FUNCTION: Return suggestions from aliases {{{ |
324 | const getSuggestionsFromAliases = (option) => Object.entries(aliasesForMapOptions[option.valueOf()] ?? {}) | 355 | const getSuggestionsFromAliases = option => |
325 | ?.map(record => { | 356 | Object.entries(aliasesForMapOptions[option.valueOf()] ?? {})?.map(record => { |
326 | const [alias, value] = record | 357 | const [alias, value] = record; |
327 | const valueString = JSON.stringify(value).replaceAll('"', '') | 358 | const valueString = JSON.stringify(value).replaceAll('"', ""); |
328 | return new menuItem.Suggestion({ | 359 | return new menuItem.Suggestion({ |
329 | text: `<span>${alias}</span><span class="truncate" style="color: gray">${valueString}</span>`, | 360 | text: `<span>${alias}</span><span class="truncate" style="color: gray">${valueString}</span>`, |
330 | replace: `${option.valueOf()}: ${valueString}`, | 361 | replace: `${option.valueOf()}: ${valueString}`, |
331 | }) | 362 | }); |
332 | }) | 363 | }) ?? []; |
333 | ?? [] | ||
334 | // }}} | 364 | // }}} |
335 | // FUCNTION: Handler for map codeblock {{{ | 365 | // FUCNTION: Handler for map codeblock {{{ |
336 | const handleTypingInCodeBlock = (anchor) => { | 366 | const handleTypingInCodeBlock = anchor => { |
337 | const text = cm.getLine(anchor.line) | 367 | const text = cm.getLine(anchor.line); |
338 | if (text.match(/^\s\+$/) && text.length % 2 !== 0) { | 368 | if (text.match(/^\s\+$/) && text.length % 2 !== 0) { |
339 | // TODO Completion for even number of spaces | 369 | // TODO Completion for even number of spaces |
340 | } else if (text.match(/^-/)) { | 370 | } else if (text.match(/^-/)) { |
341 | // TODO Completion for YAML doc separator | 371 | // TODO Completion for YAML doc separator |
342 | } else { | 372 | } else { |
343 | const suggestions = getSuggestions(anchor) | 373 | const suggestions = getSuggestions(anchor); |
344 | addSuggestions(anchor, suggestions) | 374 | addSuggestions(anchor, suggestions); |
345 | } | 375 | } |
346 | } | 376 | }; |
347 | // }}} | 377 | // }}} |
348 | // FUNCTION: get suggestions by current input {{{ | 378 | // FUNCTION: get suggestions by current input {{{ |
349 | const getSuggestions = (anchor) => { | 379 | const getSuggestions = anchor => { |
350 | const text = cm.getLine(anchor.line) | 380 | const text = cm.getLine(anchor.line); |
351 | 381 | ||
352 | // Clear marks on text | 382 | // Clear marks on text |
353 | cm.findMarks( | 383 | cm.findMarks({ ...anchor, ch: 0 }, { ...anchor, ch: text.length }).forEach( |
354 | { ...anchor, ch: 0 }, | 384 | m => m.clear(), |
355 | { ...anchor, ch: text.length } | 385 | ); |
356 | ).forEach(m => m.clear()) | ||
357 | 386 | ||
358 | // Mark user input invalid by case | 387 | // Mark user input invalid by case |
359 | const markInputIsInvalid = () => | 388 | const markInputIsInvalid = () => |
360 | cm.getDoc().markText( | 389 | cm |
361 | { ...anchor, ch: 0 }, | 390 | .getDoc() |
362 | { ...anchor, ch: text.length }, | 391 | .markText( |
363 | { className: 'invalid-input' }, | 392 | { ...anchor, ch: 0 }, |
364 | ) | 393 | { ...anchor, ch: text.length }, |
394 | { className: "invalid-input" }, | ||
395 | ); | ||
365 | 396 | ||
366 | // Check if "use: <renderer>" is set | 397 | // Check if "use: <renderer>" is set |
367 | const lineWithRenderer = getLineWithRenderer(anchor) | 398 | const lineWithRenderer = getLineWithRenderer(anchor); |
368 | const renderer = lineWithRenderer | 399 | const renderer = lineWithRenderer |
369 | ? cm.getLine(lineWithRenderer).split(' ')[1] | 400 | ? cm.getLine(lineWithRenderer).split(" ")[1] |
370 | : null | 401 | : null; |
371 | if (renderer && anchor.line !== lineWithRenderer) { | 402 | if (renderer && anchor.line !== lineWithRenderer) { |
372 | // Do not check properties | 403 | // Do not check properties |
373 | if (text.startsWith(' ')) return [] | 404 | if (text.startsWith(" ")) return []; |
374 | 405 | ||
375 | // If no valid options for current used renderer, go get it! | 406 | // If no valid options for current used renderer, go get it! |
376 | const validOptions = rendererOptions[renderer] | 407 | const validOptions = rendererOptions[renderer]; |
377 | if (!validOptions) { | 408 | if (!validOptions) { |
378 | // Get list of valid options for current renderer | 409 | // Get list of valid options for current renderer |
379 | const rendererUrl = defaultAliases.use[renderer]?.value | 410 | const rendererUrl = defaultAliases.use[renderer]?.value; |
380 | import(rendererUrl) | 411 | import(rendererUrl) |
381 | .then(rendererModule => { | 412 | .then(rendererModule => { |
382 | rendererOptions[renderer] = rendererModule.default.validOptions | 413 | rendererOptions[renderer] = rendererModule.default.validOptions; |
383 | const currentAnchor = cm.getCursor() | 414 | const currentAnchor = cm.getCursor(); |
384 | if (insideCodeblockForMap(currentAnchor)) { | 415 | if (insideCodeblockForMap(currentAnchor)) { |
385 | handleTypingInCodeBlock(currentAnchor) | 416 | handleTypingInCodeBlock(currentAnchor); |
386 | } | 417 | } |
387 | }) | 418 | }) |
388 | .catch(_ => { | 419 | .catch(_ => { |
389 | markInputIsInvalid(lineWithRenderer) | 420 | markInputIsInvalid(lineWithRenderer); |
390 | console.warn(`Fail to get valid options from Renderer typed: ${renderer}`) | 421 | console.warn( |
391 | }) | 422 | `Fail to get valid options from Renderer typed: ${renderer}`, |
392 | return [] | 423 | ); |
424 | }); | ||
425 | return []; | ||
393 | } | 426 | } |
394 | 427 | ||
395 | // If input is "key:value" (no space left after colon), then it is invalid | 428 | // If input is "key:value" (no space left after colon), then it is invalid |
396 | const isKeyFinished = text.includes(':'); | 429 | const isKeyFinished = text.includes(":"); |
397 | const isValidKeyValue = text.match(/^[^:]+:\s+/) | 430 | const isValidKeyValue = text.match(/^[^:]+:\s+/); |
398 | if (isKeyFinished && !isValidKeyValue) { | 431 | if (isKeyFinished && !isValidKeyValue) { |
399 | markInputIsInvalid() | 432 | markInputIsInvalid(); |
400 | return [] | 433 | return []; |
401 | } | 434 | } |
402 | 435 | ||
403 | // If user is typing option | 436 | // If user is typing option |
404 | const keyTyped = text.split(':')[0].trim() | 437 | const keyTyped = text.split(":")[0].trim(); |
405 | if (!isKeyFinished) { | 438 | if (!isKeyFinished) { |
406 | markInputIsInvalid() | 439 | markInputIsInvalid(); |
407 | return getSuggestionsForOptions(keyTyped, validOptions) | 440 | return getSuggestionsForOptions(keyTyped, validOptions); |
408 | } | 441 | } |
409 | 442 | ||
410 | // If user is typing value | 443 | // If user is typing value |
411 | const matchedOption = validOptions.find(o => o.name === keyTyped) | 444 | const matchedOption = validOptions.find(o => o.name === keyTyped); |
412 | if (isKeyFinished && !matchedOption) { | 445 | if (isKeyFinished && !matchedOption) { |
413 | markInputIsInvalid() | 446 | markInputIsInvalid(); |
414 | } | 447 | } |
415 | 448 | ||
416 | if (isKeyFinished && matchedOption) { | 449 | if (isKeyFinished && matchedOption) { |
417 | const valueTyped = text.substring(text.indexOf(':') + 1).trim() | 450 | const valueTyped = text.substring(text.indexOf(":") + 1).trim(); |
418 | const isValidValue = matchedOption.isValid(valueTyped) | 451 | const isValidValue = matchedOption.isValid(valueTyped); |
419 | if (!valueTyped) { | 452 | if (!valueTyped) { |
420 | return [ | 453 | return [ |
421 | getSuggestionFromMapOption(matchedOption), | 454 | getSuggestionFromMapOption(matchedOption), |
422 | ...getSuggestionsFromAliases(matchedOption) | 455 | ...getSuggestionsFromAliases(matchedOption), |
423 | ].filter(s => s instanceof menuItem.Suggestion) | 456 | ].filter(s => s instanceof menuItem.Suggestion); |
424 | } | 457 | } |
425 | if (valueTyped && !isValidValue) { | 458 | if (valueTyped && !isValidValue) { |
426 | markInputIsInvalid() | 459 | markInputIsInvalid(); |
427 | return [] | 460 | return []; |
428 | } | 461 | } |
429 | } | 462 | } |
430 | |||
431 | } else { | 463 | } else { |
432 | // Suggestion for "use" | 464 | // Suggestion for "use" |
433 | const rendererSuggestions = Object.entries(defaultAliases.use) | 465 | const rendererSuggestions = Object.entries(defaultAliases.use) |
434 | .filter(([renderer,]) => { | 466 | .filter(([renderer]) => { |
435 | const suggestion = `use: ${renderer}` | 467 | const suggestion = `use: ${renderer}`; |
436 | const suggestionPattern = suggestion.replace(' ', '').toLowerCase() | 468 | const suggestionPattern = suggestion.replace(" ", "").toLowerCase(); |
437 | const textPattern = text.replace(' ', '').toLowerCase() | 469 | const textPattern = text.replace(" ", "").toLowerCase(); |
438 | return suggestion !== text && | 470 | return suggestion !== text && suggestionPattern.includes(textPattern); |
439 | (suggestionPattern.includes(textPattern)) | ||
440 | }) | 471 | }) |
441 | .map(([renderer, info]) => | 472 | .map( |
442 | new menuItem.Suggestion({ | 473 | ([renderer, info]) => |
443 | text: `<span>use: ${renderer}</span><span class='info' title="${info.desc}">ⓘ</span>`, | 474 | new menuItem.Suggestion({ |
444 | replace: `use: ${renderer}`, | 475 | text: `<span>use: ${renderer}</span><span class='info' title="${info.desc}">ⓘ</span>`, |
445 | }) | 476 | replace: `use: ${renderer}`, |
446 | ) | 477 | }), |
447 | return rendererSuggestions.length > 0 ? rendererSuggestions : [] | 478 | ); |
479 | return rendererSuggestions.length > 0 ? rendererSuggestions : []; | ||
448 | } | 480 | } |
449 | return [] | 481 | return []; |
450 | } | 482 | }; |
451 | // }}} | 483 | // }}} |
452 | // {{{ FUNCTION: Show element about suggestions | 484 | // {{{ FUNCTION: Show element about suggestions |
453 | const addSuggestions = (anchor, suggestions) => { | 485 | const addSuggestions = (anchor, suggestions) => { |
454 | |||
455 | if (suggestions.length === 0) { | 486 | if (suggestions.length === 0) { |
456 | menu.style.display = 'none'; | 487 | menu.style.display = "none"; |
457 | return | 488 | return; |
458 | } else { | 489 | } else { |
459 | menu.style.display = 'block'; | 490 | menu.style.display = "block"; |
460 | } | 491 | } |
461 | 492 | ||
462 | menu.innerHTML = '' | 493 | menu.innerHTML = ""; |
463 | suggestions | 494 | suggestions |
464 | .map(s => s.createElement(cm)) | 495 | .map(s => s.createElement(cm)) |
465 | .forEach(option => menu.appendChild(option)) | 496 | .forEach(option => menu.appendChild(option)); |
466 | 497 | ||
467 | const widgetAnchor = document.createElement('div') | 498 | const widgetAnchor = document.createElement("div"); |
468 | cm.addWidget(anchor, widgetAnchor, true) | 499 | cm.addWidget(anchor, widgetAnchor, true); |
469 | const rect = widgetAnchor.getBoundingClientRect() | 500 | const rect = widgetAnchor.getBoundingClientRect(); |
470 | menu.style.left = `calc(${rect.left}px + 2rem)`; | 501 | menu.style.left = `calc(${rect.left}px + 2rem)`; |
471 | menu.style.top = `calc(${rect.bottom}px + 1rem)`; | 502 | menu.style.top = `calc(${rect.bottom}px + 1rem)`; |
472 | menu.style.maxWidth = `calc(${window.innerWidth}px - ${rect.x}px - 3rem)`; | 503 | menu.style.maxWidth = `calc(${window.innerWidth}px - ${rect.x}px - 3rem)`; |
473 | menu.style.display = 'block' | 504 | menu.style.display = "block"; |
474 | } | 505 | }; |
475 | // }}} | 506 | // }}} |
476 | // EVENT: Suggests for current selection {{{ | 507 | // EVENT: Suggests for current selection {{{ |
477 | // FIXME Dont show suggestion when selecting multiple chars | 508 | // FIXME Dont show suggestion when selecting multiple chars |
478 | cm.on("cursorActivity", (_) => { | 509 | cm.on("cursorActivity", _ => { |
479 | menu.style.display = 'none' | 510 | menu.style.display = "none"; |
480 | const anchor = cm.getCursor() | 511 | const anchor = cm.getCursor(); |
481 | 512 | ||
482 | if (insideCodeblockForMap(anchor)) { | 513 | if (insideCodeblockForMap(anchor)) { |
483 | handleTypingInCodeBlock(anchor) | 514 | handleTypingInCodeBlock(anchor); |
484 | } | 515 | } |
485 | }); | 516 | }); |
486 | cm.on("blur", () => { | 517 | cm.on("blur", () => { |
487 | menu.style.display = 'none' | 518 | menu.style.display = "none"; |
488 | cm.getWrapperElement().classList.remove('focus') | 519 | cm.getWrapperElement().classList.remove("focus"); |
489 | HtmlContainer.classList.add('focus') | 520 | HtmlContainer.classList.add("focus"); |
490 | }) | 521 | }); |
491 | // }}} | 522 | // }}} |
492 | // EVENT: keydown for suggestions {{{ | 523 | // EVENT: keydown for suggestions {{{ |
493 | const keyForSuggestions = ['Tab', 'Enter', 'Escape'] | 524 | const keyForSuggestions = ["Tab", "Enter", "Escape"]; |
494 | cm.on('keydown', (_, e) => { | 525 | cm.on("keydown", (_, e) => { |
495 | if (!cm.hasFocus || !keyForSuggestions.includes(e.key) || menu.style.display === 'none') return; | 526 | if ( |
527 | !cm.hasFocus || | ||
528 | !keyForSuggestions.includes(e.key) || | ||
529 | menu.style.display === "none" | ||
530 | ) | ||
531 | return; | ||
496 | 532 | ||
497 | // Directly add a newline when no suggestion is selected | 533 | // Directly add a newline when no suggestion is selected |
498 | const currentSuggestion = menu.querySelector('.container__suggestion.focus') | 534 | const currentSuggestion = menu.querySelector(".container__suggestion.focus"); |
499 | if (!currentSuggestion && e.key === 'Enter') return | 535 | if (!currentSuggestion && e.key === "Enter") return; |
500 | 536 | ||
501 | // Override default behavior | 537 | // Override default behavior |
502 | e.preventDefault(); | 538 | e.preventDefault(); |
503 | 539 | ||
504 | // Suggestion when pressing Tab or Shift + Tab | 540 | // Suggestion when pressing Tab or Shift + Tab |
505 | const nextSuggestion = currentSuggestion?.nextSibling ?? menu.querySelector('.container__suggestion:first-child') | 541 | const nextSuggestion = |
506 | const previousSuggestion = currentSuggestion?.previousSibling ?? menu.querySelector('.container__suggestion:last-child') | 542 | currentSuggestion?.nextSibling ?? |
507 | const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion | 543 | menu.querySelector(".container__suggestion:first-child"); |
544 | const previousSuggestion = | ||
545 | currentSuggestion?.previousSibling ?? | ||
546 | menu.querySelector(".container__suggestion:last-child"); | ||
547 | const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion; | ||
508 | 548 | ||
509 | // Current editor selection state | 549 | // Current editor selection state |
510 | const anchor = cm.getCursor() | 550 | const anchor = cm.getCursor(); |
511 | switch (e.key) { | 551 | switch (e.key) { |
512 | case 'Tab': | 552 | case "Tab": |
513 | Array.from(menu.children).forEach(s => s.classList.remove('focus')) | 553 | Array.from(menu.children).forEach(s => s.classList.remove("focus")); |
514 | focusSuggestion.classList.add('focus') | 554 | focusSuggestion.classList.add("focus"); |
515 | focusSuggestion.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) | 555 | focusSuggestion.scrollIntoView({ behavior: "smooth", block: "nearest" }); |
516 | break; | 556 | break; |
517 | case 'Enter': | 557 | case "Enter": |
518 | currentSuggestion.onclick() | 558 | currentSuggestion.onclick(); |
519 | break; | 559 | break; |
520 | case 'Escape': | 560 | case "Escape": |
521 | menu.style.display = 'none'; | 561 | menu.style.display = "none"; |
522 | // Focus editor again | 562 | // Focus editor again |
523 | setTimeout(() => cm.focus() && cm.setCursor(anchor), 100) | 563 | setTimeout(() => cm.focus() && cm.setCursor(anchor), 100); |
524 | break; | 564 | break; |
525 | } | 565 | } |
526 | }); | 566 | }); |
527 | 567 | ||
528 | document.onkeydown = (e) => { | 568 | document.onkeydown = e => { |
529 | if (e.altKey && e.ctrlKey && e.key === 'm') { | 569 | if (e.altKey && e.ctrlKey && e.key === "m") { |
530 | toggleEditing() | 570 | toggleEditing(); |
531 | e.preventDefault() | 571 | e.preventDefault(); |
532 | return null | 572 | return null; |
533 | } | 573 | } |
534 | 574 | ||
535 | if (!cm.hasFocus()) { | 575 | if (!cm.hasFocus()) { |
536 | if (e.key === 'F1') { | 576 | if (e.key === "F1") { |
537 | e.preventDefault() | 577 | e.preventDefault(); |
538 | cm.focus() | 578 | cm.focus(); |
539 | } | 579 | } |
540 | if (e.key === 'Tab') { | 580 | if (e.key === "Tab") { |
541 | e.preventDefault() | 581 | e.preventDefault(); |
542 | dumbymap.utils.focusNextMap(e.shiftKey) | 582 | dumbymap.utils.focusNextMap(e.shiftKey); |
543 | } | 583 | } |
544 | if (e.key === 'x' || e.key === 'X') { | 584 | if (e.key === "x" || e.key === "X") { |
545 | e.preventDefault() | 585 | e.preventDefault(); |
546 | dumbymap.utils.switchToNextLayout(e.shiftKey) | 586 | dumbymap.utils.switchToNextLayout(e.shiftKey); |
547 | } | 587 | } |
548 | if (e.key === 'n') { | 588 | if (e.key === "n") { |
549 | e.preventDefault() | 589 | e.preventDefault(); |
550 | dumbymap.utils.focusNextBlock() | 590 | dumbymap.utils.focusNextBlock(); |
551 | } | 591 | } |
552 | if (e.key === 'p') { | 592 | if (e.key === "p") { |
553 | e.preventDefault() | 593 | e.preventDefault(); |
554 | dumbymap.utils.focusNextBlock(true) | 594 | dumbymap.utils.focusNextBlock(true); |
555 | } | 595 | } |
556 | if (e.key === 'Escape') { | 596 | if (e.key === "Escape") { |
557 | e.preventDefault() | 597 | e.preventDefault(); |
558 | dumbymap.utils.removeBlockFocus() | 598 | dumbymap.utils.removeBlockFocus(); |
559 | } | 599 | } |
560 | } | 600 | } |
561 | } | 601 | }; |
562 | 602 | ||
563 | // }}} | 603 | // }}} |
564 | // }}} | 604 | // }}} |
565 | // Layout Switch {{{ | 605 | // Layout Switch {{{ |
566 | const layoutObserver = new MutationObserver(() => { | 606 | const layoutObserver = new MutationObserver(() => { |
567 | const layout = HtmlContainer.getAttribute('data-layout') | 607 | const layout = HtmlContainer.getAttribute("data-layout"); |
568 | if (layout !== 'normal') { | 608 | if (layout !== "normal") { |
569 | document.body.removeAttribute('data-mode') | 609 | document.body.removeAttribute("data-mode"); |
570 | } | 610 | } |
571 | }) | 611 | }); |
572 | 612 | ||
573 | layoutObserver.observe(HtmlContainer, { | 613 | layoutObserver.observe(HtmlContainer, { |
574 | attributes: true, | 614 | attributes: true, |
575 | attributeFilter: ["data-layout"], | 615 | attributeFilter: ["data-layout"], |
576 | attributeOldValue: true | 616 | attributeOldValue: true, |
577 | }); | 617 | }); |
578 | // }}} | 618 | // }}} |
579 | // ContextMenu {{{ | 619 | // ContextMenu {{{ |
580 | document.oncontextmenu = (e) => { | 620 | document.oncontextmenu = e => { |
581 | if (cm.hasFocus()) return | 621 | if (cm.hasFocus()) return; |
582 | 622 | ||
583 | const selection = document.getSelection() | 623 | const selection = document.getSelection(); |
584 | const range = selection.getRangeAt(0) | 624 | const range = selection.getRangeAt(0); |
585 | if (selection) { | 625 | if (selection) { |
586 | e.preventDefault() | 626 | e.preventDefault(); |
587 | menu.innerHTML = '' | 627 | menu.innerHTML = ""; |
588 | menu.style.cssText = `display: block; left: ${e.clientX + 10}px; top: ${e.clientY + 5}px;` | 628 | menu.style.cssText = `display: block; left: ${e.clientX + 10}px; top: ${e.clientY + 5}px;`; |
589 | const addGeoLink = new menuItem.GeoLink({ range }) | 629 | const addGeoLink = new menuItem.GeoLink({ range }); |
590 | menu.appendChild(addGeoLink.createElement()) | 630 | menu.appendChild(addGeoLink.createElement()); |
591 | } | 631 | } |
592 | menu.appendChild(menuItem.nextMap.bind(dumbymap)()) | 632 | menu.appendChild(menuItem.nextMap.bind(dumbymap)()); |
593 | menu.appendChild(menuItem.nextBlock.bind(dumbymap)()) | 633 | menu.appendChild(menuItem.nextBlock.bind(dumbymap)()); |
594 | menu.appendChild(menuItem.nextLayout.bind(dumbymap)()) | 634 | menu.appendChild(menuItem.nextLayout.bind(dumbymap)()); |
595 | } | 635 | }; |
596 | 636 | ||
597 | const actionOutsideMenu = (e) => { | 637 | const actionOutsideMenu = e => { |
598 | if (menu.style.display === 'none' || cm.hasFocus()) return | 638 | if (menu.style.display === "none" || cm.hasFocus()) return; |
599 | const rect = menu.getBoundingClientRect() | 639 | const rect = menu.getBoundingClientRect(); |
600 | if (e.clientX < rect.left | 640 | if ( |
601 | || e.clientX > rect.left + rect.width | 641 | e.clientX < rect.left || |
602 | || e.clientY < rect.top | 642 | e.clientX > rect.left + rect.width || |
603 | || e.clientY > rect.top + rect.height | 643 | e.clientY < rect.top || |
644 | e.clientY > rect.top + rect.height | ||
604 | ) { | 645 | ) { |
605 | menu.style.display = 'none' | 646 | menu.style.display = "none"; |
606 | } | 647 | } |
607 | } | 648 | }; |
608 | 649 | ||
609 | document.addEventListener('click', actionOutsideMenu) | 650 | document.addEventListener("click", actionOutsideMenu); |
610 | 651 | ||
611 | // }}} | 652 | // }}} |
612 | 653 | ||