@deck.gl-community/editable-layers 9.2.0-beta.2 → 9.2.0-beta.3

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.
@@ -0,0 +1,342 @@
1
+ // deck.gl-community
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {render} from 'preact';
6
+ import type {ComponentChild, JSX} from 'preact';
7
+ import {
8
+ Widget,
9
+ type WidgetProps,
10
+ type WidgetPlacement,
11
+ type Deck
12
+ } from '@deck.gl/core';
13
+ import type {
14
+ GeoJsonEditModeConstructor,
15
+ GeoJsonEditModeType
16
+ } from '../edit-modes/geojson-edit-mode';
17
+
18
+ export type EditModeTrayWidgetModeOption = {
19
+ /**
20
+ * Optional identifier for the mode button.
21
+ * If not provided, one will be inferred from the supplied mode.
22
+ */
23
+ id?: string;
24
+ /** Edit mode constructor or instance that the button should activate. */
25
+ mode: GeoJsonEditModeConstructor | GeoJsonEditModeType;
26
+ /**
27
+ * The icon or element rendered inside the button.
28
+ * A simple string can also be supplied for text labels.
29
+ */
30
+ icon?: ComponentChild;
31
+ /** Optional text label rendered below the icon when provided. */
32
+ label?: string;
33
+ /** Optional tooltip text applied to the button element. */
34
+ title?: string;
35
+ };
36
+
37
+ export type EditModeTrayWidgetSelectEvent = {
38
+ id: string;
39
+ mode: GeoJsonEditModeConstructor | GeoJsonEditModeType;
40
+ option: EditModeTrayWidgetModeOption;
41
+ };
42
+
43
+ export type EditModeTrayWidgetProps = WidgetProps & {
44
+ /** Placement for the widget root element. */
45
+ placement?: WidgetPlacement;
46
+ /** Layout direction for mode buttons. */
47
+ layout?: 'vertical' | 'horizontal';
48
+ /** Collection of modes rendered in the tray. */
49
+ modes?: EditModeTrayWidgetModeOption[];
50
+ /** Identifier of the currently active mode. */
51
+ selectedModeId?: string | null;
52
+ /** Currently active mode instance/constructor. */
53
+ activeMode?: GeoJsonEditModeConstructor | GeoJsonEditModeType | null;
54
+ /** Callback fired when the user selects a mode. */
55
+ onSelectMode?: (event: EditModeTrayWidgetSelectEvent) => void;
56
+ };
57
+
58
+ const ROOT_STYLE: Partial<CSSStyleDeclaration> = {
59
+ position: 'absolute',
60
+ display: 'flex',
61
+ pointerEvents: 'auto',
62
+ userSelect: 'none',
63
+ zIndex: '99'
64
+ };
65
+
66
+ const TRAY_BASE_STYLE: JSX.CSSProperties = {
67
+ display: 'flex',
68
+ gap: '6px',
69
+ background: 'rgba(36, 40, 41, 0.88)',
70
+ borderRadius: '999px',
71
+ padding: '6px',
72
+ alignItems: 'center',
73
+ justifyContent: 'center',
74
+ boxShadow: '0 4px 12px rgba(0, 0, 0, 0.25)'
75
+ };
76
+
77
+ const BUTTON_BASE_STYLE: JSX.CSSProperties = {
78
+ appearance: 'none',
79
+ background: 'transparent',
80
+ border: 'none',
81
+ color: '#f0f0f0',
82
+ width: '34px',
83
+ height: '34px',
84
+ display: 'flex',
85
+ flexDirection: 'column',
86
+ alignItems: 'center',
87
+ justifyContent: 'center',
88
+ borderRadius: '50%',
89
+ cursor: 'pointer',
90
+ padding: '0',
91
+ transition: 'background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease'
92
+ };
93
+
94
+ const BUTTON_ACTIVE_STYLE: JSX.CSSProperties = {
95
+ background: '#0071e3',
96
+ color: '#ffffff',
97
+ boxShadow: '0 0 0 2px rgba(255, 255, 255, 0.35)'
98
+ };
99
+
100
+ const BUTTON_LABEL_STYLE: JSX.CSSProperties = {
101
+ fontSize: '10px',
102
+ marginTop: '2px',
103
+ lineHeight: '12px'
104
+ };
105
+
106
+ export class EditModeTrayWidget extends Widget<EditModeTrayWidgetProps> {
107
+ static override defaultProps = {
108
+ id: 'edit-mode-tray',
109
+ placement: 'top-left',
110
+ layout: 'vertical',
111
+ modes: [],
112
+ style: {},
113
+ className: ''
114
+ } satisfies Required<WidgetProps> &
115
+ Required<Pick<EditModeTrayWidgetProps, 'placement' | 'layout'>> &
116
+ EditModeTrayWidgetProps;
117
+
118
+ placement: WidgetPlacement = 'top-left';
119
+ className = 'deck-widget-edit-mode-tray';
120
+ layout: 'vertical' | 'horizontal' = 'vertical';
121
+ selectedModeId: string | null = null;
122
+ deck?: Deck | null = null;
123
+ private appliedCustomClassName: string | null = null;
124
+
125
+ constructor(props: EditModeTrayWidgetProps = {}) {
126
+ super({...EditModeTrayWidget.defaultProps, ...props});
127
+ this.placement = props.placement ?? EditModeTrayWidget.defaultProps.placement;
128
+ this.layout = props.layout ?? EditModeTrayWidget.defaultProps.layout;
129
+ this.selectedModeId = this.resolveSelectedModeId(props.modes ?? [], props);
130
+ }
131
+
132
+ override setProps(props: Partial<EditModeTrayWidgetProps>): void {
133
+ if (props.placement !== undefined) {
134
+ this.placement = props.placement;
135
+ }
136
+ if (props.layout !== undefined) {
137
+ this.layout = props.layout;
138
+ }
139
+
140
+ const modes = props.modes ?? this.props.modes ?? [];
141
+ this.selectedModeId = this.resolveSelectedModeId(modes, props);
142
+
143
+ super.setProps(props);
144
+ this.renderTray();
145
+ }
146
+
147
+ override onAdd({deck}: {deck: Deck}): void {
148
+ this.deck = deck;
149
+ }
150
+
151
+ override onRemove(): void {
152
+ this.deck = null;
153
+ const root = this.rootElement;
154
+ if (root) {
155
+ render(null, root);
156
+ }
157
+ this.rootElement = null;
158
+ }
159
+
160
+ override onRenderHTML(rootElement: HTMLElement): void {
161
+ const style = {...ROOT_STYLE, ...this.props.style};
162
+ Object.assign(rootElement.style, style);
163
+ if (this.appliedCustomClassName && this.appliedCustomClassName !== this.props.className) {
164
+ rootElement.classList.remove(this.appliedCustomClassName);
165
+ this.appliedCustomClassName = null;
166
+ }
167
+ if (this.props.className) {
168
+ rootElement.classList.add(this.props.className);
169
+ this.appliedCustomClassName = this.props.className;
170
+ }
171
+ rootElement.classList.add(this.className);
172
+
173
+ this.renderTray();
174
+ }
175
+
176
+ private renderTray() {
177
+ const root = this.rootElement;
178
+ if (!root) {
179
+ return;
180
+ }
181
+
182
+ const modes = this.props.modes ?? [];
183
+ const selectedId = this.selectedModeId;
184
+ const direction = this.layout === 'horizontal' ? 'row' : 'column';
185
+
186
+ const trayStyle: JSX.CSSProperties = {
187
+ ...TRAY_BASE_STYLE,
188
+ flexDirection: direction
189
+ };
190
+
191
+ const stopEvent = (event: Event) => {
192
+ event.stopPropagation();
193
+ if (typeof (event as any).stopImmediatePropagation === 'function') {
194
+ (event as any).stopImmediatePropagation();
195
+ }
196
+ };
197
+
198
+ const ui = (
199
+ <div
200
+ style={trayStyle}
201
+ onPointerDown={stopEvent}
202
+ onPointerMove={stopEvent}
203
+ onPointerUp={stopEvent}
204
+ onMouseDown={stopEvent}
205
+ onMouseMove={stopEvent}
206
+ onMouseUp={stopEvent}
207
+ onTouchStart={stopEvent}
208
+ onTouchMove={stopEvent}
209
+ onTouchEnd={stopEvent}
210
+ >
211
+ {modes.map((option, index) => {
212
+ const id = this.getModeId(option, index);
213
+ const active = id === selectedId;
214
+ const label = option.label ?? '';
215
+ const title = option.title ?? label;
216
+
217
+ const buttonStyle: JSX.CSSProperties = {
218
+ ...BUTTON_BASE_STYLE,
219
+ ...(active ? BUTTON_ACTIVE_STYLE : {})
220
+ };
221
+
222
+ return (
223
+ <button
224
+ key={id}
225
+ type="button"
226
+ title={title || undefined}
227
+ aria-pressed={active}
228
+ style={buttonStyle}
229
+ onClick={(event) => {
230
+ stopEvent(event);
231
+ this.handleSelect(option, id);
232
+ }}
233
+ >
234
+ {option.icon}
235
+ {label ? <span style={BUTTON_LABEL_STYLE}>{label}</span> : null}
236
+ </button>
237
+ );
238
+ })}
239
+ </div>
240
+ );
241
+
242
+ render(ui, root);
243
+ }
244
+
245
+ private handleSelect(option: EditModeTrayWidgetModeOption, id: string) {
246
+ if (this.selectedModeId !== id) {
247
+ this.selectedModeId = id;
248
+ this.renderTray();
249
+ }
250
+
251
+ this.props.onSelectMode?.({
252
+ id,
253
+ mode: option.mode,
254
+ option
255
+ });
256
+ }
257
+
258
+ private resolveSelectedModeId(
259
+ modes: EditModeTrayWidgetModeOption[],
260
+ props: Partial<EditModeTrayWidgetProps>
261
+ ): string | null {
262
+ if (props.selectedModeId !== undefined) {
263
+ return props.selectedModeId;
264
+ }
265
+
266
+ const activeMode = props.activeMode ?? this.props?.activeMode ?? null;
267
+ if (activeMode) {
268
+ const match = this.findOptionByMode(modes, activeMode);
269
+ if (match) {
270
+ return this.getModeId(match.option, match.index);
271
+ }
272
+ }
273
+
274
+ if (this.selectedModeId) {
275
+ const existing = this.findOptionById(modes, this.selectedModeId);
276
+ if (existing) {
277
+ return this.selectedModeId;
278
+ }
279
+ }
280
+
281
+ const first = modes[0];
282
+ return first ? this.getModeId(first, 0) : null;
283
+ }
284
+
285
+ private findOptionByMode(
286
+ modes: EditModeTrayWidgetModeOption[],
287
+ activeMode: GeoJsonEditModeConstructor | GeoJsonEditModeType
288
+ ): {option: EditModeTrayWidgetModeOption; index: number} | null {
289
+ for (let index = 0; index < modes.length; index++) {
290
+ const option = modes[index];
291
+ if (option.mode === activeMode) {
292
+ return {option, index};
293
+ }
294
+ if (this.isSameMode(option.mode, activeMode)) {
295
+ return {option, index};
296
+ }
297
+ }
298
+ return null;
299
+ }
300
+
301
+ private findOptionById(
302
+ modes: EditModeTrayWidgetModeOption[],
303
+ id: string
304
+ ): {option: EditModeTrayWidgetModeOption; index: number} | null {
305
+ for (let index = 0; index < modes.length; index++) {
306
+ if (this.getModeId(modes[index], index) === id) {
307
+ return {option: modes[index], index};
308
+ }
309
+ }
310
+ return null;
311
+ }
312
+
313
+ private getModeId(option: EditModeTrayWidgetModeOption, index: number): string {
314
+ if (option.id) {
315
+ return option.id;
316
+ }
317
+
318
+ const mode = option.mode as any;
319
+ if (mode) {
320
+ if (typeof mode === 'function' && mode.name) {
321
+ return mode.name;
322
+ }
323
+ if (mode && mode.constructor && mode.constructor.name) {
324
+ return mode.constructor.name;
325
+ }
326
+ }
327
+
328
+ return `mode-${index}`;
329
+ }
330
+
331
+ private isSameMode(
332
+ modeA: GeoJsonEditModeConstructor | GeoJsonEditModeType,
333
+ modeB: GeoJsonEditModeConstructor | GeoJsonEditModeType
334
+ ): boolean {
335
+ if (modeA === modeB) {
336
+ return true;
337
+ }
338
+ const constructorA = (modeA as GeoJsonEditModeType)?.constructor;
339
+ const constructorB = (modeB as GeoJsonEditModeType)?.constructor;
340
+ return Boolean(constructorA && constructorB && constructorA === constructorB);
341
+ }
342
+ }