@deck.gl/widgets 9.3.0-alpha.5 → 9.3.0-beta.1
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/dist/compass-widget.d.ts +12 -0
- package/dist/compass-widget.d.ts.map +1 -1
- package/dist/compass-widget.js +9 -3
- package/dist/compass-widget.js.map +1 -1
- package/dist/dist.dev.js +205 -69
- package/dist/fullscreen-widget.d.ts +5 -0
- package/dist/fullscreen-widget.d.ts.map +1 -1
- package/dist/fullscreen-widget.js +18 -8
- package/dist/fullscreen-widget.js.map +1 -1
- package/dist/geocoder-widget.d.ts +13 -0
- package/dist/geocoder-widget.d.ts.map +1 -1
- package/dist/geocoder-widget.js +13 -1
- package/dist/geocoder-widget.js.map +1 -1
- package/dist/gimbal-widget.d.ts +12 -0
- package/dist/gimbal-widget.d.ts.map +1 -1
- package/dist/gimbal-widget.js +4 -1
- package/dist/gimbal-widget.js.map +1 -1
- package/dist/index.cjs +218 -70
- package/dist/index.cjs.map +2 -2
- package/dist/loading-widget.d.ts +5 -0
- package/dist/loading-widget.d.ts.map +1 -1
- package/dist/loading-widget.js +4 -1
- package/dist/loading-widget.js.map +1 -1
- package/dist/reset-view-widget.d.ts +9 -0
- package/dist/reset-view-widget.d.ts.map +1 -1
- package/dist/reset-view-widget.js +4 -1
- package/dist/reset-view-widget.js.map +1 -1
- package/dist/stats-widget.d.ts +18 -3
- package/dist/stats-widget.d.ts.map +1 -1
- package/dist/stats-widget.js +29 -12
- package/dist/stats-widget.js.map +1 -1
- package/dist/theme-widget.d.ts +20 -2
- package/dist/theme-widget.d.ts.map +1 -1
- package/dist/theme-widget.js +38 -14
- package/dist/theme-widget.js.map +1 -1
- package/dist/timeline-widget.d.ts +34 -3
- package/dist/timeline-widget.d.ts.map +1 -1
- package/dist/timeline-widget.js +98 -24
- package/dist/timeline-widget.js.map +1 -1
- package/dist/zoom-widget.d.ts +13 -1
- package/dist/zoom-widget.d.ts.map +1 -1
- package/dist/zoom-widget.js +7 -4
- package/dist/zoom-widget.js.map +1 -1
- package/dist.min.js +2 -2
- package/package.json +3 -3
- package/src/compass-widget.tsx +23 -3
- package/src/fullscreen-widget.tsx +24 -9
- package/src/geocoder-widget.tsx +23 -1
- package/src/gimbal-widget.tsx +17 -1
- package/src/loading-widget.tsx +11 -1
- package/src/reset-view-widget.tsx +15 -1
- package/src/stats-widget.tsx +42 -13
- package/src/theme-widget.tsx +56 -17
- package/src/timeline-widget.tsx +123 -24
- package/src/zoom-widget.tsx +20 -4
package/src/geocoder-widget.tsx
CHANGED
|
@@ -48,6 +48,15 @@ export type GeocoderWidgetProps = WidgetProps & {
|
|
|
48
48
|
apiKey?: string;
|
|
49
49
|
/** Whether to use geolocation @note Experimental*/
|
|
50
50
|
_geolocation?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Callback when a location is geocoded and the view will navigate to it.
|
|
53
|
+
*/
|
|
54
|
+
onGeocode?: (params: {
|
|
55
|
+
/** The view being updated */
|
|
56
|
+
viewId: string;
|
|
57
|
+
/** The geocoded coordinates */
|
|
58
|
+
coordinates: {longitude: number; latitude: number; zoom?: number};
|
|
59
|
+
}) => void;
|
|
51
60
|
};
|
|
52
61
|
|
|
53
62
|
/**
|
|
@@ -66,7 +75,8 @@ export class GeocoderWidget extends Widget<GeocoderWidgetProps> {
|
|
|
66
75
|
geocoder: 'coordinates',
|
|
67
76
|
customGeocoder: CoordinatesGeocoder,
|
|
68
77
|
apiKey: '',
|
|
69
|
-
_geolocation: false
|
|
78
|
+
_geolocation: false,
|
|
79
|
+
onGeocode: () => {}
|
|
70
80
|
};
|
|
71
81
|
|
|
72
82
|
className = 'deck-widget-geocoder';
|
|
@@ -188,6 +198,18 @@ export class GeocoderWidget extends Widget<GeocoderWidgetProps> {
|
|
|
188
198
|
flyTo(viewState: ViewState) {
|
|
189
199
|
const viewIds = this.viewId ? [this.viewId] : (this.deck?.getViews().map(v => v.id) ?? []);
|
|
190
200
|
for (const viewId of viewIds) {
|
|
201
|
+
// Call callback with geocoded coordinates
|
|
202
|
+
if ('longitude' in viewState && 'latitude' in viewState) {
|
|
203
|
+
this.props.onGeocode?.({
|
|
204
|
+
viewId,
|
|
205
|
+
coordinates: {
|
|
206
|
+
longitude: viewState.longitude as number,
|
|
207
|
+
latitude: viewState.latitude as number,
|
|
208
|
+
zoom: viewState.zoom as number | undefined
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
191
213
|
const currentViewState = this.getViewState(viewId);
|
|
192
214
|
const nextViewState: ViewState = {
|
|
193
215
|
...currentViewState,
|
package/src/gimbal-widget.tsx
CHANGED
|
@@ -16,6 +16,18 @@ export type GimbalWidgetProps = WidgetProps & {
|
|
|
16
16
|
strokeWidth?: number;
|
|
17
17
|
/** Transition duration in ms when resetting rotation. */
|
|
18
18
|
transitionDuration?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Callback when the gimbal reset button is clicked.
|
|
21
|
+
* Called for each viewport that will be reset.
|
|
22
|
+
*/
|
|
23
|
+
onReset?: (params: {
|
|
24
|
+
/** The view being reset */
|
|
25
|
+
viewId: string;
|
|
26
|
+
/** The new rotationOrbit value (0) */
|
|
27
|
+
rotationOrbit: number;
|
|
28
|
+
/** The new rotationX value (0) */
|
|
29
|
+
rotationX: number;
|
|
30
|
+
}) => void;
|
|
19
31
|
};
|
|
20
32
|
|
|
21
33
|
export class GimbalWidget extends Widget<GimbalWidgetProps> {
|
|
@@ -26,7 +38,8 @@ export class GimbalWidget extends Widget<GimbalWidgetProps> {
|
|
|
26
38
|
viewId: null,
|
|
27
39
|
label: 'Gimbal',
|
|
28
40
|
strokeWidth: 1.5,
|
|
29
|
-
transitionDuration: 200
|
|
41
|
+
transitionDuration: 200,
|
|
42
|
+
onReset: () => {}
|
|
30
43
|
};
|
|
31
44
|
|
|
32
45
|
className = 'deck-widget-gimbal';
|
|
@@ -122,6 +135,9 @@ export class GimbalWidget extends Widget<GimbalWidgetProps> {
|
|
|
122
135
|
const viewId = this.viewId || viewport?.id || 'OrbitView';
|
|
123
136
|
const viewState = this.getViewState(viewId);
|
|
124
137
|
if ('rotationOrbit' in viewState || 'rotationX' in viewState) {
|
|
138
|
+
// Call callback
|
|
139
|
+
this.props.onReset?.({viewId, rotationOrbit: 0, rotationX: 0});
|
|
140
|
+
|
|
125
141
|
const nextViewState = {
|
|
126
142
|
...viewState,
|
|
127
143
|
rotationOrbit: 0,
|
package/src/loading-widget.tsx
CHANGED
|
@@ -15,6 +15,11 @@ export type LoadingWidgetProps = WidgetProps & {
|
|
|
15
15
|
viewId?: string | null;
|
|
16
16
|
/** Tooltip message when loading */
|
|
17
17
|
label?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Callback when the loading state changes.
|
|
20
|
+
* Called when layers transition between loading and loaded states.
|
|
21
|
+
*/
|
|
22
|
+
onLoadingChange?: (loading: boolean) => void;
|
|
18
23
|
};
|
|
19
24
|
|
|
20
25
|
/**
|
|
@@ -26,7 +31,8 @@ export class LoadingWidget extends Widget<LoadingWidgetProps> {
|
|
|
26
31
|
id: 'loading',
|
|
27
32
|
placement: 'top-left',
|
|
28
33
|
viewId: null,
|
|
29
|
-
label: 'Loading layer data'
|
|
34
|
+
label: 'Loading layer data',
|
|
35
|
+
onLoadingChange: () => {}
|
|
30
36
|
};
|
|
31
37
|
|
|
32
38
|
className = 'deck-widget-loading';
|
|
@@ -62,6 +68,10 @@ export class LoadingWidget extends Widget<LoadingWidgetProps> {
|
|
|
62
68
|
const loading = layers.some(layer => !layer.isLoaded);
|
|
63
69
|
if (loading !== this.loading) {
|
|
64
70
|
this.loading = loading;
|
|
71
|
+
|
|
72
|
+
// Call callback when loading state changes
|
|
73
|
+
this.props.onLoadingChange?.(loading);
|
|
74
|
+
|
|
65
75
|
this.updateHTML();
|
|
66
76
|
}
|
|
67
77
|
}
|
|
@@ -21,6 +21,15 @@ export type ResetViewWidgetProps<ViewsT extends ViewOrViews = null> = WidgetProp
|
|
|
21
21
|
initialViewState?: ViewStateMap<ViewsT>;
|
|
22
22
|
/** View to interact with. Required when using multiple views. */
|
|
23
23
|
viewId?: string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Callback when the reset view button is clicked.
|
|
26
|
+
*/
|
|
27
|
+
onReset?: (params: {
|
|
28
|
+
/** The view being reset */
|
|
29
|
+
viewId: string;
|
|
30
|
+
/** The view state being reset to */
|
|
31
|
+
viewState: Record<string, unknown>;
|
|
32
|
+
}) => void;
|
|
24
33
|
};
|
|
25
34
|
|
|
26
35
|
/**
|
|
@@ -36,7 +45,8 @@ export class ResetViewWidget<ViewsT extends ViewOrViews = null> extends Widget<
|
|
|
36
45
|
placement: 'top-left',
|
|
37
46
|
label: 'Reset View',
|
|
38
47
|
initialViewState: undefined!,
|
|
39
|
-
viewId: null
|
|
48
|
+
viewId: null,
|
|
49
|
+
onReset: () => {}
|
|
40
50
|
};
|
|
41
51
|
|
|
42
52
|
className = 'deck-widget-reset-view';
|
|
@@ -78,6 +88,10 @@ export class ResetViewWidget<ViewsT extends ViewOrViews = null> extends Widget<
|
|
|
78
88
|
// transitionDuration: this.props.transitionDuration,
|
|
79
89
|
// transitionInterpolator: new FlyToInterpolator()
|
|
80
90
|
};
|
|
91
|
+
|
|
92
|
+
// Call callback
|
|
93
|
+
this.props.onReset?.({viewId, viewState: nextViewState as Record<string, unknown>});
|
|
94
|
+
|
|
81
95
|
this.setViewState(viewId, nextViewState);
|
|
82
96
|
}
|
|
83
97
|
}
|
package/src/stats-widget.tsx
CHANGED
|
@@ -38,7 +38,7 @@ export type StatsWidgetProps = WidgetProps & {
|
|
|
38
38
|
/** Expand the stats UI by default.
|
|
39
39
|
* @default false
|
|
40
40
|
*/
|
|
41
|
-
|
|
41
|
+
initialExpanded?: boolean;
|
|
42
42
|
/** Stats object to visualize. */
|
|
43
43
|
stats?: Stats;
|
|
44
44
|
/** Title shown in the header of the pop-up. Defaults to stats.id. */
|
|
@@ -49,6 +49,15 @@ export type StatsWidgetProps = WidgetProps & {
|
|
|
49
49
|
formatters?: Record<string, string | ((stat: Stat) => string)>;
|
|
50
50
|
/** Whether to reset particular stats after each update. */
|
|
51
51
|
resetOnUpdate?: Record<string, boolean>;
|
|
52
|
+
/**
|
|
53
|
+
* Controlled expanded state. When provided, the widget is in controlled mode.
|
|
54
|
+
*/
|
|
55
|
+
expanded?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Callback when the expanded state changes (user clicks header).
|
|
58
|
+
* In controlled mode, use this to update the expanded prop.
|
|
59
|
+
*/
|
|
60
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
52
61
|
};
|
|
53
62
|
|
|
54
63
|
/** Displays probe.gl stats in a floating pop-up. */
|
|
@@ -58,13 +67,15 @@ export class StatsWidget extends Widget<StatsWidgetProps> {
|
|
|
58
67
|
type: 'deck',
|
|
59
68
|
placement: 'top-left',
|
|
60
69
|
viewId: null,
|
|
61
|
-
|
|
70
|
+
initialExpanded: false,
|
|
62
71
|
stats: undefined!,
|
|
63
72
|
title: 'Stats',
|
|
64
73
|
framesPerUpdate: 1,
|
|
65
74
|
formatters: {},
|
|
66
75
|
resetOnUpdate: {},
|
|
67
|
-
id: 'stats'
|
|
76
|
+
id: 'stats',
|
|
77
|
+
expanded: undefined!,
|
|
78
|
+
onExpandedChange: () => {}
|
|
68
79
|
};
|
|
69
80
|
|
|
70
81
|
className = 'deck-widget-stats';
|
|
@@ -73,13 +84,22 @@ export class StatsWidget extends Widget<StatsWidgetProps> {
|
|
|
73
84
|
private _counter = 0;
|
|
74
85
|
private _formatters: Record<string, (stat: Stat) => string>;
|
|
75
86
|
private _resetOnUpdate: Record<string, boolean>;
|
|
76
|
-
|
|
87
|
+
private _expanded: boolean = false;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Returns the current expanded state.
|
|
91
|
+
* In controlled mode, returns the expanded prop.
|
|
92
|
+
* In uncontrolled mode, returns the internal state.
|
|
93
|
+
*/
|
|
94
|
+
getExpanded(): boolean {
|
|
95
|
+
return this.props.expanded ?? this._expanded;
|
|
96
|
+
}
|
|
77
97
|
|
|
78
98
|
constructor(props: StatsWidgetProps = {}) {
|
|
79
99
|
super(props);
|
|
80
100
|
this._formatters = {...DEFAULT_FORMATTERS};
|
|
81
101
|
this._resetOnUpdate = {...this.props.resetOnUpdate};
|
|
82
|
-
this.
|
|
102
|
+
this._expanded = Boolean(props.initialExpanded);
|
|
83
103
|
this.setProps(props);
|
|
84
104
|
}
|
|
85
105
|
|
|
@@ -107,9 +127,9 @@ export class StatsWidget extends Widget<StatsWidgetProps> {
|
|
|
107
127
|
}
|
|
108
128
|
|
|
109
129
|
onRenderHTML(rootElement: HTMLElement): void {
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
render(<FpsIcon getFps={this._getFps} onClick={this.
|
|
130
|
+
const isExpanded = this.getExpanded();
|
|
131
|
+
if (!isExpanded) {
|
|
132
|
+
render(<FpsIcon getFps={this._getFps} onClick={this._toggleExpanded} />, rootElement);
|
|
113
133
|
return;
|
|
114
134
|
}
|
|
115
135
|
|
|
@@ -139,7 +159,7 @@ export class StatsWidget extends Widget<StatsWidgetProps> {
|
|
|
139
159
|
<div
|
|
140
160
|
className="deck-widget-stats-header"
|
|
141
161
|
style={{cursor: 'pointer', pointerEvents: 'auto'}}
|
|
142
|
-
onClick={this.
|
|
162
|
+
onClick={this._toggleExpanded}
|
|
143
163
|
>
|
|
144
164
|
<b>{title}</b>
|
|
145
165
|
{deviceLabel && <span className="deck-widget-stats-device">{deviceLabel}</span>}
|
|
@@ -154,7 +174,7 @@ export class StatsWidget extends Widget<StatsWidgetProps> {
|
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
onRedraw(): void {
|
|
157
|
-
if (
|
|
177
|
+
if (this.getExpanded()) {
|
|
158
178
|
const framesPerUpdate = Math.max(1, this.props.framesPerUpdate || 1);
|
|
159
179
|
if (this._counter++ % framesPerUpdate === 0) {
|
|
160
180
|
this.updateHTML();
|
|
@@ -182,9 +202,18 @@ export class StatsWidget extends Widget<StatsWidgetProps> {
|
|
|
182
202
|
}
|
|
183
203
|
}
|
|
184
204
|
|
|
185
|
-
protected
|
|
186
|
-
|
|
187
|
-
|
|
205
|
+
protected _toggleExpanded = (): void => {
|
|
206
|
+
const nextExpanded = !this.getExpanded();
|
|
207
|
+
|
|
208
|
+
// Always call callback if provided
|
|
209
|
+
this.props.onExpandedChange?.(nextExpanded);
|
|
210
|
+
|
|
211
|
+
// Only update internal state if uncontrolled
|
|
212
|
+
if (this.props.expanded === undefined) {
|
|
213
|
+
this._expanded = nextExpanded;
|
|
214
|
+
this.updateHTML();
|
|
215
|
+
}
|
|
216
|
+
// In controlled mode, parent will update expanded prop which triggers updateHTML via setProps
|
|
188
217
|
};
|
|
189
218
|
|
|
190
219
|
protected _getFps = (): number => {
|
package/src/theme-widget.tsx
CHANGED
|
@@ -23,8 +23,19 @@ export type ThemeWidgetProps = WidgetProps & {
|
|
|
23
23
|
darkModeLabel?: string;
|
|
24
24
|
/** Styles for dark mode theme */
|
|
25
25
|
darkModeTheme?: DeckWidgetTheme;
|
|
26
|
-
/** Initial theme mode. 'auto' reads the browser default setting */
|
|
26
|
+
/** Initial theme mode for uncontrolled usage. 'auto' reads the browser default setting */
|
|
27
27
|
initialThemeMode?: 'auto' | 'light' | 'dark';
|
|
28
|
+
/**
|
|
29
|
+
* Controlled theme mode. When provided, the widget is in controlled mode
|
|
30
|
+
* and this prop determines the current theme.
|
|
31
|
+
*/
|
|
32
|
+
themeMode?: 'light' | 'dark';
|
|
33
|
+
/**
|
|
34
|
+
* Callback when the user clicks the theme toggle button.
|
|
35
|
+
* In controlled mode, use this to update the themeMode prop.
|
|
36
|
+
* In uncontrolled mode, this is called after the internal state updates.
|
|
37
|
+
*/
|
|
38
|
+
onThemeModeChange?: (newMode: 'light' | 'dark') => void;
|
|
28
39
|
};
|
|
29
40
|
|
|
30
41
|
export class ThemeWidget extends Widget<ThemeWidgetProps> {
|
|
@@ -37,7 +48,9 @@ export class ThemeWidget extends Widget<ThemeWidgetProps> {
|
|
|
37
48
|
lightModeTheme: LightGlassTheme,
|
|
38
49
|
darkModeLabel: 'Dark Mode',
|
|
39
50
|
darkModeTheme: DarkGlassTheme,
|
|
40
|
-
initialThemeMode: 'auto'
|
|
51
|
+
initialThemeMode: 'auto',
|
|
52
|
+
themeMode: undefined!,
|
|
53
|
+
onThemeModeChange: () => {}
|
|
41
54
|
};
|
|
42
55
|
|
|
43
56
|
className = 'deck-widget-theme';
|
|
@@ -52,36 +65,44 @@ export class ThemeWidget extends Widget<ThemeWidgetProps> {
|
|
|
52
65
|
|
|
53
66
|
// eslint-disable-next-line complexity
|
|
54
67
|
setProps(props: Partial<ThemeWidgetProps>) {
|
|
55
|
-
const {lightModeTheme, darkModeTheme} = this.props;
|
|
68
|
+
const {lightModeTheme, darkModeTheme, themeMode: prevThemeMode} = this.props;
|
|
56
69
|
this.placement = props.placement ?? this.placement;
|
|
57
70
|
this.viewId = props.viewId ?? this.viewId;
|
|
58
71
|
super.setProps(props);
|
|
59
72
|
|
|
60
|
-
|
|
73
|
+
const currentMode = this.getThemeMode();
|
|
74
|
+
|
|
75
|
+
// Handle controlled mode - apply theme when controlled prop changes
|
|
76
|
+
if (props.themeMode !== undefined && props.themeMode !== prevThemeMode) {
|
|
77
|
+
this._applyTheme(props.themeMode);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
switch (currentMode) {
|
|
61
82
|
case 'light':
|
|
62
83
|
if (props.lightModeTheme && !deepEqual(props.lightModeTheme, lightModeTheme, 1)) {
|
|
63
|
-
this.
|
|
84
|
+
this._applyTheme('light');
|
|
64
85
|
}
|
|
65
86
|
break;
|
|
66
87
|
case 'dark':
|
|
67
88
|
if (props.darkModeTheme && !deepEqual(props.darkModeTheme, darkModeTheme, 1)) {
|
|
68
|
-
this.
|
|
89
|
+
this._applyTheme('dark');
|
|
69
90
|
}
|
|
70
91
|
break;
|
|
71
92
|
default:
|
|
72
|
-
log.warn(`Invalid theme mode ${
|
|
93
|
+
log.warn(`Invalid theme mode ${currentMode}`)();
|
|
73
94
|
}
|
|
74
95
|
}
|
|
75
96
|
|
|
76
97
|
onRenderHTML(rootElement: HTMLElement): void {
|
|
77
98
|
const {lightModeLabel, darkModeLabel} = this.props;
|
|
78
|
-
|
|
99
|
+
const currentMode = this.getThemeMode();
|
|
79
100
|
|
|
80
101
|
render(
|
|
81
102
|
<IconButton
|
|
82
103
|
onClick={this._handleClick.bind(this)}
|
|
83
|
-
label={
|
|
84
|
-
className={
|
|
104
|
+
label={currentMode === 'dark' ? darkModeLabel : lightModeLabel}
|
|
105
|
+
className={currentMode === 'dark' ? 'deck-widget-moon' : 'deck-widget-sun'}
|
|
85
106
|
/>,
|
|
86
107
|
rootElement
|
|
87
108
|
);
|
|
@@ -89,24 +110,42 @@ export class ThemeWidget extends Widget<ThemeWidgetProps> {
|
|
|
89
110
|
|
|
90
111
|
onAdd() {
|
|
91
112
|
// Note: theme styling is applied in here onAdd() once DOM element is created
|
|
92
|
-
this.
|
|
113
|
+
this._applyTheme(this.getThemeMode());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Returns the current theme mode.
|
|
118
|
+
* In controlled mode, returns the themeMode prop.
|
|
119
|
+
* In uncontrolled mode, returns the internal state.
|
|
120
|
+
*/
|
|
121
|
+
getThemeMode(): 'light' | 'dark' {
|
|
122
|
+
return this.props.themeMode ?? this.themeMode;
|
|
93
123
|
}
|
|
94
124
|
|
|
95
125
|
_handleClick() {
|
|
96
|
-
const
|
|
97
|
-
|
|
126
|
+
const currentMode = this.getThemeMode();
|
|
127
|
+
const nextMode = currentMode === 'dark' ? 'light' : 'dark';
|
|
128
|
+
|
|
129
|
+
// Always call callback if provided
|
|
130
|
+
this.props.onThemeModeChange?.(nextMode);
|
|
131
|
+
|
|
132
|
+
// Only update internal state if uncontrolled
|
|
133
|
+
if (this.props.themeMode === undefined) {
|
|
134
|
+
this.themeMode = nextMode;
|
|
135
|
+
this._applyTheme(nextMode);
|
|
136
|
+
}
|
|
137
|
+
// In controlled mode, parent will update themeMode prop which triggers _applyTheme via setProps
|
|
98
138
|
}
|
|
99
139
|
|
|
100
|
-
|
|
101
|
-
|
|
140
|
+
/** Apply theme styling without changing internal state */
|
|
141
|
+
_applyTheme(themeMode: 'light' | 'dark') {
|
|
102
142
|
const container = this.rootElement?.closest<HTMLDivElement>('.deck-widget-container');
|
|
103
143
|
if (container) {
|
|
104
144
|
const themeStyle =
|
|
105
145
|
themeMode === 'dark' ? this.props.darkModeTheme : this.props.lightModeTheme;
|
|
106
146
|
applyStyles(container, themeStyle);
|
|
107
147
|
|
|
108
|
-
const label =
|
|
109
|
-
this.themeMode === 'dark' ? this.props.darkModeLabel : this.props.lightModeLabel;
|
|
148
|
+
const label = themeMode === 'dark' ? this.props.darkModeLabel : this.props.lightModeLabel;
|
|
110
149
|
log.log(1, `Switched theme to ${label}`, themeStyle)();
|
|
111
150
|
|
|
112
151
|
this.updateHTML();
|
package/src/timeline-widget.tsx
CHANGED
|
@@ -21,11 +21,16 @@ export type TimelineWidgetProps = WidgetProps & {
|
|
|
21
21
|
* @default 1
|
|
22
22
|
*/
|
|
23
23
|
step?: number;
|
|
24
|
-
/** Initial slider value.
|
|
24
|
+
/** Initial slider value for uncontrolled usage.
|
|
25
25
|
* @default `timeRange[0]`
|
|
26
26
|
*/
|
|
27
27
|
initialTime?: number;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Controlled time value. When provided, the widget is in controlled mode
|
|
30
|
+
* for the time slider.
|
|
31
|
+
*/
|
|
32
|
+
time?: number;
|
|
33
|
+
/** Callback when time value changes (via slider or playback). */
|
|
29
34
|
onTimeChange?: (value: number) => void;
|
|
30
35
|
/** Start playing automatically
|
|
31
36
|
* @default false
|
|
@@ -39,6 +44,16 @@ export type TimelineWidgetProps = WidgetProps & {
|
|
|
39
44
|
* @default 1000
|
|
40
45
|
*/
|
|
41
46
|
playInterval?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Controlled playing state. When provided, the widget is in controlled mode
|
|
49
|
+
* for play/pause.
|
|
50
|
+
*/
|
|
51
|
+
playing?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Callback when play/pause button is clicked.
|
|
54
|
+
* In controlled mode, use this to update the playing prop.
|
|
55
|
+
*/
|
|
56
|
+
onPlayingChange?: (playing: boolean) => void;
|
|
42
57
|
/** Callback to get label from time value */
|
|
43
58
|
formatLabel?: (value: number) => string;
|
|
44
59
|
};
|
|
@@ -48,10 +63,28 @@ export class TimelineWidget extends Widget<TimelineWidgetProps> {
|
|
|
48
63
|
className = 'deck-widget-timeline';
|
|
49
64
|
placement: WidgetPlacement = 'fill';
|
|
50
65
|
|
|
51
|
-
private
|
|
66
|
+
private _playing = false;
|
|
52
67
|
private timerId: number | null = null;
|
|
53
68
|
currentTime: number;
|
|
54
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Returns the current time value.
|
|
72
|
+
* In controlled mode, returns the time prop.
|
|
73
|
+
* In uncontrolled mode, returns the internal state.
|
|
74
|
+
*/
|
|
75
|
+
getTime(): number {
|
|
76
|
+
return this.props.time ?? this.currentTime;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Returns the current playing state.
|
|
81
|
+
* In controlled mode, returns the playing prop.
|
|
82
|
+
* In uncontrolled mode, returns the internal state.
|
|
83
|
+
*/
|
|
84
|
+
getPlaying(): boolean {
|
|
85
|
+
return this.props.playing ?? this._playing;
|
|
86
|
+
}
|
|
87
|
+
|
|
55
88
|
static defaultProps: Required<TimelineWidgetProps> = {
|
|
56
89
|
...Widget.defaultProps,
|
|
57
90
|
id: 'timeline',
|
|
@@ -61,29 +94,56 @@ export class TimelineWidget extends Widget<TimelineWidgetProps> {
|
|
|
61
94
|
timeRange: [0, 100],
|
|
62
95
|
step: 1,
|
|
63
96
|
initialTime: undefined!,
|
|
97
|
+
time: undefined!,
|
|
64
98
|
onTimeChange: () => {},
|
|
65
99
|
autoPlay: false,
|
|
66
100
|
loop: false,
|
|
67
101
|
playInterval: 1000,
|
|
102
|
+
playing: undefined!,
|
|
103
|
+
onPlayingChange: () => {},
|
|
68
104
|
formatLabel: String
|
|
69
105
|
};
|
|
70
106
|
|
|
71
107
|
constructor(props: TimelineWidgetProps = {}) {
|
|
72
108
|
super(props);
|
|
73
109
|
this.currentTime = this.props.initialTime ?? this.props.timeRange[0];
|
|
74
|
-
|
|
110
|
+
// In controlled mode, sync Timeline to the controlled time prop
|
|
111
|
+
const syncTime = this.props.time ?? this.currentTime;
|
|
112
|
+
this.props.timeline?.setTime(syncTime);
|
|
75
113
|
this.setProps(this.props);
|
|
76
114
|
}
|
|
77
115
|
|
|
78
116
|
setProps(props: Partial<TimelineWidgetProps>): void {
|
|
117
|
+
const {playing: prevPlaying, time: prevTime} = this.props;
|
|
79
118
|
this.viewId = props.viewId ?? this.viewId;
|
|
80
119
|
super.setProps(props);
|
|
120
|
+
|
|
121
|
+
// Sync Timeline object when controlled time prop changes
|
|
122
|
+
if (props.time !== undefined && props.time !== prevTime) {
|
|
123
|
+
this.props.timeline?.setTime(props.time);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Handle controlled playing state changes
|
|
127
|
+
if (props.playing !== undefined && props.playing !== prevPlaying) {
|
|
128
|
+
if (props.playing && !this._playing) {
|
|
129
|
+
this._startTimer();
|
|
130
|
+
} else if (!props.playing && this._playing) {
|
|
131
|
+
this._stopTimer();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
81
134
|
}
|
|
82
135
|
|
|
83
136
|
onAdd(): void {
|
|
84
|
-
this.
|
|
137
|
+
this._playing = false;
|
|
85
138
|
this.timerId = null;
|
|
86
|
-
if (this.props.autoPlay)
|
|
139
|
+
if (this.props.autoPlay) {
|
|
140
|
+
if (this.props.playing !== undefined) {
|
|
141
|
+
// In controlled mode, notify parent instead of starting directly
|
|
142
|
+
this.props.onPlayingChange?.(true);
|
|
143
|
+
} else {
|
|
144
|
+
this.play();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
87
147
|
}
|
|
88
148
|
|
|
89
149
|
onRemove(): void {
|
|
@@ -92,13 +152,14 @@ export class TimelineWidget extends Widget<TimelineWidgetProps> {
|
|
|
92
152
|
|
|
93
153
|
onRenderHTML(rootElement: HTMLElement): void {
|
|
94
154
|
const {timeRange, step, formatLabel} = this.props;
|
|
95
|
-
const
|
|
155
|
+
const isPlaying = this.getPlaying();
|
|
156
|
+
const currentTime = this.getTime();
|
|
96
157
|
|
|
97
158
|
rootElement.dataset.placement = this.props.placement;
|
|
98
159
|
|
|
99
160
|
render(
|
|
100
161
|
<div className="deck-widget-button-group">
|
|
101
|
-
{
|
|
162
|
+
{isPlaying ? (
|
|
102
163
|
<IconButton
|
|
103
164
|
label="Pause"
|
|
104
165
|
className="deck-widget-timeline-pause"
|
|
@@ -135,40 +196,69 @@ export class TimelineWidget extends Widget<TimelineWidgetProps> {
|
|
|
135
196
|
}
|
|
136
197
|
|
|
137
198
|
private handlePlayPause = (): void => {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
199
|
+
const isPlaying = this.getPlaying();
|
|
200
|
+
const nextPlaying = !isPlaying;
|
|
201
|
+
|
|
202
|
+
// Always call callback if provided
|
|
203
|
+
this.props.onPlayingChange?.(nextPlaying);
|
|
204
|
+
|
|
205
|
+
// Only update internal state if uncontrolled
|
|
206
|
+
if (this.props.playing === undefined) {
|
|
207
|
+
if (nextPlaying) {
|
|
208
|
+
this.play();
|
|
209
|
+
} else {
|
|
210
|
+
this.stop();
|
|
211
|
+
}
|
|
142
212
|
}
|
|
213
|
+
// In controlled mode, parent will update playing prop which triggers start/stop via setProps
|
|
143
214
|
};
|
|
144
215
|
|
|
145
216
|
private handleTimeChange = ([value]: [number, number]): void => {
|
|
146
|
-
|
|
147
|
-
this.props.timeline?.setTime(value);
|
|
217
|
+
// Always call callback
|
|
148
218
|
this.props.onTimeChange(value);
|
|
149
|
-
|
|
219
|
+
|
|
220
|
+
// Only update internal state if uncontrolled
|
|
221
|
+
if (this.props.time === undefined) {
|
|
222
|
+
this.currentTime = value;
|
|
223
|
+
this.props.timeline?.setTime(value);
|
|
224
|
+
this.updateHTML();
|
|
225
|
+
}
|
|
226
|
+
// In controlled mode, parent will update time prop which triggers updateHTML via setProps
|
|
150
227
|
};
|
|
151
228
|
|
|
152
229
|
public play(): void {
|
|
153
|
-
this.
|
|
230
|
+
this._playing = true;
|
|
154
231
|
const {
|
|
155
232
|
timeRange: [min, max]
|
|
156
233
|
} = this.props;
|
|
157
|
-
|
|
234
|
+
// In uncontrolled mode, reset to start if at end
|
|
235
|
+
if (this.props.time === undefined && this.getTime() >= max) {
|
|
158
236
|
this.currentTime = min;
|
|
159
237
|
this.props.onTimeChange(min);
|
|
238
|
+
this.props.timeline?.setTime(min);
|
|
160
239
|
}
|
|
161
240
|
this.updateHTML();
|
|
162
241
|
this.tick();
|
|
163
242
|
}
|
|
164
243
|
|
|
165
244
|
public stop(): void {
|
|
166
|
-
this.
|
|
245
|
+
this._stopTimer();
|
|
246
|
+
this.updateHTML();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Start the playback timer (used internally) */
|
|
250
|
+
private _startTimer(): void {
|
|
251
|
+
this._playing = true;
|
|
252
|
+
this.tick();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Stop the playback timer (used internally) */
|
|
256
|
+
private _stopTimer(): void {
|
|
257
|
+
this._playing = false;
|
|
167
258
|
if (this.timerId !== null) {
|
|
168
259
|
window.clearTimeout(this.timerId);
|
|
169
260
|
this.timerId = null;
|
|
170
261
|
}
|
|
171
|
-
this.updateHTML();
|
|
172
262
|
}
|
|
173
263
|
|
|
174
264
|
private tick = (): void => {
|
|
@@ -178,22 +268,31 @@ export class TimelineWidget extends Widget<TimelineWidgetProps> {
|
|
|
178
268
|
loop
|
|
179
269
|
} = this.props;
|
|
180
270
|
if (step > 0) {
|
|
181
|
-
|
|
271
|
+
const currentTime = this.getTime();
|
|
272
|
+
let next = Math.round(currentTime / step) * step + step;
|
|
182
273
|
if (next > max) {
|
|
183
|
-
if (
|
|
274
|
+
if (currentTime < max) {
|
|
184
275
|
next = max;
|
|
185
276
|
} else if (loop) {
|
|
186
277
|
next = min;
|
|
187
278
|
} else {
|
|
188
279
|
next = max;
|
|
189
|
-
this.
|
|
280
|
+
this._playing = false;
|
|
281
|
+
this.props.onPlayingChange?.(false);
|
|
190
282
|
}
|
|
191
283
|
}
|
|
192
|
-
|
|
284
|
+
|
|
285
|
+
// Always call callback
|
|
193
286
|
this.props.onTimeChange(next);
|
|
287
|
+
|
|
288
|
+
// Only update internal state if uncontrolled
|
|
289
|
+
if (this.props.time === undefined) {
|
|
290
|
+
this.currentTime = next;
|
|
291
|
+
this.props.timeline?.setTime(next);
|
|
292
|
+
}
|
|
194
293
|
this.updateHTML();
|
|
195
294
|
}
|
|
196
|
-
if (this.
|
|
295
|
+
if (this._playing) {
|
|
197
296
|
this.timerId = window.setTimeout(this.tick, this.props.playInterval);
|
|
198
297
|
} else {
|
|
199
298
|
this.timerId = null;
|