diff options
author | Hsieh Chin Fan <pham@topo.tw> | 2024-10-25 23:46:30 +0800 |
---|---|---|
committer | Hsieh Chin Fan <pham@topo.tw> | 2024-10-26 00:31:21 +0800 |
commit | 2a2acc8e31aef538a8e68e6b53cacafb38841c26 (patch) | |
tree | 3e6101a4b7090b57342df9e31db4cb184b8e8116 /src | |
parent | 587ba9e22cdbc48f675e77ec6572520a0cd12a58 (diff) |
refactor: patch 1dd8064, remove classes for GeoLink/DocLink
Seems link content script don't accecpt custom element access its own
properties. This commit revert part of 1dd8064, generate GeoLink/DocLonk
by appending features onto existing anchor element
Diffstat (limited to 'src')
-rw-r--r-- | src/Link.mjs | 366 | ||||
-rw-r--r-- | src/MenuItem.mjs | 6 | ||||
-rw-r--r-- | src/dumbyUtils.mjs | 2 | ||||
-rw-r--r-- | src/dumbymap.mjs | 10 |
4 files changed, 188 insertions, 196 deletions
diff --git a/src/Link.mjs b/src/Link.mjs index 9b58fb3..031b125 100644 --- a/src/Link.mjs +++ b/src/Link.mjs | |||
@@ -2,212 +2,204 @@ import LeaderLine from 'leader-line' | |||
2 | import { insideWindow, insideParent } from './utils' | 2 | import { insideWindow, insideParent } from './utils' |
3 | import * as markers from './marker.mjs' | 3 | import * as markers from './marker.mjs' |
4 | 4 | ||
5 | /** | ||
6 | * GeoLink: anchor element with geo scheme and properties about maps | ||
7 | * @typedef {Object} GeoLink | ||
8 | * @extends HTMLAnchorElement | ||
9 | * @property {string[]} targets - ids of target map elements | ||
10 | * @property {LeaderLine[]} lines | ||
11 | * @property {Object} dataset | ||
12 | * @property {string} dataset.lon - longitude string of geo scheme | ||
13 | * @property {string} dataset.lat - latitude string of geo scheme | ||
14 | * @property {string} dataset.crs - short name of CRS in EPSG/ESRI format | ||
15 | */ | ||
16 | |||
17 | /** | ||
18 | * DocLink: anchor element which points to DOM node by filter | ||
19 | * @typedef {Object} DocLink | ||
20 | * @extends HTMLAnchorElement | ||
21 | * @property {LeaderLine[]} lines | ||
22 | */ | ||
23 | |||
5 | /** VAR: pattern for coodinates */ | 24 | /** VAR: pattern for coodinates */ |
6 | export const coordPattern = /^geo:([-]?[0-9.]+),([-]?[0-9.]+)/ | 25 | export const coordPattern = /^geo:([-]?[0-9.]+),([-]?[0-9.]+)/ |
7 | 26 | ||
8 | /** | 27 | /** |
9 | * Class: GeoLink - link for maps | 28 | * GeoLink: append GeoLink features onto anchor element |
10 | * | 29 | * @param {HTMLAnchorElement} link |
11 | * @extends {window.HTMLAnchorElement} | 30 | * @return {GeoLink} |
12 | */ | 31 | */ |
13 | export class GeoLink extends window.HTMLAnchorElement { | 32 | export const GeoLink = (link) => { |
14 | static replaceWith = (link) => | 33 | const url = new URL(link.href) |
15 | link.replaceWith(new GeoLink(link)) | 34 | const params = new URLSearchParams(link.search) |
16 | 35 | const xyInParams = params.get('xy')?.split(',')?.map(Number) | |
17 | /** | 36 | const [lon, lat] = url.href |
18 | * Creates a new GeoLink instance | 37 | ?.match(coordPattern) |
19 | * | 38 | ?.slice(1) |
20 | * @param {HTMLAnchorElement} link | 39 | ?.reverse() |
21 | */ | 40 | ?.map(Number) |
22 | constructor (link) { | 41 | const xy = xyInParams ?? [lon, lat] |
23 | super() | 42 | |
24 | this.innerHTML = link.innerHTML | 43 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false |
25 | this.href = link.href | 44 | |
26 | 45 | // Geo information in link | |
27 | const url = new URL(link.href) | 46 | link.dataset.lon = lon |
28 | const params = new URLSearchParams(link.search) | 47 | link.dataset.lat = lat |
29 | const xyInParams = params.get('xy')?.split(',')?.map(Number) | 48 | link.dataset.crs = params.get('crs') |
30 | const [lon, lat] = url.href | 49 | link.classList.add('with-leader-line', 'geolink') |
31 | ?.match(coordPattern) | 50 | link.classList.remove('not-geolink') |
32 | ?.slice(1) | 51 | // TODO refactor as data attribute |
33 | ?.reverse() | 52 | link.title = 'Left-Click to move Camera, Middle-Click to clean anchor' |
34 | ?.map(Number) | 53 | link.targets = params.get('id')?.split(',') ?? null |
35 | const xy = xyInParams ?? [lon, lat] | 54 | link.lines = [] |
36 | |||
37 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false | ||
38 | |||
39 | // Geo information in link | ||
40 | this.dataset.lon = lon | ||
41 | this.dataset.lat = lat | ||
42 | this.dataset.crs = params.get('crs') | ||
43 | this.classList.add('with-leader-line', 'geolink') | ||
44 | this.classList.remove('not-geolink') | ||
45 | // TODO refactor as data attribute | ||
46 | this.targets = params.get('id')?.split(',') ?? null | ||
47 | this.title = 'Left-Click to move Camera, Middle-Click to clean anchor' | ||
48 | this.lines = [] | ||
49 | |||
50 | // Hover link for LeaderLine | ||
51 | this.onmouseover = () => this.getMarkersFromMaps() | ||
52 | .filter(isAnchorVisible) | ||
53 | .forEach(anchor => { | ||
54 | const labelText = new URL(this).searchParams.get('text') ?? this.textContent | ||
55 | const line = new LeaderLine({ | ||
56 | start: this, | ||
57 | end: anchor, | ||
58 | hide: true, | ||
59 | middleLabel: labelText, | ||
60 | path: 'magnet', | ||
61 | }) | ||
62 | line.show('draw', { duration: 300 }) | ||
63 | |||
64 | this.lines.push(line) | ||
65 | }) | ||
66 | 55 | ||
67 | this.onmouseout = () => removeLeaderLines(this) | 56 | // Hover link for LeaderLine |
68 | 57 | link.onmouseover = () => getMarkersFromMaps(link) | |
69 | // Click to move camera | 58 | .filter(isAnchorVisible) |
70 | this.onclick = (event) => { | 59 | .forEach(anchor => { |
71 | event.preventDefault() | 60 | const labelText = new URL(link).searchParams.get('text') ?? link.textContent |
72 | removeLeaderLines(this) | 61 | const line = new LeaderLine({ |
73 | this.getMarkersFromMaps().forEach(marker => { | 62 | start: link, |
74 | const map = marker.closest('.mapclay') | 63 | end: anchor, |
75 | map.scrollIntoView({ behavior: 'smooth' }) | 64 | hide: true, |
76 | updateMapCameraByMarker([ | 65 | middleLabel: labelText, |
77 | Number(this.dataset.lon), | 66 | path: 'magnet', |
78 | Number(this.dataset.lat), | ||
79 | ])(marker) | ||
80 | }) | 67 | }) |
81 | } | 68 | line.show('draw', { duration: 300 }) |
82 | 69 | ||
83 | // Use middle click to remove markers | 70 | link.lines.push(line) |
84 | this.onauxclick = (e) => { | 71 | }) |
85 | if (e.which !== 2) return | 72 | |
86 | e.preventDefault() | 73 | link.onmouseout = () => removeLeaderLines(link) |
87 | removeLeaderLines(this) | 74 | |
88 | this.getMarkersFromMaps() | 75 | // Click to move camera |
89 | .forEach(marker => marker.remove()) | 76 | link.onclick = (event) => { |
90 | } | 77 | event.preventDefault() |
78 | removeLeaderLines(link) | ||
79 | getMarkersFromMaps(link).forEach(marker => { | ||
80 | const map = marker.closest('.mapclay') | ||
81 | map.scrollIntoView({ behavior: 'smooth' }) | ||
82 | updateMapCameraByMarker([ | ||
83 | Number(link.dataset.lon), | ||
84 | Number(link.dataset.lat), | ||
85 | ])(marker) | ||
86 | }) | ||
91 | } | 87 | } |
92 | 88 | ||
93 | /** | 89 | // Use middle click to remove markers |
94 | * getMarkersFromMaps. Get marker elements by GeoLink | 90 | link.onauxclick = (e) => { |
95 | * | 91 | if (e.which !== 2) return |
96 | * @param {HTMLAnchorElement} link | 92 | e.preventDefault() |
97 | * @return {HTMLElement[]} markers | 93 | removeLeaderLines(link) |
98 | */ | 94 | getMarkersFromMaps(link) |
99 | getMarkersFromMaps () { | 95 | .forEach(marker => marker.remove()) |
100 | const params = new URLSearchParams(this.search) | ||
101 | const maps = Array.from( | ||
102 | this.closest('.Dumby') | ||
103 | .querySelectorAll('.mapclay[data-render="fulfilled"]'), | ||
104 | ) | ||
105 | return maps | ||
106 | .filter(map => this.targets ? this.targets.includes(map.id) : true) | ||
107 | .map(map => { | ||
108 | const renderer = map.renderer | ||
109 | const lonLat = [Number(this.dataset.lon), Number(this.dataset.lat)] | ||
110 | const type = params.get('type') ?? 'pin' | ||
111 | const svg = markers[type] | ||
112 | const element = document.createElement('div') | ||
113 | element.style.cssText = `width: ${svg.size[0]}px; height: ${svg.size[1]}px;` | ||
114 | element.innerHTML = svg.html | ||
115 | |||
116 | const marker = map.querySelector(`.marker[data-xy="${lonLat}"]`) ?? | ||
117 | renderer.addMarker({ | ||
118 | xy: lonLat, | ||
119 | element, | ||
120 | type, | ||
121 | anchor: svg.anchor, | ||
122 | size: svg.size, | ||
123 | }) | ||
124 | marker.dataset.xy = lonLat | ||
125 | marker.title = new URLSearchParams(this.search).get('xy') ?? lonLat | ||
126 | const crs = this.dataset.crs | ||
127 | if (crs && crs !== 'EPSG:4326') { | ||
128 | marker.title += '@' + this.dataset.crs | ||
129 | } | ||
130 | |||
131 | return marker | ||
132 | }) | ||
133 | } | 96 | } |
134 | } | 97 | |
135 | if (!window.customElements.get('dumby-geolink')) { | 98 | return link |
136 | window.customElements.define('dumby-geolink', GeoLink, { extends: 'a' }) | ||
137 | } | 99 | } |
138 | 100 | ||
139 | /** | 101 | /** |
140 | * Class: DocLink - link for DOM | 102 | * GeoLink: getMarkersFromMaps. Get marker elements by GeoLink |
141 | * | 103 | * |
142 | * @extends {window.HTMLAnchorElement} | 104 | * @param {GeoLink} link |
105 | * @return {HTMLElement[]} markers | ||
143 | */ | 106 | */ |
144 | export class DocLink extends window.HTMLAnchorElement { | 107 | export const getMarkersFromMaps = (link) => { |
145 | static replaceWith = (link) => | 108 | const params = new URLSearchParams(link.search) |
146 | link.replaceWith(new DocLink(link)) | 109 | const maps = Array.from( |
147 | 110 | link.closest('.Dumby') | |
148 | /** | 111 | .querySelectorAll('.mapclay[data-render="fulfilled"]'), |
149 | * Creates a new DocLink instance | 112 | ) |
150 | * | 113 | return maps |
151 | * @param {HTMLAnchorElement} link | 114 | .filter(map => link.targets ? link.targets.includes(map.id) : true) |
152 | */ | 115 | .map(map => { |
153 | constructor (link) { | 116 | const renderer = map.renderer |
154 | super() | 117 | const lonLat = [Number(link.dataset.lon), Number(link.dataset.lat)] |
155 | this.innerHTML = link.innerHTML | 118 | const type = params.get('type') ?? 'pin' |
156 | this.href = link.href | 119 | const svg = markers[type] |
157 | 120 | const element = document.createElement('div') | |
158 | const label = decodeURIComponent(link.href.split('#')[1]) | 121 | element.style.cssText = `width: ${svg.size[0]}px; height: ${svg.size[1]}px;` |
159 | const selector = link.title.split('=>')[1] ?? (label ? '#' + label : null) | 122 | element.innerHTML = svg.html |
160 | if (!selector) return false | 123 | |
161 | 124 | const marker = map.querySelector(`.marker[data-xy="${lonLat}"]`) ?? | |
162 | this.classList.add('with-leader-line', 'doclink') | 125 | renderer.addMarker({ |
163 | this.lines = [] | 126 | xy: lonLat, |
164 | 127 | element, | |
165 | this.onmouseover = () => { | 128 | // FIXME In conten script, leaflet cannot render marker with HTMLElement |
166 | const targets = document.querySelectorAll(selector) | 129 | // so pass html string here |
167 | 130 | html: svg.html, | |
168 | targets.forEach(target => { | 131 | type, |
169 | if (!target?.checkVisibility()) return | 132 | anchor: svg.anchor, |
170 | 133 | size: svg.size, | |
171 | // highlight selected target | ||
172 | target.dataset.style = target.style.cssText | ||
173 | const rect = target.getBoundingClientRect() | ||
174 | const isTiny = rect.width < 100 || rect.height < 100 | ||
175 | if (isTiny) { | ||
176 | target.style.background = 'lightPink' | ||
177 | } else { | ||
178 | target.style.outline = 'lightPink 6px dashed' | ||
179 | } | ||
180 | |||
181 | // point to selected target | ||
182 | const line = new LeaderLine({ | ||
183 | start: this, | ||
184 | end: target, | ||
185 | middleLabel: LeaderLine.pathLabel({ | ||
186 | text: label, | ||
187 | fontWeight: 'bold', | ||
188 | }), | ||
189 | hide: true, | ||
190 | path: 'magnet', | ||
191 | }) | 134 | }) |
192 | this.lines.push(line) | 135 | marker.dataset.xy = lonLat |
193 | line.show('draw', { duration: 300 }) | 136 | marker.title = new URLSearchParams(link.search).get('xy') ?? lonLat |
194 | }) | 137 | const crs = link.dataset.crs |
195 | } | 138 | if (crs && crs !== 'EPSG:4326') { |
139 | marker.title += '@' + link.dataset.crs | ||
140 | } | ||
141 | |||
142 | return marker | ||
143 | }) | ||
144 | } | ||
145 | |||
146 | /** | ||
147 | * DocLink: append DocLink features onto anchor element | ||
148 | * @param {HTMLAnchorElement} link | ||
149 | * @return {DocLink} | ||
150 | */ | ||
151 | export const DocLink = (link) => { | ||
152 | const label = decodeURIComponent(link.href.split('#')[1]) | ||
153 | const selector = link.title.split('=>')[1] ?? (label ? '#' + label : null) | ||
154 | if (!selector) return false | ||
196 | 155 | ||
197 | this.onmouseout = () => { | 156 | link.classList.add('with-leader-line', 'doclink') |
198 | removeLeaderLines(this) | 157 | link.lines = [] |
199 | 158 | ||
200 | // resume targets from highlight | 159 | link.onmouseover = () => { |
201 | const targets = document.querySelectorAll(selector) | 160 | const targets = document.querySelectorAll(selector) |
202 | targets.forEach(target => { | 161 | |
203 | target.style.cssText = target.dataset.style | 162 | targets.forEach(target => { |
204 | delete target.dataset.style | 163 | if (!target?.checkVisibility()) return |
164 | |||
165 | // highlight selected target | ||
166 | target.dataset.style = target.style.cssText | ||
167 | const rect = target.getBoundingClientRect() | ||
168 | const isTiny = rect.width < 100 || rect.height < 100 | ||
169 | if (isTiny) { | ||
170 | target.style.background = 'lightPink' | ||
171 | } else { | ||
172 | target.style.outline = 'lightPink 6px dashed' | ||
173 | } | ||
174 | |||
175 | // point to selected target | ||
176 | const line = new LeaderLine({ | ||
177 | start: link, | ||
178 | end: target, | ||
179 | middleLabel: LeaderLine.pathLabel({ | ||
180 | text: label, | ||
181 | fontWeight: 'bold', | ||
182 | }), | ||
183 | hide: true, | ||
184 | path: 'magnet', | ||
205 | }) | 185 | }) |
206 | } | 186 | link.lines.push(line) |
187 | line.show('draw', { duration: 300 }) | ||
188 | }) | ||
207 | } | 189 | } |
208 | } | 190 | |
209 | if (!window.customElements.get('dumby-doclink')) { | 191 | link.onmouseout = () => { |
210 | window.customElements.define('dumby-doclink', DocLink, { extends: 'a' }) | 192 | removeLeaderLines(link) |
193 | |||
194 | // resume targets from highlight | ||
195 | const targets = document.querySelectorAll(selector) | ||
196 | targets.forEach(target => { | ||
197 | target.style.cssText = target.dataset.style | ||
198 | delete target.dataset.style | ||
199 | }) | ||
200 | } | ||
201 | |||
202 | return link | ||
211 | } | 203 | } |
212 | 204 | ||
213 | /** | 205 | /** |
@@ -234,7 +226,7 @@ const updateMapCameraByMarker = lonLat => marker => { | |||
234 | /** | 226 | /** |
235 | * removeLeaderLines. clean lines start from link | 227 | * removeLeaderLines. clean lines start from link |
236 | * | 228 | * |
237 | * @param {HTMLAnchorElement} link | 229 | * @param {GeoLink} link |
238 | */ | 230 | */ |
239 | export const removeLeaderLines = link => { | 231 | export const removeLeaderLines = link => { |
240 | if (!link.lines) return | 232 | if (!link.lines) return |
diff --git a/src/MenuItem.mjs b/src/MenuItem.mjs index 7b1f41e..4769817 100644 --- a/src/MenuItem.mjs +++ b/src/MenuItem.mjs | |||
@@ -1,6 +1,6 @@ | |||
1 | import { shiftByWindow } from './utils.mjs' | 1 | import { shiftByWindow } from './utils.mjs' |
2 | /* eslint-disable no-unused-vars */ | 2 | /* eslint-disable no-unused-vars */ |
3 | import { GeoLink, removeLeaderLines } from './Link.mjs' | 3 | import { GeoLink, getMarkersFromMaps, removeLeaderLines } from './Link.mjs' |
4 | /* eslint-enable */ | 4 | /* eslint-enable */ |
5 | import * as markers from './marker.mjs' | 5 | import * as markers from './marker.mjs' |
6 | 6 | ||
@@ -447,9 +447,9 @@ export const setGeoLinkTypeItem = ({ link, type, ...others }) => { | |||
447 | params.set('type', type) | 447 | params.set('type', type) |
448 | link.search = params | 448 | link.search = params |
449 | removeLeaderLines(link) | 449 | removeLeaderLines(link) |
450 | link.getMarkersFromMaps() | 450 | getMarkersFromMaps(link) |
451 | .forEach(marker => marker.remove()) | 451 | .forEach(marker => marker.remove()) |
452 | link.getMarkersFromMaps() | 452 | getMarkersFromMaps(link) |
453 | }, | 453 | }, |
454 | }) | 454 | }) |
455 | } | 455 | } |
diff --git a/src/dumbyUtils.mjs b/src/dumbyUtils.mjs index 0c05973..02cabca 100644 --- a/src/dumbyUtils.mjs +++ b/src/dumbyUtils.mjs | |||
@@ -238,6 +238,6 @@ export const addGeoLinkByDrag = (container, range, endOfLeaderLine) => { | |||
238 | } | 238 | } |
239 | 239 | ||
240 | link.href = `geo:${marker.dataset.xy.split(',').reverse()}` | 240 | link.href = `geo:${marker.dataset.xy.split(',').reverse()}` |
241 | GeoLink.replaceWith(link) | 241 | GeoLink(link) |
242 | } | 242 | } |
243 | } | 243 | } |
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index 3200a05..f1e971b 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
@@ -6,7 +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 { GeoLink, DocLink, getMarkersFromMaps } from './Link.mjs' |
10 | import * as utils from './dumbyUtils' | 10 | import * as utils from './dumbyUtils' |
11 | import * as menuItem from './MenuItem' | 11 | import * as menuItem from './MenuItem' |
12 | import PlainModal from 'plain-modal' | 12 | import PlainModal from 'plain-modal' |
@@ -246,9 +246,9 @@ export const generateMaps = (container, { | |||
246 | 246 | ||
247 | // Add GeoLinks/DocLinks by pattern | 247 | // Add GeoLinks/DocLinks by pattern |
248 | target.querySelectorAll(geoLinkSelector) | 248 | target.querySelectorAll(geoLinkSelector) |
249 | .forEach(GeoLink.replaceWith) | 249 | .forEach(GeoLink) |
250 | target.querySelectorAll(docLinkSelector) | 250 | target.querySelectorAll(docLinkSelector) |
251 | .forEach(DocLink.replaceWith) | 251 | .forEach(DocLink) |
252 | 252 | ||
253 | // Add GeoLinks from text nodes | 253 | // Add GeoLinks from text nodes |
254 | // const addedNodes = Array.from(mutation.addedNodes) | 254 | // const addedNodes = Array.from(mutation.addedNodes) |
@@ -319,7 +319,7 @@ export const generateMaps = (container, { | |||
319 | values.at(-1) | 319 | values.at(-1) |
320 | .map(utils.setGeoSchemeByCRS(crsString)) | 320 | .map(utils.setGeoSchemeByCRS(crsString)) |
321 | .filter(link => link) | 321 | .filter(link => link) |
322 | .forEach(GeoLink.replaceWith) | 322 | .forEach(GeoLink) |
323 | }) | 323 | }) |
324 | } | 324 | } |
325 | 325 | ||
@@ -557,7 +557,7 @@ export const generateMaps = (container, { | |||
557 | menu.appendChild(new menuItem.Item({ | 557 | menu.appendChild(new menuItem.Item({ |
558 | text: 'Delete', | 558 | text: 'Delete', |
559 | onclick: () => { | 559 | onclick: () => { |
560 | geoLink.getMarkersFromMaps() | 560 | getMarkersFromMaps(geoLink) |
561 | .forEach(m => m.remove()) | 561 | .forEach(m => m.remove()) |
562 | geoLink.replaceWith( | 562 | geoLink.replaceWith( |
563 | document.createTextNode(geoLink.textContent), | 563 | document.createTextNode(geoLink.textContent), |