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 | /** |