diff options
| author | Hsieh Chin Fan <pham@topo.tw> | 2024-09-10 19:46:20 +0800 |
|---|---|---|
| committer | Hsieh Chin Fan <pham@topo.tw> | 2024-09-11 14:10:19 +0800 |
| commit | aded35c97a57eeb5eabff8d9a6853b01bbfa197e (patch) | |
| tree | 8481990e83404dd8aa83026f061c7640621167ff /src | |
| parent | 7ba3a70473b0a1748ca4d1dbb657fd43683094eb (diff) | |
refactor: Switch to EasyMDE
EasyMDE is based on codemirror, so completion and more features maybe
applied later.
* Now suggestions also works in EasyMDE
* For preview, change return value of markdown2HTML()
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 | ||