@bagelink/vue 0.0.1262 → 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/DropDown.vue.d.ts +51 -48
- package/dist/components/DropDown.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 +8 -2
- package/dist/components/form/inputs/TelInput.vue.d.ts.map +1 -1
- package/dist/editor-7QC0nG_c.js +4 -0
- package/dist/editor-CpMNx6Eo.cjs +4 -0
- package/dist/index.cjs +1327 -756
- package/dist/index.mjs +1327 -756
- package/dist/style.css +90 -83
- package/package.json +1 -1
- package/src/components/DataTable/DataTable.vue +1 -1
- package/src/components/Dropdown.vue +5 -2
- 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
|
@@ -1,35 +1,47 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EditorState, EditorDebugInterface, EditorDebuggerInstance } from '../richTextTypes'
|
|
2
2
|
import { useModal } from '@bagelink/vue'
|
|
3
|
-
import { reactive
|
|
4
|
-
import {
|
|
3
|
+
import { reactive } from 'vue'
|
|
4
|
+
import { EditorDebugger } from '../utils/debug'
|
|
5
5
|
import { isStyleActive } from '../utils/selection'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
content
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
6
|
+
|
|
7
|
+
function preserveIframes(content: string): { html: string, iframes: HTMLIFrameElement[] } {
|
|
8
|
+
const temp = document.createElement('div')
|
|
9
|
+
temp.innerHTML = content
|
|
10
|
+
const iframes: HTMLIFrameElement[] = []
|
|
11
|
+
const placeholders: string[] = []
|
|
12
|
+
|
|
13
|
+
// Find all iframes and replace them with placeholders
|
|
14
|
+
temp.querySelectorAll('iframe').forEach((iframe, index) => {
|
|
15
|
+
const placeholder = `<!--iframe-${index}-->`
|
|
16
|
+
iframes.push(iframe.cloneNode(true) as HTMLIFrameElement)
|
|
17
|
+
placeholders.push(placeholder)
|
|
18
|
+
iframe.replaceWith(placeholder)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
html: temp.innerHTML,
|
|
23
|
+
iframes
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function restoreIframes(doc: Document, content: string, iframes: HTMLIFrameElement[]) {
|
|
28
|
+
// Find all iframe placeholders and restore them
|
|
29
|
+
const placeholderPattern = /<!--iframe-(\d+)-->/g
|
|
30
|
+
doc.body.innerHTML = content.replace(placeholderPattern, (_, index) => {
|
|
31
|
+
const iframe = iframes[Number(index)]
|
|
32
|
+
return iframe ? iframe.outerHTML : ''
|
|
33
|
+
})
|
|
22
34
|
}
|
|
23
35
|
|
|
24
36
|
export function useEditor() {
|
|
25
|
-
const editorDebugger = ref<EditorDebugger>()
|
|
26
37
|
const modal = useModal()
|
|
38
|
+
let cleanupListeners: (() => void) | null = null
|
|
27
39
|
|
|
28
40
|
const state = reactive<EditorState>({
|
|
29
41
|
content: '',
|
|
30
42
|
doc: undefined,
|
|
31
43
|
selection: null,
|
|
32
|
-
selectedStyles: new Set(),
|
|
44
|
+
selectedStyles: new Set<string>(),
|
|
33
45
|
isFullscreen: false,
|
|
34
46
|
isSplitView: false,
|
|
35
47
|
isCodeView: false,
|
|
@@ -38,122 +50,181 @@ export function useEditor() {
|
|
|
38
50
|
redoStack: [],
|
|
39
51
|
rangeCount: 0,
|
|
40
52
|
range: null,
|
|
41
|
-
modal
|
|
53
|
+
modal,
|
|
54
|
+
debug: undefined
|
|
42
55
|
})
|
|
43
56
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
'
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
// Centralized state update functions
|
|
58
|
+
const updateState = {
|
|
59
|
+
styles: () => {
|
|
60
|
+
if (!state.doc) return
|
|
61
|
+
console.log('[updateState.styles] Starting style update')
|
|
62
|
+
const styles = new Set<string>()
|
|
63
|
+
const styleTypes = [
|
|
64
|
+
'bold',
|
|
65
|
+
'italic',
|
|
66
|
+
'underline',
|
|
67
|
+
'h1',
|
|
68
|
+
'h2',
|
|
69
|
+
'h3',
|
|
70
|
+
'h4',
|
|
71
|
+
'h5',
|
|
72
|
+
'h6',
|
|
73
|
+
'blockquote',
|
|
74
|
+
'table',
|
|
75
|
+
'p',
|
|
76
|
+
'ol',
|
|
77
|
+
'li'
|
|
78
|
+
]
|
|
79
|
+
styleTypes.forEach((style) => {
|
|
80
|
+
if (state.doc && isStyleActive(style, state.doc)) {
|
|
81
|
+
styles.add(style)
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
console.log('[updateState.styles] New styles:', Array.from(styles))
|
|
85
|
+
state.selectedStyles = styles
|
|
86
|
+
},
|
|
87
|
+
content: (source: 'html' | 'text') => {
|
|
88
|
+
if (!state.doc) return
|
|
89
|
+
console.log('[updateState.content] Starting content update, source:', source)
|
|
90
|
+
|
|
91
|
+
// Only push to undo stack if content has changed
|
|
92
|
+
const currentContent = state.doc.body.innerHTML
|
|
93
|
+
console.log('[updateState.content] Current content length:', currentContent.length)
|
|
94
|
+
console.log('[updateState.content] State content length:', state.content.length)
|
|
95
|
+
if (currentContent !== state.content) {
|
|
96
|
+
console.log('[updateState.content] Content changed, pushing to undo stack')
|
|
97
|
+
state.undoStack.push(state.content)
|
|
98
|
+
state.redoStack = []
|
|
67
99
|
}
|
|
68
|
-
})
|
|
69
|
-
state.selectedStyles = styles
|
|
70
|
-
}
|
|
71
100
|
|
|
72
|
-
|
|
73
|
-
|
|
101
|
+
// Store current selection
|
|
102
|
+
const selection = state.doc.getSelection()
|
|
103
|
+
const range = selection?.rangeCount ? selection.getRangeAt(0).cloneRange() : null
|
|
104
|
+
console.log('[updateState.content] Has selection:', !!selection, 'Has range:', !!range)
|
|
105
|
+
|
|
106
|
+
if (source === 'html') {
|
|
107
|
+
console.log('[updateState.content] Processing HTML content')
|
|
108
|
+
// Preserve iframes before setting content
|
|
109
|
+
const preserved = preserveIframes(state.content)
|
|
110
|
+
console.log('[updateState.content] Preserved iframes count:', preserved.iframes.length)
|
|
111
|
+
state.doc.body.innerHTML = preserved.html
|
|
112
|
+
|
|
113
|
+
// Restore iframes after a short delay to ensure the document is ready
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
console.log('[updateState.content] Restoring iframes')
|
|
116
|
+
if (state.doc) {
|
|
117
|
+
restoreIframes(state.doc, state.content, preserved.iframes)
|
|
118
|
+
|
|
119
|
+
// Restore selection if it existed
|
|
120
|
+
if (range && selection) {
|
|
121
|
+
try {
|
|
122
|
+
selection.removeAllRanges()
|
|
123
|
+
selection.addRange(range)
|
|
124
|
+
console.log('[updateState.content] Selection restored')
|
|
125
|
+
} catch (e) {
|
|
126
|
+
console.warn('[updateState.content] Could not restore selection:', e)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}, 0)
|
|
131
|
+
} else {
|
|
132
|
+
console.log('[updateState.content] Setting text content')
|
|
133
|
+
state.doc.body.textContent = state.content
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
selection: () => {
|
|
137
|
+
if (!state.doc) return
|
|
138
|
+
console.log('[updateState.selection] Starting selection update')
|
|
139
|
+
const newSelection = state.doc.getSelection()
|
|
140
|
+
if (!newSelection) {
|
|
141
|
+
console.log('[updateState.selection] No selection available')
|
|
142
|
+
return
|
|
143
|
+
}
|
|
74
144
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
145
|
+
try {
|
|
146
|
+
if (!state.doc.body.contains(newSelection.anchorNode)) {
|
|
147
|
+
console.log('[updateState.selection] Selection outside editor body, refocusing')
|
|
148
|
+
state.doc.body.focus()
|
|
149
|
+
return
|
|
150
|
+
}
|
|
79
151
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
152
|
+
// Only update if selection has actually changed
|
|
153
|
+
const hasSelectionChanged = !state.selection
|
|
154
|
+
|| state.selection !== newSelection
|
|
155
|
+
|| state.rangeCount !== newSelection.rangeCount
|
|
156
|
+
|| (newSelection.rangeCount > 0 && state.range && (
|
|
157
|
+
state.range.startContainer !== newSelection.getRangeAt(0).startContainer
|
|
158
|
+
|| state.range.startOffset !== newSelection.getRangeAt(0).startOffset
|
|
159
|
+
|| state.range.endContainer !== newSelection.getRangeAt(0).endContainer
|
|
160
|
+
|| state.range.endOffset !== newSelection.getRangeAt(0).endOffset
|
|
161
|
+
))
|
|
162
|
+
|
|
163
|
+
console.log('[updateState.selection] Selection changed:', hasSelectionChanged)
|
|
164
|
+
if (hasSelectionChanged) {
|
|
165
|
+
state.selection = newSelection
|
|
166
|
+
state.rangeCount = newSelection.rangeCount
|
|
167
|
+
|
|
168
|
+
if (newSelection.rangeCount > 0) {
|
|
169
|
+
state.range = newSelection.getRangeAt(0).cloneRange()
|
|
170
|
+
console.log('[updateState.selection] New range:', {
|
|
171
|
+
startOffset: state.range.startOffset,
|
|
172
|
+
endOffset: state.range.endOffset,
|
|
173
|
+
collapsed: state.range.collapsed
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Update styles less frequently
|
|
178
|
+
requestAnimationFrame(() => {
|
|
179
|
+
console.log('[updateState.selection] Updating styles in RAF')
|
|
180
|
+
updateState.styles()
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
} catch (e) {
|
|
184
|
+
console.warn('[updateState.selection] Selection error:', e)
|
|
185
|
+
state.selection = null
|
|
186
|
+
state.range = null
|
|
187
|
+
state.rangeCount = 0
|
|
188
|
+
state.selectedStyles = new Set()
|
|
189
|
+
}
|
|
84
190
|
}
|
|
85
|
-
updateActiveStyles()
|
|
86
191
|
}
|
|
87
192
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (
|
|
95
|
-
state.
|
|
96
|
-
|
|
193
|
+
// History management
|
|
194
|
+
const history = {
|
|
195
|
+
undo: () => {
|
|
196
|
+
if (state.undoStack.length === 0) return
|
|
197
|
+
state.redoStack.push(state.content)
|
|
198
|
+
const lastContent = state.undoStack.pop()
|
|
199
|
+
if (lastContent !== undefined) {
|
|
200
|
+
state.content = lastContent
|
|
201
|
+
updateState.content('html')
|
|
97
202
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
state.
|
|
203
|
+
},
|
|
204
|
+
redo: () => {
|
|
205
|
+
if (state.redoStack.length === 0) return
|
|
206
|
+
state.undoStack.push(state.content)
|
|
207
|
+
const nextContent = state.redoStack.pop()
|
|
208
|
+
if (nextContent !== undefined) {
|
|
209
|
+
state.content = nextContent
|
|
210
|
+
updateState.content('html')
|
|
105
211
|
}
|
|
106
|
-
state.range = state.selection.getRangeAt(0).cloneRange()
|
|
107
|
-
updateActiveStyles()
|
|
108
|
-
} catch (e) {
|
|
109
|
-
console.warn('Selection error:', e)
|
|
110
|
-
state.selection = null
|
|
111
|
-
state.range = null
|
|
112
|
-
state.rangeCount = 0
|
|
113
|
-
state.selectedStyles = new Set()
|
|
114
212
|
}
|
|
115
213
|
}
|
|
116
214
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
doc
|
|
120
|
-
|
|
121
|
-
updateActiveStyles()
|
|
122
|
-
})
|
|
123
|
-
|
|
124
|
-
doc.addEventListener('selectionchange', () => {
|
|
125
|
-
updateSelection()
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
doc.addEventListener('mouseup', () => {
|
|
129
|
-
updateSelection()
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
doc.addEventListener('keyup', (e) => {
|
|
133
|
-
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
|
134
|
-
updateSelection()
|
|
135
|
-
}
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
// Clean empty tags and normalize content
|
|
139
|
-
const cleanEmptyTags = () => {
|
|
140
|
-
const walker = doc.createTreeWalker(
|
|
141
|
-
doc.body,
|
|
142
|
-
NodeFilter.SHOW_ELEMENT,
|
|
143
|
-
null
|
|
144
|
-
)
|
|
145
|
-
|
|
215
|
+
// Content cleanup utilities
|
|
216
|
+
const cleanup = {
|
|
217
|
+
emptyTags: (doc: Document) => {
|
|
218
|
+
const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT, null)
|
|
146
219
|
const nodesToRemove: Element[] = []
|
|
147
220
|
let node = walker.nextNode() as Element
|
|
148
221
|
|
|
149
222
|
while (node) {
|
|
150
|
-
// Skip certain elements
|
|
151
223
|
if (['br', 'img', 'hr', 'input'].includes(node.tagName.toLowerCase())) {
|
|
152
224
|
node = walker.nextNode() as Element
|
|
153
225
|
continue
|
|
154
226
|
}
|
|
155
227
|
|
|
156
|
-
// Get text content without extra spaces
|
|
157
228
|
const textContent = node.textContent?.trim() || ''
|
|
158
229
|
const innerHTML = node.innerHTML.trim()
|
|
159
230
|
const hasOnlyBr = innerHTML === '<br>' || innerHTML === '<br/>'
|
|
@@ -161,195 +232,231 @@ export function useEditor() {
|
|
|
161
232
|
const isEmpty = !textContent && !innerHTML
|
|
162
233
|
const isDirectChildOfBody = node.parentElement === doc.body
|
|
163
234
|
|
|
164
|
-
// Handle empty or unnecessary tags
|
|
165
235
|
if (isEmpty || hasOnlyNbsp || (hasOnlyBr && !isDirectChildOfBody)) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
p.innerHTML = '<br>'
|
|
171
|
-
node.parentNode?.replaceChild(p, node)
|
|
172
|
-
}
|
|
236
|
+
if (isDirectChildOfBody && !node.matches('p')) {
|
|
237
|
+
const p = doc.createElement('p')
|
|
238
|
+
p.innerHTML = '<br>'
|
|
239
|
+
node.parentNode?.replaceChild(p, node)
|
|
173
240
|
} else {
|
|
174
241
|
nodesToRemove.push(node)
|
|
175
242
|
}
|
|
176
243
|
}
|
|
177
|
-
|
|
178
244
|
node = walker.nextNode() as Element
|
|
179
245
|
}
|
|
180
|
-
|
|
181
|
-
// Remove all marked nodes
|
|
182
246
|
nodesToRemove.forEach((node) => { node.remove() })
|
|
247
|
+
},
|
|
248
|
+
normalizeContent: (doc: Document) => {
|
|
249
|
+
if (!doc.body.firstElementChild) {
|
|
250
|
+
const p = doc.createElement('p')
|
|
251
|
+
p.dir = doc.body.dir
|
|
252
|
+
p.innerHTML = '<br>'
|
|
253
|
+
doc.body.appendChild(p)
|
|
254
|
+
} else {
|
|
255
|
+
const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT)
|
|
256
|
+
const textNodes: Text[] = []
|
|
257
|
+
let node: Node | null
|
|
258
|
+
while ((node = walker.nextNode())) {
|
|
259
|
+
if (node.parentElement === doc.body) {
|
|
260
|
+
textNodes.push(node as Text)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
textNodes.forEach((textNode) => {
|
|
264
|
+
if (textNode.textContent?.trim()) {
|
|
265
|
+
const p = doc.createElement('p')
|
|
266
|
+
p.dir = doc.body.dir
|
|
267
|
+
p.appendChild(textNode.cloneNode())
|
|
268
|
+
doc.body.replaceChild(p, textNode)
|
|
269
|
+
} else {
|
|
270
|
+
doc.body.removeChild(textNode)
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
}
|
|
183
274
|
}
|
|
275
|
+
}
|
|
184
276
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
277
|
+
function setupEventListeners(doc: Document) {
|
|
278
|
+
console.log('[setupEventListeners] Starting setup')
|
|
279
|
+
// Clean up existing listeners if they exist
|
|
280
|
+
if (cleanupListeners) {
|
|
281
|
+
console.log('[setupEventListeners] Cleaning up existing listeners')
|
|
282
|
+
cleanupListeners()
|
|
283
|
+
cleanupListeners = null
|
|
284
|
+
}
|
|
189
285
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
286
|
+
let isUpdating = false
|
|
287
|
+
let contentUpdateTimeout: number | null = null
|
|
288
|
+
let selectionUpdateTimeout: number | null = null
|
|
289
|
+
let updateCount = 0
|
|
290
|
+
|
|
291
|
+
const events = {
|
|
292
|
+
input: () => {
|
|
293
|
+
updateCount++
|
|
294
|
+
console.log(`[input event #${updateCount}] Starting, isUpdating:`, isUpdating)
|
|
295
|
+
if (isUpdating) {
|
|
296
|
+
console.log(`[input event #${updateCount}] Skipped - already updating`)
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
isUpdating = true
|
|
196
300
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
301
|
+
// Clear any pending content updates
|
|
302
|
+
if (contentUpdateTimeout) {
|
|
303
|
+
console.log(`[input event #${updateCount}] Clearing previous timeout`)
|
|
304
|
+
window.clearTimeout(contentUpdateTimeout)
|
|
305
|
+
}
|
|
203
306
|
|
|
204
|
-
|
|
205
|
-
|
|
307
|
+
contentUpdateTimeout = window.setTimeout(() => {
|
|
308
|
+
console.log(`[input event #${updateCount}] Timeout fired`)
|
|
309
|
+
const newContent = doc.body.innerHTML
|
|
310
|
+
if (newContent !== state.content) {
|
|
311
|
+
console.log(`[input event #${updateCount}] Content changed, updating state`)
|
|
312
|
+
state.content = newContent
|
|
313
|
+
} else {
|
|
314
|
+
console.log(`[input event #${updateCount}] Content unchanged`)
|
|
315
|
+
}
|
|
316
|
+
isUpdating = false
|
|
317
|
+
}, 100)
|
|
318
|
+
},
|
|
319
|
+
selectionchange: () => {
|
|
320
|
+
updateCount++
|
|
321
|
+
console.log(`[selectionchange #${updateCount}] Starting, isUpdating:`, isUpdating)
|
|
322
|
+
if (isUpdating) {
|
|
323
|
+
console.log(`[selectionchange #${updateCount}] Skipped - already updating`)
|
|
324
|
+
return
|
|
325
|
+
}
|
|
206
326
|
|
|
207
|
-
|
|
208
|
-
|
|
327
|
+
if (selectionUpdateTimeout) {
|
|
328
|
+
console.log(`[selectionchange #${updateCount}] Clearing previous timeout`)
|
|
329
|
+
window.clearTimeout(selectionUpdateTimeout)
|
|
330
|
+
}
|
|
209
331
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
332
|
+
selectionUpdateTimeout = window.setTimeout(() => {
|
|
333
|
+
console.log(`[selectionchange #${updateCount}] Timeout fired`)
|
|
334
|
+
if (!isUpdating) {
|
|
335
|
+
updateState.selection()
|
|
336
|
+
} else {
|
|
337
|
+
console.log(`[selectionchange #${updateCount}] Skipped - still updating`)
|
|
338
|
+
}
|
|
339
|
+
}, 150)
|
|
340
|
+
},
|
|
341
|
+
mouseup: () => {
|
|
342
|
+
updateCount++
|
|
343
|
+
console.log(`[mouseup #${updateCount}] Starting, isUpdating:`, isUpdating)
|
|
344
|
+
if (isUpdating) return
|
|
345
|
+
updateState.selection()
|
|
346
|
+
},
|
|
347
|
+
keyup: (e: KeyboardEvent) => {
|
|
348
|
+
updateCount++
|
|
349
|
+
console.log(`[keyup #${updateCount}] Key:`, e.key, 'isUpdating:', isUpdating)
|
|
350
|
+
if (isUpdating) return
|
|
351
|
+
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
|
352
|
+
updateState.selection()
|
|
353
|
+
}
|
|
354
|
+
}
|
|
215
355
|
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function handleRedo() {
|
|
219
|
-
if (state.redoStack.length === 0) return
|
|
220
356
|
|
|
221
|
-
//
|
|
222
|
-
|
|
357
|
+
// Only add listeners if they haven't been added yet
|
|
358
|
+
Object.entries(events).forEach(([event, handler]) => {
|
|
359
|
+
doc.addEventListener(event, handler as EventListener)
|
|
360
|
+
console.log('[setupEventListeners] Added listener for:', event)
|
|
361
|
+
})
|
|
223
362
|
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
363
|
+
// Store cleanup function
|
|
364
|
+
cleanupListeners = () => {
|
|
365
|
+
console.log('[setupEventListeners] Cleaning up event listeners')
|
|
366
|
+
if (contentUpdateTimeout) window.clearTimeout(contentUpdateTimeout)
|
|
367
|
+
if (selectionUpdateTimeout) window.clearTimeout(selectionUpdateTimeout)
|
|
368
|
+
Object.entries(events).forEach(([event, handler]) => {
|
|
369
|
+
doc.removeEventListener(event, handler as EventListener)
|
|
370
|
+
})
|
|
229
371
|
}
|
|
230
|
-
}
|
|
231
372
|
|
|
232
|
-
|
|
233
|
-
|
|
373
|
+
return cleanupListeners
|
|
374
|
+
}
|
|
234
375
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (action === 'fullScreen') {
|
|
244
|
-
state.isFullscreen = !state.isFullscreen
|
|
245
|
-
return
|
|
376
|
+
function init(doc: Document) {
|
|
377
|
+
console.log('[init] Starting initialization')
|
|
378
|
+
if (state.hasInit) {
|
|
379
|
+
console.log('[init] Already initialized, cleaning up first')
|
|
380
|
+
if (cleanupListeners) {
|
|
381
|
+
cleanupListeners()
|
|
382
|
+
cleanupListeners = null
|
|
383
|
+
}
|
|
246
384
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
385
|
+
|
|
386
|
+
state.doc = doc
|
|
387
|
+
state.hasInit = true
|
|
388
|
+
|
|
389
|
+
// Initial setup without triggering updates
|
|
390
|
+
if (state.content) {
|
|
391
|
+
const preserved = preserveIframes(state.content)
|
|
392
|
+
doc.body.innerHTML = preserved.html
|
|
393
|
+
setTimeout(() => {
|
|
394
|
+
if (state.doc) {
|
|
395
|
+
restoreIframes(doc, state.content, preserved.iframes)
|
|
396
|
+
}
|
|
397
|
+
}, 0)
|
|
250
398
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
399
|
+
|
|
400
|
+
cleanup.normalizeContent(doc)
|
|
401
|
+
|
|
402
|
+
// Set initial selection at the end
|
|
403
|
+
const range = doc.createRange()
|
|
404
|
+
const selection = doc.getSelection()
|
|
405
|
+
if (selection) {
|
|
406
|
+
range.selectNodeContents(doc.body)
|
|
407
|
+
range.collapse(false)
|
|
408
|
+
selection.removeAllRanges()
|
|
409
|
+
selection.addRange(range)
|
|
410
|
+
state.range = range.cloneRange()
|
|
411
|
+
state.selection = selection
|
|
412
|
+
state.rangeCount = selection.rangeCount
|
|
254
413
|
}
|
|
255
414
|
|
|
256
|
-
//
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
case 'h3':
|
|
277
|
-
case 'h4':
|
|
278
|
-
case 'h5':
|
|
279
|
-
case 'h6':
|
|
280
|
-
format.block(action, action)
|
|
281
|
-
break
|
|
282
|
-
case 'insertTable': {
|
|
283
|
-
const [rows, cols] = value?.split('x').map(Number) || [3, 3]
|
|
284
|
-
insertTable(rows, cols, state)
|
|
285
|
-
break
|
|
415
|
+
// Setup event listeners immediately
|
|
416
|
+
cleanupListeners = setupEventListeners(doc)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function initDebugger() {
|
|
420
|
+
if (!state.debug) {
|
|
421
|
+
const debugInstance: EditorDebuggerInstance = new EditorDebugger()
|
|
422
|
+
const debug: EditorDebugInterface = {
|
|
423
|
+
debugger: debugInstance,
|
|
424
|
+
logCommand: (command: string, value?: string) => {
|
|
425
|
+
debugInstance.logCommand(command, value, state)
|
|
426
|
+
},
|
|
427
|
+
getSession: () => debugInstance.getSession(),
|
|
428
|
+
clearSession: () => {
|
|
429
|
+
debugInstance.clearSession()
|
|
430
|
+
},
|
|
431
|
+
downloadSession: () => {
|
|
432
|
+
debugInstance.downloadSession()
|
|
433
|
+
},
|
|
434
|
+
exportDebugWithPrompt: (message?: string) => debugInstance.exportSessionWithPrompt(message)
|
|
286
435
|
}
|
|
287
|
-
|
|
288
|
-
if (state.range) deleteTable(state.range)
|
|
289
|
-
break
|
|
290
|
-
case 'mergeCells':
|
|
291
|
-
if (state.range && state.doc) mergeCells(state.range, state.doc)
|
|
292
|
-
break
|
|
293
|
-
case 'splitCells':
|
|
294
|
-
if (state.range && state.doc) splitCell(state.range, state.doc)
|
|
295
|
-
break
|
|
296
|
-
case 'addRowBefore':
|
|
297
|
-
case 'addRowAfter':
|
|
298
|
-
if (state.range && state.doc) {
|
|
299
|
-
addRow(action === 'addRowBefore' ? 'before' : 'after', state.range, state.doc)
|
|
300
|
-
}
|
|
301
|
-
break
|
|
302
|
-
case 'deleteRow':
|
|
303
|
-
if (state.range) deleteRow(state.range)
|
|
304
|
-
break
|
|
305
|
-
case 'insertColumnLeft':
|
|
306
|
-
case 'insertColumnRight':
|
|
307
|
-
if (state.range) {
|
|
308
|
-
insertColumn(action === 'insertColumnLeft' ? 'before' : 'after', state.range)
|
|
309
|
-
}
|
|
310
|
-
break
|
|
311
|
-
case 'deleteColumn':
|
|
312
|
-
if (state.range) deleteColumn(state.range)
|
|
313
|
-
break
|
|
314
|
-
case 'alignLeft':
|
|
315
|
-
case 'alignCenter':
|
|
316
|
-
case 'alignRight':
|
|
317
|
-
case 'alignJustify':
|
|
318
|
-
if (state.range) {
|
|
319
|
-
alignColumn(state.range, action.replace('align', '').toLowerCase() as 'left' | 'center' | 'right' | 'justify')
|
|
320
|
-
}
|
|
321
|
-
break
|
|
322
|
-
case 'clear':
|
|
323
|
-
format.clear()
|
|
324
|
-
break
|
|
325
|
-
default:
|
|
326
|
-
format.text(action)
|
|
436
|
+
state.debug = debug
|
|
327
437
|
}
|
|
328
|
-
|
|
329
|
-
updateContent('html')
|
|
330
438
|
}
|
|
331
439
|
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
440
|
+
// Add cleanup on component unmount
|
|
441
|
+
if (typeof window !== 'undefined') {
|
|
442
|
+
window.addEventListener('beforeunload', () => {
|
|
443
|
+
if (cleanupListeners) {
|
|
444
|
+
cleanupListeners()
|
|
445
|
+
}
|
|
446
|
+
})
|
|
447
|
+
}
|
|
338
448
|
|
|
339
449
|
return {
|
|
340
450
|
state,
|
|
341
451
|
init,
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
downloadSession: downloadDebugSession,
|
|
351
|
-
logCommand,
|
|
352
|
-
exportDebugWithPrompt
|
|
452
|
+
updateState,
|
|
453
|
+
history,
|
|
454
|
+
initDebugger,
|
|
455
|
+
cleanup: () => {
|
|
456
|
+
if (cleanupListeners) {
|
|
457
|
+
cleanupListeners()
|
|
458
|
+
cleanupListeners = null
|
|
459
|
+
}
|
|
353
460
|
}
|
|
354
461
|
}
|
|
355
462
|
}
|