@bagelink/vue 1.16.0 → 1.17.6

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 (128) hide show
  1. package/dist/assetTransport.d.ts +28 -0
  2. package/dist/assetTransport.d.ts.map +1 -0
  3. package/dist/assetTransport.test.d.ts +2 -0
  4. package/dist/assetTransport.test.d.ts.map +1 -0
  5. package/dist/assets.d.ts +65 -0
  6. package/dist/assets.d.ts.map +1 -0
  7. package/dist/assets.test.d.ts +2 -0
  8. package/dist/assets.test.d.ts.map +1 -0
  9. package/dist/components/AddressSearch.vue.d.ts +40 -36
  10. package/dist/components/AddressSearch.vue.d.ts.map +1 -1
  11. package/dist/components/ContextMenu.vue.d.ts +518 -0
  12. package/dist/components/ContextMenu.vue.d.ts.map +1 -0
  13. package/dist/components/Dropdown.types.d.ts +25 -0
  14. package/dist/components/Dropdown.types.d.ts.map +1 -0
  15. package/dist/components/Dropdown.vue.d.ts +20 -15
  16. package/dist/components/Dropdown.vue.d.ts.map +1 -1
  17. package/dist/components/FilterQuery.vue.d.ts +1 -1
  18. package/dist/components/FilterQuery.vue.d.ts.map +1 -1
  19. package/dist/components/Icon/Icon.vue.d.ts +0 -1
  20. package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
  21. package/dist/components/Icon/constants.d.ts +0 -1
  22. package/dist/components/Icon/constants.d.ts.map +1 -1
  23. package/dist/components/Icon/types.d.ts +2 -3
  24. package/dist/components/Icon/types.d.ts.map +1 -1
  25. package/dist/components/Image.vue.d.ts +7 -1
  26. package/dist/components/Image.vue.d.ts.map +1 -1
  27. package/dist/components/Swiper.vue.d.ts +1 -1
  28. package/dist/components/draggable/useDraggable.d.ts.map +1 -1
  29. package/dist/components/form/inputs/IconPickerInput.vue.d.ts +48 -37
  30. package/dist/components/form/inputs/IconPickerInput.vue.d.ts.map +1 -1
  31. package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
  32. package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
  33. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +3 -0
  34. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
  35. package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts +1 -1
  36. package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
  37. package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
  38. package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
  39. package/dist/components/form/inputs/RichText/utils/selection.d.ts +2 -0
  40. package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
  41. package/dist/components/form/inputs/RichText/utils/table.d.ts +34 -8
  42. package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
  43. package/dist/components/form/inputs/SelectInput.vue.d.ts +71 -63
  44. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  45. package/dist/components/form/inputs/TelInput.vue.d.ts +2 -2
  46. package/dist/components/form/inputs/Upload/AssetUploadInput.vue.d.ts +11 -0
  47. package/dist/components/form/inputs/Upload/AssetUploadInput.vue.d.ts.map +1 -0
  48. package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts +2 -2
  49. package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts.map +1 -1
  50. package/dist/components/index.d.ts +3 -1
  51. package/dist/components/index.d.ts.map +1 -1
  52. package/dist/components/lightbox/AssetPreview.vue.d.ts +9 -0
  53. package/dist/components/lightbox/AssetPreview.vue.d.ts.map +1 -0
  54. package/dist/components/lightbox/Lightbox.vue.d.ts.map +1 -1
  55. package/dist/components/lightbox/LightboxImage.vue.d.ts +2 -1
  56. package/dist/components/lightbox/LightboxImage.vue.d.ts.map +1 -1
  57. package/dist/components/lightbox/index.d.ts.map +1 -1
  58. package/dist/components/lightbox/lightbox.types.d.ts +7 -0
  59. package/dist/components/lightbox/lightbox.types.d.ts.map +1 -1
  60. package/dist/composables/index.d.ts +1 -1
  61. package/dist/composables/index.d.ts.map +1 -1
  62. package/dist/composables/useBoundedRectDrag.d.ts.map +1 -1
  63. package/dist/composables/useImageSrc.d.ts +8 -4
  64. package/dist/composables/useImageSrc.d.ts.map +1 -1
  65. package/dist/composables/usePointerSession.d.ts +14 -3
  66. package/dist/composables/usePointerSession.d.ts.map +1 -1
  67. package/dist/core.css +3 -3
  68. package/dist/directives/vResize.d.ts.map +1 -1
  69. package/dist/form-flow/FormFlowPlayer.vue.d.ts +3 -3
  70. package/dist/form-flow/FormFlowPlayer.vue.d.ts.map +1 -1
  71. package/dist/form-flow/form-flow.d.ts +13 -6
  72. package/dist/form-flow/form-flow.d.ts.map +1 -1
  73. package/dist/form-flow/schema-fields.d.ts +6 -0
  74. package/dist/form-flow/schema-fields.d.ts.map +1 -1
  75. package/dist/index.cjs +240 -153
  76. package/dist/index.css +1 -1
  77. package/dist/index.d.ts +3 -0
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.mjs +24167 -23417
  80. package/dist/style.css +3 -3
  81. package/dist/types/BagelForm.d.ts +5 -1
  82. package/dist/types/BagelForm.d.ts.map +1 -1
  83. package/package.json +1 -1
  84. package/src/assetTransport.test.ts +75 -0
  85. package/src/assetTransport.ts +98 -0
  86. package/src/assets.test.ts +82 -0
  87. package/src/assets.ts +162 -0
  88. package/src/components/ContextMenu.vue +113 -0
  89. package/src/components/Dropdown.types.ts +27 -0
  90. package/src/components/Dropdown.vue +129 -64
  91. package/src/components/FilterQuery.vue +1 -1
  92. package/src/components/Icon/Icon.vue +8 -30
  93. package/src/components/Icon/constants.ts +0 -166
  94. package/src/components/Icon/types.ts +2 -3
  95. package/src/components/Image.vue +19 -3
  96. package/src/components/draggable/useDraggable.ts +67 -100
  97. package/src/components/form/inputs/IconPickerInput.vue +14 -9
  98. package/src/components/form/inputs/RichText/components/EditorToolbar.vue +43 -111
  99. package/src/components/form/inputs/RichText/composables/useCommands.ts +32 -54
  100. package/src/components/form/inputs/RichText/composables/useEditor.ts +45 -171
  101. package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +90 -14
  102. package/src/components/form/inputs/RichText/index.vue +1195 -1210
  103. package/src/components/form/inputs/RichText/utils/commands.ts +689 -1028
  104. package/src/components/form/inputs/RichText/utils/selection.ts +66 -41
  105. package/src/components/form/inputs/RichText/utils/table.ts +798 -136
  106. package/src/components/form/inputs/SelectInput.vue +1 -1
  107. package/src/components/form/inputs/Upload/AssetUploadInput.vue +67 -0
  108. package/src/components/form/inputs/Upload/UploadInput.vue +5 -1
  109. package/src/components/index.ts +3 -1
  110. package/src/components/lightbox/AssetPreview.vue +16 -0
  111. package/src/components/lightbox/Lightbox.vue +8 -3
  112. package/src/components/lightbox/LightboxImage.vue +4 -3
  113. package/src/components/lightbox/index.ts +29 -4
  114. package/src/components/lightbox/lightbox.types.ts +5 -1
  115. package/src/composables/index.ts +1 -1
  116. package/src/composables/useBoundedRectDrag.ts +9 -12
  117. package/src/composables/useImageSrc.ts +47 -37
  118. package/src/composables/usePointerSession.ts +57 -12
  119. package/src/directives/vResize.ts +6 -7
  120. package/src/form-flow/form-flow.ts +31 -7
  121. package/src/form-flow/schema-fields.ts +28 -1
  122. package/src/index.ts +3 -0
  123. package/src/styles/core.css +1 -18
  124. package/src/types/BagelForm.ts +5 -1
  125. package/dist/components/form/inputs/RichText/utils/formatting.d.ts +0 -10
  126. package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +0 -1
  127. package/src/components/form/inputs/RichText/components/gridBox.vue +0 -58
  128. package/src/components/form/inputs/RichText/utils/formatting.ts +0 -331
