PageSourceSearch

https://www.visitvarmland.com/app/plugins/visitvarmland-map-plugin/assets/js/frontend.js?ver=1.0.0

js visitvarmland.com collected 2026-10-01 12:08:07 UTC 15,049 bytes, 434 lines download raw bytes

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">&times;</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">&times;</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">&lt;div id="vvm-map-embed"&gt;&lt;/div&gt;
304&lt;script src="${window.location.origin}/vvm-map-embed"&gt;&lt;/script&gt;
305&lt;script&gt;
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&lt;/script&gt;</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.