Skip to content

Commit 37eadad

Browse files
authored
Merge pull request #22 from jrafaaael/feat/drag
feat: drag zooms and trim
2 parents c8f6dd8 + d1d0879 commit 37eadad

File tree

6 files changed

+110
-33
lines changed

6 files changed

+110
-33
lines changed

src/routes/[id]/+page.svelte

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
let videoRef: Video;
2323
let paused = true;
2424
let ended: boolean;
25-
let isTrimming = false;
2625
2726
onMount(async () => {
2827
try {
@@ -116,14 +115,13 @@
116115
<Seeker
117116
startAt={$edits.startAt}
118117
endAt={$edits.endAt}
119-
{isTrimming}
120118
on:changeTime={({ detail }) => {
121119
videoRef.pause();
122120
$videoStatus.currentTime = detail.newTime;
123121
}}
124122
/>
125123
<div class="w-full h-10 relative">
126-
<Trimmer bind:isResizing={isTrimming} />
124+
<Trimmer />
127125
</div>
128126
<ZoomList />
129127
</div>

src/routes/[id]/components/resizable.svelte renamed to src/routes/[id]/components/moveable.svelte

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
export let width: number | null = null;
77
export let left: number | null = null;
88
export let className: { root?: string; handle?: string; handleE?: string; handleW?: string };
9-
export let isResizing: boolean | null = null;
9+
let moveableRef: HTMLDivElement;
10+
let isResizing: boolean | null = null;
1011
let direction: Direction | null = null;
11-
let resizableRef: HTMLDivElement;
1212
let mousePositionWhenResizingStart: number | null = null;
1313
let trimmerRectWhenResizingStart: DOMRect | null = null;
14+
let isDragging = false;
15+
let moveablePositionWhenDragStart: { top: number; left: number } | null = null;
1416
let dispatcher = createEventDispatcher();
1517
1618
function handleResizeStart(
@@ -19,7 +21,7 @@
1921
},
2022
dir: Direction
2123
) {
22-
trimmerRectWhenResizingStart = resizableRef.getBoundingClientRect();
24+
trimmerRectWhenResizingStart = moveableRef.getBoundingClientRect();
2325
mousePositionWhenResizingStart = e.pageX;
2426
isResizing = true;
2527
direction = dir;
@@ -36,9 +38,9 @@
3638
return;
3739
}
3840
39-
const trimmerRect = resizableRef.getBoundingClientRect();
41+
const trimmerRect = moveableRef.getBoundingClientRect();
4042
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
41-
const constrains = resizableRef.parentElement!.getBoundingClientRect();
43+
const constrains = moveableRef.parentElement!.getBoundingClientRect();
4244
4345
if (direction === 'right') {
4446
const delta = e.pageX - mousePositionWhenResizingStart;
@@ -53,13 +55,13 @@
5355
if (!width) {
5456
const widthInPercentage = (interalWidth * 100) / constrains.width;
5557
56-
resizableRef.style.setProperty('width', widthInPercentage.toFixed(1) + '%');
58+
moveableRef.style.setProperty('width', widthInPercentage.toFixed(1) + '%');
5759
}
5860
5961
dispatcher('resize', {
6062
direction,
6163
delta: deltaWidth,
62-
refToElement: resizableRef
64+
refToElement: moveableRef
6365
});
6466
} else if (direction === 'left') {
6567
const delta = mousePositionWhenResizingStart - e.pageX;
@@ -76,31 +78,67 @@
7678
const widthInPercentage = (internalWidth * 100) / constrains.width;
7779
const leftInPercentage = Math.max(0, (internalLeft * 100) / constrains.width);
7880
79-
resizableRef.style.setProperty('width', widthInPercentage.toFixed(1) + '%');
80-
resizableRef.style.setProperty('left', leftInPercentage.toFixed(1) + '%');
81+
moveableRef.style.setProperty('width', widthInPercentage.toFixed(1) + '%');
82+
moveableRef.style.setProperty('left', leftInPercentage.toFixed(1) + '%');
8183
}
8284
8385
dispatcher('resize', {
8486
direction,
8587
delta: deltaWidth,
86-
refToElement: resizableRef
88+
refToElement: moveableRef
8789
});
8890
}
8991
}
9092
91-
function handleResizeEnd() {
93+
function handleDragStart(e: MouseEvent) {
94+
isDragging = true;
95+
moveablePositionWhenDragStart = {
96+
top: e.clientY - moveableRef.offsetTop,
97+
left: e.clientX - moveableRef.offsetLeft
98+
};
99+
dispatcher('dragStart');
100+
}
101+
102+
function handleDrag(e: MouseEvent) {
103+
if (!isDragging) return;
104+
if (!moveablePositionWhenDragStart) return;
105+
106+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
107+
const left = e.clientX - moveablePositionWhenDragStart.left;
108+
109+
if (!left) {
110+
moveableRef.style.setProperty('left', left.toFixed(1) + 'px');
111+
}
112+
113+
dispatcher('drag', {
114+
left,
115+
refToElement: moveableRef
116+
});
117+
}
118+
119+
function handleEvents(e: MouseEvent) {
120+
if (isResizing) {
121+
handleResize(e);
122+
} else if (isDragging) {
123+
handleDrag(e);
124+
}
125+
}
126+
127+
function handleEnd() {
92128
isResizing = false;
129+
isDragging = false;
93130
direction = null;
94131
dispatcher('resizeEnd');
132+
dispatcher('dragEnd');
95133
}
96134
97135
onMount(() => {
98-
document.addEventListener('mousemove', handleResize);
99-
document.addEventListener('mouseup', handleResizeEnd);
136+
document.addEventListener('mousemove', handleEvents);
137+
document.addEventListener('mouseup', handleEnd);
100138
101139
return () => {
102-
document.removeEventListener('mousemove', handleResize);
103-
document.removeEventListener('mouseup', handleResizeEnd);
140+
document.removeEventListener('mousemove', handleEvents);
141+
document.removeEventListener('mouseup', handleEnd);
104142
};
105143
});
106144
</script>
@@ -119,22 +157,23 @@
119157
<div
120158
class={className.root}
121159
style="width: {width ?? 100}%; left: {left ?? 0}%;"
122-
bind:this={resizableRef}
160+
bind:this={moveableRef}
161+
on:mousedown={handleDragStart}
123162
on:mouseenter={(e) => dispatcher('mouseenter', { e })}
124163
on:mouseleave={(e) => dispatcher('mouseleave', { e })}
125164
>
126165
<div class="w-full h-full relative">
127166
<button
128167
class="{className.handle ??
129168
'min-w-5 h-full cursor-ew-resize absolute top-0 left-0'} {className.handleW ?? ''}"
130-
on:mousedown={(e) => handleResizeStart(e, 'left')}
169+
on:mousedown|stopPropagation={(e) => handleResizeStart(e, 'left')}
131170
>
132171
<slot name="w" />
133172
</button>
134173
<button
135174
class="{className.handle ??
136175
'min-w-5 h-full cursor-ew-resize absolute top-0 right-0'} {className.handleE ?? ''}"
137-
on:mousedown={(e) => handleResizeStart(e, 'right')}
176+
on:mousedown|stopPropagation={(e) => handleResizeStart(e, 'right')}
138177
>
139178
<slot name="e" />
140179
</button>

