leaflet-search.src.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987
  1. /*
  2. * Leaflet Control Search v2.7.2 - 2017-04-08
  3. *
  4. * Copyright 2017 Stefano Cudini
  5. * stefano.cudini@gmail.com
  6. * http://labs.easyblog.it/
  7. *
  8. * Licensed under the MIT license.
  9. *
  10. * Demo:
  11. * http://labs.easyblog.it/maps/leaflet-search/
  12. *
  13. * Source:
  14. * git@github.com:stefanocudini/leaflet-search.git
  15. *
  16. */
  17. (function (factory) {
  18. if(typeof define === 'function' && define.amd) {
  19. //AMD
  20. define(['leaflet'], factory);
  21. } else if(typeof module !== 'undefined') {
  22. // Node/CommonJS
  23. module.exports = factory(require('leaflet'));
  24. } else {
  25. // Browser globals
  26. if(typeof window.L === 'undefined')
  27. throw 'Leaflet must be loaded first';
  28. factory(window.L);
  29. }
  30. })(function (L) {
  31. function _getPath(obj, prop) {
  32. var parts = prop.split('.'),
  33. last = parts.pop(),
  34. len = parts.length,
  35. cur = parts[0],
  36. i = 1;
  37. if(len > 0)
  38. while((obj = obj[cur]) && i < len)
  39. cur = parts[i++];
  40. if(obj)
  41. return obj[last];
  42. }
  43. function _isObject(obj) {
  44. return Object.prototype.toString.call(obj) === "[object Object]";
  45. }
  46. //TODO implement can do research on multiple sources layers and remote
  47. //TODO history: false, //show latest searches in tooltip
  48. //FIXME option condition problem {autoCollapse: true, markerLocation: true} not show location
  49. //FIXME option condition problem {autoCollapse: false }
  50. //
  51. //TODO here insert function that search inputText FIRST in _recordsCache keys and if not find results..
  52. // run one of callbacks search(sourceData,jsonpUrl or options.layer) and run this.showTooltip
  53. //
  54. //TODO change structure of _recordsCache
  55. // like this: _recordsCache = {"text-key1": {loc:[lat,lng], ..other attributes.. }, {"text-key2": {loc:[lat,lng]}...}, ...}
  56. // in this mode every record can have a free structure of attributes, only 'loc' is required
  57. //TODO important optimization!!! always append data in this._recordsCache
  58. // now _recordsCache content is emptied and replaced with new data founded
  59. // always appending data on _recordsCache give the possibility of caching ajax, jsonp and layersearch!
  60. //
  61. //TODO here insert function that search inputText FIRST in _recordsCache keys and if not find results..
  62. // run one of callbacks search(sourceData,jsonpUrl or options.layer) and run this.showTooltip
  63. //
  64. //TODO change structure of _recordsCache
  65. // like this: _recordsCache = {"text-key1": {loc:[lat,lng], ..other attributes.. }, {"text-key2": {loc:[lat,lng]}...}, ...}
  66. // in this way every record can have a free structure of attributes, only 'loc' is required
  67. L.Control.Search = L.Control.extend({
  68. includes: L.Mixin.Events,
  69. //
  70. // Name Data passed Description
  71. //
  72. //Managed Events:
  73. // search:locationfound {latlng, title, layer} fired after moved and show markerLocation
  74. // search:expanded {} fired after control was expanded
  75. // search:collapsed {} fired after control was collapsed
  76. //
  77. //Public methods:
  78. // setLayer() L.LayerGroup() set layer search at runtime
  79. // showAlert() 'Text message' show alert message
  80. // searchText() 'Text searched' search text by external code
  81. //
  82. options: {
  83. url: '', //url for search by ajax request, ex: "search.php?q={s}". Can be function that returns string for dynamic parameter setting
  84. layer: null, //layer where search markers(is a L.LayerGroup)
  85. sourceData: null, //function that fill _recordsCache, passed searching text by first param and callback in second
  86. //TODO implements uniq option 'sourceData' that recognizes source type: url,array,callback or layer
  87. jsonpParam: null, //jsonp param name for search by jsonp service, ex: "callback"
  88. propertyLoc: 'loc', //field for remapping location, using array: ['latname','lonname'] for select double fields(ex. ['lat','lon'] ) support dotted format: 'prop.subprop.title'
  89. propertyName: 'title', //property in marker.options(or feature.properties for vector layer) trough filter elements in layer,
  90. formatData: null, //callback for reformat all data from source to indexed data object
  91. filterData: null, //callback for filtering data from text searched, params: textSearch, allRecords
  92. moveToLocation: null, //callback run on location found, params: latlng, title, map
  93. buildTip: null, //function that return row tip html node(or html string), receive text tooltip in first param
  94. container: '', //container id to insert Search Control
  95. zoom: null, //default zoom level for move to location
  96. minLength: 1, //minimal text length for autocomplete
  97. initial: true, //search elements only by initial text
  98. casesensitive: false, //search elements in case sensitive text
  99. autoType: true, //complete input with first suggested result and select this filled-in text.
  100. delayType: 400, //delay while typing for show tooltip
  101. tooltipLimit: -1, //limit max results to show in tooltip. -1 for no limit, 0 for no results
  102. tipAutoSubmit: true, //auto map panTo when click on tooltip
  103. firstTipSubmit: false, //auto select first result con enter click
  104. autoResize: true, //autoresize on input change
  105. collapsed: true, //collapse search control at startup
  106. autoCollapse: false, //collapse search control after submit(on button or on tips if enabled tipAutoSubmit)
  107. autoCollapseTime: 1200, //delay for autoclosing alert and collapse after blur
  108. textErr: 'Location not found', //error message
  109. textCancel: 'Cancel', //title in cancel button
  110. textPlaceholder: 'Search...', //placeholder value
  111. position: 'topleft',
  112. hideMarkerOnCollapse: false, //remove circle and marker on search control collapsed
  113. marker: { //custom L.Marker or false for hide
  114. icon: false, //custom L.Icon for maker location or false for hide
  115. animate: true, //animate a circle over location found
  116. circle: { //draw a circle in location found
  117. radius: 10,
  118. weight: 3,
  119. color: '#e03',
  120. stroke: true,
  121. fill: false
  122. }
  123. }
  124. },
  125. initialize: function(options) {
  126. L.Util.setOptions(this, options || {});
  127. this._inputMinSize = this.options.textPlaceholder ? this.options.textPlaceholder.length : 10;
  128. this._layer = this.options.layer || new L.LayerGroup();
  129. this._filterData = this.options.filterData || this._defaultFilterData;
  130. this._formatData = this.options.formatData || this._defaultFormatData;
  131. this._moveToLocation = this.options.moveToLocation || this._defaultMoveToLocation;
  132. this._autoTypeTmp = this.options.autoType; //useful for disable autoType temporarily in delete/backspace keydown
  133. this._countertips = 0; //number of tips items
  134. this._recordsCache = {}; //key,value table! that store locations! format: key,latlng
  135. this._curReq = null;
  136. },
  137. onAdd: function (map) {
  138. this._map = map;
  139. this._container = L.DomUtil.create('div', 'leaflet-control-search');
  140. this._input = this._createInput(this.options.textPlaceholder, 'search-input');
  141. this._tooltip = this._createTooltip('search-tooltip');
  142. this._cancel = this._createCancel(this.options.textCancel, 'search-cancel');
  143. this._button = this._createButton(this.options.textPlaceholder, 'search-button');
  144. this._alert = this._createAlert('search-alert');
  145. if(this.options.collapsed===false)
  146. this.expand(this.options.collapsed);
  147. if(this.options.marker) {
  148. if(this.options.marker instanceof L.Marker || this.options.marker instanceof L.CircleMarker)
  149. this._markerSearch = this.options.marker;
  150. else if(_isObject(this.options.marker))
  151. this._markerSearch = new L.Control.Search.Marker([0,0], this.options.marker);
  152. this._markerSearch._isMarkerSearch = true;
  153. }
  154. this.setLayer( this._layer );
  155. map.on({
  156. // 'layeradd': this._onLayerAddRemove,
  157. // 'layerremove': this._onLayerAddRemove
  158. 'resize': this._handleAutoresize
  159. }, this);
  160. return this._container;
  161. },
  162. addTo: function (map) {
  163. if(this.options.container) {
  164. this._container = this.onAdd(map);
  165. this._wrapper = L.DomUtil.get(this.options.container);
  166. this._wrapper.style.position = 'relative';
  167. this._wrapper.appendChild(this._container);
  168. }
  169. else
  170. L.Control.prototype.addTo.call(this, map);
  171. return this;
  172. },
  173. onRemove: function(map) {
  174. this._recordsCache = {};
  175. // map.off({
  176. // 'layeradd': this._onLayerAddRemove,
  177. // 'layerremove': this._onLayerAddRemove
  178. // }, this);
  179. },
  180. // _onLayerAddRemove: function(e) {
  181. // //without this, run setLayer also for each Markers!! to optimize!
  182. // if(e.layer instanceof L.LayerGroup)
  183. // if( L.stamp(e.layer) != L.stamp(this._layer) )
  184. // this.setLayer(e.layer);
  185. // },
  186. setLayer: function(layer) { //set search layer at runtime
  187. //this.options.layer = layer; //setting this, run only this._recordsFromLayer()
  188. this._layer = layer;
  189. this._layer.addTo(this._map);
  190. return this;
  191. },
  192. showAlert: function(text) {
  193. text = text || this.options.textErr;
  194. this._alert.style.display = 'block';
  195. this._alert.innerHTML = text;
  196. clearTimeout(this.timerAlert);
  197. var that = this;
  198. this.timerAlert = setTimeout(function() {
  199. that.hideAlert();
  200. },this.options.autoCollapseTime);
  201. return this;
  202. },
  203. hideAlert: function() {
  204. this._alert.style.display = 'none';
  205. return this;
  206. },
  207. cancel: function() {
  208. this._input.value = '';
  209. this._handleKeypress({ keyCode: 8 });//simulate backspace keypress
  210. this._input.size = this._inputMinSize;
  211. this._input.focus();
  212. this._cancel.style.display = 'none';
  213. this._hideTooltip();
  214. return this;
  215. },
  216. expand: function(toggle) {
  217. toggle = typeof toggle === 'boolean' ? toggle : true;
  218. this._input.style.display = 'block';
  219. L.DomUtil.addClass(this._container, 'search-exp');
  220. if ( toggle !== false ) {
  221. this._input.focus();
  222. this._map.on('dragstart click', this.collapse, this);
  223. }
  224. this.fire('search:expanded');
  225. return this;
  226. },
  227. collapse: function() {
  228. this._hideTooltip();
  229. this.cancel();
  230. this._alert.style.display = 'none';
  231. this._input.blur();
  232. if(this.options.collapsed)
  233. {
  234. this._input.style.display = 'none';
  235. this._cancel.style.display = 'none';
  236. L.DomUtil.removeClass(this._container, 'search-exp');
  237. if (this.options.hideMarkerOnCollapse) {
  238. this._map.removeLayer(this._markerSearch);
  239. }
  240. this._map.off('dragstart click', this.collapse, this);
  241. }
  242. this.fire('search:collapsed');
  243. return this;
  244. },
  245. collapseDelayed: function() { //collapse after delay, used on_input blur
  246. if (!this.options.autoCollapse) return this;
  247. var that = this;
  248. clearTimeout(this.timerCollapse);
  249. this.timerCollapse = setTimeout(function() {
  250. that.collapse();
  251. }, this.options.autoCollapseTime);
  252. return this;
  253. },
  254. collapseDelayedStop: function() {
  255. clearTimeout(this.timerCollapse);
  256. return this;
  257. },
  258. ////start DOM creations
  259. _createAlert: function(className) {
  260. var alert = L.DomUtil.create('div', className, this._container);
  261. alert.style.display = 'none';
  262. L.DomEvent
  263. .on(alert, 'click', L.DomEvent.stop, this)
  264. .on(alert, 'click', this.hideAlert, this);
  265. return alert;
  266. },
  267. _createInput: function (text, className) {
  268. var label = L.DomUtil.create('label', className, this._container);
  269. var input = L.DomUtil.create('input', className, this._container);
  270. input.type = 'text';
  271. input.size = this._inputMinSize;
  272. input.value = '';
  273. input.autocomplete = 'off';
  274. input.autocorrect = 'off';
  275. input.autocapitalize = 'off';
  276. input.placeholder = text;
  277. input.style.display = 'none';
  278. input.role = 'search';
  279. input.id = input.role + input.type + input.size;
  280. label.htmlFor = input.id;
  281. label.style.display = 'none';
  282. label.value = text;
  283. L.DomEvent
  284. .disableClickPropagation(input)
  285. .on(input, 'keydown', this._handleKeypress, this)
  286. .on(input, 'blur', this.collapseDelayed, this)
  287. .on(input, 'focus', this.collapseDelayedStop, this);
  288. return input;
  289. },
  290. _createCancel: function (title, className) {
  291. var cancel = L.DomUtil.create('a', className, this._container);
  292. cancel.href = '#';
  293. cancel.title = title;
  294. cancel.style.display = 'none';
  295. cancel.innerHTML = "<span>&otimes;</span>";//imageless(see css)
  296. L.DomEvent
  297. .on(cancel, 'click', L.DomEvent.stop, this)
  298. .on(cancel, 'click', this.cancel, this);
  299. return cancel;
  300. },
  301. _createButton: function (title, className) {
  302. var button = L.DomUtil.create('a', className, this._container);
  303. button.href = '#';
  304. button.title = title;
  305. L.DomEvent
  306. .on(button, 'click', L.DomEvent.stop, this)
  307. .on(button, 'click', this._handleSubmit, this)
  308. .on(button, 'focus', this.collapseDelayedStop, this)
  309. .on(button, 'blur', this.collapseDelayed, this);
  310. return button;
  311. },
  312. _createTooltip: function(className) {
  313. var tool = L.DomUtil.create('ul', className, this._container);
  314. tool.style.display = 'none';
  315. var that = this;
  316. L.DomEvent
  317. .disableClickPropagation(tool)
  318. .on(tool, 'blur', this.collapseDelayed, this)
  319. .on(tool, 'mousewheel', function(e) {
  320. that.collapseDelayedStop();
  321. L.DomEvent.stopPropagation(e);//disable zoom map
  322. }, this)
  323. .on(tool, 'mouseover', function(e) {
  324. that.collapseDelayedStop();
  325. }, this);
  326. return tool;
  327. },
  328. _createTip: function(text, val) {//val is object in recordCache, usually is Latlng
  329. var tip;
  330. if(this.options.buildTip)
  331. {
  332. tip = this.options.buildTip.call(this, text, val); //custom tip node or html string
  333. if(typeof tip === 'string')
  334. {
  335. var tmpNode = L.DomUtil.create('div');
  336. tmpNode.innerHTML = tip;
  337. tip = tmpNode.firstChild;
  338. }
  339. }
  340. else
  341. {
  342. tip = L.DomUtil.create('li', '');
  343. tip.innerHTML = text;
  344. }
  345. L.DomUtil.addClass(tip, 'search-tip');
  346. tip._text = text; //value replaced in this._input and used by _autoType
  347. if(this.options.tipAutoSubmit)
  348. L.DomEvent
  349. .disableClickPropagation(tip)
  350. .on(tip, 'click', L.DomEvent.stop, this)
  351. .on(tip, 'click', function(e) {
  352. this._input.value = text;
  353. this._handleAutoresize();
  354. this._input.focus();
  355. this._hideTooltip();
  356. this._handleSubmit();
  357. }, this);
  358. return tip;
  359. },
  360. //////end DOM creations
  361. _getUrl: function(text) {
  362. return (typeof this.options.url === 'function') ? this.options.url(text) : this.options.url;
  363. },
  364. _defaultFilterData: function(text, records) {
  365. var I, icase, regSearch, frecords = {};
  366. text = text.replace(/[.*+?^${}()|[\]\\]/g, ''); //sanitize remove all special characters
  367. if(text==='')
  368. return [];
  369. I = this.options.initial ? '^' : ''; //search only initial text
  370. icase = !this.options.casesensitive ? 'i' : undefined;
  371. regSearch = new RegExp(I + text, icase);
  372. //TODO use .filter or .map
  373. for(var key in records) {
  374. if( regSearch.test(key) )
  375. frecords[key]= records[key];
  376. }
  377. return frecords;
  378. },
  379. showTooltip: function(records) {
  380. this._countertips = 0;
  381. this._tooltip.innerHTML = '';
  382. this._tooltip.currentSelection = -1; //inizialized for _handleArrowSelect()
  383. if(this.options.tooltipLimit)
  384. {
  385. for(var key in records)//fill tooltip
  386. {
  387. if(this._countertips === this.options.tooltipLimit)
  388. break;
  389. this._countertips++;
  390. this._tooltip.appendChild( this._createTip(key, records[key]) );
  391. }
  392. }
  393. if(this._countertips > 0)
  394. {
  395. this._tooltip.style.display = 'block';
  396. if(this._autoTypeTmp)
  397. this._autoType();
  398. this._autoTypeTmp = this.options.autoType;//reset default value
  399. }
  400. else
  401. this._hideTooltip();
  402. this._tooltip.scrollTop = 0;
  403. return this._countertips;
  404. },
  405. _hideTooltip: function() {
  406. this._tooltip.style.display = 'none';
  407. this._tooltip.innerHTML = '';
  408. return 0;
  409. },
  410. _defaultFormatData: function(json) { //default callback for format data to indexed data
  411. var propName = this.options.propertyName,
  412. propLoc = this.options.propertyLoc,
  413. i, jsonret = {};
  414. if( L.Util.isArray(propLoc) )
  415. for(i in json)
  416. jsonret[ _getPath(json[i],propName) ]= L.latLng( json[i][ propLoc[0] ], json[i][ propLoc[1] ] );
  417. else
  418. for(i in json)
  419. jsonret[ _getPath(json[i],propName) ]= L.latLng( _getPath(json[i],propLoc) );
  420. //TODO throw new Error("propertyName '"+propName+"' not found in JSON data");
  421. return jsonret;
  422. },
  423. _recordsFromJsonp: function(text, callAfter) { //extract searched records from remote jsonp service
  424. L.Control.Search.callJsonp = callAfter;
  425. var script = L.DomUtil.create('script','leaflet-search-jsonp', document.getElementsByTagName('body')[0] ),
  426. url = L.Util.template(this._getUrl(text)+'&'+this.options.jsonpParam+'=L.Control.Search.callJsonp', {s: text}); //parsing url
  427. //rnd = '&_='+Math.floor(Math.random()*10000);
  428. //TODO add rnd param or randomize callback name! in recordsFromJsonp
  429. script.type = 'text/javascript';
  430. script.src = url;
  431. return { abort: function() { script.parentNode.removeChild(script); } };
  432. },
  433. _recordsFromAjax: function(text, callAfter) { //Ajax request
  434. if (window.XMLHttpRequest === undefined) {
  435. window.XMLHttpRequest = function() {
  436. try { return new ActiveXObject("Microsoft.XMLHTTP.6.0"); }
  437. catch (e1) {
  438. try { return new ActiveXObject("Microsoft.XMLHTTP.3.0"); }
  439. catch (e2) { throw new Error("XMLHttpRequest is not supported"); }
  440. }
  441. };
  442. }
  443. var IE8or9 = ( L.Browser.ie && !window.atob && document.querySelector ),
  444. request = IE8or9 ? new XDomainRequest() : new XMLHttpRequest(),
  445. url = L.Util.template(this._getUrl(text), {s: text});
  446. //rnd = '&_='+Math.floor(Math.random()*10000);
  447. //TODO add rnd param or randomize callback name! in recordsFromAjax
  448. request.open("GET", url);
  449. var that = this;
  450. request.onload = function() {
  451. callAfter( JSON.parse(request.responseText) );
  452. };
  453. request.onreadystatechange = function() {
  454. if(request.readyState === 4 && request.status === 200) {
  455. this.onload();
  456. }
  457. };
  458. request.send();
  459. return request;
  460. },
  461. _recordsFromLayer: function() { //return table: key,value from layer
  462. var that = this,
  463. retRecords = {},
  464. propName = this.options.propertyName,
  465. loc;
  466. this._layer.eachLayer(function(layer) {
  467. if(layer.hasOwnProperty('_isMarkerSearch')) return;
  468. if(layer instanceof L.Marker || layer instanceof L.CircleMarker)
  469. {
  470. try {
  471. if(_getPath(layer.options,propName))
  472. {
  473. loc = layer.getLatLng();
  474. loc.layer = layer;
  475. retRecords[ _getPath(layer.options,propName) ] = loc;
  476. }
  477. else if(_getPath(layer.feature.properties,propName)){
  478. loc = layer.getLatLng();
  479. loc.layer = layer;
  480. retRecords[ _getPath(layer.feature.properties,propName) ] = loc;
  481. }
  482. else
  483. throw new Error("propertyName '"+propName+"' not found in marker");
  484. }
  485. catch(err){
  486. if (console) { }
  487. }
  488. }
  489. else if(layer.hasOwnProperty('feature'))//GeoJSON
  490. {
  491. try {
  492. if(layer.feature.properties.hasOwnProperty(propName))
  493. {
  494. loc = layer.getBounds().getCenter();
  495. loc.layer = layer;
  496. retRecords[ layer.feature.properties[propName] ] = loc;
  497. }
  498. else
  499. throw new Error("propertyName '"+propName+"' not found in feature");
  500. }
  501. catch(err){
  502. if (console) { }
  503. }
  504. }
  505. else if(layer instanceof L.LayerGroup)
  506. {
  507. //TODO: Optimize
  508. layer.eachLayer(function(m) {
  509. loc = m.getLatLng();
  510. loc.layer = m;
  511. retRecords[ m.feature.properties[propName] ] = loc;
  512. });
  513. }
  514. },this);
  515. return retRecords;
  516. },
  517. _autoType: function() {
  518. //TODO implements autype without selection(useful for mobile device)
  519. var start = this._input.value.length,
  520. firstRecord = this._tooltip.firstChild ? this._tooltip.firstChild._text : '',
  521. end = firstRecord.length;
  522. if (firstRecord.indexOf(this._input.value) === 0) { // If prefix match
  523. this._input.value = firstRecord;
  524. this._handleAutoresize();
  525. if (this._input.createTextRange) {
  526. var selRange = this._input.createTextRange();
  527. selRange.collapse(true);
  528. selRange.moveStart('character', start);
  529. selRange.moveEnd('character', end);
  530. selRange.select();
  531. }
  532. else if(this._input.setSelectionRange) {
  533. this._input.setSelectionRange(start, end);
  534. }
  535. else if(this._input.selectionStart) {
  536. this._input.selectionStart = start;
  537. this._input.selectionEnd = end;
  538. }
  539. }
  540. },
  541. _hideAutoType: function() { // deselect text:
  542. var sel;
  543. if ((sel = this._input.selection) && sel.empty) {
  544. sel.empty();
  545. }
  546. else if (this._input.createTextRange) {
  547. sel = this._input.createTextRange();
  548. sel.collapse(true);
  549. var end = this._input.value.length;
  550. sel.moveStart('character', end);
  551. sel.moveEnd('character', end);
  552. sel.select();
  553. }
  554. else {
  555. if (this._input.getSelection) {
  556. this._input.getSelection().removeAllRanges();
  557. }
  558. this._input.selectionStart = this._input.selectionEnd;
  559. }
  560. },
  561. _handleKeypress: function (e) { //run _input keyup event
  562. switch(e.keyCode)
  563. {
  564. case 27://Esc
  565. this.collapse();
  566. break;
  567. case 13://Enter
  568. if(this._countertips == 1 || (this.options.firstTipSubmit && this._countertips > 0))
  569. this._handleArrowSelect(1);
  570. this._handleSubmit(); //do search
  571. break;
  572. case 38://Up
  573. this._handleArrowSelect(-1);
  574. break;
  575. case 40://Down
  576. this._handleArrowSelect(1);
  577. break;
  578. case 8://Backspace
  579. case 45://Insert
  580. case 46://Delete
  581. this._autoTypeTmp = false;//disable temporarily autoType
  582. break;
  583. case 37://Left
  584. case 39://Right
  585. case 16://Shift
  586. case 17://Ctrl
  587. case 35://End
  588. case 36://Home
  589. break;
  590. default://All keys
  591. if(this._input.value.length)
  592. this._cancel.style.display = 'block';
  593. else
  594. this._cancel.style.display = 'none';
  595. if(this._input.value.length >= this.options.minLength)
  596. {
  597. var that = this;
  598. clearTimeout(this.timerKeypress); //cancel last search request while type in
  599. this.timerKeypress = setTimeout(function() { //delay before request, for limit jsonp/ajax request
  600. that._fillRecordsCache();
  601. }, this.options.delayType);
  602. }
  603. else
  604. this._hideTooltip();
  605. }
  606. this._handleAutoresize();
  607. },
  608. searchText: function(text) {
  609. var code = text.charCodeAt(text.length);
  610. this._input.value = text;
  611. this._input.style.display = 'block';
  612. L.DomUtil.addClass(this._container, 'search-exp');
  613. this._autoTypeTmp = false;
  614. this._handleKeypress({keyCode: code});
  615. },
  616. _fillRecordsCache: function() {
  617. var inputText = this._input.value,
  618. that = this, records;
  619. if(this._curReq && this._curReq.abort)
  620. this._curReq.abort();
  621. //abort previous requests
  622. L.DomUtil.addClass(this._container, 'search-load');
  623. if(this.options.layer)
  624. {
  625. //TODO _recordsFromLayer must return array of objects, formatted from _formatData
  626. this._recordsCache = this._recordsFromLayer();
  627. records = this._filterData( this._input.value, this._recordsCache );
  628. this.showTooltip( records );
  629. L.DomUtil.removeClass(this._container, 'search-load');
  630. }
  631. else
  632. {
  633. if(this.options.sourceData)
  634. this._retrieveData = this.options.sourceData;
  635. else if(this.options.url) //jsonp or ajax
  636. this._retrieveData = this.options.jsonpParam ? this._recordsFromJsonp : this._recordsFromAjax;
  637. this._curReq = this._retrieveData.call(this, inputText, function(data) {
  638. that._recordsCache = that._formatData(data);
  639. //TODO refact!
  640. if(that.options.sourceData)
  641. records = that._filterData( that._input.value, that._recordsCache );
  642. else
  643. records = that._recordsCache;
  644. that.showTooltip( records );
  645. L.DomUtil.removeClass(that._container, 'search-load');
  646. });
  647. }
  648. },
  649. _handleAutoresize: function() { //autoresize this._input
  650. //TODO refact _handleAutoresize now is not accurate
  651. if (this._input.style.maxWidth != this._map._container.offsetWidth) //If maxWidth isn't the same as when first set, reset to current Map width
  652. this._input.style.maxWidth = L.DomUtil.getStyle(this._map._container, 'width');
  653. if(this.options.autoResize && (this._container.offsetWidth + 45 < this._map._container.offsetWidth))
  654. this._input.size = this._input.value.length<this._inputMinSize ? this._inputMinSize : this._input.value.length;
  655. },
  656. _handleArrowSelect: function(velocity) {
  657. var searchTips = this._tooltip.hasChildNodes() ? this._tooltip.childNodes : [];
  658. for (i=0; i<searchTips.length; i++)
  659. L.DomUtil.removeClass(searchTips[i], 'search-tip-select');
  660. if ((velocity == 1 ) && (this._tooltip.currentSelection >= (searchTips.length - 1))) {// If at end of list.
  661. L.DomUtil.addClass(searchTips[this._tooltip.currentSelection], 'search-tip-select');
  662. }
  663. else if ((velocity == -1 ) && (this._tooltip.currentSelection <= 0)) { // Going back up to the search box.
  664. this._tooltip.currentSelection = -1;
  665. }
  666. else if (this._tooltip.style.display != 'none') {
  667. this._tooltip.currentSelection += velocity;
  668. L.DomUtil.addClass(searchTips[this._tooltip.currentSelection], 'search-tip-select');
  669. this._input.value = searchTips[this._tooltip.currentSelection]._text;
  670. // scroll:
  671. var tipOffsetTop = searchTips[this._tooltip.currentSelection].offsetTop;
  672. if (tipOffsetTop + searchTips[this._tooltip.currentSelection].clientHeight >= this._tooltip.scrollTop + this._tooltip.clientHeight) {
  673. this._tooltip.scrollTop = tipOffsetTop - this._tooltip.clientHeight + searchTips[this._tooltip.currentSelection].clientHeight;
  674. }
  675. else if (tipOffsetTop <= this._tooltip.scrollTop) {
  676. this._tooltip.scrollTop = tipOffsetTop;
  677. }
  678. }
  679. },
  680. _handleSubmit: function() { //button and tooltip click and enter submit
  681. this._hideAutoType();
  682. this.hideAlert();
  683. this._hideTooltip();
  684. if(this._input.style.display == 'none') //on first click show _input only
  685. this.expand();
  686. else
  687. {
  688. if(this._input.value === '') //hide _input only
  689. this.collapse();
  690. else
  691. {
  692. var loc = this._getLocation(this._input.value);
  693. if(loc===false)
  694. this.showAlert();
  695. else
  696. {
  697. this.showLocation(loc, this._input.value);
  698. this.fire('search:locationfound', {
  699. latlng: loc,
  700. text: this._input.value,
  701. layer: loc.layer ? loc.layer : null
  702. });
  703. }
  704. }
  705. }
  706. },
  707. _getLocation: function(key) { //extract latlng from _recordsCache
  708. if( this._recordsCache.hasOwnProperty(key) )
  709. return this._recordsCache[key];//then after use .loc attribute
  710. else
  711. return false;
  712. },
  713. _defaultMoveToLocation: function(latlng, title, map) {
  714. if(this.options.zoom)
  715. this._map.setView(latlng, this.options.zoom);
  716. else
  717. this._map.panTo(latlng);
  718. },
  719. showLocation: function(latlng, title) { //set location on map from _recordsCache
  720. var self = this;
  721. self._map.once('moveend zoomend', function(e) {
  722. if(self._markerSearch) {
  723. self._markerSearch.addTo(self._map).setLatLng(latlng);
  724. }
  725. });
  726. self._moveToLocation(latlng, title, self._map);
  727. //FIXME autoCollapse option hide self._markerSearch before that visualized!!
  728. if(self.options.autoCollapse)
  729. self.collapse();
  730. return self;
  731. }
  732. });
  733. L.Control.Search.Marker = L.Marker.extend({
  734. includes: L.Mixin.Events,
  735. options: {
  736. icon: new L.Icon.Default(),
  737. animate: true,
  738. circle: {
  739. radius: 10,
  740. weight: 3,
  741. color: '#e03',
  742. stroke: true,
  743. fill: false
  744. }
  745. },
  746. initialize: function (latlng, options) {
  747. L.setOptions(this, options);
  748. if(options.icon === true)
  749. options.icon = new L.Icon.Default();
  750. L.Marker.prototype.initialize.call(this, latlng, options);
  751. if( _isObject(this.options.circle) )
  752. this._circleLoc = new L.CircleMarker(latlng, this.options.circle);
  753. },
  754. onAdd: function (map) {
  755. L.Marker.prototype.onAdd.call(this, map);
  756. if(this._circleLoc) {
  757. map.addLayer(this._circleLoc);
  758. if(this.options.animate)
  759. this.animate();
  760. }
  761. },
  762. onRemove: function (map) {
  763. L.Marker.prototype.onRemove.call(this, map);
  764. if(this._circleLoc)
  765. map.removeLayer(this._circleLoc);
  766. },
  767. setLatLng: function (latlng) {
  768. L.Marker.prototype.setLatLng.call(this, latlng);
  769. if(this._circleLoc)
  770. this._circleLoc.setLatLng(latlng);
  771. return this;
  772. },
  773. _initIcon: function () {
  774. if(this.options.icon)
  775. L.Marker.prototype._initIcon.call(this);
  776. },
  777. _removeIcon: function () {
  778. if(this.options.icon)
  779. L.Marker.prototype._removeIcon.call(this);
  780. },
  781. animate: function() {
  782. //TODO refact animate() more smooth! like this: http://goo.gl/DDlRs
  783. if(this._circleLoc)
  784. {
  785. var circle = this._circleLoc,
  786. tInt = 200, //time interval
  787. ss = 5, //frames
  788. mr = parseInt(circle._radius/ss),
  789. oldrad = this.options.circle.radius,
  790. newrad = circle._radius * 2,
  791. acc = 0;
  792. circle._timerAnimLoc = setInterval(function() {
  793. acc += 0.5;
  794. mr += acc; //adding acceleration
  795. newrad -= mr;
  796. circle.setRadius(newrad);
  797. if(newrad<oldrad)
  798. {
  799. clearInterval(circle._timerAnimLoc);
  800. circle.setRadius(oldrad);//reset radius
  801. //if(typeof afterAnimCall == 'function')
  802. //afterAnimCall();
  803. //TODO use create event 'animateEnd' in L.Control.Search.Marker
  804. }
  805. }, tInt);
  806. }
  807. return this;
  808. }
  809. });
  810. L.Map.addInitHook(function () {
  811. if (this.options.searchControl) {
  812. this.searchControl = L.control.search(this.options.searchControl);
  813. this.addControl(this.searchControl);
  814. }
  815. });
  816. L.control.search = function (options) {
  817. return new L.Control.Search(options);
  818. };
  819. return L.Control.Search;
  820. });