@geops/rvf-mobility-web-component 0.1.64 → 0.1.66

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/docutils.js +125 -51
  3. package/index.html +48 -21
  4. package/index.js +291 -234
  5. package/package.json +12 -12
  6. package/search.html +25 -22
  7. package/src/BaseLayer/BaseLayer.tsx +18 -2
  8. package/src/Copyright/Copyright.tsx +2 -2
  9. package/src/Copyright/index.tsx +1 -1
  10. package/src/LayerTree/TreeItem/TreeItem.tsx +1 -1
  11. package/src/LinesNetworkPlanLayerHighlight/LinesNetworkPlanLayerHighlight.tsx +1 -0
  12. package/src/Map/Map.tsx +27 -1
  13. package/src/MapLayout/MapLayout.tsx +1 -1
  14. package/src/MapLayout/index.tsx +1 -1
  15. package/src/MobilityMap/MobilityMap.tsx +5 -11
  16. package/src/MobilityMap/MobilityMapAttributes.ts +8 -5
  17. package/src/MobilitySearch/MobilitySearchAttributes.ts +12 -0
  18. package/src/NotificationDetails/NotificationDetails.tsx +75 -57
  19. package/src/OverlayDetailsHeader/OverlayDetailsHeader.tsx +1 -1
  20. package/src/Permalink/Permalink.tsx +17 -6
  21. package/src/PermalinkInput/PermalinkInput.tsx +4 -1
  22. package/src/RealtimeLayer/index.tsx +1 -1
  23. package/src/RvfCopyright/RvfCopyright.tsx +32 -0
  24. package/src/RvfCopyright/index.tsx +1 -0
  25. package/src/RvfInputCopy/RvfInputCopy.tsx +18 -8
  26. package/src/RvfMapLayout/RvfMapLayout.tsx +198 -0
  27. package/src/RvfMapLayout/index.tsx +1 -0
  28. package/src/RvfMobilityMap/RvfMobilityMap.tsx +10 -580
  29. package/src/RvfRealtimeLayer/RvfRealtimeLayer.tsx +64 -0
  30. package/src/RvfRealtimeLayer/index.tsx +1 -0
  31. package/src/RvfStationsLayer/RvfStationsLayer.tsx +19 -0
  32. package/src/RvfStationsLayer/index.tsx +1 -0
  33. package/src/ShareMenu/ShareMenu.tsx +3 -1
  34. package/src/StationsLayer/index.tsx +1 -1
  35. package/src/ui/InputCopy/InputCopy.tsx +21 -10
  36. package/src/utils/constants.ts +1 -1
  37. package/src/utils/getUrlFromTemplate.test.ts +23 -0
  38. package/src/utils/getUrlFromTemplate.ts +47 -0
  39. package/src/utils/hooks/useI18n.tsx +2 -4
  40. package/src/utils/hooks/useInitialLayersVisiblity.tsx +27 -4
  41. package/src/utils/hooks/useInitialPermalink.tsx +31 -21
  42. package/src/utils/hooks/usePermalink.tsx +25 -0
  43. package/src/utils/translations.ts +4 -0
@@ -6,8 +6,8 @@ import { useCallback, useEffect } from "preact/hooks";
6
6
 
7
7
  import { LAYER_PROP_IS_EXPORTING } from "../utils/constants";
8
8
  import getPermalinkParameters from "../utils/getPermalinkParameters";
9
- // import getLayersAsFlatArray from "../getLayersAsFlatArray";
10
9
  import useMapContext from "../utils/hooks/useMapContext";
10
+ import usePermalink from "../utils/hooks/usePermalink";
11
11
 
12
12
  import type { Map } from "ol";
13
13
  import type { EventsKey } from "ol/events";
