diff --git a/src/assets/dash_clientside.js b/src/assets/dash_clientside.js new file mode 100644 index 0000000..efef901 --- /dev/null +++ b/src/assets/dash_clientside.js @@ -0,0 +1,105 @@ +window.dash_clientside = Object.assign({}, window.dash_clientside, { + clientside: { + clean_filters: function (trigger) { + if (!trigger) { + return window.dash_clientside.no_update; + } + + // Helper to set value on a React text input + const setNativeValue = (element, value) => { + const valueSetter = Object.getOwnPropertyDescriptor( + element, + "value" + ).set; + const prototype = Object.getPrototypeOf(element); + const prototypeValueSetter = Object.getOwnPropertyDescriptor( + prototype, + "value" + ).set; + + if (valueSetter && valueSetter !== prototypeValueSetter) { + prototypeValueSetter.call(element, value); + } else { + valueSetter.call(element, value); + } + + element.dispatchEvent(new Event("input", { bubbles: true })); + }; + + const cleanInputs = () => { + const inputs = document.querySelectorAll( + '.dash-filter input[type="text"]' + ); + inputs.forEach((input) => { + let val = input.value; + let original = val; + + // Remove "icontains " prefix + if (/^icontains\s+/i.test(val)) { + val = val.replace(/^icontains\s+/i, ""); + // Check for surrounding quotes (single or double) and remove them + if ( + (val.startsWith('"') && val.endsWith('"')) || + (val.startsWith("'") && val.endsWith("'")) + ) { + val = val.substring(1, val.length - 1); + } + } + // Handle relational operators (i<, s>, i<=, etc.) + else if (/^[is][<>]=?/i.test(val)) { + val = val.substring(1); + } + + if (val !== original) { + try { + // Try setting it the React-friendly way + setNativeValue(input, val); + } catch (e) { + // Fallback to direct assignment if fancy way fails + input.value = val; + } + } + }); + }; + + // Use MutationObserver to wait for table to appear/update + const observer = new MutationObserver((mutations) => { + cleanInputs(); + }); + + const target = document.querySelector(".dash-table-container"); + if (target) { + observer.observe(target, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["value"], + }); + + // Disconnect after 5 seconds + setTimeout(() => { + observer.disconnect(); + }, 5000); + + // Also try immediately just in case + cleanInputs(); + } else { + // Poll briefly if container not found yet + const checkInterval = setInterval(() => { + const t = document.querySelector(".dash-table-container"); + if (t) { + clearInterval(checkInterval); + observer.observe(t, { childList: true, subtree: true }); + setTimeout(() => observer.disconnect(), 5000); + cleanInputs(); + } + }, 200); + + // Stop polling after 2s if still nothing + setTimeout(() => clearInterval(checkInterval), 2000); + } + + return window.dash_clientside.no_update; + }, + }, +}); diff --git a/src/pages/arbre/__init__.py b/src/pages/arbre/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/tableau.py b/src/pages/tableau.py index fb289c4..1207ea0 100644 --- a/src/pages/tableau.py +++ b/src/pages/tableau.py @@ -1,10 +1,22 @@ import json import os import urllib.parse +import uuid from datetime import datetime import polars as pl -from dash import Input, Output, State, callback, dcc, html, no_update, register_page +from dash import ( + ClientsideFunction, + Input, + Output, + State, + callback, + clientside_callback, + dcc, + html, + no_update, + register_page, +) from src.figures import DataTable from src.utils import ( @@ -50,6 +62,7 @@ datatable = html.Div( layout = [ dcc.Location(id="tableau_url", refresh=False), + dcc.Store(id="filter-cleanup-trigger"), html.Script( type="application/ld+json", id="dataset_jsonld", @@ -272,12 +285,13 @@ def download_data(n_clicks, filter_query, sort_by, hidden_columns: list = None): Output("table", "sort_by"), Output("table", "hidden_columns"), Output("tableau_url", "search", allow_duplicate=True), + Output("filter-cleanup-trigger", "data"), Input("tableau_url", "search"), prevent_initial_call=True, ) def restore_view_from_url(search): if not search: - return no_update, no_update, no_update, no_update + return no_update, no_update, no_update, no_update, no_update params = urllib.parse.parse_qs(search.lstrip("?")) logger.debug("params", params) @@ -285,9 +299,11 @@ def restore_view_from_url(search): filter_query = no_update sort_by = no_update hidden_columns = no_update + trigger_cleanup = no_update if "filtres" in params: filter_query = params["filtres"][0] + trigger_cleanup = str(uuid.uuid4()) if "tris" in params: try: @@ -300,7 +316,18 @@ def restore_view_from_url(search): verified_columns = [column for column in columns if column in schema.names()] hidden_columns = invert_columns(verified_columns) - return filter_query, sort_by, hidden_columns, "" + return filter_query, sort_by, hidden_columns, "", trigger_cleanup + + +clientside_callback( + ClientsideFunction( + namespace="clientside", + function_name="clean_filters", + ), + Output("filter-cleanup-trigger", "data", allow_duplicate=True), + Input("filter-cleanup-trigger", "data"), + prevent_initial_call=True, +) @callback(