Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds ability to wrap geometry subtypes with span and a elements + a element functionality within coordinates #371

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@
/bower_components/
/node_modules/
.idea/
*.iml
*.iml
test.html
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,4 @@
<layer- label="Restaurants" src="demo/restaurants.mapml" checked></layer->
</mapml-viewer>
</body>
</html>
</html>
115 changes: 84 additions & 31 deletions src/mapml/features/feature.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@
* ];
*/
export var Feature = L.Path.extend({
options: {
accessibleTitle: "Feature",
},

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

if(this.type === "POINT" || this.type === "MULTIPOINT") options.fillOpacity = 1;

if(options.wrappers.length > 0)
options = Object.assign(this._convertWrappers(options.wrappers), options);
L.setOptions(this, options);

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

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

this._convertMarkup();

if(markup.querySelector('span') || markup.querySelector('a')){
if(markup.querySelector('span') || markup.querySelector('map-a')){
this._generateOutlinePoints();
}

Expand All @@ -53,36 +53,61 @@ export var Feature = L.Path.extend({
* Removes the focus handler, and calls the leaflet L.Path.onRemove
*/
onRemove: function () {
L.DomEvent.off(this.group, "keyup keydown mousedown", this._handleFocus, this);
if(this.options.link) {
this.off({
click: this._handleLinkClick,
keypress: this._handleLinkKeypress,
});
}

if(this.options.interactive) this.off('keypress', this._handleSpaceDown);

L.Path.prototype.onRemove.call(this);
},

/**
* Creates the <g> conditionally and also applies event handlers
* @private
* Attaches link handler to the sub parts' paths
* @param path
* @param link
* @param linkType
*/
_createGroup: function(){
if(this.options.multiGroup){
this.group = this.options.multiGroup;
} else {
this.group = L.SVG.create('g');
if(this.options.interactive) this.group.setAttribute("aria-expanded", "false");
this.group.setAttribute('aria-label', this.options.accessibleTitle);
if(this.options.featureID) this.group.setAttribute("data-fid", this.options.featureID);
L.DomEvent.on(this.group, "keyup keydown mousedown", this._handleFocus, this);
}
attachLinkHandler: function (path, link, linkType) {
L.DomEvent.on(path, "click", (e) => {
this._handleLink(link, linkType);
}, this);
L.DomEvent.on(path, "keypress", (e) => {
if (e.keyCode === 13 || e.keyCode === 32)
this._handleLink(link, linkType)
}, this);
},

/**
* Handler for focus events
* @param {L.DOMEvent} e - Event that occured
* Handles the different behaviors for link target types
* @param link
* @param linkType
* @private
*/
_handleFocus: function(e) {
if((e.keyCode === 9 || e.keyCode === 16 || e.keyCode === 13) && e.type === "keyup" && e.target.tagName === "g"){
this.openTooltip();
} else {
this.closeTooltip();
_handleLink: function (link, linkType) {
let layer = document.createElement('layer-');
layer.setAttribute('src', link);
layer.setAttribute('checked', '');
switch (linkType) {
case "_blank":
this._map.options.mapEl.appendChild(layer);
break;
case "_parent":
for(let l of this._map.options.mapEl.querySelectorAll("layer-"))
if(l._layer !== this.options.featureLayer.options._leafletLayer) this._map.options.mapEl.removeChild(l);
this._map.options.mapEl.appendChild(layer);
this._map.options.mapEl.removeChild(this.options.featureLayer.options._leafletLayer._layerEl);
break;
case "_top":
window.location.href = link;
break;
case "_self":
ahmadayubi marked this conversation as resolved.
Show resolved Hide resolved
default:
this.options.featureLayer.options._leafletLayer._layerEl.insertAdjacentElement('beforebegin', layer);
this._map.options.mapEl.removeChild(this.options.featureLayer.options._leafletLayer._layerEl);
}
},

Expand Down Expand Up @@ -140,6 +165,31 @@ export var Feature = L.Path.extend({
this._renderer._updateFeature(this);
},

/**
* Converts the spans, a and divs around a geometry subtype into options for the feature
* @private
*/
_convertWrappers: function (elems) {
if(!elems || elems.length === 0) return;
let classList = '', output = {};
for(let elem of elems){
if(elem.tagName.toUpperCase() !== "MAP-A" && elem.className){
// Useful if getting other attributes off spans and divs is useful
/* let attr = elem.attributes;
for(let i = 0; i < attr.length; i++){
if(attr[i].name === "class" || attributes[attr[i].name]) continue;
attributes[attr[i].name] = attr[i].value;
}*/
classList +=`${elem.className} `;
} else if(!output.link && elem.getAttribute("href")) {
output.link = elem.getAttribute("href");
if(elem.hasAttribute("target")) output.linkType = elem.getAttribute("target");
}
}
output.className = `${classList} ${this.options.className}`.trim();
return output;
},

/**
* Converts this._markup to the internal structure of features
* @private
Expand All @@ -149,6 +199,7 @@ export var Feature = L.Path.extend({

let attr = this._markup.attributes;
this.featureAttributes = {};
if (this.options.link) this.featureAttributes.tabindex = "0";
for(let i = 0; i < attr.length; i++){
this.featureAttributes[attr[i].name] = attr[i].value;
}
Expand All @@ -163,10 +214,10 @@ export var Feature = L.Path.extend({
this._parts[0].subrings = this._parts[0].subrings.concat(subrings);
} else if (this.type === "MULTIPOINT") {
for (let point of ring[0].points.concat(subrings)) {
this._parts.push({ rings: [{ points: [point] }], subrings: [], cls: point.cls || this.options.className });
this._parts.push({ rings: [{ points: [point] }], subrings: [], cls:`${point.cls || ""} ${this.options.className || ""}`.trim() });
}
} else {
this._parts.push({ rings: ring, subrings: subrings, cls: this.featureAttributes.class || this.options.className });
this._parts.push({ rings: ring, subrings: subrings, cls: `${this.featureAttributes.class || ""} ${this.options.className || ""}`.trim() });
}
first = false;
}
Expand Down Expand Up @@ -212,11 +263,12 @@ export var Feature = L.Path.extend({
* @param {Object[]} subParts - An empty array representing the sub parts
* @param {boolean} isFirst - A true | false representing if the current HTML element is the parent coordinates element or not
* @param {string} cls - The class of the coordinate/span
* @param parents
* @private
*/
_coordinateToArrays: function (coords, main, subParts, isFirst = true, cls = undefined) {
_coordinateToArrays: function (coords, main, subParts, isFirst = true, cls = undefined, parents = []) {
for (let span of coords.children) {
this._coordinateToArrays(span, main, subParts, false, span.getAttribute("class"));
this._coordinateToArrays(span, main, subParts, false, span.getAttribute("class"), parents.concat([span]));
}
let noSpan = coords.textContent.replace(/(<([^>]+)>)/ig, ''),
pairs = noSpan.match(/(\S+\s+\S+)/gim), local = [];
Expand All @@ -230,12 +282,13 @@ export var Feature = L.Path.extend({
if (isFirst) {
main.push({ points: local });
} else {
let attrMap = {}, attr = coords.attributes;
let attrMap = {}, attr = coords.attributes, wrapperAttr = this._convertWrappers(parents);
if(wrapperAttr.link) attrMap.tabindex = "0";
for(let i = 0; i < attr.length; i++){
if(attr[i].name === "class") continue;
attrMap[attr[i].name] = attr[i].value;
}
subParts.unshift({ points: local, cls: cls || this.options.className, attr: attrMap});
subParts.unshift({ points: local, cls: `${cls || ""} ${wrapperAttr.className || ""}`.trim(), attr: attrMap, link: wrapperAttr.link, linkType: wrapperAttr.linkType});
}
},

Expand Down
44 changes: 40 additions & 4 deletions src/mapml/features/featureGroup.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,47 @@
export var FeatureGroup = L.FeatureGroup.extend({

/**
* Adds layer to feature group
* @param {M.Feature} layer - The layer to be added
* Initialize the feature group
* @param {M.Feature[]} layers
* @param {Object} options
*/
initialize: function (layers, options) {
L.LayerGroup.prototype.initialize.call(this, layers, options);

if(this.options.onEachFeature) {
this.options.group.setAttribute("aria-expanded", "false");
this.options.group.setAttribute('tabindex', '0');
L.DomUtil.addClass(this.options.group, "leaflet-interactive");
this.options.onEachFeature(this.options.properties, this);
L.DomEvent.on(this.options.group, "keyup keydown mousedown", this._handleFocus, this);
}

this.options.group.setAttribute('aria-label', this.options.accessibleTitle);
if(this.options.featureID) this.options.group.setAttribute("data-fid", this.options.featureID);
},

/**
* Handler for focus events
* @param {L.DOMEvent} e - Event that occured
* @private
*/
_handleFocus: function(e) {
if(e.target.tagName.toUpperCase() !== "G") return;
if((e.keyCode === 9 || e.keyCode === 16 || e.keyCode === 13) && e.type === "keyup" && e.target.tagName === "g") {
this.openTooltip();
} else if (e.keyCode === 13 || e.keyCode === 32){
L.DomEvent.stop(e);
this.closeTooltip();
this.openPopup();
} else {
this.closeTooltip();
}
},

addLayer: function (layer) {
layer.openTooltip = () => { this.openTooltip(); }; // needed to open tooltip of child features
layer.closeTooltip = () => { this.closeTooltip(); }; // needed to close tooltip of child features
if(!layer.options.link && this.options.onEachFeature) {
this.options.onEachFeature(this.options.properties, layer);
}
L.FeatureGroup.prototype.addLayer.call(this, layer);
},

Expand Down
15 changes: 11 additions & 4 deletions src/mapml/features/featureRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export var FeatureRenderer = L.SVG.extend({
}
if (p.subrings) {
for (let r of p.subrings) {
this._createPath(r, layer.options.className, r.attr['aria-label'], false, r.attr);
this._createPath(r, layer.options.className, r.attr['aria-label'], (r.link !== undefined), r.attr);
if(r.attr && r.attr.tabindex){
p.path.setAttribute('tabindex', r.attr.tabindex || '0');
}
Expand All @@ -40,8 +40,6 @@ export var FeatureRenderer = L.SVG.extend({
if(stampLayer){
let stamp = L.stamp(layer);
this._layers[stamp] = layer;
layer.group.setAttribute('tabindex', '0');
L.DomUtil.addClass(layer.group, "leaflet-interactive");
}
},

Expand Down Expand Up @@ -89,15 +87,24 @@ export var FeatureRenderer = L.SVG.extend({
for (let p of layer._parts) {
if (p.path)
layer.group.appendChild(p.path);
if (interactive){
if(layer.options.link) layer.attachLinkHandler(p.path, layer.options.link, layer.options.linkType);
layer.addInteractiveTarget(p.path)
}

if(!outlineAdded && layer.pixelOutline) {
layer.group.appendChild(layer.outlinePath);
outlineAdded = true;
}

for (let subP of p.subrings) {
if (subP.path)
if (subP.path) {
if (subP.link){
layer.attachLinkHandler(subP.path, subP.link, subP.linkType);
layer.addInteractiveTarget(subP.path);
}
layer.group.appendChild(subP.path);
}
}
}
c.appendChild(layer.group);
Expand Down
51 changes: 24 additions & 27 deletions src/mapml/layers/FeatureLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,10 +250,14 @@ export var MapMLFeatures = L.FeatureGroup.extend({
let zoom = mapml.getAttribute("zoom") || nativeZoom, title = mapml.querySelector("featurecaption");
title = title ? title.innerHTML : "Feature";

let layer = this.geometryToLayer(mapml, options.pointToLayer, options, nativeCS, +zoom, title);
let propertyContainer = document.createElement('div');
propertyContainer.classList.add("mapml-popup-content");
propertyContainer.insertAdjacentHTML('afterbegin', mapml.querySelector("properties").innerHTML);

options.properties = propertyContainer;

let layer = this.geometryToLayer(mapml, options, nativeCS, +zoom, title);
if (layer) {
layer.properties = mapml.getElementsByTagName('properties')[0];

// if the layer is being used as a query handler output, it will have
// a color option set. Otherwise, copy classes from the feature
if (!layer.options.color && mapml.hasAttribute('class')) {
Expand All @@ -263,15 +267,7 @@ export var MapMLFeatures = L.FeatureGroup.extend({
this.resetStyle(layer);

if (options.onEachFeature) {
options.onEachFeature(layer.properties, layer);
layer.bindTooltip(title, { interactive:true, sticky: true, });
if(layer._events){
if(!layer._events.keypress) layer._events.keypress = [];
layer._events.keypress.push({
"ctx": layer,
"fn": this._onSpacePress,
});
}
}
if(this._staticFeature){
let featureZoom = mapml.getAttribute('zoom') || nativeZoom;
Expand Down Expand Up @@ -317,30 +313,31 @@ export var MapMLFeatures = L.FeatureGroup.extend({
this._container.removeChild(toDelete[i]);
}
},
_onSpacePress: function(e){
if(e.originalEvent.keyCode === 32){
this._openPopup(e);
}
},
geometryToLayer: function (mapml, pointToLayer, vectorOptions, nativeCS, zoom, title) {
geometryToLayer: function (mapml, vectorOptions, nativeCS, zoom, title) {
let geometry = mapml.tagName.toUpperCase() === 'FEATURE' ? mapml.getElementsByTagName('geometry')[0] : mapml,
cs = geometry.getAttribute("cs") || nativeCS, subFeatures = geometry, group = [], multiGroup;

if(geometry.firstElementChild.tagName === "GEOMETRYCOLLECTION" || geometry.firstElementChild.tagName === "MULTIPOLYGON")
subFeatures = geometry.firstElementChild;

for(let geo of subFeatures.children){
if(group.length > 0) multiGroup = group[group.length - 1].group;
cs = geometry.getAttribute("cs") || nativeCS, group = [], svgGroup = L.SVG.create('g');
for(let geo of geometry.querySelectorAll('polygon, linestring, multilinestring, point, multipoint')){
group.push(M.feature(geo, Object.assign(vectorOptions,
{ nativeCS: cs,
nativeZoom: zoom,
projection: this.options.projection,
featureID: mapml.id,
multiGroup: multiGroup,
accessibleTitle: title,
group: svgGroup,
wrappers: this._getGeometryParents(geo.parentElement),
featureLayer: this,
})));
}
return M.featureGroup(group);
return M.featureGroup(group, {group:svgGroup, featureID: mapml.id, accessibleTitle: title, onEachFeature: vectorOptions.onEachFeature, properties: vectorOptions.properties});
},

_getGeometryParents: function(subType, elems = []){
if(subType && subType.tagName.toUpperCase() !== "GEOMETRY"){
if(subType.tagName.toUpperCase() === "MULTIPOLYGON" || subType.tagName.toUpperCase() === "GEOMETRYCOLLECTION")
return this._getGeometryParents(subType.parentElement, elems);
return this._getGeometryParents(subType.parentElement, elems.concat([subType]));
} else {
return elems;
}
},
});
export var mapMlFeatures = function (mapml, options) {
Expand Down
Loading