mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-09 08:47:11 -05:00
Update places layer to use Leaflet.Control.Layers.Tree for hierarchical layer control
This commit is contained in:
parent
602975eeaa
commit
e8e7bcc91b
17 changed files with 3459 additions and 224 deletions
171
LAYER_CONTROL_UPGRADE.md
Normal file
171
LAYER_CONTROL_UPGRADE.md
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
# Layer Control Upgrade - Leaflet.Control.Layers.Tree
|
||||
|
||||
## Summary
|
||||
|
||||
Successfully installed and integrated the `Leaflet.Control.Layers.Tree` plugin to replace the standard Leaflet layer control with a hierarchical tree-based control that better organizes map layers and styles.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. Installation
|
||||
|
||||
- **Plugin**: Installed `leaflet.control.layers.tree` via importmap
|
||||
- **CSS**: Added plugin CSS file at `app/assets/stylesheets/leaflet.control.layers.tree.css`
|
||||
|
||||
### 2. Maps Controller Updates
|
||||
|
||||
#### File: `app/javascript/controllers/maps_controller.js`
|
||||
|
||||
**Import Changes:**
|
||||
- Added import for `leaflet.control.layers.tree`
|
||||
- Removed import for `createPlacesControl` (now integrated into tree control)
|
||||
|
||||
**Initialization Changes:**
|
||||
- Removed standalone Places control initialization
|
||||
- Added `this.userTags` property to store user tags for places filtering
|
||||
- Updated layer control initialization to use `createTreeLayerControl()`
|
||||
|
||||
**New Methods:**
|
||||
|
||||
1. **`createTreeLayerControl(additionalLayers = {})`**
|
||||
- Creates a hierarchical tree structure for map layers
|
||||
- Organizes layers into two main groups:
|
||||
- **Map Styles**: All available base map layers
|
||||
- **Layers**: All overlay layers with nested groups
|
||||
- Supports dynamic additional layers (e.g., Family Members)
|
||||
|
||||
Structure:
|
||||
```
|
||||
+ Map Styles
|
||||
- OpenStreetMap
|
||||
- OpenStreetMap.HOT
|
||||
- ...
|
||||
+ Layers
|
||||
- Points
|
||||
- Routes
|
||||
- Tracks
|
||||
- Heatmap
|
||||
- Fog of War
|
||||
- Scratch map
|
||||
- Areas
|
||||
- Photos
|
||||
+ Visits
|
||||
- Suggested
|
||||
- Confirmed
|
||||
+ Places
|
||||
- All
|
||||
- Untagged
|
||||
- (each tag with icon)
|
||||
```
|
||||
|
||||
**Updated Methods:**
|
||||
- **`updateLayerControl()`**: Simplified to just recreate the tree control with additional layers
|
||||
- Updated all layer control recreations throughout the file to use `createTreeLayerControl()`
|
||||
|
||||
### 3. Places Manager Updates
|
||||
|
||||
#### File: `app/javascript/maps/places.js`
|
||||
|
||||
**New Methods:**
|
||||
|
||||
1. **`createFilteredLayer(tagIds)`**
|
||||
- Creates a layer group for filtered places
|
||||
- Returns a layer that loads places when added to the map
|
||||
- Supports tag-based and untagged filtering
|
||||
|
||||
2. **`loadPlacesIntoLayer(layer, tagIds)`**
|
||||
- Loads places into a specific layer with tag filtering
|
||||
- Handles API calls with tag_ids or untagged parameters
|
||||
- Creates markers using existing `createPlaceMarker()` method
|
||||
|
||||
## Features
|
||||
|
||||
### Hierarchical Organization
|
||||
- Map styles and layers are now clearly separated
|
||||
- Related layers are grouped together (Visits, Places)
|
||||
- Easy to expand/collapse sections
|
||||
|
||||
### Places Layer Integration
|
||||
- No longer needs a separate control
|
||||
- All places filters are now in the tree control
|
||||
- Each tag gets its own layer in the tree
|
||||
- Places group has "All", "Untagged", and individual tag layers regardless of tags
|
||||
- "Untagged" shows only places without tags
|
||||
|
||||
### Dynamic Layer Support
|
||||
- Family Members layer can be added dynamically
|
||||
- Additional layers can be easily integrated
|
||||
- Maintains compatibility with existing layer management
|
||||
|
||||
### Improved User Experience
|
||||
- Cleaner UI with collapsible sections
|
||||
- Better organization of many layers
|
||||
- Consistent interface for all layer types
|
||||
- Select All checkbox for grouped layers (Visits, Places)
|
||||
|
||||
## API Changes
|
||||
|
||||
### Places API
|
||||
The Places API now supports an `untagged` parameter:
|
||||
- `GET /api/v1/places?untagged=true` - Returns only untagged places
|
||||
- `GET /api/v1/places?tag_ids=1,2,3` - Returns places with specified tags
|
||||
|
||||
## Testing Recommendations
|
||||
|
||||
1. **Basic Functionality**
|
||||
- Verify all map styles load correctly
|
||||
- Test all overlay layers (Points, Routes, Tracks, etc.)
|
||||
- Confirm layer visibility persists correctly
|
||||
|
||||
2. **Places Integration**
|
||||
- Test "All" layer shows all places
|
||||
- Verify "Untagged" layer shows only untagged places
|
||||
- Test individual tag layers show correct places
|
||||
- Confirm places load when layer is enabled
|
||||
|
||||
3. **Visits Integration**
|
||||
- Test Suggested and Confirmed visits layers
|
||||
- Verify visits load correctly when enabled
|
||||
|
||||
4. **Family Members**
|
||||
- Test Family Members layer appears when family is available
|
||||
- Verify layer updates when family locations change
|
||||
|
||||
5. **Layer State Persistence**
|
||||
- Verify enabled layers are saved to user settings
|
||||
- Confirm layer state is restored on page load
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### Removed Components
|
||||
- Standalone Places control button (📍)
|
||||
- `createPlacesControl` function no longer used in maps_controller
|
||||
|
||||
### Behavioral Changes
|
||||
- Places layer is no longer managed by a separate control
|
||||
- All places filtering is now done through the layer control
|
||||
- Places markers are created on-demand when layer is enabled
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Layer Icons**: Add custom icons for each layer type
|
||||
2. **Layer Counts**: Show number of items in each layer
|
||||
3. **Custom Styling**: Theme the tree control to match app theme
|
||||
4. **Layer Search**: Add search functionality for finding layers
|
||||
5. **Layer Presets**: Allow saving custom layer combinations
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. `app/javascript/controllers/maps_controller.js` - Main map controller
|
||||
2. `app/javascript/maps/places.js` - Places manager with new filtering methods
|
||||
3. `config/importmap.rb` - Added tree control import (via bin/importmap)
|
||||
4. `app/assets/stylesheets/leaflet.control.layers.tree.css` - Plugin CSS
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If needed, to rollback:
|
||||
1. Remove `import "leaflet.control.layers.tree"` from maps_controller.js
|
||||
2. Restore `import { createPlacesControl }` from places_control
|
||||
3. Revert `createTreeLayerControl()` to `L.control.layers()`
|
||||
4. Restore Places control initialization
|
||||
5. Remove `leaflet.control.layers.tree` from importmap
|
||||
6. Remove CSS file
|
||||
194
TESTING_CHECKLIST.md
Normal file
194
TESTING_CHECKLIST.md
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
# Layer Control Upgrade - Testing Checklist
|
||||
|
||||
## Pre-Testing Setup
|
||||
|
||||
1. **Start the development server**
|
||||
```bash
|
||||
bin/dev
|
||||
```
|
||||
|
||||
2. **Clear browser cache** to ensure new JavaScript and CSS are loaded
|
||||
|
||||
3. **Log in** to the application with demo credentials or your account
|
||||
|
||||
4. **Navigate to the Map page** (`/map`)
|
||||
|
||||
## Visual Verification
|
||||
|
||||
- [ ] Layer control appears in the top-right corner
|
||||
- [ ] Layer control shows a hierarchical tree structure (not flat list)
|
||||
- [ ] Control has two main sections: "Map Styles" and "Layers"
|
||||
- [ ] Sections can be expanded/collapsed
|
||||
- [ ] No standalone Places control button (📍) is visible
|
||||
|
||||
## Map Styles Testing
|
||||
|
||||
- [ ] Expand "Map Styles" section
|
||||
- [ ] All map styles are listed (OpenStreetMap, OpenStreetMap.HOT, etc.)
|
||||
- [ ] Selecting a different style changes the base map
|
||||
- [ ] Only one map style can be selected at a time
|
||||
- [ ] Selected style is indicated with a radio button
|
||||
|
||||
## Layers Testing
|
||||
|
||||
### Basic Layers
|
||||
- [ ] Expand "Layers" section
|
||||
- [ ] All basic layers are present:
|
||||
- [ ] Points
|
||||
- [ ] Routes
|
||||
- [ ] Tracks
|
||||
- [ ] Heatmap
|
||||
- [ ] Fog of War
|
||||
- [ ] Scratch map
|
||||
- [ ] Areas
|
||||
- [ ] Photos
|
||||
|
||||
- [ ] Toggle each layer on/off
|
||||
- [ ] Verify each layer displays correctly when enabled
|
||||
- [ ] Multiple layers can be enabled simultaneously
|
||||
|
||||
### Visits Group
|
||||
- [ ] Expand "Visits" section
|
||||
- [ ] Two sub-layers are present:
|
||||
- [ ] Suggested
|
||||
- [ ] Confirmed
|
||||
- [ ] Enable "Suggested" - suggested visits appear on map
|
||||
- [ ] Enable "Confirmed" - confirmed visits appear on map
|
||||
- [ ] Disable both - no visits visible on map
|
||||
- [ ] Select All checkbox works for Visits group
|
||||
|
||||
### Places Group
|
||||
- [ ] Expand "Places" section
|
||||
- [ ] At least these options are present:
|
||||
- [ ] Places (top-level checkbox)
|
||||
- [ ] Untagged
|
||||
- [ ] (Individual tags if any exist)
|
||||
|
||||
**Testing "Places (top-level checkbox)":**
|
||||
- [ ] Enable "Places (top-level checkbox)"
|
||||
- [ ] All places appear on map regardless of tags
|
||||
- [ ] Place markers are clickable
|
||||
- [ ] Place popups show correct information
|
||||
|
||||
**Testing "Untagged":**
|
||||
- [ ] Enable "Untagged" (disable "Places (top-level checkbox)" first)
|
||||
- [ ] Only places without tags appear
|
||||
- [ ] Verify by checking places that have tags don't appear
|
||||
|
||||
**Testing Individual Tags:**
|
||||
(If you have tags created)
|
||||
- [ ] Each tag appears as a separate layer
|
||||
- [ ] Tag icon is displayed before tag name
|
||||
- [ ] Enable a tag layer
|
||||
- [ ] Only places with that tag appear
|
||||
- [ ] Multiple tag layers can be enabled simultaneously
|
||||
- [ ] Select All checkbox works for Places group
|
||||
|
||||
### Family Members (if applicable)
|
||||
- [ ] If in a family, "Family Members" layer appears
|
||||
- [ ] Enable Family Members layer
|
||||
- [ ] Family member locations appear on map
|
||||
- [ ] Family member markers are distinguishable from own markers
|
||||
|
||||
## Functional Testing
|
||||
|
||||
### Layer Persistence
|
||||
- [ ] Enable several layers (e.g., Points, Routes, Suggested Visits, Places (top-level checkbox))
|
||||
- [ ] Refresh the page
|
||||
- [ ] Verify enabled layers remain enabled after refresh
|
||||
- [ ] Verify disabled layers remain disabled after refresh
|
||||
|
||||
### Places API Integration
|
||||
- [ ] Open browser console (F12)
|
||||
- [ ] Enable "Network" tab
|
||||
- [ ] Enable "Untagged" places layer
|
||||
- [ ] Verify API call: `GET /api/v1/places?api_key=...&untagged=true`
|
||||
- [ ] Enable a tag layer
|
||||
- [ ] Verify API call: `GET /api/v1/places?api_key=...&tag_ids=<tag_id>`
|
||||
- [ ] Verify no JavaScript errors in console
|
||||
|
||||
### Layer Interaction
|
||||
- [ ] Enable Routes layer
|
||||
- [ ] Click on a route segment
|
||||
- [ ] Verify route details popup appears
|
||||
- [ ] Enable Places "Places (top-level checkbox)" layer
|
||||
- [ ] Click on a place marker
|
||||
- [ ] Verify place details popup appears
|
||||
- [ ] Verify layers don't interfere with each other
|
||||
|
||||
### Performance
|
||||
- [ ] Enable all layers simultaneously
|
||||
- [ ] Map remains responsive
|
||||
- [ ] No significant lag when toggling layers
|
||||
- [ ] No memory leaks (check browser dev tools)
|
||||
|
||||
## Edge Cases
|
||||
|
||||
### No Tags Scenario
|
||||
- [ ] If no tags exist, Places section should show:
|
||||
- [ ] Places (top-level checkbox)
|
||||
- [ ] Untagged
|
||||
- [ ] No error in console
|
||||
|
||||
### No Places Scenario
|
||||
- [ ] Disable all place layers
|
||||
- [ ] Enable "Untagged"
|
||||
- [ ] Verify appropriate message or empty state
|
||||
- [ ] No errors in console
|
||||
|
||||
### No Family Scenario
|
||||
- [ ] If not in a family, "Family Members" layer shouldn't appear
|
||||
- [ ] No errors in console
|
||||
|
||||
## Regression Testing
|
||||
|
||||
### Existing Functionality
|
||||
- [ ] Routes/Tracks selector still works (if visible with `tracks_debug=true`)
|
||||
- [ ] Settings panel still works
|
||||
- [ ] Calendar panel still works
|
||||
- [ ] Visit selection tool still works
|
||||
- [ ] Add visit button still works
|
||||
|
||||
### Other Controllers
|
||||
- [ ] Family members controller still works (if applicable)
|
||||
- [ ] Photo markers still load correctly
|
||||
- [ ] Area drawing still works
|
||||
- [ ] Fog of war updates correctly
|
||||
|
||||
## Mobile Testing (if applicable)
|
||||
|
||||
- [ ] Layer control is accessible on mobile
|
||||
- [ ] Tree structure expands/collapses on tap
|
||||
- [ ] Layers can be toggled on mobile
|
||||
- [ ] No layout issues on small screens
|
||||
|
||||
## Error Scenarios
|
||||
|
||||
- [ ] Disconnect internet, try to load a layer that requires API call
|
||||
- [ ] Verify appropriate error handling
|
||||
- [ ] Verify user gets feedback about the failure
|
||||
- [ ] Verify app doesn't crash
|
||||
|
||||
## Console Checks
|
||||
|
||||
Throughout all testing, monitor the browser console for:
|
||||
- [ ] No JavaScript errors
|
||||
- [ ] No unexpected warnings
|
||||
- [ ] No failed API requests (except during error scenario testing)
|
||||
- [ ] Appropriate log messages for debugging
|
||||
|
||||
## Sign-off
|
||||
|
||||
- [ ] All critical tests pass
|
||||
- [ ] Any failures are documented
|
||||
- [ ] Ready for production deployment
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
Record any issues, unexpected behavior, or suggestions for improvement:
|
||||
|
||||
```
|
||||
[Your notes here]
|
||||
```
|
||||
File diff suppressed because one or more lines are too long
33
app/assets/stylesheets/leaflet.control.layers.tree.css
Normal file
33
app/assets/stylesheets/leaflet.control.layers.tree.css
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
.leaflet-control-layers-toggle.leaflet-layerstree-named-toggle {
|
||||
margin: 2px 5px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.leaflet-layerstree-header input {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.leaflet-layerstree-header label {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.leaflet-layerstree-header-pointer,
|
||||
.leaflet-layerstree-expand-collapse {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.leaflet-layerstree-children {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.leaflet-layerstree-children-nopad {
|
||||
padding-left: 0px;
|
||||
}
|
||||
|
||||
.leaflet-layerstree-hide,
|
||||
.leaflet-layerstree-nevershow {
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ module Api
|
|||
def index
|
||||
@places = policy_scope(Place).includes(:tags, :visits)
|
||||
@places = @places.with_tags(params[:tag_ids]) if params[:tag_ids].present?
|
||||
@places = @places.without_tags if params[:untagged] == 'true'
|
||||
|
||||
render json: @places.map { |place| serialize_place(place) }
|
||||
end
|
||||
|
|
|
|||
23
app/javascript/controllers/icon_picker_controller.js
Normal file
23
app/javascript/controllers/icon_picker_controller.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["input", "button"]
|
||||
|
||||
select(event) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
const button = event.currentTarget
|
||||
const icon = button.dataset.icon
|
||||
|
||||
if (this.hasInputTarget && icon) {
|
||||
this.inputTarget.value = icon
|
||||
|
||||
// Close the dropdown by removing focus
|
||||
const activeElement = document.activeElement
|
||||
if (activeElement) {
|
||||
activeElement.blur()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { Controller } from "@hotwired/stimulus";
|
||||
import L from "leaflet";
|
||||
import "leaflet.heat";
|
||||
import "leaflet.control.layers.tree";
|
||||
import consumer from "../channels/consumer";
|
||||
|
||||
import { createMarkersArray } from "../maps/markers";
|
||||
|
|
@ -45,7 +46,11 @@ 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";
|
||||
import {
|
||||
addTopRightButtons,
|
||||
setCreatePlaceButtonActive,
|
||||
setCreatePlaceButtonInactive
|
||||
} from "../maps/map_controls";
|
||||
|
||||
export default class extends BaseController {
|
||||
static targets = ["container"];
|
||||
|
|
@ -218,6 +223,14 @@ export default class extends BaseController {
|
|||
this.placesManager = new PlacesManager(this.map, this.apiKey);
|
||||
this.placesManager.initialize();
|
||||
|
||||
// Parse user tags for places layer control
|
||||
try {
|
||||
this.userTags = this.element.dataset.user_tags ? JSON.parse(this.element.dataset.user_tags) : [];
|
||||
} catch (error) {
|
||||
console.error('Error parsing user tags:', error);
|
||||
this.userTags = [];
|
||||
}
|
||||
|
||||
// Expose maps controller globally for family integration
|
||||
window.mapsController = this;
|
||||
|
||||
|
|
@ -234,9 +247,6 @@ export default class extends BaseController {
|
|||
}
|
||||
this.switchRouteMode('routes', true);
|
||||
|
||||
// Initialize layers based on settings
|
||||
this.initializeLayersFromSettings();
|
||||
|
||||
// Listen for Family Members layer becoming ready
|
||||
this.setupFamilyLayerListener();
|
||||
|
||||
|
|
@ -252,22 +262,12 @@ export default class extends BaseController {
|
|||
// 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(),
|
||||
"Places": this.placesManager.placesLayer
|
||||
};
|
||||
// Initialize tree-based layer control (must be before initializeLayersFromSettings)
|
||||
this.layerControl = this.createTreeLayerControl();
|
||||
this.map.addControl(this.layerControl);
|
||||
|
||||
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
|
||||
// Initialize layers based on settings (must be after tree control creation)
|
||||
this.initializeLayersFromSettings();
|
||||
|
||||
|
||||
// Initialize Live Map Handler
|
||||
|
|
@ -447,6 +447,134 @@ export default class extends BaseController {
|
|||
return maps;
|
||||
}
|
||||
|
||||
createTreeLayerControl(additionalLayers = {}) {
|
||||
// Build base maps tree structure
|
||||
const baseMapsTree = {
|
||||
label: 'Map Styles',
|
||||
children: []
|
||||
};
|
||||
|
||||
const maps = this.baseMaps();
|
||||
Object.entries(maps).forEach(([name, layer]) => {
|
||||
baseMapsTree.children.push({
|
||||
label: name,
|
||||
layer: layer
|
||||
});
|
||||
});
|
||||
|
||||
// Build places subtree with tags
|
||||
// Store filtered layers for later restoration
|
||||
if (!this.placesFilteredLayers) {
|
||||
this.placesFilteredLayers = {};
|
||||
}
|
||||
|
||||
// Create Untagged layer
|
||||
const untaggedLayer = this.placesManager?.createFilteredLayer([]) || L.layerGroup();
|
||||
this.placesFilteredLayers['Untagged'] = untaggedLayer;
|
||||
|
||||
const placesChildren = [
|
||||
{
|
||||
label: 'Untagged',
|
||||
layer: untaggedLayer
|
||||
}
|
||||
];
|
||||
|
||||
// Add individual tag layers
|
||||
if (this.userTags && this.userTags.length > 0) {
|
||||
this.userTags.forEach(tag => {
|
||||
const icon = tag.icon || '📍';
|
||||
const label = `${icon} ${tag.name}`;
|
||||
const tagLayer = this.placesManager?.createFilteredLayer([tag.id]) || L.layerGroup();
|
||||
this.placesFilteredLayers[label] = tagLayer;
|
||||
placesChildren.push({
|
||||
label: label,
|
||||
layer: tagLayer
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Build visits subtree
|
||||
const visitsChildren = [
|
||||
{
|
||||
label: 'Suggested',
|
||||
layer: this.visitsManager?.getVisitCirclesLayer() || L.layerGroup()
|
||||
},
|
||||
{
|
||||
label: 'Confirmed',
|
||||
layer: this.visitsManager?.getConfirmedVisitCirclesLayer() || L.layerGroup()
|
||||
}
|
||||
];
|
||||
|
||||
// Build the overlays tree structure
|
||||
const overlaysTree = {
|
||||
label: 'Layers',
|
||||
selectAllCheckbox: false,
|
||||
children: [
|
||||
{
|
||||
label: 'Points',
|
||||
layer: this.markersLayer
|
||||
},
|
||||
{
|
||||
label: 'Routes',
|
||||
layer: this.polylinesLayer
|
||||
},
|
||||
{
|
||||
label: 'Tracks',
|
||||
layer: this.tracksLayer
|
||||
},
|
||||
{
|
||||
label: 'Heatmap',
|
||||
layer: this.heatmapLayer
|
||||
},
|
||||
{
|
||||
label: 'Fog of War',
|
||||
layer: this.fogOverlay
|
||||
},
|
||||
{
|
||||
label: 'Scratch map',
|
||||
layer: this.scratchLayerManager?.getLayer() || L.layerGroup()
|
||||
},
|
||||
{
|
||||
label: 'Areas',
|
||||
layer: this.areasLayer
|
||||
},
|
||||
{
|
||||
label: 'Photos',
|
||||
layer: this.photoMarkers
|
||||
},
|
||||
{
|
||||
label: 'Visits',
|
||||
selectAllCheckbox: true,
|
||||
children: visitsChildren
|
||||
},
|
||||
{
|
||||
label: 'Places',
|
||||
selectAllCheckbox: true,
|
||||
children: placesChildren
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Add Family Members layer if available
|
||||
if (additionalLayers['Family Members']) {
|
||||
overlaysTree.children.push({
|
||||
label: 'Family Members',
|
||||
layer: additionalLayers['Family Members']
|
||||
});
|
||||
}
|
||||
|
||||
// Create the tree control
|
||||
return L.control.layers.tree(
|
||||
baseMapsTree,
|
||||
overlaysTree,
|
||||
{
|
||||
namedToggle: false,
|
||||
collapsed: true,
|
||||
position: 'topright'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
removeEventListeners() {
|
||||
document.removeEventListener('click', this.handleDeleteClick);
|
||||
}
|
||||
|
|
@ -572,6 +700,15 @@ export default class extends BaseController {
|
|||
this.fogOverlay = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for place creation events to disable creation mode
|
||||
document.addEventListener('place:created', () => {
|
||||
this.disablePlaceCreationMode();
|
||||
});
|
||||
|
||||
document.addEventListener('place:create:cancelled', () => {
|
||||
this.disablePlaceCreationMode();
|
||||
});
|
||||
}
|
||||
|
||||
updatePreferredBaseLayer(selectedLayerName) {
|
||||
|
|
@ -599,32 +736,23 @@ export default class extends BaseController {
|
|||
|
||||
saveEnabledLayers() {
|
||||
const enabledLayers = [];
|
||||
const layerNames = [
|
||||
'Points', 'Routes', 'Tracks', 'Heatmap', 'Fog of War',
|
||||
'Scratch map', 'Areas', 'Photos', 'Suggested Visits', 'Confirmed Visits',
|
||||
'Family Members'
|
||||
];
|
||||
|
||||
const controlsLayer = {
|
||||
'Points': this.markersLayer,
|
||||
'Routes': this.polylinesLayer,
|
||||
'Tracks': this.tracksLayer,
|
||||
'Heatmap': this.heatmapLayer,
|
||||
'Fog of War': this.fogOverlay,
|
||||
'Scratch map': this.scratchLayerManager?.getLayer(),
|
||||
'Areas': this.areasLayer,
|
||||
'Photos': this.photoMarkers,
|
||||
'Suggested Visits': this.visitsManager?.getVisitCirclesLayer(),
|
||||
'Confirmed Visits': this.visitsManager?.getConfirmedVisitCirclesLayer(),
|
||||
'Family Members': window.familyMembersController?.familyMarkersLayer
|
||||
};
|
||||
|
||||
layerNames.forEach(name => {
|
||||
const layer = controlsLayer[name];
|
||||
if (layer && this.map.hasLayer(layer)) {
|
||||
enabledLayers.push(name);
|
||||
}
|
||||
});
|
||||
// Get all checked inputs from the tree control
|
||||
const layerControl = document.querySelector('.leaflet-control-layers');
|
||||
if (layerControl) {
|
||||
const inputs = layerControl.querySelectorAll('input[type="checkbox"]:checked');
|
||||
inputs.forEach(input => {
|
||||
// Get the label text for this checkbox
|
||||
const label = input.closest('label') || input.nextElementSibling;
|
||||
if (label) {
|
||||
const layerName = label.textContent.trim();
|
||||
// Skip group headers that might have checkboxes
|
||||
if (layerName && !layerName.includes('Map Styles') && !layerName.includes('Layers')) {
|
||||
enabledLayers.push(layerName);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetch('/api/v1/settings', {
|
||||
method: 'PATCH',
|
||||
|
|
@ -642,7 +770,7 @@ export default class extends BaseController {
|
|||
.then((data) => {
|
||||
if (data.status === 'success') {
|
||||
console.log('Enabled layers saved:', enabledLayers);
|
||||
showFlashMessage('notice', 'Map layer preferences saved');
|
||||
// showFlashMessage('notice', 'Map layer preferences saved');
|
||||
} else {
|
||||
console.error('Failed to save enabled layers:', data.message);
|
||||
showFlashMessage('error', `Failed to save layer preferences: ${data.message}`);
|
||||
|
|
@ -699,16 +827,8 @@ export default class extends BaseController {
|
|||
// Update the layer control
|
||||
if (this.layerControl) {
|
||||
this.map.removeControl(this.layerControl);
|
||||
const controlsLayer = {
|
||||
Points: this.markersLayer || L.layerGroup(),
|
||||
Routes: this.polylinesLayer || L.layerGroup(),
|
||||
Heatmap: this.heatmapLayer || L.layerGroup(),
|
||||
"Fog of War": this.fogOverlay,
|
||||
"Scratch map": this.scratchLayerManager?.getLayer() || L.layerGroup(),
|
||||
Areas: this.areasLayer || L.layerGroup(),
|
||||
Photos: this.photoMarkers || L.layerGroup()
|
||||
};
|
||||
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
|
||||
this.layerControl = this.createTreeLayerControl();
|
||||
this.map.addControl(this.layerControl);
|
||||
}
|
||||
|
||||
// Update heatmap
|
||||
|
|
@ -1280,7 +1400,8 @@ export default class extends BaseController {
|
|||
};
|
||||
|
||||
// Re-add the layer control in the same position
|
||||
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
|
||||
this.layerControl = this.createTreeLayerControl();
|
||||
this.map.addControl(this.layerControl);
|
||||
|
||||
// Restore layer visibility states
|
||||
Object.entries(layerStates).forEach(([name, wasVisible]) => {
|
||||
|
|
@ -1321,7 +1442,7 @@ 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
|
||||
// 1. Select Area, 2. Add Visit, 3. Create Place, 4. Open Calendar, 5. Open Drawer
|
||||
// Note: Layer control is added separately and appears at the top
|
||||
|
||||
this.topRightControls = addTopRightButtons(
|
||||
|
|
@ -1330,6 +1451,7 @@ export default class extends BaseController {
|
|||
onSelectArea: () => this.visitsManager.toggleSelectionMode(),
|
||||
// onAddVisit is intentionally null - the add_visit_controller will attach its handler
|
||||
onAddVisit: null,
|
||||
onCreatePlace: () => this.togglePlaceCreationMode(),
|
||||
onToggleCalendar: () => this.toggleRightPanel(),
|
||||
onToggleDrawer: () => this.visitsManager.toggleDrawer()
|
||||
},
|
||||
|
|
@ -1534,7 +1656,9 @@ export default class extends BaseController {
|
|||
'Photos': this.photoMarkers,
|
||||
'Suggested Visits': this.visitsManager?.getVisitCirclesLayer(),
|
||||
'Confirmed Visits': this.visitsManager?.getConfirmedVisitCirclesLayer(),
|
||||
'Family Members': window.familyMembersController?.familyMarkersLayer
|
||||
'Family Members': window.familyMembersController?.familyMarkersLayer,
|
||||
// Add Places filtered layers
|
||||
...this.placesFilteredLayers || {}
|
||||
};
|
||||
|
||||
// Apply saved layer preferences
|
||||
|
|
@ -1606,6 +1730,38 @@ export default class extends BaseController {
|
|||
console.log(`Disabled layer: ${name}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Update the tree control checkboxes to reflect the layer states
|
||||
// Wait a bit for the tree control to be fully initialized
|
||||
setTimeout(() => {
|
||||
this.updateTreeControlCheckboxes(enabledLayers);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
updateTreeControlCheckboxes(enabledLayers) {
|
||||
const layerControl = document.querySelector('.leaflet-control-layers');
|
||||
if (!layerControl) {
|
||||
console.log('Layer control not found, skipping checkbox update');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find and check/uncheck all layer checkboxes based on saved state
|
||||
const inputs = layerControl.querySelectorAll('input[type="checkbox"]');
|
||||
inputs.forEach(input => {
|
||||
const label = input.closest('label') || input.nextElementSibling;
|
||||
if (label) {
|
||||
const layerName = label.textContent.trim();
|
||||
const shouldBeEnabled = enabledLayers.includes(layerName);
|
||||
|
||||
// Skip group headers that might have checkboxes
|
||||
if (layerName && !layerName.includes('Map Styles') && !layerName.includes('Layers')) {
|
||||
if (shouldBeEnabled !== input.checked) {
|
||||
input.checked = shouldBeEnabled;
|
||||
console.log(`Updated checkbox for ${layerName}: ${shouldBeEnabled}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupFamilyLayerListener() {
|
||||
|
|
@ -2155,71 +2311,12 @@ export default class extends BaseController {
|
|||
updateLayerControl(additionalLayers = {}) {
|
||||
if (!this.layerControl) return;
|
||||
|
||||
// Store which base and overlay layers are currently visible
|
||||
const overlayStates = {};
|
||||
let activeBaseLayer = null;
|
||||
let activeBaseLayerName = null;
|
||||
|
||||
if (this.layerControl._layers) {
|
||||
Object.values(this.layerControl._layers).forEach(layerObj => {
|
||||
if (layerObj.overlay && layerObj.layer) {
|
||||
// Store overlay layer states
|
||||
overlayStates[layerObj.name] = this.map.hasLayer(layerObj.layer);
|
||||
} else if (!layerObj.overlay && this.map.hasLayer(layerObj.layer)) {
|
||||
// Store the currently active base layer
|
||||
activeBaseLayer = layerObj.layer;
|
||||
activeBaseLayerName = layerObj.name;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove existing layer control
|
||||
this.map.removeControl(this.layerControl);
|
||||
|
||||
// Create base controls layer object
|
||||
const baseControlsLayer = {
|
||||
Points: this.markersLayer || L.layerGroup(),
|
||||
Routes: this.polylinesLayer || L.layerGroup(),
|
||||
Tracks: this.tracksLayer || L.layerGroup(),
|
||||
Heatmap: this.heatmapLayer || L.heatLayer([]),
|
||||
"Fog of War": this.fogOverlay,
|
||||
"Scratch map": this.scratchLayerManager?.getLayer() || L.layerGroup(),
|
||||
Areas: this.areasLayer || L.layerGroup(),
|
||||
Photos: this.photoMarkers || L.layerGroup(),
|
||||
"Suggested Visits": this.visitsManager?.getVisitCirclesLayer() || L.layerGroup(),
|
||||
"Confirmed Visits": this.visitsManager?.getConfirmedVisitCirclesLayer() || L.layerGroup()
|
||||
};
|
||||
|
||||
// Merge with additional layers (like family members)
|
||||
const controlsLayer = { ...baseControlsLayer, ...additionalLayers };
|
||||
|
||||
// Get base maps and re-add the layer control
|
||||
const baseMaps = this.baseMaps();
|
||||
this.layerControl = L.control.layers(baseMaps, controlsLayer).addTo(this.map);
|
||||
|
||||
// Restore the active base layer if we had one
|
||||
if (activeBaseLayer && activeBaseLayerName) {
|
||||
console.log(`Restoring base layer: ${activeBaseLayerName}`);
|
||||
// Make sure the base layer is added to the map
|
||||
if (!this.map.hasLayer(activeBaseLayer)) {
|
||||
activeBaseLayer.addTo(this.map);
|
||||
}
|
||||
} else {
|
||||
// If no active base layer was found, ensure we have a default one
|
||||
console.log('No active base layer found, adding default');
|
||||
const defaultBaseLayer = Object.values(baseMaps)[0];
|
||||
if (defaultBaseLayer && !this.map.hasLayer(defaultBaseLayer)) {
|
||||
defaultBaseLayer.addTo(this.map);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore overlay layer visibility states
|
||||
Object.entries(overlayStates).forEach(([name, wasVisible]) => {
|
||||
const layer = controlsLayer[name];
|
||||
if (layer && wasVisible && !this.map.hasLayer(layer)) {
|
||||
layer.addTo(this.map);
|
||||
}
|
||||
});
|
||||
// Re-add the layer control with additional layers
|
||||
this.layerControl = this.createTreeLayerControl(additionalLayers);
|
||||
this.map.addControl(this.layerControl);
|
||||
}
|
||||
|
||||
togglePlaceCreationMode() {
|
||||
|
|
@ -2234,20 +2331,33 @@ export default class extends BaseController {
|
|||
// Disable creation mode
|
||||
this.placesManager.disableCreationMode();
|
||||
if (button) {
|
||||
button.classList.remove('btn-error');
|
||||
button.classList.add('btn-success');
|
||||
button.title = 'Click to create a place on the map';
|
||||
setCreatePlaceButtonInactive(button, this.userTheme);
|
||||
button.setAttribute('data-tip', 'Create a place');
|
||||
}
|
||||
} else {
|
||||
// Enable creation mode
|
||||
this.placesManager.enableCreationMode();
|
||||
if (button) {
|
||||
button.classList.remove('btn-success');
|
||||
button.classList.add('btn-error');
|
||||
button.title = 'Click map to place marker (click again to cancel)';
|
||||
setCreatePlaceButtonActive(button);
|
||||
button.setAttribute('data-tip', 'Click map to place marker (click to cancel)');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
disablePlaceCreationMode() {
|
||||
if (!this.placesManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only disable if currently in creation mode
|
||||
if (this.placesManager.creationMode) {
|
||||
this.placesManager.disableCreationMode();
|
||||
|
||||
const button = document.getElementById('create-place-btn');
|
||||
if (button) {
|
||||
setCreatePlaceButtonInactive(button, this.userTheme);
|
||||
button.setAttribute('data-tip', 'Create a place');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2362
app/javascript/controllers/maps_controller.js.bak
Normal file
2362
app/javascript/controllers/maps_controller.js.bak
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -31,11 +31,14 @@ function createStandardButton(className, svgIcon, title, userTheme, onClickCallb
|
|||
|
||||
// Disable map interactions when clicking the button
|
||||
L.DomEvent.disableClickPropagation(button);
|
||||
L.DomEvent.disableScrollPropagation(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', () => {
|
||||
L.DomEvent.on(button, 'click', (e) => {
|
||||
L.DomEvent.stopPropagation(e);
|
||||
L.DomEvent.preventDefault(e);
|
||||
onClickCallback(button);
|
||||
});
|
||||
}
|
||||
|
|
@ -121,15 +124,35 @@ export function createAddVisitControl(onClickCallback, userTheme = 'dark') {
|
|||
return AddVisitControl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a "Create Place" 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 createCreatePlaceControl(onClickCallback, userTheme = 'dark') {
|
||||
const CreatePlaceControl = 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-plus"><path d="M19.914 11.105A7.298 7.298 0 0 0 20 10a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32 32 0 0 0 .824-.738"/><circle cx="12" cy="10" r="3"/><path d="M16 18h6"/><path d="M19 15v6"/></svg>';
|
||||
const button = createStandardButton('leaflet-control-button create-place-button', svgIcon, 'Create a place', userTheme, onClickCallback);
|
||||
button.id = 'create-place-btn';
|
||||
return button;
|
||||
}
|
||||
});
|
||||
|
||||
return CreatePlaceControl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Order: 1. Select Area, 2. Add Visit, 3. Create Place, 4. Open Calendar, 5. 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.onCreatePlace - Callback for create place 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')
|
||||
|
|
@ -151,14 +174,21 @@ export function addTopRightButtons(map, callbacks, userTheme = 'dark') {
|
|||
controls.addVisitControl = new AddVisitControl({ position: 'topright' });
|
||||
map.addControl(controls.addVisitControl);
|
||||
|
||||
// 3. Open Calendar (Toggle Panel) button
|
||||
// 3. Create Place button
|
||||
if (callbacks.onCreatePlace) {
|
||||
const CreatePlaceControl = createCreatePlaceControl(callbacks.onCreatePlace, userTheme);
|
||||
controls.createPlaceControl = new CreatePlaceControl({ position: 'topright' });
|
||||
map.addControl(controls.createPlaceControl);
|
||||
}
|
||||
|
||||
// 4. 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
|
||||
// 5. Open Drawer button
|
||||
if (callbacks.onToggleDrawer) {
|
||||
const DrawerControl = createVisitsDrawerControl(callbacks.onToggleDrawer, userTheme);
|
||||
controls.drawerControl = new DrawerControl({ position: 'topright' });
|
||||
|
|
@ -191,3 +221,31 @@ export function setAddVisitButtonInactive(button, userTheme = 'dark') {
|
|||
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>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the Create Place button to show active state
|
||||
* @param {HTMLElement} button - The button element to update
|
||||
*/
|
||||
export function setCreatePlaceButtonActive(button) {
|
||||
if (!button) return;
|
||||
|
||||
button.style.backgroundColor = '#22c55e';
|
||||
button.style.color = 'white';
|
||||
button.style.border = '2px solid #16a34a';
|
||||
button.style.boxShadow = '0 0 12px rgba(34, 197, 94, 0.5)';
|
||||
button.innerHTML = '✕';
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the Create Place 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 setCreatePlaceButtonInactive(button, userTheme = 'dark') {
|
||||
if (!button) return;
|
||||
|
||||
applyThemeToButton(button, userTheme);
|
||||
button.style.border = '';
|
||||
button.style.boxShadow = '';
|
||||
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-plus"><path d="M19.914 11.105A7.298 7.298 0 0 0 20 10a8 8 0 0 0-16 0c0 4.993 5.539 10.193 7.399 11.799a1 1 0 0 0 1.202 0 32 32 0 0 0 .824-.738"/><circle cx="12" cy="10" r="3"/><path d="M16 18h6"/><path d="M19 15v6"/></svg>';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Handles displaying user places with tag icons and colors on the map
|
||||
|
||||
import L from 'leaflet';
|
||||
import { showFlashMessage } from './helpers';
|
||||
|
||||
export class PlacesManager {
|
||||
constructor(map, apiKey) {
|
||||
|
|
@ -13,12 +14,45 @@ export class PlacesManager {
|
|||
this.selectedTags = new Set();
|
||||
this.creationMode = false;
|
||||
this.creationMarker = null;
|
||||
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
this.placesLayer = L.layerGroup().addTo(this.map);
|
||||
this.placesLayer = L.layerGroup();
|
||||
|
||||
// Add event listener to reload places when layer is added to map
|
||||
this.placesLayer.on('add', () => {
|
||||
this.loadPlaces();
|
||||
});
|
||||
|
||||
console.log("[PlacesManager] Initializing, loading places for first time...");
|
||||
await this.loadPlaces();
|
||||
this.setupMapClickHandler();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Refresh places when a new place is created
|
||||
document.addEventListener('place:created', async (event) => {
|
||||
const { place } = event.detail;
|
||||
|
||||
// Show success message
|
||||
showFlashMessage('success', `Place "${place.name}" created successfully!`);
|
||||
|
||||
// Add the new place to the main places layer
|
||||
await this.refreshPlaces();
|
||||
|
||||
// Refresh all filtered layers that are currently on the map
|
||||
this.map.eachLayer((layer) => {
|
||||
if (layer._tagIds !== undefined) {
|
||||
// This is a filtered layer, reload it
|
||||
this.loadPlacesIntoLayer(layer, layer._tagIds);
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure the main Places layer is visible
|
||||
this.ensurePlacesLayerVisible();
|
||||
});
|
||||
}
|
||||
|
||||
async loadPlaces(tagIds = null) {
|
||||
|
|
@ -28,6 +62,7 @@ export class PlacesManager {
|
|||
tagIds.forEach(id => url.searchParams.append('tag_ids[]', id));
|
||||
}
|
||||
|
||||
console.log("[PlacesManager] loadPlaces called, fetching from:", url.toString());
|
||||
const response = await fetch(url, {
|
||||
headers: { 'Authorization': `Bearer ${this.apiKey}` }
|
||||
});
|
||||
|
|
@ -59,7 +94,7 @@ export class PlacesManager {
|
|||
if (!place.latitude || !place.longitude) return null;
|
||||
|
||||
const icon = this.createPlaceIcon(place);
|
||||
const marker = L.marker([place.latitude, place.longitude], { icon });
|
||||
const marker = L.marker([place.latitude, place.longitude], { icon, placeId: place.id });
|
||||
|
||||
const popupContent = this.createPopupContent(place);
|
||||
marker.bindPopup(popupContent);
|
||||
|
|
@ -129,7 +164,7 @@ export class PlacesManager {
|
|||
this.map.on('popupopen', (e) => {
|
||||
const popup = e.popup;
|
||||
const deleteBtn = popup.getElement()?.querySelector('[data-action="delete-place"]');
|
||||
|
||||
|
||||
if (deleteBtn) {
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
const placeId = deleteBtn.dataset.placeId;
|
||||
|
|
@ -176,18 +211,30 @@ export class PlacesManager {
|
|||
|
||||
if (!response.ok) throw new Error('Failed to delete place');
|
||||
|
||||
// Remove marker and reload
|
||||
// Remove marker from main layer
|
||||
if (this.markers[placeId]) {
|
||||
this.placesLayer.removeLayer(this.markers[placeId]);
|
||||
delete this.markers[placeId];
|
||||
}
|
||||
|
||||
// Remove from all layers on the map (including filtered layers)
|
||||
this.map.eachLayer((layer) => {
|
||||
if (layer instanceof L.LayerGroup) {
|
||||
layer.eachLayer((marker) => {
|
||||
if (marker.options && marker.options.placeId === parseInt(placeId)) {
|
||||
layer.removeLayer(marker);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Remove from places array
|
||||
this.places = this.places.filter(p => p.id !== parseInt(placeId));
|
||||
|
||||
this.showNotification('Place deleted successfully', 'success');
|
||||
showFlashMessage('success', 'Place deleted successfully');
|
||||
} catch (error) {
|
||||
console.error('Error deleting place:', error);
|
||||
this.showNotification('Failed to delete place', 'error');
|
||||
showFlashMessage('error', 'Failed to delete place');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -212,11 +259,98 @@ export class PlacesManager {
|
|||
this.loadPlaces(tagIds.length > 0 ? tagIds : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filtered layer for tree control
|
||||
* Returns a layer group that will be populated with filtered places
|
||||
*/
|
||||
createFilteredLayer(tagIds) {
|
||||
const filteredLayer = L.layerGroup();
|
||||
|
||||
// Store tag IDs for this layer
|
||||
filteredLayer._tagIds = tagIds;
|
||||
|
||||
// Add event listener to load places when layer is added to map
|
||||
filteredLayer.on('add', () => {
|
||||
console.log(`[PlacesManager] Filtered layer added to map, tagIds:`, tagIds);
|
||||
this.loadPlacesIntoLayer(filteredLayer, tagIds);
|
||||
});
|
||||
|
||||
console.log(`[PlacesManager] Created filtered layer for tagIds:`, tagIds);
|
||||
return filteredLayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load places into a specific layer with tag filtering
|
||||
*/
|
||||
async loadPlacesIntoLayer(layer, tagIds) {
|
||||
try {
|
||||
console.log(`[PlacesManager] loadPlacesIntoLayer called with tagIds:`, tagIds);
|
||||
let url = `/api/v1/places?api_key=${this.apiKey}`;
|
||||
|
||||
if (Array.isArray(tagIds) && tagIds.length > 0) {
|
||||
// Specific tags requested
|
||||
url += `&tag_ids=${tagIds.join(',')}`;
|
||||
} else if (Array.isArray(tagIds) && tagIds.length === 0) {
|
||||
// Empty array means untagged places only
|
||||
url += '&untagged=true';
|
||||
}
|
||||
|
||||
console.log(`[PlacesManager] Fetching from URL:`, url);
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
console.log(`[PlacesManager] Received ${data.length} places for tagIds:`, tagIds);
|
||||
|
||||
// Clear existing markers in this layer
|
||||
layer.clearLayers();
|
||||
|
||||
// Add markers to this layer
|
||||
data.forEach(place => {
|
||||
const marker = this.createPlaceMarker(place);
|
||||
layer.addLayer(marker);
|
||||
});
|
||||
|
||||
console.log(`[PlacesManager] Added ${data.length} markers to layer`);
|
||||
} catch (error) {
|
||||
console.error('Error loading places into layer:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshPlaces() {
|
||||
const tagIds = this.selectedTags.size > 0 ? Array.from(this.selectedTags) : null;
|
||||
await this.loadPlaces(tagIds);
|
||||
}
|
||||
|
||||
ensurePlacesLayerVisible() {
|
||||
// Check if the main places layer is already on the map
|
||||
if (this.map.hasLayer(this.placesLayer)) {
|
||||
console.log('Places layer already visible');
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to find and enable the Places checkbox in the tree control
|
||||
const layerControl = document.querySelector('.leaflet-control-layers');
|
||||
if (!layerControl) {
|
||||
console.log('Layer control not found, adding places layer directly');
|
||||
this.map.addLayer(this.placesLayer);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the Places checkbox and enable it
|
||||
setTimeout(() => {
|
||||
const inputs = layerControl.querySelectorAll('input[type="checkbox"]');
|
||||
inputs.forEach(input => {
|
||||
const label = input.closest('label') || input.nextElementSibling;
|
||||
if (label && label.textContent.trim() === 'Places') {
|
||||
if (!input.checked) {
|
||||
input.checked = true;
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
console.log('Enabled Places layer in tree control');
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
show() {
|
||||
if (this.placesLayer) {
|
||||
this.map.addLayer(this.placesLayer);
|
||||
|
|
|
|||
217
app/javascript/maps/places_control.js
Normal file
217
app/javascript/maps/places_control.js
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import L from 'leaflet';
|
||||
import { applyThemeToPanel } from './theme_utils';
|
||||
|
||||
/**
|
||||
* Custom Leaflet control for managing Places layer visibility and filtering
|
||||
*/
|
||||
export function createPlacesControl(placesManager, tags, userTheme = 'dark') {
|
||||
return L.Control.extend({
|
||||
options: {
|
||||
position: 'topright'
|
||||
},
|
||||
|
||||
onAdd: function(map) {
|
||||
this.placesManager = placesManager;
|
||||
this.tags = tags || [];
|
||||
this.userTheme = userTheme;
|
||||
this.activeFilters = new Set(); // Track which tags are active
|
||||
this.showUntagged = false;
|
||||
this.placesEnabled = false;
|
||||
|
||||
// Create main container
|
||||
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-places');
|
||||
|
||||
// Prevent map interactions when clicking the control
|
||||
L.DomEvent.disableClickPropagation(container);
|
||||
L.DomEvent.disableScrollPropagation(container);
|
||||
|
||||
// Create toggle button
|
||||
this.button = L.DomUtil.create('a', 'leaflet-control-places-button', container);
|
||||
this.button.href = '#';
|
||||
this.button.title = 'Places Layer';
|
||||
this.button.innerHTML = '📍';
|
||||
this.button.style.fontSize = '20px';
|
||||
this.button.style.width = '34px';
|
||||
this.button.style.height = '34px';
|
||||
this.button.style.lineHeight = '30px';
|
||||
this.button.style.textAlign = 'center';
|
||||
this.button.style.textDecoration = 'none';
|
||||
|
||||
// Create panel (hidden by default)
|
||||
this.panel = L.DomUtil.create('div', 'leaflet-control-places-panel', container);
|
||||
this.panel.style.display = 'none';
|
||||
this.panel.style.marginTop = '5px';
|
||||
this.panel.style.minWidth = '200px';
|
||||
this.panel.style.maxWidth = '280px';
|
||||
this.panel.style.maxHeight = '400px';
|
||||
this.panel.style.overflowY = 'auto';
|
||||
this.panel.style.padding = '10px';
|
||||
this.panel.style.borderRadius = '4px';
|
||||
this.panel.style.boxShadow = '0 2px 8px rgba(0,0,0,0.3)';
|
||||
|
||||
// Apply theme to panel
|
||||
applyThemeToPanel(this.panel, this.userTheme);
|
||||
|
||||
// Build panel content
|
||||
this.buildPanelContent();
|
||||
|
||||
// Toggle panel on button click
|
||||
L.DomEvent.on(this.button, 'click', (e) => {
|
||||
L.DomEvent.preventDefault(e);
|
||||
this.togglePanel();
|
||||
});
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
buildPanelContent: function() {
|
||||
const html = `
|
||||
<div style="margin-bottom: 10px; font-weight: bold; font-size: 14px; border-bottom: 1px solid rgba(128,128,128,0.3); padding-bottom: 8px;">
|
||||
📍 Places Layer
|
||||
</div>
|
||||
|
||||
<!-- All Places Toggle -->
|
||||
<label style="display: flex; align-items: center; padding: 6px; cursor: pointer; border-radius: 4px; margin-bottom: 4px;"
|
||||
class="places-control-item"
|
||||
onmouseover="this.style.backgroundColor='rgba(128,128,128,0.2)'"
|
||||
onmouseout="this.style.backgroundColor='transparent'">
|
||||
<input type="checkbox"
|
||||
data-filter="all"
|
||||
style="margin-right: 8px; cursor: pointer;"
|
||||
${this.placesEnabled ? 'checked' : ''}>
|
||||
<span style="font-weight: bold;">Show All Places</span>
|
||||
</label>
|
||||
|
||||
<!-- Untagged Places Toggle -->
|
||||
<label style="display: flex; align-items: center; padding: 6px; cursor: pointer; border-radius: 4px; margin-bottom: 8px;"
|
||||
class="places-control-item"
|
||||
onmouseover="this.style.backgroundColor='rgba(128,128,128,0.2)'"
|
||||
onmouseout="this.style.backgroundColor='transparent'">
|
||||
<input type="checkbox"
|
||||
data-filter="untagged"
|
||||
style="margin-right: 8px; cursor: pointer;"
|
||||
${this.showUntagged ? 'checked' : ''}>
|
||||
<span>Untagged Places</span>
|
||||
</label>
|
||||
|
||||
${this.tags.length > 0 ? `
|
||||
<div style="border-top: 1px solid rgba(128,128,128,0.3); padding-top: 8px; margin-top: 8px;">
|
||||
<div style="font-size: 12px; font-weight: bold; margin-bottom: 6px; opacity: 0.7;">
|
||||
FILTER BY TAG
|
||||
</div>
|
||||
<div style="max-height: 250px; overflow-y: auto; margin-right: -5px; padding-right: 5px;">
|
||||
${this.tags.map(tag => `
|
||||
<label style="display: flex; align-items: center; padding: 6px; cursor: pointer; border-radius: 4px; margin-bottom: 2px;"
|
||||
class="places-control-item"
|
||||
onmouseover="this.style.backgroundColor='rgba(128,128,128,0.2)'"
|
||||
onmouseout="this.style.backgroundColor='transparent'">
|
||||
<input type="checkbox"
|
||||
data-filter="tag"
|
||||
data-tag-id="${tag.id}"
|
||||
style="margin-right: 8px; cursor: pointer;"
|
||||
${this.activeFilters.has(tag.id) ? 'checked' : ''}>
|
||||
<span style="font-size: 18px; margin-right: 6px;">${tag.icon || '📍'}</span>
|
||||
<span style="flex: 1;">${this.escapeHtml(tag.name)}</span>
|
||||
${tag.color ? `<span style="width: 12px; height: 12px; border-radius: 50%; background-color: ${tag.color}; margin-left: 4px;"></span>` : ''}
|
||||
</label>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
` : '<div style="font-size: 12px; opacity: 0.6; padding: 8px; text-align: center;">No tags created yet</div>'}
|
||||
`;
|
||||
|
||||
this.panel.innerHTML = html;
|
||||
|
||||
// Add event listeners to checkboxes
|
||||
const checkboxes = this.panel.querySelectorAll('input[type="checkbox"]');
|
||||
checkboxes.forEach(cb => {
|
||||
L.DomEvent.on(cb, 'change', (e) => {
|
||||
this.handleFilterChange(e.target);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
handleFilterChange: function(checkbox) {
|
||||
const filterType = checkbox.dataset.filter;
|
||||
|
||||
if (filterType === 'all') {
|
||||
this.placesEnabled = checkbox.checked;
|
||||
|
||||
if (checkbox.checked) {
|
||||
// Show places layer
|
||||
this.placesManager.placesLayer.addTo(this.placesManager.map);
|
||||
this.applyCurrentFilters();
|
||||
} else {
|
||||
// Hide places layer
|
||||
this.placesManager.map.removeLayer(this.placesManager.placesLayer);
|
||||
// Uncheck all other filters
|
||||
this.activeFilters.clear();
|
||||
this.showUntagged = false;
|
||||
this.buildPanelContent();
|
||||
}
|
||||
} else if (filterType === 'untagged') {
|
||||
this.showUntagged = checkbox.checked;
|
||||
this.applyCurrentFilters();
|
||||
} else if (filterType === 'tag') {
|
||||
const tagId = parseInt(checkbox.dataset.tagId);
|
||||
|
||||
if (checkbox.checked) {
|
||||
this.activeFilters.add(tagId);
|
||||
} else {
|
||||
this.activeFilters.delete(tagId);
|
||||
}
|
||||
|
||||
this.applyCurrentFilters();
|
||||
}
|
||||
|
||||
// Update button appearance
|
||||
this.updateButtonState();
|
||||
},
|
||||
|
||||
applyCurrentFilters: function() {
|
||||
if (!this.placesEnabled) return;
|
||||
|
||||
// If no specific filters, show all places
|
||||
if (this.activeFilters.size === 0 && !this.showUntagged) {
|
||||
this.placesManager.filterByTags(null);
|
||||
} else {
|
||||
// Build filter criteria
|
||||
const tagIds = Array.from(this.activeFilters);
|
||||
|
||||
// For now, just filter by tags
|
||||
// TODO: Add support for untagged filter in PlacesManager
|
||||
if (tagIds.length > 0) {
|
||||
this.placesManager.filterByTags(tagIds);
|
||||
} else if (this.showUntagged) {
|
||||
// Show only untagged places
|
||||
this.placesManager.filterByTags([]);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateButtonState: function() {
|
||||
if (this.placesEnabled) {
|
||||
this.button.style.backgroundColor = '#4CAF50';
|
||||
this.button.style.color = 'white';
|
||||
} else {
|
||||
this.button.style.backgroundColor = '';
|
||||
this.button.style.color = '';
|
||||
}
|
||||
},
|
||||
|
||||
togglePanel: function() {
|
||||
if (this.panel.style.display === 'none') {
|
||||
this.panel.style.display = 'block';
|
||||
} else {
|
||||
this.panel.style.display = 'none';
|
||||
}
|
||||
},
|
||||
|
||||
escapeHtml: function(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ module Taggable
|
|||
has_many :tags, through: :taggings
|
||||
|
||||
scope :with_tags, ->(tag_ids) { joins(:taggings).where(taggings: { tag_id: tag_ids }).distinct }
|
||||
scope :without_tags, -> { left_joins(:taggings).where(taggings: { id: nil }) }
|
||||
scope :tagged_with, ->(tag_name, user) {
|
||||
joins(:tags).where(tags: { name: tag_name, user: user }).distinct
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@
|
|||
data-points_number="<%= @points_number %>"
|
||||
data-timezone="<%= Rails.configuration.time_zone %>"
|
||||
data-features='<%= @features.to_json.html_safe %>'
|
||||
data-user_tags='<%= current_user.tags.ordered.select(:id, :name, :icon, :color).as_json.to_json.html_safe %>'
|
||||
data-family-members-features-value='<%= @features.to_json.html_safe %>'
|
||||
data-family-members-user-theme-value="<%= current_user&.theme || 'dark' %>">
|
||||
<div data-maps-target="container" class="w-full h-full">
|
||||
|
|
@ -99,66 +100,5 @@
|
|||
|
||||
<%= render 'map/settings_modals' %>
|
||||
|
||||
<!-- Places Control Buttons -->
|
||||
<div class="absolute top-4 left-4 z-[1001] flex flex-col gap-2">
|
||||
<!-- Create Place Button -->
|
||||
<button id="create-place-btn"
|
||||
class="btn btn-circle btn-success shadow-lg"
|
||||
onclick="window.mapsController?.togglePlaceCreationMode()"
|
||||
title="Click to create a place on the map">
|
||||
<%= icon 'map-pin-plus' %>
|
||||
</button>
|
||||
|
||||
<!-- Tag Filter Toggle Button -->
|
||||
<% if current_user.tags.any? %>
|
||||
<button class="btn btn-circle btn-primary shadow-lg"
|
||||
onclick="document.getElementById('places-tag-filter').classList.toggle('hidden')"
|
||||
title="Filter Places by Tags">
|
||||
<%#= icon 'filter' %>
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- Tag Filters Panel (Floating) -->
|
||||
<% if current_user.tags.any? %>
|
||||
<div id="places-tag-filter" class="absolute top-20 left-4 bg-base-100 rounded-lg shadow-xl p-4 max-w-xs z-[1000] hidden"
|
||||
data-controller="places-filter">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h3 class="font-semibold flex items-center gap-2">
|
||||
<%#= icon 'filter' %> Filter Places by Tags
|
||||
</h3>
|
||||
<button class="btn btn-ghost btn-xs btn-circle" onclick="document.getElementById('places-tag-filter').classList.add('hidden')">
|
||||
<%#= icon 'x' %>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 max-h-96 overflow-y-auto">
|
||||
<% current_user.tags.ordered.each do |tag| %>
|
||||
<label class="flex items-center gap-2 cursor-pointer hover:bg-base-200 p-2 rounded-lg transition-colors">
|
||||
<input type="checkbox"
|
||||
data-tag-id="<%= tag.id %>"
|
||||
data-action="change->places-filter#filterPlaces"
|
||||
class="checkbox checkbox-sm checkbox-primary">
|
||||
<span class="text-xl"><%= tag.icon %></span>
|
||||
<span class="text-sm font-medium flex-1"><%= tag.name %></span>
|
||||
<% if tag.color.present? %>
|
||||
<span class="w-3 h-3 rounded-full" style="background-color: <%= tag.color %>;"></span>
|
||||
<% end %>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 pt-3 border-t border-base-300">
|
||||
<button class="btn btn-sm btn-ghost w-full" data-action="click->places-filter#clearAll">
|
||||
Clear All Filters
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-base-content/70">
|
||||
<%= icon 'info', class: 'inline w-3 h-3' %> Select tags to filter places. Uncheck all to show all places.
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Include Place Creation Modal -->
|
||||
<%= render 'shared/place_creation_modal' %>
|
||||
|
|
|
|||
|
|
@ -17,19 +17,19 @@
|
|||
<%= f.text_field :name, class: "input input-bordered w-full", placeholder: "Home, Work, Restaurant..." %>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<div class="form-control" data-controller="icon-picker">
|
||||
<%= f.label :icon, class: "label" %>
|
||||
<div class="flex gap-2">
|
||||
<%= f.text_field :icon, class: "input input-bordered flex-1", placeholder: "🏠" %>
|
||||
<%= f.text_field :icon, class: "input input-bordered flex-1 text-2xl text-center cursor-pointer", placeholder: "🏠", readonly: true, data: { icon_picker_target: "input" } %>
|
||||
<div class="dropdown dropdown-end">
|
||||
<button type="button" tabindex="0" class="btn btn-outline">
|
||||
<label tabindex="0" class="btn btn-outline">
|
||||
Pick Icon
|
||||
</button>
|
||||
<div tabindex="0" class="dropdown-content card card-compact w-72 p-2 shadow bg-base-100 z-10">
|
||||
</label>
|
||||
<div tabindex="0" class="dropdown-content card card-compact w-72 p-2 shadow bg-base-100 z-[1]">
|
||||
<div class="card-body">
|
||||
<div class="grid grid-cols-6 gap-2" data-action="click->icon-picker#select">
|
||||
<% %w[📍 🏠 🏢 🍴 ☕ 🏨 🎭 🏛️ 🌳 ⛰️ 🏖️ 🎪 🏪 🏬 🏭 🏯 🏰 🗼 🗽 ⛪ 🕌 🛕 🕍 ⛩️].each do |emoji| %>
|
||||
<button type="button" class="btn btn-sm text-2xl hover:bg-base-200" data-icon="<%= emoji %>">
|
||||
<div class="grid grid-cols-6 gap-2">
|
||||
<% %w[📍 🏠 🏢 🍴 ☕ 🏨 🎭 🏛️ 🌳 ⛰️ 🏖️ 🎪 🏪 🏬 🏭 🏯 🏰 🗼 🗽 ⛪ 🕌 🛕 🕍 ⛩️ 🙋♂️].each do |emoji| %>
|
||||
<button type="button" class="btn btn-sm text-2xl hover:bg-base-200" data-icon="<%= emoji %>" data-action="click->icon-picker#select">
|
||||
<%= emoji %>
|
||||
</button>
|
||||
<% end %>
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<label class="label">
|
||||
<span class="label-text-alt">Click an emoji or paste any emoji</span>
|
||||
<span class="label-text-alt">Select an icon from the picker</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
|
@ -61,19 +61,3 @@
|
|||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const iconButtons = document.querySelectorAll('[data-icon]');
|
||||
const iconInput = document.querySelector('input[name="tag[icon]"]');
|
||||
|
||||
iconButtons.forEach(button => {
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (iconInput) {
|
||||
iconInput.value = button.dataset.icon;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ pin '@hotwired/stimulus', to: 'stimulus.min.js', preload: true
|
|||
pin '@hotwired/stimulus-loading', to: 'stimulus-loading.js', preload: true
|
||||
pin_all_from 'app/javascript/controllers', under: 'controllers'
|
||||
|
||||
pin 'leaflet' # @1.9.4
|
||||
pin "leaflet" # @1.9.4
|
||||
pin 'leaflet-providers' # @2.0.0
|
||||
pin 'chartkick', to: 'chartkick.js'
|
||||
pin 'Chart.bundle', to: 'Chart.bundle.js'
|
||||
|
|
@ -26,3 +26,4 @@ pin 'imports_channel', to: 'channels/imports_channel.js'
|
|||
pin 'family_locations_channel', to: 'channels/family_locations_channel.js'
|
||||
pin 'trix'
|
||||
pin '@rails/actiontext', to: 'actiontext.esm.js'
|
||||
pin "leaflet.control.layers.tree" # @1.2.0
|
||||
|
|
|
|||
4
vendor/javascript/leaflet.control.layers.tree.js
vendored
Normal file
4
vendor/javascript/leaflet.control.layers.tree.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
vendor/javascript/leaflet.js
vendored
2
vendor/javascript/leaflet.js
vendored
|
|
@ -1,3 +1,5 @@
|
|||
// leaflet@1.9.4 downloaded from https://ga.jspm.io/npm:leaflet@1.9.4/dist/leaflet-src.js
|
||||
|
||||
var t="undefined"!==typeof globalThis?globalThis:"undefined"!==typeof self?self:global;var e={};
|
||||
/* @preserve
|
||||
* Leaflet 1.9.4, a JS library for interactive maps. https://leafletjs.com
|
||||
|
|
|
|||
Loading…
Reference in a new issue