Added intelligent viewport boundary detection that repositions context menus when they would appear outside the screen: - Detects right/bottom edge overflow and repositions menus accordingly - Uses accurate size estimates based on actual menu content - Event menus: 280×200px (recurring) / 180×100px (non-recurring) - Calendar/generic menus: 180×60px for single items - Maintains 5px minimum margins from screen edges - Graceful fallback to original positioning if viewport detection fails 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
167 lines
5.9 KiB
Rust
167 lines
5.9 KiB
Rust
use crate::models::ical::VEvent;
|
||
use web_sys::MouseEvent;
|
||
use yew::prelude::*;
|
||
|
||
#[derive(Clone, PartialEq, Debug)]
|
||
pub enum DeleteAction {
|
||
DeleteThis,
|
||
DeleteFollowing,
|
||
DeleteSeries,
|
||
}
|
||
|
||
#[derive(Clone, PartialEq, Debug)]
|
||
pub enum EditAction {
|
||
EditThis,
|
||
EditFuture,
|
||
EditAll,
|
||
}
|
||
|
||
#[derive(Properties, PartialEq)]
|
||
pub struct EventContextMenuProps {
|
||
pub is_open: bool,
|
||
pub x: i32,
|
||
pub y: i32,
|
||
pub event: Option<VEvent>,
|
||
pub on_edit: Callback<EditAction>,
|
||
pub on_delete: Callback<DeleteAction>,
|
||
pub on_close: Callback<()>,
|
||
}
|
||
|
||
#[function_component(EventContextMenu)]
|
||
pub fn event_context_menu(props: &EventContextMenuProps) -> Html {
|
||
let menu_ref = use_node_ref();
|
||
|
||
if !props.is_open {
|
||
return html! {};
|
||
}
|
||
|
||
// Smart positioning to keep menu within viewport
|
||
let (x, y) = {
|
||
let mut x = props.x;
|
||
let mut y = props.y;
|
||
|
||
// Try to get actual viewport dimensions
|
||
if let Some(window) = web_sys::window() {
|
||
if let (Ok(width), Ok(height)) = (window.inner_width(), window.inner_height()) {
|
||
if let (Some(w), Some(h)) = (width.as_f64(), height.as_f64()) {
|
||
let viewport_width = w as i32;
|
||
let viewport_height = h as i32;
|
||
|
||
// More accurate menu dimensions based on actual CSS and content
|
||
let menu_width = if props.event.as_ref().map_or(false, |e| e.rrule.is_some()) {
|
||
280 // Recurring: "Edit This and Future Events" is long text + padding
|
||
} else {
|
||
180 // Non-recurring: "Edit Event" + "Delete Event" + padding
|
||
};
|
||
let menu_height = if props.event.as_ref().map_or(false, |e| e.rrule.is_some()) {
|
||
200 // 6 items × ~32px per item (12px padding top/bottom + text height + borders)
|
||
} else {
|
||
100 // 2 items × ~32px per item + some extra margin
|
||
};
|
||
|
||
// Adjust horizontally if too close to right edge
|
||
if x + menu_width > viewport_width - 10 {
|
||
x = x.saturating_sub(menu_width);
|
||
}
|
||
|
||
// Adjust vertically if too close to bottom edge
|
||
if y + menu_height > viewport_height - 10 {
|
||
y = y.saturating_sub(menu_height);
|
||
}
|
||
|
||
// Ensure minimum margins from edges
|
||
x = x.max(5);
|
||
y = y.max(5);
|
||
}
|
||
}
|
||
}
|
||
|
||
(x, y)
|
||
};
|
||
|
||
let style = format!(
|
||
"position: fixed; left: {}px; top: {}px; z-index: 1001;",
|
||
x, y
|
||
);
|
||
|
||
// Check if the event is recurring
|
||
let is_recurring = props
|
||
.event
|
||
.as_ref()
|
||
.map(|event| event.rrule.is_some())
|
||
.unwrap_or(false);
|
||
|
||
let create_edit_callback = |action: EditAction| {
|
||
let on_edit = props.on_edit.clone();
|
||
let on_close = props.on_close.clone();
|
||
Callback::from(move |_: MouseEvent| {
|
||
on_edit.emit(action.clone());
|
||
on_close.emit(());
|
||
})
|
||
};
|
||
|
||
let create_delete_callback = |action: DeleteAction| {
|
||
let on_delete = props.on_delete.clone();
|
||
let on_close = props.on_close.clone();
|
||
Callback::from(move |_: MouseEvent| {
|
||
on_delete.emit(action.clone());
|
||
on_close.emit(());
|
||
})
|
||
};
|
||
|
||
html! {
|
||
<div
|
||
ref={menu_ref}
|
||
class="context-menu"
|
||
style={style}
|
||
>
|
||
{
|
||
if is_recurring {
|
||
html! {
|
||
<>
|
||
<div class="context-menu-item" onclick={create_edit_callback(EditAction::EditThis)}>
|
||
{"Edit This Event"}
|
||
</div>
|
||
<div class="context-menu-item" onclick={create_edit_callback(EditAction::EditFuture)}>
|
||
{"Edit This and Future Events"}
|
||
</div>
|
||
<div class="context-menu-item" onclick={create_edit_callback(EditAction::EditAll)}>
|
||
{"Edit All Events in Series"}
|
||
</div>
|
||
</>
|
||
}
|
||
} else {
|
||
html! {
|
||
<div class="context-menu-item" onclick={create_edit_callback(EditAction::EditThis)}>
|
||
{"Edit Event"}
|
||
</div>
|
||
}
|
||
}
|
||
}
|
||
{
|
||
if is_recurring {
|
||
html! {
|
||
<>
|
||
<div class="context-menu-item context-menu-delete" onclick={create_delete_callback(DeleteAction::DeleteThis)}>
|
||
{"Delete This Event"}
|
||
</div>
|
||
<div class="context-menu-item context-menu-delete" onclick={create_delete_callback(DeleteAction::DeleteFollowing)}>
|
||
{"Delete Following Events"}
|
||
</div>
|
||
<div class="context-menu-item context-menu-delete" onclick={create_delete_callback(DeleteAction::DeleteSeries)}>
|
||
{"Delete Entire Series"}
|
||
</div>
|
||
</>
|
||
}
|
||
} else {
|
||
html! {
|
||
<div class="context-menu-item context-menu-delete" onclick={create_delete_callback(DeleteAction::DeleteThis)}>
|
||
{"Delete Event"}
|
||
</div>
|
||
}
|
||
}
|
||
}
|
||
</div>
|
||
}
|
||
}
|