aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--index.html4
-rw-r--r--src/dumbyUtils.mjs19
-rw-r--r--src/dumbymap.mjs265
-rw-r--r--src/editor.mjs27
-rw-r--r--src/utils.mjs21
5 files changed, 192 insertions, 144 deletions
diff --git a/index.html b/index.html
index 6ddcc8a..59cf7ce 100644
--- a/index.html
+++ b/index.html
@@ -29,7 +29,9 @@
29 29
30<body> 30<body>
31 <main data-mode="editing"> 31 <main data-mode="editing">
32 <div class="DumbyMap"></div> 32 <div class="DumbyMap">
33 <div class="SemanticHtml"></div>
34 </div>
33 <div class="editor"> 35 <div class="editor">
34 <textarea></textarea> 36 <textarea></textarea>
35 </div> 37 </div>
diff --git a/src/dumbyUtils.mjs b/src/dumbyUtils.mjs
index 46594bd..0430f97 100644
--- a/src/dumbyUtils.mjs
+++ b/src/dumbyUtils.mjs
@@ -143,7 +143,7 @@ const addLeaderLine = (link, target) => {
143/** 143/**
144 * Create geolinks, which points to map by geo schema and id 144 * Create geolinks, which points to map by geo schema and id
145 * 145 *
146 * @param {HTMLElement} Elements contains anchor elements for doclinks 146 * @param {HTMLElement} Elements contains anchor elements for GeoLinks
147 * @returns {Boolean} ture is link is created, false if coordinates are invalid 147 * @returns {Boolean} ture is link is created, false if coordinates are invalid
148 */ 148 */
149export const createGeoLink = (link) => { 149export const createGeoLink = (link) => {
@@ -164,6 +164,7 @@ export const createGeoLink = (link) => {
164 link.dataset.lat = lat 164 link.dataset.lat = lat
165 link.dataset.crs = params.get('crs') 165 link.dataset.crs = params.get('crs')
166 link.classList.add('with-leader-line', 'geolink') 166 link.classList.add('with-leader-line', 'geolink')
167 link.classList.remove('not-geolink')
167 // TODO refactor as data attribute 168 // TODO refactor as data attribute
168 link.targets = params.get('id')?.split(',') ?? null 169 link.targets = params.get('id')?.split(',') ?? null
169 link.type = params.get('type') ?? null 170 link.type = params.get('type') ?? null
@@ -363,10 +364,8 @@ export const setGeoSchemeByCRS = (crs) => (link) => {
363 link.search = params 364 link.search = params
364 365
365 const unit = proj4(crs).oProj.units 366 const unit = proj4(crs).oProj.units
366 const invalidDegree = unit === 'degrees' && ( 367 const invalidDegree = unit === 'degrees' &&
367 (lon > 180 || lon < -180 || lat > 90 || lat < -90) || 368 (lon > 180 || lon < -180 || lat > 90 || lat < -90)
368 (xy.every(v => v.toString().length < 3))
369 )
370 const invalidMeter = unit === 'm' && xy.find(v => v < 100) 369 const invalidMeter = unit === 'm' && xy.find(v => v < 100)
371 if (invalidDegree || invalidMeter) { 370 if (invalidDegree || invalidMeter) {
372 link.replaceWith(document.createTextNode(link.textContent)) 371 link.replaceWith(document.createTextNode(link.textContent))
@@ -443,9 +442,15 @@ export const dragForAnchor = (container, range, endOfLeaderLine) => {
443export const addGeoSchemeByText = async (element) => { 442export const addGeoSchemeByText = async (element) => {
444 const coordPatterns = /(-?\d+\.?\d*)([,\x2F\uFF0C])(-?\d+\.?\d*)/ 443 const coordPatterns = /(-?\d+\.?\d*)([,\x2F\uFF0C])(-?\d+\.?\d*)/
445 const re = new RegExp(coordPatterns, 'g') 444 const re = new RegExp(coordPatterns, 'g')
446 replaceTextNodes(element, re, match => { 445
446 return replaceTextNodes(element, re, match => {
447 const [x, y] = [match.at(1), match.at(3)]
448 // Don't process string which can be used as date
449 if (Date.parse(match.at(0) + ' 1990')) return null
450
447 const a = document.createElement('a') 451 const a = document.createElement('a')
448 a.href = `geo:0,0?xy=${match.at(1)},${match.at(3)}` 452 a.className = 'not-geolink'
453 a.href = `geo:0,0?xy=${x},${y}`
449 a.textContent = match.at(0) 454 a.textContent = match.at(0)
450 return a 455 return a
451 }) 456 })
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)
diff --git a/src/editor.mjs b/src/editor.mjs
index d56ad4b..3a70ae6 100644
--- a/src/editor.mjs
+++ b/src/editor.mjs
@@ -42,7 +42,12 @@ new window.MutationObserver(mutations => {
42 childList: true, 42 childList: true,
43 subtree: true, 43 subtree: true,
44}) 44})
45let dumbymap 45const dumbymap = generateMaps(dumbyContainer, { crs })
46if (initialLayout) {
47 dumbyContainer.dataset.layout = initialLayout
48}
49// Set oncontextmenu callback
50dumbymap.utils.setContextMenu(menuForEditor)
46 51
47/** Variables: Reference Style Links in Markdown */ 52/** Variables: Reference Style Links in Markdown */
48const refLinkPattern = /\[([^\x5B\x5D]+)\]:\s+(\S+)(\s["'](\S+)["'])?/ 53const refLinkPattern = /\[([^\x5B\x5D]+)\]:\s+(\S+)(\s["'](\S+)["'])?/
@@ -445,7 +450,7 @@ const completeForCodeBlock = change => {
445 * @param {Event} event - Event for context menu 450 * @param {Event} event - Event for context menu
446 * @param {HTMLElement} menu - menu of dumbymap 451 * @param {HTMLElement} menu - menu of dumbymap
447 */ 452 */
448const menuForEditor = (event, menu) => { 453function menuForEditor (event, menu) {
449 event.preventDefault() 454 event.preventDefault()
450 455
451 if (document.getSelection().type === 'Range' && cm.getSelection() && refLinks.length > 0) { 456 if (document.getSelection().type === 'Range' && cm.getSelection() && refLinks.length > 0) {
@@ -490,22 +495,16 @@ const menuForEditor = (event, menu) => {
490const updateDumbyMap = (callback = null) => { 495const updateDumbyMap = (callback = null) => {
491 markdown2HTML(dumbyContainer, editor.value()) 496 markdown2HTML(dumbyContainer, editor.value())
492 // debounceForMap(dumbyContainer, afterMapRendered) 497 // debounceForMap(dumbyContainer, afterMapRendered)
493 dumbymap = generateMaps(dumbyContainer, { 498 // dumbymap = generateMaps(dumbyContainer, {
494 crs, 499 // crs,
495 }) 500 // })
496 // Set onscroll callback 501 // Set onscroll callback
497 const htmlHolder = dumbymap.htmlHolder 502 // const htmlHolder = dumbymap.htmlHolder
498 htmlHolder.onscroll = updateScrollLine(htmlHolder) 503 // htmlHolder.onscroll = updateScrollLine(htmlHolder)
499 // Set oncontextmenu callback
500 dumbymap.utils.setContextMenu(menuForEditor)
501 504
502 callback?.(dumbymap) 505 callback?.(dumbymap)
503} 506}
504updateDumbyMap(() => { 507updateDumbyMap()
505 if (initialLayout) {
506 dumbyContainer.dataset.layout = initialLayout
507 }
508})
509 508
510// Re-render HTML by editor content 509// Re-render HTML by editor content
511cm.on('change', (_, change) => { 510cm.on('change', (_, change) => {
diff --git a/src/utils.mjs b/src/utils.mjs
index 327bee4..9149ac2 100644
--- a/src/utils.mjs
+++ b/src/utils.mjs
@@ -5,7 +5,7 @@
5 * @param {Function} callback 5 * @param {Function} callback
6 */ 6 */
7export const onRemove = (element, callback) => { 7export const onRemove = (element, callback) => {
8 const parent = element.parentNode 8 const parent = element.parentElement
9 if (!parent) throw new Error('The node must already be attached') 9 if (!parent) throw new Error('The node must already be attached')
10 10
11 const obs = new window.MutationObserver(mutations => { 11 const obs = new window.MutationObserver(mutations => {
@@ -138,35 +138,40 @@ export const insideParent = (childElement, parentElement) => {
138 * replaceTextNodes. 138 * replaceTextNodes.
139 * @description Search current nodes by pattern, and replace them by new node 139 * @description Search current nodes by pattern, and replace them by new node
140 * @todo refactor to smaller methods 140 * @todo refactor to smaller methods
141 * @param {HTMLElement} element 141 * @param {HTMLElement} rootNode
142 * @param {RegExp} pattern 142 * @param {RegExp} pattern
143 * @param {Function} newNode - Create new node by each result of String.prototype.matchAll 143 * @param {Function} newNode - Create new node by each result of String.prototype.matchAll
144 */ 144 */
145export const replaceTextNodes = ( 145export const replaceTextNodes = (
146 element, 146 rootNode,
147 pattern, 147 pattern,
148 newNode = (match) => { 148 addNewNode = (match) => {
149 const link = document.createElement('a') 149 const link = document.createElement('a')
150 link.textContent(match.at(0)) 150 link.textContent(match.at(0))
151 return link 151 return link
152 }, 152 },
153) => { 153) => {
154 const nodeIterator = document.createNodeIterator( 154 const nodeIterator = document.createNodeIterator(
155 element, 155 rootNode,
156 window.NodeFilter.SHOW_TEXT, 156 window.NodeFilter.SHOW_TEXT,
157 node => node.textContent.match(pattern) 157 node => node.textContent.match(pattern) && !node.parentElement.closest('code')
158 ? window.NodeFilter.FILTER_ACCEPT 158 ? window.NodeFilter.FILTER_ACCEPT
159 : window.NodeFilter.FILTER_REJECT, 159 : window.NodeFilter.FILTER_REJECT,
160 ) 160 )
161 161
162 let node = nodeIterator.nextNode() 162 let node = nodeIterator.nextNode()
163 const nodeArray = []
163 while (node) { 164 while (node) {
164 let index = 0 165 let index = 0
165 for (const match of node.textContent.matchAll(pattern)) { 166 for (const match of node.textContent.matchAll(pattern)) {
166 const text = node.textContent.slice(index, match.index) 167 const text = node.textContent.slice(index, match.index)
168 const newNode = addNewNode(match)
169 if (!newNode) continue
170
167 index = match.index + match.at(0).length 171 index = match.index + match.at(0).length
168 node.parentElement.insertBefore(document.createTextNode(text), node) 172 node.parentElement.insertBefore(document.createTextNode(text), node)
169 node.parentElement.insertBefore(newNode(match), node) 173 node.parentElement.insertBefore(newNode, node)
174 nodeArray.push(newNode)
170 } 175 }
171 if (index < node.textContent.length) { 176 if (index < node.textContent.length) {
172 const text = node.textContent.slice(index) 177 const text = node.textContent.slice(index)
@@ -176,6 +181,8 @@ export const replaceTextNodes = (
176 node.parentElement.removeChild(node) 181 node.parentElement.removeChild(node)
177 node = nodeIterator.nextNode() 182 node = nodeIterator.nextNode()
178 } 183 }
184
185 return nodeArray
179} 186}
180 187
181/** 188/**