├── AUTHORS.md ├── LICENSE ├── README.md ├── bower.json ├── demo ├── css │ └── main.css ├── index.html ├── js │ └── main.js └── ss.png ├── leaflet-marker-menu.js └── leaflet-marker-menu.min.js /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | * [umurgdk](https://github.com/umurgdk) 4 | * [adriennn](https://github.com/adriennn) 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Umur Gedik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Leaflet Marker Menu Plugin 2 | 3 | This plugin adds animated circular menus to your leaflet markers. Unfortunately there is only support for css3 animations. 4 | Please see the demo page. 5 | 6 | ![Leaflet Marker Menu Demo Screenshot](https://raw.github.com/umurgdk/leaflet-marker-menu/master/demo/ss.png) 7 | 8 | ###Usage 9 | 10 | ```js 11 | var marker = L.marker([-33.4373, -70.6437]); 12 | marker.setMenu({ 13 | radius: 75, // radius of the circle, 14 | size: [24, 24], // menu items width and height 15 | animate: true, // [OPTIONAL] default true 16 | duration: 200, // [OPTIONAL] animate duration defaults 200ms 17 | 18 | items: [{ 19 | title: "Menu Item 1", // [OPTIONAL] will be link's title attribute 20 | className: "extra-class", // [OPTIONAL] you can add your css classes 21 | click: function () { // callback function fired on click. this points to item 22 | console.log("Menu Item 1"); 23 | } 24 | }, { 25 | title: "Menu Item 2", 26 | className: "extra-class red-circle" 27 | click: function () { 28 | console.log("Menu Item 2"); 29 | } 30 | }] 31 | }); 32 | 33 | // You can open menus programmatically 34 | marker.openMenu(); 35 | 36 | // or close 37 | marker.closeMenu(); 38 | 39 | // You can remove them 40 | marker.removeMenu(); 41 | 42 | // You can get the menu object 43 | var menu = marker.getMenu(); 44 | 45 | // and you can add items to it 46 | menu.append([{ 47 | title: 'another', 48 | click: function () { 49 | console.log('another'); 50 | } 51 | }]); 52 | 53 | menu.hide() // hide (display: none) 54 | menu.show() // show (display: block) 55 | 56 | // You can create menu's separated from marker 57 | var menu2 = L.markerMenu({ 58 | radius: 75, 59 | size: [24, 24], 60 | animate: true, 61 | duration: 200, 62 | 63 | items: [{ 64 | title: "Menu Item 1", 65 | className: "extra-class", 66 | click: function () { 67 | console.log("Menu Item 1"); 68 | } 69 | }] 70 | }); 71 | 72 | // and you can add them to markers 73 | marker.setMenu(menu2); 74 | ``` 75 | 76 | 77 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/umurgdk/leaflet-marker-menu/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 78 | 79 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Leaflet Marker Menu", 3 | "version": "0.0.1", 4 | "authors": [ 5 | "Umur Gedik " 6 | ], 7 | "description": "Circular menu for markers (see demo)", 8 | "main": "src/leaflet-marker-menu.js", 9 | "keywords": [ 10 | "leaflet", 11 | "map", 12 | "gis", 13 | "mapbox", 14 | "menu", 15 | "css3" 16 | ], 17 | "license": "MIT", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "test", 23 | "tests" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /demo/css/main.css: -------------------------------------------------------------------------------- 1 | #map { 2 | height: 400px; 3 | } 4 | 5 | .leaflet-marker-menu-item { 6 | background: #f00; 7 | border-radius: 12px; 8 | } 9 | 10 | .item-2 { 11 | background: #06f; 12 | border: 2px solid #04d; 13 | box-shadow: 0 2px 2px rgba(0,0,0,0.3); 14 | } -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Leaflet 6 | 7 | 8 | 9 | 10 |

Leaflet Marker Menu Plugin Demo Page

11 |
12 | 13 | Fork me on GitHub 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /demo/js/main.js: -------------------------------------------------------------------------------- 1 | var App = { 2 | initialize: function () { 3 | var map = new L.Map('map'); 4 | 5 | map.setView(new L.LatLng(40.990475, 29.029183), 15); 6 | map.addLayer(new L.TileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png')); 7 | 8 | var marker = L.marker([40.990475, 29.029183]); 9 | 10 | marker.setMenu({ 11 | radius: 75, 12 | 13 | items: [{ 14 | title: "Menu 1", 15 | click: function () { 16 | alert("Menu 1"); 17 | } 18 | }, { 19 | title: "Ozel Menu", 20 | className: 'item-2', 21 | click: function () { 22 | alert("I have a special class"); 23 | } 24 | }, { 25 | title: "Another one!", 26 | click: function () { 27 | alert("Wohoo there is too much!"); 28 | } 29 | }, { 30 | title: "Another meh!", 31 | click: function () { 32 | alert("Another meh!"); 33 | } 34 | }, { 35 | title: "Im get boring!", 36 | click: function () { 37 | alert("don't!"); 38 | } 39 | }] 40 | }); 41 | 42 | map.addLayer(marker); 43 | } 44 | }; 45 | 46 | App.initialize(); -------------------------------------------------------------------------------- /demo/ss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umurgdk/leaflet-marker-menu/86adc405d62602f519c82a05829a67aea9bc1a39/demo/ss.png -------------------------------------------------------------------------------- /leaflet-marker-menu.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | function array_map(array, cb, context) { 3 | for (var i = 0, l = array.length; i < l; i++) { 4 | cb.call(context || null, array[i], i, array); 5 | } 6 | } 7 | 8 | L.MarkerMenu = (L.Layer ? L.Layer : L.Class).extend({ 9 | includes: L.Mixin.Events, 10 | 11 | options: { 12 | pane: 'menuPane', 13 | radius: 100, 14 | animate: true, 15 | duration: 200, 16 | size: [24, 24] 17 | }, 18 | 19 | initialize: function (options, source) { 20 | L.setOptions(this, options); 21 | 22 | this._items = []; 23 | 24 | // DOM Element represents ul 25 | this._menuList = null; 26 | 27 | // DOM Elements array represents li's 28 | this._menuItems = []; 29 | 30 | this._appendItems(this.options.items); 31 | 32 | this._source = source; 33 | this._isOpened = false; 34 | }, 35 | 36 | hide: function () { 37 | this._container.style.display = 'none'; 38 | }, 39 | 40 | show: function () { 41 | this._container.style.display = 'block'; 42 | }, 43 | 44 | getItems: function () { 45 | return this._items; 46 | }, 47 | 48 | append: function (items) { 49 | if (items instanceof L.MarkerMenu) { 50 | this._appendItems(items.getItems()); 51 | } else { 52 | this._appendItems(items); 53 | } 54 | }, 55 | 56 | getLatLng: function () { 57 | return this._latlng; 58 | }, 59 | 60 | setLatLng: function (latlng) { 61 | this._latlng = latlng; 62 | 63 | if (this._map) { 64 | this._updatePosition(); 65 | } 66 | 67 | return this; 68 | }, 69 | 70 | onAdd: function (map) { 71 | this._map = map; 72 | 73 | if (!this._container) { 74 | this._initLayout(); 75 | this._updateMenu(); 76 | } 77 | 78 | map.on('zoomstart', this._onZoomStart, this); 79 | map.on('zoomend', this._onZoomEnd, this); 80 | 81 | this._updatePosition(); 82 | 83 | map.getPanes().markerPane.appendChild(this._container); 84 | 85 | if (this._source) { 86 | this._source.fire('menuopen', {menu: this}); 87 | } 88 | }, 89 | 90 | onRemove: function (map) { 91 | var that = this; 92 | 93 | this._resetItemsPositions(); 94 | 95 | map.off('zoomstart', this._onZoomStart); 96 | map.off('zoomend', this._onZoomEnd); 97 | 98 | setTimeout(function () { 99 | that._map = null; 100 | 101 | map.getPanes().markerPane.removeChild(that._container); 102 | 103 | if (that._source) { 104 | that._source.fire('menuclose', {menu: that}); 105 | } 106 | }, this.options.duration); 107 | }, 108 | 109 | _onZoomStart: function () { 110 | this.hide(); 111 | }, 112 | 113 | _onZoomEnd: function () { 114 | this._updatePosition(); 115 | this.show(); 116 | }, 117 | 118 | _appendItems: function (items) { 119 | array_map(items, function (item) { 120 | this._items.push(item); 121 | }, this); 122 | 123 | this._updateMenu(); 124 | this._updatePosition(); 125 | }, 126 | 127 | _initLayout: function () { 128 | var prefix = 'leaflet-marker-menu'; 129 | 130 | this._container = L.DomUtil.create('div', 131 | prefix + ' ' + (this.options.className || '') + ' leaflet-zoom-hide'); 132 | 133 | this._container.style.zIndex = -1; 134 | this._container.style.position = 'absolute'; 135 | }, 136 | 137 | _updateMenu: function () { 138 | if (!this._container) { 139 | return; 140 | } 141 | 142 | var prefix = 'leaflet-marker-menu-item'; 143 | 144 | if (this._menuList) { 145 | this._container.removeChild(this._menuList); 146 | } 147 | 148 | this._menuList = L.DomUtil.create('ul', '', this._container); 149 | this._menuItems = []; 150 | 151 | this._menuList.style.listStyle = 'none'; 152 | this._menuList.style.padding = 0; 153 | this._menuList.style.margin = 0; 154 | this._menuList.style.position = 'relative'; 155 | this._menuList.style.width = this.options.size[0] + 'px'; 156 | this._menuList.style.height = this.options.size[1] + 'px'; 157 | 158 | array_map(this._items, function (item, i, items) { 159 | var listItem = L.DomUtil.create('li','', this._menuList), 160 | menuItem = L.DomUtil.create('a', prefix + ' ' + (this.options.itemClassName || '') + 161 | ' ' + (item.className || ''), listItem); 162 | 163 | menuItem.style.width = this.options.size[0] + 'px'; 164 | menuItem.style.height = this.options.size[1] + 'px'; 165 | menuItem.style.display = 'block'; 166 | menuItem.style.cursor = 'pointer'; 167 | 168 | menuItem.title = item.title; 169 | 170 | listItem.style.zIndex = 10000000; 171 | listItem.style.position = 'absolute'; 172 | 173 | // only css3 for a now needs implementation something like PosAnimation for opacity 174 | if (this.options.animate) { 175 | listItem.style.left = 0; 176 | listItem.style.bottom = 0; 177 | 178 | L.DomUtil.setOpacity(listItem, 0); 179 | 180 | listItem.style[L.DomUtil.TRANSITION] = 'all ' + (this.options.duration / 1000) + 's'; 181 | } 182 | 183 | L.DomEvent.addListener(menuItem, 'click', item.click, item); 184 | 185 | this._menuItems.push(listItem); 186 | }, this); 187 | 188 | this._menuItems.reverse(); 189 | }, 190 | 191 | _resetItemsPositions: function () { 192 | array_map(this._menuItems, function (item) { 193 | item.style.left = 0; 194 | item.style.bottom = 0; 195 | 196 | L.DomUtil.setOpacity(item, 0); 197 | }, this); 198 | }, 199 | 200 | _updatePosition: function () { 201 | if (!this._map) { 202 | return; 203 | } 204 | 205 | var pos = this._map.latLngToLayerPoint(this._latlng), 206 | offset = L.point(this.options.offset || [0, 0]), 207 | container = this._container, 208 | style = container.style, 209 | angle = Math.PI / (this._menuItems.length + 1), 210 | radius = this.options.radius, 211 | that = this; 212 | 213 | var bottom = this._containerBottom = -offset.y - pos.y; 214 | var left = this._containerLeft = offset.x + pos.x; 215 | 216 | this._container.style.bottom = (bottom + (this.options.size[1] / 2)) + 'px'; 217 | this._container.style.left = (left - (this.options.size[0] / 2)) + 'px'; 218 | 219 | setTimeout(function () { 220 | array_map(that._menuItems, function (item, i) { 221 | var itemLeft = (Math.cos(angle * (i + 1)) * radius), 222 | itemBottom = (Math.sin(angle * (i + 1)) * radius) ; 223 | 224 | item.style.left = Math.round(itemLeft) + 'px'; 225 | item.style.bottom = Math.round(itemBottom) + 'px'; 226 | 227 | L.DomUtil.setOpacity(item, 1); 228 | }, this); 229 | }, 0); 230 | }, 231 | 232 | _resetPosition: function () { 233 | array_map(this._menuItems, function (item) { 234 | item.style.left = 0 + 'px'; 235 | item.style.bottom = 0 + 'px'; 236 | 237 | L.DomUtil.setOpacity(item, 0); 238 | }, this); 239 | } 240 | }); 241 | 242 | L.markerMenu = function (options, source) { 243 | return new L.MarkerMenu(options, source); 244 | }; 245 | 246 | L.Marker.include({ 247 | openMenu: function () { 248 | if (!this._menu) { 249 | return; 250 | } 251 | 252 | this._menu.setLatLng(this._latlng); 253 | this._menu._isOpened = true; 254 | 255 | this._map.addLayer(this._menu); 256 | 257 | return this; 258 | }, 259 | 260 | closeMenu: function () { 261 | if (!this._menu) { 262 | return; 263 | } 264 | 265 | this._map.removeLayer(this._menu); 266 | this._menu._isOpened = false; 267 | }, 268 | 269 | toggleMenu: function () { 270 | if (!this._menu) { 271 | return; 272 | } 273 | 274 | if (this._menu._isOpened) { 275 | this.closeMenu(); 276 | } else { 277 | this.openMenu(); 278 | } 279 | }, 280 | 281 | getMenu: function () { 282 | return this._menu; 283 | }, 284 | 285 | setMenu: function (options) { 286 | if (!options instanceof L.MarkerMenu && options.items.length === 0) { 287 | return; 288 | } 289 | 290 | var that = this; 291 | 292 | if (this._menu) { 293 | this.removeMenu(); 294 | } 295 | 296 | if (options instanceof L.MarkerMenu) { 297 | this._menu = options; 298 | } else { 299 | this._menu = L.markerMenu(options, this); 300 | } 301 | 302 | this.on('click', this.toggleMenu, this); 303 | this.on('move', this._moveMenu, this); 304 | this.on('remove', this.closeMenu, this); 305 | }, 306 | 307 | removeMenu: function () { 308 | if (!this._menu) { 309 | return; 310 | } 311 | 312 | this.off('click', this.toggleMenu); 313 | this.off('move', this._moveMenu); 314 | this.off('remove', this.closeMenu); 315 | 316 | this.closeMenu(); 317 | this._menu = null; 318 | }, 319 | 320 | _moveMenu: function (e) { 321 | if (!this._menu) { 322 | return; 323 | } 324 | 325 | this._menu.setLatLng(this._latlng); 326 | }, 327 | }); 328 | }()); 329 | -------------------------------------------------------------------------------- /leaflet-marker-menu.min.js: -------------------------------------------------------------------------------- 1 | !function(){function t(t,e,i){for(var s=0,n=t.length;n>s;s++)e.call(i||null,t[s],s,t)}L.MarkerMenu=L.Class.extend({includes:L.Mixin.Events,options:{pane:"menuPane",radius:100,animate:!0,duration:200,size:[24,24]},initialize:function(t,e){L.setOptions(this,t),this._items=[],this._menuList=null,this._menuItems=[],this._appendItems(this.options.items),this._source=e,this._isOpened=!1},hide:function(){this._container.style.display="none"},show:function(){this._container.style.display="block"},getItems:function(){return this._items},append:function(t){t instanceof L.MarkerMenu?this._appendItems(t.getItems()):this._appendItems(t)},getLatLng:function(){return this._latlng},setLatLng:function(t){return this._latlng=t,this._map&&this._updatePosition(),this},onAdd:function(t){this._map=t,this._container||(this._initLayout(),this._updateMenu()),t.on("zoomstart",this._onZoomStart,this),t.on("zoomend",this._onZoomEnd,this),this._updatePosition(),t.getPanes().markerPane.appendChild(this._container),this._source&&this._source.fire("menuopen",{menu:this})},onRemove:function(t){var e=this;this._resetItemsPositions(),t.off("zoomstart",this._onZoomStart),t.off("zoomend",this._onZoomEnd),setTimeout(function(){e._map=null,t.getPanes().markerPane.removeChild(e._container),e._source&&e._source.fire("menuclose",{menu:e})},this.options.duration)},_onZoomStart:function(){this.hide()},_onZoomEnd:function(){this._updatePosition(),this.show()},_appendItems:function(e){t(e,function(t){this._items.push(t)},this),this._updateMenu(),this._updatePosition()},_initLayout:function(){var t="leaflet-marker-menu";this._container=L.DomUtil.create("div",t+" "+(this.options.className||"")+" leaflet-zoom-hide"),this._container.style.zIndex=-1,this._container.style.position="absolute"},_updateMenu:function(){if(this._container){var e="leaflet-marker-menu-item";this._menuList&&this._container.removeChild(this._menuList),this._menuList=L.DomUtil.create("ul","",this._container),this._menuItems=[],this._menuList.style.listStyle="none",this._menuList.style.padding=0,this._menuList.style.margin=0,this._menuList.style.position="relative",this._menuList.style.width=this.options.size[0]+"px",this._menuList.style.height=this.options.size[1]+"px",t(this._items,function(t){var i=L.DomUtil.create("li","",this._menuList),s=L.DomUtil.create("a",e+" "+(this.options.itemClassName||"")+" "+(t.className||""),i);s.style.width=this.options.size[0]+"px",s.style.height=this.options.size[1]+"px",s.style.display="block",s.style.cursor="pointer",s.title=t.title,i.style.zIndex=1e7,i.style.position="absolute",this.options.animate&&(i.style.left=0,i.style.bottom=0,L.DomUtil.setOpacity(i,0),i.style[L.DomUtil.TRANSITION]="all "+this.options.duration/1e3+"s"),L.DomEvent.addListener(s,"click",t.click,t),this._menuItems.push(i)},this),this._menuItems.reverse()}},_resetItemsPositions:function(){t(this._menuItems,function(t){t.style.left=0,t.style.bottom=0,L.DomUtil.setOpacity(t,0)},this)},_updatePosition:function(){if(this._map){var e=this._map.latLngToLayerPoint(this._latlng),i=L.point(this.options.offset||[0,0]),s=this._container,n=(s.style,Math.PI/(this._menuItems.length+1)),o=this.options.radius,h=this,u=this._containerBottom=-i.y-e.y,a=this._containerLeft=i.x+e.x;this._container.style.bottom=u+this.options.size[1]/2+"px",this._container.style.left=a-this.options.size[0]/2+"px",setTimeout(function(){t(h._menuItems,function(t,e){var i=Math.cos(n*(e+1))*o,s=Math.sin(n*(e+1))*o;t.style.left=Math.round(i)+"px",t.style.bottom=Math.round(s)+"px",L.DomUtil.setOpacity(t,1)},this)},0)}},_resetPosition:function(){t(this._menuItems,function(t){t.style.left="0px",t.style.bottom="0px",L.DomUtil.setOpacity(t,0)},this)}}),L.markerMenu=function(t,e){return new L.MarkerMenu(t,e)},L.Marker.include({openMenu:function(){return this._menu?(this._menu.setLatLng(this._latlng),this._menu._isOpened=!0,this._map.addLayer(this._menu),this):void 0},closeMenu:function(){this._menu&&(this._map.removeLayer(this._menu),this._menu._isOpened=!1)},toggleMenu:function(){this._menu&&(this._menu._isOpened?this.closeMenu():this.openMenu())},getMenu:function(){return this._menu},setMenu:function(t){if(!(!t instanceof L.MarkerMenu&&0===t.items.length)){this._menu&&this.removeMenu(),this._menu=t instanceof L.MarkerMenu?t:L.markerMenu(t,this),this.on("click",this.toggleMenu,this),this.on("move",this._moveMenu,this),this.on("remove",this.closeMenu,this)}},removeMenu:function(){this._menu&&(this.off("click",this.toggleMenu),this.off("move",this._moveMenu),this.off("remove",this.closeMenu),this._closeMenu(),this._menu=null)},_moveMenu:function(){this._menu&&this._menu.setLatLng(this._latlng)}})}(); --------------------------------------------------------------------------------