fix: resolve compilation errors, fix lock_scroll path, and enhance DataTable attributes
All checks were successful
Build MIPS Binary / build (push) Successful in 5m27s

This commit is contained in:
spinline
2026-02-12 00:15:17 +03:00
parent d09ecd21b7
commit bbb8e8dc98
5 changed files with 299 additions and 20 deletions

View File

@@ -0,0 +1,253 @@
/**
* Scroll Lock Utility
* Handles locking and unlocking scroll for both window and all scrollable containers
* Similar to react-remove-scroll but in vanilla JavaScript
*/
(() => {
// Prevent multiple initializations
if (window.ScrollLock) {
return;
}
class ScrollLock {
constructor() {
this.locked = false;
this.scrollableElements = [];
this.scrollPositions = new Map();
this.originalStyles = new Map();
this.fixedElements = [];
}
/**
* Find all scrollable elements in the DOM (optimized)
* Uses more targeted selectors instead of querying all elements
*/
findScrollableElements() {
const scrollables = [];
// More targeted query - only look for elements with overflow properties
const candidates = document.querySelectorAll(
'[style*="overflow"], [class*="overflow"], [class*="scroll"], main, aside, section, div',
);
// Batch all style reads first to minimize reflows
const elementsToCheck = [];
for (const el of candidates) {
// Skip the element itself or if it's inside these containers
const dataName = el.getAttribute("data-name");
const isExcludedElement =
dataName === "ScrollArea" ||
dataName === "CommandList" ||
dataName === "SelectContent" ||
dataName === "MultiSelectContent" ||
dataName === "DropdownMenuContent" ||
dataName === "ContextMenuContent";
if (
el !== document.body &&
el !== document.documentElement &&
!isExcludedElement &&
!el.closest('[data-name="ScrollArea"]') &&
!el.closest('[data-name="CommandList"]') &&
!el.closest('[data-name="SelectContent"]') &&
!el.closest('[data-name="MultiSelectContent"]') &&
!el.closest('[data-name="DropdownMenuContent"]') &&
!el.closest('[data-name="ContextMenuContent"]')
) {
elementsToCheck.push(el);
}
}
// Now batch read all computed styles and dimensions
elementsToCheck.forEach((el) => {
const style = window.getComputedStyle(el);
const hasOverflow =
style.overflow === "auto" ||
style.overflow === "scroll" ||
style.overflowY === "auto" ||
style.overflowY === "scroll";
// Only check scrollHeight if overflow is set
if (hasOverflow && el.scrollHeight > el.clientHeight) {
scrollables.push(el);
}
});
return scrollables;
}
/**
* Lock scrolling on all scrollable elements (optimized)
* Batches all DOM reads before DOM writes to prevent forced reflows
*/
lock() {
if (this.locked) return;
this.locked = true;
// Find all scrollable elements
this.scrollableElements = this.findScrollableElements();
// ===== BATCH 1: READ PHASE - Read all layout properties first =====
const windowScrollY = window.scrollY;
const scrollbarWidth = window.innerWidth - document.body.clientWidth;
// Store window scroll position
this.scrollPositions.set("window", windowScrollY);
// Store original body styles
this.originalStyles.set("body", {
position: document.body.style.position,
top: document.body.style.top,
width: document.body.style.width,
overflow: document.body.style.overflow,
paddingRight: document.body.style.paddingRight,
});
// Read all fixed-position elements and their padding (only if we have scrollbar)
if (scrollbarWidth > 0) {
// Use more targeted query for fixed elements
const fixedCandidates = document.querySelectorAll(
'[style*="fixed"], [class*="fixed"], header, nav, aside, [role="dialog"], [role="alertdialog"]',
);
this.fixedElements = Array.from(fixedCandidates).filter((el) => {
const style = window.getComputedStyle(el);
return (
style.position === "fixed" &&
!el.closest('[data-name="DropdownMenuContent"]') &&
!el.closest('[data-name="MultiSelectContent"]') &&
!el.closest('[data-name="ContextMenuContent"]')
);
});
// Batch read all padding values
this.fixedElements.forEach((el) => {
const computedStyle = window.getComputedStyle(el);
const currentPadding = Number.parseInt(computedStyle.paddingRight, 10) || 0;
this.originalStyles.set(el, {
paddingRight: el.style.paddingRight,
computedPadding: currentPadding,
});
});
}
// Read scrollable elements info
const scrollableInfo = this.scrollableElements.map((el) => {
const scrollTop = el.scrollTop;
const elementScrollbarWidth = el.offsetWidth - el.clientWidth;
const computedStyle = window.getComputedStyle(el);
const currentPadding = Number.parseInt(computedStyle.paddingRight, 10) || 0;
this.scrollPositions.set(el, scrollTop);
this.originalStyles.set(el, {
overflow: el.style.overflow,
overflowY: el.style.overflowY,
paddingRight: el.style.paddingRight,
});
return { el, elementScrollbarWidth, currentPadding };
});
// ===== BATCH 2: WRITE PHASE - Apply all styles at once =====
// Apply body lock
document.body.style.position = "fixed";
document.body.style.top = `-${windowScrollY}px`;
document.body.style.width = "100%";
document.body.style.overflow = "hidden";
if (scrollbarWidth > 0) {
document.body.style.paddingRight = `${scrollbarWidth}px`;
// Apply padding compensation to fixed elements
this.fixedElements.forEach((el) => {
const stored = this.originalStyles.get(el);
if (stored) {
el.style.paddingRight = `${stored.computedPadding + scrollbarWidth}px`;
}
});
}
// Lock all scrollable containers
scrollableInfo.forEach(({ el, elementScrollbarWidth, currentPadding }) => {
el.style.overflow = "hidden";
if (elementScrollbarWidth > 0) {
el.style.paddingRight = `${currentPadding + elementScrollbarWidth}px`;
}
});
}
/**
* Unlock scrolling on all elements (optimized)
* @param {number} delay - Delay in milliseconds before unlocking (for animations)
*/
unlock(delay = 0) {
if (!this.locked) return;
const performUnlock = () => {
// Restore body scroll
const bodyStyles = this.originalStyles.get("body");
if (bodyStyles) {
document.body.style.position = bodyStyles.position;
document.body.style.top = bodyStyles.top;
document.body.style.width = bodyStyles.width;
document.body.style.overflow = bodyStyles.overflow;
document.body.style.paddingRight = bodyStyles.paddingRight;
}
// Restore window scroll position
const windowScrollY = this.scrollPositions.get("window") || 0;
window.scrollTo(0, windowScrollY);
// Restore all scrollable containers
this.scrollableElements.forEach((el) => {
const originalStyles = this.originalStyles.get(el);
if (originalStyles) {
el.style.overflow = originalStyles.overflow;
el.style.overflowY = originalStyles.overflowY;
el.style.paddingRight = originalStyles.paddingRight;
}
// Restore scroll position
const scrollPosition = this.scrollPositions.get(el) || 0;
el.scrollTop = scrollPosition;
});
// Restore fixed-position elements padding
this.fixedElements.forEach((el) => {
const styles = this.originalStyles.get(el);
if (styles && styles.paddingRight !== undefined) {
el.style.paddingRight = styles.paddingRight;
}
});
// Clear storage
this.scrollableElements = [];
this.fixedElements = [];
this.scrollPositions.clear();
this.originalStyles.clear();
this.locked = false;
};
if (delay > 0) {
setTimeout(performUnlock, delay);
} else {
performUnlock();
}
}
/**
* Check if scrolling is currently locked
*/
isLocked() {
return this.locked;
}
}
// Export as singleton
window.ScrollLock = new ScrollLock();
})();

