Skip to content

Commit 35691f6

Browse files
committed
New feature - Add BaseDraggableDialog atom component
1 parent 9f13dde commit 35691f6

File tree

1 file changed

+316
-0
lines changed

1 file changed

+316
-0
lines changed

src/atoms/BaseDraggableDialog.vue

Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
<script setup>
2+
import { ref, reactive, computed, defineExpose, onUnmounted, nextTick } from "vue";
3+
import BaseIcon from "./BaseIcon.vue";
4+
import { XMLNS } from "../lib";
5+
6+
const props = defineProps({
7+
backgroundColor: { type: String },
8+
color: { type: String },
9+
headerBg: { type: String },
10+
headerColor: { type: String }
11+
});
12+
13+
const emit = defineEmits(["close"]);
14+
15+
const isOpen = ref(false);
16+
const hasBeenOpened = ref(false);
17+
18+
const modal = reactive({
19+
left: window.innerWidth / 2 - 200,
20+
top: window.innerHeight / 2 - 120,
21+
width: 400,
22+
height: 400,
23+
dragging: false,
24+
resizing: false,
25+
dragOffsetX: 0,
26+
dragOffsetY: 0,
27+
pointerStartX: 0,
28+
pointerStartY: 0,
29+
resizeStartW: 0,
30+
resizeStartH: 0,
31+
});
32+
33+
function open() {
34+
isOpen.value = true;
35+
nextTick(() => {
36+
if (!hasBeenOpened.value) {
37+
modal.left = Math.max(0, window.innerWidth / 2 - modal.width / 2);
38+
modal.top = Math.max(0, window.innerHeight / 2 - modal.height / 2);
39+
hasBeenOpened.value = true;
40+
}
41+
});
42+
}
43+
function close() {
44+
isOpen.value = false;
45+
emit("close");
46+
}
47+
48+
defineExpose({ open, close });
49+
50+
const modalStyle = computed(() => ({
51+
position: "fixed",
52+
left: `${modal.left}px`,
53+
top: `${modal.top}px`,
54+
width: `${modal.width}px`,
55+
height: `${modal.height}px`,
56+
padding: 0,
57+
border: "none",
58+
background: props.backgroundColor,
59+
boxShadow: "0 4px 24px rgba(0,0,0,0.15)",
60+
zIndex: 9999,
61+
overflow: "visible",
62+
borderRadius: "2px"
63+
}));
64+
65+
function getPointer(e) {
66+
if (e.touches && e.touches.length) {
67+
return { x: e.touches[0].clientX, y: e.touches[0].clientY };
68+
}
69+
return { x: e.clientX, y: e.clientY };
70+
}
71+
72+
function initDrag(e) {
73+
e.preventDefault?.();
74+
modal.dragging = true;
75+
const pointer = getPointer(e);
76+
modal.dragOffsetX = pointer.x - modal.left;
77+
modal.dragOffsetY = pointer.y - modal.top;
78+
document.addEventListener("mousemove", drag);
79+
document.addEventListener("mouseup", endDrag);
80+
document.addEventListener("touchmove", drag, { passive: false });
81+
document.addEventListener("touchend", endDrag);
82+
}
83+
84+
function drag(e) {
85+
if (!modal.dragging) return;
86+
e.preventDefault?.();
87+
const pointer = getPointer(e);
88+
let newLeft = pointer.x - modal.dragOffsetX;
89+
let newTop = pointer.y - modal.dragOffsetY;
90+
newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - modal.width));
91+
newTop = Math.max(0, Math.min(newTop, window.innerHeight - modal.height));
92+
modal.left = newLeft;
93+
modal.top = newTop;
94+
}
95+
96+
function endDrag() {
97+
modal.dragging = false;
98+
document.removeEventListener("mousemove", drag);
99+
document.removeEventListener("mouseup", endDrag);
100+
document.removeEventListener("touchmove", drag);
101+
document.removeEventListener("touchend", endDrag);
102+
}
103+
104+
function initResize(e) {
105+
e.preventDefault?.();
106+
modal.resizing = true;
107+
const pointer = getPointer(e);
108+
modal.pointerStartX = pointer.x;
109+
modal.pointerStartY = pointer.y;
110+
modal.resizeStartW = modal.width;
111+
modal.resizeStartH = modal.height;
112+
document.addEventListener("mousemove", resize);
113+
document.addEventListener("mouseup", endResize);
114+
document.addEventListener("touchmove", resize, { passive: false });
115+
document.addEventListener("touchend", endResize);
116+
}
117+
118+
function resize(e) {
119+
if (!modal.resizing) return;
120+
e.preventDefault?.();
121+
const pointer = getPointer(e);
122+
let dx = pointer.x - modal.pointerStartX;
123+
let dy = pointer.y - modal.pointerStartY;
124+
modal.width = Math.max(240, modal.resizeStartW + dx);
125+
modal.height = Math.max(400, modal.resizeStartH + dy);
126+
}
127+
128+
function endResize() {
129+
modal.resizing = false;
130+
document.removeEventListener("mousemove", resize);
131+
document.removeEventListener("mouseup", endResize);
132+
document.removeEventListener("touchmove", resize);
133+
document.removeEventListener("touchend", endResize);
134+
}
135+
136+
function initResizeLeft(e) {
137+
e.preventDefault?.();
138+
modal.resizing = true;
139+
const pointer = getPointer(e);
140+
modal.pointerStartX = pointer.x;
141+
modal.pointerStartY = pointer.y;
142+
modal.resizeStartW = modal.width;
143+
modal.resizeStartH = modal.height;
144+
modal.resizeStartLeft = modal.left;
145+
modal.resizeStartTop = modal.top;
146+
document.addEventListener("mousemove", resizeLeft);
147+
document.addEventListener("mouseup", endResizeLeft);
148+
document.addEventListener("touchmove", resizeLeft, { passive: false });
149+
document.addEventListener("touchend", endResizeLeft);
150+
}
151+
152+
function resizeLeft(e) {
153+
if (!modal.resizing) return;
154+
e.preventDefault?.();
155+
const pointer = getPointer(e);
156+
let dx = pointer.x - modal.pointerStartX;
157+
let newLeft = Math.min(
158+
Math.max(0, modal.resizeStartLeft + dx),
159+
modal.resizeStartLeft + modal.resizeStartW - 240 // min width
160+
);
161+
let newWidth = modal.resizeStartW - (newLeft - modal.resizeStartLeft);
162+
let dy = pointer.y - modal.pointerStartY;
163+
let newHeight = Math.max(400, modal.resizeStartH + dy);
164+
modal.left = newLeft;
165+
modal.width = newWidth;
166+
modal.height = newHeight;
167+
}
168+
169+
function endResizeLeft() {
170+
modal.resizing = false;
171+
document.removeEventListener("mousemove", resizeLeft);
172+
document.removeEventListener("mouseup", endResizeLeft);
173+
document.removeEventListener("touchmove", resizeLeft);
174+
document.removeEventListener("touchend", endResizeLeft);
175+
}
176+
177+
onUnmounted(() => {
178+
endDrag();
179+
endResize();
180+
endResizeLeft();
181+
});
182+
</script>
183+
184+
<template>
185+
<Teleport to="body">
186+
<div v-if="isOpen" class="modal vue-ui-draggable-dialog" :style="modalStyle" @click.stop>
187+
<div class="modal-header" :style="{
188+
backgroundColor: headerBg,
189+
color: headerColor
190+
}">
191+
<span class="drag-handle" @mousedown.stop.prevent="initDrag" @touchstart.stop.prevent="initDrag">
192+
<svg
193+
:xmlns="XMLNS"
194+
width="20"
195+
height="20"
196+
viewBox="0 0 24 24"
197+
fill="none"
198+
stroke="currentColor"
199+
stroke-width="1"
200+
stroke-linecap="round"
201+
stroke-linejoin="round"
202+
>
203+
<path d="M5 9m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
204+
<path d="M5 15m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
205+
<path d="M12 9m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
206+
<path d="M12 15m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
207+
<path d="M19 9m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
208+
<path d="M19 15m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
209+
</svg>
210+
211+
212+
</span>
213+
<span class="modal-title">
214+
<slot name="title"/>
215+
</span>
216+
<button class="close" @click="close">
217+
<BaseIcon name="close" :stroke="headerColor"/>
218+
</button>
219+
</div>
220+
<div class="modal-body">
221+
<slot />
222+
</div>
223+
<div class="resize-handle" @mousedown.stop.prevent="initResize" @touchstart.stop.prevent="initResize" />
224+
<div
225+
class="resize-handle resize-handle-left"
226+
@mousedown.stop.prevent="initResizeLeft"
227+
@touchstart.stop.prevent="initResizeLeft"
228+
/>
229+
</div>
230+
</Teleport>
231+
</template>
232+
233+
<style scoped>
234+
.modal-header {
235+
display: flex;
236+
align-items: center;
237+
user-select: none;
238+
padding: 0.5em 0 0.5em 0.5em;
239+
border-radius: 2px 2px 0 0;
240+
position: relative;
241+
}
242+
243+
.drag-handle {
244+
display: flex;
245+
align-items: center;
246+
cursor: grab;
247+
margin-right: 0.5em;
248+
padding: 0.2em;
249+
}
250+
251+
.drag-handle:active {
252+
cursor: grabbing;
253+
}
254+
255+
.modal-title {
256+
flex: 1;
257+
font-weight: bold;
258+
}
259+
260+
.close {
261+
background: none;
262+
border: none;
263+
cursor: pointer;
264+
display: flex;
265+
align-items:center;
266+
justify-content: center;
267+
}
268+
269+
.modal-body {
270+
width: 100%;
271+
height: 80%;
272+
transition: all 0.2s ease-in-out;
273+
}
274+
275+
.resize-handle {
276+
width: 20px;
277+
height: 20px;
278+
position: absolute;
279+
right: 0;
280+
bottom: 0;
281+
cursor: nwse-resize;
282+
z-index: 1;
283+
background: transparent;
284+
}
285+
286+
.resize-handle:after {
287+
content: '';
288+
display: block;
289+
width: 14px;
290+
height: 14px;
291+
border-right: 2px solid v-bind(color);
292+
border-bottom: 2px solid v-bind(color);
293+
position: absolute;
294+
right: 3px;
295+
bottom: 3px;
296+
border-radius: 2px;
297+
}
298+
299+
.resize-handle-left {
300+
left: 0;
301+
cursor: nesw-resize;
302+
}
303+
.resize-handle-left:after {
304+
content: '';
305+
display: block;
306+
width: 14px;
307+
height: 14px;
308+
border-right: 0px solid transparent;
309+
border-left: 2px solid v-bind(color);
310+
border-bottom: 2px solid v-bind(color);
311+
position: absolute;
312+
left: 3px;
313+
bottom: 3px;
314+
border-radius: 2px;
315+
}
316+
</style>

0 commit comments

Comments
 (0)