1
This commit is contained in:
@ -45,7 +45,14 @@ const gridRef = ref<HTMLElement | null>(null);
|
||||
const grid = ref<any | null>(null);
|
||||
const isDragging = ref(false);
|
||||
const suppressClick = ref(false);
|
||||
const clickBlockUntil = ref(0);
|
||||
const gridOrder = ref<GridOrderEntry[]>([]);
|
||||
const resizedKeys = ref(new Set<string>());
|
||||
const pendingResizedKeys = new Set<string>();
|
||||
const widgetSizeMap = new Map<string, GridItemSize>();
|
||||
const iconSizeMap = new Map<string, GridItemSize>();
|
||||
const layoutAnimationMs = 240;
|
||||
const layoutEasing = 'cubic-bezier(0.4, 0, 0.2, 1)';
|
||||
|
||||
const buildDefaultOrder = (): GridOrderEntry[] => [
|
||||
...widgetsStore.widgets.map(widget => ({ id: widget.id, type: 'widget' as const })),
|
||||
@ -173,6 +180,13 @@ const handleClick = (event: MouseEvent) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickCapture = (event: MouseEvent) => {
|
||||
if (isDragging.value || suppressClick.value || Date.now() < clickBlockUntil.value) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
const handleContextMenu = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
const itemEl = target.closest('.grid-item') as HTMLElement | null;
|
||||
@ -204,10 +218,40 @@ const persistOrderFromGrid = async () => {
|
||||
grid.value?.layout();
|
||||
};
|
||||
|
||||
const refreshLayout = async () => {
|
||||
const refreshLayout = async (instant = false) => {
|
||||
await nextTick();
|
||||
grid.value?.refreshItems();
|
||||
grid.value?.layout();
|
||||
if (instant) {
|
||||
grid.value?.layout(true);
|
||||
} else {
|
||||
grid.value?.layout();
|
||||
}
|
||||
};
|
||||
|
||||
const markResized = (type: GridItemType, id: string) => {
|
||||
pendingResizedKeys.add(`${type}:${id}`);
|
||||
};
|
||||
|
||||
const syncSizeMap = (
|
||||
items: { id: string; size: GridItemSize }[],
|
||||
map: Map<string, GridItemSize>
|
||||
) => {
|
||||
const changed: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of items) {
|
||||
const prev = map.get(item.id);
|
||||
if (prev && prev !== item.size) {
|
||||
changed.push(item.id);
|
||||
}
|
||||
map.set(item.id, item.size);
|
||||
seen.add(item.id);
|
||||
}
|
||||
for (const id of Array.from(map.keys())) {
|
||||
if (!seen.has(id)) {
|
||||
map.delete(id);
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
@ -226,6 +270,8 @@ onMounted(async () => {
|
||||
fillGaps: true,
|
||||
rounding: true,
|
||||
},
|
||||
layoutDuration: layoutAnimationMs,
|
||||
layoutEasing,
|
||||
});
|
||||
|
||||
grid.value.on('dragStart', () => {
|
||||
@ -235,6 +281,7 @@ onMounted(async () => {
|
||||
|
||||
grid.value.on('dragEnd', () => {
|
||||
isDragging.value = false;
|
||||
clickBlockUntil.value = Date.now() + 180;
|
||||
window.setTimeout(() => {
|
||||
suppressClick.value = false;
|
||||
}, 0);
|
||||
@ -244,6 +291,17 @@ onMounted(async () => {
|
||||
persistOrderFromGrid();
|
||||
});
|
||||
|
||||
grid.value.on('layoutStart', () => {
|
||||
if (!pendingResizedKeys.size) return;
|
||||
resizedKeys.value = new Set(pendingResizedKeys);
|
||||
});
|
||||
|
||||
grid.value.on('layoutEnd', () => {
|
||||
if (!pendingResizedKeys.size) return;
|
||||
pendingResizedKeys.clear();
|
||||
resizedKeys.value = new Set<string>();
|
||||
});
|
||||
|
||||
grid.value.layout(true);
|
||||
});
|
||||
|
||||
@ -256,20 +314,35 @@ watch(
|
||||
);
|
||||
|
||||
watch(
|
||||
() => widgetsStore.widgets.map(widget => widget.size).join('|'),
|
||||
() => widgetsStore.widgets.map(widget => ({ id: widget.id, size: widget.size })),
|
||||
async () => {
|
||||
const changed = syncSizeMap(
|
||||
widgetsStore.widgets.map(widget => ({ id: widget.id, size: widget.size })),
|
||||
widgetSizeMap
|
||||
);
|
||||
for (const id of changed) {
|
||||
markResized('widget', id);
|
||||
}
|
||||
await refreshLayout();
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => layoutStore.icons.map(icon => icon.size ?? '1x1').join('|'),
|
||||
() => layoutStore.icons.map(icon => ({ id: icon.id, size: icon.size ?? '1x1' })),
|
||||
async () => {
|
||||
const changed = syncSizeMap(
|
||||
layoutStore.icons.map(icon => ({ id: icon.id, size: icon.size ?? '1x1' })),
|
||||
iconSizeMap
|
||||
);
|
||||
for (const id of changed) {
|
||||
markResized('icon', id);
|
||||
}
|
||||
await refreshLayout();
|
||||
}
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
pendingResizedKeys.clear();
|
||||
grid.value?.destroy();
|
||||
grid.value = null;
|
||||
});
|
||||
@ -279,6 +352,8 @@ onUnmounted(() => {
|
||||
<div
|
||||
ref="gridRef"
|
||||
class="grid-canvas"
|
||||
:style="{ '--layout-anim-ms': `${layoutAnimationMs}ms` }"
|
||||
@click.capture="handleClickCapture"
|
||||
@click="handleClick"
|
||||
@contextmenu.prevent="handleContextMenu"
|
||||
>
|
||||
@ -286,7 +361,10 @@ onUnmounted(() => {
|
||||
v-for="item in orderedItems"
|
||||
:key="`${item.type}-${item.id}`"
|
||||
class="grid-item"
|
||||
:class="`size-${item.type === 'widget' ? item.widget.size : (item.icon.size ?? '1x1')}`"
|
||||
:class="[
|
||||
`size-${item.type === 'widget' ? item.widget.size : (item.icon.size ?? '1x1')}`,
|
||||
{ 'is-resized': resizedKeys.has(`${item.type}:${item.id}`) }
|
||||
]"
|
||||
:data-id="item.id"
|
||||
:data-type="item.type"
|
||||
>
|
||||
@ -320,6 +398,12 @@ onUnmounted(() => {
|
||||
height: var(--cell-size);
|
||||
margin: calc(var(--cell-gap) / 2);
|
||||
cursor: grab;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.grid-item.is-resized {
|
||||
overflow: hidden;
|
||||
border-radius: $border-radius-small;
|
||||
}
|
||||
|
||||
.grid-item.size-1x2 {
|
||||
@ -343,10 +427,25 @@ onUnmounted(() => {
|
||||
.grid-item-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.grid-item.is-resized .grid-item-content {
|
||||
animation: grid-item-resize-zoom var(--layout-anim-ms) $motion-easing-standard;
|
||||
}
|
||||
|
||||
@keyframes grid-item-resize-zoom {
|
||||
0% {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.grid-item.muuri-item-dragging {
|
||||
z-index: $z-index-menu;
|
||||
cursor: grabbing;
|
||||
transition: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user