|
|
|
@ -98,18 +98,21 @@ const mergeOrderSnapshot = ref<GridOrderEntry[] | null>(null);
|
|
|
|
const mergePendingFolderId = ref<string | null>(null);
|
|
|
|
const mergePendingFolderId = ref<string | null>(null);
|
|
|
|
const mergePendingInsertIndex = ref<number | null>(null);
|
|
|
|
const mergePendingInsertIndex = ref<number | null>(null);
|
|
|
|
const OVERLAP_MERGE_THRESHOLD = 0.6;
|
|
|
|
const OVERLAP_MERGE_THRESHOLD = 0.6;
|
|
|
|
const OVERLAP_HOLD_MS = 100;
|
|
|
|
const OVERLAP_HOLD_MS = 0;
|
|
|
|
const OVERLAP_DROP_THRESHOLD = 0.35;
|
|
|
|
const OVERLAP_DROP_THRESHOLD = 0.35;
|
|
|
|
const MERGE_HIT_INSET_RATIO = 0.18;
|
|
|
|
const MERGE_HIT_INSET_RATIO = 0;
|
|
|
|
const MERGE_HIT_INSET_PX = 6;
|
|
|
|
const MERGE_HIT_INSET_PX = 0;
|
|
|
|
const POINTER_ROW_TOLERANCE_RATIO = 0.6;
|
|
|
|
const POINTER_ROW_TOLERANCE_RATIO = 0.6;
|
|
|
|
const POINTER_EDGE_PADDING = 6;
|
|
|
|
const POINTER_EDGE_PADDING = 6;
|
|
|
|
const lastPointer = ref<{ x: number; y: number } | null>(null);
|
|
|
|
const lastPointer = ref<{ x: number; y: number } | null>(null);
|
|
|
|
const pointerTargetEl = ref<HTMLElement | null>(null);
|
|
|
|
const pointerTargetEl = ref<HTMLElement | null>(null);
|
|
|
|
|
|
|
|
const isPressing = ref(false);
|
|
|
|
|
|
|
|
const pressTimer = ref<number | null>(null);
|
|
|
|
const layoutAnimationMs = 240;
|
|
|
|
const layoutAnimationMs = 240;
|
|
|
|
const layoutEasing = 'cubic-bezier(0.4, 0, 0.2, 1)';
|
|
|
|
const layoutEasing = 'cubic-bezier(0.4, 0, 0.2, 1)';
|
|
|
|
const DELETE_ANIM_MS = 240;
|
|
|
|
const DELETE_ANIM_MS = 240;
|
|
|
|
const DELETE_ANIM_EASING = 'cubic-bezier(0.4, 0, 0.2, 1)';
|
|
|
|
const DELETE_ANIM_EASING = 'cubic-bezier(0.4, 0, 0.2, 1)';
|
|
|
|
|
|
|
|
const PRESS_DELAY_MS = 100;
|
|
|
|
const normalizeGroupId = (groupId?: string | null) => groupId ?? DEFAULT_GROUP_ID;
|
|
|
|
const normalizeGroupId = (groupId?: string | null) => groupId ?? DEFAULT_GROUP_ID;
|
|
|
|
const activeGroupId = computed(() => normalizeGroupId(props.activeGroupId));
|
|
|
|
const activeGroupId = computed(() => normalizeGroupId(props.activeGroupId));
|
|
|
|
|
|
|
|
|
|
|
|
@ -415,6 +418,32 @@ const handleClick = (event: MouseEvent) => {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const clearPressTimer = () => {
|
|
|
|
|
|
|
|
if (pressTimer.value !== null) {
|
|
|
|
|
|
|
|
window.clearTimeout(pressTimer.value);
|
|
|
|
|
|
|
|
pressTimer.value = null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const stopPressing = () => {
|
|
|
|
|
|
|
|
clearPressTimer();
|
|
|
|
|
|
|
|
isPressing.value = false;
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handlePointerDown = (event: PointerEvent) => {
|
|
|
|
|
|
|
|
if (event.button !== 0) return;
|
|
|
|
|
|
|
|
const target = event.target as HTMLElement | null;
|
|
|
|
|
|
|
|
if (!target?.closest('.grid-item')) return;
|
|
|
|
|
|
|
|
clearPressTimer();
|
|
|
|
|
|
|
|
pressTimer.value = window.setTimeout(() => {
|
|
|
|
|
|
|
|
isPressing.value = true;
|
|
|
|
|
|
|
|
}, PRESS_DELAY_MS);
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const handleGlobalPointerUp = () => {
|
|
|
|
|
|
|
|
stopPressing();
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleClickCapture = (event: MouseEvent) => {
|
|
|
|
const handleClickCapture = (event: MouseEvent) => {
|
|
|
|
if (isDragging.value || suppressClick.value || Date.now() < clickBlockUntil.value) {
|
|
|
|
if (isDragging.value || suppressClick.value || Date.now() < clickBlockUntil.value) {
|
|
|
|
event.preventDefault();
|
|
|
|
event.preventDefault();
|
|
|
|
@ -798,29 +827,11 @@ const handleMergeDrop = () => {
|
|
|
|
const dragId = draggingMeta.value.id;
|
|
|
|
const dragId = draggingMeta.value.id;
|
|
|
|
const dragType = draggingMeta.value.type;
|
|
|
|
const dragType = draggingMeta.value.type;
|
|
|
|
if (!dragId || !dragType) return;
|
|
|
|
if (!dragId || !dragType) return;
|
|
|
|
let targetEl = mergeTargetEl.value;
|
|
|
|
let targetEl = mergeTargetEl.value ?? hoverTargetEl.value ?? pointerTargetEl.value;
|
|
|
|
if (!mergeActive.value || !targetEl) {
|
|
|
|
|
|
|
|
targetEl = hoverTargetEl.value;
|
|
|
|
|
|
|
|
const dragEl = draggingEl.value;
|
|
|
|
|
|
|
|
if (!targetEl && dragEl) {
|
|
|
|
|
|
|
|
const stats = findTargetStats(dragEl);
|
|
|
|
|
|
|
|
const best = stats?.best ?? null;
|
|
|
|
|
|
|
|
if (best && best.ratio >= OVERLAP_MERGE_THRESHOLD) {
|
|
|
|
|
|
|
|
targetEl = best.el;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!targetEl) {
|
|
|
|
if (!targetEl) {
|
|
|
|
clearHoverTarget();
|
|
|
|
clearHoverTarget();
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!mergeActive.value && draggingEl.value) {
|
|
|
|
|
|
|
|
const ratio = getOverlapRatio(draggingEl.value, targetEl);
|
|
|
|
|
|
|
|
if (ratio < OVERLAP_DROP_THRESHOLD) {
|
|
|
|
|
|
|
|
clearHoverTarget();
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const targetId = targetEl.dataset.id;
|
|
|
|
const targetId = targetEl.dataset.id;
|
|
|
|
const targetType = targetEl.dataset.type as GridItemType | undefined;
|
|
|
|
const targetType = targetEl.dataset.type as GridItemType | undefined;
|
|
|
|
if (!targetId || !targetType) return;
|
|
|
|
if (!targetId || !targetType) return;
|
|
|
|
@ -1232,6 +1243,8 @@ onMounted(async () => {
|
|
|
|
ensureOrderConsistency();
|
|
|
|
ensureOrderConsistency();
|
|
|
|
previousIconIds.value = new Set(layoutStore.icons.map(icon => icon.id));
|
|
|
|
previousIconIds.value = new Set(layoutStore.icons.map(icon => icon.id));
|
|
|
|
previousWidgetIds.value = new Set(widgetsStore.widgets.map(widget => widget.id));
|
|
|
|
previousWidgetIds.value = new Set(widgetsStore.widgets.map(widget => widget.id));
|
|
|
|
|
|
|
|
window.addEventListener('pointerup', handleGlobalPointerUp);
|
|
|
|
|
|
|
|
window.addEventListener('pointercancel', handleGlobalPointerUp);
|
|
|
|
await nextTick();
|
|
|
|
await nextTick();
|
|
|
|
if (!gridRef.value) return;
|
|
|
|
if (!gridRef.value) return;
|
|
|
|
grid.value = new Muuri(gridRef.value, {
|
|
|
|
grid.value = new Muuri(gridRef.value, {
|
|
|
|
@ -1266,6 +1279,7 @@ onMounted(async () => {
|
|
|
|
grid.value.on('dragStart', (item: any) => {
|
|
|
|
grid.value.on('dragStart', (item: any) => {
|
|
|
|
isDragging.value = true;
|
|
|
|
isDragging.value = true;
|
|
|
|
suppressClick.value = true;
|
|
|
|
suppressClick.value = true;
|
|
|
|
|
|
|
|
stopPressing();
|
|
|
|
const el = item.getElement() as HTMLElement;
|
|
|
|
const el = item.getElement() as HTMLElement;
|
|
|
|
draggingEl.value = el;
|
|
|
|
draggingEl.value = el;
|
|
|
|
draggingItemRef.value = item;
|
|
|
|
draggingItemRef.value = item;
|
|
|
|
@ -1305,42 +1319,35 @@ onMounted(async () => {
|
|
|
|
const now = performance.now();
|
|
|
|
const now = performance.now();
|
|
|
|
const pointerTarget = pointerTargetEl.value;
|
|
|
|
const pointerTarget = pointerTargetEl.value;
|
|
|
|
|
|
|
|
|
|
|
|
if (mergeActive.value) {
|
|
|
|
|
|
|
|
const locked = mergeTargetEl.value;
|
|
|
|
|
|
|
|
if (!locked || !locked.isConnected) {
|
|
|
|
|
|
|
|
deactivateMergeTarget(true);
|
|
|
|
|
|
|
|
} else if (pointerTarget && pointerTarget !== locked) {
|
|
|
|
|
|
|
|
deactivateMergeTarget(true);
|
|
|
|
|
|
|
|
mergeCandidateEl.value = pointerTarget;
|
|
|
|
|
|
|
|
mergeCandidateAt.value = now;
|
|
|
|
|
|
|
|
setHoverTarget(pointerTarget);
|
|
|
|
|
|
|
|
} else if (!pointerTarget) {
|
|
|
|
|
|
|
|
deactivateMergeTarget(true);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
setHoverTarget(locked);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
if (pointerTarget && pointerTarget.isConnected) {
|
|
|
|
if (pointerTarget && pointerTarget.isConnected) {
|
|
|
|
setHoverTarget(pointerTarget);
|
|
|
|
if (mergeCandidateEl.value !== pointerTarget) {
|
|
|
|
if (mergeCandidateEl.value === pointerTarget) {
|
|
|
|
|
|
|
|
if (now - mergeCandidateAt.value >= OVERLAP_HOLD_MS) {
|
|
|
|
|
|
|
|
activateMergeTarget(pointerTarget);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
mergeCandidateEl.value = pointerTarget;
|
|
|
|
mergeCandidateEl.value = pointerTarget;
|
|
|
|
mergeCandidateAt.value = now;
|
|
|
|
mergeCandidateAt.value = now;
|
|
|
|
|
|
|
|
if (mergeActive.value && mergeTargetEl.value !== pointerTarget) {
|
|
|
|
|
|
|
|
deactivateMergeTarget(false);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
const holdReady = now - mergeCandidateAt.value >= OVERLAP_HOLD_MS;
|
|
|
|
|
|
|
|
if (holdReady) {
|
|
|
|
|
|
|
|
if (!mergeActive.value || mergeTargetEl.value !== pointerTarget) {
|
|
|
|
|
|
|
|
activateMergeTarget(pointerTarget);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
setHoverTarget(pointerTarget);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
clearHoverTarget();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
mergeCandidateEl.value = null;
|
|
|
|
mergeCandidateEl.value = null;
|
|
|
|
mergeCandidateAt.value = 0;
|
|
|
|
mergeCandidateAt.value = 0;
|
|
|
|
|
|
|
|
deactivateMergeTarget(true);
|
|
|
|
clearHoverTarget();
|
|
|
|
clearHoverTarget();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
applySortLock(mergeActive.value);
|
|
|
|
applySortLock(!!pointerTarget);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
grid.value.on('dragEnd', () => {
|
|
|
|
grid.value.on('dragEnd', () => {
|
|
|
|
isDragging.value = false;
|
|
|
|
isDragging.value = false;
|
|
|
|
@ -1497,20 +1504,24 @@ watch(
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
onUnmounted(() => {
|
|
|
|
pendingResizedKeys.clear();
|
|
|
|
pendingResizedKeys.clear();
|
|
|
|
|
|
|
|
stopPressing();
|
|
|
|
|
|
|
|
window.removeEventListener('pointerup', handleGlobalPointerUp);
|
|
|
|
|
|
|
|
window.removeEventListener('pointercancel', handleGlobalPointerUp);
|
|
|
|
grid.value?.destroy();
|
|
|
|
grid.value?.destroy();
|
|
|
|
grid.value = null;
|
|
|
|
grid.value = null;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
</script>
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<template>
|
|
|
|
<div
|
|
|
|
<div
|
|
|
|
ref="gridRef"
|
|
|
|
ref="gridRef"
|
|
|
|
class="grid-canvas"
|
|
|
|
:class="['grid-canvas', { 'is-dragging': isDragging, 'is-pressing': isPressing }]"
|
|
|
|
:style="{ '--layout-anim-ms': `${layoutAnimationMs}ms` }"
|
|
|
|
:style="{ '--layout-anim-ms': `${layoutAnimationMs}ms` }"
|
|
|
|
@click.capture="handleClickCapture"
|
|
|
|
@click.capture="handleClickCapture"
|
|
|
|
@click="handleClick"
|
|
|
|
@click="handleClick"
|
|
|
|
@contextmenu.prevent="handleContextMenu"
|
|
|
|
@pointerdown="handlePointerDown"
|
|
|
|
>
|
|
|
|
@contextmenu.prevent="handleContextMenu"
|
|
|
|
|
|
|
|
>
|
|
|
|
<div
|
|
|
|
<div
|
|
|
|
v-for="item in orderedItems"
|
|
|
|
v-for="item in orderedItems"
|
|
|
|
:key="`${item.type}-${item.id}`"
|
|
|
|
:key="`${item.type}-${item.id}`"
|
|
|
|
@ -1576,6 +1587,20 @@ onUnmounted(() => {
|
|
|
|
transition: opacity 160ms ease;
|
|
|
|
transition: opacity 160ms ease;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.grid-item * {
|
|
|
|
|
|
|
|
cursor: inherit;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.grid-canvas.is-dragging,
|
|
|
|
|
|
|
|
.grid-canvas.is-dragging * {
|
|
|
|
|
|
|
|
cursor: grabbing !important;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.grid-canvas.is-pressing,
|
|
|
|
|
|
|
|
.grid-canvas.is-pressing * {
|
|
|
|
|
|
|
|
cursor: grabbing !important;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.grid-item.is-resized {
|
|
|
|
.grid-item.is-resized {
|
|
|
|
overflow: visible;
|
|
|
|
overflow: visible;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|