@bagelink/vue 1.4.141 → 1.4.147

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 (70) 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/media-clean.d.ts +2 -0
  28. package/dist/components/form/inputs/RichText/utils/media-clean.d.ts.map +1 -0
  29. package/dist/components/form/inputs/RichText/utils/media.d.ts +4 -4
  30. package/dist/components/form/inputs/RichText/utils/media.d.ts.map +1 -1
  31. package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
  32. package/dist/components/form/inputs/RichText/utils/table.d.ts +1 -1
  33. package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
  34. package/dist/components/index.d.ts +1 -0
  35. package/dist/components/index.d.ts.map +1 -1
  36. package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
  37. package/dist/components/layout/AppLayout.vue.d.ts.map +1 -1
  38. package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
  39. package/dist/index.cjs +123 -22
  40. package/dist/index.mjs +123 -22
  41. package/dist/style.css +1 -1
  42. package/package.json +1 -1
  43. package/src/components/Btn.vue +50 -42
  44. package/src/components/Modal.vue +49 -50
  45. package/src/components/analytics/BarChart.vue +118 -7
  46. package/src/components/analytics/KpiCard.vue +2 -2
  47. package/src/components/analytics/LineChart.vue +189 -105
  48. package/src/components/analytics/PieChart.vue +392 -49
  49. package/src/components/form/inputs/RichText/CheckList.md +23 -0
  50. package/src/components/form/inputs/RichText/components/EditorToolbar.vue +243 -38
  51. package/src/components/form/inputs/RichText/components/TableGridSelector.vue +94 -0
  52. package/src/components/form/inputs/RichText/composables/useCommands.ts +4 -1
  53. package/src/components/form/inputs/RichText/composables/useEditor.ts +6 -6
  54. package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +1 -0
  55. package/src/components/form/inputs/RichText/config.ts +23 -11
  56. package/src/components/form/inputs/RichText/editor.css +300 -33
  57. package/src/components/form/inputs/RichText/index.vue +3014 -75
  58. package/src/components/form/inputs/RichText/richTextTypes.ts +2 -3
  59. package/src/components/form/inputs/RichText/utils/commands.ts +279 -50
  60. package/src/components/form/inputs/RichText/utils/media-clean.ts +0 -0
  61. package/src/components/form/inputs/RichText/utils/media.ts +133 -67
  62. package/src/components/form/inputs/RichText/utils/selection.ts +10 -2
  63. package/src/components/form/inputs/RichText/utils/table.ts +1 -1
  64. package/src/components/index.ts +1 -0
  65. package/src/components/layout/AppContent.vue +26 -26
  66. package/src/components/layout/AppLayout.vue +21 -3
  67. package/src/components/layout/AppSidebar.vue +5 -2
  68. package/src/styles/layout.css +267 -0
  69. package/src/styles/mobilLayout.css +266 -0
  70. package/src/styles/modal.css +3 -17
@@ -1,5 +1,3 @@
1
- import type { ModalApi } from '../../../../plugins/useModal'
2
-
3
1
  export interface EditorDebuggerInstance {
4
2
  logCommand: (command: string, value: string | undefined, state: any) => void
5
3
  logPaste: (data: DataTransfer, state: any) => void
@@ -35,7 +33,6 @@ export interface EditorState {
35
33
  redoStack: string[]
36
34
  rangeCount: number
37
35
  range: Range | null
38
- modal: ModalApi
39
36
  debug?: EditorDebugInterface
40
37
  }
41
38
 
@@ -53,6 +50,8 @@ export type FormattingCommand =
53
50
  | 'splitView'
54
51
  | 'codeView'
55
52
  | 'textDirection'
53
+ | 'ltrDirection'
54
+ | 'rtlDirection'
56
55
  | 'alignLeft'
57
56
  | 'alignCenter'
58
57
  | 'alignRight'
@@ -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 {
@@ -97,6 +97,14 @@ function applyFormattingToWordAtCursor(doc: Document, range: Range, tagName: str
97
97
  // Regular expression for word characters (Hebrew, English, numbers)
98
98
  const wordChar = /[\u0590-\u05FF\u0600-\u06FF\w]/
99
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
+
100
108
  // Move start backwards to find word beginning
101
109
  while (start > 0 && wordChar.test(text[start - 1])) {
102
110
  start--
@@ -429,6 +437,24 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
429
437
  : commonAncestor as Element
430
438
 
431
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)
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')
432
458
  })
433
459
  }
434
460
 
@@ -439,10 +465,17 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
439
465
  const selectionInfo = getCurrentSelection(state)
