@bagelink/vue 1.4.139 → 1.4.145

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 (73) hide show
  1. package/dist/components/Btn.vue.d.ts.map +1 -1
  2. package/dist/components/Carousel.vue.d.ts +1 -1
  3. package/dist/components/Modal.vue.d.ts +3 -0
  4. package/dist/components/Modal.vue.d.ts.map +1 -1
  5. package/dist/components/Slider.vue.d.ts +1 -1
  6. package/dist/components/Slider.vue.d.ts.map +1 -1
  7. package/dist/components/analytics/BarChart.vue.d.ts +11 -3
  8. package/dist/components/analytics/BarChart.vue.d.ts.map +1 -1
  9. package/dist/components/analytics/LineChart.vue.d.ts +9 -0
  10. package/dist/components/analytics/LineChart.vue.d.ts.map +1 -1
  11. package/dist/components/analytics/PieChart.vue.d.ts +30 -2
  12. package/dist/components/analytics/PieChart.vue.d.ts.map +1 -1
  13. package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts +8 -0
  14. package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
  15. package/dist/components/form/inputs/RichText/components/TableGridSelector.vue.d.ts +9 -0
  16. package/dist/components/form/inputs/RichText/components/TableGridSelector.vue.d.ts.map +1 -0
  17. package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
  18. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +0 -14
  19. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
  20. package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
  21. package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
  22. package/dist/components/form/inputs/RichText/index.vue.d.ts +15 -15
  23. package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
  24. package/dist/components/form/inputs/RichText/richTextTypes.d.ts +1 -3
  25. package/dist/components/form/inputs/RichText/richTextTypes.d.ts.map +1 -1
  26. package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
  27. package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +1 -1
  28. package/dist/components/form/inputs/RichText/utils/media-clean.d.ts +2 -0
  29. package/dist/components/form/inputs/RichText/utils/media-clean.d.ts.map +1 -0
  30. package/dist/components/form/inputs/RichText/utils/media.d.ts +4 -4
  31. package/dist/components/form/inputs/RichText/utils/media.d.ts.map +1 -1
  32. package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
  33. package/dist/components/form/inputs/RichText/utils/table.d.ts +1 -1
  34. package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
  35. package/dist/components/index.d.ts +1 -0
  36. package/dist/components/index.d.ts.map +1 -1
  37. package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
  38. package/dist/components/layout/AppLayout.vue.d.ts.map +1 -1
  39. package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
  40. package/dist/index.cjs +123 -22
  41. package/dist/index.mjs +123 -22
  42. package/dist/style.css +1 -1
  43. package/package.json +1 -1
  44. package/src/components/Btn.vue +50 -42
  45. package/src/components/Modal.vue +49 -50
  46. package/src/components/analytics/BarChart.vue +118 -7
  47. package/src/components/analytics/KpiCard.vue +2 -2
  48. package/src/components/analytics/LineChart.vue +189 -105
  49. package/src/components/analytics/PieChart.vue +392 -49
  50. package/src/components/dataTable/DataTable.vue +1 -1
  51. package/src/components/form/inputs/RichText/CheckList.md +23 -0
  52. package/src/components/form/inputs/RichText/components/EditorToolbar.vue +243 -27
  53. package/src/components/form/inputs/RichText/components/TableGridSelector.vue +94 -0
  54. package/src/components/form/inputs/RichText/composables/useCommands.ts +45 -0
  55. package/src/components/form/inputs/RichText/composables/useEditor.ts +13 -10
  56. package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +3 -128
  57. package/src/components/form/inputs/RichText/config.ts +33 -10
  58. package/src/components/form/inputs/RichText/editor.css +300 -33
  59. package/src/components/form/inputs/RichText/index.vue +3271 -130
  60. package/src/components/form/inputs/RichText/richTextTypes.ts +7 -3
  61. package/src/components/form/inputs/RichText/utils/commands.ts +851 -90
  62. package/src/components/form/inputs/RichText/utils/formatting.ts +17 -15
  63. package/src/components/form/inputs/RichText/utils/media-clean.ts +0 -0
  64. package/src/components/form/inputs/RichText/utils/media.ts +133 -67
  65. package/src/components/form/inputs/RichText/utils/selection.ts +40 -11
  66. package/src/components/form/inputs/RichText/utils/table.ts +1 -1
  67. package/src/components/index.ts +1 -0
  68. package/src/components/layout/AppContent.vue +26 -26
  69. package/src/components/layout/AppLayout.vue +21 -3
  70. package/src/components/layout/AppSidebar.vue +5 -2
  71. package/src/styles/layout.css +267 -0
  72. package/src/styles/mobilLayout.css +266 -0
  73. package/src/styles/modal.css +3 -17
@@ -1,5 +1,5 @@
1
1
  import type { EditorState } from '../richTextTypes'
2
- import { insertImage, insertLink, insertEmbed } from './media'
2
+ import { insertLink } from './media'
3
3
  import { addRow, deleteRow, mergeCells, splitCell, insertTable, deleteTable, insertColumn, deleteColumn, alignColumn } from './table'
4
4
 
