diff options
-rw-r--r-- | src/Link.mjs | 239 | ||||
-rw-r--r-- | src/MenuItem.mjs | 11 | ||||
-rw-r--r-- | src/dumbyUtils.mjs | 292 | ||||
-rw-r--r-- | src/dumbymap.mjs | 13 |
4 files changed, 288 insertions, 267 deletions
diff --git a/src/Link.mjs b/src/Link.mjs new file mode 100644 index 0000000..61aa5f8 --- /dev/null +++ b/src/Link.mjs | |||
@@ -0,0 +1,239 @@ | |||
1 | import LeaderLine from 'leader-line' | ||
2 | import { insideWindow, insideParent } from './utils' | ||
3 | |||
4 | /** VAR: pattern for coodinates */ | ||
5 | export const coordPattern = /^geo:([-]?[0-9.]+),([-]?[0-9.]+)/ | ||
6 | |||
7 | /** | ||
8 | * Class: GeoLink - link for maps | ||
9 | * | ||
10 | * @extends {window.HTMLAnchorElement} | ||
11 | */ | ||
12 | export class GeoLink extends window.HTMLAnchorElement { | ||
13 | static replaceWith = (link) => | ||
14 | link.replaceWith(new GeoLink(link)) | ||
15 | |||
16 | /** | ||
17 | * Creates a new GeoLink instance | ||
18 | * | ||
19 | * @param {HTMLAnchorElement} link | ||
20 | */ | ||
21 | constructor (link) { | ||
22 | super() | ||
23 | this.innerHTML = link.innerHTML | ||
24 | this.href = link.href | ||
25 | |||
26 | const url = new URL(link.href) | ||
27 | const params = new URLSearchParams(link.search) | ||
28 | const xyInParams = params.get('xy')?.split(',')?.map(Number) | ||
29 | const [lon, lat] = url.href | ||
30 | ?.match(coordPattern) | ||
31 | ?.slice(1) | ||
32 | ?.reverse() | ||
33 | ?.map(Number) | ||
34 | const xy = xyInParams ?? [lon, lat] | ||
35 | |||
36 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false | ||
37 | |||
38 | // Geo information in link | ||
39 | this.dataset.lon = lon | ||
40 | this.dataset.lat = lat | ||
41 | this.dataset.crs = params.get('crs') | ||
42 | this.classList.add('with-leader-line', 'geolink') | ||
43 | this.classList.remove('not-geolink') | ||
44 | // TODO refactor as data attribute | ||
45 | this.targets = params.get('id')?.split(',') ?? null | ||
46 | this.title = 'Left-Click to move Camera, Middle-Click to clean anchor' | ||
47 | this.lines = [] | ||
48 | |||
49 | // Hover link for LeaderLine | ||
50 | this.onmouseover = () => this.getMarkersFromMaps(this) | ||
51 | .filter(isAnchorVisible) | ||
52 | .forEach(anchor => { | ||
53 | const labelText = new URL(this).searchParams.get('text') ?? this.textContent | ||
54 | const line = new LeaderLine({ | ||
55 | start: this, | ||
56 | end: anchor, | ||
57 | hide: true, | ||
58 | middleLabel: labelText, | ||
59 | path: 'magnet', | ||
60 | }) | ||
61 | line.show('draw', { duration: 300 }) | ||
62 | |||
63 | this.lines.push(line) | ||
64 | }) | ||
65 | |||
66 | this.onmouseout = () => removeLeaderLines(this) | ||
67 | |||
68 | // Click to move camera | ||
69 | this.onclick = (event) => { | ||
70 | event.preventDefault() | ||
71 | removeLeaderLines(this) | ||
72 | this.getMarkersFromMaps().forEach(marker => { | ||
73 | const map = marker.closest('.mapclay') | ||
74 | map.scrollIntoView({ behavior: 'smooth' }) | ||
75 | updateMapCameraByMarker([ | ||
76 | Number(this.dataset.lon), | ||
77 | Number(this.dataset.lat), | ||
78 | ])(marker) | ||
79 | }) | ||
80 | } | ||
81 | |||
82 | // Use middle click to remove markers | ||
83 | this.onauxclick = (e) => { | ||
84 | if (e.which !== 2) return | ||
85 | e.preventDefault() | ||
86 | removeLeaderLines(this) | ||
87 | this.getMarkersFromMaps() | ||
88 | .forEach(marker => marker.remove()) | ||
89 | } | ||
90 | } | ||
91 | |||
92 | /** | ||
93 | * getMarkersFromMaps. Get marker elements by GeoLink | ||
94 | * | ||
95 | * @param {HTMLAnchorElement} link | ||
96 | * @return {HTMLElement[]} markers | ||
97 | */ | ||
98 | getMarkersFromMaps () { | ||
99 | const params = new URLSearchParams(this.search) | ||
100 | const maps = Array.from( | ||
101 | this.closest('.Dumby') | ||
102 | .querySelectorAll('.mapclay[data-render="fulfilled"]'), | ||
103 | ) | ||
104 | return maps | ||
105 | .filter(map => this.targets ? this.targets.includes(map.id) : true) | ||
106 | .map(map => { | ||
107 | const renderer = map.renderer | ||
108 | const lonLat = [Number(this.dataset.lon), Number(this.dataset.lat)] | ||
109 | |||
110 | const marker = map.querySelector(`.marker[data-xy="${lonLat}"]`) ?? | ||
111 | renderer.addMarker({ | ||
112 | xy: lonLat, | ||
113 | type: params.get('type') ?? null, | ||
114 | }) | ||
115 | marker.dataset.xy = lonLat | ||
116 | marker.title = new URLSearchParams(this.search).get('xy') ?? lonLat | ||
117 | const crs = this.dataset.crs | ||
118 | if (crs && crs !== 'EPSG:4326') { | ||
119 | marker.title += '@' + this.dataset.crs | ||
120 | } | ||
121 | |||
122 | return marker | ||
123 | }) | ||
124 | } | ||
125 | } | ||
126 | if (!window.customElements.get('dumby-geolink')) { | ||
127 | window.customElements.define('dumby-geolink', GeoLink, { extends: 'a' }) | ||
128 | } | ||
129 | |||
130 | /** | ||
131 | * Class: DocLink - link for DOM | ||
132 | * | ||
133 | * @extends {window.HTMLAnchorElement} | ||
134 | */ | ||
135 | export class DocLink extends window.HTMLAnchorElement { | ||
136 | static replaceWith = (link) => | ||
137 | link.replaceWith(new DocLink(link)) | ||
138 | |||
139 | /** | ||
140 | * Creates a new DocLink instance | ||
141 | * | ||
142 | * @param {HTMLAnchorElement} link | ||
143 | */ | ||
144 | constructor (link) { | ||
145 | super() | ||
146 | this.innerHTML = link.innerHTML | ||
147 | this.href = link.href | ||
148 | |||
149 | const label = decodeURIComponent(link.href.split('#')[1]) | ||
150 | const selector = link.title.split('=>')[1] ?? (label ? '#' + label : null) | ||
151 | if (!selector) return false | ||
152 | |||
153 | this.classList.add('with-leader-line', 'doclink') | ||
154 | this.lines = [] | ||
155 | |||
156 | this.onmouseover = () => { | ||
157 | const targets = document.querySelectorAll(selector) | ||
158 | |||
159 | targets.forEach(target => { | ||
160 | if (!target?.checkVisibility()) return | ||
161 | |||
162 | // highlight selected target | ||
163 | target.dataset.style = target.style.cssText | ||
164 | const rect = target.getBoundingClientRect() | ||
165 | const isTiny = rect.width < 100 || rect.height < 100 | ||
166 | if (isTiny) { | ||
167 | target.style.background = 'lightPink' | ||
168 | } else { | ||
169 | target.style.outline = 'lightPink 6px dashed' | ||
170 | } | ||
171 | |||
172 | // point to selected target | ||
173 | const line = new LeaderLine({ | ||
174 | start: this, | ||
175 | end: target, | ||
176 | middleLabel: LeaderLine.pathLabel({ | ||
177 | text: label, | ||
178 | fontWeight: 'bold', | ||
179 | }), | ||
180 | hide: true, | ||
181 | path: 'magnet', | ||
182 | }) | ||
183 | this.lines.push(line) | ||
184 | line.show('draw', { duration: 300 }) | ||
185 | }) | ||
186 | } | ||
187 | |||
188 | this.onmouseout = () => { | ||
189 | removeLeaderLines(this) | ||
190 | |||
191 | // resume targets from highlight | ||
192 | const targets = document.querySelectorAll(selector) | ||
193 | targets.forEach(target => { | ||
194 | target.style.cssText = target.dataset.style | ||
195 | delete target.dataset.style | ||
196 | }) | ||
197 | } | ||
198 | } | ||
199 | } | ||
200 | if (!window.customElements.get('dumby-doclink')) { | ||
201 | window.customElements.define('dumby-doclink', DocLink, { extends: 'a' }) | ||
202 | } | ||
203 | |||
204 | /** | ||
205 | * isAnchorVisible. check anchor(marker) is visible for current map camera | ||
206 | * | ||
207 | * @param {Element} anchor | ||
208 | */ | ||
209 | const isAnchorVisible = anchor => { | ||
210 | const mapContainer = anchor.closest('.mapclay') | ||
211 | return insideWindow(anchor) && insideParent(anchor, mapContainer) | ||
212 | } | ||
213 | |||
214 | /** | ||
215 | * updateMapByMarker. get function for updating map camera by marker | ||
216 | * | ||
217 | * @param {Number[]} xy | ||
218 | * @return {Function} function | ||
219 | */ | ||
220 | const updateMapCameraByMarker = lonLat => marker => { | ||
221 | const renderer = marker.closest('.mapclay')?.renderer | ||
222 | renderer.updateCamera({ center: lonLat }, true) | ||
223 | } | ||
224 | |||
225 | /** | ||
226 | * removeLeaderLines. clean lines start from link | ||
227 | * | ||
228 | * @param {HTMLAnchorElement} link | ||
229 | */ | ||
230 | export const removeLeaderLines = link => { | ||
231 | if (!link.lines) return | ||
232 | link.lines.forEach(line => { | ||
233 | line.hide('draw', { duration: 300 }) | ||
234 | setTimeout(() => { | ||
235 | line.remove() | ||
236 | }, 300) | ||
237 | }) | ||
238 | link.lines = [] | ||
239 | } | ||
diff --git a/src/MenuItem.mjs b/src/MenuItem.mjs index 874c829..912080b 100644 --- a/src/MenuItem.mjs +++ b/src/MenuItem.mjs | |||
@@ -1,5 +1,5 @@ | |||
1 | import { shiftByWindow } from './utils.mjs' | 1 | import { shiftByWindow } from './utils.mjs' |
2 | import * as utils from './dumbyUtils.mjs' | 2 | import { GeoLink, removeLeaderLines } from './Link.mjs' |
3 | 3 | ||
4 | /** | 4 | /** |
5 | * @typedef {Object} RefLink | 5 | * @typedef {Object} RefLink |
@@ -431,7 +431,7 @@ export const addRefLink = (cm, refLinks) => | |||
431 | /** | 431 | /** |
432 | * setGeoLinkTypeItem. | 432 | * setGeoLinkTypeItem. |
433 | * | 433 | * |
434 | * @param {HTMLAnchorElement} link | 434 | * @param {GeoLink} link |
435 | * @param {String} text | 435 | * @param {String} text |
436 | * @param {String} type | 436 | * @param {String} type |
437 | */ | 437 | */ |
@@ -439,13 +439,14 @@ export const setGeoLinkTypeItem = ({ link, text, type }) => { | |||
439 | const params = new URLSearchParams(link.search) | 439 | const params = new URLSearchParams(link.search) |
440 | return new Item({ | 440 | return new Item({ |
441 | text, | 441 | text, |
442 | className: ['keep-menu'], | ||
442 | onclick: () => { | 443 | onclick: () => { |
443 | params.set('type', type) | 444 | params.set('type', type) |
444 | link.search = params | 445 | link.search = params |
445 | utils.removeLeaderLines(link) | 446 | removeLeaderLines(link) |
446 | utils.getMarkersFromMaps(link) | 447 | link.getMarkersFromMaps() |
447 | .forEach(marker => marker.remove()) | 448 | .forEach(marker => marker.remove()) |
448 | utils.getMarkersFromMaps(link) | 449 | link.getMarkersFromMaps() |
449 | }, | 450 | }, |
450 | }) | 451 | }) |
451 | } | 452 | } |
diff --git a/src/dumbyUtils.mjs b/src/dumbyUtils.mjs index 8fe23eb..6a1eaca 100644 --- a/src/dumbyUtils.mjs +++ b/src/dumbyUtils.mjs | |||
@@ -1,8 +1,7 @@ | |||
1 | import LeaderLine from 'leader-line' | 1 | import LeaderLine from 'leader-line' |
2 | import { insideWindow, insideParent, replaceTextNodes, full2Half } from './utils' | 2 | import { replaceTextNodes, full2Half } from './utils' |
3 | import proj4 from 'proj4' | 3 | import proj4 from 'proj4' |
4 | 4 | import { coordPattern, GeoLink } from './Link.mjs' | |
5 | export const coordPattern = /^geo:([-]?[0-9.]+),([-]?[0-9.]+)/ | ||
6 | 5 | ||
7 | /** | 6 | /** |
8 | * focusNextMap. | 7 | * focusNextMap. |
@@ -88,225 +87,6 @@ export function removeBlockFocus () { | |||
88 | } | 87 | } |
89 | 88 | ||
90 | /** | 89 | /** |
91 | * getMarkersFromMaps. Get marker elements by GeoLink | ||
92 | * | ||
93 | * @param {HTMLAnchorElement} link | ||
94 | * @return {HTMLElement[]} markers | ||
95 | */ | ||
96 | export const getMarkersFromMaps = link => { | ||
97 | const params = new URLSearchParams(link.search) | ||
98 | const maps = Array.from( | ||
99 | link.closest('.Dumby') | ||
100 | .querySelectorAll('.mapclay[data-render="fulfilled"]'), | ||
101 | ) | ||
102 | return maps | ||
103 | .filter(map => link.targets ? link.targets.includes(map.id) : true) | ||
104 | .map(map => { | ||
105 | const renderer = map.renderer | ||
106 | const lonLat = [Number(link.dataset.lon), Number(link.dataset.lat)] | ||
107 | |||
108 | const marker = map.querySelector(`.marker[data-xy="${lonLat}"]`) ?? | ||
109 | renderer.addMarker({ | ||
110 | xy: lonLat, | ||
111 | type: params.get('type') ?? null, | ||
112 | }) | ||
113 | marker.dataset.xy = lonLat | ||
114 | marker.title = new URLSearchParams(link.search).get('xy') ?? lonLat | ||
115 | const crs = link.dataset.crs | ||
116 | if (crs && crs !== 'EPSG:4326') { | ||
117 | marker.title += '@' + link.dataset.crs | ||
118 | } | ||
119 | |||
120 | return marker | ||
121 | }) | ||
122 | } | ||
123 | |||
124 | /** | ||
125 | * addLeaderLine, from link element to target element | ||
126 | * | ||
127 | * @param {HTMLAnchorElement} link | ||
128 | * @param {Element} target | ||
129 | */ | ||
130 | const addLeaderLine = (link, target) => { | ||
131 | const labelText = new URL(link).searchParams.get('text') ?? link.textContent | ||
132 | const line = new LeaderLine({ | ||
133 | start: link, | ||
134 | end: target, | ||
135 | hide: true, | ||
136 | middleLabel: labelText, | ||
137 | path: 'magnet', | ||
138 | }) | ||
139 | line.show('draw', { duration: 300 }) | ||
140 | |||
141 | return line | ||
142 | } | ||
143 | |||
144 | /** | ||
145 | * Create geolinks, which points to map by geo schema and id | ||
146 | * | ||
147 | * @param {HTMLElement} Elements contains anchor elements for GeoLinks | ||
148 | * @returns {Boolean} ture is link is created, false if coordinates are invalid | ||
149 | */ | ||
150 | export const createGeoLink = (link) => { | ||
151 | const url = new URL(link.href) | ||
152 | const params = new URLSearchParams(link.search) | ||
153 | const xyInParams = params.get('xy')?.split(',')?.map(Number) | ||
154 | const [lon, lat] = url.href | ||
155 | ?.match(coordPattern) | ||
156 | ?.slice(1) | ||
157 | ?.reverse() | ||
158 | ?.map(Number) | ||
159 | const xy = xyInParams ?? [lon, lat] | ||
160 | |||
161 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false | ||
162 | |||
163 | // Geo information in link | ||
164 | link.dataset.lon = lon | ||
165 | link.dataset.lat = lat | ||
166 | link.dataset.crs = params.get('crs') | ||
167 | link.classList.add('with-leader-line', 'geolink') | ||
168 | link.classList.remove('not-geolink') | ||
169 | // TODO refactor as data attribute | ||
170 | link.targets = params.get('id')?.split(',') ?? null | ||
171 | link.title = 'Left-Click to move Camera, Middle-Click to clean anchor' | ||
172 | |||
173 | link.lines = [] | ||
174 | |||
175 | // Hover link for LeaderLine | ||
176 | link.onmouseover = () => { | ||
177 | if (link.dataset.valid === 'false') return | ||
178 | |||
179 | const anchors = getMarkersFromMaps(link) | ||
180 | anchors | ||
181 | .filter(isAnchorVisible) | ||
182 | .forEach(anchor => { | ||
183 | const line = addLeaderLine(link, anchor) | ||
184 | link.lines.push(line) | ||
185 | }) | ||
186 | } | ||
187 | link.onmouseout = () => removeLeaderLines(link) | ||
188 | |||
189 | // Click to move camera | ||
190 | link.onclick = (event) => { | ||
191 | event.preventDefault() | ||
192 | if (link.dataset.valid === 'false') return | ||
193 | |||
194 | removeLeaderLines(link) | ||
195 | getMarkersFromMaps(link).forEach(marker => { | ||
196 | const map = marker.closest('.mapclay') | ||
197 | map.scrollIntoView({ behavior: 'smooth' }) | ||
198 | updateMapCameraByMarker([ | ||
199 | Number(link.dataset.lon), | ||
200 | Number(link.dataset.lat), | ||
201 | ])(marker) | ||
202 | }) | ||
203 | } | ||
204 | |||
205 | // Use middle click to remove markers | ||
206 | link.onauxclick = (e) => { | ||
207 | if (e.which !== 2) return | ||
208 | e.preventDefault() | ||
209 | removeLeaderLines(link) | ||
210 | getMarkersFromMaps(link) | ||
211 | .forEach(marker => marker.remove()) | ||
212 | } | ||
213 | return true | ||
214 | } | ||
215 | |||
216 | /** | ||
217 | * CreateDocLink. | ||
218 | * | ||
219 | * @param {HTMLElement} Elements contains anchor elements for doclinks | ||
220 | */ | ||
221 | export const createDocLink = link => { | ||
222 | const label = decodeURIComponent(link.href.split('#')[1]) | ||
223 | const selector = link.title.split('=>')[1] ?? (label ? '#' + label : null) | ||
224 | if (!selector) return false | ||
225 | |||
226 | link.classList.add('with-leader-line', 'doclink') | ||
227 | link.lines = [] | ||
228 | |||
229 | link.onmouseover = () => { | ||
230 | const targets = document.querySelectorAll(selector) | ||
231 | |||
232 | targets.forEach(target => { | ||
233 | if (!target?.checkVisibility()) return | ||
234 | |||
235 | // highlight selected target | ||
236 | target.dataset.style = target.style.cssText | ||
237 | const rect = target.getBoundingClientRect() | ||
238 | const isTiny = rect.width < 100 || rect.height < 100 | ||
239 | if (isTiny) { | ||
240 | target.style.background = 'lightPink' | ||
241 | } else { | ||
242 | target.style.outline = 'lightPink 6px dashed' | ||
243 | } | ||
244 | |||
245 | // point to selected target | ||
246 | const line = new LeaderLine({ | ||
247 | start: link, | ||
248 | end: target, | ||
249 | middleLabel: LeaderLine.pathLabel({ | ||
250 | text: label, | ||
251 | fontWeight: 'bold', | ||
252 | }), | ||
253 | hide: true, | ||
254 | path: 'magnet', | ||
255 | }) | ||
256 | link.lines.push(line) | ||
257 | line.show('draw', { duration: 300 }) | ||
258 | }) | ||
259 | } | ||
260 | link.onmouseout = () => { | ||
261 | link.onmouseout = () => removeLeaderLines(link) | ||
262 | |||
263 | // resume targets from highlight | ||
264 | const targets = document.querySelectorAll(selector) | ||
265 | targets.forEach(target => { | ||
266 | target.style.cssText = target.dataset.style | ||
267 | delete target.dataset.style | ||
268 | }) | ||
269 | } | ||
270 | } | ||
271 | |||
272 | /** | ||
273 | * removeLeaderLines. clean lines start from link | ||
274 | * | ||
275 | * @param {HTMLAnchorElement} link | ||
276 | */ | ||
277 | export const removeLeaderLines = link => { | ||
278 | if (!link.lines) return | ||
279 | link.lines.forEach(line => { | ||
280 | line.hide('draw', { duration: 300 }) | ||
281 | setTimeout(() => { | ||
282 | line.remove() | ||
283 | }, 300) | ||
284 | }) | ||
285 | link.lines = [] | ||
286 | } | ||
287 | |||
288 | /** | ||
289 | * updateMapByMarker. get function for updating map camera by marker | ||
290 | * | ||
291 | * @param {Number[]} xy | ||
292 | * @return {Function} function | ||
293 | */ | ||
294 | const updateMapCameraByMarker = lonLat => marker => { | ||
295 | const renderer = marker.closest('.mapclay')?.renderer | ||
296 | renderer.updateCamera({ center: lonLat }, true) | ||
297 | } | ||
298 | |||
299 | /** | ||
300 | * isAnchorVisible. check anchor(marker) is visible for current map camera | ||
301 | * | ||
302 | * @param {Element} anchor | ||
303 | */ | ||
304 | const isAnchorVisible = anchor => { | ||
305 | const mapContainer = anchor.closest('.mapclay') | ||
306 | return insideWindow(anchor) && insideParent(anchor, mapContainer) | ||
307 | } | ||
308 | |||
309 | /** | ||
310 | * addMarkerByPoint. | 90 | * addMarkerByPoint. |
311 | * | 91 | * |
312 | * @param {Number[]} options.point - page XY | 92 | * @param {Number[]} options.point - page XY |
@@ -328,6 +108,30 @@ export const addMarkerByPoint = ({ point, map }) => { | |||
328 | } | 108 | } |
329 | 109 | ||
330 | /** | 110 | /** |
111 | * addGeoSchemeByText. | ||
112 | * | ||
113 | * @param {Node} node | ||
114 | */ | ||
115 | export const addGeoSchemeByText = async (node) => { | ||
116 | const digit = '[\\d\\uFF10-\\uFF19]' | ||
117 | const decimal = '[.\\uFF0E]' | ||
118 | const coordPatterns = `(-?${digit}+${decimal}?${digit}*)([,\x2F\uFF0C])(-?${digit}+${decimal}?${digit}*)` | ||
119 | const re = new RegExp(coordPatterns, 'g') | ||
120 | |||
121 | return replaceTextNodes(node, re, match => { | ||
122 | const [x, y] = [full2Half(match.at(1)), full2Half(match.at(3))] | ||
123 | // Don't process string which can be used as date | ||
124 | if (Date.parse(match.at(0) + ' 1990')) return null | ||
125 | |||
126 | const a = document.createElement('a') | ||
127 | a.className = 'not-geolink from-text' | ||
128 | a.href = `geo:0,0?xy=${x},${y}` | ||
129 | a.textContent = match.at(0) | ||
130 | return a | ||
131 | }) | ||
132 | } | ||
133 | |||
134 | /** | ||
331 | * setGeoSchemeByCRS. | 135 | * setGeoSchemeByCRS. |
332 | * @description Add more information into Anchor Element within Geo Scheme by CRS | 136 | * @description Add more information into Anchor Element within Geo Scheme by CRS |
333 | * @param {String} crs - EPSG/ESRI Code for CRS | 137 | * @param {String} crs - EPSG/ESRI Code for CRS |
@@ -375,17 +179,17 @@ export const setGeoSchemeByCRS = (crs) => (link) => { | |||
375 | } | 179 | } |
376 | 180 | ||
377 | /** | 181 | /** |
378 | * dragForAnchor. | 182 | * addGeoLinkByDrag. |
379 | * | 183 | * |
380 | * @param {HTMLElement} container | 184 | * @param {HTMLElement} container |
381 | * @param {Range} range | 185 | * @param {Range} range |
382 | */ | 186 | */ |
383 | export const dragForAnchor = (container, range, endOfLeaderLine) => { | 187 | export const addGeoLinkByDrag = (container, range, endOfLeaderLine) => { |
384 | // link placeholder when dragging | 188 | // link placeholder when dragging |
385 | container.classList.add('dragging-geolink') | 189 | container.classList.add('dragging-geolink') |
386 | const geoLink = document.createElement('a') | 190 | const link = document.createElement('a') |
387 | geoLink.textContent = range.toString() | 191 | link.textContent = range.toString() |
388 | geoLink.classList.add('with-leader-line', 'geolink', 'drag', 'from-text') | 192 | link.classList.add('with-leader-line', 'geolink', 'drag', 'from-text') |
389 | 193 | ||
390 | // Replace current content with link | 194 | // Replace current content with link |
391 | const originContent = range.cloneContents() | 195 | const originContent = range.cloneContents() |
@@ -394,11 +198,11 @@ export const dragForAnchor = (container, range, endOfLeaderLine) => { | |||
394 | range.insertNode(originContent) | 198 | range.insertNode(originContent) |
395 | } | 199 | } |
396 | range.deleteContents() | 200 | range.deleteContents() |
397 | range.insertNode(geoLink) | 201 | range.insertNode(link) |
398 | 202 | ||
399 | // Add leader-line | 203 | // Add leader-line |
400 | const line = new LeaderLine({ | 204 | const line = new LeaderLine({ |
401 | start: geoLink, | 205 | start: link, |
402 | end: endOfLeaderLine, | 206 | end: endOfLeaderLine, |
403 | path: 'magnet', | 207 | path: 'magnet', |
404 | }) | 208 | }) |
@@ -416,7 +220,7 @@ export const dragForAnchor = (container, range, endOfLeaderLine) => { | |||
416 | container.classList.remove('dragging-geolink') | 220 | container.classList.remove('dragging-geolink') |
417 | container.onmousemove = null | 221 | container.onmousemove = null |
418 | container.onmouseup = null | 222 | container.onmouseup = null |
419 | geoLink.classList.remove('drag') | 223 | link.classList.remove('drag') |
420 | positionObserver.disconnect() | 224 | positionObserver.disconnect() |
421 | line.remove() | 225 | line.remove() |
422 | endOfLeaderLine.remove() | 226 | endOfLeaderLine.remove() |
@@ -434,31 +238,7 @@ export const dragForAnchor = (container, range, endOfLeaderLine) => { | |||
434 | return | 238 | return |
435 | } | 239 | } |
436 | 240 | ||
437 | geoLink.href = `geo:${marker.dataset.xy.split(',').reverse()}` | 241 | link.href = `geo:${marker.dataset.xy.split(',').reverse()}` |
438 | createGeoLink(geoLink) | 242 | GeoLink.replaceWith(link) |
439 | } | 243 | } |
440 | } | 244 | } |
441 | |||
442 | /** | ||
443 | * addGeoSchemeByText. | ||
444 | * | ||
445 | * @param {Node} node | ||
446 | */ | ||
447 | export const addGeoSchemeByText = async (node) => { | ||
448 | const digit = '[\\d\\uFF10-\\uFF19]' | ||
449 | const decimal = '[.\\uFF0E]' | ||
450 | const coordPatterns = `(-?${digit}+${decimal}?${digit}*)([,\x2F\uFF0C])(-?${digit}+${decimal}?${digit}*)` | ||
451 | const re = new RegExp(coordPatterns, 'g') | ||
452 | |||
453 | return replaceTextNodes(node, re, match => { | ||
454 | const [x, y] = [full2Half(match.at(1)), full2Half(match.at(3))] | ||
455 | // Don't process string which can be used as date | ||
456 | if (Date.parse(match.at(0) + ' 1990')) return null | ||
457 | |||
458 | const a = document.createElement('a') | ||
459 | a.className = 'not-geolink from-text' | ||
460 | a.href = `geo:0,0?xy=${x},${y}` | ||
461 | a.textContent = match.at(0) | ||
462 | return a | ||
463 | }) | ||
464 | } | ||
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 92fe785..bb3737b 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
@@ -6,6 +6,7 @@ import MarkdownItInjectLinenumbers from 'markdown-it-inject-linenumbers' | |||
6 | import * as mapclay from 'mapclay' | 6 | import * as mapclay from 'mapclay' |
7 | import { onRemove, animateRectTransition, throttle, debounce, shiftByWindow } from './utils' | 7 | import { onRemove, animateRectTransition, throttle, debounce, shiftByWindow } from './utils' |
8 | import { Layout, SideBySide, Overlay, Sticky } from './Layout' | 8 | import { Layout, SideBySide, Overlay, Sticky } from './Layout' |
9 | import { GeoLink, DocLink } from './Link.mjs' | ||
9 | import * as utils from './dumbyUtils' | 10 | import * as utils from './dumbyUtils' |
10 | import * as menuItem from './MenuItem' | 11 | import * as menuItem from './MenuItem' |
11 | import PlainModal from 'plain-modal' | 12 | import PlainModal from 'plain-modal' |
@@ -209,7 +210,7 @@ export const generateMaps = (container, { | |||
209 | if (node.matches?.('.mapclay') || node.closest?.('.mapclay')) return | 210 | if (node.matches?.('.mapclay') || node.closest?.('.mapclay')) return |
210 | 211 | ||
211 | // Add GeoLinks from plain texts | 212 | // Add GeoLinks from plain texts |
212 | addGeoLinksByText(node) | 213 | utils.addGeoSchemeByText(node) |
213 | 214 | ||
214 | // Render Map | 215 | // Render Map |
215 | const mapTarget = node.parentElement?.closest(mapBlockSelector) | 216 | const mapTarget = node.parentElement?.closest(mapBlockSelector) |
@@ -245,9 +246,9 @@ export const generateMaps = (container, { | |||
245 | 246 | ||
246 | // Add GeoLinks/DocLinks by pattern | 247 | // Add GeoLinks/DocLinks by pattern |
247 | target.querySelectorAll(geoLinkSelector) | 248 | target.querySelectorAll(geoLinkSelector) |
248 | .forEach(utils.createGeoLink) | 249 | .forEach(GeoLink.replaceWith) |
249 | target.querySelectorAll(docLinkSelector) | 250 | target.querySelectorAll(docLinkSelector) |
250 | .forEach(utils.createDocLink) | 251 | .forEach(DocLink.replaceWith) |
251 | 252 | ||
252 | // Add GeoLinks from text nodes | 253 | // Add GeoLinks from text nodes |
253 | // const addedNodes = Array.from(mutation.addedNodes) | 254 | // const addedNodes = Array.from(mutation.addedNodes) |
@@ -318,7 +319,7 @@ export const generateMaps = (container, { | |||
318 | values.at(-1) | 319 | values.at(-1) |
319 | .map(utils.setGeoSchemeByCRS(crsString)) | 320 | .map(utils.setGeoSchemeByCRS(crsString)) |
320 | .filter(link => link) | 321 | .filter(link => link) |
321 | .forEach(utils.createGeoLink) | 322 | .forEach(GeoLink.replaceWith) |
322 | }) | 323 | }) |
323 | } | 324 | } |
324 | 325 | ||
@@ -556,7 +557,7 @@ export const generateMaps = (container, { | |||
556 | menu.appendChild(new menuItem.Item({ | 557 | menu.appendChild(new menuItem.Item({ |
557 | text: 'Delete', | 558 | text: 'Delete', |
558 | onclick: () => { | 559 | onclick: () => { |
559 | utils.getMarkersFromMaps(geoLink) | 560 | geoLink.getMarkersFromMaps() |
560 | .forEach(m => m.remove()) | 561 | .forEach(m => m.remove()) |
561 | geoLink.replaceWith( | 562 | geoLink.replaceWith( |
562 | document.createTextNode(geoLink.textContent), | 563 | document.createTextNode(geoLink.textContent), |
@@ -637,7 +638,7 @@ export const generateMaps = (container, { | |||
637 | container.appendChild(pointByArrow) | 638 | container.appendChild(pointByArrow) |
638 | 639 | ||
639 | const timer = setTimeout(() => { | 640 | const timer = setTimeout(() => { |
640 | utils.dragForAnchor(container, range, pointByArrow) | 641 | utils.addGeoLinkByDrag(container, range, pointByArrow) |
641 | }, 300) | 642 | }, 300) |
642 | 643 | ||
643 | // Update leader-line with mouse move | 644 | // Update leader-line with mouse move |