diff options
| author | Hsieh Chin Fan <pham@topo.tw> | 2024-10-13 21:30:43 +0800 |
|---|---|---|
| committer | Hsieh Chin Fan <pham@topo.tw> | 2024-10-14 16:57:34 +0800 |
| commit | 2c9ffa25df5913953553e70426f19cf47832bb45 (patch) | |
| tree | 10c54538c64dc4fcb3654a658e1fd913b9802cc5 | |
| parent | 9071bd030a0564ce387134d5844e777cb535e28f (diff) | |
feat: migrate drag feature to dumbymap
| -rw-r--r-- | src/css/dumbymap.css | 10 | ||||
| -rw-r--r-- | src/dumbymap.mjs | 81 | ||||
| -rw-r--r-- | src/editor.mjs | 96 |
3 files changed, 98 insertions, 89 deletions
diff --git a/src/css/dumbymap.css b/src/css/dumbymap.css index 7ddb21a..1eb607b 100644 --- a/src/css/dumbymap.css +++ b/src/css/dumbymap.css | |||
| @@ -764,6 +764,16 @@ root { | |||
| 764 | .dumby-block > :not(:has(.mapclay[data-render="fulfilled"])) { | 764 | .dumby-block > :not(:has(.mapclay[data-render="fulfilled"])) { |
| 765 | opacity: 0.3; | 765 | opacity: 0.3; |
| 766 | } | 766 | } |
| 767 | |||
| 768 | * { | ||
| 769 | cursor: not-allowed !important; | ||
| 770 | } | ||
| 771 | |||
| 772 | .mapclay[data-render="fulfilled"] { | ||
| 773 | &, & canvas { | ||
| 774 | cursor: crosshair !important; | ||
| 775 | } | ||
| 776 | } | ||
| 767 | } | 777 | } |
| 768 | 778 | ||
| 769 | .Dumby[data-layout='sticky'] { | 779 | .Dumby[data-layout='sticky'] { |
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 4cfc484..81a65ca 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
| @@ -11,6 +11,7 @@ import * as menuItem from './MenuItem' | |||
| 11 | import PlainModal from 'plain-modal' | 11 | import PlainModal from 'plain-modal' |
| 12 | import proj4 from 'proj4' | 12 | import proj4 from 'proj4' |
| 13 | import { register, fromEPSGCode } from 'ol/proj/proj4' | 13 | import { register, fromEPSGCode } from 'ol/proj/proj4' |
| 14 | import LeaderLine from 'leader-line' | ||
| 14 | 15 | ||
| 15 | /** CSS Selector for main components */ | 16 | /** CSS Selector for main components */ |
| 16 | const mapBlockSelector = 'pre:has(.language-map)' | 17 | const mapBlockSelector = 'pre:has(.language-map)' |
| @@ -587,5 +588,85 @@ export const generateMaps = (container, { | |||
| 587 | document.removeEventListener('click', actionOutsideMenu), | 588 | document.removeEventListener('click', actionOutsideMenu), |
| 588 | ) | 589 | ) |
| 589 | 590 | ||
| 591 | /** Drag/Drop on map for new GeoLink */ | ||
| 592 | container.onmousedown = (e) => { | ||
| 593 | // Check should start drag event for GeoLink | ||
| 594 | const selection = document.getSelection() | ||
| 595 | if (e.which !== 1 || selection.type !== 'Range') return | ||
| 596 | |||
| 597 | const range = selection.getRangeAt(0) | ||
| 598 | const originContent = range.cloneContents() | ||
| 599 | const rect = range.getBoundingClientRect() | ||
| 600 | const mouseInRange = e.x < rect.right && e.x > rect.left && e.y < rect.bottom && e.y > rect.top | ||
| 601 | if (!mouseInRange) return | ||
| 602 | |||
| 603 | const geoLink = document.createElement('a') | ||
| 604 | geoLink.textContent = range.toString() | ||
| 605 | geoLink.classList.add('with-leader-line', 'geolink', 'drag') | ||
| 606 | range.deleteContents() | ||
| 607 | range.insertNode(geoLink) | ||
| 608 | |||
| 609 | const lineEnd = document.createElement('div') | ||
| 610 | lineEnd.className = 'arrow-head' | ||
| 611 | const offsetRect = container.getBoundingClientRect() | ||
| 612 | lineEnd.style.cssText = ` | ||
| 613 | position: absolute; | ||
| 614 | padding: 5px; | ||
| 615 | left: ${e.clientX}px; | ||
| 616 | top: ${e.clientY}px; | ||
| 617 | transform: translate(-${offsetRect.left + 5}px, -${offsetRect.top + 5}px);` | ||
| 618 | container.appendChild(lineEnd) | ||
| 619 | |||
| 620 | const line = new LeaderLine({ | ||
| 621 | start: geoLink, | ||
| 622 | end: lineEnd, | ||
| 623 | path: 'magnet', | ||
| 624 | }) | ||
| 625 | |||
| 626 | function onMouseMove (event) { | ||
| 627 | lineEnd.style.left = event.clientX + 'px' | ||
| 628 | lineEnd.style.top = event.clientY + 'px' | ||
| 629 | line.position() | ||
| 630 | |||
| 631 | // TODO Scroll dumbymap.htmlHolder when cursor is at upper/lower side | ||
| 632 | } | ||
| 633 | |||
| 634 | container.classList.add('dragging-geolink') | ||
| 635 | container.onmousemove = onMouseMove | ||
| 636 | container.onmouseup = function (e) { | ||
| 637 | container.classList.remove('dragging-geolink') | ||
| 638 | container.onmousemove = null | ||
| 639 | container.onmouseup = null | ||
| 640 | geoLink.classList.remove('drag') | ||
| 641 | line?.remove() | ||
| 642 | lineEnd.remove() | ||
| 643 | const resumeContent = () => { | ||
| 644 | range.deleteContents() | ||
| 645 | range.insertNode(originContent) | ||
| 646 | } | ||
| 647 | |||
| 648 | const map = document.elementFromPoint(e.clientX, e.clientY) | ||
| 649 | .closest('.mapclay[data-render="fulfilled"]') | ||
| 650 | if (!map) { | ||
| 651 | resumeContent('map/selection') | ||
| 652 | return | ||
| 653 | } | ||
| 654 | |||
| 655 | const refLink = utils.addAnchorByPoint({ | ||
| 656 | defaultName: geoLink.textContent, | ||
| 657 | point: e, | ||
| 658 | map, | ||
| 659 | }) | ||
| 660 | if (!refLink) { | ||
| 661 | resumeContent('reflink') | ||
| 662 | return | ||
| 663 | } | ||
| 664 | |||
| 665 | geoLink.href = refLink.link | ||
| 666 | utils.createGeoLink(geoLink) | ||
| 667 | } | ||
| 668 | } | ||
| 669 | container.ondragstart = () => false | ||
| 670 | |||
| 590 | return Object.seal(dumbymap) | 671 | return Object.seal(dumbymap) |
| 591 | } | 672 | } |
diff --git a/src/editor.mjs b/src/editor.mjs index a8e3e0d..df6e3bf 100644 --- a/src/editor.mjs +++ b/src/editor.mjs | |||
| @@ -4,7 +4,6 @@ import { defaultAliases, parseConfigsFromYaml } from 'mapclay' | |||
| 4 | import * as menuItem from './MenuItem' | 4 | import * as menuItem from './MenuItem' |
| 5 | import { addAnchorByPoint, createGeoLink } from './dumbyUtils.mjs' | 5 | import { addAnchorByPoint, createGeoLink } from './dumbyUtils.mjs' |
| 6 | import { shiftByWindow } from './utils.mjs' | 6 | import { shiftByWindow } from './utils.mjs' |
| 7 | import LeaderLine from 'leader-line' | ||
| 8 | import * as tutorial from './tutorial' | 7 | import * as tutorial from './tutorial' |
| 9 | 8 | ||
| 10 | /** | 9 | /** |
| @@ -27,9 +26,11 @@ const context = document.querySelector('[data-mode]') | |||
| 27 | const textArea = document.querySelector('.editor textarea') | 26 | const textArea = document.querySelector('.editor textarea') |
| 28 | const dumbyContainer = document.querySelector('.DumbyMap') | 27 | const dumbyContainer = document.querySelector('.DumbyMap') |
| 29 | dumbyContainer.dataset.scrollLine = '' | 28 | dumbyContainer.dataset.scrollLine = '' |
| 30 | /** Watch: Layout of DumbyMap */ | 29 | /** Watch: DumbyMap */ |
| 31 | new window.MutationObserver(mutaions => { | 30 | new window.MutationObserver(mutations => { |
| 32 | const mutation = mutaions.at(-1) | 31 | const mutation = mutations.at(-1) |
| 32 | |||
| 33 | /** Handle layout change */ | ||
| 33 | const layout = dumbyContainer.dataset.layout | 34 | const layout = dumbyContainer.dataset.layout |
| 34 | if (layout !== 'normal' || mutation.oldValue === 'normal') { | 35 | if (layout !== 'normal' || mutation.oldValue === 'normal') { |
| 35 | context.dataset.mode = '' | 36 | context.dataset.mode = '' |
| @@ -38,6 +39,8 @@ new window.MutationObserver(mutaions => { | |||
| 38 | attributes: true, | 39 | attributes: true, |
| 39 | attributeFilter: ['data-layout'], | 40 | attributeFilter: ['data-layout'], |
| 40 | attributeOldValue: true, | 41 | attributeOldValue: true, |
| 42 | childList: true, | ||
| 43 | subtree: true, | ||
| 41 | }) | 44 | }) |
| 42 | let dumbymap | 45 | let dumbymap |
| 43 | 46 | ||
| @@ -1070,88 +1073,3 @@ document.addEventListener('selectionchange', () => { | |||
| 1070 | cm.scrollIntoView(focus) | 1073 | cm.scrollIntoView(focus) |
| 1071 | } | 1074 | } |
| 1072 | }) | 1075 | }) |
| 1073 | |||
| 1074 | /** Drag/Drop on map for new reference style link */ | ||
| 1075 | dumbyContainer.onmousedown = (e) => { | ||
| 1076 | // Check should start drag event for GeoLink | ||
| 1077 | if (e.which !== 1) return | ||
| 1078 | const selection = document.getSelection() | ||
| 1079 | if (cm.getSelection() === '' || selection.type !== 'Range') return | ||
| 1080 | const range = selection.getRangeAt(0) | ||
| 1081 | const rect = range.getBoundingClientRect() | ||
| 1082 | const mouseInRange = e.x < rect.right && e.x > rect.left && e.y < rect.bottom && e.y > rect.top | ||
| 1083 | if (!mouseInRange) return | ||
| 1084 | |||
| 1085 | const geoLink = document.createElement('a') | ||
| 1086 | geoLink.textContent = range.toString() | ||
| 1087 | geoLink.classList.add('with-leader-line', 'geolink', 'drag') | ||
| 1088 | const originContent = range.cloneContents() | ||
| 1089 | range.deleteContents() | ||
| 1090 | range.insertNode(geoLink) | ||
| 1091 | |||
| 1092 | const lineEnd = document.createElement('div') | ||
| 1093 | lineEnd.style.cssText = `position: absolute; left: ${e.clientX}px; top: ${e.clientY}px;` | ||
| 1094 | document.body.appendChild(lineEnd) | ||
| 1095 | |||
| 1096 | const line = new LeaderLine({ | ||
| 1097 | start: geoLink, | ||
| 1098 | end: lineEnd, | ||
| 1099 | path: 'magnet', | ||
| 1100 | }) | ||
| 1101 | |||
| 1102 | function onMouseMove (event) { | ||
| 1103 | lineEnd.style.left = event.clientX + 'px' | ||
| 1104 | lineEnd.style.top = event.clientY + 'px' | ||
| 1105 | line.position() | ||
| 1106 | |||
| 1107 | // TODO Scroll dumbymap.htmlHolder when cursor is at upper/lower side | ||
| 1108 | } | ||
| 1109 | |||
| 1110 | context.classList.add('dragging-geolink') | ||
| 1111 | dumbyContainer.onmousemove = onMouseMove | ||
| 1112 | dumbymap.utils.renderedMaps().forEach(map => { map.style.cursor = 'crosshair' }) | ||
| 1113 | dumbyContainer.onmouseup = function (e) { | ||
| 1114 | context.classList.remove('dragging-geolink') | ||
| 1115 | dumbyContainer.onmousemove = null | ||
| 1116 | dumbyContainer.onmouseup = null | ||
| 1117 | line?.remove() | ||
| 1118 | lineEnd.remove() | ||
| 1119 | dumbymap.utils.renderedMaps().forEach(map => map.style.removeProperty('cursor')) | ||
| 1120 | const resumeContent = () => { | ||
| 1121 | range.deleteContents() | ||
| 1122 | range.insertNode(originContent) | ||
| 1123 | } | ||
| 1124 | |||
| 1125 | const map = document.elementFromPoint(e.clientX, e.clientY) | ||
| 1126 | .closest('.mapclay[data-render="fulfilled"]') | ||
| 1127 | const selection = cm.getSelection() | ||
| 1128 | if (!map || !selection) { | ||
| 1129 | resumeContent('map/selection') | ||
| 1130 | return | ||
| 1131 | } | ||
| 1132 | |||
| 1133 | const refLink = addAnchorByPoint({ | ||
| 1134 | defaultName: geoLink.textContent, | ||
| 1135 | point: e, | ||
| 1136 | map, | ||
| 1137 | validateAnchorName, | ||
| 1138 | }) | ||
| 1139 | if (!refLink) { | ||
| 1140 | resumeContent('reflink') | ||
| 1141 | return | ||
| 1142 | } | ||
| 1143 | |||
| 1144 | const scrollTop = dumbymap.htmlHolder.scrollTop | ||
| 1145 | geoLink.href = refLink.link | ||
| 1146 | createGeoLink(geoLink) | ||
| 1147 | appendRefLink(cm, refLink) | ||
| 1148 | if (selection === refLink.ref) { | ||
| 1149 | cm.replaceSelection(`[${selection}]`) | ||
| 1150 | } else { | ||
| 1151 | cm.replaceSelection(`[${selection}][${refLink.ref}]`) | ||
| 1152 | } | ||
| 1153 | dumbymap.htmlHolder.scrollBy(0, scrollTop) | ||
| 1154 | } | ||
| 1155 | } | ||
| 1156 | |||
| 1157 | dumbyContainer.ondragstart = () => false | ||