@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.
- package/dist/components/Btn.vue.d.ts.map +1 -1
- package/dist/components/Carousel.vue.d.ts +1 -1
- package/dist/components/Modal.vue.d.ts +3 -0
- package/dist/components/Modal.vue.d.ts.map +1 -1
- package/dist/components/Slider.vue.d.ts +1 -1
- package/dist/components/Slider.vue.d.ts.map +1 -1
- package/dist/components/analytics/BarChart.vue.d.ts +11 -3
- package/dist/components/analytics/BarChart.vue.d.ts.map +1 -1
- package/dist/components/analytics/LineChart.vue.d.ts +9 -0
- package/dist/components/analytics/LineChart.vue.d.ts.map +1 -1
- package/dist/components/analytics/PieChart.vue.d.ts +30 -2
- package/dist/components/analytics/PieChart.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts +8 -0
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/TableGridSelector.vue.d.ts +9 -0
- package/dist/components/form/inputs/RichText/components/TableGridSelector.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +0 -14
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts +15 -15
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts +1 -3
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/media-clean.d.ts +2 -0
- package/dist/components/form/inputs/RichText/utils/media-clean.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/media.d.ts +4 -4
- package/dist/components/form/inputs/RichText/utils/media.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
- package/dist/components/layout/AppLayout.vue.d.ts.map +1 -1
- package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
- package/dist/index.cjs +123 -22
- package/dist/index.mjs +123 -22
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/Btn.vue +50 -42
- package/src/components/Modal.vue +49 -50
- package/src/components/analytics/BarChart.vue +118 -7
- package/src/components/analytics/KpiCard.vue +2 -2
- package/src/components/analytics/LineChart.vue +189 -105
- package/src/components/analytics/PieChart.vue +392 -49
- package/src/components/form/inputs/RichText/CheckList.md +23 -0
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +243 -38
- package/src/components/form/inputs/RichText/components/TableGridSelector.vue +94 -0
- package/src/components/form/inputs/RichText/composables/useCommands.ts +4 -1
- package/src/components/form/inputs/RichText/composables/useEditor.ts +6 -6
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +1 -0
- package/src/components/form/inputs/RichText/config.ts +23 -11
- package/src/components/form/inputs/RichText/editor.css +300 -33
- package/src/components/form/inputs/RichText/index.vue +3014 -75
- package/src/components/form/inputs/RichText/richTextTypes.ts +2 -3
- package/src/components/form/inputs/RichText/utils/commands.ts +279 -50
- package/src/components/form/inputs/RichText/utils/media-clean.ts +0 -0
- package/src/components/form/inputs/RichText/utils/media.ts +133 -67
- package/src/components/form/inputs/RichText/utils/selection.ts +10 -2
- package/src/components/form/inputs/RichText/utils/table.ts +1 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/AppContent.vue +26 -26
- package/src/components/layout/AppLayout.vue +21 -3
- package/src/components/layout/AppSidebar.vue +5 -2
- package/src/styles/layout.css +267 -0
- package/src/styles/mobilLayout.css +266 -0
- 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 {
|
|
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
|
|
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
|
-
|
|
910
|
-
|
|
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 =>
|
|
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) => {
|
|
1063
|
-
|
|
1064
|
-
|
|
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 {
|
|
File without changes
|
|
@@ -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 {
|
|
57
|
-
if (!
|
|
55
|
+
const { doc } = state
|
|
56
|
+
if (!doc) {
|
|
57
|
+
console.log('No doc found')
|
|
58
|
+
return
|
|
59
|
+
}
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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: '
|
|
181
|
+
{ id: 'src', $el: 'text', label: 'Source URL' },
|
|
88
182
|
frm.frmRow(
|
|
89
|
-
frm.numField('width', 'Width', { min:
|
|
90
|
-
frm.numField('height', 'Height', { min:
|
|
183
|
+
frm.numField('width', 'Width', { min: 1 }),
|
|
184
|
+
frm.numField('height', 'Height', { min: 1 }),
|
|
91
185
|
),
|
|
92
|
-
{ id: '
|
|
186
|
+
{ id: 'allowfullscreen', $el: 'check', label: 'Allow Fullscreen' },
|
|
93
187
|
],
|
|
94
|
-
onSubmit: (data:
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
}
|