1/** 2 * Visit Värmland Map Frontend JavaScript 3 * 4 * Handles map initialization and interactions on the frontend. 5 * 6 * @package VisitVarmlandMap 7 */ 8 9/* global jQuery, L, vvm_data */ 10 11( function( $ ) { 12 'use strict'; 13 14 // Wait for DOM ready. 15 $( document ).ready( function() { 16 initializeMaps(); 17 } ); 18 19 /** 20 * Initialize all maps on the page. 21 * 22 * @since 1.0.0 23 */ 24 function initializeMaps() { 25 $( '.vvm-map-container' ).each( function() { 26 const $container = $( this ); 27 const mapId = $container.find( '.vvm-map' ).attr( 'id' ); 28 const categories = JSON.parse( $container.attr( 'data-categories' ) || '[]' ); 29 30 // Create map. 31 const map = createMap( mapId ); 32 33 // Create marker layer group. 34 const markerLayer = L.featureGroup().addTo( map ); 35 36 // Store marker layer on map object for access in other functions. 37 map.markerLayer = markerLayer; 38 39 // Load destinations. 40 loadDestinations( map, categories ); 41 42 // Set up controls. 43 setupControls( map, $container, categories ); 44 45 // Lägg till logotyper i kartcontainern 46 $container.prepend('<div class="vvm-map-logos"><a href="https://visitvarmland.com" target="_blank" rel="noopener noreferrer"><img src="' + vvm_data.plugin_url + 'assets/images/visitvarmland-logo.png" alt="Visit Värmland" class="vvm-logo vvm-logo-main"></a><a href="https://www.varmlandstrafik.se" target="_blank" rel="noopener noreferrer"><img src="' + vvm_data.plugin_url + 'assets/images/varmlandstrafik-logo.png" alt="Värmlandstrafik" class="vvm-logo vvm-logo-trafik"></a></div>'); 47 } ); 48 } 49 50 /** 51 * Create Leaflet map. 52 * 53 * @since 1.0.0 54 * @param {string} mapId The map container ID. 55 * @return {Object} Leaflet map instance. 56 */ 57 function createMap( mapId ) { 58 // Map center point from settings or defaults. 59 const defaultLat = vvm_data.map_center_lat || 59.7315; 60 const defaultLng = vvm_data.map_center_lng || 13.2715; 61 const defaultZoom = vvm_data.map_zoom || 9; 62 63 // Skapa karta med zoomControl: true (default) 64 const map = L.map( mapId, { zoomControl: true } ).setView( [ defaultLat, defaultLng ], defaultZoom ); 65 66 67 // Flytta zoomkontroller till övre högra hörnet 68 map.zoomControl.setPosition('topright'); 69 70 // Add OpenStreetMap tiles. 71 L.tileLayer( 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 72 attribution: '© OpenStreetMap contributors' 73 } ).addTo( map ); 74 75 // Fix size after rendering. 76 setTimeout( function() { 77 map.invalidateSize(); 78 }, 100 ); 79 80 return map; 81 } 82 83 /** 84 * Load destinations from API. 85 * 86 * @since 1.0.0 87 * @param {Object} map Leaflet map instance. 88 * @param {Array} categories Category filters. 89 */ 90 function loadDestinations( map, categories ) { 91 // Build URL with parameters. 92 const params = new URLSearchParams( { 93 categories: categories.join( ',' ) 94 } ); 95 96 // Fetch destinations. 97 $.ajax( { 98 url: `${vvm_data.api_url}destinations?${params.toString()}`, 99 method: 'GET', 100 success: function( data ) { 101 // Handle potential Content-Type issues from server 102 let destinations = data; 103 104 // If data is a string (due to incorrect Content-Type), try to parse as JSON 105 if (typeof data === 'string') { 106 try { 107 destinations = JSON.parse(data); 108 } catch (e) { 109 console.error('Failed to parse JSON response:', e); 110 return; 111 } 112 } 113 114 displayDestinations( map, destinations ); 115 }, 116 error: function( xhr, status, error ) { 117 console.error( 'Could not load destinations:', error ); 118 } 119 } ); 120 } 121 122 /** 123 * Display destinations on the map. 124 * 125 * @since 1.0.0 126 * @param {Object} map Leaflet map instance. 127 * @param {Array} destinations Array of destination objects. 128 */ 129 function displayDestinations( map, destinations ) { 130 // Clear existing markers. 131 if ( map.markerLayer ) { 132 map.markerLayer.clearLayers(); 133 } 134 135 destinations.forEach( function( destination ) { 136 // Create custom icon based on category. 137 const icon = createCategoryIcon( destination.categories[0] ); 138 139 // Create marker. 140 const marker = L.marker( [ destination.lat, destination.lng ], { icon: icon } ); 141 142 // Add popup.
143 const popupContent = createPopupContent( destination ); 144 marker.bindPopup( popupContent ); 145 146 // Add to marker layer. 147 map.markerLayer.addLayer( marker ); 148 } ); 149 150 // Fit map view to markers. 151 if ( destinations.length > 0 ) { 152 // Get current zoom level before fitting bounds 153 const currentZoom = map.getZoom(); 154 155 // Fit bounds to show all markers 156 map.fitBounds( map.markerLayer.getBounds(), { 157 padding: [ 50, 50 ], 158 maxZoom: Math.max(currentZoom, 10) // Don't zoom out beyond level 10 or current zoom 159 } ); 160 161 } 162 } 163 164 /** 165 * Create icon based on category. 166 * 167 * @since 1.0.0 168 * @param {string} category Category identifier. 169 * @return {Object} Leaflet icon instance. 170 */ 171 function createCategoryIcon( category ) { 172 // Använd SVG-ikon istället för divIcon 173 return L.icon( { 174 iconUrl: vvm_data.plugin_url + 'assets/images/marker.svg', 175 iconSize: [ 32, 32 ], // SVG-filens dimensioner 176 iconAnchor: [ 16, 32 ], // Spetsen pÃ¥ markören (botten mitten) 177 popupAnchor: [ 0, -32 ] // Popup-position relativt iconAnchor (ovanför spetsen) 178 } ); 179 } 180 181 /** 182 * Create popup content. 183 * 184 * @since 1.0.0 185 * @param {Object} destination Destination object. 186 * @return {string} HTML content for popup. 187 */ 188 function createPopupContent( destination ) { 189 let content = '<div class="vvm-popup">'; 190 191 // Bildsektion 192 if ( destination.images && destination.images.length > 0 && destination.images[0].url ) { 193 const imageUrl = destination.images[0].url; 194 content += '<div class="vvm-popup-image-wrap">'; 195 if ( destination.slug ) { 196 content += `<a href="https://visitvarmland.com/${destination.slug}" target="_blank" rel="noopener noreferrer">`; 197 } 198 content += `<img src="${imageUrl}" alt="${destination.name}" class="vvm-popup-image">`; 199 if ( destination.slug ) { 200 content += '</a>'; 201 } 202 content += '</div>'; // Stäng vvm-popup-image-wrap 203 } 204 205 // Namnsektion 206 if ( destination.name ) { 207 if ( destination.slug ) { 208 content += `<h3><a href="https://visitvarmland.com/${destination.slug}" target="_blank" rel="noopener noreferrer">${destination.name}<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#CCC"><path fill-rule="evenodd" d="M12.97 3.97a.75.75 0 0 1 1.06 0l7.5 7.5a.75.75 0 0 1 0 1.06l-7.5 7.5a.75.75 0 1 1-1.06-1.06l6.22-6.22H3a.75.75 0 0 1 0-1.5h16.19l-6.22-6.22a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd" /></svg></a></h3>`; 209 } else { 210 content += `<h3>${destination.name}</h3>`; 211 } 212 } 213 214 // Pil under popupen 215 content += '<div class="vvm-popup-arrow"></div>'; 216 content += '</div>'; // Stäng vvm-popup 217 218 return content; 219 } 220 221 /** 222 * Set up controls for the map. 223 * 224 * @since 1.0.0 225 * @param {Object} map Leaflet map instance. 226 * @param {jQuery} $container jQuery container element. 227 * @param {Array} initialCategories Initial categories from block settings. 228 */ 229 function setupControls( map, $container, initialCategories ) { 230 // Ta bort gamla kontroller 231 $container.find('.vvm-map-controls, .vvm-map-filters').remove(); 232 233 // Om inga kategorier är valda (ska inte hända), visa alla 234 if ( initialCategories.length === 0 ) { 235 initialCategories = ['eat', 'stay', 'do']; 236 } 237 238 // Hämta alla tillgängliga kategorier frÃ¥n vvm_data 239 const allCategories = vvm_data.categories || [ 240 { slug: 'eat', name: 'Ãta' }, 241 { slug: 'stay', name: 'Bo' }, 242 { slug: 'do', name: 'Göra' } 243 ]; 244 245 // Filtrera ut bara de kategorier som är valda för kartan 246 const availableCategories = allCategories.filter(cat => initialCategories.includes(cat.slug)); 247 248 // Bygg filterknappar baserat pÃ¥ valda kategorier 249 let filterButtons = ''; 250 251 // Om alla eller flera kategorier är valda, visa "Visa alla" som aktiv 252 if ( availableCategories.length > 1 ) { 253 filterButtons += `<button class="vvm-filter-btn active" data-category="all"><span class="vvm-btn-text-full">Visa alla</span><span class="vvm-btn-text-short">Alla</span></button>`; 254 255 // Lägg till knappar för varje vald kategori 256 availableCategories.forEach(cat => { 257 filterButtons += `<button class="vvm-filter-btn" data-category="${cat.slug}">${cat.name}</button>`; 258 }); 259 } else if ( availableCategories.length === 1 ) { 260 // Om bara en kategori är vald, visa den som aktiv 261 const cat = availableCategories[0]; 262 filterButtons += `<button class="vvm-filter-btn active" data-category="${cat.slug}">${cat.name}</button>`; 263 } 264 265 // Om det finns filterknappar, visa dem 266 if ( filterButtons ) { 267 const filterHtml = `<div class="vvm-map-filters">${filterButtons}</div>`; 268 $container.append(filterHtml); 269 } 270 271 // Lägg till modal-knappar i nedre högra hörnet 272 const modalButtonHtml = `
273 <div class="vvm-modal-buttons"> 274 <button class="vvm-modal-btn vvm-info-btn" aria-label="Visa information om kartan" title="Information"> 275 <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> 276 <path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" /> 277 </svg> 278 </button> 279 <button class="vvm-modal-btn vvm-embed-btn" aria-label="FÃ¥ kod för att bädda in kartan" title="Bädda in karta"> 280 <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true"> 281 <path stroke-linecap="round" stroke-linejoin="round" d="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z" /> 282 </svg> 283 </button> 284 </div> 285 `; 286 $container.append(modalButtonHtml); 287 288 // Lägg till modal HTML 289 const modalHtml = ` 290 <div class="vvm-modal-overlay vvm-info-modal-overlay" style="display: none;" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="vvm-info-modal-title" aria-describedby="vvm-info-modal-desc"> 291 <div class="vvm-modal" role="document"> 292 <button class="vvm-modal-close" aria-label="Stäng information">×</button> 293 <h2 class="vvm-modal-title" id="vvm-info-modal-title">${vvm_data.modal_title || 'Information'}</h2> 294 <div class="vvm-modal-content" id="vvm-info-modal-desc">${vvm_data.modal_content || ''}</div> 295 </div> 296 </div> 297 <div class="vvm-modal-overlay vvm-embed-modal-overlay" style="display: none;" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="vvm-embed-modal-title" aria-describedby="vvm-embed-modal-desc"> 298 <div class="vvm-modal" role="document"> 299 <button class="vvm-modal-close" aria-label="Stäng bädda in-dialog">×</button> 300 <h2 class="vvm-modal-title" id="vvm-embed-modal-title">Bädda in karta</h2> 301 <div class="vvm-modal-content" id="vvm-embed-modal-desc"> 302 <p>Kopiera koden nedan för att bädda in kartan pÃ¥ din webbplats:</p> 303 <textarea class="vvm-embed-code" readonly rows="10" aria-label="Embed-kod för karta"><div id="vvm-map-embed"></div> 304<script src="${window.location.origin}/vvm-map-embed"></script> 305<script> 306VVMap.init({ 307 container: 'vvm-map-embed', 308 categories: ${JSON.stringify(initialCategories)}, 309 height: '${$container.find('.vvm-map').height()}px', 310 button_color: '#your-brand-color' // Valfritt: anpassa knappfärg 311}); 312</script></textarea> 313 <button class="vvm-copy-btn">Kopiera kod</button> 314 </div> 315 </div> 316 </div> 317 `; 318 $container.append(modalHtml); 319 320 // Hantera info-modal klick 321 $container.find('.vvm-info-btn').on('click', function(e) { 322 e.preventDefault(); 323 e.stopPropagation(); 324 openModal($container.find('.vvm-info-modal-overlay')); 325 }); 326 327 // Hantera embed-modal klick 328 $container.find('.vvm-embed-btn').on('click', function(e) { 329 e.preventDefault(); 330 e.stopPropagation(); 331 openModal($container.find('.vvm-embed-modal-overlay')); 332 }); 333 334 // Stäng modal vid klick pÃ¥ stängknapp eller overlay 335 $container.find('.vvm-modal-close, .vvm-modal-overlay').on('click', function(e) { 336 if (e.target === this || $(e.target).hasClass('vvm-modal-close')) { 337 closeModal($container.find('.vvm-modal-overlay:visible')); 338 } 339 }); 340 341 // Funktion för att öppna modal 342 function openModal($modal) { 343 // Spara tidigare fokuserat element 344 const previousFocus = document.activeElement; 345 $modal.data('previousFocus', previousFocus); 346 347 // Visa modal 348 $modal.attr('aria-hidden', 'false').fadeIn(200, function() { 349 // Sätt fokus pÃ¥ första focusable element i modal 350 const focusableElements = $modal.find('button, input, textarea, select, a[href]').filter(':visible'); 351 if (focusableElements.length) { 352 focusableElements.first().focus(); 353 } 354 }); 355 356 // Lägg till body class för att förhindra scrollning 357 $('body').addClass('vvm-modal-open'); 358 } 359 360 // Funktion för att stänga modal 361 function closeModal($modal) { 362 $modal.attr('aria-hidden', 'true').fadeOut(200, function() { 363 // à terställ fokus 364 const previousFocus = $modal.data('previousFocus'); 365 if (previousFocus) { 366 $(previousFocus).focus(); 367 } 368 }); 369 370 // Ta bort body class 371 $('body').removeClass('vvm-modal-open'); 372 } 373 374 // Tangentbordshantering 375 $container.find('.vvm-modal-overlay').on('keydown', function(e) { 376 const $modal = $(this); 377 378 // ESC för att stänga 379 if (e.key === 'Escape') { 380 e.preventDefault(); 381 closeModal($modal); 382 return; 383 } 384 385 // Tab-fälla (focus trap) 386 if (e.key === 'Tab') { 387 const focusableElements = $modal.find('button, input, textarea, select, a[href]').filter(':visible'); 388 const firstElement = focusableElements.first()[0];
389 const lastElement = focusableElements.last()[0]; 390 391 if (e.shiftKey) { 392 // Shift + Tab 393 if (document.activeElement === firstElement) { 394 e.preventDefault(); 395 lastElement.focus(); 396 } 397 } else { 398 // Tab 399 if (document.activeElement === lastElement) { 400 e.preventDefault(); 401 firstElement.focus(); 402 } 403 } 404 } 405 }); 406 407 // Hantera kopiera-knapp 408 $container.find('.vvm-copy-btn').on('click', function() { 409 const textarea = $container.find('.vvm-embed-code')[0]; 410 textarea.select(); 411 document.execCommand('copy'); 412 $(this).text('Kopierad!'); 413 setTimeout(() => { 414 $(this).text('Kopiera kod'); 415 }, 2000); 416 }); 417 418 // Hantera filterklick 419 $container.find('.vvm-filter-btn').on('click', function() { 420 $container.find('.vvm-filter-btn').removeClass('active'); 421 $(this).addClass('active'); 422 const cat = $(this).data('category'); 423 let categories = []; 424 if (cat === 'all') { 425 categories = ['eat', 'stay', 'do']; 426 } else { 427 categories = [cat]; 428 } 429 // Ladda om destinationer 430 loadDestinations(map, categories); 431 }); 432 } 433 434} )( jQuery );
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.