sqlite-hub 2.1.0 → 2.2.0

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.
package/README.md CHANGED
@@ -154,6 +154,13 @@ The preview supports regular Markdown, ordered and unordered lists, tables, code
154
154
  - Insert Note opens saved queries that have notes and inserts the selected note directly into the document.
155
155
  - Markdown Todo column exports from query results can create a new document without embedding the original SQL query.
156
156
 
157
+ Documents also support Magic Snippets from the Insert menu:
158
+
159
+ - Insert Time Metadata adds `created at` and `last modified` lines for the document.
160
+ - Insert Database Info adds current database size, estimated pages, table count, and journal mode.
161
+
162
+ Magic Snippets are managed blocks. When a document is opened, SQLite Hub refreshes existing Time Metadata and Database Info snippets with the latest document/database values.
163
+
157
164
  <p>
158
165
  <a href="./frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp"><img src="./frontend/assets/mockups/documents_2_document_insert_table_modal_1920.webp" alt="SQLite Hub insert saved query table into a document" width="49%"></a>
159
166
  <a href="./frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp"><img src="./frontend/assets/mockups/documents_3_document_insert_note_modal_1920.webp" alt="SQLite Hub insert saved query note into a document" width="49%"></a>
package/docs/MCP.md CHANGED
@@ -58,7 +58,7 @@ Read-only and safe tools:
58
58
  - `get_indexes`: return all indexes or indexes for one table.
59
59
  - `get_foreign_keys`: return all foreign keys or foreign keys for one table.
60
60
  - `run_readonly_query`: execute a read-only `SELECT`, `PRAGMA`, or `EXPLAIN` query.
61
- - `get_stored_queries`: list saved SQL Editor queries for a database, equivalent to `sqlite-hub --database:name --queries`.
61
+ - `get_saved_queries`: list saved SQL Editor queries for a database, equivalent to `sqlite-hub --database:name --queries`.
62
62
  - `explain_query_plan`: run `EXPLAIN QUERY PLAN` and return structured plan rows plus index hints when a table scan appears.
63
63
  - `read_documents`: read database-scoped Markdown documents.
64
64
 
@@ -113,5 +113,5 @@ Use SQLite Hub MCP to explain the query plan for this SQL query.
113
113
  ```
114
114
 
115
115
  ```text
