@bagelink/vue 1.17.2 → 1.18.3
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/dist/assetTransport.d.ts +9 -31
- package/dist/assetTransport.d.ts.map +1 -1
- package/dist/assets.d.ts +39 -20
- package/dist/assets.d.ts.map +1 -1
- package/dist/components/AddressSearch.vue.d.ts +40 -36
- package/dist/components/AddressSearch.vue.d.ts.map +1 -1
- package/dist/components/ContextMenu.vue.d.ts +518 -0
- package/dist/components/ContextMenu.vue.d.ts.map +1 -0
- package/dist/components/Dropdown.types.d.ts +25 -0
- package/dist/components/Dropdown.types.d.ts.map +1 -0
- package/dist/components/Dropdown.vue.d.ts +20 -15
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/FilterQuery.vue.d.ts +1 -1
- package/dist/components/FilterQuery.vue.d.ts.map +1 -1
- package/dist/components/Icon/Icon.vue.d.ts +0 -1
- package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
- package/dist/components/Icon/constants.d.ts +0 -1
- package/dist/components/Icon/constants.d.ts.map +1 -1
- package/dist/components/Icon/types.d.ts +2 -3
- package/dist/components/Icon/types.d.ts.map +1 -1
- package/dist/components/Swiper.vue.d.ts +1 -1
- package/dist/components/draggable/useDraggable.d.ts.map +1 -1
- package/dist/components/form/inputs/IconPickerInput.vue.d.ts +48 -37
- package/dist/components/form/inputs/IconPickerInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +3 -0
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts +2 -0
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts +34 -8
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/form/inputs/SelectInput.vue.d.ts +74 -65
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/TelInput.vue.d.ts +2 -2
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/composables/index.d.ts +1 -1
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/useBoundedRectDrag.d.ts.map +1 -1
- package/dist/composables/usePointerSession.d.ts +14 -3
- package/dist/composables/usePointerSession.d.ts.map +1 -1
- package/dist/core.css +3 -3
- package/dist/directives/vResize.d.ts.map +1 -1
- package/dist/form-flow/FormFlow.vue.d.ts.map +1 -1
- package/dist/form-flow/form-flow.d.ts +4 -2
- package/dist/form-flow/form-flow.d.ts.map +1 -1
- package/dist/form-flow/schema-fields.d.ts +4 -1
- package/dist/form-flow/schema-fields.d.ts.map +1 -1
- package/dist/index.cjs +240 -153
- package/dist/index.css +1 -1
- package/dist/index.mjs +18461 -18283
- package/dist/style.css +3 -3
- package/dist/types/BagelForm.d.ts +2 -0
- package/dist/types/BagelForm.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/assetTransport.test.ts +33 -83
- package/src/assetTransport.ts +24 -90
- package/src/assets.test.ts +58 -99
- package/src/assets.ts +78 -161
- package/src/components/ContextMenu.vue +113 -0
- package/src/components/Dropdown.types.ts +27 -0
- package/src/components/Dropdown.vue +129 -64
- package/src/components/FilterQuery.vue +1 -1
- package/src/components/Icon/Icon.vue +8 -30
- package/src/components/Icon/constants.ts +0 -166
- package/src/components/Icon/types.ts +2 -3
- package/src/components/draggable/useDraggable.ts +67 -100
- package/src/components/form/inputs/IconPickerInput.vue +14 -9
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +43 -111
- package/src/components/form/inputs/RichText/composables/useCommands.ts +32 -54
- package/src/components/form/inputs/RichText/composables/useEditor.ts +45 -171
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +90 -14
- package/src/components/form/inputs/RichText/index.vue +1195 -1210
- package/src/components/form/inputs/RichText/utils/commands.ts +689 -1028
- package/src/components/form/inputs/RichText/utils/selection.ts +66 -41
- package/src/components/form/inputs/RichText/utils/table.ts +798 -136
- package/src/components/form/inputs/SelectInput.vue +6 -4
- package/src/components/form/inputs/Upload/AssetUploadInput.vue +2 -2
- package/src/components/index.ts +3 -1
- package/src/composables/index.ts +1 -1
- package/src/composables/useBoundedRectDrag.ts +9 -12
- package/src/composables/usePointerSession.ts +57 -12
- package/src/directives/vResize.ts +6 -7
- package/src/form-flow/FormFlow.vue +4 -0
- package/src/form-flow/form-flow.ts +8 -3
- package/src/form-flow/schema-fields.ts +9 -4
- package/src/styles/core.css +1 -18
- package/src/types/BagelForm.ts +2 -0
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts +0 -10
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +0 -1
- package/src/components/form/inputs/RichText/components/gridBox.vue +0 -58
- 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 {
|
|
4
|
+
import { computeActiveStyles } from '../utils/selection'
|
|
5
5
|
|
|
6
|
-
function
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
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
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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 === ' ' || 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
|
-
|
|
263
|
-
updateState.selection()
|
|
264
|
-
}
|
|
164
|
+
updateState.selection()
|
|
265
165
|
},
|
|
266
166
|
mouseup: () => {
|
|
267
|
-
|
|
268
|
-
updateState.selection()
|
|
269
|
-
}
|
|
167
|
+
updateState.selection()
|
|
270
168
|
},
|
|
271
169
|
keyup: (e: KeyboardEvent) => {
|
|
272
|
-
if (
|
|
273
|
-
|
|
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
|
-
|
|
298
|
-
|
|
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
|
|
196
|
+
state.hasInit = true
|
|
305
197
|
|
|
306
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
}
|