5
5
  export interface Command {
@@ -30,6 +30,142 @@ 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
+ // If we're at the end of a word (cursor right after the last character)
101
+ // move back one position to include that word
102
+ if (offset > 0 && wordChar.test(text[offset - 1]) &&
103
+ (offset >= text.length || !wordChar.test(text[offset]))) {
104
+ start = offset - 1
105
+ end = offset - 1
106
+ }
107
+
108
+ // Move start backwards to find word beginning
109
+ while (start > 0 && wordChar.test(text[start - 1])) {
110
+ start--
111
+ }
112
+
113
+ // Move end forwards to find word ending
114
+ while (end < text.length && wordChar.test(text[end])) {
115
+ end++
116
+ }
117
+
118
+ // If we found a word, apply formatting
119
+ if (start < end && end > start) {
120
+ try {
121
+ // Split the text node into three parts: before, word, after
122
+ const beforeText = text.substring(0, start)
123
+ const wordText = text.substring(start, end)
124
+ const afterText = text.substring(end)
125
+
126
+ // Create the formatting element
127
+ const formatElement = doc.createElement(tagName)
128
+ formatElement.textContent = wordText
129
+
130
+ // Replace the text node with the new structure
131
+ const parent = textNode.parentNode
132
+ if (parent) {
133
+ // Create text nodes for before and after
134
+ if (beforeText) {
135
+ const beforeNode = doc.createTextNode(beforeText)
136
+ parent.insertBefore(beforeNode, textNode)
137
+ }
138
+
139
+ parent.insertBefore(formatElement, textNode)
140
+
141
+ if (afterText) {
142
+ const afterNode = doc.createTextNode(afterText)
143
+ parent.insertBefore(afterNode, textNode)
144
+ }
145
+
146
+ parent.removeChild(textNode)
147
+
148
+ // Position cursor after the formatted word
149
+ const newRange = doc.createRange()
150
+ newRange.setStartAfter(formatElement)
151
+ newRange.collapse(true)
152
+
153
+ const selection = doc.getSelection()
154
+ if (selection) {
155
+ selection.removeAllRanges()
156
+ selection.addRange(newRange)
157
+ }
158
+
159
+ return true
160
+ }
161
+ } catch (error) {
162
+ console.error('Error applying formatting to word:', error)
163
+ }
164
+ }
165
+
166
+ return false
167
+ }
168
+
33
169
  function updateStateAfterCommand(state: EditorState) {
34
170
  if (!state.doc) return
35
171
 
@@ -94,81 +230,231 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
94
230
  })
95
231
  }
96
232
 
