@bagelink/vue 0.0.1260 → 0.0.1268
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/AddressSearch.vue.d.ts +6 -0
- package/dist/components/AddressSearch.vue.d.ts.map +1 -1
- package/dist/components/DataTable/DataTable.vue.d.ts.map +1 -1
- package/dist/components/DropDown.vue.d.ts +51 -48
- package/dist/components/DropDown.vue.d.ts.map +1 -1
- package/dist/components/form/BagelForm.vue.d.ts.map +1 -1
- package/dist/components/form/FieldArray.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/DateInput.vue.d.ts +4 -1
- package/dist/components/form/inputs/DateInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/PasswordInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RadioGroup.vue.d.ts +1 -1
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +31 -23
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts +2 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/config.d.ts +2 -1
- package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts +1 -0
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/media.d.ts +5 -3
- 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/SelectInput.vue.d.ts +12 -0
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/TelInput.vue.d.ts +9 -3
- package/dist/components/form/inputs/TelInput.vue.d.ts.map +1 -1
- package/dist/components/form/useBagelFormState.d.ts.map +1 -1
- package/dist/editor-7QC0nG_c.js +4 -0
- package/dist/editor-CpMNx6Eo.cjs +4 -0
- package/dist/index.cjs +1731 -1191
- package/dist/index.mjs +1732 -1192
- package/dist/style.css +90 -83
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/DataTable/DataTable.vue +7 -1
- package/src/components/Dropdown.vue +5 -2
- package/src/components/form/BagelForm.vue +2 -13
- package/src/components/form/FieldArray.vue +3 -0
- package/src/components/form/inputs/DateInput.vue +341 -162
- package/src/components/form/inputs/PasswordInput.vue +5 -1
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +2 -2
- package/src/components/form/inputs/RichText/composables/useCommands.ts +53 -97
- package/src/components/form/inputs/RichText/composables/useEditor.ts +377 -270
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +124 -58
- package/src/components/form/inputs/RichText/config.ts +27 -3
- package/src/components/form/inputs/RichText/editor.css +29 -0
- package/src/components/form/inputs/RichText/index.vue +129 -55
- package/src/components/form/inputs/RichText/richTextTypes.d.ts +35 -49
- package/src/components/form/inputs/RichText/utils/commands.ts +181 -0
- package/src/components/form/inputs/RichText/utils/media.ts +64 -3
- package/src/components/form/inputs/RichText/utils/selection.ts +40 -5
- package/src/components/form/useBagelFormState.ts +2 -14
- package/src/utils/index.ts +15 -0
|
@@ -1,66 +1,132 @@
|
|
|
1
|
-
|
|
1
|
+
import type { CommandExecutor } from '../utils/commands'
|
|
2
|
+
|
|
3
|
+
interface KeyboardShortcut {
|
|
4
|
+
key: string
|
|
5
|
+
modifiers?: {
|
|
6
|
+
ctrl?: boolean
|
|
7
|
+
alt?: boolean
|
|
8
|
+
shift?: boolean
|
|
9
|
+
}
|
|
10
|
+
command: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const shortcuts: KeyboardShortcut[] = [
|
|
14
|
+
{ key: 'b', command: 'bold' },
|
|
15
|
+
{ key: 'i', command: 'italic' },
|
|
16
|
+
{ key: 'u', command: 'underline' },
|
|
17
|
+
{ key: 'z', command: 'undo' },
|
|
18
|
+
{ key: 'z', modifiers: { shift: true }, command: 'redo' },
|
|
19
|
+
{ key: 'y', command: 'redo' },
|
|
20
|
+
{ key: '.', modifiers: { shift: true }, command: 'orderedList' },
|
|
21
|
+
{ key: '/', modifiers: { shift: true }, command: 'unorderedList' },
|
|
22
|
+
{ key: ']', command: 'indent' },
|
|
23
|
+
{ key: '[', command: 'outdent' },
|
|
24
|
+
...Array.from({ length: 6 }, (_, i) => ({
|
|
25
|
+
key: String(i + 1),
|
|
26
|
+
modifiers: { alt: true },
|
|
27
|
+
command: `h${i + 1}`
|
|
28
|
+
}))
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
export function useEditorKeyboard(doc: Document, executor: CommandExecutor): void {
|
|
32
|
+
// Handle keyboard shortcuts
|
|
2
33
|
doc.addEventListener('keydown', (e) => {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
case 'z':
|
|
18
|
-
e.preventDefault()
|
|
19
|
-
if (e.shiftKey) {
|
|
20
|
-
handleToolbarAction('redo')
|
|
21
|
-
} else {
|
|
22
|
-
handleToolbarAction('undo')
|
|
23
|
-
}
|
|
24
|
-
break
|
|
25
|
-
case 'y':
|
|
26
|
-
e.preventDefault()
|
|
27
|
-
handleToolbarAction('redo')
|
|
28
|
-
break
|
|
29
|
-
// List shortcuts
|
|
30
|
-
case '.':
|
|
31
|
-
if (e.shiftKey) {
|
|
32
|
-
e.preventDefault()
|
|
33
|
-
handleToolbarAction('orderedList')
|
|
34
|
-
}
|
|
35
|
-
break
|
|
36
|
-
case '/':
|
|
37
|
-
if (e.shiftKey) {
|
|
34
|
+
// Handle Enter key in lists
|
|
35
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
36
|
+
const selection = doc.getSelection()
|
|
37
|
+
if (!selection || !selection.rangeCount) return
|
|
38
|
+
|
|
39
|
+
const range = selection.getRangeAt(0)
|
|
40
|
+
const container = range.commonAncestorContainer
|
|
41
|
+
const listItem = (container.nodeType === 3 ? container.parentElement : container as Element)?.closest('li')
|
|
42
|
+
|
|
43
|
+
if (listItem) {
|
|
44
|
+
// If we're at the end of a list item
|
|
45
|
+
if (range.collapsed && isAtEndOfNode(listItem, range)) {
|
|
46
|
+
// If the list item is empty, break out of the list
|
|
47
|
+
if (isNodeEmpty(listItem)) {
|
|
38
48
|
e.preventDefault()
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
// Create a new paragraph after the list
|
|
50
|
+
const list = listItem.parentElement
|
|
51
|
+
if (!list) return
|
|
52
|
+
|
|
53
|
+
// Remove the empty list item
|
|
54
|
+
listItem.remove()
|
|
55
|
+
|
|
56
|
+
// If the list is now empty, remove it
|
|
57
|
+
if (!list.querySelector('li')) {
|
|
58
|
+
const p = doc.createElement('p')
|
|
59
|
+
p.innerHTML = '<br>'
|
|
60
|
+
list.parentNode?.replaceChild(p, list)
|
|
61
|
+
|
|
62
|
+
// Set cursor in the new paragraph
|
|
63
|
+
range.selectNodeContents(p)
|
|
64
|
+
range.collapse(true)
|
|
65
|
+
selection.removeAllRanges()
|
|
66
|
+
selection.addRange(range)
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// Create a new list item
|
|
50
70
|
e.preventDefault()
|
|
51
|
-
|
|
71
|
+
const newLi = doc.createElement('li')
|
|
72
|
+
newLi.innerHTML = ' <br>' // Use non-breaking space with br
|
|
73
|
+
listItem.insertAdjacentElement('afterend', newLi)
|
|
74
|
+
|
|
75
|
+
// Move cursor to new list item
|
|
76
|
+
range.selectNodeContents(newLi)
|
|
77
|
+
range.collapse(true)
|
|
78
|
+
selection.removeAllRanges()
|
|
79
|
+
selection.addRange(range)
|
|
52
80
|
}
|
|
53
|
-
|
|
54
|
-
// Indentation
|
|
55
|
-
case ']':
|
|
56
|
-
e.preventDefault()
|
|
57
|
-
handleToolbarAction('indent')
|
|
58
|
-
break
|
|
59
|
-
case '[':
|
|
60
|
-
e.preventDefault()
|
|
61
|
-
handleToolbarAction('outdent')
|
|
62
|
-
break
|
|
81
|
+
}
|
|
63
82
|
}
|
|
64
83
|
}
|
|
84
|
+
|
|
85
|
+
// Handle other keyboard shortcuts
|
|
86
|
+
if (!e.ctrlKey && !e.metaKey) return
|
|
87
|
+
|
|
88
|
+
const matchingShortcut = shortcuts.find((shortcut) => {
|
|
89
|
+
const keyMatch = shortcut.key === e.key
|
|
90
|
+
const modifiersMatch = !shortcut.modifiers || (
|
|
91
|
+
(!shortcut.modifiers.ctrl || e.ctrlKey || e.metaKey)
|
|
92
|
+
&& (!shortcut.modifiers.alt || e.altKey)
|
|
93
|
+
&& (!shortcut.modifiers.shift || e.shiftKey)
|
|
94
|
+
)
|
|
95
|
+
return keyMatch && modifiersMatch
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
if (matchingShortcut) {
|
|
99
|
+
e.preventDefault()
|
|
100
|
+
executor.execute(matchingShortcut.command)
|
|
101
|
+
}
|
|
65
102
|
})
|
|
66
103
|
}
|
|
104
|
+
|
|
105
|
+
// Helper function to check if we're at the end of a node
|
|
106
|
+
function isAtEndOfNode(node: Node, range: Range): boolean {
|
|
107
|
+
if (node.nodeType === 3) { // Text node
|
|
108
|
+
return range.startOffset === (node as Text).length
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const { lastChild } = node
|
|
112
|
+
if (!lastChild) return true
|
|
113
|
+
|
|
114
|
+
if (lastChild.nodeType === 3) { // Text node
|
|
115
|
+
return range.startContainer === lastChild && range.startOffset === lastChild.textContent?.length
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return range.startContainer === node && range.startOffset === node.childNodes.length
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Helper function to check if a node is empty (contains only whitespace or <br> or )
|
|
122
|
+
function isNodeEmpty(node: Node): boolean {
|
|
123
|
+
const text = node.textContent?.replace(/\s/g, '') || '' // Remove non-breaking spaces and whitespace
|
|
124
|
+
if (text) return false
|
|
125
|
+
|
|
126
|
+
// Check for <br> tags
|
|
127
|
+
const brElements = (node as Element).getElementsByTagName('br')
|
|
128
|
+
if (brElements.length === 0) return true
|
|
129
|
+
|
|
130
|
+
// If there's only one <br> and it's the only content (besides potential ), consider it empty
|
|
131
|
+
return brElements.length === 1 && node.childNodes.length <= 2 // Allow for + <br>
|
|
132
|
+
}
|
|
@@ -16,7 +16,31 @@ export const tableTools: ToolbarConfig = [
|
|
|
16
16
|
'deleteTable'
|
|
17
17
|
]
|
|
18
18
|
|
|
19
|
-
export const
|
|
19
|
+
export const basicToolbarConfig: ToolbarConfig = [
|
|
20
|
+
'h2',
|
|
21
|
+
'h3',
|
|
22
|
+
'h4',
|
|
23
|
+
'h5',
|
|
24
|
+
'h6',
|
|
25
|
+
'separator',
|
|
26
|
+
'p',
|
|
27
|
+
'blockquote',
|
|
28
|
+
'orderedList',
|
|
29
|
+
'unorderedList',
|
|
30
|
+
'separator',
|
|
31
|
+
'bold',
|
|
32
|
+
'italic',
|
|
33
|
+
'underline',
|
|
34
|
+
'separator',
|
|
35
|
+
'link',
|
|
36
|
+
'image',
|
|
37
|
+
'embed',
|
|
38
|
+
'clear',
|
|
39
|
+
'splitView',
|
|
40
|
+
'fullScreen',
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
export const fullToolbarConfig: ToolbarConfig = [
|
|
20
44
|
'h2',
|
|
21
45
|
'h3',
|
|
22
46
|
'h4',
|
|
@@ -39,7 +63,7 @@ export const defaultToolbarConfig: ToolbarConfig = [
|
|
|
39
63
|
'separator',
|
|
40
64
|
'link',
|
|
41
65
|
'image',
|
|
42
|
-
|
|
66
|
+
'embed',
|
|
43
67
|
'separator',
|
|
44
68
|
'splitView',
|
|
45
69
|
'clear',
|
|
@@ -66,7 +90,7 @@ export const toolbarOptions: ToolbarOption[] = [
|
|
|
66
90
|
{ name: 'unorderedList', label: 'Unordered List', icon: 'format_list_bulleted' },
|
|
67
91
|
{ name: 'link', label: 'Link', icon: 'add_link' },
|
|
68
92
|
{ name: 'image', label: 'Image', icon: 'add_photo_alternate' },
|
|
69
|
-
{ name: '
|
|
93
|
+
{ name: 'embed', label: 'Embed', icon: 'media_link' },
|
|
70
94
|
{ name: 'splitView', label: 'Split View', icon: 'code' },
|
|
71
95
|
{ name: 'clear', label: 'Clear Formatting', icon: 'format_clear' },
|
|
72
96
|
{ name: 'alignLeft', label: 'Align Left', icon: 'format_align_left' },
|
|
@@ -26,3 +26,32 @@ td {
|
|
|
26
26
|
th {
|
|
27
27
|
background-color: #f4f4f4;
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
/* Add styles for embedded content */
|
|
31
|
+
iframe {
|
|
32
|
+
max-width: 100%;
|
|
33
|
+
border: none;
|
|
34
|
+
display: block;
|
|
35
|
+
margin: 1em auto;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Responsive iframe wrapper */
|
|
39
|
+
div:has(> iframe) {
|
|
40
|
+
position: relative;
|
|
41
|
+
width: 100%;
|
|
42
|
+
margin: 1em 0;
|
|
43
|
+
text-align: center;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Ensure iframes don't overflow their containers */
|
|
47
|
+
div:has(> iframe) iframe {
|
|
48
|
+
max-width: 100%;
|
|
49
|
+
margin: 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* Add a subtle border to distinguish embedded content */
|
|
53
|
+
iframe:not([class*='editableContent']) {
|
|
54
|
+
border: 1px solid var(--border-color);
|
|
55
|
+
border-radius: 4px;
|
|
56
|
+
background: white;
|
|
57
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { ToolbarConfig } from './richTextTypes'
|
|
3
3
|
import { CodeEditor, copyText, Btn } from '@bagelink/vue'
|
|
4
|
-
import { watch } from 'vue'
|
|
4
|
+
import { watch, onUnmounted, ref } from 'vue'
|
|
5
5
|
import EditorToolbar from './components/EditorToolbar.vue'
|
|
6
6
|
import { useCommands } from './composables/useCommands'
|
|
7
7
|
import { useEditor } from './composables/useEditor'
|
|
@@ -10,70 +10,134 @@ import { useEditorKeyboard } from './composables/useEditorKeyboard'
|
|
|
10
10
|
const props = defineProps<{ modelValue: string, toolbarConfig?: ToolbarConfig, debug?: boolean, label?: string }>()
|
|
11
11
|
const emit = defineEmits(['update:modelValue'])
|
|
12
12
|
|
|
13
|
-
const iframe =
|
|
13
|
+
const iframe = ref<HTMLIFrameElement>()
|
|
14
14
|
const editor = useEditor()
|
|
15
|
-
const
|
|
15
|
+
const isInitializing = ref(false)
|
|
16
|
+
const hasInitialized = ref(false)
|
|
17
|
+
|
|
18
|
+
// Initialize debugger if debug mode is enabled
|
|
19
|
+
if (props.debug) {
|
|
20
|
+
editor.initDebugger()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const commands = useCommands(editor.state, editor.state.debug)
|
|
16
24
|
|
|
17
25
|
// Expose debug methods if debug mode is enabled
|
|
18
|
-
const debugMethods = $computed(() =>
|
|
26
|
+
const debugMethods = $computed(() => editor.state.debug)
|
|
19
27
|
const hasRTL = $computed(() => /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/.test(props.modelValue))
|
|
28
|
+
|
|
29
|
+
// Cleanup on component unmount
|
|
30
|
+
onUnmounted(() => {
|
|
31
|
+
editor.cleanup()
|
|
32
|
+
})
|
|
33
|
+
|
|
20
34
|
async function initEditor() {
|
|
21
|
-
|
|
22
|
-
|
|
35
|
+
console.log('[initEditor] Starting, isInitializing:', isInitializing.value, 'hasInitialized:', hasInitialized.value)
|
|
36
|
+
if (isInitializing.value || !iframe.value || hasInitialized.value) {
|
|
37
|
+
console.log('[initEditor] Skipped - already initializing/initialized or no iframe')
|
|
23
38
|
return
|
|
24
39
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
40
|
+
|
|
41
|
+
isInitializing.value = true
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
// Load styles first
|
|
45
|
+
const editorStyles = await import('./editor.css?inline')
|
|
46
|
+
|
|
47
|
+
// Create a complete HTML document with proper doctype and meta tags
|
|
48
|
+
const htmlContent = `
|
|
49
|
+
<!DOCTYPE html>
|
|
50
|
+
<html>
|
|
51
|
+
<head>
|
|
52
|
+
<meta charset="UTF-8">
|
|
53
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
54
|
+
<meta http-equiv="Content-Security-Policy" content="
|
|
55
|
+
default-src * data: blob: 'unsafe-inline' 'unsafe-eval';
|
|
56
|
+
img-src * data: blob:;
|
|
57
|
+
style-src * 'unsafe-inline';
|
|
58
|
+
script-src * 'unsafe-inline' 'unsafe-eval';
|
|
59
|
+
frame-src * data: blob:;
|
|
60
|
+
connect-src *;
|
|
61
|
+
media-src *;
|
|
62
|
+
">
|
|
63
|
+
<base target="_blank">
|
|
64
|
+
<style id="editor-styles">${editorStyles.default}</style>
|
|
65
|
+
</head>
|
|
66
|
+
<body>${props.modelValue || ''}</body>
|
|
67
|
+
</html>
|
|
68
|
+
`
|
|
69
|
+
|
|
70
|
+
// Write the complete HTML document to the iframe
|
|
71
|
+
const doc = iframe.value.contentDocument || iframe.value.contentWindow?.document
|
|
72
|
+
if (!doc) {
|
|
73
|
+
console.warn('[initEditor] No document found')
|
|
74
|
+
return
|
|
57
75
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
76
|
+
|
|
77
|
+
// First write the content
|
|
78
|
+
doc.open()
|
|
79
|
+
doc.write(htmlContent)
|
|
80
|
+
doc.close()
|
|
81
|
+
|
|
82
|
+
// Then make it editable after a short delay
|
|
83
|
+
await new Promise(resolve => setTimeout(resolve, 0))
|
|
84
|
+
|
|
85
|
+
doc.designMode = 'on'
|
|
86
|
+
doc.body.contentEditable = 'true'
|
|
87
|
+
|
|
88
|
+
// Set default direction based on content
|
|
89
|
+
doc.body.dir = hasRTL ? 'rtl' : 'ltr'
|
|
90
|
+
|
|
91
|
+
editor.init(doc)
|
|
92
|
+
useEditorKeyboard(doc, commands)
|
|
93
|
+
|
|
94
|
+
// Initial cleanup and ensure there's at least one paragraph
|
|
95
|
+
if (!doc.body.firstElementChild) {
|
|
96
|
+
const p = doc.createElement('p')
|
|
97
|
+
p.dir = doc.body.dir
|
|
98
|
+
p.innerHTML = '<br>'
|
|
99
|
+
doc.body.appendChild(p)
|
|
100
|
+
} else {
|
|
101
|
+
// Convert any direct text nodes to paragraphs
|
|
102
|
+
const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT)
|
|
103
|
+
const textNodes: Text[] = []
|
|
104
|
+
let node: Node | null
|
|
105
|
+
while ((node = walker.nextNode())) {
|
|
106
|
+
if (node.parentElement === doc.body) {
|
|
107
|
+
textNodes.push(node as Text)
|
|
108
|
+
}
|
|
66
109
|
}
|
|
67
|
-
|
|
68
|
-
|
|
110
|
+
textNodes.forEach((textNode) => {
|
|
111
|
+
if (textNode.textContent?.trim()) {
|
|
112
|
+
const p = doc.createElement('p')
|
|
113
|
+
p.dir = doc.body.dir
|
|
114
|
+
p.appendChild(textNode.cloneNode())
|
|
115
|
+
doc.body.replaceChild(p, textNode)
|
|
116
|
+
} else {
|
|
117
|
+
doc.body.removeChild(textNode)
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
}
|
|
69
121
|
|
|
70
|
-
|
|
122
|
+
doc.body.focus()
|
|
123
|
+
hasInitialized.value = true
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error('[initEditor] Error during initialization:', error)
|
|
126
|
+
} finally {
|
|
127
|
+
isInitializing.value = false
|
|
128
|
+
}
|
|
71
129
|
}
|
|
72
130
|
|
|
73
|
-
|
|
131
|
+
// Reset initialization state when content changes significantly
|
|
132
|
+
watch(() => props.modelValue, (newValue, oldValue) => {
|
|
74
133
|
if (newValue !== editor.state.content) {
|
|
75
|
-
|
|
76
|
-
|
|
134
|
+
// Only reset if content change is significant (not just minor edits)
|
|
135
|
+
if (!oldValue || Math.abs(newValue.length - oldValue.length) > 50) {
|
|
136
|
+
console.log('[watch] Significant content change, resetting initialization state')
|
|
137
|
+
hasInitialized.value = false
|
|
138
|
+
editor.state.content = newValue
|
|
139
|
+
editor.updateState.content('html')
|
|
140
|
+
}
|
|
77
141
|
}
|
|
78
142
|
})
|
|
79
143
|
|
|
@@ -95,11 +159,21 @@ watch(() => editor.state.content, (newValue) => {
|
|
|
95
159
|
/>
|
|
96
160
|
<div class="editor-container" :class="{ 'split-view': editor.state.isSplitView }">
|
|
97
161
|
<div class="content-area radius-05">
|
|
98
|
-
<iframe
|
|
162
|
+
<iframe
|
|
163
|
+
id="rich-text-iframe"
|
|
164
|
+
ref="iframe"
|
|
165
|
+
class="editableContent"
|
|
166
|
+
title="Editor"
|
|
167
|
+
sandbox="allow-same-origin allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-scripts allow-top-navigation allow-top-navigation-by-user-activation"
|
|
168
|
+
src="about:blank"
|
|
169
|
+
@load="initEditor"
|
|
170
|
+
/>
|
|
99
171
|
</div>
|
|
100
172
|
<CodeEditor
|
|
101
|
-
v-if="editor.state.isSplitView"
|
|
102
|
-
|
|
173
|
+
v-if="editor.state.isSplitView"
|
|
174
|
+
v-model="editor.state.content"
|
|
175
|
+
language="html"
|
|
176
|
+
@update:modelValue="editor.updateState.content('html')"
|
|
103
177
|
/>
|
|
104
178
|
</div>
|
|
105
179
|
<div v-if="debug" class="flex">
|
|
@@ -1,27 +1,42 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Modal as BagelModal } from '@bagelink/vue'
|
|
2
2
|
|
|
3
|
-
export interface
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
export interface EditorDebuggerInstance {
|
|
4
|
+
logCommand: (command: string, value: string | undefined, state: any) => void
|
|
5
|
+
logPaste: (data: DataTransfer, state: any) => void
|
|
6
|
+
logKeyboard: (event: KeyboardEvent, state: any) => void
|
|
7
|
+
logSelection: (state: any) => void
|
|
8
|
+
logInput: (inputType: string, data: string | null, state: any) => void
|
|
9
|
+
getSession: () => any
|
|
10
|
+
clearSession: () => void
|
|
11
|
+
downloadSession: () => void
|
|
12
|
+
exportSession: () => string
|
|
13
|
+
exportSessionWithPrompt: (userMessage?: string) => string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface EditorDebugInterface {
|
|
17
|
+
debugger: EditorDebuggerInstance
|
|
18
|
+
logCommand: (command: string, value?: string) => void
|
|
19
|
+
getSession: () => any
|
|
20
|
+
clearSession: () => void
|
|
21
|
+
downloadSession: () => void
|
|
22
|
+
exportDebugWithPrompt: (message?: string) => string | undefined
|
|
9
23
|
}
|
|
10
24
|
|
|
11
25
|
export interface EditorState {
|
|
12
|
-
isFullscreen: boolean
|
|
13
|
-
hasInit: boolean
|
|
14
|
-
isCodeView: boolean
|
|
15
|
-
isSplitView: boolean
|
|
16
|
-
selectedStyles: Set<string>
|
|
17
26
|
content: string
|
|
18
|
-
rangeCount: number
|
|
19
|
-
selection?: Selection | null
|
|
20
|
-
range?: Range | null
|
|
21
27
|
doc?: Document
|
|
22
|
-
|
|
28
|
+
selection: Selection | null
|
|
29
|
+
selectedStyles: Set<string>
|
|
30
|
+
isFullscreen: boolean
|
|
31
|
+
isSplitView: boolean
|
|
32
|
+
isCodeView: boolean
|
|
33
|
+
hasInit: boolean
|
|
23
34
|
undoStack: string[]
|
|
24
35
|
redoStack: string[]
|
|
36
|
+
rangeCount: number
|
|
37
|
+
range: Range | null
|
|
38
|
+
modal: BagelModal
|
|
39
|
+
debug?: EditorDebugInterface
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
export type FormattingCommand =
|
|
@@ -33,49 +48,20 @@ export type FormattingCommand =
|
|
|
33
48
|
| 'unorderedList'
|
|
34
49
|
| 'link'
|
|
35
50
|
| 'image'
|
|
36
|
-
| '
|
|
51
|
+
| 'embed'
|
|
37
52
|
| 'table'
|
|
38
53
|
| 'splitView'
|
|
39
54
|
| 'codeView'
|
|
40
55
|
|
|
41
56
|
export type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
|
42
57
|
|
|
43
|
-
export type ToolbarConfigOption =
|
|
44
|
-
| FormattingCommand
|
|
45
|
-
| HeadingLevel
|
|
46
|
-
| 'p'
|
|
47
|
-
| 'blockquote'
|
|
48
|
-
| 'separator'
|
|
49
|
-
| 'fullScreen'
|
|
50
|
-
| 'clear'
|
|
51
|
-
| 'mergeCells'
|
|
52
|
-
| 'splitCells'
|
|
53
|
-
| 'addRowBefore'
|
|
54
|
-
| 'addRowAfter'
|
|
55
|
-
| 'deleteRow'
|
|
56
|
-
| 'alignLeft'
|
|
57
|
-
| 'alignCenter'
|
|
58
|
-
| 'alignRight'
|
|
59
|
-
| 'alignJustify'
|
|
60
|
-
| 'indent'
|
|
61
|
-
| 'outdent'
|
|
62
|
-
| 'fontColor'
|
|
63
|
-
| 'bgColor'
|
|
64
|
-
| 'insertTable'
|
|
65
|
-
| 'deleteTable'
|
|
66
|
-
| 'insertRowAbove'
|
|
67
|
-
| 'insertRowBelow'
|
|
68
|
-
| 'insertColumnLeft'
|
|
69
|
-
| 'insertColumnRight'
|
|
70
|
-
| 'deleteColumn'
|
|
71
|
-
| 'undo'
|
|
72
|
-
| 'redo'
|
|
58
|
+
export type ToolbarConfigOption = string
|
|
73
59
|
|
|
74
60
|
export type ToolbarConfig = ToolbarConfigOption[]
|
|
75
61
|
|
|
76
62
|
export interface ToolbarOption {
|
|
77
|
-
name:
|
|
63
|
+
name: string
|
|
78
64
|
label?: string
|
|
79
|
-
icon?:
|
|
65
|
+
icon?: string
|
|
80
66
|
class?: string
|
|
81
67
|
}
|