diff options
Diffstat (limited to 'src/editor.mjs')
-rw-r--r-- | src/editor.mjs | 102 |
1 files changed, 91 insertions, 11 deletions
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 |