Enables event resizing via drag handles
Adds a resize handle manager to handle mouse hover effects on calendar events and display a resize indicator. This allows users to visually identify and initiate event resizing by hovering near the bottom edge of an event.
This commit is contained in:
parent
7d7a8d9208
commit
a9819a8bf1
4 changed files with 130 additions and 61 deletions
|
|
@ -8,6 +8,7 @@ import { ViewManager } from '../managers/ViewManager';
|
||||||
import { CalendarManager } from '../managers/CalendarManager';
|
import { CalendarManager } from '../managers/CalendarManager';
|
||||||
import { DragDropManager } from '../managers/DragDropManager';
|
import { DragDropManager } from '../managers/DragDropManager';
|
||||||
import { AllDayManager } from '../managers/AllDayManager';
|
import { AllDayManager } from '../managers/AllDayManager';
|
||||||
|
import { ResizeHandleManager } from '../managers/ResizeHandleManager';
|
||||||
import { CalendarManagers } from '../types/ManagerTypes';
|
import { CalendarManagers } from '../types/ManagerTypes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -39,6 +40,7 @@ export class ManagerFactory {
|
||||||
const viewManager = new ViewManager(eventBus);
|
const viewManager = new ViewManager(eventBus);
|
||||||
const dragDropManager = new DragDropManager(eventBus);
|
const dragDropManager = new DragDropManager(eventBus);
|
||||||
const allDayManager = new AllDayManager(eventManager);
|
const allDayManager = new AllDayManager(eventManager);
|
||||||
|
const resizeHandleManager = new ResizeHandleManager();
|
||||||
|
|
||||||
// CalendarManager depends on all other managers
|
// CalendarManager depends on all other managers
|
||||||
const calendarManager = new CalendarManager(
|
const calendarManager = new CalendarManager(
|
||||||
|
|
@ -59,7 +61,8 @@ export class ManagerFactory {
|
||||||
viewManager,
|
viewManager,
|
||||||
calendarManager,
|
calendarManager,
|
||||||
dragDropManager,
|
dragDropManager,
|
||||||
allDayManager
|
allDayManager,
|
||||||
|
resizeHandleManager
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,6 +73,7 @@ export class ManagerFactory {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await managers.calendarManager.initialize?.();
|
await managers.calendarManager.initialize?.();
|
||||||
|
managers.resizeHandleManager.initialize();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
89
src/managers/ResizeHandleManager.ts
Normal file
89
src/managers/ResizeHandleManager.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
export class ResizeHandleManager {
|
||||||
|
private resizeZoneHeight =15; // Must match CSS ::after height
|
||||||
|
private lastHoveredEvent: HTMLElement | null = null;
|
||||||
|
|
||||||
|
public initialize(): void {
|
||||||
|
this.setupEventListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupEventListeners(): void {
|
||||||
|
|
||||||
|
// Listen to global mouseover from document
|
||||||
|
document.addEventListener('mouseover', (e: MouseEvent) => {
|
||||||
|
this.handleGlobalMouseOver(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', (e: MouseEvent) => {
|
||||||
|
this.handleGlobalMouseMove(e);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('mouseout', (e: MouseEvent) => {
|
||||||
|
this.handleGlobalMouseOut(e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleGlobalMouseOver(e: MouseEvent): void {
|
||||||
|
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 {
|
||||||
|
// Check all events to see if mouse is in their resize zone
|
||||||
|
const events = document.querySelectorAll<HTMLElement>('swp-day-columns swp-event');
|
||||||
|
|
||||||
|
events.forEach(eventElement => {
|
||||||
|
const rect = eventElement.getBoundingClientRect();
|
||||||
|
const mouseY = e.clientY;
|
||||||
|
const mouseX = e.clientX;
|
||||||
|
|
||||||
|
// Check if mouse is within element bounds horizontally
|
||||||
|
const isInHorizontalBounds = mouseX >= rect.left && mouseX <= rect.right;
|
||||||
|
|
||||||
|
// Check if mouse is in bottom 25px of the element
|
||||||
|
const distanceFromBottom = rect.bottom - mouseY;
|
||||||
|
const isInResizeZone = distanceFromBottom >= 0 && distanceFromBottom <= this.resizeZoneHeight;
|
||||||
|
|
||||||
|
if (isInHorizontalBounds && isInResizeZone) {
|
||||||
|
this.showResizeIndicator(eventElement);
|
||||||
|
console.log('✅ In resize zone - bottom 25px');
|
||||||
|
} else {
|
||||||
|
this.hideResizeIndicator(eventElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private showResizeIndicator(eventElement: HTMLElement): void {
|
||||||
|
// Check if indicator already exists
|
||||||
|
let indicator = eventElement.querySelector<HTMLElement>('swp-resize-indicator');
|
||||||
|
|
||||||
|
if (!indicator) {
|
||||||
|
indicator = document.createElement('swp-resize-indicator');
|
||||||
|
eventElement.appendChild(indicator);
|
||||||
|
}
|
||||||
|
|
||||||
|
eventElement.setAttribute('data-resize-hover', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
private hideResizeIndicator(eventElement: HTMLElement): void {
|
||||||
|
const indicator = eventElement.querySelector<HTMLElement>('swp-resize-indicator');
|
||||||
|
if (indicator) {
|
||||||
|
indicator.remove();
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ export interface CalendarManagers {
|
||||||
calendarManager: CalendarManager;
|
calendarManager: CalendarManager;
|
||||||
dragDropManager: unknown; // Avoid interface conflicts
|
dragDropManager: unknown; // Avoid interface conflicts
|
||||||
allDayManager: unknown; // Avoid interface conflicts
|
allDayManager: unknown; // Avoid interface conflicts
|
||||||
|
resizeHandleManager: ResizeHandleManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -70,6 +71,10 @@ export interface AllDayManager extends IManager {
|
||||||
[key: string]: unknown; // Allow any properties from actual implementation
|
[key: string]: unknown; // Allow any properties from actual implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResizeHandleManager extends IManager {
|
||||||
|
// ResizeHandleManager handles hover effects for resize handles
|
||||||
|
}
|
||||||
|
|
||||||
export interface ResourceData {
|
export interface ResourceData {
|
||||||
resources: Resource[];
|
resources: Resource[];
|
||||||
assignments?: ResourceAssignment[];
|
assignments?: ResourceAssignment[];
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,35 @@ swp-day-columns swp-event:hover {
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Resize handle indicator - created by JavaScript */
|
||||||
|
swp-resize-indicator {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 15px;
|
||||||
|
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>');
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
opacity: 0;
|
||||||
|
animation: fadeIn 0.8s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
swp-day-columns swp-event[data-resize-hover="true"] {
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
swp-day-columns swp-event-time {
|
swp-day-columns swp-event-time {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
|
@ -83,64 +112,6 @@ swp-day-columns swp-event-title {
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* External resize handles */
|
|
||||||
swp-resize-handle {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 24px;
|
|
||||||
height: 4px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity var(--transition-fast);
|
|
||||||
cursor: ns-resize;
|
|
||||||
z-index: 30;
|
|
||||||
background: var(--color-primary);
|
|
||||||
border-radius: 3px;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
||||||
|
|
||||||
/* Subtle grip pattern */
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
width: 12px;
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255, 255, 255, 0.6);
|
|
||||||
border-radius: 0.5px;
|
|
||||||
box-shadow: 0 -1px 0 rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Top resize handle - positioned OUTSIDE event */
|
|
||||||
swp-resize-handle[data-position="top"] {
|
|
||||||
top: -6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bottom resize handle - positioned OUTSIDE event */
|
|
||||||
swp-resize-handle[data-position="bottom"] {
|
|
||||||
bottom: -6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Resize handles controlled by JavaScript - no general hover */
|
|
||||||
swp-handle-hitarea {
|
|
||||||
position: absolute;
|
|
||||||
left: -8px;
|
|
||||||
right: -8px;
|
|
||||||
top: -6px;
|
|
||||||
bottom: -6px;
|
|
||||||
cursor: ns-resize;
|
|
||||||
}
|
|
||||||
|
|
||||||
swp-handle-hitarea[data-position="top"] {
|
|
||||||
top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
swp-handle-hitarea[data-position="bottom"] {
|
|
||||||
bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Multi-day events */
|
/* Multi-day events */
|
||||||
swp-multi-day-event {
|
swp-multi-day-event {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue