@bagelink/vue 1.4.139 → 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.
- 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.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts +1 -1
- package/dist/components/form/inputs/RichText/richTextTypes.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/formatting.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/index.cjs +20 -20
- package/dist/index.mjs +19 -19
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/dataTable/DataTable.vue +1 -1
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +11 -0
- package/src/components/form/inputs/RichText/composables/useCommands.ts +42 -0
- package/src/components/form/inputs/RichText/composables/useEditor.ts +8 -5
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +2 -128
- package/src/components/form/inputs/RichText/config.ts +15 -4
- package/src/components/form/inputs/RichText/index.vue +275 -73
- package/src/components/form/inputs/RichText/richTextTypes.ts +5 -0
- package/src/components/form/inputs/RichText/utils/commands.ts +614 -82
- package/src/components/form/inputs/RichText/utils/formatting.ts +17 -15
- package/src/components/form/inputs/RichText/utils/selection.ts +32 -11
|
@@ -45,107 +45,290 @@ onUnmounted(() => {
|
|
|
45
45
|
})
|
|
46
46
|
|
|
47
47
|
function setupAutoWrapping(doc: Document) {
|
|
48
|
-
//
|
|
49
|
-
doc.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
88
|
+
const textNodes: Text[] = []
|
|
89
|
+
let node
|
|
90
|
+
while ((node = walker.nextNode())) {
|
|
91
|
+
textNodes.push(node as Text)
|
|
92
|
+
}
|
|
64
93
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
|
173
|
+
// Handle Enter key
|
|
82
174
|
doc.addEventListener('keydown', (e) => {
|
|
83
|
-
if (e.key === '
|
|
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
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
289
|
+
// Handle paste
|
|
121
290
|
doc.addEventListener('paste', (e) => {
|
|
291
|
+
// Give the paste operation time to complete before normalizing
|
|
122
292
|
setTimeout(() => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
}
|
|
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
|
|
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) {
|