Skip to content
141 changes: 57 additions & 84 deletions src/map-feature.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export class MapFeature extends HTMLElement {
static get observedAttributes() {
return ['zoom', 'onfocus', 'onclick', 'onblur'];
return ['zoom', 'min', 'max'];
}

get zoom() {
Expand Down Expand Up @@ -88,15 +88,6 @@ export class MapFeature extends HTMLElement {
}
break;
}
case 'onfocus':
case 'onclick':
case 'onblur':
if (this._groupEl) {
// "synchronize" the onevent properties (i.e. onfocus, onclick, onblur)
// between the mapFeature and its associated <g> element
this._groupEl[name] = this[name].bind(this._groupEl);
break;
}
}
}

Expand Down Expand Up @@ -183,15 +174,15 @@ export class MapFeature extends HTMLElement {
}

_addFeature() {
let parentEl =
this._parentEl =
this.parentNode.nodeName.toUpperCase() === 'LAYER-' ||
this.parentNode.nodeName.toUpperCase() === 'MAP-EXTENT'
? this.parentNode
: this.parentNode.host;

// arrow function is not hoisted, define before use
var _attachedToMap = (e) => {
if (!parentEl._layer._map) {
if (!this._parentEl._layer._map) {
// if the parent layer- el has not yet added to the map (i.e. not yet rendered), wait until it is added
this._layer.once(
'attached',
Expand Down Expand Up @@ -229,21 +220,21 @@ export class MapFeature extends HTMLElement {
}
};

if (!parentEl._layer) {
if (!this._parentEl._layer) {
// for custom projection cases, the MapMLLayer has not yet created and binded with the layer- at this point,
// because the "createMap" event of mapml-viewer has not yet been dispatched, the map has not yet been created
// the event will be dispatched after defineCustomProjection > projection setter
// should wait until MapMLLayer is built
let parentLayer =
parentEl.nodeName.toUpperCase() === 'LAYER-'
? parentEl
: parentEl.parentElement || parentEl.parentNode.host;
this._parentEl.nodeName.toUpperCase() === 'LAYER-'
? this._parentEl
: this._parentEl.parentElement || this._parentEl.parentNode.host;
parentLayer.parentNode.addEventListener('createmap', (e) => {
this._layer = parentLayer._layer;
_attachedToMap();
});
} else {
this._layer = parentEl._layer;
this._layer = this._parentEl._layer;
_attachedToMap();
}
}
Expand Down Expand Up @@ -274,28 +265,25 @@ export class MapFeature extends HTMLElement {
}

_setUpEvents() {
['click', 'focus', 'blur'].forEach((name) => {
// onevent properties & onevent attributes
if (this[`on${name}`] && typeof this[`on${name}`] === 'function') {
this._groupEl[`on${name}`] = this[`on${name}`];
}
// handle event handlers set via addEventlistener
// for HTMLElement
['click', 'focus', 'blur', 'keyup', 'keydown'].forEach((name) => {
// when <g> is clicked / focused / blurred
// should dispatch the click / focus / blur event listener on **linked HTMLFeatureElements**
this._groupEl.addEventListener(name, (e) => {
// this === mapFeature as arrow function does not have their own "this" pointer
// store onEvent handler of mapFeature if there is any to ensure that it will not be re-triggered when the cloned mouseevent is dispatched
// so that only the event handlers set on HTMLFeatureElement via addEventListener method will be triggered
const handler = this[`on${name}`]; // a deep copy, var handler will not change when this.onevent is set to null (i.e. store the onevent property)
this[`on${name}`] = null;
if (name === 'click') {
// dispatch a cloned mouseevent to trigger the click event handlers set on HTMLFeatureElement
this.dispatchEvent(new PointerEvent(name, { ...e }));
let clickEv = new PointerEvent(name, { cancelable: true });
clickEv.originalEvent = e;
this.dispatchEvent(clickEv);
} else if (name === 'keyup' || name === 'keydown') {
let keyEv = new KeyboardEvent(name, { cancelable: true });
keyEv.originalEvent = e;
this.dispatchEvent(keyEv);
} else {
this.dispatchEvent(new FocusEvent(name, { ...e }));
// dispatch a cloned focusevent to trigger the focus/blue event handlers set on HTMLFeatureElement
let focusEv = new FocusEvent(name, { cancelable: true });
focusEv.originalEvent = e;
this.dispatchEvent(focusEv);
}
this[`on${name}`] = handler;
});
});
}
Expand Down Expand Up @@ -324,9 +312,7 @@ export class MapFeature extends HTMLElement {
return this._layer._mapmlvectors._getNativeVariables(content);
} else if (content.nodeName.toUpperCase() === 'LAYER-') {
// for inline features, read native zoom and cs from inline map-meta
let zoomMeta = this.parentElement.querySelectorAll(
'map-meta[name=zoom]'
),
let zoomMeta = this._parentEl.querySelectorAll('map-meta[name=zoom]'),
zoomLength = zoomMeta?.length;
nativeZoom = zoomLength
? +zoomMeta[zoomLength - 1]
Expand All @@ -336,7 +322,7 @@ export class MapFeature extends HTMLElement {
?.split('=')[1]
: 0;

let csMeta = this.parentElement.querySelectorAll('map-meta[name=cs]'),
let csMeta = this._parentEl.querySelectorAll('map-meta[name=cs]'),
csLength = csMeta?.length;
nativeCS = csLength
? csMeta[csLength - 1].getAttribute('content')
Expand Down Expand Up @@ -567,68 +553,55 @@ export class MapFeature extends HTMLElement {
}

// a method that simulates a click, or invoking the user-defined click event
// event (optional): a MouseEvent object, can be passed as an argument of the user-defined click event handlers
click(event) {
click() {
let g = this._groupEl,
rect = g.getBoundingClientRect();
if (!event) {
event = new MouseEvent('click', {
clientX: rect.x + rect.width / 2,
clientY: rect.y + rect.height / 2,
button: 0
});
let event = new MouseEvent('click', {
clientX: rect.x + rect.width / 2,
clientY: rect.y + rect.height / 2,
button: 0
});
let properties = this.querySelector('map-properties');
if (g.getAttribute('role') === 'link') {
for (let path of g.children) {
path.mousedown.call(this._featureGroup, event);
path.mouseup.call(this._featureGroup, event);
}
}
if (typeof this.onclick === 'function') {
this.onclick.call(this._groupEl, event);
return;
} else {
let properties = this.querySelector('map-properties');
if (g.getAttribute('role') === 'link') {
for (let path of g.children) {
path.mousedown.call(this._featureGroup, event);
path.mouseup.call(this._featureGroup, event);
// dispatch click event for map-feature to allow events entered by 'addEventListener'
let clickEv = new PointerEvent('click', { cancelable: true });
clickEv.originalEvent = event;
this.dispatchEvent(clickEv);
// for custom projection, layer- element may disconnect and re-attach to the map after the click
// so check whether map-feature element is still connected before any further operations
if (properties && this.isConnected) {
let featureGroup = this._featureGroup,
shapes = featureGroup._layers;
// close popup if the popup is currently open
for (let id in shapes) {
if (shapes[id].isPopupOpen()) {
shapes[id].closePopup();
}
}
// for custom projection, layer- element may disconnect and re-attach to the map after the click
// so check whether map-feature element is still connected before any further operations
if (properties && this.isConnected) {
let featureGroup = this._featureGroup,
shapes = featureGroup._layers;
// close popup if the popup is currently open
for (let id in shapes) {
if (shapes[id].isPopupOpen()) {
shapes[id].closePopup();
}
}
if (featureGroup.isPopupOpen()) {
featureGroup.closePopup();
} else {
featureGroup.openPopup();
}
if (featureGroup.isPopupOpen()) {
featureGroup.closePopup();
} else if (!clickEv.originalEvent.cancelBubble) {
// If stopPropagation is not set on originalEvent by user
featureGroup.openPopup();
}
}
}

// a method that sets the current focus to the <g> element, or invoking the user-defined focus event
// event (optional): a FocusEvent object, can be passed as an argument of the user-defined focus event handlers
// options (optional): as options parameter for native HTMLelemnt
// options (optional): as options parameter for native HTMLElement
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
focus(event, options) {
let g = this._groupEl;
if (typeof this.onfocus === 'function') {
this.onfocus.call(this._groupEl, event);
return;
} else {
g.focus(options);
}
focus(options) {
this._groupEl.focus(options);
}

// a method that makes the <g> element lose focus, or invoking the user-defined blur event
// event (optional): a FocusEvent object, can be passed as an argument of the user-defined blur event handlers
blur(event) {
if (typeof this.onblur === 'function') {
this.onblur.call(this._groupEl, event);
} else if (
blur() {
if (
document.activeElement.shadowRoot?.activeElement === this._groupEl ||
document.activeElement.shadowRoot?.activeElement.parentNode ===
this._groupEl
Expand Down
1 change: 1 addition & 0 deletions src/mapml/layers/MapMLLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2042,6 +2042,7 @@ export var MapMLLayer = L.Layer.extend({
if (!(e instanceof MouseEvent) && e.keyCode !== 13) return;
e.preventDefault();
featureEl.zoomTo();
featureEl._map.closePopup();
};
content.insertBefore(
zoomLink,
Expand Down
60 changes: 60 additions & 0 deletions test/e2e/core/mapFeature.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,63 @@ test.describe('Playwright MapFeature Custom Element Tests', () => {
expect(test).toEqual(true);
});
});

test.describe('MapFeature Events', () => {
let page, context;
test.beforeAll(async () => {
context = await chromium.launchPersistentContext('');
page =
context.pages().find((page) => page.url() === 'about:blank') ||
(await context.newPage());
await page.goto('mapFeature1.html');
});
test.afterAll(async function () {
await context.close();
});

test('Custom Click event - stopPropagation', async () => {
// Click on polygon
await page
.locator(
'mapml-viewer[role="application"]:has-text("Polygon -75.5859375 45.4656690 -75.6813812 45.4533876 -75.6961441 45.4239978 -75")'
)
.click();
const popupCount = await page.$eval(
'body > mapml-viewer > div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane',
(popupPane) => popupPane.childElementCount
);
// expect no popup is binded
expect(popupCount).toEqual(0);

// custom click property displaying on div
const propertyDiv = await page.$eval(
'body > div#property',
(div) => div.firstElementChild.innerText
);
// check custom event is displaying properties
expect(propertyDiv).toEqual('This is a Polygon');
});

test('click() method - stopPropagation', async () => {
// click() method on line feature
await page.$eval(
'body > mapml-viewer > layer- > map-feature#line',
(line) => line.click()
);

const popupCount = await page.$eval(
'body > mapml-viewer > div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane',
(popupPane) => popupPane.childElementCount
);
// expect no popup is binded
expect(popupCount).toEqual(0);

// custom click property displaying on div
const propertyDiv = await page.$eval(
'body > div#property',
(div) => div.firstElementChild.innerText
);
// check custom event is displaying properties
expect(propertyDiv).toEqual('This is a Line');
});
});
71 changes: 71 additions & 0 deletions test/e2e/core/mapFeature1.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="en">

<head>
<title>map-feature Event tests</title>
<meta charset="UTF-8">
<script type="module" src="mapml-viewer.js"></script>
</head>

<body>
<mapml-viewer width="500" height="500" projection="OSMTILE" zoom="10" lon="-75.7" lat="45.4" controls>
<layer- label="Features" checked>
<map-meta name="projection" content="OSMTILE"></map-meta>
<map-feature>
<map-featurecaption>Polygon</map-featurecaption>
<map-geometry cs="gcrs">
<map-polygon class="polygon">
<map-coordinates>-75.5859375 45.4656690 -75.6813812 45.4533876 -75.6961441 45.4239978
-75.7249832 45.4083331 -75.7792282 45.3772317 -75.7534790 45.3294614 -75.5831909 45.3815724
-75.6024170 45.4273712 -75.5673981 45.4639834 -75.5859375 45.4656690</map-coordinates>
</map-polygon>
</map-geometry>
<map-properties>
<h2>This is a Polygon</h2>
</map-properties>
</map-feature>

<map-feature id="line">
<map-featurecaption>Line</map-featurecaption>
<map-geometry cs="gcrs">
<map-linestring class="line">
<map-coordinates>-75.6168365 45.471929 -75.6855011 45.458445 -75.7016373 45.4391764 -75.7030106
45.4259255 -75.7236099 45.4208652 -75.7565689 45.4117074 -75.7833481 45.384225 -75.8197403
45.3714435 -75.8516693 45.377714</map-coordinates>
</map-linestring>
</map-geometry>
<map-properties>
<h2>This is a Line</h2>
</map-properties>
</map-feature>

<map-feature>
<map-featurecaption>Point</map-featurecaption>
<map-geometry cs="gcrs">
<map-point class="point">
<map-coordinates>-75.6916809 45.4186964</map-coordinates>
</map-point>
</map-geometry>
<map-properties>
<h2>This is a Point</h2>
</map-properties>
</map-feature>
</layer->
</mapml-viewer>

<div id="property"></div>

<script>
function preventDefaultFunc(e) {
e.originalEvent.stopPropagation();
let out = document.getElementById('property');
out.innerHTML = e.srcElement.querySelector("map-properties").innerHTML;
}
// Replace map-feature click handling
document.querySelectorAll('map-feature').forEach((f) => {
f.onclick = preventDefaultFunc;
});
</script>
</body>

</html>
6 changes: 6 additions & 0 deletions test/e2e/layers/queryLink.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,12 @@ test.describe('Playwright Query Link Tests', () => {
await page.keyboard.press('Enter');
await page.waitForTimeout(200);

// zoom to here link closes popup
const popupCount = await page.evaluate(
`document.querySelector("mapml-viewer").shadowRoot.querySelector(".leaflet-popup-pane").childElementCount`
);
expect(popupCount).toBe(0);

const endTopLeft = await page.evaluate(
`document.querySelector('mapml-viewer').extent.topLeft.pcrs`
);
Expand Down