aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/dumbymap.mjs
diff options
context:
space:
mode:
authorHsieh Chin Fan <pham@topo.tw>2024-10-02 10:12:46 +0800
committerHsieh Chin Fan <pham@topo.tw>2024-10-02 10:12:46 +0800
commit1c7e77b8546ac32b8176ab54dd06ede6ba7deb55 (patch)
treeca0d50e7b8b3dcbd2cc8891786dca32f1face931 /src/dumbymap.mjs
parentb7e898bc9022a6d7caef705ee2764c139ec86299 (diff)
style: switch to standardjs
Diffstat (limited to 'src/dumbymap.mjs')
-rw-r--r--src/dumbymap.mjs469
1 files changed, 234 insertions, 235 deletions
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs
index 2efc350..fa88f3a 100644
--- a/src/dumbymap.mjs
+++ b/src/dumbymap.mjs
@@ -1,53 +1,53 @@
1import MarkdownIt from 'markdown-it'; 1import MarkdownIt from 'markdown-it'
2import MarkdownItAnchor from 'markdown-it-anchor'; 2import MarkdownItAnchor from 'markdown-it-anchor'
3import MarkdownItFootnote from 'markdown-it-footnote'; 3import MarkdownItFootnote from 'markdown-it-footnote'
4import MarkdownItFrontMatter from 'markdown-it-front-matter'; 4import MarkdownItFrontMatter from 'markdown-it-front-matter'
5import MarkdownItTocDoneRight from 'markdown-it-toc-done-right'; 5import MarkdownItTocDoneRight from 'markdown-it-toc-done-right'
6import LeaderLine from 'leader-line'; 6import LeaderLine from 'leader-line'
7import { renderWith, defaultAliases, parseConfigsFromYaml } from 'mapclay'; 7import { renderWith, defaultAliases, parseConfigsFromYaml } from 'mapclay'
8import { onRemove, animateRectTransition, throttle } from './utils'; 8import { onRemove, animateRectTransition, throttle } from './utils'
9import { Layout, SideBySide, Overlay } from './Layout'; 9import { Layout, SideBySide, Overlay } from './Layout'
10import * as utils from './dumbyUtils'; 10import * as utils from './dumbyUtils'
11import * as menuItem from './MenuItem'; 11import * as menuItem from './MenuItem'
12import { default as PlainModal } from 'plain-modal'; 12import PlainModal from 'plain-modal'
13 13
14const docLinkSelector = 'a[href^="#"][title^="=>"]'; 14const docLinkSelector = 'a[href^="#"][title^="=>"]'
15const geoLinkSelector = 'a[href^="geo:"]'; 15const geoLinkSelector = 'a[href^="geo:"]'
16 16
17const layouts = [ 17const layouts = [
18 new Layout({ name: 'normal' }), 18 new Layout({ name: 'normal' }),
19 new SideBySide({ name: 'side-by-side' }), 19 new SideBySide({ name: 'side-by-side' }),
20 new Overlay({ name: 'overlay' }), 20 new Overlay({ name: 'overlay' })
21]; 21]
22const mapCache = {}; 22const mapCache = {}
23 23
24export const markdown2HTML = (container, mdContent) => { 24export const markdown2HTML = (container, mdContent) => {
25 // Render: Markdown -> HTML {{{ 25 // Render: Markdown -> HTML {{{
26 container.replaceChildren(); 26 container.replaceChildren()
27 27
28 container.innerHTML = '<div class="SemanticHtml"></div>'; 28 container.innerHTML = '<div class="SemanticHtml"></div>'
29 const htmlHolder = container.querySelector('.SemanticHtml'); 29 const htmlHolder = container.querySelector('.SemanticHtml')
30 30
31 const md = MarkdownIt({ 31 const md = MarkdownIt({
32 html: true, 32 html: true,
33 breaks: true, 33 breaks: true
34 }) 34 })
35 .use(MarkdownItAnchor, { 35 .use(MarkdownItAnchor, {
36 permalink: MarkdownItAnchor.permalink.linkInsideHeader({ 36 permalink: MarkdownItAnchor.permalink.linkInsideHeader({
37 placement: 'before', 37 placement: 'before'
38 }), 38 })
39 }) 39 })
40 .use(MarkdownItFootnote) 40 .use(MarkdownItFootnote)
41 .use(MarkdownItFrontMatter) 41 .use(MarkdownItFrontMatter)
42 .use(MarkdownItTocDoneRight); 42 .use(MarkdownItTocDoneRight)
43 43
44 // FIXME A better way to generate blocks 44 // FIXME A better way to generate blocks
45 md.renderer.rules.dumby_block_open = () => '<div>'; 45 md.renderer.rules.dumby_block_open = () => '<div>'
46 md.renderer.rules.dumby_block_close = () => '</div>'; 46 md.renderer.rules.dumby_block_close = () => '</div>'
47 47
48 md.core.ruler.before('block', 'dumby_block', state => { 48 md.core.ruler.before('block', 'dumby_block', state => {
49 state.tokens.push(new state.Token('dumby_block_open', '', 1)); 49 state.tokens.push(new state.Token('dumby_block_open', '', 1))
50 }); 50 })
51 51
52 // Add close tag for block with more than 2 empty lines 52 // Add close tag for block with more than 2 empty lines
53 md.block.ruler.before('table', 'dumby_block', (state, startLine) => { 53 md.block.ruler.before('table', 'dumby_block', (state, startLine) => {
@@ -56,39 +56,39 @@ export const markdown2HTML = (container, mdContent) => {
56 state.src[state.bMarks[startLine - 2]] === '\n' && 56 state.src[state.bMarks[startLine - 2]] === '\n' &&
57 state.tokens.at(-1).type !== 'list_item_open' // Quick hack for not adding tag after "::marker" for <li> 57 state.tokens.at(-1).type !== 'list_item_open' // Quick hack for not adding tag after "::marker" for <li>
58 ) { 58 ) {
59 state.push('dumby_block_close', '', -1); 59 state.push('dumby_block_close', '', -1)
60 state.push('dumby_block_open', '', 1); 60 state.push('dumby_block_open', '', 1)
61 } 61 }
62 }); 62 })
63 63
64 md.core.ruler.after('block', 'dumby_block', state => { 64 md.core.ruler.after('block', 'dumby_block', state => {
65 state.tokens.push(new state.Token('dumby_block_close', '', -1)); 65 state.tokens.push(new state.Token('dumby_block_close', '', -1))
66 }); 66 })
67 67
68 const contentWithToc = '${toc}\n\n\n' + mdContent; 68 const contentWithToc = '${toc}\n\n\n' + mdContent // eslint-disable-line
69 htmlHolder.innerHTML = md.render(contentWithToc); 69 htmlHolder.innerHTML = md.render(contentWithToc)
70 70
71 // TODO Do this in markdown-it 71 // TODO Do this in markdown-it
72 const blocks = htmlHolder.querySelectorAll(':scope > div:not(:has(nav))'); 72 const blocks = htmlHolder.querySelectorAll(':scope > div:not(:has(nav))')
73 blocks.forEach(b => { 73 blocks.forEach(b => {
74 b.classList.add('dumby-block'); 74 b.classList.add('dumby-block')
75 b.setAttribute('data-total', blocks.length); 75 b.setAttribute('data-total', blocks.length)
76 }); 76 })
77 77
78 return container; 78 return container
79 //}}} 79 // }}}
80}; 80}
81export const generateMaps = (container, { delay, mapCallback }) => { 81export const generateMaps = (container, { delay, mapCallback }) => {
82 container.classList.add('Dumby'); 82 container.classList.add('Dumby')
83 const htmlHolder = container.querySelector('.SemanticHtml') ?? container; 83 const htmlHolder = container.querySelector('.SemanticHtml') ?? container
84 const blocks = Array.from(htmlHolder.querySelectorAll('.dumby-block')); 84 const blocks = Array.from(htmlHolder.querySelectorAll('.dumby-block'))
85 const showcase = document.createElement('div'); 85 const showcase = document.createElement('div')
86 container.appendChild(showcase); 86 container.appendChild(showcase)
87 showcase.classList.add('Showcase'); 87 showcase.classList.add('Showcase')
88 const renderPromises = []; 88 const renderPromises = []
89 const modalContent = document.createElement('div'); 89 const modalContent = document.createElement('div')
90 container.appendChild(modalContent); 90 container.appendChild(modalContent)
91 const modal = new PlainModal(modalContent); 91 const modal = new PlainModal(modalContent)
92 92
93 const dumbymap = { 93 const dumbymap = {
94 layouts, 94 layouts,
@@ -102,50 +102,50 @@ export const generateMaps = (container, { delay, mapCallback }) => {
102 ...utils, 102 ...utils,
103 renderedMaps: () => 103 renderedMaps: () =>
104 Array.from( 104 Array.from(
105 container.querySelectorAll('.mapclay[data-render=fulfilled]'), 105 container.querySelectorAll('.mapclay[data-render=fulfilled]')
106 ), 106 ),
107 focusNextMap: throttle(utils.focusNextMap, utils.focusDelay), 107 focusNextMap: throttle(utils.focusNextMap, utils.focusDelay),
108 switchToNextLayout: throttle(utils.switchToNextLayout, 300), 108 switchToNextLayout: throttle(utils.switchToNextLayout, 300)
109 }, 109 }
110 }; 110 }
111 Object.entries(dumbymap.utils).forEach(([util, func]) => { 111 Object.entries(dumbymap.utils).forEach(([util, func]) => {
112 dumbymap.utils[util] = func.bind(dumbymap); 112 dumbymap.utils[util] = func.bind(dumbymap)
113 }); 113 })
114 114
115 // LeaderLine {{{ 115 // LeaderLine {{{
116 116
117 Array.from(container.querySelectorAll(docLinkSelector)).filter( 117 Array.from(container.querySelectorAll(docLinkSelector)).filter(
118 utils.createDocLink, 118 utils.createDocLink
119 ); 119 )
120 120
121 // Get anchors with "geo:" scheme 121 // Get anchors with "geo:" scheme
122 htmlHolder.anchors = []; 122 htmlHolder.anchors = []
123 const geoLinkCallback = link => { 123 const geoLinkCallback = link => {
124 link.onmouseover = () => addLeaderLines(link); 124 link.onmouseover = () => addLeaderLines(link)
125 link.onmouseout = () => removeLeaderLines(link); 125 link.onmouseout = () => removeLeaderLines(link)
126 link.onclick = event => { 126 link.onclick = event => {
127 event.preventDefault(); 127 event.preventDefault()
128 htmlHolder.anchors 128 htmlHolder.anchors
129 .filter(isAnchorPointedBy(link)) 129 .filter(isAnchorPointedBy(link))
130 .forEach(updateMapByMarker(link.xy)); 130 .forEach(updateMapByMarker(link.xy))
131 // TODO Just hide leader line and show it again 131 // TODO Just hide leader line and show it again
132 removeLeaderLines(link); 132 removeLeaderLines(link)
133 }; 133 }
134 }; 134 }
135 const geoLinks = Array.from( 135 const geoLinks = Array.from(
136 container.querySelectorAll(geoLinkSelector), 136 container.querySelectorAll(geoLinkSelector)
137 ).filter(l => utils.createGeoLink(l, geoLinkCallback)); 137 ).filter(l => utils.createGeoLink(l, geoLinkCallback))
138 138
139 const isAnchorPointedBy = link => anchor => { 139 const isAnchorPointedBy = link => anchor => {
140 const mapContainer = anchor.closest('.mapclay'); 140 const mapContainer = anchor.closest('.mapclay')
141 const isTarget = !link.targets || link.targets.includes(mapContainer.id); 141 const isTarget = !link.targets || link.targets.includes(mapContainer.id)
142 return anchor.title === link.url.pathname && isTarget; 142 return anchor.title === link.url.pathname && isTarget
143 }; 143 }
144 144
145 const isAnchorVisible = anchor => { 145 const isAnchorVisible = anchor => {
146 const mapContainer = anchor.closest('.mapclay'); 146 const mapContainer = anchor.closest('.mapclay')
147 return insideWindow(anchor) && insideParent(anchor, mapContainer); 147 return insideWindow(anchor) && insideParent(anchor, mapContainer)
148 }; 148 }
149 149
150 const drawLeaderLine = link => anchor => { 150 const drawLeaderLine = link => anchor => {
151 const line = new LeaderLine({ 151 const line = new LeaderLine({
@@ -153,223 +153,222 @@ export const generateMaps = (container, { delay, mapCallback }) => {
153 end: anchor, 153 end: anchor,
154 hide: true, 154 hide: true,
155 middleLabel: link.url.searchParams.get('text'), 155 middleLabel: link.url.searchParams.get('text'),
156 path: 'magnet', 156 path: 'magnet'
157 }); 157 })
158 line.show('draw', { duration: 300 }); 158 line.show('draw', { duration: 300 })
159 return line; 159 return line
160 }; 160 }
161 161
162 const addLeaderLines = link => { 162 const addLeaderLines = link => {
163 link.lines = htmlHolder.anchors 163 link.lines = htmlHolder.anchors
164 .filter(isAnchorPointedBy(link)) 164 .filter(isAnchorPointedBy(link))
165 .filter(isAnchorVisible) 165 .filter(isAnchorVisible)
166 .map(drawLeaderLine(link)); 166 .map(drawLeaderLine(link))
167 }; 167 }
168 168
169 const removeLeaderLines = link => { 169 const removeLeaderLines = link => {
170 if (!link.lines) return; 170 if (!link.lines) return
171 link.lines.forEach(line => line.remove()); 171 link.lines.forEach(line => line.remove())
172 link.lines = []; 172 link.lines = []
173 }; 173 }
174 174
175 const updateMapByMarker = xy => marker => { 175 const updateMapByMarker = xy => marker => {
176 const renderer = marker.closest('.mapclay')?.renderer; 176 const renderer = marker.closest('.mapclay')?.renderer
177 renderer.updateCamera({ center: xy }, true); 177 renderer.updateCamera({ center: xy }, true)
178 }; 178 }
179 179
180 const insideWindow = element => { 180 const insideWindow = element => {
181 const rect = element.getBoundingClientRect(); 181 const rect = element.getBoundingClientRect()
182 return ( 182 return (
183 rect.left > 0 && 183 rect.left > 0 &&
184 rect.right < window.innerWidth + rect.width && 184 rect.right < window.innerWidth + rect.width &&
185 rect.top > 0 && 185 rect.top > 0 &&
186 rect.bottom < window.innerHeight + rect.height 186 rect.bottom < window.innerHeight + rect.height
187 ); 187 )
188 }; 188 }
189 189
190 const insideParent = (childElement, parentElement) => { 190 const insideParent = (childElement, parentElement) => {
191 const childRect = childElement.getBoundingClientRect(); 191 const childRect = childElement.getBoundingClientRect()
192 const parentRect = parentElement.getBoundingClientRect(); 192 const parentRect = parentElement.getBoundingClientRect()
193 const offset = 20; 193 const offset = 20
194 194
195 return ( 195 return (
196 childRect.left > parentRect.left + offset && 196 childRect.left > parentRect.left + offset &&
197 childRect.right < parentRect.right - offset && 197 childRect.right < parentRect.right - offset &&
198 childRect.top > parentRect.top + offset && 198 childRect.top > parentRect.top + offset &&
199 childRect.bottom < parentRect.bottom - offset 199 childRect.bottom < parentRect.bottom - offset
200 ); 200 )
201 }; 201 }
202 //}}} 202 // }}}
203 // CSS observer {{{ 203 // CSS observer {{{
204 // Focus Map {{{ 204 // Focus Map {{{
205 // Set focusArea 205 // Set focusArea
206 206
207 const mapFocusObserver = () => 207 const mapFocusObserver = () =>
208 new MutationObserver(mutations => { 208 new window.MutationObserver(mutations => {
209 const mutation = mutations.at(-1); 209 const mutation = mutations.at(-1)
210 const target = mutation.target; 210 const target = mutation.target
211 const focus = target.classList.contains('focus'); 211 const focus = target.classList.contains('focus')
212 const shouldBeInShowcase = 212 const shouldBeInShowcase =
213 focus && 213 focus &&
214 showcase.checkVisibility({ 214 showcase.checkVisibility({
215 contentVisibilityAuto: true, 215 contentVisibilityAuto: true,
216 opacityProperty: true, 216 opacityProperty: true,
217 visibilityProperty: true, 217 visibilityProperty: true
218 }); 218 })
219 219
220 if (focus) { 220 if (focus) {
221 dumbymap.utils 221 dumbymap.utils
222 .renderedMaps() 222 .renderedMaps()
223 .filter(map => map.id !== target.id) 223 .filter(map => map.id !== target.id)
224 .forEach(map => map.classList.remove('focus')); 224 .forEach(map => map.classList.remove('focus'))
225 } 225 }
226 226
227 if (shouldBeInShowcase) { 227 if (shouldBeInShowcase) {
228 if (showcase.contains(target)) return; 228 if (showcase.contains(target)) return
229 229
230 // Placeholder for map in Showcase, it should has the same DOMRect 230 // Placeholder for map in Showcase, it should has the same DOMRect
231 const placeholder = target.cloneNode(true); 231 const placeholder = target.cloneNode(true)
232 placeholder.removeAttribute('id'); 232 placeholder.removeAttribute('id')
233 placeholder.classList.remove('mapclay', 'focus'); 233 placeholder.classList.remove('mapclay', 'focus')
234 target.parentElement.replaceChild(placeholder, target); 234 target.parentElement.replaceChild(placeholder, target)
235 235
236 // FIXME Maybe use @start-style for CSS 236 // FIXME Maybe use @start-style for CSS
237 // Trigger CSS transition, if placeholde is the olny chil element in block, 237 // Trigger CSS transition, if placeholde is the olny chil element in block,
238 // reduce its height to zero. 238 // reduce its height to zero.
239 // To make sure the original height of placeholder is applied, DOM changes seems needed 239 // To make sure the original height of placeholder is applied, DOM changes seems needed
240 // then set data-attribute for CSS selector to change height to 0 240 // then set data-attribute for CSS selector to change height to 0
241 placeholder.getBoundingClientRect(); 241 placeholder.getBoundingClientRect()
242 placeholder.setAttribute('data-placeholder', target.id); 242 placeholder.setAttribute('data-placeholder', target.id)
243 243
244 // To fit showcase, remove all inline style 244 // To fit showcase, remove all inline style
245 target.removeAttribute('style'); 245 target.removeAttribute('style')
246 showcase.appendChild(target); 246 showcase.appendChild(target)
247 247
248 // Resume rect from Semantic HTML to Showcase, with animation 248 // Resume rect from Semantic HTML to Showcase, with animation
249 animateRectTransition(target, placeholder.getBoundingClientRect(), { 249 animateRectTransition(target, placeholder.getBoundingClientRect(), {
250 duration: 300, 250 duration: 300,
251 resume: true, 251 resume: true
252 }); 252 })
253 } else if (showcase.contains(target)) { 253 } else if (showcase.contains(target)) {
254 // Check placeholder is inside Semantic HTML 254 // Check placeholder is inside Semantic HTML
255 const placeholder = htmlHolder.querySelector( 255 const placeholder = htmlHolder.querySelector(
256 `[data-placeholder="${target.id}"]`, 256 `[data-placeholder="${target.id}"]`
257 ); 257 )
258 if (!placeholder) 258 if (!placeholder) { throw Error(`Cannot fine placeholder for map "${target.id}"`) }
259 throw Error(`Cannot fine placeholder for map "${target.id}"`);
260 259
261 // Consider animation may fail, write callback 260 // Consider animation may fail, write callback
262 const afterAnimation = () => { 261 const afterAnimation = () => {
263 placeholder.parentElement.replaceChild(target, placeholder); 262 placeholder.parentElement.replaceChild(target, placeholder)
264 target.style = placeholder.style.cssText; 263 target.style = placeholder.style.cssText
265 placeholder.remove(); 264 placeholder.remove()
266 }; 265 }
267 266
268 // animation from Showcase to placeholder 267 // animation from Showcase to placeholder
269 animateRectTransition(target, placeholder.getBoundingClientRect(), { 268 animateRectTransition(target, placeholder.getBoundingClientRect(), {
270 duration: 300, 269 duration: 300
271 }).finished.finally(afterAnimation); 270 }).finished.finally(afterAnimation)
272 } 271 }
273 }); 272 })
274 // }}} 273 // }}}
275 // Layout {{{ 274 // Layout {{{
276 // press key to switch layout 275 // press key to switch layout
277 276
278 // observe layout change 277 // observe layout change
279 const layoutObserver = new MutationObserver(mutations => { 278 const layoutObserver = new window.MutationObserver(mutations => {
280 const mutation = mutations.at(-1); 279 const mutation = mutations.at(-1)
281 const oldLayout = mutation.oldValue; 280 const oldLayout = mutation.oldValue
282 const newLayout = container.getAttribute(mutation.attributeName); 281 const newLayout = container.getAttribute(mutation.attributeName)
283 282
284 // Apply handler for leaving/entering layouts 283 // Apply handler for leaving/entering layouts
285 if (oldLayout) { 284 if (oldLayout) {
286 layouts 285 layouts
287 .find(l => l.name === oldLayout) 286 .find(l => l.name === oldLayout)
288 ?.leaveHandler?.call(this, dumbymap); 287 ?.leaveHandler?.call(this, dumbymap)
289 } 288 }
290 289
291 Object.values(dumbymap) 290 Object.values(dumbymap)
292 .flat() 291 .flat()
293 .filter(ele => ele instanceof HTMLElement) 292 .filter(ele => ele instanceof window.HTMLElement)
294 .forEach(ele => ele.removeAttribute('style')); 293 .forEach(ele => ele.removeAttribute('style'))
295 294
296 if (newLayout) { 295 if (newLayout) {
297 layouts 296 layouts
298 .find(l => l.name === newLayout) 297 .find(l => l.name === newLayout)
299 ?.enterHandler?.call(this, dumbymap); 298 ?.enterHandler?.call(this, dumbymap)
300 } 299 }
301 300
302 // Since layout change may show/hide showcase, the current focused map may need to go into/outside showcase 301 // Since layout change may show/hide showcase, the current focused map may need to go into/outside showcase
303 // Reset attribute triggers MutationObserver which is observing it 302 // Reset attribute triggers MutationObserver which is observing it
304 const focusMap = 303 const focusMap =
305 container.querySelector('.mapclay.focus') ?? 304 container.querySelector('.mapclay.focus') ??
306 container.querySelector('.mapclay'); 305 container.querySelector('.mapclay')
307 focusMap?.classList?.add('focus'); 306 focusMap?.classList?.add('focus')
308 }); 307 })
309 layoutObserver.observe(container, { 308 layoutObserver.observe(container, {
310 attributes: true, 309 attributes: true,
311 attributeFilter: ['data-layout'], 310 attributeFilter: ['data-layout'],
312 attributeOldValue: true, 311 attributeOldValue: true,
313 characterDataOldValue: true, 312 characterDataOldValue: true
314 }); 313 })
315 314
316 onRemove(htmlHolder, () => layoutObserver.disconnect()); 315 onRemove(htmlHolder, () => layoutObserver.disconnect())
317 //}}} 316 // }}}
318 //}}} 317 // }}}
319 // Render Maps {{{ 318 // Render Maps {{{
320 319
321 const afterMapRendered = renderer => { 320 const afterMapRendered = renderer => {
322 const mapElement = renderer.target; 321 const mapElement = renderer.target
323 //FIXME 322 // FIXME
324 mapElement.renderer = renderer; 323 mapElement.renderer = renderer
325 mapElement.setAttribute('tabindex', '-1'); 324 mapElement.setAttribute('tabindex', '-1')
326 if (mapElement.getAttribute('data-render') === 'fulfilled') { 325 if (mapElement.getAttribute('data-render') === 'fulfilled') {
327 mapCache[mapElement.id] = renderer; 326 mapCache[mapElement.id] = renderer
328 } 327 }
329 328
330 // Execute callback from caller 329 // Execute callback from caller
331 mapCallback?.call(this, mapElement); 330 mapCallback?.call(this, mapElement)
332 const markers = geoLinks 331 const markers = geoLinks
333 .filter(link => !link.targets || link.targets.includes(mapElement.id)) 332 .filter(link => !link.targets || link.targets.includes(mapElement.id))
334 .map(link => ({ xy: link.xy, title: link.url.pathname })); 333 .map(link => ({ xy: link.xy, title: link.url.pathname }))
335 334
336 // Add markers with Geolinks 335 // Add markers with Geolinks
337 renderer.addMarkers(markers); 336 renderer.addMarkers(markers)
338 mapElement 337 mapElement
339 .querySelectorAll('.marker') 338 .querySelectorAll('.marker')
340 .forEach(marker => htmlHolder.anchors.push(marker)); 339 .forEach(marker => htmlHolder.anchors.push(marker))
341 340
342 // Work with Mutation Observer 341 // Work with Mutation Observer
343 const observer = mapFocusObserver(); 342 const observer = mapFocusObserver()
344 mapFocusObserver().observe(mapElement, { 343 mapFocusObserver().observe(mapElement, {
345 attributes: true, 344 attributes: true,
346 attributeFilter: ['class'], 345 attributeFilter: ['class'],
347 attributeOldValue: true, 346 attributeOldValue: true
348 }); 347 })
349 onRemove(mapElement, () => observer.disconnect()); 348 onRemove(mapElement, () => observer.disconnect())
350 }; 349 }
351 350
352 // Set unique ID for map container 351 // Set unique ID for map container
353 const mapIdList = []; 352 const mapIdList = []
354 const assignMapId = config => { 353 const assignMapId = config => {
355 let mapId = config.id; 354 let mapId = config.id
356 if (!mapId) { 355 if (!mapId) {
357 mapId = config.use?.split('/')?.at(-1); 356 mapId = config.use?.split('/')?.at(-1)
358 let counter = 1; 357 let counter = 1
359 while (!mapId || mapIdList.includes(mapId)) { 358 while (!mapId || mapIdList.includes(mapId)) {
360 mapId = `${config.use ?? 'unnamed'}-${counter}`; 359 mapId = `${config.use ?? 'unnamed'}-${counter}`
361 counter++; 360 counter++
362 } 361 }
363 config.id = mapId; 362 config.id = mapId
364 } 363 }
365 mapIdList.push(mapId); 364 mapIdList.push(mapId)
366 return config; 365 return config
367 }; 366 }
368 367
369 // Render each code block with "language-map" class 368 // Render each code block with "language-map" class
370 const elementsWithMapConfig = Array.from( 369 const elementsWithMapConfig = Array.from(
371 container.querySelectorAll('pre:has(.language-map)') ?? [], 370 container.querySelectorAll('pre:has(.language-map)') ?? []
372 ); 371 )
373 /** 372 /**
374 * updateAttributeByStep. 373 * updateAttributeByStep.
375 * 374 *
@@ -377,21 +376,21 @@ export const generateMaps = (container, { delay, mapCallback }) => {
377 */ 376 */
378 const updateAttributeByStep = ({ results, target, steps }) => { 377 const updateAttributeByStep = ({ results, target, steps }) => {
379 let passNum = results.filter( 378 let passNum = results.filter(
380 r => r.type === 'render' && r.state.match(/success|skip/), 379 r => r.type === 'render' && r.state.match(/success|skip/)
381 ).length; 380 ).length
382 const total = steps.length; 381 const total = steps.length
383 passNum += `/${total}`; 382 passNum += `/${total}`
384 if (results.filter(r => r.type === 'render').length === total) { 383 if (results.filter(r => r.type === 'render').length === total) {
385 passNum += '\u0020'; 384 passNum += '\u0020'
386 } 385 }
387 386
388 // FIXME HACK use MutationObserver for animation 387 // FIXME HACK use MutationObserver for animation
389 if (!target.animations) target.animations = Promise.resolve(); 388 if (!target.animations) target.animations = Promise.resolve()
390 target.animations = target.animations.then(async () => { 389 target.animations = target.animations.then(async () => {
391 await new Promise(resolve => setTimeout(resolve, 100)); 390 await new Promise(resolve => setTimeout(resolve, 100))
392 target.setAttribute('data-report', passNum); 391 target.setAttribute('data-report', passNum)
393 }); 392 })
394 }; 393 }
395 /** 394 /**
396 * config converter for mapclay.renderWith() 395 * config converter for mapclay.renderWith()
397 * 396 *
@@ -404,42 +403,42 @@ export const generateMaps = (container, { delay, mapCallback }) => {
404 ...config, 403 ...config,
405 aliases: { 404 aliases: {
406 ...defaultAliases, 405 ...defaultAliases,
407 ...(config.aliases ?? {}), 406 ...(config.aliases ?? {})
408 }, 407 },
409 stepCallback: updateAttributeByStep, 408 stepCallback: updateAttributeByStep
410 }); 409 })
411 const render = renderWith(configConverter); 410 const render = renderWith(configConverter)
412 elementsWithMapConfig.forEach(target => { 411 elementsWithMapConfig.forEach(target => {
413 // Get text in code block starts with markdown text '```map' 412 // Get text in code block starts with markdown text '```map'
414 const configText = target 413 const configText = target
415 .querySelector('.language-map') 414 .querySelector('.language-map')
416 .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content 415 .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content
417 // replace it by normal space 416 // replace it by normal space
418 .replace(/\u00A0/g, '\u0020'); 417 .replace(/\u00A0/g, '\u0020')
419 418
420 let configList = []; 419 let configList = []
421 try { 420 try {
422 configList = parseConfigsFromYaml(configText).map(assignMapId); 421 configList = parseConfigsFromYaml(configText).map(assignMapId)
423 } catch (_) { 422 } catch (_) {
424 console.warn('Fail to parse yaml config for element', target); 423 console.warn('Fail to parse yaml config for element', target)
425 return; 424 return
426 } 425 }
427 426
428 // If map in cache has the same ID, just put it into target 427 // If map in cache has the same ID, just put it into target
429 // So user won't feel anything changes when editing markdown 428 // So user won't feel anything changes when editing markdown
430 configList.forEach(config => { 429 configList.forEach(config => {
431 const cache = mapCache[config.id]; 430 const cache = mapCache[config.id]
432 if (!cache) return; 431 if (!cache) return
433 432
434 target.appendChild(cache.target); 433 target.appendChild(cache.target)
435 config.target = cache.target; 434 config.target = cache.target
436 }); 435 })
437 436
438 // trivial: if map cache is applied, do not show yaml text 437 // trivial: if map cache is applied, do not show yaml text
439 if (target.querySelector('.mapclay')) { 438 if (target.querySelector('.mapclay')) {
440 target 439 target
441 .querySelectorAll(':scope > :not([data-render=fulfilled])') 440 .querySelectorAll(':scope > :not([data-render=fulfilled])')
442 .forEach(e => e.remove()); 441 .forEach(e => e.remove())
443 } 442 }
444 443
445 // TODO Use debounce of user input to decide rendering timing 444 // TODO Use debounce of user input to decide rendering timing
@@ -447,76 +446,76 @@ export const generateMaps = (container, { delay, mapCallback }) => {
447 const timer = setTimeout( 446 const timer = setTimeout(
448 () => 447 () =>
449 render(target, configList).forEach(renderPromise => { 448 render(target, configList).forEach(renderPromise => {
450 renderPromises.push(renderPromise); 449 renderPromises.push(renderPromise)
451 renderPromise.then(afterMapRendered); 450 renderPromise.then(afterMapRendered)
452 }), 451 }),
453 delay ?? 1000, 452 delay ?? 1000
454 ); 453 )
455 onRemove(htmlHolder, () => { 454 onRemove(htmlHolder, () => {
456 clearTimeout(timer); 455 clearTimeout(timer)
457 }); 456 })
458 }); 457 })
459 // }}} 458 // }}}
460 // Menu {{{ 459 // Menu {{{
461 const menu = document.createElement('div'); 460 const menu = document.createElement('div')
462 menu.className = 'menu'; 461 menu.className = 'menu'
463 menu.style.display = 'none'; 462 menu.style.display = 'none'
464 menu.onclick = () => (menu.style.display = 'none'); 463 menu.onclick = () => (menu.style.display = 'none')
465 container.appendChild(menu); 464 container.appendChild(menu)
466 465
467 // Menu Items 466 // Menu Items
468 container.oncontextmenu = e => { 467 container.oncontextmenu = e => {
469 menu.replaceChildren(); 468 menu.replaceChildren()
470 menu.style.display = 'block'; 469 menu.style.display = 'block'
471 menu.style.cssText = `left: ${e.x - menu.offsetParent.offsetLeft + 10}px; top: ${e.y - menu.offsetParent.offsetTop + 5}px;`; 470 menu.style.cssText = `left: ${e.x - menu.offsetParent.offsetLeft + 10}px; top: ${e.y - menu.offsetParent.offsetTop + 5}px;`
472 e.preventDefault(); 471 e.preventDefault()
473 472
474 // GeoLinks 473 // GeoLinks
475 const selection = document.getSelection(); 474 const selection = document.getSelection()
476 if (selection.type === 'Range') { 475 if (selection.type === 'Range') {
477 const range = selection.getRangeAt(0); 476 const range = selection.getRangeAt(0)
478 menu.appendChild(menuItem.addGeoLink(dumbymap, range)); 477 menu.appendChild(menuItem.addGeoLink(dumbymap, range))
479 } 478 }
480 479
481 // Menu Items for map 480 // Menu Items for map
482 const map = e.target.closest('.mapclay'); 481 const map = e.target.closest('.mapclay')
483 if (map?.renderer?.results) { 482 if (map?.renderer?.results) {
484 // Focus or Print Map Results 483 // Focus or Print Map Results
485 menu.appendChild(menuItem.toggleMapFocus(map)); 484 menu.appendChild(menuItem.toggleMapFocus(map))
486 menu.appendChild(menuItem.renderResults(dumbymap, map)); 485 menu.appendChild(menuItem.renderResults(dumbymap, map))
487 } else { 486 } else {
488 // Toggle block focus 487 // Toggle block focus
489 const block = e.target.closest('.dumby-block'); 488 const block = e.target.closest('.dumby-block')
490 if (block) { 489 if (block) {
491 menu.appendChild(menuItem.toggleBlockFocus(block)); 490 menu.appendChild(menuItem.toggleBlockFocus(block))
492 } 491 }
493 } 492 }
494 493
495 // Menu Items for map/block/layout 494 // Menu Items for map/block/layout
496 if (!map || map.closest('.Showcase')) { 495 if (!map || map.closest('.Showcase')) {
497 menu.appendChild(menuItem.pickMapItem(dumbymap)); 496 menu.appendChild(menuItem.pickMapItem(dumbymap))
498 menu.appendChild(menuItem.pickBlockItem(dumbymap)); 497 menu.appendChild(menuItem.pickBlockItem(dumbymap))
499 menu.appendChild(menuItem.pickLayoutItem(dumbymap)); 498 menu.appendChild(menuItem.pickLayoutItem(dumbymap))
500 } 499 }
501 }; 500 }
502 501
503 // Remove menu when click outside 502 // Remove menu when click outside
504 const actionOutsideMenu = e => { 503 const actionOutsideMenu = e => {
505 if (menu.style.display === 'none') return; 504 if (menu.style.display === 'none') return
506 const rect = menu.getBoundingClientRect(); 505 const rect = menu.getBoundingClientRect()
507 if ( 506 if (
508 e.clientX < rect.left || 507 e.clientX < rect.left ||
509 e.clientX > rect.left + rect.width || 508 e.clientX > rect.left + rect.width ||
510 e.clientY < rect.top || 509 e.clientY < rect.top ||
511 e.clientY > rect.top + rect.height 510 e.clientY > rect.top + rect.height
512 ) { 511 ) {
513 menu.style.display = 'none'; 512 menu.style.display = 'none'
514 } 513 }
515 }; 514 }
516 document.addEventListener('click', actionOutsideMenu); 515 document.addEventListener('click', actionOutsideMenu)
517 onRemove(htmlHolder, () => 516 onRemove(htmlHolder, () =>
518 document.removeEventListener('click', actionOutsideMenu), 517 document.removeEventListener('click', actionOutsideMenu)
519 ); 518 )
520 //}}} 519 // }}}
521 return Object.seal(dumbymap); 520 return Object.seal(dumbymap)
522}; 521}