97
- // Basic text formatting commands
233
+ // Basic text formatting commands with toggle functionality
98
234
  const textCommands = {
99
235
  bold: createCommand('Bold', (state) => {
100
236
  const selectionInfo = getCurrentSelection(state)
101
- if (!selectionInfo || selectionInfo.range.collapsed) return
237
+ if (!selectionInfo) return
102
238
 
103
- const { range } = selectionInfo
104
- try {
105
- const span = state.doc!.createElement('span')
106
- span.style.fontWeight = 'bold'
239
+ let { range, selection } = selectionInfo
240
+
241
+ // If no text is selected, try to apply formatting to word at cursor
242
+ if (range.collapsed) {
243
+ const success = applyFormattingToWordAtCursor(state.doc!, range, 'b')
244
+ if (success) return
245
+ else return
246
+ }
107
247
 
248
+ // Check if the selection is already bold
249
+ const commonAncestor = range.commonAncestorContainer
250
+ const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
251
+ ? commonAncestor.parentElement
252
+ : commonAncestor as Element
253
+
254
+ const boldElement = parentElement?.closest('b, strong')
255
+
256
+ if (boldElement) {
257
+ // Remove bold formatting
108
258
  try {
109
- range.surroundContents(span)
110
- } catch {
111
- const fragment = range.extractContents()
112
- span.appendChild(fragment)
113
- range.insertNode(span)
259
+ const parent = boldElement.parentNode
260
+ while (boldElement.firstChild) {
261
+ parent?.insertBefore(boldElement.firstChild, boldElement)
262
+ }
263
+ parent?.removeChild(boldElement)
264
+
265
+ // Restore selection
266
+ selection.removeAllRanges()
267
+ selection.addRange(range)
268
+ } catch (error) {
269
+ console.error('Error removing bold:', error)
114
270
  }
271
+ } else {
272
+ // Apply bold formatting
273
+ try {
274
+ const b = state.doc!.createElement('b')
275
+
276
+ try {
277
+ range.surroundContents(b)
278
+ } catch {
279
+ const fragment = range.extractContents()
280
+ b.appendChild(fragment)
281
+ range.insertNode(b)
282
+ }
115
283
 
116
- range.selectNodeContents(span)
117
- selectionInfo.selection.removeAllRanges()
118
- selectionInfo.selection.addRange(range)
119
- } catch (error) {
120
- console.error('Error applying bold:', error)
284
+ range.selectNodeContents(b)
285
+ selection.removeAllRanges()
286
+ selection.addRange(range)
287
+ } catch (error) {
288
+ console.error('Error applying bold:', error)
289
+ }
121
290
  }
291
+ }, (state) => {
292
+ // isActive function for bold
293
+ const selectionInfo = getCurrentSelection(state)
294
+ if (!selectionInfo) return false
295
+
296
+ const commonAncestor = selectionInfo.range.commonAncestorContainer
297
+ const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
298
+ ? commonAncestor.parentElement
299
+ : commonAncestor as Element
300
+
301
+ return !!parentElement?.closest('b, strong')
122
302
  }),
123
303
 
124
304
  italic: createCommand('Italic', (state) => {
125
305
  const selectionInfo = getCurrentSelection(state)
126
- if (!selectionInfo || selectionInfo.range.collapsed) return
306
+ if (!selectionInfo) return
127
307
 
128
- const { range } = selectionInfo
129
- try {
130
- const span = state.doc!.createElement('span')
131
- span.style.fontStyle = 'italic'
308
+ let { range, selection } = selectionInfo
132
309
 
310
+ // If no text is selected, try to apply formatting to word at cursor
311
+ if (range.collapsed) {
312
+ const success = applyFormattingToWordAtCursor(state.doc!, range, 'i')
313
+ if (success) return
314
+ else return
315
+ }
316
+
317
+ // Check if the selection is already italic
318
+ const commonAncestor = range.commonAncestorContainer
319
+ const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
320
+ ? commonAncestor.parentElement
321
+ : commonAncestor as Element
322
+
323
+ const italicElement = parentElement?.closest('i, em')
324
+
325
+ if (italicElement) {
326
+ // Remove italic formatting
133
327
  try {
134
- range.surroundContents(span)
135
- } catch {
136
- const fragment = range.extractContents()
137
- span.appendChild(fragment)
138
- range.insertNode(span)
328
+ const parent = italicElement.parentNode
329
+ while (italicElement.firstChild) {
330
+ parent?.insertBefore(italicElement.firstChild, italicElement)
331
+ }
332
+ parent?.removeChild(italicElement)
333
+
334
+ // Restore selection
335
+ selection.removeAllRanges()
336
+ selection.addRange(range)
337
+ } catch (error) {
338
+ console.error('Error removing italic:', error)
139
339
  }
340
+ } else {
341
+ // Apply italic formatting
342
+ try {
343
+ const i = state.doc!.createElement('i')
344
+
345
+ try {
346
+ range.surroundContents(i)
347
+ } catch {
348
+ const fragment = range.extractContents()
349
+ i.appendChild(fragment)
350
+ range.insertNode(i)
351
+ }
140
352
 
141
- range.selectNodeContents(span)
142
- selectionInfo.selection.removeAllRanges()
143
- selectionInfo.selection.addRange(range)
144
- } catch (error) {
145
- console.error('Error applying italic:', error)
353
+ range.selectNodeContents(i)
354
+ selection.removeAllRanges()
355
+ selection.addRange(range)
356
+ } catch (error) {
357
+ console.error('Error applying italic:', error)
358
+ }
146
359
  }
360
+ }, (state) => {
361
+ // isActive function for italic
362
+ const selectionInfo = getCurrentSelection(state)
363
+ if (!selectionInfo) return false
364
+
365
+ const commonAncestor = selectionInfo.range.commonAncestorContainer
366
+ const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
367
+ ? commonAncestor.parentElement
368
+ : commonAncestor as Element
369
+
370
+ return !!parentElement?.closest('i, em')
147
371
  }),
148
372
 
149
373
  underline: createCommand('Underline', (state) => {
150
374
  const selectionInfo = getCurrentSelection(state)
151
- if (!selectionInfo || selectionInfo.range.collapsed) return
375
+ if (!selectionInfo) return
152
376
 
153
- const { range } = selectionInfo
154
- try {
155
- const span = state.doc!.createElement('span')
156
- span.style.textDecoration = 'underline'
377
+ let { range, selection } = selectionInfo
378
+
379
+ // If no text is selected, try to apply formatting to word at cursor
380
+ if (range.collapsed) {
381
+ const success = applyFormattingToWordAtCursor(state.doc!, range, 'u')
382
+ if (success) return
383
+ else return
384
+ }
385
+
386
+ // Check if the selection is already underlined
387
+ const commonAncestor = range.commonAncestorContainer
388
+ const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
389
+ ? commonAncestor.parentElement
390
+ : commonAncestor as Element
157
391
 
392
+ const underlineElement = parentElement?.closest('u')
393
+
394
+ if (underlineElement) {
395
+ // Remove underline formatting
158
396
  try {
159
- range.surroundContents(span)
160
- } catch {
161
- const fragment = range.extractContents()
162
- span.appendChild(fragment)
163
- range.insertNode(span)
397
+ const parent = underlineElement.parentNode
398
+ while (underlineElement.firstChild) {
399
+ parent?.insertBefore(underlineElement.firstChild, underlineElement)
400
+ }
401
+ parent?.removeChild(underlineElement)
402
+
403
+ // Restore selection
404
+ selection.removeAllRanges()
405
+ selection.addRange(range)
406
+ } catch (error) {
407
+ console.error('Error removing underline:', error)
164
408
  }
409
+ } else {
410
+ // Apply underline formatting
411
+ try {
412
+ const u = state.doc!.createElement('u')
413
+
414
+ try {
415
+ range.surroundContents(u)
416
+ } catch {
417
+ const fragment = range.extractContents()
418
+ u.appendChild(fragment)
419
+ range.insertNode(u)
420
+ }
165
421
 
166
- range.selectNodeContents(span)
167
- selectionInfo.selection.removeAllRanges()
168
- selectionInfo.selection.addRange(range)
169
- } catch (error) {
170
- console.error('Error applying underline:', error)
422
+ range.selectNodeContents(u)
423
+ selection.removeAllRanges()
424
+ selection.addRange(range)
425
+ } catch (error) {
426
+ console.error('Error applying underline:', error)
427
+ }
428
+ }
429
+ }, (state) => {
430
+ // isActive function for underline
431
+ const selectionInfo = getCurrentSelection(state)
432
+ if (!selectionInfo) return false
433
+
434
+ const commonAncestor = selectionInfo.range.commonAncestorContainer
435
+ const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
436
+ ? commonAncestor.parentElement
437
+ : commonAncestor as Element
438
+
439
+ return !!parentElement?.closest('u')
440
+ }),
441
+
442
+ link: createCommand('Link', (state) => {
443
+ const openLinkModal = (state as any).openLinkModal
444
+ if (openLinkModal) {
445
+ insertLink(openLinkModal, state)
171
446
  }
447
+ }, (state) => {
448
+ // isActive function for link
449
+ const selectionInfo = getCurrentSelection(state)
450
+ if (!selectionInfo) return false
451
+
452
+ const commonAncestor = selectionInfo.range.commonAncestorContainer
453
+ const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
454
+ ? commonAncestor.parentElement
455
+ : commonAncestor as Element
456
+
457
+ return !!parentElement?.closest('a')
172
458
  })
173
459
  }
174
460
 
@@ -179,10 +465,17 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
179
465
  const selectionInfo = getCurrentSelection(state)
180
466
  if (!selectionInfo) return
181
467
 
182
- const { range } = selectionInfo
468
+ const { range, selection } = selectionInfo
183
469
  const currentBlock = findBlockElement(range.commonAncestorContainer)
184
470
  if (!currentBlock) return
185
471
 
472
+ // Store the original selection information before modification
473
+ const startContainer = range.startContainer
474
+ const endContainer = range.endContainer
475
+ const startOffset = range.startOffset
476
+ const endOffset = range.endOffset
477
+ const isCollapsed = range.collapsed
478
+
186
479
  // Check if we need to toggle off (already in the same heading)
187
480
  const isToggleOff = currentBlock.tagName.toLowerCase() === cmd.toLowerCase()
188
481
  const newTag = isToggleOff ? 'p' : cmd
@@ -198,11 +491,105 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
198
491
  // Replace the old block with the new one
199
492
  currentBlock.parentNode?.replaceChild(newBlock, currentBlock)
200
493
 
201
- // Move cursor into the new block
202
- range.selectNodeContents(newBlock)
203
- range.collapse(false)
204
- selectionInfo.selection.removeAllRanges()
205
- selectionInfo.selection.addRange(range)
494
+ // Restore selection within the new block
495
+ try {
496
+ const newRange = state.doc!.createRange()
497
+
498
+ // Find corresponding nodes in the new block
499
+ const findCorrespondingNode = (originalNode: Node, originalBlock: Element, newBlock: Element): Node | null => {
500
+ if (originalNode === originalBlock) return newBlock
501
+
502
+ // If it's a text node, find it by traversing the tree
503
+ if (originalNode.nodeType === Node.TEXT_NODE) {
504
+ const walker = state.doc!.createTreeWalker(
505
+ newBlock,
506
+ NodeFilter.SHOW_TEXT,
507
+ null
508
+ )
509
+
510
+ let textNodeIndex = 0
511
+ const originalWalker = state.doc!.createTreeWalker(
512
+ originalBlock,
513
+ NodeFilter.SHOW_TEXT,
514
+ null
515
+ )
516
+
517
+ let currentOriginalNode = originalWalker.nextNode()
518
+ while (currentOriginalNode && currentOriginalNode !== originalNode) {
519
+ textNodeIndex++
520
+ currentOriginalNode = originalWalker.nextNode()
521
+ }
522
+
523
+ let currentNewNode = walker.nextNode()
524
+ let currentIndex = 0
525
+ while (currentNewNode && currentIndex < textNodeIndex) {
526
+ currentIndex++
527
+ currentNewNode = walker.nextNode()
528
+ }
529
+
530
+ return currentNewNode
531
+ }
532
+
533
+ return newBlock.firstChild
534
+ }
535
+
536
+ const newStartContainer = findCorrespondingNode(startContainer, currentBlock, newBlock)
537
+ const newEndContainer = findCorrespondingNode(endContainer, currentBlock, newBlock)
538
+
539
+ if (newStartContainer && newEndContainer) {
540
+ // Set start position
541
+ if (newStartContainer.nodeType === Node.TEXT_NODE) {
542
+ const maxOffset = Math.min(startOffset, newStartContainer.textContent?.length || 0)
543
+ newRange.setStart(newStartContainer, maxOffset)
544
+ } else {
545
+ newRange.setStart(newStartContainer, 0)
546
+ }
547
+
548
+ // Set end position
549
+ if (!isCollapsed) {
550
+ if (newEndContainer.nodeType === Node.TEXT_NODE) {
551
+ const maxOffset = Math.min(endOffset, newEndContainer.textContent?.length || 0)
552
+ newRange.setEnd(newEndContainer, maxOffset)
553
+ } else {
554
+ newRange.setEnd(newEndContainer, 0)
555
+ }
556
+ } else {
557
+ newRange.collapse(true)
558
+ }
559
+
560
+ // Apply the restored selection
561
+ selection.removeAllRanges()
562
+ selection.addRange(newRange)
563
+ } else {
564
+ // Fallback: select all content in the new block
565
+ newRange.selectNodeContents(newBlock)
566
+ if (isCollapsed) {
567
+ newRange.collapse(true)
568
+ }
569
+ selection.removeAllRanges()
570
+ selection.addRange(newRange)
571
+ }
572
+ } catch (selectionError) {
573
+ console.warn('Error restoring selection after heading change:', selectionError)
574
+ // Fallback: place cursor at start of the new block
575
+ const fallbackRange = state.doc!.createRange()
576
+ fallbackRange.selectNodeContents(newBlock)
577
+ fallbackRange.collapse(true)
578
+ selection.removeAllRanges()
579
+ selection.addRange(fallbackRange)
580
+ }
581
+
582
+ // If we created a heading (not toggling off), ensure there's a paragraph after it
583
+ if (!isToggleOff) {
584
+ const nextSibling = newBlock.nextElementSibling
585
+ if (!nextSibling) {
586
+ // No element after the heading, create a paragraph
587
+ const p = state.doc!.createElement('p')
588
+ p.dir = newBlock.dir || state.doc!.body.dir
589
+ newBlock.parentNode?.insertBefore(p, newBlock.nextSibling)
590
+ }
591
+ }
592
+
206
593
  } catch (error) {
207
594
  console.error(`Error applying ${cmd} heading:`, error)
208
595
  }
@@ -318,10 +705,34 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
318
705
  // Convert unordered list to ordered list
319
706
  if (listParent.tagName.toLowerCase() === 'ul') {
320
707
  const ol = state.doc!.createElement('ol')
708
+
709
+ // Store current cursor position before conversion
710
+ const currentOffset = range.startOffset
711
+ const currentContainer = range.startContainer
712
+
321
713
  while (listParent.firstChild) {
322
714
  ol.appendChild(listParent.firstChild)
323
715
  }
324
716
  listParent.parentNode?.replaceChild(ol, listParent)
717
+
718
+ // Restore cursor position
719
+ try {
720
+ const newRange = state.doc!.createRange()
721
+ newRange.setStart(currentContainer, currentOffset)
722
+ newRange.collapse(true)
723
+ selectionInfo.selection.removeAllRanges()
724
+ selectionInfo.selection.addRange(newRange)
725
+ } catch {
726
+ // Fallback: put cursor at beginning of first list item
727
+ const firstLi = ol.querySelector('li')
728
+ if (firstLi) {
729
+ const newRange = state.doc!.createRange()
730
+ newRange.selectNodeContents(firstLi)
731
+ newRange.collapse(true)
732
+ selectionInfo.selection.removeAllRanges()
733
+ selectionInfo.selection.addRange(newRange)
734
+ }
735
+ }
325
736
  return
326
737
  }
327
738
  }
@@ -386,10 +797,35 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
386
797
  // Convert ordered list to unordered list
387
798
  if (listParent.tagName.toLowerCase() === 'ol') {
388
799
  const ul = state.doc!.createElement('ul')
800
+
801
+ // Store current cursor position before conversion
802
+ const currentOffset = range.startOffset
803
+ const currentContainer = range.startContainer
804
+
389
805
  while (listParent.firstChild) {
390
806
  ul.appendChild(listParent.firstChild)
391
807
  }
392
808
  listParent.parentNode?.replaceChild(ul, listParent)
809
+
810
+ // Restore cursor position
811
+ try {
812
+ const newRange = state.doc!.createRange()
813
+ newRange.setStart(currentContainer, currentOffset)
814
+ newRange.collapse(true)
815
+ selectionInfo.selection.removeAllRanges()
816
+ selectionInfo.selection.addRange(newRange)
817
+ } catch (e) {
818
+ // Fallback: put cursor at beginning of first list item
819
+ console.warn('Could not restore cursor position:', e)
820
+ const firstLi = ul.querySelector('li')
821
+ if (firstLi) {
822
+ const newRange = state.doc!.createRange()
823
+ newRange.selectNodeContents(firstLi)
824
+ newRange.collapse(true)
825
+ selectionInfo.selection.removeAllRanges()
826
+ selectionInfo.selection.addRange(newRange)
827
+ }
828
+ }
393
829
  return
394
830
  }
395
831
  }
