Compare commits
3 Commits
d36609d8c2
...
53ea5e3fc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53ea5e3fc1 | ||
|
|
d35fc11267 | ||
|
|
697eb64dd4 |
@@ -32,6 +32,8 @@ pub struct WeekViewProps {
|
||||
enum DragType {
|
||||
CreateEvent,
|
||||
MoveEvent(CalendarEvent),
|
||||
ResizeEventStart(CalendarEvent), // Resizing from the top edge (start time)
|
||||
ResizeEventEnd(CalendarEvent), // Resizing from the bottom edge (end time)
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
@@ -42,6 +44,7 @@ struct DragState {
|
||||
start_y: f64,
|
||||
current_y: f64,
|
||||
offset_y: f64, // For event moves, this is the offset from the event's top
|
||||
has_moved: bool, // Track if we've moved enough to constitute a real drag
|
||||
}
|
||||
|
||||
#[function_component(WeekView)]
|
||||
@@ -161,6 +164,7 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
start_y: snapped_y,
|
||||
current_y: snapped_y,
|
||||
offset_y: 0.0,
|
||||
has_moved: false,
|
||||
}));
|
||||
e.prevent_default();
|
||||
})
|
||||
@@ -173,11 +177,21 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
if let Some(mut current_drag) = (*drag_state).clone() {
|
||||
if current_drag.is_dragging {
|
||||
// Use layer_y for consistent coordinate calculation
|
||||
let relative_y = e.layer_y() as f64;
|
||||
let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 };
|
||||
let mouse_y = e.layer_y() as f64;
|
||||
let mouse_y = if mouse_y > 0.0 { mouse_y } else { e.offset_y() as f64 };
|
||||
|
||||
// For move operations, we now follow the mouse directly since we start at click position
|
||||
// For resize operations, we still use the mouse position directly
|
||||
let adjusted_y = mouse_y;
|
||||
|
||||
// Snap to increment
|
||||
let snapped_y = snap_to_increment(relative_y, time_increment);
|
||||
let snapped_y = snap_to_increment(adjusted_y, time_increment);
|
||||
|
||||
// Check if we've moved enough to constitute a real drag (5 pixels minimum)
|
||||
let movement_distance = (snapped_y - current_drag.start_y).abs();
|
||||
if movement_distance > 5.0 {
|
||||
current_drag.has_moved = true;
|
||||
}
|
||||
|
||||
current_drag.current_y = snapped_y;
|
||||
drag_state.set(Some(current_drag));
|
||||
@@ -190,9 +204,10 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
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();
|
||||
let time_increment = props.time_increment;
|
||||
Callback::from(move |_e: MouseEvent| {
|
||||
if let Some(current_drag) = (*drag_state).clone() {
|
||||
if current_drag.is_dragging {
|
||||
if current_drag.is_dragging && current_drag.has_moved {
|
||||
match ¤t_drag.drag_type {
|
||||
DragType::CreateEvent => {
|
||||
// Calculate start and end times
|
||||
@@ -221,8 +236,11 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
}
|
||||
},
|
||||
DragType::MoveEvent(event) => {
|
||||
// Calculate new start time based on drag position
|
||||
let new_start_time = pixels_to_time(current_drag.current_y);
|
||||
// Calculate new start time based on drag position (accounting for click offset)
|
||||
let unsnapped_position = current_drag.current_y - current_drag.offset_y;
|
||||
// Snap the final position to maintain time increment alignment
|
||||
let event_top_position = snap_to_increment(unsnapped_position, time_increment);
|
||||
let new_start_time = pixels_to_time(event_top_position);
|
||||
|
||||
// Calculate duration from original event
|
||||
let original_duration = if let Some(end) = event.end {
|
||||
@@ -234,6 +252,51 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
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));
|
||||
}
|
||||
},
|
||||
DragType::ResizeEventStart(event) => {
|
||||
// Calculate new start time based on drag position
|
||||
let new_start_time = pixels_to_time(current_drag.current_y);
|
||||
|
||||
// Keep the original end time
|
||||
let original_end = if let Some(end) = event.end {
|
||||
end.with_timezone(&chrono::Local).naive_local()
|
||||
} else {
|
||||
// If no end time, use start time + 1 hour as default
|
||||
event.start.with_timezone(&chrono::Local).naive_local() + chrono::Duration::hours(1)
|
||||
};
|
||||
|
||||
let new_start_datetime = NaiveDateTime::new(current_drag.start_date, new_start_time);
|
||||
|
||||
// Ensure start is before end (minimum 15 minutes)
|
||||
let new_end_datetime = if new_start_datetime >= original_end {
|
||||
new_start_datetime + chrono::Duration::minutes(15)
|
||||
} else {
|
||||
original_end
|
||||
};
|
||||
|
||||
if let Some(callback) = &on_event_update {
|
||||
callback.emit((event.clone(), new_start_datetime, new_end_datetime));
|
||||
}
|
||||
},
|
||||
DragType::ResizeEventEnd(event) => {
|
||||
// Calculate new end time based on drag position
|
||||
let new_end_time = pixels_to_time(current_drag.current_y);
|
||||
|
||||
// Keep the original start time
|
||||
let original_start = event.start.with_timezone(&chrono::Local).naive_local();
|
||||
|
||||
let new_end_datetime = NaiveDateTime::new(current_drag.start_date, new_end_time);
|
||||
|
||||
// Ensure end is after start (minimum 15 minutes)
|
||||
let new_start_datetime = if new_end_datetime <= original_start {
|
||||
new_end_datetime - chrono::Duration::minutes(15)
|
||||
} else {
|
||||
original_start
|
||||
};
|
||||
|
||||
if let Some(callback) = &on_event_update {
|
||||
callback.emit((event.clone(), new_start_datetime, new_end_datetime));
|
||||
}
|
||||
@@ -297,36 +360,39 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
let drag_state = drag_state.clone();
|
||||
let event_for_drag = event.clone();
|
||||
let date_for_drag = *date;
|
||||
let time_increment = props.time_increment;
|
||||
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)
|
||||
// Only handle left-click (button 0) for moving
|
||||
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 };
|
||||
// Calculate click position relative to event element
|
||||
let click_y_relative = e.layer_y() as f64;
|
||||
let click_y_relative = if click_y_relative > 0.0 { click_y_relative } else { e.offset_y() as f64 };
|
||||
|
||||
// Get event's current position
|
||||
// Get event's current position in day column coordinates
|
||||
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;
|
||||
// Convert click position to day column coordinates
|
||||
let click_y = event_start_pixels + click_y_relative;
|
||||
|
||||
// Snap to increment
|
||||
let snapped_y = snap_to_increment(relative_y, time_increment);
|
||||
// Store the offset from the event's top where the user clicked
|
||||
// This will be used to maintain the relative click position
|
||||
let offset_y = click_y_relative;
|
||||
|
||||
// Start drag tracking from where we clicked (in day column coordinates)
|
||||
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,
|
||||
start_y: click_y,
|
||||
current_y: click_y,
|
||||
offset_y,
|
||||
has_moved: false,
|
||||
}));
|
||||
e.prevent_default();
|
||||
})
|
||||
@@ -336,7 +402,16 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
if let Some(callback) = &props.on_event_context_menu {
|
||||
let callback = callback.clone();
|
||||
let event = event.clone();
|
||||
let drag_state_for_context = drag_state.clone();
|
||||
Some(Callback::from(move |e: web_sys::MouseEvent| {
|
||||
// Check if we're currently dragging - if so, prevent context menu
|
||||
if let Some(drag) = (*drag_state_for_context).clone() {
|
||||
if drag.is_dragging {
|
||||
e.prevent_default();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
e.prevent_default();
|
||||
e.stop_propagation(); // Prevent calendar context menu from also triggering
|
||||
callback.emit((e, event.clone()));
|
||||
@@ -377,12 +452,16 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
}
|
||||
};
|
||||
|
||||
// Check if this event is currently being dragged
|
||||
// Check if this event is currently being dragged or resized
|
||||
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
|
||||
match &drag.drag_type {
|
||||
DragType::MoveEvent(dragged_event) =>
|
||||
dragged_event.uid == event.uid && drag.is_dragging,
|
||||
DragType::ResizeEventStart(dragged_event) =>
|
||||
dragged_event.uid == event.uid && drag.is_dragging,
|
||||
DragType::ResizeEventEnd(dragged_event) =>
|
||||
dragged_event.uid == event.uid && drag.is_dragging,
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
@@ -392,6 +471,57 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
// Hide the original event while being dragged
|
||||
Some(html! {})
|
||||
} else {
|
||||
// Create resize handles for left-click resize
|
||||
let resize_start_handler = {
|
||||
let drag_state = drag_state.clone();
|
||||
let event_for_resize = event.clone();
|
||||
let date_for_drag = *date;
|
||||
let time_increment = props.time_increment;
|
||||
Callback::from(move |e: web_sys::MouseEvent| {
|
||||
e.stop_propagation();
|
||||
|
||||
let relative_y = e.layer_y() as f64;
|
||||
let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 };
|
||||
let snapped_y = snap_to_increment(relative_y, time_increment);
|
||||
|
||||
drag_state.set(Some(DragState {
|
||||
is_dragging: true,
|
||||
drag_type: DragType::ResizeEventStart(event_for_resize.clone()),
|
||||
start_date: date_for_drag,
|
||||
start_y: snapped_y,
|
||||
current_y: snapped_y,
|
||||
offset_y: 0.0,
|
||||
has_moved: false,
|
||||
}));
|
||||
e.prevent_default();
|
||||
})
|
||||
};
|
||||
|
||||
let resize_end_handler = {
|
||||
let drag_state = drag_state.clone();
|
||||
let event_for_resize = event.clone();
|
||||
let date_for_drag = *date;
|
||||
let time_increment = props.time_increment;
|
||||
Callback::from(move |e: web_sys::MouseEvent| {
|
||||
e.stop_propagation();
|
||||
|
||||
let relative_y = e.layer_y() as f64;
|
||||
let relative_y = if relative_y > 0.0 { relative_y } else { e.offset_y() as f64 };
|
||||
let snapped_y = snap_to_increment(relative_y, time_increment);
|
||||
|
||||
drag_state.set(Some(DragState {
|
||||
is_dragging: true,
|
||||
drag_type: DragType::ResizeEventEnd(event_for_resize.clone()),
|
||||
start_date: date_for_drag,
|
||||
start_y: snapped_y,
|
||||
current_y: snapped_y,
|
||||
offset_y: 0.0,
|
||||
has_moved: false,
|
||||
}));
|
||||
e.prevent_default();
|
||||
})
|
||||
};
|
||||
|
||||
Some(html! {
|
||||
<div
|
||||
class={classes!(
|
||||
@@ -409,9 +539,36 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
{oncontextmenu}
|
||||
onmousedown={onmousedown_event}
|
||||
>
|
||||
<div class="event-title">{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}</div>
|
||||
// Top resize handle
|
||||
{if !is_all_day {
|
||||
html! { <div class="event-time">{time_display}</div> }
|
||||
html! {
|
||||
<div
|
||||
class="resize-handle resize-handle-top"
|
||||
onmousedown={resize_start_handler}
|
||||
/>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
|
||||
// Event content
|
||||
<div class="event-content">
|
||||
<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>
|
||||
|
||||
// Bottom resize handle
|
||||
{if !is_all_day {
|
||||
html! {
|
||||
<div
|
||||
class="resize-handle resize-handle-bottom"
|
||||
onmousedown={resize_end_handler}
|
||||
/>
|
||||
}
|
||||
} else {
|
||||
html! {}
|
||||
}}
|
||||
@@ -446,8 +603,11 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
}
|
||||
},
|
||||
DragType::MoveEvent(event) => {
|
||||
// Show the event being moved at its new position
|
||||
let new_start_time = pixels_to_time(drag.current_y);
|
||||
// Calculate the event's new position accounting for click offset
|
||||
let unsnapped_position = drag.current_y - drag.offset_y;
|
||||
// Snap the final position to maintain time increment alignment
|
||||
let preview_position = snap_to_increment(unsnapped_position, props.time_increment);
|
||||
let new_start_time = pixels_to_time(preview_position);
|
||||
let original_duration = if let Some(end) = event.end {
|
||||
end.signed_duration_since(event.start)
|
||||
} else {
|
||||
@@ -461,12 +621,64 @@ pub fn week_view(props: &WeekViewProps) -> Html {
|
||||
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)}
|
||||
style={format!("top: {}px; height: {}px; background-color: {}; opacity: 0.7;", preview_position, 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>
|
||||
}
|
||||
},
|
||||
DragType::ResizeEventStart(event) => {
|
||||
// Show the event being resized from the start
|
||||
let new_start_time = pixels_to_time(drag.current_y);
|
||||
let original_end = if let Some(end) = event.end {
|
||||
end.with_timezone(&chrono::Local).naive_local()
|
||||
} else {
|
||||
event.start.with_timezone(&chrono::Local).naive_local() + chrono::Duration::hours(1)
|
||||
};
|
||||
|
||||
// Calculate positions for the preview
|
||||
let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date);
|
||||
let original_duration = original_end.signed_duration_since(event.start.with_timezone(&chrono::Local).naive_local());
|
||||
let original_end_pixels = original_start_pixels + (original_duration.num_minutes() as f32);
|
||||
|
||||
let new_start_pixels = drag.current_y;
|
||||
let new_height = (original_end_pixels as f64 - new_start_pixels).max(20.0);
|
||||
|
||||
let event_color = get_event_color(event);
|
||||
|
||||
html! {
|
||||
<div
|
||||
class="temp-event-box resizing-event"
|
||||
style={format!("top: {}px; height: {}px; background-color: {}; opacity: 0.7;", new_start_pixels, new_height, 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"), original_end.time().format("%I:%M %p"))}</div>
|
||||
</div>
|
||||
}
|
||||
},
|
||||
DragType::ResizeEventEnd(event) => {
|
||||
// Show the event being resized from the end
|
||||
let new_end_time = pixels_to_time(drag.current_y);
|
||||
let original_start = event.start.with_timezone(&chrono::Local).naive_local();
|
||||
|
||||
// Calculate positions for the preview
|
||||
let (original_start_pixels, _, _) = calculate_event_position(event, drag.start_date);
|
||||
|
||||
let new_end_pixels = drag.current_y;
|
||||
let new_height = (new_end_pixels - original_start_pixels as f64).max(20.0);
|
||||
|
||||
let event_color = get_event_color(event);
|
||||
|
||||
html! {
|
||||
<div
|
||||
class="temp-event-box resizing-event"
|
||||
style={format!("top: {}px; height: {}px; background-color: {}; opacity: 0.7;", original_start_pixels, new_height, event_color)}
|
||||
>
|
||||
<div class="event-title">{event.summary.as_ref().unwrap_or(&"Untitled".to_string())}</div>
|
||||
<div class="event-time">{format!("{} - {}", original_start.time().format("%I:%M %p"), new_end_time.format("%I:%M %p"))}</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
87
styles.css
87
styles.css
@@ -727,6 +727,93 @@ body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Resizing event during drag */
|
||||
.temp-event-box.resizing-event {
|
||||
background: inherit;
|
||||
border: 2px solid rgba(255, 255, 255, 0.9);
|
||||
color: white;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
transform: scale(1.01);
|
||||
}
|
||||
|
||||
.temp-event-box.resizing-event .event-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.temp-event-box.resizing-event .event-time {
|
||||
font-size: 0.65rem;
|
||||
opacity: 0.9;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Event resize zones and handles */
|
||||
.week-event {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.week-event .event-content {
|
||||
padding: 2px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
pointer-events: auto;
|
||||
z-index: 5;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Left-click drag handles */
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: transparent;
|
||||
cursor: ns-resize;
|
||||
z-index: 10;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.resize-handle-top {
|
||||
top: 0;
|
||||
border-top: 2px solid transparent;
|
||||
}
|
||||
|
||||
.resize-handle-bottom {
|
||||
bottom: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.week-event:hover .resize-handle {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.week-event:hover .resize-handle-top {
|
||||
border-top-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.week-event:hover .resize-handle-bottom {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.resize-handle:hover {
|
||||
background: rgba(255, 255, 255, 0.5) !important;
|
||||
border-color: rgba(255, 255, 255, 1) !important;
|
||||
}
|
||||
|
||||
|
||||
.week-event .event-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
|
||||
Reference in New Issue
Block a user