figrecipe 0.6.0__py3-none-any.whl → 0.9.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- figrecipe/__init__.py +161 -1030
- figrecipe/__main__.py +12 -0
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +113 -0
- figrecipe/_api/_save.py +287 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_cli/__init__.py +7 -0
- figrecipe/_cli/_compose.py +87 -0
- figrecipe/_cli/_convert.py +117 -0
- figrecipe/_cli/_crop.py +82 -0
- figrecipe/_cli/_edit.py +70 -0
- figrecipe/_cli/_extract.py +128 -0
- figrecipe/_cli/_fonts.py +47 -0
- figrecipe/_cli/_info.py +67 -0
- figrecipe/_cli/_main.py +58 -0
- figrecipe/_cli/_reproduce.py +79 -0
- figrecipe/_cli/_style.py +77 -0
- figrecipe/_cli/_validate.py +66 -0
- figrecipe/_cli/_version.py +50 -0
- figrecipe/_composition/__init__.py +32 -0
- figrecipe/_composition/_alignment.py +452 -0
- figrecipe/_composition/_compose.py +179 -0
- figrecipe/_composition/_import_axes.py +127 -0
- figrecipe/_composition/_visibility.py +125 -0
- figrecipe/_dev/__init__.py +4 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/browser/__init__.py +69 -0
- figrecipe/_dev/browser/_audio.py +240 -0
- figrecipe/_dev/browser/_caption.py +356 -0
- figrecipe/_dev/browser/_click_effect.py +146 -0
- figrecipe/_dev/browser/_cursor.py +196 -0
- figrecipe/_dev/browser/_highlight.py +105 -0
- figrecipe/_dev/browser/_narration.py +237 -0
- figrecipe/_dev/browser/_recorder.py +446 -0
- figrecipe/_dev/browser/_utils.py +178 -0
- figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
- figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
- figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
- figrecipe/_dev/demo_plotters/__init__.py +35 -166
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +61 -13
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +402 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +466 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_call_overrides.py +183 -0
- figrecipe/_editor/_datatable_plot_handlers.py +249 -0
- figrecipe/_editor/_figure_layout.py +211 -0
- figrecipe/_editor/_flask_app.py +200 -1030
- figrecipe/_editor/_helpers.py +251 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +194 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +507 -0
- figrecipe/_editor/_renderer.py +81 -186
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +482 -0
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +126 -0
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +335 -0
- figrecipe/_editor/_routes_files.py +443 -0
- figrecipe/_editor/_routes_image.py +200 -0
- figrecipe/_editor/_routes_snapshot.py +94 -0
- figrecipe/_editor/_routes_style.py +243 -0
- figrecipe/_editor/_templates/__init__.py +116 -1
- figrecipe/_editor/_templates/_html.py +154 -64
- figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
- figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
- figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
- figrecipe/_editor/_templates/_html_datatable.py +92 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +493 -0
- figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
- figrecipe/_editor/_templates/_scripts/_files.py +429 -0
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
- figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
- figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
- figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
- figrecipe/_editor/_templates/_styles/__init__.py +78 -0
- figrecipe/_editor/_templates/_styles/_base.py +111 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +430 -0
- figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
- figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
- figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
- figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
- figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
- figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +224 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +127 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +430 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
- figrecipe/_editor/static/audio/click.mp3 +0 -0
- figrecipe/_editor/static/click.mp3 +0 -0
- figrecipe/_editor/static/icons/favicon.ico +0 -0
- figrecipe/_integrations/__init__.py +17 -0
- figrecipe/_integrations/_scitex_stats.py +298 -0
- figrecipe/_params/_DECORATION_METHODS.py +8 -0
- figrecipe/_recorder.py +63 -109
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +509 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +252 -895
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +188 -1
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +42 -480
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +40 -28
- figrecipe-0.9.0.dist-info/METADATA +427 -0
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/METADATA +0 -394
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Datatable plotting JavaScript: hints, variable assignment, plotting."""
|
|
4
|
+
|
|
5
|
+
from ...._plot_types_registry import generate_js_hints
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_js_datatable_plot() -> str:
|
|
9
|
+
"""Generate JavaScript for plot type hints and variable assignment."""
|
|
10
|
+
js_hints = generate_js_hints()
|
|
11
|
+
|
|
12
|
+
return f"""
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Plot Type Hints (Generated from matplotlib signatures)
|
|
15
|
+
// ============================================================================
|
|
16
|
+
{js_hints}
|
|
17
|
+
|
|
18
|
+
function initPlotTypeButtons() {{
|
|
19
|
+
const selector = document.getElementById('datatable-plot-type');
|
|
20
|
+
if (!selector) return;
|
|
21
|
+
|
|
22
|
+
selector.addEventListener('change', (e) => {{
|
|
23
|
+
datatablePlotType = e.target.value;
|
|
24
|
+
updateVarAssignSlots();
|
|
25
|
+
updatePlotButtonState();
|
|
26
|
+
}});
|
|
27
|
+
}}
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Variable Assignment UI with Color-Coded Linking
|
|
31
|
+
// ============================================================================
|
|
32
|
+
let datatableVarColors = {{}}; // Maps varName -> colorIndex
|
|
33
|
+
|
|
34
|
+
function updateVarAssignSlots() {{
|
|
35
|
+
const container = document.getElementById('var-assign-slots');
|
|
36
|
+
const varAssignDiv = document.getElementById('datatable-var-assign');
|
|
37
|
+
if (!container || !varAssignDiv) return;
|
|
38
|
+
|
|
39
|
+
const info = PLOT_TYPE_HINTS[datatablePlotType];
|
|
40
|
+
if (!info || !datatableData) {{
|
|
41
|
+
varAssignDiv.style.display = 'none';
|
|
42
|
+
return;
|
|
43
|
+
}}
|
|
44
|
+
|
|
45
|
+
// Parse variables from hint to preserve signature order
|
|
46
|
+
const hintMatch = info.hint.match(/\\((.*)\\)/);
|
|
47
|
+
const argsStr = hintMatch ? hintMatch[1] : '';
|
|
48
|
+
|
|
49
|
+
// Parse each arg, preserving order and optional status
|
|
50
|
+
const allVars = [];
|
|
51
|
+
argsStr.split(',').forEach(arg => {{
|
|
52
|
+
arg = arg.trim();
|
|
53
|
+
if (!arg || arg === 'data') return;
|
|
54
|
+
const isOptional = arg.startsWith('[') && arg.endsWith(']');
|
|
55
|
+
const varName = arg.replace(/[\\[\\]]/g, '');
|
|
56
|
+
allVars.push({{ name: varName, optional: isOptional }});
|
|
57
|
+
}});
|
|
58
|
+
|
|
59
|
+
// Reset assignments and colors
|
|
60
|
+
datatableVarAssignments = {{}};
|
|
61
|
+
datatableVarColors = {{}};
|
|
62
|
+
|
|
63
|
+
// Assign colors to each variable (0-5 cycle)
|
|
64
|
+
allVars.forEach((v, idx) => {{
|
|
65
|
+
datatableVarColors[v.name] = idx % 6;
|
|
66
|
+
}});
|
|
67
|
+
|
|
68
|
+
// Build column options
|
|
69
|
+
const columns = datatableData.columns || [];
|
|
70
|
+
let colOptions = '<option value="">--</option>';
|
|
71
|
+
columns.forEach((col, idx) => {{
|
|
72
|
+
colOptions += `<option value="${{idx}}">${{col.name}}</option>`;
|
|
73
|
+
}});
|
|
74
|
+
|
|
75
|
+
// Build slots HTML in signature order with color classes
|
|
76
|
+
let html = '';
|
|
77
|
+
let requiredIdx = 0;
|
|
78
|
+
|
|
79
|
+
allVars.forEach((v, idx) => {{
|
|
80
|
+
const isOptional = v.optional;
|
|
81
|
+
const varName = v.name;
|
|
82
|
+
const colorClass = `var-color-${{idx % 6}}`;
|
|
83
|
+
|
|
84
|
+
if (isOptional) {{
|
|
85
|
+
html += `
|
|
86
|
+
<div class="var-assign-slot optional ${{colorClass}}" data-var="${{varName}}">
|
|
87
|
+
<span class="var-name">[${{varName}}]:</span>
|
|
88
|
+
<select onchange="assignVariable('${{varName}}', this.value, false)">
|
|
89
|
+
${{colOptions}}
|
|
90
|
+
</select>
|
|
91
|
+
</div>`;
|
|
92
|
+
}} else {{
|
|
93
|
+
const autoAssign = requiredIdx < columns.length ? requiredIdx : '';
|
|
94
|
+
if (autoAssign !== '') {{
|
|
95
|
+
datatableVarAssignments[varName] = autoAssign;
|
|
96
|
+
}}
|
|
97
|
+
const selectedOpt = autoAssign !== '' ? colOptions.replace(`value="${{autoAssign}}"`, `value="${{autoAssign}}" selected`) : colOptions;
|
|
98
|
+
html += `
|
|
99
|
+
<div class="var-assign-slot required ${{colorClass}}${{autoAssign !== '' ? ' assigned' : ''}}" data-var="${{varName}}">
|
|
100
|
+
<span class="var-name">${{varName}}:</span>
|
|
101
|
+
<select onchange="assignVariable('${{varName}}', this.value, true)">
|
|
102
|
+
${{selectedOpt}}
|
|
103
|
+
</select>
|
|
104
|
+
</div>`;
|
|
105
|
+
requiredIdx++;
|
|
106
|
+
}}
|
|
107
|
+
}});
|
|
108
|
+
|
|
109
|
+
container.innerHTML = html;
|
|
110
|
+
varAssignDiv.style.display = html ? 'block' : 'none';
|
|
111
|
+
|
|
112
|
+
updatePlotButtonState();
|
|
113
|
+
updateSelectionInfo();
|
|
114
|
+
updateColumnHighlights();
|
|
115
|
+
}}
|
|
116
|
+
|
|
117
|
+
function updateColumnHighlights() {{
|
|
118
|
+
// Remove all existing highlights from headers
|
|
119
|
+
document.querySelectorAll('.datatable-table th').forEach(th => {{
|
|
120
|
+
th.classList.remove('var-linked', 'var-color-0', 'var-color-1', 'var-color-2', 'var-color-3', 'var-color-4', 'var-color-5');
|
|
121
|
+
}});
|
|
122
|
+
|
|
123
|
+
// Remove highlights from cells
|
|
124
|
+
document.querySelectorAll('.datatable-table td.var-linked-cell').forEach(td => {{
|
|
125
|
+
td.classList.remove('var-linked-cell');
|
|
126
|
+
}});
|
|
127
|
+
|
|
128
|
+
// Add highlights based on current assignments
|
|
129
|
+
Object.entries(datatableVarAssignments).forEach(([varName, colIdx]) => {{
|
|
130
|
+
const colorIdx = datatableVarColors[varName];
|
|
131
|
+
if (colorIdx === undefined) return;
|
|
132
|
+
|
|
133
|
+
// Find the column header using data-col attribute (more reliable)
|
|
134
|
+
const th = document.querySelector(`.datatable-table th[data-col="${{colIdx}}"]`);
|
|
135
|
+
if (th) {{
|
|
136
|
+
th.classList.add('var-linked', `var-color-${{colorIdx}}`);
|
|
137
|
+
}}
|
|
138
|
+
|
|
139
|
+
// Also highlight all cells in this column
|
|
140
|
+
document.querySelectorAll(`.datatable-table td[data-col="${{colIdx}}"]`).forEach(td => {{
|
|
141
|
+
td.classList.add('var-linked-cell');
|
|
142
|
+
}});
|
|
143
|
+
}});
|
|
144
|
+
}}
|
|
145
|
+
|
|
146
|
+
function assignVariable(varName, colIdx, isRequired) {{
|
|
147
|
+
const slot = event.target.closest('.var-assign-slot');
|
|
148
|
+
|
|
149
|
+
if (colIdx === '' || colIdx === null) {{
|
|
150
|
+
delete datatableVarAssignments[varName];
|
|
151
|
+
if (slot) slot.classList.remove('assigned');
|
|
152
|
+
}} else {{
|
|
153
|
+
datatableVarAssignments[varName] = parseInt(colIdx);
|
|
154
|
+
if (slot) slot.classList.add('assigned');
|
|
155
|
+
}}
|
|
156
|
+
|
|
157
|
+
updatePlotButtonState();
|
|
158
|
+
updateSelectionInfo();
|
|
159
|
+
updateColumnHighlights();
|
|
160
|
+
}}
|
|
161
|
+
|
|
162
|
+
function updatePlotButtonState() {{
|
|
163
|
+
const plotBtn = document.getElementById('btn-datatable-plot');
|
|
164
|
+
if (!plotBtn) return;
|
|
165
|
+
|
|
166
|
+
const info = PLOT_TYPE_HINTS[datatablePlotType];
|
|
167
|
+
if (!info) {{
|
|
168
|
+
plotBtn.disabled = true;
|
|
169
|
+
return;
|
|
170
|
+
}}
|
|
171
|
+
|
|
172
|
+
// Check if all required variables are assigned
|
|
173
|
+
const requiredVars = info.required.split(',').map(s => s.trim()).filter(s => s && s !== 'data');
|
|
174
|
+
const allAssigned = requiredVars.length === 0 || requiredVars.every(v => datatableVarAssignments[v] !== undefined);
|
|
175
|
+
|
|
176
|
+
plotBtn.disabled = !allAssigned || !datatableData;
|
|
177
|
+
}}
|
|
178
|
+
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// Plot from Variable Assignments
|
|
181
|
+
// ============================================================================
|
|
182
|
+
async function plotFromVarAssignments() {{
|
|
183
|
+
if (!datatableData || Object.keys(datatableVarAssignments).length === 0) return;
|
|
184
|
+
|
|
185
|
+
// Build data and columns from variable assignments
|
|
186
|
+
const plotData = {{}};
|
|
187
|
+
const columns = [];
|
|
188
|
+
|
|
189
|
+
// Order matters: required vars first, then optional
|
|
190
|
+
const info = PLOT_TYPE_HINTS[datatablePlotType];
|
|
191
|
+
const requiredVars = info.required.split(',').map(s => s.trim()).filter(s => s && s !== 'data');
|
|
192
|
+
const optionalVars = info.optional.split(',').map(s => s.replace(/[\\[\\]]/g, '').trim()).filter(s => s);
|
|
193
|
+
|
|
194
|
+
[...requiredVars, ...optionalVars].forEach(varName => {{
|
|
195
|
+
const colIdx = datatableVarAssignments[varName];
|
|
196
|
+
if (colIdx !== undefined) {{
|
|
197
|
+
const col = datatableData.columns[colIdx];
|
|
198
|
+
plotData[col.name] = datatableData.rows.map(row => row[colIdx]);
|
|
199
|
+
columns.push(col.name);
|
|
200
|
+
}}
|
|
201
|
+
}});
|
|
202
|
+
|
|
203
|
+
if (columns.length === 0) return;
|
|
204
|
+
|
|
205
|
+
// Send to backend for plotting
|
|
206
|
+
if (typeof showSpinner === 'function') showSpinner();
|
|
207
|
+
|
|
208
|
+
try {{
|
|
209
|
+
const response = await fetch('/datatable/plot', {{
|
|
210
|
+
method: 'POST',
|
|
211
|
+
headers: {{ 'Content-Type': 'application/json' }},
|
|
212
|
+
body: JSON.stringify({{
|
|
213
|
+
data: plotData,
|
|
214
|
+
columns: columns,
|
|
215
|
+
plot_type: datatablePlotType,
|
|
216
|
+
target_axis: datatableTargetAxis
|
|
217
|
+
}})
|
|
218
|
+
}});
|
|
219
|
+
|
|
220
|
+
const result = await response.json();
|
|
221
|
+
|
|
222
|
+
if (result.success) {{
|
|
223
|
+
// Update preview
|
|
224
|
+
const previewImg = document.getElementById('preview-image');
|
|
225
|
+
if (previewImg && result.image) {{
|
|
226
|
+
previewImg.src = 'data:image/png;base64,' + result.image;
|
|
227
|
+
}}
|
|
228
|
+
|
|
229
|
+
// Update bboxes
|
|
230
|
+
if (result.bboxes && typeof currentBboxes !== 'undefined') {{
|
|
231
|
+
currentBboxes = result.bboxes;
|
|
232
|
+
}}
|
|
233
|
+
|
|
234
|
+
// Reload hitmap
|
|
235
|
+
if (typeof loadHitmap === 'function') loadHitmap();
|
|
236
|
+
|
|
237
|
+
// Refresh panel selector (in case new panel was added)
|
|
238
|
+
if (typeof updatePanelSelector === 'function') updatePanelSelector();
|
|
239
|
+
|
|
240
|
+
// Clear selection
|
|
241
|
+
if (typeof clearSelection === 'function') clearSelection();
|
|
242
|
+
}} else {{
|
|
243
|
+
console.error('Plot failed:', result.error);
|
|
244
|
+
alert('Failed to create plot: ' + (result.error || 'Unknown error'));
|
|
245
|
+
}}
|
|
246
|
+
}} catch (err) {{
|
|
247
|
+
console.error('Plot request failed:', err);
|
|
248
|
+
alert('Failed to create plot: ' + err.message);
|
|
249
|
+
}} finally {{
|
|
250
|
+
if (typeof hideSpinner === 'function') hideSpinner();
|
|
251
|
+
}}
|
|
252
|
+
}}
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
# For backward compatibility
|
|
257
|
+
JS_DATATABLE_PLOT = get_js_datatable_plot()
|
|
258
|
+
|
|
259
|
+
__all__ = ["JS_DATATABLE_PLOT", "get_js_datatable_plot"]
|
|
260
|
+
|
|
261
|
+
# EOF
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Datatable cell selection JavaScript - multi-cell selection and highlights."""
|
|
4
|
+
|
|
5
|
+
JS_DATATABLE_SELECTION = """
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Cell Selection State
|
|
8
|
+
// ============================================================================
|
|
9
|
+
let datatableCurrentCell = null; // { row, col } - active cell for input
|
|
10
|
+
let datatableSelectedCells = null; // { startRow, startCol, endRow, endCol } - range
|
|
11
|
+
let datatableIsSelecting = false; // Mouse drag selection in progress
|
|
12
|
+
let datatableEditMode = false; // Cell is being edited
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Cell Event Listeners (Event Delegation Pattern - vis_app)
|
|
16
|
+
// ============================================================================
|
|
17
|
+
let datatableListenersAttached = false;
|
|
18
|
+
|
|
19
|
+
function attachCellEventListeners() {
|
|
20
|
+
const table = document.querySelector('.datatable-table');
|
|
21
|
+
if (!table || datatableListenersAttached) return;
|
|
22
|
+
|
|
23
|
+
// Use event delegation - attach once to table, handle all cells
|
|
24
|
+
// This is efficient for large tables (100+ rows)
|
|
25
|
+
|
|
26
|
+
// Click to select cell (delegated)
|
|
27
|
+
table.addEventListener('click', handleCellClick);
|
|
28
|
+
|
|
29
|
+
// Double-click to edit (delegated)
|
|
30
|
+
table.addEventListener('dblclick', handleCellDoubleClick);
|
|
31
|
+
|
|
32
|
+
// Mouse drag for range selection (delegated)
|
|
33
|
+
table.addEventListener('mousedown', handleMouseDown);
|
|
34
|
+
|
|
35
|
+
// These need document-level for drag outside table
|
|
36
|
+
if (!datatableListenersAttached) {
|
|
37
|
+
document.addEventListener('mousemove', handleMouseMove);
|
|
38
|
+
document.addEventListener('mouseup', handleMouseUp);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Keyboard navigation and editing (delegated)
|
|
42
|
+
table.addEventListener('keydown', handleCellKeydown);
|
|
43
|
+
|
|
44
|
+
// Clipboard events (delegated)
|
|
45
|
+
table.addEventListener('copy', handleCopy);
|
|
46
|
+
table.addEventListener('paste', handlePaste);
|
|
47
|
+
table.addEventListener('cut', handleCut);
|
|
48
|
+
|
|
49
|
+
// Context menu (right-click)
|
|
50
|
+
if (typeof attachContextMenuListener === 'function') {
|
|
51
|
+
attachContextMenuListener();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
datatableListenersAttached = true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function handleCellClick(e) {
|
|
58
|
+
const cell = e.target.closest('td[data-row][data-col]');
|
|
59
|
+
if (!cell || datatableEditMode) return;
|
|
60
|
+
|
|
61
|
+
const row = parseInt(cell.dataset.row);
|
|
62
|
+
const col = parseInt(cell.dataset.col);
|
|
63
|
+
|
|
64
|
+
if (e.shiftKey && datatableCurrentCell) {
|
|
65
|
+
// Shift+click: extend selection
|
|
66
|
+
datatableSelectedCells = {
|
|
67
|
+
startRow: datatableCurrentCell.row,
|
|
68
|
+
startCol: datatableCurrentCell.col,
|
|
69
|
+
endRow: row,
|
|
70
|
+
endCol: col
|
|
71
|
+
};
|
|
72
|
+
} else {
|
|
73
|
+
// Regular click: single cell selection
|
|
74
|
+
datatableCurrentCell = { row, col };
|
|
75
|
+
datatableSelectedCells = {
|
|
76
|
+
startRow: row, startCol: col, endRow: row, endCol: col
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
updateCellSelectionDisplay();
|
|
81
|
+
cell.focus();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function handleCellDoubleClick(e) {
|
|
85
|
+
const cell = e.target.closest('td[data-row][data-col]');
|
|
86
|
+
if (!cell) return;
|
|
87
|
+
enterCellEditMode(cell);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function handleMouseDown(e) {
|
|
91
|
+
const cell = e.target.closest('td[data-row][data-col]');
|
|
92
|
+
if (!cell || datatableEditMode) return;
|
|
93
|
+
|
|
94
|
+
datatableIsSelecting = true;
|
|
95
|
+
const row = parseInt(cell.dataset.row);
|
|
96
|
+
const col = parseInt(cell.dataset.col);
|
|
97
|
+
datatableCurrentCell = { row, col };
|
|
98
|
+
datatableSelectedCells = {
|
|
99
|
+
startRow: row, startCol: col, endRow: row, endCol: col
|
|
100
|
+
};
|
|
101
|
+
updateCellSelectionDisplay();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function handleMouseMove(e) {
|
|
105
|
+
if (!datatableIsSelecting) return;
|
|
106
|
+
|
|
107
|
+
const elem = document.elementFromPoint(e.clientX, e.clientY);
|
|
108
|
+
const cell = elem?.closest('td[data-row][data-col]');
|
|
109
|
+
if (!cell) return;
|
|
110
|
+
|
|
111
|
+
const row = parseInt(cell.dataset.row);
|
|
112
|
+
const col = parseInt(cell.dataset.col);
|
|
113
|
+
datatableSelectedCells.endRow = row;
|
|
114
|
+
datatableSelectedCells.endCol = col;
|
|
115
|
+
updateCellSelectionDisplay();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function handleMouseUp() {
|
|
119
|
+
datatableIsSelecting = false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// Cell Selection Display
|
|
124
|
+
// ============================================================================
|
|
125
|
+
function updateCellSelectionDisplay() {
|
|
126
|
+
// Clear previous selection
|
|
127
|
+
document.querySelectorAll('.datatable-table td.cell-selected').forEach(td => {
|
|
128
|
+
td.classList.remove('cell-selected', 'cell-current');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!datatableSelectedCells) return;
|
|
132
|
+
|
|
133
|
+
const { startRow, startCol, endRow, endCol } = datatableSelectedCells;
|
|
134
|
+
const minRow = Math.min(startRow, endRow);
|
|
135
|
+
const maxRow = Math.max(startRow, endRow);
|
|
136
|
+
const minCol = Math.min(startCol, endCol);
|
|
137
|
+
const maxCol = Math.max(startCol, endCol);
|
|
138
|
+
|
|
139
|
+
// Highlight selected range
|
|
140
|
+
for (let r = minRow; r <= maxRow; r++) {
|
|
141
|
+
for (let c = minCol; c <= maxCol; c++) {
|
|
142
|
+
const cell = document.querySelector(
|
|
143
|
+
`td[data-row="${r}"][data-col="${c}"]`
|
|
144
|
+
);
|
|
145
|
+
if (cell) cell.classList.add('cell-selected');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Mark current cell
|
|
150
|
+
if (datatableCurrentCell) {
|
|
151
|
+
const current = document.querySelector(
|
|
152
|
+
`td[data-row="${datatableCurrentCell.row}"][data-col="${datatableCurrentCell.col}"]`
|
|
153
|
+
);
|
|
154
|
+
if (current) current.classList.add('cell-current');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function moveToNextCell(deltaRow, deltaCol) {
|
|
159
|
+
if (!datatableCurrentCell || !datatableData) return;
|
|
160
|
+
|
|
161
|
+
let newRow = datatableCurrentCell.row + deltaRow;
|
|
162
|
+
let newCol = datatableCurrentCell.col + deltaCol;
|
|
163
|
+
|
|
164
|
+
// Wrap around columns
|
|
165
|
+
if (newCol >= datatableData.columns.length) {
|
|
166
|
+
newCol = 0;
|
|
167
|
+
newRow++;
|
|
168
|
+
} else if (newCol < 0) {
|
|
169
|
+
newCol = datatableData.columns.length - 1;
|
|
170
|
+
newRow--;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Clamp rows
|
|
174
|
+
const maxRows = Math.min(datatableData.rows.length, 100);
|
|
175
|
+
newRow = Math.max(0, Math.min(newRow, maxRows - 1));
|
|
176
|
+
|
|
177
|
+
datatableCurrentCell = { row: newRow, col: newCol };
|
|
178
|
+
datatableSelectedCells = {
|
|
179
|
+
startRow: newRow, startCol: newCol, endRow: newRow, endCol: newCol
|
|
180
|
+
};
|
|
181
|
+
updateCellSelectionDisplay();
|
|
182
|
+
|
|
183
|
+
const nextCell = document.querySelector(
|
|
184
|
+
`td[data-row="${newRow}"][data-col="${newCol}"]`
|
|
185
|
+
);
|
|
186
|
+
if (nextCell) nextCell.focus();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Calculate next cell position (shared by both functions)
|
|
190
|
+
function calculateNextPosition(mode, reverse) {
|
|
191
|
+
if (!datatableCurrentCell || !datatableData) return null;
|
|
192
|
+
|
|
193
|
+
const maxRows = Math.min(datatableData.rows.length, datatableRenderedRows || 100) - 1;
|
|
194
|
+
const maxCols = datatableData.columns.length - 1;
|
|
195
|
+
|
|
196
|
+
// Get selection bounds (or just current cell if no selection)
|
|
197
|
+
let minRow = 0, minCol = 0, selMaxRow = maxRows, selMaxCol = maxCols;
|
|
198
|
+
const hasRangeSelection = datatableSelectedCells &&
|
|
199
|
+
(datatableSelectedCells.startRow !== datatableSelectedCells.endRow ||
|
|
200
|
+
datatableSelectedCells.startCol !== datatableSelectedCells.endCol);
|
|
201
|
+
|
|
202
|
+
if (hasRangeSelection) {
|
|
203
|
+
minRow = Math.min(datatableSelectedCells.startRow, datatableSelectedCells.endRow);
|
|
204
|
+
selMaxRow = Math.max(datatableSelectedCells.startRow, datatableSelectedCells.endRow);
|
|
205
|
+
minCol = Math.min(datatableSelectedCells.startCol, datatableSelectedCells.endCol);
|
|
206
|
+
selMaxCol = Math.max(datatableSelectedCells.startCol, datatableSelectedCells.endCol);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let { row, col } = datatableCurrentCell;
|
|
210
|
+
|
|
211
|
+
if (mode === 'tab') {
|
|
212
|
+
// Tab: horizontal movement (left-to-right, wrap to next row)
|
|
213
|
+
if (!reverse) {
|
|
214
|
+
if (col < selMaxCol) {
|
|
215
|
+
col++;
|
|
216
|
+
} else {
|
|
217
|
+
col = minCol;
|
|
218
|
+
row = row < selMaxRow ? row + 1 : minRow;
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
if (col > minCol) {
|
|
222
|
+
col--;
|
|
223
|
+
} else {
|
|
224
|
+
col = selMaxCol;
|
|
225
|
+
row = row > minRow ? row - 1 : selMaxRow;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
// Enter: vertical movement (top-to-bottom, wrap to next column)
|
|
230
|
+
if (!reverse) {
|
|
231
|
+
if (row < selMaxRow) {
|
|
232
|
+
row++;
|
|
233
|
+
} else {
|
|
234
|
+
row = minRow;
|
|
235
|
+
col = col < selMaxCol ? col + 1 : minCol;
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
if (row > minRow) {
|
|
239
|
+
row--;
|
|
240
|
+
} else {
|
|
241
|
+
row = selMaxRow;
|
|
242
|
+
col = col > minCol ? col - 1 : selMaxCol;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { row, col, hasRangeSelection };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Smart Tab/Enter navigation (vis_app pattern) - selection mode only (no edit)
|
|
251
|
+
// Tab: horizontal (left-to-right, wrap to next row)
|
|
252
|
+
// Enter: vertical (top-to-bottom, wrap to next column)
|
|
253
|
+
function navigateWithTabEnter(mode, reverse) {
|
|
254
|
+
const result = calculateNextPosition(mode, reverse);
|
|
255
|
+
if (!result) return;
|
|
256
|
+
|
|
257
|
+
const { row, col, hasRangeSelection } = result;
|
|
258
|
+
|
|
259
|
+
datatableCurrentCell = { row, col };
|
|
260
|
+
|
|
261
|
+
// If we have a range selection, keep it; otherwise move selection too
|
|
262
|
+
if (!hasRangeSelection) {
|
|
263
|
+
datatableSelectedCells = {
|
|
264
|
+
startRow: row, startCol: col, endRow: row, endCol: col
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
updateCellSelectionDisplay();
|
|
269
|
+
|
|
270
|
+
const nextCell = document.querySelector(`td[data-row="${row}"][data-col="${col}"]`);
|
|
271
|
+
if (nextCell) nextCell.focus();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Smart Tab/Enter navigation AND enter edit mode (scitex-cloud pattern)
|
|
275
|
+
// Used when exiting edit mode with Tab/Enter to continue editing in next cell
|
|
276
|
+
function navigateWithTabEnterAndEdit(mode, reverse) {
|
|
277
|
+
const result = calculateNextPosition(mode, reverse);
|
|
278
|
+
if (!result) return;
|
|
279
|
+
|
|
280
|
+
const { row, col, hasRangeSelection } = result;
|
|
281
|
+
|
|
282
|
+
datatableCurrentCell = { row, col };
|
|
283
|
+
|
|
284
|
+
// If we have a range selection, keep it; otherwise move selection too
|
|
285
|
+
if (!hasRangeSelection) {
|
|
286
|
+
datatableSelectedCells = {
|
|
287
|
+
startRow: row, startCol: col, endRow: row, endCol: col
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
updateCellSelectionDisplay();
|
|
292
|
+
|
|
293
|
+
const nextCell = document.querySelector(`td[data-row="${row}"][data-col="${col}"]`);
|
|
294
|
+
if (nextCell) {
|
|
295
|
+
// Enter edit mode on the next cell (scitex-cloud behavior)
|
|
296
|
+
enterCellEditMode(nextCell);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ============================================================================
|
|
301
|
+
// Keyboard Navigation
|
|
302
|
+
// ============================================================================
|
|
303
|
+
function handleCellKeydown(e) {
|
|
304
|
+
if (datatableEditMode) return;
|
|
305
|
+
|
|
306
|
+
const cell = e.target.closest('td[data-row][data-col]');
|
|
307
|
+
if (!cell) return;
|
|
308
|
+
|
|
309
|
+
switch (e.key) {
|
|
310
|
+
case 'ArrowUp':
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
moveToNextCell(-1, 0);
|
|
313
|
+
break;
|
|
314
|
+
case 'ArrowDown':
|
|
315
|
+
e.preventDefault();
|
|
316
|
+
moveToNextCell(1, 0);
|
|
317
|
+
break;
|
|
318
|
+
case 'ArrowLeft':
|
|
319
|
+
e.preventDefault();
|
|
320
|
+
moveToNextCell(0, -1);
|
|
321
|
+
break;
|
|
322
|
+
case 'ArrowRight':
|
|
323
|
+
e.preventDefault();
|
|
324
|
+
moveToNextCell(0, 1);
|
|
325
|
+
break;
|
|
326
|
+
case 'Tab':
|
|
327
|
+
e.preventDefault();
|
|
328
|
+
navigateWithTabEnter('tab', e.shiftKey);
|
|
329
|
+
break;
|
|
330
|
+
case 'Enter':
|
|
331
|
+
e.preventDefault();
|
|
332
|
+
navigateWithTabEnter('enter', e.shiftKey);
|
|
333
|
+
break;
|
|
334
|
+
case 'F2':
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
enterCellEditMode(cell);
|
|
337
|
+
break;
|
|
338
|
+
case 'Delete':
|
|
339
|
+
case 'Backspace':
|
|
340
|
+
e.preventDefault();
|
|
341
|
+
clearSelectedCells();
|
|
342
|
+
break;
|
|
343
|
+
default:
|
|
344
|
+
// Type to start editing
|
|
345
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
|
|
346
|
+
enterCellEditMode(cell);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function clearSelectedCells() {
|
|
352
|
+
if (!datatableSelectedCells || !datatableData) return;
|
|
353
|
+
|
|
354
|
+
const { startRow, startCol, endRow, endCol } = datatableSelectedCells;
|
|
355
|
+
const minRow = Math.min(startRow, endRow);
|
|
356
|
+
const maxRow = Math.max(startRow, endRow);
|
|
357
|
+
const minCol = Math.min(startCol, endCol);
|
|
358
|
+
const maxCol = Math.max(startCol, endCol);
|
|
359
|
+
|
|
360
|
+
for (let r = minRow; r <= maxRow; r++) {
|
|
361
|
+
for (let c = minCol; c <= maxCol; c++) {
|
|
362
|
+
if (datatableData.rows[r]) {
|
|
363
|
+
datatableData.rows[r][c] = '';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
renderDatatable();
|
|
369
|
+
updateCellSelectionDisplay();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ============================================================================
|
|
373
|
+
// Shortcuts Info Popup
|
|
374
|
+
// ============================================================================
|
|
375
|
+
let shortcutsPopup = null;
|
|
376
|
+
|
|
377
|
+
function showShortcutsPopup() {
|
|
378
|
+
if (!shortcutsPopup) {
|
|
379
|
+
shortcutsPopup = document.createElement('div');
|
|
380
|
+
shortcutsPopup.className = 'shortcuts-popup';
|
|
381
|
+
shortcutsPopup.innerHTML = `
|
|
382
|
+
<div class="shortcuts-header">
|
|
383
|
+
<h4>Keyboard Shortcuts</h4>
|
|
384
|
+
<button class="btn-close" onclick="hideShortcutsPopup()">×</button>
|
|
385
|
+
</div>
|
|
386
|
+
<div class="shortcuts-content">
|
|
387
|
+
<div class="shortcut-group">
|
|
388
|
+
<div class="shortcut-row"><kbd>Tab</kbd> Move right (wrap to next row)</div>
|
|
389
|
+
<div class="shortcut-row"><kbd>Shift+Tab</kbd> Move left</div>
|
|
390
|
+
<div class="shortcut-row"><kbd>Enter</kbd> Move down (wrap to next col)</div>
|
|
391
|
+
<div class="shortcut-row"><kbd>Shift+Enter</kbd> Move up</div>
|
|
392
|
+
</div>
|
|
393
|
+
<div class="shortcut-group">
|
|
394
|
+
<div class="shortcut-row"><kbd>F2</kbd> Edit cell</div>
|
|
395
|
+
<div class="shortcut-row"><kbd>Esc</kbd> Cancel edit</div>
|
|
396
|
+
<div class="shortcut-row"><kbd>Delete</kbd> Clear selected cells</div>
|
|
397
|
+
</div>
|
|
398
|
+
<div class="shortcut-group">
|
|
399
|
+
<div class="shortcut-row"><kbd>Ctrl+C</kbd> Copy</div>
|
|
400
|
+
<div class="shortcut-row"><kbd>Ctrl+X</kbd> Cut</div>
|
|
401
|
+
<div class="shortcut-row"><kbd>Ctrl+V</kbd> Paste</div>
|
|
402
|
+
</div>
|
|
403
|
+
<div class="shortcut-group">
|
|
404
|
+
<div class="shortcut-row"><kbd>Click</kbd> Select cell</div>
|
|
405
|
+
<div class="shortcut-row"><kbd>Shift+Click</kbd> Extend selection</div>
|
|
406
|
+
<div class="shortcut-row"><kbd>Drag</kbd> Range selection</div>
|
|
407
|
+
<div class="shortcut-row"><kbd>Right-click</kbd> Context menu</div>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
`;
|
|
411
|
+
document.body.appendChild(shortcutsPopup);
|
|
412
|
+
}
|
|
413
|
+
shortcutsPopup.style.display = 'block';
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function hideShortcutsPopup() {
|
|
417
|
+
if (shortcutsPopup) {
|
|
418
|
+
shortcutsPopup.style.display = 'none';
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Attach shortcuts button listener
|
|
423
|
+
function initShortcutsButton() {
|
|
424
|
+
const btn = document.getElementById('btn-shortcuts-info');
|
|
425
|
+
if (btn) {
|
|
426
|
+
btn.addEventListener('click', showShortcutsPopup);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Initialize on load
|
|
431
|
+
if (typeof window !== 'undefined') {
|
|
432
|
+
window.addEventListener('DOMContentLoaded', initShortcutsButton);
|
|
433
|
+
}
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
__all__ = ["JS_DATATABLE_SELECTION"]
|
|
437
|
+
|
|
438
|
+
# EOF
|