Extract map controls to a separate file

This commit is contained in:
Eugene Burmakin 2025-10-15 11:43:49 +02:00
parent 36289d2469
commit 44cbfff8ff
6 changed files with 369 additions and 258 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,10 @@
import { Controller } from "@hotwired/stimulus";
import L from "leaflet";
import { showFlashMessage } from "../maps/helpers";
import { applyThemeToButton } from "../maps/theme_utils";
import {
setAddVisitButtonActive,
setAddVisitButtonInactive
} from "../maps/map_controls";
export default class extends Controller {
static targets = [""];
@ -71,40 +74,26 @@ export default class extends Controller {
setupAddVisitButton() {
if (!this.map || this.addVisitButton) return;
// Create the Add Visit control
const AddVisitControl = L.Control.extend({
onAdd: (map) => {
const button = L.DomUtil.create('button', 'leaflet-control-button add-visit-button');
button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin-check-icon lucide-map-pin-check"><path d="M19.43 12.935c.357-.967.57-1.955.57-2.935a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32.197 32.197 0 0 0 .813-.728"/><circle cx="12" cy="10" r="3"/><path d="m16 18 2 2 4-4"/></svg>';
button.title = 'Add a visit';
// The Add Visit button is now created centrally by maps_controller.js
// via addTopRightButtons(). We just need to find it and attach our handler.
setTimeout(() => {
this.addVisitButton = document.querySelector('.add-visit-button');
// Style the button with theme-aware styling
applyThemeToButton(button, this.userThemeValue || 'dark');
button.style.width = '48px';
button.style.height = '48px';
button.style.borderRadius = '4px';
button.style.padding = '0';
button.style.display = 'flex';
button.style.alignItems = 'center';
button.style.justifyContent = 'center';
button.style.fontSize = '18px';
button.style.transition = 'all 0.2s ease';
// Disable map interactions when clicking the button
L.DomEvent.disableClickPropagation(button);
// Toggle add visit mode on button click
L.DomEvent.on(button, 'click', () => {
this.toggleAddVisitMode(button);
});
this.addVisitButton = button;
return button;
if (this.addVisitButton) {
// Attach our click handler to the existing button
// Use event capturing and stopPropagation to prevent map click
this.addVisitButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.toggleAddVisitMode(this.addVisitButton);
}, true); // Use capture phase
} else {
console.warn('Add visit button not found, retrying...');
// Retry if button hasn't been created yet
this.addVisitButton = null;
setTimeout(() => this.setupAddVisitButton(), 200);
}
});
// Add the control to the map (top right, below existing buttons)
this.map.addControl(new AddVisitControl({ position: 'topright' }));
}, 100);
}
toggleAddVisitMode(button) {
@ -121,15 +110,18 @@ export default class extends Controller {
this.isAddingVisit = true;
// Update button style to show active state
button.style.backgroundColor = '#dc3545';
button.style.color = 'white';
button.innerHTML = '✕';
setAddVisitButtonActive(button);
// Change cursor to crosshair
this.map.getContainer().style.cursor = 'crosshair';
// Add map click listener
this.map.on('click', this.onMapClick, this);
// Add map click listener with a small delay to prevent immediate trigger
// This ensures the button click doesn't propagate to the map
setTimeout(() => {
if (this.isAddingVisit) {
this.map.on('click', this.onMapClick, this);
}
}, 100);
showFlashMessage('notice', 'Click on the map to place a visit');
}
@ -137,9 +129,8 @@ export default class extends Controller {
exitAddVisitMode(button) {
this.isAddingVisit = false;
// Reset button style with theme-aware styling
applyThemeToButton(button, this.userThemeValue || 'dark');
button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin-check-icon lucide-map-pin-check"><path d="M19.43 12.935c.357-.967.57-1.955.57-2.935a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32.197 32.197 0 0 0 .813-.728"/><circle cx="12" cy="10" r="3"/><path d="m16 18 2 2 4-4"/></svg>';
// Reset button style to inactive state
setAddVisitButtonInactive(button, this.userThemeValue || 'dark');
// Reset cursor
this.map.getContainer().style.cursor = '';
@ -186,6 +177,12 @@ export default class extends Controller {
}
showVisitForm(lat, lng) {
// Close any existing popup first to ensure only one popup is open
if (this.currentPopup) {
this.map.closePopup(this.currentPopup);
this.currentPopup = null;
}
// Get current date/time for default values
const now = new Date();
const oneHourLater = new Date(now.getTime() + (60 * 60 * 1000));
@ -291,7 +288,8 @@ export default class extends Controller {
started_at: formData.get('started_at'),
ended_at: formData.get('ended_at'),
latitude: formData.get('latitude'),
longitude: formData.get('longitude')
longitude: formData.get('longitude'),
status: 'confirmed' // Manually created visits should be confirmed
}
};
@ -325,15 +323,14 @@ export default class extends Controller {
if (response.ok) {
showFlashMessage('notice', `Visit "${visitData.visit.name}" created successfully!`);
// Store the created visit data
const createdVisit = data;
this.exitAddVisitMode(this.addVisitButton);
// Refresh visits layer - this will clear and refetch data
this.refreshVisitsLayer();
// Ensure confirmed visits layer is enabled (with a small delay for the API call to complete)
setTimeout(() => {
this.ensureVisitsLayersEnabled();
}, 300);
// Add the newly created visit marker immediately to the map
this.addCreatedVisitToMap(createdVisit, visitData.visit.latitude, visitData.visit.longitude);
} else {
const errorMessage = data.error || data.message || 'Failed to create visit';
showFlashMessage('error', errorMessage);
@ -348,96 +345,92 @@ export default class extends Controller {
}
}
refreshVisitsLayer() {
console.log('Attempting to refresh visits layer...');
addCreatedVisitToMap(visitData, latitude, longitude) {
console.log('Adding newly created visit to map immediately', { latitude, longitude, visitData });
// Try multiple approaches to refresh the visits layer
const mapsController = document.querySelector('[data-controller*="maps"]');
if (mapsController) {
// Try to get the Stimulus controller instance
const stimulusController = this.application.getControllerForElementAndIdentifier(mapsController, 'maps');
if (stimulusController && stimulusController.visitsManager) {
console.log('Found maps controller with visits manager');
// Clear existing visits and fetch fresh data
if (stimulusController.visitsManager.visitCircles) {
stimulusController.visitsManager.visitCircles.clearLayers();
}
if (stimulusController.visitsManager.confirmedVisitCircles) {
stimulusController.visitsManager.confirmedVisitCircles.clearLayers();
}
// Refresh the visits data
if (typeof stimulusController.visitsManager.fetchAndDisplayVisits === 'function') {
console.log('Refreshing visits data...');
stimulusController.visitsManager.fetchAndDisplayVisits();
}
} else {
console.log('Could not find maps controller or visits manager');
// Fallback: Try to dispatch a custom event
const refreshEvent = new CustomEvent('visits:refresh', { bubbles: true });
mapsController.dispatchEvent(refreshEvent);
}
} else {
if (!mapsController) {
console.log('Could not find maps controller element');
return;
}
const stimulusController = this.application.getControllerForElementAndIdentifier(mapsController, 'maps');
if (!stimulusController || !stimulusController.visitsManager) {
console.log('Could not find maps controller or visits manager');
return;
}
const visitsManager = stimulusController.visitsManager;
// Create a circle for the newly created visit (always confirmed)
const circle = L.circle([latitude, longitude], {
color: '#4A90E2', // Border color for confirmed visits
fillColor: '#4A90E2', // Fill color for confirmed visits
fillOpacity: 0.5,
radius: 110, // Confirmed visit size
weight: 2,
interactive: true,
bubblingMouseEvents: false,
pane: 'confirmedVisitsPane'
});
// Add the circle to the confirmed visits layer
visitsManager.confirmedVisitCircles.addLayer(circle);
console.log('✅ Added newly created confirmed visit circle to layer');
console.log('Confirmed visits layer info:', {
layerCount: visitsManager.confirmedVisitCircles.getLayers().length,
isOnMap: this.map.hasLayer(visitsManager.confirmedVisitCircles)
});
// Make sure the layer is visible on the map
if (!this.map.hasLayer(visitsManager.confirmedVisitCircles)) {
this.map.addLayer(visitsManager.confirmedVisitCircles);
console.log('✅ Added confirmed visits layer to map');
}
// Check if the layer control has the confirmed visits layer enabled
this.ensureConfirmedVisitsLayerEnabled();
}
ensureVisitsLayersEnabled() {
console.log('Ensuring visits layers are enabled...');
const mapsController = document.querySelector('[data-controller*="maps"]');
if (mapsController) {
const stimulusController = this.application.getControllerForElementAndIdentifier(mapsController, 'maps');
if (stimulusController && stimulusController.map && stimulusController.visitsManager) {
const map = stimulusController.map;
const visitsManager = stimulusController.visitsManager;
// Get the confirmed visits layer (newly created visits are always confirmed)
const confirmedVisitsLayer = visitsManager.getConfirmedVisitCirclesLayer();
// Ensure confirmed visits layer is added to map since we create confirmed visits
if (confirmedVisitsLayer && !map.hasLayer(confirmedVisitsLayer)) {
console.log('Adding confirmed visits layer to map');
map.addLayer(confirmedVisitsLayer);
// Update the layer control checkbox to reflect the layer is now active
this.updateLayerControlCheckbox('Confirmed Visits', true);
}
// Refresh visits data to include the new visit
if (typeof visitsManager.fetchAndDisplayVisits === 'function') {
console.log('Final refresh of visits to show new visit...');
visitsManager.fetchAndDisplayVisits();
}
}
}
}
updateLayerControlCheckbox(layerName, isEnabled) {
// Find the layer control input for the specified layer
ensureConfirmedVisitsLayerEnabled() {
// Find the layer control and check/enable the "Confirmed Visits" checkbox
const layerControlContainer = document.querySelector('.leaflet-control-layers');
if (!layerControlContainer) {
console.log('Layer control container not found');
return;
}
const inputs = layerControlContainer.querySelectorAll('input[type="checkbox"]');
inputs.forEach(input => {
const label = input.nextElementSibling;
if (label && label.textContent.trim() === layerName) {
console.log(`Updating ${layerName} checkbox to ${isEnabled}`);
input.checked = isEnabled;
// Expand the layer control if it's collapsed
const layerControlExpand = layerControlContainer.querySelector('.leaflet-control-layers-toggle');
if (layerControlExpand) {
layerControlExpand.click();
}
// Trigger change event to ensure proper state management
input.dispatchEvent(new Event('change', { bubbles: true }));
}
});
setTimeout(() => {
const inputs = layerControlContainer.querySelectorAll('input[type="checkbox"]');
inputs.forEach(input => {
const label = input.nextElementSibling;
if (label && label.textContent.trim().includes('Confirmed Visits')) {
console.log('Found Confirmed Visits checkbox, current state:', input.checked);
if (!input.checked) {
console.log('Enabling Confirmed Visits layer via checkbox');
input.checked = true;
input.dispatchEvent(new Event('change', { bubbles: true }));
}
}
});
}, 100);
}
refreshVisitsLayer() {
// Don't auto-refresh after creating a visit
// The visit is already visible on the map from addCreatedVisitToMap()
// Auto-refresh would clear it because fetchAndDisplayVisits uses URL date params
// which might not include the newly created visit
console.log('Skipping auto-refresh - visit already added to map');
}
cleanup() {
if (this.map) {
this.map.off('click', this.onMapClick, this);

View file

@ -44,6 +44,7 @@ import { TileMonitor } from "../maps/tile_monitor";
import BaseController from "./base_controller";
import { createAllMapLayers } from "../maps/layers";
import { applyThemeToControl, applyThemeToButton, applyThemeToPanel } from "../maps/theme_utils";
import { addTopRightButtons } from "../maps/map_controls";
export default class extends BaseController {
static targets = ["container"];
@ -212,22 +213,6 @@ export default class extends BaseController {
// Expose maps controller globally for family integration
window.mapsController = this;
// Initialize layers for the layer control
const controlsLayer = {
Points: this.markersLayer,
Routes: this.polylinesLayer,
Tracks: this.tracksLayer,
Heatmap: this.heatmapLayer,
"Fog of War": this.fogOverlay,
"Scratch map": this.scratchLayerManager?.getLayer() || L.layerGroup(),
Areas: this.areasLayer,
Photos: this.photoMarkers,
"Suggested Visits": this.visitsManager.getVisitCirclesLayer(),
"Confirmed Visits": this.visitsManager.getConfirmedVisitCirclesLayer()
};
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
// Initialize tile monitor
this.tileMonitor = new TileMonitor(this.map, this.apiKey);
@ -253,11 +238,25 @@ export default class extends BaseController {
// Preload areas
fetchAndDrawAreas(this.areasLayer, this.apiKey);
// Add right panel toggle
this.addTogglePanelButton();
// Add all top-right buttons in the correct order
this.initializeTopRightButtons();
// Initialize layers for the layer control
const controlsLayer = {
Points: this.markersLayer,
Routes: this.polylinesLayer,
Tracks: this.tracksLayer,
Heatmap: this.heatmapLayer,
"Fog of War": this.fogOverlay,
"Scratch map": this.scratchLayerManager?.getLayer() || L.layerGroup(),
Areas: this.areasLayer,
Photos: this.photoMarkers,
"Suggested Visits": this.visitsManager.getVisitCirclesLayer(),
"Confirmed Visits": this.visitsManager.getConfirmedVisitCirclesLayer()
};
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
// Add visits buttons after calendar button to position them below
this.visitsManager.addDrawerButton();
// Initialize Live Map Handler
this.initializeLiveMapHandler();
@ -1184,48 +1183,35 @@ export default class extends BaseController {
}
}
initializeTopRightButtons() {
// Add all top-right buttons in the correct order:
// 1. Select Area, 2. Add Visit, 3. Open Calendar, 4. Open Drawer
// Note: Layer control is added separately and appears at the top
addTogglePanelButton() {
// Store reference to the controller instance for use in the control
const controller = this;
this.topRightControls = addTopRightButtons(
this.map,
{
onSelectArea: () => this.visitsManager.toggleSelectionMode(),
// onAddVisit is intentionally null - the add_visit_controller will attach its handler
onAddVisit: null,
onToggleCalendar: () => this.toggleRightPanel(),
onToggleDrawer: () => this.visitsManager.toggleDrawer()
},
this.userTheme
);
const TogglePanelControl = L.Control.extend({
onAdd: function(map) {
const button = L.DomUtil.create('button', 'toggle-panel-button');
button.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 2v4" />
<path d="M16 2v4" />
<path d="M21 14V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8" />
<path d="M3 10h18" />
<path d="m16 20 2 2 4-4" />
</svg>
`;
// Style the button with theme-aware styling
applyThemeToButton(button, controller.userTheme);
button.style.width = '48px';
button.style.height = '48px';
button.style.borderRadius = '4px';
button.style.padding = '0';
button.style.display = 'flex';
button.style.alignItems = 'center';
button.style.justifyContent = 'center';
// Disable map interactions when clicking the button
L.DomEvent.disableClickPropagation(button);
// Toggle panel on button click
L.DomEvent.on(button, 'click', () => {
controller.toggleRightPanel();
});
return button;
}
});
// Add the control to the map
this.map.addControl(new TogglePanelControl({ position: 'topright' }));
// Add CSS for selection button active state (needed by visits manager)
if (!document.getElementById('selection-tool-active-style')) {
const style = document.createElement('style');
style.id = 'selection-tool-active-style';
style.textContent = `
#selection-tool-button.active {
border: 2px dashed #3388ff !important;
box-shadow: 0 0 8px rgba(51, 136, 255, 0.5) !important;
}
`;
document.head.appendChild(style);
}
}
shouldShowTracksSelector() {

View file

@ -125,14 +125,16 @@ export function showFlashMessage(type, message) {
if (!flashContainer) {
flashContainer = document.createElement('div');
flashContainer.id = 'flash-messages';
flashContainer.className = 'fixed top-5 right-5 flex flex-col-reverse gap-2 z-50';
// Use z-[9999] to ensure flash messages appear above navbar (z-50)
flashContainer.className = 'fixed top-20 right-5 flex flex-col-reverse gap-2';
flashContainer.style.zIndex = '9999';
document.body.appendChild(flashContainer);
}
// Create the flash message div
const flashDiv = document.createElement('div');
flashDiv.setAttribute('data-controller', 'removals');
flashDiv.className = `flex items-center justify-between ${classesForFlash(type)} py-3 px-5 rounded-lg z-50`;
flashDiv.className = `flex items-center justify-between ${classesForFlash(type)} py-3 px-5 rounded-lg shadow-lg`;
// Create the message div
const messageDiv = document.createElement('div');

View file

@ -0,0 +1,193 @@
// Map control buttons and utilities
// This file contains all button controls that are positioned on the top-right corner of the map
import L from "leaflet";
import { applyThemeToButton } from "./theme_utils";
/**
* Creates a standardized button element for map controls
* @param {String} className - CSS class name for the button
* @param {String} svgIcon - SVG icon HTML
* @param {String} title - Button title/tooltip
* @param {String} userTheme - User's theme preference ('dark' or 'light')
* @param {Function} onClickCallback - Callback function to execute when button is clicked
* @returns {HTMLElement} Button element
*/
function createStandardButton(className, svgIcon, title, userTheme, onClickCallback) {
const button = L.DomUtil.create('button', className);
button.innerHTML = svgIcon;
button.title = title;
// Apply standard button styling
applyThemeToButton(button, userTheme);
button.style.width = '48px';
button.style.height = '48px';
button.style.borderRadius = '4px';
button.style.padding = '0';
button.style.display = 'flex';
button.style.alignItems = 'center';
button.style.justifyContent = 'center';
button.style.fontSize = '18px';
button.style.transition = 'all 0.2s ease';
// Disable map interactions when clicking the button
L.DomEvent.disableClickPropagation(button);
// Attach click handler if provided
// Note: Some buttons (like Add Visit) have their handlers attached separately
if (onClickCallback && typeof onClickCallback === 'function') {
L.DomEvent.on(button, 'click', () => {
onClickCallback(button);
});
}
return button;
}
/**
* Creates a "Toggle Panel" button control for the map
* @param {Function} onClickCallback - Callback function to execute when button is clicked
* @param {String} userTheme - User's theme preference ('dark' or 'light')
* @returns {L.Control} Leaflet control instance
*/
export function createTogglePanelControl(onClickCallback, userTheme = 'dark') {
const TogglePanelControl = L.Control.extend({
onAdd: function(map) {
const svgIcon = `
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 2v4" />
<path d="M16 2v4" />
<path d="M21 14V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8" />
<path d="M3 10h18" />
<path d="m16 20 2 2 4-4" />
</svg>
`;
return createStandardButton('toggle-panel-button', svgIcon, 'Toggle Panel', userTheme, onClickCallback);
}
});
return TogglePanelControl;
}
/**
* Creates a "Visits Drawer" button control for the map
* @param {Function} onClickCallback - Callback function to execute when button is clicked
* @param {String} userTheme - User's theme preference ('dark' or 'light')
* @returns {L.Control} Leaflet control instance
*/
export function createVisitsDrawerControl(onClickCallback, userTheme = 'dark') {
const DrawerControl = L.Control.extend({
onAdd: function(map) {
const svgIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-right-open-icon lucide-panel-right-open"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m10 15-3-3 3-3"/></svg>';
return createStandardButton('leaflet-control-button drawer-button', svgIcon, 'Toggle Visits Drawer', userTheme, onClickCallback);
}
});
return DrawerControl;
}
/**
* Creates an "Area Selection" button control for the map
* @param {Function} onClickCallback - Callback function to execute when button is clicked
* @param {String} userTheme - User's theme preference ('dark' or 'light')
* @returns {L.Control} Leaflet control instance
*/
export function createAreaSelectionControl(onClickCallback, userTheme = 'dark') {
const SelectionControl = L.Control.extend({
onAdd: function(map) {
const svgIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-dashed-mouse-pointer-icon lucide-square-dashed-mouse-pointer"><path d="M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"/><path d="M5 3a2 2 0 0 0-2 2"/><path d="M19 3a2 2 0 0 1 2 2"/><path d="M5 21a2 2 0 0 1-2-2"/><path d="M9 3h1"/><path d="M9 21h2"/><path d="M14 3h1"/><path d="M3 9v1"/><path d="M21 9v2"/><path d="M3 14v1"/></svg>';
const button = createStandardButton('leaflet-bar leaflet-control leaflet-control-custom', svgIcon, 'Select Area', userTheme, onClickCallback);
button.id = 'selection-tool-button';
return button;
}
});
return SelectionControl;
}
/**
* Creates an "Add Visit" button control for the map
* @param {Function} onClickCallback - Callback function to execute when button is clicked
* @param {String} userTheme - User's theme preference ('dark' or 'light')
* @returns {L.Control} Leaflet control instance
*/
export function createAddVisitControl(onClickCallback, userTheme = 'dark') {
const AddVisitControl = L.Control.extend({
onAdd: function(map) {
const svgIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin-check-icon lucide-map-pin-check"><path d="M19.43 12.935c.357-.967.57-1.955.57-2.935a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32.197 32.197 0 0 0 .813-.728"/><circle cx="12" cy="10" r="3"/><path d="m16 18 2 2 4-4"/></svg>';
return createStandardButton('leaflet-control-button add-visit-button', svgIcon, 'Add a visit', userTheme, onClickCallback);
}
});
return AddVisitControl;
}
/**
* Adds all top-right corner buttons to the map in the correct order
* Order: 1. Select Area, 2. Add Visit, 3. Open Calendar, 4. Open Drawer
* Note: Layer control is added separately by Leaflet and appears at the top
*
* @param {Object} map - Leaflet map instance
* @param {Object} callbacks - Object containing callback functions for each button
* @param {Function} callbacks.onSelectArea - Callback for select area button
* @param {Function} callbacks.onAddVisit - Callback for add visit button
* @param {Function} callbacks.onToggleCalendar - Callback for toggle calendar/panel button
* @param {Function} callbacks.onToggleDrawer - Callback for toggle drawer button
* @param {String} userTheme - User's theme preference ('dark' or 'light')
* @returns {Object} Object containing references to all created controls
*/
export function addTopRightButtons(map, callbacks, userTheme = 'dark') {
const controls = {};
// 1. Select Area button
if (callbacks.onSelectArea) {
const SelectionControl = createAreaSelectionControl(callbacks.onSelectArea, userTheme);
controls.selectionControl = new SelectionControl({ position: 'topright' });
map.addControl(controls.selectionControl);
}
// 2. Add Visit button
// Note: Button is always created, callback is optional (add_visit_controller attaches its own handler)
const AddVisitControl = createAddVisitControl(callbacks.onAddVisit, userTheme);
controls.addVisitControl = new AddVisitControl({ position: 'topright' });
map.addControl(controls.addVisitControl);
// 3. Open Calendar (Toggle Panel) button
if (callbacks.onToggleCalendar) {
const TogglePanelControl = createTogglePanelControl(callbacks.onToggleCalendar, userTheme);
controls.togglePanelControl = new TogglePanelControl({ position: 'topright' });
map.addControl(controls.togglePanelControl);
}
// 4. Open Drawer button
if (callbacks.onToggleDrawer) {
const DrawerControl = createVisitsDrawerControl(callbacks.onToggleDrawer, userTheme);
controls.drawerControl = new DrawerControl({ position: 'topright' });
map.addControl(controls.drawerControl);
}
return controls;
}
/**
* Updates the Add Visit button to show active state
* @param {HTMLElement} button - The button element to update
*/
export function setAddVisitButtonActive(button) {
if (!button) return;
button.style.backgroundColor = '#dc3545';
button.style.color = 'white';
button.innerHTML = '✕';
}
/**
* Updates the Add Visit button to show inactive/default state
* @param {HTMLElement} button - The button element to update
* @param {String} userTheme - User's theme preference ('dark' or 'light')
*/
export function setAddVisitButtonInactive(button, userTheme = 'dark') {
if (!button) return;
applyThemeToButton(button, userTheme);
button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin-check-icon lucide-map-pin-check"><path d="M19.43 12.935c.357-.967.57-1.955.57-2.935a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32.197 32.197 0 0 0 .813-.728"/><circle cx="12" cy="10" r="3"/><path d="m16 18 2 2 4-4"/></svg>';
}

View file

@ -1,6 +1,5 @@
import L from "leaflet";
import { showFlashMessage } from "./helpers";
import { applyThemeToButton } from "./theme_utils";
/**
* Manages visits functionality including displaying, fetching, and interacting with visits
@ -65,76 +64,14 @@ export class VisitsManager {
}
/**
* Adds a button to toggle the visits drawer
* Note: Drawer and selection buttons are now added centrally via addTopRightButtons()
* in maps_controller.js to ensure correct button ordering.
*
* The methods below are kept for backwards compatibility but are no longer called
* during initialization. Button callbacks are wired directly in maps_controller.js:
* - onSelectArea -> this.toggleSelectionMode()
* - onToggleDrawer -> this.toggleDrawer()
*/
addDrawerButton() {
const DrawerControl = L.Control.extend({
onAdd: (map) => {
const button = L.DomUtil.create('button', 'leaflet-control-button drawer-button');
button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-right-open-icon lucide-panel-right-open"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/><path d="m10 15-3-3 3-3"/></svg>'; // Left arrow icon
// Style the button with theme-aware styling
applyThemeToButton(button, this.userTheme);
button.style.width = '48px';
button.style.height = '48px';
button.style.borderRadius = '4px';
button.style.padding = '0';
button.style.display = 'flex';
button.style.alignItems = 'center';
button.style.justifyContent = 'center';
button.style.fontSize = '18px';
L.DomEvent.disableClickPropagation(button);
L.DomEvent.on(button, 'click', () => {
this.toggleDrawer();
});
return button;
}
});
this.map.addControl(new DrawerControl({ position: 'topright' }));
// Add the selection tool button
this.addSelectionButton();
}
/**
* Adds a button to enable/disable the area selection tool
*/
addSelectionButton() {
const SelectionControl = L.Control.extend({
onAdd: (map) => {
const button = L.DomUtil.create('button', 'leaflet-bar leaflet-control leaflet-control-custom');
button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-dashed-mouse-pointer-icon lucide-square-dashed-mouse-pointer"><path d="M12.034 12.681a.498.498 0 0 1 .647-.647l9 3.5a.5.5 0 0 1-.033.943l-3.444 1.068a1 1 0 0 0-.66.66l-1.067 3.443a.5.5 0 0 1-.943.033z"/><path d="M5 3a2 2 0 0 0-2 2"/><path d="M19 3a2 2 0 0 1 2 2"/><path d="M5 21a2 2 0 0 1-2-2"/><path d="M9 3h1"/><path d="M9 21h2"/><path d="M14 3h1"/><path d="M3 9v1"/><path d="M21 9v2"/><path d="M3 14v1"/></svg>';
button.title = 'Select Area';
button.id = 'selection-tool-button';
// Style the button with theme-aware styling
applyThemeToButton(button, this.userTheme);
button.style.width = '48px';
button.style.height = '48px';
button.style.borderRadius = '4px';
button.style.padding = '0';
button.style.display = 'flex';
button.style.alignItems = 'center';
button.style.justifyContent = 'center';
button.style.fontSize = '18px';
button.onclick = () => this.toggleSelectionMode();
return button;
}
});
new SelectionControl({ position: 'topright' }).addTo(this.map);
// Add CSS for selection button active state
const style = document.createElement('style');
style.textContent = `
#selection-tool-button.active {
border: 2px dashed #3388ff !important;
box-shadow: 0 0 8px rgba(51, 136, 255, 0.5) !important;
}
`;
document.head.appendChild(style);
}
/**
* Toggles the area selection mode