aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/dumbymap.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'src/dumbymap.mjs')
-rw-r--r--src/dumbymap.mjs215
1 files changed, 94 insertions, 121 deletions
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs
index 9a6979b..d05e7c4 100644
--- a/src/dumbymap.mjs
+++ b/src/dumbymap.mjs
@@ -4,7 +4,7 @@ import MarkdownItFootnote from 'markdown-it-footnote'
4import MarkdownItFrontMatter from 'markdown-it-front-matter' 4import MarkdownItFrontMatter from 'markdown-it-front-matter'
5import MarkdownItInjectLinenumbers from 'markdown-it-inject-linenumbers' 5import MarkdownItInjectLinenumbers from 'markdown-it-inject-linenumbers'
6import * as mapclay from 'mapclay' 6import * as mapclay from 'mapclay'
7import { onRemove, animateRectTransition, throttle, shiftByWindow } from './utils' 7import { onRemove, animateRectTransition, throttle, debounce, shiftByWindow } from './utils'
8import { Layout, SideBySide, Overlay, Sticky } from './Layout' 8import { Layout, SideBySide, Overlay, Sticky } from './Layout'
9import * as utils from './dumbyUtils' 9import * as utils from './dumbyUtils'
10import * as menuItem from './MenuItem' 10import * as menuItem from './MenuItem'
@@ -13,7 +13,7 @@ import proj4 from 'proj4'
13import { register, fromEPSGCode } from 'ol/proj/proj4' 13import { 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(code[class*=map]), .mapclay-container'
17const docLinkSelector = 'a[href^="#"][title^="=>"]:not(.doclink)' 17const docLinkSelector = 'a[href^="#"][title^="=>"]:not(.doclink)'
18const geoLinkSelector = 'a[href^="geo:"]:not(.geolink)' 18const geoLinkSelector = 'a[href^="geo:"]:not(.geolink)'
19 19
@@ -135,15 +135,17 @@ export const generateMaps = (container, {
135 crs = 'EPSG:4326', 135 crs = 'EPSG:4326',
136 initialLayout, 136 initialLayout,
137 layouts = [], 137 layouts = [],
138 delay, 138 mapDelay = 1000,
139 renderCallback, 139 renderCallback,
140 contentSelector, 140 contentSelector,
141 render = defaultRender, 141 render = defaultRender,
142} = {}) => { 142} = {}) => {
143 /** Prepare Contaner */ 143 /** Prepare Contaner */
144 if (container.classList.contains('Dumby')) return
144 container.classList.add('Dumby') 145 container.classList.add('Dumby')
145 delete container.dataset.layout 146 delete container.dataset.layout
146 container.dataset.crs = crs 147 container.dataset.crs = crs
148 container.dataset.layout = initialLayout ?? defaultLayouts.at(0).name
147 register(proj4) 149 register(proj4)
148 150
149 /** Prepare Semantic HTML part and blocks of contents inside */ 151 /** Prepare Semantic HTML part and blocks of contents inside */
@@ -158,111 +160,75 @@ export const generateMaps = (container, {
158 container.appendChild(showcase) 160 container.appendChild(showcase)
159 showcase.classList.add('Showcase') 161 showcase.classList.add('Showcase')
160 162
161 /** WATCH: content of Semantic HTML */ 163 /** WATCH: text content of Semantic HTML */
162 const contentObserver = new window.MutationObserver((mutations) => { 164 new window.MutationObserver((mutations) => {
163 const mutation = mutations.at(-1) 165 for (const mutation of mutations) {
164 const addedNodes = Array.from(mutation.addedNodes) 166 const node = mutation.target
165 const removedNodes = Array.from(mutation.removedNodes) 167 if (node.matches?.('.mapclay') || node.closest?.('.mapclay')) return
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 168
200 // Add GeoLinks from plain texts 169 // Add GeoLinks from plain texts
201 const addGeoScheme = addedNodes.map(utils.addGeoSchemeByText) 170 addGeoLinksByText(node)
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 171
211 // Remov all leader lines 172 // Render Map
212 htmlHolder.querySelectorAll('.with-leader-line') 173 const mapTarget = node.parentElement?.closest(mapBlockSelector)
213 .forEach(utils.removeLeaderLines) 174 if (mapTarget) {
214 }) 175 renderMap(mapTarget)
215 176 }
216 contentObserver.observe(container, { 177 }
217 childList: true, 178 }).observe(container, {
179 characterData: true,
218 subtree: true, 180 subtree: true,
219 }) 181 })
220 182
221 /** WATCH: Layout changes */ 183 const counter = 0
222 const layoutObserver = new window.MutationObserver(mutations => { 184 /** WATCH: children of Semantic HTML */
223 const mutation = mutations.at(-1) 185 new window.MutationObserver((mutations) => {
224 const oldLayout = mutation.oldValue 186 for (const mutation of mutations) {
225 const newLayout = container.dataset.layout 187 const target = mutation.target
226 188 if (target.matches?.('.mapclay') || target.closest?.('.mapclay')) return
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 189
234 Object.values(dumbymap) 190 // In case observer triggered by data attribute
235 .flat() 191 if (mutation.type === 'attribute') {
236 .filter(ele => ele instanceof window.HTMLElement) 192 delete target.dataset.initDumby
237 .forEach(ele => { ele.style.cssText = '' }) 193 }
238 194
239 if (newLayout) { 195 // Update dumby block
240 dumbymap.layouts 196 const dumbyBlockChanges = [...mutation.addedNodes, ...mutation.removedNodes]
241 .find(l => l.name === newLayout) 197 .find(node => node.classList?.contains('dumby-block'))
242 ?.enterHandler?.call(this, dumbymap) 198 if (dumbyBlockChanges) {
243 } 199 const blocks = container.querySelectorAll('.dumby-block')
200 blocks.forEach(b => {
201 b.dataset.total = blocks.length
202 })
203 }
244 204
245 // Since layout change may show/hide showcase, the current focused map may need to go into/outside showcase 205 // Add GeoLinks/DocLinks by pattern
246 // Reset attribute triggers MutationObserver which is observing it 206 target.querySelectorAll(geoLinkSelector)
247 const focusMap = 207 .forEach(utils.createGeoLink)
248 container.querySelector('.mapclay.focus') ?? 208 target.querySelectorAll(docLinkSelector)
249 container.querySelector('.mapclay') 209 .forEach(utils.createDocLink)
250 focusMap?.classList?.add('focus')
251 })
252 210
253 layoutObserver.observe(container, { 211 // Add GeoLinks from text nodes
212 // const addedNodes = Array.from(mutation.addedNodes)
213 if (mutation.type === 'attributes') {
214 addGeoLinksByText(target)
215 }
216
217 // Render code blocks for maps
218 const mapTargets = [
219 ...target.querySelectorAll(mapBlockSelector),
220 target.closest(mapBlockSelector),
221 ].filter(t => t)
222 mapTargets.forEach(renderMap)
223 }
224 }).observe(container, {
254 attributes: true, 225 attributes: true,
255 attributeFilter: ['data-layout'], 226 attributesFilter: ['data-init-dumby'],
256 attributeOldValue: true, 227 childList: true,
228 subtree: true,
257 }) 229 })
258 230
259 container.dataset.layout = initialLayout ?? defaultLayouts[0].name 231 container.dataset.initDumby = 'true'
260
261 /** WATCH: Disconnect observers when container is removed */
262 onRemove(container, () => {
263 contentObserver.disconnect()
264 layoutObserver.disconnect()
265 })
266 232
267 /** Prepare Other Variables */ 233 /** Prepare Other Variables */
268 const modalContent = document.createElement('div') 234 const modalContent = document.createElement('div')
@@ -306,6 +272,22 @@ export const generateMaps = (container, {
306 }) 272 })
307 273
308 /** 274 /**
275 * LINKS: addGeoLinksByText.
276 *
277 * @param {Node} node
278 */
279 function addGeoLinksByText (node) {
280 const addGeoScheme = utils.addGeoSchemeByText(node)
281 const crsString = container.dataset.crs
282 Promise.all([fromEPSGCode(crsString), addGeoScheme]).then((values) => {
283 values.at(-1)
284 .map(utils.setGeoSchemeByCRS(crsString))
285 .filter(link => link)
286 .forEach(utils.createGeoLink)
287 })
288 }
289
290 /**
309 * MAP: mapFocusObserver. observe for map focus 291 * MAP: mapFocusObserver. observe for map focus
310 * @return {MutationObserver} observer 292 * @return {MutationObserver} observer
311 */ 293 */
@@ -411,9 +393,6 @@ export const generateMaps = (container, {
411 attributeFilter: ['class'], 393 attributeFilter: ['class'],
412 attributeOldValue: true, 394 attributeOldValue: true,
413 }) 395 })
414 onRemove(mapElement.closest('.SemanticHtml'), () => {
415 observer.disconnect()
416 })
417 396
418 // Focus current map is no map is focused 397 // Focus current map is no map is focused
419 if ( 398 if (
@@ -454,13 +433,14 @@ export const generateMaps = (container, {
454 // } 433 // }
455 // 434 //
456 435
457 let order = 0
458 /** 436 /**
459 * MAP: Render each taget element for maps 437 * MAP: Render each taget element for maps by text content in YAML
460 * 438 *
461 * @param {} target 439 * @param {HTMLElement} target
462 */ 440 */
463 function renderMap (target) { 441 function renderMap (target) {
442 if (!target.isConnected) return
443
464 // Get text in code block starts with markdown text '```map' 444 // Get text in code block starts with markdown text '```map'
465 const configText = target 445 const configText = target
466 .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content 446 .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content
@@ -492,26 +472,20 @@ export const generateMaps = (container, {
492 .forEach(e => e.remove()) 472 .forEach(e => e.remove())
493 } 473 }
494 474
495 // TODO Use debounce of user input to decide rendering timing 475 if (!target.renderMap) {
496 // Render maps with delay 476 target.renderMap = debounce(() => {
497 const timer = setTimeout( 477 // Render maps
498 () => {
499 render(target, configList).forEach(renderPromise => { 478 render(target, configList).forEach(renderPromise => {
500 renderPromise.then(afterMapRendered) 479 renderPromise.then(afterMapRendered)
501 }) 480 })
502 Array.from(target.children).forEach(e => { 481 Array.from(target.children).forEach(e => {
503 e.style.order = order
504 order++
505 if (e.dataset.render === 'fulfilled') { 482 if (e.dataset.render === 'fulfilled') {
506 afterMapRendered(e.renderer) 483 afterMapRendered(e.renderer)
507 } 484 }
508 }) 485 }), mapDelay
509 }, 486 })
510 delay ?? 1000, 487 }
511 ) 488 target.renderMap()
512 onRemove(target.closest('.SemanticHtml'), () => {
513 clearTimeout(timer)
514 })
515 } 489 }
516 490
517 /** MENU: Prepare Context Menu */ 491 /** MENU: Prepare Context Menu */
@@ -525,7 +499,6 @@ export const generateMaps = (container, {
525 menu.style.display = 'none' 499 menu.style.display = 'none'
526 } 500 }
527 document.body.appendChild(menu) 501 document.body.appendChild(menu)
528 onRemove(menu, () => console.log('menu.removed'))
529 502
530 /** MENU: Menu Items for Context Menu */ 503 /** MENU: Menu Items for Context Menu */
531 container.oncontextmenu = e => { 504 container.oncontextmenu = e => {
@@ -536,7 +509,6 @@ export const generateMaps = (container, {
536 509
537 menu.replaceChildren() 510 menu.replaceChildren()
538 menu.style.display = 'block' 511 menu.style.display = 'block'
539 console.log(menu.style.display)
540 menu.style.cssText = `left: ${e.clientX - menu.offsetParent.offsetLeft + 10}px; top: ${e.clientY - menu.offsetParent.offsetTop + 5}px;` 512 menu.style.cssText = `left: ${e.clientX - menu.offsetParent.offsetLeft + 10}px; top: ${e.clientY - menu.offsetParent.offsetTop + 5}px;`
541 513
542 // Menu Items for map 514 // Menu Items for map
@@ -577,15 +549,15 @@ export const generateMaps = (container, {
577 const rect = menu.getBoundingClientRect() 549 const rect = menu.getBoundingClientRect()
578 if ( 550 if (
579 e.clientX < rect.left || 551 e.clientX < rect.left ||
580 e.clientX > rect.left + rect.width || 552 e.clientX > rect.left + rect.width ||
581 e.clientY < rect.top || 553 e.clientY < rect.top ||
582 e.clientY > rect.top + rect.height 554 e.clientY > rect.top + rect.height
583 ) { 555 ) {
584 menu.style.display = 'none' 556 menu.style.display = 'none'
585 } 557 }
586 } 558 }
587 document.addEventListener('click', actionOutsideMenu) 559 document.addEventListener('click', actionOutsideMenu)
588 onRemove(htmlHolder, () => 560 onRemove(container, () =>
589 document.removeEventListener('click', actionOutsideMenu), 561 document.removeEventListener('click', actionOutsideMenu),
590 ) 562 )
591 563
@@ -624,5 +596,6 @@ export const generateMaps = (container, {
624 } 596 }
625 } 597 }
626 598
599 /** Return Object for utils */
627 return Object.seal(dumbymap) 600 return Object.seal(dumbymap)
628} 601}