fix: resolve compilation errors, fix lock_scroll path, and enhance DataTable attributes
All checks were successful
Build MIPS Binary / build (push) Successful in 5m27s
All checks were successful
Build MIPS Binary / build (push) Successful in 5m27s
This commit is contained in:
253
frontend/public/lock_scroll.js
Normal file
253
frontend/public/lock_scroll.js
Normal 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();
|
||||||
|
})();
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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> }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user