@bagelink/vue 1.4.62 → 1.4.69
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/BglVideo.vue.d.ts.map +1 -1
- package/dist/components/NavBar.vue.d.ts.map +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.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts +128 -1
- package/dist/components/form/inputs/RichText/index.vue.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/selection.d.ts.map +1 -1
- package/dist/index.cjs +20 -20
- package/dist/index.mjs +20 -20
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/BglVideo.vue +16 -21
- package/src/components/NavBar.vue +17 -5
- package/src/components/form/inputs/RichText/composables/useCommands.ts +9 -29
- package/src/components/form/inputs/RichText/composables/useEditor.ts +45 -46
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +52 -3
- package/src/components/form/inputs/RichText/index.vue +148 -43
- package/src/components/form/inputs/RichText/utils/commands.ts +347 -282
- package/src/components/form/inputs/RichText/utils/selection.ts +55 -37
package/package.json
CHANGED
|
@@ -66,7 +66,7 @@ defineExpose({ play, pause })
|
|
|
66
66
|
const embedType = $computed<'YouTube' | 'Vimeo' | undefined>(() => {
|
|
67
67
|
const youtubeRegex = /youtube\.com|youtu\.be/
|
|
68
68
|
if (youtubeRegex.test(props.src || '')) return 'YouTube'
|
|
69
|
-
const vimeoRegex = /vimeo\.com/
|
|
69
|
+
const vimeoRegex = /(?:player\.)?vimeo\.com/
|
|
70
70
|
if (vimeoRegex.test(props.src || '')) return 'Vimeo'
|
|
71
71
|
})
|
|
72
72
|
|
|
@@ -90,10 +90,15 @@ const videoUrl = $computed(() => {
|
|
|
90
90
|
return `https://www.youtube.com/embed/${videoId}?${queryParams}`
|
|
91
91
|
}
|
|
92
92
|
if (embedType === 'Vimeo') {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
// Handle both regular Vimeo URLs and player.vimeo.com embed URLs
|
|
94
|
+
const playerVimeoRegex = /player\.vimeo\.com\/video\/(\d+)/
|
|
95
|
+
const regularVimeoRegex = /vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/)?(\d+)(?:$|\/|\?)/
|
|
96
|
+
|
|
97
|
+
const playerMatch = props.src?.match(playerVimeoRegex)
|
|
98
|
+
const regularMatch = props.src?.match(regularVimeoRegex)
|
|
99
|
+
|
|
100
|
+
const videoId = playerMatch?.[1] || regularMatch?.[3]
|
|
101
|
+
console.log(videoId, { playerMatch, regularMatch })
|
|
97
102
|
return `https://player.vimeo.com/video/${videoId}`
|
|
98
103
|
}
|
|
99
104
|
}
|
|
@@ -104,24 +109,13 @@ const videoUrl = $computed(() => {
|
|
|
104
109
|
<template>
|
|
105
110
|
<div ref="videoContainer" class="bgl_vid" :class="{ vid_empty: !src, vid_short: isYoutubeShort }">
|
|
106
111
|
<iframe
|
|
107
|
-
v-if="embedType"
|
|
108
|
-
:src="videoUrl"
|
|
109
|
-
:style="{ aspectRatio }"
|
|
110
|
-
frameborder="0"
|
|
111
|
-
title="Video"
|
|
112
|
+
v-if="embedType" :src="videoUrl" :style="{ aspectRatio }" frameborder="0" title="Video"
|
|
112
113
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
113
|
-
referrerpolicy="strict-origin-when-cross-origin"
|
|
114
|
-
allowfullscreen
|
|
114
|
+
referrerpolicy="strict-origin-when-cross-origin" allowfullscreen
|
|
115
115
|
/>
|
|
116
116
|
<video
|
|
117
|
-
v-else-if="src"
|
|
118
|
-
|
|
119
|
-
:autoplay="autoplay === true"
|
|
120
|
-
:muted="mute"
|
|
121
|
-
:loop="loop"
|
|
122
|
-
:style="{ aspectRatio }"
|
|
123
|
-
:controls="controls"
|
|
124
|
-
:playsinline="playsinline"
|
|
117
|
+
v-else-if="src" ref="video" :autoplay="autoplay === true" :muted="mute" :loop="loop"
|
|
118
|
+
:style="{ aspectRatio }" :controls="controls" :playsinline="playsinline"
|
|
125
119
|
>
|
|
126
120
|
<source :src="src" :type="`video/${videoFormat}`">
|
|
127
121
|
</video>
|
|
@@ -145,7 +139,8 @@ const videoUrl = $computed(() => {
|
|
|
145
139
|
}
|
|
146
140
|
|
|
147
141
|
.bgl_vid.vid_short {
|
|
148
|
-
max-width: 56.25vh;
|
|
142
|
+
max-width: 56.25vh;
|
|
143
|
+
/* Limit width for shorts to maintain aspect ratio */
|
|
149
144
|
margin: 0 auto;
|
|
150
145
|
}
|
|
151
146
|
</style>
|
|
@@ -41,7 +41,11 @@ onMounted(calcIsOpen)
|
|
|
41
41
|
<div :class="{ open: isOpen, closed: !isOpen }">
|
|
42
42
|
<slot name="top" :isOpen="isOpen" />
|
|
43
43
|
<div
|
|
44
|
-
class="nav-expend"
|
|
44
|
+
class="nav-expend"
|
|
45
|
+
role="button"
|
|
46
|
+
aria-label="Toggle Navigation"
|
|
47
|
+
tabindex="0"
|
|
48
|
+
@click="toggleMenu"
|
|
45
49
|
@keypress.enter="toggleMenu"
|
|
46
50
|
>
|
|
47
51
|
<Icon icon="chevron_right" class="top-arrow" />
|
|
@@ -51,8 +55,12 @@ onMounted(calcIsOpen)
|
|
|
51
55
|
<div class="nav-scroll">
|
|
52
56
|
<div class="nav-links-wrapper">
|
|
53
57
|
<component
|
|
54
|
-
:is="link.to ? 'router-link' : 'div'"
|
|
55
|
-
|
|
58
|
+
:is="link.to ? 'router-link' : 'div'"
|
|
59
|
+
v-for="link in links"
|
|
60
|
+
:key="link.label"
|
|
61
|
+
class="nav-button"
|
|
62
|
+
:to="link.to"
|
|
63
|
+
@click="link.onClick?.()"
|
|
56
64
|
>
|
|
57
65
|
<Icon :icon="link.icon" />
|
|
58
66
|
<div class="tooltip">
|
|
@@ -64,8 +72,12 @@ onMounted(calcIsOpen)
|
|
|
64
72
|
|
|
65
73
|
<div class="bot-buttons-wrapper">
|
|
66
74
|
<component
|
|
67
|
-
:is="link.to ? 'router-link' : 'div'"
|
|
68
|
-
|
|
75
|
+
:is="link.to ? 'router-link' : 'div'"
|
|
76
|
+
v-for="link in footerLinks"
|
|
77
|
+
:key="link.label"
|
|
78
|
+
class="nav-button"
|
|
79
|
+
:to="link.to"
|
|
80
|
+
@click="link.onClick?.()"
|
|
69
81
|
>
|
|
70
82
|
<Icon :icon="link.icon" />
|
|
71
83
|
<div class="tooltip">
|
|
@@ -7,8 +7,6 @@ export function useCommands(state: EditorState, debug?: { logCommand: (command:
|
|
|
7
7
|
|
|
8
8
|
return {
|
|
9
9
|
execute: (command: string, value?: string) => {
|
|
10
|
-
console.log(`[useCommands] Executing command: ${command}`, value ? `with value: ${value}` : '')
|
|
11
|
-
|
|
12
10
|
if (!state.doc) {
|
|
13
11
|
console.log('[useCommands] No document available, skipping command')
|
|
14
12
|
return
|
|
@@ -17,9 +15,8 @@ export function useCommands(state: EditorState, debug?: { logCommand: (command:
|
|
|
17
15
|
// Log command if debug is enabled
|
|
18
16
|
debug?.logCommand(command, value)
|
|
19
17
|
|
|
20
|
-
// Handle view state commands directly
|
|
18
|
+
// Handle view state commands directly (these don't need DOM manipulation)
|
|
21
19
|
if (['splitView', 'codeView', 'fullScreen'].includes(command)) {
|
|
22
|
-
console.log(`[useCommands] Handling view state command: ${command}`)
|
|
23
20
|
switch (command) {
|
|
24
21
|
case 'splitView':
|
|
25
22
|
state.isSplitView = !state.isSplitView
|
|
@@ -36,41 +33,24 @@ export function useCommands(state: EditorState, debug?: { logCommand: (command:
|
|
|
36
33
|
// Focus the editor before executing command
|
|
37
34
|
try {
|
|
38
35
|
state.doc.body.focus()
|
|
39
|
-
console.log('[useCommands] Editor focused')
|
|
40
36
|
} catch (e) {
|
|
41
37
|
console.error('[useCommands] Error focusing editor:', e)
|
|
42
38
|
}
|
|
43
39
|
|
|
40
|
+
// Update current selection state before command execution
|
|
41
|
+
const selection = state.doc.getSelection()
|
|
42
|
+
if (selection?.rangeCount) {
|
|
43
|
+
state.selection = selection
|
|
44
|
+
state.range = selection.getRangeAt(0).cloneRange()
|
|
45
|
+
state.rangeCount = selection.rangeCount
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
// Execute the command
|
|
45
49
|
try {
|
|
46
|
-
console.log('[useCommands] Executing command via executor')
|
|
47
50
|
executor.execute(command, value)
|
|
48
|
-
console.log('[useCommands] Command execution completed')
|
|
49
51
|
} catch (e) {
|
|
50
52
|
console.error('[useCommands] Error during command execution:', e)
|
|
51
53
|
}
|
|
52
|
-
|
|
53
|
-
// Update content state only if it has changed
|
|
54
|
-
const newContent = state.doc.body.innerHTML
|
|
55
|
-
if (newContent !== state.content) {
|
|
56
|
-
console.log('[useCommands] Content changed, updating state')
|
|
57
|
-
state.content = newContent
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Update selection state if needed
|
|
61
|
-
const selection = state.doc.getSelection()
|
|
62
|
-
if (selection?.rangeCount) {
|
|
63
|
-
const hasSelectionChanged = !state.selection
|
|
64
|
-
|| state.selection !== selection
|
|
65
|
-
|| state.rangeCount !== selection.rangeCount
|
|
66
|
-
|
|
67
|
-
if (hasSelectionChanged) {
|
|
68
|
-
console.log('[useCommands] Selection changed, updating state')
|
|
69
|
-
state.selection = selection
|
|
70
|
-
state.range = selection.getRangeAt(0).cloneRange()
|
|
71
|
-
state.rangeCount = selection.rangeCount
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
54
|
},
|
|
75
55
|
isActive: executor.isActive,
|
|
76
56
|
getValue: executor.getValue
|
|
@@ -70,10 +70,9 @@ export function useEditor() {
|
|
|
70
70
|
'h5',
|
|
71
71
|
'h6',
|
|
72
72
|
'blockquote',
|
|
73
|
-
'table',
|
|
74
73
|
'p',
|
|
75
|
-
'
|
|
76
|
-
'
|
|
74
|
+
'orderedList',
|
|
75
|
+
'unorderedList'
|
|
77
76
|
]
|
|
78
77
|
styleTypes.forEach((style) => {
|
|
79
78
|
if (state.doc && isStyleActive(style, state.doc)) {
|
|
@@ -209,44 +208,28 @@ export function useEditor() {
|
|
|
209
208
|
const isDirectChildOfBody = node.parentElement === doc.body
|
|
210
209
|
|
|
211
210
|
if (isEmpty || hasOnlyNbsp || (hasOnlyBr && !isDirectChildOfBody)) {
|
|
212
|
-
|
|
213
|
-
const p = doc.createElement('p')
|
|
214
|
-
p.innerHTML = '<br>'
|
|
215
|
-
node.parentNode?.replaceChild(p, node)
|
|
216
|
-
} else {
|
|
217
|
-
nodesToRemove.push(node)
|
|
218
|
-
}
|
|
211
|
+
nodesToRemove.push(node)
|
|
219
212
|
}
|
|
220
213
|
node = walker.nextNode() as Element
|
|
221
214
|
}
|
|
222
215
|
nodesToRemove.forEach((node) => { node.remove() })
|
|
223
216
|
},
|
|
224
217
|
normalizeContent: (doc: Document) => {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const textNodes: Text[] = []
|
|
233
|
-
let node: Node | null
|
|
234
|
-
while ((node = walker.nextNode())) {
|
|
235
|
-
if (node.parentElement === doc.body) {
|
|
236
|
-
textNodes.push(node as Text)
|
|
237
|
-
}
|
|
218
|
+
// Only normalize direct text nodes to paragraphs, don't force any structure
|
|
219
|
+
const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT)
|
|
220
|
+
const textNodes: Text[] = []
|
|
221
|
+
let node: Node | null
|
|
222
|
+
while ((node = walker.nextNode())) {
|
|
223
|
+
if (node.parentElement === doc.body && node.textContent?.trim()) {
|
|
224
|
+
textNodes.push(node as Text)
|
|
238
225
|
}
|
|
239
|
-
textNodes.forEach((textNode) => {
|
|
240
|
-
if (textNode.textContent?.trim()) {
|
|
241
|
-
const p = doc.createElement('p')
|
|
242
|
-
p.dir = doc.body.dir
|
|
243
|
-
p.appendChild(textNode.cloneNode())
|
|
244
|
-
doc.body.replaceChild(p, textNode)
|
|
245
|
-
} else {
|
|
246
|
-
doc.body.removeChild(textNode)
|
|
247
|
-
}
|
|
248
|
-
})
|
|
249
226
|
}
|
|
227
|
+
textNodes.forEach((textNode) => {
|
|
228
|
+
const p = doc.createElement('p')
|
|
229
|
+
p.dir = doc.body.dir
|
|
230
|
+
p.appendChild(textNode.cloneNode())
|
|
231
|
+
doc.body.replaceChild(p, textNode)
|
|
232
|
+
})
|
|
250
233
|
}
|
|
251
234
|
}
|
|
252
235
|
|
|
@@ -320,8 +303,9 @@ export function useEditor() {
|
|
|
320
303
|
// Store state reference in document for table operations
|
|
321
304
|
(doc as any).editorState = state
|
|
322
305
|
|
|
323
|
-
// Initial setup without triggering updates
|
|
324
|
-
if (state.content)
|
|
306
|
+
// Initial setup without triggering updates - only set content if it exists and is not just empty paragraphs
|
|
307
|
+
if (state.content && state.content.trim()
|
|
308
|
+
&& !state.content.match(/^<p(?:\s[^>]*)?>(?:<br\s*\/?>)?\s*<\/p>$/i)) {
|
|
325
309
|
const preserved = preserveIframes(state.content)
|
|
326
310
|
doc.body.innerHTML = preserved.html
|
|
327
311
|
setTimeout(() => {
|
|
@@ -333,17 +317,32 @@ export function useEditor() {
|
|
|
333
317
|
|
|
334
318
|
cleanup.normalizeContent(doc)
|
|
335
319
|
|
|
336
|
-
// Set initial selection
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
320
|
+
// Set initial selection only if there's content
|
|
321
|
+
if (doc.body.firstElementChild) {
|
|
322
|
+
const range = doc.createRange()
|
|
323
|
+
const selection = doc.getSelection()
|
|
324
|
+
if (selection) {
|
|
325
|
+
range.selectNodeContents(doc.body)
|
|
326
|
+
range.collapse(false)
|
|
327
|
+
selection.removeAllRanges()
|
|
328
|
+
selection.addRange(range)
|
|
329
|
+
state.range = range.cloneRange()
|
|
330
|
+
state.selection = selection
|
|
331
|
+
state.rangeCount = selection.rangeCount
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
// For empty editor, set cursor at the beginning of body
|
|
335
|
+
const selection = doc.getSelection()
|
|
336
|
+
if (selection) {
|
|
337
|
+
const range = doc.createRange()
|
|
338
|
+
range.setStart(doc.body, 0)
|
|
339
|
+
range.setEnd(doc.body, 0)
|
|
340
|
+
selection.removeAllRanges()
|
|
341
|
+
selection.addRange(range)
|
|
342
|
+
state.range = range.cloneRange()
|
|
343
|
+
state.selection = selection
|
|
344
|
+
state.rangeCount = selection.rangeCount
|
|
345
|
+
}
|
|
347
346
|
}
|
|
348
347
|
|
|
349
348
|
// Setup event listeners immediately
|
|
@@ -31,7 +31,7 @@ const shortcuts: KeyboardShortcut[] = [
|
|
|
31
31
|
export function useEditorKeyboard(doc: Document, executor: CommandExecutor): void {
|
|
32
32
|
// Handle keyboard shortcuts
|
|
33
33
|
doc.addEventListener('keydown', (e) => {
|
|
34
|
-
// Handle Enter key in lists
|
|
34
|
+
// Handle Enter key in lists and empty editor
|
|
35
35
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
36
36
|
const selection = doc.getSelection()
|
|
37
37
|
if (!selection || !selection.rangeCount) return
|
|
@@ -56,7 +56,7 @@ export function useEditorKeyboard(doc: Document, executor: CommandExecutor): voi
|
|
|
56
56
|
// If the list is now empty, remove it
|
|
57
57
|
if (!list.querySelector('li')) {
|
|
58
58
|
const p = doc.createElement('p')
|
|
59
|
-
p.innerHTML = '
|
|
59
|
+
p.innerHTML = ''
|
|
60
60
|
list.parentNode?.replaceChild(p, list)
|
|
61
61
|
|
|
62
62
|
// Set cursor in the new paragraph
|
|
@@ -69,7 +69,7 @@ export function useEditorKeyboard(doc: Document, executor: CommandExecutor): voi
|
|
|
69
69
|
// Create a new list item
|
|
70
70
|
e.preventDefault()
|
|
71
71
|
const newLi = doc.createElement('li')
|
|
72
|
-
newLi.innerHTML = '
|
|
72
|
+
newLi.innerHTML = '' // Start with empty list item
|
|
73
73
|
listItem.insertAdjacentElement('afterend', newLi)
|
|
74
74
|
|
|
75
75
|
// Move cursor to new list item
|
|
@@ -79,6 +79,55 @@ export function useEditorKeyboard(doc: Document, executor: CommandExecutor): voi
|
|
|
79
79
|
selection.addRange(range)
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
+
} else {
|
|
83
|
+
// Handle Enter in regular content - create new paragraph
|
|
84
|
+
const blockElement = (container.nodeType === 3 ? container.parentElement : container as Element)?.closest('p,h1,h2,h3,h4,h5,h6,blockquote,div')
|
|
85
|
+
|
|
86
|
+
if (blockElement && range.collapsed) {
|
|
87
|
+
// If we're at the end of a block element, create a new paragraph
|
|
88
|
+
if (isAtEndOfNode(blockElement, range)) {
|
|
89
|
+
e.preventDefault()
|
|
90
|
+
const newP = doc.createElement('p')
|
|
91
|
+
newP.innerHTML = ''
|
|
92
|
+
blockElement.insertAdjacentElement('afterend', newP)
|
|
93
|
+
|
|
94
|
+
// Move cursor to new paragraph
|
|
95
|
+
range.selectNodeContents(newP)
|
|
96
|
+
range.collapse(true)
|
|
97
|
+
selection.removeAllRanges()
|
|
98
|
+
selection.addRange(range)
|
|
99
|
+
}
|
|
100
|
+
} else if (!blockElement && doc.body.textContent?.trim()) {
|
|
101
|
+
// If we're typing directly in the body and press Enter, wrap in paragraphs
|
|
102
|
+
e.preventDefault()
|
|
103
|
+
|
|
104
|
+
// Split content at cursor position
|
|
105
|
+
const textContent = doc.body.textContent || ''
|
|
106
|
+
const cursorPos = range.startOffset
|
|
107
|
+
const beforeText = textContent.substring(0, cursorPos).trim()
|
|
108
|
+
const afterText = textContent.substring(cursorPos).trim()
|
|
109
|
+
|
|
110
|
+
// Clear body
|
|
111
|
+
doc.body.innerHTML = ''
|
|
112
|
+
|
|
113
|
+
// Create first paragraph with before text
|
|
114
|
+
if (beforeText) {
|
|
115
|
+
const p1 = doc.createElement('p')
|
|
116
|
+
p1.textContent = beforeText
|
|
117
|
+
doc.body.appendChild(p1)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Create second paragraph for after text
|
|
121
|
+
const p2 = doc.createElement('p')
|
|
122
|
+
p2.textContent = afterText
|
|
123
|
+
doc.body.appendChild(p2)
|
|
124
|
+
|
|
125
|
+
// Set cursor at beginning of second paragraph
|
|
126
|
+
range.selectNodeContents(p2)
|
|
127
|
+
range.collapse(true)
|
|
128
|
+
selection.removeAllRanges()
|
|
129
|
+
selection.addRange(range)
|
|
130
|
+
}
|
|
82
131
|
}
|
|
83
132
|
}
|
|
84
133
|
|
|
@@ -44,6 +44,107 @@ onUnmounted(() => {
|
|
|
44
44
|
editor.cleanup()
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
+
function setupAutoWrapping(doc: Document) {
|
|
48
|
+
// Handle typing in empty editor
|
|
49
|
+
doc.addEventListener('input', () => {
|
|
50
|
+
// If the editor is empty or only has whitespace, wrap content in a paragraph
|
|
51
|
+
if (!doc.body.firstElementChild
|
|
52
|
+
|| (!doc.body.textContent?.trim() && !doc.body.querySelector('p,h1,h2,h3,h4,h5,h6,blockquote,ul,ol,table'))) {
|
|
53
|
+
// If there's any text content, wrap it in a paragraph
|
|
54
|
+
if (doc.body.textContent?.trim()) {
|
|
55
|
+
const p = doc.createElement('p')
|
|
56
|
+
p.dir = doc.body.dir
|
|
57
|
+
|
|
58
|
+
// Move all content to the paragraph
|
|
59
|
+
while (doc.body.firstChild) {
|
|
60
|
+
p.appendChild(doc.body.firstChild)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
doc.body.appendChild(p)
|
|
64
|
+
|
|
65
|
+
// Set cursor at the end of the paragraph
|
|
66
|
+
const selection = doc.getSelection()
|
|
67
|
+
if (selection) {
|
|
68
|
+
const range = doc.createRange()
|
|
69
|
+
range.selectNodeContents(p)
|
|
70
|
+
range.collapse(false)
|
|
71
|
+
selection.removeAllRanges()
|
|
72
|
+
selection.addRange(range)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Update editor state
|
|
76
|
+
editor.state.content = doc.body.innerHTML
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// Handle backspace to prevent completely empty editor
|
|
82
|
+
doc.addEventListener('keydown', (e) => {
|
|
83
|
+
if (e.key === 'Backspace') {
|
|
84
|
+
// Check if this would make the editor completely empty
|
|
85
|
+
const selection = doc.getSelection()
|
|
86
|
+
if (!selection || !selection.rangeCount) return
|
|
87
|
+
|
|
88
|
+
const range = selection.getRangeAt(0)
|
|
89
|
+
|
|
90
|
+
// If we're about to delete the last block element
|
|
91
|
+
const blockElements = doc.body.querySelectorAll('p,h1,h2,h3,h4,h5,h6,blockquote,li,div')
|
|
92
|
+
if (blockElements.length <= 1) {
|
|
93
|
+
const lastBlock = blockElements[0]
|
|
94
|
+
|
|
95
|
+
// If we're in the last block and it's about to be empty
|
|
96
|
+
if (lastBlock && (
|
|
97
|
+
(lastBlock.textContent?.trim() === '' && range.collapsed)
|
|
98
|
+
|| (range.startContainer === lastBlock && range.endContainer === lastBlock
|
|
99
|
+
&& range.startOffset === 0 && range.endOffset === lastBlock.childNodes.length)
|
|
100
|
+
)) {
|
|
101
|
+
e.preventDefault()
|
|
102
|
+
|
|
103
|
+
// Clear the content but keep the block structure
|
|
104
|
+
lastBlock.innerHTML = ''
|
|
105
|
+
|
|
106
|
+
// Set cursor in the empty block
|
|
107
|
+
const newRange = doc.createRange()
|
|
108
|
+
newRange.selectNodeContents(lastBlock)
|
|
109
|
+
newRange.collapse(true)
|
|
110
|
+
selection.removeAllRanges()
|
|
111
|
+
selection.addRange(newRange)
|
|
112
|
+
|
|
113
|
+
// Update editor state
|
|
114
|
+
editor.state.content = doc.body.innerHTML
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Handle paste events to ensure content is properly wrapped
|
|
121
|
+
doc.addEventListener('paste', (e) => {
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
// After paste, ensure any unwrapped text gets wrapped in paragraphs
|
|
124
|
+
const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT)
|
|
125
|
+
const textNodes: Text[] = []
|
|
126
|
+
let node: Node | null
|
|
127
|
+
while ((node = walker.nextNode())) {
|
|
128
|
+
if (node.parentElement === doc.body && node.textContent?.trim()) {
|
|
129
|
+
textNodes.push(node as Text)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
textNodes.forEach((textNode) => {
|
|
134
|
+
const p = doc.createElement('p')
|
|
135
|
+
p.dir = doc.body.dir
|
|
136
|
+
p.appendChild(textNode.cloneNode())
|
|
137
|
+
doc.body.replaceChild(p, textNode)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// Update editor state if changes were made
|
|
141
|
+
if (textNodes.length > 0) {
|
|
142
|
+
editor.state.content = doc.body.innerHTML
|
|
143
|
+
}
|
|
144
|
+
}, 0)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
47
148
|
async function initEditor() {
|
|
48
149
|
if (isInitializing.value || !iframe.value || hasInitialized.value) {
|
|
49
150
|
return
|
|
@@ -103,37 +204,31 @@ async function initEditor() {
|
|
|
103
204
|
editor.init(doc)
|
|
104
205
|
useEditorKeyboard(doc, commands)
|
|
105
206
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
doc.body
|
|
112
|
-
|
|
113
|
-
editor.state.content = doc.body.innerHTML
|
|
114
|
-
} else {
|
|
115
|
-
// Convert any direct text nodes to paragraphs
|
|
116
|
-
const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT)
|
|
117
|
-
const textNodes: Text[] = []
|
|
118
|
-
let node: Node | null
|
|
119
|
-
while ((node = walker.nextNode())) {
|
|
120
|
-
if (node.parentElement === doc.body) {
|
|
121
|
-
textNodes.push(node as Text)
|
|
122
|
-
}
|
|
207
|
+
// Clean up any existing content and convert direct text nodes to paragraphs
|
|
208
|
+
const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT)
|
|
209
|
+
const textNodes: Text[] = []
|
|
210
|
+
let node: Node | null
|
|
211
|
+
while ((node = walker.nextNode())) {
|
|
212
|
+
if (node.parentElement === doc.body) {
|
|
213
|
+
textNodes.push(node as Text)
|
|
123
214
|
}
|
|
124
|
-
textNodes.forEach((textNode) => {
|
|
125
|
-
if (textNode.textContent?.trim()) {
|
|
126
|
-
const p = doc.createElement('p')
|
|
127
|
-
p.dir = doc.body.dir
|
|
128
|
-
p.appendChild(textNode.cloneNode())
|
|
129
|
-
doc.body.replaceChild(p, textNode)
|
|
130
|
-
} else {
|
|
131
|
-
doc.body.removeChild(textNode)
|
|
132
|
-
}
|
|
133
|
-
})
|
|
134
|
-
// Update state.content after cleanup
|
|
135
|
-
editor.state.content = doc.body.innerHTML
|
|
136
215
|
}
|
|
216
|
+
textNodes.forEach((textNode) => {
|
|
217
|
+
if (textNode.textContent?.trim()) {
|
|
218
|
+
const p = doc.createElement('p')
|
|
219
|
+
p.dir = doc.body.dir
|
|
220
|
+
p.appendChild(textNode.cloneNode())
|
|
221
|
+
doc.body.replaceChild(p, textNode)
|
|
222
|
+
} else {
|
|
223
|
+
doc.body.removeChild(textNode)
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
// Setup auto-wrapping for typed content
|
|
228
|
+
setupAutoWrapping(doc)
|
|
229
|
+
|
|
230
|
+
// Update state.content after cleanup
|
|
231
|
+
editor.state.content = doc.body.innerHTML
|
|
137
232
|
|
|
138
233
|
doc.body.focus()
|
|
139
234
|
hasInitialized.value = true
|
|
@@ -163,32 +258,39 @@ watch(() => editor.state.content, (newValue) => {
|
|
|
163
258
|
}
|
|
164
259
|
emit('update:modelValue', newValue)
|
|
165
260
|
})
|
|
261
|
+
|
|
262
|
+
// Expose for testing
|
|
263
|
+
defineExpose({
|
|
264
|
+
editor,
|
|
265
|
+
commands
|
|
266
|
+
})
|
|
166
267
|
</script>
|
|
167
268
|
|
|
168
269
|
<template>
|
|
169
270
|
<div class="bagel-input">
|
|
170
271
|
<label>{{ label }}</label>
|
|
171
|
-
|
|
272
|
+
|
|
273
|
+
<div
|
|
274
|
+
class="rich-text-editor rounded pt-05 px-05 pb-075"
|
|
275
|
+
:class="{ 'fullscreen-mode': editor.state.isFullscreen }"
|
|
276
|
+
>
|
|
172
277
|
<EditorToolbar
|
|
173
|
-
v-if="editor.state.hasInit" :config="toolbarConfig"
|
|
174
|
-
@action="commands.execute"
|
|
278
|
+
v-if="editor.state.hasInit" :config="toolbarConfig"
|
|
279
|
+
:selectedStyles="editor.state.selectedStyles" @action="commands.execute"
|
|
175
280
|
/>
|
|
176
281
|
<div class="editor-container" :class="{ 'split-view': editor.state.isSplitView }">
|
|
177
|
-
<div
|
|
282
|
+
<div
|
|
283
|
+
class="content-area radius-05"
|
|
284
|
+
:style="{ height: editor.state.isFullscreen ? 'calc(100vh - 4rem)' : editorHeight }"
|
|
285
|
+
>
|
|
178
286
|
<iframe
|
|
179
|
-
id="rich-text-iframe"
|
|
180
|
-
ref="iframe"
|
|
181
|
-
class="editableContent"
|
|
182
|
-
title="Editor"
|
|
183
|
-
srcdoc=""
|
|
287
|
+
id="rich-text-iframe" ref="iframe" class="editableContent" title="Editor" srcdoc=""
|
|
184
288
|
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"
|
|
185
289
|
@load="initEditor"
|
|
186
290
|
/>
|
|
187
291
|
</div>
|
|
188
292
|
<CodeEditor
|
|
189
|
-
v-if="editor.state.isSplitView"
|
|
190
|
-
v-model="editor.state.content"
|
|
191
|
-
language="html"
|
|
293
|
+
v-if="editor.state.isSplitView" v-model="editor.state.content" language="html"
|
|
192
294
|
:height="editor.state.isFullscreen ? 'calc(100vh - 4rem)' : editorHeight"
|
|
193
295
|
@update:modelValue="editor.updateState.content('html')"
|
|
194
296
|
/>
|
|
@@ -203,7 +305,10 @@ watch(() => editor.state.content, (newValue) => {
|
|
|
203
305
|
<Btn thin color="gray" icon="download" @click="debugMethods?.downloadSession">
|
|
204
306
|
Download Log
|
|
205
307
|
</Btn>
|
|
206
|
-
<Btn
|
|
308
|
+
<Btn
|
|
309
|
+
thin color="gray" icon="content_copy"
|
|
310
|
+
@click="copyText(debugMethods?.exportDebugWithPrompt() || '')"
|
|
311
|
+
>
|
|
207
312
|
Copy Log
|
|
208
313
|
</Btn>
|
|
209
314
|
</div>
|
|
@@ -214,7 +319,7 @@ watch(() => editor.state.content, (newValue) => {
|
|
|
214
319
|
<style>
|
|
215
320
|
.content-area p,
|
|
216
321
|
.content-area span,
|
|
217
|
-
.content-area li{
|
|
322
|
+
.content-area li {
|
|
218
323
|
line-height: 1.65;
|
|
219
324
|
}
|
|
220
325
|
</style>
|