From 5aede2679a7d9424d8e47f5d839fd209a72315a1 Mon Sep 17 00:00:00 2001 From: Hsieh Chin Fan Date: Tue, 8 Oct 2024 17:32:53 +0800 Subject: feat: create GeoLink by drag/drop --- src/css/dumbymap.css | 2 +- src/dumbyUtils.mjs | 8 ++-- src/dumbymap.mjs | 4 +- src/editor.mjs | 105 ++++++++++++++++++++++++++++++++++++++++----------- 4 files changed, 92 insertions(+), 27 deletions(-) (limited to 'src') 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, background-color: #9ee7ea; } - &:hover { + &:hover, &.drag { background-image: none; 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 => { return insideWindow(anchor) && insideParent(anchor, mapContainer) } -export const addAnchorByEvent = ({ - event, +export const addAnchorByPoint = ({ + point, map, validateAnchorName = () => true }) => { const rect = map.getBoundingClientRect() const [x, y] = map.renderer - .unproject([event.x - rect.left, event.y - rect.top]) + .unproject([point.x - rect.left, point.y - rect.top]) .map(coord => Number(coord.toFixed(7))) let prompt @@ -264,7 +264,7 @@ export const addAnchorByEvent = ({ while (anchorName !== null && !validateAnchorName(anchorName)) if (anchorName === null) return - const link = `geo:${y},${x}?xy=${x},${y}&id=${map.id} "${anchorName}"` + const link = `geo:${y},${x}?xy=${x},${y}&id=${map.id}&text=${anchorName}` map.renderer.addMarker({ xy: [x, y], 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) => { * @param {Number} options.delay -- delay of map generation, milliseconds * @return {Object} dumbymap -- Include and Elements and Methods about managing contents */ -export const generateMaps = (container, { delay } = {}) => { +export const generateMaps = (container, { delay, renderCallback } = {}) => { container.classList.add('Dumby') container.removeAttribute('data-layout') container.setAttribute('data-layout', layouts[0].name) @@ -296,6 +296,8 @@ export const generateMaps = (container, { delay } = {}) => { return } + renderCallback?.(renderer) + // Work with Mutation Observer const observer = mapFocusObserver() 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' import { defaultAliases, parseConfigsFromYaml } from 'mapclay' import * as menuItem from './MenuItem' import { shiftByWindow } from './utils.mjs' -import { addAnchorByEvent } from './dumbyUtils.mjs' +import { addAnchorByPoint } from './dumbyUtils.mjs' +import LeaderLine from 'leader-line' // Set up Containers {{{ @@ -16,6 +17,16 @@ let dumbymap const refLinkPattern = /\[([^\x5B\x5D]+)\]:\s+(.+)/ let refLinks = [] +const validateAnchorName = anchorName => + !refLinks.find(obj => obj.ref === anchorName) +const appendRefLink = ({ cm, ref, link }) => { + let refLinkString = `\n[${ref}]: ${link}` + const lastLineIsRefLink = cm.getLine(cm.lastLine()).match(refLinkPattern) + if (!lastLineIsRefLink) refLinkString = '\n' + refLinkString + cm.replaceRange(refLinkString, { line: Infinity }) + + refLinks.push({ ref, link }) +} /** * Watch for changes of editing mode @@ -201,6 +212,7 @@ const editor = new EasyMDE({ action: () => { editor.value(defaultContent) refLinks = getRefLinks() + updateDumbyMap() } } ] @@ -245,7 +257,6 @@ const getContentFromHash = hash => { const contentFromHash = getContentFromHash(window.location.hash) if (url.searchParams.get('content') === 'tutorial') { - console.log('tutorial') editor.value(defaultContent) } else if (contentFromHash) { // Seems like autosave would overwrite initialValue, set content from hash here @@ -443,20 +454,8 @@ const menuForEditor = (event, menu) => { const item = new menuItem.Item({ text: 'Add Anchor', onclick: (event) => { - const validateAnchorName = anchorName => - !refLinks.find(obj => obj.ref === anchorName) - const { ref, link } = addAnchorByEvent({ - event, - map, - validateAnchorName - }) - - let refLinkString = `\n[${ref}]: ${link}` - const lastLineIsRefLink = cm.getLine(cm.lastLine()).match(refLinkPattern) - if (lastLineIsRefLink) refLinkString = '\n' + refLinkString - cm.replaceRange(refLinkString, { line: Infinity }) - - refLinks.push({ ref, link }) + const { ref, link } = addAnchorByPoint({ point: event, map, validateAnchorName }) + appendRefLink({ cm, ref, link }) } }) menu.insertBefore(item, menu.firstChild) @@ -469,7 +468,7 @@ const menuForEditor = (event, menu) => { const updateDumbyMap = () => { markdown2HTML(dumbyContainer, editor.value()) // debounceForMap(dumbyContainer, afterMapRendered) - dumbymap = generateMaps(dumbyContainer) + dumbymap = generateMaps(dumbyContainer, { renderCallback }) // Set onscroll callback const htmlHolder = dumbymap.htmlHolder htmlHolder.onscroll = htmlOnScroll(htmlHolder) @@ -1022,7 +1021,7 @@ cm.getWrapperElement().oncontextmenu = e => { /** HACK Sync selection from HTML to CodeMirror */ document.addEventListener('selectionchange', () => { - if (cm.hasFocus()) { + if (cm.hasFocus() || dumbyContainer.onmousemove) { return } @@ -1052,7 +1051,7 @@ document.addEventListener('selectionchange', () => { .map(t => t.replace('\n', '')) .reverse() .forEach(text => { - let index = cm.getLine(anchor.line).indexOf(text, anchor.ch) + let index = cm.getLine(anchor.line)?.indexOf(text, anchor.ch) while (index === -1) { anchor.line += 1 anchor.ch = 0 @@ -1060,13 +1059,77 @@ document.addEventListener('selectionchange', () => { cm.setSelection(cm.setCursor()) return } - index = cm.getLine(anchor.line).indexOf(text) + index = cm.getLine(anchor.line)?.indexOf(text) } anchor.ch = index + text.length }) - cm.setSelection({ ...anchor, ch: anchor.ch - content.length }, anchor) + cm.setSelection({ line: anchor.line, ch: anchor.ch - content.length }, anchor) } }) +dumbyContainer.onmousedown = (e) => { + // Check should start drag event for GeoLink + const selection = document.getSelection() + if (cm.getSelection() === '' || selection.type !== 'Range') return + const range = selection.getRangeAt(0) + const rect = range.getBoundingClientRect() + const mouseInRange = e.x < rect.right && e.x > rect.left && e.y < rect.bottom && e.y > rect.top + if (!mouseInRange) return + + const geoLink = document.createElement('a') + geoLink.textContent = range.toString() + geoLink.classList.add('with-leader-line', 'geolink', 'drag') + range.deleteContents() + range.insertNode(geoLink) + + const lineEnd = document.createElement('div') + lineEnd.style.cssText = `position: absolute; left: ${e.clientX}px; top: ${e.clientY}px;` + document.body.appendChild(lineEnd) + + menu.style.display = 'block' + const line = new LeaderLine({ + start: geoLink, + end: lineEnd, + path: 'magnet' + }) + + function onMouseMove(event) { + lineEnd.style.left = event.clientX + 'px' + lineEnd.style.top = event.clientY + 'px' + line.position() + } + + dumbyContainer.onmousemove = onMouseMove + dumbyContainer.onmouseup = function(e) { + dumbyContainer.onmousemove = null + dumbyContainer.onmouseup = null + line?.remove() + lineEnd.remove() + + const map = document.elementFromPoint(e.clientX, e.clientY).closest('.mapclay') + const selection = cm.getSelection() + if (!map || !selection) { + updateDumbyMap() + return + } + + const refLink = addAnchorByPoint({ point: e, map, validateAnchorName }) + if (!refLink) { + updateDumbyMap() + return + } + + const {ref, link} = refLink + appendRefLink({ cm, ref, link }) + if (selection === ref) { + cm.replaceSelection(`[${selection}]`) + } else { + cm.replaceSelection(`[${selection}][${ref}]`) + } + }; +} + +dumbyContainer.ondragstart = () => false + // vim: sw=2 ts=2 foldmethod=marker foldmarker={{{,}}} -- cgit v1.2.3-70-g09d2