View File

@@ -204,7 +204,7 @@ pub fn TorrentTable() -> impl IntoView {
</div> </div>
</div> </div>
</div> </div>
} }.into_any()
} }
#[component] #[component]
@@ -269,7 +269,7 @@ fn TorrentRow(
{format_date(t.added_date)} {format_date(t.added_date)}
</TableCell> </TableCell>
</TableRow> </TableRow>
} }.into_any()
} }
} }
</Show> </Show>

View File

@@ -145,7 +145,7 @@ pub fn ContextMenuHoldAction(
{children()} {children()}
</span> </span>
</div> </div>
} }.into_any()
} }
#[derive(Clone)] #[derive(Clone)]
@@ -237,7 +237,7 @@ pub fn ContextMenuContent(
let target_id_for_script = ctx.target_id.clone(); let target_id_for_script = ctx.target_id.clone();
view! { view! {
<script src="/hooks/lock_scroll.js"></script> <script src="/lock_scroll.js"></script>
<div <div
data-name="ContextMenuContent" data-name="ContextMenuContent"
@@ -399,7 +399,7 @@ pub fn ContextMenuContent(
target_id_for_script, target_id_for_script,
)} )}
</script> </script>
} }.into_any()
} }
#[component] #[component]

View File

@@ -5,3 +5,4 @@ pub mod toast;
pub mod context_menu; pub mod context_menu;
pub mod theme_toggle; pub mod theme_toggle;
pub mod svg_icon; pub mod svg_icon;
pub mod table;

