/**
* 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 = `
`;
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;
}