531 lines
21 KiB
JavaScript
531 lines
21 KiB
JavaScript
(function(global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('leaflet')) :
|
|
typeof define === 'function' && define.amd ? define(['exports', 'leaflet'], factory) :
|
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.LayersTree = {}, global.L));
|
|
})(this, (function(_exports, L) { 'use strict';
|
|
|
|
/*
|
|
* Control like L.Control.Layers, but showing layers in a tree.
|
|
* Do not forget to include the css file.
|
|
*/
|
|
|
|
if (typeof L === 'undefined') {
|
|
throw new Error('Leaflet must be included first');
|
|
}
|
|
|
|
/*
|
|
* L.Control.Layers.Tree extends L.Control.Layers because it reuses
|
|
* most of its functionality. Only the HTML creation is different.
|
|
*/
|
|
L.Control.Layers.Tree = L.Control.Layers.extend({
|
|
options: {
|
|
closedSymbol: '+',
|
|
openedSymbol: '−',
|
|
spaceSymbol: ' ',
|
|
selectorBack: false,
|
|
namedToggle: false,
|
|
collapseAll: '',
|
|
expandAll: '',
|
|
labelIsSelector: 'both',
|
|
},
|
|
|
|
// Class names are error prone texts, so write them once here
|
|
_initClassesNames: function() {
|
|
this.cls = {
|
|
children: 'leaflet-layerstree-children',
|
|
childrenNopad: 'leaflet-layerstree-children-nopad',
|
|
hide: 'leaflet-layerstree-hide',
|
|
closed: 'leaflet-layerstree-closed',
|
|
opened: 'leaflet-layerstree-opened',
|
|
space: 'leaflet-layerstree-header-space',
|
|
pointer: 'leaflet-layerstree-header-pointer',
|
|
header: 'leaflet-layerstree-header',
|
|
neverShow: 'leaflet-layerstree-nevershow',
|
|
node: 'leaflet-layerstree-node',
|
|
name: 'leaflet-layerstree-header-name',
|
|
label: 'leaflet-layerstree-header-label',
|
|
selAllCheckbox: 'leaflet-layerstree-sel-all-checkbox',
|
|
};
|
|
},
|
|
|
|
initialize: function(baseTree, overlaysTree, options) {
|
|
this._scrollTop = 0;
|
|
this._initClassesNames();
|
|
this._baseTree = null;
|
|
this._overlaysTree = null;
|
|
L.Util.setOptions(this, options);
|
|
L.Control.Layers.prototype.initialize.call(this, null, null, options);
|
|
this._setTrees(baseTree, overlaysTree);
|
|
},
|
|
|
|
setBaseTree: function(tree) {
|
|
return this._setTrees(tree);
|
|
},
|
|
|
|
setOverlayTree: function(tree) {
|
|
return this._setTrees(undefined, tree);
|
|
},
|
|
|
|
addBaseLayer: function(_layer, _name) {
|
|
throw 'addBaseLayer is disabled';
|
|
},
|
|
|
|
addOverlay: function(_layer, _name) {
|
|
throw 'addOverlay is disabled';
|
|
},
|
|
|
|
removeLayer: function(_layer) {
|
|
throw 'removeLayer is disabled';
|
|
},
|
|
|
|
collapse: function() {
|
|
this._scrollTop = this._sect().scrollTop;
|
|
return L.Control.Layers.prototype.collapse.call(this);
|
|
},
|
|
|
|
expand: function() {
|
|
L.Control.Layers.prototype.expand.call(this);
|
|
this._sect().scrollTop = this._scrollTop;
|
|
},
|
|
|
|
onAdd: function(map) {
|
|
function changeName(layer) {
|
|
if (layer._layersTreeName) {
|
|
toggle.innerHTML = layer._layersTreeName;
|
|
}
|
|
}
|
|
|
|
var ret = L.Control.Layers.prototype.onAdd.call(this, map);
|
|
if (this.options.namedToggle) {
|
|
var toggle = this._container.getElementsByClassName('leaflet-control-layers-toggle')[0];
|
|
L.DomUtil.addClass(toggle, 'leaflet-layerstree-named-toggle');
|
|
// Start with this value...
|
|
map.eachLayer(function(layer) {changeName(layer);});
|
|
// ... and change it whenever the baselayer is changed.
|
|
map.on('baselayerchange', function(e) {changeName(e.layer);}, this);
|
|
}
|
|
return ret;
|
|
},
|
|
|
|
// Expands the whole tree (base other overlays)
|
|
expandTree: function(overlay) {
|
|
var container = overlay ? this._overlaysList : this._baseLayersList;
|
|
if (container) {
|
|
this._applyOnTree(container, false);
|
|
}
|
|
return this._localExpand();
|
|
},
|
|
|
|
// Collapses the whole tree (base other overlays)
|
|
collapseTree: function(overlay) {
|
|
var container = overlay ? this._overlaysList : this._baseLayersList;
|
|
if (container) {
|
|
this._applyOnTree(container, true);
|
|
}
|
|
return this._localExpand();
|
|
},
|
|
|
|
// Expands the tree, only to show the selected inputs
|
|
expandSelected: function(overlay) {
|
|
function iter(el) {
|
|
// Function to iterate the whole DOM upwards
|
|
var p = el.parentElement;
|
|
if (p) {
|
|
if (L.DomUtil.hasClass(p, that.cls.children) &&
|
|
!L.DomUtil.hasClass(el, that.cls.childrenNopad)) {
|
|
L.DomUtil.removeClass(p, hide);
|
|
}
|
|
|
|
if (L.DomUtil.hasClass(p, that.cls.node)) {
|
|
var h = p.getElementsByClassName(that.cls.header)[0];
|
|
that._applyOnTree(h, false);
|
|
}
|
|
iter(p);
|
|
}
|
|
}
|
|
|
|
var that = this;
|
|
var container = overlay ? this._overlaysList : this._baseLayersList;
|
|
if (!container) return this;
|
|
var hide = this.cls.hide;
|
|
var inputs = this._layerControlInputs || container.getElementsByTagName('input');
|
|
for (var i = 0; i < inputs.length; i++) {
|
|
// Loop over every (valid) input.
|
|
var input = inputs[i];
|
|
if (this._getLayer && !!this._getLayer(input.layerId).overlay != !!overlay) continue;
|
|
if (input.checked) {
|
|
// Get out of the header,
|
|
// to not open the possible (but rare) children
|
|
iter(input.parentElement.parentElement.parentElement.parentElement);
|
|
}
|
|
}
|
|
return this._localExpand();
|
|
},
|
|
|
|
// "private" methods, not exposed in the API
|
|
_sect: function() {
|
|
// to keep compatibility after 1.3 https://github.com/Leaflet/Leaflet/pull/6380
|
|
return this._section || this._form;
|
|
},
|
|
|
|
_setTrees: function(base, overlays) {
|
|
var id = 0; // to keep unique id
|
|
function iterate(tree, output, overlays) {
|
|
if (tree && tree.layer) {
|
|
if (!overlays) {
|
|
tree.layer._layersTreeName = tree.name || tree.label;
|
|
}
|
|
output[id++] = tree.layer;
|
|
}
|
|
if (tree && tree.children && tree.children.length) {
|
|
tree.children.forEach(function(child) {
|
|
iterate(child, output, overlays);
|
|
});
|
|
}
|
|
return output;
|
|
}
|
|
|
|
// We accept arrays, but convert into an object with children
|
|
function forArrays(input) {
|
|
if (Array.isArray(input)) {
|
|
return {noShow: true, children: input};
|
|
} else {
|
|
return input;
|
|
}
|
|
}
|
|
|
|
// Clean everything, and start again.
|
|
if (this._layerControlInputs) {
|
|
this._layerControlInputs = [];
|
|
}
|
|
for (var i = 0; i < this._layers.length; ++i) {
|
|
this._layers[i].layer.off('add remove', this._onLayerChange, this);
|
|
}
|
|
this._layers = [];
|
|
|
|
if (base !== undefined) this._baseTree = forArrays(base);
|
|
if (overlays !== undefined) this._overlaysTree = forArrays(overlays);
|
|
|
|
var bflat = iterate(this._baseTree, {});
|
|
for (var j in bflat) {
|
|
this._addLayer(bflat[j], j);
|
|
}
|
|
|
|
var oflat = iterate(this._overlaysTree, {}, true);
|
|
for (var k in oflat) {
|
|
this._addLayer(oflat[k], k, true);
|
|
}
|
|
return (this._map) ? this._update() : this;
|
|
},
|
|
|
|
// Used to update the vertical scrollbar
|
|
_localExpand: function() {
|
|
if (this._map && L.DomUtil.hasClass(this._container, 'leaflet-control-layers-expanded')) {
|
|
var top = this._sect().scrollTop;
|
|
this.expand();
|
|
this._sect().scrollTop = top; // to keep the scroll location
|
|
this._scrollTop = top;
|
|
}
|
|
return this;
|
|
},
|
|
|
|
// collapses or expands the tree in the container.
|
|
_applyOnTree: function(container, collapse) {
|
|
var iters = [
|
|
{cls: this.cls.children, hide: collapse},
|
|
{cls: this.cls.opened, hide: collapse},
|
|
{cls: this.cls.closed, hide: !collapse},
|
|
];
|
|
iters.forEach(function(it) {
|
|
var els = container.getElementsByClassName(it.cls);
|
|
for (var i = 0; i < els.length; i++) {
|
|
var el = els[i];
|
|
if (L.DomUtil.hasClass(el, this.cls.childrenNopad)) {
|
|
// do nothing
|
|
} else if (it.hide) {
|
|
L.DomUtil.addClass(el, this.cls.hide);
|
|
} else {
|
|
L.DomUtil.removeClass(el, this.cls.hide);
|
|
}
|
|
}
|
|
}, this);
|
|
},
|
|
|
|
// it is called in the original _update, and shouldn't do anything.
|
|
_addItem: function(_obj) {
|
|
},
|
|
|
|
// overwrite _update function in Control.Layers
|
|
_update: function() {
|
|
if (!this._container) { return this; }
|
|
L.Control.Layers.prototype._update.call(this);
|
|
this._addTreeLayout(this._baseTree, false);
|
|
this._addTreeLayout(this._overlaysTree, true);
|
|
return this._localExpand();
|
|
},
|
|
|
|
// Create the DOM objects for the tree
|
|
_addTreeLayout: function(tree, overlay) {
|
|
if (!tree) return;
|
|
var container = overlay ? this._overlaysList : this._baseLayersList;
|
|
this._expandCollapseAll(overlay, this.options.collapseAll, this.collapseTree);
|
|
this._expandCollapseAll(overlay, this.options.expandAll, this.expandTree);
|
|
this._iterateTreeLayout(tree, container, overlay, [], tree.noShow);
|
|
if (this._checkDisabledLayers) {
|
|
// to keep compatibility
|
|
this._checkDisabledLayers();
|
|
}
|
|
},
|
|
|
|
// Create the "Collapse all" or expand, if needed.
|
|
_expandCollapseAll: function(overlay, text, fn, ctx) {
|
|
var container = overlay ? this._overlaysList : this._baseLayersList;
|
|
ctx = ctx ? ctx : this;
|
|
if (text) {
|
|
var o = document.createElement('div');
|
|
o.className = 'leaflet-layerstree-expand-collapse';
|
|
container.appendChild(o);
|
|
o.innerHTML = text;
|
|
o.tabIndex = 0;
|
|
L.DomEvent.on(o, 'click keydown', function(e) {
|
|
if (e.type !== 'keydown' || e.keyCode === 32) {
|
|
o.blur();
|
|
fn.call(ctx, overlay);
|
|
this._localExpand();
|
|
}
|
|
}, this);
|
|
}
|
|
},
|
|
|
|
// recursive function to create the DOM children
|
|
_iterateTreeLayout: function(tree, container, overlay, selAllNodes, noShow) {
|
|
if (!tree) return;
|
|
function creator(type, cls, append, innerHTML) {
|
|
var obj = L.DomUtil.create(type, cls, append);
|
|
if (innerHTML) obj.innerHTML = innerHTML;
|
|
return obj;
|
|
}
|
|
|
|
// create the header with it fields
|
|
var header = creator('div', this.cls.header, container);
|
|
var sel = creator('span');
|
|
var entry = creator('span');
|
|
var closed = creator('span', this.cls.closed, sel, this.options.closedSymbol);
|
|
var opened = creator('span', this.cls.opened, sel, this.options.openedSymbol);
|
|
var space = creator('span', this.cls.space, null, this.options.spaceSymbol);
|
|
if (this.options.selectorBack) {
|
|
sel.insertBefore(space, closed);
|
|
header.appendChild(entry);
|
|
header.appendChild(sel);
|
|
} else {
|
|
sel.appendChild(space);
|
|
header.appendChild(sel);
|
|
header.appendChild(entry);
|
|
}
|
|
|
|
function updateSelAllCheckbox(ancestor) {
|
|
var selector = ancestor.querySelector('input[type=checkbox]');
|
|
var selectedAll = true;
|
|
var selectedNone = true;
|
|
var inputs = ancestor.querySelectorAll('input[type=checkbox]');
|
|
[].forEach.call(inputs, function(inp) { // to work in node for tests
|
|
if (inp === selector) {
|
|
// ignore
|
|
} else if (inp.indeterminate) {
|
|
selectedAll = false;
|
|
selectedNone = false;
|
|
} else if (inp.checked) {
|
|
selectedNone = false;
|
|
} else if (!inp.checked) {
|
|
selectedAll = false;
|
|
}
|
|
});
|
|
if (selectedAll) {
|
|
selector.indeterminate = false;
|
|
selector.checked = true;
|
|
} else if (selectedNone) {
|
|
selector.indeterminate = false;
|
|
selector.checked = false;
|
|
} else {
|
|
selector.indeterminate = true;
|
|
selector.checked = false;
|
|
}
|
|
}
|
|
|
|
function manageSelectorsAll(input, ctx) {
|
|
selAllNodes.forEach(function(ancestor) {
|
|
L.DomEvent.on(input, 'click', function(_ev) {
|
|
updateSelAllCheckbox(ancestor);
|
|
}, ctx);
|
|
}, ctx);
|
|
}
|
|
|
|
var selAll;
|
|
if (tree.selectAllCheckbox) {
|
|
selAll = this._createCheckboxElement(false);
|
|
selAll.className += ' ' + this.cls.selAllCheckbox;
|
|
}
|
|
|
|
var hide = this.cls.hide; // To toggle state
|
|
// create the children group, with the header event click
|
|
if (tree.children) {
|
|
var children = creator('div', this.cls.children, container);
|
|
var sensible = tree.layer ? sel : header;
|
|
L.DomUtil.addClass(sensible, this.cls.pointer);
|
|
sensible.tabIndex = 0;
|
|
L.DomEvent.on(sensible, 'click keydown', function(e) {
|
|
// leaflet internal flag to prevent click propagation and collapsing tree on mobile browsers
|
|
if (this._preventClick) {
|
|
return;
|
|
}
|
|
if (e.type === 'keydown' && e.keyCode !== 32) {
|
|
return;
|
|
}
|
|
sensible.blur();
|
|
|
|
if (L.DomUtil.hasClass(opened, hide)) {
|
|
// it is not opened, so open it
|
|
L.DomUtil.addClass(closed, hide);
|
|
L.DomUtil.removeClass(opened, hide);
|
|
L.DomUtil.removeClass(children, hide);
|
|
} else {
|
|
// close it
|
|
L.DomUtil.removeClass(closed, hide);
|
|
L.DomUtil.addClass(opened, hide);
|
|
L.DomUtil.addClass(children, hide);
|
|
}
|
|
this._localExpand();
|
|
}, this);
|
|
if (selAll) {
|
|
selAllNodes.splice(0, 0, container);
|
|
}
|
|
tree.children.forEach(function(child) {
|
|
var node = creator('div', this.cls.node, children);
|
|
this._iterateTreeLayout(child, node, overlay, selAllNodes);
|
|
}, this);
|
|
if (selAll) {
|
|
selAllNodes.splice(0, 1);
|
|
}
|
|
} else {
|
|
// no children, so the selector makes no sense.
|
|
L.DomUtil.addClass(sel, this.cls.neverShow);
|
|
}
|
|
|
|
// make (or not) the label clickable to toggle the layer
|
|
var labelType;
|
|
if (tree.layer) {
|
|
if ((this.options.labelIsSelector === 'both') || // if option is set to both
|
|
(overlay && this.options.labelIsSelector === 'overlay') || // if an overlay layer and options is set to overlay
|
|
(!overlay && this.options.labelIsSelector === 'base')) { // if a base layer and option is set to base
|
|
labelType = 'label';
|
|
} else { // if option is set to something else
|
|
labelType = 'span';
|
|
}
|
|
} else {
|
|
labelType = 'span';
|
|
}
|
|
// create the input and label
|
|
var label = creator(labelType, this.cls.label, entry);
|
|
if (tree.layer) {
|
|
// now create the element like in _addItem
|
|
var checked = this._map.hasLayer(tree.layer);
|
|
var input;
|
|
var radioGroup = overlay ? tree.radioGroup : 'leaflet-base-layers_' + L.Util.stamp(this);
|
|
if (radioGroup) {
|
|
input = this._createRadioElement(radioGroup, checked);
|
|
} else {
|
|
input = this._createCheckboxElement(checked);
|
|
manageSelectorsAll(input, this);
|
|
}
|
|
if (this._layerControlInputs) {
|
|
// to keep compatibility with 1.0.3
|
|
this._layerControlInputs.push(input);
|
|
}
|
|
input.layerId = L.Util.stamp(tree.layer);
|
|
L.DomEvent.on(input, 'click', this._onInputClick, this);
|
|
label.appendChild(input);
|
|
}
|
|
|
|
function isText(variable) {
|
|
return (typeof variable === 'string' || variable instanceof String);
|
|
}
|
|
|
|
function isFunction(functionToCheck) {
|
|
return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
|
|
}
|
|
|
|
function selectAllCheckboxes(select, ctx) {
|
|
var inputs = container.getElementsByTagName('input');
|
|
for (var i = 0; i < inputs.length; i++) {
|
|
var input = inputs[i];
|
|
if (input.type !== 'checkbox') continue;
|
|
input.checked = select;
|
|
input.indeterminate = false;
|
|
}
|
|
ctx._onInputClick();
|
|
}
|
|
if (tree.selectAllCheckbox) {
|
|
// selAll is already created
|
|
label.appendChild(selAll);
|
|
if (isText(tree.selectAllCheckbox)) {
|
|
selAll.title = tree.selectAllCheckbox;
|
|
}
|
|
L.DomEvent.on(selAll, 'click', function(ev) {
|
|
ev.stopPropagation();
|
|
selectAllCheckboxes(selAll.checked, this);
|
|
}, this);
|
|
updateSelAllCheckbox(container);
|
|
manageSelectorsAll(selAll, this);
|
|
}
|
|
|
|
creator('span', this.cls.name, label, tree.label);
|
|
|
|
// hide the button which doesn't fit the collapsed state, then hide children conditionally
|
|
L.DomUtil.addClass(tree.collapsed ? opened : closed, hide);
|
|
tree.collapsed && children && L.DomUtil.addClass(children, hide);
|
|
|
|
if (noShow) {
|
|
L.DomUtil.addClass(header, this.cls.neverShow);
|
|
L.DomUtil.addClass(children, this.cls.childrenNopad);
|
|
}
|
|
|
|
var eventeds = tree.eventedClasses;
|
|
if (!(eventeds instanceof Array)) {
|
|
eventeds = [eventeds];
|
|
}
|
|
|
|
for (var e = 0; e < eventeds.length; e++) {
|
|
var evented = eventeds[e];
|
|
if (evented && evented.className) {
|
|
var obj = container.querySelector('.' + evented.className);
|
|
if (obj) {
|
|
L.DomEvent.on(obj, evented.event || 'click', (function(selectAll) {
|
|
return function(ev) {
|
|
ev.stopPropagation();
|
|
var select = isFunction(selectAll) ? selectAll(ev, container, tree, this._map) : selectAll;
|
|
if (select !== undefined && select !== null) {
|
|
selectAllCheckboxes(select, this);
|
|
}
|
|
};
|
|
})(evented.selectAll), this);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
_createCheckboxElement: function(checked) {
|
|
var input = document.createElement('input');
|
|
input.type = 'checkbox';
|
|
input.className = 'leaflet-control-layers-selector';
|
|
input.defaultChecked = checked;
|
|
return input;
|
|
},
|
|
|
|
});
|
|
|
|
L.control.layers.tree = function(base, overlays, options) {
|
|
return new L.Control.Layers.Tree(base, overlays, options);
|
|
};
|
|
|
|
}));
|