feat: rebuild Ref-compatible command editors

This commit is contained in:
2026-08-05 09:02:55 +00:00
parent f731f1f38e
commit 26045f8d63
16 changed files with 1815 additions and 923 deletions
@@ -0,0 +1,99 @@
<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"><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>