src/routes/[id]/components/seeker.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
import { createEventDispatcher, onMount } from 'svelte';
33
import { recording } from '../stores/recording.store';
44
import { videoStatus } from '../stores/video-status.store';
5+
import { isEditingTrim } from '../stores/is-editing-trim.store';
56
67
export let startAt: number;
78
export let endAt: number;
8-
export let isTrimming: boolean;
99
let isDragging = false;
1010
let seekbarRef: HTMLButtonElement;
1111
let dispatcher = createEventDispatcher();
@@ -57,7 +57,7 @@
5757
class="h-[calc(100%+8px)] px-2 absolute bottom-0 z-50 cursor-col-resize {$videoStatus.currentTime <=
5858
startAt ||
5959
isDragging ||
60-
(isTrimming && (endAt <= $videoStatus.currentTime || startAt >= $videoStatus.currentTime))
60+
($isEditingTrim && (endAt <= $videoStatus.currentTime || startAt >= $videoStatus.currentTime))
6161
? 'transition-none'
6262
: 'transition-[left] ease-linear duration-100'}"
6363
style="--position: {position}%; left: calc(var(--position, 0%) - 8px);"

src/routes/[id]/components/trimmer.svelte

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
<script lang="ts">
22
import { edits } from '../stores/edits.store';
3+
import { isEditingTrim } from '../stores/is-editing-trim.store';
34
import { recording } from '../stores/recording.store';
45
import { videoStatus } from '../stores/video-status.store';
5-
import Resizable from './resizable.svelte';
6+
import Moveable from './moveable.svelte';
67
7-
export let isResizing: boolean;
88
const MIN_VIDEO_DURATION_IN_SECONDS = 1;
99
$: width = (($edits.endAt - $edits.startAt) * 100) / $recording?.duration;
1010
$: left = ($edits.startAt * 100) / $recording?.duration;
1111
</script>
1212

