widgets: add a search control to map widget (#72558) #67
|
@ -3270,7 +3270,7 @@ class MapWidget(CompositeWidget):
|
|||
return None
|
||||
|
||||
def add_media(self):
|
||||
get_response().add_javascript(['qommon.map.js'])
|
||||
get_response().add_javascript(['qommon.map.js', 'leaflet-search.js'])
|
||||
|
||||
def _parse(self, request):
|
||||
CompositeWidget._parse(self, request)
|
||||
|
|
|
@ -612,3 +612,35 @@ div.location-icon {
|
|||
div.file-button .widget-message {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.leaflet-top.leaflet-right {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.leaflet-search {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
align-items: start;
|
||||
|
||||
&--control {
|
||||
width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
&.open &--control {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
&--result-list {
|
||||
background-image: none;
|
||||
padding-right: 0.7em;
|
||||
}
|
||||
|
||||
&--result-item {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,247 @@
|
|||
/* global L, $ */
|
||||
class SearchControl extends L.Control {
|
||||
options = {
|
||||
labels: {
|
||||
hint: 'Search adresses',
|
||||
error: 'An error occured while fetching results',
|
||||
searching: 'Searching...'
|
||||
},
|
||||
position: 'topright',
|
||||
searchUrl: '/api/geocoding',
|
||||
maxResults: 5
|
||||
}
|
||||
|
||||
constructor (options) {
|
||||
super()
|
||||
L.Util.setOptions(this, options)
|
||||
this._refreshTimeout = 0
|
||||
}
|
||||
|
||||
onAdd (map) {
|
||||
this._map = map
|
||||
this._container = L.DomUtil.create('div', 'leaflet-search')
|
||||
this._resultLocations = []
|
||||
|
||||
this._buttonBar = L.DomUtil.create('div', 'leaflet-bar', this._container)
|
||||
|
||||
this._toggleButton = L.DomUtil.create('a', '', this._buttonBar)
|
||||
this._toggleButton.href = '#'
|
||||
this._toggleButton.role = 'button'
|
||||
this._toggleButton.style.fontFamily = 'FontAwesome'
|
||||
this._toggleButton.text = '\uf002'
|
||||
this._toggleButton.title = this.options.labels.hint
|
||||
this._toggleButton.setAttribute('aria-label', this.options.labels.hint)
|
||||
|
||||
this._control = L.DomUtil.create('div', 'leaflet-search--control', this._container)
|
||||
this._control.style.visibility = 'collapse'
|
||||
|
||||
this._searchInput = L.DomUtil.create('input', '', this._control)
|
||||
this._searchInput.placeholder = this.options.labels.hint
|
||||
|
||||
this._feedback = L.DomUtil.create('div', '', this._control)
|
||||
|
||||
this._resultList = L.DomUtil.create('select', 'leaflet-search--result-list', this._control)
|
||||
this._resultList.size = this.options.maxResults
|
||||
this._resultList.style.visibility = "collapse"
|
||||
|
||||
L.DomEvent
|
||||
.on(this._container, 'click', L.DomEvent.stop, this)
|
||||
.on(this._control, 'focusin', this._onControlFocusIn, this)
|
||||
.on(this._control, 'focusout', this._onControlFocusOut, this)
|
||||
.on(this._control, 'keydown', this._onControlKeyDown, this)
|
||||
.on(this._toggleButton, 'click', this._onToggleButtonClick, this)
|
||||
.on(this._searchInput, 'keydown', this._onSearchInputKeyDown, this)
|
||||
.on(this._searchInput, 'input', this._onSearchInput, this)
|
||||
.on(this._resultList, 'change', this._onResultListChange, this)
|
||||
.on(this._resultList, 'click', this._onResultListClick, this)
|
||||
.on(this._resultList, 'keydown', this._onResultListKeyDown, this)
|
||||
|
||||
return this._container
|
||||
}
|
||||
|
||||
onRemove (map) {
|
||||
}
|
||||
|
||||
_showControl() {
|
||||
this._container.classList.add("open")
|
||||
this._buttonBar.style.visibility = 'collapse'
|
||||
this._control.style.removeProperty('visibility')
|
||||
this._initialBounds = this._map.getBounds()
|
||||
setTimeout(() => this._searchInput.focus(), 50)
|
||||
}
|
||||
|
||||
_hideControl(resetBounds) {
|
||||
this._container.classList.remove("open")
|
||||
if (resetBounds) {
|
||||
this._map.fitBounds(this._initialBounds)
|
||||
}
|
||||
|
||||
this._buttonBar.style.removeProperty('visibility')
|
||||
this._control.style.visibility = 'collapse'
|
||||
this._toggleButton.focus()
|
||||
this._resultList.selectedIndex = -1
|
||||
}
|
||||
|
||||
_onControlFocusIn (event) {
|
||||
clearTimeout(this._hideTimeout)
|
||||
}
|
||||
|
||||
_onControlFocusOut (event) {
|
||||
// need to debounce here because leaflet raises focusout then focusin when
|
||||
// clicking on an already focused child element.
|
||||
this._hideTimeout = setTimeout(() => this._hideControl(), 50)
|
||||
}
|
||||
|
||||
_getSelectedLocation() {
|
||||
let selectedIndex = this._resultList.selectedIndex
|
||||
if(selectedIndex == -1) {
|
||||
if(this._resultLocations.length === 0) {
|
||||
return null
|
||||
}
|
||||
selectedIndex = 0
|
||||
}
|
||||
|
||||
return this._resultLocations[selectedIndex]
|
||||
}
|
||||
|
||||
_focusLocation (location) {
|
||||
this._map.fitBounds(location.bounds)
|
||||
}
|
||||
|
||||
_validateLocation (location) {
|
||||
this._focusLocation(location)
|
||||
this._hideControl()
|
||||
}
|
||||
|
||||
_onControlKeyDown (event) {
|
||||
if (event.keyCode === 27) { // escape
|
||||
this._hideControl(true)
|
||||
event.preventDefault()
|
||||
} else if (event.keyCode === 13) { // enter
|
||||
const selectedLocation = this._getSelectedLocation()
|
||||
if (selectedLocation) {
|
||||
this._validateLocation(selectedLocation)
|
||||
}
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
_onToggleButtonClick () {
|
||||
this._showControl()
|
||||
}
|
||||
|
||||
_selectIndex(index) {
|
||||
this._resultList.selectedIndex = index
|
||||
const selectedLocation = this._getSelectedLocation()
|
||||
this._focusLocation(selectedLocation)
|
||||
this._resultList.focus()
|
||||
}
|
||||
|
||||
_onSearchInputKeyDown (event) {
|
||||
const results = this._resultList.options
|
||||
if (results.length == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (event.keyCode === 38) {
|
||||
this._selectIndex(results.length - 1)
|
||||
event.preventDefault()
|
||||
} else if (event.keyCode === 40) {
|
||||
this._selectIndex(0)
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
_clearResults() {
|
||||
while (this._resultList.lastElementChild) {
|
||||
this._resultList.removeChild(this._resultList.lastElementChild)
|
||||
}
|
||||
this._resultList.style.visibility = 'collapse'
|
||||
this._resultLocations = []
|
||||
}
|
||||
|
||||
_fetchResults () {
|
||||
const searchString = this._searchInput.value
|
||||
|
||||
if (!searchString) {
|
||||
return
|
||||
}
|
||||
|
||||
this._clearResults()
|
||||
|
||||
this._feedback.innerHTML = this.options.labels.searching
|
||||
this._feedback.classList.remove("error")
|
||||
|
||||
$.ajax({
|
||||
url: this.options.searchUrl,
|
||||
data: { q: searchString },
|
||||
success: (data) => {
|
||||
this._feedback.innerHTML = ""
|
||||
this._resultLocations = []
|
||||
var firstResults = data.slice(0, this.options.maxResults)
|
||||
|
||||
if(firstResults.length == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
this._resultList.style.removeProperty('visibility')
|
||||
this._resultList.size = Math.max(2, firstResults.length)
|
||||
|
||||
for (const result of firstResults) {
|
||||
const resultItem = L.DomUtil.create('option', 'leaflet-search--result-item', this._resultList)
|
||||
resultItem.innerHTML = result.display_name
|
||||
resultItem.title = result.display_name
|
||||
|
||||
const bbox = result.boundingbox
|
||||
|
||||
this._resultLocations.push({
|
||||
latlng: L.latLng(result.lat, result.lon),
|
||||
bounds: L.latLngBounds(
|
||||
L.latLng(bbox[0], bbox[2]),
|
||||
L.latLng(bbox[1], bbox[3])
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this._feedback.innerHTML = this.options.labels.error
|
||||
this._feedback.classList.add("error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_onSearchInput () {
|
||||
clearTimeout(this._refreshTimeout)
|
||||
if (this._searchInput.value === '') {
|
||||
this._clearResults()
|
||||
} else {
|
||||
this._refreshTimeout = setTimeout(() => this._fetchResults(), 250)
|
||||
}
|
||||
}
|
||||
|
||||
_onResultListChange (event) {
|
||||
const selectedLocation = this._getSelectedLocation()
|
||||
this._focusLocation(selectedLocation)
|
||||
}
|
||||
|
||||
_onResultListClick (event) {
|
||||
const selectedLocation = this._getSelectedLocation()
|
||||
this._validateLocation(selectedLocation)
|
||||
}
|
||||
|
||||
_onResultListKeyDown (event) {
|
||||
const results = this._resultList.options
|
||||
if (
|
||||
(event.keyCode === 38 && this._resultList.selectedIndex === 0) ||
|
||||
(event.keyCode === 40 && this._resultList.selectedIndex === results.length - 1)
|
||||
) {
|
||||
this._searchInput.focus()
|
||||
this._resultList.selectedIndex = -1
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(SearchControl.prototype, L.Mixin.Events)
|
||||
|
||||
L.Control.Search = SearchControl
|
|
@ -121,6 +121,18 @@ $(window).on('wcs:maps-init', function() {
|
|||
$map_widget.trigger('set-geolocation', e.latlng);
|
||||
});
|
||||
}
|
||||
|
||||
var search_control = new L.Control.Search({
|
||||
labels: {
|
||||
hint: WCS_I18N.map_search_hint,
|
||||
error: WCS_I18N.map_search_error,
|
||||
searching: WCS_I18N.map_search_searching,
|
||||
},
|
||||
searchUrl: $map_widget.data('search-url')
|
||||
});
|
||||
|
||||
map.addControl(search_control);
|
||||
|
||||
$map_widget.on('set-geolocation', function(e, coords, options) {
|
||||
if (map.marker === null) {
|
||||
map.marker = L.marker([0, 0]);
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
data-init-lat="{{ widget.initial_position.lat }}"
|
||||
data-init-lng="{{ widget.initial_position.lng }}"
|
||||
{% endif %}
|
||||
data-search-url="{% url 'api-geocoding' %}"
|
||||
{% block widget-control-attributes %}{% endblock %}
|
||||
></div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -74,6 +74,9 @@ def i18n_js(request):
|
|||
'geoloc_permission_denied': _('Geolocation: permission denied'),
|
||||
'geoloc_position_unavailable': _('Geolocation: position unavailable'),
|
||||
'geoloc_timeout': _('Geolocation: timeout'),
|
||||
'map_search_error': _('An error occured while fetching results'),
|
||||
'map_search_hint': _('Search address'),
|
||||
'map_search_searching': _('Searching...'),
|
||||
'map_zoom_in': _('Zoom in'),
|
||||
'map_zoom_out': _('Zoom out'),
|
||||
'map_display_position': _('Display my position'),
|
||||
|
|
Loading…
Reference in New Issue