@@ -418,78 +854,145 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
418
854
  })
419
855
  }
420
856
 
421
- // Clear formatting command - simplified
857
+ // Clear formatting command - enhanced
422
858
  const formatCommands = {
423
859
  clear: createCommand('Clear Formatting', (state) => {
424
860
  const selectionInfo = getCurrentSelection(state)
425
- if (!selectionInfo || selectionInfo.range.collapsed) return
861
+ if (!selectionInfo) return
862
+
863
+ const { range, selection } = selectionInfo
426
864
 
427
- const { range } = selectionInfo
428
865
  try {
429
- // Get the selected content
430
- const fragment = range.cloneContents()
431
- const tempDiv = state.doc!.createElement('div')
432
- tempDiv.appendChild(fragment)
866
+ // Function to clean text from unwanted characters
867
+ function cleanText(text: string): string {
868
+ return text
869
+ // Remove zero-width characters
870
+ .replace(/[\u200B-\u200D\uFEFF]/g, '')
871
+ // Remove extra whitespace
872
+ .replace(/\s+/g, ' ')
873
+ // Remove invisible characters
874
+ .replace(/[\u00A0]/g, ' ') // non-breaking space to regular space
875
+ .trim()
876
+ }
433
877
 
434
878
  // Function to clean a node recursively
435
879
  function cleanNode(node: Node): Node {
436
880
  if (node.nodeType === Node.TEXT_NODE) {
437
- return node.cloneNode(true)
881
+ const cleanedText = cleanText(node.textContent || '')
882
+ return state.doc!.createTextNode(cleanedText)
438
883
  }
439
884
 
440
885
  if (node.nodeType === Node.ELEMENT_NODE) {
441
886
  const element = node as Element
442
887
  const tagName = element.tagName.toLowerCase()
443
888
 
444
- // 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']
889
+ // Preserve structural elements, links, and media
890
+ const structuralTags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'div', 'table', 'tr', 'td', 'th', 'thead', 'tbody', 'b', 'i', 'u', 'a', 'img', 'iframe', 'video', 'audio', 'figure', 'figcaption']
891
+ // Remove formatting elements (excluding basic HTML formatting tags)
892
+ const formattingTags = ['strong', 'em', 'span', 'font', 'strike', 'sub', 'sup', 'mark', 'del', 'ins', 'small', 'big']
448
893
 
449
894
  if (formattingTags.includes(tagName)) {
450
- // For formatting elements, just return the text content
451
- return state.doc!.createTextNode(element.textContent || '')
895
+ // For formatting elements, just return the cleaned text content
896
+ const cleanedText = cleanText(element.textContent || '')
897
+ return state.doc!.createTextNode(cleanedText)
452
898
  } else if (structuralTags.includes(tagName)) {
453
- // For structural elements, keep the element but remove attributes
899
+ // For structural elements, keep the element but remove all styling attributes
454
900
  const cleanElement = state.doc!.createElement(tagName)
455
901
 
456
- // Preserve certain attributes for links and images
902
+ // Only preserve essential attributes
457
903
  if (tagName === 'a' && element.hasAttribute('href')) {
458
904
  cleanElement.setAttribute('href', element.getAttribute('href') || '')
459
905
  if (element.hasAttribute('target')) {
460
906
  cleanElement.setAttribute('target', element.getAttribute('target') || '')
461
907
  }
908
+ if (element.hasAttribute('rel')) {
909
+ cleanElement.setAttribute('rel', element.getAttribute('rel') || '')
910
+ }
462
911
  } else if (tagName === 'img') {
463
912
  if (element.hasAttribute('src')) cleanElement.setAttribute('src', element.getAttribute('src') || '')
464
913
  if (element.hasAttribute('alt')) cleanElement.setAttribute('alt', element.getAttribute('alt') || '')
914
+ if (element.hasAttribute('width')) cleanElement.setAttribute('width', element.getAttribute('width') || '')
915
+ if (element.hasAttribute('height')) cleanElement.setAttribute('height', element.getAttribute('height') || '')
916
+ } else if (tagName === 'iframe') {
917
+ if (element.hasAttribute('src')) cleanElement.setAttribute('src', element.getAttribute('src') || '')
918
+ if (element.hasAttribute('width')) cleanElement.setAttribute('width', element.getAttribute('width') || '')
919
+ if (element.hasAttribute('height')) cleanElement.setAttribute('height', element.getAttribute('height') || '')
920
+ if (element.hasAttribute('frameborder')) cleanElement.setAttribute('frameborder', element.getAttribute('frameborder') || '')
921
+ if (element.hasAttribute('allowfullscreen')) cleanElement.setAttribute('allowfullscreen', element.getAttribute('allowfullscreen') || '')
922
+ } else if (tagName === 'video' || tagName === 'audio') {
923
+ if (element.hasAttribute('src')) cleanElement.setAttribute('src', element.getAttribute('src') || '')
924
+ if (element.hasAttribute('controls')) cleanElement.setAttribute('controls', element.getAttribute('controls') || '')
925
+ if (element.hasAttribute('width')) cleanElement.setAttribute('width', element.getAttribute('width') || '')
926
+ if (element.hasAttribute('height')) cleanElement.setAttribute('height', element.getAttribute('height') || '')
465
927
  }
466
928
 
467
929
  // Clean and append child nodes
468
930
  Array.from(element.childNodes).forEach((child) => {
469
- cleanElement.appendChild(cleanNode(child))
931
+ const cleanedChild = cleanNode(child)
932
+ if (cleanedChild.textContent?.trim() || cleanedChild.nodeType === Node.ELEMENT_NODE) {
933
+ cleanElement.appendChild(cleanedChild)
934
+ }
470
935
  })
471
936
 
472
937
  return cleanElement
473
938
  }
474
939
  }
475
940
 
476
- return node.cloneNode(true)
941
+ return state.doc!.createTextNode('')
477
942
  }
478
943
 
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
- })
944
+ if (range.collapsed) {
945
+ // If no selection, clean the entire document
946
+ const body = state.doc!.body
947
+ const cleanedFragment = state.doc!.createDocumentFragment()
484
948
 
485
- // Replace the selection with cleaned content
486
- range.deleteContents()
487
- range.insertNode(cleanedFragment)
949
+ Array.from(body.childNodes).forEach((node) => {
950
+ const cleanedNode = cleanNode(node)
951
+ if (cleanedNode.textContent?.trim() || cleanedNode.nodeType === Node.ELEMENT_NODE) {
952
+ cleanedFragment.appendChild(cleanedNode)
953
+ }
954
+ })
955
+
956
+ // Clear body and append cleaned content
957
+ body.innerHTML = ''
958
+ body.appendChild(cleanedFragment)
959
+
960
+ // If body is empty, add a paragraph
961
+ if (!body.firstElementChild) {
962
+ const p = state.doc!.createElement('p')
963
+ body.appendChild(p)
964
+
965
+ // Set cursor in the paragraph
966
+ const newRange = state.doc!.createRange()
967
+ newRange.selectNodeContents(p)
968
+ newRange.collapse(true)
969
+ selection.removeAllRanges()
970
+ selection.addRange(newRange)
971
+ }
972
+ } else {
973
+ // If there's a selection, clean only the selected content
974
+ const fragment = range.cloneContents()
975
+ const tempDiv = state.doc!.createElement('div')
976
+ tempDiv.appendChild(fragment)
977
+
978
+ // Clean all nodes in the temp div
979
+ const cleanedFragment = state.doc!.createDocumentFragment()
980
+ Array.from(tempDiv.childNodes).forEach((node) => {
981
+ const cleanedNode = cleanNode(node)
982
+ if (cleanedNode.textContent?.trim() || cleanedNode.nodeType === Node.ELEMENT_NODE) {
983
+ cleanedFragment.appendChild(cleanedNode)
984
+ }
985
+ })
488
986
 
489
- // Restore selection
490
- range.collapse(false)
491
- selectionInfo.selection.removeAllRanges()
492
- selectionInfo.selection.addRange(range)
987
+ // Replace the selection with cleaned content
988
+ range.deleteContents()
989
+ range.insertNode(cleanedFragment)
990
+
991
+ // Restore selection
992
+ range.collapse(false)
993
+ selection.removeAllRanges()
994
+ selection.addRange(range)
995
+ }
493
996
  } catch (error) {
494
997
  console.error('Error clearing formatting:', error)
495
998
  }