13-
<Resizable
13+
<Moveable
1414
className={{
15-
root: `group h-10 bg-white/5 border-2 border-white/10 rounded-lg absolute ring ring-transparent ring-offset-0 [&.current-trim]:bg-blue-700/20 [&.current-trim]:border-blue-700/50 [&.current-trim]:focus-within:ring-blue-700/20 hover:bg-blue-700/20 hover:border-blue-700/50 has-[:active]:bg-blue-700/20 has-[:active]:border-blue-700/50 focus-within:ring-white/5 focus-within:hover:ring-blue-700/20 hover:z-10 ${
15+
root: `group h-10 bg-white/5 border-2 border-white/10 rounded-lg absolute ring ring-transparent ring-offset-0 cursor-grab active:cursor-grabbing [&.current-trim]:bg-blue-700/20 [&.current-trim]:border-blue-700/50 [&.current-trim]:focus-within:ring-blue-700/20 hover:bg-blue-700/20 hover:border-blue-700/50 has-[:active]:bg-blue-700/20 has-[:active]:border-blue-700/50 focus-within:ring-white/5 focus-within:hover:ring-blue-700/20 hover:z-10 ${
1616
$videoStatus.currentTime >= $edits.startAt && $videoStatus.currentTime <= $edits.endAt
1717
? 'current-trim'
1818
: ''
@@ -24,7 +24,7 @@
2424
}}
2525
{width}
2626
{left}
27-
bind:isResizing
27+
on:resizeStart={() => ($isEditingTrim = true)}
2828
on:resize={({ detail }) => {
2929
const { direction, delta, refToElement } = detail;
3030
const zoomRect = refToElement.getBoundingClientRect();
@@ -46,11 +46,27 @@
4646
$edits.endAt = Math.max(end, Math.abs($edits.startAt + MIN_VIDEO_DURATION_IN_SECONDS));
4747
}
4848
}}
49+
on:resizeEnd={() => ($isEditingTrim = false)}
50+
on:dragStart={() => ($isEditingTrim = true)}
51+
on:drag={({ detail }) => {
52+
const { refToElement, left } = detail;
53+
const constrains = refToElement.parentElement.getBoundingClientRect();
54+
const dif = $edits.endAt - $edits.startAt;
55+
const start = +Math.max(
56+
0,
57+
(left * $recording.duration) / (constrains.right - constrains.left)
58+
).toFixed(2);
59+
const end = +Math.min($recording?.duration ?? Infinity, start + dif).toFixed(2);
60+
61+
$edits.startAt = Math.min(start, end - dif);
62+
$edits.endAt = end;
63+
}}
64+
on:dragEnd={() => ($isEditingTrim = false)}
4965
>
5066
<div slot="w" class="w-[12px] h-[75%] bg-blue-900 rounded-l-md flex justify-center items-center">
5167
<div class="w-[2px] h-[45%] bg-neutral-50/50 rounded-full" />
5268
</div>
5369
<div slot="e" class="w-[12px] h-[75%] bg-blue-900 rounded-r-md flex justify-center items-center">
5470
<div class="w-[2px] h-[45%] bg-neutral-50/50 rounded-full" />
5571
</div>
56-
</Resizable>
72+
</Moveable>

