1function debounce(fn, wait) { 2 let timeout; 3 4 return function debounced(...args) { 5 const context = this; 6 clearTimeout(timeout); 7 timeout = setTimeout(() => fn.apply(context, args), wait); 8 }; 9} 10 11const template = ` 12 <div id="search" :class="['search', { 'is-active': isActive }]"> 13 <div class="search__overlay" v-cloak v-if="isActive" @click="isActive = false"></div> 14 <form class="search__form" :class="{ 'is-active': isActive }" method="get" action="/search" @submit="saveQuery" @keydown.enter.prevent="submitFormIfKeyboard" ref="form"> 15 <div class="search__head"> 16 <button class="search__mobile-close" type="button" @click="isActive = false" v-if="isActive" v-cloak><svg class="icon icon-close"><use xlink:href="#icon-close"></use></svg></button> 17 <label class="search__mobile-open" for="search-input"><svg class="icon icon-search"><use xlink:href="#icon-search"></use></svg></label> 18 <input :value="query" @input="loading = true; query = $event.target.value" @focus="isActive = true" id="search-input" class="search__input" type="search" name="q" placeholder="Vyhľadaj trasu - skús Rysy" autocomplete="off"> 19 <button type="submit" class="button submit">VyhľadaÅ¥</button> 20 </div> 21 22 <div class="search__live-panel" v-cloak v-if="isActive"> 23 <div class="search__live-results" :class="{ 'is-loading': loading }"> 24 <span class="search__fail" v-if="query.length > 2 && !loading && !resultsCount"> 25 <svg id="SVGDoc" width="105" height="55" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 105 55"><g opacity="0.1"><path d="M95.00083,40.32v0l-10.11083,14.7v0h20.16v0z" fill="#000000" fill-opacity="1"></path><path d="M27.1516,33.44828v0v20.18874v0l-13.12343,-19.00967v0l-13.92817,20.35442v0h27.0516v0h0.92855v0h54.22763v0l-28.97121,-54.98177v0z" fill="#000000" fill-opacity="1"></path></g></svg> 26 <strong>Žiadne výsledky</strong> 27 <small>Skús zadaÅ¥ iný výraz</small> 28 </span> 29 <ul v-if="resultsCount"> 30 <li v-for="result in results" :key="result.id"> 31 <div v-if="result.type === 'city'"> 32 <svg class="icon icon-city"><use xlink:href="#icon-city"></use></svg> 33 <strong class="headline">{{ result.name }} <span>+ 50km</span><small>trasy v okruhu</small></strong> 34 <ul class="simple menu"> 35 <li><a @click="saveQuery(result.name)" :href="'/search?osm_id=' + result.id + '&radius=20'" class="is-default">do 20km</a></li> 36 <li><a @click="saveQuery(result.name)" :href="'/search?osm_id=' + result.id + '&radius=50'">do 50km</a></li> 37 <li><a @click="saveQuery(result.name)" :href="'/search?osm_id=' + result.id + '&radius=120'">do 120km</a></li> 38 </ul> 39 </div> 40 <a v-else-if="result.type === 'district'" @click="saveQuery(null)" :href="'/search?d=' + result.name"> 41 <svg class="icon icon-region"><use xlink:href="#icon-region"></use></svg> 42 <strong class="headline" v-if="result.name.includes('okres')">{{ result.name }}<small>trasy v okrese</small></strong> 43 <strong class="headline" v-else>{{ result.name }}<small>trasy v meste</small></strong> 44 </a> 45 <a v-else-if="result.type === 'random'" href="/search"> 46 <svg class="icon icon-search-random"><use xlink:href="#icon-search-random"></use></svg> 47 <strong class="headline">{{ result.name }}</strong> 48 </a> 49 <div v-else-if="result.type === 'location'"> 50 <svg class="icon icon-search-location"><use xlink:href="#icon-search-location"></use></svg> 51 <strong class="headline">{{ result.name }} <span>+ 50km</span><small>trasy v okruhu</small></strong> 52 <ul class="simple menu"> 53 <li><a @click.prevent="searchNearMe(20)" href="#" class="is-default">do 20km</a></li> 54 <li><a @click.prevent="searchNearMe(50)" href="#">do 50km</a></li> 55 <li><a @click.prevent="searchNearMe(120)" href="#">do 120km</a></li> 56 </ul> 57 <small v-if="locationLoading">ZisÅ¥ujem tvoju polohuâ¦</small> 58 <small v-if="locationError" class="search__location-error">{{ locationError }}</small> 59 </div> 60 <div v-else-if="result.type === 'loader'" class="result is-loading"></div> 61 <a v-else :href="'/hike/' + result.url" target="_blank"> 62 <svg class="icon icon-steps"><use xlink:href="#icon-steps"></use></svg> 63 <strong class="headline">{{ result.name }} {{ result.subtitle }}<small>{{ result.mountain_range }}</small></strong> 64 </a> 65 </li> 66 <li> 67 <a class="all" v-if="hikesCount" @click="submitForm">ZobraziÅ¥ vÅ¡etky výsledky ({{ hikesCount }})</a> 68 </li> 69 </ul> 70 </div> 71 </div> 72 </form> 73 </div> 74`; 75 76const SearchInputComponent = { 77 template, 78 data() { 79 return { 80 query: '', 81 resultsCount: 0, 82 hikesCount: 0, 83 results: [], 84 loading: false, 85 isActive: false, 86 request: null, 87 locationLoading: false, 88 locationError: null, 89 initialResults: [ 90 { 91 id: -999999, 92 name: 'Kamkoľvek', 93 type: 'random', 94 }, 95 { 96 id: -99999999, 97 name: 'Tvoja poloha', 98 type: 'location', 99 }, 100 { 101 id: -9999999, 102 type: 'loader', 103 }, 104 ], 105 hintsLoaded: false, 106 }; 107 }, 108 methods: { 109 send() { 110 if (this.request) this.cancel();
111 this.makeRequest(); 112 }, 113 114 cancel() { 115 this.request.cancel(); 116 this.request = null; 117 }, 118 119 makeRequest: debounce(function makeRequest() { 120 const axiosSource = axios.CancelToken.source(); 121 const config = { 122 method: 'get', 123 url: '/search-live', 124 params: { q: this.query }, 125 headers: { 'X-Requested-With': 'XMLHttpRequest' }, 126 cancelToken: axiosSource.token, 127 }; 128 this.request = { cancel: axiosSource.cancel }; 129 130 axios(config).then((response) => { 131 this.results = response.data.results; 132 this.resultsCount = response.data.count; 133 this.hikesCount = response.data.hikes_count; 134 this.loading = false; 135 this.request = null; 136 }).catch(this.logResponseErrors); 137 }, 150), 138 139 logResponseErrors(err) { 140 if (axios.isCancel(err)) { 141 return; 142 } 143 144 this.loading = false; 145 this.results = []; 146 }, 147 148 saveQuery(title) { 149 if (title !== null) { 150 localStorage.setItem('search-title', title); 151 } 152 localStorage.setItem('search-query', this.query); 153 return true; 154 }, 155 156 submitFormIfKeyboard() { 157 if (whatInput.ask() === 'keyboard' || whatInput.ask() === 'mouse') { 158 this.submitForm(); 159 } 160 }, 161 162 submitForm() { 163 this.saveQuery(null); 164 this.$refs.form.submit(); 165 }, 166 167 /* 168 * Ask for geolocation permission and navigate to the search results for the 169 * given radius around the user. Permission is requested only here, when the 170 * user actively picks "Tvoja poloha" - never proactively on page load. 171 */ 172 searchNearMe(radius) { 173 if (!navigator.geolocation) { 174 this.locationError = 'Geolokácia nie je podporovaná tvojÃm prehliadaÄom.'; 175 return; 176 } 177 178 this.locationError = null; 179 this.locationLoading = true; 180 181 const options = { 182 enableHighAccuracy: true, 183 timeout: 10000, 184 maximumAge: 5000, 185 }; 186 187 navigator.geolocation.getCurrentPosition((position) => { 188 this.saveQuery('Tvoja poloha'); 189 window.location.href = '/search?lat=' + position.coords.latitude + '&lon=' + position.coords.longitude + '&radius=' + radius; 190 }, (err) => { 191 this.locationLoading = false; 192 this.locationError = 'Nepodarilo sa zistiÅ¥ tvoju polohu. Skontroluj povolenia v prehliadaÄi.'; 193 console.log(err.message); 194 }, options); 195 }, 196 197 loadHints() { 198 if (this.request) { 199 this.request.cancel(); 200 this.request = null; 201 } 202 203 const axiosSource = axios.CancelToken.source(); 204 const config = { 205 method: 'get', 206 url: '/search-hint', 207 headers: { 'X-Requested-With': 'XMLHttpRequest' }, 208 cancelToken: axiosSource.token, 209 }; 210 this.request = { cancel: axiosSource.cancel }; 211 212 axios(config).then((response) => { 213 const results = response.data.results; 214 215 // The server only includes a "Tvoja poloha" entry when it already knows 216 // the user's position, which we deliberately don't send here - see 217 // searchNearMe(). Add the entry ourselves so it's always offered. 218 if (!results.some((result) => result.type === 'location')) { 219 results.splice(1, 0, { 220 id: -99999999, 221 name: 'Tvoja poloha', 222 type: 'location', 223 }); 224 } 225 226 this.results = results; 227 this.initialResults = results; 228 this.resultsCount = 1; 229 this.hikesCount = 0; 230 this.loading = false;
231 this.request = null; 232 this.hintsLoaded = true; 233 }).catch(this.logResponseErrors); 234 }, 235 236 showInitialResults() { 237 this.results = this.initialResults; 238 this.resultsCount = 1; 239 this.hikesCount = 0; 240 241 if (!this.hintsLoaded) { 242 this.loadHints(); 243 } 244 }, 245 246 getUrlVars() { 247 const vars = {}; 248 window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, (match, key, value) => { 249 vars[key] = value; 250 return match; 251 }); 252 return vars; 253 }, 254 }, 255 watch: { 256 query(val) { 257 this.results = []; 258 this.resultsCount = 0; 259 this.hikesCount = 0; 260 261 if (val.length > 2) { 262 this.send(); 263 } else if (val.length === 0) { 264 this.loading = false; 265 this.showInitialResults(); 266 } else { 267 this.loading = false; 268 } 269 }, 270 271 isActive(val) { 272 document.body.style.overflow = val ? 'hidden' : ''; 273 274 if (val && !this.query.length) { 275 this.showInitialResults(); 276 } 277 }, 278 }, 279 mounted() { 280 if (localStorage.getItem('search-query')) { 281 this.query = localStorage.getItem('search-query'); 282 localStorage.removeItem('search-query'); 283 return; 284 } 285 286 const urlParams = this.getUrlVars(); 287 if (urlParams.q !== undefined) { 288 this.query = decodeURI(urlParams.q); 289 } else if (urlParams.d !== undefined) { 290 this.query = decodeURI(urlParams.d); 291 } else if (urlParams.osm_id !== undefined) { 292 if ($('#search_osm_name').length && $('#search_osm_name').val() !== undefined) { 293 this.query = $('#search_osm_name').val(); 294 } 295 } 296 }, 297}; 298 299const Vue = window.Vue; 300 301if (!Vue) { 302 throw new Error('Vue is not available on window. Ensure base template loads Vue before search-input module.'); 303} 304 305new Vue({ 306 render: (h) => h(SearchInputComponent), 307}).$mount('#search');
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.