@geops/rvf-mobility-web-component 0.1.11 → 0.1.13

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 (69) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/docutils.js +9 -9
  3. package/index.html +1 -1
  4. package/index.js +332 -263
  5. package/input.css +2 -8
  6. package/package.json +15 -15
  7. package/scripts/build.mjs +3 -3
  8. package/scripts/dev.mjs +3 -1
  9. package/src/BaseLayer/BaseLayer.tsx +20 -12
  10. package/src/RealtimeLayer/RealtimeLayer.tsx +1 -2
  11. package/src/RouteSchedule/RouteSchedule.tsx +3 -1
  12. package/src/RouteStop/RouteStop.tsx +1 -1
  13. package/src/RvfButton/RvfButton.tsx +2 -2
  14. package/src/RvfCheckbox/RvfCheckbox.tsx +25 -0
  15. package/src/RvfCheckbox/index.tsx +1 -0
  16. package/src/RvfExportMenu/RvfExportMenu.tsx +31 -11
  17. package/src/RvfFloatingMenu/RvfFloatingMenu.tsx +44 -0
  18. package/src/RvfFloatingMenu/index.tsx +1 -0
  19. package/src/RvfIconButton/RvfIconButton.tsx +1 -1
  20. package/src/{LayerTree/LayerTree.tsx → RvfLayerTree/RvfLayerTree.tsx} +11 -18
  21. package/src/RvfLayerTree/TreeItem/TreeItem.tsx +130 -0
  22. package/src/RvfLayerTree/index.tsx +1 -0
  23. package/src/{LayerTree → RvfLayerTree}/layersTreeReducer.ts +1 -4
  24. package/src/RvfLineNetworkPlanLayer/RvfLineNetworkPlanLayer.tsx +49 -0
  25. package/src/RvfLineNetworkPlanLayer/index.tsx +1 -0
  26. package/src/RvfMobilityMap/RvfMobilityMap.tsx +100 -35
  27. package/src/RvfPoisLayer/RvfPoisLayer.tsx +6 -0
  28. package/src/RvfRadioButton/RvfRadioButton.tsx +16 -0
  29. package/src/RvfRadioButton/index.tsx +1 -0
  30. package/src/RvfSelect/RvfSelect.tsx +22 -0
  31. package/src/RvfSelect/index.tsx +1 -0
  32. package/src/RvfSellingPointsLayer/RvfSellingPointsLayer.tsx +45 -0
  33. package/src/RvfSellingPointsLayer/index.tsx +1 -0
  34. package/src/RvfSharedMobilityLayerGroup/RvfSharedMobilityLayerGroup.tsx +93 -44
  35. package/src/RvfSingleClickListener/RvfSingleClickListener.tsx +20 -3
  36. package/src/RvfTarifZonenLayer/RvfTarifZonenLayer.tsx +48 -0
  37. package/src/RvfTarifZonenLayer/index.tsx +1 -0
  38. package/src/RvfTopics/RvfTopics.tsx +44 -0
  39. package/src/RvfTopics/index.tsx +1 -0
  40. package/src/icons/ArrowDown/ArrowDown.tsx +22 -0
  41. package/src/icons/ArrowDown/down-open.svg +7 -0
  42. package/src/icons/ArrowDown/index.tsx +1 -0
  43. package/src/icons/ArrowUp/ArrowUp.tsx +22 -0
  44. package/src/icons/ArrowUp/index.tsx +1 -0
  45. package/src/icons/ArrowUp/up-open.svg +7 -0
  46. package/src/icons/Bicycle/verkehrstraeger-rad-2px-white.svg +19 -0
  47. package/src/icons/Car/verkehrstraeger-auto-2px-white.svg +14 -0
  48. package/src/icons/CargoBicycle/verkehrstraeger-lastenrad-2px-white.svg +27 -0
  49. package/src/icons/DownOpen/DownOpen.tsx +24 -0
  50. package/src/icons/DownOpen/down-open.svg +7 -0
  51. package/src/icons/DownOpen/index.tsx +1 -0
  52. package/src/icons/Ok/ok-grey.svg +7 -0
  53. package/src/icons/Ok/ok.svg +4 -0
  54. package/src/icons/Scooter/scooter.svg +10 -0
  55. package/src/utils/constants.ts +2 -0
  56. package/src/utils/createMobiDataBwWfsLayer.ts +31 -23
  57. package/src/utils/createSharedMobilityLayer.ts +176 -0
  58. package/src/utils/exportPdf.ts +20 -29
  59. package/src/utils/getAllLayers.ts +25 -0
  60. package/src/utils/hooks/useRvfContext.tsx +33 -0
  61. package/tailwind.config.mjs +31 -50
  62. package/src/LayerTree/TreeItem/TreeItem.tsx +0 -145
  63. package/src/LayerTree/TreeItemContainer/TreeItemContainer.tsx +0 -16
  64. package/src/LayerTree/TreeItemContainer/index.tsx +0 -1
  65. package/src/LayerTree/index.tsx +0 -1
  66. package/src/TopicMenu/TopicMenu.tsx +0 -143
  67. package/src/TopicMenu/index.tsx +0 -1
  68. /package/src/{LayerTree → RvfLayerTree}/TreeItem/index.tsx +0 -0
  69. /package/src/{LayerTree → RvfLayerTree}/layersTreeContext.ts +0 -0
