|
230 | 230 | </div>
|
231 | 231 | <div v-show="tab === 'erd'" class="px-4 w-full min-h-[calc(100vh-56px-32px-56px)] relative overflow-hidden">
|
232 | 232 | <div class="absolute inset-0" :style="zoomStyle" ref="zoomArea">
|
233 |
| - <div id="preview"></div> |
| 233 | + <div id="preview" :class="{ 'cursor-grab': isSpacePressed, 'cursor-grabbing': isDragging }"></div> |
| 234 | + </div> |
| 235 | + |
| 236 | + <div class="absolute bottom-0 left-1/2 -translate-x-1/2 p-4"> |
| 237 | + <div class="bg-gray-800 text-white text-[10px] px-3 py-1.5 rounded inline-flex items-center space-x-6"> |
| 238 | + <div class="inline-flex items-center"> |
| 239 | + <span class="font-medium">{{i18n[language]["controls"]["movement"]}}:</span> |
| 240 | + <span class="ml-1 text-white/60">{{i18n[language]["controls"]["space_drag"]}} / {{i18n[language]["controls"]["middle_drag"]}}</span> |
| 241 | + </div> |
| 242 | + <div class="inline-flex items-center"> |
| 243 | + <span class="font-medium">{{i18n[language]["controls"]["zoom"]}}:</span> |
| 244 | + <span class="ml-1 text-white/60">{{i18n[language]["controls"]["mouse_wheel"]}} / {{i18n[language]["controls"]["pinch"]}}</span> |
| 245 | + </div> |
| 246 | + </div> |
234 | 247 | </div>
|
235 | 248 |
|
236 | 249 | <div class="absolute bottom-0 right-0 p-4 space-x-4 flex">
|
|
247 | 260 | <button class="text-xs py-1 px-2 rounded hover:bg-white focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 bg-gray-400 text-gray-900" @click="moveRight">→</button>
|
248 | 261 | </div>
|
249 | 262 | </div>
|
250 |
| - |
251 | 263 | </div>
|
252 | 264 | <textarea v-show="tab === 'code'" class="px-4 bg-gray-900 text-gray-300 font-mono w-full text-xs min-h-[calc(100vh-56px-32px-56px)] border-0 focus:ring-0" readonly v-model="mermaidErd"></textarea>
|
253 | 265 | </div>
|
|
302 | 314 | erd: 'ERD',
|
303 | 315 | code: 'Code',
|
304 | 316 | },
|
| 317 | + controls: { |
| 318 | + movement: 'Movement', |
| 319 | + zoom: 'Zoom', |
| 320 | + space_drag: 'Space + Mouse Drag', |
| 321 | + middle_drag: 'Middle Click + Drag', |
| 322 | + mouse_wheel: 'Mouse Wheel', |
| 323 | + pinch: 'Pinch In/Out', |
| 324 | + } |
305 | 325 | },
|
306 | 326 | ja: {
|
307 | 327 | actions: {
|
|
336 | 356 | erd: 'ER図',
|
337 | 357 | code: 'コード',
|
338 | 358 | },
|
| 359 | + controls: { |
| 360 | + movement: '移動方法', |
| 361 | + zoom: '拡大/縮小', |
| 362 | + space_drag: 'スペースキー + マウスドラッグ', |
| 363 | + middle_drag: '中クリック + ドラッグ', |
| 364 | + mouse_wheel: 'マウスホイール', |
| 365 | + pinch: 'ピンチイン/アウト', |
| 366 | + } |
339 | 367 | }
|
340 | 368 | }
|
341 | 369 | </script>
|
|
362 | 390 | const posX = Vue.ref(0)
|
363 | 391 | const posY = Vue.ref(0)
|
364 | 392 | const zoomArea = Vue.ref(null)
|
| 393 | + // Manage space key press state |
| 394 | + const isSpacePressed = Vue.ref(false) |
| 395 | + // Manage dragging state |
| 396 | + const isDragging = Vue.ref(false) |
| 397 | + let startX = 0 |
| 398 | + let startY = 0 |
| 399 | + // Record distance between two touch points |
| 400 | + let lastTouchDistance = 0 |
| 401 | + // Record mouse position |
| 402 | + let lastMouseX = 0 |
| 403 | + let lastMouseY = 0 |
| 404 | + |
| 405 | + // Calculate distance between two touch points |
| 406 | + const getDistance = (touches) => { |
| 407 | + return Math.hypot( |
| 408 | + touches[0].clientX - touches[1].clientX, |
| 409 | + touches[0].clientY - touches[1].clientY |
| 410 | + ) |
| 411 | + } |
| 412 | + |
| 413 | + // Calculate center point of two touch positions |
| 414 | + const getTouchCenter = (touches) => { |
| 415 | + return { |
| 416 | + x: (touches[0].clientX + touches[1].clientX) / 2, |
| 417 | + y: (touches[0].clientY + touches[1].clientY) / 2 |
| 418 | + } |
| 419 | + } |
| 420 | + |
| 421 | + // Handle touch start |
| 422 | + const handleTouchStart = (e) => { |
| 423 | + if (e.touches.length === 2) { |
| 424 | + e.preventDefault() |
| 425 | + lastTouchDistance = getDistance(e.touches) |
| 426 | + } |
| 427 | + } |
| 428 | + |
| 429 | + // Handle touch move (pinch in/out for zoom) |
| 430 | + const handleTouchMove = (e) => { |
| 431 | + if (e.touches.length === 2) { |
| 432 | + e.preventDefault() |
| 433 | + const preview = document.getElementById('preview') |
| 434 | + if (!preview.contains(e.target)) { |
| 435 | + return |
| 436 | + } |
| 437 | + |
| 438 | + const newDistance = getDistance(e.touches) |
| 439 | + const delta = (newDistance - lastTouchDistance) * 0.01 |
| 440 | + lastTouchDistance = newDistance |
| 441 | + |
| 442 | + const center = getTouchCenter(e.touches) |
| 443 | + const rect = preview.getBoundingClientRect() |
| 444 | + const touchX = center.x - rect.left |
| 445 | + const touchY = center.y - rect.top |
| 446 | + |
| 447 | + const x = touchX / scale.value - posX.value |
| 448 | + const y = touchY / scale.value - posY.value |
| 449 | + |
| 450 | + const newScale = Math.min(Math.max(scale.value + delta, 0.5), 3) |
| 451 | + if (newScale === scale.value) return |
| 452 | + |
| 453 | + posX.value = touchX / newScale - x |
| 454 | + posY.value = touchY / newScale - y |
| 455 | + scale.value = newScale |
| 456 | + } |
| 457 | + } |
| 458 | + |
| 459 | + // Handle touch end |
| 460 | + const handleTouchEnd = (e) => { |
| 461 | + if (e.touches.length < 2) { |
| 462 | + lastTouchDistance = 0 |
| 463 | + } |
| 464 | + } |
| 465 | + |
| 466 | + // Handle key down (space key for drag mode toggle) |
| 467 | + const handleKeyDown = (e) => { |
| 468 | + if (e.code === 'Space' && !isSpacePressed.value) { |
| 469 | + if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') { |
| 470 | + return |
| 471 | + } |
| 472 | + isSpacePressed.value = true |
| 473 | + e.preventDefault() |
| 474 | + } |
| 475 | + } |
| 476 | + |
| 477 | + // Handle key up |
| 478 | + const handleKeyUp = (e) => { |
| 479 | + if (e.code === 'Space') { |
| 480 | + isSpacePressed.value = false |
| 481 | + isDragging.value = false |
| 482 | + } |
| 483 | + } |
| 484 | + |
| 485 | + // Handle mouse down (start dragging) |
| 486 | + const handleMouseDown = (e) => { |
| 487 | + if (isSpacePressed.value || e.button === 1) { |
| 488 | + isDragging.value = true |
| 489 | + lastMouseX = e.clientX |
| 490 | + lastMouseY = e.clientY |
| 491 | + e.preventDefault() |
| 492 | + } |
| 493 | + } |
| 494 | + |
| 495 | + // Handle mouse move (movement while dragging) |
| 496 | + const handleMouseMove = (e) => { |
| 497 | + if (isDragging.value) { |
| 498 | + const dx = (e.clientX - lastMouseX) / scale.value |
| 499 | + const dy = (e.clientY - lastMouseY) / scale.value |
| 500 | + posX.value += dx |
| 501 | + posY.value += dy |
| 502 | + lastMouseX = e.clientX |
| 503 | + lastMouseY = e.clientY |
| 504 | + e.preventDefault() |
| 505 | + } |
| 506 | + } |
| 507 | + |
| 508 | + // Handle mouse up (end dragging) |
| 509 | + const handleMouseUp = (e) => { |
| 510 | + if (e.button === 1) { |
| 511 | + e.preventDefault() |
| 512 | + } |
| 513 | + isDragging.value = false |
| 514 | + } |
| 515 | + |
| 516 | + // Handle mouse wheel (zoom) |
| 517 | + const handleWheel = (e) => { |
| 518 | + const preview = document.getElementById('preview') |
| 519 | + if (!preview.contains(e.target)) { |
| 520 | + return |
| 521 | + } |
| 522 | + |
| 523 | + e.preventDefault() |
| 524 | + |
| 525 | + const rect = preview.getBoundingClientRect() |
| 526 | + const mouseX = e.clientX - rect.left |
| 527 | + const mouseY = e.clientY - rect.top |
| 528 | + |
| 529 | + // Calculate relative coordinates based on mouse position |
| 530 | + const x = mouseX / scale.value - posX.value |
| 531 | + const y = mouseY / scale.value - posY.value |
| 532 | + |
| 533 | + // Change scale |
| 534 | + const delta = e.deltaY < 0 ? 0.1 : -0.1 |
| 535 | + const newScale = Math.min(Math.max(scale.value + delta, 0.5), 3) |
| 536 | + |
| 537 | + // Calculate new position with new scale |
| 538 | + posX.value = mouseX / newScale - x |
| 539 | + posY.value = mouseY / newScale - y |
| 540 | + scale.value = newScale |
| 541 | + } |
365 | 542 |
|
366 | 543 | const zoomStyle = Vue.computed(() => {
|
367 | 544 | return {
|
|
613 | 790 | setLanguage(window.navigator.language)
|
614 | 791 | restoreFromHash()
|
615 | 792 | reRender()
|
| 793 | + |
| 794 | + window.addEventListener('keydown', handleKeyDown) |
| 795 | + window.addEventListener('keyup', handleKeyUp) |
| 796 | + window.addEventListener('mousedown', handleMouseDown) |
| 797 | + window.addEventListener('mousemove', handleMouseMove) |
| 798 | + window.addEventListener('mouseup', handleMouseUp) |
| 799 | + window.addEventListener('wheel', handleWheel, { passive: false }) |
| 800 | + window.addEventListener('touchstart', handleTouchStart, { passive: false }) |
| 801 | + window.addEventListener('touchmove', handleTouchMove, { passive: false }) |
| 802 | + window.addEventListener('touchend', handleTouchEnd) |
| 803 | + }) |
| 804 | + |
| 805 | + Vue.onUnmounted(() => { |
| 806 | + window.removeEventListener('keydown', handleKeyDown) |
| 807 | + window.removeEventListener('keyup', handleKeyUp) |
| 808 | + window.removeEventListener('mousedown', handleMouseDown) |
| 809 | + window.removeEventListener('mousemove', handleMouseMove) |
| 810 | + window.removeEventListener('mouseup', handleMouseUp) |
| 811 | + window.removeEventListener('wheel', handleWheel) |
| 812 | + window.removeEventListener('touchstart', handleTouchStart) |
| 813 | + window.removeEventListener('touchmove', handleTouchMove) |
| 814 | + window.removeEventListener('touchend', handleTouchEnd) |
616 | 815 | })
|
617 | 816 |
|
618 | 817 | window.addEventListener('hashchange', () => {
|
|
656 | 855 | moveUp,
|
657 | 856 | moveDown,
|
658 | 857 | moveLeft,
|
659 |
| - moveRight |
| 858 | + moveRight, |
| 859 | + isSpacePressed, |
| 860 | + isDragging |
660 | 861 | }
|
661 | 862 | }
|
662 | 863 | }
|
|
0 commit comments