@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
|
@@ -30,6 +30,134 @@ function getCurrentSelection(state: EditorState): { selection: Selection, range:
|
|
|
30
30
|
return { selection, range }
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Alternative helper function to apply formatting to word at cursor
|
|
34
|
+
function applyFormattingToWordAtCursor(doc: Document, range: Range, tagName: string): boolean {
|
|
35
|
+
if (!range.collapsed) return false
|
|
36
|
+
|
|
37
|
+
let textNode = range.startContainer
|
|
38
|
+
let offset = range.startOffset
|
|
39
|
+
|
|
40
|
+
// If we're in an element node, try to find a text node
|
|
41
|
+
if (textNode.nodeType !== Node.TEXT_NODE) {
|
|
42
|
+
const element = textNode as Element
|
|
43
|
+
if (element.childNodes.length > 0 && offset < element.childNodes.length) {
|
|
44
|
+
const childNode = element.childNodes[offset]
|
|
45
|
+
if (childNode.nodeType === Node.TEXT_NODE) {
|
|
46
|
+
textNode = childNode
|
|
47
|
+
offset = 0
|
|
48
|
+
} else {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if we're already inside a formatting element
|
|
57
|
+
const parentElement = textNode.parentElement
|
|
58
|
+
const existingFormatElement = parentElement?.closest(tagName.toLowerCase())
|
|
59
|
+
|
|
60
|
+
if (existingFormatElement) {
|
|
61
|
+
// Remove existing formatting
|
|
62
|
+
try {
|
|
63
|
+
const parent = existingFormatElement.parentNode
|
|
64
|
+
if (parent) {
|
|
65
|
+
// Move all children of the format element to its parent
|
|
66
|
+
while (existingFormatElement.firstChild) {
|
|
67
|
+
parent.insertBefore(existingFormatElement.firstChild, existingFormatElement)
|
|
68
|
+
}
|
|
69
|
+
parent.removeChild(existingFormatElement)
|
|
70
|
+
|
|
71
|
+
// Position cursor at the same relative position
|
|
72
|
+
const newRange = doc.createRange()
|
|
73
|
+
newRange.setStart(textNode, offset)
|
|
74
|
+
newRange.collapse(true)
|
|
75
|
+
|
|
76
|
+
const selection = doc.getSelection()
|
|
77
|
+
if (selection) {
|
|
78
|
+
selection.removeAllRanges()
|
|
79
|
+
selection.addRange(newRange)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('Error removing formatting from word:', error)
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const text = textNode.textContent || ''
|
|
91
|
+
if (!text) return false
|
|
92
|
+
|
|
93
|
+
// Find word boundaries - support Hebrew, English, and numbers
|
|
94
|
+
let start = offset
|
|
95
|
+
let end = offset
|
|
96
|
+
|
|
97
|
+
// Regular expression for word characters (Hebrew, English, numbers)
|
|
98
|
+
const wordChar = /[\u0590-\u05FF\u0600-\u06FF\w]/
|
|
99
|
+
|
|
100
|
+
// Move start backwards to find word beginning
|
|
101
|
+
while (start > 0 && wordChar.test(text[start - 1])) {
|
|
102
|
+
start--
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Move end forwards to find word ending
|
|
106
|
+
while (end < text.length && wordChar.test(text[end])) {
|
|
107
|
+
end++
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// If we found a word, apply formatting
|
|
111
|
+
if (start < end && end > start) {
|
|
112
|
+
try {
|
|
113
|
+
// Split the text node into three parts: before, word, after
|
|
114
|
+
const beforeText = text.substring(0, start)
|
|
115
|
+
const wordText = text.substring(start, end)
|
|
116
|
+
const afterText = text.substring(end)
|
|
117
|
+
|
|
118
|
+
// Create the formatting element
|
|
119
|
+
const formatElement = doc.createElement(tagName)
|
|
120
|
+
formatElement.textContent = wordText
|
|
121
|
+
|
|
122
|
+
// Replace the text node with the new structure
|
|
123
|
+
const parent = textNode.parentNode
|
|
124
|
+
if (parent) {
|
|
125
|
+
// Create text nodes for before and after
|
|
126
|
+
if (beforeText) {
|
|
127
|
+
const beforeNode = doc.createTextNode(beforeText)
|
|
128
|
+
parent.insertBefore(beforeNode, textNode)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
parent.insertBefore(formatElement, textNode)
|
|
132
|
+
|
|
133
|
+
if (afterText) {
|
|
134
|
+
const afterNode = doc.createTextNode(afterText)
|
|
135
|
+
parent.insertBefore(afterNode, textNode)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
parent.removeChild(textNode)
|
|
139
|
+
|
|
140
|
+
// Position cursor after the formatted word
|
|
141
|
+
const newRange = doc.createRange()
|
|
142
|
+
newRange.setStartAfter(formatElement)
|
|
143
|
+
newRange.collapse(true)
|
|
144
|
+
|
|
145
|
+
const selection = doc.getSelection()
|
|
146
|
+
if (selection) {
|
|
147
|
+
selection.removeAllRanges()
|
|
148
|
+
selection.addRange(newRange)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.error('Error applying formatting to word:', error)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return false
|
|
159
|
+
}
|
|
160
|
+
|
|
33
161
|
function updateStateAfterCommand(state: EditorState) {
|
|
34
162
|
if (!state.doc) return
|
|
35
163
|
|
|
@@ -94,81 +222,213 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
94
222
|
})
|
|
95
223
|
}
|
|
96
224
|
|
|
97
|
-
// Basic text formatting commands
|
|
225
|
+
// Basic text formatting commands with toggle functionality
|
|
98
226
|
const textCommands = {
|
|
99
227
|
bold: createCommand('Bold', (state) => {
|
|
100
228
|
const selectionInfo = getCurrentSelection(state)
|
|
101
|
-
if (!selectionInfo
|
|
229
|
+
if (!selectionInfo) return
|
|
102
230
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
231
|
+
let { range, selection } = selectionInfo
|
|
232
|
+
|
|
233
|
+
// If no text is selected, try to apply formatting to word at cursor
|
|
234
|
+
if (range.collapsed) {
|
|
235
|
+
const success = applyFormattingToWordAtCursor(state.doc!, range, 'b')
|
|
236
|
+
if (success) return
|
|
237
|
+
else return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Check if the selection is already bold
|
|
241
|
+
const commonAncestor = range.commonAncestorContainer
|
|
242
|
+
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
|
|
243
|
+
? commonAncestor.parentElement
|
|
244
|
+
: commonAncestor as Element
|
|
245
|
+
|
|
246
|
+
const boldElement = parentElement?.closest('b, strong')
|
|
107
247
|
|
|
248
|
+
if (boldElement) {
|
|
249
|
+
// Remove bold formatting
|
|
108
250
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
251
|
+
const parent = boldElement.parentNode
|
|
252
|
+
while (boldElement.firstChild) {
|
|
253
|
+
parent?.insertBefore(boldElement.firstChild, boldElement)
|
|
254
|
+
}
|
|
255
|
+
parent?.removeChild(boldElement)
|
|
256
|
+
|
|
257
|
+
// Restore selection
|
|
258
|
+
selection.removeAllRanges()
|
|
259
|
+
selection.addRange(range)
|
|
260
|
+
} catch (error) {
|
|
261
|
+
console.error('Error removing bold:', error)
|
|
114
262
|
}
|
|
263
|
+
} else {
|
|
264
|
+
// Apply bold formatting
|
|
265
|
+
try {
|
|
266
|
+
const b = state.doc!.createElement('b')
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
range.surroundContents(b)
|
|
270
|
+
} catch {
|
|
271
|
+
const fragment = range.extractContents()
|
|
272
|
+
b.appendChild(fragment)
|
|
273
|
+
range.insertNode(b)
|
|
274
|
+
}
|
|
115
275
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
276
|
+
range.selectNodeContents(b)
|
|
277
|
+
selection.removeAllRanges()
|
|
278
|
+
selection.addRange(range)
|
|
279
|
+
} catch (error) {
|
|
280
|
+
console.error('Error applying bold:', error)
|
|
281
|
+
}
|
|
121
282
|
}
|
|
283
|
+
}, (state) => {
|
|
284
|
+
// isActive function for bold
|
|
285
|
+
const selectionInfo = getCurrentSelection(state)
|
|
286
|
+
if (!selectionInfo) return false
|
|
287
|
+
|
|
288
|
+
const commonAncestor = selectionInfo.range.commonAncestorContainer
|
|
289
|
+
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
|
|
290
|
+
? commonAncestor.parentElement
|
|
291
|
+
: commonAncestor as Element
|
|
292
|
+
|
|
293
|
+
return !!parentElement?.closest('b, strong')
|
|
122
294
|
}),
|
|
123
295
|
|
|
124
296
|
italic: createCommand('Italic', (state) => {
|
|
125
297
|
const selectionInfo = getCurrentSelection(state)
|
|
126
|
-
if (!selectionInfo
|
|
298
|
+
if (!selectionInfo) return
|
|
127
299
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
300
|
+
let { range, selection } = selectionInfo
|
|
301
|
+
|
|
302
|
+
// If no text is selected, try to apply formatting to word at cursor
|
|
303
|
+
if (range.collapsed) {
|
|
304
|
+
const success = applyFormattingToWordAtCursor(state.doc!, range, 'i')
|
|
305
|
+
if (success) return
|
|
306
|
+
else return
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Check if the selection is already italic
|
|
310
|
+
const commonAncestor = range.commonAncestorContainer
|
|
311
|
+
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
|
|
312
|
+
? commonAncestor.parentElement
|
|
313
|
+
: commonAncestor as Element
|
|
132
314
|
|
|
315
|
+
const italicElement = parentElement?.closest('i, em')
|
|
316
|
+
|
|
317
|
+
if (italicElement) {
|
|
318
|
+
// Remove italic formatting
|
|
133
319
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
320
|
+
const parent = italicElement.parentNode
|
|
321
|
+
while (italicElement.firstChild) {
|
|
322
|
+
parent?.insertBefore(italicElement.firstChild, italicElement)
|
|
323
|
+
}
|
|
324
|
+
parent?.removeChild(italicElement)
|
|
325
|
+
|
|
326
|
+
// Restore selection
|
|
327
|
+
selection.removeAllRanges()
|
|
328
|
+
selection.addRange(range)
|
|
329
|
+
} catch (error) {
|
|
330
|
+
console.error('Error removing italic:', error)
|
|
139
331
|
}
|
|
332
|
+
} else {
|
|
333
|
+
// Apply italic formatting
|
|
334
|
+
try {
|
|
335
|
+
const i = state.doc!.createElement('i')
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
range.surroundContents(i)
|
|
339
|
+
} catch {
|
|
340
|
+
const fragment = range.extractContents()
|
|
341
|
+
i.appendChild(fragment)
|
|
342
|
+
range.insertNode(i)
|
|
343
|
+
}
|
|
140
344
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
345
|
+
range.selectNodeContents(i)
|
|
346
|
+
selection.removeAllRanges()
|
|
347
|
+
selection.addRange(range)
|
|
348
|
+
} catch (error) {
|
|
349
|
+
console.error('Error applying italic:', error)
|
|
350
|
+
}
|
|
146
351
|
}
|
|
352
|
+
}, (state) => {
|
|
353
|
+
// isActive function for italic
|
|
354
|
+
const selectionInfo = getCurrentSelection(state)
|
|
355
|
+
if (!selectionInfo) return false
|
|
356
|
+
|
|
357
|
+
const commonAncestor = selectionInfo.range.commonAncestorContainer
|
|
358
|
+
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
|
|
359
|
+
? commonAncestor.parentElement
|
|
360
|
+
: commonAncestor as Element
|
|
361
|
+
|
|
362
|
+
return !!parentElement?.closest('i, em')
|
|
147
363
|
}),
|
|
148
364
|
|
|
149
365
|
underline: createCommand('Underline', (state) => {
|
|
150
366
|
const selectionInfo = getCurrentSelection(state)
|
|
151
|
-
if (!selectionInfo
|
|
367
|
+
if (!selectionInfo) return
|
|
152
368
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
369
|
+
let { range, selection } = selectionInfo
|
|
370
|
+
|
|
371
|
+
// If no text is selected, try to apply formatting to word at cursor
|
|
372
|
+
if (range.collapsed) {
|
|
373
|
+
const success = applyFormattingToWordAtCursor(state.doc!, range, 'u')
|
|
374
|
+
if (success) return
|
|
375
|
+
else return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Check if the selection is already underlined
|
|
379
|
+
const commonAncestor = range.commonAncestorContainer
|
|
380
|
+
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
|
|
381
|
+
? commonAncestor.parentElement
|
|
382
|
+
: commonAncestor as Element
|
|
157
383
|
|
|
384
|
+
const underlineElement = parentElement?.closest('u')
|
|
385
|
+
|
|
386
|
+
if (underlineElement) {
|
|
387
|
+
// Remove underline formatting
|
|
158
388
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
389
|
+
const parent = underlineElement.parentNode
|
|
390
|
+
while (underlineElement.firstChild) {
|
|
391
|
+
parent?.insertBefore(underlineElement.firstChild, underlineElement)
|
|
392
|
+
}
|
|
393
|
+
parent?.removeChild(underlineElement)
|
|
394
|
+
|
|
395
|
+
// Restore selection
|
|
396
|
+
selection.removeAllRanges()
|
|
397
|
+
selection.addRange(range)
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error('Error removing underline:', error)
|
|
164
400
|
}
|
|
401
|
+
} else {
|
|
402
|
+
// Apply underline formatting
|
|
403
|
+
try {
|
|
404
|
+
const u = state.doc!.createElement('u')
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
range.surroundContents(u)
|
|
408
|
+
} catch {
|
|
409
|
+
const fragment = range.extractContents()
|
|
410
|
+
u.appendChild(fragment)
|
|
411
|
+
range.insertNode(u)
|
|
412
|
+
}
|
|
165
413
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
414
|
+
range.selectNodeContents(u)
|
|
415
|
+
selection.removeAllRanges()
|
|
416
|
+
selection.addRange(range)
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.error('Error applying underline:', error)
|
|
419
|
+
}
|
|
171
420
|
}
|
|
421
|
+
}, (state) => {
|
|
422
|
+
// isActive function for underline
|
|
423
|
+
const selectionInfo = getCurrentSelection(state)
|
|
424
|
+
if (!selectionInfo) return false
|
|
425
|
+
|
|
426
|
+
const commonAncestor = selectionInfo.range.commonAncestorContainer
|
|
427
|
+
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
|
|
428
|
+
? commonAncestor.parentElement
|
|
429
|
+
: commonAncestor as Element
|
|
430
|
+
|
|
431
|
+
return !!parentElement?.closest('u')
|
|
172
432
|
})
|
|
173
433
|
}
|
|
174
434
|
|
|
@@ -198,11 +458,56 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
198
458
|
// Replace the old block with the new one
|
|
199
459
|
currentBlock.parentNode?.replaceChild(newBlock, currentBlock)
|
|
200
460
|
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
461
|
+
// If we created a heading (not toggling off), ensure there's a paragraph after it
|
|
462
|
+
if (!isToggleOff) {
|
|
463
|
+
// Check if there's already a next sibling element
|
|
464
|
+
const nextSibling = newBlock.nextElementSibling
|
|
465
|
+
if (!nextSibling) {
|
|
466
|
+
// No element after the heading, create a paragraph
|
|
467
|
+
const p = state.doc!.createElement('p')
|
|
468
|
+
p.dir = newBlock.dir || state.doc!.body.dir
|
|
469
|
+
newBlock.parentNode?.insertBefore(p, newBlock.nextSibling)
|
|
470
|
+
|
|
471
|
+
// Move cursor to the new paragraph
|
|
472
|
+
const newRange = state.doc!.createRange()
|
|
473
|
+
newRange.selectNodeContents(p)
|
|
474
|
+
newRange.collapse(true)
|
|
475
|
+
selectionInfo.selection.removeAllRanges()
|
|
476
|
+
selectionInfo.selection.addRange(newRange)
|
|
477
|
+
} else {
|
|
478
|
+
// There is a next element, just move cursor to the heading
|
|
479
|
+
range.selectNodeContents(newBlock)
|
|
480
|
+
range.collapse(false)
|
|
481
|
+
selectionInfo.selection.removeAllRanges()
|
|
482
|
+
selectionInfo.selection.addRange(range)
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
// Toggling off to paragraph, move cursor to it
|
|
486
|
+
range.selectNodeContents(newBlock)
|
|
487
|
+
range.collapse(false)
|
|
488
|
+
selectionInfo.selection.removeAllRanges()
|
|
489
|
+
selectionInfo.selection.addRange(range)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Clean up empty elements after creating heading
|
|
493
|
+
setTimeout(() => {
|
|
494
|
+
const emptyElements = Array.from(state.doc!.body.querySelectorAll('div, p')).filter(el =>
|
|
495
|
+
!el.textContent?.trim() && !el.querySelector('img, br, hr, input, textarea, select')
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
emptyElements.forEach(el => {
|
|
499
|
+
// Remove empty elements that come right before or after headings
|
|
500
|
+
const nextSibling = el.nextElementSibling
|
|
501
|
+
const prevSibling = el.previousElementSibling
|
|
502
|
+
if ((nextSibling && /^H[1-6]$/.test(nextSibling.tagName)) ||
|
|
503
|
+
(prevSibling && /^H[1-6]$/.test(prevSibling.tagName))) {
|
|
504
|
+
el.remove()
|
|
505
|
+
} else if (el === state.doc!.body.firstElementChild && state.doc!.body.children.length > 1) {
|
|
506
|
+
el.remove()
|
|
507
|
+
}
|
|
508
|
+
})
|
|
509
|
+
}, 0)
|
|
510
|
+
|
|
206
511
|
} catch (error) {
|
|
207
512
|
console.error(`Error applying ${cmd} heading:`, error)
|
|
208
513
|
}
|
|
@@ -318,10 +623,34 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
318
623
|
// Convert unordered list to ordered list
|
|
319
624
|
if (listParent.tagName.toLowerCase() === 'ul') {
|
|
320
625
|
const ol = state.doc!.createElement('ol')
|
|
626
|
+
|
|
627
|
+
// Store current cursor position before conversion
|
|
628
|
+
const currentOffset = range.startOffset
|
|
629
|
+
const currentContainer = range.startContainer
|
|
630
|
+
|
|
321
631
|
while (listParent.firstChild) {
|
|
322
632
|
ol.appendChild(listParent.firstChild)
|
|
323
633
|
}
|
|
324
634
|
listParent.parentNode?.replaceChild(ol, listParent)
|
|
635
|
+
|
|
636
|
+
// Restore cursor position
|
|
637
|
+
try {
|
|
638
|
+
const newRange = state.doc!.createRange()
|
|
639
|
+
newRange.setStart(currentContainer, currentOffset)
|
|
640
|
+
newRange.collapse(true)
|
|
641
|
+
selectionInfo.selection.removeAllRanges()
|
|
642
|
+
selectionInfo.selection.addRange(newRange)
|
|
643
|
+
} catch (e) {
|
|
644
|
+
// Fallback: put cursor at beginning of first list item
|
|
645
|
+
const firstLi = ol.querySelector('li')
|
|
646
|
+
if (firstLi) {
|
|
647
|
+
const newRange = state.doc!.createRange()
|
|
648
|
+
newRange.selectNodeContents(firstLi)
|
|
649
|
+
newRange.collapse(true)
|
|
650
|
+
selectionInfo.selection.removeAllRanges()
|
|
651
|
+
selectionInfo.selection.addRange(newRange)
|
|
652
|
+
}
|
|
653
|
+
}
|
|
325
654
|
return
|
|
326
655
|
}
|
|
327
656
|
}
|
|
@@ -386,10 +715,35 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
386
715
|
// Convert ordered list to unordered list
|
|
387
716
|
if (listParent.tagName.toLowerCase() === 'ol') {
|
|
388
717
|
const ul = state.doc!.createElement('ul')
|
|
718
|
+
|
|
719
|
+
// Store current cursor position before conversion
|
|
720
|
+
const currentOffset = range.startOffset
|
|
721
|
+
const currentContainer = range.startContainer
|
|
722
|
+
|
|
389
723
|
while (listParent.firstChild) {
|
|
390
724
|
ul.appendChild(listParent.firstChild)
|
|
391
725
|
}
|
|
392
726
|
listParent.parentNode?.replaceChild(ul, listParent)
|
|
727
|
+
|
|
728
|
+
// Restore cursor position
|
|
729
|
+
try {
|
|
730
|
+
const newRange = state.doc!.createRange()
|
|
731
|
+
newRange.setStart(currentContainer, currentOffset)
|
|
732
|
+
newRange.collapse(true)
|
|
733
|
+
selectionInfo.selection.removeAllRanges()
|
|
734
|
+
selectionInfo.selection.addRange(newRange)
|
|
735
|
+
} catch (e) {
|
|
736
|
+
// Fallback: put cursor at beginning of first list item
|
|
737
|
+
console.warn('Could not restore cursor position:', e)
|
|
738
|
+
const firstLi = ul.querySelector('li')
|
|
739
|
+
if (firstLi) {
|
|
740
|
+
const newRange = state.doc!.createRange()
|
|
741
|
+
newRange.selectNodeContents(firstLi)
|
|
742
|
+
newRange.collapse(true)
|
|
743
|
+
selectionInfo.selection.removeAllRanges()
|
|
744
|
+
selectionInfo.selection.addRange(newRange)
|
|
745
|
+
}
|
|
746
|
+
}
|
|
393
747
|
return
|
|
394
748
|
}
|
|
395
749
|
}
|
|
@@ -418,23 +772,32 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
418
772
|
})
|
|
419
773
|
}
|
|
420
774
|
|
|
421
|
-
// Clear formatting command -
|
|
775
|
+
// Clear formatting command - enhanced
|
|
422
776
|
const formatCommands = {
|
|
423
777
|
clear: createCommand('Clear Formatting', (state) => {
|
|
424
778
|
const selectionInfo = getCurrentSelection(state)
|
|
425
|
-
if (!selectionInfo
|
|
779
|
+
if (!selectionInfo) return
|
|
780
|
+
|
|
781
|
+
const { range, selection } = selectionInfo
|
|
426
782
|
|
|
427
|
-
const { range } = selectionInfo
|
|
428
783
|
try {
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
784
|
+
// Function to clean text from unwanted characters
|
|
785
|
+
function cleanText(text: string): string {
|
|
786
|
+
return text
|
|
787
|
+
// Remove zero-width characters
|
|
788
|
+
.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
|
789
|
+
// Remove extra whitespace
|
|
790
|
+
.replace(/\s+/g, ' ')
|
|
791
|
+
// Remove invisible characters
|
|
792
|
+
.replace(/[\u00A0]/g, ' ') // non-breaking space to regular space
|
|
793
|
+
.trim()
|
|
794
|
+
}
|
|
433
795
|
|
|
434
796
|
// Function to clean a node recursively
|
|
435
797
|
function cleanNode(node: Node): Node {
|
|
436
798
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
437
|
-
|
|
799
|
+
const cleanedText = cleanText(node.textContent || '')
|
|
800
|
+
return state.doc!.createTextNode(cleanedText)
|
|
438
801
|
}
|
|
439
802
|
|
|
440
803
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
@@ -442,18 +805,19 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
442
805
|
const tagName = element.tagName.toLowerCase()
|
|
443
806
|
|
|
444
807
|
// Preserve structural elements
|
|
445
|
-
const structuralTags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'div', 'table', 'tr', 'td', 'th']
|
|
446
|
-
// Remove formatting elements
|
|
447
|
-
const formattingTags = ['
|
|
808
|
+
const structuralTags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'div', 'table', 'tr', 'td', 'th', 'thead', 'tbody', 'b', 'i', 'u']
|
|
809
|
+
// Remove formatting elements (excluding basic HTML formatting tags)
|
|
810
|
+
const formattingTags = ['strong', 'em', 'span', 'font', 'strike', 'sub', 'sup', 'mark', 'del', 'ins', 'small', 'big']
|
|
448
811
|
|
|
449
812
|
if (formattingTags.includes(tagName)) {
|
|
450
|
-
// For formatting elements, just return the text content
|
|
451
|
-
|
|
813
|
+
// For formatting elements, just return the cleaned text content
|
|
814
|
+
const cleanedText = cleanText(element.textContent || '')
|
|
815
|
+
return state.doc!.createTextNode(cleanedText)
|
|
452
816
|
} else if (structuralTags.includes(tagName)) {
|
|
453
|
-
// For structural elements, keep the element but remove attributes
|
|
817
|
+
// For structural elements, keep the element but remove all styling attributes
|
|
454
818
|
const cleanElement = state.doc!.createElement(tagName)
|
|
455
819
|
|
|
456
|
-
//
|
|
820
|
+
// Only preserve essential attributes
|
|
457
821
|
if (tagName === 'a' && element.hasAttribute('href')) {
|
|
458
822
|
cleanElement.setAttribute('href', element.getAttribute('href') || '')
|
|
459
823
|
if (element.hasAttribute('target')) {
|
|
@@ -462,34 +826,77 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
462
826
|
} else if (tagName === 'img') {
|
|
463
827
|
if (element.hasAttribute('src')) cleanElement.setAttribute('src', element.getAttribute('src') || '')
|
|
464
828
|
if (element.hasAttribute('alt')) cleanElement.setAttribute('alt', element.getAttribute('alt') || '')
|
|
829
|
+
if (element.hasAttribute('width')) cleanElement.setAttribute('width', element.getAttribute('width') || '')
|
|
830
|
+
if (element.hasAttribute('height')) cleanElement.setAttribute('height', element.getAttribute('height') || '')
|
|
465
831
|
}
|
|
466
832
|
|
|
467
833
|
// Clean and append child nodes
|
|
468
834
|
Array.from(element.childNodes).forEach((child) => {
|
|
469
|
-
|
|
835
|
+
const cleanedChild = cleanNode(child)
|
|
836
|
+
if (cleanedChild.textContent?.trim() || cleanedChild.nodeType === Node.ELEMENT_NODE) {
|
|
837
|
+
cleanElement.appendChild(cleanedChild)
|
|
838
|
+
}
|
|
470
839
|
})
|
|
471
840
|
|
|
472
841
|
return cleanElement
|
|
473
842
|
}
|
|
474
843
|
}
|
|
475
844
|
|
|
476
|
-
return
|
|
845
|
+
return state.doc!.createTextNode('')
|
|
477
846
|
}
|
|
478
847
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
cleanedFragment.
|
|
483
|
-
})
|
|
848
|
+
if (range.collapsed) {
|
|
849
|
+
// If no selection, clean the entire document
|
|
850
|
+
const body = state.doc!.body
|
|
851
|
+
const cleanedFragment = state.doc!.createDocumentFragment()
|
|
484
852
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
853
|
+
Array.from(body.childNodes).forEach((node) => {
|
|
854
|
+
const cleanedNode = cleanNode(node)
|
|
855
|
+
if (cleanedNode.textContent?.trim() || cleanedNode.nodeType === Node.ELEMENT_NODE) {
|
|
856
|
+
cleanedFragment.appendChild(cleanedNode)
|
|
857
|
+
}
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
// Clear body and append cleaned content
|
|
861
|
+
body.innerHTML = ''
|
|
862
|
+
body.appendChild(cleanedFragment)
|
|
863
|
+
|
|
864
|
+
// If body is empty, add a paragraph
|
|
865
|
+
if (!body.firstElementChild) {
|
|
866
|
+
const p = state.doc!.createElement('p')
|
|
867
|
+
body.appendChild(p)
|
|
868
|
+
|
|
869
|
+
// Set cursor in the paragraph
|
|
870
|
+
const newRange = state.doc!.createRange()
|
|
871
|
+
newRange.selectNodeContents(p)
|
|
872
|
+
newRange.collapse(true)
|
|
873
|
+
selection.removeAllRanges()
|
|
874
|
+
selection.addRange(newRange)
|
|
875
|
+
}
|
|
876
|
+
} else {
|
|
877
|
+
// If there's a selection, clean only the selected content
|
|
878
|
+
const fragment = range.cloneContents()
|
|
879
|
+
const tempDiv = state.doc!.createElement('div')
|
|
880
|
+
tempDiv.appendChild(fragment)
|
|
881
|
+
|
|
882
|
+
// Clean all nodes in the temp div
|
|
883
|
+
const cleanedFragment = state.doc!.createDocumentFragment()
|
|
884
|
+
Array.from(tempDiv.childNodes).forEach((node) => {
|
|
885
|
+
const cleanedNode = cleanNode(node)
|
|
886
|
+
if (cleanedNode.textContent?.trim() || cleanedNode.nodeType === Node.ELEMENT_NODE) {
|
|
887
|
+
cleanedFragment.appendChild(cleanedNode)
|
|
888
|
+
}
|
|
889
|
+
})
|
|
488
890
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
891
|
+
// Replace the selection with cleaned content
|
|
892
|
+
range.deleteContents()
|
|
893
|
+
range.insertNode(cleanedFragment)
|
|
894
|
+
|
|
895
|
+
// Restore selection
|
|
896
|
+
range.collapse(false)
|
|
897
|
+
selection.removeAllRanges()
|
|
898
|
+
selection.addRange(range)
|
|
899
|
+
}
|
|
493
900
|
} catch (error) {
|
|
494
901
|
console.error('Error clearing formatting:', error)
|
|
495
902
|
}
|
|
@@ -513,12 +920,136 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
513
920
|
deleteColumn: createCommand('Delete Column', state => state.range && deleteColumn(state.range))
|
|
514
921
|
}
|
|
515
922
|
|
|
516
|
-
//
|
|
923
|
+
// Table alignment commands
|
|
517
924
|
const alignmentCommands = ['Left', 'Center', 'Right', 'Justify'].reduce((acc, align) => ({
|
|
518
925
|
...acc,
|
|
519
|
-
[`
|
|
926
|
+
[`tableAlign${align}`]: createCommand(`Table Align ${align}`, state => state.range && alignColumn(state.range, align.toLowerCase() as 'left' | 'center' | 'right' | 'justify'))
|
|
520
927
|
}), {})
|
|
521
928
|
|
|
929
|
+
// Text alignment commands (for paragraphs)
|
|
930
|
+
const textAlignmentCommands = {
|
|
931
|
+
alignLeft: createCommand('Align Left', (state) => {
|
|
932
|
+
const selectionInfo = getCurrentSelection(state)
|
|
933
|
+
if (!selectionInfo) return
|
|
934
|
+
|
|
935
|
+
const { range } = selectionInfo
|
|
936
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
937
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
938
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
939
|
+
|
|
940
|
+
if (paragraph) {
|
|
941
|
+
(paragraph as HTMLElement).style.textAlign = 'left'
|
|
942
|
+
}
|
|
943
|
+
}, (state) => {
|
|
944
|
+
const selectionInfo = getCurrentSelection(state)
|
|
945
|
+
if (!selectionInfo) return false
|
|
946
|
+
|
|
947
|
+
const { range } = selectionInfo
|
|
948
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
949
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
950
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
951
|
+
|
|
952
|
+
return (paragraph as HTMLElement)?.style.textAlign === 'left'
|
|
953
|
+
}),
|
|
954
|
+
|
|
955
|
+
alignCenter: createCommand('Align Center', (state) => {
|
|
956
|
+
const selectionInfo = getCurrentSelection(state)
|
|
957
|
+
if (!selectionInfo) return
|
|
958
|
+
|
|
959
|
+
const { range } = selectionInfo
|
|
960
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
961
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
962
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
963
|
+
|
|
964
|
+
if (paragraph) {
|
|
965
|
+
(paragraph as HTMLElement).style.textAlign = 'center'
|
|
966
|
+
}
|
|
967
|
+
}, (state) => {
|
|
968
|
+
const selectionInfo = getCurrentSelection(state)
|
|
969
|
+
if (!selectionInfo) return false
|
|
970
|
+
|
|
971
|
+
const { range } = selectionInfo
|
|
972
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
973
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
974
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
975
|
+
|
|
976
|
+
return (paragraph as HTMLElement)?.style.textAlign === 'center'
|
|
977
|
+
}),
|
|
978
|
+
|
|
979
|
+
alignRight: createCommand('Align Right', (state) => {
|
|
980
|
+
const selectionInfo = getCurrentSelection(state)
|
|
981
|
+
if (!selectionInfo) return
|
|
982
|
+
|
|
983
|
+
const { range } = selectionInfo
|
|
984
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
985
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
986
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
987
|
+
|
|
988
|
+
if (paragraph) {
|
|
989
|
+
(paragraph as HTMLElement).style.textAlign = 'right'
|
|
990
|
+
}
|
|
991
|
+
}, (state) => {
|
|
992
|
+
const selectionInfo = getCurrentSelection(state)
|
|
993
|
+
if (!selectionInfo) return false
|
|
994
|
+
|
|
995
|
+
const { range } = selectionInfo
|
|
996
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
997
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
998
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
999
|
+
|
|
1000
|
+
return (paragraph as HTMLElement)?.style.textAlign === 'right'
|
|
1001
|
+
}),
|
|
1002
|
+
|
|
1003
|
+
alignJustify: createCommand('Align Justify', (state) => {
|
|
1004
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1005
|
+
if (!selectionInfo) return
|
|
1006
|
+
|
|
1007
|
+
const { range } = selectionInfo
|
|
1008
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1009
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1010
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1011
|
+
|
|
1012
|
+
if (paragraph) {
|
|
1013
|
+
(paragraph as HTMLElement).style.textAlign = 'justify'
|
|
1014
|
+
}
|
|
1015
|
+
}, (state) => {
|
|
1016
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1017
|
+
if (!selectionInfo) return false
|
|
1018
|
+
|
|
1019
|
+
const { range } = selectionInfo
|
|
1020
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1021
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1022
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1023
|
+
|
|
1024
|
+
return (paragraph as HTMLElement)?.style.textAlign === 'justify'
|
|
1025
|
+
}),
|
|
1026
|
+
|
|
1027
|
+
textDirection: createCommand('Toggle Text Direction', (state) => {
|
|
1028
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1029
|
+
if (!selectionInfo) return
|
|
1030
|
+
|
|
1031
|
+
const { range } = selectionInfo
|
|
1032
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1033
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1034
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1035
|
+
|
|
1036
|
+
if (paragraph) {
|
|
1037
|
+
const currentDir = (paragraph as HTMLElement).dir || 'ltr'
|
|
1038
|
+
; (paragraph as HTMLElement).dir = currentDir === 'ltr' ? 'rtl' : 'ltr'
|
|
1039
|
+
}
|
|
1040
|
+
}, (state) => {
|
|
1041
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1042
|
+
if (!selectionInfo) return false
|
|
1043
|
+
|
|
1044
|
+
const { range } = selectionInfo
|
|
1045
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1046
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1047
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1048
|
+
|
|
1049
|
+
return (paragraph as HTMLElement)?.dir === 'rtl'
|
|
1050
|
+
})
|
|
1051
|
+
}
|
|
1052
|
+
|
|
522
1053
|
// View state commands
|
|
523
1054
|
const viewCommands = {
|
|
524
1055
|
fullScreen: createCommand('Full Screen', (state) => { state.isFullscreen = !state.isFullscreen }, state => state.isFullscreen),
|
|
@@ -542,6 +1073,7 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
542
1073
|
...formatCommands,
|
|
543
1074
|
...tableCommands,
|
|
544
1075
|
...alignmentCommands,
|
|
1076
|
+
...textAlignmentCommands,
|
|
545
1077
|
...viewCommands,
|
|
546
1078
|
...mediaCommands
|
|
547
1079
|
}
|