440
466
  if (!selectionInfo) return
441
467
 
442
- const { range } = selectionInfo
468
+ const { range, selection } = selectionInfo
443
469
  const currentBlock = findBlockElement(range.commonAncestorContainer)
444
470
  if (!currentBlock) return
445
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
+
446
479
  // Check if we need to toggle off (already in the same heading)
447
480
  const isToggleOff = currentBlock.tagName.toLowerCase() === cmd.toLowerCase()
448
481
  const newTag = isToggleOff ? 'p' : cmd
@@ -458,56 +491,105 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
458
491
  // Replace the old block with the new one
459
492
  currentBlock.parentNode?.replaceChild(newBlock, currentBlock)
460
493
 
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
+
461
582
  // If we created a heading (not toggling off), ensure there's a paragraph after it
462
583
  if (!isToggleOff) {
463
- // Check if there's already a next sibling element
464
584
  const nextSibling = newBlock.nextElementSibling
465
585
  if (!nextSibling) {
466
586
  // No element after the heading, create a paragraph
467
587
  const p = state.doc!.createElement('p')
468
588
  p.dir = newBlock.dir || state.doc!.body.dir
469
589
  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
590
  }
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
591
  }
491
592
 
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
-
511
593
  } catch (error) {
512
594
  console.error(`Error applying ${cmd} heading:`, error)
513
595
  }
@@ -640,7 +722,7 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
640
722
  newRange.collapse(true)
641
723
  selectionInfo.selection.removeAllRanges()
642
724
  selectionInfo.selection.addRange(newRange)
643
- } catch (e) {
725
+ } catch {
644
726
  // Fallback: put cursor at beginning of first list item
645
727
  const firstLi = ol.querySelector('li')
646
728
  if (firstLi) {
@@ -804,8 +886,8 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
804
886
  const element = node as Element
805
887
  const tagName = element.tagName.toLowerCase()
806
888
 
807
- // Preserve structural elements
808
- const structuralTags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'div', 'table', 'tr', 'td', 'th', 'thead', 'tbody', 'b', 'i', 'u']
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']
809
891
  // Remove formatting elements (excluding basic HTML formatting tags)
810
892
  const formattingTags = ['strong', 'em', 'span', 'font', 'strike', 'sub', 'sup', 'mark', 'del', 'ins', 'small', 'big']
811
893
 
@@ -823,11 +905,25 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
823
905
  if (element.hasAttribute('target')) {
824
906
  cleanElement.setAttribute('target', element.getAttribute('target') || '')
825
907
  }
908
+ if (element.hasAttribute('rel')) {
909
+ cleanElement.setAttribute('rel', element.getAttribute('rel') || '')
910
+ }
826
911
  } else if (tagName === 'img') {
827
912
  if (element.hasAttribute('src')) cleanElement.setAttribute('src', element.getAttribute('src') || '')
828
913
  if (element.hasAttribute('alt')) cleanElement.setAttribute('alt', element.getAttribute('alt') || '')
829
914
  if (element.hasAttribute('width')) cleanElement.setAttribute('width', element.getAttribute('width') || '')
830
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') || '')
831
927
  }
832
928
 
833
929
  // Clean and append child nodes
@@ -906,8 +1002,67 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
906
1002
  // Table commands
907
1003
  const tableCommands = {
908
1004
  insertTable: createCommand('Insert Table', (state, value) => {
909
- const [rows, cols] = value?.split('x').map(Number) || [3, 3]
910
- 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
+ }
911
1066
  }),
912
1067
  deleteTable: createCommand('Delete Table', state => state.range && deleteTable(state.range)),
913
1068
  mergeCells: createCommand('Merge Cells', state => state.range && state.doc && mergeCells(state.range, state.doc)),
@@ -923,7 +1078,12 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
923
1078
  // Table alignment commands
