diff options
Diffstat (limited to 'src/dumbymap.mjs')
-rw-r--r-- | src/dumbymap.mjs | 265 |
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 */ |
16 | const mapBlockSelector = 'pre:has(.language-map), .mapclay-container' | 16 | const mapBlockSelector = 'pre:has(.language-map), .mapclay-container' |
17 | const docLinkSelector = 'a[href^="#"][title^="=>"]' | 17 | const docLinkSelector = 'a[href^="#"][title^="=>"]:not(.doclink)' |
18 | const geoLinkSelector = 'a[href^="geo:"]' | 18 | const geoLinkSelector = 'a[href^="geo:"]:not(.geolink)' |
19 | 19 | ||
20 | /** Default Layouts */ | 20 | /** Default Layouts */ |
21 | const defaultLayouts = [ | 21 | const 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) |