From 2a2acc8e31aef538a8e68e6b53cacafb38841c26 Mon Sep 17 00:00:00 2001 From: Hsieh Chin Fan Date: Fri, 25 Oct 2024 23:46:30 +0800 Subject: 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 --- src/Link.mjs | 366 ++++++++++++++++++++++++++--------------------------- src/MenuItem.mjs | 6 +- src/dumbyUtils.mjs | 2 +- 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' import { insideWindow, insideParent } from './utils' import * as markers from './marker.mjs' +/** + * GeoLink: anchor element with geo scheme and properties about maps + * @typedef {Object} GeoLink + * @extends HTMLAnchorElement + * @property {string[]} targets - ids of target map elements + * @property {LeaderLine[]} lines + * @property {Object} dataset + * @property {string} dataset.lon - longitude string of geo scheme + * @property {string} dataset.lat - latitude string of geo scheme + * @property {string} dataset.crs - short name of CRS in EPSG/ESRI format + */ + +/** + * DocLink: anchor element which points to DOM node by filter + * @typedef {Object} DocLink + * @extends HTMLAnchorElement + * @property {LeaderLine[]} lines + */ + /** VAR: pattern for coodinates */ export const coordPattern = /^geo:([-]?[0-9.]+),([-]?[0-9.]+)/ /** - * Class: GeoLink - link for maps - * - * @extends {window.HTMLAnchorElement} + * GeoLink: append GeoLink features onto anchor element + * @param {HTMLAnchorElement} link + * @return {GeoLink} */ -export class GeoLink extends window.HTMLAnchorElement { - static replaceWith = (link) => - link.replaceWith(new GeoLink(link)) - - /** - * Creates a new GeoLink instance - * - * @param {HTMLAnchorElement} link - */ - constructor (link) { - super() - this.innerHTML = link.innerHTML - this.href = link.href - - const url = new URL(link.href) - const params = new URLSearchParams(link.search) - const xyInParams = params.get('xy')?.split(',')?.map(Number) - const [lon, lat] = url.href - ?.match(coordPattern) - ?.slice(1) - ?.reverse() - ?.map(Number) - const xy = xyInParams ?? [lon, lat] - - if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false - - // Geo information in link - this.dataset.lon = lon - this.dataset.lat = lat - this.dataset.crs = params.get('crs') - this.classList.add('with-leader-line', 'geolink') - this.classList.remove('not-geolink') - // TODO refactor as data attribute - this.targets = params.get('id')?.split(',') ?? null - this.title = 'Left-Click to move Camera, Middle-Click to clean anchor' - this.lines = [] - - // Hover link for LeaderLine - this.onmouseover = () => this.getMarkersFromMaps() - .filter(isAnchorVisible) - .forEach(anchor => { - const labelText = new URL(this).searchParams.get('text') ?? this.textContent - const line = new LeaderLine({ - start: this, - end: anchor, - hide: true, - middleLabel: labelText, - path: 'magnet', - }) - line.show('draw', { duration: 300 }) - - this.lines.push(line) - }) +export const GeoLink = (link) => { + const url = new URL(link.href) + const params = new URLSearchParams(link.search) + const xyInParams = params.get('xy')?.split(',')?.map(Number) + const [lon, lat] = url.href + ?.match(coordPattern) + ?.slice(1) + ?.reverse() + ?.map(Number) + const xy = xyInParams ?? [lon, lat] + + if (!xy || isNaN(xy[0]) || isNaN(xy[1])) return false + + // Geo information in link + link.dataset.lon = lon + link.dataset.lat = lat + link.dataset.crs = params.get('crs') + link.classList.add('with-leader-line', 'geolink') + link.classList.remove('not-geolink') + // TODO refactor as data attribute + link.title = 'Left-Click to move Camera, Middle-Click to clean anchor' + link.targets = params.get('id')?.split(',') ?? null + link.lines = [] - this.onmouseout = () => removeLeaderLines(this) - - // Click to move camera - this.onclick = (event) => { - event.preventDefault() - removeLeaderLines(this) - this.getMarkersFromMaps().forEach(marker => { - const map = marker.closest('.mapclay') - map.scrollIntoView({ behavior: 'smooth' }) - updateMapCameraByMarker([ - Number(this.dataset.lon), - Number(this.dataset.lat), - ])(marker) + // Hover link for LeaderLine + link.onmouseover = () => getMarkersFromMaps(link) + .filter(isAnchorVisible) + .forEach(anchor => { + const labelText = new URL(link).searchParams.get('text') ?? link.textContent + const line = new LeaderLine({ + start: link, + end: anchor, + hide: true, + middleLabel: labelText, + path: 'magnet', }) - } - - // Use middle click to remove markers - this.onauxclick = (e) => { - if (e.which !== 2) return - e.preventDefault() - removeLeaderLines(this) - this.getMarkersFromMaps() - .forEach(marker => marker.remove()) - } + line.show('draw', { duration: 300 }) + + link.lines.push(line) + }) + + link.onmouseout = () => removeLeaderLines(link) + + // Click to move camera + link.onclick = (event) => { + event.preventDefault() + removeLeaderLines(link) + getMarkersFromMaps(link).forEach(marker => { + const map = marker.closest('.mapclay') + map.scrollIntoView({ behavior: 'smooth' }) + updateMapCameraByMarker([ + Number(link.dataset.lon), + Number(link.dataset.lat), + ])(marker) + }) } - /** - * getMarkersFromMaps. Get marker elements by GeoLink - * - * @param {HTMLAnchorElement} link - * @return {HTMLElement[]} markers - */ - getMarkersFromMaps () { - const params = new URLSearchParams(this.search) - const maps = Array.from( - this.closest('.Dumby') - .querySelectorAll('.mapclay[data-render="fulfilled"]'), - ) - return maps - .filter(map => this.targets ? this.targets.includes(map.id) : true) - .map(map => { - const renderer = map.renderer - const lonLat = [Number(this.dataset.lon), Number(this.dataset.lat)] - const type = params.get('type') ?? 'pin' - const svg = markers[type] - const element = document.createElement('div') - element.style.cssText = `width: ${svg.size[0]}px; height: ${svg.size[1]}px;` - element.innerHTML = svg.html - - const marker = map.querySelector(`.marker[data-xy="${lonLat}"]`) ?? - renderer.addMarker({ - xy: lonLat, - element, - type, - anchor: svg.anchor, - size: svg.size, - }) - marker.dataset.xy = lonLat - marker.title = new URLSearchParams(this.search).get('xy') ?? lonLat - const crs = this.dataset.crs - if (crs && crs !== 'EPSG:4326') { - marker.title += '@' + this.dataset.crs - } - - return marker - }) + // Use middle click to remove markers + link.onauxclick = (e) => { + if (e.which !== 2) return + e.preventDefault() + removeLeaderLines(link) + getMarkersFromMaps(link) + .forEach(marker => marker.remove()) } -} -if (!window.customElements.get('dumby-geolink')) { - window.customElements.define('dumby-geolink', GeoLink, { extends: 'a' }) + + return link } /** - * Class: DocLink - link for DOM + * GeoLink: getMarkersFromMaps. Get marker elements by GeoLink * - * @extends {window.HTMLAnchorElement} + * @param {GeoLink} link + * @return {HTMLElement[]} markers */ -export class DocLink extends window.HTMLAnchorElement { - static replaceWith = (link) => - link.replaceWith(new DocLink(link)) - - /** - * Creates a new DocLink instance - * - * @param {HTMLAnchorElement} link - */ - constructor (link) { - super() - this.innerHTML = link.innerHTML - this.href = link.href - - const label = decodeURIComponent(link.href.split('#')[1]) - const selector = link.title.split('=>')[1] ?? (label ? '#' + label : null) - if (!selector) return false - - this.classList.add('with-leader-line', 'doclink') - this.lines = [] - - this.onmouseover = () => { - const targets = document.querySelectorAll(selector) - - targets.forEach(target => { - if (!target?.checkVisibility()) return - - // highlight selected target - target.dataset.style = target.style.cssText - const rect = target.getBoundingClientRect() - const isTiny = rect.width < 100 || rect.height < 100 - if (isTiny) { - target.style.background = 'lightPink' - } else { - target.style.outline = 'lightPink 6px dashed' - } - - // point to selected target - const line = new LeaderLine({ - start: this, - end: target, - middleLabel: LeaderLine.pathLabel({ - text: label, - fontWeight: 'bold', - }), - hide: true, - path: 'magnet', +export const getMarkersFromMaps = (link) => { + const params = new URLSearchParams(link.search) + 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 + const lonLat = [Number(link.dataset.lon), Number(link.dataset.lat)] + const type = params.get('type') ?? 'pin' + const svg = markers[type] + const element = document.createElement('div') + element.style.cssText = `width: ${svg.size[0]}px; height: ${svg.size[1]}px;` + element.innerHTML = svg.html + + const marker = map.querySelector(`.marker[data-xy="${lonLat}"]`) ?? + renderer.addMarker({ + xy: lonLat, + element, + // FIXME In conten script, leaflet cannot render marker with HTMLElement + // so pass html string here + html: svg.html, + type, + anchor: svg.anchor, + size: svg.size, }) - this.lines.push(line) - line.show('draw', { duration: 300 }) - }) - } + marker.dataset.xy = lonLat + marker.title = new URLSearchParams(link.search).get('xy') ?? lonLat + const crs = link.dataset.crs + if (crs && crs !== 'EPSG:4326') { + marker.title += '@' + link.dataset.crs + } + + return marker + }) +} + +/** + * DocLink: append DocLink features onto anchor element + * @param {HTMLAnchorElement} link + * @return {DocLink} + */ +export const DocLink = (link) => { + const label = decodeURIComponent(link.href.split('#')[1]) + const selector = link.title.split('=>')[1] ?? (label ? '#' + label : null) + if (!selector) return false - this.onmouseout = () => { - removeLeaderLines(this) + link.classList.add('with-leader-line', 'doclink') + link.lines = [] - // resume targets from highlight - const targets = document.querySelectorAll(selector) - targets.forEach(target => { - target.style.cssText = target.dataset.style - delete target.dataset.style + link.onmouseover = () => { + const targets = document.querySelectorAll(selector) + + targets.forEach(target => { + if (!target?.checkVisibility()) return + + // highlight selected target + target.dataset.style = target.style.cssText + const rect = target.getBoundingClientRect() + const isTiny = rect.width < 100 || rect.height < 100 + if (isTiny) { + target.style.background = 'lightPink' + } else { + target.style.outline = 'lightPink 6px dashed' + } + + // point to selected target + 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 }) + }) } -} -if (!window.customElements.get('dumby-doclink')) { - window.customElements.define('dumby-doclink', DocLink, { extends: 'a' }) + + link.onmouseout = () => { + removeLeaderLines(link) + + // resume targets from highlight + const targets = document.querySelectorAll(selector) + targets.forEach(target => { + target.style.cssText = target.dataset.style + delete target.dataset.style + }) + } + + return link } /** @@ -234,7 +226,7 @@ const updateMapCameraByMarker = lonLat => marker => { /** * removeLeaderLines. clean lines start from link * - * @param {HTMLAnchorElement} link + * @param {GeoLink} link */ export const removeLeaderLines = link => { 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 @@ import { shiftByWindow } from './utils.mjs' /* eslint-disable no-unused-vars */ -import { GeoLink, removeLeaderLines } from './Link.mjs' +import { GeoLink, getMarkersFromMaps, removeLeaderLines } from './Link.mjs' /* eslint-enable */ import * as markers from './marker.mjs' @@ -447,9 +447,9 @@ export const setGeoLinkTypeItem = ({ link, type, ...others }) => { params.set('type', type) link.search = params removeLeaderLines(link) - link.getMarkersFromMaps() + getMarkersFromMaps(link) .forEach(marker => marker.remove()) - link.getMarkersFromMaps() + getMarkersFromMaps(link) }, }) } 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) => { } link.href = `geo:${marker.dataset.xy.split(',').reverse()}` - GeoLink.replaceWith(link) + GeoLink(link) } } 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' import * as mapclay from 'mapclay' import { onRemove, animateRectTransition, throttle, debounce, shiftByWindow } from './utils' import { Layout, SideBySide, Overlay, Sticky } from './Layout' -import { GeoLink, DocLink } from './Link.mjs' +import { GeoLink, DocLink, getMarkersFromMaps } from './Link.mjs' import * as utils from './dumbyUtils' import * as menuItem from './MenuItem' import PlainModal from 'plain-modal' @@ -246,9 +246,9 @@ export const generateMaps = (container, { // Add GeoLinks/DocLinks by pattern target.querySelectorAll(geoLinkSelector) - .forEach(GeoLink.replaceWith) + .forEach(GeoLink) target.querySelectorAll(docLinkSelector) - .forEach(DocLink.replaceWith) + .forEach(DocLink) // Add GeoLinks from text nodes // const addedNodes = Array.from(mutation.addedNodes) @@ -319,7 +319,7 @@ export const generateMaps = (container, { values.at(-1) .map(utils.setGeoSchemeByCRS(crsString)) .filter(link => link) - .forEach(GeoLink.replaceWith) + .forEach(GeoLink) }) } @@ -557,7 +557,7 @@ export const generateMaps = (container, { menu.appendChild(new menuItem.Item({ text: 'Delete', onclick: () => { - geoLink.getMarkersFromMaps() + getMarkersFromMaps(geoLink) .forEach(m => m.remove()) geoLink.replaceWith( document.createTextNode(geoLink.textContent), -- cgit v1.2.3-70-g09d2