Files
core2026/app/game-frontend/src/components/command/DragSelect.vue
T
Hide_D ba5f870b06 fix: 모바일 고급 턴 드래그 선택 복구
Ref DragSelect와 같이 활성 선택 영역에 touch-action none을 적용해 브라우저 스크롤의 pointercancel을 막는다. 일반·사령턴의 trusted mobile touch 선택과 일반 모드 스크롤 계약을 production Chromium으로 검증한다.
2026-08-21 01:26:51 +00:00

102 lines
3.6 KiB
Vue

<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
const props = withDefaults(defineProps<{ disabled?: boolean; attribute?: string }>(), {
disabled: false,
attribute: 'data-turn-index',
});
const emit = defineEmits<{ (event: 'drag-start'): void; (event: 'drag-done', selected: Set<number>): void }>();
const root = ref<HTMLElement | null>(null);
const preview = ref(new Set<number>());
let dragging = false;
let startX = 0;
let startY = 0;
let box: HTMLDivElement | null = null;
let activePointerId: number | null = null;
const intersects = (left: DOMRect, right: DOMRect) =>
left.left <= right.right && left.right >= right.left && left.top <= right.bottom && left.bottom >= right.top;
const update = (clientX: number, clientY: number) => {
if (!root.value || !box) return;
const rootRect = root.value.getBoundingClientRect();
const x = clientX - rootRect.left;
const y = clientY - rootRect.top;
box.style.left = `${Math.min(startX, x)}px`;
box.style.top = `${Math.min(startY, y)}px`;
box.style.width = `${Math.abs(x - startX)}px`;
box.style.height = `${Math.abs(y - startY)}px`;
const boxRect = box.getBoundingClientRect();
const next = new Set<number>();
for (const element of root.value.children) {
if (element === box || !intersects(boxRect, element.getBoundingClientRect())) continue;
const raw = element.getAttribute(props.attribute);
if (raw !== null) next.add(Number(raw));
}
preview.value = next;
};
const pointerMove = (event: PointerEvent) =>
dragging && event.pointerId === activePointerId && update(event.clientX, event.clientY);
const pointerUp = (event: PointerEvent) => {
if (event.pointerId !== activePointerId) return;
if (!dragging) return;
dragging = false;
if (root.value?.hasPointerCapture(event.pointerId)) root.value.releasePointerCapture(event.pointerId);
activePointerId = null;
box?.remove();
box = null;
emit('drag-done', new Set(preview.value));
preview.value = new Set();
};
const pointerDown = (event: PointerEvent) => {
if (props.disabled || !root.value || event.button !== 0) return;
event.preventDefault();
const rect = root.value.getBoundingClientRect();
startX = event.clientX - rect.left;
startY = event.clientY - rect.top;
box = document.createElement('div');
box.className = 'drag-selection-box';
root.value.append(box);
dragging = true;
activePointerId = event.pointerId;
root.value.setPointerCapture(event.pointerId);
update(event.clientX, event.clientY);
emit('drag-start');
};
onMounted(() => {
root.value?.addEventListener('pointerdown', pointerDown);
root.value?.addEventListener('pointermove', pointerMove);
root.value?.addEventListener('pointerup', pointerUp);
root.value?.addEventListener('pointercancel', pointerUp);
});
onBeforeUnmount(() => {
root.value?.removeEventListener('pointerdown', pointerDown);
root.value?.removeEventListener('pointermove', pointerMove);
root.value?.removeEventListener('pointerup', pointerUp);
root.value?.removeEventListener('pointercancel', pointerUp);
});
</script>
<template>
<div ref="root" class="drag-select" :style="{ touchAction: props.disabled ? undefined : 'none' }">
<slot :selected="preview" />
</div>
</template>
<style scoped>
.drag-select {
position: relative;
min-width: 0;
user-select: none;
}
.drag-select :deep(.drag-selection-box) {
position: absolute;
z-index: 30;
border: 1px solid #7ee8ff;
background: rgb(0 180 255 / 28%);
pointer-events: none;
}
</style>