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

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 (80) hide show
  1. package/.github/CODEOWNERS +37 -0
  2. package/.github/workflows/conventional-pr-title.yml +36 -0
  3. package/CHANGELOG.md +55 -0
  4. package/README.md +3 -1
  5. package/doc/package.json +5 -5
  6. package/doc/src/app/components/GeopsMobilityDoc.tsx +19 -0
  7. package/docutils.js +198 -0
  8. package/index.html +48 -217
  9. package/index.js +683 -91
  10. package/input.css +15 -1
  11. package/jest-setup.js +3 -2
  12. package/package.json +9 -8
  13. package/scripts/dev.mjs +1 -1
  14. package/search.html +38 -69
  15. package/src/GeolocationButton/GeolocationButton.tsx +6 -17
  16. package/src/LayerTree/LayerTree.tsx +44 -0
  17. package/src/LayerTree/TreeItem/TreeItem.tsx +145 -0
  18. package/src/LayerTree/TreeItem/index.tsx +1 -0
  19. package/src/LayerTree/TreeItemContainer/TreeItemContainer.tsx +16 -0
  20. package/src/LayerTree/TreeItemContainer/index.tsx +1 -0
  21. package/src/LayerTree/index.tsx +1 -0
  22. package/src/LayerTree/layersTreeContext.ts +4 -0
  23. package/src/LayerTree/layersTreeReducer.ts +156 -0
  24. package/src/Map/Map.tsx +57 -12
  25. package/src/MobilityMap/MobilityMap.tsx +22 -9
  26. package/src/MobilityMap/index.css +0 -13
  27. package/src/RealtimeLayer/RealtimeLayer.tsx +1 -1
  28. package/src/RvfButton/RvfButton.tsx +45 -0
  29. package/src/RvfButton/index.tsx +1 -0
  30. package/src/RvfExportMenu/RvfExportMenu.tsx +95 -0
  31. package/src/RvfExportMenu/index.tsx +1 -0
  32. package/src/RvfExportMenuButton/RvfExportMenuButton.tsx +27 -0
  33. package/src/RvfExportMenuButton/index.tsx +1 -0
  34. package/src/RvfFeatureDetails/RvfFeatureDetails.tsx +29 -0
  35. package/src/RvfFeatureDetails/index.tsx +1 -0
  36. package/src/RvfIconButton/RvfIconButton.tsx +35 -0
  37. package/src/RvfIconButton/index.tsx +1 -0
  38. package/src/RvfMobilityMap/RvfMobilityMap.tsx +132 -52
  39. package/src/RvfMobilityMap/index.css +0 -13
  40. package/src/RvfModal/RvfModal.tsx +52 -0
  41. package/src/RvfModal/index.tsx +1 -0
  42. package/src/RvfPoisLayer/RvfPoisLayer.tsx +39 -0
  43. package/src/RvfPoisLayer/index.tsx +1 -0
  44. package/src/RvfSharedMobilityLayerGroup/RvfSharedMobilityLayerGroup.tsx +88 -0
  45. package/src/RvfSharedMobilityLayerGroup/index.tsx +1 -0
  46. package/src/RvfSingleClickListener/RvfSingleClickListener.tsx +137 -0
  47. package/src/RvfSingleClickListener/index.tsx +1 -0
  48. package/src/RvfZoomButtons/RvfZoomButtons.tsx +73 -0
  49. package/src/RvfZoomButtons/index.tsx +1 -0
  50. package/src/Search/Search.tsx +11 -9
  51. package/src/SingleClickListener/index.tsx +1 -1
  52. package/src/StationsLayer/StationsLayer.tsx +0 -1
  53. package/src/StopsSearch/StopsSearch.tsx +38 -6
  54. package/src/TopicMenu/TopicMenu.tsx +143 -0
  55. package/src/TopicMenu/index.tsx +1 -0
  56. package/src/icons/Cancel/Cancel.tsx +21 -0
  57. package/src/icons/Cancel/cancel.svg +7 -0
  58. package/src/icons/Cancel/index.tsx +1 -0
  59. package/src/icons/Download/Download.tsx +20 -0
  60. package/src/icons/Download/download.svg +15 -0
  61. package/src/icons/Download/index.tsx +1 -0
  62. package/src/icons/Elevator/Elevator.tsx +1 -1
  63. package/src/icons/Geolocation/Geolocation.tsx +21 -0
  64. package/src/icons/Geolocation/index.tsx +1 -0
  65. package/src/icons/Menu/Menu.tsx +32 -0
  66. package/src/icons/Menu/index.tsx +1 -0
  67. package/src/icons/Menu/menu.svg +9 -0
  68. package/src/icons/Minus/Minus.tsx +19 -0
  69. package/src/icons/Minus/index.tsx +1 -0
  70. package/src/icons/Minus/minus.svg +7 -0
  71. package/src/icons/Plus/Plus.tsx +19 -0
  72. package/src/icons/Plus/index.tsx +1 -0
  73. package/src/icons/Plus/plus.svg +7 -0
  74. package/src/index.tsx +2 -0
  75. package/src/utils/constants.ts +9 -0
  76. package/src/utils/createMobiDataBwWfsLayer.ts +120 -0
  77. package/src/utils/exportPdf.ts +677 -0
  78. package/src/utils/hooks/useRvfContext.tsx +37 -0
  79. package/src/utils/hooks/useUpdatePermalink.tsx +2 -9
  80. package/tailwind.config.mjs +60 -8