@@ -20,6 +20,9 @@ import type { EventsKey } from "ol/events";
20
20
  const Permalink = ({ replaceState = false }: { replaceState?: boolean }) => {
21
21
  const { map, setPermalinkUrlSearchParams } = useMapContext();
22
22
 
23
+ const permalink = usePermalink();
24
+
25
+ // Update the search parameters when the map moves
23
26
  const updatePermalink = useCallback(
24
27
  (currentMap: Map) => {
25
28
  // No update when exporting
@@ -28,15 +31,23 @@ const Permalink = ({ replaceState = false }: { replaceState?: boolean }) => {
28
31
  }
29
32
  const currentUrlParams = new URLSearchParams(window.location.search);
30
33
  const urlParams = getPermalinkParameters(currentMap, currentUrlParams);
31
- urlParams.set("permalink", "true");
32
34
  setPermalinkUrlSearchParams(urlParams);
33
- if (replaceState) {
34
- window.history.replaceState(null, null, `?${urlParams.toString()}`);
35
- }
36
35
  },
37
- [map, replaceState, setPermalinkUrlSearchParams],
36
+ [map, setPermalinkUrlSearchParams],
38
37
  );
39
38
 
39
+ // Replace the window url when permalink changes
40
+ useEffect(() => {
41
+ if (
42
+ replaceState &&
43
+ permalink &&
44
+ permalink !== window.location.href &&
45
+ !permalink.includes(encodeURIComponent("{{"))
46
+ ) {
47
+ window.history.replaceState(null, null, permalink);
48
+ }
49
+ }, [map, permalink, replaceState]);
50
+
40
51
  useEffect(() => {
41
52
  let moveEndKey: EventsKey;
42
53
  let loadEndKey: EventsKey;
@@ -2,6 +2,7 @@ import { memo } from "preact/compat";
2
2
 
3
3
  import InputCopy from "../ui/InputCopy";
4
4
  import useI18n from "../utils/hooks/useI18n";
5
+ import usePermalink from "../utils/hooks/usePermalink";
5
6
 
6
7
  import type { HTMLAttributes, PreactDOMAttributes } from "preact";
7
8
 
@@ -19,9 +20,11 @@ function PermalinkInput({
19
20
  ...props
20
21
  }: PermalinkInputProps) {
21
22
  const { t } = useI18n();
23
+ const permalink = usePermalink();
24
+
22
25
  return (
23
26
  <div {...props}>
24
- <InputCopy value={window?.location.href} {...inputProps} />
27
+ <InputCopy value={permalink} {...inputProps} readonly={true} />
25
28
  <p className="py-2">{t("permalink_input_hint")}</p>
26
29
  </div>
27
30
  );
@@ -1 +1 @@
1
- export { default } from "./RealtimeLayer";
1
+ export { default } from "../RvfRealtimeLayer";
@@ -0,0 +1,32 @@
1
+ import Copyright from "../Copyright/Copyright";
2
+
3
+ import type { CopyrightProps } from "../Copyright/Copyright";
4
+
5
+ const format = (copyrights) => {
6
+ const newCopyrights = [];
7
+ let alreadyAGeopsLink = false;
8
+ copyrights.forEach((copyright) => {
9
+ if (/geops/i.test(copyright)) {
10
+ // Hack until all geOps tiles are the same
11
+ // Currently there is some geops.com or geops.ch links
12
+ if (!alreadyAGeopsLink) {
13
+ alreadyAGeopsLink = true;
14
+ newCopyrights.push(copyright);
15
+ }
16
+ } else if (!/(sbb|rvf)/i.test(copyright)) {
17
+ newCopyrights.push(copyright);
18
+ }
19
+ });
20
+
21
+ return newCopyrights.join("&nbsp;|&nbsp;");
22
+ };
23
+
24
+ const options = {
25
+ format,
26
+ };
27
+
28
+ function RvfCopyright(props: CopyrightProps) {
29
+ return <Copyright options={options} {...props} />;
30
+ }
31
+
32
+ export default RvfCopyright;
@@ -0,0 +1 @@
1
+ export { default } from "./RvfCopyright";
@@ -1,37 +1,47 @@
1
- import { useRef, useState } from "preact/hooks";
1
+ import { useId, useState } from "preact/hooks";
2
2
 
3
3
  import Copy from "../icons/Copy";
4
4
  import IconButton from "../ui/IconButton";
5
+ import useI18n from "../utils/hooks/useI18n";
5
6
 
6
7
  import type { JSX, PreactDOMAttributes } from "preact";
7
8
 
8
9
  function RvfInputCopy(props: JSX.InputHTMLAttributes & PreactDOMAttributes) {
10
+ const { t } = useI18n();
9
11
  const [positionTooltip, setPositionTooltip] = useState<DOMRect>();
10
12
  const [isTooptipShowed, setIsTooltipShowed] = useState(false);
11
- const inputRef = useRef(null);
13
+ const inputId = useId();
14
+ const [node, setNode] = useState<HTMLDivElement | null>(null);
12
15
 
13
16
  const handleCopyClick = (event) => {
14
17
  setPositionTooltip(event.currentTarget.getBoundingClientRect());
15
- void navigator.clipboard.writeText(window?.location.href).then(() => {
18
+ const input: HTMLInputElement | null = node.querySelector(`#${inputId}`);
19
+ void navigator.clipboard.writeText(input?.value).then(() => {
16
20
  setIsTooltipShowed(true);
17
21
  setTimeout(() => {
18
22
  setIsTooltipShowed(false);
19
23
  }, 1000);
20
24
  });
21
- inputRef.current.select();
25
+ input?.select();
22
26
  };
23
27
 
24
28
  const handleInputFocus = () => {
25
- inputRef.current.select();
29
+ const input: HTMLInputElement | null = node.querySelector(`#${inputId}`);
30
+ input?.select();
26
31
  };
27
32
 
28
33
  return (
29
- <div className="text-grey flex items-center">
34
+ <div
35
+ className="text-grey flex items-center"
36
+ ref={(elt) => {
37
+ setNode(elt);
38
+ }}
39
+ >
30
40
  <input
31
41
  className="h-7 w-full rounded-sm rounded-r-none border border-r-0 border-current p-1 leading-4 outline-none"
42
+ id={inputId}
32
43
  onFocus={handleInputFocus}
33
44
  readOnly
34
- ref={inputRef}
35
45
  type="text"
36
46
  {...props}
37
47
  />
@@ -48,7 +58,7 @@ function RvfInputCopy(props: JSX.InputHTMLAttributes & PreactDOMAttributes) {
48
58
  top: positionTooltip?.top - 40,
49
59
  }}
50
60
  >
51
- Link kopiert!
61
+ {t("input_copy_success")}
52
62
  </div>
53
63
  </div>
54
64
  );
@@ -0,0 +1,198 @@
1
+ import { memo } from "preact/compat";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ import Copyright from "../Copyright";
5
+ import EmbedNavigation from "../EmbedNavigation";
6
+ import ExportMenuButton from "../ExportMenuButton";
7
+ import GeolocationButton from "../GeolocationButton";
8
+ import LayerTreeButton from "../LayerTreeButton";
9
+ import Map from "../Map";
10
+ import Overlay from "../Overlay";
11
+ import OverlayContent from "../OverlayContent";
12
+ import RvfMainLinkButton from "../RvfMainLinkButton";
13
+ import RvfPoisLayer from "../RvfPoisLayer";
14
+ import RvfSelectedFeatureHighlightLayer from "../RvfSelectedFeatureHighlightLayer";
15
+ import RvfSellingPointsLayer from "../RvfSellingPointsLayer";
16
+ import RvfSharedMobilityLayerGroup from "../RvfSharedMobilityLayerGroup";
17
+ import RvfTarifZonenLayer from "../RvfTarifZonenLayer";
18
+ import ScaleLine from "../ScaleLine";
19
+ import Search from "../Search";
20
+ import SearchButton from "../SearchButton";
21
+ import ShareMenuButton from "../ShareMenuButton";
22
+ import StationsLayer from "../StationsLayer";
23
+ import useMapContext from "../utils/hooks/useMapContext";
24
+ import ZoomButtons from "../ZoomButtons";
25
+
26
+ import type { HTMLAttributes } from "preact";
27
+
28
+ const scrollableHandlerProps = {
29
+ style: { width: "calc(100% - 60px)" },
30
+ };
31
+
32
+ const stationsLayerProps = () => {
33
+ return {
34
+ layersFilter: ({ metadata }) => {
35
+ return metadata?.["general.filter"] === "stations";
36
+ },
37
+ };
38
+ };
39
+
40
+ function RvfMapLayout({
41
+ className,
42
+ ...props
43
+ }: { className?: string } & HTMLAttributes<HTMLDivElement>) {
44
+ const {
45
+ hasDetails,
46
+ hasGeolocation,
47
+ hasLayerTree,
48
+ hasPrint,
49
+ hasRealtime,
50
+ hasSearch,
51
+ hasShare,
52
+ hasToolbar,
53
+ isEmbed,
54
+ isOverlayOpen,
55
+ isSearchOpen,
56
+ mainlink,
57
+ } = useMapContext();
58
+
59
+ return (
60
+ <div
61
+ className={twMerge(
62
+ "relative flex size-full flex-col text-base @lg/main:flex-row-reverse",
63
+ className,
64
+ )}
65
+ {...props}
66
+ >
67
+ <RvfSelectedFeatureHighlightLayer />
68
+ <StationsLayer minZoom={10} {...stationsLayerProps} />
69
+ <RvfTarifZonenLayer />
70
+ <RvfSellingPointsLayer />
71
+ <RvfPoisLayer />
72
+ <RvfSharedMobilityLayerGroup />
73
+
74
+ <Map className="relative flex-1 overflow-visible">
75
+ {isEmbed && <EmbedNavigation />}
76
+ {mainlink && (
77
+ <RvfMainLinkButton className="absolute inset-x-2 bottom-8 z-10" />
78
+ )}
79
+
80
+ <div className="pointer-events-none absolute inset-x-2 bottom-2 z-10 flex items-end justify-between gap-2 text-[10px]">
81
+ <ScaleLine className="bg-slate-50/70" />
82
+ <Copyright className="pointer-events-auto bg-slate-50/70" />
83
+ </div>
84
+ <div className="absolute top-2 right-2 z-10 flex">
85
+ {hasGeolocation && <GeolocationButton title={"Geolokalisierung"} />}
86
+ </div>
87
+ <div className="absolute right-2 bottom-10 z-10 flex flex-col justify-between gap-2">
88
+ <ZoomButtons />
89
+ </div>
90
+
91
+ {!hasToolbar && hasSearch && (
92
+ <div
93
+ className={twMerge(
94
+ "absolute top-2 right-2 left-2 z-10 max-w-96",
95
+ isOverlayOpen && "@lg:left-68",
96
+ )}
97
+ >
98
+ <Search />
99
+ </div>
100
+ )}
101
+ </Map>
102
+
103
+ <div className="pointer-events-none absolute top-2 bottom-2 left-2 z-10 flex flex-col gap-2">
104
+ {hasToolbar && (
105
+ <div
106
+ className={
107
+ "pointer-events-none relative z-10 w-fit rounded-2xl bg-black/10 p-0 backdrop-blur-sm *:pointer-events-auto"
108
+ }
109
+ // className="w-fit rounded-2xl bg-black/10 p-1 backdrop-blur-sm">
110
+ >
111
+ {hasSearch && (
112
+ <div
113
+ className={twMerge(
114
+ "absolute top-12 left-0 w-0 p-0 opacity-0 transition-all @sm:top-0 @sm:left-[calc(100%-43px)] @md:left-[calc(100%-47px)]",
115
+ isSearchOpen ? "w-64 opacity-100" : "",
116
+ )}
117
+ >
118
+ <Search
119
+ className={
120
+ "border-grey @container m-0 h-[40px] rounded-2xl border p-2 px-4 text-base @sm/main:h-[44px] @sm/main:rounded-l-none @sm/main:rounded-r-2xl @md/main:h-[48px]"
121
+ }
122
+ inputClassName="h-6 text-base"
123
+ inputContainerClassName="border-none"
124
+ resultClassName="text-base **:hover:cursor-pointer hover:text-red-500 p-2"
125
+ resultsContainerClassName="@container rounded-b-2xl max-h-[200px] overflow-y-auto border border-grey border-t-0 "
126
+ withResultsClassName="text-base !rounded-b-none"
127
+ />
128
+ </div>
129
+ )}
130
+
131
+ <div
132
+ className={twMerge(
133
+ "border-grey relative flex gap-[1px] overflow-hidden rounded-2xl border @sm/main:h-[44px] @md/main:h-[48px]",
134
+ "*:size-[38px] *:rounded-none *:border-none *:@sm/main:size-[42px] *:@md/main:!size-[46px]",
135
+ "*:first:!rounded-l-2xl",
136
+ "*:last:!rounded-r-2xl",
137
+ isSearchOpen
138
+ ? "@sm:rounded-r-none @sm:border-r-0 @sm:*:last:!rounded-r-none @sm:*:last:border-r-0"
139
+ : "",
140
+ )}
141
+ >
142
+ {hasPrint && <ExportMenuButton />}
143
+ {hasShare && <ShareMenuButton />}
144
+ {hasLayerTree && <LayerTreeButton />}
145
+ {hasSearch && <SearchButton />}
146
+ </div>
147
+ </div>
148
+ )}
149
+
150
+ {/* Desktop (>= lg) */}
151
+ <div
152
+ className={twMerge(
153
+ "pointer-events-none flex w-0 flex-1 flex-col overflow-hidden rounded-2xl",
154
+ isOverlayOpen ? "@lg:min-w-[320px]" : "p-0",
155
+ )}
156
+ style={{ containerType: "normal" }}
157
+ >
158
+ <Overlay
159
+ className={
160
+ "border-grey @container/overlay pointer-events-auto relative hidden flex-col overflow-hidden rounded-2xl border bg-white text-base shadow-lg @lg:flex"
161
+ }
162
+ ScrollableHandlerProps={scrollableHandlerProps}
163
+ >
164
+ <OverlayContent
165
+ hasDetails={hasDetails}
166
+ hasLayerTree={hasLayerTree}
167
+ hasPrint={hasPrint}
168
+ hasRealtime={hasRealtime}
169
+ hasSearch={false}
170
+ hasShare={hasShare}
171
+ />
172
+ </Overlay>
173
+ </div>
174
+ </div>
175
+
176
+ {/* Mobile */}
177
+ <Overlay
178
+ className={
179
+ isOverlayOpen
180
+ ? "absolute bottom-0 z-20 flex max-h-[70%] min-h-[75px] w-full flex-col border-t bg-white @lg:hidden"
181
+ : "@lg:hidden"
182
+ }
183
+ ScrollableHandlerProps={scrollableHandlerProps}
184
+ >
185
+ <OverlayContent
186
+ hasDetails={hasDetails}
187
+ hasLayerTree={hasLayerTree}
188
+ hasPrint={hasPrint}
189
+ hasRealtime={hasRealtime}
190
+ hasSearch={false} // The search could be in the overlay but we decide to put it in the toolbar for better UX
191
+ hasShare={hasShare}
192
+ />
193
+ </Overlay>
194
+ </div>
195
+ );
196
+ }
197
+
198
+ export default memo(RvfMapLayout);
@@ -0,0 +1 @@
1
+ export { default } from "./RvfMapLayout";