@micha.bigler/survey-renderer 0.1.0
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.
- package/dist/SurveyCompletionLayout.js +26 -0
- package/dist/SurveyDiscoverySelect.js +271 -0
- package/dist/SurveyFilteredPlaceSelect.js +74 -0
- package/dist/SurveyGeoSelect.js +361 -0
- package/dist/SurveyMapSelect.js +5 -0
- package/dist/SurveyQuestionHeader.js +50 -0
- package/dist/SurveyRenderer.js +985 -0
- package/dist/discoveryDatasets.js +17 -0
- package/dist/discoveryEngine.js +160 -0
- package/dist/geoDatasets.js +17 -0
- package/dist/i18n/surveyRendererTranslations.js +242 -0
- package/dist/imageDownscale.js +64 -0
- package/dist/index.js +11 -0
- package/dist/localizeSurvey.js +24 -0
- package/dist/questionDefaults.js +13 -0
- package/dist/questionUiConfig.js +43 -0
- package/dist/stagedGroups.js +258 -0
- package/dist/types.js +22 -0
- package/package.json +36 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { createElement as _createElement } from "react";
|
|
13
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
|
+
import React from "react";
|
|
15
|
+
import AddIcon from "@mui/icons-material/Add";
|
|
16
|
+
import RemoveIcon from "@mui/icons-material/Remove";
|
|
17
|
+
import { Alert, Autocomplete, Box, Chip, IconButton, Stack, TextField, } from "@mui/material";
|
|
18
|
+
import { useTranslation } from "react-i18next";
|
|
19
|
+
import { GeoJSON, MapContainer, Pane, TileLayer, useMap } from "react-leaflet";
|
|
20
|
+
import "leaflet/dist/leaflet.css";
|
|
21
|
+
import { getGeoDataset } from "./geoDatasets";
|
|
22
|
+
import { normalizeLabel } from "./questionUiConfig";
|
|
23
|
+
import SurveyQuestionHeader from "./SurveyQuestionHeader";
|
|
24
|
+
const ESRI_LIGHT_BASE_TILE_URL = "https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}";
|
|
25
|
+
const ESRI_LIGHT_REFERENCE_TILE_URL = "https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Reference/MapServer/tile/{z}/{y}/{x}";
|
|
26
|
+
const ESRI_ATTRIBUTION = "Tiles © Esri — Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community";
|
|
27
|
+
function collectBoundsFromCoordinates(coordinates, bounds) {
|
|
28
|
+
if (!Array.isArray(coordinates)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (coordinates.length >= 2 && typeof coordinates[0] === "number" && typeof coordinates[1] === "number") {
|
|
32
|
+
const [lon, lat] = coordinates;
|
|
33
|
+
bounds.minLon = Math.min(bounds.minLon, lon);
|
|
34
|
+
bounds.maxLon = Math.max(bounds.maxLon, lon);
|
|
35
|
+
bounds.minLat = Math.min(bounds.minLat, lat);
|
|
36
|
+
bounds.maxLat = Math.max(bounds.maxLat, lat);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
coordinates.forEach((item) => collectBoundsFromCoordinates(item, bounds));
|
|
40
|
+
}
|
|
41
|
+
function getFeatureCollectionBounds(featureCollection) {
|
|
42
|
+
const bounds = {
|
|
43
|
+
minLon: Number.POSITIVE_INFINITY,
|
|
44
|
+
maxLon: Number.NEGATIVE_INFINITY,
|
|
45
|
+
minLat: Number.POSITIVE_INFINITY,
|
|
46
|
+
maxLat: Number.NEGATIVE_INFINITY,
|
|
47
|
+
};
|
|
48
|
+
((featureCollection === null || featureCollection === void 0 ? void 0 : featureCollection.features) || []).forEach((feature) => {
|
|
49
|
+
var _a;
|
|
50
|
+
collectBoundsFromCoordinates((_a = feature === null || feature === void 0 ? void 0 : feature.geometry) === null || _a === void 0 ? void 0 : _a.coordinates, bounds);
|
|
51
|
+
});
|
|
52
|
+
if (!Number.isFinite(bounds.minLon) ||
|
|
53
|
+
!Number.isFinite(bounds.maxLon) ||
|
|
54
|
+
!Number.isFinite(bounds.minLat) ||
|
|
55
|
+
!Number.isFinite(bounds.maxLat)) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return bounds;
|
|
59
|
+
}
|
|
60
|
+
function useGeoDataset(uiConfig) {
|
|
61
|
+
const dataset = React.useMemo(() => getGeoDataset(uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.datasetKey), [uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.datasetKey]);
|
|
62
|
+
const [featureCollection, setFeatureCollection] = React.useState(() => (dataset === null || dataset === void 0 ? void 0 : dataset.featureCollection) || null);
|
|
63
|
+
const [loading, setLoading] = React.useState(Boolean((dataset === null || dataset === void 0 ? void 0 : dataset.url) && !(dataset === null || dataset === void 0 ? void 0 : dataset.featureCollection)));
|
|
64
|
+
const [loadFailed, setLoadFailed] = React.useState(false);
|
|
65
|
+
React.useEffect(() => {
|
|
66
|
+
let active = true;
|
|
67
|
+
if (dataset === null || dataset === void 0 ? void 0 : dataset.featureCollection) {
|
|
68
|
+
setFeatureCollection(dataset.featureCollection);
|
|
69
|
+
setLoading(false);
|
|
70
|
+
setLoadFailed(false);
|
|
71
|
+
return () => {
|
|
72
|
+
active = false;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (!(dataset === null || dataset === void 0 ? void 0 : dataset.url)) {
|
|
76
|
+
setFeatureCollection(null);
|
|
77
|
+
setLoading(false);
|
|
78
|
+
setLoadFailed(false);
|
|
79
|
+
return () => {
|
|
80
|
+
active = false;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const loadFeatureCollection = async () => {
|
|
84
|
+
if (active) {
|
|
85
|
+
setLoading(true);
|
|
86
|
+
setLoadFailed(false);
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const response = await fetch(dataset.url, { credentials: "same-origin" });
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(`Geo dataset request failed with status ${response.status}`);
|
|
92
|
+
}
|
|
93
|
+
const nextFeatureCollection = await response.json();
|
|
94
|
+
if (active) {
|
|
95
|
+
setFeatureCollection(nextFeatureCollection);
|
|
96
|
+
setLoading(false);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (_error) {
|
|
100
|
+
if (active) {
|
|
101
|
+
setFeatureCollection(null);
|
|
102
|
+
setLoading(false);
|
|
103
|
+
setLoadFailed(true);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
loadFeatureCollection();
|
|
108
|
+
return () => {
|
|
109
|
+
active = false;
|
|
110
|
+
};
|
|
111
|
+
}, [dataset]);
|
|
112
|
+
return {
|
|
113
|
+
dataset,
|
|
114
|
+
featureCollection,
|
|
115
|
+
loading,
|
|
116
|
+
loadFailed,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function MapBoundsController({ featureCollection }) {
|
|
120
|
+
const map = useMap();
|
|
121
|
+
const hasFittedRef = React.useRef(false);
|
|
122
|
+
React.useEffect(() => {
|
|
123
|
+
if (hasFittedRef.current) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const bounds = getFeatureCollectionBounds(featureCollection);
|
|
127
|
+
if (!bounds) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
map.invalidateSize();
|
|
131
|
+
const latPadding = (bounds.maxLat - bounds.minLat) * 0.01;
|
|
132
|
+
const lonPadding = (bounds.maxLon - bounds.minLon) * 0.01;
|
|
133
|
+
const paddedBounds = [
|
|
134
|
+
[bounds.minLat - latPadding, bounds.minLon - lonPadding],
|
|
135
|
+
[bounds.maxLat + latPadding, bounds.maxLon + lonPadding],
|
|
136
|
+
];
|
|
137
|
+
map.fitBounds(paddedBounds, { maxZoom: 13.5, padding: [8, 8] });
|
|
138
|
+
hasFittedRef.current = true;
|
|
139
|
+
}, [featureCollection, map]);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
function MapPolygonsLayer({ featureCollection, multiple = false, onSelect, selectedValueKeys }) {
|
|
143
|
+
const style = React.useCallback((feature) => {
|
|
144
|
+
var _a;
|
|
145
|
+
const optionValue = String(((_a = feature === null || feature === void 0 ? void 0 : feature.properties) === null || _a === void 0 ? void 0 : _a.optionValue) || "");
|
|
146
|
+
const isSelected = selectedValueKeys.has(optionValue);
|
|
147
|
+
return {
|
|
148
|
+
color: isSelected ? "#0DAED1" : "#332733",
|
|
149
|
+
fillColor: isSelected ? "#0DAED1" : "#f2dce8",
|
|
150
|
+
fillOpacity: isSelected ? 0.38 : 0.22,
|
|
151
|
+
opacity: 0.95,
|
|
152
|
+
weight: isSelected ? 3 : 1.4,
|
|
153
|
+
};
|
|
154
|
+
}, [selectedValueKeys]);
|
|
155
|
+
// Refs damit Leaflet-Closures immer den aktuellen Stand lesen.
|
|
156
|
+
// onEachFeature wird von Leaflet nur einmal aufgerufen (bei Layer-Erstellung),
|
|
157
|
+
// danach sind alle Closures eingefroren — Refs umgehen das Problem.
|
|
158
|
+
const styleRef = React.useRef(style);
|
|
159
|
+
styleRef.current = style;
|
|
160
|
+
const onSelectRef = React.useRef(onSelect);
|
|
161
|
+
onSelectRef.current = onSelect;
|
|
162
|
+
const onEachFeature = React.useCallback((feature, layer) => {
|
|
163
|
+
var _a, _b;
|
|
164
|
+
const optionValue = String(((_a = feature === null || feature === void 0 ? void 0 : feature.properties) === null || _a === void 0 ? void 0 : _a.optionValue) || "");
|
|
165
|
+
const optionLabel = ((_b = feature === null || feature === void 0 ? void 0 : feature.properties) === null || _b === void 0 ? void 0 : _b.optionLabel) || "";
|
|
166
|
+
if (!optionValue) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
layer.on({
|
|
170
|
+
click: () => onSelectRef.current(optionValue, multiple),
|
|
171
|
+
mouseover: () => {
|
|
172
|
+
if (!(layer === null || layer === void 0 ? void 0 : layer._map)) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
layer.setStyle({ fillOpacity: 0.34, weight: 2.2 });
|
|
176
|
+
},
|
|
177
|
+
mouseout: () => {
|
|
178
|
+
if (!(layer === null || layer === void 0 ? void 0 : layer._map)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
layer.setStyle(styleRef.current(feature));
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
if (optionLabel) {
|
|
185
|
+
layer.bindTooltip(optionLabel, {
|
|
186
|
+
direction: "center",
|
|
187
|
+
opacity: 0.92,
|
|
188
|
+
sticky: true,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}, [multiple]);
|
|
192
|
+
return (_jsx(Pane, { name: "districts", style: { zIndex: 450 }, children: _jsx(GeoJSON, { data: featureCollection, onEachFeature: onEachFeature, pane: "districts", style: style }) }));
|
|
193
|
+
}
|
|
194
|
+
function MapZoomControls() {
|
|
195
|
+
const { t } = useTranslation();
|
|
196
|
+
const map = useMap();
|
|
197
|
+
return (_jsxs(Stack, { spacing: 1, sx: {
|
|
198
|
+
left: 12,
|
|
199
|
+
position: "absolute",
|
|
200
|
+
top: 12,
|
|
201
|
+
zIndex: 500,
|
|
202
|
+
}, children: [_jsx(IconButton, { "aria-label": t("surveyRenderer.MAP_ZOOM_IN"), onClick: () => map.zoomIn(), size: "small", sx: {
|
|
203
|
+
backgroundColor: "rgba(255, 255, 255, 0.94)",
|
|
204
|
+
border: "1px solid rgba(17, 17, 17, 0.08)",
|
|
205
|
+
borderRadius: 1.5,
|
|
206
|
+
boxShadow: "0 8px 20px rgba(17, 17, 17, 0.12)",
|
|
207
|
+
color: "#171117",
|
|
208
|
+
"&:hover": {
|
|
209
|
+
backgroundColor: "#ffffff",
|
|
210
|
+
color: "#0DAED1",
|
|
211
|
+
},
|
|
212
|
+
}, title: t("surveyRenderer.MAP_ZOOM_IN"), children: _jsx(AddIcon, { fontSize: "small" }) }), _jsx(IconButton, { "aria-label": t("surveyRenderer.MAP_ZOOM_OUT"), onClick: () => map.zoomOut(), size: "small", sx: {
|
|
213
|
+
backgroundColor: "rgba(255, 255, 255, 0.94)",
|
|
214
|
+
border: "1px solid rgba(17, 17, 17, 0.08)",
|
|
215
|
+
borderRadius: 1.5,
|
|
216
|
+
boxShadow: "0 8px 20px rgba(17, 17, 17, 0.12)",
|
|
217
|
+
color: "#171117",
|
|
218
|
+
"&:hover": {
|
|
219
|
+
backgroundColor: "#ffffff",
|
|
220
|
+
color: "#0DAED1",
|
|
221
|
+
},
|
|
222
|
+
}, title: t("surveyRenderer.MAP_ZOOM_OUT"), children: _jsx(RemoveIcon, { fontSize: "small" }) })] }));
|
|
223
|
+
}
|
|
224
|
+
function LeafletMap({ featureCollection, multiple = false, onSelect, selectedValueKeys }) {
|
|
225
|
+
const isCoarsePointer = React.useMemo(() => {
|
|
226
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
return window.matchMedia("(pointer: coarse)").matches;
|
|
230
|
+
}, []);
|
|
231
|
+
return (_jsx(Box, { onClick: (event) => event.stopPropagation(), onTouchEnd: (event) => event.stopPropagation(), onTouchStart: (event) => event.stopPropagation(), sx: {
|
|
232
|
+
backgroundColor: "rgba(255, 243, 249, 0.96)",
|
|
233
|
+
border: "1px solid",
|
|
234
|
+
borderColor: "rgba(17, 17, 17, 0.08)",
|
|
235
|
+
borderRadius: 2,
|
|
236
|
+
height: { xs: 300, sm: 360 },
|
|
237
|
+
overflow: "hidden",
|
|
238
|
+
".leaflet-container": {
|
|
239
|
+
backgroundColor: "#f8eef4",
|
|
240
|
+
display: "block",
|
|
241
|
+
fontFamily: "inherit",
|
|
242
|
+
height: "100%",
|
|
243
|
+
width: "100%",
|
|
244
|
+
},
|
|
245
|
+
".leaflet-tile-pane": {
|
|
246
|
+
opacity: 0.92,
|
|
247
|
+
},
|
|
248
|
+
".leaflet-control-attribution": {
|
|
249
|
+
backgroundColor: "rgba(255, 243, 249, 0.96)",
|
|
250
|
+
fontSize: "10px",
|
|
251
|
+
},
|
|
252
|
+
".leaflet-tooltip": {
|
|
253
|
+
backgroundColor: "rgba(17, 17, 17, 0.78)",
|
|
254
|
+
border: "none",
|
|
255
|
+
borderRadius: "10px",
|
|
256
|
+
boxShadow: "none",
|
|
257
|
+
color: "#ffffff",
|
|
258
|
+
padding: "4px 8px",
|
|
259
|
+
},
|
|
260
|
+
".leaflet-tooltip:before": {
|
|
261
|
+
display: "none",
|
|
262
|
+
},
|
|
263
|
+
}, children: _jsxs(MapContainer, { attributionControl: true, boxZoom: false, center: [47.5596, 7.5886], doubleClickZoom: false, dragging: !isCoarsePointer, maxZoom: 13.5, minZoom: 11, scrollWheelZoom: false, style: { height: "100%", minHeight: "300px", width: "100%" }, tap: false, touchZoom: false, zoom: 12.5, zoomSnap: 0.5, zoomDelta: 0.5, zoomControl: false, children: [_jsx(TileLayer, { attribution: ESRI_ATTRIBUTION, maxNativeZoom: 13, maxZoom: 13.5, url: ESRI_LIGHT_BASE_TILE_URL }), _jsx(TileLayer, { attribution: ESRI_ATTRIBUTION, maxNativeZoom: 13, maxZoom: 13.5, opacity: 0.45, url: ESRI_LIGHT_REFERENCE_TILE_URL }), _jsx(MapZoomControls, {}), _jsx(MapBoundsController, { featureCollection: featureCollection }), _jsx(MapPolygonsLayer, { featureCollection: featureCollection, multiple: multiple, onSelect: onSelect, selectedValueKeys: selectedValueKeys })] }) }));
|
|
264
|
+
}
|
|
265
|
+
function buildFeatureEntries(options, uiConfig, featureCollection, dataset) {
|
|
266
|
+
const features = Array.isArray(featureCollection === null || featureCollection === void 0 ? void 0 : featureCollection.features) ? featureCollection.features : [];
|
|
267
|
+
const featureLabelProperty = (uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.featureLabelProperty) || (dataset === null || dataset === void 0 ? void 0 : dataset.featureLabelProperty) || "name";
|
|
268
|
+
const optionByLabel = new Map(options.map((option) => [normalizeLabel(option.label), option]));
|
|
269
|
+
return features.map((feature, index) => {
|
|
270
|
+
var _a, _b;
|
|
271
|
+
const featureLabel = ((_a = feature === null || feature === void 0 ? void 0 : feature.properties) === null || _a === void 0 ? void 0 : _a[featureLabelProperty]) || "";
|
|
272
|
+
const matchedOption = optionByLabel.get(normalizeLabel(featureLabel)) || null;
|
|
273
|
+
return {
|
|
274
|
+
type: "Feature",
|
|
275
|
+
geometry: (feature === null || feature === void 0 ? void 0 : feature.geometry) || null,
|
|
276
|
+
properties: {
|
|
277
|
+
id: (feature === null || feature === void 0 ? void 0 : feature.id) || ((_b = feature === null || feature === void 0 ? void 0 : feature.properties) === null || _b === void 0 ? void 0 : _b.ID) || `${featureLabel}-${index}`,
|
|
278
|
+
optionLabel: (matchedOption === null || matchedOption === void 0 ? void 0 : matchedOption.label) || "",
|
|
279
|
+
optionValue: (matchedOption === null || matchedOption === void 0 ? void 0 : matchedOption.value) || "",
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
function defaultPlaceholderKey(multiple) {
|
|
285
|
+
return multiple ? "surveyRenderer.MAP_MULTI_AUTOCOMPLETE_PLACEHOLDER" : "surveyRenderer.MAP_AUTOCOMPLETE_PLACEHOLDER";
|
|
286
|
+
}
|
|
287
|
+
const SurveyGeoSelect = ({ errorText, hint, label, maxSelections = 1, multiple = false, onChange, options, uiConfig, value, }) => {
|
|
288
|
+
const { t } = useTranslation();
|
|
289
|
+
const { dataset, featureCollection: rawFeatureCollection, loading, loadFailed } = useGeoDataset(uiConfig);
|
|
290
|
+
const normalizedMultipleValue = React.useMemo(() => (Array.isArray(value) ? value.map((entry) => String(entry)) : []), [value]);
|
|
291
|
+
const [localSelectedValues, setLocalSelectedValues] = React.useState(normalizedMultipleValue);
|
|
292
|
+
const preparedFeatures = React.useMemo(() => buildFeatureEntries(options, uiConfig, rawFeatureCollection, dataset), [dataset, options, rawFeatureCollection, uiConfig]);
|
|
293
|
+
const featureCollection = React.useMemo(() => ({
|
|
294
|
+
type: "FeatureCollection",
|
|
295
|
+
features: preparedFeatures.filter((feature) => feature.geometry),
|
|
296
|
+
}), [preparedFeatures]);
|
|
297
|
+
React.useEffect(() => {
|
|
298
|
+
if (!multiple) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
setLocalSelectedValues(normalizedMultipleValue);
|
|
302
|
+
}, [multiple, normalizedMultipleValue]);
|
|
303
|
+
const selectedValues = multiple ? localSelectedValues : [value || ""];
|
|
304
|
+
const selectedValueKeys = React.useMemo(() => new Set(selectedValues.map((entry) => String(entry || "")).filter(Boolean)), [selectedValues]);
|
|
305
|
+
const selectedItems = options.filter((option) => selectedValueKeys.has(String(option.value)));
|
|
306
|
+
const selectedItem = selectedItems[0] || null;
|
|
307
|
+
const hasMapFeatures = ((featureCollection === null || featureCollection === void 0 ? void 0 : featureCollection.features) || []).length > 0;
|
|
308
|
+
const placeholderKey = (uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.autocompletePlaceholderKey) || defaultPlaceholderKey(multiple);
|
|
309
|
+
const handleSelect = React.useCallback((nextValue) => {
|
|
310
|
+
if (!multiple) {
|
|
311
|
+
onChange(nextValue);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const nextValueKey = String(nextValue || "");
|
|
315
|
+
const alreadySelected = selectedValues.some((entry) => String(entry) === nextValueKey);
|
|
316
|
+
if (alreadySelected) {
|
|
317
|
+
const nextValues = selectedValues.filter((entry) => String(entry) !== nextValueKey);
|
|
318
|
+
setLocalSelectedValues(nextValues);
|
|
319
|
+
onChange(nextValues);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (selectedValues.length >= maxSelections) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const nextValues = [...selectedValues, nextValueKey];
|
|
326
|
+
setLocalSelectedValues(nextValues);
|
|
327
|
+
onChange(nextValues);
|
|
328
|
+
}, [maxSelections, multiple, onChange, selectedValues]);
|
|
329
|
+
const handleAutocompleteChange = React.useCallback((_event, nextOption) => {
|
|
330
|
+
if (!multiple) {
|
|
331
|
+
onChange((nextOption === null || nextOption === void 0 ? void 0 : nextOption.value) || "");
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const nextValues = (nextOption || []).map((entry) => String(entry.value)).slice(0, maxSelections);
|
|
335
|
+
setLocalSelectedValues(nextValues);
|
|
336
|
+
onChange(nextValues);
|
|
337
|
+
}, [maxSelections, multiple, onChange]);
|
|
338
|
+
return (_jsxs(Stack, { spacing: 1.5, children: [(label || hint || errorText) ? (_jsx(SurveyQuestionHeader, { errorText: errorText, hint: hint, label: label })) : null, hasMapFeatures ? (_jsx(LeafletMap, { featureCollection: featureCollection, multiple: multiple, onSelect: handleSelect, selectedValueKeys: selectedValueKeys })) : loading ? (_jsx(Alert, { severity: "info", children: t((uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.loadingMessageKey) || "surveyRenderer.MAP_LOADING") })) : (_jsx(Alert, { severity: loadFailed ? "warning" : "info", children: t((uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.emptyMessageKey) || (loadFailed ? "surveyRenderer.MAP_DATA_PENDING" : "surveyRenderer.MAP_AUTOCOMPLETE_EMPTY")) })), multiple && selectedValues.length >= maxSelections ? (_jsx(Alert, { severity: "info", children: t("surveyRenderer.MAP_MAX_REACHED", { max: maxSelections }) })) : null, _jsx(Autocomplete, { fullWidth: true, getOptionLabel: (option) => option.label || "", isOptionEqualToValue: (option, candidate) => option.value === candidate.value, multiple: multiple, noOptionsText: t((uiConfig === null || uiConfig === void 0 ? void 0 : uiConfig.emptyMessageKey) || "surveyRenderer.MAP_AUTOCOMPLETE_EMPTY", "Keine passenden Quartiere gefunden"), onChange: handleAutocompleteChange, options: options, renderTags: (tagValue, getTagProps) => tagValue.map((option, index) => {
|
|
339
|
+
const _a = getTagProps({ index }), { key } = _a, tagProps = __rest(_a, ["key"]);
|
|
340
|
+
return (_createElement(Chip, Object.assign({}, tagProps, { key: key, label: option.label, sx: {
|
|
341
|
+
backgroundColor: "#ffffff",
|
|
342
|
+
border: "1px solid rgba(15, 23, 42, 0.10)",
|
|
343
|
+
borderRadius: 999,
|
|
344
|
+
fontSize: 14,
|
|
345
|
+
fontWeight: 600,
|
|
346
|
+
height: 34,
|
|
347
|
+
px: 0.5,
|
|
348
|
+
} })));
|
|
349
|
+
}), renderInput: (params) => (_jsx(TextField, Object.assign({}, params, { placeholder: t(placeholderKey, "Quartier eingeben oder auswählen"), sx: {
|
|
350
|
+
".MuiAutocomplete-tag": {
|
|
351
|
+
margin: "4px 6px 4px 0",
|
|
352
|
+
},
|
|
353
|
+
".MuiOutlinedInput-root": {
|
|
354
|
+
backgroundColor: "rgba(255, 243, 249, 0.96)",
|
|
355
|
+
borderRadius: 3,
|
|
356
|
+
paddingBottom: multiple ? "8px !important" : undefined,
|
|
357
|
+
paddingTop: multiple ? "8px !important" : undefined,
|
|
358
|
+
},
|
|
359
|
+
} }))), value: multiple ? selectedItems : selectedItem })] }));
|
|
360
|
+
};
|
|
361
|
+
export default SurveyGeoSelect;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Alert, Box, Stack, Typography } from "@mui/material";
|
|
4
|
+
const richTextLinkSx = {
|
|
5
|
+
backgroundColor: "rgba(13, 174, 209, 0.12)",
|
|
6
|
+
borderRadius: 1,
|
|
7
|
+
boxDecorationBreak: "clone",
|
|
8
|
+
color: "primary.dark",
|
|
9
|
+
display: "inline",
|
|
10
|
+
fontWeight: 600,
|
|
11
|
+
px: 0.35,
|
|
12
|
+
py: 0.1,
|
|
13
|
+
textDecoration: "underline",
|
|
14
|
+
textDecorationColor: "rgba(13, 174, 209, 0.72)",
|
|
15
|
+
textDecorationThickness: "0.08em",
|
|
16
|
+
textUnderlineOffset: "0.16em",
|
|
17
|
+
transition: "background-color 150ms ease, text-decoration-color 150ms ease, color 150ms ease",
|
|
18
|
+
wordBreak: "break-word",
|
|
19
|
+
"&:hover": {
|
|
20
|
+
backgroundColor: "rgba(13, 174, 209, 0.18)",
|
|
21
|
+
textDecorationColor: "rgba(13, 174, 209, 0.95)",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
export function renderRichText(text, keyPrefix = "rich-text") {
|
|
25
|
+
const parts = String(text)
|
|
26
|
+
.split(/(\*\*[\s\S]+?\*\*|\[[^\]]+\]\(https?:\/\/[^\s)]+\)|https?:\/\/[^\s<]+)/g)
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
return parts.map((part, index) => {
|
|
29
|
+
const boldMatch = part.match(/^\*\*(.+)\*\*$/);
|
|
30
|
+
if (boldMatch) {
|
|
31
|
+
return (_jsx(Box, { component: "strong", sx: { fontWeight: 700 }, children: renderRichText(boldMatch[1], `${keyPrefix}-bold-${index}`) }, `${keyPrefix}-bold-${index}`));
|
|
32
|
+
}
|
|
33
|
+
const markdownLinkMatch = part.match(/^\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)$/);
|
|
34
|
+
if (markdownLinkMatch) {
|
|
35
|
+
return (_jsx(Box, { component: "a", href: markdownLinkMatch[2], onClick: (event) => event.stopPropagation(), rel: "noreferrer", sx: richTextLinkSx, target: "_blank", children: renderRichText(markdownLinkMatch[1], `${keyPrefix}-link-label-${index}`) }, `${keyPrefix}-link-${index}`));
|
|
36
|
+
}
|
|
37
|
+
const urlMatch = part.match(/^(https?:\/\/[^\s<]+)$/);
|
|
38
|
+
if (urlMatch) {
|
|
39
|
+
return (_jsx(Box, { component: "a", href: urlMatch[1], onClick: (event) => event.stopPropagation(), rel: "noreferrer", sx: richTextLinkSx, target: "_blank", children: urlMatch[1] }, `${keyPrefix}-url-${index}`));
|
|
40
|
+
}
|
|
41
|
+
return _jsx(React.Fragment, { children: part }, `${part}-${index}`);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const SurveyQuestionHeader = ({ errorText, hint, label }) => (_jsxs(Stack, { spacing: 0.5, children: [_jsx(Typography, { sx: {
|
|
45
|
+
fontSize: { xs: "1.15rem", md: "1.25rem" },
|
|
46
|
+
fontWeight: 700,
|
|
47
|
+
lineHeight: 1.45,
|
|
48
|
+
whiteSpace: "pre-line",
|
|
49
|
+
}, variant: "h6", children: renderRichText(label, "question-label") }), hint ? (_jsx(Typography, { color: "text.secondary", sx: { whiteSpace: "pre-line" }, children: renderRichText(hint, "question-hint") })) : null, errorText ? _jsx(Alert, { severity: "error", children: errorText }) : null] }));
|
|
50
|
+
export default SurveyQuestionHeader;
|