Implement hybrid print preview with CSS-based approach

- Add hidden print-preview-copy div at app level for clean print isolation
- Use @media print rules to show only print copy (960x720) in landscape
- Auto-sync print copy with preview content on modal render via use_effect
- Copy CSS variables and data attributes for proper hour filtering
- Set explicit dimensions matching print-preview-paper content area
- Force landscape orientation with @page rule for better calendar printing

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-09-12 13:17:21 -04:00
parent a297d38276
commit 96585440d1
7 changed files with 105 additions and 607 deletions

View File

@@ -1702,6 +1702,9 @@ pub fn App() -> Html {
on_close={on_mobile_warning_close}
/>
</div>
// Hidden print copy that gets shown only during printing
<div id="print-preview-copy" class="print-preview-paper" style="display: none;"></div>
</BrowserRouter>
}
}

View File

@@ -101,147 +101,81 @@ pub fn PrintPreviewModal(props: &PrintPreviewModalProps) -> Html {
// 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);
let on_print = {
// Effect to update print copy whenever modal renders or content changes
{
let start_hour = *start_hour;
let end_hour = *end_hour;
let view_mode = props.view_mode.clone();
let base_unit_value = base_unit;
let pixels_per_hour_value = pixels_per_hour;
Callback::from(move |_: MouseEvent| {
use_effect(move || {
if let Some(window) = web_sys::window() {
if let Some(document) = window.document() {
// Find the print preview content element
if let Some(preview_content) = document.query_selector(".print-preview-content").ok().flatten() {
// Clone the content to print
if let Some(content_clone) = preview_content.clone_node_with_deep(true).ok() {
if let Some(body) = document.body() {
// Store original body content and styling
let original_body_html = body.inner_html();
let original_body_style = body.get_attribute("style").unwrap_or_default();
// Set CSS variables on document root for print
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-base-unit", &format!("{:.2}", base_unit_value));
let _ = style.set_property("--print-pixels-per-hour", &format!("{:.2}", pixels_per_hour_value));
let _ = style.set_property("--print-start-hour", &start_hour.to_string());
let _ = style.set_property("--print-end-hour", &end_hour.to_string());
}
}
// Clear body and add only the preview content
body.set_inner_html("");
let _ = body.set_attribute("style", "margin: 0; padding: 0; background: white;");
let _ = body.append_child(&content_clone);
// Set up restore content variables for later use
use std::rc::Rc;
let restore_body = Rc::new(body.clone());
let restore_document = Rc::new(document.clone());
let original_html = Rc::new(original_body_html.clone());
let original_style = Rc::new(original_body_style.clone());
let setup_window = Rc::new(window.clone());
// Add a small delay before printing to make the change visible
let print_window = window.clone();
let print_callback = Closure::wrap(Box::new(move || {
// Use visibility change event to detect when user returns to page
let restore_body = restore_body.clone();
let restore_document = restore_document.clone();
let original_html = original_html.clone();
let original_style = original_style.clone();
let setup_document1 = document.clone();
let setup_document2 = document.clone();
// Create restore function for both visibility and focus events
let restore_body_vis = restore_body.clone();
let restore_document_vis = restore_document.clone();
let original_html_vis = original_html.clone();
let original_style_vis = original_style.clone();
let setup_document_vis = setup_document1.clone();
let restore_body_focus = restore_body.clone();
let restore_document_focus = restore_document.clone();
let original_html_focus = original_html.clone();
let original_style_focus = original_style.clone();
// Try multiple approaches - visibility change
let visibility_callback = Closure::wrap(Box::new(move || {
if !setup_document_vis.hidden() {
// Restore original body content and style
restore_body_vis.set_inner_html(&original_html_vis);
if original_style_vis.is_empty() {
let _ = restore_body_vis.remove_attribute("style");
} else {
let _ = restore_body_vis.set_attribute("style", &original_style_vis);
}
// Clean up CSS variables
if let Some(document_element) = restore_document_vis.document_element() {
if let Some(html_element) = document_element.dyn_ref::<web_sys::HtmlElement>() {
let style = html_element.style();
let _ = style.remove_property("--print-base-unit");
let _ = style.remove_property("--print-pixels-per-hour");
let _ = style.remove_property("--print-start-hour");
let _ = style.remove_property("--print-end-hour");
}
}
}
}) as Box<dyn FnMut()>);
// And also try focus event as backup
let focus_callback = Closure::wrap(Box::new(move || {
// Restore original body content and style
restore_body_focus.set_inner_html(&original_html_focus);
if original_style_focus.is_empty() {
let _ = restore_body_focus.remove_attribute("style");
} else {
let _ = restore_body_focus.set_attribute("style", &original_style_focus);
}
// Clean up CSS variables
if let Some(document_element) = restore_document_focus.document_element() {
if let Some(html_element) = document_element.dyn_ref::<web_sys::HtmlElement>() {
let style = html_element.style();
let _ = style.remove_property("--print-base-unit");
let _ = style.remove_property("--print-pixels-per-hour");
let _ = style.remove_property("--print-start-hour");
let _ = style.remove_property("--print-end-hour");
}
}
}) as Box<dyn FnMut()>);
// Set up both listeners
let _ = setup_document2.add_event_listener_with_callback(
"visibilitychange",
visibility_callback.as_ref().unchecked_ref()
);
let _ = print_window.add_event_listener_with_callback(
"focus",
focus_callback.as_ref().unchecked_ref()
);
visibility_callback.forget();
focus_callback.forget();
// Now trigger the print dialog
let _ = print_window.print();
}) as Box<dyn FnMut()>);
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
print_callback.as_ref().unchecked_ref(),
100 // 100ms delay to see the content change
);
print_callback.forget();
}
// 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-base-unit", &format!("{:.2}", base_unit_value));
let _ = style.set_property("--print-pixels-per-hour", &format!("{:.2}", pixels_per_hour_value));
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);
// Copy the CSS variables and data attributes from the print-preview-paper
if let Some(preview_paper) = document.query_selector(".print-preview-paper").ok().flatten() {
if let Some(_paper_element) = preview_paper.dyn_ref::<web_sys::HtmlElement>() {
// Copy CSS custom properties (variables)
if let Some(print_copy_html) = print_copy.dyn_ref::<web_sys::HtmlElement>() {
let style = print_copy_html.style();
let _ = style.set_property("--print-start-hour", &start_hour.to_string());
let _ = style.set_property("--print-end-hour", &end_hour.to_string());
let _ = style.set_property("--print-base-unit", &format!("{:.2}", base_unit_value));
let _ = style.set_property("--print-pixels-per-hour", &format!("{:.2}", pixels_per_hour_value));
}
// 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());
}
}
}
}
}
};
// 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();
}
})
};