@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.
Files changed (48) hide show
  1. package/dist/components/AddressSearch.vue.d.ts +6 -0
  2. package/dist/components/AddressSearch.vue.d.ts.map +1 -1
  3. package/dist/components/DropDown.vue.d.ts +51 -48
  4. package/dist/components/DropDown.vue.d.ts.map +1 -1
  5. package/dist/components/form/FieldArray.vue.d.ts.map +1 -1
  6. package/dist/components/form/inputs/DateInput.vue.d.ts +4 -1
  7. package/dist/components/form/inputs/DateInput.vue.d.ts.map +1 -1
  8. package/dist/components/form/inputs/PasswordInput.vue.d.ts.map +1 -1
  9. package/dist/components/form/inputs/RadioGroup.vue.d.ts +1 -1
  10. package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
  11. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +31 -23
  12. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
  13. package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts +2 -1
  14. package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
  15. package/dist/components/form/inputs/RichText/config.d.ts +2 -1
  16. package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
  17. package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
  18. package/dist/components/form/inputs/RichText/utils/commands.d.ts +1 -0
  19. package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
  20. package/dist/components/form/inputs/RichText/utils/media.d.ts +5 -3
  21. package/dist/components/form/inputs/RichText/utils/media.d.ts.map +1 -1
  22. package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
  23. package/dist/components/form/inputs/SelectInput.vue.d.ts +12 -0
  24. package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
  25. package/dist/components/form/inputs/TelInput.vue.d.ts +8 -2
  26. package/dist/components/form/inputs/TelInput.vue.d.ts.map +1 -1
  27. package/dist/editor-7QC0nG_c.js +4 -0
  28. package/dist/editor-CpMNx6Eo.cjs +4 -0
  29. package/dist/index.cjs +1327 -756
  30. package/dist/index.mjs +1327 -756
  31. package/dist/style.css +90 -83
  32. package/package.json +1 -1
  33. package/src/components/DataTable/DataTable.vue +1 -1
  34. package/src/components/Dropdown.vue +5 -2
  35. package/src/components/form/FieldArray.vue +3 -0
  36. package/src/components/form/inputs/DateInput.vue +341 -162
  37. package/src/components/form/inputs/PasswordInput.vue +5 -1
  38. package/src/components/form/inputs/RichText/components/EditorToolbar.vue +2 -2
  39. package/src/components/form/inputs/RichText/composables/useCommands.ts +53 -97
  40. package/src/components/form/inputs/RichText/composables/useEditor.ts +377 -270
  41. package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +124 -58
  42. package/src/components/form/inputs/RichText/config.ts +27 -3
  43. package/src/components/form/inputs/RichText/editor.css +29 -0
  44. package/src/components/form/inputs/RichText/index.vue +129 -55
  45. package/src/components/form/inputs/RichText/richTextTypes.d.ts +35 -49
  46. package/src/components/form/inputs/RichText/utils/commands.ts +181 -0
  47. package/src/components/form/inputs/RichText/utils/media.ts +64 -3
  48. package/src/components/form/inputs/RichText/utils/selection.ts +40 -5
@@ -1,35 +1,47 @@
1
- import type { EditorDebugger } from '../utils/debug'
1
+ import type { EditorState, EditorDebugInterface, EditorDebuggerInstance } from '../richTextTypes'
2
2
  import { useModal } from '@bagelink/vue'
3
- import { reactive, ref } from 'vue'
4
- import { formatting } from '../utils/formatting'
3
+ import { reactive } from 'vue'
4
+ import { EditorDebugger } from '../utils/debug'
5
5
  import { isStyleActive } from '../utils/selection'
