Skip to content

Commit

Permalink
feat: make the video in song editor draggable, resizable & snappable (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
oliver139 authored Nov 19, 2024
1 parent 57efffa commit 0126138
Show file tree
Hide file tree
Showing 4 changed files with 372 additions and 44 deletions.
175 changes: 135 additions & 40 deletions app/components/DraggableWindow.vue
Original file line number Diff line number Diff line change
@@ -1,81 +1,176 @@
<script setup lang="ts">
import Moveable, { type OnDrag, type OnResize, type OnResizeStart } from 'vue3-moveable'
interface Props {
x?: number
y?: number
initialWidth?: number
initialHeight?: number
minWidth?: number
minHeight?: number
maxInset?: number
resizable?: boolean
}
const props = withDefaults(defineProps<Props>(), {
x: 0,
y: 0,
maxInset: 0,
initialWidth: 240,
initialHeight: 240,
maxInset: 7,
resizable: false,
})
let stickRight = false
let stickBottom = false
const pos = readonly({
x: props.x,
y: props.y,
})
const dragWindow = ref<HTMLElement | null>(null)
const dragHandle = ref<HTMLElement | null>(null)
const dragWindow = useTemplateRef<HTMLElement>('dragWindow')
const dragHandle = useTemplateRef<HTMLElement>('dragHandle')
const { x: dragX, y: dragY, style, isDragging } = useDraggable(dragWindow, {
initialValue: { x: props.x, y: props.y },
handle: dragHandle,
preventDefault: true,
onEnd() {
stickRight = false
stickBottom = false
limitPosition()
},
})
// #region : Draggable
const isDragging = ref(false)
function handleDrag(e: OnDrag) {
isDragging.value = true
e.target.style.transform = e.transform
}
// #endregion
// #region : Avoid the window stayed outside
const moveableRef = useTemplateRef<InstanceType<typeof Moveable>>('moveableRef')
const { width: windowWidth, height: windowHeight } = useWindowSize()
const dragWindowBounding = useElementBounding(dragWindow)
function limitPosition() {
let x = dragWindowBounding.left.value
let y = dragWindowBounding.top.value
if (dragWindowBounding.left.value < props.maxInset) {
dragX.value = props.maxInset
x = props.maxInset
}
else if (stickRight || dragWindowBounding.right.value > windowWidth.value - props.maxInset) {
dragX.value = windowWidth.value - props.maxInset - dragWindowBounding.width.value
stickRight = true
else if (dragWindowBounding.right.value > windowWidth.value - props.maxInset) {
x = Math.max(windowWidth.value - props.maxInset - dragWindowBounding.width.value, 0)
}
if (dragWindowBounding.top.value < props.maxInset) {
dragY.value = props.maxInset
y = props.maxInset
}
else if (stickBottom || dragWindowBounding.bottom.value > windowHeight.value - props.maxInset) {
dragY.value = windowHeight.value - props.maxInset - dragWindowBounding.height.value
stickBottom = true
else if (dragWindowBounding.bottom.value > windowHeight.value - props.maxInset) {
y = Math.max(windowHeight.value - props.maxInset - dragWindowBounding.height.value, 0)
}
moveableRef.value?.request('draggable', { x, y }, true)
}
onMounted(() => {
const limitPositionDebounced = useDebounceFn(limitPosition, 250)
useEventListener('resize', limitPositionDebounced, { passive: true })
})
// #endregion
// #region : Resizable
function handleResize(e: OnResize) {
e.target.style.width = `${e.width}px`
e.target.style.height = `${e.height}px`
e.target.style.transform = e.drag.transform
}
function handleResizeStart(e: OnResizeStart) {
e.setMin([240, 160])
}
// #endregion
// #region : Reset
function reset() {
if (!dragWindow.value || !moveableRef.value)
return
dragWindow.value.style.top = ''
dragWindow.value.style.left = ''
dragWindow.value.style.width = ''
dragWindow.value.style.height = ''
dragWindow.value.style.transform = ''
limitPosition()
moveableRef.value.updateRect()
}
// #endregion
</script>

<template>
<div
ref="dragWindow"
class="draggable-window"
:class="{
'pointer-events-none': isDragging,
}"
fixed
:style
z-floating
>
<Teleport to="body">
<div
ref="dragHandle"
class="@hover:(bg-gray/10 op100)"
flex="~ items-center justify-center"
draggable pointer-events-auto mxa w-20 cursor-move rounded-full op25
ref="dragWindow"
class="drag-window"
:class="{
'pointer-events-none': isDragging,
'pointer-events-auto': !isDragging,
}"
:style="{
'--moveable-left': `${pos.x}px`,
'--moveable-top': `${pos.y}px`,
'--moveable-width': `${props.initialWidth}px`,
'--moveable-height': `${props.initialHeight}px`,
}"
fixed h-full w-full
v-bind="$attrs"
>
<div i-mdi-drag-horizontal ma text-size-xl />
<div
ref="dragHandle"
class="drag-handle left-50% top-0 transform-translate-x--50% transform-translate-y--100% @hover:(bg-gray/10 op100)"
flex="~ items-center justify-center"
draggable absolute mxa h-6 w-20 cursor-move rounded-full op25
@dblclick.left="reset"
>
<div i-mdi-drag-horizontal ma text-size-xl />
</div>
<slot />
</div>
<slot />
</div>
<Moveable
ref="moveableRef"
:target="dragWindow"
:origin="false"
:draggable="true"
:drag-target="dragHandle"
:throttle-drag="3"
:resizable="resizable"
:throttle-resize="3"
:snappable="true"
:bounds="{
left: 7,
right: 7,
top: 31,
bottom: 7,
position: 'css',
}"
:style="{
'--bounds-color': 'transparent',
'--moveable-color': 'transparent',
}"
@resize="handleResize"
@resize-start="handleResizeStart"
@drag="handleDrag"
@drag-end="isDragging = false"
/>
</Teleport>
</template>

