diff --git a/static/index.html b/static/index.html index 99d3704..c1d126f 100644 --- a/static/index.html +++ b/static/index.html @@ -1,6 +1,7 @@ + Wildlife Map + + + + + + + +
- - + diff --git a/static/sidebar/easy-button.css b/static/sidebar/easy-button.css new file mode 100644 index 0000000..b654071 --- /dev/null +++ b/static/sidebar/easy-button.css @@ -0,0 +1,56 @@ +.leaflet-bar button, +.leaflet-bar button:hover { + background-color: #fff; + border: none; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; +} + +.leaflet-bar button { + background-position: 50% 50%; + background-repeat: no-repeat; + overflow: hidden; + display: block; +} + +.leaflet-bar button:hover { + background-color: #f4f4f4; +} + +.leaflet-bar button:first-of-type { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.leaflet-bar button:last-of-type { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; +} + +.leaflet-bar.disabled, +.leaflet-bar button.disabled { + cursor: default; + pointer-events: none; + opacity: .4; +} + +.easy-button-button .button-state{ + display: block; + width: 100%; + height: 100%; + position: relative; +} + + +.leaflet-touch .leaflet-bar button { + width: 30px; + height: 30px; + line-height: 30px; +} \ No newline at end of file diff --git a/static/sidebar/easy-button.js b/static/sidebar/easy-button.js new file mode 100644 index 0000000..7671a78 --- /dev/null +++ b/static/sidebar/easy-button.js @@ -0,0 +1,376 @@ +(function(){ + +// This is for grouping buttons into a bar +// takes an array of `L.easyButton`s and +// then the usual `.addTo(map)` +L.Control.EasyBar = L.Control.extend({ + + options: { + position: 'topleft', // part of leaflet's defaults + id: null, // an id to tag the Bar with + leafletClasses: true // use leaflet classes? + }, + + + initialize: function(buttons, options){ + + if(options){ + L.Util.setOptions( this, options ); + } + + this._buildContainer(); + this._buttons = []; + + for(var i = 0; i < buttons.length; i++){ + buttons[i]._bar = this; + buttons[i]._container = buttons[i].button; + this._buttons.push(buttons[i]); + this.container.appendChild(buttons[i].button); + } + + }, + + + _buildContainer: function(){ + this._container = this.container = L.DomUtil.create('div', ''); + this.options.leafletClasses && L.DomUtil.addClass(this.container, 'leaflet-bar easy-button-container leaflet-control'); + this.options.id && (this.container.id = this.options.id); + }, + + + enable: function(){ + L.DomUtil.addClass(this.container, 'enabled'); + L.DomUtil.removeClass(this.container, 'disabled'); + this.container.setAttribute('aria-hidden', 'false'); + return this; + }, + + + disable: function(){ + L.DomUtil.addClass(this.container, 'disabled'); + L.DomUtil.removeClass(this.container, 'enabled'); + this.container.setAttribute('aria-hidden', 'true'); + return this; + }, + + + onAdd: function () { + return this.container; + }, + + addTo: function (map) { + this._map = map; + + for(var i = 0; i < this._buttons.length; i++){ + this._buttons[i]._map = map; + } + + var container = this._container = this.onAdd(map), + pos = this.getPosition(), + corner = map._controlCorners[pos]; + + L.DomUtil.addClass(container, 'leaflet-control'); + + if (pos.indexOf('bottom') !== -1) { + corner.insertBefore(container, corner.firstChild); + } else { + corner.appendChild(container); + } + + return this; + } + +}); + +L.easyBar = function(){ + var args = [L.Control.EasyBar]; + for(var i = 0; i < arguments.length; i++){ + args.push( arguments[i] ); + } + return new (Function.prototype.bind.apply(L.Control.EasyBar, args)); +}; + +// L.EasyButton is the actual buttons +// can be called without being grouped into a bar +L.Control.EasyButton = L.Control.extend({ + + options: { + position: 'topleft', // part of leaflet's defaults + + id: null, // an id to tag the button with + + type: 'replace', // [(replace|animate)] + // replace swaps out elements + // animate changes classes with all elements inserted + + states: [], // state names look like this + // { + // stateName: 'untracked', + // onClick: function(){ handle_nav_manually(); }; + // title: 'click to make inactive', + // icon: 'fa-circle', // wrapped with + // } + + leafletClasses: true, // use leaflet styles for the button + tagName: 'button', + }, + + + + initialize: function(icon, onClick, title, id){ + + // clear the states manually + this.options.states = []; + + // add id to options + if(id != null){ + this.options.id = id; + } + + // storage between state functions + this.storage = {}; + + // is the last item an object? + if( typeof arguments[arguments.length-1] === 'object' ){ + + // if so, it should be the options + L.Util.setOptions( this, arguments[arguments.length-1] ); + } + + // if there aren't any states in options + // use the early params + if( this.options.states.length === 0 && + typeof icon === 'string' && + typeof onClick === 'function'){ + + // turn the options object into a state + this.options.states.push({ + icon: icon, + onClick: onClick, + title: typeof title === 'string' ? title : '' + }); + } + + // curate and move user's states into + // the _states for internal use + this._states = []; + + for(var i = 0; i < this.options.states.length; i++){ + this._states.push( new State(this.options.states[i], this) ); + } + + this._buildButton(); + + this._activateState(this._states[0]); + + }, + + _buildButton: function(){ + + this.button = L.DomUtil.create(this.options.tagName, ''); + + if (this.options.tagName === 'button') { + this.button.setAttribute('type', 'button'); + } + + if (this.options.id ){ + this.button.id = this.options.id; + } + + if (this.options.leafletClasses){ + L.DomUtil.addClass(this.button, 'easy-button-button leaflet-bar-part leaflet-interactive'); + } + + // don't let double clicks and mousedown get to the map + L.DomEvent.addListener(this.button, 'dblclick', L.DomEvent.stop); + L.DomEvent.addListener(this.button, 'mousedown', L.DomEvent.stop); + L.DomEvent.addListener(this.button, 'mouseup', L.DomEvent.stop); + + // take care of normal clicks + L.DomEvent.addListener(this.button,'click', function(e){ + L.DomEvent.stop(e); + this._currentState.onClick(this, this._map ? this._map : null ); + this._map && this._map.getContainer().focus(); + }, this); + + // prep the contents of the control + if(this.options.type == 'replace'){ + this.button.appendChild(this._currentState.icon); + } else { + for(var i=0;i"']/) ){ + + // if so, the user should have put in html + // so move forward as such + tmpIcon = ambiguousIconString; + + // then it wasn't html, so + // it's a class list, figure out what kind + } else { + ambiguousIconString = ambiguousIconString.replace(/(^\s*|\s*$)/g,''); + tmpIcon = L.DomUtil.create('span', ''); + + if( ambiguousIconString.indexOf('fa-') === 0 ){ + L.DomUtil.addClass(tmpIcon, 'fa ' + ambiguousIconString) + } else if ( ambiguousIconString.indexOf('glyphicon-') === 0 ) { + L.DomUtil.addClass(tmpIcon, 'glyphicon ' + ambiguousIconString) + } else { + L.DomUtil.addClass(tmpIcon, /*rollwithit*/ ambiguousIconString) + } + + // make this a string so that it's easy to set innerHTML below + tmpIcon = tmpIcon.outerHTML; + } + + return tmpIcon; +} + +})(); \ No newline at end of file diff --git a/static/sidebar/filter.css b/static/sidebar/filter.css new file mode 100644 index 0000000..6f1d6df --- /dev/null +++ b/static/sidebar/filter.css @@ -0,0 +1,143 @@ +.leaflet-bar .tag-filter-tags-container * {margin: 0; padding: 0;} + +.leaflet-bar .tag-filter-tags-container { + display: none; + position: absolute; + top:0px; + z-index: 1000; + padding-bottom: 10px; + padding-left: 5px; +} + + +.leaflet-bar.easy-button-container.leaflet-control { + text-align: left !important; +} + +.leaflet-bar span.filter-info-box { + position: absolute; + margin-top: -5px; + margin-left: 10px; + color: white; + font-size: 12px; + text-transform: uppercase; + padding: .2em .6em .3em; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; + background-color: #454b52; + box-sizing: inherit !important; +} + +.leaflet-bar .tag-filter-tags-container ul { + border: 1px solid hsl(180, 40%, 60%); + box-shadow: 0 1px 7px #999; + width: 150px; margin: 0 auto; + overflow-y: auto; + max-height: 200px; + -webkit-border-bottom-right-radius: 5px; + -webkit-border-bottom-left-radius: 5px; + -moz-border-radius-bottomright: 5px; + -moz-border-radius-bottomleft: 5px; + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; +} + +.leaflet-bar .tag-filter-tags-container ul li:last-child { + border: 0px; +} + +.leaflet-bar .tag-filter-tags-container ul.header li:last-child { + border: 0px; +} + +.leaflet-bar .tag-filter-tags-container ul.header { + height: 28px; + overflow: hidden; + border-top: 1px solid hsl(180, 40%, 60%); + border-bottom: 1px solid hsl(180, 40%, 60%); + -webkit-border-bottom-right-radius: 0px; + -webkit-border-bottom-left-radius: 0px; + -moz-border-radius-bottomright: 0px; + -moz-border-radius-bottomleft: 0px; + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; + -webkit-border-top-left-radius: 5px; + -webkit-border-top-right-radius: 5px; + -moz-border-radius-topleft: 5px; + -moz-border-radius-topright: 5px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +.leaflet-bar .tag-filter-tags-container ul.header li a { + padding: 0px; +} + +.leaflet-bar .tag-filter-tags-container ul.header li { + text-align: center; +} + +.leaflet-bar .tag-filter-tags-container ul li { + background: #fcfdff; + list-style-type: none; + position: relative; + overflow: hidden; + cursor: pointer; + border-bottom: 1px solid hsl(180, 40%, 60%); +} + +.leaflet-bar .tag-filter-tags-container ul li a { + background-color: transparent !important; + font-size: 12px; + font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; + color: hsl(180, 40%, 40%); + display: inline; + padding: 5px 15px; + text-decoration: none; + cursor: pointer; + user-select: none; + position: relative; +} + +.leaflet-bar .tag-filter-tags-container .ink { + display: block; position: absolute; + background: hsl(180, 40%, 80%); + border-radius: 100%; + transform: scale(0); +} + +.leaflet-bar .tag-filter-tags-container .checkbox { + display: none; + font-size: 14px; + color: hsl(180, 40%, 40%); + margin-left: 5px; +} + + +.leaflet-bar .tag-filter-tags-container ::-webkit-scrollbar { + width: 8px; +} +.leaflet-bar .tag-filter-tags-container ::-webkit-scrollbar-button { + width: 8px; + height:5px; +} +.leaflet-bar .tag-filter-tags-container ::-webkit-scrollbar-track { + background:#eee; + border: thin solid lightgray; + box-shadow: 0px 0px 3px #dfdfdf inset; + border-radius:10px; +} +.leaflet-bar .tag-filter-tags-container ::-webkit-scrollbar-thumb { + background:#999; + border: thin solid gray; + border-radius:10px; +} +.leaflet-bar .tag-filter-tags-container ::-webkit-scrollbar-thumb:hover { + background:#7d7d7d; +} +.leaflet-bar .tag-filter-tags-container a, .leaflet-bar .tag-filter-tags-container .checkbox { + white-space: nowrap; +} \ No newline at end of file diff --git a/static/sidebar/filter.js b/static/sidebar/filter.js new file mode 100644 index 0000000..0a20612 --- /dev/null +++ b/static/sidebar/filter.js @@ -0,0 +1,473 @@ +(function() { + + L.Control.TagFilterButton = L.Control.extend({ + + options: { + icon: "fa-filter", //buton icon default is fa-filter + onSelectionComplete: null, // the callback function for selected tags + data: null, // the data to be used for tags popup, it can be array or function + clearText: 'clear', // the text of the clear button + filterOnEveryClick: false, // if set as true the plugin do filtering operation on every click event on the checkboxes + openPopupOnHover: false, // if set as true the popup that contains tags will be open at mouse hover time + + ajaxData: null // it can be used for remote data TODO: implement it! + }, + + _map: null, + _container: null, + _easyButton: null, + _tagEl: null, + _clearEl: null, + _filterInfo: null, + _selectedTags: [], + _invisibles: null, + _releatedFilterButtons: null, + layerSources: null, + + // GLOBAL FUNCTIONS + + /** + * @function: resetCaches + * Resets marker caches + * @param update: if send as true, the @update function is called after cleaning the cache + * */ + resetCaches: function(update) { + if (typeof update !== 'boolean') + update = true; + this._invisibles = []; + if (update) { + this.update(); + } + }, + + /** + * @function: update + * Update markers by last selected tags + * + * */ + update: function() { + var filteredCount = this.layerSources.currentSource.hide.call(this, this.layerSources.currentSource); + this._showFilterInfo(filteredCount); + }, + + /** + * @function: hasFiltered + * returns true if any tag(s) selected otherwise false + * + * */ + hasFiltered: function () { + return this._selectedTags.length > 0; + }, + + /** + * @function: registerCustomSource + * Register @source object for filtering markers by tags. If you want to use this function + * you must implement @hide function. + * @param source reference of the new marker source. It must have name and source item + * + * */ + registerCustomSource: function(source) { + if (source.name && source.source && typeof source.source.hide === 'function') { + this.layerSources.sources[source.name] = source.source; + } else { + throw 'Layer source is incompatible'; + } + }, + + /** + * @function: enablePruneCluster + * @param pruneClusterInstance adds pruneCluster reference to layersources + * + * */ + enablePruneCluster: function (pruneClusterInstance) { + this.registerCustomSource({ + "name": "pruneCluster", + "source": { + pruneCluster: pruneClusterInstance, + hide: function(layerSource) { + var toBeRemovedFromInvisibles = [], i, j; + + for (i = 0; i < this._invisibles.length; i++) { + for (j = 0; j < this._invisibles[i].data.tags.length; j++) { + if (this._selectedTags.length == 0 || this._selectedTags.indexOf(this._invisibles[i].data.tags[j]) !== -1) { + layerSource.pruneCluster.RegisterMarker(this._invisibles[i]); + toBeRemovedFromInvisibles.push(i); + break; + } + } + } + + while(toBeRemovedFromInvisibles.length > 0) { + this._invisibles.splice(toBeRemovedFromInvisibles.pop(), 1); + } + + var removedMarkers = []; + var totalCount = 0; + + if (this._selectedTags.length > 0) { + + var releatedLayers = []; + + for (var r = 0; r < this._releatedFilterButtons.length; r++) { + releatedLayers = releatedLayers.concat(this._releatedFilterButtons[r].getInvisibles()); + } + + var markers = layerSource.pruneCluster.GetMarkers(); + for (i = 0; i < markers.length; i++) { + if (releatedLayers.indexOf(markers[i]) == -1 && markers[i].data && markers[i].data.tags) { + totalCount++; + var found = false; + for (var j = 0; j < markers[i].data.tags.length; j++) { + found = this._selectedTags.indexOf(markers[i].data.tags[j]) !== -1; + if (found) { + break; + } + } + if (!found) { + removedMarkers.push(markers[i]); + } + } + } + + for (i = 0; i < removedMarkers.length; i++) { + this._invisibles.push(removedMarkers[i]); + } + + layerSource.pruneCluster.RemoveMarkers(removedMarkers); + + } + + + layerSource.pruneCluster.ProcessView(); + + return totalCount - removedMarkers.length; + } + } + }); + + this.layerSources.currentSource = this.layerSources.sources["pruneCluster"]; + }, + + /** + * @function: addToReleated + * @param other adds another linked tagFilterButton reference to _releatedFilterButtons + * + * */ + addToReleated: function(other) { + if (other && other instanceof L.Control.TagFilterButton && this._releatedFilterButtons.indexOf(other) == -1) { + this._releatedFilterButtons.push(other); + return other.addToReleated(this); + } + console.error("could not add tagFilterButton instance to releated"); + return false; + }, + + /** + * @function: getInvisibles + * @param other gets invisibles layers hiding by this plugin + * + * */ + getInvisibles: function() { + return this._invisibles; + }, + + _prepareLayerSources: function() { + + this.layerSources = new Object(); + this.layerSources["sources"] = new Object(); + + this.registerCustomSource({ + "name": "default", + "source": { + hide: function() { + + var releatedLayers = []; + + for (var r = 0; r < this._releatedFilterButtons.length; r++) { + releatedLayers = releatedLayers.concat(this._releatedFilterButtons[r].getInvisibles()); + } + + var toBeRemovedFromInvisibles = [], i; + + for (i = 0; i < this._invisibles.length; i++) { + if (releatedLayers.indexOf(this._invisibles[i]) == -1) { + for (j = 0; j < this._invisibles[i].options.tags.length; j++) { + if (this._selectedTags.length == 0 || this._selectedTags.indexOf(this._invisibles[i].options.tags[j]) !== -1) { + this._map.addLayer(this._invisibles[i]); + toBeRemovedFromInvisibles.push(i); + break; + } + } + } + } + + while(toBeRemovedFromInvisibles.length > 0) { + this._invisibles.splice(toBeRemovedFromInvisibles.pop(), 1); + } + + var removedMarkers = []; + var totalCount = 0; + + if (this._selectedTags.length > 0) { + + this._map.eachLayer(function(layer) { + if (layer && layer.options && layer.options.tags) { + totalCount++; + if (releatedLayers.indexOf(layer) == -1) { + var found = false; + for (var i = 0; i < layer.options.tags.length; i++) { + found = this._selectedTags.indexOf(layer.options.tags[i]) !== -1; + if (found) { + break; + } + } + if (!found) { + removedMarkers.push(layer); + } + } + } + }.bind(this)); + + for (i = 0; i < removedMarkers.length; i++) { + this._map.removeLayer(removedMarkers[i]); + this._invisibles.push(removedMarkers[i]); + } + + } + + return totalCount - removedMarkers.length; + } + } + }); + this.layerSources.currentSource = this.layerSources.sources["default"]; + }, + + _showFilterInfo: function(filteredCount) { + if (this._selectedTags.length > 0) { + this._filterInfo.innerText = filteredCount.toString(); + this._filterInfo.style.display = ""; + } else { + this._filterInfo.style.display = "none"; + } + }, + + _checkItem: function(item) { + item.getElementsByClassName('checkbox')[0].style.display = "inline-block"; + item.dataset.checked = "checked"; + }, + + _uncheckItem: function(item) { + item.dataset.checked = ""; + item.getElementsByClassName('checkbox')[0].style.display = "none"; + }, + + _onClickToItem: function(e) { + L.DomEvent.stop(e); + var li = this.element; + var context = this.context; + if (!li.dataset.checked) { + context._checkItem(li); + } else { + context._uncheckItem(li); + } + if (context.options.filterOnEveryClick) { + context.filter.call(context); + } + }, + + _preparePopup: function(data) { + + this._tagEl.innerHTML = ''; + + for (var i = 0; i < data.length; i++) { + var li = L.DomUtil.create('li', 'ripple', this._tagEl); + var checkbox = L.DomUtil.create('span', 'checkbox', li); + var a = L.DomUtil.create('a', '', li); + var text = data[i]; + var value = data[i]; + if (typeof text == 'object' && Object.keys(data[i]).length == 2) { // key,value + text = data[i].name; + value = data[i].value; + } + + var checked = this._selectedTags.indexOf(value) !== -1; + if (checked) { + checkbox.style.display = "inline-block"; + li.dataset.checked = "checked"; + } + checkbox.innerHTML= "✔"; + li.dataset.value = value; + a.innerText = text; + + L.DomEvent.addListener(li, 'dblclick', this._onClickToItem.bind({ context: this, element: li })); + L.DomEvent.addListener(li, 'click', this._onClickToItem.bind({ context: this, element: li })); + + L.DomEvent.addListener(a, 'dblclick', function(e) { + L.DomEvent.stop(e); + this.context._onClickToItem.call(this, e); + }.bind({ context: this, element: li })); + + L.DomEvent.addListener(a, 'click', function(e) { + L.DomEvent.stop(e); + this.context._onClickToItem.call(this, e); + }.bind({ context: this, element: li })); + + + } + this._container.style.display = "block"; + }, + + _clearSelections: function(e) { + L.DomEvent.stop(e); + this._selectedTags = []; + var childCount = this._tagEl.childElementCount, + children = this._tagEl.children, + childCheckbox, i; + + this._selectedTags = []; + + for (i = 0; i < childCount; i++) { + childCheckbox = children[i]; + if (childCheckbox) { + this._uncheckItem(childCheckbox); + } + } + + if (this.options.filterOnEveryClick) { + this.filter(); + } + }, + + _showTagFilterPopup: function() { + + if (this._tagFilterPopupIsOpen()) { + return; + } + + this._easyButton.button.style.display = "none"; + this._filterInfo.style.display = "none"; + + if (!this._container) { + throw 'container is not initialized!'; + } + + if (!this.options.data && !this.options.ajaxData) { + throw 'data is empty!'; + } + + if (this.options.data) { + if (typeof this.options.data === 'function') { + this._preparePopup(this.options.data()); + } else { + this._preparePopup(this.options.data); + } + } + + }, + + _tagFilterPopupIsOpen: function() { + return this._container.style.display == 'block'; + }, + + filter: function(withTags) { + var checkboxContainer = (this._container.getElementsByTagName('div')[0]), + childCount = this._tagEl.childElementCount, + children = this._tagEl.children, + childCheckbox, i, j; + + this._selectedTags = []; + + if (withTags) { + var acceptingTags = []; + for (i = 0; i < withTags.length; i++) { + if (this.options.data.indexOf(withTags[i]) !== -1) { + acceptingTags.push(withTags[i]); + } + } + withTags = acceptingTags; + } + + if (!withTags || !withTags.length) { + for (i = 0; i < childCount; i++) { + childCheckbox = children[i]; + if (childCheckbox && childCheckbox.dataset.checked) { + this._selectedTags.push(childCheckbox.dataset.value); + } + } + } else { + this._selectedTags = withTags; + } + + var filteredCount = this.layerSources.currentSource.hide.call(this, this.layerSources.currentSource); + this._showFilterInfo(filteredCount); + + if (this.options.onSelectionComplete && typeof this.options.onSelectionComplete == 'function') { + this.options.onSelectionComplete.call(this, this._selectedTags); + } + }, + + hide: function(accept) { + if (this._container && (this._container.style.display == "none" || this._container.style.display == "")) { + return; + } + if (this._container) { + this._container.style.display = "none"; + } + this.filter(); + this._easyButton.button.style.display = "block"; + }, + + initialize: function(options) { + this._invisibles = []; + this._releatedFilterButtons = []; + L.Util.setOptions(this, options || {}); + this._prepareLayerSources(); + }, + + addTo: function(map) { + this._map = map; + if (this.options.openPopupOnHover) { + this._easyButton = L.easyButton(this.options.icon, function() { + }).addTo(map); + L.DomEvent.addListener(this._easyButton._container, 'mouseover', this._showTagFilterPopup.bind(this)); + } else { + this._easyButton = L.easyButton(this.options.icon, this._showTagFilterPopup.bind(this)).addTo(map); + } + this._container = L.DomUtil.create('div', 'tag-filter-tags-container', this._easyButton._container); + + if (!L.Browser.touch) { + L.DomEvent.disableClickPropagation(this._container); + L.DomEvent.disableScrollPropagation(this._container); + } else { + L.DomEvent.disableClickPropagation(this._container); + L.DomEvent.disableScrollPropagation(this._container); + } + + this._filterInfo = L.DomUtil.create('span', 'filter-info-box', this._easyButton._container); + this._showFilterInfo(0); + + this._clearEl = L.DomUtil.create('ul', 'header', this._container); + this._clearEl.innerHTML = "
  • " + this.options.clearText + "
  • "; + + L.DomEvent.addListener(this._clearEl.getElementsByTagName('a')[0], 'click', this._clearSelections.bind(this)); + L.DomEvent.addListener(this._clearEl.getElementsByTagName('li')[0], 'click', this._clearSelections.bind(this)); + + this._tagEl = L.DomUtil.create('ul', '', this._container); + this._map.on('dragstart click', this.hide, this); + L.DomEvent.addListener(this._container, 'dblclick', L.DomEvent.stop); + L.DomEvent.addListener(this._container, 'click', L.DomEvent.stop); + return this; + }, + + onRemove: function(map) { + this._container.parentNode.removeChild(this._container); + return this; + } + + }); + + L.control.tagFilterButton = function(options) { + return new L.Control.TagFilterButton(options); + }; + +}).call(this); \ No newline at end of file diff --git a/static/sidebar/search.css b/static/sidebar/search.css new file mode 100644 index 0000000..8048d33 --- /dev/null +++ b/static/sidebar/search.css @@ -0,0 +1,129 @@ +.leaflet-searchbox-wrapper { + display: flex; + justify-content: right; + height: 40px; + border-top-left-radius: 20px 50%; + border-bottom-left-radius: 20px 50%; + border-top-right-radius: 20px 50%; + border-bottom-right-radius: 20px 50%; +} + +.leaflet-searchbox-wrapper .leaflet-searchbox, +.leaflet-searchbox-wrapper .leaflet-searchbox-button, +.leaflet-searchbox-container .leaflet-searchbox-autocomplete, +.leaflet-searchbox-autocomplete .leaflet-searchbox-autocomplete-item { + background-color: white; + border: 1px rgb(0, 0, 0, 0.3) solid; + outline: none; + transition-duration: 0.2s; +} + +.leaflet-searchbox-wrapper .leaflet-searchbox { + width: 300px; + font-size: 17px; + transition-delay: 0.1s; + z-index: 702; +} + +.leaflet-searchbox-wrapper .leaflet-searchbox-left { + border-top-left-radius: 20px 50%; + border-bottom-left-radius: 20px 50%; + border-right: none; + padding: 0px 0px 0px 15px; +} + +.leaflet-searchbox-wrapper .leaflet-searchbox-right { + border-top-right-radius: 20px 50%; + border-bottom-right-radius: 20px 50%; + border-left: none; + padding: 0px 15px 0px 0px; +} + +.leaflet-searchbox-wrapper .leaflet-searchbox-button { + width: 40px; + transition-delay: 0s; + z-index: 702; +} + +.leaflet-searchbox-wrapper .leaflet-searchbox-button:hover { + cursor: pointer; +} + +.leaflet-searchbox-wrapper .leaflet-searchbox-button-right { + border-top-right-radius: 20px 50%; + border-bottom-right-radius: 20px 50%; + border-left: none; +} + +.leaflet-searchbox-wrapper .leaflet-searchbox-button-left { + border-top-left-radius: 20px 50%; + border-bottom-left-radius: 20px 50%; + border-right: none; +} + +.leaflet-searchbox-wrapper .leaflet-searchbox-button i { + font-size: 130%; +} + +.leaflet-searchbox-container .leaflet-searchbox-icon { + width: 69%; + height: auto; +} + +.leaflet-searchbox-container .leaflet-searchbox-autocomplete { + position: absolute; + top: 20px; + width: 100%; + list-style-type: none; + margin: 0px; + padding: 20px 0px 0px 0px; + background-color: white; + border-top: none; + z-index: 701; +} + +.leaflet-searchbox-container .leaflet-searchbox-autocomplete:empty { + display: none; +} + +.leaflet-searchbox-autocomplete .leaflet-searchbox-autocomplete-item { + width: 100%; + border-left: none; + border-right: none; + border-top: none; + z-index: 701; + font-size: 15px; + padding: 3px 10px; + cursor: pointer; +} + +.leaflet-searchbox-autocomplete .leaflet-searchbox-autocomplete-item:hover { + background-color: #f3f3f3; +} + +.collapsed .leaflet-searchbox { + width: 0px !important; + padding: 0px; + transition-delay: 0s; +} + +.collapsed .leaflet-searchbox { + border-left: none; + border-right: none; +} + +.collapsed .leaflet-searchbox-button { + border-radius: 50%; + transition-delay: 0.2s; + border: 1px rgb(0, 0, 0, 0.3) solid; +} + +.collapsed .leaflet-searchbox-autocomplete { + display: none; +} + +.leaflet-searchbox-wrapper.open .leaflet-searchbox, +.leaflet-searchbox-wrapper.open .leaflet-searchbox-button { + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; +} \ No newline at end of file diff --git a/static/sidebar/search.js b/static/sidebar/search.js new file mode 100644 index 0000000..afc2273 --- /dev/null +++ b/static/sidebar/search.js @@ -0,0 +1,264 @@ +(function (factory, window) { + + // define an AMD module that relies on 'leaflet' + if (typeof define === 'function' && define.amd) { + define(['leaflet'], factory); + + // define a Common JS module that relies on 'leaflet' + } else if (typeof exports === 'object') { + module.exports = factory(require('leaflet')); + } + + // attach your plugin to the global 'L' variable + if (typeof window !== 'undefined' && window.L) { + window.L.YourPlugin = factory(L); + } +}(function (L) { + L.Control.Searchbox = L.Control.extend({ + options: { + class: '', + id: '', + position: 'topright', + expand: 'left', + collapsed: true, + width: null, + iconPath: 'img/search_icon.png', + autocompleteFeatures: ['setValueOnClick'] + }, + + onAdd: function (map) { + this._create(); + + this._collapsed = this.options.collapsed; + if (this.options.collapsed) { + this.hide(); + } + + L.DomEvent.disableClickPropagation(this._container); + + L.DomEvent.on(this._button, 'click', this._onClick, this); + + // Autocomplete behaviour + if (this.options.autocompleteFeatures.includes('setValueOnClick')) { + this.onAutocomplete('click', function (e) { + this._onListItemClick(e.target); + }); + } + + return this._container; + }, + + onRemove: function (map) { + + }, + + + getValue: function () { + return this._input.value + }, + + setValue: function (value) { + this._input.value = value; + return this + }, + + addItem: function (item) { + var listItem = L.DomUtil.create('li', 'leaflet-searchbox-autocomplete-item', this._autocomplete); + listItem.textContent = item; + this._items.push(listItem); + + L.DomUtil.addClass(this._searchboxWrapper, 'open'); + + return this + }, + + addItems: function (items) { + for (var i = 0; i < items.length; i++) { + this.addItem(items[i]); + } + + return this + }, + + setItems: function (items) { + this.clearItems(); + this.addItems(items); + + return this + }, + + clearItems: function () { + this._autocomplete.innerHTML = ''; + this._items = []; + + L.DomUtil.removeClass(this._searchboxWrapper, 'open'); + + return this + }, + + hide: function () { + L.DomUtil.addClass(this._container, "collapsed"); + this._input.blur(); + this._button.blur(); + setTimeout(() => { + this._collapsed = true; + }, 600); + + return this; + }, + + show: function () { + L.DomUtil.removeClass(this._container, "collapsed"); + setTimeout(() => { + this._collapsed = false; + }, 600); + + return this; + }, + + toggle: function () { + if (L.DomUtil.hasClass(this._container, "collapsed")) { + this.show(); + } else { + this.hide(); + } + + return this; + }, + + isCollapsed: function () { + return L.DomUtil.hasClass(this._container, "collapsed") + }, + + clearInput: function () { + this._input.value = ''; + + return this + }, + + clear: function () { + this.clearInput(); + this.clearItems(); + + return this; + }, + + onInput: function (event, handler) { + L.DomEvent.on(this._input, event, handler, this); + + return this + }, + + offInput: function (event, handler) { + L.DomEvent.off(this._input, event, handler, this); + + return this + }, + + onButton: function (event, handler) { + var wrapper = this._buttonHandlerWrapper(handler); + L.DomEvent.on(this._button, event, wrapper, this); + + return this + }, + + offButton: function (event, handler) { + var wrapper = this._buttonHandlerWrapper(handler); + L.DomEvent.off(this._button, event, wrapper, this); + + return this + }, + + onAutocomplete: function (event, handler) { + L.DomEvent.on(this._autocomplete, event, handler, this); + + return this + }, + + offAutocomplete: function (event, handler) { + L.DomEvent.off(this._autocomplete, event, handler, this); + + return this + }, + + _onClick: function () { + if (this._collapsed) { + this.show(); + this._input.focus(); + } + }, + + _onListItemClick: function (item) { + this.setValue(item.innerHTML); + this._input.focus(); + }, + + _buttonHandlerWrapper: function (handler) { + return function () { + if (!this._collapsed) { + handler(); + } + } + }, + + _create: function () { + this._container = L.DomUtil.create('div', 'leaflet-control leaflet-searchbox-container'); + if (this.options.class != '') { + L.DomUtil.addClass(this._container, this.options.class); + } + if (this.options.id != '') { + this._container.id = this.options.id; + } + + this._searchboxWrapper = L.DomUtil.create('div', 'leaflet-searchbox-wrapper', this._container); + + if (this.options.expand == 'left') { + this._createInput('left'); + this._createButton('right'); + } else if (this.options.expand == 'right') { + this._createButton('left'); + this._createInput('right'); + } + this._createAutocomplete(); + }, + + _createInput: function (position) { + this._input = L.DomUtil.create( + 'input', + 'leaflet-searchbox leaflet-searchbox-' + position, + this._searchboxWrapper); + this._input.setAttribute('type', 'text'); + if (this.options.width != null) { + this._input.style.width = this.options.width; + } + }, + + _createButton: function (position) { + this._button = L.DomUtil.create( + 'button', + 'leaflet-searchbox-button leaflet-searchbox-button-' + position, + this._searchboxWrapper); + this._button.setAttribute('type', 'button'); + this._button.style.width = this.options.height; + this._button.style.height = this.options.height; + this._icon = L.DomUtil.create('img', 'leaflet-searchbox-icon', this._button); + this._icon.setAttribute('src', this.options.iconPath); + }, + + _createAutocomplete: function () { + this._autocomplete = L.DomUtil.create( + 'ul', + 'leaflet-searchbox-autocomplete', + this._container); + + this._items = []; + + } + }); + + return L.Control.Searchbox; +}, window)); + +L.control.searchbox = function (options) { + return new L.Control.Searchbox(options); +} diff --git a/static/sidebar/sidebar.css b/static/sidebar/sidebar.css new file mode 100644 index 0000000..6e41c4c --- /dev/null +++ b/static/sidebar/sidebar.css @@ -0,0 +1,198 @@ +.leaflet-sidebar { + position: absolute; + top: 0; + bottom: 0; + width: 100%; + overflow: hidden; + z-index: 2000; } + .leaflet-sidebar.collapsed { + width: 40px; } + @media (min-width: 768px) { + .leaflet-sidebar { + top: 10px; + bottom: 10px; + transition: width 500ms; } } + @media (min-width: 768px) and (max-width: 991px) { + .leaflet-sidebar { + width: 305px; + max-width: 305px; } } + @media (min-width: 992px) and (max-width: 1199px) { + .leaflet-sidebar { + width: 390px; + max-width: 390px; } } + @media (min-width: 1200px) { + .leaflet-sidebar { + width: 460px; + max-width: 460px; } } + +.leaflet-sidebar-left { + left: 0; } + @media (min-width: 768px) { + .leaflet-sidebar-left { + left: 10px; } } + +.leaflet-sidebar-right { + right: 0; } + @media (min-width: 768px) { + .leaflet-sidebar-right { + right: 10px; } } + +.leaflet-sidebar-tabs { + top: 0; + bottom: 0; + height: 100%; + background-color: #fff; } + .leaflet-sidebar-left .leaflet-sidebar-tabs { + left: 0; } + .leaflet-sidebar-right .leaflet-sidebar-tabs { + right: 0; } + .leaflet-sidebar-tabs, .leaflet-sidebar-tabs > ul { + position: absolute; + width: 40px; + margin: 0; + padding: 0; + list-style-type: none; } + .leaflet-sidebar-tabs > li, .leaflet-sidebar-tabs > ul > li { + width: 100%; + height: 40px; + color: #333; + font-size: 12pt; + overflow: hidden; + transition: all 80ms; } + .leaflet-sidebar-tabs > li:hover, .leaflet-sidebar-tabs > ul > li:hover { + color: #000; + background-color: #eee; } + .leaflet-sidebar-tabs > li.active, .leaflet-sidebar-tabs > ul > li.active { + color: #fff; + background-color: #0074d9; } + .leaflet-sidebar-tabs > li.disabled, .leaflet-sidebar-tabs > ul > li.disabled { + color: rgba(51, 51, 51, 0.4); } + .leaflet-sidebar-tabs > li.disabled:hover, .leaflet-sidebar-tabs > ul > li.disabled:hover { + background: transparent; } + .leaflet-sidebar-tabs > li.disabled > a, .leaflet-sidebar-tabs > ul > li.disabled > a { + cursor: default; } + .leaflet-sidebar-tabs > li > a, .leaflet-sidebar-tabs > ul > li > a { + display: block; + width: 100%; + height: 100%; + line-height: 40px; + color: inherit; + text-decoration: none; + text-align: center; + cursor: pointer; } + .leaflet-sidebar-tabs > ul + ul { + bottom: 0; } + +.leaflet-sidebar-content { + position: absolute; + top: 0; + bottom: 0; + background-color: rgba(255, 255, 255, 0.95); + overflow-x: hidden; + overflow-y: auto; } + .leaflet-sidebar-left .leaflet-sidebar-content { + left: 40px; + right: 0; } + .leaflet-sidebar-right .leaflet-sidebar-content { + left: 0; + right: 40px; } + .leaflet-sidebar.collapsed > .leaflet-sidebar-content { + overflow-y: hidden; } + +.collapsed > .leaflet-sidebar-content { + overflow-y: hidden; } + +.leaflet-sidebar-pane { + display: none; + left: 0; + right: 0; + box-sizing: border-box; + padding: 10px 20px; } + .leaflet-sidebar-pane.active { + display: block; } + @media (min-width: 768px) and (max-width: 991px) { + .leaflet-sidebar-pane { + min-width: 265px; } } + @media (min-width: 992px) and (max-width: 1199px) { + .leaflet-sidebar-pane { + min-width: 350px; } } + @media (min-width: 1200px) { + .leaflet-sidebar-pane { + min-width: 420px; } } + +.leaflet-sidebar-header { + margin: -10px -20px 0; + height: 40px; + padding: 0 20px; + line-height: 40px; + font-size: 14.4pt; + color: #fff; + background-color: #0074d9; } + .leaflet-sidebar-right .leaflet-sidebar-header { + padding-left: 40px; } + +.leaflet-sidebar-close { + position: absolute; + top: 0; + width: 40px; + height: 40px; + text-align: center; + cursor: pointer; } + .leaflet-sidebar-left .leaflet-sidebar-close { + right: 0; } + .leaflet-sidebar-right .leaflet-sidebar-close { + left: 0; } + +.leaflet-sidebar { + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); } + @media (min-width: 768px) { + .leaflet-sidebar { + border-radius: 4px; } + .leaflet-sidebar.leaflet-touch { + border: 2px solid rgba(0, 0, 0, 0.2); } } + +.leaflet-sidebar-left.leaflet-touch { + box-shadow: none; + border-right: 2px solid rgba(0, 0, 0, 0.2); } + +@media (min-width: 768px) { + .leaflet-sidebar-left ~ .leaflet-control-container .leaflet-left { + transition: left 500ms; } } + +@media (min-width: 768px) and (max-width: 991px) { + .leaflet-sidebar-left ~ .leaflet-control-container .leaflet-left { + left: 315px; } } + +@media (min-width: 992px) and (max-width: 1199px) { + .leaflet-sidebar-left ~ .leaflet-control-container .leaflet-left { + left: 400px; } } + +@media (min-width: 1200px) { + .leaflet-sidebar-left ~ .leaflet-control-container .leaflet-left { + left: 470px; } } + +.leaflet-sidebar-left.collapsed ~ .leaflet-control-container .leaflet-left { + left: 50px; } + +.leaflet-sidebar-right.leaflet-touch { + box-shadow: none; + border-left: 2px solid rgba(0, 0, 0, 0.2); } + +@media (min-width: 768px) { + .leaflet-sidebar-right ~ .leaflet-control-container .leaflet-right { + transition: right 500ms; } } + +@media (min-width: 768px) and (max-width: 991px) { + .leaflet-sidebar-right ~ .leaflet-control-container .leaflet-right { + right: 315px; } } + +@media (min-width: 992px) and (max-width: 1199px) { + .leaflet-sidebar-right ~ .leaflet-control-container .leaflet-right { + right: 400px; } } + +@media (min-width: 1200px) { + .leaflet-sidebar-right ~ .leaflet-control-container .leaflet-right { + right: 470px; } } + +.leaflet-sidebar-right.collapsed ~ .leaflet-control-container .leaflet-right { + right: 50px; } \ No newline at end of file diff --git a/static/sidebar/sidebar.js b/static/sidebar/sidebar.js new file mode 100644 index 0000000..f139547 --- /dev/null +++ b/static/sidebar/sidebar.js @@ -0,0 +1,526 @@ +// @ts-nocheck +/** + * @name Sidebar + * @class L.Control.Sidebar + * @extends L.Control + * @param {string} id - The id of the sidebar element (without the # character) + * @param {Object} [options] - Optional options object + * @param {string} [options.autopan=false] - whether to move the map when opening the sidebar to make maintain the visible center point + * @param {string} [options.position=left] - Position of the sidebar: 'left' or 'right' + * @param {string} [options.id] - ID of a predefined sidebar container that should be used + * @param {boolean} [data.close=true] Whether to add a close button to the pane header + * @see L.control.sidebar + */ +L.Control.Sidebar = L.Control.extend(/** @lends L.Control.Sidebar.prototype */ { + includes: L.Evented ? L.Evented.prototype : L.Mixin.Events, + + options: { + autopan: false, + closeButton: true, + container: null, + position: 'left' + }, + + /** + * Create a new sidebar on this object. + * + * @constructor + * @param {Object} [options] - Optional options object + * @param {string} [options.autopan=false] - whether to move the map when opening the sidebar to make maintain the visible center point + * @param {string} [options.position=left] - Position of the sidebar: 'left' or 'right' + * @param {string} [options.container] - ID of a predefined sidebar container that should be used + * @param {bool} [data.close=true] Whether to add a close button to the pane header + */ + initialize: function(options, deprecatedOptions) { + if (typeof options === 'string') { + console.warn('this syntax is deprecated. please use L.control.sidebar({ container }) now'); + options = { container: options }; + } + + if (typeof options === 'object' && options.id) { + console.warn('this syntax is deprecated. please use L.control.sidebar({ container }) now'); + options.container = options.id; + } + + this._tabitems = []; + this._panes = []; + this._closeButtons = []; + + L.setOptions(this, options); + L.setOptions(this, deprecatedOptions); + return this; + }, + + /** + * Add this sidebar to the specified map. + * + * @param {L.Map} map + * @returns {Sidebar} + */ + onAdd: function(map) { + var i, child, tabContainers, newContainer, container; + + // use container from previous onAdd() + container = this._container + + // use the container given via options. + if (!container) { + container = this._container || typeof this.options.container === 'string' + ? L.DomUtil.get(this.options.container) + : this.options.container; + } + + // if no container was specified or not found, create it and apply an ID + if (!container) { + container = L.DomUtil.create('div', 'leaflet-sidebar collapsed'); + if (typeof this.options.container === 'string') + container.id = this.options.container; + } + + // Find paneContainer in DOM & store reference + this._paneContainer = container.querySelector('div.leaflet-sidebar-content'); + + // If none is found, create it + if (this._paneContainer === null) + this._paneContainer = L.DomUtil.create('div', 'leaflet-sidebar-content', container); + + // Find tabContainerTop & tabContainerBottom in DOM & store reference + tabContainers = container.querySelectorAll('ul.leaflet-sidebar-tabs, div.leaflet-sidebar-tabs > ul'); + this._tabContainerTop = tabContainers[0] || null; + this._tabContainerBottom = tabContainers[1] || null; + + // If no container was found, create it + if (this._tabContainerTop === null) { + newContainer = L.DomUtil.create('div', 'leaflet-sidebar-tabs', container); + newContainer.setAttribute('role', 'tablist'); + this._tabContainerTop = L.DomUtil.create('ul', '', newContainer); + } + if (this._tabContainerBottom === null) { + newContainer = this._tabContainerTop.parentNode; + this._tabContainerBottom = L.DomUtil.create('ul', '', newContainer); + } + + // Store Tabs in Collection for easier iteration + for (i = 0; i < this._tabContainerTop.children.length; i++) { + child = this._tabContainerTop.children[i]; + child._sidebar = this; + child._id = child.querySelector('a').hash.slice(1); // FIXME: this could break for links! + this._tabitems.push(child); + } + for (i = 0; i < this._tabContainerBottom.children.length; i++) { + child = this._tabContainerBottom.children[i]; + child._sidebar = this; + child._id = child.querySelector('a').hash.slice(1); // FIXME: this could break for links! + this._tabitems.push(child); + } + + // Store Panes in Collection for easier iteration + for (i = 0; i < this._paneContainer.children.length; i++) { + child = this._paneContainer.children[i]; + if (child.tagName === 'DIV' && + L.DomUtil.hasClass(child, 'leaflet-sidebar-pane')) { + this._panes.push(child); + + // Save references to close buttons + var closeButtons = child.querySelectorAll('.leaflet-sidebar-close'); + if (closeButtons.length) { + this._closeButtons.push(closeButtons[closeButtons.length - 1]); + this._closeClick(closeButtons[closeButtons.length - 1], 'on'); + } + } + } + + // set click listeners for tab & close buttons + for (i = 0; i < this._tabitems.length; i++) { + this._tabClick(this._tabitems[i], 'on'); + } + + // leaflet moves the returned container to the right place in the DOM + return container; + }, + + /** + * Remove this sidebar from the map. + * + * @param {L.Map} map + * @returns {Sidebar} + */ + onRemove: function (map) { + // Remove click listeners for tab & close buttons + for (var i = 0; i < this._tabitems.length; i++) + this._tabClick(this._tabitems[i], 'off'); + for (var i = 0; i < this._closeButtons.length; i++) + this._closeClick(this._closeButtons[i], 'off'); + + this._tabitems = []; + this._panes = []; + this._closeButtons = []; + + return this; + }, + + /** + * @method addTo(map: Map): this + * Adds the control to the given map. Overrides the implementation of L.Control, + * changing the DOM mount target from map._controlContainer.topleft to map._container + */ + addTo: function (map) { + this.onRemove(); + this._map = map; + + this._container = this.onAdd(map); + + L.DomUtil.addClass(this._container, 'leaflet-control'); + L.DomUtil.addClass(this._container, 'leaflet-sidebar-' + this.getPosition()); + if (L.Browser.touch) + L.DomUtil.addClass(this._container, 'leaflet-touch'); + + // when adding to the map container, we should stop event propagation + L.DomEvent.disableScrollPropagation(this._container); + L.DomEvent.disableClickPropagation(this._container); + L.DomEvent.on(this._container, 'contextmenu', L.DomEvent.stopPropagation); + + // insert as first child of map container (important for css) + map._container.insertBefore(this._container, map._container.firstChild); + + return this; + }, + + /** + * @deprecated - Please use remove() instead of removeFrom(), as of Leaflet 0.8-dev, the removeFrom() has been replaced with remove() + * Removes this sidebar from the map. + * @param {L.Map} map + * @returns {Sidebar} + */ + removeFrom: function(map) { + console.warn('removeFrom() has been deprecated, please use remove() instead as support for this function will be ending soon.'); + this._map._container.removeChild(this._container); + this.onRemove(map); + + return this; + }, + + /** + * Open sidebar (if it's closed) and show the specified tab. + * + * @param {string} id - The ID of the tab to show (without the # character) + * @returns {L.Control.Sidebar} + */ + open: function(id) { + var i, child, tab; + + // If panel is disabled, stop right here + tab = this._getTab(id); + if (L.DomUtil.hasClass(tab, 'disabled')) + return this; + + // Hide old active contents and show new content + for (i = 0; i < this._panes.length; i++) { + child = this._panes[i]; + if (child.id === id) + L.DomUtil.addClass(child, 'active'); + else if (L.DomUtil.hasClass(child, 'active')) + L.DomUtil.removeClass(child, 'active'); + } + + // Remove old active highlights and set new highlight + for (i = 0; i < this._tabitems.length; i++) { + child = this._tabitems[i]; + if (child.querySelector('a').hash === '#' + id) + L.DomUtil.addClass(child, 'active'); + else if (L.DomUtil.hasClass(child, 'active')) + L.DomUtil.removeClass(child, 'active'); + } + + this.fire('content', { id: id }); + + // Open sidebar if it's closed + if (L.DomUtil.hasClass(this._container, 'collapsed')) { + this.fire('opening'); + L.DomUtil.removeClass(this._container, 'collapsed'); + if (this.options.autopan) this._panMap('open'); + } + + return this; + }, + + /** + * Close the sidebar (if it's open). + * + * @returns {L.Control.Sidebar} + */ + close: function() { + var i; + + // Remove old active highlights + for (i = 0; i < this._tabitems.length; i++) { + var child = this._tabitems[i]; + if (L.DomUtil.hasClass(child, 'active')) + L.DomUtil.removeClass(child, 'active'); + } + + // close sidebar, if it's opened + if (!L.DomUtil.hasClass(this._container, 'collapsed')) { + this.fire('closing'); + L.DomUtil.addClass(this._container, 'collapsed'); + if (this.options.autopan) this._panMap('close'); + } + + return this; + }, + + /** + * Add a panel to the sidebar + * + * @example + * sidebar.addPanel({ + * id: 'userinfo', + * tab: '', + * pane: someDomNode.innerHTML, + * position: 'bottom' + * }); + * + * @param {Object} [data] contains the data for the new Panel: + * @param {String} [data.id] the ID for the new Panel, must be unique for the whole page + * @param {String} [data.position='top'] where the tab will appear: + * on the top or the bottom of the sidebar. 'top' or 'bottom' + * @param {HTMLString} {DOMnode} [data.tab] content of the tab item, as HTMLstring or DOM node + * @param {HTMLString} {DOMnode} [data.pane] content of the panel, as HTMLstring or DOM node + * @param {String} [data.link] URL to an (external) link that will be opened instead of a panel + * @param {String} [data.title] Title for the pane header + * @param {String} {Function} [data.button] URL to an (external) link or a click listener function that will be opened instead of a panel + * @param {bool} [data.disabled] If the tab should be disabled by default + * + * @returns {L.Control.Sidebar} + */ + addPanel: function(data) { + var pane, tab, tabHref, closeButtons, content; + + // Create tab node + tab = L.DomUtil.create('li', data.disabled ? 'disabled' : ''); + tabHref = L.DomUtil.create('a', '', tab); + tabHref.href = '#' + data.id; + tabHref.setAttribute('role', 'tab'); + tabHref.innerHTML = data.tab; + tab._sidebar = this; + tab._id = data.id; + tab._button = data.button; // to allow links to be disabled, the href cannot be used + if (data.title && data.title[0] !== '<') tab.title = data.title; + + // append it to the DOM and store JS references + if (data.position === 'bottom') + this._tabContainerBottom.appendChild(tab); + else + this._tabContainerTop.appendChild(tab); + + this._tabitems.push(tab); + + // Create pane node + if (data.pane) { + if (typeof data.pane === 'string') { + // pane is given as HTML string + pane = L.DomUtil.create('DIV', 'leaflet-sidebar-pane', this._paneContainer); + content = ''; + if (data.title) + content += '

    ' + data.title; + if (this.options.closeButton) + content += ''; + if (data.title) + content += '

    '; + pane.innerHTML = content + data.pane; + } else { + // pane is given as DOM object + pane = data.pane; + this._paneContainer.appendChild(pane); + } + pane.id = data.id; + + this._panes.push(pane); + + // Save references to close button & register click listener + closeButtons = pane.querySelectorAll('.leaflet-sidebar-close'); + if (closeButtons.length) { + // select last button, because thats rendered on top + this._closeButtons.push(closeButtons[closeButtons.length - 1]); + this._closeClick(closeButtons[closeButtons.length - 1], 'on'); + } + } + + // Register click listeners, if the sidebar is on the map + this._tabClick(tab, 'on'); + + return this; + }, + + /** + * Removes a panel from the sidebar + * + * @example + * sidebar.remove('userinfo'); + * + * @param {String} [id] the ID of the panel that is to be removed + * @returns {L.Control.Sidebar} + */ + removePanel: function(id) { + var i, j, tab, pane, closeButtons; + + // find the tab & panel by ID, remove them, and clean up + for (i = 0; i < this._tabitems.length; i++) { + if (this._tabitems[i]._id === id) { + tab = this._tabitems[i]; + + // Remove click listeners + this._tabClick(tab, 'off'); + + tab.remove(); + this._tabitems.splice(i, 1); + break; + } + } + + for (i = 0; i < this._panes.length; i++) { + if (this._panes[i].id === id) { + pane = this._panes[i]; + closeButtons = pane.querySelectorAll('.leaflet-sidebar-close'); + for (j = 0; j < closeButtons.length; j++) { + this._closeClick(closeButtons[j], 'off'); + } + + pane.remove(); + this._panes.splice(i, 1); + + break; + } + } + + return this; + }, + + /** + * enables a disabled tab/panel + * + * @param {String} [id] ID of the panel to enable + * @returns {L.Control.Sidebar} + */ + enablePanel: function(id) { + var tab = this._getTab(id); + L.DomUtil.removeClass(tab, 'disabled'); + + return this; + }, + + /** + * disables an enabled tab/panel + * + * @param {String} [id] ID of the panel to disable + * @returns {L.Control.Sidebar} + */ + disablePanel: function(id) { + var tab = this._getTab(id); + L.DomUtil.addClass(tab, 'disabled'); + + return this; + }, + + onTabClick: function(e) { + // `this` points to the tab DOM element! + if (L.DomUtil.hasClass(this, 'active')) { + this._sidebar.close(); + } else if (!L.DomUtil.hasClass(this, 'disabled')) { + if (typeof this._button === 'string') // an url + window.location.href = this._button; + else if (typeof this._button === 'function') // a clickhandler + this._button(e); + else // a normal pane + this._sidebar.open(this.querySelector('a').hash.slice(1)); + } + }, + + /** + * (un)registers the onclick event for the given tab, + * depending on the second argument. + * @private + * + * @param {DOMelement} [tab] + * @param {String} [on] 'on' or 'off' + */ + _tabClick: function(tab, on) { + var link = tab.querySelector('a'); + if (!link.hasAttribute('href') || link.getAttribute('href')[0] !== '#') + return; + + if (on === 'on') { + L.DomEvent + .on(tab.querySelector('a'), 'click', L.DomEvent.preventDefault, tab) + .on(tab.querySelector('a'), 'click', this.onTabClick, tab); + } else { + L.DomEvent.off(tab.querySelector('a'), 'click', this.onTabClick, tab); + } + }, + + onCloseClick: function() { + this.close(); + }, + + /** + * (un)registers the onclick event for the given close button + * depending on the second argument + * @private + * + * @param {DOMelement} [closeButton] + * @param {String} [on] 'on' or 'off' + */ + _closeClick: function(closeButton, on) { + if (on === 'on') { + L.DomEvent.on(closeButton, 'click', this.onCloseClick, this); + } else { + L.DomEvent.off(closeButton, 'click', this.onCloseClick); + } + }, + + /** + * Finds & returns the DOMelement of a tab + * + * @param {String} [id] the id of the tab + * @returns {DOMelement} the tab specified by id, null if not found + */ + _getTab: function(id) { + for (var i = 0; i < this._tabitems.length; i++) { + if (this._tabitems[i]._id === id) + return this._tabitems[i]; + } + + throw Error('tab "' + id + '" not found'); + }, + + /** + * Helper for autopan: Pans the map for open/close events + * + * @param {String} [openClose] The behaviour to enact ('open' | 'close') + */ + _panMap: function(openClose) { + var panWidth = Number.parseInt(L.DomUtil.getStyle(this._container, 'max-width')) / 2; + if ( + openClose === 'open' && this.options.position === 'left' || + openClose === 'close' && this.options.position === 'right' + ) panWidth *= -1; + this._map.panBy([panWidth, 0], { duration: 0.5 }); + } +}); + +/** + * Create a new sidebar. + * + * @example + * var sidebar = L.control.sidebar({ container: 'sidebar' }).addTo(map); + * + * @param {Object} [options] - Optional options object + * @param {string} [options.autopan=false] - whether to move the map when opening the sidebar to make maintain the visible center point + * @param {string} [options.position=left] - Position of the sidebar: 'left' or 'right' + * @param {string} [options.container] - ID of a predefined sidebar container that should be used + * @param {boolean} [data.close=true] Whether to add a close button to the pane header + * @returns {Sidebar} A new sidebar instance + */ +L.control.sidebar = function(options, deprecated) { + return new L.Control.Sidebar(options, deprecated); +}; \ No newline at end of file