@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
@@ -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 || selectionInfo.range.collapsed) return
229
+ if (!selectionInfo) return
102
230
 
103
- const { range } = selectionInfo
104
- try {
105
- const span = state.doc!.createElement('span')
106
- span.style.fontWeight = 'bold'
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
- range.surroundContents(span)
110
- } catch {
111
- const fragment = range.extractContents()
112
- span.appendChild(fragment)
113
- range.insertNode(span)
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
- range.selectNodeContents(span)
117
- selectionInfo.selection.removeAllRanges()
118
- selectionInfo.selection.addRange(range)
119
- } catch (error) {
120
- console.error('Error applying bold:', error)
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 || selectionInfo.range.collapsed) return
298
+ if (!selectionInfo) return
127
299
 
128
- const { range } = selectionInfo
129
- try {
130
- const span = state.doc!.createElement('span')
131
- span.style.fontStyle = 'italic'
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
- range.surroundContents(span)
135
- } catch {
136
- const fragment = range.extractContents()
137
- span.appendChild(fragment)
138
- range.insertNode(span)
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
- range.selectNodeContents(span)
142
- selectionInfo.selection.removeAllRanges()
143
- selectionInfo.selection.addRange(range)
144
- } catch (error) {
145
- console.error('Error applying italic:', error)
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 || selectionInfo.range.collapsed) return
367
+ if (!selectionInfo) return
152
368
 
153
- const { range } = selectionInfo
154
- try {
155
- const span = state.doc!.createElement('span')
156
- span.style.textDecoration = 'underline'
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
- range.surroundContents(span)
160
- } catch {
161
- const fragment = range.extractContents()
162
- span.appendChild(fragment)
163
- range.insertNode(span)
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
- range.selectNodeContents(span)
167
- selectionInfo.selection.removeAllRanges()
168
- selectionInfo.selection.addRange(range)
169
- } catch (error) {
170
- console.error('Error applying underline:', error)
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
- // Move cursor into the new block
202
- range.selectNodeContents(newBlock)
203
- range.collapse(false)
204
- selectionInfo.selection.removeAllRanges()
205
- selectionInfo.selection.addRange(range)
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 - simplified
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 || selectionInfo.range.collapsed) return
779
+ if (!selectionInfo) return
780
+
781
+ const { range, selection } = selectionInfo
426
782
 
427
- const { range } = selectionInfo
428
783
  try {
429
- // Get the selected content
430
- const fragment = range.cloneContents()
431
- const tempDiv = state.doc!.createElement('div')
432
- tempDiv.appendChild(fragment)
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
- return node.cloneNode(true)
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 = ['b', 'i', 'u', 'strong', 'em', 'span', 'font', 'strike', 'sub', 'sup']
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
- return state.doc!.createTextNode(element.textContent || '')
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
- // Preserve certain attributes for links and images
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
- cleanElement.appendChild(cleanNode(child))
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 node.cloneNode(true)
845
+ return state.doc!.createTextNode('')
477
846
  }
478
847
 
479
- // Clean all nodes in the temp div
480
- const cleanedFragment = state.doc!.createDocumentFragment()
481
- Array.from(tempDiv.childNodes).forEach((node) => {
482
- cleanedFragment.appendChild(cleanNode(node))
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
- // Replace the selection with cleaned content
486
- range.deleteContents()
487
- range.insertNode(cleanedFragment)
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
- // Restore selection
490
- range.collapse(false)
491
- selectionInfo.selection.removeAllRanges()
492
- selectionInfo.selection.addRange(range)
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
- // Alignment commands
923
+ // Table alignment commands
517
924
  const alignmentCommands = ['Left', 'Center', 'Right', 'Justify'].reduce((acc, align) => ({
518
925
  ...acc,
519
- [`align${align}`]: createCommand(`Align ${align}`, state => state.range && alignColumn(state.range, align.toLowerCase() as 'left' | 'center' | 'right' | 'justify'))
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
  }