Fixed keyboard latency issues
This commit is contained in:
@@ -26,6 +26,7 @@ tools.db
|
|||||||
**/*.prof
|
**/*.prof
|
||||||
**/*.db
|
**/*.db
|
||||||
screenshot*
|
screenshot*
|
||||||
|
Capture*
|
||||||
|
|
||||||
# Created by .ignore support plugin (hsz.mobi)
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
### Python template
|
### Python template
|
||||||
|
|||||||
@@ -17,9 +17,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
window.HTMX_DEBUG = false;
|
window.HTMX_DEBUG = false;
|
||||||
|
window.HTMX_DOM_DEBUG = false;
|
||||||
|
window.HTMX_SETTLE_DEBUG = false;
|
||||||
(function () {
|
(function () {
|
||||||
console.log('Debug HTMX: htmx.logAll();');
|
console.log('Debug HTMX: htmx.logAll();');
|
||||||
console.log('Perf HTMX: window.HTMX_DEBUG=true;');
|
console.log('Perf HTMX: window.HTMX_DEBUG=true;');
|
||||||
|
console.log('DOM mutations: window.HTMX_DOM_DEBUG=true;');
|
||||||
|
console.log('Settle/transition debug: window.HTMX_SETTLE_DEBUG=true;');
|
||||||
})();
|
})();
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
@@ -38,12 +42,108 @@ window.HTMX_DEBUG = false;
|
|||||||
let counter = 0;
|
let counter = 0;
|
||||||
const requests = new WeakMap();
|
const requests = new WeakMap();
|
||||||
|
|
||||||
|
function _startDomObserver(requestId, startTime) {
|
||||||
|
const observer = new MutationObserver(mutations => {
|
||||||
|
mutations.forEach(m => {
|
||||||
|
const el = m.target;
|
||||||
|
const label = el.id ? `#${el.id}` : `<${el.tagName.toLowerCase()} class="${el.className}">`;
|
||||||
|
const t = (performance.now() - startTime).toFixed(2);
|
||||||
|
console.debug(`[HTMX DOM] #${requestId} +${t}ms class changed on ${label} → "${el.className}"`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
observer.observe(document.body, {attributes: true, subtree: true, attributeFilter: ['class']});
|
||||||
|
return observer;
|
||||||
|
}
|
||||||
|
|
||||||
function getInfo(detail) {
|
function getInfo(detail) {
|
||||||
const key = detail?.requestConfig ?? detail?.xhr ?? null;
|
const key = detail?.requestConfig ?? detail?.xhr ?? null;
|
||||||
if (!key || !requests.has(key)) return null;
|
if (!key || !requests.has(key)) return null;
|
||||||
return requests.get(key);
|
return requests.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTMX_SETTLE_DEBUG: trace transitions/animations + computed style on tsm_ after swap
|
||||||
|
(function () {
|
||||||
|
['transitionstart', 'transitionend', 'transitioncancel',
|
||||||
|
'animationstart', 'animationend'].forEach(evtName => {
|
||||||
|
document.addEventListener(evtName, e => {
|
||||||
|
if (!window.HTMX_SETTLE_DEBUG) return;
|
||||||
|
const t = window._perfT0 ? (performance.now() - window._perfT0).toFixed(1) : '?';
|
||||||
|
const label = e.target.id ? `#${e.target.id}` : `<${e.target.tagName.toLowerCase()}>`;
|
||||||
|
console.debug(`[SETTLE +${t}ms] ${evtName} on ${label} | property: ${e.propertyName ?? ''} | duration: ${e.elapsedTime ?? ''}s`);
|
||||||
|
}, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('htmx:afterSwap', e => {
|
||||||
|
if (!window.HTMX_SETTLE_DEBUG) return;
|
||||||
|
const target = e.detail?.target;
|
||||||
|
if (!target?.id?.startsWith('tsm_')) return;
|
||||||
|
const t = window._perfT0 ? (performance.now() - window._perfT0).toFixed(1) : '?';
|
||||||
|
const cs = getComputedStyle(target);
|
||||||
|
console.debug(`[SETTLE +${t}ms] tsm_ computed transitionDuration="${cs.transitionDuration}" transition="${cs.transition}"`);
|
||||||
|
const child = target.firstElementChild;
|
||||||
|
if (child) {
|
||||||
|
const cs2 = getComputedStyle(child);
|
||||||
|
console.debug(`[SETTLE +${t}ms] tsm_>child computed transitionDuration="${cs2.transitionDuration}" transition="${cs2.transition}"`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Patch setTimeout to detect delayed settle timers (main thread blocked by rendering)
|
||||||
|
const _origST = window.setTimeout;
|
||||||
|
window.setTimeout = function (fn, delay, ...args) {
|
||||||
|
const createdAt = window._perfT0 ? (performance.now() - window._perfT0) : null;
|
||||||
|
return _origST.call(window, function () {
|
||||||
|
if (window.HTMX_SETTLE_DEBUG && createdAt !== null && delay >= 1) {
|
||||||
|
const firedAt = performance.now() - window._perfT0;
|
||||||
|
const late = (firedAt - createdAt - delay).toFixed(1);
|
||||||
|
console.debug(`[TIMER] delay=${delay}ms | scheduled +${createdAt.toFixed(1)}ms | fired +${firedAt.toFixed(1)}ms | late=${late}ms`);
|
||||||
|
}
|
||||||
|
return fn.apply(this, args);
|
||||||
|
}, delay, ...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
// PerformanceObserver: log all long tasks (>50ms JS blocks) with timing relative to _perfT0
|
||||||
|
try {
|
||||||
|
new PerformanceObserver(list => {
|
||||||
|
if (!window.HTMX_SETTLE_DEBUG || !window._perfT0) return;
|
||||||
|
list.getEntries().forEach(entry => {
|
||||||
|
const start = (entry.startTime - window._perfT0).toFixed(1);
|
||||||
|
const dur = entry.duration.toFixed(1);
|
||||||
|
console.debug(`[LONG TASK] start +${start}ms | duration ${dur}ms`);
|
||||||
|
});
|
||||||
|
}).observe({entryTypes: ['longtask']});
|
||||||
|
} catch (e) { /* longtask not supported */ }
|
||||||
|
|
||||||
|
// After swap: measure time until next rendering frame to detect rendering blockage
|
||||||
|
document.addEventListener('htmx:afterSwap', e => {
|
||||||
|
if (!window.HTMX_SETTLE_DEBUG || !window._perfT0) return;
|
||||||
|
if (!e.detail?.target?.id?.startsWith('tsm_')) return;
|
||||||
|
const swapAt = performance.now() - window._perfT0;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const rafAt = (performance.now() - window._perfT0).toFixed(1);
|
||||||
|
console.debug(`[RENDER] swap at +${swapAt.toFixed(1)}ms | next rAF at +${rafAt}ms | rendering blocked ${(rafAt - swapAt).toFixed(1)}ms`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Patch requestAnimationFrame to identify which callback causes the long task
|
||||||
|
const _origRAF = window.requestAnimationFrame;
|
||||||
|
window.requestAnimationFrame = function (fn) {
|
||||||
|
const stack = (new Error().stack || '').split('\n')[2]?.trim() || '?';
|
||||||
|
const scheduledAt = window._perfT0 ? (performance.now() - window._perfT0) : null;
|
||||||
|
return _origRAF.call(window, function (timestamp) {
|
||||||
|
const t0 = performance.now();
|
||||||
|
const result = fn(timestamp);
|
||||||
|
if (window.HTMX_SETTLE_DEBUG && scheduledAt !== null) {
|
||||||
|
const cost = (performance.now() - t0).toFixed(1);
|
||||||
|
const firedAt = (t0 - window._perfT0).toFixed(1);
|
||||||
|
if (parseFloat(cost) > 5) {
|
||||||
|
console.debug(`[RAF SLOW] scheduled +${scheduledAt.toFixed(1)}ms | fired +${firedAt}ms | cost ${cost}ms | ${stack}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
EVENTS.forEach(eventName => {
|
EVENTS.forEach(eventName => {
|
||||||
document.addEventListener(eventName, (e) => {
|
document.addEventListener(eventName, (e) => {
|
||||||
if (!window.HTMX_DEBUG) return;
|
if (!window.HTMX_DEBUG) return;
|
||||||
@@ -59,7 +159,8 @@ window.HTMX_DEBUG = false;
|
|||||||
if (key) {
|
if (key) {
|
||||||
const id = ++counter;
|
const id = ++counter;
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
requests.set(key, {id, start: now, last: now});
|
const observer = window.HTMX_DOM_DEBUG ? _startDomObserver(id, now) : null;
|
||||||
|
requests.set(key, {id, start: now, last: now, observer});
|
||||||
prefix = `#${String(id).padStart(3)} + 0.0ms (Δ 0.0ms)`;
|
prefix = `#${String(id).padStart(3)} + 0.0ms (Δ 0.0ms)`;
|
||||||
} else {
|
} else {
|
||||||
prefix = `# ? + 0.0ms (Δ 0.0ms)`;
|
prefix = `# ? + 0.0ms (Δ 0.0ms)`;
|
||||||
@@ -72,6 +173,10 @@ window.HTMX_DEBUG = false;
|
|||||||
const step = (now - info.last).toFixed(1);
|
const step = (now - info.last).toFixed(1);
|
||||||
info.last = now;
|
info.last = now;
|
||||||
prefix = `#${String(info.id).padStart(3)} +${String(total).padStart(7)}ms (Δ${String(step).padStart(7)}ms)`;
|
prefix = `#${String(info.id).padStart(3)} +${String(total).padStart(7)}ms (Δ${String(step).padStart(7)}ms)`;
|
||||||
|
|
||||||
|
if (eventName === 'htmx:afterSettle' || eventName === 'htmx:sendError') {
|
||||||
|
info.observer?.disconnect();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
prefix = `# ? + ?.?ms (Δ ?.?ms)`;
|
prefix = `# ? + ?.?ms (Δ ?.?ms)`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
|
|
||||||
// Set window.KEYBOARD_DEBUG = true in the browser console to enable traces
|
// Set window.KEYBOARD_DEBUG = true in the browser console to enable traces
|
||||||
window.KEYBOARD_DEBUG = false;
|
window.KEYBOARD_DEBUG = false;
|
||||||
|
(function () {
|
||||||
|
console.log('Perf Keyboard: window.KEYBOARD_DEBUG=true;');
|
||||||
|
})();
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
function kbLog(...args) {
|
function kbLog(...args) {
|
||||||
@@ -169,6 +172,8 @@ window.KEYBOARD_DEBUG = false;
|
|||||||
* @param {KeyboardEvent} event - The keyboard event
|
* @param {KeyboardEvent} event - The keyboard event
|
||||||
*/
|
*/
|
||||||
function handleKeyboardEvent(event) {
|
function handleKeyboardEvent(event) {
|
||||||
|
window._perfT0 = performance.now();
|
||||||
|
kbLog(`[PERF] keyboard_start: 0ms`);
|
||||||
const key = normalizeKey(event.key);
|
const key = normalizeKey(event.key);
|
||||||
|
|
||||||
// Add key to current pressed keys
|
// Add key to current pressed keys
|
||||||
@@ -260,7 +265,9 @@ window.KEYBOARD_DEBUG = false;
|
|||||||
// We have matches and NO element has longer sequences possible
|
// We have matches and NO element has longer sequences possible
|
||||||
// Trigger ALL matches immediately
|
// Trigger ALL matches immediately
|
||||||
for (const match of currentMatches) {
|
for (const match of currentMatches) {
|
||||||
|
kbLog(`[PERF] before_trigger: ${(performance.now() - window._perfT0).toFixed(2)}ms`);
|
||||||
triggerHtmxAction(match.elementId, match.config, match.combinationStr, match.isInside, event);
|
triggerHtmxAction(match.elementId, match.config, match.combinationStr, match.isInside, event);
|
||||||
|
kbLog(`[PERF] after_trigger: ${(performance.now() - window._perfT0).toFixed(2)}ms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear history after triggering
|
// Clear history after triggering
|
||||||
@@ -299,6 +306,8 @@ window.KEYBOARD_DEBUG = false;
|
|||||||
KeyboardRegistry.snapshotHistory = [];
|
KeyboardRegistry.snapshotHistory = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kbLog(`[PERF] keyboard_end: ${(performance.now() - window._perfT0).toFixed(2)}ms`);
|
||||||
|
|
||||||
// If we found no match at all, clear the history
|
// If we found no match at all, clear the history
|
||||||
// This handles invalid sequences like "A C" when only "A B" exists
|
// This handles invalid sequences like "A C" when only "A B" exists
|
||||||
if (!foundAnyMatch) {
|
if (!foundAnyMatch) {
|
||||||
|
|||||||
@@ -376,8 +376,13 @@ function triggerHtmxAction(elementId, config, combinationStr, isInside, event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use the HTMX target element as source so htmx-request is added there
|
||||||
|
// instead of <body>, avoiding a full-document style recalculation.
|
||||||
|
const targetSelector = config['hx-target'];
|
||||||
|
const sourceElement = targetSelector ? document.querySelector(targetSelector) : element;
|
||||||
|
htmxOptions.source = sourceElement ?? element;
|
||||||
|
|
||||||
// Make AJAX call with htmx
|
// Make AJAX call with htmx
|
||||||
//console.debug(`Triggering HTMX action for element ${elementId}: ${method} ${url}`, htmxOptions);
|
//console.debug(`Triggering HTMX action for element ${elementId}: ${method} ${url}`, htmxOptions);
|
||||||
htmx.ajax(method, url, htmxOptions);
|
htmx.ajax(method, url, htmxOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -232,6 +232,7 @@
|
|||||||
-ms-overflow-style: none; /* IE/Edge: hide scrollbar */
|
-ms-overflow-style: none; /* IE/Edge: hide scrollbar */
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
contain: layout style paint; /* Isolate from sibling DOM changes (perf: prevents style recalc cascade) */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Chrome/Safari: hide scrollbar */
|
/* Chrome/Safari: hide scrollbar */
|
||||||
|
|||||||
@@ -687,9 +687,10 @@ function updateDatagridSelection(datagridId) {
|
|||||||
element.style.userSelect = '';
|
element.style.userSelect = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Loop through the children of the selection manager
|
// Loop through the children of the inner selection manager (#tsmi_)
|
||||||
|
const tsmi = document.getElementById(`tsmi_${datagridId}`) ?? selectionManager;
|
||||||
let hasFocusedCell = false;
|
let hasFocusedCell = false;
|
||||||
Array.from(selectionManager.children).forEach((selection) => {
|
Array.from(tsmi.children).forEach((selection) => {
|
||||||
const selectionType = selection.getAttribute('selection-type');
|
const selectionType = selection.getAttribute('selection-type');
|
||||||
const elementId = selection.getAttribute('element-id');
|
const elementId = selection.getAttribute('element-id');
|
||||||
|
|
||||||
|
|||||||
@@ -157,21 +157,21 @@ class Commands(BaseCommands):
|
|||||||
"Click on the table",
|
"Click on the table",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.handle_on_click
|
self._owner.handle_on_click
|
||||||
).htmx(target=f"#tsm_{self._id}")
|
).htmx(target=f"#tsmi_{self._id}")
|
||||||
|
|
||||||
def on_key_pressed(self):
|
def on_key_pressed(self):
|
||||||
return Command("OnKeyPressed",
|
return Command("OnKeyPressed",
|
||||||
"Key pressed on the table",
|
"Key pressed on the table",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.on_key_pressed
|
self._owner.on_key_pressed
|
||||||
).htmx(target=f"#tsm_{self._id}")
|
).htmx(target=f"#tsmi_{self._id}")
|
||||||
|
|
||||||
def on_mouse_selection(self):
|
def on_mouse_selection(self):
|
||||||
return Command("OnMouseSelection",
|
return Command("OnMouseSelection",
|
||||||
"Range selection with mouse",
|
"Range selection with mouse",
|
||||||
self._owner,
|
self._owner,
|
||||||
self._owner.on_mouse_selection
|
self._owner.on_mouse_selection
|
||||||
).htmx(target=f"#tsm_{self._id}")
|
).htmx(target=f"#tsmi_{self._id}")
|
||||||
|
|
||||||
def toggle_columns_manager(self):
|
def toggle_columns_manager(self):
|
||||||
return Command("ToggleColumnsManager",
|
return Command("ToggleColumnsManager",
|
||||||
@@ -690,7 +690,7 @@ class DataGrid(MultipleInstance):
|
|||||||
else:
|
else:
|
||||||
logger.debug(f" is_inside=False")
|
logger.debug(f" is_inside=False")
|
||||||
|
|
||||||
return self.render_partial()
|
return self.render_partial(inner_mode=True)
|
||||||
|
|
||||||
def on_mouse_selection(self, combination, is_inside, cell_id_mousedown, cell_id_mouseup):
|
def on_mouse_selection(self, combination, is_inside, cell_id_mousedown, cell_id_mouseup):
|
||||||
logger.debug(f"on_mouse_selection {combination=} {is_inside=} {cell_id_mousedown=} {cell_id_mouseup=}")
|
logger.debug(f"on_mouse_selection {combination=} {is_inside=} {cell_id_mousedown=} {cell_id_mouseup=}")
|
||||||
@@ -708,14 +708,14 @@ class DataGrid(MultipleInstance):
|
|||||||
self._state.selection.extra_selected.clear()
|
self._state.selection.extra_selected.clear()
|
||||||
self._state.selection.extra_selected.append(("range", (min_col, min_row, max_col, max_row)))
|
self._state.selection.extra_selected.append(("range", (min_col, min_row, max_col, max_row)))
|
||||||
|
|
||||||
return self.render_partial()
|
return self.render_partial(inner_mode=True)
|
||||||
|
|
||||||
@profiler.trace_calls()
|
@profiler.trace_calls()
|
||||||
def on_key_pressed(self, combination, has_focus, is_inside):
|
def on_key_pressed(self, combination, has_focus, is_inside):
|
||||||
logger.debug(f"on_key_pressed table={self.get_table_name()} {combination=} {has_focus=} {is_inside=}")
|
logger.debug(f"on_key_pressed table={self.get_table_name()} {combination=} {has_focus=} {is_inside=}")
|
||||||
if combination == "esc":
|
if combination == "esc":
|
||||||
self._update_current_position(None, reset_selection=True)
|
self._update_current_position(None, reset_selection=True)
|
||||||
return self.render_partial("cell", pos=self._state.selection.last_selected)
|
return self.render_partial("cell", inner_mode=True, pos=self._state.selection.last_selected)
|
||||||
|
|
||||||
elif (combination == "enter" and
|
elif (combination == "enter" and
|
||||||
self._settings.enable_edition and
|
self._settings.enable_edition and
|
||||||
@@ -731,7 +731,7 @@ class DataGrid(MultipleInstance):
|
|||||||
new_pos = self._navigate(current_pos, direction)
|
new_pos = self._navigate(current_pos, direction)
|
||||||
self._update_current_position(new_pos)
|
self._update_current_position(new_pos)
|
||||||
|
|
||||||
return self.render_partial()
|
return self.render_partial(inner_mode=True)
|
||||||
|
|
||||||
def on_column_changed(self):
|
def on_column_changed(self):
|
||||||
logger.debug("on_column_changed")
|
logger.debug("on_column_changed")
|
||||||
@@ -1180,11 +1180,28 @@ class DataGrid(MultipleInstance):
|
|||||||
selected.append(extra_sel)
|
selected.append(extra_sel)
|
||||||
|
|
||||||
return Div(
|
return Div(
|
||||||
*[Div(selection_type=s_type, element_id=f"{elt_id}") for s_type, elt_id in selected],
|
Div(
|
||||||
|
*[Div(selection_type=s_type, element_id=f"{elt_id}") for s_type, elt_id in selected],
|
||||||
|
id=f"tsmi_{self._id}",
|
||||||
|
),
|
||||||
id=f"tsm_{self._id}",
|
id=f"tsm_{self._id}",
|
||||||
selection_mode=f"{self._state.selection.selection_mode}",
|
selection_mode=f"{self._state.selection.selection_mode}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def mk_selection_manager_inner(self):
|
||||||
|
selected = []
|
||||||
|
|
||||||
|
if self._state.selection.selected:
|
||||||
|
selected.append(("focus", self._get_element_id_from_pos("cell", self._state.selection.selected)))
|
||||||
|
|
||||||
|
for extra_sel in self._state.selection.extra_selected:
|
||||||
|
selected.append(extra_sel)
|
||||||
|
|
||||||
|
return Div(
|
||||||
|
*[Div(selection_type=s_type, element_id=f"{elt_id}") for s_type, elt_id in selected],
|
||||||
|
id=f"tsmi_{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):
|
||||||
"""
|
"""
|
||||||
Generates a footer cell for a data table based on the provided column definition,
|
Generates a footer cell for a data table based on the provided column definition,
|
||||||
@@ -1270,17 +1287,21 @@ class DataGrid(MultipleInstance):
|
|||||||
cls="grid",
|
cls="grid",
|
||||||
style="height: 100%; grid-template-rows: auto 1fr;",
|
style="height: 100%; grid-template-rows: auto 1fr;",
|
||||||
tabindex="-1",
|
tabindex="-1",
|
||||||
**{"hx-on:htmx:after-swap": f"if(event.detail.target.id==='tsm_{self._id}') updateDatagridSelection('{self._id}');"}
|
**{"hx-on:htmx:after-swap": f"if(event.detail.target.id==='tsm_{self._id}'||event.detail.target.id==='tsmi_{self._id}') updateDatagridSelection('{self._id}');"}
|
||||||
)
|
)
|
||||||
|
|
||||||
def render_partial(self, fragment="cell", **kwargs):
|
def render_partial(self, fragment="cell", inner_mode=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
:param fragment: cell | body | table | header
|
:param fragment: cell | body | table | header
|
||||||
|
:param inner_mode: When True, returns mk_selection_manager_inner() (outerHTML swap on #tsmi_)
|
||||||
|
instead of mk_selection_manager() (outerHTML swap on #tsm_).
|
||||||
|
Use inner_mode=True for navigation commands (arrow keys, click, mouse selection)
|
||||||
|
to avoid style recalculation on #tsm_ siblings (e.g. the full table).
|
||||||
:param kwargs: Additional parameters for specific fragments (col_id, optimal_width for header)
|
:param kwargs: Additional parameters for specific fragments (col_id, optimal_width for header)
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
res = [self.mk_selection_manager()]
|
res = [self.mk_selection_manager_inner() if inner_mode else self.mk_selection_manager()]
|
||||||
|
|
||||||
extra_attr = {
|
extra_attr = {
|
||||||
"hx-on::after-settle": f"initDataGrid('{self._id}');",
|
"hx-on::after-settle": f"initDataGrid('{self._id}');",
|
||||||
|
|||||||
Reference in New Issue
Block a user