Improves event resize handle interaction

Enhances the resize handle indicator for calendar events by using cached event elements for efficiency.

This eliminates the need to constantly query the DOM, and only refreshes the cache on relevant event changes.

Additionally updates the resize indicator style for improved visual clarity and user experience.
This commit is contained in:
Janus C. H. Knudsen 2025-10-07 17:16:00 +02:00
parent a9819a8bf1
commit 70dce9fd59
2 changed files with 49 additions and 44 deletions

View file

@ -1,39 +1,37 @@
import { eventBus } from '../core/EventBus';
import { CoreEvents } from '../constants/CoreEvents';
export class ResizeHandleManager { export class ResizeHandleManager {
private resizeZoneHeight = 15; // Must match CSS ::after height private resizeZoneHeight = 15; // Must match CSS ::after height
private lastHoveredEvent: HTMLElement | null = null; private cachedEvents: HTMLElement[] = [];
public initialize(): void { public initialize(): void {
this.refreshEventCache();
this.setupEventListeners(); this.setupEventListeners();
} }
private refreshEventCache(): void {
this.cachedEvents = Array.from(
document.querySelectorAll<HTMLElement>('swp-day-columns swp-event')
);
}
private setupEventListeners(): void { private setupEventListeners(): void {
// Listen to global mouseover from document
document.addEventListener('mouseover', (e: MouseEvent) => {
this.handleGlobalMouseOver(e);
});
document.addEventListener('mousemove', (e: MouseEvent) => { document.addEventListener('mousemove', (e: MouseEvent) => {
this.handleGlobalMouseMove(e); this.handleGlobalMouseMove(e);
}); });
document.addEventListener('mouseout', (e: MouseEvent) => { eventBus.on(CoreEvents.GRID_RENDERED, () => this.refreshEventCache());
this.handleGlobalMouseOut(e); eventBus.on(CoreEvents.EVENTS_RENDERED, () => this.refreshEventCache());
}); eventBus.on(CoreEvents.EVENT_CREATED, () => this.refreshEventCache());
} eventBus.on(CoreEvents.EVENT_UPDATED, () => this.refreshEventCache());
eventBus.on(CoreEvents.EVENT_DELETED, () => this.refreshEventCache());
private handleGlobalMouseOver(e: MouseEvent): void { eventBus.on('drag:end', () => this.refreshEventCache());
const target = e.target as HTMLElement;
const eventElement = target.closest<HTMLElement>('swp-day-columns swp-event');
if (eventElement) {
this.lastHoveredEvent = eventElement;
}
} }
private handleGlobalMouseMove(e: MouseEvent): void { private handleGlobalMouseMove(e: MouseEvent): void {
// Check all events to see if mouse is in their resize zone // Check all cached events to see if mouse is in their resize zone
const events = document.querySelectorAll<HTMLElement>('swp-day-columns swp-event'); const events = this.cachedEvents;
events.forEach(eventElement => { events.forEach(eventElement => {
const rect = eventElement.getBoundingClientRect(); const rect = eventElement.getBoundingClientRect();
@ -43,13 +41,13 @@ export class ResizeHandleManager {
// Check if mouse is within element bounds horizontally // Check if mouse is within element bounds horizontally
const isInHorizontalBounds = mouseX >= rect.left && mouseX <= rect.right; const isInHorizontalBounds = mouseX >= rect.left && mouseX <= rect.right;
// Check if mouse is in bottom 25px of the element // Check if mouse is in bottom resize zone of the element
const distanceFromBottom = rect.bottom - mouseY; const distanceFromBottom = rect.bottom - mouseY;
const isInResizeZone = distanceFromBottom >= 0 && distanceFromBottom <= this.resizeZoneHeight; const isInResizeZone = distanceFromBottom >= 0 && distanceFromBottom <= this.resizeZoneHeight;
if (isInHorizontalBounds && isInResizeZone) { if (isInHorizontalBounds && isInResizeZone) {
this.showResizeIndicator(eventElement); this.showResizeIndicator(eventElement);
console.log('✅ In resize zone - bottom 25px'); console.log(`✅ In resize zone - bottom ${this.resizeZoneHeight}px`);
} else { } else {
this.hideResizeIndicator(eventElement); this.hideResizeIndicator(eventElement);
} }
@ -75,15 +73,4 @@ export class ResizeHandleManager {
} }
eventElement.removeAttribute('data-resize-hover'); eventElement.removeAttribute('data-resize-hover');
} }
private handleGlobalMouseOut(e: MouseEvent): void {
const target = e.target as HTMLElement;
const eventElement = target.closest<HTMLElement>('swp-day-columns swp-event');
if (eventElement) {
// Don't remove immediately - let mousemove handle it
}
}
} }

View file

@ -73,17 +73,34 @@ swp-day-columns swp-event:hover {
/* Resize handle indicator - created by JavaScript */ /* Resize handle indicator - created by JavaScript */
swp-resize-indicator { swp-resize-indicator {
position: absolute; position: absolute;
bottom: 0; bottom: -3px;
left: 0; left: 50%;
right: 0; transform: translateX(-50%);
height: 15px; width: 40px;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="4"><rect x="0" y="0" width="16" height="1" fill="%23666"/><rect x="0" y="3" width="16" height="1" fill="%23666"/></svg>'); height: 6px;
background-position: center center; background: var(--color-primary);
background-repeat: no-repeat; border-radius: 3px;
pointer-events: none; pointer-events: none;
z-index: 10; z-index: 30;
opacity: 0; opacity: 0;
animation: fadeIn 0.8s ease forwards; animation: fadeIn 0.3s ease forwards;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
/* Grip dots on handle */
swp-resize-indicator::before {
content: '';
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 12px;
height: 2px;
background: rgba(255, 255, 255, 0.7);
border-radius: 1px;
box-shadow:
-5px 0 0 rgba(255, 255, 255, 0.7),
5px 0 0 rgba(255, 255, 255, 0.7);
} }
@keyframes fadeIn { @keyframes fadeIn {
@ -97,6 +114,7 @@ swp-resize-indicator {
swp-day-columns swp-event[data-resize-hover="true"] { swp-day-columns swp-event[data-resize-hover="true"] {
cursor: ns-resize; cursor: ns-resize;
overflow: visible;
} }
swp-day-columns swp-event-time { swp-day-columns swp-event-time {