116
- Use SQLite Hub MCP to list my stored queries and execute the one named "Company List".
116
+ Use SQLite Hub MCP to list my saved queries and execute the one named "Company List".
117
117
  ```
package/docs/changelog.md CHANGED
@@ -1,3 +1,9 @@
1
+ # v2.2.0
2
+
3
+ - better documents features, magic snippets, better auto save, complette overhaul
4
+ - directories for documents
5
+ - better logging
6
+
1
7
  # v2.1.0
2
8
 
3
9
  - tags for databases
package/docs/todo.md CHANGED
@@ -1,3 +1,5 @@
1
1
  # Maybe
2
2
 
3
3
  - Data Pipeline Light/hook, programmable triggers?
4
+ - autocomplete in sql editor (Schema-aware autocomplete)
5
+ - CLI overhaul
@@ -671,6 +671,13 @@ export function createDocument(payload = {}) {
671
671
  });
672
672
  }
673
673
 
674
+ export function createDocumentFolder(payload = {}) {
675
+ return request("/api/documents/folders", {
676
+ method: "POST",
677
+ body: payload,
678
+ });
679
+ }
680
+
674
681
  export function getDocument(documentId) {
675
682
  return request(`/api/documents/${encodeURIComponent(documentId)}`);
676
683
  }
@@ -47,6 +47,9 @@ import {
47
47
  exportCurrentQueryFormat,
48
48
  getState,
49
49
  initializeApp,
50
+ insertCurrentDocumentDatabaseInfo,
51
+ insertCurrentDocumentSavedQueries,
52
+ insertCurrentDocumentTimeMetadata,
50
53
  insertMarkdownIntoLastOpenDocument,
51
54
  loadMoreQueryHistory,
52
55
  loadMoreLogs,
@@ -67,6 +70,7 @@ import {
67
70
  openDeleteQueryChartModal,
68
71
  openDataExportModal,
69
72
  openGenerateDataModal,
73
+ openCreateDocumentFolderModal,
70
74
  openDeleteDocumentModal,
71
75
  openQueryExportModal,
72
76
  openDataRowByIdentity,
@@ -76,6 +80,7 @@ import {
76
80
  openEditQueryChartModal,
77
81
  openDocumentInsertNoteModal,
78
82
  openDocumentInsertTableModal,
83
+ openDocumentInsertTableDefinitionModal,
79
84
  preserveCurrentDataRowSelectionForReload,
80
85
  openDataRowUpdatePreview,
81
86
  openEditorRowUpdatePreview,
@@ -135,6 +140,8 @@ import {
135
140
  submitDeleteDocumentConfirmation,
136
141
  submitDocumentInsertNote,
137
142
  submitDocumentInsertTable,
143
+ submitDocumentInsertTableDefinition,
144
+ submitCreateDocumentFolder,
138
145
  submitCreateMediaTaggingTagTable,
139
146
  submitCreateMediaTaggingMappingTable,
140
147
  submitDeleteQueryHistoryConfirmation,
@@ -171,6 +178,7 @@ import {
171
178
  storeCopyColumnPreferences,
172
179
  submitCreateConnection,
173
180
  createCurrentMediaTag,
181
+ moveCurrentDocumentToFolder,
174
182
  submitDeleteRowConfirmation,
175
183
  submitEditConnection,
176
184
  submitImportSql,
@@ -184,6 +192,9 @@ import {
184
192
  updateCurrentMediaTaggingTagFormField,
185
193
  updateCopyColumnModalFormatField,
186
194
  updateCurrentDocumentDraftField,
195
+ updateDocumentTableDefinitionOption,
196
+ updateDocumentTableDefinitionSampleRowCount,
197
+ updateDocumentTableDefinitionSelection,
187
198
  updateDocumentInsertQuerySelection,
188
199
  updateCurrentQueryChartDraftConfigField,
189
200
  updateCurrentQueryChartDraftField,
@@ -202,7 +213,7 @@ import {
202
213
  import { renderChartsDetail, renderChartsView } from './views/charts.js';
203
214
  import { renderBackupsView } from './views/backups.js';
204
215
  import { renderConnectionsView } from './views/connections.js';
205
- import { renderDataRowEditorPanel, renderDataView } from './views/data.js';
216
+ import { buildDataSidebarSignature, renderDataRowEditorPanel, renderDataView } from './views/data.js';
206
217
  import { renderDocumentsView } from './views/documents.js';
207
218
  import { renderEditorView } from './views/editor.js';
208
219
  import { renderLandingView } from './views/landing.js';
@@ -274,6 +285,7 @@ let lastRenderedToastMarkup = '';
274
285
  let lastRenderedChartsHistorySignature = '';
275
286
  let lastRenderedChartsDetailSignature = '';
276
287
  let lastRenderedChartsCardSignature = '';
288
+ let lastRenderedDataSidebarSignature = '';
277
289
  let lastRenderedPanelOpen = false;
278
290
  let lastRenderedLockedRoute = false;
279
291
  let pendingNewTableDesignerAutofocus = false;
@@ -281,8 +293,9 @@ let pendingQueryEditorFocus = false;
281
293
  let pendingMediaTaggingTagSearchFocus = false;
282
294
  let documentAutosaveTimer = null;
283
295
  let pendingDocumentAutosaveId = null;
296
+ let lastDocumentEditorInsertionRange = null;
284
297
 
285
- const DOCUMENT_AUTOSAVE_DELAY_MS = 5000;
298
+ const DOCUMENT_AUTOSAVE_DELAY_MS = 2000;
286
299
 
287
300
  const APP_TITLE = 'SQLite Hub';
288
301
  const TABLE_DESIGNER_IMPORT_FORMATS = {
@@ -650,6 +663,81 @@ function syncDataRowSelectionUi(selectedRowIndex = null) {
650
663
  return true;
651
664
  }
652
665
 
666
+ function syncDataSidebarActiveTable(state) {
667
+ if (state.route.name !== 'data') {
668
+ return false;
669
+ }
670
+
671
+ const sidebarNode = shellRefs.view.querySelector('[data-data-sidebar]');
672
+
673
+ if (!(sidebarNode instanceof HTMLElement)) {
674
+ return state.dataBrowser.tablesVisible === false;
675
+ }
676
+
677
+ const selectedTableName = String(state.dataBrowser.selectedTable ?? '');
678
+
679
+ for (const itemNode of sidebarNode.querySelectorAll('[data-data-table-item]')) {
680
+ if (!(itemNode instanceof HTMLElement)) {
681
+ continue;
682
+ }
683
+
684
+ const isActive = itemNode.dataset.dataTableName === selectedTableName;
685
+
686
+ itemNode.classList.toggle('is-active', isActive);
687
+ itemNode.classList.toggle('border-primary-container/30', isActive);
688
+ itemNode.classList.toggle('bg-surface-container-high', isActive);
689
+ itemNode.classList.toggle('border-outline-variant/10', !isActive);
690
+ itemNode.classList.toggle('bg-surface-container-lowest', !isActive);
691
+ itemNode.classList.toggle('hover:bg-surface-container-high', !isActive);
692
+
693
+ const labelNode = itemNode.querySelector('[data-data-table-name-label]');
694
+
695
+ if (labelNode instanceof HTMLElement) {
696
+ labelNode.classList.toggle('is-active', isActive);
697
+ }
698
+ }
699
+
700
+ return true;
701
+ }
702
+
703
+ function renderDataMainIntoScratch(state) {
704
+ const scratch = document.createElement('div');
705
+
706
+ replaceChildrenFromRenderedMarkup(scratch, renderDataView(state).main);
707
+ return scratch;
708
+ }
709
+
710
+ function patchDataMainUi(state, { tableHorizontalScrollState = null } = {}) {
711
+ const dataView = shellRefs.view.querySelector('[data-data-view]');
712
+
713
+ if (!(dataView instanceof HTMLElement)) {
714
+ return false;
715
+ }
716
+
717
+ const currentWorkspace = dataView.querySelector('[data-data-workspace]');
718
+ const currentSidebar = dataView.querySelector('[data-data-sidebar]');
719
+ const scratch = renderDataMainIntoScratch(state);
720
+ const nextWorkspace = scratch.querySelector('[data-data-workspace]');
721
+ const nextSidebar = scratch.querySelector('[data-data-sidebar]');
722
+
723
+ if (
724
+ !(currentWorkspace instanceof HTMLElement) ||
725
+ !(nextWorkspace instanceof HTMLElement) ||
726
+ Boolean(currentSidebar) !== Boolean(nextSidebar)
727
+ ) {
728
+ return false;
729
+ }
730
+
731
+ currentWorkspace.replaceWith(nextWorkspace);
732
+ restoreTableHorizontalScrollState({
733
+ snapshot: tableHorizontalScrollState,
734
+ routeName: state.route.name,
735
+ scrollNodes: nextWorkspace.querySelectorAll('[data-table-horizontal-scroll]'),
736
+ });
737
+
738
+ return syncDataSidebarActiveTable(state);
739
+ }
740
+
653
741
  function syncQueryHistoryUi(historyId) {
654
742
  const state = getState();
655
743
  const numericId = Number(historyId);
@@ -1124,6 +1212,71 @@ function patchLogsTableUi(state) {
1124
1212
  return true;
1125
1213
  }
1126
1214
 
1215
+ function normalizeDocumentsTextPatchMarkup(markup) {
1216
+ return String(markup ?? '')
1217
+ .replace(
1218
+ /(<span\b[^>]*data-document-save-state=")(?:saved|unsaved)("[^>]*>)[\s\S]*?(<\/span>)/g,
1219
+ '$1__document_save_state__$2__document_save_state__$3',
1220
+ )
1221
+ .replace(
1222
+ /(<span\b[^>]*data-document-list-meta\b[^>]*>)[\s\S]*?(<\/span>)/g,
1223
+ '$1__document_list_meta__$2',
1224
+ );
1225
+ }
1226
+
1227
+ function renderDocumentsMainIntoScratch(state) {
1228
+ const scratch = document.createElement('div');
1229
+
1230
+ replaceChildrenFromRenderedMarkup(scratch, renderDocumentsView(state).main);
1231
+ return scratch;
1232
+ }
1233
+
1234
+ function patchDocumentsTextNodes(selector, scratch) {
1235
+ const currentNodes = shellRefs.view.querySelectorAll(selector);
1236
+ const nextNodes = scratch.querySelectorAll(selector);
1237
+
1238
+ if (currentNodes.length !== nextNodes.length) {
1239
+ return false;
1240
+ }
1241
+
1242
+ for (let index = 0; index < currentNodes.length; index += 1) {
1243
+ const currentNode = currentNodes[index];
1244
+ const nextNode = nextNodes[index];
1245
+
1246
+ if (!(currentNode instanceof HTMLElement) || !(nextNode instanceof HTMLElement)) {
1247
+ return false;
1248
+ }
1249
+
1250
+ currentNode.textContent = nextNode.textContent;
1251
+
1252
+ if (selector === '[data-document-save-state]') {
1253
+ currentNode.dataset.documentSaveState = nextNode.dataset.documentSaveState ?? '';
1254
+ }
1255
+ }
1256
+
1257
+ return true;
1258
+ }
1259
+
1260
+ function patchDocumentsTextOnlyUi(state, nextMainMarkup) {
1261
+ if (lastRenderedRouteName !== 'documents' || state.route.name !== 'documents') {
1262
+ return false;
1263
+ }
1264
+
1265
+ const previousComparable = normalizeDocumentsTextPatchMarkup(lastRenderedMainMarkup);
1266
+ const nextComparable = normalizeDocumentsTextPatchMarkup(nextMainMarkup);
1267
+
1268
+ if (previousComparable !== nextComparable) {
1269
+ return false;
1270
+ }
1271
+
1272
+ const scratch = renderDocumentsMainIntoScratch(state);
1273
+
1274
+ return (
1275
+ patchDocumentsTextNodes('[data-document-save-state]', scratch) &&
1276
+ patchDocumentsTextNodes('[data-document-list-meta]', scratch)
1277
+ );
1278
+ }
1279
+
1127
1280
  function readFileAsBase64(file) {
1128
1281
  return new Promise((resolve, reject) => {
1129
1282
  const reader = new FileReader();
@@ -1433,6 +1586,29 @@ function focusMediaTaggingTagSearchInput() {
1433
1586
  return true;
1434
1587
  }
1435
1588
 
1589
+ function focusModalAutofocusElement() {
1590
+ const element = shellRefs.modal.querySelector('[autofocus]');
1591
+
1592
+ if (
1593
+ !(
1594
+ element instanceof HTMLInputElement ||
1595
+ element instanceof HTMLTextAreaElement ||
1596
+ element instanceof HTMLSelectElement ||
1597
+ element instanceof HTMLButtonElement
1598
+ )
1599
+ ) {
1600
+ return false;
1601
+ }
1602
+
1603
+ element.focus({ preventScroll: true });
1604
+
1605
+ if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) {
1606
+ element.setSelectionRange(element.value.length, element.value.length);
1607
+ }
1608
+
1609
+ return true;
1610
+ }
1611
+
1436
1612
  function syncSidebarActiveRoute(routeName) {
1437
1613
  if (!isMediaTaggingRouteName(routeName)) {
1438
1614
  return false;
@@ -1560,6 +1736,7 @@ function renderApp(state) {
1560
1736
  const chartsHistorySignature = buildChartsHistorySignature(state);
1561
1737
  const chartsDetailSignature = buildChartsDetailSignature(state);
1562
1738
  const chartsCardSignature = buildChartsCardSignature(state);
1739
+ const dataSidebarSignature = buildDataSidebarSignature(state);
1563
1740
  const isLockedRoute = [
1564
1741
  'editor',
1565
1742
  'editorResults',
@@ -1583,6 +1760,7 @@ function renderApp(state) {
1583
1760
  const chartsHistoryChanged = chartsHistorySignature !== lastRenderedChartsHistorySignature;
1584
1761
  const chartsDetailChanged = chartsDetailSignature !== lastRenderedChartsDetailSignature;
1585
1762
  const chartsCardsChanged = chartsCardSignature !== lastRenderedChartsCardSignature;
1763
+ const dataSidebarChanged = dataSidebarSignature !== lastRenderedDataSidebarSignature;
1586
1764
  const panelOpenChanged = panelOpen !== lastRenderedPanelOpen;
1587
1765
  const lockedRouteChanged = isLockedRoute !== lastRenderedLockedRoute;
1588
1766
  const shellMarkupUnchanged =
@@ -1623,9 +1801,16 @@ function renderApp(state) {
1623
1801
 
1624
1802
  const canPatchChartsMain = mainChanged && previousRouteName === 'charts' && state.route.name === 'charts';
1625
1803
  const canPatchLogsMain = mainChanged && previousRouteName === 'logs' && state.route.name === 'logs';
1804
+ const canPatchDocumentsMain = mainChanged && previousRouteName === 'documents' && state.route.name === 'documents';
1805
+ const canPatchDataMain =
1806
+ mainChanged && previousRouteName === 'data' && state.route.name === 'data' && !dataSidebarChanged;
1626
1807
  let mainPatched = false;
1627
1808
  let preservedChartsDom = false;
1628
1809
 
1810
+ if (canPatchDataMain) {
1811
+ mainPatched = patchDataMainUi(state, { tableHorizontalScrollState });
1812
+ }
1813
+
1629
1814
  if (canPatchChartsMain) {
1630
1815
  const historyPatched = !chartsHistoryChanged || patchChartsHistoryUi(state);
1631
1816
 
@@ -1652,6 +1837,10 @@ function renderApp(state) {
1652
1837
  mainPatched = patchLogsTableUi(state);
1653
1838
  }
1654
1839
 
1840
+ if (!mainPatched && canPatchDocumentsMain) {
1841
+ mainPatched = patchDocumentsTextOnlyUi(state, main);
1842
+ }
1843
+
1655
1844
  if (mainChanged) {
1656
1845
  if (!mainPatched) {
1657
1846
  teardownStructureGraph();
@@ -1731,6 +1920,8 @@ function renderApp(state) {
1731
1920
  if (focusNewTableDesignerNameField()) {
1732
1921
  pendingNewTableDesignerAutofocus = false;
1733
1922
  }
1923
+ } else if (modalChanged && state.modal && focusModalAutofocusElement()) {
1924
+ // Modal content is inserted dynamically, so native autofocus is not reliable.
1734
1925
  } else {
1735
1926
  restoreFocusedInputState(focusedInput);
1736
1927
  }
@@ -1747,6 +1938,7 @@ function renderApp(state) {
1747
1938
  lastRenderedChartsHistorySignature = chartsHistorySignature;
1748
1939
  lastRenderedChartsDetailSignature = chartsDetailSignature;
1749
1940
  lastRenderedChartsCardSignature = chartsCardSignature;
1941
+ lastRenderedDataSidebarSignature = dataSidebarSignature;
1750
1942
  lastRenderedPanelOpen = panelOpen;
1751
1943
  lastRenderedLockedRoute = isLockedRoute;
1752
1944
 
@@ -2436,16 +2628,60 @@ function exportCurrentDocumentMarkdown() {
2436
2628
  showToast(`Document "${filename}" exported.`, 'success');
2437
2629
  }
2438
2630
 
2631
+ function isDocumentEditorInput(node) {
2632
+ return (
2633
+ node instanceof HTMLTextAreaElement &&
2634
+ node.classList.contains('documents-editor-input') &&
2635
+ node.dataset.bind === 'document-field' &&
2636
+ node.dataset.field === 'content'
2637
+ );
2638
+ }
2639
+
2640
+ function rememberDocumentEditorInsertionRange(textarea) {
2641
+ if (!isDocumentEditorInput(textarea)) {
2642
+ return null;
2643
+ }
2644
+
2645
+ const documents = getState().documents;
2646
+ const range = {
2647
+ documentId: String(documents.selectedId ?? ''),
2648
+ start: textarea.selectionStart,
2649
+ end: textarea.selectionEnd,
2650
+ };
2651
+
2652
+ lastDocumentEditorInsertionRange = range;
2653
+ return range;
2654
+ }
2655
+
2656
+ function rememberDocumentEditorInsertionRangeFromTarget(target) {
2657
+ if (isDocumentEditorInput(target)) {
2658
+ rememberDocumentEditorInsertionRange(target);
2659
+ }
2660
+ }
2661
+
2439
2662
  function getCurrentDocumentEditorInsertionRange() {
2440
2663
  const textarea = document.querySelector('.documents-editor-input');
2441
2664
 
2442
- if (!(textarea instanceof HTMLTextAreaElement)) {
2665
+ if (isDocumentEditorInput(textarea)) {
2666
+ const range = rememberDocumentEditorInsertionRange(textarea);
2667
+
2668
+ return range
2669
+ ? {
2670
+ start: range.start,
2671
+ end: range.end,
2672
+ }
2673
+ : null;
2674
+ }
2675
+
2676
+ const documents = getState().documents;
2677
+
2678
+ if (String(lastDocumentEditorInsertionRange?.documentId ?? '') !== String(documents.selectedId ?? '')) {
2443
2679
  return null;
2444
2680
  }
2445
2681
 
2446
2682
  return {
2447
- start: textarea.selectionStart,
2448
- end: textarea.selectionEnd,
2683
+ start: lastDocumentEditorInsertionRange.start,
2684
+ end: lastDocumentEditorInsertionRange.end,
2449
2685
  };
2450
2686
  }
2451
2687
 
@@ -2678,6 +2914,12 @@ async function handleAction(actionNode) {
2678
2914
  }
2679
2915
  return;
2680
2916
  }
2917
+ case 'open-create-document-folder-modal':
2918
+ openCreateDocumentFolderModal();
2919
+ return;
2920
+ case 'move-document-to-folder':
2921
+ await moveCurrentDocumentToFolder(actionNode.dataset.folderId || null);
2922
+ return;
2681
2923
  case 'select-document': {
2682
2924
  const documentId = String(actionNode.dataset.documentId ?? '').trim();
2683
2925
 
@@ -2700,6 +2942,33 @@ async function handleAction(actionNode) {
2700
2942
  case 'open-document-insert-note-modal':
2701
2943
  await openDocumentInsertNoteModal(getCurrentDocumentEditorInsertionRange());
2702
2944
  return;
2945
+ case 'open-document-insert-table-definition-modal':
2946
+ await openDocumentInsertTableDefinitionModal(getCurrentDocumentEditorInsertionRange());
2947
+ return;
2948
+ case 'insert-document-saved-queries': {
2949
+ const inserted = await insertCurrentDocumentSavedQueries(getCurrentDocumentEditorInsertionRange());
2950
+
2951
+ if (inserted) {
2952
+ scheduleDocumentAutosave(getState().documents.selectedId);
2953
+ }
2954
+ return;
2955
+ }
2956
+ case 'insert-document-time-metadata': {
2957
+ const inserted = await insertCurrentDocumentTimeMetadata(getCurrentDocumentEditorInsertionRange());
2958
+
2959
+ if (inserted) {
2960
+ scheduleDocumentAutosave(getState().documents.selectedId);
2961
+ }
2962
+ return;
2963
+ }
2964
+ case 'insert-document-database-info': {
2965
+ const inserted = await insertCurrentDocumentDatabaseInfo(getCurrentDocumentEditorInsertionRange());
2966
+
2967
+ if (inserted) {
2968
+ scheduleDocumentAutosave(getState().documents.selectedId);
2969
+ }
2970
+ return;
2971
+ }
2703
2972
  case 'import-document-markdown': {
2704
2973
  const fileInput = document.querySelector('[data-bind="document-import-file"]');
2705
2974
 
@@ -2719,8 +2988,9 @@ async function handleAction(actionNode) {
2719
2988
  toggleDocumentsPane(actionNode.dataset.pane);
2720
2989
  return;
2721
2990
  case 'toggle-document-todo':
2722
- clearDocumentAutosaveTimer();
2723
- await toggleCurrentDocumentTodo(actionNode.dataset.lineIndex);
2991
+ if (toggleCurrentDocumentTodo(actionNode.dataset.lineIndex)) {
2992
+ scheduleDocumentAutosave(getState().documents.selectedId);
2993
+ }
2724
2994
  return;
2725
2995
  case 'open-create-query-chart-modal':
2726
2996
  openCreateQueryChartModal();
@@ -3448,6 +3718,30 @@ document.addEventListener('contextmenu', event => {
3448
3718
  openCopyColumnMenu(headerNode);
3449
3719
  });
3450
3720
 
3721
+ document.addEventListener('focusin', event => {
3722
+ rememberDocumentEditorInsertionRangeFromTarget(event.target);
3723
+ });
3724
+
3725
+ document.addEventListener('keyup', event => {
3726
+ rememberDocumentEditorInsertionRangeFromTarget(event.target);
3727
+ });
3728
+
3729
+ document.addEventListener('pointerup', event => {
3730
+ rememberDocumentEditorInsertionRangeFromTarget(event.target);
3731
+ });
3732
+
3733
+ document.addEventListener(
3734
+ 'select',
3735
+ event => {
3736
+ rememberDocumentEditorInsertionRangeFromTarget(event.target);
3737
+ },
3738
+ true,
3739
+ );
3740
+
3741
+ document.addEventListener('selectionchange', () => {
3742
+ rememberDocumentEditorInsertionRangeFromTarget(document.activeElement);
3743
+ });
3744
+
3451
3745
  document.addEventListener('keydown', event => {
3452
3746
  const target = event.target;
3453
3747
  const state = getState();
@@ -3714,6 +4008,7 @@ document.addEventListener('input', event => {
3714
4008
  }
3715
4009
 
3716
4010
  if (bindNode.dataset.bind === 'document-field') {
4011
+ rememberDocumentEditorInsertionRangeFromTarget(bindNode);
3717
4012
  updateCurrentDocumentDraftField(bindNode.dataset.field, bindNode.value);
3718
4013
  scheduleDocumentAutosave(getState().documents.selectedId);
3719
4014
  return;
@@ -3974,6 +4269,21 @@ document.addEventListener('change', event => {
3974
4269
  return;
3975
4270
  }
3976
4271
 
4272
+ if (bindNode.dataset.bind === 'document-table-definition-select') {
4273
+ updateDocumentTableDefinitionSelection(bindNode.value);
4274
+ return;
4275
+ }
4276
+
4277
+ if (bindNode.dataset.bind === 'document-table-definition-option') {
4278
+ updateDocumentTableDefinitionOption(bindNode.dataset.option, bindNode.checked);
4279
+ return;
4280
+ }
4281
+
4282
+ if (bindNode.dataset.bind === 'document-table-definition-row-count') {
4283
+ updateDocumentTableDefinitionSampleRowCount(bindNode.value);
4284
+ return;
4285
+ }
4286
+
3977
4287
  if (bindNode.dataset.bind === 'table-designer-field') {
3978
4288
  if (bindNode instanceof HTMLInputElement && bindNode.type === 'checkbox') {
3979
4289
  updateCurrentTableDesignerField(bindNode.dataset.field, bindNode.checked);
@@ -4188,6 +4498,10 @@ document.addEventListener('submit', async event => {
4188
4498
  case 'delete-api-token-confirm':
4189
4499
  await submitDeleteSettingsApiTokenConfirmation();
4190
4500
  return;
4501
+ case 'create-document-folder': {
4502
+ await submitCreateDocumentFolder(String(formData.get('name') ?? ''));
4503
+ return;
4504
+ }
4191
4505
  case 'document-insert-table': {
4192
4506
  const inserted = await submitDocumentInsertTable();
4193
4507
 
@@ -4196,6 +4510,14 @@ document.addEventListener('submit', async event => {
4196
4510
  }
4197
4511
  return;
4198
4512
  }
4513
+ case 'document-insert-table-definition': {
4514
+ const inserted = await submitDocumentInsertTableDefinition();
4515
+
4516
+ if (inserted) {
4517
+ scheduleDocumentAutosave(getState().documents.selectedId);
4518
+ }
4519
+ return;
4520
+ }
4199
4521
  case 'document-insert-note': {
4200
4522
  const inserted = submitDocumentInsertNote();
4201
4523