Implement drag-to-move functionality for events in week view with CalDAV server integration
- Add drag-to-move event handlers to existing events in week view - Extend drag state management to support both create and move operations - Implement visual feedback with event preview during drag and hidden original - Calculate new start/end times while preserving event duration - Add CalDAV server update integration via calendar service - Wire event update callbacks through component hierarchy (WeekView → Calendar → RouteHandler → App) - Preserve all original event properties (title, description, location, reminders, etc.) - Handle timezone conversion from local to UTC for server storage - Add error handling with user feedback and success confirmation - Include moving event CSS styling with enhanced visual feedback 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -21,17 +21,27 @@ pub struct WeekViewProps {
|
||||
#[prop_or_default]
|
||||
pub on_create_event: Option<Callback<(NaiveDate, NaiveDateTime, NaiveDateTime)>>,
|
||||
#[prop_or_default]
|
||||
pub on_event_update: Option<Callback<(CalendarEvent, NaiveDateTime, NaiveDateTime)>>,
|
||||
#[prop_or_default]
|
||||
pub context_menus_open: bool,
|
||||
#[prop_or_default]
|
||||
pub time_increment: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
enum DragType {
|
||||
CreateEvent,
|
||||
MoveEvent(CalendarEvent),
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct DragState {
|
||||
is_dragging: bool,
|
||||
drag_type: DragType,
|
||||
start_date: NaiveDate,
|
||||
start_y: f64,
|
||||
current_y: f64,
|
||||
offset_y: f64, // For event moves, this is the offset from the event's top
|
||||
}
|
||||
|
||||
#[function_component(WeekView)]
|
||||
@@ -146,9 +156,11 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
|
||||
drag_state.set(Some(DragState {
|
||||
is_dragging: true,
|
||||
drag_type: DragType::CreateEvent,
|
||||
start_date: date_for_drag,
|
||||
start_y: snapped_y,
|
||||
current_y: snapped_y,
|
||||
offset_y: 0.0,
|
||||
}));
|
||||
e.prevent_default();
|
||||
})
|
||||
@@ -177,32 +189,55 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
let onmouseup = {
|
||||
let drag_state = drag_state_clone.clone();
|
||||
let on_create_event = props.on_create_event.clone();
|
||||
let on_event_update = props.on_event_update.clone();
|
||||
Callback::from(move |_e: MouseEvent| {
|
||||
if let Some(current_drag) = (*drag_state).clone() {
|
||||
if current_drag.is_dragging {
|
||||
// Calculate start and end times
|
||||
let start_time = pixels_to_time(current_drag.start_y);
|
||||
let end_time = pixels_to_time(current_drag.current_y);
|
||||
|
||||
// Ensure start is before end
|
||||
let (actual_start, actual_end) = if start_time <= end_time {
|
||||
(start_time, end_time)
|
||||
} else {
|
||||
(end_time, start_time)
|
||||
};
|
||||
|
||||
// Ensure minimum duration (15 minutes)
|
||||
let actual_end = if actual_end.signed_duration_since(actual_start).num_minutes() < 15 {
|
||||
actual_start + chrono::Duration::minutes(15)
|
||||
} else {
|
||||
actual_end
|
||||
};
|
||||
|
||||
let start_datetime = NaiveDateTime::new(current_drag.start_date, actual_start);
|
||||
let end_datetime = NaiveDateTime::new(current_drag.start_date, actual_end);
|
||||
|
||||
if let Some(callback) = &on_create_event {
|
||||
callback.emit((current_drag.start_date, start_datetime, end_datetime));
|
||||
match ¤t_drag.drag_type {
|
||||
DragType::CreateEvent => {
|
||||
// Calculate start and end times
|
||||
let start_time = pixels_to_time(current_drag.start_y);
|
||||
let end_time = pixels_to_time(current_drag.current_y);
|
||||
|
||||
// Ensure start is before end
|
||||
let (actual_start, actual_end) = if start_time <= end_time {
|
||||
(start_time, end_time)
|
||||
} else {
|
||||
(end_time, start_time)
|
||||
};
|
||||
|
||||
// Ensure minimum duration (15 minutes)
|
||||
let actual_end = if actual_end.signed_duration_since(actual_start).num_minutes() < 15 {
|
||||
actual_start + chrono::Duration::minutes(15)
|
||||
} else {
|
||||
actual_end
|
||||
};
|
||||
|
||||
let start_datetime = NaiveDateTime::new(current_drag.start_date, actual_start);
|
||||
let end_datetime = NaiveDateTime::new(current_drag.start_date, actual_end);
|
||||
|
||||
if let Some(callback) = &on_create_event {
|
||||
callback.emit((current_drag.start_date, start_datetime, end_datetime));
|
||||
}
|
||||
},
|
||||
DragType::MoveEvent(event) => {
|
||||
// Calculate new start time based on drag position
|
||||
let new_start_time = pixels_to_time(current_drag.current_y);
|
||||
|
||||
// Calculate duration from original event
|
||||
let original_duration = if let Some(end) = event.end {
|
||||
end.signed_duration_since(event.start)
|
||||
} else {
|
||||
chrono::Duration::hours(1) // Default 1 hour
|
||||
};
|
||||
|
||||
let new_start_datetime = NaiveDateTime::new(current_drag.start_date, new_start_time);
|
||||
let new_end_datetime = new_start_datetime + original_duration;
|
||||
|
||||
if let Some(callback) = &on_event_update {
|
||||
callback.emit((event.clone(), new_start_datetime, new_end_datetime));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drag_state.set(None);
|
||||
@@ -259,8 +294,41 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
};
|
||||
|
||||
let onmousedown_event = {
|
||||
let drag_state = drag_state.clone();
|
||||
let event_for_drag = event.clone();
|
||||
let date_for_drag = *date;
|
||||
let time_increment = props.time_increment;
|
||||
Callback::from(move |e: MouseEvent| {
|
||||
e.stop_propagation(); // Prevent drag-to-create from starting on event clicks
|
||||
|
||||
// Only handle left-click (button 0)
|
||||
if e.button() != 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate Y position relative to the day column
|
||||
let relative_y = e.layer_y() as f64;
|
||||
let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 };
|
||||
|
||||
// Get event's current position
|
||||
let (event_start_pixels, _, _) = calculate_event_position(&event_for_drag, date_for_drag);
|
||||
let event_start_pixels = event_start_pixels as f64;
|
||||
|
||||
// Calculate offset from the top of the event
|
||||
let offset_y = relative_y - event_start_pixels;
|
||||
|
||||
// Snap to increment
|
||||
let snapped_y = snap_to_increment(relative_y, time_increment);
|
||||
|
||||
drag_state.set(Some(DragState {
|
||||
is_dragging: true,
|
||||
drag_type: DragType::MoveEvent(event_for_drag.clone()),
|
||||
start_date: date_for_drag,
|
||||
start_y: snapped_y,
|
||||
current_y: snapped_y,
|
||||
offset_y,
|
||||
}));
|
||||
e.prevent_default();
|
||||
})
|
||||
};
|
||||
|
||||
@@ -309,31 +377,47 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
}
|
||||
};
|
||||
|
||||
Some(html! {
|
||||
<div
|
||||
class={classes!(
|
||||
"week-event",
|
||||
if is_refreshing { Some("refreshing") } else { None },
|
||||
if is_all_day { Some("all-day") } else { None }
|
||||
)}
|
||||
style={format!(
|
||||
"background-color: {}; top: {}px; height: {}px;",
|
||||
event_color,
|
||||
start_pixels,
|
||||
duration_pixels
|
||||
)}
|
||||
{onclick}
|
||||
{oncontextmenu}
|
||||
onmousedown={onmousedown_event}
|
||||
>
|
||||
<div class="event-title">{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}</div>
|
||||
{if !is_all_day {
|
||||
html! { <div class="event-time">{time_display}</div> }
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
</div>
|
||||
})
|
||||
// Check if this event is currently being dragged
|
||||
let is_being_dragged = if let Some(drag) = (*drag_state).clone() {
|
||||
if let DragType::MoveEvent(dragged_event) = &drag.drag_type {
|
||||
dragged_event.uid == event.uid && drag.is_dragging
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if is_being_dragged {
|
||||
// Hide the original event while being dragged
|
||||
Some(html! {})
|
||||
} else {
|
||||
Some(html! {
|
||||
<div
|
||||
class={classes!(
|
||||
"week-event",
|
||||
if is_refreshing { Some("refreshing") } else { None },
|
||||
if is_all_day { Some("all-day") } else { None }
|
||||
)}
|
||||
style={format!(
|
||||
"background-color: {}; top: {}px; height: {}px;",
|
||||
event_color,
|
||||
start_pixels,
|
||||
duration_pixels
|
||||
)}
|
||||
{onclick}
|
||||
{oncontextmenu}
|
||||
onmousedown={onmousedown_event}
|
||||
>
|
||||
<div class="event-title">{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}</div>
|
||||
{if !is_all_day {
|
||||
html! { <div class="event-time">{time_display}</div> }
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
</div>
|
||||
})
|
||||
}
|
||||
}).collect::<Html>()
|
||||
}
|
||||
</div>
|
||||
@@ -342,21 +426,48 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
{
|
||||
if let Some(drag) = (*drag_state).clone() {
|
||||
if drag.is_dragging && drag.start_date == *date {
|
||||
let start_y = drag.start_y.min(drag.current_y);
|
||||
let end_y = drag.start_y.max(drag.current_y);
|
||||
let height = (drag.current_y - drag.start_y).abs().max(20.0);
|
||||
|
||||
// Convert pixels to times for display
|
||||
let start_time = pixels_to_time(start_y);
|
||||
let end_time = pixels_to_time(end_y);
|
||||
|
||||
html! {
|
||||
<div
|
||||
class="temp-event-box"
|
||||
style={format!("top: {}px; height: {}px;", start_y, height)}
|
||||
>
|
||||
{format!("{} - {}", start_time.format("%I:%M %p"), end_time.format("%I:%M %p"))}
|
||||
</div>
|
||||
match &drag.drag_type {
|
||||
DragType::CreateEvent => {
|
||||
let start_y = drag.start_y.min(drag.current_y);
|
||||
let end_y = drag.start_y.max(drag.current_y);
|
||||
let height = (drag.current_y - drag.start_y).abs().max(20.0);
|
||||
|
||||
// Convert pixels to times for display
|
||||
let start_time = pixels_to_time(start_y);
|
||||
let end_time = pixels_to_time(end_y);
|
||||
|
||||
html! {
|
||||
<div
|
||||
class="temp-event-box"
|
||||
style={format!("top: {}px; height: {}px;", start_y, height)}
|
||||
>
|
||||
{format!("{} - {}", start_time.format("%I:%M %p"), end_time.format("%I:%M %p"))}
|
||||
</div>
|
||||
}
|
||||
},
|
||||
DragType::MoveEvent(event) => {
|
||||
// Show the event being moved at its new position
|
||||
let new_start_time = pixels_to_time(drag.current_y);
|
||||
let original_duration = if let Some(end) = event.end {
|
||||
end.signed_duration_since(event.start)
|
||||
} else {
|
||||
chrono::Duration::hours(1)
|
||||
};
|
||||
let duration_pixels = (original_duration.num_minutes() as f64).max(20.0);
|
||||
let new_end_time = new_start_time + original_duration;
|
||||
|
||||
let event_color = get_event_color(event);
|
||||
|
||||
html! {
|
||||
<div
|
||||
class="temp-event-box moving-event"
|
||||
style={format!("top: {}px; height: {}px; background-color: {}; opacity: 0.7;", drag.current_y, duration_pixels, event_color)}
|
||||
>
|
||||
<div class="event-title">{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}</div>
|
||||
<div class="event-time">{format!("{} - {}", new_start_time.format("%I:%M %p"), new_end_time.format("%I:%M %p"))}</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
|
||||
Reference in New Issue
Block a user