924
1079
  const alignmentCommands = ['Left', 'Center', 'Right', 'Justify'].reduce((acc, align) => ({
925
1080
  ...acc,
926
- [`tableAlign${align}`]: createCommand(`Table 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
+ })
927
1087
  }), {})
928
1088
 
929
1089
  // Text alignment commands (for paragraphs)
@@ -1041,6 +1201,54 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
1041
1201
  const selectionInfo = getCurrentSelection(state)
1042
1202
  if (!selectionInfo) return false
1043
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
+
1044
1252
  const { range } = selectionInfo
1045
1253
  const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
1046
1254
  ? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
@@ -1059,9 +1267,30 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
1059
1267
 
1060
1268
  // Media commands
1061
1269
  const mediaCommands = {
1062
- image: createCommand('Insert Image', (state) => { state.modal && insertImage(state.modal, state) }),
1063
- link: createCommand('Insert Link', state => state.modal && state.range && insertLink(state.modal, state)),
1064
- 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
+ })
1065
1294
  }
1066
1295
 
1067
1296
  return {
@@ -1,5 +1,4 @@
1
1
  import type { ModalApi } from '../../../../../plugins/useModal'
2
-
3
2
  import type { EditorState } from '../richTextTypes'
4
3
  import { bagelFormUtils as frm } from '../../../../../utils'
5
4
 
@@ -53,87 +52,154 @@ export function insertImage(modal: ModalApi, state: EditorState) {
53
52
  }
54
53
 
55
54
  export function insertLink(modal: ModalApi, state: EditorState) {
56
- const { range, doc } = state
57
- if (!range || !doc) return
55
+ const { doc } = state
56
+ if (!doc) {
57
+ console.log('No doc found')
58
+ return
59
+ }
58
60
 
59
- modal.showModalForm({
60
- title: 'Insert Link',
61
- schema: [
62
- { id: 'url', $el: 'text', label: 'URL' },
63
- { id: 'openInNewTab', $el: 'check', label: 'Open in new tab' },
64
- ],
65
- onSubmit: (data: { url: string, openInNewTab: boolean }) => {
66
- if (data.url) {
67
- const anchor = doc.createElement('a')
68
- anchor.href = data.url
69
- if (data.openInNewTab) anchor.target = '_blank'
70
- range.surroundContents(anchor)
71
- state.content = doc.body.innerHTML
61
+ // Get current selection
62
+ const selection = doc.getSelection()
63
+ if (!selection || !selection.rangeCount) {
64
+ console.log('No selection found')
65
+ return
66
+ }
67
+
68
+ let range = selection.getRangeAt(0)
69
+
70
+ // If no text is selected, try to select word at cursor
71
+ if (range.collapsed) {
72
+ const success = selectWordAtCursor(doc, range)
73
+ if (!success) {
74
+ console.log('Failed to select word at cursor')
75
+ // Show a helpful tooltip to the user
76
+ const showTooltipMessage = (state as any).showTooltipMessage
77
+ if (showTooltipMessage) {
78
+ showTooltipMessage('To add a link, please select text first or place cursor within a word')
79
+ } else {
80
+ // Fallback to alert if tooltip system is not available
81
+ alert('To add a link, please select text first or place the cursor within a word you want to turn into a link.')
72
82
  }
83
+ return
73
84
  }
74
- })
85
+ // Get the updated range after word selection
86
+ range = selection.getRangeAt(0)
87
+ }
88
+
89
+ // Check if we're editing an existing link
90
+ const commonAncestor = range.commonAncestorContainer
91
+ const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
92
+ ? commonAncestor.parentElement
93
+ : commonAncestor as Element
94
+
95
+ const existingLink = parentElement?.closest('a')
96
+
97
+ // Use the new modal function if available
98
+ if ((state as any).openLinkModal) {
99
+ (state as any).openLinkModal(selection, range, existingLink)
100
+ return
101
+ }
102
+
103
+ // Fallback - just show an alert if modal is not available
104
+ console.warn('Link modal not available - openLinkModal function not found')
105
+ alert('Link functionality requires proper modal setup')
75
106
  }
76
107
 
77
- export interface InsertImbedModalData { url: string, width?: number, height?: number, allowFullscreen?: boolean }
108
+ // Helper function to select word at cursor
109
+ function selectWordAtCursor(doc: Document, range: Range): boolean {
110
+ if (!range.collapsed) return true // Already has selection
111
+
112
+ let textNode = range.startContainer
113
+ let offset = range.startOffset
114
+
115
+ // If we're in an element node, try to find a text node
116
+ if (textNode.nodeType !== Node.TEXT_NODE) {
117
+ const element = textNode as Element
118
+ if (element.childNodes.length > 0 && offset < element.childNodes.length) {
119
+ const childNode = element.childNodes[offset]
120
+ if (childNode.nodeType === Node.TEXT_NODE) {
121
+ textNode = childNode
122
+ offset = 0
123
+ } else {
124
+ return false
125
+ }
126
+ } else {
127
+ return false
128
+ }
129
+ }
130
+
131
+ const text = textNode.textContent || ''
132
+ if (!text) return false
133
+
134
+ // Find word boundaries - support Hebrew, English, and numbers
135
+ let start = offset
136
+ let end = offset
137
+
138
+ // Define word characters (Hebrew, English, numbers)
139
+ const wordRegex = /[\u0590-\u05FF\u0600-\u06FF\w]/
140
+
141
+ // Move start backwards to find beginning of word
142
+ while (start > 0 && wordRegex.test(text[start - 1])) {
143
+ start--
144
+ }
145
+
146
+ // Move end forwards to find end of word
147
+ while (end < text.length && wordRegex.test(text[end])) {
148
+ end++
149
+ }
150
+
151
+ // If we found a word
152
+ if (start < end) {
153
+ range.setStart(textNode, start)
154
+ range.setEnd(textNode, end)
155
+
156
+ const selection = doc.getSelection()
157
+ if (selection) {
158
+ selection.removeAllRanges()
159
+ selection.addRange(range)
160
+ }
161
+ return true
162
+ }
163
+
164
+ return false
165
+ }
166
+
167
+ export interface InsertImbedModalData {
168
+ src: string
169
+ width: number
170
+ height: number
171
+ allowfullscreen: boolean
172
+ }
78
173
 
79
174
  export function insertEmbed(modal: ModalApi, state: EditorState) {
80
175
  const { range, doc } = state
81
176
  if (!range || !doc) return
82
177
 
83
- modal.showModalForm<InsertImbedModalData
84
- >({
85
- title: 'Insert Embed',
178
+ modal.showModalForm<InsertImbedModalData>({
179
+ title: 'Embed iframe',
86
180
  schema: [
87
- { id: 'url', $el: 'text', label: 'URL', attrs: { placeholder: 'Enter URL (YouTube, Vimeo, etc.)' } },
181
+ { id: 'src', $el: 'text', label: 'Source URL' },
88
182
  frm.frmRow(
89
- frm.numField('width', 'Width', { min: 200, placeholder: '560' }),
90
- frm.numField('height', 'Height', { min: 200, placeholder: '315' })
183
+ frm.numField('width', 'Width', { min: 1 }),
184
+ frm.numField('height', 'Height', { min: 1 }),
91
185
  ),
92
- { id: 'allowFullscreen', $el: 'check', label: 'Allow Fullscreen', attrs: { value: true } },
186
+ { id: 'allowfullscreen', $el: 'check', label: 'Allow Fullscreen' },
93
187
  ],
94
- onSubmit: (data: { url: string, width?: number, height?: number, allowFullscreen?: boolean }) => {
95
- if (!data.url) return
96
-
97
- // Convert common video URLs to embed URLs
98
- const url = new URL(data.url)
99
- let embedUrl = data.url
100
-
101
- // YouTube
102
- if (url.hostname.includes('youtube.com') || url.hostname === 'youtu.be') {
103
- const videoId = url.hostname === 'youtu.be'
104
- ? url.pathname.slice(1)
105
- : url.searchParams.get('v')
106
- if (videoId) {
107
- embedUrl = `https://www.youtube.com/embed/${videoId}`
108
- }
109
- }
110
- // Vimeo
111
- else if (url.hostname.includes('vimeo.com')) {
112
- const videoId = url.pathname.split('/').pop()
113
- if (videoId) {
114
- embedUrl = `https://player.vimeo.com/video/${videoId}`
115
- }
116
- }
188
+ onSubmit: (data: InsertImbedModalData) => {
189
+ if (data.src) {
190
+ const iframe = doc.createElement('iframe')
191
+ Object.assign(iframe, {
192
+ src: data.src,
193
+ width: data.width || 560,
194
+ height: data.height || 315,
195
+ allowfullscreen: data.allowfullscreen || false,
196
+ frameborder: '0'
197
+ })
117
198
 
118
- const iframe = doc.createElement('iframe')
119
- Object.assign(iframe, {
120
- src: embedUrl,
121
- width: data.width || 560,
122
- height: data.height || 315,
123
- frameBorder: '0',
124
- allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
125
- allowFullscreen: data.allowFullscreen
126
- })
127
-
128
- // Create a wrapper div for proper alignment and spacing
129
- const wrapper = doc.createElement('div')
130
- wrapper.style.textAlign = 'center'
131
- wrapper.style.margin = '1em 0'
132
- wrapper.appendChild(iframe)
133
-
134
- range.deleteContents()
135
- range.insertNode(wrapper)
136
- state.content = doc.body.innerHTML
199
+ range.collapse(false)
200
+ range.insertNode(iframe)
201
+ state.content = doc.body.innerHTML
202
+ }
137
203
  }
138
204
  })
139
205
  }