aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/editor.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'src/editor.mjs')
-rw-r--r--src/editor.mjs655
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"*/
3import { markdown2HTML, generateMaps } from './dumbymap' 3import { markdown2HTML, generateMaps } from "./dumbymap";
4import { defaultAliases, parseConfigsFromYaml } from 'mapclay' 4import { defaultAliases, parseConfigsFromYaml } from "mapclay";
5import * as menuItem from './MenuItem' 5import * as menuItem from "./MenuItem";
6 6
7// Set up Containers {{{ 7// Set up Containers {{{
8 8
9const HtmlContainer = document.querySelector(".DumbyMap") 9const HtmlContainer = document.querySelector(".DumbyMap");
10const textArea = document.querySelector(".editor textarea") 10const textArea = document.querySelector(".editor textarea");
11let dumbymap 11let dumbymap;
12 12
13const toggleEditing = () => { 13const 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
26const 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' 26const 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';
27const editor = new EasyMDE({ 28const 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
68const cm = editor.codemirror 88const cm = editor.codemirror;
69 89
70const getStateFromHash = (hash) => { 90const 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
77const getContentFromHash = (hash) => { 100const getContentFromHash = hash => {
78 const state = getStateFromHash(hash) 101 const state = getStateFromHash(hash);
79 return state.content 102 return state.content;
80} 103};
81 104
82const initialState = getStateFromHash(window.location.hash) 105const initialState = getStateFromHash(window.location.hash);
83window.location.hash = '' 106window.location.hash = "";
84const contentFromHash = initialState.content 107const 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
87if (contentFromHash) { 110if (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 {{{
93const afterMapRendered = (_) => { 116const 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};
100markdown2HTML(HtmlContainer, editor.value()) 123markdown2HTML(HtmlContainer, editor.value());
101dumbymap = generateMaps(HtmlContainer, afterMapRendered) 124dumbymap = generateMaps(HtmlContainer, afterMapRendered);
102 125
103// Quick hack to style lines inside code block 126// Quick hack to style lines inside code block
104const addClassToCodeLines = () => { 127const 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};
117addClassToCodeLines() 140addClassToCodeLines();
118 141
119const completeForCodeBlock = (change) => { 142const 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
156const debounceForMap = (() => { 183const 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
168const updateDumbyMap = () => { 195const 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
175updateDumbyMap() 202updateDumbyMap();
176 203
177// Re-render HTML by editor content 204// Re-render HTML by editor content
178cm.on("change", (_, change) => { 205cm.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
185cm.on("focus", () => { 212cm.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
190cm.on("beforeChange", (_, change) => { 217cm.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
201window.onhashchange = () => { 228window.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 {{{
215const menu = document.createElement('div') 242const menu = document.createElement("div");
216menu.id = 'menu' 243menu.id = "menu";
217menu.onclick = () => menu.style.display = 'none' 244menu.onclick = () => (menu.style.display = "none");
218document.body.append(menu) 245document.body.append(menu);
219 246
220const rendererOptions = {} 247const rendererOptions = {};
221 248
222// }}} 249// }}}
223// Aliases for map options {{{ 250// Aliases for map options {{{
224const aliasesForMapOptions = {} 251const aliasesForMapOptions = {};
225const defaultApply = './dist/default.yml' 252const defaultApply = "./dist/default.yml";
226fetch(defaultApply) 253fetch(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 {{{
235const insideCodeblockForMap = (anchor) => { 262const 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 {{{
254const getLineWithRenderer = (anchor) => { 283const 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 {{{
290const getSuggestionsForOptions = (optionTyped, validOptions) => { 318const 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 {{{
310const getSuggestionFromMapOption = (option) => { 341const 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 {{{
324const getSuggestionsFromAliases = (option) => Object.entries(aliasesForMapOptions[option.valueOf()] ?? {}) 355const 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 {{{
336const handleTypingInCodeBlock = (anchor) => { 366const 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 {{{
349const getSuggestions = (anchor) => { 379const 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
453const addSuggestions = (anchor, suggestions) => { 485const 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
478cm.on("cursorActivity", (_) => { 509cm.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});
486cm.on("blur", () => { 517cm.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 {{{
493const keyForSuggestions = ['Tab', 'Enter', 'Escape'] 524const keyForSuggestions = ["Tab", "Enter", "Escape"];
494cm.on('keydown', (_, e) => { 525cm.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
528document.onkeydown = (e) => { 568document.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 {{{
566const layoutObserver = new MutationObserver(() => { 606const 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
573layoutObserver.observe(HtmlContainer, { 613layoutObserver.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 {{{
580document.oncontextmenu = (e) => { 620document.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
597const actionOutsideMenu = (e) => { 637const 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
609document.addEventListener('click', actionOutsideMenu) 650document.addEventListener("click", actionOutsideMenu);
610 651
611// }}} 652// }}}
612 653