diff options
-rw-r--r-- | index.html | 4 | ||||
-rw-r--r-- | src/dumbyUtils.mjs | 19 | ||||
-rw-r--r-- | src/dumbymap.mjs | 265 | ||||
-rw-r--r-- | src/editor.mjs | 27 | ||||
-rw-r--r-- | src/utils.mjs | 21 |
5 files changed, 192 insertions, 144 deletions
@@ -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 | */ |
149 | export const createGeoLink = (link) => { | 149 | export 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) => { | |||
443 | export const addGeoSchemeByText = async (element) => { | 442 | export 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 */ |
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) |
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 | }) |
45 | let dumbymap | 45 | const dumbymap = generateMaps(dumbyContainer, { crs }) |
46 | if (initialLayout) { | ||
47 | dumbyContainer.dataset.layout = initialLayout | ||
48 | } | ||
49 | // Set oncontextmenu callback | ||
50 | dumbymap.utils.setContextMenu(menuForEditor) | ||
46 | 51 | ||
47 | /** Variables: Reference Style Links in Markdown */ | 52 | /** Variables: Reference Style Links in Markdown */ |
48 | const refLinkPattern = /\[([^\x5B\x5D]+)\]:\s+(\S+)(\s["'](\S+)["'])?/ | 53 | const 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 | */ |
448 | const menuForEditor = (event, menu) => { | 453 | function 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) => { | |||
490 | const updateDumbyMap = (callback = null) => { | 495 | const 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 | } |
504 | updateDumbyMap(() => { | 507 | updateDumbyMap() |
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 |
511 | cm.on('change', (_, change) => { | 510 | cm.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 | */ |
7 | export const onRemove = (element, callback) => { | 7 | export 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 | */ |
145 | export const replaceTextNodes = ( | 145 | export 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 | /** |