aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/dumbymap.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'src/dumbymap.mjs')
-rw-r--r--src/dumbymap.mjs265
1 files changed, 150 insertions, 115 deletions
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs
index 97c13af..9a6979b 100644
--- a/src/dumbymap.mjs
+++ b/src/dumbymap.mjs
@@ -14,8 +14,8 @@ import { register, fromEPSGCode } from 'ol/proj/proj4'
14 14
15/** CSS Selector for main components */ 15/** CSS Selector for main components */
16const mapBlockSelector = 'pre:has(.language-map), .mapclay-container' 16const mapBlockSelector = 'pre:has(.language-map), .mapclay-container'
17const docLinkSelector = 'a[href^="#"][title^="=>"]' 17const docLinkSelector = 'a[href^="#"][title^="=>"]:not(.doclink)'
18const geoLinkSelector = 'a[href^="geo:"]' 18const geoLinkSelector = 'a[href^="geo:"]:not(.geolink)'
19 19
20/** Default Layouts */ 20/** Default Layouts */
21const defaultLayouts = [ 21const defaultLayouts = [
@@ -137,31 +137,134 @@ export const generateMaps = (container, {
137 layouts = [], 137 layouts = [],
138 delay, 138 delay,
139 renderCallback, 139 renderCallback,
140 autoMap = false, 140 contentSelector,
141 render = defaultRender, 141 render = defaultRender,
142} = {}) => { 142} = {}) => {
143 /** Prepare Contaner */ 143 /** Prepare Contaner */
144 container.classList.add('Dumby') 144 container.classList.add('Dumby')
145 delete container.dataset.layout 145 delete container.dataset.layout
146 container.dataset.crs = crs
147 register(proj4)
146 148
147 /** Prepare Semantic HTML part and blocks of contents inside */ 149 /** Prepare Semantic HTML part and blocks of contents inside */
148 const htmlHolder = container.querySelector('.SemanticHtml, main, :scope > article') ?? 150 const htmlHolder = container.querySelector(contentSelector) ??
151 container.querySelector('.SemanticHtml, main, :scope > article') ??
149 Array.from(container.children).find(e => e.id?.match(/main|content/) || e.className?.match?.(/main|content/)) ?? 152 Array.from(container.children).find(e => e.id?.match(/main|content/) || e.className?.match?.(/main|content/)) ??
150 Array.from(container.children).sort((a, b) => a.textContent.length < b.textContent.length).at(0) 153 Array.from(container.children).sort((a, b) => a.textContent.length < b.textContent.length).at(0)
151 htmlHolder.classList.add('SemanticHtml') 154 htmlHolder.classList.add('SemanticHtml')
152 155
153 const blocks = htmlHolder.querySelectorAll('.dumby-block')
154 blocks.forEach(b => {
155 b.dataset.total = blocks.length
156 })
157
158 /** Prepare Showcase */ 156 /** Prepare Showcase */
159 const showcase = document.createElement('div') 157 const showcase = document.createElement('div')
160 container.appendChild(showcase) 158 container.appendChild(showcase)
161 showcase.classList.add('Showcase') 159 showcase.classList.add('Showcase')
162 160
161 /** WATCH: content of Semantic HTML */
162 const contentObserver = new window.MutationObserver((mutations) => {
163 const mutation = mutations.at(-1)
164 const addedNodes = Array.from(mutation.addedNodes)
165 const removedNodes = Array.from(mutation.removedNodes)
166 if (
167 addedNodes.length === 0 ||
168 [...addedNodes, ...removedNodes].find(node => node.classList?.contains('not-geolink'))
169 ) return
170
171 // Update dumby block
172 if ([...addedNodes, ...removedNodes].find(node => node.classList?.contains('dumby-block'))) {
173 const blocks = container.querySelectorAll('.dumby-block')
174 blocks.forEach(b => {
175 b.dataset.total = blocks.length
176 })
177 }
178
179 // Add GeoLinks/DocLinks by pattern
180 addedNodes.forEach((node) => {
181 if (!(node instanceof window.HTMLElement)) return
182
183 const linksWithGeoScheme = node.matches(geoLinkSelector)
184 ? [node]
185 : Array.from(node.querySelectorAll(geoLinkSelector))
186 linksWithGeoScheme.forEach(utils.createGeoLink)
187
188 const linksWithDocPattern = node.matches(docLinkSelector)
189 ? [node]
190 : Array.from(node.querySelectorAll(docLinkSelector))
191 linksWithDocPattern.forEach(utils.createDocLink)
192
193 // Render each code block with "language-map" class
194 const mapTargets = node.matches(mapBlockSelector)
195 ? [node]
196 : Array.from(node.querySelectorAll(mapBlockSelector))
197 mapTargets.forEach(renderMap)
198 })
199
200 // Add GeoLinks from plain texts
201 const addGeoScheme = addedNodes.map(utils.addGeoSchemeByText)
202 const crsString = container.dataset.crs
203 Promise.all([fromEPSGCode(crsString), ...addGeoScheme]).then((values) => {
204 values.slice(1)
205 .flat()
206 .map(utils.setGeoSchemeByCRS(crsString))
207 .filter(link => link)
208 .forEach(utils.createGeoLink)
209 })
210
211 // Remov all leader lines
212 htmlHolder.querySelectorAll('.with-leader-line')
213 .forEach(utils.removeLeaderLines)
214 })
215
216 contentObserver.observe(container, {
217 childList: true,
218 subtree: true,
219 })
220
221 /** WATCH: Layout changes */
222 const layoutObserver = new window.MutationObserver(mutations => {
223 const mutation = mutations.at(-1)
224 const oldLayout = mutation.oldValue
225 const newLayout = container.dataset.layout
226
227 // Apply handler for leaving/entering layouts
228 if (oldLayout) {
229 dumbymap.layouts
230 .find(l => l.name === oldLayout)
231 ?.leaveHandler?.call(this, dumbymap)
232 }
233
234 Object.values(dumbymap)
235 .flat()
236 .filter(ele => ele instanceof window.HTMLElement)
237 .forEach(ele => { ele.style.cssText = '' })
238
239 if (newLayout) {
240 dumbymap.layouts
241 .find(l => l.name === newLayout)
242 ?.enterHandler?.call(this, dumbymap)
243 }
244
245 // Since layout change may show/hide showcase, the current focused map may need to go into/outside showcase
246 // Reset attribute triggers MutationObserver which is observing it
247 const focusMap =
248 container.querySelector('.mapclay.focus') ??
249 container.querySelector('.mapclay')
250 focusMap?.classList?.add('focus')
251 })
252
253 layoutObserver.observe(container, {
254 attributes: true,
255 attributeFilter: ['data-layout'],
256 attributeOldValue: true,
257 })
258
259 container.dataset.layout = initialLayout ?? defaultLayouts[0].name
260
261 /** WATCH: Disconnect observers when container is removed */
262 onRemove(container, () => {
263 contentObserver.disconnect()
264 layoutObserver.disconnect()
265 })
266
163 /** Prepare Other Variables */ 267 /** Prepare Other Variables */
164 const renderPromises = []
165 const modalContent = document.createElement('div') 268 const modalContent = document.createElement('div')
166 container.appendChild(modalContent) 269 container.appendChild(modalContent)
167 const modal = new PlainModal(modalContent) 270 const modal = new PlainModal(modalContent)
@@ -172,7 +275,7 @@ export const generateMaps = (container, {
172 container, 275 container,
173 htmlHolder, 276 htmlHolder,
174 showcase, 277 showcase,
175 get blocks () { return Array.from(htmlHolder.querySelectorAll('.dumby-block')) }, 278 get blocks () { return Array.from(container.querySelectorAll('.dumby-block')) },
176 modal, 279 modal,
177 modalContent, 280 modalContent,
178 utils: { 281 utils: {
@@ -202,39 +305,8 @@ export const generateMaps = (container, {
202 } 305 }
203 }) 306 })
204 307
205 /** LINK: Create DocLinks */
206 container.querySelectorAll(docLinkSelector)
207 .forEach(utils.createDocLink)
208
209 /** LINK: Add external symbol on anchors */
210 container.querySelectorAll('a')
211 .forEach(a => {
212 if (typeof a.href === 'string' && a.href.startsWith('http') && !a.href.startsWith(window.location.origin)) {
213 a.classList.add('external')
214 }
215 })
216
217 /** LINK: Set CRS and GeoLinks */
218 const setCRS = (async () => {
219 register(proj4)
220 await fromEPSGCode(crs)
221 })()
222 const addGeoScheme = utils.addGeoSchemeByText(htmlHolder)
223
224 Promise.all([setCRS, addGeoScheme]).then(() => {
225 Array.from(container.querySelectorAll(geoLinkSelector))
226 .map(utils.setGeoSchemeByCRS(crs))
227 .filter(link => link instanceof window.HTMLAnchorElement)
228 .forEach(utils.createGeoLink)
229 })
230
231 /** LINK: remove all leaderline when onRemove() */
232 onRemove(htmlHolder, () =>
233 htmlHolder.querySelectorAll('.with-leader-line')
234 .forEach(utils.removeLeaderLines),
235 )
236 /** 308 /**
237 * mapFocusObserver. observe for map focus 309 * MAP: mapFocusObserver. observe for map focus
238 * @return {MutationObserver} observer 310 * @return {MutationObserver} observer
239 */ 311 */
240 const mapClassObserver = () => 312 const mapClassObserver = () =>
@@ -310,49 +382,8 @@ export const generateMaps = (container, {
310 } 382 }
311 }) 383 })
312 384
313 /** Observer for layout changes */
314 const layoutObserver = new window.MutationObserver(mutations => {
315 const mutation = mutations.at(-1)
316 const oldLayout = mutation.oldValue
317 const newLayout = container.dataset.layout
318
319 // Apply handler for leaving/entering layouts
320 if (oldLayout) {
321 dumbymap.layouts
322 .find(l => l.name === oldLayout)
323 ?.leaveHandler?.call(this, dumbymap)
324 }
325
326 Object.values(dumbymap)
327 .flat()
328 .filter(ele => ele instanceof window.HTMLElement)
329 .forEach(ele => { ele.style.cssText = '' })
330
331 if (newLayout) {
332 dumbymap.layouts
333 .find(l => l.name === newLayout)
334 ?.enterHandler?.call(this, dumbymap)
335 }
336
337 // Since layout change may show/hide showcase, the current focused map may need to go into/outside showcase
338 // Reset attribute triggers MutationObserver which is observing it
339 const focusMap =
340 container.querySelector('.mapclay.focus') ??
341 container.querySelector('.mapclay')
342 focusMap?.classList?.add('focus')
343 })
344 layoutObserver.observe(container, {
345 attributes: true,
346 attributeFilter: ['data-layout'],
347 attributeOldValue: true,
348 characterDataOldValue: true,
349 })
350
351 onRemove(htmlHolder, () => layoutObserver.disconnect())
352 container.dataset.layout = initialLayout ?? defaultLayouts[0].name
353
354 /** 385 /**
355 * afterMapRendered. callback of each map rendered 386 * MAP: afterMapRendered. callback of each map rendered
356 * 387 *
357 * @param {Object} renderer 388 * @param {Object} renderer
358 */ 389 */
@@ -380,7 +411,7 @@ export const generateMaps = (container, {
380 attributeFilter: ['class'], 411 attributeFilter: ['class'],
381 attributeOldValue: true, 412 attributeOldValue: true,
382 }) 413 })
383 onRemove(dumbymap.htmlHolder, () => { 414 onRemove(mapElement.closest('.SemanticHtml'), () => {
384 observer.disconnect() 415 observer.disconnect()
385 }) 416 })
386 417
@@ -394,8 +425,10 @@ export const generateMaps = (container, {
394 } 425 }
395 426
396 // Set unique ID for map container 427 // Set unique ID for map container
397 const mapIdList = [] 428 function assignMapId (config) {
398 const assignMapId = config => { 429 const mapIdList = Array.from(document.querySelectorAll('.mapclay'))
430 .map(map => map.id)
431 .filter(id => id)
399 let mapId = config.id?.replaceAll('\x20', '_') 432 let mapId = config.id?.replaceAll('\x20', '_')
400 if (!mapId) { 433 if (!mapId) {
401 mapId = config.use?.split('/')?.at(-1) 434 mapId = config.use?.split('/')?.at(-1)
@@ -410,23 +443,24 @@ export const generateMaps = (container, {
410 mapIdList.push(mapId) 443 mapIdList.push(mapId)
411 return config 444 return config
412 } 445 }
446 //
447 // if (autoMap && elementsWithMapConfig.length === 0) {
448 // const mapContainer = document.createElement('pre')
449 // mapContainer.className = 'mapclay-container'
450 // mapContainer.textContent = '#Created by DumbyMap'
451 // mapContainer.style.cssText = 'display: none;'
452 // htmlHolder.insertBefore(mapContainer, htmlHolder.firstElementChild)
453 // elementsWithMapConfig.push(mapContainer)
454 // }
455 //
413 456
414 // Render each code block with "language-map" class
415 const elementsWithMapConfig = Array.from(
416 container.querySelectorAll(mapBlockSelector) ?? [],
417 )
418 if (autoMap && elementsWithMapConfig.length === 0) {
419 const mapContainer = document.createElement('pre')
420 mapContainer.className = 'mapclay-container'
421 mapContainer.textContent = '#Created by DumbyMap'
422 mapContainer.style.cssText = 'display: none;'
423 htmlHolder.insertBefore(mapContainer, htmlHolder.firstElementChild)
424 elementsWithMapConfig.push(mapContainer)
425 }
426
427 /** Render each taget element for maps */
428 let order = 0 457 let order = 0
429 elementsWithMapConfig.forEach(target => { 458 /**
459 * MAP: Render each taget element for maps
460 *
461 * @param {} target
462 */
463 function renderMap (target) {
430 // Get text in code block starts with markdown text '```map' 464 // Get text in code block starts with markdown text '```map'
431 const configText = target 465 const configText = target
432 .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content 466 .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content
@@ -463,7 +497,6 @@ export const generateMaps = (container, {
463 const timer = setTimeout( 497 const timer = setTimeout(
464 () => { 498 () => {
465 render(target, configList).forEach(renderPromise => { 499 render(target, configList).forEach(renderPromise => {
466 renderPromises.push(renderPromise)
467 renderPromise.then(afterMapRendered) 500 renderPromise.then(afterMapRendered)
468 }) 501 })
469 Array.from(target.children).forEach(e => { 502 Array.from(target.children).forEach(e => {
@@ -476,14 +509,14 @@ export const generateMaps = (container, {
476 }, 509 },
477 delay ?? 1000, 510 delay ?? 1000,
478 ) 511 )
479 onRemove(htmlHolder, () => { 512 onRemove(target.closest('.SemanticHtml'), () => {
480 clearTimeout(timer) 513 clearTimeout(timer)
481 }) 514 })
482 }) 515 }
483 516
484 /** Prepare Context Menu */ 517 /** MENU: Prepare Context Menu */
485 const menu = document.createElement('div') 518 const menu = document.createElement('div')
486 menu.className = 'menu' 519 menu.classList.add('menu', 'dumby-menu')
487 menu.style.display = 'none' 520 menu.style.display = 'none'
488 menu.onclick = (e) => { 521 menu.onclick = (e) => {
489 const keepMenu = e.target.closest('.keep-menu') || e.target.classList.contains('.keep-menu') 522 const keepMenu = e.target.closest('.keep-menu') || e.target.classList.contains('.keep-menu')
@@ -491,9 +524,10 @@ export const generateMaps = (container, {
491 524
492 menu.style.display = 'none' 525 menu.style.display = 'none'
493 } 526 }
494 container.appendChild(menu) 527 document.body.appendChild(menu)
528 onRemove(menu, () => console.log('menu.removed'))
495 529
496 /** Menu Items for Context Menu */ 530 /** MENU: Menu Items for Context Menu */
497 container.oncontextmenu = e => { 531 container.oncontextmenu = e => {
498 const map = e.target.closest('.mapclay') 532 const map = e.target.closest('.mapclay')
499 const block = e.target.closest('.dumby-block') 533 const block = e.target.closest('.dumby-block')
@@ -502,6 +536,7 @@ export const generateMaps = (container, {
502 536
503 menu.replaceChildren() 537 menu.replaceChildren()
504 menu.style.display = 'block' 538 menu.style.display = 'block'
539 console.log(menu.style.display)
505 menu.style.cssText = `left: ${e.clientX - menu.offsetParent.offsetLeft + 10}px; top: ${e.clientY - menu.offsetParent.offsetTop + 5}px;` 540 menu.style.cssText = `left: ${e.clientX - menu.offsetParent.offsetLeft + 10}px; top: ${e.clientY - menu.offsetParent.offsetTop + 5}px;`
506 541
507 // Menu Items for map 542 // Menu Items for map
@@ -533,7 +568,7 @@ export const generateMaps = (container, {
533 return menu 568 return menu
534 } 569 }
535 570
536 /** Event Handler when clicking outside of Context Manu */ 571 /** MENU: Event Handler when clicking outside of Context Manu */
537 const actionOutsideMenu = e => { 572 const actionOutsideMenu = e => {
538 if (menu.style.display === 'none') return 573 if (menu.style.display === 'none') return
539 const keepMenu = e.target.closest('.keep-menu') || e.target.classList.contains('.keep-menu') 574 const keepMenu = e.target.closest('.keep-menu') || e.target.classList.contains('.keep-menu')
@@ -542,9 +577,9 @@ export const generateMaps = (container, {
542 const rect = menu.getBoundingClientRect() 577 const rect = menu.getBoundingClientRect()
543 if ( 578 if (
544 e.clientX < rect.left || 579 e.clientX < rect.left ||
545 e.clientX > rect.left + rect.width || 580 e.clientX > rect.left + rect.width ||
546 e.clientY < rect.top || 581 e.clientY < rect.top ||
547 e.clientY > rect.top + rect.height 582 e.clientY > rect.top + rect.height
548 ) { 583 ) {
549 menu.style.display = 'none' 584 menu.style.display = 'none'
550 } 585 }
@@ -554,7 +589,7 @@ export const generateMaps = (container, {
554 document.removeEventListener('click', actionOutsideMenu), 589 document.removeEventListener('click', actionOutsideMenu),
555 ) 590 )
556 591
557 /** Drag/Drop on map for new GeoLink */ 592 /** MOUSE: Drag/Drop on map for new GeoLink */
558 const pointByArrow = document.createElement('div') 593 const pointByArrow = document.createElement('div')
559 pointByArrow.className = 'point-by-arrow' 594 pointByArrow.className = 'point-by-arrow'
560 container.appendChild(pointByArrow) 595 container.appendChild(pointByArrow)