figrecipe 0.7.4__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 +74 -76
- figrecipe/__main__.py +12 -0
- figrecipe/_api/_panel.py +67 -0
- figrecipe/_api/_save.py +100 -4
- 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 +2 -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/_editor/__init__.py +36 -36
- figrecipe/_editor/_bbox/_extract.py +155 -9
- figrecipe/_editor/_bbox/_extract_text.py +124 -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 +157 -16
- figrecipe/_editor/_helpers.py +17 -8
- figrecipe/_editor/_hitmap/_detect.py +89 -32
- figrecipe/_editor/_hitmap_main.py +4 -4
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_render_overrides.py +38 -11
- figrecipe/_editor/_renderer.py +46 -1
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +35 -6
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +15 -173
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +37 -19
- 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 +28 -8
- figrecipe/_editor/_templates/__init__.py +40 -2
- figrecipe/_editor/_templates/_html.py +97 -103
- 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 +58 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +1 -1
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +94 -37
- 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/_element_editor.py +17 -2
- figrecipe/_editor/_templates/_scripts/_files.py +274 -40
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
- 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 +8 -1
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
- figrecipe/_editor/_templates/_styles/__init__.py +9 -0
- figrecipe/_editor/_templates/_styles/_base.py +47 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +168 -3
- 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 +5 -5
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +98 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
- figrecipe/_editor/_templates/_styles/_modals.py +29 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
- figrecipe/_editor/_templates/_styles/_preview.py +213 -8
- 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 +2 -0
- figrecipe/_recorder.py +28 -3
- figrecipe/_reproducer/_core.py +60 -49
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +150 -2
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +26 -1
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/styles/_style_applier.py +10 -2
- figrecipe/styles/presets/SCITEX.yaml +11 -4
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe-0.7.4.dist-info/RECORD +0 -188
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
SCRIPTS_CORE = """
|
|
6
6
|
// ==================== CORE STATE & INITIALIZATION ====================
|
|
7
7
|
|
|
8
|
+
// Debug mode - enabled via FIGRECIPE_DEBUG_MODE=1 env var
|
|
9
|
+
const DEBUG_MODE = DEBUG_MODE_PLACEHOLDER;
|
|
10
|
+
|
|
8
11
|
// State
|
|
9
12
|
let currentBboxes = initialBboxes;
|
|
10
13
|
let colorMap = initialColorMap;
|
|
@@ -34,6 +37,7 @@ const ZOOM_MIN = 0.1;
|
|
|
34
37
|
const ZOOM_MAX = 5.0;
|
|
35
38
|
const ZOOM_STEP = 0.25;
|
|
36
39
|
let isPanning = false;
|
|
40
|
+
let panTarget = null; // Current scrollable element being panned
|
|
37
41
|
let panStartX = 0;
|
|
38
42
|
let panStartY = 0;
|
|
39
43
|
let scrollStartX = 0;
|
|
@@ -86,6 +90,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
86
90
|
|
|
87
91
|
// Initialize measurement overlay controls
|
|
88
92
|
initializeOverlayControls();
|
|
93
|
+
|
|
94
|
+
// Initialize context menus
|
|
95
|
+
if (typeof initializeCanvasContextMenu === 'function') initializeCanvasContextMenu();
|
|
96
|
+
if (typeof initializeFilesContextMenu === 'function') initializeFilesContextMenu();
|
|
89
97
|
});
|
|
90
98
|
|
|
91
99
|
// Theme values are passed from server via initialValues
|
|
@@ -164,38 +172,43 @@ function updateAllModifiedStates() {
|
|
|
164
172
|
function initializeEventListeners() {
|
|
165
173
|
// Preview image click for element selection
|
|
166
174
|
const previewImg = document.getElementById('preview-image');
|
|
167
|
-
previewImg.addEventListener('click', handlePreviewClick);
|
|
175
|
+
if (previewImg) previewImg.addEventListener('click', handlePreviewClick);
|
|
168
176
|
|
|
169
177
|
// SVG overlay click - deselect when clicking on empty area (not on a shape)
|
|
170
178
|
const hitregionOverlay = document.getElementById('hitregion-overlay');
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
});
|
|
179
|
+
if (hitregionOverlay) {
|
|
180
|
+
hitregionOverlay.addEventListener('click', function(event) {
|
|
181
|
+
if (event.target === hitregionOverlay) clearSelection();
|
|
182
|
+
});
|
|
183
|
+
}
|
|
177
184
|
|
|
178
185
|
// Selection overlay click - same behavior
|
|
179
186
|
const selectionOverlay = document.getElementById('selection-overlay');
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
clearSelection();
|
|
183
|
-
}
|
|
184
|
-
}
|
|
187
|
+
if (selectionOverlay) {
|
|
188
|
+
selectionOverlay.addEventListener('click', function(event) {
|
|
189
|
+
if (event.target === selectionOverlay) clearSelection();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
185
192
|
|
|
186
|
-
// Dark mode toggle
|
|
193
|
+
// Dark mode toggle button
|
|
187
194
|
const darkModeToggle = document.getElementById('dark-mode-toggle');
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
195
|
+
if (darkModeToggle) {
|
|
196
|
+
const updateThemeIcon = (theme) => { darkModeToggle.textContent = theme === 'dark' ? '🌙' : '☀️'; };
|
|
197
|
+
darkModeToggle.addEventListener('click', function() {
|
|
198
|
+
const currentTheme = document.documentElement.getAttribute('data-theme');
|
|
199
|
+
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
200
|
+
document.documentElement.setAttribute('data-theme', newTheme);
|
|
201
|
+
updateThemeIcon(newTheme);
|
|
202
|
+
scheduleUpdate();
|
|
203
|
+
});
|
|
204
|
+
updateThemeIcon(document.documentElement.getAttribute('data-theme'));
|
|
205
|
+
}
|
|
192
206
|
|
|
193
207
|
// Form inputs - auto update on change
|
|
194
208
|
// Exclude panel position inputs - they have their own Apply button
|
|
195
|
-
const panelPositionInputIds = ['panel_left', 'panel_top', 'panel_width', 'panel_height'
|
|
209
|
+
const panelPositionInputIds = ['panel_left', 'panel_top', 'panel_width', 'panel_height'];
|
|
196
210
|
const inputs = document.querySelectorAll('input, select');
|
|
197
211
|
inputs.forEach(input => {
|
|
198
|
-
if (input.id === 'dark-mode-toggle') return;
|
|
199
212
|
if (panelPositionInputIds.includes(input.id)) return; // Skip panel position inputs
|
|
200
213
|
|
|
201
214
|
// Update modified state and trigger preview update
|
|
@@ -220,31 +233,36 @@ function initializeEventListeners() {
|
|
|
220
233
|
}
|
|
221
234
|
});
|
|
222
235
|
|
|
223
|
-
// Buttons
|
|
224
|
-
document.getElementById('btn-refresh')
|
|
225
|
-
document.getElementById('btn-reset')
|
|
226
|
-
document.getElementById('btn-save')
|
|
227
|
-
document.getElementById('btn-restore')
|
|
228
|
-
|
|
236
|
+
// Buttons - with null checks
|
|
237
|
+
const btnRefresh = document.getElementById('btn-refresh');
|
|
238
|
+
const btnReset = document.getElementById('btn-reset');
|
|
239
|
+
const btnSave = document.getElementById('btn-save');
|
|
240
|
+
const btnRestore = document.getElementById('btn-restore');
|
|
241
|
+
if (btnRefresh) btnRefresh.addEventListener('click', updatePreview);
|
|
242
|
+
if (btnReset) btnReset.addEventListener('click', resetValues);
|
|
243
|
+
if (btnSave) btnSave.addEventListener('click', saveOverrides);
|
|
244
|
+
if (btnRestore) btnRestore.addEventListener('click', restoreOriginal);
|
|
229
245
|
const hitmapBtn = document.getElementById('btn-show-hitmap');
|
|
230
246
|
if (hitmapBtn) hitmapBtn.addEventListener('click', toggleHitmapOverlay);
|
|
231
247
|
|
|
232
|
-
// Download dropdown
|
|
248
|
+
// Download dropdown, label inputs, and captions
|
|
233
249
|
initializeDownloadDropdown();
|
|
234
|
-
|
|
235
|
-
// Label input handlers
|
|
236
250
|
initializeLabelInputs();
|
|
251
|
+
if (typeof initializeCaptionInputs === 'function') initializeCaptionInputs();
|
|
237
252
|
|
|
238
|
-
// View mode toggle buttons (legacy
|
|
253
|
+
// View mode toggle buttons (legacy)
|
|
239
254
|
const btnAll = document.getElementById('btn-show-all');
|
|
240
255
|
const btnSelected = document.getElementById('btn-show-selected');
|
|
241
256
|
if (btnAll) btnAll.addEventListener('click', () => setViewMode('all'));
|
|
242
257
|
if (btnSelected) btnSelected.addEventListener('click', () => setViewMode('selected'));
|
|
243
258
|
|
|
244
259
|
// Tab navigation
|
|
245
|
-
document.getElementById('tab-figure')
|
|
246
|
-
document.getElementById('tab-axis')
|
|
247
|
-
document.getElementById('tab-element')
|
|
260
|
+
const tabFigure = document.getElementById('tab-figure');
|
|
261
|
+
const tabAxis = document.getElementById('tab-axis');
|
|
262
|
+
const tabElement = document.getElementById('tab-element');
|
|
263
|
+
if (tabFigure) tabFigure.addEventListener('click', () => switchTab('figure'));
|
|
264
|
+
if (tabAxis) tabAxis.addEventListener('click', () => switchTab('axis'));
|
|
265
|
+
if (tabElement) tabElement.addEventListener('click', () => switchTab('element'));
|
|
248
266
|
|
|
249
267
|
// Theme modal handlers
|
|
250
268
|
initializeThemeModal();
|
|
@@ -282,6 +300,26 @@ function handleKeyboardShortcuts(event) {
|
|
|
282
300
|
return;
|
|
283
301
|
}
|
|
284
302
|
|
|
303
|
+
// Alt+I (without Ctrl): Element Inspector toggle (DEBUG MODE ONLY)
|
|
304
|
+
if (DEBUG_MODE && event.altKey && !event.ctrlKey && !event.shiftKey && (event.key === 'i' || event.key === 'I')) {
|
|
305
|
+
event.preventDefault();
|
|
306
|
+
event.stopPropagation();
|
|
307
|
+
if (typeof toggleElementInspector === 'function') {
|
|
308
|
+
toggleElementInspector();
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Alt+B: Show All Bboxes toggle (DEBUG MODE ONLY)
|
|
314
|
+
if (DEBUG_MODE && event.altKey && !event.ctrlKey && !event.shiftKey && (event.key === 'b' || event.key === 'B')) {
|
|
315
|
+
event.preventDefault();
|
|
316
|
+
event.stopPropagation();
|
|
317
|
+
if (typeof toggleAllBboxes === 'function') {
|
|
318
|
+
toggleAllBboxes();
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
285
323
|
// Ctrl+S: Save overrides
|
|
286
324
|
if (event.ctrlKey && event.key === 's') {
|
|
287
325
|
event.preventDefault();
|
|
@@ -299,6 +337,25 @@ function handleKeyboardShortcuts(event) {
|
|
|
299
337
|
return;
|
|
300
338
|
}
|
|
301
339
|
|
|
340
|
+
// Ctrl+Z: Undo
|
|
341
|
+
if (event.ctrlKey && !event.shiftKey && event.key === 'z') {
|
|
342
|
+
event.preventDefault();
|
|
343
|
+
if (typeof undo === 'function') {
|
|
344
|
+
undo();
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Ctrl+Shift+Z or Ctrl+Y: Redo
|
|
350
|
+
if ((event.ctrlKey && event.shiftKey && event.key === 'Z') ||
|
|
351
|
+
(event.ctrlKey && event.key === 'y')) {
|
|
352
|
+
event.preventDefault();
|
|
353
|
+
if (typeof redo === 'function') {
|
|
354
|
+
redo();
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
302
359
|
// Ctrl+Shift+S: Download PNG
|
|
303
360
|
if (event.ctrlKey && event.shiftKey && event.key === 'S') {
|
|
304
361
|
event.preventDefault();
|
|
@@ -306,11 +363,11 @@ function handleKeyboardShortcuts(event) {
|
|
|
306
363
|
return;
|
|
307
364
|
}
|
|
308
365
|
|
|
309
|
-
// F5 or Ctrl+R:
|
|
366
|
+
// F5 or Ctrl+R: Render preview
|
|
310
367
|
if (event.key === 'F5' || (event.ctrlKey && event.key === 'r')) {
|
|
311
368
|
event.preventDefault();
|
|
312
369
|
updatePreview();
|
|
313
|
-
showToast('
|
|
370
|
+
showToast('Rendered', 'info');
|
|
314
371
|
return;
|
|
315
372
|
}
|
|
316
373
|
|
|
@@ -342,10 +399,10 @@ function handleKeyboardShortcuts(event) {
|
|
|
342
399
|
return;
|
|
343
400
|
}
|
|
344
401
|
|
|
345
|
-
// R:
|
|
402
|
+
// R: Render (re-render figure)
|
|
346
403
|
if (event.key === 'r' || event.key === 'R') {
|
|
347
|
-
|
|
348
|
-
showToast('
|
|
404
|
+
updatePreview();
|
|
405
|
+
showToast('Rendered', 'info');
|
|
349
406
|
return;
|
|
350
407
|
}
|
|
351
408
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Datatable JavaScript modules - orchestrator.
|
|
4
|
+
|
|
5
|
+
Combines all datatable JavaScript modules:
|
|
6
|
+
- _core.py: State, panel toggle, initialization
|
|
7
|
+
- _tabs.py: Multi-tab management for multiple datasets
|
|
8
|
+
- _import.py: Drag-drop, file parsing
|
|
9
|
+
- _table.py: Table rendering with smart truncation
|
|
10
|
+
- _selection.py: Multi-cell selection and highlights
|
|
11
|
+
- _cell_edit.py: Inline cell editing
|
|
12
|
+
- _clipboard.py: Copy, paste, cut operations
|
|
13
|
+
- _plot.py: Plot type hints, variable assignment, plotting
|
|
14
|
+
- _editable.py: Create and edit tables manually
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from ._cell_edit import JS_DATATABLE_CELL_EDIT
|
|
18
|
+
from ._clipboard import JS_DATATABLE_CLIPBOARD
|
|
19
|
+
from ._context_menu import JS_DATATABLE_CONTEXT_MENU
|
|
20
|
+
from ._core import JS_DATATABLE_CORE
|
|
21
|
+
from ._editable import JS_DATATABLE_EDITABLE
|
|
22
|
+
from ._import import JS_DATATABLE_IMPORT
|
|
23
|
+
from ._plot import get_js_datatable_plot
|
|
24
|
+
from ._selection import JS_DATATABLE_SELECTION
|
|
25
|
+
from ._table import JS_DATATABLE_TABLE
|
|
26
|
+
from ._tabs import JS_DATATABLE_TABS
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_scripts_datatable() -> str:
|
|
30
|
+
"""Generate combined datatable JavaScript."""
|
|
31
|
+
return (
|
|
32
|
+
JS_DATATABLE_CORE
|
|
33
|
+
+ "\n"
|
|
34
|
+
+ JS_DATATABLE_TABS
|
|
35
|
+
+ "\n"
|
|
36
|
+
+ JS_DATATABLE_IMPORT
|
|
37
|
+
+ "\n"
|
|
38
|
+
+ JS_DATATABLE_TABLE
|
|
39
|
+
+ "\n"
|
|
40
|
+
+ JS_DATATABLE_SELECTION
|
|
41
|
+
+ "\n"
|
|
42
|
+
+ JS_DATATABLE_CELL_EDIT
|
|
43
|
+
+ "\n"
|
|
44
|
+
+ JS_DATATABLE_CLIPBOARD
|
|
45
|
+
+ "\n"
|
|
46
|
+
+ JS_DATATABLE_CONTEXT_MENU
|
|
47
|
+
+ "\n"
|
|
48
|
+
+ JS_DATATABLE_EDITABLE
|
|
49
|
+
+ "\n"
|
|
50
|
+
+ get_js_datatable_plot()
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# For backward compatibility
|
|
55
|
+
SCRIPTS_DATATABLE = get_scripts_datatable()
|
|
56
|
+
|
|
57
|
+
__all__ = ["SCRIPTS_DATATABLE", "get_scripts_datatable"]
|
|
58
|
+
|
|
59
|
+
# EOF
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Datatable inline cell editing JavaScript."""
|
|
4
|
+
|
|
5
|
+
JS_DATATABLE_CELL_EDIT = """
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Cell Editing (Inline Edit Mode)
|
|
8
|
+
// ============================================================================
|
|
9
|
+
let datatableEditingCell = null; // Track which cell is being edited
|
|
10
|
+
let datatableSkipBlur = false; // Flag to skip blur during Tab/Enter navigation
|
|
11
|
+
|
|
12
|
+
function enterCellEditMode(cell) {
|
|
13
|
+
if (datatableEditMode) return;
|
|
14
|
+
|
|
15
|
+
datatableEditMode = true;
|
|
16
|
+
datatableEditingCell = cell;
|
|
17
|
+
cell.classList.add('cell-editing');
|
|
18
|
+
|
|
19
|
+
const span = cell.querySelector('.cell-text');
|
|
20
|
+
const originalValue = span ? span.textContent : '';
|
|
21
|
+
|
|
22
|
+
// Replace span with input
|
|
23
|
+
cell.innerHTML = `<input type="text" class="cell-edit-input" value="${originalValue}">`;
|
|
24
|
+
const input = cell.querySelector('input');
|
|
25
|
+
input.focus();
|
|
26
|
+
input.select();
|
|
27
|
+
|
|
28
|
+
// Handle input events
|
|
29
|
+
input.addEventListener('blur', (e) => {
|
|
30
|
+
// Skip if Tab/Enter is handling navigation, or if this isn't the editing cell
|
|
31
|
+
if (datatableSkipBlur || datatableEditingCell !== cell) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (datatableEditMode && datatableEditingCell === cell) {
|
|
35
|
+
exitCellEditMode(cell, input.value, false);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
input.addEventListener('keydown', (e) => {
|
|
39
|
+
if (e.key === 'Enter') {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
datatableSkipBlur = true; // Prevent blur from interfering
|
|
42
|
+
exitCellEditMode(cell, input.value, true);
|
|
43
|
+
navigateWithTabEnterAndEdit('enter', e.shiftKey);
|
|
44
|
+
datatableSkipBlur = false;
|
|
45
|
+
} else if (e.key === 'Escape') {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
exitCellEditMode(cell, originalValue, false);
|
|
48
|
+
} else if (e.key === 'Tab') {
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
datatableSkipBlur = true; // Prevent blur from interfering
|
|
51
|
+
exitCellEditMode(cell, input.value, true);
|
|
52
|
+
navigateWithTabEnterAndEdit('tab', e.shiftKey);
|
|
53
|
+
datatableSkipBlur = false;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function exitCellEditMode(cell, value, continueEditing = false) {
|
|
59
|
+
if (!datatableEditMode) return;
|
|
60
|
+
|
|
61
|
+
const row = parseInt(cell.dataset.row);
|
|
62
|
+
const col = parseInt(cell.dataset.col);
|
|
63
|
+
|
|
64
|
+
// Update data - preserve empty values as empty (not 0)
|
|
65
|
+
if (datatableData && datatableData.rows[row]) {
|
|
66
|
+
if (value === '' || value === null || value === undefined) {
|
|
67
|
+
datatableData.rows[row][col] = '';
|
|
68
|
+
} else {
|
|
69
|
+
const colType = datatableData.columns[col]?.type;
|
|
70
|
+
if (colType === 'numeric') {
|
|
71
|
+
const num = parseFloat(value);
|
|
72
|
+
datatableData.rows[row][col] = isNaN(num) ? value : num;
|
|
73
|
+
} else {
|
|
74
|
+
datatableData.rows[row][col] = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Restore cell display with span wrapper
|
|
80
|
+
const displayValue = value === null || value === undefined ? '' : value;
|
|
81
|
+
cell.innerHTML = `<span class="cell-text">${displayValue}</span>`;
|
|
82
|
+
cell.classList.remove('cell-editing');
|
|
83
|
+
cell.setAttribute('title', displayValue);
|
|
84
|
+
|
|
85
|
+
datatableEditMode = false;
|
|
86
|
+
datatableEditingCell = null; // Clear editing cell reference
|
|
87
|
+
|
|
88
|
+
// Only focus this cell if we're NOT continuing to next cell
|
|
89
|
+
if (!continueEditing) {
|
|
90
|
+
cell.focus();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
__all__ = ["JS_DATATABLE_CELL_EDIT"]
|
|
96
|
+
|
|
97
|
+
# EOF
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Datatable clipboard operations JavaScript - copy, paste, cut."""
|
|
4
|
+
|
|
5
|
+
JS_DATATABLE_CLIPBOARD = """
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Clipboard Operations (Copy, Paste, Cut)
|
|
8
|
+
// ============================================================================
|
|
9
|
+
let datatableCopiedRange = null; // Track copied cell range for marching ants
|
|
10
|
+
let datatableIsCutOperation = false; // Track if it was cut (not copy)
|
|
11
|
+
|
|
12
|
+
function handleCopy(e) {
|
|
13
|
+
if (!datatableSelectedCells || datatableEditMode) return;
|
|
14
|
+
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
const text = getSelectedCellsAsTSV();
|
|
17
|
+
e.clipboardData.setData('text/plain', text);
|
|
18
|
+
|
|
19
|
+
// Store copied range and show marching ants
|
|
20
|
+
datatableCopiedRange = { ...datatableSelectedCells };
|
|
21
|
+
datatableIsCutOperation = false;
|
|
22
|
+
showMarchingAnts();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function handleCut(e) {
|
|
26
|
+
if (!datatableSelectedCells || datatableEditMode) return;
|
|
27
|
+
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
const text = getSelectedCellsAsTSV();
|
|
30
|
+
e.clipboardData.setData('text/plain', text);
|
|
31
|
+
|
|
32
|
+
// Store cut range and show marching ants with faded cells
|
|
33
|
+
datatableCopiedRange = { ...datatableSelectedCells };
|
|
34
|
+
datatableIsCutOperation = true;
|
|
35
|
+
showMarchingAnts();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Show Excel-style marching ants border around copied/cut cells
|
|
39
|
+
function showMarchingAnts() {
|
|
40
|
+
clearMarchingAnts(); // Clear any existing marching ants
|
|
41
|
+
if (!datatableCopiedRange) return;
|
|
42
|
+
|
|
43
|
+
const { startRow, startCol, endRow, endCol } = datatableCopiedRange;
|
|
44
|
+
const minRow = Math.min(startRow, endRow);
|
|
45
|
+
const maxRow = Math.max(startRow, endRow);
|
|
46
|
+
const minCol = Math.min(startCol, endCol);
|
|
47
|
+
const maxCol = Math.max(startCol, endCol);
|
|
48
|
+
|
|
49
|
+
for (let r = minRow; r <= maxRow; r++) {
|
|
50
|
+
for (let c = minCol; c <= maxCol; c++) {
|
|
51
|
+
const cell = document.querySelector(`td[data-row="${r}"][data-col="${c}"]`);
|
|
52
|
+
if (!cell) continue;
|
|
53
|
+
|
|
54
|
+
// Add border classes based on position in selection
|
|
55
|
+
if (r === minRow) cell.classList.add('copy-border-top');
|
|
56
|
+
if (r === maxRow) cell.classList.add('copy-border-bottom');
|
|
57
|
+
if (c === minCol) cell.classList.add('copy-border-left');
|
|
58
|
+
if (c === maxCol) cell.classList.add('copy-border-right');
|
|
59
|
+
|
|
60
|
+
// Add faded effect for cut operation
|
|
61
|
+
if (datatableIsCutOperation) {
|
|
62
|
+
cell.classList.add('cut-pending');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Clear marching ants border
|
|
69
|
+
function clearMarchingAnts() {
|
|
70
|
+
document.querySelectorAll('.copy-border-top, .copy-border-bottom, .copy-border-left, .copy-border-right, .cut-pending').forEach(cell => {
|
|
71
|
+
cell.classList.remove('copy-border-top', 'copy-border-bottom', 'copy-border-left', 'copy-border-right', 'cut-pending');
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function handlePaste(e) {
|
|
76
|
+
if (!datatableCurrentCell || datatableEditMode || !datatableData) return;
|
|
77
|
+
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
const text = e.clipboardData.getData('text/plain');
|
|
80
|
+
if (!text) return;
|
|
81
|
+
|
|
82
|
+
// If this was a cut operation, clear the source cells
|
|
83
|
+
if (datatableIsCutOperation && datatableCopiedRange) {
|
|
84
|
+
const { startRow, startCol, endRow, endCol } = datatableCopiedRange;
|
|
85
|
+
const minRow = Math.min(startRow, endRow);
|
|
86
|
+
const maxRow = Math.max(startRow, endRow);
|
|
87
|
+
const minCol = Math.min(startCol, endCol);
|
|
88
|
+
const maxCol = Math.max(startCol, endCol);
|
|
89
|
+
|
|
90
|
+
for (let r = minRow; r <= maxRow; r++) {
|
|
91
|
+
for (let c = minCol; c <= maxCol; c++) {
|
|
92
|
+
if (datatableData.rows[r]) {
|
|
93
|
+
datatableData.rows[r][c] = '';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const rows = text.split('\\n').map(line => line.split('\\t'));
|
|
100
|
+
const startRow = datatableCurrentCell.row;
|
|
101
|
+
const startCol = datatableCurrentCell.col;
|
|
102
|
+
|
|
103
|
+
rows.forEach((rowData, rOffset) => {
|
|
104
|
+
const targetRow = startRow + rOffset;
|
|
105
|
+
if (targetRow >= datatableData.rows.length) return;
|
|
106
|
+
|
|
107
|
+
rowData.forEach((value, cOffset) => {
|
|
108
|
+
const targetCol = startCol + cOffset;
|
|
109
|
+
if (targetCol >= datatableData.columns.length) return;
|
|
110
|
+
|
|
111
|
+
// Preserve empty strings as empty (not convert to 0)
|
|
112
|
+
if (value === '' || value === null || value === undefined) {
|
|
113
|
+
datatableData.rows[targetRow][targetCol] = '';
|
|
114
|
+
} else {
|
|
115
|
+
const colType = datatableData.columns[targetCol]?.type;
|
|
116
|
+
if (colType === 'numeric') {
|
|
117
|
+
const num = parseFloat(value);
|
|
118
|
+
datatableData.rows[targetRow][targetCol] = isNaN(num) ? value : num;
|
|
119
|
+
} else {
|
|
120
|
+
datatableData.rows[targetRow][targetCol] = value;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Clear marching ants and reset cut state
|
|
127
|
+
clearMarchingAnts();
|
|
128
|
+
datatableCopiedRange = null;
|
|
129
|
+
datatableIsCutOperation = false;
|
|
130
|
+
|
|
131
|
+
renderDatatable();
|
|
132
|
+
updateCellSelectionDisplay();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function getSelectedCellsAsTSV() {
|
|
136
|
+
if (!datatableSelectedCells || !datatableData) return '';
|
|
137
|
+
|
|
138
|
+
const { startRow, startCol, endRow, endCol } = datatableSelectedCells;
|
|
139
|
+
const minRow = Math.min(startRow, endRow);
|
|
140
|
+
const maxRow = Math.max(startRow, endRow);
|
|
141
|
+
const minCol = Math.min(startCol, endCol);
|
|
142
|
+
const maxCol = Math.max(startCol, endCol);
|
|
143
|
+
|
|
144
|
+
const lines = [];
|
|
145
|
+
for (let r = minRow; r <= maxRow; r++) {
|
|
146
|
+
const cells = [];
|
|
147
|
+
for (let c = minCol; c <= maxCol; c++) {
|
|
148
|
+
const value = datatableData.rows[r]?.[c];
|
|
149
|
+
// Preserve None as "None" string for copy
|
|
150
|
+
if (value === null || value === undefined) {
|
|
151
|
+
cells.push('');
|
|
152
|
+
} else {
|
|
153
|
+
cells.push(String(value));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
lines.push(cells.join('\\t'));
|
|
157
|
+
}
|
|
158
|
+
return lines.join('\\n');
|
|
159
|
+
}
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
__all__ = ["JS_DATATABLE_CLIPBOARD"]
|
|
163
|
+
|
|
164
|
+
# EOF
|