src/routes/[id]/components/zoom-list.svelte

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { videoStatus } from '../stores/video-status.store';
44
import { zooms } from '../stores/zooms.store';
55
import { ZOOM_TRANSITION_DURATION } from '../utils/constants';
6-
import Resizable from './resizable.svelte';
6+
import Moveable from './moveable.svelte';
77
</script>
88

99
<div class="w-full h-10 relative">
@@ -12,9 +12,9 @@
1212
{@const left = (zoom.start * 100) / $recording?.duration}
1313
{@const nextZoom = $zooms.at(idx + 1)}
1414
{@const prevZoom = idx === 0 ? null : $zooms.at(idx - 1)}
15-
<Resizable
15+
<Moveable
1616
className={{
17-
root: `group h-10 bg-white/5 border-2 border-white/10 rounded-lg absolute ring ring-transparent ring-offset-0 [&.current-zoom]:bg-emerald-800/30 [&.current-zoom]:border-emerald-800/80 [&.current-zoom]:focus-within:ring-emerald-800/30 hover:bg-emerald-800/30 hover:border-emerald-800/80 has-[:active]:bg-emerald-800/30 has-[:active]:border-emerald-800/80 focus-within:ring-white/5 focus-within:hover:ring-emerald-800/30 focus-within:active:ring-emerald-800/30 ${
17+
root: `group h-10 bg-white/5 border-2 border-white/10 rounded-lg absolute ring ring-transparent ring-offset-0 cursor-grab active:cursor-grabbing [&.current-zoom]:bg-emerald-800/30 [&.current-zoom]:border-emerald-800/80 [&.current-zoom]:focus-within:ring-emerald-800/30 hover:bg-emerald-800/30 hover:border-emerald-800/80 has-[:active]:bg-emerald-800/30 has-[:active]:border-emerald-800/80 focus-within:ring-white/5 focus-within:hover:ring-emerald-800/30 focus-within:active:ring-emerald-800/30 ${
1818
$videoStatus.currentTime >= zoom.start && $videoStatus.currentTime <= zoom.end
1919
? 'current-zoom'
2020
: ''
@@ -96,6 +96,27 @@
9696
}
9797
}
9898
}}
99+
on:drag={({ detail }) => {
100+
const { refToElement, left } = detail;
101+
const constrains = refToElement.parentElement.getBoundingClientRect();
102+
const dif = zoom.end - zoom.start;
103+
const start = +Math.max(
104+
0,
105+
(left * $recording.duration) / (constrains.right - constrains.left),
106+
prevZoom?.end ?? -Infinity
107+
).toFixed(2);
108+
const end = +Math.min(
109+
$recording?.duration ?? Infinity,
110+
start + dif,
111+
nextZoom?.start ?? Infinity
112+
).toFixed(2);
113+
114+
zooms.updateZoomById({
115+
...zoom,
116+
start: Math.min(start, end - dif),
117+
end
118+
});
119+
}}
99120
on:mouseenter={({ detail }) => {
100121
const { e } = detail;
101122

@@ -129,6 +150,6 @@
129150
>
130151
<div class="w-[2px] h-[45%] bg-neutral-50/50 rounded-full" />
131152
</div>
132-
</Resizable>
153+
</Moveable>
133154
{/each}
134155
</div>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { writable } from 'svelte/store';
2+
3+
export const isEditingTrim = writable(false);

0 commit comments

Comments
 (0)