- Measure actual print copy div height after aspect-ratio scaling - Recalculate base-unit based on measured height vs original 720px assumption - Apply position scaling to .week-event elements in print copy only - Parse and recalculate top/height pixel values using scale factor - Add landscape orientation and fit-to-page CSS hints for better printing - Preserve hour filtering with proper data attributes and CSS variables This ensures events display correctly when print copy is scaled to fit page while maintaining proper aspect ratio and hour range filtering. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
362 lines
21 KiB
Rust
362 lines
21 KiB
Rust
use crate::components::{ViewMode, WeekView, MonthView};
|
||
use crate::models::ical::VEvent;
|
||
use crate::services::calendar_service::{UserInfo, ExternalCalendar};
|
||
use chrono::NaiveDate;
|
||
use std::collections::HashMap;
|
||
use wasm_bindgen::{closure::Closure, JsCast};
|
||
use yew::prelude::*;
|
||
|
||
#[derive(Properties, PartialEq)]
|
||
pub struct PrintPreviewModalProps {
|
||
pub on_close: Callback<()>,
|
||
pub view_mode: ViewMode,
|
||
pub current_date: NaiveDate,
|
||
pub selected_date: NaiveDate,
|
||
pub events: HashMap<NaiveDate, Vec<VEvent>>,
|
||
pub user_info: Option<UserInfo>,
|
||
pub external_calendars: Vec<ExternalCalendar>,
|
||
pub time_increment: u32,
|
||
pub today: NaiveDate,
|
||
}
|
||
|
||
#[function_component]
|
||
pub fn PrintPreviewModal(props: &PrintPreviewModalProps) -> Html {
|
||
let start_hour = use_state(|| 6u32);
|
||
let end_hour = use_state(|| 22u32);
|
||
let zoom_level = use_state(|| 0.4f64); // Default 40% zoom
|
||
|
||
let close_modal = {
|
||
let on_close = props.on_close.clone();
|
||
Callback::from(move |_| {
|
||
on_close.emit(());
|
||
})
|
||
};
|
||
|
||
let backdrop_click = {
|
||
let on_close = props.on_close.clone();
|
||
Callback::from(move |e: MouseEvent| {
|
||
if e.target() == e.current_target() {
|
||
on_close.emit(());
|
||
}
|
||
})
|
||
};
|
||
|
||
let on_start_hour_change = {
|
||
let start_hour = start_hour.clone();
|
||
let end_hour = end_hour.clone();
|
||
Callback::from(move |e: Event| {
|
||
let target = e.target_dyn_into::<web_sys::HtmlSelectElement>();
|
||
if let Some(select) = target {
|
||
if let Ok(hour) = select.value().parse::<u32>() {
|
||
if hour < *end_hour {
|
||
start_hour.set(hour);
|
||
}
|
||
}
|
||
}
|
||
})
|
||
};
|
||
|
||
let on_end_hour_change = {
|
||
let start_hour = start_hour.clone();
|
||
let end_hour = end_hour.clone();
|
||
Callback::from(move |e: Event| {
|
||
let target = e.target_dyn_into::<web_sys::HtmlSelectElement>();
|
||
if let Some(select) = target {
|
||
if let Ok(hour) = select.value().parse::<u32>() {
|
||
if hour > *start_hour && hour <= 24 {
|
||
end_hour.set(hour);
|
||
}
|
||
}
|
||
}
|
||
})
|
||
};
|
||
|
||
|
||
let format_hour = |hour: u32| -> String {
|
||
if hour == 0 {
|
||
"12 AM".to_string()
|
||
} else if hour < 12 {
|
||
format!("{} AM", hour)
|
||
} else if hour == 12 {
|
||
"12 PM".to_string()
|
||
} else {
|
||
format!("{} PM", hour - 12)
|
||
}
|
||
};
|
||
|
||
// Calculate dynamic base unit for print preview
|
||
let calculate_print_dimensions = |start_hour: u32, end_hour: u32, time_increment: u32| -> (f64, f64, f64) {
|
||
let visible_hours = (end_hour - start_hour) as f64;
|
||
let slots_per_hour = if time_increment == 15 { 4.0 } else { 2.0 };
|
||
let header_height = 50.0; // Fixed week header height in print preview
|
||
let header_border = 2.0; // Week header bottom border (2px solid)
|
||
let container_spacing = 8.0; // Additional container spacing/margins
|
||
let total_overhead = header_height + header_border + container_spacing;
|
||
let available_height = 720.0 - total_overhead; // Available for time content
|
||
let base_unit = available_height / (visible_hours * slots_per_hour);
|
||
let pixels_per_hour = base_unit * slots_per_hour;
|
||
|
||
(base_unit, pixels_per_hour, available_height)
|
||
};
|
||
|
||
// Calculate print dimensions for the current hour range
|
||
let (base_unit, pixels_per_hour, _available_height) = calculate_print_dimensions(*start_hour, *end_hour, props.time_increment);
|
||
|
||
// Effect to update print copy whenever modal renders or content changes
|
||
{
|
||
let start_hour = *start_hour;
|
||
let end_hour = *end_hour;
|
||
let time_increment = props.time_increment;
|
||
let original_base_unit = base_unit;
|
||
use_effect(move || {
|
||
if let Some(window) = web_sys::window() {
|
||
if let Some(document) = window.document() {
|
||
// Set CSS variables on document root
|
||
if let Some(document_element) = document.document_element() {
|
||
if let Some(html_element) = document_element.dyn_ref::<web_sys::HtmlElement>() {
|
||
let style = html_element.style();
|
||
let _ = style.set_property("--print-start-hour", &start_hour.to_string());
|
||
let _ = style.set_property("--print-end-hour", &end_hour.to_string());
|
||
}
|
||
}
|
||
|
||
// Copy content from print-preview-content to the hidden print-preview-copy div
|
||
let copy_content = move || {
|
||
if let Some(preview_content) = document.query_selector(".print-preview-content").ok().flatten() {
|
||
if let Some(print_copy) = document.get_element_by_id("print-preview-copy") {
|
||
// Clone the preview content
|
||
if let Some(content_clone) = preview_content.clone_node_with_deep(true).ok() {
|
||
// Clear the print copy div and add the cloned content
|
||
print_copy.set_inner_html("");
|
||
let _ = print_copy.append_child(&content_clone);
|
||
|
||
// Get the actual rendered height of the print copy div and recalculate base-unit
|
||
if let Some(print_copy_html) = print_copy.dyn_ref::<web_sys::HtmlElement>() {
|
||
// Temporarily make visible to measure height, then hide again
|
||
let original_display = print_copy_html.style().get_property_value("display").unwrap_or_default();
|
||
let _ = print_copy_html.style().set_property("display", "block");
|
||
let _ = print_copy_html.style().set_property("visibility", "hidden");
|
||
let _ = print_copy_html.style().set_property("position", "absolute");
|
||
let _ = print_copy_html.style().set_property("top", "-9999px");
|
||
|
||
// Now measure the height
|
||
let actual_height = print_copy_html.client_height() as f64;
|
||
|
||
// Restore original display
|
||
let _ = print_copy_html.style().set_property("display", &original_display);
|
||
let _ = print_copy_html.style().remove_property("visibility");
|
||
let _ = print_copy_html.style().remove_property("position");
|
||
let _ = print_copy_html.style().remove_property("top");
|
||
|
||
// Recalculate base-unit and pixels-per-hour based on actual height
|
||
let visible_hours = (end_hour - start_hour) as f64;
|
||
let slots_per_hour = if time_increment == 15 { 4.0 } else { 2.0 };
|
||
let header_height = 50.0;
|
||
let header_border = 2.0;
|
||
let container_spacing = 8.0;
|
||
let total_overhead = header_height + header_border + container_spacing;
|
||
let available_height = actual_height - total_overhead;
|
||
let actual_base_unit = available_height / (visible_hours * slots_per_hour);
|
||
let actual_pixels_per_hour = actual_base_unit * slots_per_hour;
|
||
|
||
|
||
// Set CSS variables with recalculated values
|
||
let style = print_copy_html.style();
|
||
let _ = style.set_property("--print-base-unit", &format!("{:.2}", actual_base_unit));
|
||
let _ = style.set_property("--print-pixels-per-hour", &format!("{:.2}", actual_pixels_per_hour));
|
||
let _ = style.set_property("--print-start-hour", &start_hour.to_string());
|
||
let _ = style.set_property("--print-end-hour", &end_hour.to_string());
|
||
|
||
// Copy data attributes
|
||
let _ = print_copy.set_attribute("data-start-hour", &start_hour.to_string());
|
||
let _ = print_copy.set_attribute("data-end-hour", &end_hour.to_string());
|
||
|
||
// Recalculate event positions using the new base-unit
|
||
let events = print_copy.query_selector_all(".week-event").unwrap();
|
||
let scale_factor = actual_base_unit / original_base_unit;
|
||
|
||
for i in 0..events.length() {
|
||
if let Some(event_element) = events.get(i) {
|
||
if let Some(event_html) = event_element.dyn_ref::<web_sys::HtmlElement>() {
|
||
let event_style = event_html.style();
|
||
|
||
// Get current positioning values and recalculate
|
||
if let Ok(current_top) = event_style.get_property_value("top") {
|
||
if current_top.ends_with("px") {
|
||
if let Ok(top_px) = current_top[..current_top.len()-2].parse::<f64>() {
|
||
let new_top = top_px * scale_factor;
|
||
let _ = event_style.set_property("top", &format!("{:.2}px", new_top));
|
||
}
|
||
}
|
||
}
|
||
|
||
if let Ok(current_height) = event_style.get_property_value("height") {
|
||
if current_height.ends_with("px") {
|
||
if let Ok(height_px) = current_height[..current_height.len()-2].parse::<f64>() {
|
||
let new_height = height_px * scale_factor;
|
||
let _ = event_style.set_property("height", &format!("{:.2}px", new_height));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
web_sys::console::log_1(&format!("Height: {:.2}, Original base-unit: {:.2}, New base-unit: {:.2}, Scale factor: {:.2}",
|
||
actual_height, original_base_unit, actual_base_unit, scale_factor).into());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
// Copy content immediately
|
||
copy_content();
|
||
|
||
// Also set up a small delay to catch any async rendering
|
||
let copy_callback = Closure::wrap(Box::new(copy_content) as Box<dyn FnMut()>);
|
||
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||
copy_callback.as_ref().unchecked_ref(),
|
||
100
|
||
);
|
||
copy_callback.forget();
|
||
}
|
||
}
|
||
|| ()
|
||
});
|
||
}
|
||
|
||
let on_print = {
|
||
Callback::from(move |_: MouseEvent| {
|
||
if let Some(window) = web_sys::window() {
|
||
// Print copy is already updated by the use_effect, just trigger print
|
||
let _ = window.print();
|
||
}
|
||
})
|
||
};
|
||
|
||
html! {
|
||
<div class="modal-backdrop print-preview-modal-backdrop" onclick={backdrop_click}>
|
||
<div class="modal-content print-preview-modal">
|
||
<div class="modal-header">
|
||
<h3>{"Print Preview"}</h3>
|
||
<button class="modal-close" onclick={close_modal.clone()}>{"×"}</button>
|
||
</div>
|
||
<div class="modal-body print-preview-body">
|
||
<div class="print-preview-controls">
|
||
{
|
||
if props.view_mode == ViewMode::Week {
|
||
html! {
|
||
<>
|
||
<div class="control-group">
|
||
<label for="start-hour">{"Start Hour:"}</label>
|
||
<select id="start-hour" onchange={on_start_hour_change}>
|
||
{
|
||
(0..24).map(|hour| {
|
||
html! {
|
||
<option value={hour.to_string()} selected={hour == *start_hour}>
|
||
{format_hour(hour)}
|
||
</option>
|
||
}
|
||
}).collect::<Html>()
|
||
}
|
||
</select>
|
||
</div>
|
||
<div class="control-group">
|
||
<label for="end-hour">{"End Hour:"}</label>
|
||
<select id="end-hour" onchange={on_end_hour_change}>
|
||
{
|
||
(1..=24).map(|hour| {
|
||
html! {
|
||
<option value={hour.to_string()} selected={hour == *end_hour}>
|
||
{if hour == 24 { "12 AM".to_string() } else { format_hour(hour) }}
|
||
</option>
|
||
}
|
||
}).collect::<Html>()
|
||
}
|
||
</select>
|
||
</div>
|
||
<div class="hour-range-info">
|
||
{format!("Will print from {} to {}",
|
||
format_hour(*start_hour),
|
||
if *end_hour == 24 { "12 AM".to_string() } else { format_hour(*end_hour) }
|
||
)}
|
||
</div>
|
||
</>
|
||
}
|
||
} else {
|
||
html! {
|
||
<div class="month-info">
|
||
{"Will print entire month view"}
|
||
</div>
|
||
}
|
||
}
|
||
}
|
||
<div class="zoom-display-info">
|
||
<label>{"Zoom: "}</label>
|
||
<span>{format!("{}%", (*zoom_level * 100.0) as i32)}</span>
|
||
<span class="zoom-hint">{"(scroll to zoom)"}</span>
|
||
</div>
|
||
<div class="preview-actions">
|
||
<button class="btn-primary" onclick={on_print}>{"Print"}</button>
|
||
<button class="btn-secondary" onclick={close_modal}>{"Cancel"}</button>
|
||
</div>
|
||
</div>
|
||
<div class="print-preview-display" onwheel={{
|
||
let zoom_level = zoom_level.clone();
|
||
Callback::from(move |e: WheelEvent| {
|
||
e.prevent_default(); // Prevent page scroll
|
||
let delta_y = e.delta_y();
|
||
let zoom_change = if delta_y < 0.0 { 1.1 } else { 1.0 / 1.1 };
|
||
let new_zoom = (*zoom_level * zoom_change).clamp(0.2, 1.5);
|
||
zoom_level.set(new_zoom);
|
||
})
|
||
}}>
|
||
<div class="print-preview-paper"
|
||
data-start-hour={start_hour.to_string()}
|
||
data-end-hour={end_hour.to_string()}
|
||
style={format!(
|
||
"--print-start-hour: {}; --print-end-hour: {}; --print-base-unit: {:.2}; --print-pixels-per-hour: {:.2}; transform: scale({}); transform-origin: top center;",
|
||
*start_hour, *end_hour, base_unit, pixels_per_hour, *zoom_level
|
||
)}>
|
||
<div class="print-preview-content">
|
||
{
|
||
match props.view_mode {
|
||
ViewMode::Week => html! {
|
||
<WeekView
|
||
key={format!("week-preview-{}-{}", *start_hour, *end_hour)}
|
||
current_date={props.current_date}
|
||
today={props.today}
|
||
events={props.events.clone()}
|
||
on_event_click={Callback::noop()}
|
||
user_info={props.user_info.clone()}
|
||
external_calendars={props.external_calendars.clone()}
|
||
time_increment={props.time_increment}
|
||
print_mode={true}
|
||
print_pixels_per_hour={Some(pixels_per_hour)}
|
||
print_start_hour={Some(*start_hour)}
|
||
/>
|
||
},
|
||
ViewMode::Month => html! {
|
||
<MonthView
|
||
key={format!("month-preview-{}-{}", *start_hour, *end_hour)}
|
||
current_month={props.current_date}
|
||
selected_date={Some(props.selected_date)}
|
||
today={props.today}
|
||
events={props.events.clone()}
|
||
on_day_select={None::<Callback<NaiveDate>>}
|
||
on_event_click={Callback::noop()}
|
||
user_info={props.user_info.clone()}
|
||
external_calendars={props.external_calendars.clone()}
|
||
/>
|
||
},
|
||
}
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
} |