diff options
| author | Hsieh Chin Fan <pham@topo.tw> | 2024-09-24 20:07:19 +0800 |
|---|---|---|
| committer | Hsieh Chin Fan <pham@topo.tw> | 2024-09-25 02:05:56 +0800 |
| commit | 54f0c9381fce41c4ca46baebfd6281a8f9f9ff93 (patch) | |
| tree | 4d81f07eeebf22a535d1e673d94856767910f724 /src | |
| parent | 34587b0d17cddaf5d5a11ffda4019acda9bc0864 (diff) | |
refactor: general menu
* rename suggestionEle -> menu for general use cases
* set Suggestion as module
Diffstat (limited to 'src')
| -rw-r--r-- | src/MenuItem.mjs | 33 | ||||
| -rw-r--r-- | src/css/index.css | 4 | ||||
| -rw-r--r-- | src/editor.mjs | 73 |
3 files changed, 58 insertions, 52 deletions
diff --git a/src/MenuItem.mjs b/src/MenuItem.mjs new file mode 100644 index 0000000..50afdb5 --- /dev/null +++ b/src/MenuItem.mjs | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | export class Suggestion { | ||
| 2 | constructor({ text, replace }) { | ||
| 3 | this.text = text | ||
| 4 | this.replace = replace | ||
| 5 | } | ||
| 6 | |||
| 7 | createElement(codemirror) { | ||
| 8 | const option = document.createElement('div'); | ||
| 9 | if (this.text.startsWith('<')) { | ||
| 10 | option.innerHTML = this.text; | ||
| 11 | } else { | ||
| 12 | option.innerText = this.text; | ||
| 13 | } | ||
| 14 | option.classList.add('container__suggestion'); | ||
| 15 | option.onmouseover = () => { | ||
| 16 | Array.from(menu.children).forEach(s => s.classList.remove('focus')) | ||
| 17 | option.classList.add('focus') | ||
| 18 | } | ||
| 19 | option.onmouseout = () => { | ||
| 20 | option.classList.remove('focus') | ||
| 21 | } | ||
| 22 | option.onclick = () => { | ||
| 23 | const anchor = codemirror.getCursor() | ||
| 24 | codemirror.setSelection(anchor, { ...anchor, ch: 0 }) | ||
| 25 | codemirror.replaceSelection(this.replace) | ||
| 26 | codemirror.focus(); | ||
| 27 | const newAnchor = { ...anchor, ch: this.replace.length } | ||
| 28 | codemirror.setCursor(newAnchor); | ||
| 29 | }; | ||
| 30 | |||
| 31 | return option | ||
| 32 | } | ||
| 33 | } | ||
diff --git a/src/css/index.css b/src/css/index.css index 47c5c1e..4737535 100644 --- a/src/css/index.css +++ b/src/css/index.css | |||
| @@ -106,14 +106,14 @@ body { | |||
| 106 | } | 106 | } |
| 107 | } | 107 | } |
| 108 | 108 | ||
| 109 | .container__suggestions { | 109 | #menu { |
| 110 | display: none; | 110 | display: none; |
| 111 | width: fit-content; | 111 | width: fit-content; |
| 112 | 112 | ||
| 113 | position: absolute; | 113 | position: absolute; |
| 114 | z-index: 100; | 114 | z-index: 100; |
| 115 | 115 | ||
| 116 | border: 2px solid lightgray; | 116 | border: 2px solid gray; |
| 117 | border-radius: 0.5rem; | 117 | border-radius: 0.5rem; |
| 118 | 118 | ||
| 119 | background: white; | 119 | background: white; |
diff --git a/src/editor.mjs b/src/editor.mjs index 9ff8be6..92513a3 100644 --- a/src/editor.mjs +++ b/src/editor.mjs | |||
| @@ -1,8 +1,8 @@ | |||
| 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, createDocLinks } from './dumbymap' |
| 4 | import { defaultAliases, parseConfigsFromYaml } from 'mapclay' | 4 | import { defaultAliases, parseConfigsFromYaml } from 'mapclay' |
| 5 | import { createDocLinks } from './dumbymap.mjs' | 5 | import { Suggestion } from './MenuItem' |
| 6 | 6 | ||
| 7 | // Set up Containers {{{ | 7 | // Set up Containers {{{ |
| 8 | 8 | ||
| @@ -214,18 +214,13 @@ window.onhashchange = () => { | |||
| 214 | // }}} | 214 | // }}} |
| 215 | // Completion in Code Blok {{{ | 215 | // Completion in Code Blok {{{ |
| 216 | // Elements about suggestions {{{ | 216 | // Elements about suggestions {{{ |
| 217 | const suggestionsEle = document.createElement('div') | 217 | const menu = document.createElement('div') |
| 218 | suggestionsEle.classList.add('container__suggestions'); | 218 | menu.id = 'menu' |
| 219 | document.body.append(suggestionsEle) | 219 | menu.onclick = () => menu.style.display = 'none' |
| 220 | document.body.append(menu) | ||
| 220 | 221 | ||
| 221 | const rendererOptions = {} | 222 | const rendererOptions = {} |
| 222 | 223 | ||
| 223 | class Suggestion { | ||
| 224 | constructor({ text, replace }) { | ||
| 225 | this.text = text | ||
| 226 | this.replace = replace | ||
| 227 | } | ||
| 228 | } | ||
| 229 | // }}} | 224 | // }}} |
| 230 | // Aliases for map options {{{ | 225 | // Aliases for map options {{{ |
| 231 | const aliasesForMapOptions = {} | 226 | const aliasesForMapOptions = {} |
| @@ -460,51 +455,29 @@ const getSuggestions = (anchor) => { | |||
| 460 | const addSuggestions = (anchor, suggestions) => { | 455 | const addSuggestions = (anchor, suggestions) => { |
| 461 | 456 | ||
| 462 | if (suggestions.length === 0) { | 457 | if (suggestions.length === 0) { |
| 463 | suggestionsEle.style.display = 'none'; | 458 | menu.style.display = 'none'; |
| 464 | return | 459 | return |
| 465 | } else { | 460 | } else { |
| 466 | suggestionsEle.style.display = 'block'; | 461 | menu.style.display = 'block'; |
| 467 | } | 462 | } |
| 468 | 463 | ||
| 469 | suggestionsEle.innerHTML = '' | 464 | menu.innerHTML = '' |
| 470 | suggestions.forEach((suggestion) => { | 465 | suggestions |
| 471 | const option = document.createElement('div'); | 466 | .map(s => s.createElement(cm)) |
| 472 | if (suggestion.text.startsWith('<')) { | 467 | .forEach(option => menu.appendChild(option)) |
| 473 | option.innerHTML = suggestion.text; | ||
| 474 | } else { | ||
| 475 | option.innerText = suggestion.text; | ||
| 476 | } | ||
| 477 | option.classList.add('container__suggestion'); | ||
| 478 | option.onmouseover = () => { | ||
| 479 | Array.from(suggestionsEle.children).forEach(s => s.classList.remove('focus')) | ||
| 480 | option.classList.add('focus') | ||
| 481 | } | ||
| 482 | option.onmouseout = () => { | ||
| 483 | option.classList.remove('focus') | ||
| 484 | } | ||
| 485 | option.onclick = () => { | ||
| 486 | cm.setSelection(anchor, { ...anchor, ch: 0 }) | ||
| 487 | cm.replaceSelection(suggestion.replace) | ||
| 488 | cm.focus(); | ||
| 489 | const newAnchor = { ...anchor, ch: suggestion.replace.length } | ||
| 490 | cm.setCursor(newAnchor); | ||
| 491 | }; | ||
| 492 | suggestionsEle.appendChild(option); | ||
| 493 | }); | ||
| 494 | 468 | ||
| 495 | const widgetAnchor = document.createElement('div') | 469 | const widgetAnchor = document.createElement('div') |
| 496 | cm.addWidget(anchor, widgetAnchor, true) | 470 | cm.addWidget(anchor, widgetAnchor, true) |
| 497 | const rect = widgetAnchor.getBoundingClientRect() | 471 | const rect = widgetAnchor.getBoundingClientRect() |
| 498 | suggestionsEle.style.left = `calc(${rect.left}px + 2rem)`; | 472 | menu.style.left = `calc(${rect.left}px + 2rem)`; |
| 499 | suggestionsEle.style.top = `calc(${rect.bottom}px + 1rem)`; | 473 | menu.style.top = `calc(${rect.bottom}px + 1rem)`; |
| 500 | suggestionsEle.style.maxWidth = `calc(${window.innerWidth}px - ${rect.x}px - 3rem)`; | 474 | menu.style.maxWidth = `calc(${window.innerWidth}px - ${rect.x}px - 3rem)`; |
| 501 | suggestionsEle.style.display = 'block' | 475 | menu.style.display = 'block' |
| 502 | } | 476 | } |
| 503 | // }}} | 477 | // }}} |
| 504 | // EVENT: Suggests for current selection {{{ | 478 | // EVENT: Suggests for current selection {{{ |
| 505 | // FIXME Dont show suggestion when selecting multiple chars | 479 | // FIXME Dont show suggestion when selecting multiple chars |
| 506 | cm.on("cursorActivity", (_) => { | 480 | cm.on("cursorActivity", (_) => { |
| 507 | suggestionsEle.style.display = 'none' | ||
| 508 | const anchor = cm.getCursor() | 481 | const anchor = cm.getCursor() |
| 509 | 482 | ||
| 510 | if (insideCodeblockForMap(anchor)) { | 483 | if (insideCodeblockForMap(anchor)) { |
| @@ -512,7 +485,7 @@ cm.on("cursorActivity", (_) => { | |||
| 512 | } | 485 | } |
| 513 | }); | 486 | }); |
| 514 | cm.on("blur", () => { | 487 | cm.on("blur", () => { |
| 515 | suggestionsEle.style.display = 'none' | 488 | menu.style.display = 'none' |
| 516 | cm.getWrapperElement().classList.remove('focus') | 489 | cm.getWrapperElement().classList.remove('focus') |
| 517 | HtmlContainer.classList.add('focus') | 490 | HtmlContainer.classList.add('focus') |
| 518 | }) | 491 | }) |
| @@ -520,25 +493,25 @@ cm.on("blur", () => { | |||
| 520 | // EVENT: keydown for suggestions {{{ | 493 | // EVENT: keydown for suggestions {{{ |
| 521 | const keyForSuggestions = ['Tab', 'Enter', 'Escape'] | 494 | const keyForSuggestions = ['Tab', 'Enter', 'Escape'] |
| 522 | cm.on('keydown', (_, e) => { | 495 | cm.on('keydown', (_, e) => { |
| 523 | if (!cm.hasFocus || !keyForSuggestions.includes(e.key) || suggestionsEle.style.display === 'none') return; | 496 | if (!cm.hasFocus || !keyForSuggestions.includes(e.key) || menu.style.display === 'none') return; |
| 524 | 497 | ||
| 525 | // Directly add a newline when no suggestion is selected | 498 | // Directly add a newline when no suggestion is selected |
| 526 | const currentSuggestion = suggestionsEle.querySelector('.container__suggestion.focus') | 499 | const currentSuggestion = menu.querySelector('.container__suggestion.focus') |
| 527 | if (!currentSuggestion && e.key === 'Enter') return | 500 | if (!currentSuggestion && e.key === 'Enter') return |
| 528 | 501 | ||
| 529 | // Override default behavior | 502 | // Override default behavior |
| 530 | e.preventDefault(); | 503 | e.preventDefault(); |
| 531 | 504 | ||
| 532 | // Suggestion when pressing Tab or Shift + Tab | 505 | // Suggestion when pressing Tab or Shift + Tab |
| 533 | const nextSuggestion = currentSuggestion?.nextSibling ?? suggestionsEle.querySelector('.container__suggestion:first-child') | 506 | const nextSuggestion = currentSuggestion?.nextSibling ?? menu.querySelector('.container__suggestion:first-child') |
| 534 | const previousSuggestion = currentSuggestion?.previousSibling ?? suggestionsEle.querySelector('.container__suggestion:last-child') | 507 | const previousSuggestion = currentSuggestion?.previousSibling ?? menu.querySelector('.container__suggestion:last-child') |
| 535 | const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion | 508 | const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion |
| 536 | 509 | ||
| 537 | // Current editor selection state | 510 | // Current editor selection state |
| 538 | const anchor = cm.getCursor() | 511 | const anchor = cm.getCursor() |
| 539 | switch (e.key) { | 512 | switch (e.key) { |
| 540 | case 'Tab': | 513 | case 'Tab': |
| 541 | Array.from(suggestionsEle.children).forEach(s => s.classList.remove('focus')) | 514 | Array.from(menu.children).forEach(s => s.classList.remove('focus')) |
| 542 | focusSuggestion.classList.add('focus') | 515 | focusSuggestion.classList.add('focus') |
| 543 | focusSuggestion.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) | 516 | focusSuggestion.scrollIntoView({ behavior: 'smooth', block: 'nearest' }) |
| 544 | break; | 517 | break; |
| @@ -546,7 +519,7 @@ cm.on('keydown', (_, e) => { | |||
| 546 | currentSuggestion.onclick() | 519 | currentSuggestion.onclick() |
| 547 | break; | 520 | break; |
| 548 | case 'Escape': | 521 | case 'Escape': |
| 549 | suggestionsEle.style.display = 'none'; | 522 | menu.style.display = 'none'; |
| 550 | // Focus editor again | 523 | // Focus editor again |
| 551 | setTimeout(() => cm.focus() && cm.setCursor(anchor), 100) | 524 | setTimeout(() => cm.focus() && cm.setCursor(anchor), 100) |
| 552 | break; | 525 | break; |