calendar: enforce two-phase zoom-in morph before view switch
This commit is contained in:
@@ -2281,6 +2281,7 @@ type CalendarZoomGhost = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const calendarContentWrapRef = ref<HTMLElement | null>(null);
|
const calendarContentWrapRef = ref<HTMLElement | null>(null);
|
||||||
|
const calendarZoomOverlayRef = ref<HTMLElement | null>(null);
|
||||||
const calendarHoveredMonthIndex = ref<number | null>(null);
|
const calendarHoveredMonthIndex = ref<number | null>(null);
|
||||||
const calendarHoveredWeekStartKey = ref("");
|
const calendarHoveredWeekStartKey = ref("");
|
||||||
const calendarHoveredDayKey = ref("");
|
const calendarHoveredDayKey = ref("");
|
||||||
@@ -2400,13 +2401,29 @@ function getElementRectInCalendar(element: HTMLElement | null): CalendarRect | n
|
|||||||
const wrapRect = calendarContentWrapRef.value?.getBoundingClientRect();
|
const wrapRect = calendarContentWrapRef.value?.getBoundingClientRect();
|
||||||
if (!wrapRect) return null;
|
if (!wrapRect) return null;
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
const left = Math.max(0, rect.left - wrapRect.left);
|
const left = Math.max(0, Math.min(rect.left - wrapRect.left, wrapRect.width));
|
||||||
const top = Math.max(0, rect.top - wrapRect.top);
|
const top = Math.max(0, Math.min(rect.top - wrapRect.top, wrapRect.height));
|
||||||
const width = Math.max(24, Math.min(rect.width, wrapRect.width - left));
|
const right = Math.max(0, Math.min(rect.right - wrapRect.left, wrapRect.width));
|
||||||
const height = Math.max(24, Math.min(rect.height, wrapRect.height - top));
|
const bottom = Math.max(0, Math.min(rect.bottom - wrapRect.top, wrapRect.height));
|
||||||
|
const visibleWidth = right - left;
|
||||||
|
const visibleHeight = bottom - top;
|
||||||
|
if (visibleWidth < 2 || visibleHeight < 2) return null;
|
||||||
|
const width = Math.min(Math.max(24, visibleWidth), wrapRect.width - left);
|
||||||
|
const height = Math.min(Math.max(24, visibleHeight), wrapRect.height - top);
|
||||||
return { left, top, width, height };
|
return { left, top, width, height };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fallbackZoomOriginRect(viewportRect: CalendarRect): CalendarRect {
|
||||||
|
const width = Math.max(96, Math.round(viewportRect.width * 0.28));
|
||||||
|
const height = Math.max(64, Math.round(viewportRect.height * 0.24));
|
||||||
|
return {
|
||||||
|
left: Math.max(0, Math.round((viewportRect.width - width) / 2)),
|
||||||
|
top: Math.max(0, Math.round((viewportRect.height - height) / 2)),
|
||||||
|
width: Math.min(width, viewportRect.width),
|
||||||
|
height: Math.min(height, viewportRect.height),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function weekRowStartForDate(key: string) {
|
function weekRowStartForDate(key: string) {
|
||||||
const date = new Date(`${key}T00:00:00`);
|
const date = new Date(`${key}T00:00:00`);
|
||||||
date.setDate(date.getDate() - date.getDay());
|
date.setDate(date.getDate() - date.getDay());
|
||||||
@@ -2490,7 +2507,6 @@ function primeCalendarRect(rect: CalendarRect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function morphCalendarRect(toRect: CalendarRect) {
|
function morphCalendarRect(toRect: CalendarRect) {
|
||||||
requestAnimationFrame(() => {
|
|
||||||
calendarZoomOverlay.value = {
|
calendarZoomOverlay.value = {
|
||||||
active: true,
|
active: true,
|
||||||
left: toRect.left,
|
left: toRect.left,
|
||||||
@@ -2498,22 +2514,54 @@ function morphCalendarRect(toRect: CalendarRect) {
|
|||||||
width: toRect.width,
|
width: toRect.width,
|
||||||
height: toRect.height,
|
height: toRect.height,
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextAnimationFrame() {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
requestAnimationFrame(() => resolve());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function waitCalendarZoom() {
|
async function flushCalendarZoomStartFrame() {
|
||||||
|
await nextTick();
|
||||||
|
await nextAnimationFrame();
|
||||||
|
calendarZoomOverlayRef.value?.getBoundingClientRect();
|
||||||
|
await nextAnimationFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitCalendarZoomTransition() {
|
||||||
|
const overlay = calendarZoomOverlayRef.value;
|
||||||
|
if (!overlay) {
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
setTimeout(() => resolve(), CALENDAR_ZOOM_DURATION_MS);
|
setTimeout(() => resolve(), CALENDAR_ZOOM_DURATION_MS);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
let settled = false;
|
||||||
|
const finish = () => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
overlay.removeEventListener("transitionend", onTransitionEnd);
|
||||||
|
clearTimeout(fallbackTimer);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
const onTransitionEnd = (event: TransitionEvent) => {
|
||||||
|
if (event.target !== overlay) return;
|
||||||
|
if (!["left", "top", "width", "height"].includes(event.propertyName)) return;
|
||||||
|
finish();
|
||||||
|
};
|
||||||
|
const fallbackTimer = setTimeout(() => finish(), CALENDAR_ZOOM_DURATION_MS + 140);
|
||||||
|
overlay.addEventListener("transitionend", onTransitionEnd);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function animateCalendarZoomIn(sourceElement: HTMLElement | null, ghost: CalendarZoomGhost, apply: () => void) {
|
async function animateCalendarZoomIn(sourceElement: HTMLElement | null, ghost: CalendarZoomGhost, apply: () => void) {
|
||||||
const fromRect = getElementRectInCalendar(sourceElement);
|
|
||||||
const viewportRect = getCalendarViewportRect();
|
const viewportRect = getCalendarViewportRect();
|
||||||
if (!fromRect || !viewportRect) {
|
if (!viewportRect) {
|
||||||
apply();
|
apply();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const fromRect = getElementRectInCalendar(sourceElement) ?? fallbackZoomOriginRect(viewportRect);
|
||||||
|
|
||||||
clearCalendarZoomPrime();
|
clearCalendarZoomPrime();
|
||||||
calendarZoomBusy.value = true;
|
calendarZoomBusy.value = true;
|
||||||
@@ -2521,9 +2569,9 @@ async function animateCalendarZoomIn(sourceElement: HTMLElement | null, ghost: C
|
|||||||
primeCalendarRect(fromRect);
|
primeCalendarRect(fromRect);
|
||||||
calendarZoomGhost.value = ghost;
|
calendarZoomGhost.value = ghost;
|
||||||
calendarSceneMasked.value = true;
|
calendarSceneMasked.value = true;
|
||||||
await nextTick();
|
await flushCalendarZoomStartFrame();
|
||||||
morphCalendarRect(viewportRect);
|
morphCalendarRect(viewportRect);
|
||||||
await waitCalendarZoom();
|
await waitCalendarZoomTransition();
|
||||||
apply();
|
apply();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2546,7 +2594,7 @@ async function animateCalendarZoomOut(apply: () => void, resolveTarget: () => HT
|
|||||||
primeCalendarRect(viewportRect);
|
primeCalendarRect(viewportRect);
|
||||||
calendarZoomGhost.value = zoomGhostForCurrentView();
|
calendarZoomGhost.value = zoomGhostForCurrentView();
|
||||||
calendarSceneMasked.value = true;
|
calendarSceneMasked.value = true;
|
||||||
await nextTick();
|
await flushCalendarZoomStartFrame();
|
||||||
apply();
|
apply();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
const targetRect = getElementRectInCalendar(resolveTarget());
|
const targetRect = getElementRectInCalendar(resolveTarget());
|
||||||
@@ -2558,7 +2606,7 @@ async function animateCalendarZoomOut(apply: () => void, resolveTarget: () => HT
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
morphCalendarRect(targetRect);
|
morphCalendarRect(targetRect);
|
||||||
await waitCalendarZoom();
|
await waitCalendarZoomTransition();
|
||||||
} finally {
|
} finally {
|
||||||
clearCalendarZoomOverlay();
|
clearCalendarZoomOverlay();
|
||||||
calendarSceneMasked.value = false;
|
calendarSceneMasked.value = false;
|
||||||
@@ -4964,6 +5012,7 @@ async function decideFeedCard(card: FeedCard, decision: "accepted" | "rejected")
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="calendarZoomOverlay.active"
|
v-if="calendarZoomOverlay.active"
|
||||||
|
ref="calendarZoomOverlayRef"
|
||||||
class="calendar-zoom-overlay"
|
class="calendar-zoom-overlay"
|
||||||
:style="calendarZoomOverlayStyle"
|
:style="calendarZoomOverlayStyle"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user