aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorHsieh Chin Fan <pham@topo.tw>2024-10-08 17:32:53 +0800
committerHsieh Chin Fan <pham@topo.tw>2024-10-09 00:25:34 +0800
commit5aede2679a7d9424d8e47f5d839fd209a72315a1 (patch)
tree7c13e8da8a7b8c349e171d2f69aaf65e7d64eac5
parentaf4831dd8ad2df0b25a64ae3529a20b921e26c7c (diff)
feat: create GeoLink by drag/drop
-rw-r--r--src/css/dumbymap.css2
-rw-r--r--src/dumbyUtils.mjs8
-rw-r--r--src/dumbymap.mjs4
-rw-r--r--src/editor.mjs105
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
247export const addAnchorByEvent = ({ 247export 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 */
110export const generateMaps = (container, { delay } = {}) => { 110export 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'
4import { defaultAliases, parseConfigsFromYaml } from 'mapclay' 4import { defaultAliases, parseConfigsFromYaml } from 'mapclay'
5import * as menuItem from './MenuItem' 5import * as menuItem from './MenuItem'
6import { shiftByWindow } from './utils.mjs' 6import { shiftByWindow } from './utils.mjs'
7import { addAnchorByEvent } from './dumbyUtils.mjs' 7import { addAnchorByPoint } from './dumbyUtils.mjs'
8import LeaderLine from 'leader-line'
8 9
9// Set up Containers {{{ 10// Set up Containers {{{
10 11
@@ -16,6 +17,16 @@ let dumbymap
16 17
17const refLinkPattern = /\[([^\x5B\x5D]+)\]:\s+(.+)/ 18const refLinkPattern = /\[([^\x5B\x5D]+)\]:\s+(.+)/
18let refLinks = [] 19let refLinks = []
20const validateAnchorName = anchorName =>
21 !refLinks.find(obj => obj.ref === anchorName)
22const 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 => {
245const contentFromHash = getContentFromHash(window.location.hash) 257const contentFromHash = getContentFromHash(window.location.hash)
246 258
247if (url.searchParams.get('content') === 'tutorial') { 259if (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) => {
469const updateDumbyMap = () => { 468const 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 */
1024document.addEventListener('selectionchange', () => { 1023document.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
1071dumbyContainer.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
1133dumbyContainer.ondragstart = () => false
1134
1072// vim: sw=2 ts=2 foldmethod=marker foldmarker={{{,}}} 1135// vim: sw=2 ts=2 foldmethod=marker foldmarker={{{,}}}