feat(ui): Optimize draggable and window positioning with performance improvements
- commit
- e81bc01444512a4ab0ff72e159ed97edd709c070
- author
- Kit · · 2025-02-18 16:28 UTC
- parent
- ea6754c
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
2 files · +56 -40
M src/lib/actions/draggable.ts +45 -39
| @@ -29,34 +29,42 @@ export const draggable: Action<HTMLElement, DraggableParameters, DraggableAttrib | |||
| 29 | 29 | return; | |
| 30 | 30 | } | |
| 31 | 31 | ||
| 32 | - | // Set initial position from existing inline styles or computed styles | |
| 33 | - | const computedStyle = getComputedStyle(node); | |
| 34 | - | const existingLeft = node.style.left || computedStyle.left; | |
| 35 | - | const existingTop = node.style.top || computedStyle.top; | |
| 36 | - | ||
| 37 | - | x = parseInt(existingLeft) || 0; | |
| 38 | - | y = parseInt(existingTop) || 0; | |
| 39 | - | ||
| 40 | - | // Ensure the element is positioned | |
| 41 | - | const position = computedStyle.position; | |
| 42 | - | if (position !== 'relative' && position !== 'absolute' && position !== 'fixed') { | |
| 32 | + | // Ensure proper positioning | |
| 33 | + | if (getComputedStyle(node).position === 'static') { | |
| 43 | 34 | node.style.position = 'fixed'; | |
| 44 | 35 | } | |
| 45 | 36 | ||
| 46 | - | node.style.left = `${x}px`; | |
| 47 | - | node.style.top = `${y}px`; | |
| 37 | + | function getNodePosition() { | |
| 38 | + | const rect = node.getBoundingClientRect(); | |
| 39 | + | return { | |
| 40 | + | x: rect.left, | |
| 41 | + | y: rect.top | |
| 42 | + | }; | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | function updatePosition(newX: number, newY: number) { | |
| 46 | + | x = newX; | |
| 47 | + | y = newY; | |
| 48 | + | node.style.left = `${x}px`; | |
| 49 | + | node.style.top = `${y}px`; | |
| 50 | + | } | |
| 48 | 51 | ||
| 49 | 52 | function onMouseDown(event: MouseEvent) { | |
| 50 | - | if (!isEnabled) return; | |
| 53 | + | if (!isEnabled || event.button !== 0) return; | |
| 51 | 54 | ||
| 52 | - | // Only handle left mouse button | |
| 53 | - | if (event.button !== 0) return; | |
| 55 | + | // Check if we're clicking a button in the title bar | |
| 56 | + | const target = event.target as HTMLElement; | |
| 57 | + | if (target.tagName === 'BUTTON' || target.closest('button')) { | |
| 58 | + | return; | |
| 59 | + | } | |
| 54 | 60 | ||
| 55 | 61 | isDragging = true; | |
| 56 | - | const rect = node.getBoundingClientRect(); | |
| 57 | 62 | ||
| 58 | - | x = rect.left; | |
| 59 | - | y = rect.top; | |
| 63 | + | // Get current position | |
| 64 | + | const pos = getNodePosition(); | |
| 65 | + | x = pos.x; | |
| 66 | + | y = pos.y; | |
| 67 | + | ||
| 60 | 68 | startX = event.clientX - x; | |
| 61 | 69 | startY = event.clientY - y; | |
| 62 | 70 | ||
| @@ -64,7 +72,9 @@ export const draggable: Action<HTMLElement, DraggableParameters, DraggableAttrib | |||
| 64 | 72 | detail: { x, y } | |
| 65 | 73 | })); | |
| 66 | 74 | ||
| 67 | - | // Prevent text selection while dragging | |
| 75 | + | // Add dragging class for styling | |
| 76 | + | node.classList.add('dragging'); | |
| 77 | + | ||
| 68 | 78 | event.preventDefault(); | |
| 69 | 79 | } | |
| 70 | 80 | ||
| @@ -75,52 +85,48 @@ export const draggable: Action<HTMLElement, DraggableParameters, DraggableAttrib | |||
| 75 | 85 | let newY = event.clientY - startY; | |
| 76 | 86 | ||
| 77 | 87 | if (bounds === 'window') { | |
| 78 | - | // Keep within window bounds | |
| 79 | 88 | newX = Math.max(0, Math.min(newX, window.innerWidth - node.offsetWidth)); | |
| 80 | 89 | newY = Math.max(0, Math.min(newY, window.innerHeight - node.offsetHeight)); | |
| 81 | 90 | } else if (bounds === 'parent' && node.parentElement) { | |
| 82 | - | // Keep within parent bounds | |
| 83 | 91 | const parent = node.parentElement; | |
| 84 | 92 | newX = Math.max(0, Math.min(newX, parent.offsetWidth - node.offsetWidth)); | |
| 85 | 93 | newY = Math.max(0, Math.min(newY, parent.offsetHeight - node.offsetHeight)); | |
| 86 | 94 | } | |
| 87 | 95 | ||
| 88 | - | x = newX; | |
| 89 | - | y = newY; | |
| 90 | - | ||
| 91 | - | node.style.left = `${x}px`; | |
| 92 | - | node.style.top = `${y}px`; | |
| 93 | - | ||
| 94 | - | node.dispatchEvent(new CustomEvent('dragmove', { | |
| 95 | - | detail: { x, y } | |
| 96 | - | })); | |
| 96 | + | requestAnimationFrame(() => { | |
| 97 | + | updatePosition(newX, newY); | |
| 98 | + | node.dispatchEvent(new CustomEvent('dragmove', { | |
| 99 | + | detail: { x: newX, y: newY } | |
| 100 | + | })); | |
| 101 | + | }); | |
| 97 | 102 | } | |
| 98 | 103 | ||
| 99 | 104 | function onMouseUp() { | |
| 100 | 105 | if (!isDragging) return; | |
| 101 | 106 | isDragging = false; | |
| 102 | 107 | ||
| 108 | + | node.classList.remove('dragging'); | |
| 109 | + | ||
| 103 | 110 | node.dispatchEvent(new CustomEvent('dragend', { | |
| 104 | 111 | detail: { x, y } | |
| 105 | 112 | })); | |
| 106 | 113 | } | |
| 107 | 114 | ||
| 108 | 115 | handleElement.style.cursor = isEnabled ? 'move' : 'default'; | |
| 109 | - | handleElement.addEventListener('mousedown', onMouseDown as EventListener); | |
| 110 | - | window.addEventListener('mousemove', onMouseMove as EventListener); | |
| 111 | - | window.addEventListener('mouseup', onMouseUp as EventListener); | |
| 116 | + | handleElement.addEventListener('mousedown', onMouseDown); | |
| 117 | + | window.addEventListener('mousemove', onMouseMove, { passive: true }); | |
| 118 | + | window.addEventListener('mouseup', onMouseUp); | |
| 112 | 119 | ||
| 113 | 120 | return { | |
| 114 | 121 | update(newParams: DraggableParameters) { | |
| 115 | - | const newEnabled = newParams.enabled !== undefined ? newParams.enabled : true; | |
| 116 | - | isEnabled = newEnabled; | |
| 122 | + | isEnabled = newParams.enabled !== undefined ? newParams.enabled : true; | |
| 117 | 123 | handleElement.style.cursor = isEnabled ? 'move' : 'default'; | |
| 118 | 124 | Object.assign(params, newParams); | |
| 119 | 125 | }, | |
| 120 | 126 | destroy() { | |
| 121 | - | handleElement.removeEventListener('mousedown', onMouseDown as EventListener); | |
| 122 | - | window.removeEventListener('mousemove', onMouseMove as EventListener); | |
| 123 | - | window.removeEventListener('mouseup', onMouseUp as EventListener); | |
| 127 | + | handleElement.removeEventListener('mousedown', onMouseDown); | |
| 128 | + | window.removeEventListener('mousemove', onMouseMove); | |
| 129 | + | window.removeEventListener('mouseup', onMouseUp); | |
| 124 | 130 | } | |
| 125 | 131 | }; | |
| 126 | 132 | }; ⏎̸ | |
| @@ -29,34 +29,42 @@ export const draggable: Action<HTMLElement, DraggableParameters, DraggableAttrib | |||||
| 29 | return; | 29 | return; | ||
| 30 | } | 30 | } | ||
| 31 | 31 | ||||
| 32 | - | // Set initial position from existing inline styles or computed styles | 32 | + | // Ensure proper positioning |
| 33 | - | const computedStyle = getComputedStyle(node); | 33 | + | if (getComputedStyle(node).position === 'static') { |
| 34 | - | const existingLeft = node.style.left || computedStyle.left; | |||
| 35 | - | const existingTop = node.style.top || computedStyle.top; | |||
| 36 | - | ||||
| 37 | - | x = parseInt(existingLeft) || 0; | |||
| 38 | - | y = parseInt(existingTop) || 0; | |||
| 39 | - | ||||
| 40 | - | // Ensure the element is positioned | |||
| 41 | - | const position = computedStyle.position; | |||
| 42 | - | if (position !== 'relative' && position !== 'absolute' && position !== 'fixed') { | |||
| 43 | node.style.position = 'fixed'; | 34 | node.style.position = 'fixed'; | ||
| 44 | } | 35 | } | ||
| 45 | 36 | ||||
| 46 | - | node.style.left = `${x}px`; | 37 | + | function getNodePosition() { |
| 47 | - | node.style.top = `${y}px`; | 38 | + | const rect = node.getBoundingClientRect(); |
| 39 | + | return { | |||
| 40 | + | x: rect.left, | |||
| 41 | + | y: rect.top | |||
| 42 | + | }; | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | function updatePosition(newX: number, newY: number) { | |||
| 46 | + | x = newX; | |||
| 47 | + | y = newY; | |||
| 48 | + | node.style.left = `${x}px`; | |||
| 49 | + | node.style.top = `${y}px`; | |||
| 50 | + | } | |||
| 48 | 51 | ||||
| 49 | function onMouseDown(event: MouseEvent) { | 52 | function onMouseDown(event: MouseEvent) { | ||
| 50 | - | if (!isEnabled) return; | 53 | + | if (!isEnabled || event.button !== 0) return; |
| 51 | 54 | ||||
| 52 | - | // Only handle left mouse button | 55 | + | // Check if we're clicking a button in the title bar |
| 53 | - | if (event.button !== 0) return; | 56 | + | const target = event.target as HTMLElement; |
| 57 | + | if (target.tagName === 'BUTTON' || target.closest('button')) { | |||
| 58 | + | return; | |||
| 59 | + | } | |||
| 54 | 60 | ||||
| 55 | isDragging = true; | 61 | isDragging = true; | ||
| 56 | - | const rect = node.getBoundingClientRect(); | |||
| 57 | 62 | ||||
| 58 | - | x = rect.left; | 63 | + | // Get current position |
| 59 | - | y = rect.top; | 64 | + | const pos = getNodePosition(); |
| 65 | + | x = pos.x; | |||
| 66 | + | y = pos.y; | |||
| 67 | + | ||||
| 60 | startX = event.clientX - x; | 68 | startX = event.clientX - x; | ||
| 61 | startY = event.clientY - y; | 69 | startY = event.clientY - y; | ||
| 62 | 70 | ||||
| @@ -64,7 +72,9 @@ export const draggable: Action<HTMLElement, DraggableParameters, DraggableAttrib | |||||
| 64 | detail: { x, y } | 72 | detail: { x, y } | ||
| 65 | })); | 73 | })); | ||
| 66 | 74 | ||||
| 67 | - | // Prevent text selection while dragging | 75 | + | // Add dragging class for styling |
| 76 | + | node.classList.add('dragging'); | |||
| 77 | + | ||||
| 68 | event.preventDefault(); | 78 | event.preventDefault(); | ||
| 69 | } | 79 | } | ||
| 70 | 80 | ||||
| @@ -75,52 +85,48 @@ export const draggable: Action<HTMLElement, DraggableParameters, DraggableAttrib | |||||
| 75 | let newY = event.clientY - startY; | 85 | let newY = event.clientY - startY; | ||
| 76 | 86 | ||||
| 77 | if (bounds === 'window') { | 87 | if (bounds === 'window') { | ||
| 78 | - | // Keep within window bounds | |||
| 79 | newX = Math.max(0, Math.min(newX, window.innerWidth - node.offsetWidth)); | 88 | newX = Math.max(0, Math.min(newX, window.innerWidth - node.offsetWidth)); | ||
| 80 | newY = Math.max(0, Math.min(newY, window.innerHeight - node.offsetHeight)); | 89 | newY = Math.max(0, Math.min(newY, window.innerHeight - node.offsetHeight)); | ||
| 81 | } else if (bounds === 'parent' && node.parentElement) { | 90 | } else if (bounds === 'parent' && node.parentElement) { | ||
| 82 | - | // Keep within parent bounds | |||
| 83 | const parent = node.parentElement; | 91 | const parent = node.parentElement; | ||
| 84 | newX = Math.max(0, Math.min(newX, parent.offsetWidth - node.offsetWidth)); | 92 | newX = Math.max(0, Math.min(newX, parent.offsetWidth - node.offsetWidth)); | ||
| 85 | newY = Math.max(0, Math.min(newY, parent.offsetHeight - node.offsetHeight)); | 93 | newY = Math.max(0, Math.min(newY, parent.offsetHeight - node.offsetHeight)); | ||
| 86 | } | 94 | } | ||
| 87 | 95 | ||||
| 88 | - | x = newX; | 96 | + | requestAnimationFrame(() => { |
| 89 | - | y = newY; | 97 | + | updatePosition(newX, newY); |
| 90 | - | 98 | + | node.dispatchEvent(new CustomEvent('dragmove', { | |
| 91 | - | node.style.left = `${x}px`; | 99 | + | detail: { x: newX, y: newY } |
| 92 | - | node.style.top = `${y}px`; | 100 | + | })); |
| 93 | - | 101 | + | }); | |
| 94 | - | node.dispatchEvent(new CustomEvent('dragmove', { | |||
| 95 | - | detail: { x, y } | |||
| 96 | - | })); | |||
| 97 | } | 102 | } | ||
| 98 | 103 | ||||
| 99 | function onMouseUp() { | 104 | function onMouseUp() { | ||
| 100 | if (!isDragging) return; | 105 | if (!isDragging) return; | ||
| 101 | isDragging = false; | 106 | isDragging = false; | ||
| 102 | 107 | ||||
| 108 | + | node.classList.remove('dragging'); | |||
| 109 | + | ||||
| 103 | node.dispatchEvent(new CustomEvent('dragend', { | 110 | node.dispatchEvent(new CustomEvent('dragend', { | ||
| 104 | detail: { x, y } | 111 | detail: { x, y } | ||
| 105 | })); | 112 | })); | ||
| 106 | } | 113 | } | ||
| 107 | 114 | ||||
| 108 | handleElement.style.cursor = isEnabled ? 'move' : 'default'; | 115 | handleElement.style.cursor = isEnabled ? 'move' : 'default'; | ||
| 109 | - | handleElement.addEventListener('mousedown', onMouseDown as EventListener); | 116 | + | handleElement.addEventListener('mousedown', onMouseDown); |
| 110 | - | window.addEventListener('mousemove', onMouseMove as EventListener); | 117 | + | window.addEventListener('mousemove', onMouseMove, { passive: true }); |
| 111 | - | window.addEventListener('mouseup', onMouseUp as EventListener); | 118 | + | window.addEventListener('mouseup', onMouseUp); |
| 112 | 119 | ||||
| 113 | return { | 120 | return { | ||
| 114 | update(newParams: DraggableParameters) { | 121 | update(newParams: DraggableParameters) { | ||
| 115 | - | const newEnabled = newParams.enabled !== undefined ? newParams.enabled : true; | 122 | + | isEnabled = newParams.enabled !== undefined ? newParams.enabled : true; |
| 116 | - | isEnabled = newEnabled; | |||
| 117 | handleElement.style.cursor = isEnabled ? 'move' : 'default'; | 123 | handleElement.style.cursor = isEnabled ? 'move' : 'default'; | ||
| 118 | Object.assign(params, newParams); | 124 | Object.assign(params, newParams); | ||
| 119 | }, | 125 | }, | ||
| 120 | destroy() { | 126 | destroy() { | ||
| 121 | - | handleElement.removeEventListener('mousedown', onMouseDown as EventListener); | 127 | + | handleElement.removeEventListener('mousedown', onMouseDown); |
| 122 | - | window.removeEventListener('mousemove', onMouseMove as EventListener); | 128 | + | window.removeEventListener('mousemove', onMouseMove); |
| 123 | - | window.removeEventListener('mouseup', onMouseUp as EventListener); | 129 | + | window.removeEventListener('mouseup', onMouseUp); |
| 124 | } | 130 | } | ||
| 125 | }; | 131 | }; | ||
| 126 | }; ⏎̸ | 132 | }; ⏎̸ | ||
M src/lib/components/chat-room.svelte +11 -1
| @@ -640,17 +640,27 @@ function handleLoginSuccess() { | |||
| 640 | 640 | .title-bar { | |
| 641 | 641 | height: 2rem; | |
| 642 | 642 | position: relative; | |
| 643 | - | cursor: move; | |
| 643 | + | cursor: default; | |
| 644 | 644 | user-select: none; | |
| 645 | 645 | display: flex; | |
| 646 | 646 | align-items: center; | |
| 647 | 647 | padding: 0 0.5rem; | |
| 648 | 648 | } | |
| 649 | 649 | ||
| 650 | + | .title-bar:not(:has(button:hover)) { | |
| 651 | + | cursor: move; | |
| 652 | + | } | |
| 653 | + | ||
| 650 | 654 | .chat-window { | |
| 651 | 655 | position: fixed; | |
| 652 | 656 | box-sizing: border-box; | |
| 653 | 657 | transition: width 0.3s ease, height 0.3s ease, left 0.3s ease, top 0.3s ease; | |
| 658 | + | will-change: transform; | |
| 659 | + | } | |
| 660 | + | ||
| 661 | + | .chat-window.dragging { | |
| 662 | + | transition: none !important; | |
| 663 | + | user-select: none; | |
| 654 | 664 | } | |
| 655 | 665 | ||
| 656 | 666 | .chat-area { | |
| @@ -640,17 +640,27 @@ function handleLoginSuccess() { | |||||
| 640 | .title-bar { | 640 | .title-bar { | ||
| 641 | height: 2rem; | 641 | height: 2rem; | ||
| 642 | position: relative; | 642 | position: relative; | ||
| 643 | - | cursor: move; | 643 | + | cursor: default; |
| 644 | user-select: none; | 644 | user-select: none; | ||
| 645 | display: flex; | 645 | display: flex; | ||
| 646 | align-items: center; | 646 | align-items: center; | ||
| 647 | padding: 0 0.5rem; | 647 | padding: 0 0.5rem; | ||
| 648 | } | 648 | } | ||
| 649 | 649 | ||||
| 650 | + | .title-bar:not(:has(button:hover)) { | |||
| 651 | + | cursor: move; | |||
| 652 | + | } | |||
| 653 | + | ||||
| 650 | .chat-window { | 654 | .chat-window { | ||
| 651 | position: fixed; | 655 | position: fixed; | ||
| 652 | box-sizing: border-box; | 656 | box-sizing: border-box; | ||
| 653 | transition: width 0.3s ease, height 0.3s ease, left 0.3s ease, top 0.3s ease; | 657 | transition: width 0.3s ease, height 0.3s ease, left 0.3s ease, top 0.3s ease; | ||
| 658 | + | will-change: transform; | |||
| 659 | + | } | |||
| 660 | + | ||||
| 661 | + | .chat-window.dragging { | |||
| 662 | + | transition: none !important; | |||
| 663 | + | user-select: none; | |||
| 654 | } | 664 | } | ||
| 655 | 665 | ||||
| 656 | .chat-area { | 666 | .chat-area { | ||