@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.
- package/.github/CODEOWNERS +37 -0
- package/.github/workflows/conventional-pr-title.yml +36 -0
- package/CHANGELOG.md +55 -0
- package/README.md +3 -1
- package/doc/package.json +5 -5
- package/doc/src/app/components/GeopsMobilityDoc.tsx +19 -0
- package/docutils.js +198 -0
- package/index.html +48 -217
- package/index.js +683 -91
- package/input.css +15 -1
- package/jest-setup.js +3 -2
- package/package.json +9 -8
- package/scripts/dev.mjs +1 -1
- package/search.html +38 -69
- package/src/GeolocationButton/GeolocationButton.tsx +6 -17
- package/src/LayerTree/LayerTree.tsx +44 -0
- package/src/LayerTree/TreeItem/TreeItem.tsx +145 -0
- package/src/LayerTree/TreeItem/index.tsx +1 -0
- package/src/LayerTree/TreeItemContainer/TreeItemContainer.tsx +16 -0
- package/src/LayerTree/TreeItemContainer/index.tsx +1 -0
- package/src/LayerTree/index.tsx +1 -0
- package/src/LayerTree/layersTreeContext.ts +4 -0
- package/src/LayerTree/layersTreeReducer.ts +156 -0
- package/src/Map/Map.tsx +57 -12
- package/src/MobilityMap/MobilityMap.tsx +22 -9
- package/src/MobilityMap/index.css +0 -13
- package/src/RealtimeLayer/RealtimeLayer.tsx +1 -1
- package/src/RvfButton/RvfButton.tsx +45 -0
- package/src/RvfButton/index.tsx +1 -0
- package/src/RvfExportMenu/RvfExportMenu.tsx +95 -0
- package/src/RvfExportMenu/index.tsx +1 -0
- package/src/RvfExportMenuButton/RvfExportMenuButton.tsx +27 -0
- package/src/RvfExportMenuButton/index.tsx +1 -0
- package/src/RvfFeatureDetails/RvfFeatureDetails.tsx +29 -0
- package/src/RvfFeatureDetails/index.tsx +1 -0
- package/src/RvfIconButton/RvfIconButton.tsx +35 -0
- package/src/RvfIconButton/index.tsx +1 -0
- package/src/RvfMobilityMap/RvfMobilityMap.tsx +132 -52
- package/src/RvfMobilityMap/index.css +0 -13
- package/src/RvfModal/RvfModal.tsx +52 -0
- package/src/RvfModal/index.tsx +1 -0
- package/src/RvfPoisLayer/RvfPoisLayer.tsx +39 -0
- package/src/RvfPoisLayer/index.tsx +1 -0
- package/src/RvfSharedMobilityLayerGroup/RvfSharedMobilityLayerGroup.tsx +88 -0
- package/src/RvfSharedMobilityLayerGroup/index.tsx +1 -0
- package/src/RvfSingleClickListener/RvfSingleClickListener.tsx +137 -0
- package/src/RvfSingleClickListener/index.tsx +1 -0
- package/src/RvfZoomButtons/RvfZoomButtons.tsx +73 -0
- package/src/RvfZoomButtons/index.tsx +1 -0
- package/src/Search/Search.tsx +11 -9
- package/src/SingleClickListener/index.tsx +1 -1
- package/src/StationsLayer/StationsLayer.tsx +0 -1
- package/src/StopsSearch/StopsSearch.tsx +38 -6
- package/src/TopicMenu/TopicMenu.tsx +143 -0
- package/src/TopicMenu/index.tsx +1 -0
- package/src/icons/Cancel/Cancel.tsx +21 -0
- package/src/icons/Cancel/cancel.svg +7 -0
- package/src/icons/Cancel/index.tsx +1 -0
- package/src/icons/Download/Download.tsx +20 -0
- package/src/icons/Download/download.svg +15 -0
- package/src/icons/Download/index.tsx +1 -0
- package/src/icons/Elevator/Elevator.tsx +1 -1
- package/src/icons/Geolocation/Geolocation.tsx +21 -0
- package/src/icons/Geolocation/index.tsx +1 -0
- package/src/icons/Menu/Menu.tsx +32 -0
- package/src/icons/Menu/index.tsx +1 -0
- package/src/icons/Menu/menu.svg +9 -0
- package/src/icons/Minus/Minus.tsx +19 -0
- package/src/icons/Minus/index.tsx +1 -0
- package/src/icons/Minus/minus.svg +7 -0
- package/src/icons/Plus/Plus.tsx +19 -0
- package/src/icons/Plus/index.tsx +1 -0
- package/src/icons/Plus/plus.svg +7 -0
- package/src/index.tsx +2 -0
- package/src/utils/constants.ts +9 -0
- package/src/utils/createMobiDataBwWfsLayer.ts +120 -0
- package/src/utils/exportPdf.ts +677 -0
- package/src/utils/hooks/useRvfContext.tsx +37 -0
- package/src/utils/hooks/useUpdatePermalink.tsx +2 -9
- 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
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.
|
|
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.
|
|
12
|
-
"ol": "^10.3.
|
|
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.
|
|
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.
|
|
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.
|
|
50
|
+
"prettier": "^3.4.2",
|
|
50
51
|
"standard-version": "^9.5.0",
|
|
51
|
-
"tailwindcss": "^3.4.
|
|
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:
|
|
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
|
-
<script
|
|
54
|
-
type="module"
|
|
55
|
-
src="https://www.unpkg.com/@geops/mobility-web-component">
|
|
56
|
-
</script>
|
|
57
|
-
<geops-mobility-search
|
|
58
|
-
apikey="YOUR_GEOPS_API_KEY"
|
|
59
|
-
limit="5"
|
|
60
|
-
mots="rail,bus"
|
|
61
|
-
style="display: block;width: 800px;height: 800px;">
|
|
62
|
-
</geops-mobility></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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
<
|
|
57
|
-
className="
|
|
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
|
-
<
|
|
65
|
-
|
|
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,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;
|