diff --git a/src/assets/main.js b/src/assets/main.js index 33e777d..97a8be6 100644 --- a/src/assets/main.js +++ b/src/assets/main.js @@ -25,11 +25,19 @@ function bindTooltipsWithDelegation() { // Add a single mouseenter and mouseleave listener to the parent element element.addEventListener("mouseenter", (event) => { + //console.debug("Entering element", event.target) + const cell = event.target.closest("[data-tooltip]"); - if (!cell) return; + if (!cell) { + // console.debug(" No 'data-tooltip' attribute found. Stopping."); + return; + } const no_tooltip = element.hasAttribute("mmt-no-tooltip"); - if (no_tooltip) return; + if (no_tooltip) { + // console.debug(" Attribute 'mmt-no-tooltip' found. Cancelling."); + return; + } const content = cell.querySelector(".truncate") || cell; const isOverflowing = content.scrollWidth > content.clientWidth; diff --git a/src/components/datagrid_new/assets/Datagrid.js b/src/components/datagrid_new/assets/Datagrid.js index 02b2aad..479dba7 100644 --- a/src/components/datagrid_new/assets/Datagrid.js +++ b/src/components/datagrid_new/assets/Datagrid.js @@ -1,10 +1,6 @@ function bindDatagrid(datagridId, allowColumnsReordering) { - bindScrollbars(datagridId); - makeResizable(datagridId) - - document.body.addEventListener('htmx:sseBeforeMessage', function (e) { - console.log("htmx:sseBeforeMessage", e) - }) + manageScrollbars(datagridId, true); + makeResizable(datagridId); } function bindScrollbars(datagridId) { @@ -180,6 +176,224 @@ function bindScrollbars(datagridId) { }); } +function manageScrollbars(datagridId, binding) { + console.debug("manageScrollbars on element " + datagridId + " with binding=" + binding); + + const datagrid = document.getElementById(datagridId); + + if (!datagrid) { + console.error(`Datagrid with id "${datagridId}" not found.`); + return; + } + + const verticalScrollbar = datagrid.querySelector(".dt2-scrollbars-vertical"); + const verticalWrapper = datagrid.querySelector(".dt2-scrollbars-vertical-wrapper"); + const horizontalScrollbar = datagrid.querySelector(".dt2-scrollbars-horizontal"); + const horizontalWrapper = datagrid.querySelector(".dt2-scrollbars-horizontal-wrapper"); + const body = datagrid.querySelector(".dt2-body"); + const table = datagrid.querySelector(".dt2-table"); + + if (!verticalScrollbar || !verticalWrapper || !horizontalScrollbar || !horizontalWrapper || !body || !table) { + console.error("Essential scrollbars or content elements are missing in the datagrid."); + return; + } + + const computeScrollbarVisibility = () => { + // Determine if the content is clipped + const isVerticalRequired = body.scrollHeight > body.clientHeight; + const isHorizontalRequired = table.scrollWidth > table.clientWidth; + + // Show or hide the scrollbar wrappers + requestAnimationFrame(() => { + verticalWrapper.style.display = isVerticalRequired ? "block" : "none"; + horizontalWrapper.style.display = isHorizontalRequired ? "block" : "none"; + }); + }; + + const computeScrollbarSize = () => { + // Vertical scrollbar height + const visibleHeight = body.clientHeight; + const totalHeight = body.scrollHeight; + const wrapperHeight = verticalWrapper.offsetHeight; + + let scrollbarHeight = 0; + if (totalHeight > 0) { + scrollbarHeight = (visibleHeight / totalHeight) * wrapperHeight; + } + + // Horizontal scrollbar width + const visibleWidth = table.clientWidth; + const totalWidth = table.scrollWidth; + 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 updateVerticalScrollbarForMouseWheel = () => { + const maxScrollTop = body.scrollHeight - body.clientHeight; + const wrapperHeight = verticalWrapper.offsetHeight; + + if (maxScrollTop > 0) { + const scrollRatio = wrapperHeight / body.scrollHeight; + verticalScrollbar.style.top = `${body.scrollTop * scrollRatio}px`; + } + }; + + if (binding) { + // Clean up existing managers if they exist + if (datagrid._managers) { + // Remove drag events + if (datagrid._managers.dragManager) { + verticalScrollbar.removeEventListener("mousedown", datagrid._managers.dragManager.verticalMouseDown); + horizontalScrollbar.removeEventListener("mousedown", datagrid._managers.dragManager.horizontalMouseDown); + document.removeEventListener("mousemove", datagrid._managers.dragManager.mouseMove); + document.removeEventListener("mouseup", datagrid._managers.dragManager.mouseUp); + } + + // Remove wheel events + if (datagrid._managers.wheelManager) { + body.removeEventListener("wheel", datagrid._managers.wheelManager.handleWheelScrolling); + } + + // Remove resize events + if (datagrid._managers.resizeManager) { + window.removeEventListener("resize", datagrid._managers.resizeManager.handleResize); + } + } + + // Create managers + const dragManager = { + isDragging: false, + startY: 0, + startX: 0, + + updateVerticalScrollbar: (deltaX, deltaY) => { + const wrapperHeight = verticalWrapper.offsetHeight; + const scrollbarHeight = verticalScrollbar.offsetHeight; + const maxScrollTop = body.scrollHeight - body.clientHeight; + const scrollRatio = maxScrollTop / (wrapperHeight - scrollbarHeight); + + let newTop = parseFloat(verticalScrollbar.style.top || "0") + deltaY; + newTop = Math.max(0, Math.min(newTop, wrapperHeight - scrollbarHeight)); + + verticalScrollbar.style.top = `${newTop}px`; + body.scrollTop = newTop * scrollRatio; + }, + + updateHorizontalScrollbar: (deltaX, deltaY) => { + const wrapperWidth = horizontalWrapper.offsetWidth; + const scrollbarWidth = horizontalScrollbar.offsetWidth; + const maxScrollLeft = table.scrollWidth - table.clientWidth; + const scrollRatio = maxScrollLeft / (wrapperWidth - scrollbarWidth); + + let newLeft = parseFloat(horizontalScrollbar.style.left || "0") + deltaX; + newLeft = Math.max(0, Math.min(newLeft, wrapperWidth - scrollbarWidth)); + + horizontalScrollbar.style.left = `${newLeft}px`; + table.scrollLeft = newLeft * scrollRatio; + }, + + verticalMouseDown: (e) => { + disableTooltip(); + dragManager.isDragging = true; + dragManager.startY = e.clientY; + dragManager.startX = e.clientX; + document.body.style.userSelect = "none"; + verticalScrollbar.classList.add("dt2-dragging"); + }, + + horizontalMouseDown: (e) => { + disableTooltip(); + dragManager.isDragging = true; + dragManager.startY = e.clientY; + dragManager.startX = e.clientX; + document.body.style.userSelect = "none"; + horizontalScrollbar.classList.add("dt2-dragging"); + }, + + mouseMove: (e) => { + if (dragManager.isDragging) { + const deltaY = e.clientY - dragManager.startY; + const deltaX = e.clientX - dragManager.startX; + + // Determine which scrollbar is being dragged + if (verticalScrollbar.classList.contains("dt2-dragging")) { + dragManager.updateVerticalScrollbar(deltaX, deltaY); + } else if (horizontalScrollbar.classList.contains("dt2-dragging")) { + dragManager.updateHorizontalScrollbar(deltaX, deltaY); + } + + // Reset start points for next update + dragManager.startY = e.clientY; + dragManager.startX = e.clientX; + } + }, + + mouseUp: () => { + dragManager.isDragging = false; + document.body.style.userSelect = ""; + verticalScrollbar.classList.remove("dt2-dragging"); + horizontalScrollbar.classList.remove("dt2-dragging"); + enableTooltip(); + } + }; + + const wheelManager = { + handleWheelScrolling: (event) => { + const deltaX = event.deltaX; + const deltaY = event.deltaY; + + // Scroll the body and table content + body.scrollTop += deltaY; // Vertical scrolling + table.scrollLeft += deltaX; // Horizontal scrolling + + // Update the vertical scrollbar position + updateVerticalScrollbarForMouseWheel(); + + // Prevent default behavior to fully manage the scroll + event.preventDefault(); + } + }; + + const resizeManager = { + handleResize: () => { + computeScrollbarVisibility(); + computeScrollbarSize(); + updateVerticalScrollbarForMouseWheel(); + } + }; + + // Store managers on datagrid for cleanup + datagrid._managers = { + dragManager, + wheelManager, + resizeManager + }; + + // Bind events + verticalScrollbar.addEventListener("mousedown", dragManager.verticalMouseDown); + horizontalScrollbar.addEventListener("mousedown", dragManager.horizontalMouseDown); + document.addEventListener("mousemove", dragManager.mouseMove); + document.addEventListener("mouseup", dragManager.mouseUp); + + body.addEventListener("wheel", wheelManager.handleWheelScrolling, {passive: false}); + + window.addEventListener("resize", resizeManager.handleResize); + } + + // Always execute computations + computeScrollbarVisibility(); + computeScrollbarSize(); +} + function makeResizable(datagridId) { console.debug("makeResizable on element " + datagridId); diff --git a/src/components/datagrid_new/components/DataGrid.py b/src/components/datagrid_new/components/DataGrid.py index 977ec12..dbdc222 100644 --- a/src/components/datagrid_new/components/DataGrid.py +++ b/src/components/datagrid_new/components/DataGrid.py @@ -446,8 +446,8 @@ class DataGrid(BaseComponent): _mk_keyboard_management(), Div( self.mk_table_header(), - # self.mk_table_body(), - # self.mk_table_body_sse(), + #self.mk_table_body(), + #self.mk_table_body_sse(), self.mk_table_body_page(), self.mk_table_footer(), cls="dt2-inner-table"), @@ -490,7 +490,12 @@ class DataGrid(BaseComponent): ) def mk_table_body_sse(self): - + """ + This function is used to create a sse update + Unfortunately, the sse does not update the correct element + tb_{self._id} is not updated + Plus UI refreshment issues + """ max_height = self._compute_body_max_height() return Div( @@ -505,7 +510,10 @@ class DataGrid(BaseComponent): ) def mk_table_body_page(self): - + """ + This function is used to update the table body when the vertical scrollbar reaches the end + A new page is added when requested + """ max_height = self._compute_body_max_height() return Div( @@ -552,13 +560,17 @@ class DataGrid(BaseComponent): else: last_row = None - return [Div( + rows = [Div( *[self.mk_body_cell(col_pos, row_index, col_def) for col_pos, col_def in enumerate(self._state.columns)], cls="dt2-row", data_row=f"{row_index}", id=f"tr_{self._id}-{row_index}", **self.commands.get_page(page_index + 1) if row_index == last_row else {} ) for row_index in df.index[start:end]] + + rows.append(Script(f"manageScrollbars('{self._id}', false);"),) + + return rows async def mk_body_content_sse(self): df = self._get_filtered_df() diff --git a/src/components/datagrid_new/components/commands.py b/src/components/datagrid_new/components/commands.py index 55c0ef8..048a426 100644 --- a/src/components/datagrid_new/components/commands.py +++ b/src/components/datagrid_new/components/commands.py @@ -100,9 +100,10 @@ class DataGridCommandManager(BaseCommandManager): def get_page(self, page_index=0): return { "hx-get": f"{ROUTE_ROOT}{Routes.GetPage}", - "hx-swap": "afterend", - "hx-vals": f'{{"_id": "{self._id}", "page": "{page_index}"}}', - "hx-trigger": "intersect once", + "hx-target": f"#tb_{self._id}", + "hx-swap": "beforeend", + "hx-vals": f'{{"_id": "{self._id}", "page_index": "{page_index}"}}', + "hx-trigger": f"intersect root:#tb_{self._id} once", } def _get_hide_show_columns_attrs(self, mode, col_defs: list, new_value, cls=""): diff --git a/src/core/fasthtml_helper.py b/src/core/fasthtml_helper.py index c40bb17..e9bc874 100644 --- a/src/core/fasthtml_helper.py +++ b/src/core/fasthtml_helper.py @@ -7,6 +7,9 @@ attr_map = { "_id": "id", } +def safe_attr(attr_name): + attr_name = attr_name.replace("hx_", "hx-") + return attr_map.get(attr_name, attr_name) def to_html(item): if item is None: @@ -31,7 +34,7 @@ class MyFt: def to_html(self): body_items = [to_html(item) for item in self.children] - return f"<{self.name} {' '.join(f'{attr_map.get(k, k)}="{v}"' for k, v in self.attrs.items())}>{' '.join(body_items)}" + return f"<{self.name} {' '.join(f'{safe_attr(k)}="{v}"' for k, v in self.attrs.items())}>{' '.join(body_items)}" def __ft__(self): return NotStr(self.to_html())