diff options
author | Hsieh Chin Fan <pham@topo.tw> | 2024-10-04 21:17:13 +0800 |
---|---|---|
committer | Hsieh Chin Fan <pham@topo.tw> | 2024-10-04 21:17:14 +0800 |
commit | 9a0fdb914d50c32f062de58704b40ea1ceb23203 (patch) | |
tree | 32c429bcdefede5a19694d914c5bb9aa588b1ce9 | |
parent | cb0056b995c7e458d11ef363cb5ef452b214e735 (diff) |
refactor: add GeoLinks
* In current implementation, marks are generated in the first time link
is hovered. This prevent complexity since rendering maps is async.
-rw-r--r-- | src/dumbyUtils.mjs | 153 | ||||
-rw-r--r-- | src/dumbymap.mjs | 93 | ||||
-rw-r--r-- | src/utils.mjs | 38 |
3 files changed, 161 insertions, 123 deletions
diff --git a/src/dumbyUtils.mjs b/src/dumbyUtils.mjs index 5182a8f..c7b45f4 100644 --- a/src/dumbyUtils.mjs +++ b/src/dumbyUtils.mjs | |||
@@ -1,11 +1,12 @@ | |||
1 | import LeaderLine from 'leader-line' | 1 | import LeaderLine from 'leader-line' |
2 | import { insideWindow, insideParent } from './utils' | ||
2 | 3 | ||
3 | /** | 4 | /** |
4 | * focusNextMap. | 5 | * focusNextMap. |
5 | * | 6 | * |
6 | * @param {Boolean} reverse -- focus previous map | 7 | * @param {Boolean} reverse -- focus previous map |
7 | */ | 8 | */ |
8 | export function focusNextMap (reverse = false) { | 9 | export function focusNextMap(reverse = false) { |
9 | const renderedList = this.utils.renderedMaps() | 10 | const renderedList = this.utils.renderedMaps() |
10 | const index = renderedList.findIndex(e => e.classList.contains('focus')) | 11 | const index = renderedList.findIndex(e => e.classList.contains('focus')) |
11 | const nextIndex = (index + (reverse ? -1 : 1)) % renderedList.length | 12 | const nextIndex = (index + (reverse ? -1 : 1)) % renderedList.length |
@@ -20,7 +21,7 @@ export function focusNextMap (reverse = false) { | |||
20 | * | 21 | * |
21 | * @param {Boolean} reverse -- focus previous block | 22 | * @param {Boolean} reverse -- focus previous block |
22 | */ | 23 | */ |
23 | export function focusNextBlock (reverse = false) { | 24 | export function focusNextBlock(reverse = false) { |
24 | const blocks = this.blocks.filter(b => | 25 | const blocks = this.blocks.filter(b => |
25 | b.checkVisibility({ | 26 | b.checkVisibility({ |
26 | contentVisibilityAuto: true, | 27 | contentVisibilityAuto: true, |
@@ -55,7 +56,7 @@ export const scrollToBlock = block => { | |||
55 | /** | 56 | /** |
56 | * focusDelay. Delay of throttle, value changes by cases | 57 | * focusDelay. Delay of throttle, value changes by cases |
57 | */ | 58 | */ |
58 | export function focusDelay () { | 59 | export function focusDelay() { |
59 | return window.window.getComputedStyle(this.showcase).display === 'none' ? 50 : 300 | 60 | return window.window.getComputedStyle(this.showcase).display === 'none' ? 50 : 300 |
60 | } | 61 | } |
61 | 62 | ||
@@ -64,7 +65,7 @@ export function focusDelay () { | |||
64 | * | 65 | * |
65 | * @param {Boolean} reverse -- Switch to previous one | 66 | * @param {Boolean} reverse -- Switch to previous one |
66 | */ | 67 | */ |
67 | export function switchToNextLayout (reverse = false) { | 68 | export function switchToNextLayout(reverse = false) { |
68 | const layouts = this.layouts | 69 | const layouts = this.layouts |
69 | const currentLayoutName = this.container.getAttribute('data-layout') | 70 | const currentLayoutName = this.container.getAttribute('data-layout') |
70 | const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName) | 71 | const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName) |
@@ -80,27 +81,68 @@ export function switchToNextLayout (reverse = false) { | |||
80 | /** | 81 | /** |
81 | * removeBlockFocus. | 82 | * removeBlockFocus. |
82 | */ | 83 | */ |
83 | export function removeBlockFocus () { | 84 | export function removeBlockFocus() { |
84 | this.blocks.forEach(b => b.classList.remove('focus')) | 85 | this.blocks.forEach(b => b.classList.remove('focus')) |
85 | } | 86 | } |
86 | 87 | ||
87 | /** | 88 | /** |
89 | * getMarkersFromMaps. Get marker elements by GeoLink | ||
90 | * | ||
91 | * @param {HTMLAnchorElement} link | ||
92 | * @return {HTMLElement[]} markers | ||
93 | */ | ||
94 | const getMarkersFromMaps = link => { | ||
95 | const maps = Array.from( | ||
96 | link.closest('.Dumby') | ||
97 | .querySelectorAll('.mapclay[data-render="fulfilled"]') | ||
98 | ) | ||
99 | return maps | ||
100 | .filter(map => link.targets ? link.targets.includes(map.id) : true) | ||
101 | .map(map => { | ||
102 | const renderer = map.renderer | ||
103 | |||
104 | return map.querySelector(`.marker[title="${link.title}"]`) ?? | ||
105 | renderer.addMarker({ | ||
106 | xy: link.xy, | ||
107 | title: link.title | ||
108 | }) | ||
109 | }) | ||
110 | } | ||
111 | |||
112 | /** | ||
113 | * addLeaderLine, from link element to target element | ||
114 | * | ||
115 | * @param {HTMLAnchorElement} link | ||
116 | * @param {Element} target | ||
117 | */ | ||
118 | const addLeaderLine = (link, target) => { | ||
119 | const line = new LeaderLine({ | ||
120 | start: link, | ||
121 | end: target, | ||
122 | hide: true, | ||
123 | middleLabel: link.url.searchParams.get('text'), | ||
124 | path: 'magnet' | ||
125 | }) | ||
126 | line.show('draw', { duration: 300 }) | ||
127 | |||
128 | return line | ||
129 | } | ||
130 | |||
131 | /** | ||
88 | * Create geolinks, which points to map by geo schema and id | 132 | * Create geolinks, which points to map by geo schema and id |
89 | * | 133 | * |
90 | * @param {HTMLElement} Elements contains anchor elements for doclinks | 134 | * @param {HTMLElement} Elements contains anchor elements for doclinks |
91 | * @returns {Boolean} ture is link is created, false if coordinates are invalid | 135 | * @returns {Boolean} ture is link is created, false if coordinates are invalid |
92 | */ | 136 | */ |
93 | export const createGeoLink = (link, callback = null) => { | 137 | export const createGeoLink = (link) => { |
94 | const url = new URL(link.href) | 138 | const url = new URL(link.href) |
95 | const xyInParams = url.searchParams.get('xy') | 139 | const xyInParams = url.searchParams.get('xy')?.split(',')?.map(Number) |
96 | const xy = xyInParams | 140 | const xy = xyInParams ?? url?.href |
97 | ? xyInParams.split(',')?.map(Number) | 141 | ?.match(/^geo:([0-9.,]+)/) |
98 | : url?.href | 142 | ?.at(1) |
99 | ?.match(/^geo:([0-9.,]+)/) | 143 | ?.split(',') |
100 | ?.at(1) | 144 | ?.reverse() |
101 | ?.split(',') | 145 | ?.map(Number) |
102 | ?.reverse() | ||
103 | ?.map(Number) | ||
104 | 146 | ||
105 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false | 147 | if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false |
106 | 148 | ||
@@ -110,10 +152,23 @@ export const createGeoLink = (link, callback = null) => { | |||
110 | link.classList.add('with-leader-line', 'geolink') | 152 | link.classList.add('with-leader-line', 'geolink') |
111 | link.targets = link.url.searchParams.get('id')?.split(',') ?? null | 153 | link.targets = link.url.searchParams.get('id')?.split(',') ?? null |
112 | 154 | ||
113 | // LeaderLine | ||
114 | link.lines = [] | 155 | link.lines = [] |
115 | callback?.call(this, link) | ||
116 | 156 | ||
157 | // LeaderLine | ||
158 | link.onmouseover = () => { | ||
159 | const anchors = getMarkersFromMaps(link) | ||
160 | anchors | ||
161 | .filter(isAnchorVisible) | ||
162 | .forEach(anchor => { | ||
163 | const line = addLeaderLine(link, anchor) | ||
164 | link.lines.push(line) | ||
165 | }) | ||
166 | } | ||
167 | link.onmouseout = () => removeLeaderLines(link) | ||
168 | link.onclick = (event) => { | ||
169 | event.preventDefault() | ||
170 | getMarkersFromMaps(link).forEach(updateMapCameraByMarker(link.xy)) | ||
171 | } | ||
117 | return true | 172 | return true |
118 | } | 173 | } |
119 | 174 | ||
@@ -129,24 +184,60 @@ export const createDocLink = link => { | |||
129 | link.onmouseover = () => { | 184 | link.onmouseover = () => { |
130 | const label = decodeURIComponent(link.href.split('#')[1]) | 185 | const label = decodeURIComponent(link.href.split('#')[1]) |
131 | const selector = link.title.split('=>')[1] ?? '#' + label | 186 | const selector = link.title.split('=>')[1] ?? '#' + label |
132 | const target = document.querySelector(selector) | 187 | const targets = document.querySelectorAll(selector) |
133 | if (!target?.checkVisibility()) return | 188 | |
134 | 189 | targets.forEach(target => { | |
135 | const line = new LeaderLine({ | 190 | if (!target?.checkVisibility()) return |
136 | start: link, | 191 | |
137 | end: target, | 192 | const line = new LeaderLine({ |
138 | middleLabel: LeaderLine.pathLabel({ | 193 | start: link, |
139 | text: label, | 194 | end: target, |
140 | fontWeight: 'bold' | 195 | middleLabel: LeaderLine.pathLabel({ |
141 | }), | 196 | text: label, |
142 | hide: true, | 197 | fontWeight: 'bold' |
143 | path: 'magnet' | 198 | }), |
199 | hide: true, | ||
200 | path: 'magnet' | ||
201 | }) | ||
202 | link.lines.push(line) | ||
203 | line.show('draw', { duration: 300 }) | ||
144 | }) | 204 | }) |
145 | link.lines.push(line) | ||
146 | line.show('draw', { duration: 300 }) | ||
147 | } | 205 | } |
148 | link.onmouseout = () => { | 206 | link.onmouseout = () => { |
149 | link.lines.forEach(line => line.remove()) | 207 | link.lines.forEach(line => line.remove()) |
150 | link.lines.length = 0 | 208 | link.lines.length = 0 |
151 | } | 209 | } |
152 | } | 210 | } |
211 | |||
212 | /** | ||
213 | * removeLeaderLines. clean lines start from link | ||
214 | * | ||
215 | * @param {HTMLAnchorElement} link | ||
216 | */ | ||
217 | const removeLeaderLines = link => { | ||
218 | if (!link.lines) return | ||
219 | link.lines.forEach(line => line.remove()) | ||
220 | link.lines = [] | ||
221 | } | ||
222 | |||
223 | /** | ||
224 | * updateMapByMarker. get function for updating map camera by marker | ||
225 | * | ||
226 | * @param {Number[]} xy | ||
227 | * @return {Function} function | ||
228 | */ | ||
229 | const updateMapCameraByMarker = xy => marker => { | ||
230 | console.log('update') | ||
231 | const renderer = marker.closest('.mapclay')?.renderer | ||
232 | renderer.updateCamera({ center: xy }, true) | ||
233 | } | ||
234 | |||
235 | /** | ||
236 | * isAnchorVisible. check anchor(marker) is visible for current map camera | ||
237 | * | ||
238 | * @param {Element} anchor | ||
239 | */ | ||
240 | const isAnchorVisible = anchor => { | ||
241 | const mapContainer = anchor.closest('.mapclay') | ||
242 | return insideWindow(anchor) && insideParent(anchor, mapContainer) | ||
243 | } | ||
diff --git a/src/dumbymap.mjs b/src/dumbymap.mjs index edc1521..bb98cea 100644 --- a/src/dumbymap.mjs +++ b/src/dumbymap.mjs | |||
@@ -152,87 +152,10 @@ export const generateMaps = (container, { delay, mapCallback }) => { | |||
152 | utils.createDocLink | 152 | utils.createDocLink |
153 | ) | 153 | ) |
154 | 154 | ||
155 | // Get anchors with "geo:" scheme | 155 | // Add GeoLinks |
156 | htmlHolder.anchors = [] | 156 | container.querySelectorAll(geoLinkSelector) |
157 | const geoLinkCallback = link => { | 157 | .forEach(utils.createGeoLink) |
158 | link.onmouseover = () => addLeaderLines(link) | ||
159 | link.onmouseout = () => removeLeaderLines(link) | ||
160 | link.onclick = event => { | ||
161 | event.preventDefault() | ||
162 | htmlHolder.anchors | ||
163 | .filter(isAnchorPointedBy(link)) | ||
164 | .forEach(updateMapByMarker(link.xy)) | ||
165 | // TODO Just hide leader line and show it again | ||
166 | removeLeaderLines(link) | ||
167 | } | ||
168 | } | ||
169 | const geoLinks = Array.from( | ||
170 | container.querySelectorAll(geoLinkSelector) | ||
171 | ).filter(l => utils.createGeoLink(l, geoLinkCallback)) | ||
172 | |||
173 | const isAnchorPointedBy = link => anchor => { | ||
174 | const mapContainer = anchor.closest('.mapclay') | ||
175 | const isTarget = !link.targets || link.targets.includes(mapContainer.id) | ||
176 | return anchor.title === link.url.searchParams.get('text') && isTarget | ||
177 | } | ||
178 | |||
179 | const isAnchorVisible = anchor => { | ||
180 | const mapContainer = anchor.closest('.mapclay') | ||
181 | return insideWindow(anchor) && insideParent(anchor, mapContainer) | ||
182 | } | ||
183 | |||
184 | const drawLeaderLine = link => anchor => { | ||
185 | const line = new LeaderLine({ | ||
186 | start: link, | ||
187 | end: anchor, | ||
188 | hide: true, | ||
189 | middleLabel: link.url.searchParams.get('text'), | ||
190 | path: 'magnet' | ||
191 | }) | ||
192 | line.show('draw', { duration: 300 }) | ||
193 | return line | ||
194 | } | ||
195 | |||
196 | const addLeaderLines = link => { | ||
197 | link.lines = htmlHolder.anchors | ||
198 | .filter(isAnchorPointedBy(link)) | ||
199 | .filter(isAnchorVisible) | ||
200 | .map(drawLeaderLine(link)) | ||
201 | } | ||
202 | 158 | ||
203 | const removeLeaderLines = link => { | ||
204 | if (!link.lines) return | ||
205 | link.lines.forEach(line => line.remove()) | ||
206 | link.lines = [] | ||
207 | } | ||
208 | |||
209 | const updateMapByMarker = xy => marker => { | ||
210 | const renderer = marker.closest('.mapclay')?.renderer | ||
211 | renderer.updateCamera({ center: xy }, true) | ||
212 | } | ||
213 | |||
214 | const insideWindow = element => { | ||
215 | const rect = element.getBoundingClientRect() | ||
216 | return ( | ||
217 | rect.left > 0 && | ||
218 | rect.right < window.innerWidth + rect.width && | ||
219 | rect.top > 0 && | ||
220 | rect.bottom < window.innerHeight + rect.height | ||
221 | ) | ||
222 | } | ||
223 | |||
224 | const insideParent = (childElement, parentElement) => { | ||
225 | const childRect = childElement.getBoundingClientRect() | ||
226 | const parentRect = parentElement.getBoundingClientRect() | ||
227 | const offset = 20 | ||
228 | |||
229 | return ( | ||
230 | childRect.left > parentRect.left + offset && | ||
231 | childRect.right < parentRect.right - offset && | ||
232 | childRect.top > parentRect.top + offset && | ||
233 | childRect.bottom < parentRect.bottom - offset | ||
234 | ) | ||
235 | } | ||
236 | // }}} | 159 | // }}} |
237 | // CSS observer {{{ | 160 | // CSS observer {{{ |
238 | // Focus Map {{{ | 161 | // Focus Map {{{ |
@@ -378,16 +301,6 @@ export const generateMaps = (container, { delay, mapCallback }) => { | |||
378 | 301 | ||
379 | // Execute callback from caller | 302 | // Execute callback from caller |
380 | mapCallback?.call(this, mapElement) | 303 | mapCallback?.call(this, mapElement) |
381 | const markers = geoLinks | ||
382 | .filter(link => !link.targets || link.targets.includes(mapElement.id)) | ||
383 | .map(link => ({ xy: link.xy, title: link.url.searchParams.get('text') })) | ||
384 | |||
385 | // FIXME Here may cause error | ||
386 | // Add markers with Geolinks | ||
387 | renderer.addMarkers(markers) | ||
388 | mapElement | ||
389 | .querySelectorAll('.marker') | ||
390 | .forEach(marker => htmlHolder.anchors.push(marker)) | ||
391 | } | 304 | } |
392 | 305 | ||
393 | // Set unique ID for map container | 306 | // Set unique ID for map container |
diff --git a/src/utils.mjs b/src/utils.mjs index ffd8978..4eedf82 100644 --- a/src/utils.mjs +++ b/src/utils.mjs | |||
@@ -72,10 +72,10 @@ export const animateRectTransition = (element, rect, options = {}) => { | |||
72 | * @param {Number} delay milliseconds | 72 | * @param {Number} delay milliseconds |
73 | * @returns {Any} return value of function call, or null if throttled | 73 | * @returns {Any} return value of function call, or null if throttled |
74 | */ | 74 | */ |
75 | export function throttle (func, delay) { | 75 | export function throttle(func, delay) { |
76 | let timerFlag = null | 76 | let timerFlag = null |
77 | 77 | ||
78 | return function (...args) { | 78 | return function(...args) { |
79 | const context = this | 79 | const context = this |
80 | if (timerFlag !== null) return null | 80 | if (timerFlag !== null) return null |
81 | 81 | ||
@@ -99,3 +99,37 @@ export const shiftByWindow = element => { | |||
99 | const offsetY = window.innerHeight - rect.top - rect.height | 99 | const offsetY = window.innerHeight - rect.top - rect.height |
100 | element.style.transform = `translate(${offsetX < 0 ? offsetX : 0}px, ${offsetY < 0 ? offsetY : 0}px)` | 100 | element.style.transform = `translate(${offsetX < 0 ? offsetX : 0}px, ${offsetY < 0 ? offsetY : 0}px)` |
101 | } | 101 | } |
102 | |||
103 | /** | ||
104 | * insideWindow. check DOMRect is inside window | ||
105 | * | ||
106 | * @param {HTMLElement} element | ||
107 | */ | ||
108 | export const insideWindow = element => { | ||
109 | const rect = element.getBoundingClientRect() | ||
110 | return ( | ||
111 | rect.left > 0 && | ||
112 | rect.right < window.innerWidth + rect.width && | ||
113 | rect.top > 0 && | ||
114 | rect.bottom < window.innerHeight + rect.height | ||
115 | ) | ||
116 | } | ||
117 | |||
118 | /** | ||
119 | * insideParent. check children element is inside DOMRect of parent element | ||
120 | * | ||
121 | * @param {HTMLElement} childElement | ||
122 | * @param {HTMLElement} parentElement | ||
123 | */ | ||
124 | export const insideParent = (childElement, parentElement) => { | ||
125 | const childRect = childElement.getBoundingClientRect() | ||
126 | const parentRect = parentElement.getBoundingClientRect() | ||
127 | const offset = 20 | ||
128 | |||
129 | return ( | ||
130 | childRect.left > parentRect.left + offset && | ||
131 | childRect.right < parentRect.right - offset && | ||
132 | childRect.top > parentRect.top + offset && | ||
133 | childRect.bottom < parentRect.bottom - offset | ||
134 | ) | ||
135 | } | ||