Compare commits
2 Commits
a9eb23ad76
...
d909f2125d
| Author | SHA1 | Date | |
|---|---|---|---|
| d909f2125d | |||
| 5d6c02001e |
@@ -690,7 +690,7 @@
|
|||||||
.mf-panel-main {
|
.mf-panel-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
min-width: 0; /* Important to allow the shrinking of flexbox */
|
min-width: 0; /* Important to allow the shrinking of flexbox */
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -829,169 +829,33 @@
|
|||||||
/* ********************************************* */
|
/* ********************************************* */
|
||||||
/* ************* Datagrid Component ************ */
|
/* ************* Datagrid Component ************ */
|
||||||
/* ********************************************* */
|
/* ********************************************* */
|
||||||
input:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dt2-drag-drop {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
top: 100%;
|
|
||||||
z-index: var(--datagrid-drag-drop-zindex);
|
|
||||||
width: 100px;
|
|
||||||
border: 1px solid var(--color-base-300);
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 10px;
|
|
||||||
box-shadow: 0 0 40px rgba(0, 0, 0, 0.3);
|
|
||||||
background: var(--color-base-100);
|
|
||||||
box-sizing: border-box;
|
|
||||||
overflow-x: auto;
|
|
||||||
pointer-events: none; /* Prevent interfering with mouse events */
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.dt2-main {
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dt2-sidebar {
|
|
||||||
opacity: 0; /* Default to invisible */
|
|
||||||
visibility: hidden; /* Prevent interaction when invisible */
|
|
||||||
transition: opacity 0.3s ease, visibility 0s linear 0.3s; /* Delay visibility removal */
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
width: 75%;
|
|
||||||
max-height: 710px;
|
|
||||||
overflow-y: auto;
|
|
||||||
background-color: var(--color-base-100);
|
|
||||||
z-index: var(--datagrid-sidebar-zindex);
|
|
||||||
box-shadow: -5px 0 15px rgba(0, 0, 0, 0.5); /* Stronger shadow */
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dt2-sidebar.active {
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dt2-container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dt2-scrollbars {
|
|
||||||
position: absolute;
|
|
||||||
top: 24px;
|
|
||||||
bottom: 0px;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
pointer-events: none; /* Ensures parents don't intercept pointer events */
|
|
||||||
z-index: var(--datagrid-scrollbars-zindex);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar Wrappers common attributes*/
|
|
||||||
.dt2-scrollbars-vertical-wrapper,
|
|
||||||
.dt2-scrollbars-horizontal-wrapper {
|
|
||||||
position: absolute;
|
|
||||||
background-color: var(--color-base-200);
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.2s ease-in-out; /* Smooth fade in/out */
|
|
||||||
pointer-events: auto; /* Allow interaction */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar Wrappers */
|
|
||||||
.dt2-scrollbars-vertical-wrapper {
|
|
||||||
left: auto;
|
|
||||||
right: 3px;
|
|
||||||
top: 3px;
|
|
||||||
bottom: 3px;
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dt2-scrollbars-horizontal-wrapper {
|
|
||||||
left: 3px;
|
|
||||||
right: 3px;
|
|
||||||
top: auto;
|
|
||||||
bottom: -12px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbars */
|
|
||||||
.dt2-scrollbars-vertical,
|
|
||||||
.dt2-scrollbars-horizontal {
|
|
||||||
background-color: var(--color-base-300);
|
|
||||||
border-radius: 3px;
|
|
||||||
pointer-events: auto; /* Allow interaction with the scrollbar */
|
|
||||||
cursor: pointer;
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 3px; /* Rounded corners */
|
|
||||||
pointer-events: auto; /* Enable interaction */
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Vertical Scrollbar */
|
|
||||||
.dt2-scrollbars-vertical {
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: auto;
|
|
||||||
bottom: auto;
|
|
||||||
width: 100%; /* Fits inside its wrapper */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Horizontal Scrollbar */
|
|
||||||
.dt2-scrollbars-horizontal {
|
|
||||||
left: auto;
|
|
||||||
right: auto;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
height: 100%; /* Fits inside its wrapper */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar hover effects */
|
|
||||||
.dt2-scrollbars-vertical:hover,
|
|
||||||
.dt2-scrollbars-horizontal:hover,
|
|
||||||
.dt2-scrollbars-vertical.dt2-dragging,
|
|
||||||
.dt2-scrollbars-horizontal.dt2-dragging {
|
|
||||||
background-color: var(--color-base-content);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dt2-table {
|
|
||||||
--color-border: color-mix(in oklab, var(--color-base-content) 20%, #0000);
|
|
||||||
--color-resize: color-mix(in oklab, var(--color-base-content) 50%, #0000);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dt2-table:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/* Header and Footer */
|
||||||
.dt2-header,
|
.dt2-header,
|
||||||
.dt2-footer {
|
.dt2-footer {
|
||||||
background-color: var(--color-base-200);
|
background-color: var(--color-base-200);
|
||||||
border-radius: 10px 10px 0 0;
|
border-radius: 10px 10px 0 0;
|
||||||
min-width: max-content;
|
min-width: max-content; /* Content width propagates to scrollable parent */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Body */
|
||||||
.dt2-body {
|
.dt2-body {
|
||||||
overflow: hidden; /* You can change this to auto if horizontal scrolling is required */
|
overflow: hidden;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
min-width: max-content;
|
min-width: max-content; /* Content width propagates to scrollable parent */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Row */
|
||||||
.dt2-row {
|
.dt2-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 22px;
|
height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cell */
|
||||||
.dt2-cell {
|
.dt2-cell {
|
||||||
|
--color-border: color-mix(in oklab, var(--color-base-content) 20%, #0000);
|
||||||
|
--color-resize: color-mix(in oklab, var(--color-base-content) 50%, #0000);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
@@ -1002,11 +866,12 @@ input:focus {
|
|||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
box-sizing: border-box; /* to include the borders in the computations */
|
box-sizing: border-box;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Cell content types */
|
||||||
.dt2-cell-content-text {
|
.dt2-cell-content-text {
|
||||||
text-align: inherit;
|
text-align: inherit;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -1016,8 +881,8 @@ input:focus {
|
|||||||
.dt2-cell-content-checkbox {
|
.dt2-cell-content-checkbox {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center; /* Horizontally center the icon */
|
justify-content: center;
|
||||||
align-items: center; /* Vertically center the icon */
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dt2-cell-content-number {
|
.dt2-cell-content-number {
|
||||||
@@ -1026,36 +891,12 @@ input:focus {
|
|||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Footer cell */
|
||||||
.dt2-footer-cell {
|
.dt2-footer-cell {
|
||||||
cursor: pointer
|
cursor: pointer;
|
||||||
}
|
|
||||||
|
|
||||||
.dt2-footer-menu {
|
|
||||||
position: absolute;
|
|
||||||
display: None;
|
|
||||||
z-index: var(--datagrid-menu-zindex);
|
|
||||||
border: 1px solid oklch(var(--b3));
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 80px;
|
|
||||||
background-color: var(--color-base-100); /* Add background color */
|
|
||||||
opacity: 1; /* Ensure full opacity */
|
|
||||||
}
|
|
||||||
|
|
||||||
.dt2-footer-menu.show {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dt2-footer-menu-item {
|
|
||||||
padding: 0 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--color-base-100); /* Add background color */
|
|
||||||
}
|
|
||||||
|
|
||||||
.dt2-footer-menu-item:hover {
|
|
||||||
background: color-mix(in oklab, var(--color-base-100, var(--color-base-200)), #000 7%);
|
|
||||||
cursor: pointer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Resize handle */
|
||||||
.dt2-resize-handle {
|
.dt2-resize-handle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -1066,100 +907,149 @@ input:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dt2-resize-handle::after {
|
.dt2-resize-handle::after {
|
||||||
content: ''; /* This is required */
|
content: '';
|
||||||
position: absolute; /* Position as needed */
|
position: absolute;
|
||||||
z-index: var(--datagrid-resize-zindex);
|
z-index: var(--datagrid-resize-zindex);
|
||||||
display: block; /* Makes it a block element */
|
display: block;
|
||||||
width: 3px;
|
width: 3px;
|
||||||
height: 60%;
|
height: 60%;
|
||||||
top: calc(50% - 60% * 0.5);
|
top: calc(50% - 60% * 0.5);
|
||||||
background-color: var(--color-resize);
|
background-color: var(--color-resize);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dt2-header-hidden {
|
/* Hidden column */
|
||||||
width: 5px;
|
|
||||||
background: var(--color-neutral-content);
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dt2-col-hidden {
|
.dt2-col-hidden {
|
||||||
width: 5px;
|
width: 5px;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Highlight */
|
||||||
.dt2-highlight-1 {
|
.dt2-highlight-1 {
|
||||||
color: var(--color-accent);
|
color: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dt2-item-handle {
|
/* *********************************************** */
|
||||||
background-image: radial-gradient(var(--color-primary-content) 40%, transparent 0);
|
/* ******** DataGrid Fixed Header/Footer ******** */
|
||||||
background-repeat: repeat;
|
/* *********************************************** */
|
||||||
background-size: 4px 4px;
|
|
||||||
cursor: grab;
|
/*
|
||||||
display: inline-block;
|
* DataGrid with CSS Grid + Custom Scrollbars
|
||||||
height: 16px;
|
* - Wrapper takes 100% of parent height
|
||||||
margin: auto;
|
* - Table uses Grid: header (auto) + body (1fr) + footer (auto)
|
||||||
|
* - Native scrollbars hidden, custom scrollbars overlaid
|
||||||
|
* - Vertical scrollbar: right side of entire table
|
||||||
|
* - Horizontal scrollbar: bottom, under footer
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Main wrapper - takes full parent height, contains table + scrollbars */
|
||||||
|
.dt2-table-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 1px;
|
|
||||||
width: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* **************************************************************************** */
|
/* Table with Grid layout - horizontal scroll enabled, scrollbars hidden */
|
||||||
/* COLUMNS SETTINGS */
|
.dt2-table {
|
||||||
/* **************************************************************************** */
|
height: 100%;
|
||||||
|
|
||||||
.dt2-cs-header {
|
|
||||||
background-color: var(--color-base-200);
|
|
||||||
min-width: max-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dt2-cs-columns {
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 20px 1fr 0.5fr 0.5fr 0.5fr 0.5fr;
|
grid-template-rows: auto 1fr auto; /* header, body, footer */
|
||||||
|
overflow-x: auto; /* Enable horizontal scroll */
|
||||||
|
overflow-y: hidden; /* No vertical scroll on table */
|
||||||
|
scrollbar-width: none; /* Firefox: hide scrollbar */
|
||||||
|
-ms-overflow-style: none; /* IE/Edge: hide scrollbar */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dt2-cs-body input {
|
/* Chrome/Safari: hide scrollbar */
|
||||||
outline: none;
|
.dt2-table::-webkit-scrollbar {
|
||||||
border-color: transparent;
|
display: none;
|
||||||
box-shadow: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dt2-cs-body input[type="checkbox"],
|
/* Header - no scroll, takes natural height */
|
||||||
.dt2-cs-body input.checkbox {
|
.dt2-header-container {
|
||||||
outline: initial;
|
overflow: hidden;
|
||||||
border-color: var(--color-border);
|
min-width: max-content; /* Force table to be as wide as content */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Body - scrollable vertically via JS, scrollbars hidden */
|
||||||
.dt2-cs-cell {
|
.dt2-body-container {
|
||||||
padding: 0 6px 0 6px;
|
overflow: hidden; /* Scrollbars hidden, scroll via JS */
|
||||||
margin: auto;
|
min-height: 0; /* Important for Grid to allow shrinking */
|
||||||
|
min-width: max-content; /* Force table to be as wide as content */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dt2-cs-checkbox-cell {
|
/* Footer - no scroll, takes natural height */
|
||||||
margin: auto;
|
.dt2-footer-container {
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: max-content; /* Force table to be as wide as content */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dt2-cs-number-cell {
|
/* Custom scrollbars container - overlaid on table */
|
||||||
padding: 0 6px 0 6px;
|
.dt2-scrollbars {
|
||||||
text-align: right;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
pointer-events: none; /* Let clicks pass through */
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dt2-cs-select-cell {
|
/* Scrollbar wrappers - clickable/draggable */
|
||||||
padding: 0 6px;
|
.dt2-scrollbars-vertical-wrapper,
|
||||||
margin: 3px 0;
|
.dt2-scrollbars-horizontal-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--color-base-200);
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.2s ease-in-out;
|
||||||
|
pointer-events: auto; /* Enable interaction */
|
||||||
}
|
}
|
||||||
|
|
||||||
.dt2-cs-body input:hover {
|
/* Vertical scrollbar wrapper - right side, full table height */
|
||||||
border: 1px solid #ccc; /* Provide a subtle border on focus */
|
.dt2-scrollbars-vertical-wrapper {
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Horizontal scrollbar wrapper - bottom, full width minus vertical scrollbar */
|
||||||
.dt2-views-container-select {
|
.dt2-scrollbars-horizontal-wrapper {
|
||||||
width: 170px;
|
left: 0;
|
||||||
|
right: 8px; /* Leave space for vertical scrollbar */
|
||||||
|
bottom: 0;
|
||||||
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dt2-views-container-create {
|
/* Scrollbar thumbs */
|
||||||
width: 300px;
|
.dt2-scrollbars-vertical,
|
||||||
|
.dt2-scrollbars-horizontal {
|
||||||
|
background-color: color-mix(in oklab, var(--color-base-content) 20%, #0000);
|
||||||
|
border-radius: 3px;
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical scrollbar thumb */
|
||||||
|
.dt2-scrollbars-vertical {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal scrollbar thumb */
|
||||||
|
.dt2-scrollbars-horizontal {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover and dragging states */
|
||||||
|
.dt2-scrollbars-vertical:hover,
|
||||||
|
.dt2-scrollbars-horizontal:hover,
|
||||||
|
.dt2-scrollbars-vertical.dt2-dragging,
|
||||||
|
.dt2-scrollbars-horizontal.dt2-dragging {
|
||||||
|
background-color: color-mix(in oklab, var(--color-base-content) 30%, #0000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1466,3 +1466,280 @@ function updateTabs(controllerId) {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize DataGrid with CSS Grid layout + Custom Scrollbars
|
||||||
|
*
|
||||||
|
* Adapted from previous custom scrollbar implementation to work with CSS Grid.
|
||||||
|
* - Grid handles layout (no height calculations needed)
|
||||||
|
* - Custom scrollbars for visual consistency and positioning control
|
||||||
|
* - Vertical scroll: on body container (.dt2-body-container)
|
||||||
|
* - Horizontal scroll: on table (.dt2-table) to scroll header, body, footer together
|
||||||
|
*
|
||||||
|
* @param {string} gridId - The ID of the DataGrid instance
|
||||||
|
*/
|
||||||
|
function initDataGridScrollbars(gridId) {
|
||||||
|
console.debug("initDataGridScrollbars on element " + gridId);
|
||||||
|
|
||||||
|
const wrapper = document.getElementById(`tw_${gridId}`);
|
||||||
|
|
||||||
|
if (!wrapper) {
|
||||||
|
console.error(`DataGrid wrapper "tw_${gridId}" not found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const verticalScrollbar = wrapper.querySelector(".dt2-scrollbars-vertical");
|
||||||
|
const verticalWrapper = wrapper.querySelector(".dt2-scrollbars-vertical-wrapper");
|
||||||
|
const horizontalScrollbar = wrapper.querySelector(".dt2-scrollbars-horizontal");
|
||||||
|
const horizontalWrapper = wrapper.querySelector(".dt2-scrollbars-horizontal-wrapper");
|
||||||
|
const bodyContainer = wrapper.querySelector(".dt2-body-container");
|
||||||
|
const table = wrapper.querySelector(".dt2-table");
|
||||||
|
|
||||||
|
if (!verticalScrollbar || !verticalWrapper || !horizontalScrollbar || !horizontalWrapper || !bodyContainer || !table) {
|
||||||
|
console.error("Essential scrollbar or content elements are missing in the datagrid.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OPTIMIZATION: Cache element references to avoid repeated querySelector calls
|
||||||
|
const header = table.querySelector(".dt2-header");
|
||||||
|
const body = table.querySelector(".dt2-body");
|
||||||
|
|
||||||
|
// OPTIMIZATION: RequestAnimationFrame flags to throttle visual updates
|
||||||
|
let rafScheduledVertical = false;
|
||||||
|
let rafScheduledHorizontal = false;
|
||||||
|
|
||||||
|
const computeScrollbarVisibility = () => {
|
||||||
|
// Vertical: check if body content exceeds body container height
|
||||||
|
const isVerticalRequired = bodyContainer.scrollHeight > bodyContainer.clientHeight;
|
||||||
|
|
||||||
|
// Horizontal: check if content width exceeds table width (use cached references)
|
||||||
|
const contentWidth = Math.max(
|
||||||
|
header ? header.scrollWidth : 0,
|
||||||
|
body ? body.scrollWidth : 0
|
||||||
|
);
|
||||||
|
const isHorizontalRequired = contentWidth > table.clientWidth;
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
verticalWrapper.style.display = isVerticalRequired ? "block" : "none";
|
||||||
|
horizontalWrapper.style.display = isHorizontalRequired ? "block" : "none";
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const computeScrollbarSize = () => {
|
||||||
|
// Vertical scrollbar height
|
||||||
|
const visibleHeight = bodyContainer.clientHeight;
|
||||||
|
const totalHeight = bodyContainer.scrollHeight;
|
||||||
|
const wrapperHeight = verticalWrapper.offsetHeight;
|
||||||
|
|
||||||
|
let scrollbarHeight = 0;
|
||||||
|
if (totalHeight > 0) {
|
||||||
|
scrollbarHeight = (visibleHeight / totalHeight) * wrapperHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal scrollbar width (use cached references)
|
||||||
|
const visibleWidth = table.clientWidth;
|
||||||
|
const totalWidth = Math.max(
|
||||||
|
header ? header.scrollWidth : 0,
|
||||||
|
body ? body.scrollWidth : 0
|
||||||
|
);
|
||||||
|
const wrapperWidth = horizontalWrapper.offsetWidth;
|
||||||
|
|
||||||
|
let scrollbarWidth = 0;
|
||||||
|
if (totalWidth > 0) {
|
||||||
|
scrollbarWidth = (visibleWidth / totalWidth) * wrapperWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
verticalScrollbar.style.height = `${scrollbarHeight}px`;
|
||||||
|
horizontalScrollbar.style.width = `${scrollbarWidth}px`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateVerticalScrollbarPosition = () => {
|
||||||
|
const maxScrollTop = bodyContainer.scrollHeight - bodyContainer.clientHeight;
|
||||||
|
const wrapperHeight = verticalWrapper.offsetHeight;
|
||||||
|
|
||||||
|
if (maxScrollTop > 0) {
|
||||||
|
const scrollRatio = wrapperHeight / bodyContainer.scrollHeight;
|
||||||
|
verticalScrollbar.style.top = `${bodyContainer.scrollTop * scrollRatio}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateHorizontalScrollbarPosition = () => {
|
||||||
|
// Use cached references
|
||||||
|
const totalWidth = Math.max(
|
||||||
|
header ? header.scrollWidth : 0,
|
||||||
|
body ? body.scrollWidth : 0
|
||||||
|
);
|
||||||
|
const maxScrollLeft = totalWidth - table.clientWidth;
|
||||||
|
const wrapperWidth = horizontalWrapper.offsetWidth;
|
||||||
|
|
||||||
|
if (maxScrollLeft > 0 && totalWidth > 0) {
|
||||||
|
const scrollRatio = wrapperWidth / totalWidth;
|
||||||
|
horizontalScrollbar.style.left = `${table.scrollLeft * scrollRatio}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Drag management for vertical scrollbar
|
||||||
|
let isDraggingVertical = false;
|
||||||
|
let startYVertical = 0;
|
||||||
|
let pendingVerticalScroll = null;
|
||||||
|
|
||||||
|
verticalScrollbar.addEventListener("mousedown", (e) => {
|
||||||
|
isDraggingVertical = true;
|
||||||
|
startYVertical = e.clientY;
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
verticalScrollbar.classList.add("dt2-dragging");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", (e) => {
|
||||||
|
if (isDraggingVertical) {
|
||||||
|
const deltaY = e.clientY - startYVertical;
|
||||||
|
startYVertical = e.clientY;
|
||||||
|
|
||||||
|
// OPTIMIZATION: Store the scroll delta, update visual in RAF
|
||||||
|
if (pendingVerticalScroll === null) {
|
||||||
|
pendingVerticalScroll = 0;
|
||||||
|
}
|
||||||
|
pendingVerticalScroll += deltaY;
|
||||||
|
|
||||||
|
if (!rafScheduledVertical) {
|
||||||
|
rafScheduledVertical = true;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
rafScheduledVertical = false;
|
||||||
|
|
||||||
|
const wrapperHeight = verticalWrapper.offsetHeight;
|
||||||
|
const scrollbarHeight = verticalScrollbar.offsetHeight;
|
||||||
|
const maxScrollTop = bodyContainer.scrollHeight - bodyContainer.clientHeight;
|
||||||
|
const scrollRatio = maxScrollTop / (wrapperHeight - scrollbarHeight);
|
||||||
|
|
||||||
|
let newTop = parseFloat(verticalScrollbar.style.top || "0") + pendingVerticalScroll;
|
||||||
|
newTop = Math.max(0, Math.min(newTop, wrapperHeight - scrollbarHeight));
|
||||||
|
|
||||||
|
verticalScrollbar.style.top = `${newTop}px`;
|
||||||
|
bodyContainer.scrollTop = newTop * scrollRatio;
|
||||||
|
|
||||||
|
pendingVerticalScroll = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("mouseup", () => {
|
||||||
|
if (isDraggingVertical) {
|
||||||
|
isDraggingVertical = false;
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
verticalScrollbar.classList.remove("dt2-dragging");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Drag management for horizontal scrollbar
|
||||||
|
let isDraggingHorizontal = false;
|
||||||
|
let startXHorizontal = 0;
|
||||||
|
let pendingHorizontalScroll = null;
|
||||||
|
|
||||||
|
horizontalScrollbar.addEventListener("mousedown", (e) => {
|
||||||
|
isDraggingHorizontal = true;
|
||||||
|
startXHorizontal = e.clientX;
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
horizontalScrollbar.classList.add("dt2-dragging");
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("mousemove", (e) => {
|
||||||
|
if (isDraggingHorizontal) {
|
||||||
|
const deltaX = e.clientX - startXHorizontal;
|
||||||
|
startXHorizontal = e.clientX;
|
||||||
|
|
||||||
|
// OPTIMIZATION: Store the scroll delta, update visual in RAF
|
||||||
|
if (pendingHorizontalScroll === null) {
|
||||||
|
pendingHorizontalScroll = 0;
|
||||||
|
}
|
||||||
|
pendingHorizontalScroll += deltaX;
|
||||||
|
|
||||||
|
if (!rafScheduledHorizontal) {
|
||||||
|
rafScheduledHorizontal = true;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
rafScheduledHorizontal = false;
|
||||||
|
|
||||||
|
const wrapperWidth = horizontalWrapper.offsetWidth;
|
||||||
|
const scrollbarWidth = horizontalScrollbar.offsetWidth;
|
||||||
|
|
||||||
|
// Use cached references
|
||||||
|
const totalWidth = Math.max(
|
||||||
|
header ? header.scrollWidth : 0,
|
||||||
|
body ? body.scrollWidth : 0
|
||||||
|
);
|
||||||
|
const maxScrollLeft = totalWidth - table.clientWidth;
|
||||||
|
const scrollRatio = maxScrollLeft / (wrapperWidth - scrollbarWidth);
|
||||||
|
|
||||||
|
let newLeft = parseFloat(horizontalScrollbar.style.left || "0") + pendingHorizontalScroll;
|
||||||
|
newLeft = Math.max(0, Math.min(newLeft, wrapperWidth - scrollbarWidth));
|
||||||
|
|
||||||
|
horizontalScrollbar.style.left = `${newLeft}px`;
|
||||||
|
table.scrollLeft = newLeft * scrollRatio;
|
||||||
|
|
||||||
|
pendingHorizontalScroll = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("mouseup", () => {
|
||||||
|
if (isDraggingHorizontal) {
|
||||||
|
isDraggingHorizontal = false;
|
||||||
|
document.body.style.userSelect = "";
|
||||||
|
horizontalScrollbar.classList.remove("dt2-dragging");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wheel scrolling - OPTIMIZED with RAF throttling
|
||||||
|
let rafScheduledWheel = false;
|
||||||
|
let pendingWheelDeltaX = 0;
|
||||||
|
let pendingWheelDeltaY = 0;
|
||||||
|
|
||||||
|
const handleWheelScrolling = (event) => {
|
||||||
|
// Accumulate wheel deltas
|
||||||
|
pendingWheelDeltaX += event.deltaX;
|
||||||
|
pendingWheelDeltaY += event.deltaY;
|
||||||
|
|
||||||
|
// Schedule update in next animation frame (throttle)
|
||||||
|
if (!rafScheduledWheel) {
|
||||||
|
rafScheduledWheel = true;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
rafScheduledWheel = false;
|
||||||
|
|
||||||
|
// Apply accumulated scroll
|
||||||
|
bodyContainer.scrollTop += pendingWheelDeltaY;
|
||||||
|
table.scrollLeft += pendingWheelDeltaX;
|
||||||
|
|
||||||
|
// Update scrollbar positions
|
||||||
|
updateVerticalScrollbarPosition();
|
||||||
|
updateHorizontalScrollbarPosition();
|
||||||
|
|
||||||
|
// Reset pending deltas
|
||||||
|
pendingWheelDeltaX = 0;
|
||||||
|
pendingWheelDeltaY = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
wrapper.addEventListener("wheel", handleWheelScrolling, { passive: false });
|
||||||
|
|
||||||
|
// Initialize scrollbars
|
||||||
|
computeScrollbarVisibility();
|
||||||
|
computeScrollbarSize();
|
||||||
|
updateVerticalScrollbarPosition();
|
||||||
|
updateHorizontalScrollbarPosition();
|
||||||
|
|
||||||
|
// Recompute on window resize
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
computeScrollbarVisibility();
|
||||||
|
computeScrollbarSize();
|
||||||
|
updateVerticalScrollbarPosition();
|
||||||
|
updateHorizontalScrollbarPosition();
|
||||||
|
});
|
||||||
|
|
||||||
|
console.info(`DataGrid "${gridId}" initialized with custom scrollbars`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from myfasthtml.controls.BaseCommands import BaseCommands
|
|||||||
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
|
from myfasthtml.controls.datagrid_objects import DataGridColumnState, DataGridRowState, \
|
||||||
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
|
DatagridSelectionState, DataGridHeaderFooterConf, DatagridEditionState
|
||||||
from myfasthtml.controls.helpers import mk
|
from myfasthtml.controls.helpers import mk
|
||||||
|
from myfasthtml.core.commands import Command
|
||||||
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
|
from myfasthtml.core.constants import ColumnType, ROW_INDEX_ID, FooterAggregation, DATAGRID_PAGE_SIZE, FILTER_INPUT_CID
|
||||||
from myfasthtml.core.dbmanager import DbObject
|
from myfasthtml.core.dbmanager import DbObject
|
||||||
from myfasthtml.core.instances import MultipleInstance
|
from myfasthtml.core.instances import MultipleInstance
|
||||||
@@ -41,7 +42,7 @@ class DatagridState(DbObject):
|
|||||||
with self.initializing():
|
with self.initializing():
|
||||||
self.sidebar_visible: bool = False
|
self.sidebar_visible: bool = False
|
||||||
self.selected_view: str = None
|
self.selected_view: str = None
|
||||||
self.row_index: bool = False
|
self.row_index: bool = True
|
||||||
self.columns: list[DataGridColumnState] = []
|
self.columns: list[DataGridColumnState] = []
|
||||||
self.rows: list[DataGridRowState] = [] # only the rows that have a specific state
|
self.rows: list[DataGridRowState] = [] # only the rows that have a specific state
|
||||||
self.headers: list[DataGridHeaderFooterConf] = []
|
self.headers: list[DataGridHeaderFooterConf] = []
|
||||||
@@ -70,7 +71,17 @@ class DatagridSettings(DbObject):
|
|||||||
|
|
||||||
|
|
||||||
class Commands(BaseCommands):
|
class Commands(BaseCommands):
|
||||||
pass
|
def get_page(self, page_index: int):
|
||||||
|
return Command("GetPage",
|
||||||
|
"Get a specific page of data",
|
||||||
|
self._owner,
|
||||||
|
self._owner.mk_body_content_page,
|
||||||
|
kwargs={"page_index": page_index}
|
||||||
|
).htmx(target=f"#tb_{self._id}",
|
||||||
|
swap="beforeend",
|
||||||
|
trigger=f"intersect root:#tb_{self._id} once",
|
||||||
|
auto_swap_oob=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DataGrid(MultipleInstance):
|
class DataGrid(MultipleInstance):
|
||||||
@@ -253,7 +264,6 @@ class DataGrid(MultipleInstance):
|
|||||||
else:
|
else:
|
||||||
last_row = None
|
last_row = None
|
||||||
|
|
||||||
# OPTIMIZATION: Extract filter keyword once (was being checked 10,000 times)
|
|
||||||
filter_keyword = self._state.filtered.get(FILTER_INPUT_CID)
|
filter_keyword = self._state.filtered.get(FILTER_INPUT_CID)
|
||||||
filter_keyword_lower = filter_keyword.lower() if filter_keyword else None
|
filter_keyword_lower = filter_keyword.lower() if filter_keyword else None
|
||||||
|
|
||||||
@@ -262,7 +272,8 @@ class DataGrid(MultipleInstance):
|
|||||||
for col_pos, col_def in enumerate(self._state.columns)],
|
for col_pos, col_def in enumerate(self._state.columns)],
|
||||||
cls="dt2-row",
|
cls="dt2-row",
|
||||||
data_row=f"{row_index}",
|
data_row=f"{row_index}",
|
||||||
_id=f"tr_{self._id}-{row_index}",
|
id=f"tr_{self._id}-{row_index}",
|
||||||
|
**self.commands.get_page(page_index + 1).get_htmx_params(escaped=True) if row_index == last_row else {}
|
||||||
) for row_index in df.index[start:end]]
|
) for row_index in df.index[start:end]]
|
||||||
|
|
||||||
return rows
|
return rows
|
||||||
@@ -270,11 +281,11 @@ class DataGrid(MultipleInstance):
|
|||||||
def mk_body(self):
|
def mk_body(self):
|
||||||
return Div(
|
return Div(
|
||||||
*self.mk_body_content_page(0),
|
*self.mk_body_content_page(0),
|
||||||
cls="dt2-body",
|
cls="dt2-body"
|
||||||
id=f"tb_{self._id}",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def mk_footers(self):
|
def mk_footers(self):
|
||||||
|
return self.mk_headers()
|
||||||
return Div(
|
return Div(
|
||||||
*[Div(
|
*[Div(
|
||||||
*[self.mk_aggregation_cell(col_def, row_index, footer) for col_def in self._state.columns],
|
*[self.mk_aggregation_cell(col_def, row_index, footer) for col_def in self._state.columns],
|
||||||
@@ -288,9 +299,43 @@ class DataGrid(MultipleInstance):
|
|||||||
|
|
||||||
def mk_table(self):
|
def mk_table(self):
|
||||||
return Div(
|
return Div(
|
||||||
|
# Grid table with header, body, footer
|
||||||
|
Div(
|
||||||
|
# Header container - no scroll
|
||||||
|
Div(
|
||||||
self.mk_headers(),
|
self.mk_headers(),
|
||||||
|
cls="dt2-header-container"
|
||||||
|
),
|
||||||
|
# Body container - scroll via JS, scrollbars hidden
|
||||||
|
Div(
|
||||||
self.mk_body(),
|
self.mk_body(),
|
||||||
self.mk_footers()
|
cls="dt2-body-container",
|
||||||
|
id=f"tb_{self._id}"
|
||||||
|
),
|
||||||
|
# Footer container - no scroll
|
||||||
|
Div(
|
||||||
|
self.mk_footers(),
|
||||||
|
cls="dt2-footer-container"
|
||||||
|
),
|
||||||
|
cls="dt2-table",
|
||||||
|
id=f"t_{self._id}"
|
||||||
|
),
|
||||||
|
# Custom scrollbars overlaid
|
||||||
|
Div(
|
||||||
|
# Vertical scrollbar wrapper (right side)
|
||||||
|
Div(
|
||||||
|
Div(cls="dt2-scrollbars-vertical"),
|
||||||
|
cls="dt2-scrollbars-vertical-wrapper"
|
||||||
|
),
|
||||||
|
# Horizontal scrollbar wrapper (bottom)
|
||||||
|
Div(
|
||||||
|
Div(cls="dt2-scrollbars-horizontal"),
|
||||||
|
cls="dt2-scrollbars-horizontal-wrapper"
|
||||||
|
),
|
||||||
|
cls="dt2-scrollbars"
|
||||||
|
),
|
||||||
|
cls="dt2-table-wrapper",
|
||||||
|
id=f"tw_{self._id}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def mk_aggregation_cell(self, col_def, row_index: int, footer_conf, oob=False):
|
def mk_aggregation_cell(self, col_def, row_index: int, footer_conf, oob=False):
|
||||||
@@ -360,11 +405,10 @@ class DataGrid(MultipleInstance):
|
|||||||
return Div("No data to display !")
|
return Div("No data to display !")
|
||||||
|
|
||||||
return Div(
|
return Div(
|
||||||
Div(
|
|
||||||
self.mk_table(),
|
self.mk_table(),
|
||||||
# Script(f"bindDatagrid('{self._id}', false);"),
|
Script(f"initDataGridScrollbars('{self._id}');"),
|
||||||
),
|
id=self._id,
|
||||||
id=self._id
|
style="height: 100%;"
|
||||||
)
|
)
|
||||||
|
|
||||||
def __ft__(self):
|
def __ft__(self):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from fasthtml.components import Div
|
from fasthtml.components import Div
|
||||||
@@ -90,7 +91,7 @@ class DataGridsManager(MultipleInstance):
|
|||||||
|
|
||||||
def open_from_excel(self, tab_id, file_upload: FileUpload):
|
def open_from_excel(self, tab_id, file_upload: FileUpload):
|
||||||
excel_content = file_upload.get_content()
|
excel_content = file_upload.get_content()
|
||||||
df = pd.read_excel(excel_content, file_upload.get_sheet_name())
|
df = pd.read_excel(BytesIO(excel_content), file_upload.get_sheet_name())
|
||||||
dg = DataGrid(self._tabs_manager, save_state=True)
|
dg = DataGrid(self._tabs_manager, save_state=True)
|
||||||
dg.init_from_dataframe(df)
|
dg.init_from_dataframe(df)
|
||||||
document = DocumentDefinition(
|
document = DocumentDefinition(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from myfasthtml.core.constants import ColumnType, DEFAULT_COLUMN_WIDTH, ViewType
|
from myfasthtml.core.constants import ColumnType, DATAGRID_DEFAULT_COLUMN_WIDTH, ViewType
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -17,7 +17,7 @@ class DataGridColumnState:
|
|||||||
type: ColumnType = ColumnType.Text
|
type: ColumnType = ColumnType.Text
|
||||||
visible: bool = True
|
visible: bool = True
|
||||||
usable: bool = True
|
usable: bool = True
|
||||||
width: int = DEFAULT_COLUMN_WIDTH
|
width: int = DATAGRID_DEFAULT_COLUMN_WIDTH
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import html
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -11,6 +12,7 @@ from myfasthtml.core.utils import flatten
|
|||||||
|
|
||||||
logger = logging.getLogger("Commands")
|
logger = logging.getLogger("Commands")
|
||||||
|
|
||||||
|
AUTO_SWAP_OOB = "__auto_swap_oob__"
|
||||||
|
|
||||||
class Command:
|
class Command:
|
||||||
"""
|
"""
|
||||||
@@ -71,7 +73,7 @@ class Command:
|
|||||||
self.callback = callback
|
self.callback = callback
|
||||||
self.default_args = args or []
|
self.default_args = args or []
|
||||||
self.default_kwargs = kwargs or {}
|
self.default_kwargs = kwargs or {}
|
||||||
self._htmx_extra = {}
|
self._htmx_extra = {AUTO_SWAP_OOB: True}
|
||||||
self._bindings = []
|
self._bindings = []
|
||||||
self._ft = None
|
self._ft = None
|
||||||
self._callback_parameters = dict(inspect.signature(callback).parameters) if callback else {}
|
self._callback_parameters = dict(inspect.signature(callback).parameters) if callback else {}
|
||||||
@@ -97,7 +99,7 @@ class Command:
|
|||||||
def get_key(self):
|
def get_key(self):
|
||||||
return self._key
|
return self._key
|
||||||
|
|
||||||
def get_htmx_params(self):
|
def get_htmx_params(self, escaped=False):
|
||||||
res = {
|
res = {
|
||||||
"hx-post": f"{ROUTE_ROOT}{Routes.Commands}",
|
"hx-post": f"{ROUTE_ROOT}{Routes.Commands}",
|
||||||
"hx-swap": "outerHTML",
|
"hx-swap": "outerHTML",
|
||||||
@@ -115,10 +117,13 @@ class Command:
|
|||||||
# kwarg are given to the callback as values
|
# kwarg are given to the callback as values
|
||||||
res["hx-vals"] |= self.default_kwargs
|
res["hx-vals"] |= self.default_kwargs
|
||||||
|
|
||||||
|
if escaped:
|
||||||
|
res["hx-vals"] = html.escape(json.dumps(res["hx-vals"]))
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def execute(self, client_response: dict = None):
|
def execute(self, client_response: dict = None):
|
||||||
logger.debug(f"Executing command {self.name}")
|
logger.debug(f"Executing command {self.name} with arguments {client_response=}")
|
||||||
with ObservableResultCollector(self._bindings) as collector:
|
with ObservableResultCollector(self._bindings) as collector:
|
||||||
kwargs = self._create_kwargs(self.default_kwargs,
|
kwargs = self._create_kwargs(self.default_kwargs,
|
||||||
client_response,
|
client_response,
|
||||||
@@ -135,6 +140,7 @@ class Command:
|
|||||||
all_ret = flatten(ret, ret_from_bound_commands, collector.results)
|
all_ret = flatten(ret, ret_from_bound_commands, collector.results)
|
||||||
|
|
||||||
# Set the hx-swap-oob attribute on all elements returned by the callback
|
# Set the hx-swap-oob attribute on all elements returned by the callback
|
||||||
|
if self._htmx_extra[AUTO_SWAP_OOB]:
|
||||||
for r in all_ret[1:]:
|
for r in all_ret[1:]:
|
||||||
if (hasattr(r, 'attrs')
|
if (hasattr(r, 'attrs')
|
||||||
and "hx-swap-oob" not in r.attrs
|
and "hx-swap-oob" not in r.attrs
|
||||||
@@ -143,7 +149,9 @@ class Command:
|
|||||||
|
|
||||||
return all_ret[0] if len(all_ret) == 1 else all_ret
|
return all_ret[0] if len(all_ret) == 1 else all_ret
|
||||||
|
|
||||||
def htmx(self, target: Optional[str] = "this", swap="outerHTML", trigger=None):
|
def htmx(self, target: Optional[str] = "this", swap="outerHTML", trigger=None, auto_swap_oob=True):
|
||||||
|
self._htmx_extra[AUTO_SWAP_OOB] = auto_swap_oob
|
||||||
|
|
||||||
# Note that the default value is the same than in get_htmx_params()
|
# Note that the default value is the same than in get_htmx_params()
|
||||||
if target is None:
|
if target is None:
|
||||||
self._htmx_extra["hx-swap"] = "none"
|
self._htmx_extra["hx-swap"] = "none"
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
DEFAULT_COLUMN_WIDTH = 100
|
NO_DEFAULT_VALUE = object()
|
||||||
|
|
||||||
ROUTE_ROOT = "/myfasthtml"
|
ROUTE_ROOT = "/myfasthtml"
|
||||||
|
|
||||||
# Datagrid
|
# Datagrid
|
||||||
ROW_INDEX_ID = "__row_index__"
|
ROW_INDEX_ID = "__row_index__"
|
||||||
|
DATAGRID_DEFAULT_COLUMN_WIDTH = 100
|
||||||
DATAGRID_PAGE_SIZE = 1000
|
DATAGRID_PAGE_SIZE = 1000
|
||||||
FILTER_INPUT_CID = "__filter_input__"
|
FILTER_INPUT_CID = "__filter_input__"
|
||||||
|
|
||||||
|
|
||||||
class Routes:
|
class Routes:
|
||||||
Commands = "/commands"
|
Commands = "/commands"
|
||||||
Bindings = "/bindings"
|
Bindings = "/bindings"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import uuid
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from myfasthtml.controls.helpers import Ids
|
from myfasthtml.controls.helpers import Ids
|
||||||
|
from myfasthtml.core.constants import NO_DEFAULT_VALUE
|
||||||
from myfasthtml.core.utils import pascal_to_snake, get_class, snake_to_pascal
|
from myfasthtml.core.utils import pascal_to_snake, get_class, snake_to_pascal
|
||||||
|
|
||||||
logger = logging.getLogger("InstancesManager")
|
logger = logging.getLogger("InstancesManager")
|
||||||
@@ -183,7 +184,7 @@ class InstancesManager:
|
|||||||
return instance
|
return instance
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get(session: dict, instance_id: str, default="**__no_default__**"):
|
def get(session: dict, instance_id: str, default=NO_DEFAULT_VALUE):
|
||||||
"""
|
"""
|
||||||
Get or create an instance of the given type (from its id)
|
Get or create an instance of the given type (from its id)
|
||||||
:param session:
|
:param session:
|
||||||
@@ -196,9 +197,9 @@ class InstancesManager:
|
|||||||
key = (session_id, instance_id)
|
key = (session_id, instance_id)
|
||||||
return InstancesManager.instances[key]
|
return InstancesManager.instances[key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
if default != "**__non__**":
|
if default is NO_DEFAULT_VALUE:
|
||||||
return default
|
|
||||||
raise
|
raise
|
||||||
|
return default
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_by_type(session: dict, cls: type):
|
def get_by_type(session: dict, cls: type):
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from functools import lru_cache
|
|||||||
|
|
||||||
from fasthtml.common import NotStr
|
from fasthtml.common import NotStr
|
||||||
|
|
||||||
|
from myfasthtml.core.constants import NO_DEFAULT_VALUE
|
||||||
|
|
||||||
|
|
||||||
class OptimizedFt:
|
class OptimizedFt:
|
||||||
"""Lightweight FastHTML-compatible element that generates HTML directly."""
|
"""Lightweight FastHTML-compatible element that generates HTML directly."""
|
||||||
@@ -74,6 +76,14 @@ class OptimizedFt:
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.to_html()
|
return self.to_html()
|
||||||
|
|
||||||
|
def get(self, attr_name, default=NO_DEFAULT_VALUE):
|
||||||
|
try:
|
||||||
|
return self.attrs[self.safe_attr(attr_name)]
|
||||||
|
except KeyError:
|
||||||
|
if default is NO_DEFAULT_VALUE:
|
||||||
|
raise
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
class OptimizedDiv(OptimizedFt):
|
class OptimizedDiv(OptimizedFt):
|
||||||
"""Optimized Div element."""
|
"""Optimized Div element."""
|
||||||
|
|||||||
Reference in New Issue
Block a user