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.
Files changed (143) hide show
  1. figrecipe/__init__.py +74 -76
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/_panel.py +67 -0
  4. figrecipe/_api/_save.py +100 -4
  5. figrecipe/_cli/__init__.py +7 -0
  6. figrecipe/_cli/_compose.py +87 -0
  7. figrecipe/_cli/_convert.py +117 -0
  8. figrecipe/_cli/_crop.py +82 -0
  9. figrecipe/_cli/_edit.py +70 -0
  10. figrecipe/_cli/_extract.py +128 -0
  11. figrecipe/_cli/_fonts.py +47 -0
  12. figrecipe/_cli/_info.py +67 -0
  13. figrecipe/_cli/_main.py +58 -0
  14. figrecipe/_cli/_reproduce.py +79 -0
  15. figrecipe/_cli/_style.py +77 -0
  16. figrecipe/_cli/_validate.py +66 -0
  17. figrecipe/_cli/_version.py +50 -0
  18. figrecipe/_composition/__init__.py +32 -0
  19. figrecipe/_composition/_alignment.py +452 -0
  20. figrecipe/_composition/_compose.py +179 -0
  21. figrecipe/_composition/_import_axes.py +127 -0
  22. figrecipe/_composition/_visibility.py +125 -0
  23. figrecipe/_dev/__init__.py +2 -0
  24. figrecipe/_dev/browser/__init__.py +69 -0
  25. figrecipe/_dev/browser/_audio.py +240 -0
  26. figrecipe/_dev/browser/_caption.py +356 -0
  27. figrecipe/_dev/browser/_click_effect.py +146 -0
  28. figrecipe/_dev/browser/_cursor.py +196 -0
  29. figrecipe/_dev/browser/_highlight.py +105 -0
  30. figrecipe/_dev/browser/_narration.py +237 -0
  31. figrecipe/_dev/browser/_recorder.py +446 -0
  32. figrecipe/_dev/browser/_utils.py +178 -0
  33. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  34. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  35. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  36. figrecipe/_editor/__init__.py +36 -36
  37. figrecipe/_editor/_bbox/_extract.py +155 -9
  38. figrecipe/_editor/_bbox/_extract_text.py +124 -0
  39. figrecipe/_editor/_call_overrides.py +183 -0
  40. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  41. figrecipe/_editor/_figure_layout.py +211 -0
  42. figrecipe/_editor/_flask_app.py +157 -16
  43. figrecipe/_editor/_helpers.py +17 -8
  44. figrecipe/_editor/_hitmap/_detect.py +89 -32
  45. figrecipe/_editor/_hitmap_main.py +4 -4
  46. figrecipe/_editor/_overrides.py +4 -1
  47. figrecipe/_editor/_plot_types_registry.py +190 -0
  48. figrecipe/_editor/_render_overrides.py +38 -11
  49. figrecipe/_editor/_renderer.py +46 -1
  50. figrecipe/_editor/_routes_annotation.py +114 -0
  51. figrecipe/_editor/_routes_axis.py +35 -6
  52. figrecipe/_editor/_routes_captions.py +130 -0
  53. figrecipe/_editor/_routes_composition.py +270 -0
  54. figrecipe/_editor/_routes_core.py +15 -173
  55. figrecipe/_editor/_routes_datatable.py +364 -0
  56. figrecipe/_editor/_routes_element.py +37 -19
  57. figrecipe/_editor/_routes_files.py +443 -0
  58. figrecipe/_editor/_routes_image.py +200 -0
  59. figrecipe/_editor/_routes_snapshot.py +94 -0
  60. figrecipe/_editor/_routes_style.py +28 -8
  61. figrecipe/_editor/_templates/__init__.py +40 -2
  62. figrecipe/_editor/_templates/_html.py +97 -103
  63. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  64. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  65. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  66. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  67. figrecipe/_editor/_templates/_scripts/__init__.py +58 -0
  68. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  69. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  70. figrecipe/_editor/_templates/_scripts/_api.py +1 -1
  71. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  72. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  73. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  74. figrecipe/_editor/_templates/_scripts/_core.py +94 -37
  75. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  76. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  77. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  78. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  79. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  80. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  81. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  82. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  83. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  84. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  85. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  86. figrecipe/_editor/_templates/_scripts/_element_editor.py +17 -2
  87. figrecipe/_editor/_templates/_scripts/_files.py +274 -40
  88. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  89. figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
  90. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  91. figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
  92. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  93. figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
  94. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  95. figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
  96. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  97. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  98. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  99. figrecipe/_editor/_templates/_scripts/_selection.py +8 -1
  100. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  101. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  102. figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
  103. figrecipe/_editor/_templates/_styles/__init__.py +9 -0
  104. figrecipe/_editor/_templates/_styles/_base.py +47 -0
  105. figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
  106. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  107. figrecipe/_editor/_templates/_styles/_controls.py +168 -3
  108. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  109. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  110. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  111. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  112. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  113. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  114. figrecipe/_editor/_templates/_styles/_dynamic_props.py +5 -5
  115. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  116. figrecipe/_editor/_templates/_styles/_forms.py +98 -0
  117. figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
  118. figrecipe/_editor/_templates/_styles/_modals.py +29 -0
  119. figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
  120. figrecipe/_editor/_templates/_styles/_preview.py +213 -8
  121. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  122. figrecipe/_editor/static/audio/click.mp3 +0 -0
  123. figrecipe/_editor/static/click.mp3 +0 -0
  124. figrecipe/_editor/static/icons/favicon.ico +0 -0
  125. figrecipe/_integrations/__init__.py +17 -0
  126. figrecipe/_integrations/_scitex_stats.py +298 -0
  127. figrecipe/_params/_DECORATION_METHODS.py +2 -0
  128. figrecipe/_recorder.py +28 -3
  129. figrecipe/_reproducer/_core.py +60 -49
  130. figrecipe/_utils/__init__.py +3 -0
  131. figrecipe/_utils/_bundle.py +205 -0
  132. figrecipe/_wrappers/_axes.py +150 -2
  133. figrecipe/_wrappers/_caption_generator.py +218 -0
  134. figrecipe/_wrappers/_figure.py +26 -1
  135. figrecipe/_wrappers/_stat_annotation.py +274 -0
  136. figrecipe/styles/_style_applier.py +10 -2
  137. figrecipe/styles/presets/SCITEX.yaml +11 -4
  138. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
  139. figrecipe-0.9.0.dist-info/RECORD +277 -0
  140. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  141. figrecipe-0.7.4.dist-info/RECORD +0 -188
  142. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  143. {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
- hitregionOverlay.addEventListener('click', function(event) {
172
- // Only clear if clicking directly on the SVG (not on a shape inside it)
173
- if (event.target === hitregionOverlay) {
174
- clearSelection();
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
- selectionOverlay.addEventListener('click', function(event) {
181
- if (event.target === selectionOverlay) {
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
- darkModeToggle.addEventListener('change', function() {
189
- document.documentElement.setAttribute('data-theme', this.checked ? 'dark' : 'light');
190
- scheduleUpdate();
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', 'panel_selector'];
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').addEventListener('click', updatePreview);
225
- document.getElementById('btn-reset').addEventListener('click', resetValues);
226
- document.getElementById('btn-save').addEventListener('click', saveOverrides);
227
- document.getElementById('btn-restore').addEventListener('click', restoreOriginal);
228
- // Hit regions toggle (optional - button may be hidden in production)
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 buttons
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 - replaced by tabs)
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').addEventListener('click', () => switchTab('figure'));
246
- document.getElementById('tab-axis').addEventListener('click', () => switchTab('axis'));
247
- document.getElementById('tab-element').addEventListener('click', () => switchTab('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: Refresh preview
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('Refreshed', 'info');
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: Reset to theme defaults
402
+ // R: Render (re-render figure)
346
403
  if (event.key === 'r' || event.key === 'R') {
347
- resetValues();
348
- showToast('Reset to defaults', 'info');
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