@mongoosejs/studio 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1515,7 +1515,9 @@ module.exports = {
1515
1515
  this.$toast.success('Chat thread created!');
1516
1516
  }
1517
1517
 
1518
+ const userChatMessageIndex = this.chatMessages.length;
1518
1519
  this.chatMessages.push({
1520
+ _id: Math.random().toString(36).substr(2, 9),
1519
1521
  content,
1520
1522
  role: 'user'
1521
1523
  });
@@ -1533,11 +1535,12 @@ module.exports = {
1533
1535
  if (event.chatMessage) {
1534
1536
  if (!userChatMessage) {
1535
1537
  userChatMessage = event.chatMessage;
1536
- } else if (!assistantChatMessage) {
1538
+ this.chatMessages.splice(userChatMessageIndex, 1, userChatMessage);
1539
+ } else {
1537
1540
  const assistantChatMessageIndex = this.chatMessages.indexOf(assistantChatMessage);
1538
1541
  assistantChatMessage = event.chatMessage;
1539
1542
  if (assistantChatMessageIndex !== -1) {
1540
- this.chatMessages[assistantChatMessageIndex] = assistantChatMessage;
1543
+ this.chatMessages.splice(assistantChatMessageIndex, 1, assistantChatMessage);
1541
1544
  } else {
1542
1545
  this.chatMessages.push(assistantChatMessage);
1543
1546
  }
@@ -1551,6 +1554,7 @@ module.exports = {
1551
1554
  } else if (event.textPart) {
1552
1555
  if (!assistantChatMessage) {
1553
1556
  assistantChatMessage = {
1557
+ _id: Math.random().toString(36).substr(2, 9),
1554
1558
  content: event.textPart,
1555
1559
  role: 'assistant'
1556
1560
  };
@@ -2713,7 +2717,7 @@ module.exports = app => app.component('detail-array', {
2713
2717
  (module) {
2714
2718
 
2715
2719
  "use strict";
2716
- module.exports = "<div class=\"w-full\">\n <pre class=\"w-full whitespace-pre-wrap break-words font-mono text-sm text-gray-700 m-0\">{{displayValue}}</pre>\n</div>";
2720
+ module.exports = "<div class=\"w-full\">\n <pre v-if=\"!isGeoJsonGeometry || !mapVisible\" class=\"w-full whitespace-pre-wrap break-words font-mono text-sm text-gray-700 m-0\">{{displayValue}}</pre>\n <div v-show=\"isGeoJsonGeometry && mapVisible\" class=\"mt-2 border border-gray-200 rounded relative\">\n <div ref=\"map\" class=\"h-64 w-full\" style=\"min-height: 256px; height: 256px;\"></div>\n <!-- Undo button below map -->\n <div v-if=\"isEditable && canUndo\" class=\"mt-2 flex justify-end\">\n <button\n @click=\"undoDelete\"\n class=\"text-xs px-3 py-1.5 bg-gray-600 text-white rounded hover:bg-gray-700 transition-colors\"\n title=\"Undo all changes\"\n >\n Undo\n </button>\n </div>\n </div>\n</div>\n";
2717
2721
 
2718
2722
  /***/ },
2719
2723
 
@@ -2726,10 +2730,63 @@ module.exports = "<div class=\"w-full\">\n <pre class=\"w-full whitespace-pre-w
2726
2730
  "use strict";
2727
2731
 
2728
2732
 
2733
+ /* global L */
2729
2734
  const template = __webpack_require__(/*! ./detail-default.html */ "./frontend/src/detail-default/detail-default.html");
2735
+ const appendCSS = __webpack_require__(/*! ../appendCSS */ "./frontend/src/appendCSS.js");
2736
+
2737
+ // Add CSS for polygon vertex markers and context menu
2738
+ appendCSS(`
2739
+ .polygon-vertex-marker {
2740
+ pointer-events: auto !important;
2741
+ }
2742
+ .polygon-vertex-marker > div {
2743
+ pointer-events: auto !important;
2744
+ }
2745
+ .leaflet-context-menu {
2746
+ position: absolute;
2747
+ background: white;
2748
+ border: 1px solid #ccc;
2749
+ border-radius: 4px;
2750
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
2751
+ z-index: 10000;
2752
+ min-width: 120px;
2753
+ padding: 4px 0;
2754
+ }
2755
+ .leaflet-context-menu-item {
2756
+ padding: 8px 16px;
2757
+ cursor: pointer;
2758
+ font-size: 14px;
2759
+ color: #333;
2760
+ }
2761
+ .leaflet-context-menu-item:hover {
2762
+ background-color: #f0f0f0;
2763
+ }
2764
+ .leaflet-context-menu-item.delete {
2765
+ color: #dc3545;
2766
+ }
2767
+ .leaflet-context-menu-item.delete:hover {
2768
+ background-color: #fee;
2769
+ }
2770
+ `);
2771
+
2730
2772
  module.exports = app => app.component('detail-default', {
2731
2773
  template: template,
2732
- props: ['value'],
2774
+ props: ['value', 'viewMode', 'onChange'],
2775
+ data() {
2776
+ return {
2777
+ mapVisible: false,
2778
+ mapInstance: null,
2779
+ mapLayer: null,
2780
+ draggableMarker: null,
2781
+ draggableMarkers: [], // For polygon vertices
2782
+ hasUnsavedChanges: false,
2783
+ currentEditedGeometry: null, // Track the current edited geometry state
2784
+ contextMenu: null, // Custom context menu element
2785
+ contextMenuMarker: null, // Marker that triggered context menu
2786
+ originalGeometry: null, // Store the original geometry when editing starts
2787
+ isCreatingMarkers: false // Guard against re-entrant marker creation
2788
+ };
2789
+ },
2733
2790
  computed: {
2734
2791
  displayValue() {
2735
2792
  if (this.value === null) {
@@ -2749,10 +2806,1022 @@ module.exports = app => app.component('detail-default', {
2749
2806
  } catch (err) {
2750
2807
  return String(this.value);
2751
2808
  }
2809
+ },
2810
+ isGeoJsonGeometry() {
2811
+ return this.value != null
2812
+ && typeof this.value === 'object'
2813
+ && !Array.isArray(this.value)
2814
+ && Object.prototype.hasOwnProperty.call(this.value, 'type')
2815
+ && Object.prototype.hasOwnProperty.call(this.value, 'coordinates');
2816
+ },
2817
+ isGeoJsonPoint() {
2818
+ return this.isGeoJsonGeometry && this.value.type === 'Point';
2819
+ },
2820
+ isGeoJsonPolygon() {
2821
+ return this.isGeoJsonGeometry && (this.value.type === 'Polygon' || this.value.type === 'MultiPolygon');
2822
+ },
2823
+ isMultiPolygon() {
2824
+ return this.isGeoJsonGeometry && this.value.type === 'MultiPolygon';
2825
+ },
2826
+ isEditable() {
2827
+ return (this.isGeoJsonPoint || this.isGeoJsonPolygon) && typeof this.onChange === 'function';
2828
+ },
2829
+ canUndo() {
2830
+ // Can undo if there are any changes from the original geometry
2831
+ return this.hasUnsavedChanges && this.originalGeometry != null;
2832
+ }
2833
+ },
2834
+ watch: {
2835
+ viewMode: {
2836
+ handler(newValue) {
2837
+ this.mapVisible = newValue === 'map';
2838
+ if (this.mapVisible) {
2839
+ this.$nextTick(() => {
2840
+ this.ensureMap();
2841
+ });
2842
+ }
2843
+ },
2844
+ immediate: true
2845
+ },
2846
+ value: {
2847
+ handler(newValue) {
2848
+ if (this.mapVisible) {
2849
+ this.$nextTick(() => {
2850
+ this.ensureMap();
2851
+ });
2852
+ }
2853
+ // Reset unsaved changes flag when value changes externally (e.g., after save)
2854
+ if (this.hasUnsavedChanges && (this.isGeoJsonPoint || this.isGeoJsonPolygon)) {
2855
+ this.hasUnsavedChanges = false;
2856
+ this.currentEditedGeometry = null; // Reset edited geometry when value changes externally
2857
+ }
2858
+ // Store the new value as the original geometry for future edits
2859
+ if (newValue && this.isGeoJsonGeometry) {
2860
+ this.originalGeometry = JSON.parse(JSON.stringify(newValue));
2861
+ } else {
2862
+ this.originalGeometry = null;
2863
+ }
2864
+ },
2865
+ deep: true,
2866
+ immediate: true
2867
+ }
2868
+ },
2869
+ beforeDestroy() {
2870
+ this.hideContextMenu();
2871
+ if (this.draggableMarker) {
2872
+ this.draggableMarker.remove();
2873
+ this.draggableMarker = null;
2874
+ }
2875
+ // Clean up polygon vertex markers
2876
+ this.draggableMarkers.forEach(marker => {
2877
+ if (marker) {
2878
+ marker.remove();
2879
+ }
2880
+ });
2881
+ this.draggableMarkers = [];
2882
+ if (this.mapInstance) {
2883
+ this.mapInstance.remove();
2884
+ this.mapInstance = null;
2885
+ this.mapLayer = null;
2886
+ }
2887
+ },
2888
+ methods: {
2889
+ ensureMap() {
2890
+ if (!this.mapVisible || !this.isGeoJsonGeometry || !this.$refs.map) {
2891
+ return;
2892
+ }
2893
+
2894
+ if (typeof L === 'undefined') {
2895
+ return;
2896
+ }
2897
+
2898
+ if (!this.mapInstance) {
2899
+ // Ensure the map container has explicit dimensions
2900
+ const mapElement = this.$refs.map;
2901
+ if (mapElement) {
2902
+ // Set explicit dimensions inline with !important to override any CSS
2903
+ mapElement.style.setProperty('height', '256px', 'important');
2904
+ mapElement.style.setProperty('min-height', '256px', 'important');
2905
+ mapElement.style.setProperty('width', '100%', 'important');
2906
+ mapElement.style.setProperty('display', 'block', 'important');
2907
+
2908
+ // Force a reflow to ensure dimensions are applied
2909
+ void mapElement.offsetHeight;
2910
+ }
2911
+
2912
+ try {
2913
+ this.mapInstance = L.map(this.$refs.map, {
2914
+ preferCanvas: false
2915
+ }).setView([0, 0], 1);
2916
+
2917
+ // Ensure map container has relative positioning for context menu
2918
+ const mapContainer = this.mapInstance.getContainer();
2919
+ if (mapContainer) {
2920
+ mapContainer.style.position = 'relative';
2921
+ }
2922
+
2923
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
2924
+ attribution: '&copy; OpenStreetMap contributors'
2925
+ }).addTo(this.mapInstance);
2926
+
2927
+ // Explicitly invalidate size after creation to ensure proper rendering
2928
+ this.$nextTick(() => {
2929
+ if (this.mapInstance) {
2930
+ this.mapInstance.invalidateSize();
2931
+ }
2932
+ });
2933
+ } catch (error) {
2934
+ return;
2935
+ }
2936
+ }
2937
+
2938
+ this.updateMapLayer();
2939
+ this.$nextTick(() => {
2940
+ if (this.mapInstance) {
2941
+ this.mapInstance.invalidateSize();
2942
+ }
2943
+ });
2944
+ },
2945
+ updateMapLayer() {
2946
+ if (!this.mapInstance || !this.isGeoJsonGeometry) {
2947
+ return;
2948
+ }
2949
+
2950
+ // For Point geometries in edit mode, use a draggable marker
2951
+ if (this.isGeoJsonPoint && this.isEditable) {
2952
+ const [lng, lat] = this.value.coordinates;
2953
+
2954
+ // If marker exists, update its position instead of recreating
2955
+ if (this.draggableMarker) {
2956
+ const currentLatLng = this.draggableMarker.getLatLng();
2957
+ // Only update if coordinates actually changed (avoid interrupting drag)
2958
+ if (Math.abs(currentLatLng.lat - lat) > 0.0001 || Math.abs(currentLatLng.lng - lng) > 0.0001) {
2959
+ this.draggableMarker.setLatLng([lat, lng]);
2960
+ }
2961
+ } else {
2962
+ // Create new draggable marker
2963
+ this.draggableMarker = L.marker([lat, lng], {
2964
+ draggable: true
2965
+ }).addTo(this.mapInstance);
2966
+
2967
+ // Add dragend event handler
2968
+ this.draggableMarker.on('dragend', () => {
2969
+ const newLat = this.draggableMarker.getLatLng().lat;
2970
+ const newLng = this.draggableMarker.getLatLng().lng;
2971
+ const newGeometry = {
2972
+ type: 'Point',
2973
+ coordinates: [newLng, newLat]
2974
+ };
2975
+ this.hasUnsavedChanges = true;
2976
+ if (this.onChange) {
2977
+ this.onChange(newGeometry);
2978
+ }
2979
+ });
2980
+
2981
+ // Center map on marker with appropriate zoom level
2982
+ const currentZoom = this.mapInstance.getZoom();
2983
+ // If zoom is too low (less than 10), set a good default zoom level (13)
2984
+ const targetZoom = currentZoom < 10 ? 13 : currentZoom;
2985
+ this.mapInstance.setView([lat, lng], targetZoom);
2986
+ }
2987
+
2988
+ // Remove any existing non-draggable layer
2989
+ if (this.mapLayer) {
2990
+ this.mapLayer.remove();
2991
+ this.mapLayer = null;
2992
+ }
2993
+
2994
+ // Clean up polygon markers if they exist
2995
+ this.draggableMarkers.forEach(marker => {
2996
+ if (marker) marker.remove();
2997
+ });
2998
+ this.draggableMarkers = [];
2999
+ } else if (this.isGeoJsonPolygon && this.isEditable) {
3000
+ // Initialize current edited geometry if not set
3001
+ if (!this.currentEditedGeometry) {
3002
+ this.currentEditedGeometry = JSON.parse(JSON.stringify(this.value));
3003
+ }
3004
+
3005
+ // For Polygon geometries in edit mode, create polygon layer with draggable vertex markers
3006
+ // Use current edited geometry if available, otherwise use value
3007
+ const polygonGeometryToUse = this.currentEditedGeometry || this.value;
3008
+ const feature = {
3009
+ type: 'Feature',
3010
+ geometry: polygonGeometryToUse,
3011
+ properties: {}
3012
+ };
3013
+
3014
+ // Update or create polygon layer
3015
+ if (this.mapLayer) {
3016
+ this.mapLayer.remove();
3017
+ }
3018
+ this.mapLayer = L.geoJSON(feature, {
3019
+ style: {
3020
+ color: '#3388ff',
3021
+ weight: 2,
3022
+ opacity: 0.8,
3023
+ fillOpacity: 0.2
3024
+ },
3025
+ interactive: this.isMultiPolygon // Only interactive for MultiPolygon (to allow edge clicks)
3026
+ }).addTo(this.mapInstance);
3027
+
3028
+ // Add contextmenu handler to polygon edges to add vertices (only for MultiPolygon)
3029
+ if (this.isMultiPolygon) {
3030
+ this.mapLayer.eachLayer((layer) => {
3031
+ layer.on('contextmenu', (e) => {
3032
+ e.originalEvent.preventDefault();
3033
+ e.originalEvent.stopPropagation();
3034
+
3035
+ // Check if clicking near an existing marker (using pixel distance)
3036
+ const clickPoint = e.latlng;
3037
+ const clickContainerPoint = this.mapInstance.latLngToContainerPoint(clickPoint);
3038
+
3039
+ const isClickingOnMarker = this.draggableMarkers.some(marker => {
3040
+ if (!marker || !marker._icon) return false;
3041
+ const markerLatLng = marker.getLatLng();
3042
+ const markerContainerPoint = this.mapInstance.latLngToContainerPoint(markerLatLng);
3043
+
3044
+ // Calculate pixel distance
3045
+ const dx = clickContainerPoint.x - markerContainerPoint.x;
3046
+ const dy = clickContainerPoint.y - markerContainerPoint.y;
3047
+ const pixelDistance = Math.sqrt(dx * dx + dy * dy);
3048
+
3049
+ // 15 pixel threshold - marker icon is about 12px, so 15px gives some buffer
3050
+ return pixelDistance < 15;
3051
+ });
3052
+
3053
+ if (!isClickingOnMarker) {
3054
+ this.showAddVertexContextMenu(e.originalEvent, clickPoint);
3055
+ }
3056
+ });
3057
+ });
3058
+ }
3059
+
3060
+ // Make polygon layers non-interactive to avoid interfering with marker dragging
3061
+ this.mapLayer.eachLayer((layer) => {
3062
+ layer.setStyle({
3063
+ interactive: this.isMultiPolygon, // Only interactive for MultiPolygon (for context menu)
3064
+ stroke: true,
3065
+ weight: 2,
3066
+ opacity: 0.8
3067
+ });
3068
+ layer._path.style.pointerEvents = this.isMultiPolygon ? 'stroke' : 'none';
3069
+ });
3070
+
3071
+ // Get the outer ring coordinates
3072
+ // For Polygon: coordinates[0] is the outer ring
3073
+ // For MultiPolygon: coordinates[0][0] is the first polygon's outer ring
3074
+ // Use current edited geometry if available
3075
+ const ringGeometryToUse = this.currentEditedGeometry || this.value;
3076
+ let outerRing = [];
3077
+
3078
+ if (this.isMultiPolygon) {
3079
+ // MultiPolygon structure: [[[[lng, lat], ...]], [[[lng, lat], ...]]]
3080
+ // Get the first polygon's outer ring
3081
+ if (ringGeometryToUse.coordinates[0] && ringGeometryToUse.coordinates[0][0]) {
3082
+ outerRing = ringGeometryToUse.coordinates[0][0];
3083
+ }
3084
+ } else {
3085
+ // Polygon structure: [[[lng, lat], ...]]
3086
+ outerRing = ringGeometryToUse.coordinates[0] || [];
3087
+ }
3088
+
3089
+ if (outerRing.length === 0) {
3090
+ return;
3091
+ }
3092
+
3093
+ // Determine how many markers we should have (accounting for closed rings)
3094
+ const isClosedRing = outerRing.length > 0 &&
3095
+ outerRing[0][0] === outerRing[outerRing.length - 1][0] &&
3096
+ outerRing[0][1] === outerRing[outerRing.length - 1][1];
3097
+ const expectedMarkerCount = isClosedRing ? outerRing.length - 1 : outerRing.length;
3098
+
3099
+ // Remove existing markers if count doesn't match
3100
+ if (this.draggableMarkers.length !== expectedMarkerCount && !this.isCreatingMarkers) {
3101
+ this.isCreatingMarkers = true;
3102
+ this.draggableMarkers.forEach(marker => {
3103
+ if (marker) marker.remove();
3104
+ });
3105
+ this.draggableMarkers = [];
3106
+
3107
+ // Create draggable markers for each vertex
3108
+ // Use setTimeout to ensure markers are added after polygon layer is fully rendered
3109
+ this.$nextTick(() => {
3110
+ // GeoJSON polygons typically have the first and last coordinate the same (closed ring)
3111
+ // We'll create markers for all coordinates except the last one if it's a duplicate
3112
+ // But we need to track the original index for updating the correct coordinate
3113
+ const isClosedRing = outerRing.length > 0 &&
3114
+ outerRing[0][0] === outerRing[outerRing.length - 1][0] &&
3115
+ outerRing[0][1] === outerRing[outerRing.length - 1][1];
3116
+
3117
+ const coordsToProcess = isClosedRing
3118
+ ? outerRing.slice(0, -1) // Skip last coordinate if it's a duplicate of first
3119
+ : outerRing;
3120
+
3121
+ coordsToProcess.forEach((coord, visibleIndex) => {
3122
+ // visibleIndex is the index in the visible markers array
3123
+ // actualIndex is the index in the coordinates array (same for non-closed rings)
3124
+ const actualIndex = visibleIndex;
3125
+ if (!Array.isArray(coord) || coord.length < 2) {
3126
+ return;
3127
+ }
3128
+
3129
+ const [lng, lat] = coord;
3130
+
3131
+ // Create a custom icon for the vertex marker
3132
+ const icon = L.divIcon({
3133
+ className: 'polygon-vertex-marker',
3134
+ html: '<div style="width: 12px; height: 12px; background-color: #3388ff; border: 2px solid white; border-radius: 50%; cursor: move;"></div>',
3135
+ iconSize: [12, 12],
3136
+ iconAnchor: [6, 6]
3137
+ });
3138
+
3139
+ const marker = L.marker([lat, lng], {
3140
+ draggable: true,
3141
+ icon: icon,
3142
+ zIndexOffset: 1000, // Ensure markers are above polygon layer
3143
+ riseOnHover: true, // Bring marker to front on hover
3144
+ bubblingMouseEvents: false // Prevent events from bubbling to polygon
3145
+ });
3146
+
3147
+ // Add marker to map
3148
+ marker.addTo(this.mapInstance);
3149
+
3150
+ // Ensure marker is in the marker pane (above overlay pane where polygon is)
3151
+ // Note: markers don't have bringToFront(), we use zIndexOffset and DOM manipulation
3152
+ if (marker._icon) {
3153
+ marker._icon.style.pointerEvents = 'auto';
3154
+ marker._icon.style.zIndex = (1001 + visibleIndex).toString(); // Unique z-index for each marker
3155
+ // Move marker icon to top of marker pane
3156
+ if (marker._icon.parentNode) {
3157
+ marker._icon.parentNode.appendChild(marker._icon);
3158
+ }
3159
+ }
3160
+
3161
+ // Add dragend event handler
3162
+ // Store the actualIndex in closure for this marker
3163
+ const markerActualIndex = actualIndex;
3164
+ const markerIsFirstInClosedRing = isClosedRing && actualIndex === 0;
3165
+
3166
+ // Add right-click handler to show context menu
3167
+ marker.on('contextmenu', (e) => {
3168
+ e.originalEvent.preventDefault();
3169
+ e.originalEvent.stopPropagation();
3170
+ this.showContextMenu(e.originalEvent, markerActualIndex, marker);
3171
+ return false;
3172
+ });
3173
+
3174
+ // Also attach directly to icon element as the event might not bubble to marker
3175
+ if (marker._icon) {
3176
+ marker._icon.addEventListener('contextmenu', (e) => {
3177
+ e.preventDefault();
3178
+ e.stopPropagation();
3179
+ this.showContextMenu(e, markerActualIndex, marker);
3180
+ return false;
3181
+ }, true); // Use capture phase
3182
+ }
3183
+
3184
+ marker.on('dragend', () => {
3185
+ const newLat = marker.getLatLng().lat;
3186
+ const newLng = marker.getLatLng().lng;
3187
+
3188
+ // Use current edited geometry if available, otherwise use original value
3189
+ const baseGeometry = this.currentEditedGeometry || this.value;
3190
+ const newCoordinates = JSON.parse(JSON.stringify(baseGeometry.coordinates));
3191
+
3192
+ // Get the outer ring to check if it's closed
3193
+ let outerRing = [];
3194
+ if (this.isMultiPolygon) {
3195
+ outerRing = newCoordinates[0][0] || [];
3196
+ } else {
3197
+ outerRing = newCoordinates[0] || [];
3198
+ }
3199
+
3200
+ // Check if this is a closed ring
3201
+ const isClosedRingNow = outerRing.length > 0 &&
3202
+ outerRing[0][0] === outerRing[outerRing.length - 1][0] &&
3203
+ outerRing[0][1] === outerRing[outerRing.length - 1][1];
3204
+
3205
+ // Update the coordinate
3206
+ if (this.isMultiPolygon) {
3207
+ // MultiPolygon: coordinates[0][0][index] = [lng, lat]
3208
+ newCoordinates[0][0][markerActualIndex] = [newLng, newLat];
3209
+ // If closed ring and this is the first coordinate, also update the closing coordinate
3210
+ if (isClosedRingNow && markerIsFirstInClosedRing) {
3211
+ newCoordinates[0][0][outerRing.length - 1] = [newLng, newLat];
3212
+ }
3213
+ } else {
3214
+ // Polygon: coordinates[0][index] = [lng, lat]
3215
+ newCoordinates[0][markerActualIndex] = [newLng, newLat];
3216
+ // If closed ring and this is the first coordinate, also update the closing coordinate
3217
+ if (isClosedRingNow && markerIsFirstInClosedRing) {
3218
+ newCoordinates[0][outerRing.length - 1] = [newLng, newLat];
3219
+ }
3220
+ }
3221
+
3222
+ // Validate coordinate structure before creating geometry
3223
+ try {
3224
+ // Ensure coordinates are valid numbers
3225
+ if (isNaN(newLng) || isNaN(newLat)) {
3226
+ throw new Error(`Invalid coordinates: [${newLng}, ${newLat}]`);
3227
+ }
3228
+
3229
+ const newGeometry = {
3230
+ type: baseGeometry.type, // Preserve Polygon or MultiPolygon type
3231
+ coordinates: newCoordinates
3232
+ };
3233
+
3234
+ // Validate the geometry structure
3235
+ if (!newGeometry.type || !Array.isArray(newGeometry.coordinates)) {
3236
+ throw new Error('Invalid geometry structure');
3237
+ }
3238
+
3239
+ // Store the current edited geometry state
3240
+ this.currentEditedGeometry = newGeometry;
3241
+ this.hasUnsavedChanges = true;
3242
+
3243
+ // Update the polygon layer immediately for visual feedback
3244
+ this.updatePolygonLayer(newGeometry);
3245
+
3246
+ // Notify parent of the change
3247
+ if (this.onChange) {
3248
+ this.onChange(newGeometry);
3249
+ }
3250
+ } catch (error) {
3251
+ // Log errors to aid debugging while still preventing UI crashes
3252
+ console.error('Error updating geometry on marker dragend:', error);
3253
+ }
3254
+ });
3255
+
3256
+ this.draggableMarkers.push(marker);
3257
+ });
3258
+ // Reset the guard after all markers are created
3259
+ this.isCreatingMarkers = false;
3260
+ });
3261
+ } else {
3262
+ // Update existing marker positions if coordinates changed
3263
+ // Get the correct outer ring based on geometry type
3264
+ // Use current edited geometry if available
3265
+ const updateGeometryToUse = this.currentEditedGeometry || this.value;
3266
+ let currentOuterRing = [];
3267
+ if (this.isMultiPolygon) {
3268
+ if (updateGeometryToUse.coordinates[0] && updateGeometryToUse.coordinates[0][0]) {
3269
+ currentOuterRing = updateGeometryToUse.coordinates[0][0];
3270
+ }
3271
+ } else {
3272
+ currentOuterRing = updateGeometryToUse.coordinates[0] || [];
3273
+ }
3274
+
3275
+ currentOuterRing.forEach((coord, index) => {
3276
+ const [lng, lat] = coord;
3277
+ const marker = this.draggableMarkers[index];
3278
+ if (marker) {
3279
+ const currentLatLng = marker.getLatLng();
3280
+ // Only update if coordinates actually changed (avoid interrupting drag)
3281
+ if (Math.abs(currentLatLng.lat - lat) > 0.0001 || Math.abs(currentLatLng.lng - lng) > 0.0001) {
3282
+ marker.setLatLng([lat, lng]);
3283
+ }
3284
+ }
3285
+ });
3286
+
3287
+ // Update polygon layer to reflect any coordinate changes
3288
+ // Use current edited geometry if available, otherwise use value
3289
+ const layerGeometryToUse = this.currentEditedGeometry || this.value;
3290
+ this.updatePolygonLayer(layerGeometryToUse);
3291
+ }
3292
+
3293
+ // Remove point marker if it exists
3294
+ if (this.draggableMarker) {
3295
+ this.draggableMarkers = this.draggableMarkers.filter(marker => marker !== this.draggableMarker);
3296
+ this.draggableMarker.remove();
3297
+ this.draggableMarker = null;
3298
+ }
3299
+
3300
+ // Fit bounds to polygon
3301
+ const bounds = this.mapLayer.getBounds();
3302
+ if (bounds.isValid()) {
3303
+ const currentZoom = this.mapInstance.getZoom();
3304
+ if (currentZoom < 10) {
3305
+ this.mapInstance.fitBounds(bounds, { maxZoom: 16 });
3306
+ } else {
3307
+ // Just ensure polygon is visible, don't change zoom if already zoomed in
3308
+ if (!bounds.contains(this.mapInstance.getBounds())) {
3309
+ this.mapInstance.fitBounds(bounds, { maxZoom: 16 });
3310
+ }
3311
+ }
3312
+ }
3313
+ } else {
3314
+ // For other geometries or non-editable mode, use standard GeoJSON layer
3315
+ if (this.draggableMarker) {
3316
+ this.draggableMarker.remove();
3317
+ this.draggableMarker = null;
3318
+ }
3319
+
3320
+ // Clean up polygon markers
3321
+ this.draggableMarkers.forEach(marker => {
3322
+ if (marker) marker.remove();
3323
+ });
3324
+ this.draggableMarkers = [];
3325
+
3326
+ if (this.mapLayer) {
3327
+ this.mapLayer.remove();
3328
+ }
3329
+
3330
+ const feature = {
3331
+ type: 'Feature',
3332
+ geometry: this.value,
3333
+ properties: {}
3334
+ };
3335
+
3336
+ this.mapLayer = L.geoJSON(feature).addTo(this.mapInstance);
3337
+ const bounds = this.mapLayer.getBounds();
3338
+ if (bounds.isValid()) {
3339
+ this.mapInstance.fitBounds(bounds, { maxZoom: 16 });
3340
+ }
3341
+ }
3342
+ },
3343
+ updatePolygonLayer(geometry) {
3344
+ if (!this.mapInstance || !this.mapLayer) {
3345
+ return;
3346
+ }
3347
+
3348
+ // Remove old layer
3349
+ this.mapLayer.remove();
3350
+
3351
+ // Create new layer with updated geometry
3352
+ const feature = {
3353
+ type: 'Feature',
3354
+ geometry: geometry,
3355
+ properties: {}
3356
+ };
3357
+
3358
+ this.mapLayer = L.geoJSON(feature, {
3359
+ style: {
3360
+ color: '#3388ff',
3361
+ weight: 2,
3362
+ opacity: 0.8,
3363
+ fillOpacity: 0.2
3364
+ },
3365
+ interactive: this.isMultiPolygon // Only interactive for MultiPolygon (to allow edge clicks)
3366
+ }).addTo(this.mapInstance);
3367
+
3368
+ // Add contextmenu handler to polygon edges to add vertices (only for MultiPolygon)
3369
+ if (this.isMultiPolygon && this.mapLayer.eachLayer) {
3370
+ this.mapLayer.eachLayer((layer) => {
3371
+ // Remove any existing contextmenu handlers first
3372
+ layer.off('contextmenu');
3373
+
3374
+ layer.on('contextmenu', (e) => {
3375
+ e.originalEvent.preventDefault();
3376
+ e.originalEvent.stopPropagation();
3377
+
3378
+ const clickPoint = e.latlng;
3379
+ const clickContainerPoint = this.mapInstance.latLngToContainerPoint(clickPoint);
3380
+
3381
+ // Check if clicking near an existing marker
3382
+ const isClickingOnMarker = this.draggableMarkers.some(marker => {
3383
+ if (!marker || !marker._icon) return false;
3384
+ const markerLatLng = marker.getLatLng();
3385
+ const markerContainerPoint = this.mapInstance.latLngToContainerPoint(markerLatLng);
3386
+
3387
+ const dx = clickContainerPoint.x - markerContainerPoint.x;
3388
+ const dy = clickContainerPoint.y - markerContainerPoint.y;
3389
+ const pixelDistance = Math.sqrt(dx * dx + dy * dy);
3390
+
3391
+ return pixelDistance < 15;
3392
+ });
3393
+
3394
+ if (!isClickingOnMarker) {
3395
+ this.showAddVertexContextMenu(e.originalEvent, clickPoint);
3396
+ }
3397
+ });
3398
+
3399
+ // Style polygon layer
3400
+ if (layer.setStyle) {
3401
+ layer.setStyle({
3402
+ interactive: true,
3403
+ stroke: true,
3404
+ weight: 2,
3405
+ opacity: 0.8
3406
+ });
3407
+ }
3408
+
3409
+ if (layer._path) {
3410
+ layer._path.style.pointerEvents = 'stroke';
3411
+ }
3412
+ });
3413
+ } else if (!this.isMultiPolygon && this.mapLayer.eachLayer) {
3414
+ // For regular Polygon, ensure it's non-interactive
3415
+ this.mapLayer.eachLayer((layer) => {
3416
+ layer.off('contextmenu'); // Remove any contextmenu handlers
3417
+ if (layer.setStyle) {
3418
+ layer.setStyle({ interactive: false });
3419
+ }
3420
+ if (layer._path) {
3421
+ layer._path.style.pointerEvents = 'none';
3422
+ }
3423
+ });
3424
+ }
3425
+
3426
+ // Bring all markers to front after updating polygon
3427
+ // Note: markers don't have bringToFront(), we manipulate DOM directly
3428
+ this.draggableMarkers.forEach((marker, index) => {
3429
+ if (marker && marker._icon && marker._icon.parentNode) {
3430
+ marker._icon.style.zIndex = (1001 + index).toString();
3431
+ marker._icon.parentNode.appendChild(marker._icon);
3432
+ }
3433
+ });
3434
+ },
3435
+ showContextMenu(event, index, marker) {
3436
+ // Hide any existing context menu
3437
+ this.hideContextMenu();
3438
+
3439
+ // Store the marker for deletion
3440
+ this.contextMenuMarker = { index, marker, type: 'vertex' };
3441
+
3442
+ // Create context menu if it doesn't exist
3443
+ if (!this.contextMenu) {
3444
+ this.contextMenu = document.createElement('div');
3445
+ this.contextMenu.className = 'leaflet-context-menu';
3446
+
3447
+ // Append to map container so it's positioned relative to the map
3448
+ if (this.mapInstance && this.mapInstance.getContainer()) {
3449
+ this.mapInstance.getContainer().appendChild(this.contextMenu);
3450
+ } else {
3451
+ document.body.appendChild(this.contextMenu);
3452
+ }
3453
+ }
3454
+
3455
+ // Clear existing menu items
3456
+ this.contextMenu.innerHTML = '';
3457
+
3458
+ // Add Delete option for vertices
3459
+ const deleteItem = document.createElement('div');
3460
+ deleteItem.className = 'leaflet-context-menu-item delete';
3461
+ deleteItem.textContent = 'Delete';
3462
+ deleteItem.addEventListener('click', (e) => {
3463
+ e.stopPropagation();
3464
+ if (this.contextMenuMarker && this.contextMenuMarker.type === 'vertex') {
3465
+ this.deleteVertex(this.contextMenuMarker.index, this.contextMenuMarker.marker);
3466
+ }
3467
+ this.hideContextMenu();
3468
+ });
3469
+ this.contextMenu.appendChild(deleteItem);
3470
+
3471
+ // Get map container position for relative positioning
3472
+ const mapContainer = this.mapInstance ? this.mapInstance.getContainer() : null;
3473
+ let left = event.clientX;
3474
+ let top = event.clientY;
3475
+
3476
+ if (mapContainer) {
3477
+ const rect = mapContainer.getBoundingClientRect();
3478
+ left = event.clientX - rect.left;
3479
+ top = event.clientY - rect.top;
3480
+ this.contextMenu.style.position = 'absolute';
3481
+ } else {
3482
+ this.contextMenu.style.position = 'fixed';
3483
+ }
3484
+
3485
+ // Position the context menu at the click location
3486
+ this.contextMenu.style.left = left + 'px';
3487
+ this.contextMenu.style.top = top + 'px';
3488
+ this.contextMenu.style.display = 'block';
3489
+
3490
+ // Hide context menu when clicking elsewhere
3491
+ const hideMenu = (e) => {
3492
+ if (this.contextMenu && !this.contextMenu.contains(e.target)) {
3493
+ this.hideContextMenu();
3494
+ document.removeEventListener('click', hideMenu);
3495
+ document.removeEventListener('contextmenu', hideMenu);
3496
+ }
3497
+ };
3498
+
3499
+ // Use setTimeout to avoid immediate hide from the current click
3500
+ setTimeout(() => {
3501
+ document.addEventListener('click', hideMenu);
3502
+ document.addEventListener('contextmenu', hideMenu);
3503
+ }, 10);
3504
+ },
3505
+ hideContextMenu() {
3506
+ if (this.contextMenu) {
3507
+ this.contextMenu.style.display = 'none';
3508
+ }
3509
+ this.contextMenuMarker = null;
3510
+ },
3511
+ showAddVertexContextMenu(event, latlng) {
3512
+ // Hide any existing context menu
3513
+ this.hideContextMenu();
3514
+
3515
+ // Store the location for adding vertex
3516
+ this.contextMenuMarker = { latlng, type: 'edge' };
3517
+
3518
+ // Create context menu if it doesn't exist
3519
+ if (!this.contextMenu) {
3520
+ this.contextMenu = document.createElement('div');
3521
+ this.contextMenu.className = 'leaflet-context-menu';
3522
+
3523
+ // Append to map container so it's positioned relative to the map
3524
+ if (this.mapInstance && this.mapInstance.getContainer()) {
3525
+ this.mapInstance.getContainer().appendChild(this.contextMenu);
3526
+ } else {
3527
+ document.body.appendChild(this.contextMenu);
3528
+ }
3529
+ }
3530
+
3531
+ // Clear existing menu items
3532
+ this.contextMenu.innerHTML = '';
3533
+
3534
+ // Add "Add Vertex" option
3535
+ const addVertexItem = document.createElement('div');
3536
+ addVertexItem.className = 'leaflet-context-menu-item';
3537
+ addVertexItem.textContent = 'Add Vertex';
3538
+ addVertexItem.addEventListener('click', (e) => {
3539
+ e.stopPropagation();
3540
+ if (this.contextMenuMarker && this.contextMenuMarker.type === 'edge') {
3541
+ this.addVertexAtLocation(this.contextMenuMarker.latlng);
3542
+ }
3543
+ this.hideContextMenu();
3544
+ });
3545
+ this.contextMenu.appendChild(addVertexItem);
3546
+
3547
+ // Position the context menu
3548
+ const mapContainer = this.mapInstance ? this.mapInstance.getContainer() : null;
3549
+ let left = event.clientX;
3550
+ let top = event.clientY;
3551
+
3552
+ if (mapContainer) {
3553
+ const rect = mapContainer.getBoundingClientRect();
3554
+ left = event.clientX - rect.left;
3555
+ top = event.clientY - rect.top;
3556
+ this.contextMenu.style.position = 'absolute';
3557
+ } else {
3558
+ this.contextMenu.style.position = 'fixed';
3559
+ }
3560
+
3561
+ this.contextMenu.style.left = left + 'px';
3562
+ this.contextMenu.style.top = top + 'px';
3563
+ this.contextMenu.style.display = 'block';
3564
+
3565
+ // Hide menu when clicking elsewhere
3566
+ const hideMenu = (e) => {
3567
+ if (!this.contextMenu || !this.contextMenu.contains(e.target)) {
3568
+ this.hideContextMenu();
3569
+ document.removeEventListener('click', hideMenu);
3570
+ }
3571
+ };
3572
+ setTimeout(() => {
3573
+ document.addEventListener('click', hideMenu);
3574
+ }, 0);
3575
+ },
3576
+ addVertexAtLocation(latlng) {
3577
+ // Get current geometry
3578
+ const baseGeometry = this.currentEditedGeometry || this.value;
3579
+ const newCoordinates = JSON.parse(JSON.stringify(baseGeometry.coordinates));
3580
+
3581
+ // Get the outer ring
3582
+ let outerRing = [];
3583
+ if (this.isMultiPolygon) {
3584
+ outerRing = newCoordinates[0][0] || [];
3585
+ } else {
3586
+ outerRing = newCoordinates[0] || [];
3587
+ }
3588
+
3589
+ if (outerRing.length === 0) {
3590
+ return;
3591
+ }
3592
+
3593
+ // Check if this is a closed ring
3594
+ const isClosedRing = outerRing.length > 0 &&
3595
+ outerRing[0][0] === outerRing[outerRing.length - 1][0] &&
3596
+ outerRing[0][1] === outerRing[outerRing.length - 1][1];
3597
+
3598
+ // Convert latlng to [lng, lat] format
3599
+ const newCoord = [latlng.lng, latlng.lat];
3600
+
3601
+ // Find the closest edge segment and insert the vertex
3602
+ let closestEdgeIndex = 0;
3603
+ let minDistance = Infinity;
3604
+
3605
+ // Check each edge segment
3606
+ for (let i = 0; i < outerRing.length - 1; i++) {
3607
+ const p1 = outerRing[i];
3608
+ const p2 = outerRing[i + 1];
3609
+
3610
+ // Calculate distance from click point to edge segment
3611
+ const distance = this.distanceToLineSegment(
3612
+ [latlng.lng, latlng.lat],
3613
+ [p1[0], p1[1]],
3614
+ [p2[0], p2[1]]
3615
+ );
3616
+
3617
+ if (distance < minDistance) {
3618
+ minDistance = distance;
3619
+ closestEdgeIndex = i + 1; // Insert after point i
3620
+ }
3621
+ }
3622
+
3623
+ // For closed rings, also check the edge from last to first
3624
+ if (isClosedRing) {
3625
+ const p1 = outerRing[outerRing.length - 1];
3626
+ const p2 = outerRing[0];
3627
+ const distance = this.distanceToLineSegment(
3628
+ [latlng.lng, latlng.lat],
3629
+ [p1[0], p1[1]],
3630
+ [p2[0], p2[1]]
3631
+ );
3632
+
3633
+ if (distance < minDistance) {
3634
+ minDistance = distance;
3635
+ closestEdgeIndex = outerRing.length; // Insert at end (before closing coordinate)
3636
+ }
3637
+ }
3638
+
3639
+ // Insert the new coordinate
3640
+ if (this.isMultiPolygon) {
3641
+ newCoordinates[0][0].splice(closestEdgeIndex, 0, newCoord);
3642
+ // If it was a closed ring, update the closing coordinate
3643
+ if (isClosedRing) {
3644
+ newCoordinates[0][0][newCoordinates[0][0].length - 1] = newCoordinates[0][0][0];
3645
+ }
3646
+ } else {
3647
+ newCoordinates[0].splice(closestEdgeIndex, 0, newCoord);
3648
+ // If it was a closed ring, update the closing coordinate
3649
+ if (isClosedRing) {
3650
+ newCoordinates[0][newCoordinates[0].length - 1] = newCoordinates[0][0];
3651
+ }
3652
+ }
3653
+
3654
+ const newGeometry = {
3655
+ type: baseGeometry.type,
3656
+ coordinates: newCoordinates
3657
+ };
3658
+
3659
+ // Store the current edited geometry state
3660
+ this.currentEditedGeometry = newGeometry;
3661
+ this.hasUnsavedChanges = true;
3662
+
3663
+ // Update the polygon layer and recreate markers
3664
+ this.updatePolygonLayer(newGeometry);
3665
+ this.$nextTick(() => {
3666
+ this.updateMapLayer();
3667
+ });
3668
+
3669
+ // Notify parent of the change
3670
+ if (this.onChange) {
3671
+ this.onChange(newGeometry);
3672
+ }
3673
+ },
3674
+ distanceToLineSegment(point, lineStart, lineEnd) {
3675
+ // Calculate distance from point to line segment
3676
+ // point, lineStart, lineEnd are [lng, lat] arrays
3677
+ const A = point[0] - lineStart[0];
3678
+ const B = point[1] - lineStart[1];
3679
+ const C = lineEnd[0] - lineStart[0];
3680
+ const D = lineEnd[1] - lineStart[1];
3681
+
3682
+ const dot = A * C + B * D;
3683
+ const lenSq = C * C + D * D;
3684
+ let param = -1;
3685
+
3686
+ if (lenSq !== 0) {
3687
+ param = dot / lenSq;
3688
+ }
3689
+
3690
+ let xx, yy;
3691
+
3692
+ if (param < 0) {
3693
+ xx = lineStart[0];
3694
+ yy = lineStart[1];
3695
+ } else if (param > 1) {
3696
+ xx = lineEnd[0];
3697
+ yy = lineEnd[1];
3698
+ } else {
3699
+ xx = lineStart[0] + param * C;
3700
+ yy = lineStart[1] + param * D;
3701
+ }
3702
+
3703
+ const dx = point[0] - xx;
3704
+ const dy = point[1] - yy;
3705
+
3706
+ // Use simple Euclidean distance (approximation for small areas)
3707
+ return Math.sqrt(dx * dx + dy * dy);
3708
+ },
3709
+ deleteVertex(index, marker) {
3710
+ // Get current geometry
3711
+ const baseGeometry = this.currentEditedGeometry || this.value;
3712
+ const newCoordinates = JSON.parse(JSON.stringify(baseGeometry.coordinates));
3713
+
3714
+ // Get the outer ring
3715
+ let outerRing = [];
3716
+ if (this.isMultiPolygon) {
3717
+ outerRing = newCoordinates[0][0] || [];
3718
+ } else {
3719
+ outerRing = newCoordinates[0] || [];
3720
+ }
3721
+
3722
+ // Check if this is a closed ring
3723
+ const isClosedRing = outerRing.length > 0 &&
3724
+ outerRing[0][0] === outerRing[outerRing.length - 1][0] &&
3725
+ outerRing[0][1] === outerRing[outerRing.length - 1][1];
3726
+
3727
+ // Minimum vertices for a valid polygon (3 for triangle, but GeoJSON requires at least 4 for closed ring)
3728
+ const minVertices = isClosedRing ? 4 : 3;
3729
+
3730
+ // Don't allow deletion if we'd have too few vertices
3731
+ const currentVertexCount = isClosedRing ? outerRing.length - 1 : outerRing.length;
3732
+ if (currentVertexCount < minVertices) {
3733
+ const message = isClosedRing
3734
+ ? `Cannot delete vertex. A polygon requires at least ${minVertices} vertices (including the closing vertex).`
3735
+ : `Cannot delete vertex. A polygon requires at least ${minVertices} vertices.`;
3736
+ this.$toast.error(message, {
3737
+ timeout: 5000
3738
+ });
3739
+ this.hideContextMenu();
3740
+ return; // Can't delete - would make invalid polygon
3741
+ }
3742
+
3743
+ // Remove the coordinate
3744
+ if (this.isMultiPolygon) {
3745
+ newCoordinates[0][0].splice(index, 1);
3746
+ // If it was a closed ring and we removed a coordinate, update the closing coordinate
3747
+ if (isClosedRing && index === 0) {
3748
+ // If we deleted the first coordinate, the new first becomes the closing coordinate
3749
+ newCoordinates[0][0][newCoordinates[0][0].length - 1] = newCoordinates[0][0][0];
3750
+ } else if (isClosedRing && index === outerRing.length - 1) {
3751
+ // If we deleted the closing coordinate, update it to match the first
3752
+ newCoordinates[0][0][newCoordinates[0][0].length - 1] = newCoordinates[0][0][0];
3753
+ }
3754
+ } else {
3755
+ newCoordinates[0].splice(index, 1);
3756
+ // If it was a closed ring and we removed a coordinate, update the closing coordinate
3757
+ if (isClosedRing && index === 0) {
3758
+ // If we deleted the first coordinate, the new first becomes the closing coordinate
3759
+ newCoordinates[0][newCoordinates[0].length - 1] = newCoordinates[0][0];
3760
+ } else if (isClosedRing && index === outerRing.length - 1) {
3761
+ // If we deleted the closing coordinate, update it to match the first
3762
+ newCoordinates[0][newCoordinates[0].length - 1] = newCoordinates[0][0];
3763
+ }
3764
+ }
3765
+
3766
+ const newGeometry = {
3767
+ type: baseGeometry.type,
3768
+ coordinates: newCoordinates
3769
+ };
3770
+
3771
+ // Store the current edited geometry state
3772
+ this.currentEditedGeometry = newGeometry;
3773
+ this.hasUnsavedChanges = true;
3774
+
3775
+ // Remove all markers and force recreation (needed because marker closures capture indices)
3776
+ this.draggableMarkers.forEach(m => {
3777
+ if (m) m.remove();
3778
+ });
3779
+ this.draggableMarkers = [];
3780
+
3781
+ // Update the polygon layer and recreate markers
3782
+ this.updatePolygonLayer(newGeometry);
3783
+ this.$nextTick(() => {
3784
+ this.updateMapLayer();
3785
+ });
3786
+
3787
+ // Notify parent of the change
3788
+ if (this.onChange) {
3789
+ this.onChange(newGeometry);
3790
+ }
3791
+ },
3792
+ undoDelete() {
3793
+ // Restore the original geometry (undo all changes)
3794
+ if (!this.originalGeometry) {
3795
+ return;
3796
+ }
3797
+
3798
+ const restoredGeometry = JSON.parse(JSON.stringify(this.originalGeometry));
3799
+
3800
+ // Clear all edited state
3801
+ this.currentEditedGeometry = null;
3802
+ this.hasUnsavedChanges = false;
3803
+
3804
+ // Update the map to show the original geometry
3805
+ if (this.isGeoJsonPoint) {
3806
+ this.$nextTick(() => {
3807
+ this.updateMapLayer();
3808
+ });
3809
+ } else if (this.isGeoJsonPolygon) {
3810
+ this.updatePolygonLayer(restoredGeometry);
3811
+ this.$nextTick(() => {
3812
+ this.updateMapLayer();
3813
+ });
3814
+ }
3815
+
3816
+ // Notify parent of the change (restore to original)
3817
+ if (this.onChange) {
3818
+ this.onChange(restoredGeometry);
3819
+ }
2752
3820
  }
2753
3821
  }
2754
3822
  });
2755
3823
 
3824
+
2756
3825
  /***/ },
2757
3826
 
2758
3827
  /***/ "./frontend/src/document-details/document-details.css"
@@ -3289,7 +4358,7 @@ module.exports = ".document-details {\n width: 100%;\n }\n \n .document-de
3289
4358
  (module) {
3290
4359
 
3291
4360
  "use strict";
3292
- module.exports = "<div class=\"border border-gray-200 bg-white rounded-lg mb-2\">\n <!-- Collapsible Header -->\n <div\n @click=\"toggleCollapse\"\n class=\"p-1 cursor-pointer flex items-center justify-between border-b border-gray-200 transition-colors duration-200 ease-in-out\"\n :class=\"{ 'bg-amber-100 hover:bg-amber-200': highlight, 'bg-slate-100 hover:bg-gray-100': !highlight }\"\n >\n <div class=\"flex items-center\" >\n <svg\n :class=\"isCollapsed ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-gray-500 mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-gray-900\">{{path.path}}</span>\n <span class=\"ml-2 text-sm text-gray-500\">({{(path.instance || 'unknown').toLowerCase()}})</span>\n </div>\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800 px-2 py-1 rounded-md border border-transparent hover:border-gray-300 bg-white\"\n @click.stop.prevent=\"copyPropertyValue\"\n title=\"Copy value\"\n aria-label=\"Copy property value\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7h8m-8 4h8m-8 4h5m-7-9a2 2 0 012-2h7a2 2 0 012 2v10a2 2 0 01-2 2H8l-4-4V7a2 2 0 012-2z\" />\n </svg>\n {{copyButtonLabel}}\n </button>\n <router-link\n v-if=\"path.ref && getValueForPath(path.path)\"\n :to=\"`/model/${path.ref}/document/${getValueForPath(path.path)}`\"\n class=\"bg-ultramarine-600 hover:bg-ultramarine-500 text-white px-2 py-1 text-sm rounded-md\"\n @click.stop\n >View Document\n </router-link>\n </div>\n </div>\n\n <!-- Collapsible Content -->\n <div v-if=\"!isCollapsed\" class=\"p-2\">\n <!-- Date Type Selector (when editing dates) -->\n <div v-if=\"editting && path.instance === 'Date'\" class=\"mb-3 flex gap-1.5\">\n <div\n @click=\"dateType = 'picker'\"\n :class=\"dateType === 'picker' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'picker' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n Date Picker\n </div>\n </div>\n <div\n @click=\"dateType = 'iso'\"\n :class=\"dateType === 'iso' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'iso' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n ISO String\n </div>\n </div>\n </div>\n\n <!-- Field Content -->\n <div v-if=\"editting && path.path !== '_id'\">\n <component\n :is=\"getEditComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :format=\"dateType\"\n v-bind=\"getEditComponentProps(path)\"\n @input=\"handleInputChange($event)\"\n @error=\"invalid[path.path] = $event;\"\n >\n </component>\n </div>\n <div v-else>\n <!-- Show truncated or full value based on needsTruncation and isValueExpanded -->\n <!-- Special handling for truncated arrays -->\n <div v-if=\"isArray && shouldShowTruncated\" class=\"w-full\">\n <div class=\"mt-2\">\n <div\n v-for=\"(item, index) in truncatedArrayItems\"\n :key=\"index\"\n class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative hover:bg-slate-50 hover:border-l-blue-600\">\n <div class=\"absolute -left-2 top-1/2 -translate-y-1/2 w-5 h-5 bg-blue-500 text-white rounded-full flex items-center justify-center text-[10px] font-semibold font-mono z-10 hover:bg-blue-600\">{{ index }}</div>\n <div v-if=\"arrayUtils.isObjectItem(item)\" class=\"flex flex-col gap-1 mt-1 px-2\">\n <div\n v-for=\"key in arrayUtils.getItemKeys(item)\"\n :key=\"key\"\n class=\"flex items-start gap-2 text-xs font-mono\">\n <span class=\"font-semibold text-gray-600 flex-shrink-0 min-w-[80px]\">{{ key }}:</span>\n <span class=\"text-gray-800 break-words whitespace-pre-wrap flex-1\">{{ arrayUtils.formatItemValue(item, key) }}</span>\n </div>\n </div>\n <div v-else class=\"text-xs py-1.5 px-2 font-mono text-gray-800 break-words whitespace-pre-wrap mt-1\">{{ arrayUtils.formatValue(item) }}</div>\n </div>\n <div class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-none border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative opacity-70 hover:opacity-100\">\n <div class=\"text-xs py-1.5 px-2 font-mono text-gray-500 italic break-words whitespace-pre-wrap mt-1\">\n ... and {{ remainingArrayCount }} more item{{ remainingArrayCount !== 1 ? 's' : '' }}\n </div>\n </div>\n </div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show all {{ arrayValue.length }} items\n </button>\n </div>\n <!-- Non-array truncated view -->\n <div v-else-if=\"shouldShowTruncated && !isArray\" class=\"relative\">\n <div class=\"text-gray-700 whitespace-pre-wrap break-words font-mono text-sm\">{{truncatedString}}</div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show more ({{valueAsString.length}} characters)\n </button>\n </div>\n <!-- Expanded view -->\n <div v-else-if=\"needsTruncation && isValueExpanded\" class=\"relative\">\n <component :is=\"getComponentForPath(path)\" :value=\"getValueForPath(path.path)\"></component>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4 rotate-180\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show less\n </button>\n </div>\n <!-- Full view (no truncation needed) -->\n <div v-else>\n <component :is=\"getComponentForPath(path)\" :value=\"getValueForPath(path.path)\"></component>\n </div>\n </div>\n </div>\n</div>\n";
4361
+ module.exports = "<div class=\"border border-gray-200 bg-white rounded-lg mb-2\" style=\"overflow: visible;\">\n <!-- Collapsible Header -->\n <div\n @click=\"toggleCollapse\"\n class=\"p-1 cursor-pointer flex items-center justify-between border-b border-gray-200 transition-colors duration-200 ease-in-out\"\n :class=\"{ 'bg-amber-100 hover:bg-amber-200': highlight, 'bg-slate-100 hover:bg-gray-100': !highlight }\"\n style=\"overflow: visible; position: relative;\"\n >\n <div class=\"flex items-center\" >\n <svg\n :class=\"isCollapsed ? 'rotate-0' : 'rotate-90'\"\n class=\"w-4 h-4 text-gray-500 mr-2 transition-transform duration-200\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9 5l7 7-7 7\"></path>\n </svg>\n <span class=\"font-medium text-gray-900\">{{path.path}}</span>\n <span class=\"ml-2 text-sm text-gray-500\">({{(path.instance || 'unknown').toLowerCase()}})</span>\n <div v-if=\"isGeoJsonGeometry\" class=\"ml-3 inline-flex items-center gap-2\">\n <div class=\"inline-flex items-center rounded-full bg-gray-200 p-0.5 text-xs font-semibold\">\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"detailViewMode === 'text' ? 'bg-blue-600 text-white shadow' : 'text-gray-700 hover:text-gray-900'\"\n :style=\"detailViewMode === 'text' ? 'color: white !important; background-color: #2563eb !important;' : ''\"\n @click.stop=\"setDetailViewMode('text')\">\n Text\n </button>\n <button\n type=\"button\"\n class=\"rounded-full px-2.5 py-0.5 transition\"\n :class=\"detailViewMode === 'map' ? 'bg-blue-600 text-white shadow' : 'text-gray-700 hover:text-gray-900'\"\n :style=\"detailViewMode === 'map' ? 'color: white !important; background-color: #2563eb !important;' : ''\"\n @click.stop=\"setDetailViewMode('map')\">\n Map\n </button>\n </div>\n <!-- Info icon with tooltip -->\n <div v-if=\"editting\" class=\"relative inline-block\" style=\"z-index: 10002;\" @mouseenter=\"showTooltip = true\" @mouseleave=\"showTooltip = false\" @click.stop>\n <svg\n ref=\"infoIcon\"\n class=\"w-6 h-6 text-gray-400 hover:text-gray-600 cursor-help\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"></path>\n </svg>\n <div\n v-show=\"showTooltip\"\n ref=\"tooltip\"\n class=\"absolute left-full top-0 ml-2 w-64 p-3 text-white text-xs rounded-lg shadow-xl\"\n style=\"z-index: 99999; pointer-events: none; white-space: normal; position: fixed; background-color: #111827;\"\n :style=\"getTooltipStyle()\"\n >\n <div class=\"font-semibold mb-2\">Map Controls:</div>\n <div v-if=\"isGeoJsonPoint\" class=\"space-y-1\">\n <div>• Drag pin to move location</div>\n </div>\n <div v-else-if=\"isGeoJsonPolygon\" class=\"space-y-1\">\n <div>• Drag vertices to reshape polygon</div>\n <div v-if=\"isMultiPolygon\">• Right-click edge to add new vertex</div>\n <div>• Right-click vertex to delete</div>\n </div>\n <div class=\"absolute top-2 -left-1 w-0 h-0 border-t-4 border-b-4 border-r-4 border-transparent border-r-gray-900\"></div>\n </div>\n </div>\n </div>\n </div>\n <div class=\"flex items-center gap-2\">\n <button\n type=\"button\"\n class=\"flex items-center gap-1 text-sm text-gray-600 hover:text-gray-800 px-2 py-1 rounded-md border border-transparent hover:border-gray-300 bg-white\"\n @click.stop.prevent=\"copyPropertyValue\"\n title=\"Copy value\"\n aria-label=\"Copy property value\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M8 7h8m-8 4h8m-8 4h5m-7-9a2 2 0 012-2h7a2 2 0 012 2v10a2 2 0 01-2 2H8l-4-4V7a2 2 0 012-2z\" />\n </svg>\n {{copyButtonLabel}}\n </button>\n <router-link\n v-if=\"path.ref && getValueForPath(path.path)\"\n :to=\"`/model/${path.ref}/document/${getValueForPath(path.path)}`\"\n class=\"bg-ultramarine-600 hover:bg-ultramarine-500 text-white px-2 py-1 text-sm rounded-md\"\n @click.stop\n >View Document\n </router-link>\n </div>\n </div>\n\n <!-- Collapsible Content -->\n <div v-if=\"!isCollapsed\" class=\"p-2\">\n <!-- Date Type Selector (when editing dates) -->\n <div v-if=\"editting && path.instance === 'Date'\" class=\"mb-3 flex gap-1.5\">\n <div\n @click=\"dateType = 'picker'\"\n :class=\"dateType === 'picker' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'picker' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n Date Picker\n </div>\n </div>\n <div\n @click=\"dateType = 'iso'\"\n :class=\"dateType === 'iso' ? 'bg-teal-600' : ''\"\n class=\"self-stretch px-2 py-1 rounded-sm justify-center items-center gap-1.5 flex cursor-pointer\">\n <div\n :class=\"dateType === 'iso' ? 'text-white' : ''\"\n class=\"text-xs font-medium font-['Lato'] capitalize leading-tight\">\n ISO String\n </div>\n </div>\n </div>\n\n <!-- Field Content -->\n <div v-if=\"editting && path.path !== '_id'\">\n <!-- Use detail-default with map editing for GeoJSON geometries -->\n <component\n v-if=\"isGeoJsonGeometry\"\n :is=\"getComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :view-mode=\"detailViewMode\"\n :on-change=\"handleInputChange\"\n >\n </component>\n <!-- Use standard edit components for other types -->\n <component\n v-else\n :is=\"getEditComponentForPath(path)\"\n :value=\"getEditValueForPath(path)\"\n :format=\"dateType\"\n v-bind=\"getEditComponentProps(path)\"\n @input=\"handleInputChange($event)\"\n @error=\"invalid[path.path] = $event;\"\n >\n </component>\n </div>\n <div v-else>\n <!-- Show truncated or full value based on needsTruncation and isValueExpanded -->\n <!-- Special handling for truncated arrays -->\n <div v-if=\"isArray && shouldShowTruncated\" class=\"w-full\">\n <div class=\"mt-2\">\n <div\n v-for=\"(item, index) in truncatedArrayItems\"\n :key=\"index\"\n class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative hover:bg-slate-50 hover:border-l-blue-600\">\n <div class=\"absolute -left-2 top-1/2 -translate-y-1/2 w-5 h-5 bg-blue-500 text-white rounded-full flex items-center justify-center text-[10px] font-semibold font-mono z-10 hover:bg-blue-600\">{{ index }}</div>\n <div v-if=\"arrayUtils.isObjectItem(item)\" class=\"flex flex-col gap-1 mt-1 px-2\">\n <div\n v-for=\"key in arrayUtils.getItemKeys(item)\"\n :key=\"key\"\n class=\"flex items-start gap-2 text-xs font-mono\">\n <span class=\"font-semibold text-gray-600 flex-shrink-0 min-w-[80px]\">{{ key }}:</span>\n <span class=\"text-gray-800 break-words whitespace-pre-wrap flex-1\">{{ arrayUtils.formatItemValue(item, key) }}</span>\n </div>\n </div>\n <div v-else class=\"text-xs py-1.5 px-2 font-mono text-gray-800 break-words whitespace-pre-wrap mt-1\">{{ arrayUtils.formatValue(item) }}</div>\n </div>\n <div class=\"mb-1.5 py-2.5 px-3 pl-4 bg-transparent border-none border-l-[3px] border-l-blue-500 rounded-none transition-all duration-200 cursor-pointer relative opacity-70 hover:opacity-100\">\n <div class=\"text-xs py-1.5 px-2 font-mono text-gray-500 italic break-words whitespace-pre-wrap mt-1\">\n ... and {{ remainingArrayCount }} more item{{ remainingArrayCount !== 1 ? 's' : '' }}\n </div>\n </div>\n </div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show all {{ arrayValue.length }} items\n </button>\n </div>\n <!-- Non-array truncated view -->\n <div v-else-if=\"shouldShowTruncated && !isArray\" class=\"relative\">\n <div class=\"text-gray-700 whitespace-pre-wrap break-words font-mono text-sm\">{{truncatedString}}</div>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show more ({{valueAsString.length}} characters)\n </button>\n </div>\n <!-- Expanded view -->\n <div v-else-if=\"needsTruncation && isValueExpanded\" class=\"relative\">\n <component\n :is=\"getComponentForPath(path)\"\n :value=\"getValueForPath(path.path)\"\n :view-mode=\"detailViewMode\"></component>\n <button\n @click=\"toggleValueExpansion\"\n class=\"mt-2 text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center gap-1 transform transition-all duration-200 ease-in-out hover:translate-x-0.5\"\n >\n <svg class=\"w-4 h-4 rotate-180\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M19 9l-7 7-7-7\"></path>\n </svg>\n Show less\n </button>\n </div>\n <!-- Full view (no truncation needed) -->\n <div v-else>\n <component\n :is=\"getComponentForPath(path)\"\n :value=\"getValueForPath(path.path)\"\n :view-mode=\"detailViewMode\"></component>\n </div>\n </div>\n </div>\n</div>\n";
3293
4362
 
3294
4363
  /***/ },
3295
4364
 
@@ -3319,8 +4388,10 @@ module.exports = app => app.component('document-property', {
3319
4388
  dateType: 'picker', // picker, iso
3320
4389
  isCollapsed: false, // Start uncollapsed by default
3321
4390
  isValueExpanded: false, // Track if the value is expanded
4391
+ detailViewMode: 'text',
3322
4392
  copyButtonLabel: 'Copy',
3323
- copyResetTimeoutId: null
4393
+ copyResetTimeoutId: null,
4394
+ showTooltip: false
3324
4395
  };
3325
4396
  },
3326
4397
  beforeDestroy() {
@@ -3394,9 +4465,58 @@ module.exports = app => app.component('document-property', {
3394
4465
  return this.arrayValue.length - 2;
3395
4466
  }
3396
4467
  return 0;
4468
+ },
4469
+ isGeoJsonGeometry() {
4470
+ const value = this.getValueForPath(this.path.path);
4471
+ return value != null
4472
+ && typeof value === 'object'
4473
+ && !Array.isArray(value)
4474
+ && Object.prototype.hasOwnProperty.call(value, 'type')
4475
+ && Object.prototype.hasOwnProperty.call(value, 'coordinates');
4476
+ },
4477
+ isGeoJsonPoint() {
4478
+ const value = this.getValueForPath(this.path.path);
4479
+ return this.isGeoJsonGeometry && value.type === 'Point';
4480
+ },
4481
+ isGeoJsonPolygon() {
4482
+ const value = this.getValueForPath(this.path.path);
4483
+ return this.isGeoJsonGeometry && (value.type === 'Polygon' || value.type === 'MultiPolygon');
4484
+ },
4485
+ isMultiPolygon() {
4486
+ const value = this.getValueForPath(this.path.path);
4487
+ return this.isGeoJsonGeometry && value.type === 'MultiPolygon';
4488
+ }
4489
+ },
4490
+ watch: {
4491
+ isGeoJsonGeometry(newValue) {
4492
+ if (!newValue) {
4493
+ this.detailViewMode = 'text';
4494
+ } else if (this.editting) {
4495
+ // Default to map view when editing GeoJSON
4496
+ this.detailViewMode = 'map';
4497
+ }
4498
+ },
4499
+ editting(newValue) {
4500
+ // When entering edit mode for GeoJSON, default to map view
4501
+ if (newValue && this.isGeoJsonGeometry) {
4502
+ this.detailViewMode = 'map';
4503
+ }
3397
4504
  }
3398
4505
  },
3399
4506
  methods: {
4507
+ setDetailViewMode(mode) {
4508
+ this.detailViewMode = mode;
4509
+
4510
+ // When switching to map view, expand the container and value so the map is visible
4511
+ if (mode === 'map' && this.isGeoJsonGeometry) {
4512
+ if (this.isCollapsed) {
4513
+ this.isCollapsed = false;
4514
+ }
4515
+ if (this.needsTruncation && !this.isValueExpanded) {
4516
+ this.isValueExpanded = true;
4517
+ }
4518
+ }
4519
+ },
3400
4520
  handleInputChange(newValue) {
3401
4521
  const currentValue = this.getValueForPath(this.path.path);
3402
4522
 
@@ -3467,6 +4587,11 @@ module.exports = app => app.component('document-property', {
3467
4587
  if (!this.document) {
3468
4588
  return;
3469
4589
  }
4590
+ // If there are unsaved changes for this path, use the changed value
4591
+ if (Object.prototype.hasOwnProperty.call(this.changes, path)) {
4592
+ return this.changes[path];
4593
+ }
4594
+ // Otherwise, use the document value
3470
4595
  const documentValue = mpath.get(path, this.document);
3471
4596
  return documentValue;
3472
4597
  },
@@ -3486,6 +4611,16 @@ module.exports = app => app.component('document-property', {
3486
4611
  this.copyResetTimeoutId = null;
3487
4612
  }, 5000);
3488
4613
  },
4614
+ getTooltipStyle() {
4615
+ if (!this.$refs.infoIcon || !this.showTooltip) {
4616
+ return {};
4617
+ }
4618
+ const rect = this.$refs.infoIcon.getBoundingClientRect();
4619
+ return {
4620
+ left: (rect.right + 8) + 'px',
4621
+ top: rect.top + 'px'
4622
+ };
4623
+ },
3489
4624
  copyPropertyValue() {
3490
4625
  const textToCopy = this.valueAsString;
3491
4626
  if (textToCopy == null) {
@@ -32454,7 +33589,7 @@ const compile = () => {
32454
33589
  (module) {
32455
33590
 
32456
33591
  "use strict";
32457
- module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.2.5","description":"A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","node-inspect-extracted":"3.x","tailwindcss":"3.4.0","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x"},"peerDependencies":{"mongoose":"7.x || 8.x || ^9.0.0"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongoose":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js","test:frontend":"mocha test/frontend/*.test.js"}}');
33592
+ module.exports = /*#__PURE__*/JSON.parse('{"name":"@mongoosejs/studio","version":"0.2.6","description":"A Mongoose-native MongoDB UI with schema-aware autocomplete, AI-assisted queries, and dashboards that understand your models - not just your data.","homepage":"https://mongoosestudio.app/","repository":{"type":"git","url":"https://github.com/mongoosejs/studio"},"license":"Apache-2.0","dependencies":{"@ai-sdk/anthropic":"2.x","@ai-sdk/google":"2.x","@ai-sdk/openai":"2.x","ai":"5.x","archetype":"0.13.1","csv-stringify":"6.3.0","ejson":"^2.2.3","extrovert":"^0.2.0","marked":"15.0.12","node-inspect-extracted":"3.x","tailwindcss":"3.4.0","vue":"3.x","vue-toastification":"^2.0.0-rc.5","webpack":"5.x"},"peerDependencies":{"mongoose":"7.x || 8.x || ^9.0.0"},"devDependencies":{"@masteringjs/eslint-config":"0.1.1","axios":"1.2.2","dedent":"^1.6.0","eslint":"9.30.0","express":"4.x","mocha":"10.2.0","mongoose":"9.x","sinon":"^21.0.1"},"scripts":{"lint":"eslint .","tailwind":"tailwindcss -o ./frontend/public/tw.css","tailwind:watch":"tailwindcss -o ./frontend/public/tw.css --watch","test":"mocha test/*.test.js","test:frontend":"mocha test/frontend/*.test.js"}}');
32458
33593
 
32459
33594
  /***/ }
32460
33595