@bagelink/vue 1.4.136 → 1.4.141

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 (36) hide show
  1. package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
  2. package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
  3. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
  4. package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
  5. package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
  6. package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
  7. package/dist/components/form/inputs/RichText/richTextTypes.d.ts +1 -1
  8. package/dist/components/form/inputs/RichText/richTextTypes.d.ts.map +1 -1
  9. package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
  10. package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +1 -1
  11. package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
  12. package/dist/composables/useSchemaField.d.ts.map +1 -1
  13. package/dist/index.cjs +10 -10
  14. package/dist/index.mjs +10 -10
  15. package/dist/style.css +1 -1
  16. package/dist/types/BagelForm.d.ts +13 -5
  17. package/dist/types/BagelForm.d.ts.map +1 -1
  18. package/dist/utils/BagelFormUtils.d.ts +28 -6
  19. package/dist/utils/BagelFormUtils.d.ts.map +1 -1
  20. package/package.json +1 -1
  21. package/src/components/dataTable/DataTable.vue +1 -1
  22. package/src/components/form/inputs/RadioGroup.vue +4 -4
  23. package/src/components/form/inputs/RichText/components/EditorToolbar.vue +14 -0
  24. package/src/components/form/inputs/RichText/composables/useCommands.ts +42 -0
  25. package/src/components/form/inputs/RichText/composables/useEditor.ts +8 -5
  26. package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +2 -128
  27. package/src/components/form/inputs/RichText/config.ts +18 -4
  28. package/src/components/form/inputs/RichText/index.vue +275 -73
  29. package/src/components/form/inputs/RichText/richTextTypes.ts +5 -0
  30. package/src/components/form/inputs/RichText/utils/commands.ts +614 -82
  31. package/src/components/form/inputs/RichText/utils/formatting.ts +17 -15
  32. package/src/components/form/inputs/RichText/utils/selection.ts +32 -11
  33. package/src/composables/useSchemaField.ts +31 -17
  34. package/src/types/BagelForm.ts +26 -12
  35. package/src/utils/BagelFormUtils.ts +97 -7
  36. package/src/utils/index.ts +1 -1
@@ -45,107 +45,290 @@ onUnmounted(() => {
45
45
  })
46
46
 