6
- import { addRow, deleteRow, mergeCells, splitCell, insertTable, deleteTable, insertColumn, deleteColumn, alignColumn } from '../utils/table'
7
-
8
- interface EditorState {
9
- content: string
10
- doc: Document | undefined
11
- selection: Selection | null
12
- selectedStyles: Set<string>
13
- isFullscreen: boolean
14
- isSplitView: boolean
15
- isCodeView: boolean
16
- hasInit: boolean
17
- undoStack: string[]
18
- redoStack: string[]
19
- rangeCount: number
20
- range: Range | null
21
- modal: ReturnType<typeof useModal>
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
- function updateActiveStyles() {
45
- if (!state.doc) return
46
- const styles = new Set<string>()
47
- const styleTypes = [
48
- 'bold',
49
- 'italic',
50
- 'underline',
51
- 'h1',
52
- 'h2',
53
- 'h3',
54
- 'h4',
55
- 'h5',
56
- 'h6',
57
- 'blockquote',
58
- 'table',
59
- 'p',
60
- 'ol',
61
- 'li'
62
- ]
63
-
64
- styleTypes.forEach((style) => {
65
- if (state.doc && isStyleActive(style, state.doc)) {
66
- styles.add(style)
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
- function updateContent(source: 'html' | 'text') {
73
- if (!state.doc) return
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
- // Save current state for undo
76
- state.undoStack.push(state.content)
77
- // Clear redo stack on new content
78
- state.redoStack = []
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
- if (source === 'html') {
81
- state.doc.body.innerHTML = state.content
82
- } else {
83
- state.doc.body.textContent = state.content
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
- function updateSelection() {
89
- if (!state.doc) return
90
- state.selection = state.doc.getSelection()
91
- if (!state.selection) return
92
-
93
- try {
94
- if (!state.doc.body.contains(state.selection.anchorNode)) {
95
- state.doc.body.focus()
96
- return
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
- state.rangeCount = state.selection.rangeCount
99
- if (!state.rangeCount) {
100
- const range = state.doc.createRange()
101
- range.selectNodeContents(state.doc.body)
102
- range.collapse(false)
103
- state.selection.removeAllRanges()
104
- state.selection.addRange(range)
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
- function setupEventListeners(doc: Document) {
118
- // Input and selection events
119
- doc.addEventListener('input', () => {
120
- state.content = doc.body.innerHTML
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
- // If it's a direct child of body, replace with a proper paragraph
167
- if (isDirectChildOfBody) {
168
- if (!node.matches('p')) {
169
- const p = doc.createElement('p')
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
- // Regular cleanup
186
- const observer = new MutationObserver(() => {
187
- cleanEmptyTags()
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
- observer.observe(doc.body, {
191
- childList: true,
192
- subtree: true,
193
- characterData: true
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
- function init(doc: Document) {
198
- state.doc = doc
199
- state.hasInit = true
200
- setupEventListeners(doc)
201
- updateContent('html')
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
- function handleUndo() {
205
- if (state.undoStack.length === 0) return
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
- // Save current state to redo stack
208
- state.redoStack.push(state.content)
327
+ if (selectionUpdateTimeout) {
328
+ console.log(`[selectionchange #${updateCount}] Clearing previous timeout`)
329
+ window.clearTimeout(selectionUpdateTimeout)
330
+ }
209
331
 
210
- // Pop and apply last state from undo stack
211
- const lastContent = state.undoStack.pop()
212
- if (lastContent !== undefined) {
213
- state.content = lastContent
214
- updateContent('html')
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
- // Save current state to undo stack
222
- state.undoStack.push(state.content)
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
- // Pop and apply last state from redo stack
225
- const nextContent = state.redoStack.pop()
226
- if (nextContent !== undefined) {
227
- state.content = nextContent
228
- updateContent('html')
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
- function handleToolbarAction(action: string, value?: string) {
233
- if (!state.doc) return
373
+ return cleanupListeners
374
+ }
234
375
 
235
- if (action === 'undo') {
236
- handleUndo()
237
- return
238
- }
239
- if (action === 'redo') {
240
- handleRedo()
241
- return
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
- if (action === 'splitView') {
248
- state.isSplitView = !state.isSplitView
249
- return
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
- if (action === 'codeView') {
252
- state.isCodeView = !state.isCodeView
253
- return
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
- // Apply formatting based on action
257
- const format = formatting(state)
258
- state.doc.body.focus()
259
-
260
- switch (action) {
261
- case 'bold':
262
- case 'italic':
263
- case 'underline':
264
- format.text(action)
265
- break
266
- case 'orderedList':
267
- format.list('ol')
268
- break
269
- case 'unorderedList':
270
- format.list('ul')
271
- break
272
- case 'blockquote':
273
- case 'p':
274
- case 'h1':
275
- case 'h2':
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
- case 'deleteTable':
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
- // Debug methods
333
- const getDebugSession = () => editorDebugger.value?.getSession()
334
- const clearDebugSession = () => editorDebugger.value?.clearSession()
335
- const downloadDebugSession = () => editorDebugger.value?.downloadSession()
336
- const logCommand = (command: string, value?: string) => editorDebugger.value?.logCommand(command, value, state)
337
- const exportDebugWithPrompt = (message?: string) => editorDebugger.value?.exportSessionWithPrompt(message)
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
- handleToolbarAction,
343
- updateContent,
344
- handleUndo,
345
- handleRedo,
346
- // Debug methods
347
- debug: {
348
- getSession: getDebugSession,
349
- clearSession: clearDebugSession,
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
  }