<style>
.drag-window {
left: var(--moveable-left);
top: var(--moveable-top);
width: var(--moveable-width);
height: var(--moveable-height);
max-width: calc(100vw - 14px);
max-height: calc(100vh - 38px);
max-height: calc(100dvh - 38px);
}
.drag-window:hover + .moveable-control-box,
.moveable-control-box:hover {
--moveable-control-opacity: 0.2;
}
.moveable-control-box .moveable-control {
opacity: var(--moveable-control-opacity, 0);
background: rgba(0 0 0 / 0.5) !important;
transition: opacity 150ms ease;
}
.moveable-control-box .moveable-control:hover {
--moveable-control-opacity: 1;
}
</style>
22 changes: 18 additions & 4 deletions app/components/editors/SongEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,17 @@ onMounted(() => {
</script>

<template>
<DraggableWindow :x="windowWidth - 32 - 480" :y="60">
<div flex="~ col">
<YouTubePlayer w-120 rounded-lg border="~ base" />
<div flex="~ gap-2 items-center" mt--2 w-max self-end p1 px2 text-sm floating-glass>
<DraggableWindow
id="editor-video"
:x="windowWidth - 32 - 480"
:y="84"
:initial-width="480"
:initial-height="300"
resizable
>
<div flex="~ col" relative h-full w-full pb-6.5>
<YouTubePlayer h-full w-full rounded-lg border="~ base" />
<div flex="~ gap-2 items-center" absolute bottom-0 mt--2 self-end p1 px2 text-sm floating-glass>
<!-- <IconButton
:icon="controls.status.value === 'playing' ? 'i-uil-pause' : 'i-uil-play'"
@click="controls.toggle()"
Expand Down Expand Up @@ -489,3 +496,10 @@ onMounted(() => {
</div>
</div>
</template>

<style>
#editor-video iframe {
width: 100% !important;
height: 100% !important;
}
</style>
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"uqr": "^0.1.2",
"valibot": "^0.42.1",
"vue-virtual-scroller": "2.0.0-beta.8",
"vue3-moveable": "^0.28.0",
"weq8": "^0.2.2"
},
"devDependencies": {
Expand Down
Loading

0 comments on commit 0126138

Please sign in to comment.