@@ -499,8 +1002,67 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
499
1002
  // Table commands
500
1003
  const tableCommands = {
501
1004
  insertTable: createCommand('Insert Table', (state, value) => {
502
- const [rows, cols] = value?.split('x').map(Number) || [3, 3]
503
- insertTable(rows, cols, state)
1005
+ console.log('insertTable command called with value:', value)
1006
+
1007
+ // If we have an openTableEditor function in state, use it for new tables
1008
+ if ((state as any).openTableEditor && !value) {
1009
+ console.log('Opening table editor modal for new table')
1010
+ ; (state as any).openTableEditor(null)
1011
+ return
1012
+ }
1013
+
1014
+ if (!value) {
1015
+ // Default fallback
1016
+ const [rows, cols] = [3, 3]
1017
+ insertTable(rows, cols, state)
1018
+ return
1019
+ }
1020
+
1021
+ // Check if value is HTML string (from our advanced table editor)
1022
+ if (value.includes('<table')) {
1023
+ console.log('Inserting HTML table')
1024
+ // Insert HTML table directly
1025
+ const selectionInfo = getCurrentSelection(state)
1026
+ if (!selectionInfo || !state.doc) {
1027
+ console.error('No selection info or document')
1028
+ return
1029
+ }
1030
+
1031
+ const { range } = selectionInfo
1032
+
1033
+ try {
1034
+ // Create a temporary div to parse the HTML
1035
+ const tempDiv = state.doc.createElement('div')
1036
+ tempDiv.innerHTML = value
1037
+ const table = tempDiv.querySelector('table')
1038
+
1039
+ if (table) {
1040
+ console.log('Table parsed successfully, inserting...')
1041
+ // Insert the table at the current selection
1042
+ const insertedTable = table.cloneNode(true) as HTMLTableElement
1043
+ range.insertNode(insertedTable)
1044
+
1045
+ // Move cursor after the table
1046
+ range.setStartAfter(insertedTable)
1047
+ range.collapse(true)
1048
+
1049
+ const selection = state.doc.getSelection()
1050
+ if (selection) {
1051
+ selection.removeAllRanges()
1052
+ selection.addRange(range)
1053
+ }
1054
+ console.log('Table inserted successfully')
1055
+ } else {
1056
+ console.error('Could not parse table from HTML')
1057
+ }
1058
+ } catch (error) {
1059
+ console.error('Error inserting HTML table:', error)
1060
+ }
1061
+ } else {
1062
+ // Original grid format (e.g., "3x3")
1063
+ const [rows, cols] = value.split('x').map(Number) || [3, 3]
1064
+ insertTable(rows, cols, state)
1065
+ }
504
1066
  }),
505
1067
  deleteTable: createCommand('Delete Table', state => state.range && deleteTable(state.range)),
506
1068
  mergeCells: createCommand('Merge Cells', state => state.range && state.doc && mergeCells(state.range, state.doc)),
@@ -513,12 +1075,189 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
513
1075
  deleteColumn: createCommand('Delete Column', state => state.range && deleteColumn(state.range))
514
1076
  }