View File

@@ -1,19 +1,44 @@
use leptos::prelude::*; use leptos::prelude::*;
use leptos_ui::clx; use tw_merge::tw_merge;
mod components { #[component]
use super::*; pub fn TableWrapper(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
clx! {TableWrapper, div, "overflow-hidden rounded-md border"} let class = tw_merge!("overflow-hidden rounded-md border", class);
clx! {Table, table, "w-full max-w-7xl text-sm caption-bottom"} view! { <div class=class>{children()}</div> }
clx! {TableCaption, caption, "mt-4 text-sm text-muted-foreground"}
clx! {TableHeader, thead, "[&_tr]:border-b"}
clx! {TableRow, tr, "border-b transition-colors data-[state=selected]:bg-muted hover:bg-muted/50"}
clx! {TableHead, th, "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]"}
clx! {TableBody, tbody, "[&_tr:last-child]:border-0"}
clx! {TableCell, td, "p-4 align-middle [&:has([role=checkbox])]:pr-0 &:has([role=checkbox])]:pl-3"}
clx! {TableFooter, tfoot, "font-medium border border-t bg-muted/50 [&>tr]:last:border-b-0"}
clx! {CardContent, div, "pt-4"}
clx! {CardFooter, div, "mt-4", "flex items-center justify-end"}
} }
pub use components::*; #[component]
pub fn Table(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let class = tw_merge!("w-full text-sm caption-bottom", class);
view! { <table class=class>{children()}</table> }
}
#[component]
pub fn TableHeader(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let class = tw_merge!("[&_tr]:border-b", class);
view! { <thead class=class>{children()}</thead> }
}
#[component]
pub fn TableRow(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let class = tw_merge!("border-b transition-colors data-[state=selected]:bg-muted hover:bg-muted/50", class);
view! { <tr class=class>{children()}</tr> }
}
#[component]
pub fn TableHead(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let class = tw_merge!("h-10 px-2 text-left align-middle font-medium text-muted-foreground", class);
view! { <th class=class>{children()}</th> }
}
#[component]
pub fn TableBody(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let class = tw_merge!("[&_tr:last-child]:border-0", class);
view! { <tbody class=class>{children()}</tbody> }
}
#[component]
pub fn TableCell(children: Children, #[prop(optional, into)] class: String) -> impl IntoView {
let class = tw_merge!("p-2 align-middle", class);
view! { <td class=class>{children()}</td> }
}