diff options
| author | Hsieh Chin Fan <pham@topo.tw> | 2024-10-05 14:40:28 +0800 |
|---|---|---|
| committer | Hsieh Chin Fan <pham@topo.tw> | 2024-10-05 18:58:15 +0800 |
| commit | 849a9ededf3aa90a8a0e7ccd5d752f51f47a6642 (patch) | |
| tree | ad5a69efef98e07ef73c719f6d5b128601878998 | |
| parent | 54171e6bfbe066c86ed5e4924eb330ac5014f4db (diff) | |
feat: sync srolling of editor and dumbymap
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | src/css/index.css | 2 | ||||
| -rw-r--r-- | src/dumbymap.mjs | 2 | ||||
| -rw-r--r-- | src/editor.mjs | 102 |
4 files changed, 95 insertions, 12 deletions
diff --git a/package.json b/package.json index eda0341..7d688c8 100644 --- a/package.json +++ b/package.json | |||
| @@ -52,6 +52,7 @@ | |||
| 52 | "markdown-it-anchor": "^9.2.0", | 52 | "markdown-it-anchor": "^9.2.0", |
| 53 | "markdown-it-footnote": "^4.0.0", | 53 | "markdown-it-footnote": "^4.0.0", |
| 54 | "markdown-it-front-matter": "^0.2.4", | 54 | "markdown-it-front-matter": "^0.2.4", |
| 55 | "markdown-it-inject-linenumbers": "^0.3.0", | ||
| 55 | "markdown-it-toc-done-right": "^4.2.0", | 56 | "markdown-it-toc-done-right": "^4.2.0", |
| 56 | "plain-draggable": "^2.5.14", | 57 | "plain-draggable": "^2.5.14", |
| 57 | "plain-modal": "^1.0.34" | 58 | "plain-modal": "^1.0.34" |
diff --git a/src/css/index.css b/src/css/index.css index c7ba231..8ae7337 100644 --- a/src/css/index.css +++ b/src/css/index.css | |||
| @@ -58,7 +58,7 @@ body { | |||
| 58 | 58 | ||
| 59 | .CodeMirror { | 59 | .CodeMirror { |
| 60 | flex: 1 0 0; | 60 | flex: 1 0 0; |
| 61 | padding-inline: 0; | 61 | padding: 0; |
| 62 | 62 | ||
| 63 | border: var(--content-border); | 63 | border: var(--content-border); |
| 64 | border-radius: var(--content-border-radius); | 64 | border-radius: var(--content-border-radius); |
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 3d2cc0c..e1bf220 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
| @@ -8,6 +8,7 @@ import { Layout, SideBySide, Overlay } from './Layout' | |||
| 8 | import * as utils from './dumbyUtils' | 8 | import * as utils from './dumbyUtils' |
| 9 | import * as menuItem from './MenuItem' | 9 | import * as menuItem from './MenuItem' |
| 10 | import PlainModal from 'plain-modal' | 10 | import PlainModal from 'plain-modal' |
| 11 | import markdownItInjectLinenumbers from 'markdown-it-inject-linenumbers' | ||
| 11 | 12 | ||
| 12 | const mapBlockSelector = 'pre:has(.language-map)' | 13 | const mapBlockSelector = 'pre:has(.language-map)' |
| 13 | const docLinkSelector = 'a[href^="#"][title^="=>"]' | 14 | const docLinkSelector = 'a[href^="#"][title^="=>"]' |
| @@ -45,6 +46,7 @@ export const markdown2HTML = (container, mdContent) => { | |||
| 45 | }) | 46 | }) |
| 46 | .use(MarkdownItFootnote) | 47 | .use(MarkdownItFootnote) |
| 47 | .use(MarkdownItFrontMatter) | 48 | .use(MarkdownItFrontMatter) |
| 49 | .use(markdownItInjectLinenumbers) | ||
| 48 | 50 | ||
| 49 | // Create links with geo scheme | 51 | // Create links with geo scheme |
| 50 | const coordinateRegex = /^(\D*)(-?\d+\.?\d*)\s*([,\x2F\uFF0C])\s*(-?\d+\.?\d*)/g | 52 | const coordinateRegex = /^(\D*)(-?\d+\.?\d*)\s*([,\x2F\uFF0C])\s*(-?\d+\.?\d*)/g |
diff --git a/src/editor.mjs b/src/editor.mjs index e0c90c3..5409c3f 100644 --- a/src/editor.mjs +++ b/src/editor.mjs | |||
| @@ -8,21 +8,22 @@ import { shiftByWindow } from './utils.mjs' | |||
| 8 | // Set up Containers {{{ | 8 | // Set up Containers {{{ |
| 9 | 9 | ||
| 10 | const context = document.querySelector('[data-mode]') | 10 | const context = document.querySelector('[data-mode]') |
| 11 | const HtmlContainer = document.querySelector('.DumbyMap') | 11 | const dumbyContainer = document.querySelector('.DumbyMap') |
| 12 | const textArea = document.querySelector('.editor textarea') | 12 | const textArea = document.querySelector('.editor textarea') |
| 13 | let dumbymap | 13 | let dumbymap |
| 14 | 14 | ||
| 15 | new window.MutationObserver(() => { | 15 | new window.MutationObserver(() => { |
| 16 | const mode = context.getAttribute('data-mode') | 16 | const mode = context.getAttribute('data-mode') |
| 17 | const layout = HtmlContainer.getAttribute('data-layout') | 17 | const layout = dumbyContainer.getAttribute('data-layout') |
| 18 | if (mode === 'editing' && layout !== 'normal') { | 18 | if (mode === 'editing' && layout !== 'normal') { |
| 19 | HtmlContainer.setAttribute('data-layout', 'normal') | 19 | dumbyContainer.setAttribute('data-layout', 'normal') |
| 20 | } | 20 | } |
| 21 | }).observe(context, { | 21 | }).observe(context, { |
| 22 | attributes: true, | 22 | attributes: true, |
| 23 | attributeFilter: ['data-mode'], | 23 | attributeFilter: ['data-mode'], |
| 24 | attributeOldValue: true | 24 | attributeOldValue: true |
| 25 | }) | 25 | }) |
| 26 | |||
| 26 | /** | 27 | /** |
| 27 | * toggle editing mode | 28 | * toggle editing mode |
| 28 | */ | 29 | */ |
| @@ -167,6 +168,82 @@ if (contentFromHash) { | |||
| 167 | } | 168 | } |
| 168 | // }}} | 169 | // }}} |
| 169 | // Set up logic about editor content {{{ | 170 | // Set up logic about editor content {{{ |
| 171 | |||
| 172 | const htmlOnScroll = (ele) => () => { | ||
| 173 | if (textArea.dataset.scrollLine) return | ||
| 174 | |||
| 175 | const threshold = ele.scrollTop + window.innerHeight / 2 + 30 | ||
| 176 | const block = Array.from(ele.children) | ||
| 177 | .findLast(e => e.offsetTop < threshold) ?? | ||
| 178 | ele.firstChild | ||
| 179 | |||
| 180 | const line = Array.from(block.querySelectorAll('p')) | ||
| 181 | .findLast(e => e.offsetTop + block.offsetTop < threshold) | ||
| 182 | const linenumber = line?.dataset?.sourceLine | ||
| 183 | if (!linenumber) return | ||
| 184 | const offset = (line.offsetTop + block.offsetTop - ele.scrollTop) | ||
| 185 | |||
| 186 | clearTimeout(dumbyContainer.timer) | ||
| 187 | if (linenumber) { | ||
| 188 | dumbyContainer.dataset.scrollLine = linenumber + '/' + offset | ||
| 189 | dumbyContainer.timer = setTimeout( | ||
| 190 | () => delete dumbyContainer.dataset.scrollLine, | ||
| 191 | 50 | ||
| 192 | ) | ||
| 193 | } | ||
| 194 | } | ||
| 195 | |||
| 196 | // Sync CodeMirror LineNumber with HTML Contents | ||
| 197 | new window.MutationObserver(() => { | ||
| 198 | const line = dumbyContainer.dataset.scrollLine | ||
| 199 | if (line) { | ||
| 200 | const [lineNumber, offset] = line.split('/') | ||
| 201 | |||
| 202 | if (!isNaN(lineNumber)) { | ||
| 203 | cm.scrollIntoView({ line: lineNumber, ch: 0 }, offset) | ||
| 204 | } | ||
| 205 | } | ||
| 206 | }).observe(dumbyContainer, { | ||
| 207 | attributes: true, | ||
| 208 | attributeFilter: ['data-scroll-line'] | ||
| 209 | }) | ||
| 210 | |||
| 211 | cm.on('scroll', () => { | ||
| 212 | if (dumbyContainer.dataset.scrollLine) return | ||
| 213 | |||
| 214 | const scrollInfo = cm.getScrollInfo() | ||
| 215 | const lineNumber = cm.lineAtHeight(scrollInfo.top, 'local') | ||
| 216 | textArea.dataset.scrollLine = lineNumber | ||
| 217 | |||
| 218 | clearTimeout(textArea.timer) | ||
| 219 | textArea.timer = setTimeout( | ||
| 220 | () => delete textArea.dataset.scrollLine, | ||
| 221 | 1000 | ||
| 222 | ) | ||
| 223 | }) | ||
| 224 | |||
| 225 | // Sync HTML Contents with CodeMirror LineNumber | ||
| 226 | new window.MutationObserver(() => { | ||
| 227 | const line = textArea.dataset.scrollLine | ||
| 228 | let lineNumber = Number(line) | ||
| 229 | let p | ||
| 230 | if (isNaN(lineNumber)) return | ||
| 231 | |||
| 232 | const paragraphs = Array.from(dumbymap.htmlHolder.querySelectorAll('p')) | ||
| 233 | do { | ||
| 234 | p = paragraphs.find(p => Number(p.dataset.sourceLine) === lineNumber) | ||
| 235 | lineNumber++ | ||
| 236 | } while (!p && lineNumber < cm.doc.size) | ||
| 237 | if (!p) return | ||
| 238 | |||
| 239 | const coords = cm.charCoords({ line: lineNumber, ch: 0 }, 'window') | ||
| 240 | p.scrollIntoView() | ||
| 241 | dumbymap.htmlHolder.scrollBy(0, -coords.top + 30) | ||
| 242 | }).observe(textArea, { | ||
| 243 | attributes: true, | ||
| 244 | attributeFilter: ['data-scroll-line'] | ||
| 245 | }) | ||
| 246 | |||
| 170 | /** | 247 | /** |
| 171 | * afterMapRendered. Callback of map rendered | 248 | * afterMapRendered. Callback of map rendered |
| 172 | * | 249 | * |
| @@ -180,8 +257,8 @@ const afterMapRendered = map => { | |||
| 180 | // // TODO... | 257 | // // TODO... |
| 181 | // } | 258 | // } |
| 182 | } | 259 | } |
| 183 | markdown2HTML(HtmlContainer, editor.value()) | 260 | markdown2HTML(dumbyContainer, editor.value()) |
| 184 | dumbymap = generateMaps(HtmlContainer, afterMapRendered) | 261 | dumbymap = generateMaps(dumbyContainer, afterMapRendered) |
| 185 | 262 | ||
| 186 | /** | 263 | /** |
| 187 | * addClassToCodeLines. Quick hack to style lines inside code block | 264 | * addClassToCodeLines. Quick hack to style lines inside code block |
| @@ -264,10 +341,13 @@ const completeForCodeBlock = change => { | |||
| 264 | * update content of HTML about Dumbymap | 341 | * update content of HTML about Dumbymap |
| 265 | */ | 342 | */ |
| 266 | const updateDumbyMap = () => { | 343 | const updateDumbyMap = () => { |
| 267 | markdown2HTML(HtmlContainer, editor.value()) | 344 | markdown2HTML(dumbyContainer, editor.value()) |
| 268 | // TODO Test if generate maps intantly is OK with map cache | 345 | // TODO Test if generate maps intantly is OK with map cache |
| 269 | // debounceForMap(HtmlContainer, afterMapRendered) | 346 | // debounceForMap(HtmlContainer, afterMapRendered) |
| 270 | dumbymap = generateMaps(HtmlContainer, afterMapRendered) | 347 | dumbymap = generateMaps(dumbyContainer, afterMapRendered) |
| 348 | |||
| 349 | const htmlHolder = dumbymap.htmlHolder | ||
| 350 | htmlHolder.onscroll = htmlOnScroll(htmlHolder) | ||
| 271 | } | 351 | } |
| 272 | 352 | ||
| 273 | updateDumbyMap() | 353 | updateDumbyMap() |
| @@ -282,7 +362,7 @@ cm.on('change', (_, change) => { | |||
| 282 | // Set class for focus | 362 | // Set class for focus |
| 283 | cm.on('focus', () => { | 363 | cm.on('focus', () => { |
| 284 | cm.getWrapperElement().classList.add('focus') | 364 | cm.getWrapperElement().classList.add('focus') |
| 285 | HtmlContainer.classList.remove('focus') | 365 | dumbyContainer.classList.remove('focus') |
| 286 | }) | 366 | }) |
| 287 | 367 | ||
| 288 | cm.on('beforeChange', (_, change) => { | 368 | cm.on('beforeChange', (_, change) => { |
| @@ -643,7 +723,7 @@ cm.on('blur', () => { | |||
| 643 | cm.focus() | 723 | cm.focus() |
| 644 | } else { | 724 | } else { |
| 645 | cm.getWrapperElement().classList.remove('focus') | 725 | cm.getWrapperElement().classList.remove('focus') |
| 646 | HtmlContainer.classList.add('focus') | 726 | dumbyContainer.classList.add('focus') |
| 647 | } | 727 | } |
| 648 | }) | 728 | }) |
| 649 | // }}} | 729 | // }}} |
| @@ -730,11 +810,11 @@ document.onkeydown = e => { | |||
| 730 | // Layout Switch {{{ | 810 | // Layout Switch {{{ |
| 731 | new window.MutationObserver(mutaions => { | 811 | new window.MutationObserver(mutaions => { |
| 732 | const mutation = mutaions.at(-1) | 812 | const mutation = mutaions.at(-1) |
| 733 | const layout = HtmlContainer.getAttribute('data-layout') | 813 | const layout = dumbyContainer.getAttribute('data-layout') |
| 734 | if (layout !== 'normal' || mutation.oldValue === 'normal') { | 814 | if (layout !== 'normal' || mutation.oldValue === 'normal') { |
| 735 | context.setAttribute('data-mode', '') | 815 | context.setAttribute('data-mode', '') |
| 736 | } | 816 | } |
| 737 | }).observe(HtmlContainer, { | 817 | }).observe(dumbyContainer, { |
| 738 | attributes: true, | 818 | attributes: true, |
| 739 | attributeFilter: ['data-layout'], | 819 | attributeFilter: ['data-layout'], |
| 740 | attributeOldValue: true | 820 | attributeOldValue: true |