47
47
  function setupAutoWrapping(doc: Document) {
48
- // Handle typing in empty editor
49
- doc.addEventListener('input', () => {
50
- // If the editor is empty or only has whitespace, wrap content in a paragraph
51
- if (!doc.body.firstElementChild
52
- || (!doc.body.textContent?.trim() && !doc.body.querySelector('p,h1,h2,h3,h4,h5,h6,blockquote,ul,ol,table'))) {
53
- // If there's any text content, wrap it in a paragraph
54
- if (doc.body.textContent?.trim()) {
55
- const p = doc.createElement('p')
56
- p.dir = doc.body.dir
48
+ // Initialize editor with paragraph
49
+ if (!doc.body.innerHTML.trim()) {
50
+ doc.body.innerHTML = '<p><br></p>'
51
+ }
52
+
53
+ // After any change, ensure proper structure
54
+ function normalizeContent() {
55
+ // Only proceed if the body exists
56
+ if (!doc.body) {
57
+ return
58
+ }
59
+
60
+ // If body is completely empty, add a paragraph and return
61
+ if (!doc.body.innerHTML.trim() || doc.body.innerHTML === '') {
62
+ doc.body.innerHTML = '<p><br></p>'
63
+ return
64
+ }
57
65
 
58
- // Move all content to the paragraph
59
- while (doc.body.firstChild) {
60
- p.appendChild(doc.body.firstChild)
66
+ // Mark body as being normalized to prevent recursive processing
67
+ if (doc.body.dataset.normalizing === 'true') {
68
+ return
69
+ }
70
+ doc.body.dataset.normalizing = 'true'
71
+
72
+ try {
73
+ // Only wrap loose text nodes that are direct children of body
74
+ const walker = doc.createTreeWalker(
75
+ doc.body,
76
+ NodeFilter.SHOW_TEXT,
77
+ node => {
78
+ // Only accept text nodes that are direct children of body
79
+ // AND have meaningful content
80
+ const parent = node.parentNode as HTMLElement
81
+ return (parent === doc.body &&
82
+ node.textContent?.trim() &&
83
+ node.textContent.trim().length > 0) ?
84
+ NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
61
85
  }
86
+ )
62
87
 
63
- doc.body.appendChild(p)
88
+ const textNodes: Text[] = []
89
+ let node
90
+ while ((node = walker.nextNode())) {
91
+ textNodes.push(node as Text)
92
+ }
64
93
 
65
- // Set cursor at the end of the paragraph
66
- const selection = doc.getSelection()
67
- if (selection) {
68
- const range = doc.createRange()
69
- range.selectNodeContents(p)
70
- range.collapse(false)
71
- selection.removeAllRanges()
72
- selection.addRange(range)
94
+ // Wrap loose text nodes very carefully, preserving existing structure
95
+ textNodes.forEach(textNode => {
96
+ // Triple check the node is still valid and not corrupted
97
+ if (textNode.parentNode === doc.body &&
98
+ textNode.textContent?.trim() &&
99
+ textNode.nodeType === Node.TEXT_NODE) {
100
+
101
+ const p = doc.createElement('p')
102
+ p.dir = doc.body.dir || 'ltr'
103
+
104
+ // Clone the text content to avoid any reference issues
105
+ const textContent = textNode.textContent
106
+ p.textContent = textContent
107
+
108
+ // Replace the text node with the paragraph
109
+ try {
110
+ textNode.parentNode.replaceChild(p, textNode)
111
+ } catch (error) {
112
+ // If replacement fails, just remove the problematic text node
113
+ textNode.parentNode.removeChild(textNode)
114
+ }
73
115
  }
116
+ })
117
+
118
+ // Ensure empty body has a paragraph
119
+ if (!doc.body.children.length || !doc.body.innerHTML.trim()) {
120
+ doc.body.innerHTML = '<p><br></p>'
121
+ }
74
122
 
75
- // Update editor state
76
- editor.state.content = doc.body.innerHTML
123
+ // Clean up empty paragraphs more conservatively
124
+ const paras = Array.from(doc.body.querySelectorAll('p'))
125
+ for (let i = 0; i < paras.length; i++) {
126
+ const current = paras[i]
127
+ const isEmpty = !current.textContent?.trim() || current.innerHTML === '<br>'
128
+
129
+ if (isEmpty) {
130
+ // Only remove if we have consecutive empty paragraphs at the end
131
+ const isLastPara = i === paras.length - 1
132
+ const prevPara = i > 0 ? paras[i - 1] : null
133
+ const isPrevEmpty = prevPara && (!prevPara.textContent?.trim() || prevPara.innerHTML === '<br>')
134
+
135
+ // Keep at least one empty paragraph for cursor placement
136
+ if (isPrevEmpty && !isLastPara) {
137
+ current.remove()
138
+ }
139
+ }
77
140
  }
141
+
142
+ } finally {
143
+ // Always clean up the processing flag
144
+ delete doc.body.dataset.normalizing
145
+ }
146
+ }
147
+
148
+ // Handle input events
149
+ doc.addEventListener('input', (e) => {
150
+ // Handle complete content deletion immediately
151
+ if (!doc.body.innerHTML.trim() || doc.body.innerHTML === '') {
152
+ doc.body.innerHTML = '<p><br></p>'
153
+ editor.state.content = doc.body.innerHTML
154
+ return
78
155
  }
156
+
157
+ // Don't normalize during normal typing - only on paste/drop
158
+ const inputEvent = e as InputEvent
159
+ const normalizeInputTypes = ['insertFromPaste', 'insertFromDrop']
160
+
161
+ // Only normalize on paste/drop operations to avoid interfering with typing
162
+ if (inputEvent.inputType && normalizeInputTypes.includes(inputEvent.inputType)) {
163
+ // Add a delay to let the browser finish processing
164
+ setTimeout(() => {
165
+ normalizeContent()
166
+ }, 100)
167
+ }
168
+
169
+ // Always update content to keep state in sync
170
+ editor.state.content = doc.body.innerHTML
79
171
  })
80
172
 
81
- // Handle backspace to prevent completely empty editor
173
+ // Handle Enter key
82
174
  doc.addEventListener('keydown', (e) => {
83
- if (e.key === 'Backspace') {
84
- // Check if this would make the editor completely empty
175
+ if (e.key === 'Enter') {
85
176
  const selection = doc.getSelection()
86
177
  if (!selection || !selection.rangeCount) return
87
178
 
88
179
  const range = selection.getRangeAt(0)
89
180
 
90
- // If we're about to delete the last block element
91
- const blockElements = doc.body.querySelectorAll('p,h1,h2,h3,h4,h5,h6,blockquote,li,div')
92
- if (blockElements.length <= 1) {
93
- const lastBlock = blockElements[0]
94
-
95
- // If we're in the last block and it's about to be empty
96
- if (lastBlock && (
97
- (lastBlock.textContent?.trim() === '' && range.collapsed)
98
- || (range.startContainer === lastBlock && range.endContainer === lastBlock
99
- && range.startOffset === 0 && range.endOffset === lastBlock.childNodes.length)
100
- )) {
101
- e.preventDefault()
102
-
103
- // Clear the content but keep the block structure
104
- lastBlock.innerHTML = ''
105
-
106
- // Set cursor in the empty block
107
- const newRange = doc.createRange()
108
- newRange.selectNodeContents(lastBlock)
109
- newRange.collapse(true)
110
- selection.removeAllRanges()
111
- selection.addRange(newRange)
112
-
113
- // Update editor state
114
- editor.state.content = doc.body.innerHTML
181
+ // Check if we're in any list context - let browser handle lists
182
+ let currentElement = range.startContainer
183
+
184
+ // If it's a text node, get its parent
185
+ if (currentElement.nodeType === Node.TEXT_NODE) {
186
+ currentElement = currentElement.parentElement!
187
+ }
188
+
189
+ // Check if we're in a list item or list container
190
+ const li = (currentElement as Element).closest('li')
191
+ const listContainer = (currentElement as Element).closest('ul, ol')
192
+
193
+ if (li || listContainer) {
194
+ // We're in a list context - let browser handle everything
195
+ return
196
+ } // Find the paragraph we're in (only for confirmed non-list content)
197
+ let paragraph = range.startContainer
198
+ if (paragraph.nodeType === Node.TEXT_NODE) {
199
+ paragraph = paragraph.parentElement!
200
+ }
201
+ paragraph = (paragraph as Element).closest('p,h1,h2,h3,h4,h5,h6') as HTMLElement
202
+
203
+ if (!paragraph) return
204
+
205
+ // Check if current paragraph is empty and previous paragraph is also empty
206
+ const currentIsEmpty = !paragraph.textContent?.trim()
207
+ const prevSibling = (paragraph as Element).previousElementSibling as HTMLElement
208
+ const prevIsEmpty = prevSibling &&
209
+ (prevSibling.tagName === 'P') &&
210
+ !prevSibling.textContent?.trim()
211
+
212
+ // If both current and previous paragraphs are empty, don't create another empty paragraph
213
+ if (currentIsEmpty && prevIsEmpty) {
214
+ e.preventDefault()
215
+ return
216
+ }
217
+
218
+ // For RTL text, be more careful about splitting
219
+ const isRTLContent = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/.test(paragraph.textContent || '')
220
+
221
+ // Prevent default and handle manually to avoid browser inconsistencies
222
+ e.preventDefault()
223
+ e.stopPropagation()
224
+
225
+ // Get the text content before and after cursor
226
+ const textBeforeCursor = range.startContainer.textContent?.substring(0, range.startOffset) || ''
227
+ const textAfterCursor = range.startContainer.textContent?.substring(range.startOffset) || ''
228
+
229
+ // Debug logging to understand the issue
230
+ if (props.debug) {
231
+ console.log('Enter pressed:', {
232
+ startContainer: range.startContainer,
233
+ startOffset: range.startOffset,
234
+ textBeforeCursor,
235
+ textAfterCursor,
236
+ paragraphContent: paragraph.textContent,
237
+ isRTLContent
238
+ })
239
+ }
240
+
241
+ // Create new paragraph with proper direction
242
+ const newParagraph = doc.createElement('p')
243
+ newParagraph.dir = isRTLContent ? 'rtl' : doc.body.dir || 'ltr'
244
+
245
+ // If we're splitting text, handle it carefully
246
+ if (range.startContainer.nodeType === Node.TEXT_NODE && textAfterCursor.trim()) {
247
+ // We're in the middle of text - split properly
248
+ const textNode = range.startContainer as Text
249
+
250
+ // Store the original parent element for formatting preservation
251
+ const parentElement = textNode.parentElement
252
+
253
+ // Keep text before cursor in current paragraph
254
+ textNode.textContent = textBeforeCursor
255
+
256
+ // Put text after cursor in new paragraph
257
+ if (textAfterCursor.trim()) {
258
+ // Preserve any formatting from the parent element
259
+ if (parentElement && parentElement !== paragraph) {
260
+ const newFormattedElement = parentElement.cloneNode(false) as HTMLElement
261
+ newFormattedElement.textContent = textAfterCursor
262
+ newParagraph.appendChild(newFormattedElement)
263
+ } else {
264
+ newParagraph.textContent = textAfterCursor
265
+ }
266
+ } else {
267
+ newParagraph.innerHTML = '<br>'
115
268
  }
269
+ } else {
270
+ // We're at the end of text or in empty space
271
+ newParagraph.innerHTML = '<br>'
116
272
  }
273
+
274
+ // Insert the new paragraph
275
+ paragraph.parentNode?.insertBefore(newParagraph, paragraph.nextSibling)
276
+
277
+ // Set cursor at the beginning of the new paragraph
278
+ const newRange = doc.createRange()
279
+ newRange.selectNodeContents(newParagraph)
280
+ newRange.collapse(true)
281
+ selection.removeAllRanges()
282
+ selection.addRange(newRange)
283
+
284
+ // Update content immediately to reflect changes
285
+ editor.state.content = doc.body.innerHTML
117
286
  }
118
287
  })
119
288
 
120
- // Handle paste events to ensure content is properly wrapped
289
+ // Handle paste
121
290
  doc.addEventListener('paste', (e) => {
291
+ // Give the paste operation time to complete before normalizing
122
292
  setTimeout(() => {
123
- // After paste, ensure any unwrapped text gets wrapped in paragraphs
124
- const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT)
125
- const textNodes: Text[] = []
126
- let node: Node | null
127
- while ((node = walker.nextNode())) {
128
- if (node.parentElement === doc.body && node.textContent?.trim()) {
129
- textNodes.push(node as Text)
130
- }
131
- }
132
-
133
- textNodes.forEach((textNode) => {
134
- const p = doc.createElement('p')
135
- p.dir = doc.body.dir
136
- p.appendChild(textNode.cloneNode())
137
- doc.body.replaceChild(p, textNode)
138
- })
293
+ normalizeContent()
294
+ editor.state.content = doc.body.innerHTML
295
+ }, 150) // Longer timeout for reliable paste processing
296
+ })
139
297
 
140
- // Update editor state if changes were made
141
- if (textNodes.length > 0) {
142
- editor.state.content = doc.body.innerHTML
298
+ // Add a MutationObserver to catch structural changes that might create loose text
299
+ const observer = new MutationObserver((mutations) => {
300
+ for (const mutation of mutations) {
301
+ if (mutation.type === 'childList') {
302
+ // Check if any loose text nodes were added to body
303
+ Array.from(mutation.addedNodes).forEach(addedNode => {
304
+ if (addedNode.nodeType === Node.TEXT_NODE &&
305
+ addedNode.parentNode === doc.body &&
306
+ addedNode.textContent?.trim()) {
307
+ // Wrap loose text node immediately
308
+ const p = doc.createElement('p')
309
+ p.dir = doc.body.dir || 'ltr'
310
+ p.textContent = addedNode.textContent
311
+ addedNode.parentNode.replaceChild(p, addedNode)
312
+ editor.state.content = doc.body.innerHTML
313
+ }
314
+ })
143
315
  }
144
- }, 0)
316
+ }
317
+ })
318
+
319
+ // Start observing
320
+ observer.observe(doc.body, {
321
+ childList: true,
322
+ subtree: false // Only watch direct children of body
145
323
  })
324
+
325
+ // Store observer for cleanup
326
+ if (!doc.body.dataset.observers) {
327
+ doc.body.dataset.observers = 'mutation'
328
+ }
146
329
  }
147
330
 
148
- async function initEditor() {
331
+ const initEditor = async () => {
149
332
  if (isInitializing.value || !iframe.value || hasInitialized.value) {
150
333
  return
151
334
  }
@@ -230,6 +413,25 @@ async function initEditor() {
230
413
  // Update state.content after cleanup
231
414
  editor.state.content = doc.body.innerHTML
232
415
 
416
+ // If editor is empty, add an initial paragraph
417
+ if (!doc.body.innerHTML.trim() || !doc.body.querySelector('p,h1,h2,h3,h4,h5,h6,blockquote,ul,ol,table')) {
418
+ const p = doc.createElement('p')
419
+ p.dir = doc.body.dir
420
+ doc.body.appendChild(p)
421
+
422
+ // Set cursor in the new paragraph
423
+ const range = doc.createRange()
424
+ range.selectNodeContents(p)
425
+ range.collapse(true)
426
+ const selection = doc.getSelection()
427
+ if (selection) {
428
+ selection.removeAllRanges()
429
+ selection.addRange(range)
430
+ }
431
+
432
+ editor.state.content = doc.body.innerHTML
433
+ }
434
+
233
435
  doc.body.focus()
234
436
  hasInitialized.value = true
235
437
  } catch (error) {
@@ -52,6 +52,11 @@ export type FormattingCommand =
52
52
  | 'table'
53
53
  | 'splitView'
54
54
  | 'codeView'
55
+ | 'textDirection'
56
+ | 'alignLeft'
57
+ | 'alignCenter'
58
+ | 'alignRight'
59
+ | 'alignJustify'
55
60
 
56
61
  export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
57
62