515
1077
 
516
- // Alignment commands
1078
+ // Table alignment commands
517
1079
  const alignmentCommands = ['Left', 'Center', 'Right', 'Justify'].reduce((acc, align) => ({
518
1080
  ...acc,
519
- [`align${align}`]: createCommand(`Align ${align}`, state => state.range && alignColumn(state.range, align.toLowerCase() as 'left' | 'center' | 'right' | 'justify'))
1081
+ [`tableAlign${align}`]: createCommand(`Table Align ${align}`, state => {
1082
+ if (state.range) {
1083
+ const alignment = align === 'Left' ? 'start' : align === 'Right' ? 'end' : align.toLowerCase() as 'center' | 'justify'
1084
+ return alignColumn(state.range, alignment)
1085
+ }
1086
+ })
520
1087
  }), {})
521
1088
 
1089
+ // Text alignment commands (for paragraphs)
1090
+ const textAlignmentCommands = {
1091
+ alignLeft: createCommand('Align Left', (state) => {
1092
+ const selectionInfo = getCurrentSelection(state)
1093
+ if (!selectionInfo) return
1094
+
1095
+ const { range } = selectionInfo
1096
+ const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
1097
+ ? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
1098
+ : (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
1099
+
1100
+ if (paragraph) {
1101
+ (paragraph as HTMLElement).style.textAlign = 'left'
1102
+ }
1103
+ }, (state) => {
1104
+ const selectionInfo = getCurrentSelection(state)
1105
+ if (!selectionInfo) return false
1106
+
1107
+ const { range } = selectionInfo
1108
+ const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
1109
+ ? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
1110
+ : (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
1111
+
1112
+ return (paragraph as HTMLElement)?.style.textAlign === 'left'
1113
+ }),
1114
+
1115
+ alignCenter: createCommand('Align Center', (state) => {
1116
+ const selectionInfo = getCurrentSelection(state)
1117
+ if (!selectionInfo) return
1118
+
1119
+ const { range } = selectionInfo
1120
+ const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
1121
+ ? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
1122
+ : (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
1123
+
1124
+ if (paragraph) {
1125
+ (paragraph as HTMLElement).style.textAlign = 'center'
1126
+ }
1127
+ }, (state) => {
1128
+ const selectionInfo = getCurrentSelection(state)
1129
+ if (!selectionInfo) return false
1130
+
1131
+ const { range } = selectionInfo
1132
+ const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
1133
+ ? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
1134
+ : (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
1135
+
1136
+ return (paragraph as HTMLElement)?.style.textAlign === 'center'
1137
+ }),
1138
+
1139
+ alignRight: createCommand('Align Right', (state) => {
1140
+ const selectionInfo = getCurrentSelection(state)
1141
+ if (!selectionInfo) return
1142
+
1143
+ const { range } = selectionInfo
1144
+ const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
1145
+ ? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
1146
+ : (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
1147
+
1148
+ if (paragraph) {
1149
+ (paragraph as HTMLElement).style.textAlign = 'right'
1150
+ }
1151
+ }, (state) => {
1152
+ const selectionInfo = getCurrentSelection(state)
1153
+ if (!selectionInfo) return false
1154
+
1155
+ const { range } = selectionInfo
1156
+ const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
1157
+ ? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
1158
+ : (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
1159
+
1160
+ return (paragraph as HTMLElement)?.style.textAlign === 'right'
1161
+ }),
1162
+
1163
+ alignJustify: createCommand('Align Justify', (state) => {
1164
+ const selectionInfo = getCurrentSelection(state)
1165
+ if (!selectionInfo) return
1166
+
1167
+ const { range } = selectionInfo
1168
+ const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
1169
+ ? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
1170
+ : (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
1171
+
1172
+ if (paragraph) {
1173
+ (paragraph as HTMLElement).style.textAlign = 'justify'
1174
+ }
1175
+ }, (state) => {
1176
+ const selectionInfo = getCurrentSelection(state)
1177
+ if (!selectionInfo) return false
1178
+
1179
+ const { range } = selectionInfo
1180
+ const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
1181
+ ? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
1182
+ : (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
1183
+
1184
+ return (paragraph as HTMLElement)?.style.textAlign === 'justify'
1185
+ }),
1186
+
1187
+ textDirection: createCommand('Toggle Text Direction', (state) => {
1188
+ const selectionInfo = getCurrentSelection(state)
1189
+ if (!selectionInfo) return
1190
+
1191
+ const { range } = selectionInfo
1192
+ const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
1193
+ ? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
1194
+ : (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
1195
+
1196
+ if (paragraph) {
1197
+ const currentDir = (paragraph as HTMLElement).dir || 'ltr'
1198
+ ; (paragraph as HTMLElement).dir = currentDir === 'ltr' ? 'rtl' : 'ltr'
1199
+ }
1200
+ }, (state) => {
1201
+ const selectionInfo = getCurrentSelection(state)
1202
+ if (!selectionInfo) return false
1203
+
1204
+ const { range } = selectionInfo
1205
+ const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
1206
+ ? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
1207
+ : (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
1208
+
1209
+ return (paragraph as HTMLElement)?.dir === 'rtl'
1210
+ }),
1211
+
1212
+ ltrDirection: createCommand('Set Left to Right Direction', (state) => {
1213
+ const selectionInfo = getCurrentSelection(state)
1214
+ if (!selectionInfo) return
1215
+
1216
+ const { range } = selectionInfo
1217
+ const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
1218
+ ? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
1219
+ : (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
1220
+
1221
+ if (paragraph) {
1222
+ (paragraph as HTMLElement).dir = 'ltr'
1223
+ }
1224
+ }, (state) => {
1225
+ const selectionInfo = getCurrentSelection(state)
1226
+ if (!selectionInfo) return false
1227
+
1228
+ const { range } = selectionInfo
1229
+ const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
1230
+ ? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
1231
+ : (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
1232
+
1233
+ return (paragraph as HTMLElement)?.dir === 'ltr'
1234
+ }),
1235
+
1236
+ rtlDirection: createCommand('Set Right to Left Direction', (state) => {
1237
+ const selectionInfo = getCurrentSelection(state)
1238
+ if (!selectionInfo) return
1239
+
1240
+ const { range } = selectionInfo
1241
+ const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
1242
+ ? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
1243
+ : (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
1244
+
1245
+ if (paragraph) {
1246
+ (paragraph as HTMLElement).dir = 'rtl'
1247
+ }
1248
+ }, (state) => {
1249
+ const selectionInfo = getCurrentSelection(state)
1250
+ if (!selectionInfo) return false
1251
+
1252
+ const { range } = selectionInfo
1253
+ const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
1254
+ ? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
1255
+ : (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
1256
+
1257
+ return (paragraph as HTMLElement)?.dir === 'rtl'
1258
+ })
1259
+ }
1260
+
522
1261
  // View state commands
523
1262
  const viewCommands = {
524
1263
  fullScreen: createCommand('Full Screen', (state) => { state.isFullscreen = !state.isFullscreen }, state => state.isFullscreen),
@@ -528,9 +1267,30 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
528
1267
 
529
1268
  // Media commands
530
1269
  const mediaCommands = {
531
- image: createCommand('Insert Image', (state) => { state.modal && insertImage(state.modal, state) }),
532
- link: createCommand('Insert Link', state => state.modal && state.range && insertLink(state.modal, state)),
533
- embed: createCommand('Insert Embed', (state) => { state.modal && insertEmbed(state.modal, state) })
1270
+ image: createCommand('Insert Image', (state) => {
1271
+ const openImageModal = (state as any).openImageModal
1272
+ if (openImageModal) {
1273
+ openImageModal()
1274
+ } else {
1275
+ console.warn('Image insertion requires modal implementation')
1276
+ }
1277
+ }),
1278
+ video: createCommand('Insert Video', (state) => {
1279
+ const openVideoModal = (state as any).openVideoModal
1280
+ if (openVideoModal) {
1281
+ openVideoModal()
1282
+ } else {
1283
+ console.warn('Video insertion requires modal implementation')
1284
+ }
1285
+ }),
1286
+ embed: createCommand('Insert Embed', (state) => {
1287
+ const openEmbedModal = (state as any).openEmbedModal
1288
+ if (openEmbedModal) {
1289
+ openEmbedModal()
1290
+ } else {
1291
+ console.warn('Embed insertion requires modal implementation')
1292
+ }
1293
+ })
534
1294
  }
535
1295
 
536
1296
  return {
@@ -542,6 +1302,7 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
542
1302
  ...formatCommands,
543
1303
  ...tableCommands,
544
1304
  ...alignmentCommands,
1305
+ ...textAlignmentCommands,
545
1306
  ...viewCommands,
546
1307
  ...mediaCommands
547
1308
  }