├── index.html
├── dist
└── slidingpanel.css
└── src
└── slidingpanel.js
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Leaflet Sliding Panel
7 |
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/dist/slidingpanel.css:
--------------------------------------------------------------------------------
1 | .lsp-container {
2 | position: absolute;
3 | left: 0;
4 | right: 0;
5 | margin-left: auto;
6 | margin-right: auto;
7 | top: 100%;
8 | width: 100%;
9 | height: 100%;
10 | max-width: 70em;
11 | z-index: 2000;
12 | background-color: white;
13 | transition: top 0.3s;
14 | overflow: hidden;
15 | transform: translate3d(0px, 0px, 0px);
16 | cursor: auto;
17 | }
18 | .lsp-container.lsp-touch {
19 | cursor: grab;
20 | }
21 | .lsp-handle {
22 | margin-left: 40%;
23 | margin-right: 40%;
24 | display: none;
25 | }
26 | .lsp-touch .lsp-handle {
27 | display: block;
28 | }
29 | .lsp-nav {
30 | position: absolute;
31 | right: 0.25em;
32 | top: 0.25em;
33 | z-index: 3000;
34 | }
35 | .lsp-nav button {
36 | color: #999;
37 | border: 1px solid #999;
38 | font-size: 1em;
39 | width: 1.7em;
40 | height: 1.7em;
41 | border-radius: 0.4em;
42 | margin: 0.15em;
43 | padding: 0;
44 | text-align: center;
45 | background-color: white;
46 | opacity: 0.5;
47 | outline: 0;
48 | float: right;
49 | cursor: pointer;
50 | }
51 | .lsp-nav button:hover {
52 | opacity: 0.75;
53 | }
54 | .lsp-touch .lsp-nav button {
55 | opacity: 0.3;
56 | }
57 |
58 | .lsp-header {
59 | margin-top: 0;
60 | padding-bottom: 0.5em;
61 | margin-bottom: 0.5em;
62 | border-bottom: 1px solid #ccc;
63 | }
64 |
65 | .lsp-container.lsp-minimal {
66 | top: calc(100% - 10em);
67 | }
68 | .lsp-container.lsp-medium {
69 | top: 45%;
70 | }
71 | .lsp-container.lsp-full {
72 | top: 5%;
73 | }
74 |
75 | .lsp-content {
76 | white-space: nowrap;
77 | min-height: 100%;
78 | }
79 | .lsp-card {
80 | width: 100%;
81 | max-width: 100%;
82 | padding: 0.5em;
83 | box-sizing: border-box;
84 | overflow: hidden;
85 | display: inline-block;
86 | white-space: normal;
87 | vertical-align: top;
88 | min-height: 100%;
89 | transition: transform 0.3s;
90 | }
91 |
92 | .lsp-card img {
93 | max-width: 100% !important;
94 | }
95 |
--------------------------------------------------------------------------------
/src/slidingpanel.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * leaflet-sliding-panel
3 | * A mobile-friendly Leaflet popup alternative
4 | * (c) 2017, Houston Engineering, Inc.
5 | * MIT LICENSE
6 | */
7 |
8 | (function (factory) {
9 | // Module systems magic dance, Leaflet edition
10 | if (typeof define === 'function' && define.amd) {
11 | // AMD
12 | define(['leaflet'], factory);
13 | } else if (typeof module !== 'undefined') {
14 | // Node/CommonJS
15 | module.exports = factory(require('leaflet'));
16 | } else {
17 | // Browser globals
18 | if (typeof this.L === 'undefined')
19 | throw 'Leaflet must be loaded first!';
20 | factory(this.L);
21 | }
22 | }(function (L) {
23 |
24 | // var SlidingPanel = L.Evented.extend({ // Leaflet 1.0
25 | var SlidingPanel = L.Class.extend({
26 | 'includes': L.Mixin.Events,
27 |
28 | 'options': {
29 | 'parentElement': null,
30 | 'threshold': 50,
31 | 'doubleThreshold': 200,
32 | 'activeFeatureColor': 'cyan',
33 | 'inactiveFeatureColor': 'blue'
34 | },
35 | 'initialize': function(options) {
36 | L.Util.setOptions(this, options);
37 | this.on('swipeup', function(evt) {
38 | if (evt.value > this.options.doubleThreshold) {
39 | this.full();
40 | } else {
41 | this.expand();
42 | }
43 | });
44 | this.on('swipedown', function(evt) {
45 | if (evt.value > this.options.doubleThreshold) {
46 | this.close();
47 | } else {
48 | this.shrink();
49 | }
50 | });
51 | this.on('swipeleft', function(evt) {
52 | this.navRight();
53 | });
54 | this.on('swiperight', function(evt) {
55 | this.navLeft();
56 | });
57 | this.setContent(options.content || []);
58 | this.setFeatures(options.features || []);
59 | this.setState(options.state || 'closed');
60 | },
61 | 'open': function() {
62 | this.setState('minimal');
63 | },
64 | 'minimize': function() {
65 | this.setState('minimal');
66 | },
67 | 'full': function() {
68 | this.setState('full');
69 | },
70 | 'close': function() {
71 | this.setState('closed');
72 | },
73 | 'expand': function() {
74 | this.setState(this._expandMap[this._state]);
75 | },
76 | 'shrink': function() {
77 | this.setState(this._shrinkMap[this._state]);
78 | },
79 | 'setState': function(state) {
80 | if (this._state && this._container) {
81 | L.DomUtil.removeClass(this._container, 'lsp-' + this._state);
82 | }
83 | this._state = state;
84 | if (this._container) {
85 | L.DomUtil.addClass(this._container, 'lsp-' + state);
86 | }
87 | if (state != 'closed' && this._map) {
88 | this._map.once('click', function(evt) {
89 | this.close();
90 | }, this);
91 | }
92 | if (state == 'closed') {
93 | this._currentIndex = 0;
94 | if (this._featureGroup) {
95 | this._featureGroup.clearLayers();
96 | }
97 | }
98 | this._updateNav();
99 | },
100 | 'setContent': function(content) {
101 | if (!L.Util.isArray(content)) {
102 | content = [content];
103 | }
104 | this._content = content;
105 | this._updateContent();
106 | },
107 | 'setFeatures': function(features) {
108 | if (!features) {
109 | features = [];
110 | } else if (features && features.type && features.features) {
111 | features = features.features;
112 | }
113 | this._features = features;
114 | features = features.map(function(feature, index) {
115 | feature._index = index;
116 | return feature;
117 | });
118 | this._initFeatureGroup();
119 | this._featureGroup.clearLayers().addData(features.reverse());
120 | this._updateNav();
121 | },
122 | '_initFeatureGroup': function() {
123 | if (!this._featureGroup) {
124 | this._featureGroup = L.geoJson(null, {
125 | 'pointToLayer': function (feature, latlng) {
126 | return L.circleMarker(latlng, {
127 | 'radius': 8
128 | });
129 | }
130 | });
131 | this._featureGroup.on('click', function(evt) {
132 | if (evt.layer && evt.layer.feature) {
133 | this.navigateTo(evt.layer.feature._index);
134 | }
135 | }, this);
136 | }
137 | if (this._map) {
138 | if (this._featureGroup._map !== this._map) {
139 | this._featureGroup.addTo(this._map);
140 | }
141 | }
142 | },
143 | 'addTo': function(map) {
144 | // c.f. L.Control.onAdd
145 | this.remove();
146 | this._map = map;
147 | this._container = this.onAdd(map);
148 | L.DomEvent.disableClickPropagation(this._container);
149 | this._contentNode = L.DomUtil.create(
150 | 'div', 'lsp-content', this._container
151 | );
152 | L.DomEvent.on(this._contentNode, 'touchstart', this._startSwipe, this);
153 | L.DomEvent.on(this._contentNode, 'touchend', this._endSwipe, this);
154 | this._updateContent();
155 | var parentElement = this.options.parentElement || map._container;
156 | parentElement.appendChild(this._container);
157 | if (this._state) {
158 | this.setState(this._state);
159 | }
160 | if (this._features) {
161 | this.setFeatures(this._features);
162 | }
163 | return this;
164 | },
165 | 'remove': function() {
166 | if (!this._map) {
167 | return;
168 | }
169 | if (this._featureGroup) {
170 | this._map.removeLayer(this._featureGroup);
171 | }
172 | this._map = null;
173 |
174 | // L.DomUtil.remove(this._container); // Leaflet 1.0
175 | var parentNode = this._container.parentNode;
176 | if (parentNode) {
177 | parentNode.removeChild(this._container);
178 | }
179 |
180 | return this;
181 | },
182 | 'onAdd': function(map) {
183 | var container = L.DomUtil.create('div', 'lsp-container');
184 | if (L.Browser.touch) {
185 | L.DomUtil.addClass(container, 'lsp-touch');
186 | }
187 |
188 | var hr = L.DomUtil.create('hr', 'lsp-handle', container);
189 | L.DomEvent.on(hr, 'click', this.expand, this);
190 |
191 | var navbar = L.DomUtil.create('div', 'lsp-nav', container);
192 |
193 | this._createButton('close', '×', navbar);
194 | this._createButton('minimize', '—', navbar);
195 | this._createButton('full', '□', navbar); // Future: 🗖
196 | this._createButton('navRight', '❯', navbar);
197 | this._createButton('navLeft', '❮', navbar);
198 |
199 | this._updateNav();
200 |
201 | return container;
202 | },
203 | 'navRight': function() {
204 | this._navigate(1);
205 | },
206 | 'navLeft': function() {
207 | this._navigate(-1);
208 | },
209 | '_updateContent': function() {
210 | if (!this._contentNode || !this._content) {
211 | return;
212 | }
213 |
214 | // L.DomUtil.empty(this._contentNode); // Leaflet 1.0
215 | this._contentNode.innerHTML = '';
216 |
217 | this._content.forEach(function(content) {
218 | var html;
219 | if (content.title || content.content) {
220 | html = L.Util.template(
221 | "" +
222 | "{content}
",
223 | content
224 | );
225 | } else {
226 | html = content;
227 | }
228 | var node = L.DomUtil.create(
229 | 'div', 'lsp-card', this._contentNode
230 | );
231 | node.innerHTML = html;
232 | }, this);
233 | this._currentIndex = 0;
234 | this._updateNav();
235 | },
236 | '_createButton': function(name, text, container) {
237 | var button = L.DomUtil.create('button', 'lsp-' + name, container);
238 | button.type = 'button';
239 | button.innerHTML = text;
240 | L.DomEvent.on(button, 'click', this[name], this);
241 | this._buttons = this._buttons || {};
242 | this._buttons[name] = button;
243 | return button;
244 | },
245 | '_updateNav': function() {
246 | if (!this._buttons || !this._contentNode) {
247 | return;
248 | }
249 |
250 | var index = this._currentIndex || 0,
251 | content = this._content || [],
252 | buttons = this._buttons;
253 |
254 | if (this._state != 'closed') {
255 | showButton('navRight', (index < content.length - 1));
256 | showButton('navLeft', (index > 0));
257 | showButton('minimize', (
258 | !L.Browser.touch && this._state != 'minimal'
259 | ));
260 | showButton('full', (
261 | !L.Browser.touch && this._state != 'full'
262 | ));
263 | }
264 |
265 | function showButton(name, show) {
266 | buttons[name].style.display = show ? 'block': 'none';
267 | }
268 |
269 | Array.prototype.forEach.call(this._contentNode.children, function(el) {
270 | el.style.transform = (
271 | 'translate(' + (-100 * index) + '%, 0px)'
272 | );
273 | });
274 |
275 | if (this._featureGroup) {
276 | this._featureGroup.eachLayer(function(layer) {
277 | var color,
278 | index = layer.feature && layer.feature._index;
279 | if (index == this._currentIndex) {
280 | color = this.options.activeFeatureColor;
281 | } else {
282 | color = this.options.inactiveFeatureColor;
283 | }
284 | layer.setStyle({
285 | 'color': color
286 | });
287 | }, this);
288 | }
289 | },
290 | 'navigateTo': function(index) {
291 | this._currentIndex = index;
292 | this._navigate(0);
293 | },
294 | '_navigate': function(direction) {
295 | var index = this._currentIndex || 0;
296 | index += direction;
297 | if (index < 0) {
298 | index = 0;
299 | }
300 | if (index >= this._content.length) {
301 | index = this._content.length - 1;
302 | }
303 | this._currentIndex = index;
304 | this._updateNav();
305 | },
306 | '_expandMap': {
307 | 'closed': 'minimal',
308 | 'minimal': 'medium',
309 | 'medium': 'full',
310 | 'full': 'full'
311 | },
312 | '_shrinkMap': {
313 | 'closed': 'closed',
314 | 'minimal': 'closed',
315 | 'medium': 'minimal',
316 | 'full': 'medium'
317 | },
318 | '_startSwipe': function(evt) {
319 | var touch = evt.touches && evt.touches[0];
320 | if (!touch || !this._map) {
321 | return;
322 | }
323 | if (evt.target && evt.target.tagName == 'A') {
324 | return;
325 | }
326 | this._startPoint = this._map.mouseEventToContainerPoint(touch);
327 | L.DomEvent.preventDefault(evt);
328 | },
329 | '_endSwipe': function(evt) {
330 | var touch = evt.changedTouches && evt.changedTouches[0];
331 | if (!touch || !this._startPoint || !this._map) {
332 | return;
333 | }
334 | var endPoint = this._map.mouseEventToContainerPoint(touch);
335 | var diff = endPoint.subtract(this._startPoint),
336 | absX = Math.abs(diff.x),
337 | absY = Math.abs(diff.y);
338 | this._startPoint = null;
339 |
340 | if (absX < this.options.threshold && absY < this.options.threshold) {
341 | // Not enough distance
342 | return;
343 | }
344 | if (absX / absY > 0.5 && absX / absY < 2) {
345 | // Unclear direction
346 | return;
347 | }
348 | var direction, value;
349 |
350 | if (absX > absY) {
351 | value = absX;
352 | if (diff.x < 0) {
353 | direction = 'left';
354 | } else {
355 | direction = 'right';
356 | }
357 | } else {
358 | value = absY;
359 | if (diff.y < 0) {
360 | direction = 'up';
361 | } else {
362 | direction = 'down';
363 | }
364 | }
365 | this.fire('swipe' + direction, {
366 | 'direction': direction,
367 | 'value': value
368 | });
369 | }
370 | });
371 |
372 | function slidingPanel(content) {
373 | return new SlidingPanel(content);
374 | }
375 |
376 | L.Map.include({
377 | 'openPanel': function(content, features, options) {
378 | if (!options) {
379 | options = {};
380 | }
381 | var state = options.state;
382 | delete options.state;
383 |
384 | if (content instanceof SlidingPanel) {
385 | this.removePanel();
386 | this._panel = content.addTo(this);
387 | } else if (this._panel) {
388 | this._panel.setContent(content);
389 | this._panel.setFeatures(features);
390 | } else {
391 | this._panel = new SlidingPanel(L.Util.extend(
392 | {
393 | "content": content,
394 | "features": features
395 | },
396 | options
397 | )).addTo(this);
398 | }
399 | // Delay open until next tick to ensure CSS animation works
400 | var panel = this._panel;
401 | setTimeout(function() {
402 | if (state) {
403 | panel.setState(state);
404 | } else {
405 | panel.open();
406 | }
407 | }, 0);
408 | },
409 | 'closePanel': function() {
410 | if (!this._panel) {
411 | return;
412 | }
413 | this._panel.close();
414 | },
415 | 'removePanel': function() {
416 | if (!this._panel) {
417 | return;
418 | }
419 | this.closePanel();
420 | this._panel.remove();
421 | delete this._panel;
422 | }
423 | });
424 |
425 |
426 | L.SlidingPanel = slidingPanel.SlidingPanel = SlidingPanel;
427 | L.slidingPanel = slidingPanel;
428 |
429 | return slidingPanel;
430 |
431 | }));
432 |
--------------------------------------------------------------------------------