/** * Leaflet Map Picker - Reusable Component * * Usage: * const mapPicker = new LeafletMapPicker({ * mapElementId: 'map-picker', * latInputId: 'latitude-input', * lngInputId: 'longitude-input', * searchInputId: 'search-location', * searchResultsId: 'search-results', * clearSearchBtnId: 'clear-search', * alamatInputSelector: 'textarea[name="alamat_lengkap"]', * initialLat: -6.2088, * initialLng: 106.8456, * initialZoom: 12 * }); */ class LeafletMapPicker { constructor(options) { // Default options this.options = { mapElementId: 'map-picker', latInputId: 'latitude-input', lngInputId: 'longitude-input', searchInputId: 'search-location', searchResultsId: 'search-results', clearSearchBtnId: 'clear-search', alamatInputSelector: null, initialLat: -6.2088, initialLng: 106.8456, initialZoom: 12, maxZoom: 18, minZoom: 4, searchDebounceMs: 500, countryCode: 'id', language: 'id', ...options }; // State this.currentMarker = null; this.searchTimeout = null; // Initialize this.init(); } init() { // Get DOM elements this.mapElement = document.getElementById(this.options.mapElementId); this.latInput = document.getElementById(this.options.latInputId); this.lngInput = document.getElementById(this.options.lngInputId); this.searchInput = document.getElementById(this.options.searchInputId); this.searchResults = document.getElementById(this.options.searchResultsId); this.clearSearchBtn = document.getElementById(this.options.clearSearchBtnId); this.alamatInput = this.options.alamatInputSelector ? document.querySelector(this.options.alamatInputSelector) : null; if (!this.mapElement) { console.error(`Map element with id "${this.options.mapElementId}" not found`); return; } // Initialize map this.initMap(); // Bind events this.bindEvents(); // Initialize marker if coordinates already exist this.initializeExistingMarker(); } initMap() { // Initialize map this.map = L.map(this.mapElement).setView( [this.options.initialLat, this.options.initialLng], this.options.initialZoom ); // Add tile layer L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: this.options.maxZoom, minZoom: this.options.minZoom }).addTo(this.map); // Handle map click this.map.on('click', (e) => this.onMapClick(e)); } bindEvents() { // Search input events if (this.searchInput) { this.searchInput.addEventListener('input', (e) => this.onSearchInput(e)); this.searchInput.addEventListener('keydown', (e) => this.onSearchKeydown(e)); } // Clear search button if (this.clearSearchBtn) { this.clearSearchBtn.addEventListener('click', () => this.clearSearch()); } // Manual coordinate input events if (this.latInput) { this.latInput.addEventListener('change', () => this.onCoordinateChange()); } if (this.lngInput) { this.lngInput.addEventListener('change', () => this.onCoordinateChange()); } // Close search results when clicking outside document.addEventListener('click', (e) => { if (this.searchInput && this.searchResults && !this.searchInput.contains(e.target) && !this.searchResults.contains(e.target)) { this.searchResults.classList.add('hidden'); } }); } initializeExistingMarker() { window.addEventListener('load', () => { const lat = parseFloat(this.latInput?.value); const lng = parseFloat(this.lngInput?.value); if (!isNaN(lat) && !isNaN(lng)) { this.updateMarker(lat, lng); } }); } onMapClick(e) { const lat = e.latlng.lat; const lng = e.latlng.lng; // Update form inputs if (this.latInput) this.latInput.value = lat.toFixed(8); if (this.lngInput) this.lngInput.value = lng.toFixed(8); // Update marker and fetch address this.updateMarker(lat, lng, true); } onSearchInput(e) { const query = e.target.value.trim(); // Show/hide clear button if (this.clearSearchBtn) { if (query.length > 0) { this.clearSearchBtn.classList.remove('hidden'); } else { this.clearSearchBtn.classList.add('hidden'); if (this.searchResults) { this.searchResults.classList.add('hidden'); } } } // Debounce search clearTimeout(this.searchTimeout); this.searchTimeout = setTimeout(() => { this.searchLocation(query); }, this.options.searchDebounceMs); } onSearchKeydown(e) { // Prevent form submission on Enter if (e.key === 'Enter') { e.preventDefault(); } } onCoordinateChange() { const lat = parseFloat(this.latInput?.value); const lng = parseFloat(this.lngInput?.value); if (!isNaN(lat) && !isNaN(lng)) { this.updateMarker(lat, lng); } } async searchLocation(query) { if (!this.searchResults || query.length < 3) { if (this.searchResults) { this.searchResults.classList.add('hidden'); } return; } // Show loading state this.searchResults.innerHTML = `
Mencari lokasi...
`; this.searchResults.classList.remove('hidden'); try { const response = await fetch( `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&countrycodes=${this.options.countryCode}&limit=5&addressdetails=1`, { headers: { 'Accept-Language': this.options.language } } ); const data = await response.json(); this.displaySearchResults(data); } catch (error) { console.error('Error searching location:', error); this.searchResults.innerHTML = `
Gagal mencari lokasi. Silakan coba lagi.
`; } } displaySearchResults(results) { if (!this.searchResults) return; if (results.length === 0) { this.searchResults.innerHTML = `
Lokasi tidak ditemukan
`; this.searchResults.classList.remove('hidden'); return; } this.searchResults.innerHTML = results.map(result => { const escapedDisplayName = result.display_name.replace(/'/g, "\\'"); return `

${result.display_name}

${parseFloat(result.lat).toFixed(6)}, ${parseFloat(result.lon).toFixed(6)}

`; }).join(''); this.searchResults.classList.remove('hidden'); } selectSearchResult(lat, lon, displayName) { // Update coordinates if (this.latInput) this.latInput.value = parseFloat(lat).toFixed(8); if (this.lngInput) this.lngInput.value = parseFloat(lon).toFixed(8); // Update marker this.updateMarker(lat, lon, true); // Clear search if (this.searchResults) this.searchResults.classList.add('hidden'); if (this.searchInput) this.searchInput.value = ''; if (this.clearSearchBtn) this.clearSearchBtn.classList.add('hidden'); } clearSearch() { if (this.searchInput) this.searchInput.value = ''; if (this.searchResults) this.searchResults.classList.add('hidden'); if (this.clearSearchBtn) this.clearSearchBtn.classList.add('hidden'); if (this.searchInput) this.searchInput.focus(); } async fetchAddress(lat, lng) { if (!this.alamatInput) return; // Show loading state const originalPlaceholder = this.alamatInput.placeholder; this.alamatInput.placeholder = 'Mengambil alamat dari koordinat...'; this.alamatInput.disabled = true; try { const response = await fetch( `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1`, { headers: { 'Accept-Language': this.options.language } } ); const data = await response.json(); if (data && data.display_name) { // Build formatted address const addr = data.address || {}; let formattedAddress = ''; // Try to build a structured address if (addr.road || addr.suburb || addr.village) { const parts = []; if (addr.road) parts.push(addr.road); if (addr.house_number) parts.push('No. ' + addr.house_number); if (addr.suburb || addr.village) parts.push(addr.suburb || addr.village); if (addr.city || addr.town || addr.city_district) { parts.push(addr.city || addr.town || addr.city_district); } if (addr.state) parts.push(addr.state); if (addr.postcode) parts.push(addr.postcode); formattedAddress = parts.join(', '); } else { // Fallback to display_name formattedAddress = data.display_name; } this.alamatInput.value = formattedAddress; this.alamatInput.placeholder = originalPlaceholder; // Show success feedback this.alamatInput.classList.add('border-success'); setTimeout(() => this.alamatInput.classList.remove('border-success'), 2000); } } catch (error) { console.error('Error fetching address:', error); this.alamatInput.placeholder = 'Gagal mengambil alamat, silakan isi manual...'; setTimeout(() => this.alamatInput.placeholder = originalPlaceholder, 3000); } finally { this.alamatInput.disabled = false; } } updateMarker(lat, lng, fetchAddr = false) { // Remove existing marker if (this.currentMarker) { this.map.removeLayer(this.currentMarker); } // Add new draggable marker this.currentMarker = L.marker([lat, lng], { draggable: true }).addTo(this.map); this.currentMarker.bindPopup(`
Lokasi Dipilih
${lat.toFixed(8)}, ${lng.toFixed(8)}
Drag marker untuk mengubah lokasi
`).openPopup(); // Handle marker drag this.currentMarker.on('dragend', (e) => { const marker = e.target; const position = marker.getLatLng(); // Update input values if (this.latInput) this.latInput.value = position.lat.toFixed(8); if (this.lngInput) this.lngInput.value = position.lng.toFixed(8); // Update popup content marker.setPopupContent(`
Lokasi Dipilih
${position.lat.toFixed(8)}, ${position.lng.toFixed(8)}
Drag marker untuk mengubah lokasi
`); // Fetch address for new position this.fetchAddress(position.lat, position.lng); }); // Center map on marker this.map.setView([lat, lng], 15); // Fetch address if requested if (fetchAddr) { this.fetchAddress(lat, lng); } } // Public API methods getCoordinates() { return { lat: parseFloat(this.latInput?.value), lng: parseFloat(this.lngInput?.value) }; } setCoordinates(lat, lng, updateMarker = true) { if (this.latInput) this.latInput.value = lat.toFixed(8); if (this.lngInput) this.lngInput.value = lng.toFixed(8); if (updateMarker) { this.updateMarker(lat, lng); } } destroy() { // Clean up event listeners and map if (this.map) { this.map.remove(); } clearTimeout(this.searchTimeout); } } // Export for use in modules or expose globally if (typeof module !== 'undefined' && module.exports) { module.exports = LeafletMapPicker; } else { window.LeafletMapPicker = LeafletMapPicker; }