diff options
| author | Hsieh Chin Fan <pham@topo.tw> | 2024-10-08 17:32:53 +0800 |
|---|---|---|
| committer | Hsieh Chin Fan <pham@topo.tw> | 2024-10-09 00:25:34 +0800 |
| commit | 5aede2679a7d9424d8e47f5d839fd209a72315a1 (patch) | |
| tree | 7c13e8da8a7b8c349e171d2f69aaf65e7d64eac5 | |
| parent | af4831dd8ad2df0b25a64ae3529a20b921e26c7c (diff) | |
feat: create GeoLink by drag/drop
| -rw-r--r-- | src/css/dumbymap.css | 2 | ||||
| -rw-r--r-- | src/dumbyUtils.mjs | 8 | ||||
| -rw-r--r-- | src/dumbymap.mjs | 4 | ||||
| -rw-r--r-- | src/editor.mjs | 105 |
4 files changed, 92 insertions, 27 deletions
diff --git a/src/css/dumbymap.css b/src/css/dumbymap.css index ae58b3e..0c1fc81 100644 --- a/src/css/dumbymap.css +++ b/src/css/dumbymap.css | |||
| @@ -152,7 +152,7 @@ a[href^='http']:not(:has(img))::after, | |||
| 152 | background-color: #9ee7ea; | 152 | background-color: #9ee7ea; |
| 153 | } | 153 | } |
| 154 | 154 | ||
| 155 | &:hover { | 155 | &:hover, &.drag { |
| 156 | background-image: none; | 156 | background-image: none; |
| 157 | 157 | ||
| 158 | font-weight: bolder; | 158 | font-weight: bolder; |
diff --git a/src/dumbyUtils.mjs b/src/dumbyUtils.mjs index be45139..b6b63d8 100644 --- a/src/dumbyUtils.mjs +++ b/src/dumbyUtils.mjs | |||
| @@ -244,14 +244,14 @@ const isAnchorVisible = anchor => { | |||
| 244 | return insideWindow(anchor) && insideParent(anchor, mapContainer) | 244 | return insideWindow(anchor) && insideParent(anchor, mapContainer) |
| 245 | } | 245 | } |
| 246 | 246 | ||
| 247 | export const addAnchorByEvent = ({ | 247 | export const addAnchorByPoint = ({ |
| 248 | event, | 248 | point, |
| 249 | map, | 249 | map, |
| 250 | validateAnchorName = () => true | 250 | validateAnchorName = () => true |
| 251 | }) => { | 251 | }) => { |
| 252 | const rect = map.getBoundingClientRect() | 252 | const rect = map.getBoundingClientRect() |
| 253 | const [x, y] = map.renderer | 253 | const [x, y] = map.renderer |
| 254 | .unproject([event.x - rect.left, event.y - rect.top]) | 254 | .unproject([point.x - rect.left, point.y - rect.top]) |
| 255 | .map(coord => Number(coord.toFixed(7))) | 255 | .map(coord => Number(coord.toFixed(7))) |
| 256 | 256 | ||
| 257 | let prompt | 257 | let prompt |
| @@ -264,7 +264,7 @@ export const addAnchorByEvent = ({ | |||
| 264 | while (anchorName !== null && !validateAnchorName(anchorName)) | 264 | while (anchorName !== null && !validateAnchorName(anchorName)) |
| 265 | if (anchorName === null) return | 265 | if (anchorName === null) return |
| 266 | 266 | ||
| 267 | const link = `geo:${y},${x}?xy=${x},${y}&id=${map.id} "${anchorName}"` | 267 | const link = `geo:${y},${x}?xy=${x},${y}&id=${map.id}&text=${anchorName}` |
| 268 | map.renderer.addMarker({ | 268 | map.renderer.addMarker({ |
| 269 | xy: [x, y], | 269 | xy: [x, y], |
| 270 | title: `${map.id}@${x}, ${y}`, | 270 | title: `${map.id}@${x}, ${y}`, |
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index a228765..1da5bb6 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
| @@ -107,7 +107,7 @@ export const markdown2HTML = (container, mdContent) => { | |||
| 107 | * @param {Number} options.delay -- delay of map generation, milliseconds | 107 | * @param {Number} options.delay -- delay of map generation, milliseconds |
| 108 | * @return {Object} dumbymap -- Include and Elements and Methods about managing contents | 108 | * @return {Object} dumbymap -- Include and Elements and Methods about managing contents |
| 109 | */ | 109 | */ |
| 110 | export const generateMaps = (container, { delay } = {}) => { | 110 | export const generateMaps = (container, { delay, renderCallback } = {}) => { |
| 111 | container.classList.add('Dumby') | 111 | container.classList.add('Dumby') |
| 112 | container.removeAttribute('data-layout') | 112 | container.removeAttribute('data-layout') |
| 113 | container.setAttribute('data-layout', layouts[0].name) | 113 | container.setAttribute('data-layout', layouts[0].name) |
| @@ -296,6 +296,8 @@ export const generateMaps = (container, { delay } = {}) => { | |||
| 296 | return | 296 | return |
| 297 | } | 297 | } |
| 298 | 298 | ||
| 299 | renderCallback?.(renderer) | ||
| 300 | |||
| 299 | // Work with Mutation Observer | 301 | // Work with Mutation Observer |
| 300 | const observer = mapFocusObserver() | 302 | const observer = mapFocusObserver() |
| 301 | observer.observe(mapElement, { | 303 | observer.observe(mapElement, { |
diff --git a/src/editor.mjs b/src/editor.mjs index 1c444eb..ba2799f 100644 --- a/src/editor.mjs +++ b/src/editor.mjs | |||
| @@ -4,7 +4,8 @@ import { markdown2HTML, generateMaps } from './dumbymap' | |||
| 4 | import { defaultAliases, parseConfigsFromYaml } from 'mapclay' | 4 | import { defaultAliases, parseConfigsFromYaml } from 'mapclay' |
| 5 | import * as menuItem from './MenuItem' | 5 | import * as menuItem from './MenuItem' |
| 6 | import { shiftByWindow } from './utils.mjs' | 6 | import { shiftByWindow } from './utils.mjs' |
| 7 | import { addAnchorByEvent } from './dumbyUtils.mjs' | 7 | import { addAnchorByPoint } from './dumbyUtils.mjs' |
| 8 | import LeaderLine from 'leader-line' | ||
| 8 | 9 | ||
| 9 | // Set up Containers {{{ | 10 | // Set up Containers {{{ |
| 10 | 11 | ||
| @@ -16,6 +17,16 @@ let dumbymap | |||
| 16 | 17 | ||
| 17 | const refLinkPattern = /\[([^\x5B\x5D]+)\]:\s+(.+)/ | 18 | const refLinkPattern = /\[([^\x5B\x5D]+)\]:\s+(.+)/ |
| 18 | let refLinks = [] | 19 | let refLinks = [] |
| 20 | const validateAnchorName = anchorName => | ||
| 21 | !refLinks.find(obj => obj.ref === anchorName) | ||
| 22 | const appendRefLink = ({ cm, ref, link }) => { | ||
| 23 | let refLinkString = `\n[${ref}]: ${link}` | ||
| 24 | const lastLineIsRefLink = cm.getLine(cm.lastLine()).match(refLinkPattern) | ||
| 25 | if (!lastLineIsRefLink) refLinkString = '\n' + refLinkString | ||
| 26 | cm.replaceRange(refLinkString, { line: Infinity }) | ||
| 27 | |||
| 28 | refLinks.push({ ref, link }) | ||
| 29 | } | ||
| 19 | 30 | ||
| 20 | /** | 31 | /** |
| 21 | * Watch for changes of editing mode | 32 | * Watch for changes of editing mode |
| @@ -201,6 +212,7 @@ const editor = new EasyMDE({ | |||
| 201 | action: () => { | 212 | action: () => { |
| 202 | editor.value(defaultContent) | 213 | editor.value(defaultContent) |
| 203 | refLinks = getRefLinks() | 214 | refLinks = getRefLinks() |
| 215 | updateDumbyMap() | ||
| 204 | } | 216 | } |
| 205 | } | 217 | } |
| 206 | ] | 218 | ] |
| @@ -245,7 +257,6 @@ const getContentFromHash = hash => { | |||
| 245 | const contentFromHash = getContentFromHash(window.location.hash) | 257 | const contentFromHash = getContentFromHash(window.location.hash) |
| 246 | 258 | ||
| 247 | if (url.searchParams.get('content') === 'tutorial') { | 259 | if (url.searchParams.get('content') === 'tutorial') { |
| 248 | console.log('tutorial') | ||
| 249 | editor.value(defaultContent) | 260 | editor.value(defaultContent) |
| 250 | } else if (contentFromHash) { | 261 | } else if (contentFromHash) { |
| 251 | // Seems like autosave would overwrite initialValue, set content from hash here | 262 | // Seems like autosave would overwrite initialValue, set content from hash here |
| @@ -443,20 +454,8 @@ const menuForEditor = (event, menu) => { | |||
| 443 | const item = new menuItem.Item({ | 454 | const item = new menuItem.Item({ |
| 444 | text: 'Add Anchor', | 455 | text: 'Add Anchor', |
| 445 | onclick: (event) => { | 456 | onclick: (event) => { |
| 446 | const validateAnchorName = anchorName => | 457 | const { ref, link } = addAnchorByPoint({ point: event, map, validateAnchorName }) |
| 447 | !refLinks.find(obj => obj.ref === anchorName) | 458 | appendRefLink({ cm, ref, link }) |
| 448 | const { ref, link } = addAnchorByEvent({ | ||
| 449 | event, | ||
| 450 | map, | ||
| 451 | validateAnchorName | ||
| 452 | }) | ||
| 453 | |||
| 454 | let refLinkString = `\n[${ref}]: ${link}` | ||
| 455 | const lastLineIsRefLink = cm.getLine(cm.lastLine()).match(refLinkPattern) | ||
| 456 | if (lastLineIsRefLink) refLinkString = '\n' + refLinkString | ||
| 457 | cm.replaceRange(refLinkString, { line: Infinity }) | ||
| 458 | |||
| 459 | refLinks.push({ ref, link }) | ||
| 460 | } | 459 | } |
| 461 | }) | 460 | }) |
| 462 | menu.insertBefore(item, menu.firstChild) | 461 | menu.insertBefore(item, menu.firstChild) |
| @@ -469,7 +468,7 @@ const menuForEditor = (event, menu) => { | |||
| 469 | const updateDumbyMap = () => { | 468 | const updateDumbyMap = () => { |
| 470 | markdown2HTML(dumbyContainer, editor.value()) | 469 | markdown2HTML(dumbyContainer, editor.value()) |
| 471 | // debounceForMap(dumbyContainer, afterMapRendered) | 470 | // debounceForMap(dumbyContainer, afterMapRendered) |
| 472 | dumbymap = generateMaps(dumbyContainer) | 471 | dumbymap = generateMaps(dumbyContainer, { renderCallback }) |
| 473 | // Set onscroll callback | 472 | // Set onscroll callback |
| 474 | const htmlHolder = dumbymap.htmlHolder | 473 | const htmlHolder = dumbymap.htmlHolder |
| 475 | htmlHolder.onscroll = htmlOnScroll(htmlHolder) | 474 | htmlHolder.onscroll = htmlOnScroll(htmlHolder) |
| @@ -1022,7 +1021,7 @@ cm.getWrapperElement().oncontextmenu = e => { | |||
| 1022 | 1021 | ||
| 1023 | /** HACK Sync selection from HTML to CodeMirror */ | 1022 | /** HACK Sync selection from HTML to CodeMirror */ |
| 1024 | document.addEventListener('selectionchange', () => { | 1023 | document.addEventListener('selectionchange', () => { |
| 1025 | if (cm.hasFocus()) { | 1024 | if (cm.hasFocus() || dumbyContainer.onmousemove) { |
| 1026 | return | 1025 | return |
| 1027 | } | 1026 | } |
| 1028 | 1027 | ||
| @@ -1052,7 +1051,7 @@ document.addEventListener('selectionchange', () => { | |||
| 1052 | .map(t => t.replace('\n', '')) | 1051 | .map(t => t.replace('\n', '')) |
| 1053 | .reverse() | 1052 | .reverse() |
| 1054 | .forEach(text => { | 1053 | .forEach(text => { |
| 1055 | let index = cm.getLine(anchor.line).indexOf(text, anchor.ch) | 1054 | let index = cm.getLine(anchor.line)?.indexOf(text, anchor.ch) |
| 1056 | while (index === -1) { | 1055 | while (index === -1) { |
| 1057 | anchor.line += 1 | 1056 | anchor.line += 1 |
| 1058 | anchor.ch = 0 | 1057 | anchor.ch = 0 |
| @@ -1060,13 +1059,77 @@ document.addEventListener('selectionchange', () => { | |||
| 1060 | cm.setSelection(cm.setCursor()) | 1059 | cm.setSelection(cm.setCursor()) |
| 1061 | return | 1060 | return |
| 1062 | } | 1061 | } |
| 1063 | index = cm.getLine(anchor.line).indexOf(text) | 1062 | index = cm.getLine(anchor.line)?.indexOf(text) |
| 1064 | } | 1063 | } |
| 1065 | anchor.ch = index + text.length | 1064 | anchor.ch = index + text.length |
| 1066 | }) | 1065 | }) |
| 1067 | 1066 | ||
| 1068 | cm.setSelection({ ...anchor, ch: anchor.ch - content.length }, anchor) | 1067 | cm.setSelection({ line: anchor.line, ch: anchor.ch - content.length }, anchor) |
| 1069 | } | 1068 | } |
| 1070 | }) | 1069 | }) |
| 1071 | 1070 | ||
| 1071 | dumbyContainer.onmousedown = (e) => { | ||
| 1072 | // Check should start drag event for GeoLink | ||
| 1073 | const selection = document.getSelection() | ||
| 1074 | if (cm.getSelection() === '' || selection.type !== 'Range') return | ||
| 1075 | const range = selection.getRangeAt(0) | ||
| 1076 | const rect = range.getBoundingClientRect() | ||
| 1077 | const mouseInRange = e.x < rect.right && e.x > rect.left && e.y < rect.bottom && e.y > rect.top | ||
| 1078 | if (!mouseInRange) return | ||
| 1079 | |||
| 1080 | const geoLink = document.createElement('a') | ||
| 1081 | geoLink.textContent = range.toString() | ||
| 1082 | geoLink.classList.add('with-leader-line', 'geolink', 'drag') | ||
| 1083 | range.deleteContents() | ||
| 1084 | range.insertNode(geoLink) | ||
| 1085 | |||
| 1086 | const lineEnd = document.createElement('div') | ||
| 1087 | lineEnd.style.cssText = `position: absolute; left: ${e.clientX}px; top: ${e.clientY}px;` | ||
| 1088 | document.body.appendChild(lineEnd) | ||
| 1089 | |||
| 1090 | menu.style.display = 'block' | ||
| 1091 | const line = new LeaderLine({ | ||
| 1092 | start: geoLink, | ||
| 1093 | end: lineEnd, | ||
| 1094 | path: 'magnet' | ||
| 1095 | }) | ||
| 1096 | |||
| 1097 | function onMouseMove(event) { | ||
| 1098 | lineEnd.style.left = event.clientX + 'px' | ||
| 1099 | lineEnd.style.top = event.clientY + 'px' | ||
| 1100 | line.position() | ||
| 1101 | } | ||
| 1102 | |||
| 1103 | dumbyContainer.onmousemove = onMouseMove | ||
| 1104 | dumbyContainer.onmouseup = function(e) { | ||
| 1105 | dumbyContainer.onmousemove = null | ||
| 1106 | dumbyContainer.onmouseup = null | ||
| 1107 | line?.remove() | ||
| 1108 | lineEnd.remove() | ||
| 1109 | |||
| 1110 | const map = document.elementFromPoint(e.clientX, e.clientY).closest('.mapclay') | ||
| 1111 | const selection = cm.getSelection() | ||
| 1112 | if (!map || !selection) { | ||
| 1113 | updateDumbyMap() | ||
| 1114 | return | ||
| 1115 | } | ||
| 1116 | |||
| 1117 | const refLink = addAnchorByPoint({ point: e, map, validateAnchorName }) | ||
| 1118 | if (!refLink) { | ||
| 1119 | updateDumbyMap() | ||
| 1120 | return | ||
| 1121 | } | ||
| 1122 | |||
| 1123 | const {ref, link} = refLink | ||
| 1124 | appendRefLink({ cm, ref, link }) | ||
| 1125 | if (selection === ref) { | ||
| 1126 | cm.replaceSelection(`[${selection}]`) | ||
| 1127 | } else { | ||
| 1128 | cm.replaceSelection(`[${selection}][${ref}]`) | ||
| 1129 | } | ||
| 1130 | }; | ||
| 1131 | } | ||
| 1132 | |||
| 1133 | dumbyContainer.ondragstart = () => false | ||
| 1134 | |||
| 1072 | // vim: sw=2 ts=2 foldmethod=marker foldmarker={{{,}}} | 1135 | // vim: sw=2 ts=2 foldmethod=marker foldmarker={{{,}}} |