Skip to content

Commit 202e8f6

Browse files
Adds ability to wrap geometry subtypes with span and a elements + a element functionality within coordinates (Maps4HTML#383)
* Adds ability to wrap geometry subtypes with spans and a * Ability to wrap geometry subtypes in links and spans * Link update * Templated tile fix * Add support for links within coordinates * Adds link tests * Removes handlers, might be needed in the future * Remove redundant self case * Fix JSHints * Add annotated testing links - run root index.html to view * Fix popup + add type attribute to map-a * Adds group link behavior * Fix grouping, and update test * Fix single feature behavior * Resolve conflict * Remove unneeded changes * Adds text/html link tests Co-authored-by: Peter Rushforth <[email protected]>
1 parent 0f32fe4 commit 202e8f6

14 files changed

+504
-88
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
/bower_components/
1010
/node_modules/
1111
.idea/
12-
*.iml
12+
*.iml
13+
test.html

demo/canada.mapml

Lines changed: 44 additions & 7 deletions
Large diffs are not rendered by default.

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
<mapml-viewer projection="CBMTILE" zoom="2" lat="45" lon="-90" controls>
5858
<layer- label="CBMT" src="https://geogratis.gc.ca/mapml/en/cbmtile/cbmt/" checked></layer->
5959
<layer- label="Restaurants" src="demo/restaurants.mapml" checked></layer->
60+
<layer- label="Links Testing" src="demo/canada.mapml" checked></layer->
6061
</mapml-viewer>
6162
</body>
6263
</html>

src/mapml/features/feature.js

Lines changed: 69 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,6 @@
1919
* ];
2020
*/
2121
export var Feature = L.Path.extend({
22-
options: {
23-
accessibleTitle: "Feature",
24-
},
2522

2623
/**
2724
* Initializes the M.Feature
@@ -32,17 +29,20 @@ export var Feature = L.Path.extend({
3229
this.type = markup.tagName.toUpperCase();
3330

3431
if(this.type === "POINT" || this.type === "MULTIPOINT") options.fillOpacity = 1;
32+
33+
if(options.wrappers.length > 0)
34+
options = Object.assign(this._convertWrappers(options.wrappers), options);
3535
L.setOptions(this, options);
3636

37-
this._createGroup(); // creates the <g> element for the feature, or sets the one passed in options as the <g>
37+
this.group = this.options.group;
3838

3939
this._parts = [];
4040
this._markup = markup;
4141
this.options.zoom = markup.getAttribute('zoom') || this.options.nativeZoom;
4242

4343
this._convertMarkup();
4444

45-
if(markup.querySelector('span') || markup.querySelector('a')){
45+
if(markup.querySelector('span') || markup.querySelector('map-a')){
4646
this._generateOutlinePoints();
4747
}
4848

@@ -53,37 +53,39 @@ export var Feature = L.Path.extend({
5353
* Removes the focus handler, and calls the leaflet L.Path.onRemove
5454
*/
5555
onRemove: function () {
56-
L.DomEvent.off(this.group, "keyup keydown mousedown", this._handleFocus, this);
57-
L.Path.prototype.onRemove.call(this);
58-
},
59-
60-
/**
61-
* Creates the <g> conditionally and also applies event handlers
62-
* @private
63-
*/
64-
_createGroup: function(){
65-
if(this.options.multiGroup){
66-
this.group = this.options.multiGroup;
67-
} else {
68-
this.group = L.SVG.create('g');
69-
if(this.options.interactive) this.group.setAttribute("aria-expanded", "false");
70-
this.group.setAttribute('aria-label', this.options.accessibleTitle);
71-
if(this.options.featureID) this.group.setAttribute("data-fid", this.options.featureID);
72-
L.DomEvent.on(this.group, "keyup keydown mousedown", this._handleFocus, this);
56+
if(this.options.link) {
57+
this.off({
58+
click: this._handleLinkClick,
59+
keypress: this._handleLinkKeypress,
60+
});
7361
}
62+
63+
if(this.options.interactive) this.off('keypress', this._handleSpaceDown);
64+
65+
L.Path.prototype.onRemove.call(this);
7466
},
7567

7668
/**
77-
* Handler for focus events
78-
* @param {L.DOMEvent} e - Event that occured
79-
* @private
69+
* Attaches link handler to the sub parts' paths
70+
* @param path
71+
* @param link
72+
* @param linkTarget
73+
* @param linkType
74+
* @param leafletLayer
8075
*/
81-
_handleFocus: function(e) {
82-
if((e.keyCode === 9 || e.keyCode === 16 || e.keyCode === 13) && e.type === "keyup" && e.target.tagName === "g"){
83-
this.openTooltip();
84-
} else {
85-
this.closeTooltip();
86-
}
76+
attachLinkHandler: function (path, link, linkTarget, linkType, leafletLayer) {
77+
let drag = false; //prevents click from happening on drags
78+
L.DomEvent.on(path, 'mousedown', () =>{ drag = false;}, this);
79+
L.DomEvent.on(path, 'mousemove', () =>{ drag = true;}, this);
80+
L.DomEvent.on(path, "mouseup", (e) => {
81+
L.DomEvent.stop(e);
82+
if(!drag) M.handleLink(link, linkTarget, linkType, leafletLayer);
83+
}, this);
84+
L.DomEvent.on(path, "keypress", (e) => {
85+
L.DomEvent.stop(e);
86+
if(e.keyCode === 13 || e.keyCode === 32)
87+
M.handleLink(link, linkTarget, linkType, leafletLayer);
88+
}, this);
8789
},
8890

8991
/**
@@ -140,6 +142,32 @@ export var Feature = L.Path.extend({
140142
this._renderer._updateFeature(this);
141143
},
142144

145+
/**
146+
* Converts the spans, a and divs around a geometry subtype into options for the feature
147+
* @private
148+
*/
149+
_convertWrappers: function (elems) {
150+
if(!elems || elems.length === 0) return;
151+
let classList = '', output = {};
152+
for(let elem of elems){
153+
if(elem.tagName.toUpperCase() !== "MAP-A" && elem.className){
154+
// Useful if getting other attributes off spans and divs is useful
155+
/* let attr = elem.attributes;
156+
for(let i = 0; i < attr.length; i++){
157+
if(attr[i].name === "class" || attributes[attr[i].name]) continue;
158+
attributes[attr[i].name] = attr[i].value;
159+
}*/
160+
classList +=`${elem.className} `;
161+
} else if(!output.link && elem.getAttribute("href")) {
162+
output.link = elem.getAttribute("href");
163+
if(elem.hasAttribute("target")) output.linkTarget = elem.getAttribute("target");
164+
if(elem.hasAttribute("type")) output.linkType = elem.getAttribute("type");
165+
}
166+
}
167+
output.className = `${classList} ${this.options.className}`.trim();
168+
return output;
169+
},
170+
143171
/**
144172
* Converts this._markup to the internal structure of features
145173
* @private
@@ -149,6 +177,8 @@ export var Feature = L.Path.extend({
149177

150178
let attr = this._markup.attributes;
151179
this.featureAttributes = {};
180+
if(this.options.link && this._markup.parentElement.tagName.toUpperCase() === "MAP-A" && this._markup.parentElement.parentElement.tagName.toUpperCase() !== "GEOMETRY")
181+
this.featureAttributes.tabindex = "0";
152182
for(let i = 0; i < attr.length; i++){
153183
this.featureAttributes[attr[i].name] = attr[i].value;
154184
}
@@ -163,10 +193,10 @@ export var Feature = L.Path.extend({
163193
this._parts[0].subrings = this._parts[0].subrings.concat(subrings);
164194
} else if (this.type === "MULTIPOINT") {
165195
for (let point of ring[0].points.concat(subrings)) {
166-
this._parts.push({ rings: [{ points: [point] }], subrings: [], cls: point.cls || this.options.className });
196+
this._parts.push({ rings: [{ points: [point] }], subrings: [], cls:`${point.cls || ""} ${this.options.className || ""}`.trim() });
167197
}
168198
} else {
169-
this._parts.push({ rings: ring, subrings: subrings, cls: this.featureAttributes.class || this.options.className });
199+
this._parts.push({ rings: ring, subrings: subrings, cls: `${this.featureAttributes.class || ""} ${this.options.className || ""}`.trim() });
170200
}
171201
first = false;
172202
}
@@ -212,11 +242,12 @@ export var Feature = L.Path.extend({
212242
* @param {Object[]} subParts - An empty array representing the sub parts
213243
* @param {boolean} isFirst - A true | false representing if the current HTML element is the parent coordinates element or not
214244
* @param {string} cls - The class of the coordinate/span
245+
* @param parents
215246
* @private
216247
*/
217-
_coordinateToArrays: function (coords, main, subParts, isFirst = true, cls = undefined) {
248+
_coordinateToArrays: function (coords, main, subParts, isFirst = true, cls = undefined, parents = []) {
218249
for (let span of coords.children) {
219-
this._coordinateToArrays(span, main, subParts, false, span.getAttribute("class"));
250+
this._coordinateToArrays(span, main, subParts, false, span.getAttribute("class"), parents.concat([span]));
220251
}
221252
let noSpan = coords.textContent.replace(/(<([^>]+)>)/ig, ''),
222253
pairs = noSpan.match(/(\S+\s+\S+)/gim), local = [];
@@ -230,12 +261,13 @@ export var Feature = L.Path.extend({
230261
if (isFirst) {
231262
main.push({ points: local });
232263
} else {
233-
let attrMap = {}, attr = coords.attributes;
264+
let attrMap = {}, attr = coords.attributes, wrapperAttr = this._convertWrappers(parents);
265+
if(wrapperAttr.link) attrMap.tabindex = "0";
234266
for(let i = 0; i < attr.length; i++){
235267
if(attr[i].name === "class") continue;
236268
attrMap[attr[i].name] = attr[i].value;
237269
}
238-
subParts.unshift({ points: local, cls: cls || this.options.className, attr: attrMap});
270+
subParts.unshift({ points: local, cls: `${cls || ""} ${wrapperAttr.className || ""}`.trim(), attr: attrMap, link: wrapperAttr.link, linkTarget: wrapperAttr.linkTarget, linkType: wrapperAttr.linkType});
239271
}
240272
},
241273

src/mapml/features/featureGroup.js

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,63 @@
11
export var FeatureGroup = L.FeatureGroup.extend({
2+
23
/**
3-
* Adds layer to feature group
4-
* @param {M.Feature} layer - The layer to be added
4+
* Initialize the feature group
5+
* @param {M.Feature[]} layers
6+
* @param {Object} options
57
*/
8+
initialize: function (layers, options) {
9+
if(options.wrappers && options.wrappers.length > 0)
10+
options = Object.assign(M.Feature.prototype._convertWrappers(options.wrappers), options);
11+
12+
L.LayerGroup.prototype.initialize.call(this, layers, options);
13+
14+
if(this.options.onEachFeature || this.options.link) {
15+
this.options.group.setAttribute('tabindex', '0');
16+
L.DomUtil.addClass(this.options.group, "leaflet-interactive");
17+
L.DomEvent.on(this.options.group, "keyup keydown mousedown", this._handleFocus, this);
18+
let firstLayer = layers[Object.keys(layers)[0]];
19+
if(layers.length === 1 && firstLayer.options.link){ //if it's the only layer and it has a link, take it's link
20+
this.options.link = firstLayer.options.link;
21+
this.options.linkTarget = firstLayer.options.linkTarget;
22+
this.options.linkType = firstLayer.options.linkType;
23+
}
24+
if(this.options.link){
25+
M.Feature.prototype.attachLinkHandler.call(this, this.options.group, this.options.link, this.options.linkTarget, this.options.linkType, this.options._leafletLayer);
26+
} else {
27+
this.options.group.setAttribute("aria-expanded", "false");
28+
this.options.onEachFeature(this.options.properties, this);
29+
this.off("click", this._openPopup);
30+
}
31+
}
32+
33+
this.options.group.setAttribute('aria-label', this.options.accessibleTitle);
34+
if(this.options.featureID) this.options.group.setAttribute("data-fid", this.options.featureID);
35+
},
36+
37+
/**
38+
* Handler for focus events
39+
* @param {L.DOMEvent} e - Event that occured
40+
* @private
41+
*/
42+
_handleFocus: function(e) {
43+
if(e.target.tagName.toUpperCase() !== "G") return;
44+
if((e.keyCode === 9 || e.keyCode === 16 || e.keyCode === 13) && e.type === "keyup") {
45+
this.openTooltip();
46+
} else if (e.keyCode === 13 || e.keyCode === 32){
47+
this.closeTooltip();
48+
if(!this.options.link && this.options.onEachFeature){
49+
L.DomEvent.stop(e);
50+
this.openPopup();
51+
}
52+
} else {
53+
this.closeTooltip();
54+
}
55+
},
56+
657
addLayer: function (layer) {
7-
layer.openTooltip = () => { this.openTooltip(); }; // needed to open tooltip of child features
8-
layer.closeTooltip = () => { this.closeTooltip(); }; // needed to close tooltip of child features
58+
if(!layer.options.link && this.options.onEachFeature) {
59+
this.options.onEachFeature(this.options.properties, layer);
60+
}
961
L.FeatureGroup.prototype.addLayer.call(this, layer);
1062
},
1163

src/mapml/features/featureRenderer.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export var FeatureRenderer = L.SVG.extend({
2929
}
3030
if (p.subrings) {
3131
for (let r of p.subrings) {
32-
this._createPath(r, layer.options.className, r.attr['aria-label'], false, r.attr);
32+
this._createPath(r, layer.options.className, r.attr['aria-label'], (r.link !== undefined), r.attr);
3333
if(r.attr && r.attr.tabindex){
3434
p.path.setAttribute('tabindex', r.attr.tabindex || '0');
3535
}
@@ -40,8 +40,6 @@ export var FeatureRenderer = L.SVG.extend({
4040
if(stampLayer){
4141
let stamp = L.stamp(layer);
4242
this._layers[stamp] = layer;
43-
layer.group.setAttribute('tabindex', '0');
44-
L.DomUtil.addClass(layer.group, "leaflet-interactive");
4543
}
4644
},
4745

@@ -89,15 +87,24 @@ export var FeatureRenderer = L.SVG.extend({
8987
for (let p of layer._parts) {
9088
if (p.path)
9189
layer.group.appendChild(p.path);
90+
if (interactive){
91+
if(layer.options.link) layer.attachLinkHandler(p.path, layer.options.link, layer.options.linkTarget, layer.options.linkType, layer.options._leafletLayer);
92+
layer.addInteractiveTarget(p.path);
93+
}
9294

9395
if(!outlineAdded && layer.pixelOutline) {
9496
layer.group.appendChild(layer.outlinePath);
9597
outlineAdded = true;
9698
}
9799

98100
for (let subP of p.subrings) {
99-
if (subP.path)
101+
if (subP.path) {
102+
if (subP.link){
103+
layer.attachLinkHandler(subP.path, subP.link, subP.linkTarget, subP.linkType, layer.options._leafletLayer);
104+
layer.addInteractiveTarget(subP.path);
105+
}
100106
layer.group.appendChild(subP.path);
107+
}
101108
}
102109
}
103110
c.appendChild(layer.group);
@@ -180,6 +187,13 @@ export var FeatureRenderer = L.SVG.extend({
180187
if (!path || !layer) { return; }
181188
let options = layer.options, isClosed = layer.isClosed;
182189
if ((options.stroke && (!isClosed || isOutline)) || (isMain && !layer.outlinePath)) {
190+
if (options.link){
191+
path.style.stroke = "#0000EE";
192+
path.style.strokeOpacity = "1";
193+
path.style.strokeWidth = "1px";
194+
path.style.strokeDasharray = "none";
195+
196+
}
183197
path.setAttribute('stroke', options.color);
184198
path.setAttribute('stroke-opacity', options.opacity);
185199
path.setAttribute('stroke-width', options.weight);

src/mapml/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,7 @@ window.M = M;
577577
});
578578
}());
579579

580+
M.handleLink = Util.handleLink;
580581
M.convertPCRSBounds = Util.convertPCRSBounds;
581582
M.axisToXY = Util.axisToXY;
582583
M.csToAxes = Util.csToAxes;

src/mapml/layers/Crosshair.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,15 @@ export var Crosshair = L.Layer.extend({
8787

8888
_isMapFocused: function (e) {
8989
//set this._map.isFocused = true if arrow buttons are used
90-
if (this._map._container.parentNode.activeElement.classList.contains("leaflet-container") && ["keydown"].includes(e.type) && (e.shiftKey && e.keyCode === 9)) {
91-
this._map.isFocused = false;
92-
} else if (this._map._container.parentNode.activeElement.classList.contains("leaflet-container") && ["keyup", "keydown"].includes(e.type)) {
93-
this._map.isFocused = true;
94-
} else {
90+
if(!this._map._container.parentNode.activeElement){
9591
this._map.isFocused = false;
92+
return;
9693
}
94+
let isLeafletContainer = this._map._container.parentNode.activeElement.classList.contains("leaflet-container");
95+
if (isLeafletContainer && ["keydown"].includes(e.type) && (e.shiftKey && e.keyCode === 9)) {
96+
this._map.isFocused = false;
97+
} else this._map.isFocused = isLeafletContainer && ["keyup", "keydown"].includes(e.type);
98+
9799
this._addOrRemoveMapOutline();
98100
this._addOrRemoveCrosshair();
99101
},

0 commit comments

Comments
 (0)