package/input.css CHANGED
@@ -35,14 +35,8 @@ html {
35
35
  h4 {
36
36
  @apply text-lg;
37
37
  }
38
- button {
38
+ /* button {
39
39
  @apply text-button font-semibold;
40
- }
41
- }
42
-
43
- .button-map {
44
- @apply bg-blue-500 text-white;
45
- background: lightgray;
46
- z-index: 5;
40
+ } */
47
41
  }
48
42
 
package/package.json CHANGED
@@ -2,16 +2,16 @@
2
2
  "name": "@geops/rvf-mobility-web-component",
3
3
  "license": "UNLICENSED",
4
4
  "description": "Web components for rvf in the domains of mobility and logistics.",
5
- "version": "0.1.11",
5
+ "version": "0.1.13",
6
6
  "homepage": "https://rvf-mobility-web-component-geops.vercel.app/",
7
7
  "type": "module",
8
8
  "main": "index.js",
9
9
  "dependencies": {
10
10
  "jspdf": "^2.5.2",
11
11
  "maplibre-gl": "^4.7.1",
12
- "mobility-toolbox-js": "3.1.0-beta.2",
12
+ "mobility-toolbox-js": "3.1.0",
13
13
  "ol": "^10.3.1",
14
- "preact": "^10.25.1",
14
+ "preact": "^10.25.4",
15
15
  "preact-custom-element": "^4.3.0",
16
16
  "react": "npm:@preact/compat@^18.3.1",
17
17
  "react-dom": "npm:@preact/compat@^18.3.1",
@@ -19,24 +19,24 @@
19
19
  "rosetta": "^1.1.0"
20
20
  },
21
21
  "devDependencies": {
22
- "@commitlint/cli": "^19.6.0",
22
+ "@commitlint/cli": "^19.6.1",
23
23
  "@commitlint/config-conventional": "^19.6.0",
24
- "@eslint/js": "^9.16.0",
24
+ "@eslint/js": "^9.17.0",
25
25
  "@tailwindcss/container-queries": "^0.1.1",
26
26
  "@testing-library/preact": "^3.2.4",
27
- "@types/geojson": "^7946.0.14",
27
+ "@types/geojson": "^7946.0.15",
28
28
  "@types/jest": "^29.5.14",
29
29
  "@types/preact-custom-element": "^4.0.4",
30
- "concurrently": "^9.1.0",
31
- "esbuild": "^0.24.0",
30
+ "concurrently": "^9.1.2",
31
+ "esbuild": "^0.24.2",
32
32
  "esbuild-sass-plugin": "^3.3.1",
33
- "eslint": "^9.16.0",
33
+ "eslint": "^9.17.0",
34
34
  "eslint-config-prettier": "9.1.0",
35
35
  "eslint-plugin-jsx-a11y": "^6.10.2",
36
- "eslint-plugin-perfectionist": "^4.2.0",
36
+ "eslint-plugin-perfectionist": "^4.5.0",
37
37
  "eslint-plugin-prettier": "^5.2.1",
38
- "eslint-plugin-react": "^7.37.2",
39
- "eslint-plugin-react-hooks": "^5.1.0-rc.0",
38
+ "eslint-plugin-react": "^7.37.3",
39
+ "eslint-plugin-react-hooks": "^5.1.0",
40
40
  "eslint-plugin-tailwindcss": "^3.17.5",
41
41
  "fixpack": "^4.0.0",
42
42
  "generact": "^0.4.0",
@@ -46,13 +46,13 @@
46
46
  "jest-environment-jsdom": "^29.7.0",
47
47
  "jest-preset-preact": "^4.1.1",
48
48
  "next": "15.0.3",
49
- "preact-render-to-string": "^6.5.11",
49
+ "preact-render-to-string": "^6.5.12",
50
50
  "prettier": "^3.4.2",
51
51
  "standard-version": "^9.5.0",
52
- "tailwindcss": "^3.4.16",
52
+ "tailwindcss": "^3.4.17",
53
53
  "ts-jest": "^29.2.5",
54
54
  "typescript": "^5.7.2",
55
- "typescript-eslint": "^8.17.0"
55
+ "typescript-eslint": "^8.19.0"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "yarn build:css && yarn build:js",
package/scripts/build.mjs CHANGED
@@ -1,15 +1,15 @@
1
- /* eslint-disable import/no-extraneous-dependencies */
2
1
  import * as esbuild from "esbuild";
3
2
  import { sassPlugin } from "esbuild-sass-plugin";
4
3
 
5
4
  await esbuild.build({
6
- entryPoints: ["./src/index.js"],
7
5
  bundle: true,
8
- minify: true,
6
+ entryPoints: ["./src/index.js"],
9
7
  external: ["mapbox-gl"],
10
8
  loader: {
11
9
  ".png": "dataurl",
10
+ ".svg": "dataurl",
12
11
  },
12
+ minify: true,
13
13
  outfile: "index.js",
14
14
  plugins: [sassPlugin({ type: "css-text" })],
15
15
  sourcemap: false,
package/scripts/dev.mjs CHANGED
@@ -1,4 +1,3 @@
1
- /* eslint-disable import/no-extraneous-dependencies */
2
1
  import * as esbuild from "esbuild";
3
2
  import { sassPlugin } from "esbuild-sass-plugin";
4
3
 
@@ -8,6 +7,7 @@ const ctx = await esbuild.context({
8
7
  external: ["mapbox-gl"],
9
8
  loader: {
10
9
  ".png": "dataurl",
10
+ ".svg": "dataurl",
11
11
  },
12
12
  outfile: "index.js",
13
13
  plugins: [sassPlugin({ type: "css-text" })],
@@ -19,6 +19,8 @@ const { host, port } = await ctx.serve({
19
19
  });
20
20
 
21
21
  await ctx.watch();
22
+
23
+ // eslint-disable-next-line no-undef
22
24
  console.log(
23
25
  `watching... and running at ${
24
26
  host === "0.0.0.0" ? "http://localhost" : host
@@ -1,34 +1,42 @@
1
1
  import { MaplibreLayer } from "mobility-toolbox-js/ol";
2
2
  import { MaplibreLayerOptions } from "mobility-toolbox-js/ol/layers/MaplibreLayer";
3
- import { Layer } from "ol/layer";
4
3
  import { memo } from "preact/compat";
5
- import { useEffect } from "preact/hooks";
4
+ import { useEffect, useMemo } from "preact/hooks";
6
5
 
7
6
  import useMapContext from "../utils/hooks/useMapContext";
8
7
 
9
- function BaseLayer(props: MaplibreLayerOptions) {
8
+ export type BaseLayerProps = MaplibreLayerOptions;
9
+
10
+ function BaseLayer(props: BaseLayerProps) {
10
11
  const { apikey, baselayer, map, mapsurl, setBaseLayer } = useMapContext();
11
- useEffect(() => {
12
- if (!map || !baselayer || !apikey) {
12
+
13
+ const layer = useMemo(() => {
14
+ if (!baselayer || !apikey) {
13
15
  return;
14
16
  }
15
- const layer = new MaplibreLayer({
17
+ return new MaplibreLayer({
16
18
  apiKey: apikey,
17
19
  style: baselayer,
18
20
  url: mapsurl,
21
+ zIndex: 0,
19
22
  ...(props || {}),
20
23
  });
21
- const baseLayer = layer as unknown as Layer;
24
+ }, [baselayer, apikey, props, mapsurl]);
22
25
 
23
- // TODO: find why the setZIndex is not found
24
- baseLayer.setZIndex(0);
25
- map.addLayer(baseLayer);
26
+ useEffect(() => {
26
27
  setBaseLayer(layer);
28
+ }, [layer, setBaseLayer]);
29
+
30
+ useEffect(() => {
31
+ if (!map || !layer) {
32
+ return;
33
+ }
34
+ map.addLayer(layer);
27
35
 
28
36
  return () => {
29
- map?.removeLayer(baseLayer);
37
+ map.removeLayer(layer);
30
38
  };
31
- }, [map, baselayer, apikey, setBaseLayer, props, mapsurl]);
39
+ }, [map, layer]);
32
40
 
33
41
  return null;
34
42
  }
@@ -50,6 +50,7 @@ function RealtimeLayer(props: RealtimeLayerProps) {
50
50
  : undefined,
51
51
  tenant,
52
52
  url: realtimeurl,
53
+ zIndex: 1,
53
54
  ...props,
54
55
  styleOptions: {
55
56
  getDelayColor: getDelayColorForVehicle,
@@ -61,8 +62,6 @@ function RealtimeLayer(props: RealtimeLayerProps) {
61
62
  },
62
63
  });
63
64
 
64
- layer.setZIndex(1);
65
-
66
65
  return layer;
67
66
  }, [apikey, mots, realtimeurl, tenant, props]);
68
67
 
@@ -24,8 +24,10 @@ function RouteSchedule(props: RouteScheduleProps) {
24
24
  }
25
25
  const nextStation = elt.querySelector("[data-station-passed=false]");
26
26
  if (nextStation) {
27
- nextStation.scrollIntoView({
27
+ // We use scrollTo avoid scrolling the entire window.
28
+ (nextStation.parentNode as Element).scrollTo({
28
29
  behavior: "smooth",
30
+ top: (nextStation as HTMLElement).offsetTop || 0,
29
31
  });
30
32
  }
31
33
  clearInterval(interval);
@@ -110,7 +110,7 @@ function RouteStop({
110
110
  <RouteStopTime className="ml-4 flex w-10 shrink-0 flex-col justify-center text-xs" />
111
111
  <RouteStopDelay className="flex w-8 shrink-0 flex-col justify-center text-[0.6rem]" />
112
112
  <RouteStopProgress className="relative flex w-8 shrink-0 items-center" />
113
- <RouteStopStation className="flex grow flex-col items-start justify-center pr-2 text-sm font-medium" />
113
+ <RouteStopStation className="flex grow flex-col items-start justify-center pr-2" />
114
114
  </>
115
115
  )}
116
116
  </button>
@@ -9,12 +9,12 @@ export type RvfButtonProps = {
9
9
  PreactDOMAttributes;
10
10
 
11
11
  const baseClasses =
12
- "flex h-8 md:h-9 lg:h-10 px-5 py-1.75 max-h-button items-center justify-center rounded-full border";
12
+ "flex text-[14px] @sm/main:text-[16px] @md/main:text-[18px] h-[32px] @sm/main:h-[36px] @md/main:h-[40px] px-[22px] @sm/main:px-[22.5px] @md/main:px-[27.5px] pt-[7px] pb-[5px] @sm/main:pt-[8px] @sm/main:pb-[6px] @md/main:pt-[8.5px] @md/main:pb-[6.5px] items-center justify-center rounded-full border font-semibold text-button";
13
13
 
14
14
  export const themes = {
15
15
  primary: {
16
16
  classes:
17
- "border-red bg-red text-white disabled:bg-lightgrey disabled:border-lightgrey hover:bg-darkred hover:border-darkred active:bg-lightred active:border-lightred",
17
+ "border-red bg-red text-white disabled:bg-lightgrey disabled:border-lightgrey hover:bg-darkred hover:border-darkred active:bg-lightred active:border-lightred ",
18
18
  selectedClasses: "bg-darkred border-darkred",
19
19
  },
20
20
  secondary: {
@@ -0,0 +1,25 @@
1
+ import type { JSX } from "preact";
2
+
3
+ import { memo } from "preact/compat";
4
+
5
+ // @ts-expect-error - required for htm to resolve the JSX pragma
6
+ import ok from "../icons/Ok/ok-grey.svg";
7
+
8
+ export type RvfCheckboxProps = {} & JSX.InputHTMLAttributes<HTMLInputElement>;
9
+
10
+ function RvfCheckbox({ className, ...props }: RvfCheckboxProps) {
11
+ return (
12
+ <input
13
+ className={`
14
+ box-border size-[20px] flex-none cursor-pointer appearance-none rounded border-2 border-grey bg-white bg-contain bg-center text-grey disabled:cursor-default disabled:border-lightgrey ${className}`}
15
+ style={{
16
+ backgroundImage:
17
+ props.checked && !props.disabled ? `url('` + ok + `')` : "",
18
+ }}
19
+ {...props}
20
+ type="checkbox"
21
+ />
22
+ );
23
+ }
24
+
25
+ export default memo(RvfCheckbox);
@@ -0,0 +1 @@
1
+ export { default } from "./RvfCheckbox";
@@ -4,15 +4,19 @@ import { memo, useId, useState } from "preact/compat";
4
4
 
5
5
  import Cancel from "../icons/Cancel";
6
6
  import RvfButton from "../RvfButton";
7
+ import RvfCheckbox from "../RvfCheckbox";
7
8
  import RvfIconButton from "../RvfIconButton";
9
+ import RvfSelect from "../RvfSelect";
10
+ import { LAYER_PROP_IS_EXPORTING } from "../utils/constants";
8
11
  import exportPdf from "../utils/exportPdf";
12
+ import getAllLayers from "../utils/getAllLayers";
9
13
  import useMapContext from "../utils/hooks/useMapContext";
10
14
  import useRvfContext from "../utils/hooks/useRvfContext";
11
15
 
12
16
  export type RvfExportMenuButtonProps = JSX.HTMLAttributes<HTMLDivElement> &
13
17
  PreactDOMAttributes;
14
18
 
15
- const formats = ["A4", "A1", "A3", "A0"];
19
+ const formats = ["A4", "A3", "A1", "A0"];
16
20
 
17
21
  function RvfExportMenu({ ...props }: RvfExportMenuButtonProps) {
18
22
  const { setIsExportMenuOpen } = useRvfContext();
@@ -28,8 +32,9 @@ function RvfExportMenu({ ...props }: RvfExportMenuButtonProps) {
28
32
  <div className={"flex h-full flex-col gap-2 p-2"}>
29
33
  {/* <!-- Header --> */}
30
34
  <div className={"flex flex-row items-center justify-between gap-2"}>
31
- <h1>Export </h1>
35
+ <span className={"font-bold"}>Drucken </span>
32
36
  <RvfIconButton
37
+ className={"!size-[32px]"}
33
38
  onClick={() => {
34
39
  setIsExportMenuOpen(false);
35
40
  }}
@@ -39,31 +44,29 @@ function RvfExportMenu({ ...props }: RvfExportMenuButtonProps) {
39
44
  </div>
40
45
  {/* <!-- Content --> */}
41
46
  <div className="flex flex-1 flex-col gap-2">
42
- <div className={"flex gap-2"}>
43
- <input
47
+ <div className={"flex flex-wrap items-center gap-2"}>
48
+ <RvfCheckbox
44
49
  checked={useMaxExtent}
45
50
  id={checkboxId}
46
51
  onChange={() => {
47
- setUseMaxExtent(!useMaxExtent);
52
+ return setUseMaxExtent(!useMaxExtent);
48
53
  }}
49
- type="checkbox"
50
54
  />
51
55
  <label htmlFor={checkboxId}>Ganze Region exportieren</label>
52
56
  </div>
53
- <div className={"flex gap-2"}>
57
+ <div className={"flex flex-wrap items-center gap-2"}>
54
58
  <label htmlFor={selectId}>Format:</label>
55
- <select
59
+ <RvfSelect
56
60
  className={"w-24"}
57
61
  id={selectId}
58
62
  onChange={(evt) => {
59
63
  setFormat((evt.target as HTMLSelectElement).value);
60
64
  }}
61
- value={format}
62
65
  >
63
66
  {formats.map((format) => {
64
67
  return <option key={format}>{format}</option>;
65
68
  })}
66
- </select>
69
+ </RvfSelect>
67
70
  </div>
68
71
  </div>
69
72
  {/* <!-- Footer --> */}
@@ -73,7 +76,24 @@ function RvfExportMenu({ ...props }: RvfExportMenuButtonProps) {
73
76
  onClick={async () => {
74
77
  setIsExportingError(false);
75
78
  setIsExporting(true);
76
- const result = await exportPdf(map, { format }, { useMaxExtent });
79
+ const result = await exportPdf(
80
+ map,
81
+ { format },
82
+ {
83
+ onAfter: (map, layers) => {
84
+ getAllLayers(layers).forEach((layer) => {
85
+ layer.set(LAYER_PROP_IS_EXPORTING, false);
86
+ });
87
+ },
88
+
89
+ onBefore: (map, layers) => {
90
+ getAllLayers(layers).forEach((layer) => {
91
+ layer.set(LAYER_PROP_IS_EXPORTING, true);
92
+ });
93
+ },
94
+ useMaxExtent,
95
+ },
96
+ );
77
97
  setTimeout(() => {
78
98
  setIsExporting(false);
79
99
  setIsExportingError(!result);
@@ -0,0 +1,44 @@
1
+ import type { JSX, PreactDOMAttributes } from "preact";
2
+
3
+ import ArrowDown from "../icons/ArrowDown";
4
+ import ArrowUp from "../icons/ArrowUp";
5
+ import useMapContext from "../utils/hooks/useMapContext";
6
+
7
+ export type RvfFloatingMenuProps = {
8
+ isOpen: boolean;
9
+ onClick: () => void;
10
+ } & JSX.HTMLAttributes<HTMLDivElement> &
11
+ PreactDOMAttributes;
12
+
13
+ function RvfFloatingMenu({
14
+ children,
15
+ isOpen,
16
+ onClick,
17
+ title,
18
+ }: RvfFloatingMenuProps) {
19
+ const { map } = useMapContext();
20
+
21
+ if (!map) {
22
+ return null;
23
+ }
24
+
25
+ return (
26
+ <div className="pointer-events-none absolute bottom-8 left-2 top-2 z-10 flex flex-col overflow-hidden rounded-lg">
27
+ <div className="pointer-events-auto max-h-full w-48 rounded-lg border border-grey bg-white shadow-lg @lg/main:w-52">
28
+ <button
29
+ className="flex w-full items-center justify-between px-2 py-1.5 font-bold"
30
+ onClick={onClick}
31
+ >
32
+ {title} {isOpen ? <ArrowDown /> : <ArrowUp />}
33
+ </button>
34
+ {!isOpen && (
35
+ <div className="flex h-[calc(100%-39px)] w-full flex-1 overflow-y-auto">
36
+ {children}
37
+ </div>
38
+ )}
39
+ </div>
40
+ </div>
41
+ );
42
+ }
43
+
44
+ export default RvfFloatingMenu;
@@ -0,0 +1 @@
1
+ export { default } from "./RvfFloatingMenu";
@@ -11,7 +11,7 @@ export type RvfIconButtonProps = {
11
11
  RvfButtonProps;
12
12
 
13
13
  const baseClasses =
14
- "flex h-8 w-8 md:h-9 md:w-9 lg:w-10 lg:h-10 p-1.75 max-w-button max-h-button items-center justify-center rounded-full border";
14
+ "flex size-[32px] @sm/main:size-[36px] @md/main:size-[40px] p-[7px] items-center justify-center rounded-full border";
15
15
 
16
16
  function RvfIconButton({
17
17
  children,
@@ -1,3 +1,5 @@
1
+ import { JSX, PreactDOMAttributes } from "preact";
2
+ import { memo } from "preact/compat";
1
3
  import { useEffect, useReducer } from "preact/hooks";
2
4
 
3
5
  import {
@@ -6,39 +8,30 @@ import {
6
8
  } from "./layersTreeContext";
7
9
  import layersTreeReducer from "./layersTreeReducer";
8
10
  import TreeItem, { TreeItemProps } from "./TreeItem/TreeItem";
9
- import TreeItemContainer from "./TreeItemContainer";
10
11
 
11
- export interface LayerTreeProps {
12
+ export type RvfLayerTreeProps = {
12
13
  layers: TreeItemProps[];
13
- }
14
+ } & JSX.HTMLAttributes<HTMLDivElement> &
15
+ PreactDOMAttributes;
14
16
 
15
- function LayerTree({ layers }: LayerTreeProps) {
17
+ function RvfLayerTree({ layers, ...props }: RvfLayerTreeProps) {
16
18
  const [tree, dispatch] = useReducer(layersTreeReducer, layers);
17
19
 
18
20
  useEffect(() => {
19
21
  dispatch({ payload: layers, type: "INIT" });
20
- console.log("INIT", layers);
21
22
  }, [layers]);
22
23
 
23
- const renderedLayers = layers.map((item, idx) => {
24
- return (
25
- <div className="border-b border-grey" key={idx}>
26
- <TreeItem {...item} />
27
- </div>
28
- );
29
- });
30
-
31
24
  return (
32
25
  <LayersTreeContext.Provider value={tree}>
33
26
  <LayersTreeDispatchContext.Provider value={dispatch}>
34
- <div className="flex flex-col">
35
- <TreeItemContainer selectionType={tree[0]?.selectionType}>
36
- {renderedLayers}
37
- </TreeItemContainer>
27
+ <div {...props}>
28
+ {layers.map((item) => {
29
+ return <TreeItem className="w-full" key={item.id} {...item} />;
30
+ })}
38
31
  </div>
39
32
  </LayersTreeDispatchContext.Provider>
40
33
  </LayersTreeContext.Provider>
41
34
  );
42
35
  }
43
36
 
44
- export default LayerTree;
37
+ export default memo(RvfLayerTree);
@@ -0,0 +1,130 @@
1
+ import type { JSX, PreactDOMAttributes } from "preact";
2
+
3
+ import BaseLayer from "ol/layer/Base";
4
+ import {
5
+ SVGProps,
6
+ useContext,
7
+ useEffect,
8
+ useId,
9
+ useState,
10
+ } from "preact/compat";
11
+
12
+ import ArrowDown from "../../icons/ArrowDown";
13
+ import ArrowUp from "../../icons/ArrowUp";
14
+ import RvfCheckbox from "../../RvfCheckbox";
15
+ import RvfRadioButton from "../../RvfRadioButton";
16
+ import { LayersTreeDispatchContext } from "../layersTreeContext";
17
+
18
+ export enum SelectionType {
19
+ CHECKBOX = "checkbox",
20
+ RADIO = "radio",
21
+ }
22
+
23
+ export type TreeItemProps = {
24
+ childItems: TreeItemProps[];
25
+ Icon?: (props: SVGProps<SVGSVGElement>) => preact.JSX.Element;
26
+ id: string;
27
+ isCollapsedOnControlClick?: boolean;
28
+ isControlChecked?: boolean;
29
+ layer?: BaseLayer;
30
+ onIconClick?: () => void;
31
+ selectionType: SelectionType;
32
+ title: string;
33
+ } & JSX.HTMLAttributes<HTMLButtonElement> &
34
+ PreactDOMAttributes;
35
+
36
+ function TreeItem({
37
+ childItems,
38
+ isCollapsedOnControlClick,
39
+ isControlChecked,
40
+ layer,
41
+ selectionType,
42
+ title,
43
+ }: TreeItemProps) {
44
+ const [isContainerVisible, setIsContainerVisible] = useState(true);
45
+ const dispatch = useContext(LayersTreeDispatchContext);
46
+ const inputId = useId();
47
+ const buttonId = useId();
48
+
49
+ useEffect(() => {
50
+ if (isCollapsedOnControlClick) {
51
+ setIsContainerVisible(isControlChecked);
52
+ }
53
+ }, [isControlChecked, isCollapsedOnControlClick, layer]);
54
+
55
+ const handleItemClick = () => {
56
+ setIsContainerVisible(!isContainerVisible);
57
+
58
+ if (isCollapsedOnControlClick && !isContainerVisible) {
59
+ dispatch({
60
+ payload: {
61
+ ...this["props"],
62
+ isControlChecked: true,
63
+ },
64
+ type: "SELECT_ITEM",
65
+ });
66
+ }
67
+ };
68
+
69
+ const handleSelectionChange = (event) => {
70
+ dispatch({
71
+ payload: {
72
+ ...this["props"],
73
+ isControlChecked: event.target.checked,
74
+ },
75
+ type: "SELECT_ITEM",
76
+ });
77
+ };
78
+
79
+ const renderedLayers = childItems
80
+ .filter(({ title }) => {
81
+ return !!title;
82
+ })
83
+ .map((item, idx) => {
84
+ return <TreeItem key={idx} {...item} />;
85
+ });
86
+
87
+ if (!title) {
88
+ return null;
89
+ }
90
+
91
+ return (
92
+ <div>
93
+ <div className="flex items-center gap-2 border-b py-2 pr-1">
94
+ {selectionType === SelectionType.RADIO ? (
95
+ <RvfRadioButton
96
+ checked={isControlChecked}
97
+ id={inputId}
98
+ onChange={handleSelectionChange}
99
+ />
100
+ ) : (
101
+ <RvfCheckbox
102
+ checked={isControlChecked}
103
+ id={inputId}
104
+ onChange={handleSelectionChange}
105
+ />
106
+ )}
107
+ <label
108
+ className={`flex-1 cursor-pointer`}
109
+ htmlFor={renderedLayers.length > 0 ? buttonId : inputId}
110
+ >
111
+ {title}
112
+ </label>
113
+ {renderedLayers.length > 0 && (
114
+ <button
115
+ className={`flex cursor-pointer items-center gap-2`}
116
+ id={buttonId}
117
+ onClick={handleItemClick}
118
+ >
119
+ {isContainerVisible ? <ArrowUp /> : <ArrowDown />}
120
+ </button>
121
+ )}
122
+ </div>
123
+ {isContainerVisible && renderedLayers.length > 0 && (
124
+ <div className="pl-6">{renderedLayers}</div>
125
+ )}
126
+ </div>
127
+ );
128
+ }
129
+
130
+ export default TreeItem;
@@ -0,0 +1 @@
1
+ export { default } from "./RvfLayerTree";
@@ -10,8 +10,6 @@ const initTree = (tree) => {
10
10
  initializedTree.childItems = tree;
11
11
  mapNode(initializedTree.childItems, initializedTree);
12
12
 
13
- console.log("initializedTree", initializedTree);
14
-
15
13
  return initializedTree;
16
14
  };
17
15
 
@@ -109,6 +107,7 @@ const updateRadioChildNodes = (parent) => {
109
107
  const updateCheckboxChildNodes = (parent) => {
110
108
  for (const child of parent.childItems) {
111
109
  child.isControlChecked = parent.isControlChecked;
110
+ child.layer.setVisible(child.isControlChecked);
112
111
 
113
112
  if (child.childItems.length) {
114
113
  updateChildNodes(child);
@@ -146,8 +145,6 @@ function layersTreeReducer(state = ROOT, action) {
146
145
  return initTree(action.payload);
147
146
  case "SELECT_ITEM":
148
147
  return setNewControlCheckedStatus(state, action.payload);
149
- case "SELECT_RADIO_ITEM":
150
- return setNewControlCheckedStatus(state, action.payload);
151
148
  default:
152
149
  return state;
153
150
  }