@@ -1,35 +1,24 @@
1
1
  import type { EditorState, EditorDebugInterface, EditorDebuggerInstance } from '../richTextTypes'
2
2
  import { reactive } from 'vue'
3
3
  import { EditorDebugger } from '../utils/debug'
4
- import { isStyleActive } from '../utils/selection'
4
+ import { computeActiveStyles } from '../utils/selection'
5
5
 
6
- function preserveIframes(content: string): { html: string, iframes: HTMLIFrameElement[] } {
7
- const temp = document.createElement('div')
8
- temp.innerHTML = content
9
- const iframes: HTMLIFrameElement[] = []
10
- const placeholders: string[] = []
11
-
12
- // Find all iframes and replace them with placeholders
13
- temp.querySelectorAll('iframe').forEach((iframe, index) => {
14
- const placeholder = `<!--iframe-${index}-->`
15
- iframes.push(iframe.cloneNode(true) as HTMLIFrameElement)
16
- placeholders.push(placeholder)
17
- iframe.replaceWith(placeholder)
18
- })
19
-
20
- return {
21
- html: temp.innerHTML,
22
- iframes
23
- }
6
+ export function restoreHistoryNodes(doc: Document, html: string) {
7
+ const parsed = new DOMParser().parseFromString(html, 'text/html')
8
+ doc.body.replaceChildren(...Array.from(parsed.body.childNodes).map(n => doc.importNode(n, true)))
9
+ const addButtons = (doc as any).__addEditButtonsToTables as (() => void) | undefined
10
+ addButtons?.()
24
11
  }
25
12
 
26
- function restoreIframes(doc: Document, content: string, iframes: HTMLIFrameElement[]) {
27
- // Find all iframe placeholders and restore them
28
- const placeholderPattern = /<!--iframe-(\d+)-->/g
29
- doc.body.innerHTML = content.replace(placeholderPattern, (_, index) => {
30
- const iframe = iframes[Number(index)]
31
- return iframe ? iframe.outerHTML : ''
32
- })
13
+ export function pushHistorySnapshot(state: EditorState, snapshot: string) {
14
+ if (state.undoStack[state.undoStack.length - 1] === snapshot) {
15
+ return
16
+ }
17
+ state.undoStack.push(snapshot)
18
+ state.redoStack = []
19
+ if (state.undoStack.length > 50) {
20
+ state.undoStack.shift()
21
+ }
33
22
  }
34
23
 
35
24
  export function useEditor() {
@@ -51,75 +40,34 @@ export function useEditor() {
51
40
  debug: undefined
52
41
  })
53
42
 
54
- // Centralized state update functions
55
43
  const updateState = {
56
44
  styles: () => {
57
45
  if (!state.doc) { return }
58
- const styles = new Set<string>()
59
- const styleTypes = [
60
- 'bold',
61
- 'italic',
62
- 'underline',
63
- 'h1',
64
- 'h2',
65
- 'h3',
66
- 'h4',
67
- 'h5',
68
- 'h6',
69
- 'blockquote',
70
- 'p',
71
- 'orderedList',
72
- 'unorderedList',
73
- 'alignLeft',
74
- 'alignCenter',
75
- 'alignRight',
76
- 'alignJustify',
77
- 'textDirection',
78
- 'ltrDirection',
79
- 'rtlDirection'
80
- ]
81
- styleTypes.forEach((style) => {
82
- if (state.doc && isStyleActive(style, state.doc)) {
83
- styles.add(style)
84
- }
85
- })
86
- state.selectedStyles = styles
46
+ state.selectedStyles = computeActiveStyles(state.doc)
87
47
  },
88
48
  content: (source: 'html' | 'text') => {
89
49
  if (!state.doc) { return }
90
50
 
91
- // Only push to undo stack if content has changed
92
51
  const currentContent = state.doc.body.innerHTML
93
52
  if (currentContent !== state.content) {
94
- state.undoStack.push(state.content)
95
- state.redoStack = []
53
+ pushHistorySnapshot(state, currentContent)
96
54
  }
97
55
 
98
- // Store current selection
99
56
  const selection = state.doc.getSelection()
100
57
  const range = selection?.rangeCount ? selection.getRangeAt(0).cloneRange() : null
101
58
 
102
59
  if (source === 'html') {
103
- // Preserve iframes before setting content
104
- const preserved = preserveIframes(state.content)
105
- state.doc.body.innerHTML = preserved.html
106
-
107
- // Restore iframes after a short delay to ensure the document is ready
108
- setTimeout(() => {
109
- if (state.doc) {
110
- restoreIframes(state.doc, state.content, preserved.iframes)
111
-
112
- // Restore selection if it existed
113
- if (range && selection) {
114
- try {
115
- selection.removeAllRanges()
116
- selection.addRange(range)
117
- } catch {
118
- // Range restoration failed, ignore
119
- }
120
- }
60
+ state.doc.body.innerHTML = state.content
61
+ const addButtons = (state.doc as any).__addEditButtonsToTables as (() => void) | undefined
62
+ addButtons?.()
63
+ if (range && selection) {
64
+ try {
65
+ selection.removeAllRanges()
66
+ selection.addRange(range)
67
+ } catch {
68
+ // Ignore range restoration errors when structure changed
121
69
  }
122
- }, 0)
70
+ }
123
71
  } else {
124
72
  state.doc.body.textContent = state.content
125
73
  }
@@ -133,11 +81,9 @@ export function useEditor() {
133
81
 
134
82
  try {
135
83
  if (!state.doc.body.contains(newSelection.anchorNode)) {
136
- state.doc.body.focus()
137
84
  return
138
85
  }
139
86
 
140
- // Only update if selection has actually changed
141
87
  const hasSelectionChanged = !state.selection
142
88
  || state.selection !== newSelection
143
89
  || state.rangeCount !== newSelection.rangeCount
@@ -156,7 +102,6 @@ export function useEditor() {
156
102
  state.range = newSelection.getRangeAt(0).cloneRange()
157
103
  }
158
104
 
159
- // Update styles immediately for better responsiveness
160
105
  updateState.styles()
161
106
  }
162
107
  } catch {
@@ -168,57 +113,29 @@ export function useEditor() {
168
113
  }
169
114
  }
170
115
 
171
- // History management
172
116
  const history = {
173
117
  undo: () => {
174
- if (state.undoStack.length === 0) { return }
175
- state.redoStack.push(state.content)
118
+ if (state.undoStack.length === 0 || !state.doc) { return }
176
119
  const lastContent = state.undoStack.pop()
177
120
  if (lastContent !== undefined) {
121
+ state.redoStack.push(state.content)
178
122
  state.content = lastContent
179
- updateState.content('html')
123
+ restoreHistoryNodes(state.doc, lastContent)
180
124
  }
181
125
  },
182
126
  redo: () => {
183
- if (state.redoStack.length === 0) { return }
184
- state.undoStack.push(state.content)
127
+ if (state.redoStack.length === 0 || !state.doc) { return }
185
128
  const nextContent = state.redoStack.pop()
186
129
  if (nextContent !== undefined) {
130
+ state.undoStack.push(state.content)
187
131
  state.content = nextContent
188
- updateState.content('html')
132
+ restoreHistoryNodes(state.doc, nextContent)
189
133
  }
190
134
  }
191
135
  }
192
136
 
193
- // Content cleanup utilities
194
137
  const cleanup = {
195
- emptyTags: (doc: Document) => {
196
- const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT, null)
197
- const nodesToRemove: Element[] = []
198
- let node = walker.nextNode() as Element
199
-
200
- while (node) {
201
- if (['br', 'img', 'hr', 'input'].includes(node.tagName.toLowerCase())) {
202
- node = walker.nextNode() as Element
203
- continue
204
- }
205
-
206
- const textContent = node.textContent?.trim() || ''
207
- const innerHTML = node.innerHTML.trim()
208
- const hasOnlyBr = innerHTML === '<br>' || innerHTML === '<br/>'
209
- const hasOnlyNbsp = innerHTML === '&nbsp;' || textContent === '\u00A0'
210
- const isEmpty = !textContent && !innerHTML
211
- const isDirectChildOfBody = node.parentElement === doc.body
212
-
213
- if (isEmpty || hasOnlyNbsp || (hasOnlyBr && !isDirectChildOfBody)) {
214
- nodesToRemove.push(node)
215
- }
216
- node = walker.nextNode() as Element
217
- }
218
- nodesToRemove.forEach((node) => { node.remove() })
219
- },
220
138
  normalizeContent: (doc: Document) => {
221
- // Only normalize direct text nodes to paragraphs, don't force any structure
222
139
  const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT)
223
140
  const textNodes: Text[] = []
224
141
  let node: Node | null
@@ -237,52 +154,29 @@ export function useEditor() {
237
154
  }
238
155
 
239
156
  function setupEventListeners(doc: Document) {
240
- // Clean up existing listeners if they exist
241
157
  if (cleanupListeners) {
242
158
  cleanupListeners()
243
159
  cleanupListeners = null
244
160
  }
245
161
 
246
- let isUpdating = false
247
-
248
162
  const events = {
249
- input: () => {
250
- if (isUpdating) { return }
251
- isUpdating = true
252
-
253
- // Immediately update content
254
- const newContent = doc.body.innerHTML
255
- if (newContent !== state.content) {
256
- state.content = newContent
257
- }
258
-
259
- isUpdating = false
260
- },
261
163
  selectionchange: () => {
262
- if (!isUpdating) {
263
- updateState.selection()
264
- }
164
+ updateState.selection()
265
165
  },
266
166
  mouseup: () => {
267
- if (!isUpdating) {
268
- updateState.selection()
269
- }
167
+ updateState.selection()
270
168
  },
271
169
  keyup: (e: KeyboardEvent) => {
272
- if (!isUpdating) {
273
- if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
274
- updateState.selection()
275
- }
170
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
171
+ updateState.selection()
276
172
  }
277
173
  }
278
174
  }
279
175
 
280
- // Only add listeners if they haven't been added yet
281
176
  Object.entries(events).forEach(([event, handler]) => {
282
177
  doc.addEventListener(event, handler as EventListener)
283
178
  })
284
179
 
285
- // Store cleanup function
286
180
  cleanupListeners = () => {
287
181
  Object.entries(events).forEach(([event, handler]) => {
288
182
  doc.removeEventListener(event, handler as EventListener)
@@ -293,34 +187,25 @@ export function useEditor() {
293
187
  }
294
188
 
295
189
  function init(doc: Document) {
296
- if (state.hasInit) {
297
- if (cleanupListeners) {
298
- cleanupListeners()
299
- cleanupListeners = null
300
- }
190
+ if (state.hasInit && cleanupListeners) {
191
+ cleanupListeners()
192
+ cleanupListeners = null
301
193
  }
302
194
 
303
195
  state.doc = doc
304
- state.hasInit = true as const
196
+ state.hasInit = true
305
197
 
306
- // Store state reference in document for table operations
307
- (doc as any).editorState = state
198
+ ;(doc as any).editorState = state
308
199
 
309
- // Initial setup without triggering updates - only set content if it exists and is not just empty paragraphs
310
200
  if (state.content && state.content.trim()
311
201
  && !state.content.match(/^<p(?:\s[^>]*)?>(?:<br\s*\/?>)?\s*<\/p>$/i)) {
312
- const preserved = preserveIframes(state.content)
313
- doc.body.innerHTML = preserved.html
314
- setTimeout(() => {
315
- if (state.doc) {
316
- restoreIframes(doc, state.content, preserved.iframes)
317
- }
318
- }, 0)
202
+ if (doc.body.innerHTML !== state.content) {
203
+ doc.body.innerHTML = state.content
204
+ }
319
205
  }
320
206
 
321
207
  cleanup.normalizeContent(doc)
322
208
 
323
- // Set initial selection only if there's content
324
209
  if (doc.body.firstElementChild) {
325
210
  const range = doc.createRange()
326
211
  const selection = doc.getSelection()
@@ -334,7 +219,6 @@ export function useEditor() {
334
219
  state.rangeCount = selection.rangeCount
335
220
  }
336
221
  } else {
337
- // For empty editor, set cursor at the beginning of body
338
222
  const selection = doc.getSelection()
339
223
  if (selection) {
340
224
  const range = doc.createRange()
@@ -348,7 +232,6 @@ export function useEditor() {
348
232
  }
349
233
  }
350
234
 
351
- // Setup event listeners immediately
352
235
  cleanupListeners = setupEventListeners(doc)
353
236
  }
354
237
 
@@ -373,15 +256,6 @@ export function useEditor() {
373
256
  }
374
257
  }
375
258
 
376
- // Add cleanup on component unmount
377
- if (typeof window !== 'undefined') {
378
- window.addEventListener('beforeunload', () => {
379
- if (cleanupListeners) {
380
- cleanupListeners()
381
- }
382
- })
383
- }
384
-
385
259
  return {
386
260
  state,
387
261
  init,
@@ -1,4 +1,5 @@
1
1
  import type { CommandExecutor } from '../utils/commands'
2
+ import { addRow, buildTableGrid, findCellCoords, focusTableCell, resolveCellFromNode } from '../utils/table'
2
3
 
3
4
  interface KeyboardShortcut {
4
5
  key: string
@@ -29,17 +30,91 @@ const shortcuts: KeyboardShortcut[] = [
29
30
  }))
30
31
  ]
31
32
 
32
- export function useEditorKeyboard(doc: Document, executor: CommandExecutor): void {
33
- // Handle keyboard shortcuts
34
- doc.addEventListener('keydown', (e) => {
35
- // Remove Enter key handling from here - it's handled in setupAutoWrapping in index.vue
36
- // This was causing conflicts with the main Enter key handler
33
+ export function useEditorKeyboard(doc: Document, executor: CommandExecutor): () => void {
34
+ const previousCleanup = (doc as any).__keyboardCleanup as (() => void) | undefined
35
+ previousCleanup?.()
37
36
 
38
- // Handle other keyboard shortcuts
37
+ const onKeyDown = (e: KeyboardEvent) => {
38
+ if (e.isComposing || e.keyCode === 229) {
39
+ return
40
+ }
41
+
42
+ const selection = doc.getSelection()
43
+ const range = selection?.rangeCount ? selection.getRangeAt(0) : null
44
+ const activeCell = range ? resolveCellFromNode(range.startContainer) : null
45
+
46
+ // Table Tab / Shift+Tab navigation
47
+ if (e.key === 'Tab' && activeCell) {
48
+ const table = activeCell.closest('table') as HTMLTableElement | null
49
+ if (table) {
50
+ e.preventDefault()
51
+ const cells = Array.from(table.querySelectorAll<HTMLTableCellElement>('th, td'))
52
+ const currentIndex = cells.indexOf(activeCell)
53
+ if (currentIndex === -1) { return }
54
+
55
+ if (e.shiftKey) {
56
+ if (currentIndex > 0) {
57
+ focusTableCell(cells[currentIndex - 1], false)
58
+ }
59
+ } else {
60
+ if (currentIndex < cells.length - 1) {
61
+ focusTableCell(cells[currentIndex + 1], false)
62
+ } else {
63
+ // At the last cell of the table: insert a new row below and move cursor to its first cell
64
+ const newRow = addRow('after', range, doc, activeCell)
65
+ const firstNewCell = newRow?.cells[0]
66
+ if (firstNewCell) {
67
+ focusTableCell(firstNewCell, true)
68
+ }
69
+ }
70
+ }
71
+ return
72
+ }
73
+ }
74
+
75
+ // Table ArrowUp / ArrowDown boundary escape when table is at start or end of document
76
+ if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && activeCell && range?.collapsed) {
77
+ const table = activeCell.closest('table') as HTMLTableElement | null
78
+ if (table) {
79
+ const map = buildTableGrid(table)
80
+ const coords = findCellCoords(map, activeCell)
81
+ const containerEl = (table.closest('.table-wrapper') || table) as HTMLElement
82
+
83
+ if (coords && e.key === 'ArrowUp' && coords.row === 0 && !containerEl.previousElementSibling) {
84
+ e.preventDefault()
85
+ const p = doc.createElement('p')
86
+ p.dir = doc.body.dir || 'ltr'
87
+ p.innerHTML = '<br>'
88
+ containerEl.insertAdjacentElement('beforebegin', p)
89
+ const nextRange = doc.createRange()
90
+ nextRange.selectNodeContents(p)
91
+ nextRange.collapse(true)
92
+ selection?.removeAllRanges()
93
+ selection?.addRange(nextRange)
94
+ return
95
+ }
96
+
97
+ if (coords && e.key === 'ArrowDown' && coords.row + coords.rowspan >= map.rowCount && !containerEl.nextElementSibling) {
98
+ e.preventDefault()
99
+ const p = doc.createElement('p')
100
+ p.dir = doc.body.dir || 'ltr'
101
+ p.innerHTML = '<br>'
102
+ containerEl.insertAdjacentElement('afterend', p)
103
+ const nextRange = doc.createRange()
104
+ nextRange.selectNodeContents(p)
105
+ nextRange.collapse(true)
106
+ selection?.removeAllRanges()
107
+ selection?.addRange(nextRange)
108
+ return
109
+ }
110
+ }
111
+ }
112
+
113
+ // Handle modifier keyboard shortcuts
39
114
  if (!e.ctrlKey && !e.metaKey) { return }
40
115
 
41
116
  const matchingShortcut = shortcuts.find((shortcut) => {
42
- const keyMatch = shortcut.key === e.key
117
+ const keyMatch = shortcut.key.toLowerCase() === e.key.toLowerCase()
43
118
  const modifiersMatch = Boolean(shortcut.modifiers?.alt) === e.altKey
44
119
  && Boolean(shortcut.modifiers?.shift) === e.shiftKey
45
120
  return keyMatch && modifiersMatch
@@ -47,13 +122,14 @@ export function useEditorKeyboard(doc: Document, executor: CommandExecutor): voi
47
122
 
48
123
  if (matchingShortcut) {
49
124
  e.preventDefault()
50
- // Keep text editing history scoped to the iframe. The browser groups
51
- // typing into granular transactions, while the editor's HTML snapshot
52
- // history is intended for structural toolbar operations.
53
- if (matchingShortcut.command === 'undo' || matchingShortcut.command === 'redo') {
54
- if (doc.execCommand(matchingShortcut.command)) { return }
55
- }
56
125
  executor.execute(matchingShortcut.command)
57
126
  }
58
- })
127
+ }
128
+
129
+ doc.addEventListener('keydown', onKeyDown)
130
+ const cleanup = () => {
131
+ doc.removeEventListener('keydown', onKeyDown)
132
+ }
133
+ ;(doc as any).__keyboardCleanup = cleanup
134
+ return cleanup
59
135
  }