diff options
Diffstat (limited to 'src/dumbymap.mjs')
-rw-r--r-- | src/dumbymap.mjs | 282 |
1 files changed, 167 insertions, 115 deletions
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 6b64ee4..5800351 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
@@ -217,90 +217,6 @@ export const generateMaps = async (container, callback) => { | |||
217 | childRect.bottom < parentRect.bottom - offset | 217 | childRect.bottom < parentRect.bottom - offset |
218 | } | 218 | } |
219 | //}}} | 219 | //}}} |
220 | // Render Maps {{{ | ||
221 | |||
222 | const afterEachMapLoaded = (mapContainer) => { | ||
223 | const focusClickedMap = () => { | ||
224 | if (container.getAttribute('data-layout') !== 'none') return | ||
225 | |||
226 | container.querySelectorAll('.map-container') | ||
227 | .forEach(c => c.classList.remove('focus')) | ||
228 | mapContainer.classList.add('focus') | ||
229 | } | ||
230 | mapContainer.onclick = focusClickedMap | ||
231 | } | ||
232 | |||
233 | // Set unique ID for map container | ||
234 | const mapIdList = [] | ||
235 | const assignMapId = (config) => { | ||
236 | let mapId = config.id | ||
237 | if (!mapId) { | ||
238 | mapId = config.use?.split('/')?.at(-1) | ||
239 | let counter = 2 | ||
240 | while (mapIdList.includes(mapId)) { | ||
241 | mapId = `${config.use}.${counter}` | ||
242 | counter++ | ||
243 | } | ||
244 | config.id = mapId | ||
245 | } | ||
246 | mapIdList.push(mapId) | ||
247 | return config | ||
248 | } | ||
249 | |||
250 | // Render each code block with "language-map" class | ||
251 | const render = renderWith(config => ({ width: "100%", ...config })) | ||
252 | const renderTargets = Array.from(container.querySelectorAll('pre:has(.language-map)')) | ||
253 | .map(async (target) => { | ||
254 | // Get text in code block starts with '```map' | ||
255 | const configText = target.querySelector('.language-map') | ||
256 | .textContent | ||
257 | // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content | ||
258 | // replace it by normal space | ||
259 | .replace(/\u00A0/g, '\u0020') | ||
260 | |||
261 | let configList = [] | ||
262 | try { | ||
263 | configList = parseConfigsFromYaml(configText).map(assignMapId) | ||
264 | } catch (_) { | ||
265 | console.warn('Fail to parse yaml config for element', target) | ||
266 | } | ||
267 | |||
268 | // Render maps | ||
269 | return render(target, configList) | ||
270 | .then(results => { | ||
271 | results.forEach((mapByConfig) => { | ||
272 | if (mapByConfig.status === 'fulfilled') { | ||
273 | afterEachMapLoaded(mapByConfig.value) | ||
274 | return mapByConfig.value | ||
275 | } else { | ||
276 | console.error('Fail to render target element', mapByConfig.reason) | ||
277 | } | ||
278 | }) | ||
279 | }) | ||
280 | }) | ||
281 | |||
282 | const renderAllTargets = Promise.all(renderTargets) | ||
283 | renderAllTargets.then(() => { | ||
284 | console.info('Finish Rendering') | ||
285 | |||
286 | const maps = htmlHolder.querySelectorAll('.map-container') ?? [] | ||
287 | Array.from(maps) | ||
288 | .forEach(ele => { | ||
289 | callback(ele) | ||
290 | const markers = geoLinks | ||
291 | .filter(link => !link.targets || link.targets.includes(ele.id)) | ||
292 | .map(link => ({ | ||
293 | xy: link.xy, | ||
294 | title: link.url.pathname | ||
295 | })) | ||
296 | ele?.renderer?.addMarkers(markers) | ||
297 | }) | ||
298 | |||
299 | htmlHolder.querySelectorAll('.marker') | ||
300 | .forEach(marker => htmlHolder.anchors.push(marker)) | ||
301 | }) | ||
302 | |||
303 | //}}} | ||
304 | // Draggable Blocks{{{ | 220 | // Draggable Blocks{{{ |
305 | // Add draggable part for blocks | 221 | // Add draggable part for blocks |
306 | htmlHolder.blocks = Array.from(htmlHolder.querySelectorAll('.draggable-block')) | 222 | htmlHolder.blocks = Array.from(htmlHolder.querySelectorAll('.draggable-block')) |
@@ -322,16 +238,35 @@ export const generateMaps = async (container, callback) => { | |||
322 | 238 | ||
323 | // }}} | 239 | // }}} |
324 | // CSS observer {{{ | 240 | // CSS observer {{{ |
325 | |||
326 | // Set focusArea | 241 | // Set focusArea |
327 | const showcase = document.createElement('div') | 242 | const showcase = document.createElement('div') |
328 | container.appendChild(showcase) | 243 | container.appendChild(showcase) |
329 | showcase.classList.add('Showcase') | 244 | showcase.classList.add('Showcase') |
330 | const mapPlaceholder = document.createElement('div') | ||
331 | mapPlaceholder.id = 'mapPlaceholder' | ||
332 | showcase.appendChild(mapPlaceholder) | ||
333 | 245 | ||
334 | // Layout{{{ | 246 | // Focus Map {{{ |
247 | const mapFocusObserver = () => new MutationObserver((mutations) => { | ||
248 | const mutation = mutations.at(-1) | ||
249 | const target = mutation.target | ||
250 | const focus = target.getAttribute(mutation.attributeName) === 'true' | ||
251 | const shouldBeInShowcase = focus && getComputedStyle(showcase).display !== 'none' | ||
252 | |||
253 | if (shouldBeInShowcase) { | ||
254 | if (showcase.contains(target)) return | ||
255 | |||
256 | const placeholder = document.createElement('div') | ||
257 | placeholder.setAttribute('data-placeholder', target.id) | ||
258 | placeholder.style.width = target.style.width | ||
259 | target.parentElement.replaceChild(placeholder, target) | ||
260 | showcase.appendChild(target) | ||
261 | } else if (showcase.contains(target)) { | ||
262 | const placeholder = htmlHolder.querySelector(`[data-placeholder="${target.id}"]`) | ||
263 | if (!placeholder) throw Error(`Cannot fine placeholder for map "${target.id}"`) | ||
264 | placeholder.parentElement.replaceChild(target, placeholder) | ||
265 | placeholder.remove() | ||
266 | } | ||
267 | }) | ||
268 | // }}} | ||
269 | // Layout {{{ | ||
335 | 270 | ||
336 | // press key to switch layout | 271 | // press key to switch layout |
337 | const layouts = ['none', 'side', 'overlay'] | 272 | const layouts = ['none', 'side', 'overlay'] |
@@ -339,41 +274,62 @@ export const generateMaps = async (container, callback) => { | |||
339 | 274 | ||
340 | // FIXME Use UI to switch layouts | 275 | // FIXME Use UI to switch layouts |
341 | const originalKeyDown = document.onkeydown | 276 | const originalKeyDown = document.onkeydown |
342 | document.onkeydown = (event) => { | 277 | document.onkeydown = (e) => { |
343 | originalKeyDown(event) | 278 | const event = originalKeyDown(e) |
279 | if (!event) return | ||
280 | |||
344 | if (event.key === 'x' && container.querySelector('.map-container')) { | 281 | if (event.key === 'x' && container.querySelector('.map-container')) { |
282 | e.preventDefault() | ||
345 | let currentLayout = container.getAttribute('data-layout') | 283 | let currentLayout = container.getAttribute('data-layout') |
346 | currentLayout = currentLayout ? currentLayout : 'none' | 284 | currentLayout = currentLayout ? currentLayout : 'none' |
347 | const nextLayout = layouts[(layouts.indexOf(currentLayout) + 1) % layouts.length] | 285 | const nextIndex = (layouts.indexOf(currentLayout) + 1) % layouts.length |
286 | const nextLayout = layouts[nextIndex] | ||
348 | 287 | ||
349 | container.setAttribute("data-layout", nextLayout) | 288 | container.setAttribute("data-layout", nextLayout) |
350 | } | 289 | } |
290 | |||
291 | if (event.key === 'Tab') { | ||
292 | e.preventDefault() | ||
293 | |||
294 | const selector = '.map-container, [data-placeholder]' | ||
295 | const candidates = Array.from(htmlHolder.querySelectorAll(selector)) | ||
296 | if (candidates.length === 0) return | ||
297 | |||
298 | const currentFocus = htmlHolder.querySelector('.map-container[data-focus=true]') | ||
299 | ?? htmlHolder.querySelector('[data-placeholder]') | ||
300 | Array.from(container.querySelectorAll('.map-container')).forEach(e => | ||
301 | e.removeAttribute('data-focus') | ||
302 | ); | ||
303 | const index = currentFocus | ||
304 | ? (candidates.indexOf(currentFocus) + (event.shiftKey ? -1 : 1)) % candidates.length | ||
305 | : 0 | ||
306 | const nextFocus = candidates.at(index) | ||
307 | nextFocus.setAttribute('data-focus', "true") | ||
308 | } | ||
351 | } | 309 | } |
352 | 310 | ||
353 | // observe layout change | 311 | // observe layout change |
354 | const layoutObserver = new MutationObserver(() => { | 312 | const layoutObserver = new MutationObserver((mutations) => { |
355 | const layout = container.getAttribute('data-layout') | 313 | const mutation = mutations.at(-1) |
356 | htmlHolder.blocks.forEach(b => b.style.display = "block") | 314 | const layout = container.getAttribute(mutation.attributeName) |
357 | 315 | ||
358 | if (layout === 'none') { | 316 | // Trigger Mutation Observer |
359 | mapPlaceholder.innerHTML = "" | 317 | const focusMap = container.querySelector('.map-container[data-focus=true]') |
360 | const map = showcase.querySelector('.map-container') | 318 | ?? container.querySelector('.map-container') |
361 | // Swap focused map and palceholder in markdown | 319 | focusMap?.setAttribute('data-focus', 'true') |
362 | if (map) { | 320 | |
363 | mapPlaceholder.parentElement?.replaceChild(map, mapPlaceholder) | 321 | // Check empty block with map-container in showcase |
364 | showcase.append(mapPlaceholder) | 322 | htmlHolder.blocks.forEach(b => { |
365 | } | 323 | const contentChildren = Array.from(b.querySelectorAll(':scope > :not(.draggable)')) ?? [] |
366 | } else { | 324 | if (contentChildren.length === 1 |
367 | // If paceholder is not set, create one and put map into focusArea | 325 | && elementsWithMapConfig.includes(contentChildren[0]) |
368 | if (showcase.contains(mapPlaceholder)) { | 326 | && !contentChildren[0].querySelector('.map-container') |
369 | const mapContainer = container.querySelector('.map-container.focus') ?? container.querySelector('.map-container') | 327 | ) { |
370 | mapPlaceholder.innerHTML = `<div>Placeholder</div>` | 328 | b.style.display = "none" |
371 | // TODO Get snapshot image | 329 | } else { |
372 | // mapPlaceholder.src = map.map.getCanvas().toDataURL() | 330 | b.style.display = "block" |
373 | mapContainer.parentElement?.replaceChild(mapPlaceholder, mapContainer) | ||
374 | showcase.appendChild(mapContainer) | ||
375 | } | 331 | } |
376 | } | 332 | }) |
377 | 333 | ||
378 | if (layout === 'overlay') { | 334 | if (layout === 'overlay') { |
379 | let [x, y] = [0, 0]; | 335 | let [x, y] = [0, 0]; |
@@ -398,7 +354,9 @@ export const generateMaps = async (container, callback) => { | |||
398 | block.removeAttribute('style') | 354 | block.removeAttribute('style') |
399 | try { | 355 | try { |
400 | block.draggableInstance.remove() | 356 | block.draggableInstance.remove() |
401 | } catch (_) { null } | 357 | } catch (_) { |
358 | null | ||
359 | } | ||
402 | }) | 360 | }) |
403 | } | 361 | } |
404 | }); | 362 | }); |
@@ -411,5 +369,99 @@ export const generateMaps = async (container, callback) => { | |||
411 | onRemove(htmlHolder, () => layoutObserver.disconnect()) | 369 | onRemove(htmlHolder, () => layoutObserver.disconnect()) |
412 | //}}} | 370 | //}}} |
413 | //}}} | 371 | //}}} |
372 | // Render Maps {{{ | ||
373 | |||
374 | const afterEachMapLoaded = (mapContainer) => { | ||
375 | const focusClickedMap = () => { | ||
376 | container.querySelectorAll('.map-container') | ||
377 | .forEach(c => c.removeAttribute('data-focus')) | ||
378 | mapContainer.setAttribute('data-focus', true) | ||
379 | } | ||
380 | mapContainer.onclick = focusClickedMap | ||
381 | mapContainer.setAttribute('tabindex', "-1") | ||
382 | |||
383 | const observer = mapFocusObserver() | ||
384 | mapFocusObserver().observe(mapContainer, { | ||
385 | attributes: true, | ||
386 | attributeFilter: ["data-focus"], | ||
387 | attributeOldValue: true | ||
388 | }); | ||
389 | onRemove(mapContainer, () => observer.disconnect()) | ||
390 | } | ||
391 | |||
392 | // Set unique ID for map container | ||
393 | const mapIdList = [] | ||
394 | const assignMapId = (config) => { | ||
395 | let mapId = config.id | ||
396 | if (!mapId) { | ||
397 | mapId = config.use?.split('/')?.at(-1) | ||
398 | let counter = 1 | ||
399 | while (!mapId || mapIdList.includes(mapId)) { | ||
400 | mapId = `${config.use ?? "unnamed"}-${counter}` | ||
401 | counter++ | ||
402 | } | ||
403 | config.id = mapId | ||
404 | } | ||
405 | mapIdList.push(mapId) | ||
406 | return config | ||
407 | } | ||
408 | |||
409 | // Render each code block with "language-map" class | ||
410 | const elementsWithMapConfig = Array.from(container.querySelectorAll('pre:has(.language-map)') ?? []) | ||
411 | const render = renderWith(config => ({ width: "100%", ...config })) | ||
412 | const renderTargets = elementsWithMapConfig | ||
413 | .map(async (target) => { | ||
414 | // Get text in code block starts with '```map' | ||
415 | const configText = target.querySelector('.language-map') | ||
416 | .textContent | ||
417 | // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content | ||
418 | // replace it by normal space | ||
419 | .replace(/\u00A0/g, '\u0020') | ||
420 | |||
421 | let configList = [] | ||
422 | try { | ||
423 | configList = parseConfigsFromYaml(configText).map(assignMapId) | ||
424 | } catch (_) { | ||
425 | console.warn('Fail to parse yaml config for element', target) | ||
426 | } | ||
427 | |||
428 | // Render maps | ||
429 | return render(target, configList) | ||
430 | .then(results => { | ||
431 | results.forEach((mapByConfig) => { | ||
432 | if (mapByConfig.status === 'fulfilled') { | ||
433 | afterEachMapLoaded(mapByConfig.value) | ||
434 | return mapByConfig.value | ||
435 | } else { | ||
436 | console.error('Fail to render target element', mapByConfig.reason) | ||
437 | } | ||
438 | }) | ||
439 | }) | ||
440 | }) | ||
441 | |||
442 | const renderAllTargets = Promise.all(renderTargets) | ||
443 | .then(() => { | ||
444 | console.info('Finish Rendering') | ||
445 | |||
446 | const maps = htmlHolder.querySelectorAll('.map-container') ?? [] | ||
447 | Array.from(maps) | ||
448 | .forEach(ele => { | ||
449 | callback(ele) | ||
450 | const markers = geoLinks | ||
451 | .filter(link => !link.targets || link.targets.includes(ele.id)) | ||
452 | .map(link => ({ | ||
453 | xy: link.xy, | ||
454 | title: link.url.pathname | ||
455 | })) | ||
456 | ele?.renderer?.addMarkers(markers) | ||
457 | }) | ||
458 | |||
459 | htmlHolder.querySelectorAll('.marker') | ||
460 | .forEach(marker => htmlHolder.anchors.push(marker)) | ||
461 | |||
462 | return maps | ||
463 | }) | ||
464 | |||
465 | //}}} | ||
414 | return renderAllTargets | 466 | return renderAllTargets |
415 | } | 467 | } |