nakarte

Source code of https://map.sikmir.ru (fork)
git clone git://git.sikmir.ru/nakarte
Log | Files | Refs | LICENSE

index.js (10691B)


      1 import ko from 'knockout';
      2 import L from 'leaflet';
      3 
      4 import '~/lib/leaflet.placemark'; // eslint-disable-line import/no-unassigned-import
      5 import {stopContainerEvents} from '~/lib/leaflet.control.commons';
      6 import '~/lib/leaflet.hashState/leaflet.hashState'; // eslint-disable-line import/no-unassigned-import
      7 
      8 import controlTemplate from './control.html';
      9 import {providers, magicProviders} from './providers';
     10 import './style.css';
     11 
     12 ko.bindingHandlers.hasFocusNested = {
     13     init: function (element, valueAccessor) {
     14         function hasFocusNested() {
     15             let active = document.activeElement;
     16             while (active) {
     17                 if (element === active) {
     18                     return true;
     19                 }
     20                 active = active.parentElement;
     21             }
     22             return false;
     23         }
     24 
     25         function handleFocusChange() {
     26             // wait for all related focus/blur events to fire
     27             setTimeout(() => {
     28                 valueAccessor()(hasFocusNested());
     29             }, 0);
     30         }
     31         element.addEventListener('focus', handleFocusChange, true);
     32         element.addEventListener('blur', handleFocusChange, true);
     33     },
     34 };
     35 
     36 class SearchViewModel {
     37     query = ko.observable('');
     38     inputPlaceholder = ko.observable(null);
     39     helpText = ko.observable(null);
     40     items = ko.observableArray([]);
     41     error = ko.observable(null);
     42     inputHasFocus = ko.observable(false);
     43     controlOrChildHasFocus = ko.observable(false);
     44     highlightedIndex = ko.observable(null);
     45     attribution = ko.observable(null);
     46 
     47     allowMinimize = ko.observable(true);
     48 
     49     /* eslint-disable no-invalid-this */
     50     controlHasFocus = ko.pureComputed(function () {
     51         return this.inputHasFocus() || this.controlOrChildHasFocus();
     52     }, this);
     53 
     54     showResults = ko.pureComputed(function () {
     55         return this.items().length > 0 && this.controlHasFocus();
     56     }, this);
     57 
     58     showError = ko.pureComputed(function () {
     59         return this.error() !== null && this.controlHasFocus();
     60     }, this);
     61 
     62     isQueryLengthOk = ko.computed(function () {
     63         return this.query().trim().length >= this.minSearchQueryLength;
     64     }, this);
     65 
     66     showWarningTooShort = ko.pureComputed(function () {
     67         return this.controlHasFocus() && this.query() && !this.isQueryLengthOk();
     68     }, this);
     69 
     70     minimizeToButton = ko.pureComputed(function () {
     71         return this.allowMinimize() && !this.controlHasFocus();
     72     }, this);
     73 
     74     onItemMouseOver = (item) => {
     75         this.highlightedIndex(this.items.indexOf(item));
     76     };
     77 
     78     onItemClick = (item) => {
     79         this.itemSelected(item);
     80     };
     81 
     82     onButtonClick = (_, e) => {
     83         L.DomEvent.preventDefault(e);
     84         this.inputHasFocus(true);
     85     };
     86 
     87     onShowResults(show) {
     88         if (show) {
     89             this.highlightedIndex(0);
     90         }
     91     }
     92 
     93     onControlHasFocusChange(active) {
     94         if (!active) {
     95             this.items.removeAll();
     96             this.error(null);
     97         }
     98     }
     99 
    100     onInputHasFocusChange(active) {
    101         if (active) {
    102             this.maybeRequestSearch(this.query());
    103         }
    104     }
    105 
    106     onClearClick() {
    107         this.query('');
    108         this.inputHasFocus(true);
    109     }
    110 
    111     defaultEventHandle(_, e) {
    112         L.DomEvent.stopPropagation(e);
    113         return true;
    114     }
    115 
    116     onQueryChange() {
    117         this.items.removeAll();
    118         this.error(null);
    119         this.searchAborted(null);
    120         this.maybeRequestSearch();
    121     }
    122 
    123     onKeyDown = (_, e) => {
    124         let n;
    125         switch (e.which) {
    126             case 38: // up
    127                 n = this.highlightedIndex();
    128                 if (n === null) {
    129                     n = this.items().length - 1;
    130                 } else {
    131                     n -= 1;
    132                 }
    133                 if (n === -1) {
    134                     n = this.items().length - 1;
    135                 }
    136                 this.highlightedIndex(n);
    137                 break;
    138             case 40: // down
    139                 n = this.highlightedIndex();
    140                 if (n === null) {
    141                     n = 0;
    142                 } else {
    143                     n += 1;
    144                 }
    145                 if (n === this.items().length) {
    146                     n = 0;
    147                 }
    148                 this.highlightedIndex(n);
    149                 break;
    150             case 13: // enter
    151                 if (this.items().length > 0) {
    152                     this.itemSelected(this.items()[this.highlightedIndex()]);
    153                 }
    154                 break;
    155             case 27: // esc
    156                 this.escapePressed(null);
    157                 break;
    158             default:
    159                 return true;
    160         }
    161         return false;
    162     };
    163     /* eslint-enable no-invalid-this */
    164 
    165     maybeRequestSearch() {
    166         if (this.isQueryLengthOk() && this.controlHasFocus()) {
    167             this.searchRequested(null);
    168         }
    169     }
    170 
    171     // public events
    172     itemSelected = ko.observable().extend({notify: 'always'});
    173     searchRequested = ko.observable().extend({notify: 'always'});
    174     searchAborted = ko.observable().extend({notify: 'always'});
    175     escapePressed = ko.observable().extend({notify: 'always'});
    176 
    177     // public methods
    178     setResult(items, attribution) {
    179         this.items.splice(0, this.items().length, ...items);
    180         this.error(null);
    181         this.attribution(attribution);
    182     }
    183 
    184     setResultError(error) {
    185         this.items.removeAll();
    186         this.error(error);
    187     }
    188 
    189     setFocus() {
    190         this.inputHasFocus(true);
    191     }
    192 
    193     setInputPlaceholder(s) {
    194         this.inputPlaceholder(s);
    195     }
    196 
    197     setHelpText(s) {
    198         this.helpText(s);
    199     }
    200 
    201     setMinimizeAllowed(allowed) {
    202         this.allowMinimize(allowed);
    203     }
    204 
    205     constructor(minSearchQueryLength) {
    206         this.minSearchQueryLength = minSearchQueryLength;
    207         this.query.subscribe(this.onQueryChange.bind(this));
    208         this.showResults.subscribe(this.onShowResults.bind(this));
    209         this.controlHasFocus.subscribe(this.onControlHasFocusChange.bind(this));
    210         this.inputHasFocus.subscribe(this.onInputHasFocusChange.bind(this));
    211     }
    212 }
    213 
    214 const SearchControl = L.Control.extend({
    215     includes: L.Mixin.Events,
    216 
    217     options: {
    218         provider: 'photon',
    219         providerOptions: {
    220             maxResponses: 5,
    221         },
    222         minQueryLength: 3,
    223         hotkey: 'L',
    224         maxMapHeightToMinimize: 567,
    225         maxMapWidthToMinimize: 450,
    226         tooltip: 'Search places, coordinates, links (Alt-{hotkey})',
    227         help: 'Coordinates in any format. Links to maps: Yandex, Google, OSM, Mapy.cz, Nakarte',
    228     },
    229 
    230     initialize: function (options) {
    231         L.Control.prototype.initialize.call(this, options);
    232         this.provider = new providers[this.options.provider](this.options.providerOptions);
    233         this.magicProviders = magicProviders.map((Cls) => new Cls());
    234         this.searchPromise = null;
    235         this.viewModel = new SearchViewModel(this.options.minQueryLength);
    236         this.viewModel.setInputPlaceholder(L.Util.template(this.options.tooltip, this.options));
    237         this.viewModel.setHelpText(this.options.help);
    238         this.viewModel.searchRequested.subscribe(this.onSearchRequested.bind(this));
    239         this.viewModel.searchAborted.subscribe(this.onSearchAborted.bind(this));
    240         this.viewModel.itemSelected.subscribe(this.onResultItemClicked.bind(this));
    241         this.viewModel.query.subscribe(() => this.fire('querychange'));
    242         this.viewModel.escapePressed.subscribe(this.setFocusToMap.bind(this));
    243     },
    244 
    245     onAdd: function (map) {
    246         this._map = map;
    247         const container = L.DomUtil.create('div', 'leaflet-search-container');
    248         container.innerHTML = controlTemplate;
    249         stopContainerEvents(container);
    250         ko.applyBindings(this.viewModel, container);
    251 
    252         this.searchPromise = null;
    253 
    254         L.DomEvent.on(document, 'keyup', this.onDocumentKeyUp, this);
    255         map.on('resize', this.updateMinimizeAllowed, this);
    256         this.updateMinimizeAllowed();
    257 
    258         // enable setting focus to map container
    259         const mapContainer = map.getContainer();
    260         if (mapContainer.tabIndex === undefined) {
    261             mapContainer.tabIndex = -1;
    262         }
    263         return container;
    264     },
    265 
    266     setFocusToMap: function () {
    267         this._map.getContainer().focus();
    268     },
    269 
    270     onSearchRequested: async function () {
    271         const query = this.viewModel.query();
    272         const searchOptions = {
    273             bbox: this._map.getBounds(),
    274             latlng: this._map.getCenter(),
    275             zoom: this._map.getZoom(),
    276         };
    277         let provider = this.provider;
    278         for (const magicProvider of this.magicProviders) {
    279             if (magicProvider.isOurQuery(query)) {
    280                 provider = magicProvider;
    281             }
    282         }
    283         this.searchPromise = provider.search(query, searchOptions);
    284         const searchPromise = this.searchPromise;
    285         const result = await searchPromise;
    286         this.fire('resultreceived', {provider: provider.name, query, result});
    287         if (this.searchPromise === searchPromise) {
    288             if (result.error) {
    289                 this.viewModel.setResultError(result.error);
    290             } else if (result.results.length === 0) {
    291                 this.viewModel.setResultError('Nothing found');
    292             } else {
    293                 this.viewModel.setResult(result.results, provider.attribution);
    294             }
    295         }
    296     },
    297 
    298     onSearchAborted: function () {
    299         this.searchPromise = null;
    300     },
    301 
    302     onResultItemClicked: function (item) {
    303         if (item.bbox) {
    304             this._map.fitBounds(item.bbox);
    305         } else {
    306             this._map.setView(item.latlng, item.zoom);
    307         }
    308         this._map.showPlacemark(item.latlng, item.title);
    309         this.setFocusToMap();
    310     },
    311 
    312     onDocumentKeyUp: function (e) {
    313         if (e.keyCode === this.options.hotkey.codePointAt(0) && e.altKey) {
    314             this.viewModel.setFocus();
    315         }
    316     },
    317 
    318     updateMinimizeAllowed: function () {
    319         const mapSize = this._map.getSize();
    320         this.viewModel.setMinimizeAllowed(
    321             mapSize.y < this.options.maxMapHeightToMinimize || mapSize.x < this.options.maxMapWidthToMinimize
    322         );
    323     },
    324 });
    325 
    326 SearchControl.include(L.Mixin.HashState);
    327 SearchControl.include({
    328     stateChangeEvents: ['querychange'],
    329 
    330     serializeState: function () {
    331         const query = this.viewModel.query();
    332         if (query) {
    333             return [encodeURIComponent(query)];
    334         }
    335         return null;
    336     },
    337 
    338     unserializeState: function (state) {
    339         if (state?.length === 1) {
    340             this.viewModel.query(decodeURIComponent(state[0]));
    341             return true;
    342         }
    343         return false;
    344     },
    345 });
    346 
    347 export {SearchControl};