diff options
Diffstat (limited to 'src/dumbymap.mjs')
-rw-r--r-- | src/dumbymap.mjs | 153 |
1 files changed, 91 insertions, 62 deletions
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 8e26ee6..706e874 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
@@ -3,7 +3,7 @@ import MarkdownItAnchor from 'markdown-it-anchor' | |||
3 | import MarkdownItFootnote from 'markdown-it-footnote' | 3 | import MarkdownItFootnote from 'markdown-it-footnote' |
4 | import MarkdownItFrontMatter from 'markdown-it-front-matter' | 4 | import MarkdownItFrontMatter from 'markdown-it-front-matter' |
5 | import MarkdownItInjectLinenumbers from 'markdown-it-inject-linenumbers' | 5 | import MarkdownItInjectLinenumbers from 'markdown-it-inject-linenumbers' |
6 | import { renderWith, defaultAliases, parseConfigsFromYaml } from 'mapclay' | 6 | import * as mapclay from 'mapclay' |
7 | import { replaceTextNodes, onRemove, animateRectTransition, throttle, shiftByWindow } from './utils' | 7 | import { replaceTextNodes, onRemove, animateRectTransition, throttle, shiftByWindow } from './utils' |
8 | import { Layout, SideBySide, Overlay } from './Layout' | 8 | import { Layout, SideBySide, Overlay } from './Layout' |
9 | import * as utils from './dumbyUtils' | 9 | import * as utils from './dumbyUtils' |
@@ -14,7 +14,7 @@ import { register, fromEPSGCode } from 'ol/proj/proj4' | |||
14 | import LeaderLine from 'leader-line' | 14 | import LeaderLine from 'leader-line' |
15 | 15 | ||
16 | /** CSS Selector for main components */ | 16 | /** CSS Selector for main components */ |
17 | const mapBlockSelector = 'pre:has(.language-map)' | 17 | const mapBlockSelector = 'pre:has(.language-map), .mapclay-container' |
18 | const docLinkSelector = 'a[href^="#"][title^="=>"]' | 18 | const docLinkSelector = 'a[href^="#"][title^="=>"]' |
19 | const geoLinkSelector = 'a[href^="geo:"]' | 19 | const geoLinkSelector = 'a[href^="geo:"]' |
20 | 20 | ||
@@ -86,6 +86,68 @@ export const markdown2HTML = (container, mdContent) => { | |||
86 | } | 86 | } |
87 | 87 | ||
88 | /** | 88 | /** |
89 | * defaultBlocks. | ||
90 | * @description Default way to get blocks from Semantic HTML | ||
91 | * @param {HTMLElement} root | ||
92 | */ | ||
93 | const defaultBlocks = root => { | ||
94 | const articles = root.querySelectorAll('article') | ||
95 | if (articles.length > 2) return articles | ||
96 | |||
97 | const others = Array.from( | ||
98 | root.querySelectorAll(':has(>p, >blockquote, >pre, >ul, >ol, >table, >details)'), | ||
99 | ) | ||
100 | .map(e => { | ||
101 | e.classList.add('dumby-block') | ||
102 | return e | ||
103 | }) | ||
104 | .filter(e => { | ||
105 | const isContained = e.parentElement.closest('.dumby-block') | ||
106 | if (isContained) e.classList.remove('dumby-block') | ||
107 | return !isContained | ||
108 | }) | ||
109 | |||
110 | return others | ||
111 | } | ||
112 | |||
113 | /** | ||
114 | * updateAttributeByStep. | ||
115 | * @description Update data attribute by steps of map render | ||
116 | * @param {Object} - renderer which is running steps | ||
117 | */ | ||
118 | const updateAttributeByStep = ({ results, target, steps }) => { | ||
119 | let passNum = results.filter( | ||
120 | r => r.type === 'render' && r.state.match(/success|skip/), | ||
121 | ).length | ||
122 | const total = steps.length | ||
123 | passNum += `/${total}` | ||
124 | |||
125 | const final = results.filter(r => r.type === 'render').length === total | ||
126 | |||
127 | // FIXME HACK use MutationObserver for animation | ||
128 | if (!target.animations) target.animations = Promise.resolve() | ||
129 | target.animations = target.animations.then(async () => { | ||
130 | await new Promise(resolve => setTimeout(resolve, 100)) | ||
131 | if (final) passNum += '\x20' | ||
132 | target.dataset.report = passNum | ||
133 | |||
134 | if (final) setTimeout(() => delete target.dataset.report, 100) | ||
135 | }) | ||
136 | } | ||
137 | |||
138 | /** Get default render method by converter */ | ||
139 | const defaultRender = mapclay.renderWith(config => ({ | ||
140 | use: config.use ?? 'Leaflet', | ||
141 | width: '100%', | ||
142 | ...config, | ||
143 | aliases: { | ||
144 | ...mapclay.defaultAliases, | ||
145 | ...(config.aliases ?? {}), | ||
146 | }, | ||
147 | stepCallback: updateAttributeByStep, | ||
148 | })) | ||
149 | |||
150 | /** | ||
89 | * Generates maps based on the provided configuration | 151 | * Generates maps based on the provided configuration |
90 | * | 152 | * |
91 | * @param {HTMLElement} container - The container element for the maps | 153 | * @param {HTMLElement} container - The container element for the maps |
@@ -96,30 +158,38 @@ export const markdown2HTML = (container, mdContent) => { | |||
96 | */ | 158 | */ |
97 | export const generateMaps = (container, { | 159 | export const generateMaps = (container, { |
98 | crs = 'EPSG:4326', | 160 | crs = 'EPSG:4326', |
161 | initialLayout, | ||
99 | layouts = [], | 162 | layouts = [], |
100 | delay, | 163 | delay, |
101 | renderCallback, | 164 | renderCallback, |
102 | addBlocks = htmlHolder => Array.from(htmlHolder.querySelectorAll('article')), | 165 | addBlocks = defaultBlocks, |
166 | autoMap = false, | ||
167 | render = defaultRender, | ||
103 | } = {}) => { | 168 | } = {}) => { |
104 | /** Prepare Contaner/HTML-Holder/Showcase */ | 169 | /** Prepare Contaner */ |
105 | container.classList.add('Dumby') | 170 | container.classList.add('Dumby') |
106 | delete container.dataset.layout | 171 | delete container.dataset.layout |
107 | container.dataset.layout = defaultLayouts[0].name | 172 | container.dataset.layout = initialLayout ?? defaultLayouts[0].name |
108 | 173 | ||
109 | const htmlHolder = container.querySelector('.SemanticHtml, :has(article, section)') ?? container.firstElementChild | 174 | /** Prepare Semantic HTML part and blocks of contents inside */ |
175 | const htmlHolder = container.querySelector('.SemanticHtml') ?? | ||
176 | Array.from(container.children).find(e => e.id?.includes('main') || e.className.includes('main')) ?? | ||
177 | Array.from(container.children).sort((a, b) => a.textContent.length < b.textContent.length).at(0) | ||
110 | htmlHolder.classList.add('.SemanticHtml') | 178 | htmlHolder.classList.add('.SemanticHtml') |
179 | |||
111 | const blocks = addBlocks(htmlHolder) | 180 | const blocks = addBlocks(htmlHolder) |
112 | blocks.forEach(b => { | 181 | blocks.forEach(b => { |
113 | b.classList.add('dumby-block') | 182 | b.classList.add('dumby-block') |
114 | b.dataset.total = blocks.length | 183 | b.dataset.total = blocks.length |
115 | }) | 184 | }) |
116 | 185 | ||
186 | /** Prepare Showcase */ | ||
117 | const showcase = document.createElement('div') | 187 | const showcase = document.createElement('div') |
118 | container.appendChild(showcase) | 188 | container.appendChild(showcase) |
119 | showcase.classList.add('Showcase') | 189 | showcase.classList.add('Showcase') |
120 | const renderPromises = [] | ||
121 | 190 | ||
122 | /** Prepare Modal */ | 191 | /** Prepare Other Variables */ |
192 | const renderPromises = [] | ||
123 | const modalContent = document.createElement('div') | 193 | const modalContent = document.createElement('div') |
124 | container.appendChild(modalContent) | 194 | container.appendChild(modalContent) |
125 | const modal = new PlainModal(modalContent) | 195 | const modal = new PlainModal(modalContent) |
@@ -175,13 +245,10 @@ export const generateMaps = (container, { | |||
175 | register(proj4) | 245 | register(proj4) |
176 | fromEPSGCode(crs).then(() => resolve()) | 246 | fromEPSGCode(crs).then(() => resolve()) |
177 | }) | 247 | }) |
178 | const addGeoSchemeByText = new Promise(resolve => { | 248 | const addGeoSchemeByText = (async () => { |
179 | const coordPatterns = [ | 249 | const coordPatterns = /(-?\d+\.?\d*)([,\x2F\uFF0C])(-?\d+\.?\d*)/ |
180 | /[\x28\x5B\uFF08]\D*(-?\d+\.?\d*)([\x2F\s])(-?\d+\.?\d*)\D*[\x29\x5D\uFF09]/, | 250 | const re = new RegExp(coordPatterns, 'g') |
181 | /(-?\d+\.?\d*)([,\uFF0C])(-?\d+\.?\d*)/, | 251 | htmlHolder.querySelectorAll('.dumby-block') |
182 | ] | ||
183 | const re = new RegExp(coordPatterns.map(p => p.source).join('|'), 'g') | ||
184 | htmlHolder.querySelectorAll('p') | ||
185 | .forEach(p => { | 252 | .forEach(p => { |
186 | replaceTextNodes(p, re, match => { | 253 | replaceTextNodes(p, re, match => { |
187 | const a = document.createElement('a') | 254 | const a = document.createElement('a') |
@@ -190,8 +257,7 @@ export const generateMaps = (container, { | |||
190 | return a | 257 | return a |
191 | }) | 258 | }) |
192 | }) | 259 | }) |
193 | resolve() | 260 | })() |
194 | }) | ||
195 | 261 | ||
196 | Promise.all([setCRS, addGeoSchemeByText]).then(() => { | 262 | Promise.all([setCRS, addGeoSchemeByText]).then(() => { |
197 | Array.from(container.querySelectorAll(geoLinkSelector)) | 263 | Array.from(container.querySelectorAll(geoLinkSelector)) |
@@ -392,63 +458,26 @@ export const generateMaps = (container, { | |||
392 | const elementsWithMapConfig = Array.from( | 458 | const elementsWithMapConfig = Array.from( |
393 | container.querySelectorAll(mapBlockSelector) ?? [], | 459 | container.querySelectorAll(mapBlockSelector) ?? [], |
394 | ) | 460 | ) |
395 | /** | 461 | if (autoMap && elementsWithMapConfig.length === 0) { |
396 | * updateAttributeByStep. | 462 | const mapContainer = document.createElement('pre') |
397 | * | 463 | mapContainer.className = 'mapclay-container' |
398 | * @param {Object} - renderer which is running steps | 464 | mapContainer.textContent = '#Created by DumbyMap' |
399 | */ | 465 | htmlHolder.insertBefore(mapContainer, htmlHolder.firstElementChild) |
400 | const updateAttributeByStep = ({ results, target, steps }) => { | 466 | elementsWithMapConfig.push(mapContainer) |
401 | let passNum = results.filter( | ||
402 | r => r.type === 'render' && r.state.match(/success|skip/), | ||
403 | ).length | ||
404 | const total = steps.length | ||
405 | passNum += `/${total}` | ||
406 | |||
407 | const final = results.filter(r => r.type === 'render').length === total | ||
408 | |||
409 | // FIXME HACK use MutationObserver for animation | ||
410 | if (!target.animations) target.animations = Promise.resolve() | ||
411 | target.animations = target.animations.then(async () => { | ||
412 | await new Promise(resolve => setTimeout(resolve, 100)) | ||
413 | if (final) passNum += '\x20' | ||
414 | target.dataset.report = passNum | ||
415 | |||
416 | if (final) setTimeout(() => delete target.dataset.report, 100) | ||
417 | }) | ||
418 | } | 467 | } |
419 | /** | ||
420 | * config converter for mapclay.renderWith() | ||
421 | * | ||
422 | * @param {Object} config | ||
423 | * @return {Object} - converted config | ||
424 | */ | ||
425 | const configConverter = config => ({ | ||
426 | use: config.use ?? 'Leaflet', | ||
427 | width: '100%', | ||
428 | ...config, | ||
429 | aliases: { | ||
430 | ...defaultAliases, | ||
431 | ...(config.aliases ?? {}), | ||
432 | }, | ||
433 | stepCallback: updateAttributeByStep, | ||
434 | }) | ||
435 | |||
436 | /** Get render method by converter */ | ||
437 | const render = renderWith(configConverter) | ||
438 | 468 | ||
439 | /** Render each taget element for maps */ | 469 | /** Render each taget element for maps */ |
440 | let order = 0 | 470 | let order = 0 |
441 | elementsWithMapConfig.forEach(target => { | 471 | elementsWithMapConfig.forEach(target => { |
442 | // Get text in code block starts with markdown text '```map' | 472 | // Get text in code block starts with markdown text '```map' |
443 | const configText = target | 473 | const configText = target |
444 | .querySelector('.language-map') | ||
445 | .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content | 474 | .textContent // BE CAREFUL!!! 0xa0 char is "non-breaking spaces" in HTML text content |
446 | // replace it by normal space | 475 | // replace it by normal space |
447 | .replace(/\u00A0/g, '\u0020') | 476 | .replace(/\u00A0/g, '\u0020') |
448 | 477 | ||
449 | let configList = [] | 478 | let configList = [] |
450 | try { | 479 | try { |
451 | configList = parseConfigsFromYaml(configText).map(assignMapId) | 480 | configList = mapclay.parseConfigsFromYaml(configText).map(assignMapId) |
452 | } catch (_) { | 481 | } catch (_) { |
453 | console.warn('Fail to parse yaml config for element', target) | 482 | console.warn('Fail to parse yaml config for element', target) |
454 | return | 483 | return |
@@ -510,7 +539,7 @@ export const generateMaps = (container, { | |||
510 | container.oncontextmenu = e => { | 539 | container.oncontextmenu = e => { |
511 | menu.replaceChildren() | 540 | menu.replaceChildren() |
512 | menu.style.display = 'block' | 541 | menu.style.display = 'block' |
513 | menu.style.cssText = `left: ${e.x - menu.offsetParent.offsetLeft + 10}px; top: ${e.y - menu.offsetParent.offsetTop + 5}px;` | 542 | menu.style.cssText = `left: ${e.clientX - menu.offsetParent.offsetLeft + 10}px; top: ${e.clientY - menu.offsetParent.offsetTop + 5}px;` |
514 | e.preventDefault() | 543 | e.preventDefault() |
515 | 544 | ||
516 | // Menu Items for map | 545 | // Menu Items for map |