From 9a0fdb914d50c32f062de58704b40ea1ceb23203 Mon Sep 17 00:00:00 2001 From: Hsieh Chin Fan Date: Fri, 4 Oct 2024 21:17:13 +0800 Subject: refactor: add GeoLinks * In current implementation, marks are generated in the first time link is hovered. This prevent complexity since rendering maps is async. --- src/dumbyUtils.mjs | 153 ++++++++++++++++++++++++++++++++++++++++++----------- src/dumbymap.mjs | 93 ++------------------------------ src/utils.mjs | 38 ++++++++++++- 3 files changed, 161 insertions(+), 123 deletions(-) (limited to 'src') 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 @@ import LeaderLine from 'leader-line' +import { insideWindow, insideParent } from './utils' /** * focusNextMap. * * @param {Boolean} reverse -- focus previous map */ -export function focusNextMap (reverse = false) { +export function focusNextMap(reverse = false) { const renderedList = this.utils.renderedMaps() const index = renderedList.findIndex(e => e.classList.contains('focus')) const nextIndex = (index + (reverse ? -1 : 1)) % renderedList.length @@ -20,7 +21,7 @@ export function focusNextMap (reverse = false) { * * @param {Boolean} reverse -- focus previous block */ -export function focusNextBlock (reverse = false) { +export function focusNextBlock(reverse = false) { const blocks = this.blocks.filter(b => b.checkVisibility({ contentVisibilityAuto: true, @@ -55,7 +56,7 @@ export const scrollToBlock = block => { /** * focusDelay. Delay of throttle, value changes by cases */ -export function focusDelay () { +export function focusDelay() { return window.window.getComputedStyle(this.showcase).display === 'none' ? 50 : 300 } @@ -64,7 +65,7 @@ export function focusDelay () { * * @param {Boolean} reverse -- Switch to previous one */ -export function switchToNextLayout (reverse = false) { +export function switchToNextLayout(reverse = false) { const layouts = this.layouts const currentLayoutName = this.container.getAttribute('data-layout') const currentIndex = layouts.map(l => l.name).indexOf(currentLayoutName) @@ -80,27 +81,68 @@ export function switchToNextLayout (reverse = false) { /** * removeBlockFocus. */ -export function removeBlockFocus () { +export function removeBlockFocus() { this.blocks.forEach(b => b.classList.remove('focus')) } +/** + * getMarkersFromMaps. Get marker elements by GeoLink + * + * @param {HTMLAnchorElement} link + * @return {HTMLElement[]} markers + */ +const getMarkersFromMaps = link => { + const maps = Array.from( + link.closest('.Dumby') + .querySelectorAll('.mapclay[data-render="fulfilled"]') + ) + return maps + .filter(map => link.targets ? link.targets.includes(map.id) : true) + .map(map => { + const renderer = map.renderer + + return map.querySelector(`.marker[title="${link.title}"]`) ?? + renderer.addMarker({ + xy: link.xy, + title: link.title + }) + }) +} + +/** + * addLeaderLine, from link element to target element + * + * @param {HTMLAnchorElement} link + * @param {Element} target + */ +const addLeaderLine = (link, target) => { + const line = new LeaderLine({ + start: link, + end: target, + hide: true, + middleLabel: link.url.searchParams.get('text'), + path: 'magnet' + }) + line.show('draw', { duration: 300 }) + + return line +} + /** * Create geolinks, which points to map by geo schema and id * * @param {HTMLElement} Elements contains anchor elements for doclinks * @returns {Boolean} ture is link is created, false if coordinates are invalid */ -export const createGeoLink = (link, callback = null) => { +export const createGeoLink = (link) => { const url = new URL(link.href) - const xyInParams = url.searchParams.get('xy') - const xy = xyInParams - ? xyInParams.split(',')?.map(Number) - : url?.href - ?.match(/^geo:([0-9.,]+)/) - ?.at(1) - ?.split(',') - ?.reverse() - ?.map(Number) + const xyInParams = url.searchParams.get('xy')?.split(',')?.map(Number) + const xy = xyInParams ?? url?.href + ?.match(/^geo:([0-9.,]+)/) + ?.at(1) + ?.split(',') + ?.reverse() + ?.map(Number) if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false @@ -110,10 +152,23 @@ export const createGeoLink = (link, callback = null) => { link.classList.add('with-leader-line', 'geolink') link.targets = link.url.searchParams.get('id')?.split(',') ?? null - // LeaderLine link.lines = [] - callback?.call(this, link) + // LeaderLine + link.onmouseover = () => { + const anchors = getMarkersFromMaps(link) + anchors + .filter(isAnchorVisible) + .forEach(anchor => { + const line = addLeaderLine(link, anchor) + link.lines.push(line) + }) + } + link.onmouseout = () => removeLeaderLines(link) + link.onclick = (event) => { + event.preventDefault() + getMarkersFromMaps(link).forEach(updateMapCameraByMarker(link.xy)) + } return true } @@ -129,24 +184,60 @@ export const createDocLink = link => { link.onmouseover = () => { const label = decodeURIComponent(link.href.split('#')[1]) const selector = link.title.split('=>')[1] ?? '#' + label - const target = document.querySelector(selector) - if (!target?.checkVisibility()) return - - const line = new LeaderLine({ - start: link, - end: target, - middleLabel: LeaderLine.pathLabel({ - text: label, - fontWeight: 'bold' - }), - hide: true, - path: 'magnet' + const targets = document.querySelectorAll(selector) + + targets.forEach(target => { + if (!target?.checkVisibility()) return + + const line = new LeaderLine({ + start: link, + end: target, + middleLabel: LeaderLine.pathLabel({ + text: label, + fontWeight: 'bold' + }), + hide: true, + path: 'magnet' + }) + link.lines.push(line) + line.show('draw', { duration: 300 }) }) - link.lines.push(line) - line.show('draw', { duration: 300 }) } link.onmouseout = () => { link.lines.forEach(line => line.remove()) link.lines.length = 0 } } + +/** + * removeLeaderLines. clean lines start from link + * + * @param {HTMLAnchorElement} link + */ +const removeLeaderLines = link => { + if (!link.lines) return + link.lines.forEach(line => line.remove()) + link.lines = [] +} + +/** + * updateMapByMarker. get function for updating map camera by marker + * + * @param {Number[]} xy + * @return {Function} function + */ +const updateMapCameraByMarker = xy => marker => { + console.log('update') + const renderer = marker.closest('.mapclay')?.renderer + renderer.updateCamera({ center: xy }, true) +} + +/** + * isAnchorVisible. check anchor(marker) is visible for current map camera + * + * @param {Element} anchor + */ +const isAnchorVisible = anchor => { + const mapContainer = anchor.closest('.mapclay') + return insideWindow(anchor) && insideParent(anchor, mapContainer) +} 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 }) => { utils.createDocLink ) - // Get anchors with "geo:" scheme - htmlHolder.anchors = [] - const geoLinkCallback = link => { - link.onmouseover = () => addLeaderLines(link) - link.onmouseout = () => removeLeaderLines(link) - link.onclick = event => { - event.preventDefault() - htmlHolder.anchors - .filter(isAnchorPointedBy(link)) - .forEach(updateMapByMarker(link.xy)) - // TODO Just hide leader line and show it again - removeLeaderLines(link) - } - } - const geoLinks = Array.from( - container.querySelectorAll(geoLinkSelector) - ).filter(l => utils.createGeoLink(l, geoLinkCallback)) - - const isAnchorPointedBy = link => anchor => { - const mapContainer = anchor.closest('.mapclay') - const isTarget = !link.targets || link.targets.includes(mapContainer.id) - return anchor.title === link.url.searchParams.get('text') && isTarget - } - - const isAnchorVisible = anchor => { - const mapContainer = anchor.closest('.mapclay') - return insideWindow(anchor) && insideParent(anchor, mapContainer) - } - - const drawLeaderLine = link => anchor => { - const line = new LeaderLine({ - start: link, - end: anchor, - hide: true, - middleLabel: link.url.searchParams.get('text'), - path: 'magnet' - }) - line.show('draw', { duration: 300 }) - return line - } - - const addLeaderLines = link => { - link.lines = htmlHolder.anchors - .filter(isAnchorPointedBy(link)) - .filter(isAnchorVisible) - .map(drawLeaderLine(link)) - } + // Add GeoLinks + container.querySelectorAll(geoLinkSelector) + .forEach(utils.createGeoLink) - const removeLeaderLines = link => { - if (!link.lines) return - link.lines.forEach(line => line.remove()) - link.lines = [] - } - - const updateMapByMarker = xy => marker => { - const renderer = marker.closest('.mapclay')?.renderer - renderer.updateCamera({ center: xy }, true) - } - - const insideWindow = element => { - const rect = element.getBoundingClientRect() - return ( - rect.left > 0 && - rect.right < window.innerWidth + rect.width && - rect.top > 0 && - rect.bottom < window.innerHeight + rect.height - ) - } - - const insideParent = (childElement, parentElement) => { - const childRect = childElement.getBoundingClientRect() - const parentRect = parentElement.getBoundingClientRect() - const offset = 20 - - return ( - childRect.left > parentRect.left + offset && - childRect.right < parentRect.right - offset && - childRect.top > parentRect.top + offset && - childRect.bottom < parentRect.bottom - offset - ) - } // }}} // CSS observer {{{ // Focus Map {{{ @@ -378,16 +301,6 @@ export const generateMaps = (container, { delay, mapCallback }) => { // Execute callback from caller mapCallback?.call(this, mapElement) - const markers = geoLinks - .filter(link => !link.targets || link.targets.includes(mapElement.id)) - .map(link => ({ xy: link.xy, title: link.url.searchParams.get('text') })) - - // FIXME Here may cause error - // Add markers with Geolinks - renderer.addMarkers(markers) - mapElement - .querySelectorAll('.marker') - .forEach(marker => htmlHolder.anchors.push(marker)) } // 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 = {}) => { * @param {Number} delay milliseconds * @returns {Any} return value of function call, or null if throttled */ -export function throttle (func, delay) { +export function throttle(func, delay) { let timerFlag = null - return function (...args) { + return function(...args) { const context = this if (timerFlag !== null) return null @@ -99,3 +99,37 @@ export const shiftByWindow = element => { const offsetY = window.innerHeight - rect.top - rect.height element.style.transform = `translate(${offsetX < 0 ? offsetX : 0}px, ${offsetY < 0 ? offsetY : 0}px)` } + +/** + * insideWindow. check DOMRect is inside window + * + * @param {HTMLElement} element + */ +export const insideWindow = element => { + const rect = element.getBoundingClientRect() + return ( + rect.left > 0 && + rect.right < window.innerWidth + rect.width && + rect.top > 0 && + rect.bottom < window.innerHeight + rect.height + ) +} + +/** + * insideParent. check children element is inside DOMRect of parent element + * + * @param {HTMLElement} childElement + * @param {HTMLElement} parentElement + */ +export const insideParent = (childElement, parentElement) => { + const childRect = childElement.getBoundingClientRect() + const parentRect = parentElement.getBoundingClientRect() + const offset = 20 + + return ( + childRect.left > parentRect.left + offset && + childRect.right < parentRect.right - offset && + childRect.top > parentRect.top + offset && + childRect.bottom < parentRect.bottom - offset + ) +} -- cgit v1.2.3-70-g09d2