|
| 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