aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/css/dumbymap.css23
-rw-r--r--src/css/easymde.min.css7
-rw-r--r--src/css/index.css81
-rw-r--r--src/dumbymap.mjs101
-rw-r--r--src/editor.mjs301
5 files changed, 279 insertions, 234 deletions
diff --git a/src/css/dumbymap.css b/src/css/dumbymap.css
index d31c97e..383c4ec 100644
--- a/src/css/dumbymap.css
+++ b/src/css/dumbymap.css
@@ -45,7 +45,7 @@
45 } 45 }
46 } 46 }
47 47
48 #markdown { 48 .SemanticHtml {
49 flex: 1; 49 flex: 1;
50 padding: 20px; 50 padding: 20px;
51 overflow-y: scroll; 51 overflow-y: scroll;
@@ -59,20 +59,19 @@
59 background-color: white; 59 background-color: white;
60 margin-bottom: 3rem; 60 margin-bottom: 3rem;
61 61
62 * {
63 width: fit-content;
64 }
65
66 .draggable { 62 .draggable {
67 display: none; 63 display: none;
68 } 64 }
69 } 65 }
70 66
71 pre:has(.map-container) { 67 pre {
72 display: flex;
73 justify-content: flex-start;
74 width: 100%; 68 width: 100%;
75 background-color: inherit; 69
70 &:has(.map-container) {
71 display: flex;
72 justify-content: flex-start;
73 background-color: inherit;
74 }
76 } 75 }
77 76
78 .map-container { 77 .map-container {
@@ -122,7 +121,7 @@
122 121
123.result-html[data-layout=side] { 122.result-html[data-layout=side] {
124 #map, 123 #map,
125 #markdown { 124 .SemanticHtml {
126 flex: 50%; 125 flex: 50%;
127 overflow-y: scroll; 126 overflow-y: scroll;
128 height: 100vh; 127 height: 100vh;
@@ -131,13 +130,13 @@
131 130
132.result-html[data-layout=overlay] { 131.result-html[data-layout=overlay] {
133 #map, 132 #map,
134 #markdown { 133 .SemanticHtml {
135 position: fixed; 134 position: fixed;
136 height: 100%; 135 height: 100%;
137 width: 100%; 136 width: 100%;
138 } 137 }
139 138
140 #markdown { 139 .SemanticHtml {
141 font-size: 12px; 140 font-size: 12px;
142 pointer-events: none; 141 pointer-events: none;
143 142
diff --git a/src/css/easymde.min.css b/src/css/easymde.min.css
new file mode 100644
index 0000000..cb01784
--- /dev/null
+++ b/src/css/easymde.min.css
@@ -0,0 +1,7 @@
1/**
2 * easymde v2.18.0
3 * Copyright Jeroen Akkerman
4 * @link https://github.com/ionaru/easy-markdown-editor
5 * @license MIT
6 */
7.CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor .CodeMirror-line::selection,.cm-fat-cursor .CodeMirror-line>span::selection,.cm-fat-cursor .CodeMirror-line>span>span::selection{background:0 0}.cm-fat-cursor .CodeMirror-line::-moz-selection,.cm-fat-cursor .CodeMirror-line>span::-moz-selection,.cm-fat-cursor .CodeMirror-line>span>span::-moz-selection{background:0 0}.cm-fat-cursor{caret-color:transparent}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:0;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-type,.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta{color:#555}.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error{color:red}.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-50px;margin-right:-50px;padding-bottom:50px;height:100%;outline:0;position:relative;z-index:0}.CodeMirror-sizer{position:relative;border-right:50px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none;outline:0}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-50px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre.CodeMirror-line,.CodeMirror-wrap pre.CodeMirror-line-like{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.EasyMDEContainer{display:block}.CodeMirror-rtl pre{direction:rtl}.EasyMDEContainer.sided--no-fullscreen{display:flex;flex-direction:row;flex-wrap:wrap}.EasyMDEContainer .CodeMirror{box-sizing:border-box;height:auto;border:1px solid #ced4da;border-bottom-left-radius:4px;border-bottom-right-radius:4px;padding:10px;font:inherit;z-index:0;word-wrap:break-word}.EasyMDEContainer .CodeMirror-scroll{cursor:text}.EasyMDEContainer .CodeMirror-fullscreen{background:#fff;position:fixed!important;top:50px;left:0;right:0;bottom:0;height:auto;z-index:8;border-right:none!important;border-bottom-right-radius:0!important}.EasyMDEContainer .CodeMirror-sided{width:50%!important}.EasyMDEContainer.sided--no-fullscreen .CodeMirror-sided{border-right:none!important;border-bottom-right-radius:0;position:relative;flex:1 1 auto}.EasyMDEContainer .CodeMirror-placeholder{opacity:.5}.EasyMDEContainer .CodeMirror-focused .CodeMirror-selected{background:#d9d9d9}.editor-toolbar{position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;padding:9px 10px;border-top:1px solid #ced4da;border-left:1px solid #ced4da;border-right:1px solid #ced4da;border-top-left-radius:4px;border-top-right-radius:4px}.editor-toolbar.fullscreen{width:100%;height:50px;padding-top:10px;padding-bottom:10px;box-sizing:border-box;background:#fff;border:0;position:fixed;top:0;left:0;opacity:1;z-index:9}.editor-toolbar.fullscreen::before{width:20px;height:50px;background:-moz-linear-gradient(left,#fff 0,rgba(255,255,255,0) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,#fff),color-stop(100%,rgba(255,255,255,0)));background:-webkit-linear-gradient(left,#fff 0,rgba(255,255,255,0) 100%);background:-o-linear-gradient(left,#fff 0,rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left,#fff 0,rgba(255,255,255,0) 100%);background:linear-gradient(to right,#fff 0,rgba(255,255,255,0) 100%);position:fixed;top:0;left:0;margin:0;padding:0}.editor-toolbar.fullscreen::after{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,0) 0,#fff 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,0)),color-stop(100%,#fff));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0,#fff 100%);background:-o-linear-gradient(left,rgba(255,255,255,0) 0,#fff 100%);background:-ms-linear-gradient(left,rgba(255,255,255,0) 0,#fff 100%);background:linear-gradient(to right,rgba(255,255,255,0) 0,#fff 100%);position:fixed;top:0;right:0;margin:0;padding:0}.EasyMDEContainer.sided--no-fullscreen .editor-toolbar{width:100%}.editor-toolbar .easymde-dropdown,.editor-toolbar button{background:0 0;display:inline-block;text-align:center;text-decoration:none!important;height:30px;margin:0;padding:0;border:1px solid transparent;border-radius:3px;cursor:pointer}.editor-toolbar button{font-weight:700;min-width:30px;padding:0 6px;white-space:nowrap}.editor-toolbar button.active,.editor-toolbar button:hover{background:#fcfcfc;border-color:#95a5a6}.editor-toolbar i.separator{display:inline-block;width:0;border-left:1px solid #d9d9d9;border-right:1px solid #fff;color:transparent;text-indent:-10px;margin:0 6px}.editor-toolbar button:after{font-family:Arial,"Helvetica Neue",Helvetica,sans-serif;font-size:65%;vertical-align:text-bottom;position:relative;top:2px}.editor-toolbar button.heading-1:after{content:"1"}.editor-toolbar button.heading-2:after{content:"2"}.editor-toolbar button.heading-3:after{content:"3"}.editor-toolbar button.heading-bigger:after{content:"▲"}.editor-toolbar button.heading-smaller:after{content:"▼"}.editor-toolbar.disabled-for-preview button:not(.no-disable){opacity:.6;pointer-events:none}@media only screen and (max-width:700px){.editor-toolbar i.no-mobile{display:none}}.editor-statusbar{padding:8px 10px;font-size:12px;color:#959694;text-align:right}.EasyMDEContainer.sided--no-fullscreen .editor-statusbar{width:100%}.editor-statusbar span{display:inline-block;min-width:4em;margin-left:1em}.editor-statusbar .lines:before{content:'lines: '}.editor-statusbar .words:before{content:'words: '}.editor-statusbar .characters:before{content:'characters: '}.editor-preview-full{position:absolute;width:100%;height:100%;top:0;left:0;z-index:7;overflow:auto;display:none;box-sizing:border-box}.editor-preview-side{position:fixed;bottom:0;width:50%;top:50px;right:0;z-index:9;overflow:auto;display:none;box-sizing:border-box;border:1px solid #ddd;word-wrap:break-word}.editor-preview-active-side{display:block}.EasyMDEContainer.sided--no-fullscreen .editor-preview-active-side{flex:1 1 auto;height:auto;position:static}.editor-preview-active{display:block}.editor-preview{padding:10px;background:#fafafa}.editor-preview>p{margin-top:0}.editor-preview pre{background:#eee;margin-bottom:10px}.editor-preview table td,.editor-preview table th{border:1px solid #ddd;padding:5px}.cm-s-easymde .cm-tag{color:#63a35c}.cm-s-easymde .cm-attribute{color:#795da3}.cm-s-easymde .cm-string{color:#183691}.cm-s-easymde .cm-header-1{font-size:calc(1.375rem + 1.5vw)}.cm-s-easymde .cm-header-2{font-size:calc(1.325rem + .9vw)}.cm-s-easymde .cm-header-3{font-size:calc(1.3rem + .6vw)}.cm-s-easymde .cm-header-4{font-size:calc(1.275rem + .3vw)}.cm-s-easymde .cm-header-5{font-size:1.25rem}.cm-s-easymde .cm-header-6{font-size:1rem}.cm-s-easymde .cm-header-1,.cm-s-easymde .cm-header-2,.cm-s-easymde .cm-header-3,.cm-s-easymde .cm-header-4,.cm-s-easymde .cm-header-5,.cm-s-easymde .cm-header-6{margin-bottom:.5rem;line-height:1.2}.cm-s-easymde .cm-comment{background:rgba(0,0,0,.05);border-radius:2px}.cm-s-easymde .cm-link{color:#7f8c8d}.cm-s-easymde .cm-url{color:#aab2b3}.cm-s-easymde .cm-quote{color:#7f8c8d;font-style:italic}.editor-toolbar .easymde-dropdown{position:relative;background:linear-gradient(to bottom right,#fff 0,#fff 84%,#333 50%,#333 100%);border-radius:0;border:1px solid #fff}.editor-toolbar .easymde-dropdown:hover{background:linear-gradient(to bottom right,#fff 0,#fff 84%,#333 50%,#333 100%)}.easymde-dropdown-content{display:block;visibility:hidden;position:absolute;background-color:#f9f9f9;box-shadow:0 8px 16px 0 rgba(0,0,0,.2);padding:8px;z-index:2;top:30px}.easymde-dropdown:active .easymde-dropdown-content,.easymde-dropdown:focus .easymde-dropdown-content,.easymde-dropdown:focus-within .easymde-dropdown-content{visibility:visible}.easymde-dropdown-content button{display:block}span[data-img-src]::after{content:'';background-image:var(--bg-image);display:block;max-height:100%;max-width:100%;background-size:contain;height:0;padding-top:var(--height);width:var(--width);background-repeat:no-repeat}.CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word){background:rgba(255,0,0,.15)}
diff --git a/src/css/index.css b/src/css/index.css
index adc1cfe..3764dac 100644
--- a/src/css/index.css
+++ b/src/css/index.css
@@ -1,34 +1,70 @@
1:root {
2 --content-border: solid lightgray 2px;
3 --content-border-radius: 5px;
4}
5
1body { 6body {
2 display: flex; 7 display: flex;
3 justify-items: stretch; 8 justify-items: stretch;
9 width: 100%;
4} 10}
5 11
6.result-html { 12.result-html {
7 flex: 1; 13 flex: 0 0 50%;
8 height: calc(100vh - 20px); 14 border: var(--content-border);
9 border: solid lightgray 2px; 15 border-radius: var(--content-border-radius);
10 border-radius: 5px;
11 margin: 10px; 16 margin: 10px;
12} 17}
13 18
14.editor { 19.editor {
15 flex: 1; 20 flex: 0 0 50%;
16 max-width: 50vw; 21 max-width: 50vw;
22 height: calc(100vh - 15px);
17 margin: 10px; 23 margin: 10px;
18} 24}
19 25
20.TinyMDE { 26.EasyMDEContainer {
21 height: calc(100vh - 55px); 27 display: flex;
22 padding: 16px; 28 flex-direction: column;
23 overflow-y: scroll; 29 height: calc(100vh - 20px);
24 border: 2px solid lightgray; 30 box-sizing: border-box;
25 border-radius: 5px; 31
32 .CodeMirror {
33 order: 1;
34 flex-grow: 1;
35 border: var(--content-border);
36 border-radius: var(--content-border-radius);
37 padding-inline: 0;
38
39 span {
40 white-space: pre;
41 }
42 .invalid-input {
43 text-decoration: red wavy underline 1px;
44 }
45 }
46
47 .editor-toolbar {
48 order: 2;
49 margin-top: 0.5rem;
50 border-left: 1px solid #ced4da;
51 border-right: 1px solid #ced4da;
52 border-bottom: 1px solid #ced4da;
53 border-bottom-left-radius: 4px;
54 border-bottom-right-radius: 4px;
55 }
26 56
27 span { 57 .editor-statusbar {
28 white-space: pre; 58 order: 3;
29 } 59 }
30 .invalid-input { 60}
31 text-decoration: red wavy underline 1px; 61
62/* FIXME For those empty line (no child with cm-comment) */
63.CodeMirror-line:has(.cm-comment) {
64 background: rgba(0,0,0,.05) !important;
65
66 .cm-comment {
67 background: none !important;
32 } 68 }
33} 69}
34 70
@@ -41,6 +77,7 @@ body {
41 overflow-y: scroll; 77 overflow-y: scroll;
42 border: 2px solid lightgray; 78 border: 2px solid lightgray;
43 border-radius: 0.5rem; 79 border-radius: 0.5rem;
80 z-index: 100;
44 81
45 background: #fff; 82 background: #fff;
46} 83}
@@ -55,6 +92,13 @@ body {
55 white-space: nowrap; 92 white-space: nowrap;
56 align-items: center; 93 align-items: center;
57 94
95 &:not(:first-child) {
96 border-top: 1px solid rgb(203 213 225);
97 }
98 &.focus {
99 background: rgb(226 232 240);
100 }
101
58 * { 102 * {
59 flex-shrink: 0; 103 flex-shrink: 0;
60 display: inline-block; 104 display: inline-block;
@@ -66,15 +110,10 @@ body {
66 font-weight: bold; 110 font-weight: bold;
67 } 111 }
68 .truncate { 112 .truncate {
113 flex-shrink: 1;
69 text-overflow: ellipsis; 114 text-overflow: ellipsis;
70 ::before { 115 ::before {
71 width: 2rem 116 width: 2rem
72 } 117 }
73 } 118 }
74} 119}
75.container__suggestion:not(:first-child) {
76 border-top: 1px solid rgb(203 213 225);
77}
78.container__suggestion.focus {
79 background: rgb(226 232 240);
80}
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs
index b95e7a9..cd3b8ff 100644
--- a/src/dumbymap.mjs
+++ b/src/dumbymap.mjs
@@ -1,4 +1,3 @@
1// vim:foldmethod
2import MarkdownIt from 'markdown-it' 1import MarkdownIt from 'markdown-it'
3import MarkdownItAnchor from 'markdown-it-anchor' 2import MarkdownItAnchor from 'markdown-it-anchor'
4import MarkdownItFootnote from 'markdown-it-footnote' 3import MarkdownItFootnote from 'markdown-it-footnote'
@@ -8,15 +7,30 @@ import LeaderLine from 'leader-line'
8import PlainDraggable from 'plain-draggable' 7import PlainDraggable from 'plain-draggable'
9import { render, parseConfigsFromYaml } from 'mapclay' 8import { render, parseConfigsFromYaml } from 'mapclay'
10 9
11const observers = new Map() 10function onRemove(element, callback) {
11 const parent = element.parentNode;
12 if (!parent) throw new Error("The node must already be attached");
12 13
13export const markdown2HTML = async (container, mdContent) => { 14 const obs = new MutationObserver(mutations => {
14 // Render: Markdown -> HTML {{{ 15 for (const mutation of mutations) {
16 for (const el of mutation.removedNodes) {
17 if (el === element) {
18 obs.disconnect();
19 callback();
20 }
21 }
22 }
23 });
24 obs.observe(parent, { childList: true, });
25}
26
27// Render: Markdown -> HTML {{{
28export const markdown2HTML = (container, mdContent) => {
29
30 Array.from(container.children).map(e => e.remove())
15 31
16 container.innerHTML = ` 32 container.innerHTML = '<div class="SemanticHtml"></div>'
17 <div id="map"></div> 33 const htmlHolder = container.querySelector('.SemanticHtml')
18 <div id="markdown"></div>
19 `
20 34
21 const md = MarkdownIt({ html: true }) 35 const md = MarkdownIt({ html: true })
22 .use(MarkdownItAnchor, { 36 .use(MarkdownItAnchor, {
@@ -46,15 +60,15 @@ export const markdown2HTML = async (container, mdContent) => {
46 state.tokens.push(new state.Token('draggable_block_close', '', -1)) 60 state.tokens.push(new state.Token('draggable_block_close', '', -1))
47 }) 61 })
48 62
49 const markdown = container.querySelector('#markdown')
50 const contentWithToc = '${toc}\n\n\n' + mdContent 63 const contentWithToc = '${toc}\n\n\n' + mdContent
51 markdown.innerHTML = md.render(contentWithToc); 64 htmlHolder.innerHTML = md.render(contentWithToc);
52 markdown.querySelectorAll('*> div:not(:has(nav))') 65 // TODO Do this in markdown-it
66 htmlHolder.querySelectorAll('*> div:not(:has(nav))')
53 .forEach(b => b.classList.add('draggable-block')) 67 .forEach(b => b.classList.add('draggable-block'))
54 68
55 69
56 // TODO Improve it! 70 // TODO Improve it!
57 const docLinks = Array.from(container.querySelectorAll('#markdown a[href^="#"][title^="doc"]')) 71 const docLinks = Array.from(container.querySelectorAll('a[href^="#"][title^="doc"]'))
58 docLinks.forEach(link => { 72 docLinks.forEach(link => {
59 link.classList.add('with-leader-line', 'doclink') 73 link.classList.add('with-leader-line', 'doclink')
60 link.lines = [] 74 link.lines = []
@@ -77,24 +91,29 @@ export const markdown2HTML = async (container, mdContent) => {
77 link.lines.length = 0 91 link.lines.length = 0
78 } 92 }
79 }) 93 })
94
95 return container
80 //}}} 96 //}}}
81} 97}
82 98
99// FIXME Don't use hard-coded CSS selector
83export const generateMaps = async (container) => { 100export const generateMaps = async (container) => {
84 // LeaderLine {{{ 101 // LeaderLine {{{
85 102
86 // Get anchors with "geo:" scheme 103 // Get anchors with "geo:" scheme
87 const markdown = container.querySelector('#markdown') 104 const htmlHolder = container.querySelector('.SemanticHtml') ?? container
88 markdown.anchors = [] 105 htmlHolder.anchors = []
89 106
90 // Set focusArea 107 // Set focusArea
91 const focusArea = container.querySelector('#map') 108 const showcase = document.createElement('div')
109 container.appendChild(showcase)
110 showcase.classList.add('Showcase')
92 const mapPlaceholder = document.createElement('div') 111 const mapPlaceholder = document.createElement('div')
93 mapPlaceholder.id = 'mapPlaceholder' 112 mapPlaceholder.id = 'mapPlaceholder'
94 focusArea.appendChild(mapPlaceholder) 113 showcase.appendChild(mapPlaceholder)
95 114
96 // Links points to map by geo schema and id 115 // Links points to map by geo schema and id
97 const geoLinks = Array.from(container.querySelectorAll('#markdown a[href^="geo:"]')) 116 const geoLinks = Array.from(htmlHolder.querySelectorAll('a[href^="geo:"]'))
98 .filter(link => { 117 .filter(link => {
99 const url = new URL(link.href) 118 const url = new URL(link.href)
100 const xy = url?.href?.match(/^geo:([0-9.,]+)/)?.at(1)?.split(',')?.reverse()?.map(Number) 119 const xy = url?.href?.match(/^geo:([0-9.,]+)/)?.at(1)?.split(',')?.reverse()?.map(Number)
@@ -113,7 +132,7 @@ export const generateMaps = async (container) => {
113 link.onmouseout = () => removeLeaderLines(link) 132 link.onmouseout = () => removeLeaderLines(link)
114 link.onclick = (event) => { 133 link.onclick = (event) => {
115 event.preventDefault() 134 event.preventDefault()
116 markdown.anchors 135 htmlHolder.anchors
117 .filter(isAnchorPointedBy(link)) 136 .filter(isAnchorPointedBy(link))
118 .forEach(updateMapByMarker(xy)) 137 .forEach(updateMapByMarker(xy))
119 // TODO Just hide leader line and show it again 138 // TODO Just hide leader line and show it again
@@ -147,7 +166,7 @@ export const generateMaps = async (container) => {
147 } 166 }
148 167
149 const addLeaderLines = (link) => { 168 const addLeaderLines = (link) => {
150 link.lines = markdown.anchors 169 link.lines = htmlHolder.anchors
151 .filter(isAnchorPointedBy(link)) 170 .filter(isAnchorPointedBy(link))
152 .filter(isAnchorVisible) 171 .filter(isAnchorVisible)
153 .map(drawLeaderLine(link)) 172 .map(drawLeaderLine(link))
@@ -187,7 +206,7 @@ export const generateMaps = async (container) => {
187 206
188 const afterEachMapLoaded = (mapContainer) => { 207 const afterEachMapLoaded = (mapContainer) => {
189 mapContainer.querySelectorAll('.marker') 208 mapContainer.querySelectorAll('.marker')
190 .forEach(marker => markdown.anchors.push(marker)) 209 .forEach(marker => htmlHolder.anchors.push(marker))
191 210
192 const focusClickedMap = () => { 211 const focusClickedMap = () => {
193 if (container.getAttribute('data-layout') !== 'none') return 212 if (container.getAttribute('data-layout') !== 'none') return
@@ -215,6 +234,7 @@ export const generateMaps = async (container) => {
215 mapIdList.push(mapId) 234 mapIdList.push(mapId)
216 } 235 }
217 236
237 // FIXME Create markers after maps are created
218 const markerOptions = geoLinks.map(link => ({ 238 const markerOptions = geoLinks.map(link => ({
219 targets: link.targets, 239 targets: link.targets,
220 xy: link.xy, 240 xy: link.xy,
@@ -263,20 +283,16 @@ export const generateMaps = async (container) => {
263 283
264 //}}} 284 //}}}
265 // CSS observer {{{ 285 // CSS observer {{{
266 if (!observers.get(container)) {
267 observers.set(container, [])
268 }
269 const obs = observers.get(container)
270 if (obs.length) {
271 obs.forEach(o => o.disconnect())
272 obs.length = 0
273 }
274 // Layout{{{ 286 // Layout{{{
275 287
276 // press key to switch layout 288 // press key to switch layout
277 const layouts = ['none', 'side', 'overlay'] 289 const layouts = ['none', 'side', 'overlay']
278 container.setAttribute("data-layout", layouts[0]) 290 container.setAttribute("data-layout", layouts[0])
291
292 // FIXME Use UI to switch layouts
293 const originalKeyDown = document.onkeydown
279 document.onkeydown = (event) => { 294 document.onkeydown = (event) => {
295 originalKeyDown(event)
280 if (event.key === 'x' && container.querySelector('.map-container')) { 296 if (event.key === 'x' && container.querySelector('.map-container')) {
281 let currentLayout = container.getAttribute('data-layout') 297 let currentLayout = container.getAttribute('data-layout')
282 currentLayout = currentLayout ? currentLayout : 'none' 298 currentLayout = currentLayout ? currentLayout : 'none'
@@ -287,8 +303,8 @@ export const generateMaps = async (container) => {
287 } 303 }
288 304
289 // Add draggable part for blocks 305 // Add draggable part for blocks
290 markdown.blocks = Array.from(markdown.querySelectorAll('.draggable-block')) 306 htmlHolder.blocks = Array.from(htmlHolder.querySelectorAll('.draggable-block'))
291 markdown.blocks.forEach(block => { 307 htmlHolder.blocks.forEach(block => {
292 const draggablePart = document.createElement('div'); 308 const draggablePart = document.createElement('div');
293 draggablePart.classList.add('draggable') 309 draggablePart.classList.add('draggable')
294 draggablePart.textContent = '☰' 310 draggablePart.textContent = '☰'
@@ -303,30 +319,30 @@ export const generateMaps = async (container) => {
303 // observe layout change 319 // observe layout change
304 const layoutObserver = new MutationObserver(() => { 320 const layoutObserver = new MutationObserver(() => {
305 const layout = container.getAttribute('data-layout') 321 const layout = container.getAttribute('data-layout')
306 markdown.blocks.forEach(b => b.style.display = "block") 322 htmlHolder.blocks.forEach(b => b.style.display = "block")
307 323
308 if (layout === 'none') { 324 if (layout === 'none') {
309 mapPlaceholder.innerHTML = "" 325 mapPlaceholder.innerHTML = ""
310 const map = focusArea.querySelector('.map-container') 326 const map = showcase.querySelector('.map-container')
311 // Swap focused map and palceholder in markdown 327 // Swap focused map and palceholder in markdown
312 if (map) { 328 if (map) {
313 mapPlaceholder.parentElement?.replaceChild(map, mapPlaceholder) 329 mapPlaceholder.parentElement?.replaceChild(map, mapPlaceholder)
314 focusArea.append(mapPlaceholder) 330 showcase.append(mapPlaceholder)
315 } 331 }
316 } else { 332 } else {
317 // If paceholder is not set, create one and put map into focusArea 333 // If paceholder is not set, create one and put map into focusArea
318 if (focusArea.contains(mapPlaceholder)) { 334 if (showcase.contains(mapPlaceholder)) {
319 const mapContainer = container.querySelector('.map-container.focus') ?? container.querySelector('.map-container') 335 const mapContainer = container.querySelector('.map-container.focus') ?? container.querySelector('.map-container')
320 mapPlaceholder.innerHTML = `<div>Placeholder</div>` 336 mapPlaceholder.innerHTML = `<div>Placeholder</div>`
321 // TODO Get snapshot image 337 // TODO Get snapshot image
322 // mapPlaceholder.src = map.map.getCanvas().toDataURL() 338 // mapPlaceholder.src = map.map.getCanvas().toDataURL()
323 mapContainer.parentElement?.replaceChild(mapPlaceholder, mapContainer) 339 mapContainer.parentElement?.replaceChild(mapPlaceholder, mapContainer)
324 focusArea.appendChild(mapContainer) 340 showcase.appendChild(mapContainer)
325 } 341 }
326 } 342 }
327 343
328 if (layout === 'overlay') { 344 if (layout === 'overlay') {
329 markdown.blocks.forEach(block => { 345 htmlHolder.blocks.forEach(block => {
330 block.draggableInstance = new PlainDraggable(block, { handle: block.querySelector('.draggable') }) 346 block.draggableInstance = new PlainDraggable(block, { handle: block.querySelector('.draggable') })
331 block.draggableInstance.snap = { x: { step: 20 }, y: { step: 20 } } 347 block.draggableInstance.snap = { x: { step: 20 }, y: { step: 20 } }
332 // block.draggableInstance.onDragEnd = () => { 348 // block.draggableInstance.onDragEnd = () => {
@@ -334,13 +350,9 @@ export const generateMaps = async (container) => {
334 // } 350 // }
335 }) 351 })
336 } else { 352 } else {
337 markdown.blocks.forEach(block => { 353 htmlHolder.blocks.forEach(block => {
338 try { 354 block.style.transform = 'none'
339 block.style.transform = 'none' 355 block.draggableInstance?.remove()
340 block.draggableInstance.remove()
341 } catch (err) {
342 console.warn('Fail to remove draggable instance', err)
343 }
344 }) 356 })
345 } 357 }
346 }); 358 });
@@ -349,7 +361,8 @@ export const generateMaps = async (container) => {
349 attributeFilter: ["data-layout"], 361 attributeFilter: ["data-layout"],
350 attributeOldValue: true 362 attributeOldValue: true
351 }); 363 });
352 obs.push(layoutObserver) 364
365 onRemove(htmlHolder, () => layoutObserver.disconnect())
353 //}}} 366 //}}}
354 //}}} 367 //}}}
355 return container 368 return container
diff --git a/src/editor.mjs b/src/editor.mjs
index dd18a31..b283a90 100644
--- a/src/editor.mjs
+++ b/src/editor.mjs
@@ -1,12 +1,22 @@
1import TinyMDE from 'tiny-markdown-editor'
2import { markdown2HTML, generateMaps } from './dumbymap' 1import { markdown2HTML, generateMaps } from './dumbymap'
3import { defaultAliasesForRenderer, parseConfigsFromYaml } from 'mapclay' 2import { defaultAliasesForRenderer, parseConfigsFromYaml } from 'mapclay'
4 3
5// Set up Editor {{{ 4// Set up Editor {{{
6 5
7const HtmlContainer = document.querySelector(".result-html") 6const HtmlContainer = document.querySelector(".result-html")
8const mdeElement = document.querySelector("#tinymde") 7const textArea = document.querySelector(".editor textarea")
9 8
9const toggleMaps = (container) => {
10 if (!container.querySelector('.Showcase')) {
11 generateMaps(container)
12 document.activeElement.blur();
13 } else {
14 markdown2HTML(HtmlContainer, editor.value())
15 container.setAttribute('data-layout', 'none')
16 }
17}
18
19// Content values for editor
10const getContentFromHash = (cleanHash = false) => { 20const getContentFromHash = (cleanHash = false) => {
11 const hashValue = location.hash.substring(1); 21 const hashValue = location.hash.substring(1);
12 if (cleanHash) window.location.hash = '' 22 if (cleanHash) window.location.hash = ''
@@ -14,58 +24,60 @@ const getContentFromHash = (cleanHash = false) => {
14 ? decodeURIComponent(hashValue.substring(5)) 24 ? decodeURIComponent(hashValue.substring(5))
15 : null 25 : null
16} 26}
17
18// Add Editor
19const contentFromHash = getContentFromHash(true) 27const contentFromHash = getContentFromHash(true)
20const lastContent = localStorage.getItem('editorContent') 28const lastContent = localStorage.getItem('editorContent')
21const 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' 29const defaultContent = '## Links\n\n- [Go to marker](geo:24,121?id=foo,leaflet&text=normal "Link Test")\n\n```map\nid: foo\nuse: Maplibre\n```\n'
22 30
23const tinyEditor = new TinyMDE.Editor({ 31// Set up EasyMDE {{{
24 element: 'tinymde', 32const editor = new EasyMDE({
25 content: contentFromHash ?? lastContent ?? defaultContent 33 element: textArea,
34 indentWithTabs: false,
35 initialValue: contentFromHash ?? lastContent ?? defaultContent,
36 lineNumbers: true,
37 promptURLs: true,
38 uploadImage: true,
39 spellChecker: false,
40 toolbarButtonClassPrefix: 'mde',
41 status: false,
42 shortcuts: {
43 "map": "Ctrl-Alt-M",
44 "debug": "Ctrl-Alt-D",
45 "toggleUnorderedList": "Ctrl-Shift-L",
46 },
47 toolbar: [
48 {
49 name: 'map',
50 title: 'Toggle Map Generation',
51 text: "🌏",
52 action: toggleMaps(HtmlContainer),
53 },
54 {
55 name: 'debug',
56 title: 'Save content as URL',
57 text: "🤔",
58 action: () => {
59 window.location.hash = '#text=' + encodeURIComponent(editor.value())
60 navigator.clipboard.writeText(window.location.href)
61 alert('URL copied to clipboard')
62 },
63 }, 'undo', 'redo', '|', 'heading-1', 'heading-2', '|', 'link', 'image', '|', 'bold', 'italic', 'strikethrough', 'code', 'clean-block', '|', 'unordered-list', 'ordered-list', 'quote', 'table', '|', 'fullscreen'
64 ],
26}); 65});
27mdeElement.querySelectorAll('span').forEach(e => e.setAttribute('spellcheck', 'false'))
28 66
67const cm = editor.codemirror
68markdown2HTML(HtmlContainer, editor.value())
69
70cm.on("change", () => {
71 markdown2HTML(HtmlContainer, editor.value())
72})
73// }}}
74
75// Reload editor content by hash value
29onhashchange = () => { 76onhashchange = () => {
30 const contentFromHash = getContentFromHash() 77 const contentFromHash = getContentFromHash()
31 if (contentFromHash) tinyEditor.setContent(contentFromHash) 78 if (contentFromHash) editor.value(contentFromHash)
32} 79}
33 80
34// Add command bar for editor
35// Use this command to render maps and geoLinks
36const mapCommand = {
37 name: 'map',
38 title: 'Switch Map Generation',
39 innerHTML: `<div style="font-size: 16px; line-height: 1.1;">🌏</div>`,
40 action: () => {
41 if (!HtmlContainer.querySelector('.map-container')) {
42 generateMaps(HtmlContainer)
43 document.activeElement.blur();
44 } else {
45 markdown2HTML(HtmlContainer, tinyEditor.getContent())
46 HtmlContainer.setAttribute('data-layout', 'none')
47 }
48 },
49 hotkey: 'Ctrl-m'
50}
51const debugCommand = {
52 name: 'debug',
53 title: 'show debug message',
54 innerHTML: `<div style="font-size: 16px; line-height: 1.1;">🤔</div>`,
55 action: () => {
56 window.location.hash = '#text=' + encodeURIComponent(tinyEditor.getContent())
57 },
58 hotkey: 'Ctrl-i'
59}
60// Set up command bar
61new TinyMDE.CommandBar({
62 element: 'tinymde_commandbar', editor: tinyEditor,
63 commands: [mapCommand, debugCommand, '|', 'h1', 'h2', '|', 'insertLink', 'insertImage', '|', 'bold', 'italic', 'strikethrough', 'code', '|', 'ul', 'ol', '|', 'blockquote']
64});
65
66// Render HTML to result container
67markdown2HTML(HtmlContainer, tinyEditor.getContent())
68
69// FIXME DEBUGONLY 81// FIXME DEBUGONLY
70// generateMaps(HtmlContainer) 82// generateMaps(HtmlContainer)
71// setTimeout(() => { 83// setTimeout(() => {
@@ -73,34 +85,10 @@ markdown2HTML(HtmlContainer, tinyEditor.getContent())
73// }, 500) 85// }, 500)
74 86
75// }}} 87// }}}
76// Event Listener: change {{{
77
78// Save editor content to local storage, set timeout for 3 seconds
79let cancelLastSave
80const saveContent = (content) => {
81 new Promise((resolve, reject) => {
82 // If user is typing, the last change cancel previous ones
83 if (cancelLastSave) cancelLastSave(content.length)
84 cancelLastSave = reject
85
86 setTimeout(() => {
87 localStorage.setItem('editorContent', content)
88 resolve('Content Saved')
89 }, 3000)
90 }).catch(() => null)
91}
92
93// Render HTML to result container and save current content
94tinyEditor.addEventListener('change', e => {
95 markdown2HTML(HtmlContainer, e.content)
96 saveContent(e.content)
97});
98// }}}
99// Completion in Code Blok {{{ 88// Completion in Code Blok {{{
100// Elements about suggestions {{{ 89// Elements about suggestions {{{
101const suggestionsEle = document.createElement('div') 90const suggestionsEle = document.createElement('div')
102suggestionsEle.classList.add('container__suggestions'); 91suggestionsEle.classList.add('container__suggestions');
103mdeElement.appendChild(suggestionsEle)
104 92
105const rendererOptions = {} 93const rendererOptions = {}
106 94
@@ -110,7 +98,6 @@ class Suggestion {
110 this.replace = replace 98 this.replace = replace
111 } 99 }
112} 100}
113
114// }}} 101// }}}
115// {{{ Aliases for map options 102// {{{ Aliases for map options
116const aliasesForMapOptions = {} 103const aliasesForMapOptions = {}
@@ -124,50 +111,54 @@ fetch(defaultApply)
124 .catch(err => console.warn(`Fail to get aliases from ${defaultApply}`, err)) 111 .catch(err => console.warn(`Fail to get aliases from ${defaultApply}`, err))
125// }}} 112// }}}
126// FUNCTION: Check cursor is inside map code block {{{ 113// FUNCTION: Check cursor is inside map code block {{{
127const insideCodeblockForMap = (element) => { 114// const insideCodeblockForMap = (currentLine) => {
128 const code = element.closest('.TMFencedCodeBacktick') 115// let tokens = cm.getLineTokens(currentLine)
129 if (!code) return false 116//
130 117// if (!tokens.includes("comment") || tokens.includes('formatting-code-block')) return false
131 let ps = code.previousSibling 118//
132 if (!ps) return false 119// do {
133 120// line = line - 1
134 // Look backward to find pattern of code block: /```map/ 121// if (line < 0) return false
135 while (!ps.classList.contains('TMCodeFenceBacktickOpen')) { 122// tokens = cm.getLineTokens(line)
136 ps = ps.previousSibling 123// } while (!tokens.includes('formatting-code-block'))
137 if (!ps) return false 124//
138 if (ps.classList.contains('TMCodeFenceBacktickClose')) return false 125// return true
139 } 126// }
140 127// }}}
141 return ps.querySelector('.TMInfoString')?.textContent === 'map' 128// Check if current token is inside code block {{{
142} 129const insideCodeblockForMap = (token) =>
130 token.state.overlay.codeBlock && !token.string.match(/^````*/)
143// }}} 131// }}}
144// FUNCTION: Get renderer by cursor position in code block {{{ 132// FUNCTION: Get renderer by cursor position in code block {{{
145const getLineWithRenderer = (element) => { 133const getLineWithRenderer = (anchor) => {
146 const currentLine = element.closest('.TMFencedCodeBacktick') 134 const currentLine = anchor.line
147 if (!currentLine) return null 135 const match = (line) => cm.getLine(line).match(/^use: /)
136 if (match(currentLine)) return currentLine
137
138 const getToken = (line) => cm.getTokenAt({ line: line, ch: 1 })
148 139
149 // Look backward/forward for pattern of used renderer: /use: .+/ 140 // Look backward/forward for pattern of used renderer: /use: .+/
150 let ps = currentLine 141 let ps = currentLine - 1
151 do { 142 while (ps > 0 && insideCodeblockForMap(getToken(ps))) {
152 ps = ps.previousSibling 143 if (match(ps)) {
153 if (ps.textContent.match(/^use: /)) {
154 return ps 144 return ps
155 } else if (ps.textContent.match(/^---/)) { 145 } else if (cm.getLine(ps).match(/^---/)) {
156 // If yaml doc separator is found 146 // If yaml doc separator is found
157 break 147 break
158 } 148 }
159 } while (ps && ps.classList.contains('TMFencedCodeBacktick')) 149 ps = ps - 1
150 }
160 151
161 let ns = currentLine 152 let ns = currentLine + 1
162 do { 153 while (insideCodeblockForMap(getToken(ns))) {
163 ns = ns.nextSibling 154 if (match(ns)) {
164 if (ns.textContent.match(/^use: /)) {
165 return ns 155 return ns
166 } else if (ns.textContent.match(/^---/)) { 156 } else if (cm.getLine(ns).match(/^---/)) {
167 // If yaml doc separator is found 157 // If yaml doc separator is found
168 return null 158 return null
169 } 159 }
170 } while (ns && ns.classList.contains('TMFencedCodeBacktick')) 160 ns = ns + 1
161 }
171 162
172 return null 163 return null
173} 164}
@@ -218,29 +209,35 @@ const getSuggestionsFromAliases = (option) => Object.entries(aliasesForMapOption
218 }) 209 })
219 ?? [] 210 ?? []
220// }}} 211// }}}
221const handleTypingInCodeBlock = (currentLine, selection) => { 212// FUCNTION: Handler for map codeblock {{{
222 const text = currentLine.textContent 213const handleTypingInCodeBlock = (anchor) => {
214 const text = cm.getLine(anchor.line)
223 if (text.match(/^\s\+$/) && text.length % 2 !== 0) { 215 if (text.match(/^\s\+$/) && text.length % 2 !== 0) {
224 // TODO Completion for even number of spaces 216 // TODO Completion for even number of spaces
225 } else if (text.match(/^-/)){ 217 } else if (text.match(/^-/)) {
226 // TODO Completion for YAML doc separator 218 // TODO Completion for YAML doc separator
227 } else { 219 } else {
228 addSuggestions(currentLine, selection) 220 const suggestions = getSuggestions(anchor)
221 addSuggestions(anchor, suggestions)
229 } 222 }
230} 223}
231// FUNCTION: Add HTML element for List of suggestions {{{ 224// }}}
232const addSuggestions = (currentLine, selection) => { 225// FUNCTION: get suggestions by current input {{{
233 const text = currentLine.textContent 226const getSuggestions = (anchor) => {
234 const markInputIsInvalid = (ele) => (ele ?? currentLine).classList.add('invalid-input') 227 const text = cm.getLine(anchor.line)
228 const markInputIsInvalid = () => cm.getDoc().markText(
229 { ...anchor, ch: 0 },
230 { ...anchor, ch: -1 },
231 { className: 'invalid-input' },
232 )
235 let suggestions = [] 233 let suggestions = []
236 234
237 // Check if "use: <renderer>" is set 235 // Check if "use: <renderer>" is set
238 const lineWithRenderer = getLineWithRenderer(currentLine) 236 const lineWithRenderer = getLineWithRenderer(anchor)
239 const renderer = lineWithRenderer?.textContent.split(' ')[1] 237 const renderer = cm.getLine(lineWithRenderer).split(' ')[1]
240 if (renderer) { 238 if (renderer) {
241
242 // Do not check properties 239 // Do not check properties
243 if (text.startsWith(' ')) return 240 if (text.startsWith(' ')) return []
244 241
245 // If no valid options for current used renderer, go get it! 242 // If no valid options for current used renderer, go get it!
246 const validOptions = rendererOptions[renderer] 243 const validOptions = rendererOptions[renderer]
@@ -253,9 +250,9 @@ const addSuggestions = (currentLine, selection) => {
253 }) 250 })
254 .catch(() => { 251 .catch(() => {
255 markInputIsInvalid(lineWithRenderer) 252 markInputIsInvalid(lineWithRenderer)
256 console.error('Fail to get valid options from renderer, URL is', rendererUrl) 253 console.warn(`Fail to get valid options from renderer with URL ${rendererUrl}` )
257 }) 254 })
258 return 255 return []
259 } 256 }
260 257
261 // If input is "key:value" (no space left after colon), then it is invalid 258 // If input is "key:value" (no space left after colon), then it is invalid
@@ -263,7 +260,7 @@ const addSuggestions = (currentLine, selection) => {
263 const isValidKeyValue = text.match(/^[^:]+:\s+/) 260 const isValidKeyValue = text.match(/^[^:]+:\s+/)
264 if (isKeyFinished && !isValidKeyValue) { 261 if (isKeyFinished && !isValidKeyValue) {
265 markInputIsInvalid() 262 markInputIsInvalid()
266 return 263 return []
267 } 264 }
268 265
269 // If user is typing option 266 // If user is typing option
@@ -311,6 +308,11 @@ const addSuggestions = (currentLine, selection) => {
311 ) 308 )
312 suggestions = rendererSuggestions.length > 0 ? rendererSuggestions : [] 309 suggestions = rendererSuggestions.length > 0 ? rendererSuggestions : []
313 } 310 }
311 return suggestions
312}
313// }}}
314// {{{ FUNCTION: Show element about suggestions
315const addSuggestions = (anchor, suggestions) => {
314 316
315 if (suggestions.length === 0) { 317 if (suggestions.length === 0) {
316 suggestionsEle.style.display = 'none'; 318 suggestionsEle.style.display = 'none';
@@ -336,55 +338,35 @@ const addSuggestions = (currentLine, selection) => {
336 option.classList.remove('focus') 338 option.classList.remove('focus')
337 } 339 }
338 option.onclick = () => { 340 option.onclick = () => {
339 const newFocus = { ...selection.focus, col: 0 } 341 cm.setSelection(anchor, { ...anchor, ch: 0 })
340 const newAnchor = { ...selection.anchor, col: text.length } 342 cm.replaceSelection(suggestion.replace)
341 tinyEditor.paste(suggestion.replace, newFocus, newAnchor) 343 cm.focus();
342 suggestionsEle.style.display = 'none'; 344 const newAnchor = { ...anchor, ch: suggestion.replace.length }
343 option.classList.remove('focus') 345 cm.setCursor(newAnchor);
344 }; 346 };
345 suggestionsEle.appendChild(option); 347 suggestionsEle.appendChild(option);
346 }); 348 });
347 349
348 const rect = currentLine.getBoundingClientRect(); 350 cm.addWidget(anchor, suggestionsEle, true)
349 suggestionsEle.style.top = `${rect.top + rect.height + 12}px`; 351 const rect = suggestionsEle.getBoundingClientRect()
350 suggestionsEle.style.left = `${rect.right}px`; 352 suggestionsEle.style.maxWidth = `calc(${window.innerWidth}px - ${rect.x}px - 2rem)`;
351 suggestionsEle.style.maxWidth = `calc(${window.innerWidth}px - ${rect.right}px - 2rem)`; 353 suggestionsEle.style.display = 'block'
352} 354}
353// }}} 355// }}}
354// EVENT: suggests for current selection {{{ 356// EVENT: suggests for current selection {{{
355tinyEditor.addEventListener('selection', selection => { 357// FIXME Dont show suggestion when selecting multiple chars
356 // Check selection is inside editor contents 358cm.on("beforeSelectionChange", (_, obj) => {
357 const node = selection?.anchor?.node 359 const anchor = (obj.ranges[0].anchor)
358 if (!node) return 360 const token = cm.getTokenAt(anchor)
359
360 // FIXME Better way to prevent spellcheck across editor
361 // Get HTML element for current selection
362 const element = node instanceof HTMLElement
363 ? node
364 : node.parentNode
365 element.setAttribute('spellcheck', 'false')
366
367 // To trigger click event on suggestions list, don't set suggestion list invisible
368 if (suggestionsEle.querySelector('.container__suggestion.focus:hover') !== null) {
369 return
370 } else {
371 suggestionsEle.style.display = 'none';
372 }
373
374 // Do not show suggestion by attribute
375 if (suggestionsEle.getAttribute('data-keep-close') === 'true') {
376 suggestionsEle.setAttribute('data-keep-close', 'false')
377 return
378 }
379 361
380 // Show suggestions for map code block 362 if (insideCodeblockForMap(token)) {
381 if (insideCodeblockForMap(element)) { 363 handleTypingInCodeBlock(anchor)
382 handleTypingInCodeBlock(element, selection)
383 } 364 }
384}); 365});
385// }}} 366// }}}
386// EVENT: keydown for suggestions {{{ 367// EVENT: keydown for suggestions {{{
387mdeElement.addEventListener('keydown', (e) => { 368cm.on('keydown', (_, e) => {
369
388 // Only the following keys are used 370 // Only the following keys are used
389 const keyForSuggestions = ['Tab', 'Enter', 'Escape'].includes(e.key) 371 const keyForSuggestions = ['Tab', 'Enter', 'Escape'].includes(e.key)
390 if (!keyForSuggestions || suggestionsEle.style.display === 'none') return; 372 if (!keyForSuggestions || suggestionsEle.style.display === 'none') return;
@@ -402,7 +384,6 @@ mdeElement.addEventListener('keydown', (e) => {
402 const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion 384 const focusSuggestion = e.shiftKey ? previousSuggestion : nextSuggestion
403 385
404 // Current editor selection state 386 // Current editor selection state
405 const selection = tinyEditor.getSelection(true)
406 switch (e.key) { 387 switch (e.key) {
407 case 'Tab': 388 case 'Tab':
408 Array.from(suggestionsEle.children).forEach(s => s.classList.remove('focus')) 389 Array.from(suggestionsEle.children).forEach(s => s.classList.remove('focus'))
@@ -411,16 +392,22 @@ mdeElement.addEventListener('keydown', (e) => {
411 break; 392 break;
412 case 'Enter': 393 case 'Enter':
413 currentSuggestion.onclick() 394 currentSuggestion.onclick()
414 suggestionsEle.style.display = 'none';
415 break; 395 break;
416 case 'Escape': 396 case 'Escape':
397 const anchor = cm.getCursor()
417 suggestionsEle.style.display = 'none'; 398 suggestionsEle.style.display = 'none';
418 // Prevent trigger selection event again 399 // Focus editor again
419 suggestionsEle.setAttribute('data-keep-close', 'true') 400 setTimeout(() => cm.focus() && cm.setCursor(anchor), 100)
420 setTimeout(() => tinyEditor.setSelection(selection), 100)
421 break; 401 break;
422 } 402 }
423}); 403});
404
405document.onkeydown = (e) => {
406 if (e.altKey && e.ctrlKey && e.key === 'm') {
407 toggleMaps(HtmlContainer)
408 }
409}
410
424// }}} 411// }}}
425// }}} 412// }}}
426 413