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