').addClass('search-bar-autocomplete-result-item-wrapper')
if (item.icon) {
wrapper.append(`
`)
}
if ('element' === item.type) {
wrapper.addClass('nested')
}
wrapper.append(`
${item.label}
`)
if (item.subLabel) {
wrapper.append(`
${item.subLabel}
`)
}
return li.append(wrapper).appendTo(ul)
},
})
this.searchInput().gogoAutocomplete({
appendTo: '.autocomplete-container',
classes: {
'ui-autocomplete': 'search-bar-autocomplete-results-container gogo-section-content',
},
position: {
at: 'left bottom+5',
},
source: ({ term }, response) => {
this.beforeSearch()
App.search.searchTerm(term, (elementsResults, optionsResults, locationsResults) => {
this.searchLoading(true)
this.setAutocompleteItems(term, elementsResults, optionsResults, locationsResults, response)
})
},
focus: (event) => event.preventDefault(),
select: (event, ui) => {
this.beforeSearch()
switch (ui.item.type) {
case 'search_geocoded':
this.searchGeocoded(ui.item.value)
break
case 'geocoded':
this.showGeocoded(ui.item.value)
break
case 'option':
this.searchOption(ui.item.value)
break
case 'search_elements':
this.searchElements(ui.item.value.term, ui.item.value.results)
break
case 'element':
this.searchElement(ui.item.value)
break
}
event.preventDefault()
},
})
this.searchInput().keypress((e) => {
// Enter pressed
if (e.keyCode === 13) {
this.handleSearchAction()
}
})
this.searchInput().click(() => this.searchInput().gogoAutocomplete('search'))
$('.search-bar-icon, .search-bar-btn').click(() => this.handleSearchAction())
$('#btn-close-search-result').click(() => this.resetElementsSearchResult())
$('.search-geolocalize').tooltip()
$('.search-geolocalize').click(() => this.geolocateUser())
$('#search-overlay-mobile .overlay').click(() => this.hideMobileSearchBar())
}
private setAutocompleteItems(
term: string,
elementsResults: SearchResult[],
optionsResults: SearchResult[],
locationsResults: SearchResult[],
response: (data: AutocompleteItem[]) => void,
): void {
let items: AutocompleteItem[] = []
if (App.config.isFeatureAvailable('searchPlace') && !App.config.search.canAutocomplete) {
items = [
...items,
{
label: App.config.translate('geographic.location'),
type: 'search_geocoded',
value: term,
icon: 'gogo-icon-marker-symbol',
},
]
}
if (locationsResults.length > 0) {
items = [
...items,
...locationsResults.slice(0, 4).map(({ type, value }: { type: string; value: Geocoded }) => ({
type,
value,
label: `
${
value.getFormattedAddress() ||
value.getStreetName() ||
value.getSubLocality() ||
value.getLocality() ||
value.getRegion() ||
value.getCountry()
}`,
icon: 'gogo-icon-marker-symbol',
})),
]
}
if (optionsResults.length > 0) {
items = [
...items,
...optionsResults.slice(0, 10).map(({ type, value }: { type: string; value: Option }) => ({
type,
value,
label: `
${App.config.translate('category')}
${value.name}`,
icon: value.icon || 'gogo-icon-stamp-2',
})),
]
}
if (elementsResults.length > 0) {
items.push({
label: `${App.config.translate('elements.containing')}
${term} (${
elementsResults.length
})`,
type: 'search_elements',
value: {
term,
results: {
data: elementsResults.map((elementResult) => elementResult.value),
},
},
icon: 'gogo-icon-database',
})
items = [
...items,
...elementsResults.slice(0, 4).map(({ type, value }) => {
const elementItem: AutocompleteItem = {
type,
value,
label: `
${value.name}`,
}
if (value.address) {
let subLabel = ''
if (value.address.postalCode) {
subLabel += value.address.postalCode
}
if (value.address.addressLocality) {
subLabel += (subLabel ? ' ' : '') + value.address.addressLocality
}
if (subLabel) {
elementItem.subLabel = subLabel
}
}
return elementItem
}),
]
}
response(items)
}
private searchGeocoded(address: string): void {
if (App.config.mode.autocompleteOnly) {
const route = App.routerModule.generate('normal', { mode: 'carte', addressAndViewport: address })
this.searchInput().trigger('searchRoute', route)
this.searchInput().trigger('searchGeocoder', { value: address })
return
}
App.geocoder.geocodeAddress(
address,
(results) => {
this.showGeocoded(results[0])
},
() => {
this.searchLoading(true)
$('.search-no-result').show()
},
)
}
private showGeocodedFromArea(results: Geocoded | any[]): void {
return this.showGeocoded(results, true)
}
private showGeocoded(results: Geocoded | any[], keepState: boolean = false): void {
if (!Array.isArray(results)) {
results = [results]
}
if (App.config.mode.autocompleteOnly) {
const address = (results[0] as any).getDisplayName() // seems an error in universal gecoder, getFormatedAddress does not work
const route = App.routerModule.generate('normal', { mode: 'carte', addressAndViewport: address })
this.searchInput().trigger('searchRoute', route)
this.searchInput().trigger('searchGeocoder', { value: address })
return
} else {
this.searchLoading(true)
this.resetOptionSearchResult()
this.resetElementsSearchResult(false, keepState)
this.hideMobileSearchBar()
results.forEach((result) => {
const geojson = (result as any).shape
if (geojson && ['Polygon', 'MultiPolygon'].includes(geojson.type)) {
this.locationShapes.push(
L.geoJSON(geojson, {
style: function (feature) {
return { color: App.config.colors.primary, fillOpacity: 0.05, opacity: 0.7 }
},
}).addTo(App.map()),
)
} else {
this.displaySearchResultMarkerOnMap(
L.latLng(
result.getCoordinates().latitude, result.getCoordinates().longitude),
)
}
})
App.mapComponent.fitBounds(
App.geocoder.latLngBoundsFromRawBounds(this.getBoundsFromMultipleShapes(results)),
true,
)
}
}
getBoundsFromObject(object: any): BoundingBox {
try {
return object.getBounds()
} catch {
return {
latitudeSW: object.bounds[0],
longitudeSW: object.bounds[2],
latitudeNE: object.bounds[1],
longitudeNE: object.bounds[3],
}
}
}
getBoundsFromMultipleShapes(results: any[]): BoundingBox {
if (results.length === 1) {
const bounds = this.getBoundsFromObject(results[0])
return bounds
} else {
let minLat = 90
let maxLat = -90
let minLng = 180
let maxLng = -180
results.forEach((result: Geocoded) => {
const bounds = this.getBoundsFromObject(result)
minLat = Math.min(bounds.latitudeSW, bounds.latitudeNE, minLat)
maxLat = Math.max(bounds.latitudeSW, bounds.latitudeNE, maxLat)
minLng = Math.min(bounds.longitudeSW, bounds.longitudeNE, minLng)
maxLng = Math.max(bounds.longitudeSW, bounds.longitudeNE, maxLng)
})
const bounds = {
latitudeSW: minLat,
longitudeSW: minLng,
latitudeNE: maxLat,
longitudeNE: maxLng,
}
return bounds
}
}
searchOption(option: Option): void {
if (App.config.mode.autocompleteOnly) {
const route = App.routerModule.generate('search_option', { name: option.name, id: option.id })
this.searchInput().trigger('searchRoute', route)
this.searchInput().trigger('searchCategory', { name: option.name, id: option.id })
return
}
this.searchLoading(true)
this.resetOptionSearchResult(false)
this.resetElementsSearchResult(false)
this.hideMobileSearchBar()
if (App.config.menu.showOnePanePerMainOption) {
// Uncheck only the pane categories
option.parentCategoryIds.forEach((parentCategoryId) => {
const category = App.taxonomyModule.getCategoryById(parentCategoryId.id)
if (category.isRootCategory) {
return
}
category.toggle(false, false)
})
} else {
// Uncheck all categories
App.taxonomyModule.categories.forEach((category) => category.toggle(false, false))
// Re-check all root categories except the parent one
const parentCategoryIds = option.parentCategoryIds.map((item) => item.id)
App.taxonomyModule.categories
.filter((category) => category.isRootCategory && !parentCategoryIds.includes(category.id))
.forEach((category) => category.toggle(true, false))
}
// Expand the related categories and check the mandatory sibling categories
option.parentCategoryIds.forEach((parentCategoryId) => {
App.taxonomyModule.getCategoryById(parentCategoryId.id).toggleChildrenDetail(true)
parentCategoryId.mandatorySiblingIds.forEach((categoryId) =>
App.taxonomyModule.getCategoryById(categoryId).toggle(true, false),
)
})
// Expand the related options
option.parentOptionIds.forEach((parentOptionId) =>
App.filtersComponent.setOption(parentOptionId, false, false, true),
)
// Check the option
App.filtersComponent.setOption(option.id, false, true, false, true)
}
private searchElements(term: string, searchResults, backFromHistory = false): void {
if (App.config.mode.autocompleteOnly) {
const route = App.routerModule.generate('search', { mode: 'carte', text: term })
this.searchInput().trigger('searchRoute', route)
this.searchInput().trigger('searchElements', { value: term })
return
}
this.searchLoading(true)
this.resetOptionSearchResult()
this.hideMobileSearchBar()
this.hideSearchBarAutocomplete()
this.currSearchText = term
App.setDataType(AppDataType.SearchResults, backFromHistory, searchResults)
this.showSearchResultLabel(searchResults.data.length)
App.gogoControlComponent.updatePosition()
}
private searchElement(element): void {
if (App.config.mode.autocompleteOnly) {
const route = App.routerModule.generate('show_element', { name: element.name, id: element.id })
this.searchInput().trigger('searchRoute', route)
this.searchInput().trigger('searchElement', { name: element.name, id: element.id })
return
}
this.searchLoading(true)
this.resetOptionSearchResult()
this.resetSearchResult(false)
this.hideMobileSearchBar()
this.hideSearchBarAutocomplete()
if (!element.isFullyLoaded) App.elementsJsonModule.convertJsonElements([element], true, true) // add it so it can retrieved again by it's ID
App.setState(AppStates.ShowElement, { id: element.id, mapPan: true })
}
// Handle forced search action by user (input key enter pressed, icon click)
private handleSearchAction(): void {
const searchTermRaw = this.searchInput().val()
const searchTerm = String(searchTermRaw || '')
this.beforeSearch()
App.search.searchTerm(
searchTerm,
(elementsResults, _, locationsResults) => {
this.searchLoading(true)
if (locationsResults.length > 0) {
const matchingLocations: Geocoded[] = locationsResults
.map((locationResult) => locationResult.value)
.filter(
(location: Geocoded) =>
App.search.compareResult(searchTerm, location.getLocality()) ||
App.search.compareResult(searchTerm, location.getRegion()),
)
if (matchingLocations.length > 0) {
this.searchGeocoded(searchTerm)
return
}
}
if (elementsResults.length > 0) {
if (elementsResults.length == 1) {
this.searchElement(elementsResults[0].value)
} else {
this.searchElements(searchTerm, {
data: elementsResults.map((elementResult) => elementResult.value),
})
}
return
}
// Fallback to geocoded search if no precise results found in locations or elements.
if (App.config.isFeatureAvailable('searchPlace')) {
this.searchGeocoded(searchTerm)
}
},
true,
)
}
public handleGeocodeResult(): void {
this.setValue(App.geocoder.getLocationAddress())
}
public geolocateUser(): void {
this.beforeSearch()
App.geocoder.geolocateUser(
(viewport: ViewPort) => {
this.resetOptionSearchResult()
this.resetElementsSearchResult()
this.setValue(App.config.translate('geolocalized'))
this.displaySearchResultMarkerOnMap(L.latLng(viewport.toLocation()))
},
() => {
this.searchLoading(true)
},
)
}
public loadSearchElements(text: string, backFromHistory = false): void {
this.setValue(text)
this.currSearchText = text
const route = App.config.features.searchElements.url
const data = { text }
if (route) {
App.ajaxModule.sendRequest(
route,
'get',
data,
(searchResult) => this.searchElements(text, searchResult, backFromHistory),
() => {
this.searchLoading(true)
//App.geocoder.geocodeAddress('');
},
)
} else {
this.searchElements(
text,
{ data: App.search.searchInResults(text, App.elementsModule.allElements(), (element: any) => element.name) },
backFromHistory,
)
}
}
public showMobileSearchBar(): void {
$('#search-overlay-mobile').fadeIn(250)
$('.search-bar-with-options-container').show()
$('.search-bar').focus()
App.gogoControlComponent.hide(0)
}
public hideMobileSearchBar(): void {
$('#search-overlay-mobile').fadeOut(150)
$('.search-bar-with-options-container.mobile').hide()
App.gogoControlComponent.show(0)
}
private hideSearchBarAutocomplete() {
$('.search-bar-autocomplete-results-container').hide()
}
lastComponentWidth = null
public update(): void {
// prevent updating for nothing, because if you are typing something in the search bar
// while the resize happen, it loose the focus
if (App.component.width() == this.lastComponentWidth) return
if (App.component.width() <= 600) {
const mobileSearchBar = $('.search-bar-with-options-container')
if (mobileSearchBar.parent('#search-overlay-mobile').length != 1) {
$('.search-bar-with-options-container')
.appendTo('#search-overlay-mobile')
.addClass('mobile gogo-section-content')
}
} else {
$('.search-bar-with-options-container')
.removeClass('mobile gogo-section-content')
.prependTo('.directory-menu-header')
.show()
}
this.lastComponentWidth = App.component.width()
}
private searchLoading(stop = false): void {
if (stop) {
$('.search-bar-icon').removeClass('loading')
return
}
$('.search-bar-icon').addClass('loading')
}
private showSearchResultLabel($number: number): void {
$('.search-result-number').text($number)
$('.search-result-value').text(this.currSearchText)
$('.search-results').show()
$('#element-info-bar').addClass('with-search-result-header')
}
private hideSearchResult(): void {
$('.search-results').hide()
$('#element-info-bar').removeClass('with-search-result-header')
App.gogoControlComponent.updatePosition()
}
private resetOptionSearchResult(setAllOption = true): void {
App.taxonomyModule.categories.forEach((category) => category.toggle(true, false))
if (setAllOption) {
App.filtersComponent.setMainOption('all')
}
}
private resetElementsSearchResult(resetValue = true, keepState = true): void {
this.resetSearchResult(resetValue)
App.setMode(AppModes.Map, false, false)
if (keepState) {
if (App.component.isMobileScreen() && App.infoBarComponent.isDetailsVisible) {
App.infoBarComponent.toggleDetails()
}
} else {
App.setState(AppStates.Normal)
}
}
resetSearchResult(resetValue = true): void {
App.setDataType(AppDataType.All)
this.hideSearchResult()
this.clearLocationMarker()
this.currSearchText = ''
if (resetValue) {
this.setValue('')
App.elementListComponent.setTitle('')
}
}
private clearLocationMarker(): void {
if (this.locationMarker) {
this.locationMarker.remove()
}
this.clearLocationShapes()
}
public clearLocationShapes(): void {
if (this.locationShapes) {
this.locationShapes.forEach((locationShape) => {
locationShape.remove()
})
}
}
public setValue($value: string): void {
this.searchInput().val($value)
}
public getCurrSearchText(): string {
return this.currSearchText
}
private beforeSearch(): void {
$('.search-no-result').hide()
this.searchLoading()
}
private displaySearchResultMarkerOnMap(location: L.LatLng): void {
if (!App.map()) {
return
}
this.locationMarker = new L.Marker(location, {
icon: this.locationIcon,
}).addTo(App.map())
}
}