package/input.css CHANGED
@@ -3,14 +3,18 @@
3
3
  @tailwind utilities;
4
4
 
5
5
  ::-webkit-scrollbar {
6
- height: 3px;
7
6
  width: 3px;
7
+ height: 3px;
8
8
  }
9
9
 
10
10
  ::-webkit-scrollbar-thumb {
11
11
  background: gray;
12
12
  }
13
13
 
14
+ ::-webkit-scrollbar-track {
15
+ background: transparent;
16
+ }
17
+
14
18
  html {
15
19
  @apply text-base;
16
20
  }
@@ -31,4 +35,14 @@ html {
31
35
  h4 {
32
36
  @apply text-lg;
33
37
  }
38
+ button {
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;
34
47
  }
48
+
package/jest-setup.js CHANGED
@@ -1,4 +1,5 @@
1
- /* eslint-disable import/no-extraneous-dependencies */
2
1
  import "jest-canvas-mock";
3
2
 
4
- global.URL.createObjectURL = jest.fn(() => "fooblob");
3
+ global.URL.createObjectURL = jest.fn(() => {
4
+ return "fooblob";
5
+ });
package/package.json CHANGED
@@ -2,19 +2,20 @@
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.9",
5
+ "version": "0.1.11",
6
6
  "homepage": "https://rvf-mobility-web-component-geops.vercel.app/",
7
7
  "type": "module",
8
8
  "main": "index.js",
9
9
  "dependencies": {
10
+ "jspdf": "^2.5.2",
10
11
  "maplibre-gl": "^4.7.1",
11
- "mobility-toolbox-js": "3.0.0-beta.38",
12
- "ol": "^10.3.0",
12
+ "mobility-toolbox-js": "3.1.0-beta.2",
13
+ "ol": "^10.3.1",
13
14
  "preact": "^10.25.1",
14
15
  "preact-custom-element": "^4.3.0",
15
16
  "react": "npm:@preact/compat@^18.3.1",
16
17
  "react-dom": "npm:@preact/compat@^18.3.1",
17
- "react-icons": "^5.3.0",
18
+ "react-icons": "^5.4.0",
18
19
  "rosetta": "^1.1.0"
19
20
  },
20
21
  "devDependencies": {
@@ -32,7 +33,7 @@
32
33
  "eslint": "^9.16.0",
33
34
  "eslint-config-prettier": "9.1.0",
34
35
  "eslint-plugin-jsx-a11y": "^6.10.2",
35
- "eslint-plugin-perfectionist": "^4.1.2",
36
+ "eslint-plugin-perfectionist": "^4.2.0",
36
37
  "eslint-plugin-prettier": "^5.2.1",
37
38
  "eslint-plugin-react": "^7.37.2",
38
39
  "eslint-plugin-react-hooks": "^5.1.0-rc.0",
@@ -46,9 +47,9 @@
46
47
  "jest-preset-preact": "^4.1.1",
47
48
  "next": "15.0.3",
48
49
  "preact-render-to-string": "^6.5.11",
49
- "prettier": "^3.4.1",
50
+ "prettier": "^3.4.2",
50
51
  "standard-version": "^9.5.0",
51
- "tailwindcss": "^3.4.15",
52
+ "tailwindcss": "^3.4.16",
52
53
  "ts-jest": "^29.2.5",
53
54
  "typescript": "^5.7.2",
54
55
  "typescript-eslint": "^8.17.0"
@@ -70,7 +71,7 @@
70
71
  "publish:public:dryrun": "yarn release --dry-run",
71
72
  "release": "standard-version",
72
73
  "start": "concurrently \"yarn tailwind:component --watch\" \"yarn tailwind:website --watch\" \"yarn dev\"",
73
- "tailwind:component": "tailwindcss --output=src/style.css --content=src/**/*.tsx",
74
+ "tailwind:component": "tailwindcss --input=./input.css --output=src/style.css --content=src/**/*.tsx",
74
75
  "tailwind:website": "tailwindcss --input=./input.css --output=output.css --content=*.html --minify",
75
76
  "test": "TZ=UTC jest",
76
77
  "up": "yarn upgrade-interactive --latest",
package/scripts/dev.mjs CHANGED
@@ -3,8 +3,8 @@ import * as esbuild from "esbuild";
3
3
  import { sassPlugin } from "esbuild-sass-plugin";
4
4
 
5
5
  const ctx = await esbuild.context({
6
- entryPoints: ["./src/index.js"],
7
6
  bundle: true,
7
+ entryPoints: ["./src/index.js"],
8
8
  external: ["mapbox-gl"],
9
9
  loader: {
10
10
  ".png": "dataurl",
package/search.html CHANGED
@@ -16,17 +16,22 @@
16
16
  }
17
17
  </script>
18
18
  <script type="module" src="./index.js"></script>
19
+ <script src="./docutils.js"></script>
19
20
  <link rel="stylesheet" type="text/css" href="./output.css" />
20
21
  <style>
21
22
  ::-webkit-scrollbar {
22
- width: 10px;
23
+ width: 3px;
24
+ height: 3px;
23
25
  }
24
26
  a {
25
27
  text-decoration: underline;
26
28
  }
27
29
  </style>
28
30
  </head>
31
+ </head>
29
32
  <body class="p-8">
33
+ <!-- tailwind hack to add class used in docutils -->
34
+ <div class="border px-4 py-2 table-auto w-full flex gap-4 p-2 bg-black text-white hover:bg-gray-700" style="display:none;"></div>
30
35
  <div
31
36
  id="doc"
32
37
  style="display: none"
@@ -49,28 +54,19 @@
49
54
  </p>
50
55
 
51
56
  <h2 class="text-xl font-bold">Usage example</h2>
52
- <pre class="bg-slate-800 text-slate-200 p-4 rounded">
53
- &lt;script
54
- type=&quot;module&quot;
55
- src=&quot;https://www.unpkg.com/@geops/mobility-web-component&quot;&gt;
56
- &lt;/script&gt;
57
- &lt;geops-mobility-search
58
- apikey=&quot;YOUR_GEOPS_API_KEY&quot;
59
- limit=&quot;5&quot;
60
- mots=&quot;rail,bus&quot;
61
- style=&quot;display: block;width: 800px;height: 800px;&quot;&gt;
62
- &lt;/geops-mobility&gt;</pre
63
- >
57
+ <pre id="code" class="bg-slate-800 text-slate-200 p-4 rounded"></pre>
64
58
 
65
- <!-- Default -->
66
59
  <geops-mobility-search
67
60
  class="max-w-3xl block border"
68
61
  limit="5"
69
62
  mots="rail,bus"
70
63
  ></geops-mobility-search>
71
64
 
72
- <pre id="textarea" class="w-full h-96 p-2"></pre>
73
-
65
+ <br />
66
+ <h2 class="text-xl font-bold">Attributes</h2>
67
+ <div id="attributes"></div>
68
+ <h2 class="text-xl font-bold">Events</h2>
69
+ <div id="events"></div>
74
70
  <br />
75
71
  <br />
76
72
  <h1 class="text-xl font-bold">More mobility web components</h1>
@@ -80,65 +76,38 @@
80
76
  >
81
77
  </p>
82
78
  </div>
79
+ <br />
80
+ <br />
83
81
  <script type="text/javascript">
84
- let params = new URLSearchParams(window.location.search);
85
- const searchElement = document.querySelector("geops-mobility-search");
86
- const eventLog = document.querySelector("#textarea");
87
-
88
-
89
- // Listen window event
90
- window.addEventListener("message", (event) => {
91
- const { type } = event.data || {};
92
- console.log("message event: " + type, event.data);
93
- });
94
- </script>
95
-
96
- <script type="text/javascript">
97
- params = new URLSearchParams(window.location.search);
98
-
99
- // There should be only one webcompoennt on the html page at this point
100
- const doc = document.querySelectorAll("#doc");
82
+ const pkgSrc = "https://www.unpkg.com/@geops/mobility-web-component";
101
83
  const wc = document.querySelector("geops-mobility-search");
102
- if (params.get("fullscreen") === "true") {
103
- wc.parentElement.removeChild(wc);
104
- wc.className = "absolute w-full h-full inset-0";
105
- document.body.appendChild(wc);
106
- document.body.style = "padding:0;";
107
- } else {
108
- doc.forEach((d) => (d.style.display = "block"));
109
- }
110
- params.delete("fullscreen");
111
84
 
112
- // Apply all url parameters as attribute of the web component
113
- params.forEach((value, key) => {
114
- wc.setAttribute(key, value);
115
- });
116
-
117
- if (!wc.getAttribute("apikey")) {
118
- fetch("https://backend.developer.geops.io/publickey")
119
- .then((response) => response.json())
120
- .then((data) => {
121
- if (data && data.success) {
122
- wc.setAttribute("apikey", data.key);
123
- }
124
- });
125
- }
85
+ const attrs = [
86
+ "apikey",
87
+ "bbox",
88
+ "countrycode",
89
+ "event",
90
+ "field",
91
+ "limit",
92
+ "mots",
93
+ "onselect",
94
+ "params",
95
+ "prefagencies",
96
+ "reflocation",
97
+ "url",
98
+ ];
126
99
 
100
+ const events = [
101
+ "mwc:stopssearchselect",
102
+ ];
127
103
 
128
- // Listen to element event
129
- searchElement.addEventListener("mwc:stopssearchselect", (event) => {
130
- const data = event.data;
131
- if (!data) {
132
- eventLog.innerText = "";
133
- } else {
134
- eventLog.innerText =
135
- "Event " +
136
- event.type +
137
- " received :\n " +
138
- JSON.stringify(data, undefined, " ");
139
- window.top.postMessage(data, "*");
140
- }
104
+ document.querySelector('#attributes').innerHTML = generateAttributesTable(wc, attrs);
105
+ document.querySelector('#events').innerHTML = generateEventsTable(wc, events);
106
+ document.querySelector('#code').innerHTML = generateCodeText(wc, attrs, pkgSrc);
107
+ wc.addEventListener('mwc:attribute', (event) => {
108
+ document.querySelector('#code').innerHTML = generateCodeText(wc, attrs, pkgSrc);
141
109
  });
110
+ applyPermalinkParameters(wc);
142
111
  </script>
143
112
  </body>
144
113
  </html>
@@ -5,6 +5,8 @@ import { unByKey } from "ol/Observable";
5
5
  import { fromLonLat } from "ol/proj";
6
6
  import { useEffect, useMemo } from "preact/hooks";
7
7
 
8
+ import GeolocationIcon from "../icons/Geolocation";
9
+ import RvfIconButton from "../RvfIconButton";
8
10
  import useMapContext from "../utils/hooks/useMapContext";
9
11
 
10
12
  export type GeolocationButtonProps = JSX.HTMLAttributes<HTMLButtonElement> &
@@ -53,28 +55,15 @@ function GeolocationButton({ ...props }: GeolocationButtonProps) {
53
55
  }, [geolocation, isTracking]);
54
56
 
55
57
  return (
56
- <button
57
- className="rounded-full bg-white p-1 shadow-lg"
58
+ <RvfIconButton
59
+ className={isTracking ? "animate-pulse" : ""}
58
60
  onClick={() => {
59
61
  setIsTracking(!isTracking);
60
62
  }}
61
- type="button"
62
63
  {...props}
63
64
  >
64
- <svg
65
- className={isTracking ? "animate-pulse" : ""}
66
- fill="currentColor"
67
- focusable="false"
68
- height="1.5em"
69
- stroke="currentColor"
70
- strokeWidth="0"
71
- viewBox="0 0 512 512"
72
- width="1.5em"
73
- xmlns="http://www.w3.org/2000/svg"
74
- >
75
- <path d="M256 56c110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200-110.532 0-200-89.451-200-200 0-110.532 89.451-200 200-200m0-48C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 168c-44.183 0-80 35.817-80 80s35.817 80 80 80 80-35.817 80-80-35.817-80-80-80z" />
76
- </svg>
77
- </button>
65
+ <GeolocationIcon />
66
+ </RvfIconButton>
78
67
  );
79
68
  }
80
69
 
@@ -0,0 +1,44 @@
1
+ import { useEffect, useReducer } from "preact/hooks";
2
+
3
+ import {
4
+ LayersTreeContext,
5
+ LayersTreeDispatchContext,
6
+ } from "./layersTreeContext";
7
+ import layersTreeReducer from "./layersTreeReducer";
8
+ import TreeItem, { TreeItemProps } from "./TreeItem/TreeItem";
9
+ import TreeItemContainer from "./TreeItemContainer";
10
+
11
+ export interface LayerTreeProps {
12
+ layers: TreeItemProps[];
13
+ }
14
+
15
+ function LayerTree({ layers }: LayerTreeProps) {
16
+ const [tree, dispatch] = useReducer(layersTreeReducer, layers);
17
+
18
+ useEffect(() => {
19
+ dispatch({ payload: layers, type: "INIT" });
20
+ console.log("INIT", layers);
21
+ }, [layers]);
22
+
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
+ return (
32
+ <LayersTreeContext.Provider value={tree}>
33
+ <LayersTreeDispatchContext.Provider value={dispatch}>
34
+ <div className="flex flex-col">
35
+ <TreeItemContainer selectionType={tree[0]?.selectionType}>
36
+ {renderedLayers}
37
+ </TreeItemContainer>
38
+ </div>
39
+ </LayersTreeDispatchContext.Provider>
40
+ </LayersTreeContext.Provider>
41
+ );
42
+ }
43
+
44
+ export default LayerTree;
@@ -0,0 +1,145 @@
1
+ import type { JSX, PreactDOMAttributes } from "preact";
2
+
3
+ import BaseLayer from "ol/layer/Base";
4
+ import { SVGProps, useContext, useEffect, useState } from "preact/compat";
5
+
6
+ import { LayersTreeDispatchContext } from "../layersTreeContext";
7
+ import TreeItemContainer from "../TreeItemContainer";
8
+
9
+ export enum SelectionType {
10
+ CHECKBOX = "checkbox",
11
+ RADIO = "radio",
12
+ }
13
+
14
+ export type TreeItemProps = {
15
+ childItems: TreeItemProps[];
16
+ Icon?: (props: SVGProps<SVGSVGElement>) => preact.JSX.Element;
17
+ id: string;
18
+ isCollapsedOnControlClick?: boolean;
19
+ isControlChecked?: boolean;
20
+ layer?: BaseLayer;
21
+ onIconClick?: () => void;
22
+ selectionType: SelectionType;
23
+ title: string;
24
+ } & JSX.HTMLAttributes<HTMLButtonElement> &
25
+ PreactDOMAttributes;
26
+
27
+ function TreeItem({
28
+ childItems,
29
+ Icon,
30
+ isCollapsedOnControlClick,
31
+ isControlChecked,
32
+ layer,
33
+ onIconClick,
34
+ selectionType,
35
+ title,
36
+ }: TreeItemProps) {
37
+ const [isContainerVisible, setIsContainerVisible] = useState(true);
38
+ const dispatch = useContext(LayersTreeDispatchContext);
39
+
40
+ useEffect(() => {
41
+ if (isCollapsedOnControlClick) {
42
+ setIsContainerVisible(isControlChecked);
43
+ }
44
+ }, [isControlChecked, isCollapsedOnControlClick]);
45
+
46
+ const handleItemClick = () => {
47
+ setIsContainerVisible(!isContainerVisible);
48
+
49
+ if (isCollapsedOnControlClick && !isContainerVisible) {
50
+ if (selectionType === SelectionType.RADIO) {
51
+ dispatch({
52
+ payload: {
53
+ ...this["props"],
54
+ isControlChecked: true,
55
+ },
56
+ type: "SELECT_RADIO_ITEM",
57
+ });
58
+ }
59
+ }
60
+ };
61
+
62
+ const handleSelectionChange = (event) => {
63
+ console.log("TREE LAYER", layer);
64
+
65
+ if (selectionType === SelectionType.RADIO) {
66
+ dispatch({
67
+ payload: {
68
+ ...this["props"],
69
+ isControlChecked: event.target.checked,
70
+ },
71
+ type: "SELECT_RADIO_ITEM",
72
+ });
73
+ } else {
74
+ dispatch({
75
+ payload: {
76
+ ...this["props"],
77
+ isControlChecked: event.target.checked,
78
+ },
79
+ type: "SELECT_ITEM",
80
+ });
81
+ }
82
+ };
83
+
84
+ const arrowShowedClass = childItems.length > 0 ? "block" : "hidden";
85
+ const changeArrowClass = isContainerVisible
86
+ ? "border-l border-t"
87
+ : "border-b border-r";
88
+ const radioButtonClass =
89
+ "appearance-none w-inputControl h-inputControl border-2 border-grey rounded-full mr-2 peer";
90
+ const checkboxClass =
91
+ "appearance-none w-inputControl h-inputControl border-2 border-grey rounded mr-2 peer";
92
+
93
+ const innerCircle = (
94
+ <span className="pointer-events-none absolute ml-4px h-innerControl w-innerControl rounded-full peer-checked:bg-red" />
95
+ );
96
+ const checkSign = (
97
+ <span className="pointer-events-none absolute mb-px ml-7px hidden h-innerControl w-check rotate-45 border-b-2 border-r-2 border-grey peer-checked:block" />
98
+ );
99
+
100
+ const renderedLayers = childItems.map((item, idx) => {
101
+ return <TreeItem key={idx} {...item} />;
102
+ });
103
+
104
+ return (
105
+ <div className="flex flex-col pl-6">
106
+ <div className="flex h-8 cursor-pointer items-center">
107
+ <div className="relative flex items-center">
108
+ <input
109
+ checked={isControlChecked}
110
+ className={
111
+ selectionType === SelectionType.RADIO
112
+ ? radioButtonClass
113
+ : checkboxClass
114
+ }
115
+ onChange={handleSelectionChange}
116
+ type={selectionType}
117
+ />
118
+ {selectionType === SelectionType.RADIO ? innerCircle : checkSign}
119
+ </div>
120
+ <div
121
+ onClick={handleItemClick}
122
+ onKeyDown={handleItemClick}
123
+ role="button"
124
+ tabIndex={0}
125
+ >
126
+ <div className="flex items-center">
127
+ {title}
128
+ <div
129
+ className={`mb-0.5 ml-2 size-1.5 rotate-45 border-grey ${arrowShowedClass} ${changeArrowClass}`}
130
+ ></div>
131
+ </div>
132
+ </div>
133
+ <span>{Icon ? <Icon onClick={onIconClick} /> : null}</span>
134
+ </div>
135
+ <TreeItemContainer
136
+ className={isContainerVisible ? "block" : "hidden"}
137
+ selectionType={childItems[0]?.selectionType}
138
+ >
139
+ {renderedLayers}
140
+ </TreeItemContainer>
141
+ </div>
142
+ );
143
+ }
144
+
145
+ export default TreeItem;
@@ -0,0 +1 @@
1
+ export { default } from "./TreeItem";
@@ -0,0 +1,16 @@
1
+ import { type JSX, type PreactDOMAttributes } from "preact";
2
+
3
+ import { SelectionType } from "../TreeItem/TreeItem";
4
+
5
+ export type TreeItemContainerProps = {
6
+ selectionType: SelectionType;
7
+ } & JSX.HTMLAttributes<HTMLButtonElement> &
8
+ PreactDOMAttributes;
9
+
10
+ function TreeItemContainer({ children, className }: TreeItemContainerProps) {
11
+ const classes = `flex flex-col ${className || ""}`;
12
+
13
+ return <div className={classes}>{children}</div>;
14
+ }
15
+
16
+ export default TreeItemContainer;
@@ -0,0 +1 @@
1
+ export { default } from "./TreeItemContainer";
@@ -0,0 +1 @@
1
+ export { default } from "./LayerTree";
@@ -0,0 +1,4 @@
1
+ import { createContext } from "preact";
2
+
3
+ export const LayersTreeContext = createContext(null);
4
+ export const LayersTreeDispatchContext = createContext(null);
@@ -0,0 +1,156 @@
1
+ import { SelectionType } from "./TreeItem/TreeItem";
2
+
3
+ const ROOT = {
4
+ childItems: [],
5
+ parent: null,
6
+ };
7
+
8
+ const initTree = (tree) => {
9
+ const initializedTree = { ...ROOT };
10
+ initializedTree.childItems = tree;
11
+ mapNode(initializedTree.childItems, initializedTree);
12
+
13
+ console.log("initializedTree", initializedTree);
14
+
15
+ return initializedTree;
16
+ };
17
+
18
+ const mapNode = (childItems, parent) => {
19
+ for (const child of childItems) {
20
+ child.parent = parent;
21
+
22
+ if (child.childItems.length) {
23
+ mapNode(child.childItems, child);
24
+ }
25
+ }
26
+ };
27
+
28
+ const findNodeInTree = (node, nodeToFind) => {
29
+ if (node.id === nodeToFind.id) {
30
+ return node;
31
+ }
32
+
33
+ if (node.childItems.length) {
34
+ for (const child of node.childItems) {
35
+ const res = findNodeInTree(child, nodeToFind);
36
+ if (res) {
37
+ return res;
38
+ }
39
+ }
40
+ }
41
+ };
42
+
43
+ const setNewControlCheckedStatus = (tree, newItem) => {
44
+ const currentItem = findNodeInTree(tree, newItem);
45
+
46
+ if (currentItem) {
47
+ updateCheckedControlStatus(currentItem, newItem, false);
48
+ }
49
+
50
+ return { ...tree };
51
+ };
52
+
53
+ const updateCheckedControlStatus = (currentItem, newItem, isParentUpdate) => {
54
+ if (newItem.isControlChecked) {
55
+ if (newItem.selectionType === SelectionType.CHECKBOX) {
56
+ currentItem.isControlChecked = newItem.isControlChecked;
57
+ currentItem.layer.setVisible(currentItem.isControlChecked);
58
+ } else {
59
+ for (const child of currentItem.parent.childItems) {
60
+ child.isControlChecked = child.id === currentItem.id;
61
+
62
+ if (!child.isControlChecked) {
63
+ updateRadioChildNodes(child);
64
+ }
65
+ }
66
+ }
67
+ } else {
68
+ if (newItem.selectionType === SelectionType.CHECKBOX) {
69
+ currentItem.isControlChecked = newItem.isControlChecked;
70
+ currentItem.layer.setVisible(currentItem.isControlChecked);
71
+ }
72
+ }
73
+
74
+ // check all children
75
+ if (currentItem.childItems.length && !isParentUpdate) {
76
+ updateChildNodes(currentItem);
77
+ }
78
+
79
+ // check all parents
80
+ updateParentNodes(currentItem);
81
+ };
82
+
83
+ const updateChildNodes = (node) => {
84
+ if (node.childItems[0].selectionType === SelectionType.RADIO) {
85
+ updateRadioChildNodes(node);
86
+ } else {
87
+ updateCheckboxChildNodes(node);
88
+ }
89
+ };
90
+
91
+ const updateRadioChildNodes = (parent) => {
92
+ if (parent.isControlChecked) {
93
+ for (let i = 0; i < parent.childItems.length; i++) {
94
+ parent.childItems[i].isControlChecked = i === 0;
95
+ }
96
+ } else {
97
+ for (const child of parent.childItems) {
98
+ child.isControlChecked = false;
99
+ }
100
+ }
101
+
102
+ if (parent.childItems.length) {
103
+ if (parent.childItems[0].childItems.length) {
104
+ updateChildNodes(parent.childItems[0]);
105
+ }
106
+ }
107
+ };
108
+
109
+ const updateCheckboxChildNodes = (parent) => {
110
+ for (const child of parent.childItems) {
111
+ child.isControlChecked = parent.isControlChecked;
112
+
113
+ if (child.childItems.length) {
114
+ updateChildNodes(child);
115
+ }
116
+ }
117
+ };
118
+
119
+ const updateParentNodes = (node) => {
120
+ if (node.parent) {
121
+ if (node.parent.selectionType === SelectionType.CHECKBOX) {
122
+ const newItem = {
123
+ ...node.parent,
124
+ isControlChecked: node.parent.childItems.some((child) => {
125
+ return child.isControlChecked;
126
+ }),
127
+ };
128
+ updateCheckedControlStatus(node.parent, newItem, true);
129
+ } else {
130
+ if (node?.parent?.parent) {
131
+ const newItem = {
132
+ ...node.parent,
133
+ isControlChecked: node.parent.childItems.some((child) => {
134
+ return child.isControlChecked;
135
+ }),
136
+ };
137
+ updateCheckedControlStatus(node.parent, newItem, true);
138
+ }
139
+ }
140
+ }
141
+ };
142
+
143
+ function layersTreeReducer(state = ROOT, action) {
144
+ switch (action.type) {
145
+ case "INIT":
146
+ return initTree(action.payload);
147
+ case "SELECT_ITEM":
148
+ return setNewControlCheckedStatus(state, action.payload);
149
+ case "SELECT_RADIO_ITEM":
150
+ return setNewControlCheckedStatus(state, action.payload);
151
+ default:
152
+ return state;
153
+ }
154
+ }
155
+
156
+ export default layersTreeReducer;