leaflet-search
Version:
Leaflet Control for searching markers/features by attribute on map or remote searching in jsonp/ajax
953 lines (803 loc) • 33.7 kB
JavaScript
/*
Name Data passed Description
Managed Events:
search:locationfound {latlng, title, layer} fired after moved and show markerLocation
search:expanded {} fired after control was expanded
search:collapsed {} fired after control was collapsed
search:cancel {} fired after cancel button clicked
Public methods:
setLayer() L.LayerGroup() set layer search at runtime
showAlert() 'Text message' show alert message
searchText() 'Text searched' search text by external code
*/
// TODO implement can do research on multiple sources layers and remote
// TODO history: false, //show latest searches in tooltip
// FIXME option condition problem {autoCollapse: true, markerLocation: true} not show location
// FIXME option condition problem {autoCollapse: false }
//
// TODO here insert function search inputText FIRST in _recordsCache keys and if not find results..
// run one of callbacks search(sourceData,jsonpUrl or options.layer) and run this.showTooltip
//
// TODO change structure of _recordsCache
// like this: _recordsCache = {"text-key1": {loc:[lat,lng], ..other attributes.. }, {"text-key2": {loc:[lat,lng]}...}, ...}
// in this mode every record can have a free structure of attributes, only 'loc' is required
// TODO important optimization!!! always append data in this._recordsCache
// now _recordsCache content is emptied and replaced with new data founded
// always appending data on _recordsCache give the possibility of caching ajax, jsonp and layersearch!
//
// TODO here insert function search inputText FIRST in _recordsCache keys and if not find results..
// run one of callbacks search(sourceData,jsonpUrl or options.layer) and run this.showTooltip
//
// TODO change structure of _recordsCache
// like this: _recordsCache = {"text-key1": {loc:[lat,lng], ..other attributes.. }, {"text-key2": {loc:[lat,lng]}...}, ...}
// in this way every record can have a free structure of attributes, only 'loc' is required
(function (factory) {
// eslint-disable-next-line
if (typeof define === 'function' && define.amd) {
// AMD
// eslint-disable-next-line
define(['leaflet'], factory)
} else if (typeof module !== 'undefined') {
// Node/CommonJS
module.exports = factory(require('leaflet'))
} else {
// Browser globals
if (typeof window.L === 'undefined') { throw new Error('Leaflet must be loaded first') }
factory(window.L)
}
})(function (L) {
L.Control.Search = L.Control.extend({
includes: L.version[0] === '1' ? L.Evented.prototype : L.Mixin.Events,
options: {
url: '', // url for search by ajax request, ex: "search.php?q={s}". Can be function to returns string for dynamic parameter setting
layer: null, // layer where search markers(is a L.LayerGroup)
sourceData: null, // function to fill _recordsCache, passed searching text by first param and callback in second
// TODO implements uniq option 'sourceData' to recognizes source type: url,array,callback or layer
jsonpParam: null, // jsonp param name for search by jsonp service, ex: "callback"
propertyLoc: 'loc', // field for remapping location, using array: ['latname','lonname'] for select double fields(ex. ['lat','lon'] ) support dotted format: 'prop.subprop.title'
propertyName: 'title', // property in marker.options(or feature.properties for vector layer) trough filter elements in layer,
formatData: null, // callback for reformat all data from source to indexed data object
filterData: null, // callback for filtering data from text searched, params: textSearch, allRecords
moveToLocation: null, // callback run on location found, params: latlng, title, map
buildTip: null, // function to return row tip html node(or html string), receive text tooltip in first param
container: '', // container id to insert Search Control
zoom: null, // default zoom level for move to location
minLength: 1, // minimal text length for autocomplete
initial: true, // search elements only by initial text
casesensitive: false, // search elements in case sensitive text
autoType: true, // complete input with first suggested result and select this filled-in text.
delayType: 400, // delay while typing for show tooltip
tooltipLimit: -1, // limit max results to show in tooltip. -1 for no limit, 0 for no results
tipAutoSubmit: true, // auto map panTo when click on tooltip
firstTipSubmit: false, // auto select first result con enter click
autoResize: true, // autoresize on input change
collapsed: true, // collapse search control at startup
autoCollapse: false, // collapse search control after submit(on button or on tips if enabled tipAutoSubmit)
autoCollapseTime: 1200, // delay for autoclosing alert and collapse after blur
textErr: 'Location not found', // error message
textCancel: 'Cancel', // title in cancel button
textPlaceholder: 'Search...', // placeholder value
hideMarkerOnCollapse: false, // remove circle and marker on search control collapsed
position: 'topleft',
marker: { // custom L.Marker or false for hide
icon: false, // custom L.Icon for maker location or false for hide
animate: true, // animate a circle over location found
circle: { // draw a circle in location found
radius: 10,
weight: 3,
color: '#e03',
stroke: true,
fill: false
}
}
},
_getPath: function (obj, prop) {
const parts = prop.split('.')
const last = parts.pop()
const len = parts.length
let cur = parts[0]
let i = 1
if (len > 0) {
while ((obj = obj[cur]) && i < len) { cur = parts[i++] }
}
if (obj) { return obj[last] }
},
_isObject: function (obj) {
return Object.prototype.toString.call(obj) === '[object Object]'
},
initialize: function (options) {
L.Util.setOptions(this, options || {})
this._inputMinSize = this.options.textPlaceholder ? this.options.textPlaceholder.length : 10
this._layer = this.options.layer || new L.LayerGroup()
this._filterData = this.options.filterData || this._defaultFilterData
this._formatData = this.options.formatData || this._defaultFormatData
this._moveToLocation = this.options.moveToLocation || this._defaultMoveToLocation
this._autoTypeTmp = this.options.autoType // useful for disable autoType temporarily in delete/backspace keydown
this._countertips = 0 // number of tips items
this._recordsCache = {} // key,value table! to store locations! format: key,latlng
this._curReq = null
},
onAdd: function (map) {
this._map = map
this._container = L.DomUtil.create('div', 'leaflet-control-search')
this._input = this._createInput(this.options.textPlaceholder, 'search-input')
this._tooltip = this._createTooltip('search-tooltip')
this._cancel = this._createCancel(this.options.textCancel, 'search-cancel')
this._button = this._createButton(this.options.textPlaceholder, 'search-button')
this._alert = this._createAlert('search-alert')
if (this.options.collapsed === false) { this.expand(this.options.collapsed) }
if (this.options.marker) {
if (this.options.marker instanceof L.Marker || this.options.marker instanceof L.CircleMarker) { this._markerSearch = this.options.marker } else if (this._isObject(this.options.marker)) { this._markerSearch = new L.Control.Search.Marker([0, 0], this.options.marker) }
this._markerSearch._isMarkerSearch = true
}
this.setLayer(this._layer)
map.on({
// 'layeradd': this._onLayerAddRemove,
// 'layerremove': this._onLayerAddRemove
resize: this._handleAutoresize
}, this)
return this._container
},
addTo: function (map) {
if (this.options.container) {
this._container = this.onAdd(map)
this._wrapper = L.DomUtil.get(this.options.container)
this._wrapper.style.position = 'relative'
this._wrapper.appendChild(this._container)
} else { L.Control.prototype.addTo.call(this, map) }
return this
},
onRemove: function (map) {
this._recordsCache = {}
// map.off({
// 'layeradd': this._onLayerAddRemove,
// 'layerremove': this._onLayerAddRemove
// }, this);
map.off({
// 'layeradd': this._onLayerAddRemove,
// 'layerremove': this._onLayerAddRemove
resize: this._handleAutoresize
}, this)
},
// _onLayerAddRemove: function(e) {
// //without this, run setLayer also for each Markers!! to optimize!
// if(e.layer instanceof L.LayerGroup)
// if( L.stamp(e.layer) != L.stamp(this._layer) )
// this.setLayer(e.layer);
// },
setLayer: function (layer) { // set search layer at runtime
// this.options.layer = layer; //setting this, run only this._recordsFromLayer()
this._layer = layer
this._layer.addTo(this._map)
return this
},
showAlert: function (text) {
const self = this
text = text || this.options.textErr
this._alert.style.display = 'block'
this._alert.innerHTML = text
clearTimeout(this.timerAlert)
this.timerAlert = setTimeout(function () {
self.hideAlert()
}, this.options.autoCollapseTime)
return this
},
hideAlert: function () {
this._alert.style.display = 'none'
return this
},
cancel: function () {
this._input.value = ''
this._handleKeypress({ keyCode: 8 })// simulate backspace keypress
this._input.size = this._inputMinSize
this._input.focus()
this._cancel.style.display = 'none'
this._hideTooltip()
this.fire('search:cancel')
return this
},
expand: function (toggle) {
toggle = typeof toggle === 'boolean' ? toggle : true
this._input.style.display = 'block'
L.DomUtil.addClass(this._container, 'search-exp')
if (toggle !== false) {
this._input.focus()
this._map.on('dragstart click', this.collapse, this)
}
this.fire('search:expanded')
return this
},
collapse: function () {
this._hideTooltip()
this.cancel()
this._alert.style.display = 'none'
this._input.blur()
if (this.options.collapsed) {
this._input.style.display = 'none'
this._cancel.style.display = 'none'
L.DomUtil.removeClass(this._container, 'search-exp')
if (this.options.hideMarkerOnCollapse) {
this._map.removeLayer(this._markerSearch)
}
this._map.off('dragstart click', this.collapse, this)
}
this.fire('search:collapsed')
return this
},
collapseDelayed: function () { // collapse after delay, used on_input blur
const self = this
if (!this.options.autoCollapse) return this
clearTimeout(this.timerCollapse)
this.timerCollapse = setTimeout(function () {
self.collapse()
}, this.options.autoCollapseTime)
return this
},
collapseDelayedStop: function () {
clearTimeout(this.timerCollapse)
return this
},
/// /start DOM creations
_createAlert: function (className) {
const alert = L.DomUtil.create('div', className, this._container)
alert.style.display = 'none'
L.DomEvent
.on(alert, 'click', L.DomEvent.stop, this)
.on(alert, 'click', this.hideAlert, this)
return alert
},
_createInput: function (text, className) {
const self = this
const label = L.DomUtil.create('label', className, this._container)
const input = L.DomUtil.create('input', className, this._container)
input.type = 'text'
input.size = this._inputMinSize
input.value = ''
input.autocomplete = 'off'
input.autocorrect = 'off'
input.autocapitalize = 'off'
input.placeholder = text
input.style.display = 'none'
input.role = 'search'
input.id = input.role + input.type + input.size
label.htmlFor = input.id
label.style.display = 'none'
label.value = text
L.DomEvent
.disableClickPropagation(input)
.on(input, 'keyup', this._handleKeypress, this)
.on(input, 'paste', function (e) {
setTimeout(function (e) {
self._handleKeypress(e)
}, 10, e)
}, this)
.on(input, 'blur', this.collapseDelayed, this)
.on(input, 'focus', this.collapseDelayedStop, this)
return input
},
_createCancel: function (title, className) {
const cancel = L.DomUtil.create('a', className, this._container)
cancel.href = '#'
cancel.title = title
cancel.style.display = 'none'
cancel.innerHTML = '<span>⊗</span>'// imageless(see css)
L.DomEvent
.on(cancel, 'click', L.DomEvent.stop, this)
.on(cancel, 'click', this.cancel, this)
return cancel
},
_createButton: function (title, className) {
const button = L.DomUtil.create('a', className, this._container)
button.href = '#'
button.title = title
L.DomEvent
.on(button, 'click', L.DomEvent.stop, this)
.on(button, 'click', this._handleSubmit, this)
.on(button, 'focus', this.collapseDelayedStop, this)
.on(button, 'blur', this.collapseDelayed, this)
return button
},
_createTooltip: function (className) {
const self = this
const tool = L.DomUtil.create('ul', className, this._container)
tool.style.display = 'none'
L.DomEvent
.disableClickPropagation(tool)
.on(tool, 'blur', this.collapseDelayed, this)
.on(tool, 'wheel', function (e) {
self.collapseDelayedStop()
L.DomEvent.stopPropagation(e)// disable zoom map
}, this)
.on(tool, 'mouseover', function (e) {
self.collapseDelayedStop()
}, this)
return tool
},
_createTip: function (text, val) { // val is object in recordCache, usually is Latlng
let tip
if (this.options.buildTip) {
tip = this.options.buildTip.call(this, text, val) // custom tip node or html string
if (typeof tip === 'string') {
const tmpNode = L.DomUtil.create('div')
tmpNode.innerHTML = tip
tip = tmpNode.firstChild
}
} else {
tip = L.DomUtil.create('li', '')
tip.innerHTML = text
}
L.DomUtil.addClass(tip, 'search-tip')
tip._text = text // value replaced in this._input and used by _autoType
if (this.options.tipAutoSubmit) {
L.DomEvent
.disableClickPropagation(tip)
.on(tip, 'click', L.DomEvent.stop, this)
.on(tip, 'click', function (e) {
this._input.value = text
this._handleAutoresize()
this._input.focus()
this._hideTooltip()
this._handleSubmit()
}, this)
}
return tip
},
/// ///end DOM creations
_getUrl: function (text) {
return (typeof this.options.url === 'function') ? this.options.url(text) : this.options.url
},
_defaultFilterData: function (text, records) {
const frecords = {}
text = text.replace(new RegExp('[.*+?^${}()|[\]\\]','g'), '')
// sanitize remove all special characters
if (text === '') {
return []
}
const init = this.options.initial ? '^' : ''
const icase = !this.options.casesensitive ? 'i' : undefined
const regSearch = new RegExp(init + text, icase)
for (const key in records) {
if (regSearch.test(key)) {
frecords[key] = records[key]
}
}
return frecords
},
showTooltip: function (records) {
this._countertips = 0
this._tooltip.innerHTML = ''
this._tooltip.currentSelection = -1 // inizialized for _handleArrowSelect()
if (this.options.tooltipLimit) {
for (const key in records) { // fill tooltip
if (this._countertips === this.options.tooltipLimit) {
break
}
this._countertips++
this._tooltip.appendChild(this._createTip(key, records[key]))
}
}
if (this._countertips > 0) {
this._tooltip.style.display = 'block'
if (this._autoTypeTmp) {
this._autoType()
}
this._autoTypeTmp = this.options.autoType// reset default value
} else {
this._hideTooltip()
}
this._tooltip.scrollTop = 0
return this._countertips
},
_hideTooltip: function () {
this._tooltip.style.display = 'none'
this._tooltip.innerHTML = ''
return 0
},
_defaultFormatData: function (json) { // default callback for format data to indexed data
const self = this
const propName = this.options.propertyName
const propLoc = this.options.propertyLoc
const jsonret = {}
if (L.Util.isArray(propLoc)) {
for (const i in json) {
jsonret[self._getPath(json[i], propName)] = L.latLng(self._getPath(json[i], propLoc[0]), self._getPath(json[i], propLoc[1]))
}
} else {
for (const i in json) {
jsonret[self._getPath(json[i], propName)] = L.latLng(self._getPath(json[i], propLoc))
}
}
// TODO throw new Error("propertyName '"+propName+"' not found in JSON data");
return jsonret
},
_recordsFromJsonp: function (text, callAfter) { // extract searched records from remote jsonp service
L.Control.Search.callJsonp = callAfter
const script = L.DomUtil.create('script', 'leaflet-search-jsonp', document.getElementsByTagName('body')[0])
const url = L.Util.template(this._getUrl(text) + '&' + this.options.jsonpParam + '=L.Control.Search.callJsonp', { s: text }) // parsing url
// rnd = '&_='+Math.floor(Math.random()*10000);
// TODO add rnd param or randomize callback name! in recordsFromJsonp
script.type = 'text/javascript'
script.src = url
return { abort: function () { script.parentNode.removeChild(script) } }
},
_recordsFromAjax: function (text, callAfter) { // Ajax request
/*
if (window.XMLHttpRequest === undefined) {
window.XMLHttpRequest = function () {
try {
return new ActiveXObject('Microsoft.XMLHTTP.6.0')
} catch (e1) {
try {
return new ActiveXObject('Microsoft.XMLHTTP.3.0')
} catch (e2) {
throw new Error('XMLHttpRequest is not supported')
}
}
}
}
const IE8or9 = (L.Browser.ie && !window.atob && document.querySelector)
const request = IE8or9 ? new XDomainRequest() : new XMLHttpRequest()
*/
let request
try {
request = new window.XMLHttpRequest()
} catch (e) {
throw new Error('XMLHttpRequest is not supported')
}
const url = L.Util.template(this._getUrl(text), { s: text })
// rnd = '&_='+Math.floor(Math.random()*10000);
// TODO add rnd param or randomize callback name! in recordsFromAjax
request.open('GET', url)
request.onload = function () {
callAfter(JSON.parse(request.responseText))
}
request.onreadystatechange = function () {
if (request.readyState === 4 && request.status === 200) {
this.onload()
}
}
request.send()
return request
},
_searchInLayer: function (layer, retRecords, propName) {
const self = this; let loc
if (layer instanceof L.Control.Search.Marker) return
if (layer instanceof L.Marker || layer instanceof L.CircleMarker) {
if (self._getPath(layer.options, propName)) {
loc = layer.getLatLng()
loc.layer = layer
retRecords[self._getPath(layer.options, propName)] = loc
} else if (self._getPath(layer.feature.properties, propName)) {
loc = layer.getLatLng()
loc.layer = layer
retRecords[self._getPath(layer.feature.properties, propName)] = loc
} else {
console.warn(`propertyName '${propName}' not found in marker`);
}
} else if (layer instanceof L.Path || layer instanceof L.Polyline || layer instanceof L.Polygon) {
if (self._getPath(layer.options, propName)) {
loc = layer.getBounds().getCenter()
loc.layer = layer
retRecords[self._getPath(layer.options, propName)] = loc
} else if (self._getPath(layer.feature.properties, propName)) {
loc = layer.getBounds().getCenter()
loc.layer = layer
retRecords[self._getPath(layer.feature.properties, propName)] = loc
} else {
console.warn(`propertyName '${propName}' not found in shape`);
}
} else if (Object.prototype.hasOwnProperty.call(layer, 'feature')) { // GeoJSON
if (Object.prototype.hasOwnProperty.call(layer.feature.properties, propName)) {
if (layer.getLatLng && typeof layer.getLatLng === 'function') {
loc = layer.getLatLng()
loc.layer = layer
retRecords[layer.feature.properties[propName]] = loc
} else if (layer.getBounds && typeof layer.getBounds === 'function') {
loc = layer.getBounds().getCenter()
loc.layer = layer
retRecords[layer.feature.properties[propName]] = loc
} else {
console.warn(`Unknown type of Layer`);
}
} else {
console.warn(`propertyName '${propName}' not found in feature`);
}
} else if (layer instanceof L.LayerGroup) {
layer.eachLayer(function (layer) {
self._searchInLayer(layer, retRecords, propName)
})
}
},
_recordsFromLayer: function () { // return table: key,value from layer
const self = this
const retRecords = {}
const propName = this.options.propertyName
this._layer.eachLayer(function (layer) {
self._searchInLayer(layer, retRecords, propName)
})
return retRecords
},
_autoType: function () {
// TODO implements autype without selection(useful for mobile device)
const start = this._input.value.length
const firstRecord = this._tooltip.firstChild ? this._tooltip.firstChild._text : ''
const end = firstRecord.length
if (firstRecord.indexOf(this._input.value) === 0) { // If prefix match
this._input.value = firstRecord
this._handleAutoresize()
if (this._input.createTextRange) {
const selRange = this._input.createTextRange()
selRange.collapse(true)
selRange.moveStart('character', start)
selRange.moveEnd('character', end)
selRange.select()
} else if (this._input.setSelectionRange) {
this._input.setSelectionRange(start, end)
} else if (this._input.selectionStart) {
this._input.selectionStart = start
this._input.selectionEnd = end
}
}
},
_hideAutoType: function () { // deselect text:
let sel
if ((sel = this._input.selection) && sel.empty) {
sel.empty()
} else if (this._input.createTextRange) {
sel = this._input.createTextRange()
sel.collapse(true)
const end = this._input.value.length
sel.moveStart('character', end)
sel.moveEnd('character', end)
sel.select()
} else {
if (this._input.getSelection) {
this._input.getSelection().removeAllRanges()
}
this._input.selectionStart = this._input.selectionEnd
}
},
_handleKeypress: function (e) { // run _input keyup event
const self = this
switch (e.keyCode) {
case 27: /* Esc */
this.collapse()
break
case 13: /* Enter */
if (this._countertips === 1 || (this.options.firstTipSubmit && this._countertips > 0)) {
if (this._tooltip.currentSelection === -1) {
this._handleArrowSelect(1)
}
}
this._handleSubmit() // do search
break
case 38: /* Up */
this._handleArrowSelect(-1)
break
case 40: /* Down */
this._handleArrowSelect(1)
break
case 45: /* Insert */
case 46: /* Delete */
this._autoTypeTmp = false// disable temporarily autoType
break
case 37: /* Left */
case 39: /* Right */
case 16: /* Shift */
case 17: /* Ctrl */
case 35: /* End */
case 36: /* Home */
break
default: /* All keys */
if (this._input.value.length) {
this._cancel.style.display = 'block'
}
else {
this._cancel.style.display = 'none'
}
if (this._input.value.length >= this.options.minLength) {
clearTimeout(this.timerKeypress) // cancel last search request while type in
this.timerKeypress = setTimeout(function () { // delay before request, for limit jsonp/ajax request
self._fillRecordsCache()
}, this.options.delayType)
} else { this._hideTooltip() }
}
this._handleAutoresize()
},
searchText: function (text) {
const code = text.charCodeAt(text.length)
this._input.value = text
this._input.style.display = 'block'
L.DomUtil.addClass(this._container, 'search-exp')
this._autoTypeTmp = false
this._handleKeypress({ keyCode: code })
},
_fillRecordsCache: function () {
const self = this
const inputText = this._input.value; let records
if (this._curReq && this._curReq.abort) { this._curReq.abort() }
// abort previous requests
L.DomUtil.addClass(this._container, 'search-load')
if (this.options.layer) {
// TODO _recordsFromLayer must return array of objects, formatted from _formatData
this._recordsCache = this._recordsFromLayer()
records = this._filterData(this._input.value, this._recordsCache)
this.showTooltip(records)
L.DomUtil.removeClass(this._container, 'search-load')
} else {
if (this.options.sourceData) { this._retrieveData = this.options.sourceData } else if (this.options.url) { // jsonp or ajax
this._retrieveData = this.options.jsonpParam ? this._recordsFromJsonp : this._recordsFromAjax
}
this._curReq = this._retrieveData(this, inputText, function (data) {
self._recordsCache = self._formatData(self, data)
// TODO refact!
if (self.options.sourceData) { records = self._filterData(self._input.value, self._recordsCache) } else { records = self._recordsCache }
self.showTooltip(records)
L.DomUtil.removeClass(self._container, 'search-load')
})
}
},
_handleAutoresize: function () {
let maxWidth
if (this._input.style.maxWidth !== this._map._container.offsetWidth) {
maxWidth = this._map._container.clientWidth
// other side margin + padding + width border + width search-button + width search-cancel
maxWidth -= 10 + 20 + 1 + 30 + 22
this._input.style.maxWidth = maxWidth.toString() + 'px'
}
if (this.options.autoResize && (this._container.offsetWidth + 20 < this._map._container.offsetWidth)) {
this._input.size = this._input.value.length < this._inputMinSize ? this._inputMinSize : this._input.value.length
}
},
_handleArrowSelect: function (velocity) {
const searchTips = this._tooltip.hasChildNodes() ? this._tooltip.childNodes : []
for (let i = 0; i < searchTips.length; i++) {
L.DomUtil.removeClass(searchTips[i], 'search-tip-select')
}
if ((velocity === 1) && (this._tooltip.currentSelection >= (searchTips.length - 1))) { // If at end of list.
L.DomUtil.addClass(searchTips[this._tooltip.currentSelection], 'search-tip-select')
} else if ((velocity === -1) && (this._tooltip.currentSelection <= 0)) { // Going back up to the search box.
this._tooltip.currentSelection = -1
} else if (this._tooltip.style.display !== 'none') {
this._tooltip.currentSelection += velocity
L.DomUtil.addClass(searchTips[this._tooltip.currentSelection], 'search-tip-select')
this._input.value = searchTips[this._tooltip.currentSelection]._text
// scroll:
const tipOffsetTop = searchTips[this._tooltip.currentSelection].offsetTop
if (tipOffsetTop + searchTips[this._tooltip.currentSelection].clientHeight >= this._tooltip.scrollTop + this._tooltip.clientHeight) {
this._tooltip.scrollTop = tipOffsetTop - this._tooltip.clientHeight + searchTips[this._tooltip.currentSelection].clientHeight
} else if (tipOffsetTop <= this._tooltip.scrollTop) {
this._tooltip.scrollTop = tipOffsetTop
}
}
},
_handleSubmit: function () { // button and tooltip click and enter submit
this._hideAutoType()
this.hideAlert()
this._hideTooltip()
if (this._input.style.display === 'none') { // on first click show _input only
this.expand()
} else {
if (this._input.value === '') { // hide _input only
this.collapse()
} else {
const loc = this._getLocation(this._input.value)
if (!loc) {
this.showAlert()
} else {
this.showLocation(loc, this._input.value)
this.fire('search:locationfound', {
latlng: loc,
text: this._input.value,
layer: loc.layer ? loc.layer : null
})
}
}
}
},
_getLocation: function (key) { // extract latlng from _recordsCache
if (Object.prototype.hasOwnProperty.call(this._recordsCache, key)) {
return this._recordsCache[key]
} else {
return false
}
},
_defaultMoveToLocation: function (latlng, title, map) {
if (this.options.zoom) {
this._map.setView(latlng, this.options.zoom)
} else {
this._map.panTo(latlng)
}
},
showLocation: function (latlng, title) { // set location on map from _recordsCache
const self = this
self._map.once('moveend zoomend', function (e) {
if (self._markerSearch) {
self._markerSearch.addTo(self._map).setLatLng(latlng)
}
})
self._moveToLocation(latlng, title, self._map)
// FIXME autoCollapse option hide self._markerSearch before visualized!!
if (self.options.autoCollapse) { self.collapse() }
return self
}
})
L.Control.Search.Marker = L.Marker.extend({
includes: L.version[0] === '1' ? L.Evented.prototype : L.Mixin.Events,
options: {
icon: new L.Icon.Default(),
animate: true,
circle: {
radius: 10,
weight: 3,
color: '#e03',
stroke: true,
fill: false
}
},
initialize: function (latlng, options) {
L.setOptions(this, options)
if (options.icon === true) { options.icon = new L.Icon.Default() }
L.Marker.prototype.initialize.call(this, latlng, options)
if (L.Control.Search.prototype._isObject(this.options.circle)) { this._circleLoc = new L.CircleMarker(latlng, this.options.circle) }
},
onAdd: function (map) {
L.Marker.prototype.onAdd.call(this, map)
if (this._circleLoc) {
map.addLayer(this._circleLoc)
if (this.options.animate) { this.animate() }
}
},
onRemove: function (map) {
L.Marker.prototype.onRemove.call(this, map)
if (this._circleLoc) { map.removeLayer(this._circleLoc) }
},
setLatLng: function (latlng) {
L.Marker.prototype.setLatLng.call(this, latlng)
if (this._circleLoc) { this._circleLoc.setLatLng(latlng) }
return this
},
_initIcon: function () {
if (this.options.icon) { L.Marker.prototype._initIcon.call(this) }
},
_removeIcon: function () {
if (this.options.icon) { L.Marker.prototype._removeIcon.call(this) }
},
animate: function () {
// TODO refact animate() more smooth! like this: http://goo.gl/DDlRs
if (this._circleLoc) {
const circle = this._circleLoc
const tInt = 200 // time interval
const ss = 5 // frames
let mr = parseInt(circle._radius / ss)
const oldrad = this.options.circle.radius
let newrad = circle._radius * 2
let acc = 0
circle._timerAnimLoc = setInterval(function () {
acc += 0.5
mr += acc // adding acceleration
newrad -= mr
circle.setRadius(newrad)
if (newrad < oldrad) {
clearInterval(circle._timerAnimLoc)
circle.setRadius(oldrad)// reset radius
// if(typeof afterAnimCall == 'function')
// afterAnimCall();
// TODO use create event 'animateEnd' in L.Control.Search.Marker
}
}, tInt)
}
return this
}
})
L.Map.addInitHook(function () {
if (this.options.searchControl) {
this.searchControl = L.control.search(this.options.searchControl)
this.addControl(this.searchControl)
}
})
L.control.search = function (options) {
return new L.Control.Search(options)
}
return L.Control.Search
})