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={{{,}}} |