diff options
Diffstat (limited to 'src/dumbymap.mjs')
-rw-r--r-- | src/dumbymap.mjs | 101 |
1 files changed, 57 insertions, 44 deletions
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 |