Skip to content

Commit 342deda

Browse files
authored
feat(widgets): minimal TimelineWidget (#9587)
1 parent baa7f33 commit 342deda

File tree

4 files changed

+216
-2
lines changed

4 files changed

+216
-2
lines changed

examples/get-started/pure-js/widgets/app.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import {
99
ZoomWidget,
1010
FullscreenWidget,
1111
DarkGlassTheme,
12-
LightGlassTheme
12+
LightGlassTheme,
13+
TimelineWidget
1314
} from '@deck.gl/widgets';
1415
import '@deck.gl/widgets/stylesheet.css';
1516

@@ -77,6 +78,15 @@ new Deck({
7778
widgets: [
7879
new ZoomWidget({style: widgetTheme}),
7980
new CompassWidget({style: widgetTheme}),
80-
new FullscreenWidget({style: widgetTheme})
81+
new FullscreenWidget({style: widgetTheme}),
82+
new TimelineWidget({
83+
style: widgetTheme,
84+
domain: [0, 24],
85+
step: 1,
86+
value: 0,
87+
playInterval: 1000,
88+
// eslint-disable-next-line no-console, no-undef
89+
onTimeChange: time => console.log('Time:', time)
90+
})
8191
]
8292
});

modules/widgets/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,6 @@ export {_components};
4343

4444
export type {WidgetImplProps} from './widget-impl';
4545
export {WidgetImpl as _WidgetImpl} from './widget-impl';
46+
// Timeline widget
47+
export {TimelineWidget} from './timeline-widget';
48+
export type {TimelineWidgetProps} from './timeline-widget';

modules/widgets/src/stylesheet.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,32 @@
200200
url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="black"><path d="M480-320v-100q0-25 17.5-42.5T540-480h100v60H540v100h-60Zm60 240q-25 0-42.5-17.5T480-140v-100h60v100h100v60H540Zm280-240v-100H720v-60h100q25 0 42.5 17.5T880-420v100h-60ZM720-80v-60h100v-100h60v100q0 25-17.5 42.5T820-80H720Zm111-480h-83q-26-88-99-144t-169-56q-117 0-198.5 81.5T200-480q0 72 32.5 132t87.5 98v-110h80v240H160v-80h94q-62-50-98-122.5T120-480q0-75 28.5-140.5t77-114q48.5-48.5 114-77T480-840q129 0 226.5 79.5T831-560Z"/></svg>')
201201
);
202202
}
203+
204+
/* Timeline widget styles */
205+
.deck-widget.deck-widget-timeline {
206+
display: flex;
207+
align-items: center;
208+
pointer-events: 'auto';
209+
}
210+
.deck-widget.deck-widget-timeline .timeline-slider {
211+
margin: 0 8px;
212+
flex: 1;
213+
pointer-events: 'auto';
214+
cursor: pointer;
215+
}
216+
.deck-widget.deck-widget-timeline button.timeline-play-pause {
217+
background: var(--button-background, #fff);
218+
border: var(--button-inner-stroke, unset);
219+
border-radius: var(--button-corner-radius, 4px);
220+
width: var(--button-size, 28px);
221+
height: var(--button-size, 28px);
222+
cursor: pointer;
223+
display: flex;
224+
align-items: center;
225+
justify-content: center;
226+
padding: 0;
227+
pointer-events: 'auto';
228+
}
203229

204230
.deck-widget-spinner-icon {
205231
mask-image: var(
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// deck.gl
2+
// SPDX-License-Identifier: MIT
3+
// Copyright (c) vis.gl contributors
4+
5+
/* global document */
6+
import type {Deck, Widget} from '@deck.gl/core';
7+
import {render} from 'preact';
8+
9+
export type TimelineWidgetProps = {
10+
/**
11+
* Widget id
12+
*/
13+
id?: string;
14+
/**
15+
* CSS inline style overrides.
16+
*/
17+
style?: Partial<CSSStyleDeclaration>;
18+
/**
19+
* Additional CSS class.
20+
*/
21+
className?: string;
22+
/**
23+
* Slider domain [min, max].
24+
*/
25+
domain?: [number, number];
26+
/**
27+
* Slider step.
28+
*/
29+
step?: number;
30+
/**
31+
* Current slider value.
32+
*/
33+
value?: number;
34+
/**
35+
* Callback when value changes.
36+
*/
37+
onTimeChange?: (value: number) => void;
38+
/**
39+
* Play interval in milliseconds.
40+
*/
41+
playInterval?: number;
42+
};
43+
44+
export class TimelineWidget implements Widget<TimelineWidgetProps> {
45+
id = 'timeline';
46+
props: Required<TimelineWidgetProps>;
47+
deck?: Deck<any>;
48+
element?: HTMLDivElement;
49+
private playing = false;
50+
private timerId: number | null = null;
51+
52+
static defaultProps: Required<TimelineWidgetProps> = {
53+
id: 'timeline',
54+
style: {},
55+
className: undefined!,
56+
domain: [0, 100],
57+
step: 1,
58+
value: 0,
59+
onTimeChange: () => {},
60+
playInterval: 1000
61+
};
62+
63+
constructor(props: TimelineWidgetProps = {}) {
64+
this.id = props.id ?? this.id;
65+
this.props = {...TimelineWidget.defaultProps, ...props};
66+
}
67+
68+
onAdd({deck}: {deck: Deck<any>}): HTMLDivElement {
69+
this.deck = deck;
70+
const el = document.createElement('div');
71+
el.classList.add('deck-widget', 'deck-widget-timeline');
72+
if (this.props.className) {
73+
el.classList.add(this.props.className);
74+
}
75+
Object.assign(el.style, this.props.style);
76+
this.element = el;
77+
this.renderUI();
78+
return el;
79+
}
80+
81+
onRemove(): void {
82+
this.stop();
83+
this.deck = undefined;
84+
this.element = undefined;
85+
}
86+
87+
setProps(props: Partial<TimelineWidgetProps>): void {
88+
Object.assign(this.props, props);
89+
if (this.element) {
90+
// update className
91+
this.element.className = ['deck-widget', 'deck-widget-timeline', this.props.className]
92+
.filter(Boolean)
93+
.join(' ');
94+
// update style
95+
if (props.style) {
96+
Object.assign(this.element.style, this.props.style);
97+
}
98+
}
99+
this.renderUI();
100+
}
101+
102+
private handlePlayPause = (): void => {
103+
if (this.playing) {
104+
this.stop();
105+
} else {
106+
this.start();
107+
}
108+
};
109+
110+
private handleSliderChange = (e: Event): void => {
111+
const input = e.target as HTMLInputElement;
112+
const val = Number(input.value);
113+
this.props.value = val;
114+
this.props.onTimeChange(val);
115+
this.renderUI();
116+
};
117+
118+
private start(): void {
119+
this.playing = true;
120+
this.renderUI();
121+
this.tick();
122+
}
123+
124+
private stop(): void {
125+
this.playing = false;
126+
if (this.timerId != null) {
127+
window.clearTimeout(this.timerId);
128+
this.timerId = null;
129+
}
130+
this.renderUI();
131+
}
132+
133+
private tick = (): void => {
134+
const [min, max] = this.props.domain;
135+
let next = this.props.value + this.props.step;
136+
if (next > max) {
137+
next = min;
138+
}
139+
this.props.value = next;
140+
this.props.onTimeChange(next);
141+
this.renderUI();
142+
if (this.playing) {
143+
this.timerId = window.setTimeout(this.tick, this.props.playInterval);
144+
}
145+
};
146+
147+
private renderUI(): void {
148+
const el = this.element;
149+
if (!el) {
150+
return;
151+
}
152+
const ui = (
153+
<div style={{display: 'flex', alignItems: 'center', pointerEvents: 'auto'}}>
154+
<button
155+
type="button"
156+
onClick={this.handlePlayPause}
157+
className="timeline-play-pause"
158+
title={this.playing ? 'Pause' : 'Play'}
159+
>
160+
{this.playing ? '⏸' : '▶'}
161+
</button>
162+
<input
163+
type="range"
164+
min={this.props.domain[0]}
165+
max={this.props.domain[1]}
166+
step={this.props.step}
167+
value={this.props.value}
168+
onInput={this.handleSliderChange}
169+
className="timeline-slider"
170+
/>
171+
</div>
172+
);
173+
render(ui, el);
174+
}
175+
}

0 commit comments

Comments
 (0)