widgets: add a search control to map widget (#72558) #67

Merged
csechet merged 1 commits from wip/72558-integrer-un-bouton-de-recherche- into main 2023-02-16 20:56:58 +01:00
6 changed files with 296 additions and 1 deletions

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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

View File

@ -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]);

View File

@ -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 %}

View File

@ -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'),