├── .npmignore ├── README.md ├── example ├── index.html ├── main.1d19a39e7c585bf6fcfc.js └── style.1d19a39e7c585bf6fcfc.css ├── package.json └── src ├── L.multiControl.css └── L.multiControl.js /.npmignore: -------------------------------------------------------------------------------- 1 | example/ 2 | example.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # L.multiControl 2 | Leaflet plugin to implements layers control with multiple functionality such as opacity, color, bringToFront, bringToBack, zoomToLayer, delete and legend. 3 | 4 | ![](https://media.giphy.com/media/G7qJ8OPXH9E77XdVJM/giphy-downsized-large.gif) 5 | 6 | ### [DEMO](https://serene-heyrovsky-2fb229.netlify.app/) 7 | 8 | ----------------------------------------------------------------------------------- 9 | ## Requirements 10 | 11 | 15 | 16 | This plugin require font awesome library to display icons in buttons. 17 | 18 | ## Install 19 | 20 | ### NPM 21 | 22 | ``` 23 | npm i leaflet-multicontrol 24 | ``` 25 | ## Supported features 26 | 31 | 32 | If you want to add support to specific feature please let me know in an issue or pull request. 33 | 34 | ## Usage Example 35 | 36 | An easy way to implement layers control with multiple functionality. This plugin detects layer properties and will displays buttons (inputs) depend on it. 37 | 38 | ### Create features 39 | 40 | ```javascript 41 | const marker = L.marker([51.5, -0.09]).addTo(map); 42 | const marker2 = L.marker([51.51, -0.09]); 43 | const marker3 = L.marker([51.52, -0.09]); 44 | const polygon = L.polygon([[51.51, -0.1],[51.5, -0.08],[51.53, -0.07],[51.50, -0.06]], {color: '#FF0000'}).addTo(map); 45 | const polygon2 = L.polygon([[51.51, -0.1],[51.5, -0.08],[51.53, -0.07],[51.50, -0.06]], {color: '#0122FF'}).addTo(map); 46 | 47 | const mylines = [{ 48 | "type": "LineString", 49 | "coordinates": [[-0.1,51.51], [-0.07,51.53]] 50 | }, { 51 | "type": "LineString", 52 | "coordinates": [[-0.1,51.5], [-0.07,51.50]] 53 | }]; 54 | const geojson = L.geoJSON(null).addTo(map); 55 | geojson.addData(mylines); 56 | 57 | const states = [{ 58 | "type": "Feature", 59 | "properties": {"party": "Republican"}, 60 | "geometry": { 61 | "type": "Polygon", 62 | "coordinates": [[ 63 | [-104.05, 48.99], 64 | [-97.22, 48.98], 65 | [-96.58, 45.94], 66 | [-104.03, 45.94], 67 | [-104.05, 48.99] 68 | ]] 69 | } 70 | }, { 71 | "type": "Feature", 72 | "properties": {"party": "Democrat"}, 73 | "geometry": { 74 | "type": "Polygon", 75 | "coordinates": [[ 76 | [-109.05, 41.00], 77 | [-102.06, 40.99], 78 | [-102.03, 36.99], 79 | [-109.04, 36.99], 80 | [-109.05, 41.00] 81 | ]] 82 | } 83 | }, { 84 | "type": "Feature", 85 | "properties": {"party": "Democrat"}, 86 | "geometry": { 87 | "type": "Polygon", 88 | "coordinates": [[ 89 | [-109.05, 41.00], 90 | [-102.06, 40.99], 91 | [-102.03, 36.99], 92 | [-109.04, 36.99], 93 | [-109.05, 41.00] 94 | ]] 95 | } 96 | }]; 97 | 98 | const geojsonStates = L.geoJSON(states, {style: function(state) { 99 | return (state.properties.party === 'Republican') 100 | ? {fillColor:'red', color:'red', opacity:1, legendLabel: state.properties.party} : {fillColor:'blue', color:'blue', opacity:1, legendLabel: state.properties.party} 101 | }}).addTo(map); 102 | 103 | ``` 104 | 105 | ### Implements 106 | 107 | ```javascript 108 | const overlays = [ 109 | {name: 'Marker', layer: marker}, 110 | {name: 'Marker2', layer: marker2}, 111 | {name: 'polygon', layer: polygon}, 112 | {name: 'polygon2', layer: polygon2}, 113 | {name: 'geojson', layer: geojson}, 114 | {name: 'geojsonStates', layer: geojsonStates}, 115 | ]; 116 | 117 | const legend = L.multiControl(overlays, {position:'topright', label: 'Control de capas'}).addTo(map); 118 | ``` 119 | #### Note: If you have a geojson with classification representation (such as example "geojsonStates") and you want visualize it in legend, you should add "legendLabel" property in L.path config that are included in style function. 120 | 121 | L.multiControl receives two arguments: 122 | 126 | 127 | ### Overlay config 128 | 129 | | Property | Type | Required | Description | 130 | | ------------|--- | -------- | ----------------------------------------- | 131 | | name | String |true| Name of the layer. | 132 | | layer | Leaflet layer | true | A leaflet layer included in Supported features | 133 | 134 | 135 | ### Options 136 | | Option | Type | Default | Description | 137 | | ------------|--- | -------- | ----------------------------------------- | 138 | | position |String | 'topright' | Position of the control. Options: [leaflet control positions](https://docs.eegeo.com/eegeo.js/v0.1.665/docs/leaflet/L.Control/#control-positions) | 139 | | label |String | 'Layer Control' | Label that will be display in the header | 140 | 141 | ## Methods 142 | 143 | | Method | Description | 144 | | ------------|----------------------------------------- | 145 | | toggle() | Method to collapse / expand the control | 146 | | addOverlay(overlay config) | Method to add Overlay into control | 147 | 148 | ## Rules 149 | 150 | 153 | 154 | 155 | 156 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | L.multiControl
-------------------------------------------------------------------------------- /example/main.1d19a39e7c585bf6fcfc.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={79:()=>{function e(e){return function(e){if(Array.isArray(e))return r(e)}(e)||function(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}(e)||function(e,n){if(!e)return;if("string"==typeof e)return r(e,n);var t=Object.prototype.toString.call(e).slice(8,-1);"Object"===t&&e.constructor&&(t=e.constructor.name);if("Map"===t||"Set"===t)return Array.from(e);if("Arguments"===t||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t))return r(e,n)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function r(e,r){(null==r||r>e.length)&&(r=e.length);for(var n=0,t=new Array(r);n0){var n=L.DomUtil.create("div");this.overlays.forEach((function(r,t){var o=e.createChild(r,t);n.append(o)})),r.append(n)}return r},createHead:function(){var e=this,r=L.DomUtil.create("div","leaflet-controllable-legend-head"),n=L.DomUtil.create("div","left"),t=L.DomUtil.create("div","right");n.innerHTML='\n \n
'.concat(this.options.label,"
\n ");var o=L.DomUtil.create("button","btn fas fa-compress"),a=L.DomUtil.create("button","btn fas fa-times");return a.addEventListener("click",(function(){return e._map.removeControl(e._container)})),o.addEventListener("click",(function(r){e.toggle(),r.stopPropagation()})),t.append(o),t.append(a),r.append(n),r.append(t),r},toggleLayer:function(e,r){e.target.checked?r.addTo(this._map):this._map.removeLayer(r)},createChild:function(r,n){var t,o=this;this.validateNames();var a=this.defineLayerElements(r.layer),i=L.DomUtil.create("div","child-container"),l=L.DomUtil.create("div","childOverlay"),c=L.DomUtil.create("div","left"),d=L.DomUtil.create("div","right"),s=L.DomUtil.create("i","fas fa-caret-right"),p=L.DomUtil.create("input","switch"),u=L.DomUtil.create("button","btn fas fa-crosshairs"),f=L.DomUtil.create("button","btn fas fa-times"),m=L.DomUtil.create("div","inputsContainer hidden");p.setAttribute("type","checkbox"),p.setAttribute("id","".concat(r.name+n));var g=L.DomUtil.create("label");g.setAttribute("for","".concat(r.name+n)),g.innerText=r.name;var h=L.DomUtil.create("div","hidden");p.addEventListener("click",(function(e){o.toggleLayer(e,r.layer),s.classList.toggle("fa-caret-right"),s.classList.toggle("fa-caret-down"),m.classList.toggle("hidden"),h.classList.toggle("hidden"),e.stopPropagation()})),null!==(t=r.layer)&&void 0!==t&&t._map&&p.click(),g.addEventListener("click",(function(e){return e.stopPropagation()})),f.addEventListener("click",(function(e){o._map.removeLayer(r.layer),i.remove(),o.overlays.splice(o.findIndexByName(r.name),1),o.evalLength(),e.stopPropagation()})),u.addEventListener("click",(function(e){a.zoom(),e.stopPropagation()})),l.addEventListener("click",(function(e){return p.click()})),c.append(s),c.append(p),c.append(g),d.append(u),d.append(f),l.append(c),l.append(d),i.append(l);var v=L.DomUtil.create("div","left"),y=L.DomUtil.create("div","right");if(m.append(v),m.append(y),i.append(m),a.opacity){var b=L.DomUtil.create("input");b.type="range",b.min=0,b.max=1,b.step=.1,b.value=a.opacity.value,b.addEventListener("input",(function(e){return a.opacity.func(e.target.value)})),v.append(b)}if(a.color&&a.legend){var x,w=document.createElement("input");if(w.type="color",w.value=a.color.value(),i.append(h),r.layer.options.style){var k=r.layer.getLayers().map((function(e){return{color:e.options.color,elemName:e.options.legendLabel}})),C=e(new Map(k.map((function(e){return[e.color,e]}))).values());x=this.createLegend(C),h.append(x)}else{var T={elemName:r.name,color:a.color.value()};x=this.createLegend([T]),h.append(x),v.prepend(w)}w.addEventListener("input",(function(e){a.color.func(e.target.value),x.firstChild.firstChild.style.backgroundColor=e.target.value}))}if(a.bringToFront){var D=L.DomUtil.create("button","btn fas fa-arrow-up");D.setAttribute("type","button"),D.addEventListener("click",(function(e){return a.bringToFront()})),y.append(D)}if(a.bringToBack){var U=L.DomUtil.create("button","btn fas fa-arrow-down");U.setAttribute("type","button"),U.addEventListener("click",(function(e){return a.bringToBack()})),y.append(U)}return i},defineLayerElements:function(e){var r=this,n={opacity:void 0,color:void 0,bringToFront:void 0,bringToBack:void 0,legend:void 0,zoom:void 0};return e instanceof L.Marker?(n.opacity={value:1,func:function(r){return e.setOpacity(r)}},n.zoom=function(){return r._map.setView(e.getLatLng(),r._map.getMaxZoom())}):e instanceof L.Polygon?(n.opacity={value:.2,func:function(r){return e.setStyle({fillOpacity:r})}},n.color={value:function(){return e.options.color||"#3388ff"},func:function(r){return e.setStyle({fillColor:r,color:r})}},n.legend=!0,n.bringToFront=function(){return e.bringToFront()},n.bringToBack=function(){return e.bringToBack()},n.zoom=function(){return r._map.fitBounds(e.getBounds())}):e instanceof L.GeoJSON&&(n.opacity={value:.2,func:function(r){return e.setStyle({fillOpacity:r,opacity:r})}},n.color={value:function(){return e.options.color||"#3388ff"},func:function(r){return e.setStyle({fillColor:r,color:r})}},n.legend=!0,n.bringToFront=function(){return e.bringToFront()},n.bringToBack=function(){return e.bringToBack()},n.zoom=function(){return r._map.fitBounds(e.getBounds())}),n},createLegend:function(e){var r=L.DomUtil.create("div","ml-2");return e.forEach((function(e){var n=document.createElement("div"),t=document.createElement("div");t.style.float="left",t.style.margin="0.1rem",t.style.width="10px",t.style.height="10px",t.style.backgroundColor=e.color,n.append(t);var o=document.createElement("div");o.innerText=e.elemName,o.style.fontSize="10px",n.append(o),r.append(n)})),r},addOverlay:function(e){this.overlays.push(e);var r=document.querySelector(".leaflet-controllable-legend-body").firstChild,n=r.childNodes.length,t=this.createChild(e,n);r.append(t)},toggle:function(){this._container.childNodes.forEach((function(e){return e.classList.toggle("hidden")}))},evalLength:function(){0===this.overlays.length&&this.toggle()},findIndexByName:function(e){return this.overlays.findIndex((function(r){return r.name===e}))},validateNames:function(){this.overlays.map((function(e){return e.name})).sort().reduce((function(e,r){if(e!==r)return r;throw new Error("Overlays names must be unique. Repeated name: '".concat(r,"'"))}))}}),L.multiControl=function(e,r){return new L.Control.multiControl(e,r)}},897:(e,r,n)=>{"use strict";n.d(r,{Z:()=>a});var t=n(645),o=n.n(t)()((function(e){return e[1]}));o.push([e.id,'.mr-2 {\r\n margin-right: 5px;\r\n}\r\n.mr-4 {\r\n margin-right: 10px;\r\n}\r\n\r\n.ml-2 {\r\n margin-left: 5px;\r\n}\r\n\r\n.pointer {\r\n cursor: pointer;\r\n}\r\n\r\n.hidden {\r\n display: none !important;\r\n}\r\n\r\n.leaflet-controllable-legend {\r\n width: auto;\r\n height: auto;\r\n background-color:white;\r\n box-shadow: 0 7px 3px -4px rgb(0 0 0 / 30%), 0 8px 8px rgb(0 0 0 / 20%);\r\n border-radius:4px;\r\n}\r\n\r\n.leaflet-controllable-legend-head{\r\n background-color:#333333;\r\n color:white;\r\n height: auto;\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 5px;\r\n font-size:0.8rem;\r\n border-top-left-radius: 4px;\r\n border-top-right-radius: 4px;\r\n}\r\n\r\n.leaflet-controllable-legend-head div{\r\n display: flex;\r\n align-items: center;\r\n}\r\n\r\n.leaflet-controllable-legend-body {\r\n min-width: 170px;\r\n height: auto;\r\n padding:0.3rem;\r\n max-height: 365px;\r\n overflow-y: auto;\r\n}\r\n.leaflet-controllable-legend-body::-webkit-scrollbar {\r\n width: 6px;\r\n}\r\n \r\n.leaflet-controllable-legend-body::-webkit-scrollbar-track {\r\n background-color: #e4e4e4;\r\n border-radius: 100px;\r\n}\r\n \r\n.leaflet-controllable-legend-body::-webkit-scrollbar-thumb {\r\n background-color: #333333af;\r\n border-radius: 100px;\r\n}\r\n\r\n.child-container{\r\n padding: 0.2rem;\r\n border: 1px solid #c4c4c4;\r\n margin-bottom: 5px;\r\n border-radius: 4px;\r\n}\r\n\r\n.inputsContainer {\r\n display: flex;\r\n align-items: center;\r\n padding: 3px;\r\n justify-content: space-between;\r\n}\r\n\r\n.childOverlay{\r\n display: flex;\r\n padding:0.2rem;\r\n cursor:pointer;\r\n justify-content: space-between;\r\n}\r\n\r\n.collapse {\r\n width: 1.8rem !important;\r\n height: 1.8rem !important;\r\n font-size: 1.8rem;\r\n padding: 0.25rem;\r\n cursor: pointer;\r\n color: #333333 !important;\r\n}\r\n\r\n.collapse:hover {\r\n background-color: #eeeeee;\r\n}\r\n\r\n.collapse:before{\r\n content: "\\f5fd";\r\n}\r\n\r\n.left {\r\n display: flex;\r\n align-items: center;\r\n margin-right: 10px;\r\n}\r\n\r\n.right {\r\n display: flex;\r\n align-items: center;\r\n}\r\n\r\n.child-container:hover{\r\n background-color:#eeeeee;\r\n}\r\n\r\n.switch {\r\n position: relative;\r\n background: white;\r\n width: 40px;\r\n height: 20px;\r\n -webkit-appearance: initial;\r\n border-radius: 10px;\r\n outline: none;\r\n cursor: pointer;\r\n border: 1px solid #ddd;\r\n}\r\n\r\n.switch:after {\r\n position: absolute;\r\n top: 5%;\r\n display: block;\r\n width: 45%;\r\n height: 90%;\r\n background: #fff;\r\n box-sizing: border-box;\r\n transition: all 0.3s ease-in 0s;\r\n color: black;\r\n border: #888 1px solid;\r\n border-radius: 10px;\r\n}\r\n\r\n.switch:after {\r\n left: 2%;\r\n content: "";\r\n}\r\n\r\n.switch:checked:after {\r\n left: 53%;\r\n content: "";\r\n background: #333333;\r\n}\r\n\r\n.childOverlay label{\r\n cursor: pointer;\r\n}\r\n\r\ninput[type="range"] {\r\n height:4px;\r\n width: 60px;\r\n accent-color:#333333;\r\n}\r\n\r\ninput[type="color"] {\r\n\twidth: 30px;\r\n\theight: 21px;\r\n\tborder: none;\r\n\tbackground: none;\r\n}\r\n\r\n.btn{\r\n border: 0;\r\n font-size: 0.7rem;\r\n\tbackground-color:transparent;\r\n\tcolor: currentColor;\r\n}\r\n\r\n.btn:hover{\r\n cursor:pointer;\r\n\topacity: 0.4\r\n}',""]);const a=o},645:e=>{"use strict";e.exports=function(e){var r=[];return r.toString=function(){return this.map((function(r){var n=e(r);return r[2]?"@media ".concat(r[2]," {").concat(n,"}"):n})).join("")},r.i=function(e,n,t){"string"==typeof e&&(e=[[null,e,""]]);var o={};if(t)for(var a=0;a{"use strict";var t,o=function(){return void 0===t&&(t=Boolean(window&&document&&document.all&&!window.atob)),t},a=function(){var e={};return function(r){if(void 0===e[r]){var n=document.querySelector(r);if(window.HTMLIFrameElement&&n instanceof window.HTMLIFrameElement)try{n=n.contentDocument.head}catch(e){n=null}e[r]=n}return e[r]}}(),i=[];function l(e){for(var r=-1,n=0;n{var r=e&&e.__esModule?()=>e.default:()=>e;return n.d(r,{a:r}),r},n.d=(e,r)=>{for(var t in r)n.o(r,t)&&!n.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},n.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),(()=>{"use strict";var e=n(379),r=n.n(e),t=n(897),o={insert:"head",singleton:!1};r()(t.Z,o);t.Z.locals;n(79);var a=L.map("map").setView([51.505,-.09],13);L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",{attribution:'© OpenStreetMap contributors'}).addTo(a);var i=L.marker([51.5,-.09]).addTo(a),l=L.marker([51.51,-.09]),c=L.marker([51.52,-.09]),d=L.polygon([[51.51,-.1],[51.5,-.08],[51.53,-.07],[51.5,-.06]],{color:"#FF0000"}).addTo(a),s=L.polygon([[51.51,-.1],[51.5,-.08],[51.53,-.07],[51.5,-.06]],{color:"#0122FF"}).addTo(a),p=L.geoJSON(null).addTo(a);p.addData([{type:"LineString",coordinates:[[-.1,51.51],[-.07,51.53]]},{type:"LineString",coordinates:[[-.1,51.5],[-.07,51.5]]}]);var u=[{name:"Marker",layer:i},{name:"Marker2",layer:l},{name:"polygon",layer:d},{name:"polygon2",layer:s},{name:"geojson",layer:p},{name:"geojsonStates",layer:L.geoJSON([{type:"Feature",properties:{party:"Republican"},geometry:{type:"Polygon",coordinates:[[[-104.05,48.99],[-97.22,48.98],[-96.58,45.94],[-104.03,45.94],[-104.05,48.99]]]}},{type:"Feature",properties:{party:"Democrat"},geometry:{type:"Polygon",coordinates:[[[-109.05,41],[-102.06,40.99],[-102.03,36.99],[-109.04,36.99],[-109.05,41]]]}},{type:"Feature",properties:{party:"Democrat"},geometry:{type:"Polygon",coordinates:[[[-109.05,41],[-102.06,40.99],[-102.03,36.99],[-109.04,36.99],[-109.05,41]]]}}],{style:function(e){return"Republican"===e.properties.party?{fillColor:"red",color:"red",opacity:1,legendLabel:e.properties.party}:{fillColor:"blue",color:"blue",opacity:1,legendLabel:e.properties.party}}}).addTo(a)}];L.multiControl(u,{position:"topright",label:"Control de capas"}).addTo(a).addOverlay({name:"Marker3",layer:c})})()})(); -------------------------------------------------------------------------------- /example/style.1d19a39e7c585bf6fcfc.css: -------------------------------------------------------------------------------- 1 | #map,body,html{height:100%;margin:0;width:100%} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leaflet-multicontrol", 3 | "version": "1.0.3", 4 | "description": "Leaflet plugin to implements layers control with multiple functionality such as opacity, color, bringToFront, bringToBack, zoomToLayer, delete and legend.", 5 | "main": "./src/L.multiControl.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/clavijojuan/L.multiControl.git" 12 | }, 13 | "keywords": [ 14 | "leaflet", 15 | "map", 16 | "layers", 17 | "control", 18 | "legend", 19 | "javascript", 20 | "webmapping", 21 | "webgis", 22 | "opacity", 23 | "color", 24 | "legend" 25 | ], 26 | "author": "Juan Camilo Clavijo Sandoval", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/clavijojuan/L.multiControl/issues" 30 | }, 31 | "homepage": "https://github.com/clavijojuan/L.multiControl#readme" 32 | } 33 | -------------------------------------------------------------------------------- /src/L.multiControl.css: -------------------------------------------------------------------------------- 1 | .mr-2 { 2 | margin-right: 5px; 3 | } 4 | .mr-4 { 5 | margin-right: 10px; 6 | } 7 | 8 | .ml-2 { 9 | margin-left: 5px; 10 | } 11 | 12 | .pointer { 13 | cursor: pointer; 14 | } 15 | 16 | .hidden { 17 | display: none !important; 18 | } 19 | 20 | .leaflet-controllable-legend { 21 | width: auto; 22 | height: auto; 23 | background-color:white; 24 | box-shadow: 0 7px 3px -4px rgb(0 0 0 / 30%), 0 8px 8px rgb(0 0 0 / 20%); 25 | border-radius:4px; 26 | } 27 | 28 | .leaflet-controllable-legend-head{ 29 | background-color:#333333; 30 | color:white; 31 | height: auto; 32 | display: flex; 33 | flex-direction: row; 34 | align-items: center; 35 | justify-content: space-between; 36 | padding: 5px; 37 | font-size:0.8rem; 38 | border-top-left-radius: 4px; 39 | border-top-right-radius: 4px; 40 | } 41 | 42 | .leaflet-controllable-legend-head div{ 43 | display: flex; 44 | align-items: center; 45 | } 46 | 47 | .leaflet-controllable-legend-body { 48 | min-width: 170px; 49 | height: auto; 50 | padding:0.3rem; 51 | max-height: 365px; 52 | overflow-y: auto; 53 | } 54 | .leaflet-controllable-legend-body::-webkit-scrollbar { 55 | width: 6px; 56 | } 57 | 58 | .leaflet-controllable-legend-body::-webkit-scrollbar-track { 59 | background-color: #e4e4e4; 60 | border-radius: 100px; 61 | } 62 | 63 | .leaflet-controllable-legend-body::-webkit-scrollbar-thumb { 64 | background-color: #333333af; 65 | border-radius: 100px; 66 | } 67 | 68 | .child-container{ 69 | padding: 0.2rem; 70 | border: 1px solid #c4c4c4; 71 | margin-bottom: 5px; 72 | border-radius: 4px; 73 | } 74 | 75 | .inputsContainer { 76 | display: flex; 77 | align-items: center; 78 | padding: 3px; 79 | justify-content: space-between; 80 | } 81 | 82 | .childOverlay{ 83 | display: flex; 84 | padding:0.2rem; 85 | cursor:pointer; 86 | justify-content: space-between; 87 | } 88 | 89 | .collapse { 90 | font-size: 1.8rem; 91 | padding: 0.25rem; 92 | cursor: pointer; 93 | color: #333333 !important; 94 | } 95 | 96 | .collapse:hover { 97 | background-color: #eeeeee; 98 | } 99 | 100 | .collapse:before{ 101 | content: "\f5fd"; 102 | } 103 | 104 | .left { 105 | display: flex; 106 | align-items: center; 107 | margin-right: 10px; 108 | } 109 | 110 | .right { 111 | display: flex; 112 | align-items: center; 113 | } 114 | 115 | .child-container:hover{ 116 | background-color:#eeeeee; 117 | } 118 | 119 | .switch { 120 | position: relative; 121 | background: white; 122 | width: 40px; 123 | height: 20px; 124 | -webkit-appearance: initial; 125 | border-radius: 10px; 126 | outline: none; 127 | cursor: pointer; 128 | border: 1px solid #ddd; 129 | } 130 | 131 | .switch:after { 132 | position: absolute; 133 | top: 5%; 134 | display: block; 135 | width: 45%; 136 | height: 90%; 137 | background: #fff; 138 | box-sizing: border-box; 139 | transition: all 0.3s ease-in 0s; 140 | color: black; 141 | border: #888 1px solid; 142 | border-radius: 10px; 143 | } 144 | 145 | .switch:after { 146 | left: 2%; 147 | content: ""; 148 | } 149 | 150 | .switch:checked:after { 151 | left: 53%; 152 | content: ""; 153 | background: #333333; 154 | } 155 | 156 | .childOverlay label{ 157 | cursor: pointer; 158 | } 159 | 160 | input[type="range"] { 161 | height:4px; 162 | width: 60px; 163 | accent-color:#333333; 164 | } 165 | 166 | input[type="color"] { 167 | width: 30px; 168 | height: 21px; 169 | border: none; 170 | background: none; 171 | } 172 | 173 | .btn{ 174 | border: 0; 175 | font-size: 0.7rem; 176 | background-color:transparent; 177 | color: currentColor; 178 | } 179 | 180 | .btn:hover{ 181 | cursor:pointer; 182 | opacity: 0.4 183 | } -------------------------------------------------------------------------------- /src/L.multiControl.js: -------------------------------------------------------------------------------- 1 | L.Control.multiControl = L.Control.extend({ 2 | options: { 3 | position: 'topright', 4 | label: 'Layer Control' 5 | }, 6 | 7 | initialize: function(overlays, options){ 8 | this.overlays = overlays; 9 | L.Util.setOptions(this, options); 10 | }, 11 | 12 | onAdd: function (map){ 13 | this._map = map 14 | const container = this._container = this.createStructure(); 15 | if(!this.overlays) this.toggle(); 16 | return container; 17 | }, 18 | 19 | createStructure: function(){ 20 | const container = L.DomUtil.create('div', 'leaflet-controllable-legend'); 21 | const collapseIcon = L.DomUtil.create('div', 'fas collapse hidden'); 22 | collapseIcon.addEventListener('click', () => this.toggle()); 23 | 24 | const head = this.createHead(); 25 | const body = this.createBody(); 26 | 27 | container.append(collapseIcon); 28 | container.append(head); 29 | container.append(body); 30 | 31 | L.DomEvent.disableClickPropagation(container); 32 | 33 | return container 34 | }, 35 | 36 | createBody: function(){ 37 | const body = L.DomUtil.create('div', 'leaflet-controllable-legend-body'); 38 | const overlaysContainer = L.DomUtil.create('div'); 39 | body.append(overlaysContainer); 40 | 41 | if(this.overlays && this.overlays.length > 0){ 42 | this.overlays.forEach((overlay, i)=>{ 43 | const div = this.createChild(overlay, i); 44 | overlaysContainer.append(div); 45 | }); 46 | } 47 | 48 | return body 49 | }, 50 | 51 | createHead() { 52 | const head = L.DomUtil.create('div', 'leaflet-controllable-legend-head'); 53 | const left = L.DomUtil.create('div', 'left'); 54 | const right = L.DomUtil.create('div', 'right'); 55 | 56 | left.innerHTML = ` 57 | 58 |
${this.options.label}
59 | ` 60 | 61 | const toggle = L.DomUtil.create('button', 'btn fas fa-compress'); 62 | const close = L.DomUtil.create('button', 'btn fas fa-times'); 63 | close.addEventListener('click', () => this._map.removeControl(this._container)); 64 | toggle.addEventListener('click', (e) => { 65 | this.toggle(); 66 | e.stopPropagation(); 67 | }); 68 | 69 | right.append(toggle); 70 | right.append(close); 71 | 72 | head.append(left); 73 | head.append(right); 74 | 75 | return head 76 | }, 77 | 78 | toggleLayer(e, layer){ 79 | if(e.target.checked){ 80 | layer.addTo(this._map) 81 | } 82 | else { 83 | this._map.removeLayer(layer) 84 | } 85 | }, 86 | 87 | createChild(overlay, i){ 88 | 89 | this.validateNames(); 90 | 91 | const elements = this.defineLayerElements(overlay.layer), 92 | div = L.DomUtil.create('div', 'child-container'), 93 | childOverlay = L.DomUtil.create('div', 'childOverlay'), 94 | left = L.DomUtil.create('div', 'left'), 95 | right = L.DomUtil.create('div', 'right'), 96 | caret = L.DomUtil.create('i', 'fas fa-caret-right'), 97 | check = L.DomUtil.create('input', 'switch'), 98 | zoom = L.DomUtil.create('button', 'btn fas fa-crosshairs'), 99 | close = L.DomUtil.create('button', 'btn fas fa-times'), 100 | inputsContainer = L.DomUtil.create('div', 'inputsContainer hidden') 101 | 102 | check.setAttribute('type', 'checkbox'); 103 | check.setAttribute('id', `${overlay.name+i}`); 104 | 105 | const label = L.DomUtil.create('label'); 106 | label.setAttribute("for", `${overlay.name+i}`); 107 | label.innerText = overlay.name; 108 | 109 | const legend = L.DomUtil.create('div', 'hidden'); 110 | 111 | check.addEventListener('click', (e) => { 112 | this.toggleLayer(e, overlay.layer); 113 | caret.classList.toggle('fa-caret-right'); 114 | caret.classList.toggle('fa-caret-down'); 115 | inputsContainer.classList.toggle('hidden'); 116 | legend.classList.toggle('hidden'); 117 | e.stopPropagation(); 118 | }); 119 | 120 | if(overlay.layer?._map) check.click(); 121 | 122 | label.addEventListener('click', e => e.stopPropagation()); 123 | 124 | close.addEventListener('click', (e) => { 125 | this._map.removeLayer(overlay.layer); 126 | div.remove(); 127 | this.overlays.splice(this.findIndexByName(overlay.name), 1) 128 | this.evalLength(); 129 | e.stopPropagation(); 130 | }) 131 | 132 | zoom.addEventListener('click', (e) => { 133 | elements.zoom(); 134 | e.stopPropagation(); 135 | }) 136 | 137 | childOverlay.addEventListener('click', e => check.click()); 138 | 139 | left.append(caret) 140 | left.append(check) 141 | left.append(label) 142 | right.append(zoom) 143 | right.append(close) 144 | 145 | childOverlay.append(left) 146 | childOverlay.append(right) 147 | 148 | 149 | div.append(childOverlay); 150 | 151 | const inputsContainerLeft = L.DomUtil.create('div', 'left'); 152 | const inputsContainerRight = L.DomUtil.create('div', 'right'); 153 | 154 | inputsContainer.append(inputsContainerLeft); 155 | inputsContainer.append(inputsContainerRight); 156 | div.append(inputsContainer); 157 | 158 | if(elements.opacity){ 159 | const range = L.DomUtil.create('input'); 160 | range.type = 'range'; 161 | range.min = 0; 162 | range.max = 1; 163 | range.step = 0.1; 164 | range.value = elements.opacity.value; 165 | range.addEventListener('input', (e) => elements.opacity.func(e.target.value)); 166 | inputsContainerLeft.append(range); 167 | } 168 | 169 | if(elements.color && elements.legend){ 170 | 171 | const color = document.createElement('input'); 172 | color.type = 'color'; 173 | color.value = elements.color.value(); 174 | 175 | div.append(legend); 176 | 177 | let legendContainer; 178 | if(overlay.layer.options.style){ 179 | const groups = overlay.layer.getLayers().map((sublayer)=>{ 180 | return { 181 | color: sublayer.options.color, 182 | elemName: sublayer.options.legendLabel || overlay.name 183 | } 184 | }); 185 | 186 | let uniques = [...new Map(groups.map((item) => [item["color"], item])).values()]; 187 | 188 | legendContainer = this.createLegend(uniques); 189 | legend.append(legendContainer); 190 | 191 | } 192 | else { 193 | const obj = { 194 | elemName: overlay.name, 195 | color: elements.color.value() 196 | }; 197 | legendContainer = this.createLegend([obj]); 198 | 199 | legend.append(legendContainer); 200 | inputsContainerLeft.prepend(color); 201 | } 202 | 203 | color.addEventListener('input', (e) => { 204 | elements.color.func(e.target.value); 205 | legendContainer.firstChild.firstChild.style.backgroundColor = e.target.value 206 | }); 207 | } 208 | 209 | if(elements.bringToFront){ 210 | const front = L.DomUtil.create('button', 'btn fas fa-arrow-up'); 211 | front.setAttribute('type', 'button'); 212 | front.addEventListener('click', (e) => elements.bringToFront()); 213 | inputsContainerRight.append(front); 214 | } 215 | 216 | if(elements.bringToBack){ 217 | const back = L.DomUtil.create('button', 'btn fas fa-arrow-down'); 218 | back.setAttribute('type', 'button'); 219 | back.addEventListener('click', (e) => elements.bringToBack()); 220 | inputsContainerRight.append(back); 221 | } 222 | 223 | return div 224 | }, 225 | 226 | defineLayerElements(layer){ 227 | let elements = { 228 | opacity: undefined, 229 | color:undefined, 230 | bringToFront: undefined, 231 | bringToBack: undefined , 232 | legend:undefined, 233 | zoom: undefined 234 | } 235 | 236 | if(layer instanceof L.Marker){ 237 | elements.opacity = { 238 | value: 1, 239 | func: (value) => layer.setOpacity(value) 240 | }; 241 | elements.zoom = () => this._map.setView(layer.getLatLng(), this._map.getMaxZoom()); 242 | } 243 | else if(layer instanceof L.Polygon){ 244 | elements.opacity = { 245 | value: 0.2, 246 | func: (value) => layer.setStyle({fillOpacity:value}) 247 | }; 248 | elements.color = { 249 | value: () => {return layer.options.color || '#3388ff'}, 250 | func: (value) => layer.setStyle({fillColor:value, color:value}) 251 | }; 252 | elements.legend = true; 253 | elements.bringToFront = () => layer.bringToFront(); 254 | elements.bringToBack = () => layer.bringToBack(); 255 | elements.zoom = () => this._map.fitBounds(layer.getBounds()); 256 | } 257 | else if(layer instanceof L.GeoJSON){ 258 | elements.opacity = { 259 | value: 0.2, 260 | func: (value) => layer.setStyle({fillOpacity:value, opacity: value}) 261 | }; 262 | elements.color = { 263 | value: () => {return layer.options.color || '#3388ff'}, 264 | func: (value) => layer.setStyle({fillColor:value, color:value}) 265 | }; 266 | elements.legend = true; 267 | elements.bringToFront = () => layer.bringToFront(); 268 | elements.bringToBack = () => layer.bringToBack(); 269 | elements.zoom = () => this._map.fitBounds(layer.getBounds()); 270 | } 271 | 272 | return elements 273 | }, 274 | 275 | createLegend(arr){ 276 | 277 | const container = L.DomUtil.create('div', 'ml-2'); 278 | arr.forEach((elem) => { 279 | const div = document.createElement('div'); 280 | 281 | const square = document.createElement('div'); 282 | square.style.float = 'left'; 283 | square.style.margin = '0.1rem'; 284 | square.style.width = '10px'; 285 | square.style.height = '10px'; 286 | square.style.backgroundColor = elem.color; 287 | div.append(square); 288 | 289 | const txt = document.createElement('div'); 290 | txt.innerText = elem.elemName; 291 | txt.style.fontSize = '10px'; 292 | div.append(txt); 293 | 294 | container.append(div); 295 | }); 296 | 297 | return container; 298 | }, 299 | 300 | addOverlay(layer){ 301 | 302 | if(!this.overlays) { 303 | this.overlays = []; 304 | this.toggle(); 305 | } 306 | 307 | this.overlays.push(layer); 308 | 309 | const body = document.querySelector('.leaflet-controllable-legend-body'); 310 | const overlaysContainer = body.firstChild; 311 | const index = overlaysContainer?.childNodes?.length || 0; 312 | const div = this.createChild(layer, index) 313 | 314 | overlaysContainer.append(div); 315 | }, 316 | 317 | toggle(){ 318 | this._container.childNodes.forEach(child => child.classList.toggle('hidden')); 319 | }, 320 | 321 | evalLength(){ 322 | if(this.overlays.length === 0) this.toggle(); 323 | }, 324 | 325 | findIndexByName(name){ 326 | return this.overlays.findIndex(overlay => overlay.name === name) 327 | }, 328 | 329 | validateNames(){ 330 | const names = (this.overlays.map(overlay => overlay.name)).sort(); 331 | names.reduce((pv, cv) => { 332 | if(pv !== cv ) return cv 333 | else throw new Error(`Overlays names must be unique. Repeated name: '${cv}'`) 334 | }) 335 | } 336 | }) 337 | 338 | L.multiControl = function(overlays, options){ 339 | return new L.Control.multiControl(overlays, options); 340 | } 341 | --------------------------------------------------------------------------------