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