├── 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 | "

{title}

" + 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 | --------------------------------------------------------------------------------