├── 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 | 
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 | [](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 |
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)}})}();
--------------------------------------------------------------------------------