@bagelink/vue 1.4.141 → 1.4.147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/components/Btn.vue.d.ts.map +1 -1
  2. package/dist/components/Carousel.vue.d.ts +1 -1
  3. package/dist/components/Modal.vue.d.ts +3 -0
  4. package/dist/components/Modal.vue.d.ts.map +1 -1
  5. package/dist/components/Slider.vue.d.ts +1 -1
  6. package/dist/components/Slider.vue.d.ts.map +1 -1
  7. package/dist/components/analytics/BarChart.vue.d.ts +11 -3
  8. package/dist/components/analytics/BarChart.vue.d.ts.map +1 -1
  9. package/dist/components/analytics/LineChart.vue.d.ts +9 -0
  10. package/dist/components/analytics/LineChart.vue.d.ts.map +1 -1
  11. package/dist/components/analytics/PieChart.vue.d.ts +30 -2
  12. package/dist/components/analytics/PieChart.vue.d.ts.map +1 -1
  13. package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts +8 -0
  14. package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
  15. package/dist/components/form/inputs/RichText/components/TableGridSelector.vue.d.ts +9 -0
  16. package/dist/components/form/inputs/RichText/components/TableGridSelector.vue.d.ts.map +1 -0
  17. package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
  18. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +0 -14
  19. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
  20. package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
  21. package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
  22. package/dist/components/form/inputs/RichText/index.vue.d.ts +15 -15
  23. package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
  24. package/dist/components/form/inputs/RichText/richTextTypes.d.ts +1 -3
  25. package/dist/components/form/inputs/RichText/richTextTypes.d.ts.map +1 -1
  26. package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
  27. package/dist/components/form/inputs/RichText/utils/media-clean.d.ts +2 -0
  28. package/dist/components/form/inputs/RichText/utils/media-clean.d.ts.map +1 -0
  29. package/dist/components/form/inputs/RichText/utils/media.d.ts +4 -4
  30. package/dist/components/form/inputs/RichText/utils/media.d.ts.map +1 -1
  31. package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
  32. package/dist/components/form/inputs/RichText/utils/table.d.ts +1 -1
  33. package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
  34. package/dist/components/index.d.ts +1 -0
  35. package/dist/components/index.d.ts.map +1 -1
  36. package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
  37. package/dist/components/layout/AppLayout.vue.d.ts.map +1 -1
  38. package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
  39. package/dist/index.cjs +123 -22
  40. package/dist/index.mjs +123 -22
  41. package/dist/style.css +1 -1
  42. package/package.json +1 -1
  43. package/src/components/Btn.vue +50 -42
  44. package/src/components/Modal.vue +49 -50
  45. package/src/components/analytics/BarChart.vue +118 -7
  46. package/src/components/analytics/KpiCard.vue +2 -2
  47. package/src/components/analytics/LineChart.vue +189 -105
  48. package/src/components/analytics/PieChart.vue +392 -49
  49. package/src/components/form/inputs/RichText/CheckList.md +23 -0
  50. package/src/components/form/inputs/RichText/components/EditorToolbar.vue +243 -38
  51. package/src/components/form/inputs/RichText/components/TableGridSelector.vue +94 -0
  52. package/src/components/form/inputs/RichText/composables/useCommands.ts +4 -1
  53. package/src/components/form/inputs/RichText/composables/useEditor.ts +6 -6
  54. package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +1 -0
  55. package/src/components/form/inputs/RichText/config.ts +23 -11
  56. package/src/components/form/inputs/RichText/editor.css +300 -33
  57. package/src/components/form/inputs/RichText/index.vue +3014 -75
  58. package/src/components/form/inputs/RichText/richTextTypes.ts +2 -3
  59. package/src/components/form/inputs/RichText/utils/commands.ts +279 -50
  60. package/src/components/form/inputs/RichText/utils/media-clean.ts +0 -0
  61. package/src/components/form/inputs/RichText/utils/media.ts +133 -67
  62. package/src/components/form/inputs/RichText/utils/selection.ts +10 -2
  63. package/src/components/form/inputs/RichText/utils/table.ts +1 -1
  64. package/src/components/index.ts +1 -0
  65. package/src/components/layout/AppContent.vue +26 -26
  66. package/src/components/layout/AppLayout.vue +21 -3
  67. package/src/components/layout/AppSidebar.vue +5 -2
  68. package/src/styles/layout.css +267 -0
  69. package/src/styles/mobilLayout.css +266 -0
  70. package/src/styles/modal.css +3 -17
@@ -1,13 +1,47 @@
1
1
  <script setup lang="ts">
2
2
  import type { ToolbarConfig } from './richTextTypes'
3
- import { CodeEditor, copyText, Btn } from '@bagelink/vue'
4
- import { watch, onUnmounted, ref } from 'vue'
3
+ import { CodeEditor, copyText, Btn, Modal, BglVideo, Icon, Card, ColorInput } from '@bagelink/vue'
4
+ import { watch, onUnmounted, onBeforeUnmount, ref, computed, useAttrs } from 'vue'
5
5
  import EditorToolbar from './components/EditorToolbar.vue'
6
6
  import { useCommands } from './composables/useCommands'
7
7
  import { useEditor } from './composables/useEditor'
8
8
  import { useEditorKeyboard } from './composables/useEditorKeyboard'
9
+ import TextInput from '../TextInput.vue'
10
+ import CheckInput from '../CheckInput.vue'
11
+ import SelectInput from '../SelectInput.vue'
12
+ import NumberInput from '../NumberInput.vue'
13
+
14
+ // Disable automatic inheritance of non-prop attributes
15
+ defineOptions({
16
+ inheritAttrs: false
17
+ })
9
18
 
10
- const props = defineProps<{ modelValue: string, toolbarConfig?: ToolbarConfig, debug?: boolean, label?: string, height?: number | string }>()
19
+ const attrs = useAttrs()
20
+
21
+ const props = defineProps<{
22
+ modelValue: string,
23
+ toolbarConfig?: ToolbarConfig,
24
+ debug?: boolean,
25
+ label?: string,
26
+ height?: number | string,
27
+ basic?: boolean,
28
+ simple?: boolean,
29
+ placeholder?: string,
30
+ // Manual hide options
31
+ hideToolbar?: boolean,
32
+ hideInlineToolbar?: boolean,
33
+ hideImages?: boolean,
34
+ hideVideos?: boolean,
35
+ hideEmbed?: boolean,
36
+ hideTables?: boolean,
37
+ hideAlignment?: boolean,
38
+ hideDirections?: boolean,
39
+ hideH5H6?: boolean,
40
+ // Simple array-based hide option
41
+ hide?: string[],
42
+ // Control autofocus behavior
43
+ autofocus?: boolean
44
+ }>()
11
45
  const emit = defineEmits(['update:modelValue'])
12
46
 
13
47
  const iframe = ref<HTMLIFrameElement>()
@@ -15,14 +49,1890 @@ const editor = useEditor()
15
49
  const isInitializing = ref(false)
16
50
  const hasInitialized = ref(false)
17
51
 
52
+ // Computed properties for UI control
53
+ const shouldShowToolbar = computed(() => {
54
+ if (props.hideToolbar) return false
55
+ if (props.basic) return false
56
+ return true
57
+ })
58
+
59
+ const shouldShowInlineToolbar = computed(() => {
60
+ if (props.hideInlineToolbar) return false
61
+ return true
62
+ })
63
+
64
+ // Simple toolbar config - just pass the original config, filtering happens in EditorToolbar
65
+ const effectiveToolbarConfig = computed(() => {
66
+ return props.toolbarConfig
67
+ })
68
+
69
+ // Compute effective hide array - combine manual hide array with simple mode
70
+ const effectiveHideArray = computed(() => {
71
+ const hideArray = [...(props.hide || [])]
72
+
73
+ // If simple mode is enabled, add the simple mode hide items
74
+ if (props.simple) {
75
+ const simpleHideItems = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'image', 'video', 'embed', 'ul', 'ol', 'blockquote', 'direction', 'table', 'alignment']
76
+ simpleHideItems.forEach(item => {
77
+ if (!hideArray.includes(item)) {
78
+ hideArray.push(item)
79
+ }
80
+ })
81
+ }
82
+
83
+ return hideArray
84
+ })
85
+
86
+ // Link modal state
87
+ const showLinkModal = ref(false)
88
+ const linkForm = ref({
89
+ url: '',
90
+ openInNewTab: true
91
+ })
92
+
93
+ // Tooltip state
94
+ const showTooltip = ref(false)
95
+ const tooltipData = ref({
96
+ message: '',
97
+ x: 0,
98
+ y: 0
99
+ })
100
+
101
+ let pendingLinkData: {
102
+ selection: Selection,
103
+ range: Range,
104
+ existingLink: HTMLAnchorElement | null
105
+ } | null = null
106
+
107
+ // Image modal state
108
+ const showImageModal = ref(false)
109
+ const imageForm = ref({
110
+ src: '',
111
+ alt: '',
112
+ width: '',
113
+ height: '',
114
+ credit: '',
115
+ figcaption: false
116
+ })
117
+ let pendingImageData: {
118
+ selection: Selection,
119
+ range: Range,
120
+ existingImage: HTMLElement | null
121
+ } | null = null
122
+
123
+ // Embed modal state
124
+ const showEmbedModal = ref(false)
125
+ const embedForm = ref({
126
+ src: '',
127
+ width: '560',
128
+ height: '315',
129
+ alt: ''
130
+ })
131
+ let pendingEmbedData: {
132
+ selection: Selection,
133
+ range: Range,
134
+ existingEmbed: HTMLElement | null
135
+ } | null = null
136
+
137
+ // Inline toolbar state
138
+ const showInlineToolbar = ref(false)
139
+ const inlineToolbarPosition = ref({ top: 0, left: 0 })
140
+ const inlineToolbarSelection = ref<Selection | null>(null)
141
+
142
+ // Video modal state
143
+ const showVideoModal = ref(false)
144
+ const videoForm = ref({
145
+ src: '',
146
+ width: '',
147
+ autoplay: false,
148
+ mute: false,
149
+ controls: true,
150
+ loop: false,
151
+ aspectRatio: '16:9',
152
+ customWidth: '',
153
+ customHeight: '',
154
+ caption: '',
155
+ showCaption: false
156
+ })
157
+ let pendingVideoData: {
158
+ selection: Selection,
159
+ range: Range,
160
+ existingVideo: HTMLElement | null
161
+ } | null = null
162
+
163
+ // Table editor state
164
+ const showTableEditor = ref(false)
165
+ const tableForm = ref({
166
+ rows: 3,
167
+ cols: 3,
168
+ width: 100,
169
+ borderWidth: 1,
170
+ borderColor: '#dddddd',
171
+ cellPadding: 8,
172
+ showHeaders: true,
173
+ headerBgColor: '#f4f4f4',
174
+ headerTextColor: '#333333',
175
+ cellBgColor: '#ffffff',
176
+ cellTextColor: '#333333',
177
+ alternateRows: false,
178
+ alternateRowBgColor: '#f9f9f9',
179
+ alternateRowTextColor: '#333333',
180
+ fixedLayout: true, // תאים ברוחב קבוע
181
+ alignment: 'left', // left, center, right
182
+ direction: 'ltr' // ltr, rtl
183
+ })
184
+
185
+ // Table context menu state
186
+ const showTableContextMenu = ref(false)
187
+ const contextMenuPosition = ref({ x: 0, y: 0 })
188
+ const contextMenuCell = ref<HTMLTableCellElement | null>(null)
189
+
190
+ let pendingTableData: {
191
+ selection: Selection,
192
+ range: Range,
193
+ existingTable: HTMLTableElement | null
194
+ } | null = null
195
+
196
+ // Computed property for table preview
197
+ const tablePreviewHtml = computed(() => {
198
+ const form = tableForm.value
199
+
200
+ let tableStyle = `
201
+ width: ${form.width}%;
202
+ border-collapse: collapse;
203
+ margin-bottom: 1rem;
204
+ border: ${form.borderWidth}px solid ${form.borderColor};
205
+ table-layout: ${form.fixedLayout ? 'fixed' : 'auto'};
206
+ `
207
+
208
+ // For cell text alignment, we'll apply it to individual cells, not the table
209
+
210
+ let html = `<table style="${tableStyle}" dir="${form.direction}">`
211
+
212
+ // Add header row if enabled
213
+ if (form.showHeaders) {
214
+ html += '<thead><tr>'
215
+ for (let j = 0; j < form.cols; j++) {
216
+ const thStyle = `
217
+ padding: ${form.cellPadding}px;
218
+ border: ${form.borderWidth}px solid ${form.borderColor};
219
+ background-color: ${form.headerBgColor};
220
+ color: ${form.headerTextColor};
221
+ text-align: ${form.alignment};
222
+ ${form.fixedLayout ? `width: ${100 / form.cols}%;` : ''}
223
+ `
224
+ html += `<th style="${thStyle}">Header ${j + 1}</th>`
225
+ }
226
+ html += '</tr></thead>'
227
+ }
228
+
229
+ // Add body rows
230
+ html += '<tbody>'
231
+ for (let i = 0; i < form.rows; i++) {
232
+ html += '<tr>'
233
+ for (let j = 0; j < form.cols; j++) {
234
+ const tdStyle = `
235
+ padding: ${form.cellPadding}px;
236
+ border: ${form.borderWidth}px solid ${form.borderColor};
237
+ text-align: ${form.alignment};
238
+ ${form.fixedLayout ? `width: ${100 / form.cols}%;` : ''}
239
+ ${form.alternateRows && i % 2 === 1 ?
240
+ `background-color: ${form.alternateRowBgColor}; color: ${form.alternateRowTextColor};` :
241
+ `background-color: ${form.cellBgColor}; color: ${form.cellTextColor};`}
242
+ `
243
+ html += `<td style="${tdStyle}">Cell ${i + 1},${j + 1}</td>`
244
+ }
245
+ html += '</tr>'
246
+ }
247
+ html += '</tbody></table>'
248
+
249
+ return html
250
+ })
251
+
18
252
  // Initialize content from modelValue
19
253
  editor.state.content = props.modelValue
20
254
 
255
+ // Function to detect Hebrew text and set direction
256
+ function detectAndSetDirection() {
257
+ const doc = editor.state.doc
258
+ if (!doc?.body) {
259
+ console.log('No doc.body found')
260
+ return
261
+ }
262
+
263
+ // Get all text content from body
264
+ const allText = doc.body.textContent || ''
265
+ const firstChars = allText.trim().substring(0, 10) // Check first 10 characters
266
+
267
+ console.log('Checking text:', firstChars, 'Length:', firstChars.length)
268
+
269
+ // Hebrew regex
270
+ const hebrewRegex = /[\u0590-\u05FF]/
271
+ const hasHebrew = hebrewRegex.test(firstChars)
272
+
273
+ console.log('Has Hebrew:', hasHebrew)
274
+
275
+ // Only change direction if it's different from current
276
+ const currentDir = doc.body.dir || 'ltr'
277
+ const shouldBeRtl = hasHebrew
278
+ const newDirection = shouldBeRtl ? 'rtl' : 'ltr'
279
+
280
+ console.log('Current dir:', currentDir, 'Should be RTL:', shouldBeRtl)
281
+
282
+ if (newDirection !== currentDir) {
283
+ doc.body.dir = newDirection
284
+ doc.body.style.direction = newDirection
285
+
286
+ // Update all paragraphs to match the new direction
287
+ const paragraphs = doc.querySelectorAll('p')
288
+ paragraphs.forEach(p => {
289
+ if (!p.classList.contains('placeholder')) {
290
+ p.setAttribute('dir', newDirection)
291
+ }
292
+ })
293
+
294
+ console.log(`✅ Switched to ${newDirection.toUpperCase()}`)
295
+ }
296
+ }
297
+
298
+ // Function to get current direction based on content
299
+ function getCurrentDirection() {
300
+ const doc = editor.state.doc
301
+ if (!doc?.body) return 'ltr'
302
+
303
+ const allText = doc.body.textContent || ''
304
+ const firstChars = allText.trim().substring(0, 10)
305
+ const hebrewRegex = /[\u0590-\u05FF]/
306
+
307
+ return hebrewRegex.test(firstChars) ? 'rtl' : 'ltr'
308
+ }
309
+
310
+ // Helper function to update content with history tracking
311
+ function updateContentWithHistory(doc: Document, skipHistory = false) {
312
+ if (!doc) return
313
+
314
+ const newContent = doc.body.innerHTML
315
+ if (newContent !== editor.state.content) {
316
+ if (!skipHistory) {
317
+ // Save to undo stack before updating
318
+ editor.state.undoStack.push(editor.state.content)
319
+ // Clear redo stack when new content is added
320
+ editor.state.redoStack = []
321
+ // Limit undo stack size to prevent memory issues
322
+ if (editor.state.undoStack.length > 50) {
323
+ editor.state.undoStack.shift()
324
+ }
325
+ }
326
+ editor.state.content = newContent
327
+
328
+ // Check direction after content update
329
+ if (doc === editor.state.doc) {
330
+ detectAndSetDirection()
331
+ }
332
+ }
333
+ }
334
+
335
+ // Debug function to show current content
336
+ function debugShowContent() {
337
+ const doc = editor.state.doc
338
+ if (doc) {
339
+ console.log('=== Current Editor Content ===')
340
+ console.log('HTML:', doc.body.innerHTML)
341
+ console.log('=== All iframes ===')
342
+ const iframes = doc.querySelectorAll('iframe')
343
+ iframes.forEach((iframe, index) => {
344
+ console.log(`Iframe ${index}:`, {
345
+ src: iframe.src,
346
+ width: iframe.width,
347
+ height: iframe.height,
348
+ className: iframe.className,
349
+ style: iframe.style.cssText
350
+ })
351
+ })
352
+ console.log('=== All figures ===')
353
+ const figures = doc.querySelectorAll('figure')
354
+ figures.forEach((figure, index) => {
355
+ console.log(`Figure ${index}:`, {
356
+ className: figure.className,
357
+ innerHTML: figure.innerHTML
358
+ })
359
+ })
360
+ }
361
+ }
362
+
363
+ // Make debug function available globally
364
+ ; (window as any).debugRichText = debugShowContent
365
+
366
+ // Function to show inline toolbar
367
+ function showInlineToolbarForSelection() {
368
+ // Check if inline toolbar should be shown
369
+ if (!shouldShowInlineToolbar.value) {
370
+ return
371
+ }
372
+
373
+ const doc = editor.state.doc
374
+ if (!doc) return
375
+
376
+ const selection = doc.getSelection()
377
+ if (!selection || selection.rangeCount === 0) {
378
+ hideInlineToolbar()
379
+ return
380
+ }
381
+
382
+ const range = selection.getRangeAt(0)
383
+ if (range.collapsed) {
384
+ hideInlineToolbar()
385
+ return
386
+ }
387
+
388
+ // Get the selected text
389
+ const selectedText = selection.toString().trim()
390
+ if (!selectedText) {
391
+ hideInlineToolbar()
392
+ return
393
+ }
394
+
395
+ // Calculate position relative to viewport
396
+ const rect = range.getBoundingClientRect()
397
+ const iframeRect = iframe.value?.getBoundingClientRect()
398
+
399
+ if (rect && iframeRect) {
400
+ // Calculate position with better centering and offset
401
+ const toolbarWidth = 200 // Approximate width of toolbar
402
+ const leftPosition = Math.max(10, Math.min(
403
+ window.innerWidth - toolbarWidth - 10,
404
+ iframeRect.left + rect.left + (rect.width / 2) - (toolbarWidth / 2)
405
+ ))
406
+
407
+ inlineToolbarPosition.value = {
408
+ top: iframeRect.top + rect.top - 45, // 45px above selection
409
+ left: leftPosition
410
+ }
411
+
412
+ // Store a clone of the selection to preserve it
413
+ const rangeClone = range.cloneRange()
414
+ inlineToolbarSelection.value = selection
415
+
416
+ // Store the cloned range for later use
417
+ ; (inlineToolbarSelection.value as any)._storedRange = rangeClone
418
+
419
+ showInlineToolbar.value = true
420
+ }
421
+ }
422
+
423
+ // Function to hide inline toolbar
424
+ function hideInlineToolbar() {
425
+ showInlineToolbar.value = false
426
+ inlineToolbarSelection.value = null
427
+ }
428
+
429
+ // Function to run inline toolbar action
430
+ function runInlineAction(actionName: string) {
431
+ if (!inlineToolbarSelection.value) return
432
+
433
+ const doc = editor.state.doc
434
+ if (!doc) return
435
+
436
+ // Get the stored range
437
+ const storedRange = (inlineToolbarSelection.value as any)._storedRange as Range
438
+ if (!storedRange) return
439
+
440
+ // Restore selection using the stored range
441
+ const selection = doc.getSelection()
442
+ if (selection) {
443
+ selection.removeAllRanges()
444
+ selection.addRange(storedRange)
445
+ }
446
+
447
+ // Run the command
448
+ commands.execute(actionName)
449
+
450
+ // Keep the toolbar visible but update the stored selection
451
+ setTimeout(() => {
452
+ const newSelection = doc.getSelection()
453
+ if (newSelection && newSelection.rangeCount > 0) {
454
+ const newRange = newSelection.getRangeAt(0)
455
+ if (!newRange.collapsed) {
456
+ ; (inlineToolbarSelection.value as any)._storedRange = newRange.cloneRange()
457
+ } else {
458
+ hideInlineToolbar()
459
+ }
460
+ }
461
+ }, 50)
462
+ }
463
+
21
464
  // Initialize debugger if debug mode is enabled
22
465
  if (props.debug) {
23
466
  editor.initDebugger()
24
467
  }
25
468
 
469
+ // Function to show tooltip
470
+ function showTooltipMessage(message: string, x?: number, y?: number) {
471
+ // If coordinates not provided, try to get cursor position
472
+ if (x === undefined || y === undefined) {
473
+ const selection = editor.state.doc?.getSelection()
474
+ if (selection && selection.rangeCount > 0) {
475
+ const range = selection.getRangeAt(0)
476
+ const rect = range.getBoundingClientRect()
477
+ // Get iframe offset
478
+ const iframeRect = iframe.value?.getBoundingClientRect() || { left: 0, top: 0 }
479
+ tooltipData.value = {
480
+ message,
481
+ x: rect.left + iframeRect.left + window.scrollX,
482
+ y: rect.top + iframeRect.top + window.scrollY - 40 // Show above cursor
483
+ }
484
+ } else {
485
+ // Fallback to center of editor
486
+ const iframeRect = iframe.value?.getBoundingClientRect()
487
+ tooltipData.value = {
488
+ message,
489
+ x: iframeRect ? iframeRect.left + iframeRect.width / 2 : 200,
490
+ y: iframeRect ? iframeRect.top + 50 : 100
491
+ }
492
+ }
493
+ } else {
494
+ tooltipData.value = { message, x, y }
495
+ }
496
+
497
+ showTooltip.value = true
498
+
499
+ // Auto hide after 3 seconds
500
+ setTimeout(() => {
501
+ showTooltip.value = false
502
+ }, 3000)
503
+ }
504
+
505
+ // Function to open link modal
506
+ function openLinkModal(selection: Selection, range: Range, existingLink: HTMLAnchorElement | null) {
507
+ pendingLinkData = { selection, range, existingLink }
508
+ linkForm.value = {
509
+ url: existingLink?.href || '',
510
+ openInNewTab: existingLink?.target !== '_self'
511
+ }
512
+ showLinkModal.value = true
513
+ }
514
+
515
+ // Function to submit link
516
+ function submitLink() {
517
+ if (!pendingLinkData || !linkForm.value.url) return
518
+
519
+ const { selection, range, existingLink } = pendingLinkData
520
+ const { url, openInNewTab } = linkForm.value
521
+ const doc = editor.state.doc
522
+
523
+ if (!doc) return
524
+
525
+ if (existingLink) {
526
+ // Update existing link
527
+ existingLink.href = url
528
+ existingLink.target = openInNewTab ? '_blank' : '_self'
529
+ if (openInNewTab) {
530
+ existingLink.rel = 'noopener noreferrer'
531
+ } else {
532
+ existingLink.removeAttribute('rel')
533
+ }
534
+ } else {
535
+ // Create new link
536
+ try {
537
+ const anchor = doc.createElement('a')
538
+ anchor.href = url
539
+ anchor.target = openInNewTab ? '_blank' : '_self'
540
+ if (openInNewTab) {
541
+ anchor.rel = 'noopener noreferrer'
542
+ }
543
+
544
+ try {
545
+ range.surroundContents(anchor)
546
+ } catch {
547
+ // If surroundContents fails, use extractContents method
548
+ const fragment = range.extractContents()
549
+ anchor.appendChild(fragment)
550
+ range.insertNode(anchor)
551
+ }
552
+
553
+ // Position cursor after the link
554
+ range.selectNodeContents(anchor)
555
+ range.collapse(false)
556
+ selection.removeAllRanges()
557
+ selection.addRange(range)
558
+ } catch (error) {
559
+ console.error('Error creating link:', error)
560
+ }
561
+ }
562
+
563
+ updateContentWithHistory(doc)
564
+ showLinkModal.value = false
565
+ pendingLinkData = null
566
+ }
567
+
568
+ // Function to visit link
569
+ function visitLink() {
570
+ if (!linkForm.value.url || !isValidUrl(linkForm.value.url)) return
571
+
572
+ let url = linkForm.value.url.trim()
573
+
574
+ // Add https:// if no protocol is specified
575
+ if (!url.match(/^https?:\/\//)) {
576
+ url = 'https://' + url
577
+ }
578
+
579
+ if (linkForm.value.openInNewTab) {
580
+ window.open(url, '_blank', 'noopener,noreferrer')
581
+ } else {
582
+ window.location.href = url
583
+ }
584
+ }
585
+
586
+ // Function to validate URL
587
+ function isValidUrl(url: string): boolean {
588
+ if (!url || url.trim() === '') return false
589
+
590
+ try {
591
+ new URL(url)
592
+ return true
593
+ } catch {
594
+ // Try with https:// prefix if it's missing
595
+ try {
596
+ new URL('https://' + url)
597
+ return true
598
+ } catch {
599
+ return false
600
+ }
601
+ }
602
+ }
603
+
604
+ // Function to open image modal
605
+ function openImageModal(existingImage: HTMLElement | null = null) {
606
+ const doc = editor.state.doc
607
+ if (!doc) return
608
+
609
+ // Get current selection for new images
610
+ if (!existingImage) {
611
+ const selection = doc.getSelection()
612
+ if (!selection || !selection.rangeCount) return
613
+ const range = selection.getRangeAt(0)
614
+ pendingImageData = { selection, range, existingImage: null }
615
+ } else {
616
+ // For existing images, set pendingImageData with the existing element
617
+ pendingImageData = {
618
+ selection: doc.getSelection()!,
619
+ range: doc.createRange(),
620
+ existingImage
621
+ }
622
+ }
623
+
624
+ if (existingImage) {
625
+ // Populate form with existing image data
626
+ // All images are now in figures
627
+ const img = existingImage.querySelector('img')
628
+ const figcaption = existingImage.querySelector('figcaption')
629
+ const credit = existingImage.getAttribute('data-credit') || img?.getAttribute('data-credit') || ''
630
+
631
+ imageForm.value = {
632
+ src: img?.src || '',
633
+ alt: img?.alt || '',
634
+ width: img?.getAttribute('data-width') || img?.style.width || '',
635
+ height: img?.getAttribute('data-height') || img?.style.height || '',
636
+ credit: credit,
637
+ figcaption: !!figcaption
638
+ }
639
+ } else {
640
+ // Reset form for new image
641
+ imageForm.value = {
642
+ src: '',
643
+ alt: '',
644
+ width: '',
645
+ height: '',
646
+ credit: '',
647
+ figcaption: false
648
+ }
649
+ }
650
+
651
+ showImageModal.value = true
652
+ }
653
+
654
+ // Function to submit image
655
+ function submitImage() {
656
+ if (!imageForm.value.src || !pendingImageData) return
657
+
658
+ const doc = editor.state.doc
659
+ if (!doc) return
660
+
661
+ const { existingImage } = pendingImageData
662
+
663
+ // Create image element
664
+ const img = doc.createElement('img')
665
+ img.src = imageForm.value.src
666
+ img.alt = imageForm.value.alt || ''
667
+
668
+ // Store original width/height values in data attributes
669
+ if (imageForm.value.width) {
670
+ img.setAttribute('data-width', imageForm.value.width)
671
+ if (imageForm.value.width.includes('%') || imageForm.value.width.includes('px') || imageForm.value.width.includes('vw') || imageForm.value.width.includes('rem') || imageForm.value.width.includes('em')) {
672
+ img.style.width = imageForm.value.width
673
+ } else {
674
+ img.style.width = imageForm.value.width + 'px'
675
+ }
676
+ } else {
677
+ img.style.width = '100%'
678
+ }
679
+
680
+ if (imageForm.value.height) {
681
+ img.setAttribute('data-height', imageForm.value.height)
682
+ if (imageForm.value.height.includes('%') || imageForm.value.height.includes('px') || imageForm.value.height.includes('vh') || imageForm.value.height.includes('rem') || imageForm.value.height.includes('em') || imageForm.value.height === 'auto') {
683
+ img.style.height = imageForm.value.height
684
+ } else {
685
+ img.style.height = imageForm.value.height + 'px'
686
+ }
687
+ } else {
688
+ img.style.height = 'auto'
689
+ }
690
+
691
+ // Helper function to create figcaption with alt text and credit
692
+ function createFigcaption() {
693
+ const figcaption = doc!.createElement('figcaption')
694
+ let captionHTML = ''
695
+
696
+ // Add alt text only if figcaption checkbox is checked (with bold styling)
697
+ if (imageForm.value.figcaption && imageForm.value.alt) {
698
+ captionHTML = `<strong class="alt-text">${imageForm.value.alt}</strong>`
699
+ }
700
+
701
+ // Always add credit if it exists (with lighter styling)
702
+ if (imageForm.value.credit) {
703
+ captionHTML += (captionHTML ? ' • ' : '') + `<span class="photo-credit">${imageForm.value.credit}</span>`
704
+ }
705
+
706
+ figcaption.innerHTML = captionHTML
707
+ return figcaption
708
+ }
709
+
710
+ // Create or update figure/image - always wrap in figure
711
+ let elementToUpdate: HTMLElement
712
+ if (existingImage) {
713
+ if (existingImage.tagName.toLowerCase() === 'img') {
714
+ // Converting standalone image to figure (always)
715
+ const figure = doc.createElement('figure')
716
+ figure.className = 'image-figure'
717
+ if (imageForm.value.credit) {
718
+ figure.setAttribute('data-credit', imageForm.value.credit)
719
+ }
720
+ figure.appendChild(img)
721
+
722
+ // Add caption if needed (alt with checkbox checked OR credit exists)
723
+ if ((imageForm.value.figcaption && imageForm.value.alt) || imageForm.value.credit) {
724
+ figure.appendChild(createFigcaption())
725
+ }
726
+
727
+ existingImage.parentNode?.replaceChild(figure, existingImage)
728
+ elementToUpdate = figure
729
+ } else {
730
+ // Updating an existing figure
731
+ existingImage.innerHTML = '' // Clear existing content
732
+ if (imageForm.value.credit) {
733
+ existingImage.setAttribute('data-credit', imageForm.value.credit)
734
+ }
735
+ existingImage.appendChild(img)
736
+
737
+ // Add caption if needed (alt with checkbox checked OR credit exists)
738
+ if ((imageForm.value.figcaption && imageForm.value.alt) || imageForm.value.credit) {
739
+ existingImage.appendChild(createFigcaption())
740
+ }
741
+ elementToUpdate = existingImage
742
+ }
743
+ } else {
744
+ // Creating new image - always wrap in figure
745
+ const figure = doc.createElement('figure')
746
+ figure.className = 'image-figure'
747
+ if (imageForm.value.credit) {
748
+ figure.setAttribute('data-credit', imageForm.value.credit)
749
+ }
750
+ figure.appendChild(img)
751
+
752
+ // Add caption if needed (alt with checkbox checked OR credit exists)
753
+ if ((imageForm.value.figcaption && imageForm.value.alt) || imageForm.value.credit) {
754
+ figure.appendChild(createFigcaption())
755
+ }
756
+ elementToUpdate = figure
757
+
758
+ // Insert new element
759
+ const { selection, range } = pendingImageData
760
+ range.collapse(false)
761
+ range.insertNode(elementToUpdate)
762
+
763
+ // Move cursor after the inserted element
764
+ range.setStartAfter(elementToUpdate)
765
+ range.collapse(true)
766
+ selection.removeAllRanges()
767
+ selection.addRange(range)
768
+ }
769
+
770
+ updateContentWithHistory(doc)
771
+ showImageModal.value = false
772
+ pendingImageData = null
773
+ }
774
+
775
+ // Function to open embed modal
776
+ function openEmbedModal(existingEmbed: HTMLElement | null = null) {
777
+ const doc = editor.state.doc
778
+ if (!doc) return
779
+
780
+ // Get current selection for new embeds
781
+ if (!existingEmbed) {
782
+ const selection = doc.getSelection()
783
+ if (!selection || !selection.rangeCount) return
784
+ const range = selection.getRangeAt(0)
785
+ pendingEmbedData = { selection, range, existingEmbed: null }
786
+ } else {
787
+ // For existing embeds, set pendingEmbedData with the existing element
788
+ pendingEmbedData = {
789
+ selection: doc.getSelection()!,
790
+ range: doc.createRange(),
791
+ existingEmbed
792
+ }
793
+ }
794
+
795
+ if (existingEmbed) {
796
+ // Populate form with existing embed data
797
+ const iframe = existingEmbed.querySelector('iframe')
798
+ const caption = existingEmbed.querySelector('figcaption')
799
+ embedForm.value = {
800
+ src: iframe?.src || '',
801
+ width: iframe?.width || iframe?.style.width?.replace('px', '') || '560',
802
+ height: iframe?.height || iframe?.style.height?.replace('px', '') || '315',
803
+ alt: caption?.textContent || ''
804
+ }
805
+ } else {
806
+ // Reset form for new embed
807
+ embedForm.value = {
808
+ src: '',
809
+ width: '560',
810
+ height: '315',
811
+ alt: ''
812
+ }
813
+ }
814
+
815
+ showEmbedModal.value = true
816
+ }
817
+
818
+ // Function to extract URL from iframe code or return the URL as-is
819
+ function extractEmbedUrl(input: string): string {
820
+ // If it's already a clean URL, return it
821
+ if (input.startsWith('http')) {
822
+ return input
823
+ }
824
+
825
+ // If it's iframe code, extract the src attribute
826
+ const srcMatch = input.match(/src=["']([^"']+)["']/)
827
+ if (srcMatch) {
828
+ return srcMatch[1]
829
+ }
830
+
831
+ // If no match, return the input as-is
832
+ return input
833
+ }
834
+
835
+ // Function to submit embed
836
+ function submitEmbed() {
837
+ if (!embedForm.value.src || !pendingEmbedData) return
838
+
839
+ const doc = editor.state.doc
840
+ if (!doc) return
841
+
842
+ const { existingEmbed } = pendingEmbedData
843
+
844
+ // Create iframe element
845
+ const iframe = doc.createElement('iframe')
846
+ const cleanUrl = extractEmbedUrl(embedForm.value.src)
847
+ iframe.src = cleanUrl
848
+ iframe.frameBorder = '0'
849
+ iframe.setAttribute('allowfullscreen', '')
850
+ iframe.setAttribute('data-media-type', 'embed')
851
+ iframe.className = 'embed-iframe'
852
+
853
+ // Debug: Log the iframe details
854
+ console.log('Creating embed iframe:', {
855
+ originalSrc: embedForm.value.src,
856
+ cleanUrl: cleanUrl,
857
+ width: embedForm.value.width,
858
+ height: embedForm.value.height
859
+ })
860
+
861
+ // Set width and height - always set them
862
+ iframe.width = embedForm.value.width || '560'
863
+ iframe.height = embedForm.value.height || '315'
864
+ iframe.style.width = (embedForm.value.width || '560') + 'px'
865
+ iframe.style.height = (embedForm.value.height || '315') + 'px'
866
+
867
+ // Create or update figure
868
+ let figure: HTMLElement
869
+ if (existingEmbed) {
870
+ figure = existingEmbed
871
+ figure.innerHTML = '' // Clear existing content
872
+ figure.appendChild(iframe)
873
+
874
+ // Add caption if provided
875
+ if (embedForm.value.alt) {
876
+ const caption = doc.createElement('figcaption')
877
+ caption.textContent = embedForm.value.alt
878
+ figure.appendChild(caption)
879
+ }
880
+ } else {
881
+ // Wrap iframe in figure for consistent styling
882
+ figure = doc.createElement('figure')
883
+ figure.className = 'embed-figure'
884
+ figure.appendChild(iframe)
885
+
886
+ // Add caption if provided
887
+ if (embedForm.value.alt) {
888
+ const caption = doc.createElement('figcaption')
889
+ caption.textContent = embedForm.value.alt
890
+ figure.appendChild(caption)
891
+ }
892
+
893
+ // Insert the figure
894
+ const { selection, range } = pendingEmbedData
895
+ range.collapse(false)
896
+ range.insertNode(figure)
897
+
898
+ // Move cursor after the inserted figure
899
+ range.setStartAfter(figure)
900
+ range.collapse(true)
901
+ selection.removeAllRanges()
902
+ selection.addRange(range)
903
+ }
904
+
905
+ // Debug: Log the final HTML
906
+ console.log('Embed figure created:', figure.outerHTML)
907
+ console.log('Editor content updated:', doc.body.innerHTML.includes('embed-figure'))
908
+
909
+ updateContentWithHistory(doc)
910
+ showEmbedModal.value = false
911
+ pendingEmbedData = null
912
+ }
913
+
914
+ // Function to open video modal
915
+ function openVideoModal(existingVideo: HTMLElement | null = null) {
916
+ const doc = editor.state.doc
917
+ if (!doc) return
918
+
919
+ // Get current selection for new videos
920
+ if (!existingVideo) {
921
+ const selection = doc.getSelection()
922
+ if (!selection || !selection.rangeCount) return
923
+ const range = selection.getRangeAt(0)
924
+ pendingVideoData = { selection, range, existingVideo: null }
925
+ } else {
926
+ // For existing videos, set pendingVideoData with the existing element
927
+ pendingVideoData = {
928
+ selection: doc.getSelection()!,
929
+ range: doc.createRange(),
930
+ existingVideo
931
+ }
932
+ }
933
+
934
+ if (existingVideo) {
935
+ // Populate form with existing video data
936
+ const container = existingVideo.querySelector('.video-container')
937
+ const isCustom = container?.getAttribute('data-custom-aspect-ratio') === 'true'
938
+ videoForm.value = {
939
+ src: container?.getAttribute('data-video-src') || '',
940
+ width: container?.getAttribute('data-width') || '',
941
+ autoplay: container?.getAttribute('data-autoplay') === 'true',
942
+ mute: container?.getAttribute('data-mute') === 'true',
943
+ controls: container?.getAttribute('data-controls') === 'true',
944
+ loop: container?.getAttribute('data-loop') === 'true',
945
+ aspectRatio: isCustom ? 'custom' : (container?.getAttribute('data-aspect-ratio') || '16:9'),
946
+ customWidth: container?.getAttribute('data-custom-width') || '',
947
+ customHeight: container?.getAttribute('data-custom-height') || '',
948
+ caption: existingVideo.querySelector('figcaption')?.textContent || '',
949
+ showCaption: !!existingVideo.querySelector('figcaption')
950
+ }
951
+ } else {
952
+ // Reset form for new video
953
+ videoForm.value = {
954
+ src: '',
955
+ width: '',
956
+ autoplay: false,
957
+ mute: false,
958
+ controls: true,
959
+ loop: false,
960
+ aspectRatio: '16:9',
961
+ customWidth: '',
962
+ customHeight: '',
963
+ caption: '',
964
+ showCaption: false
965
+ }
966
+ }
967
+
968
+ showVideoModal.value = true
969
+ }
970
+
971
+ // Function to validate video URL
972
+ function isValidVideoUrl(url: string): boolean {
973
+ if (!url || url.trim() === '') return false
974
+
975
+ try {
976
+ // Check for iframe tags
977
+ if (url.includes('<iframe') && url.includes('</iframe>')) {
978
+ return true
979
+ }
980
+
981
+ // Check for supported video formats
982
+ const videoFormats = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv']
983
+ const hasVideoFormat = videoFormats.some(format => url.toLowerCase().includes(format))
984
+
985
+ // Check for supported platforms
986
+ const supportedPlatforms = [
987
+ 'youtube.com',
988
+ 'youtu.be',
989
+ 'vimeo.com',
990
+ 'embed',
991
+ 'player.'
992
+ ]
993
+ const hasSupportedPlatform = supportedPlatforms.some(platform => url.toLowerCase().includes(platform))
994
+
995
+ // Try to parse as URL for general validation
996
+ if (hasVideoFormat || hasSupportedPlatform) {
997
+ return true
998
+ }
999
+
1000
+ // Basic URL validation
1001
+ new URL(url)
1002
+ return true
1003
+ } catch {
1004
+ return false
1005
+ }
1006
+ }
1007
+ function submitVideo() {
1008
+ if (!videoForm.value.src || !pendingVideoData) return
1009
+
1010
+ const doc = editor.state.doc
1011
+ if (!doc) return
1012
+
1013
+ const { existingVideo } = pendingVideoData
1014
+
1015
+ // Calculate aspect ratio
1016
+ let aspectRatio = videoForm.value.aspectRatio
1017
+ const isCustom = videoForm.value.aspectRatio === 'custom'
1018
+ if (isCustom && videoForm.value.customWidth && videoForm.value.customHeight) {
1019
+ aspectRatio = `${videoForm.value.customWidth}:${videoForm.value.customHeight}`
1020
+ }
1021
+
1022
+ // Auto-adjust aspect ratio for YouTube Shorts
1023
+ const isYoutubeShort = videoForm.value.src.includes('youtube.com/shorts/')
1024
+ if (isYoutubeShort && aspectRatio === '16:9') {
1025
+ aspectRatio = '9:16'
1026
+ }
1027
+
1028
+ // Create video container
1029
+ const videoContainer = doc.createElement('div')
1030
+ videoContainer.className = 'video-container'
1031
+ videoContainer.setAttribute('data-video-src', videoForm.value.src)
1032
+ videoContainer.setAttribute('data-component', 'BglVideo')
1033
+ videoContainer.setAttribute('data-media-type', 'video')
1034
+ videoContainer.setAttribute('data-width', videoForm.value.width)
1035
+ videoContainer.setAttribute('data-autoplay', videoForm.value.autoplay.toString())
1036
+ videoContainer.setAttribute('data-mute', videoForm.value.mute.toString())
1037
+ videoContainer.setAttribute('data-controls', videoForm.value.controls.toString())
1038
+ videoContainer.setAttribute('data-loop', videoForm.value.loop.toString())
1039
+ videoContainer.setAttribute('data-aspect-ratio', aspectRatio)
1040
+ videoContainer.setAttribute('data-custom-aspect-ratio', isCustom.toString())
1041
+ videoContainer.setAttribute('data-custom-width', videoForm.value.customWidth)
1042
+ videoContainer.setAttribute('data-custom-height', videoForm.value.customHeight)
1043
+
1044
+ // Set width
1045
+ if (videoForm.value.width) {
1046
+ videoContainer.style.width = videoForm.value.width.includes('%') || videoForm.value.width.includes('px')
1047
+ ? videoForm.value.width
1048
+ : videoForm.value.width + 'px'
1049
+ }
1050
+
1051
+ // Create the video placeholder/thumbnail instead of actual video
1052
+ const videoWrapper = doc.createElement('div')
1053
+ videoWrapper.className = 'bgl_vid'
1054
+
1055
+ // Create video placeholder/thumbnail
1056
+ const placeholder = doc.createElement('div')
1057
+ placeholder.className = 'video-placeholder'
1058
+ placeholder.style.aspectRatio = aspectRatio.replace(':', '/')
1059
+
1060
+ // Detect video type for display
1061
+ const isYoutube = videoForm.value.src.includes('youtube.com') || videoForm.value.src.includes('youtu.be')
1062
+ const isVimeo = videoForm.value.src.includes('vimeo.com')
1063
+
1064
+ // Create play icon
1065
+ const playIcon = doc.createElement('div')
1066
+ playIcon.className = 'video-placeholder-icon'
1067
+
1068
+ // Create text description
1069
+ const description = doc.createElement('div')
1070
+ description.className = 'video-placeholder-description'
1071
+ let platformText = 'video'
1072
+ if (isYoutube) platformText = 'YouTube video'
1073
+ else if (isVimeo) platformText = 'Vimeo video'
1074
+ description.textContent = platformText
1075
+
1076
+ // Try to get YouTube thumbnail if possible
1077
+ if (isYoutube) {
1078
+ let videoId = ''
1079
+ if (videoForm.value.src.includes('youtube.com/watch?v=')) {
1080
+ videoId = videoForm.value.src.split('watch?v=')[1]?.split('&')[0]
1081
+ } else if (videoForm.value.src.includes('youtu.be/')) {
1082
+ videoId = videoForm.value.src.split('youtu.be/')[1]?.split('?')[0]
1083
+ } else if (videoForm.value.src.includes('youtube.com/shorts/')) {
1084
+ videoId = videoForm.value.src.split('shorts/')[1]?.split('?')[0]
1085
+ }
1086
+
1087
+ if (videoId) {
1088
+ // Add thumbnail class and create thumbnail image
1089
+ placeholder.classList.add('has-thumbnail')
1090
+
1091
+ const thumbnail = doc.createElement('img')
1092
+ thumbnail.className = 'video-thumbnail'
1093
+ thumbnail.src = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`
1094
+ placeholder.appendChild(thumbnail)
1095
+
1096
+ // Add overlay with play button
1097
+ const overlay = doc.createElement('div')
1098
+ overlay.className = 'video-overlay'
1099
+
1100
+ const overlayPlayIcon = doc.createElement('div')
1101
+ overlayPlayIcon.className = 'video-overlay-icon'
1102
+
1103
+ overlay.appendChild(overlayPlayIcon)
1104
+ placeholder.appendChild(overlay)
1105
+ } else {
1106
+ placeholder.appendChild(playIcon)
1107
+ placeholder.appendChild(description)
1108
+ }
1109
+ } else {
1110
+ placeholder.appendChild(playIcon)
1111
+ placeholder.appendChild(description)
1112
+ }
1113
+
1114
+ // Add special class for YouTube Shorts
1115
+ if (isYoutube && videoForm.value.src.includes('youtube.com/shorts/')) {
1116
+ videoWrapper.classList.add('vid_short')
1117
+ }
1118
+
1119
+ videoWrapper.appendChild(placeholder)
1120
+
1121
+ videoContainer.appendChild(videoWrapper)
1122
+
1123
+ // Create or update figure
1124
+ let figure: HTMLElement
1125
+ if (existingVideo) {
1126
+ figure = existingVideo
1127
+ figure.innerHTML = '' // Clear existing content
1128
+ } else {
1129
+ figure = doc.createElement('figure')
1130
+ figure.className = 'video-figure'
1131
+ }
1132
+
1133
+ figure.appendChild(videoContainer)
1134
+
1135
+ // Add caption if enabled
1136
+ if (videoForm.value.showCaption && videoForm.value.caption) {
1137
+ const figcaption = doc.createElement('figcaption')
1138
+ figcaption.textContent = videoForm.value.caption
1139
+ figure.appendChild(figcaption)
1140
+ }
1141
+
1142
+ // Insert new video or update existing
1143
+ if (!existingVideo) {
1144
+ const { selection, range } = pendingVideoData
1145
+ range.collapse(false)
1146
+ range.insertNode(figure)
1147
+
1148
+ // Move cursor after the inserted figure
1149
+ range.setStartAfter(figure)
1150
+ range.collapse(true)
1151
+ selection.removeAllRanges()
1152
+ selection.addRange(range)
1153
+ }
1154
+
1155
+ updateContentWithHistory(doc)
1156
+ showVideoModal.value = false
1157
+ pendingVideoData = null
1158
+ }
1159
+
1160
+ // Function to delete video
1161
+ function deleteVideo() {
1162
+ if (!pendingVideoData?.existingVideo) return
1163
+
1164
+ const { existingVideo } = pendingVideoData
1165
+ existingVideo.remove()
1166
+
1167
+ updateContentWithHistory(editor.state.doc!)
1168
+ showVideoModal.value = false
1169
+ pendingVideoData = null
1170
+ }
1171
+
1172
+ // Function to open table editor
1173
+ // Function to detect table alignment from table or wrapper
1174
+ function detectTableAlignment(table: HTMLTableElement): 'left' | 'center' | 'right' {
1175
+ // Check table margins to determine alignment
1176
+ const marginLeft = table.style.marginLeft
1177
+ const marginRight = table.style.marginRight
1178
+
1179
+ console.log('Table margins:', { marginLeft, marginRight })
1180
+
1181
+ if (marginLeft === 'auto' && marginRight === 'auto') {
1182
+ return 'center'
1183
+ } else if (marginLeft === 'auto' && marginRight === '0') {
1184
+ return 'right'
1185
+ } else {
1186
+ return 'left'
1187
+ }
1188
+ }
1189
+
1190
+ function openTableEditor(existingTable: HTMLTableElement | null = null) {
1191
+ const doc = editor.state.doc
1192
+ if (!doc) return
1193
+
1194
+ // Get current selection for new tables
1195
+ if (!existingTable) {
1196
+ const selection = doc.getSelection()
1197
+ if (!selection || !selection.rangeCount) return
1198
+ const range = selection.getRangeAt(0)
1199
+ pendingTableData = { selection, range, existingTable: null }
1200
+ } else {
1201
+ // For existing tables, set pendingTableData with the existing element
1202
+ pendingTableData = {
1203
+ selection: doc.getSelection()!,
1204
+ range: doc.createRange(),
1205
+ existingTable
1206
+ }
1207
+ }
1208
+
1209
+ if (existingTable) {
1210
+ // Populate form with existing table data
1211
+ const tbody = existingTable.querySelector('tbody')
1212
+ const thead = existingTable.querySelector('thead')
1213
+
1214
+ // Detect alternating rows by comparing background colors
1215
+ let hasAlternatingRows = false
1216
+ let alternateRowBg = '#f9f9f9'
1217
+ let alternateRowTextColor = '#333333'
1218
+
1219
+ if (tbody && tbody.rows.length >= 2) {
1220
+ const firstRow = tbody.rows[0]
1221
+ const secondRow = tbody.rows[1]
1222
+
1223
+ if (firstRow.cells.length > 0 && secondRow.cells.length > 0) {
1224
+ const firstCell = firstRow.cells[0] as HTMLElement
1225
+ const secondCell = secondRow.cells[0] as HTMLElement
1226
+
1227
+ const firstBgColor = getComputedStyle(firstCell).backgroundColor
1228
+ const secondBgColor = getComputedStyle(secondCell).backgroundColor
1229
+
1230
+ // Check if colors are different (indicating alternating rows)
1231
+ if (firstBgColor !== secondBgColor) {
1232
+ hasAlternatingRows = true
1233
+ alternateRowBg = secondBgColor
1234
+ alternateRowTextColor = getComputedStyle(secondCell).color
1235
+ }
1236
+ }
1237
+ }
1238
+
1239
+ tableForm.value = {
1240
+ rows: tbody ? tbody.rows.length : 3,
1241
+ cols: tbody && tbody.rows.length > 0 ? tbody.rows[0].cells.length : 3,
1242
+ width: parseInt(existingTable.style.width) || 100,
1243
+ borderWidth: parseInt(existingTable.style.borderWidth) || 1,
1244
+ borderColor: existingTable.style.borderColor || '#dddddd',
1245
+ cellPadding: 8, // default, hard to extract
1246
+ showHeaders: !!thead,
1247
+ headerBgColor: thead ? getComputedStyle(thead.querySelector('th') || thead).backgroundColor || '#f4f4f4' : '#f4f4f4',
1248
+ headerTextColor: thead ? getComputedStyle(thead.querySelector('th') || thead).color || '#333333' : '#333333',
1249
+ cellBgColor: tbody ? getComputedStyle(tbody.querySelector('td') || tbody).backgroundColor || '#ffffff' : '#ffffff',
1250
+ cellTextColor: tbody ? getComputedStyle(tbody.querySelector('td') || tbody).color || '#333333' : '#333333',
1251
+ alternateRows: hasAlternatingRows,
1252
+ alternateRowBgColor: alternateRowBg,
1253
+ alternateRowTextColor: alternateRowTextColor,
1254
+ fixedLayout: existingTable.style.tableLayout === 'fixed' || true, // ברירת מחדל true
1255
+ alignment: detectTableAlignment(existingTable),
1256
+ direction: existingTable.dir || 'ltr'
1257
+ }
1258
+ } else {
1259
+ // Reset form for new table
1260
+ tableForm.value = {
1261
+ rows: 3,
1262
+ cols: 3,
1263
+ width: 100,
1264
+ borderWidth: 1,
1265
+ borderColor: '#dddddd',
1266
+ cellPadding: 8,
1267
+ showHeaders: true,
1268
+ headerBgColor: '#f4f4f4',
1269
+ headerTextColor: '#333333',
1270
+ cellBgColor: '#ffffff',
1271
+ cellTextColor: '#333333',
1272
+ alternateRows: false,
1273
+ alternateRowBgColor: '#f9f9f9',
1274
+ alternateRowTextColor: '#333333',
1275
+ fixedLayout: true,
1276
+ alignment: 'left',
1277
+ direction: 'ltr'
1278
+ }
1279
+ } showTableEditor.value = true
1280
+ }
1281
+
1282
+ // Function to submit table changes
1283
+ function submitTable() {
1284
+ if (!pendingTableData) return
1285
+
1286
+ const doc = editor.state.doc
1287
+ if (!doc) return
1288
+
1289
+ if (pendingTableData.existingTable) {
1290
+ // Update existing table instead of creating new one
1291
+ const table = pendingTableData.existingTable
1292
+
1293
+ // Update table styles
1294
+ table.style.width = `${tableForm.value.width}%`
1295
+ table.style.borderCollapse = 'collapse'
1296
+ table.style.marginBottom = '1rem'
1297
+ table.style.border = `${tableForm.value.borderWidth}px solid ${tableForm.value.borderColor}`
1298
+
1299
+ // Set table layout
1300
+ if (tableForm.value.fixedLayout) {
1301
+ table.style.tableLayout = 'fixed'
1302
+ } else {
1303
+ table.style.tableLayout = 'auto'
1304
+ }
1305
+
1306
+ // Don't force text alignment for all cells - let them inherit from table direction
1307
+ // console.log('Setting cell text alignment:', tableForm.value.alignment)
1308
+ // const allCells = table.querySelectorAll('td, th')
1309
+ // allCells.forEach(cell => {
1310
+ // const cellEl = cell as HTMLElement
1311
+ // cellEl.style.textAlign = tableForm.value.alignment
1312
+ // })
1313
+
1314
+ // Set table direction
1315
+ table.dir = tableForm.value.direction
1316
+
1317
+ // Update border and padding for all cells
1318
+ const allCells = table.querySelectorAll('td, th')
1319
+ allCells.forEach(cell => {
1320
+ const cellEl = cell as HTMLElement
1321
+ cellEl.style.padding = `${tableForm.value.cellPadding}px`
1322
+ cellEl.style.border = `${tableForm.value.borderWidth}px solid ${tableForm.value.borderColor}`
1323
+
1324
+ // Set fixed width for fixed layout
1325
+ if (tableForm.value.fixedLayout) {
1326
+ const colCount = table.querySelector('tr')?.querySelectorAll('td, th').length || tableForm.value.cols
1327
+ cellEl.style.width = `${100 / colCount}%`
1328
+ }
1329
+ })
1330
+
1331
+ // Update header styles if headers exist
1332
+ const headers = table.querySelectorAll('th')
1333
+ headers.forEach(th => {
1334
+ const thEl = th as HTMLElement
1335
+ thEl.style.backgroundColor = tableForm.value.headerBgColor
1336
+ thEl.style.color = tableForm.value.headerTextColor
1337
+ })
1338
+
1339
+ // Update body cell styles
1340
+ const tbody = table.querySelector('tbody')
1341
+ if (tbody) {
1342
+ const rows = Array.from(tbody.querySelectorAll('tr'))
1343
+ rows.forEach((row, i) => {
1344
+ const cells = row.querySelectorAll('td')
1345
+ cells.forEach(cell => {
1346
+ const cellEl = cell as HTMLElement
1347
+ if (tableForm.value.alternateRows && i % 2 === 1) {
1348
+ // Alternate rows: use special colors
1349
+ cellEl.style.backgroundColor = tableForm.value.alternateRowBgColor
1350
+ cellEl.style.color = tableForm.value.alternateRowTextColor
1351
+ } else {
1352
+ // Regular rows: use cell colors
1353
+ cellEl.style.backgroundColor = tableForm.value.cellBgColor
1354
+ cellEl.style.color = tableForm.value.cellTextColor
1355
+ }
1356
+ })
1357
+ })
1358
+ }
1359
+
1360
+ updateContentWithHistory(doc)
1361
+ } else {
1362
+ // Create new table (existing logic)
1363
+ createNewTable(doc)
1364
+ }
1365
+
1366
+ showTableEditor.value = false
1367
+ pendingTableData = null
1368
+ }
1369
+
1370
+ // Function to create new table (moved from submitTable)
1371
+ function createNewTable(doc: Document) {
1372
+ if (!pendingTableData) return
1373
+
1374
+ // Create new table with current settings
1375
+ const table = doc.createElement('table')
1376
+ table.style.width = `${tableForm.value.width}%`
1377
+ table.style.borderCollapse = 'collapse'
1378
+ table.style.marginBottom = '1rem'
1379
+ table.style.border = `${tableForm.value.borderWidth}px solid ${tableForm.value.borderColor}`
1380
+
1381
+ // Set table layout based on fixedLayout setting
1382
+ if (tableForm.value.fixedLayout) {
1383
+ table.style.tableLayout = 'fixed'
1384
+ } else {
1385
+ table.style.tableLayout = 'auto'
1386
+ }
1387
+
1388
+ // Set table alignment (will be applied directly to table since no wrapper yet)
1389
+ // Don't set textAlign on table - let it inherit from document direction
1390
+ // table.style.textAlign = tableForm.value.alignment
1391
+
1392
+ if (tableForm.value.alignment === 'center') {
1393
+ table.style.marginLeft = 'auto'
1394
+ table.style.marginRight = 'auto'
1395
+ } else if (tableForm.value.alignment === 'right') {
1396
+ table.style.marginLeft = 'auto'
1397
+ table.style.marginRight = '0'
1398
+ } else {
1399
+ table.style.marginLeft = '0'
1400
+ table.style.marginRight = 'auto'
1401
+ }
1402
+
1403
+ // Set table direction
1404
+ table.dir = tableForm.value.direction
1405
+
1406
+ // Add header if enabled
1407
+ if (tableForm.value.showHeaders) {
1408
+ const thead = doc.createElement('thead')
1409
+ const headerRow = thead.insertRow()
1410
+ for (let j = 0; j < tableForm.value.cols; j++) {
1411
+ const th = doc.createElement('th')
1412
+ th.innerHTML = `Header ${j + 1}`
1413
+ th.style.padding = `${tableForm.value.cellPadding}px`
1414
+ th.style.border = `${tableForm.value.borderWidth}px solid ${tableForm.value.borderColor}`
1415
+ th.style.backgroundColor = tableForm.value.headerBgColor
1416
+ th.style.color = tableForm.value.headerTextColor
1417
+ // Don't set textAlign - let it inherit from table direction
1418
+ // th.style.textAlign = tableForm.value.alignment
1419
+
1420
+ // Set fixed width for fixed layout
1421
+ if (tableForm.value.fixedLayout) {
1422
+ th.style.width = `${100 / tableForm.value.cols}%`
1423
+ }
1424
+
1425
+ headerRow.appendChild(th)
1426
+ }
1427
+ table.appendChild(thead)
1428
+ }
1429
+
1430
+ // Add body
1431
+ const tbody = doc.createElement('tbody')
1432
+ for (let i = 0; i < tableForm.value.rows; i++) {
1433
+ const row = tbody.insertRow()
1434
+ for (let j = 0; j < tableForm.value.cols; j++) {
1435
+ const cell = row.insertCell()
1436
+ cell.innerHTML = '&nbsp;'
1437
+ cell.style.padding = `${tableForm.value.cellPadding}px`
1438
+ cell.style.border = `${tableForm.value.borderWidth}px solid ${tableForm.value.borderColor}`
1439
+ // Don't set textAlign - let it inherit from table direction
1440
+ // cell.style.textAlign = tableForm.value.alignment
1441
+
1442
+ // Set fixed width for fixed layout
1443
+ if (tableForm.value.fixedLayout) {
1444
+ cell.style.width = `${100 / tableForm.value.cols}%`
1445
+ }
1446
+
1447
+ // Apply background and text colors
1448
+ if (tableForm.value.alternateRows && i % 2 === 1) {
1449
+ // Alternate rows: use special colors
1450
+ cell.style.backgroundColor = tableForm.value.alternateRowBgColor
1451
+ cell.style.color = tableForm.value.alternateRowTextColor
1452
+ } else {
1453
+ // Regular rows: use cell colors
1454
+ cell.style.backgroundColor = tableForm.value.cellBgColor
1455
+ cell.style.color = tableForm.value.cellTextColor
1456
+ }
1457
+ }
1458
+ }
1459
+ table.appendChild(tbody)
1460
+
1461
+ // Insert new table
1462
+ const { range } = pendingTableData
1463
+ range.insertNode(table)
1464
+ range.setStartAfter(table)
1465
+ range.collapse(true)
1466
+ const selection = doc.getSelection()
1467
+ if (selection) {
1468
+ selection.removeAllRanges()
1469
+ selection.addRange(range)
1470
+ }
1471
+
1472
+ updateContentWithHistory(doc)
1473
+
1474
+ // Add edit button to the new table immediately
1475
+ setupTableEditButtons(doc)
1476
+
1477
+ // Add edit button to the new table
1478
+ setTimeout(() => {
1479
+ console.log('Trying to add edit button to new table...')
1480
+ if (doc && (doc as any).__addEditButtonsToTables) {
1481
+ console.log('Calling __addEditButtonsToTables...')
1482
+ ; (doc as any).__addEditButtonsToTables()
1483
+ } else {
1484
+ console.log('__addEditButtonsToTables not found, calling setupTableEditButtons...')
1485
+ setupTableEditButtons(doc)
1486
+ }
1487
+ }, 10)
1488
+ }
1489
+
1490
+ // Function to delete table
1491
+ function deleteTable() {
1492
+ if (!pendingTableData?.existingTable) return
1493
+
1494
+ const { existingTable } = pendingTableData
1495
+ existingTable.remove()
1496
+
1497
+ updateContentWithHistory(editor.state.doc!)
1498
+ showTableEditor.value = false
1499
+ pendingTableData = null
1500
+ }
1501
+
1502
+ // Function to apply default table settings to externally created tables
1503
+ function applyDefaultTableSettings(table: HTMLTableElement) {
1504
+ console.log('Applying default settings to table:', table)
1505
+
1506
+ // Apply default styles from tableForm
1507
+ const defaultSettings = {
1508
+ fixedLayout: true,
1509
+ alignment: 'start',
1510
+ direction: 'ltr'
1511
+ }
1512
+
1513
+ // Set table layout
1514
+ if (defaultSettings.fixedLayout) {
1515
+ table.style.tableLayout = 'fixed'
1516
+
1517
+ // Set equal width for all columns if not already set
1518
+ const headers = table.querySelectorAll('th')
1519
+ const cells = table.querySelectorAll('td')
1520
+
1521
+ if (headers.length > 0) {
1522
+ const colWidth = `${100 / headers.length}%`
1523
+ headers.forEach(th => {
1524
+ if (!(th as HTMLElement).style.width) {
1525
+ (th as HTMLElement).style.width = colWidth
1526
+ }
1527
+ })
1528
+ }
1529
+
1530
+ if (cells.length > 0 && headers.length === 0) {
1531
+ // If no headers, use first row to determine column count
1532
+ const firstRow = table.querySelector('tr')
1533
+ if (firstRow) {
1534
+ const colCount = firstRow.querySelectorAll('td').length
1535
+ const colWidth = `${100 / colCount}%`
1536
+ cells.forEach(td => {
1537
+ if (!(td as HTMLElement).style.width) {
1538
+ (td as HTMLElement).style.width = colWidth
1539
+ }
1540
+ })
1541
+ }
1542
+ }
1543
+ }
1544
+
1545
+ // Set cell text alignment
1546
+ const allCells = table.querySelectorAll('td, th')
1547
+ allCells.forEach(cell => {
1548
+ const cellEl = cell as HTMLElement
1549
+ cellEl.style.textAlign = defaultSettings.alignment
1550
+ })
1551
+
1552
+ // Set table direction
1553
+ table.dir = defaultSettings.direction
1554
+
1555
+ console.log('Default settings applied successfully')
1556
+ }
1557
+
1558
+ // Function to delete image
1559
+ function deleteImage() {
1560
+ if (!pendingImageData?.existingImage) return
1561
+
1562
+ const { existingImage } = pendingImageData
1563
+ existingImage.remove()
1564
+
1565
+ updateContentWithHistory(editor.state.doc!)
1566
+ showImageModal.value = false
1567
+ pendingImageData = null
1568
+ }
1569
+
1570
+ // Function to delete embed
1571
+ function deleteEmbed() {
1572
+ if (!pendingEmbedData?.existingEmbed) return
1573
+
1574
+ const { existingEmbed } = pendingEmbedData
1575
+ existingEmbed.remove()
1576
+
1577
+ updateContentWithHistory(editor.state.doc!)
1578
+ showEmbedModal.value = false
1579
+ pendingEmbedData = null
1580
+ }
1581
+
1582
+ // Expose openLinkModal to editor state
1583
+ ; (editor.state as any).openLinkModal = openLinkModal
1584
+ ; (editor.state as any).showTooltipMessage = showTooltipMessage
1585
+
1586
+ // Expose openTableEditor to editor state
1587
+ ; (editor.state as any).openTableEditor = openTableEditor
1588
+ ; (editor.state as any).openImageModal = openImageModal
1589
+ ; (editor.state as any).openEmbedModal = openEmbedModal
1590
+ ; (editor.state as any).openVideoModal = openVideoModal
1591
+
1592
+ // Table manipulation functions
1593
+ function mergeCellRight() {
1594
+ const doc = editor.state.doc
1595
+ if (!doc || !contextMenuCell.value) return
1596
+
1597
+ const cell = contextMenuCell.value
1598
+ const row = cell.parentElement as HTMLTableRowElement
1599
+ const cellIndex = Array.from(row.cells).indexOf(cell)
1600
+ const table = cell.closest('table') as HTMLTableElement
1601
+ const isRTL = table?.dir === 'rtl'
1602
+
1603
+ // In RTL, "right" means the previous cell, in LTR it means the next cell
1604
+ const targetIndex = isRTL ? cellIndex - 1 : cellIndex + 1
1605
+ const targetCell = row.cells[targetIndex]
1606
+
1607
+ if (targetCell && targetIndex >= 0 && targetIndex < row.cells.length) {
1608
+ // Combine content
1609
+ if (targetCell.innerHTML.trim()) {
1610
+ cell.innerHTML += ' ' + targetCell.innerHTML
1611
+ }
1612
+
1613
+ // Update colspan
1614
+ const currentColspan = parseInt(cell.getAttribute('colspan') || '1')
1615
+ const targetColspan = parseInt(targetCell.getAttribute('colspan') || '1')
1616
+ cell.setAttribute('colspan', (currentColspan + targetColspan).toString())
1617
+
1618
+ // Remove the target cell
1619
+ targetCell.remove()
1620
+
1621
+ updateContentWithHistory(doc)
1622
+ }
1623
+ }
1624
+
1625
+ function mergeCellDown() {
1626
+ const doc = editor.state.doc
1627
+ if (!doc || !contextMenuCell.value) return
1628
+
1629
+ const cell = contextMenuCell.value
1630
+ const table = cell.closest('table') as HTMLTableElement
1631
+ const row = cell.parentElement as HTMLTableRowElement
1632
+ const cellIndex = Array.from(row.cells).indexOf(cell)
1633
+
1634
+ // Find the row below
1635
+ const tbody = table.querySelector('tbody') || table
1636
+ const rows = Array.from(tbody.querySelectorAll('tr'))
1637
+ const currentRowIndex = rows.indexOf(row)
1638
+ const nextRow = rows[currentRowIndex + 1]
1639
+
1640
+ if (nextRow) {
1641
+ const targetCell = nextRow.cells[cellIndex]
1642
+ if (targetCell) {
1643
+ // Combine content
1644
+ if (targetCell.innerHTML.trim()) {
1645
+ cell.innerHTML += '<br>' + targetCell.innerHTML
1646
+ }
1647
+
1648
+ // Update rowspan
1649
+ const currentRowspan = parseInt(cell.getAttribute('rowspan') || '1')
1650
+ const targetRowspan = parseInt(targetCell.getAttribute('rowspan') || '1')
1651
+ cell.setAttribute('rowspan', (currentRowspan + targetRowspan).toString())
1652
+
1653
+ // Remove the target cell
1654
+ targetCell.remove()
1655
+
1656
+ updateContentWithHistory(doc)
1657
+ }
1658
+ }
1659
+ }
1660
+
1661
+ function splitCell() {
1662
+ const doc = editor.state.doc
1663
+ if (!doc || !contextMenuCell.value) return
1664
+
1665
+ const cell = contextMenuCell.value
1666
+ const table = cell.closest('table') as HTMLTableElement
1667
+ const isRTL = table?.dir === 'rtl'
1668
+ const colspan = parseInt(cell.getAttribute('colspan') || '1')
1669
+ const rowspan = parseInt(cell.getAttribute('rowspan') || '1')
1670
+
1671
+ if (colspan > 1) {
1672
+ // Split horizontally
1673
+ cell.setAttribute('colspan', '1')
1674
+ const row = cell.parentElement as HTMLTableRowElement
1675
+ const cellIndex = Array.from(row.cells).indexOf(cell)
1676
+
1677
+ // Add new cells to the right (or left in RTL)
1678
+ for (let i = 1; i < colspan; i++) {
1679
+ const newCell = doc.createElement(cell.tagName.toLowerCase() as 'td' | 'th')
1680
+ newCell.innerHTML = '&nbsp;'
1681
+ newCell.style.cssText = cell.style.cssText
1682
+
1683
+ if (isRTL) {
1684
+ row.insertBefore(newCell, cell)
1685
+ } else {
1686
+ if (cell.nextSibling) {
1687
+ row.insertBefore(newCell, cell.nextSibling)
1688
+ } else {
1689
+ row.appendChild(newCell)
1690
+ }
1691
+ }
1692
+ }
1693
+ } else if (rowspan > 1) {
1694
+ // Split vertically
1695
+ cell.setAttribute('rowspan', '1')
1696
+ const row = cell.parentElement as HTMLTableRowElement
1697
+ const cellIndex = Array.from(row.cells).indexOf(cell)
1698
+ const tbody = table.querySelector('tbody') || table
1699
+ const rows = Array.from(tbody.querySelectorAll('tr'))
1700
+ const currentRowIndex = rows.indexOf(row)
1701
+
1702
+ // Add new cells to rows below
1703
+ for (let i = 1; i < rowspan; i++) {
1704
+ const targetRow = rows[currentRowIndex + i]
1705
+ if (targetRow) {
1706
+ const newCell = doc.createElement(cell.tagName.toLowerCase() as 'td' | 'th')
1707
+ newCell.innerHTML = '&nbsp;'
1708
+ newCell.style.cssText = cell.style.cssText
1709
+
1710
+ // Insert at the correct position
1711
+ if (cellIndex < targetRow.cells.length) {
1712
+ targetRow.insertBefore(newCell, targetRow.cells[cellIndex])
1713
+ } else {
1714
+ targetRow.appendChild(newCell)
1715
+ }
1716
+ }
1717
+ }
1718
+ }
1719
+
1720
+ updateContentWithHistory(doc)
1721
+ }
1722
+
1723
+ function insertRowAbove() {
1724
+ const doc = editor.state.doc
1725
+ if (!doc || !contextMenuCell.value) return
1726
+
1727
+ const cell = contextMenuCell.value
1728
+ const row = cell.parentElement as HTMLTableRowElement
1729
+
1730
+ // Create new row with same number of columns
1731
+ const newRow = doc.createElement('tr')
1732
+ const columnCount = Array.from(row.cells).reduce((total, cell) => {
1733
+ return total + parseInt(cell.getAttribute('colspan') || '1')
1734
+ }, 0)
1735
+
1736
+ for (let i = 0; i < columnCount; i++) {
1737
+ const newCell = doc.createElement('td')
1738
+ newCell.innerHTML = '&nbsp;'
1739
+ newCell.style.padding = '8px'
1740
+ newCell.style.border = '1px solid #ddd'
1741
+ newRow.appendChild(newCell)
1742
+ }
1743
+
1744
+ // Insert before current row
1745
+ row.insertAdjacentElement('beforebegin', newRow)
1746
+
1747
+ updateContentWithHistory(doc)
1748
+ }
1749
+
1750
+ function insertRowBelow() {
1751
+ const doc = editor.state.doc
1752
+ if (!doc || !contextMenuCell.value) return
1753
+
1754
+ const cell = contextMenuCell.value
1755
+ const row = cell.parentElement as HTMLTableRowElement
1756
+
1757
+ // Create new row with same number of columns
1758
+ const newRow = doc.createElement('tr')
1759
+ const columnCount = Array.from(row.cells).reduce((total, cell) => {
1760
+ return total + parseInt(cell.getAttribute('colspan') || '1')
1761
+ }, 0)
1762
+
1763
+ for (let i = 0; i < columnCount; i++) {
1764
+ const newCell = doc.createElement('td')
1765
+ newCell.innerHTML = '&nbsp;'
1766
+ newCell.style.padding = '8px'
1767
+ newCell.style.border = '1px solid #ddd'
1768
+ newRow.appendChild(newCell)
1769
+ }
1770
+
1771
+ // Insert after current row
1772
+ row.insertAdjacentElement('afterend', newRow)
1773
+
1774
+ updateContentWithHistory(doc)
1775
+ }
1776
+
1777
+ function deleteRow() {
1778
+ const doc = editor.state.doc
1779
+ if (!doc || !contextMenuCell.value) return
1780
+
1781
+ const cell = contextMenuCell.value
1782
+ const row = cell.parentElement as HTMLTableRowElement
1783
+ const table = cell.closest('table') as HTMLTableElement
1784
+
1785
+ // Don't delete if it's the only row
1786
+ const tbody = table.querySelector('tbody') || table
1787
+ const rows = tbody.querySelectorAll('tr')
1788
+
1789
+ if (rows.length <= 1) {
1790
+ alert('Cannot delete the last row')
1791
+ return
1792
+ }
1793
+
1794
+ row.remove()
1795
+ updateContentWithHistory(doc)
1796
+ }
1797
+
1798
+ function insertColumnLeft() {
1799
+ const doc = editor.state.doc
1800
+ if (!doc || !contextMenuCell.value) return
1801
+
1802
+ const cell = contextMenuCell.value
1803
+ const table = cell.closest('table') as HTMLTableElement
1804
+ const cellIndex = Array.from((cell.parentElement as HTMLTableRowElement).cells).indexOf(cell)
1805
+
1806
+ // Add new cell to each row at the same index
1807
+ const rows = table.querySelectorAll('tr')
1808
+ rows.forEach(row => {
1809
+ const newCell = doc.createElement(row.parentElement?.tagName === 'THEAD' ? 'th' : 'td')
1810
+ newCell.innerHTML = '&nbsp;'
1811
+ newCell.style.padding = '8px'
1812
+ newCell.style.border = '1px solid #ddd'
1813
+
1814
+ if (cellIndex < row.cells.length) {
1815
+ row.insertBefore(newCell, row.cells[cellIndex])
1816
+ } else {
1817
+ row.appendChild(newCell)
1818
+ }
1819
+ })
1820
+
1821
+ updateContentWithHistory(doc)
1822
+ }
1823
+
1824
+ function insertColumnRight() {
1825
+ const doc = editor.state.doc
1826
+ if (!doc || !contextMenuCell.value) return
1827
+
1828
+ const cell = contextMenuCell.value
1829
+ const table = cell.closest('table') as HTMLTableElement
1830
+ const cellIndex = Array.from((cell.parentElement as HTMLTableRowElement).cells).indexOf(cell)
1831
+
1832
+ // Add new cell to each row after the current index
1833
+ const rows = table.querySelectorAll('tr')
1834
+ rows.forEach(row => {
1835
+ const newCell = doc.createElement(row.parentElement?.tagName === 'THEAD' ? 'th' : 'td')
1836
+ newCell.innerHTML = '&nbsp;'
1837
+ newCell.style.padding = '8px'
1838
+ newCell.style.border = '1px solid #ddd'
1839
+
1840
+ const targetIndex = cellIndex + 1
1841
+ if (targetIndex < row.cells.length) {
1842
+ row.insertBefore(newCell, row.cells[targetIndex])
1843
+ } else {
1844
+ row.appendChild(newCell)
1845
+ }
1846
+ })
1847
+
1848
+ updateContentWithHistory(doc)
1849
+ }
1850
+
1851
+ function deleteColumn() {
1852
+ const doc = editor.state.doc
1853
+ if (!doc || !contextMenuCell.value) return
1854
+
1855
+ const cell = contextMenuCell.value
1856
+ const table = cell.closest('table') as HTMLTableElement
1857
+ const cellIndex = Array.from((cell.parentElement as HTMLTableRowElement).cells).indexOf(cell)
1858
+
1859
+ // Check if this is the only column
1860
+ const firstRow = table.querySelector('tr')
1861
+ if (firstRow && firstRow.cells.length <= 1) {
1862
+ alert('Cannot delete the last column')
1863
+ return
1864
+ }
1865
+
1866
+ // Remove cell from each row at the same index
1867
+ const rows = table.querySelectorAll('tr')
1868
+ rows.forEach(row => {
1869
+ if (row.cells[cellIndex]) {
1870
+ row.cells[cellIndex].remove()
1871
+ }
1872
+ })
1873
+
1874
+ updateContentWithHistory(doc)
1875
+ }
1876
+
1877
+ // Table context menu functions
1878
+ const canMergeRight = computed(() => {
1879
+ if (!contextMenuCell.value) return false
1880
+ const cell = contextMenuCell.value
1881
+ const row = cell.parentElement as HTMLTableRowElement
1882
+ const cellIndex = Array.from(row.cells).indexOf(cell)
1883
+ const table = cell.closest('table') as HTMLTableElement
1884
+ const isRTL = table?.dir === 'rtl'
1885
+ const targetIndex = isRTL ? cellIndex - 1 : cellIndex + 1
1886
+ return targetIndex >= 0 && targetIndex < row.cells.length
1887
+ })
1888
+
1889
+ const canMergeDown = computed(() => {
1890
+ if (!contextMenuCell.value) return false
1891
+ const cell = contextMenuCell.value
1892
+ const table = cell.closest('table') as HTMLTableElement
1893
+ const rows = Array.from(table.rows)
1894
+ const currentRow = cell.parentElement as HTMLTableRowElement
1895
+ const currentRowIndex = rows.indexOf(currentRow)
1896
+ return currentRowIndex < rows.length - 1
1897
+ })
1898
+
1899
+ const canSplit = computed(() => {
1900
+ if (!contextMenuCell.value) return false
1901
+ const cell = contextMenuCell.value
1902
+ const colspan = parseInt(cell.getAttribute('colspan') || '1')
1903
+ const rowspan = parseInt(cell.getAttribute('rowspan') || '1')
1904
+ return colspan > 1 || rowspan > 1
1905
+ })
1906
+
1907
+ function handleTableContextMenu(event: MouseEvent) {
1908
+ const target = event.target as HTMLElement
1909
+ const cell = target.closest('td, th') as HTMLTableCellElement
1910
+
1911
+ if (cell && cell.closest('table')) {
1912
+ event.preventDefault()
1913
+ contextMenuCell.value = cell
1914
+ contextMenuPosition.value = { x: event.clientX, y: event.clientY }
1915
+ showTableContextMenu.value = true
1916
+ }
1917
+ }
1918
+
1919
+ // Close context menu when clicking outside
1920
+ function closeTableContextMenu() {
1921
+ showTableContextMenu.value = false
1922
+ contextMenuCell.value = null
1923
+ }
1924
+
1925
+ // Handle clicks outside context menu
1926
+ function handleDocumentClick(event: MouseEvent) {
1927
+ if (showTableContextMenu.value) {
1928
+ const target = event.target as HTMLElement
1929
+ const contextMenu = target.closest('.table-context-menu')
1930
+ if (!contextMenu) {
1931
+ closeTableContextMenu()
1932
+ }
1933
+ }
1934
+ }
1935
+
26
1936
  const commands = useCommands(editor.state, editor.state.debug)
27
1937
 
28
1938
  // Expose debug methods if debug mode is enabled
@@ -36,18 +1946,148 @@ const editorHeight = $computed(() => {
36
1946
  } else if (typeof props.height === 'string') {
37
1947
  return props.height
38
1948
  }
39
- return '240px' // default height
40
- })
1949
+ return '240px' // default height
1950
+ })
1951
+
1952
+ // Cleanup on component unmount
1953
+ onUnmounted(() => {
1954
+ editor.cleanup()
1955
+ })
1956
+
1957
+ function setupTableEditButtons(doc: Document) {
1958
+ console.log('setupTableEditButtons called with doc:', doc)
1959
+
1960
+ // Simple function to add edit buttons to all tables
1961
+ function addEditButtonsToTables() {
1962
+ console.log('Adding edit buttons to tables...')
1963
+ const tables = doc.querySelectorAll('table:not([data-edit-button-added])') as NodeListOf<HTMLTableElement>
1964
+ console.log('Found tables:', tables.length)
1965
+
1966
+ tables.forEach((table, index) => {
1967
+ console.log(`Processing table ${index + 1}`)
1968
+
1969
+ // Create edit button as a span element instead of button
1970
+ const editBtn = doc.createElement('span')
1971
+ editBtn.className = 'table-edit-btn'
1972
+ editBtn.textContent = '✎ Edit'
1973
+ editBtn.title = 'Edit Table'
1974
+
1975
+ // Prevent any content insertion into button
1976
+ editBtn.setAttribute('contenteditable', 'false')
1977
+ editBtn.setAttribute('tabindex', '-1')
1978
+ editBtn.style.pointerEvents = 'auto'
1979
+ editBtn.style.userSelect = 'none'
1980
+
1981
+ editBtn.addEventListener('click', (e) => {
1982
+ console.log('Edit button clicked!')
1983
+ e.preventDefault()
1984
+ e.stopPropagation()
1985
+ e.stopImmediatePropagation()
1986
+ openTableEditor(table)
1987
+ })
1988
+
1989
+ // Prevent focus and content insertion
1990
+ editBtn.addEventListener('focus', (e) => {
1991
+ e.preventDefault()
1992
+ e.stopPropagation()
1993
+ editBtn.blur()
1994
+ })
1995
+
1996
+ editBtn.addEventListener('mousedown', (e) => {
1997
+ e.preventDefault()
1998
+ e.stopPropagation()
1999
+ })
2000
+
2001
+ // Create a wrapper div to contain both table and button
2002
+ const wrapper = doc.createElement('div')
2003
+ wrapper.className = 'table-wrapper'
2004
+ wrapper.style.position = 'relative'
2005
+ wrapper.style.display = 'flex'
2006
+ wrapper.style.flexDirection = 'column'
2007
+ wrapper.style.width = '100%'
2008
+ wrapper.style.marginBottom = '1rem'
2009
+
2010
+ // Copy table alignment to wrapper and convert to flex
2011
+ if (table.style.marginLeft && table.style.marginRight) {
2012
+ // Detect current alignment
2013
+ let alignment = 'left'
2014
+ if (table.style.marginLeft === 'auto' && table.style.marginRight === 'auto') {
2015
+ alignment = 'center'
2016
+ } else if (table.style.marginLeft === 'auto') {
2017
+ alignment = 'right'
2018
+ }
2019
+
2020
+ // Apply flex alignment to wrapper
2021
+ if (alignment === 'center') {
2022
+ wrapper.style.alignItems = 'center'
2023
+ } else if (alignment === 'right') {
2024
+ wrapper.style.alignItems = 'flex-end'
2025
+ } else {
2026
+ wrapper.style.alignItems = 'flex-start'
2027
+ }
2028
+
2029
+ // Reset table margins since wrapper handles alignment now
2030
+ table.style.marginLeft = '0'
2031
+ table.style.marginRight = '0'
2032
+ } else {
2033
+ // Default to left alignment
2034
+ wrapper.style.alignItems = 'flex-start'
2035
+ }
2036
+
2037
+ // Insert wrapper before table
2038
+ table.parentNode?.insertBefore(wrapper, table)
2039
+ // Move table into wrapper
2040
+ wrapper.appendChild(table)
2041
+ // Add button to wrapper (not to table)
2042
+ wrapper.appendChild(editBtn)
2043
+
2044
+ // Remove margin from table since wrapper handles it
2045
+ table.style.marginBottom = '0'
2046
+
2047
+ table.setAttribute('data-edit-button-added', 'true')
2048
+
2049
+ console.log(`Added edit button to table ${index + 1}`)
2050
+ })
2051
+ }
41
2052
 
42
- // Cleanup on component unmount
43
- onUnmounted(() => {
44
- editor.cleanup()
45
- })
2053
+ // Store function for external access
2054
+ ; (doc as any).__addEditButtonsToTables = addEditButtonsToTables
2055
+
2056
+ // Add buttons immediately
2057
+ addEditButtonsToTables()
2058
+ }
46
2059
 
47
2060
  function setupAutoWrapping(doc: Document) {
48
- // Initialize editor with paragraph
2061
+ // Function to remove placeholder
2062
+ function removePlaceholder() {
2063
+ const placeholderElement = doc.querySelector('.placeholder')
2064
+ if (placeholderElement) {
2065
+ placeholderElement.remove()
2066
+ // Add empty paragraph if body becomes empty
2067
+ if (!doc.body.innerHTML.trim()) {
2068
+ const direction = getCurrentDirection()
2069
+ doc.body.innerHTML = `<p dir="${direction}"><br></p>`
2070
+ }
2071
+ }
2072
+ }
2073
+
2074
+ // Function to add placeholder if content is empty
2075
+ function addPlaceholder() {
2076
+ const direction = getCurrentDirection()
2077
+ const emptyContent = `<p dir="${direction}"><br></p>`
2078
+ if (props.placeholder && (!doc.body.innerHTML.trim() || doc.body.innerHTML === emptyContent)) {
2079
+ doc.body.innerHTML = `<p class="placeholder">${props.placeholder}</p>`
2080
+ }
2081
+ }
2082
+
2083
+ // Initialize editor with paragraph or placeholder
49
2084
  if (!doc.body.innerHTML.trim()) {
50
- doc.body.innerHTML = '<p><br></p>'
2085
+ if (props.placeholder) {
2086
+ addPlaceholder()
2087
+ } else {
2088
+ const direction = getCurrentDirection()
2089
+ doc.body.innerHTML = `<p dir="${direction}"><br></p>`
2090
+ }
51
2091
  }
52
2092
 
53
2093
  // After any change, ensure proper structure
@@ -59,7 +2099,8 @@ function setupAutoWrapping(doc: Document) {
59
2099
 
60
2100
  // If body is completely empty, add a paragraph and return
61
2101
  if (!doc.body.innerHTML.trim() || doc.body.innerHTML === '') {
62
- doc.body.innerHTML = '<p><br></p>'
2102
+ const direction = getCurrentDirection()
2103
+ doc.body.innerHTML = `<p dir="${direction}"><br></p>`
63
2104
  return
64
2105
  }
65
2106
 
@@ -115,9 +2156,14 @@ function setupAutoWrapping(doc: Document) {
115
2156
  }
116
2157
  })
117
2158
 
118
- // Ensure empty body has a paragraph
2159
+ // Ensure empty body has a paragraph or placeholder
119
2160
  if (!doc.body.children.length || !doc.body.innerHTML.trim()) {
120
- doc.body.innerHTML = '<p><br></p>'
2161
+ if (props.placeholder) {
2162
+ addPlaceholder()
2163
+ } else {
2164
+ const direction = getCurrentDirection()
2165
+ doc.body.innerHTML = `<p dir="${direction}"><br></p>`
2166
+ }
121
2167
  }
122
2168
 
123
2169
  // Clean up empty paragraphs more conservatively
@@ -147,14 +2193,23 @@ function setupAutoWrapping(doc: Document) {
147
2193
 
148
2194
  // Handle input events
149
2195
  doc.addEventListener('input', (e) => {
2196
+ // Remove placeholder on first input
2197
+ removePlaceholder()
2198
+
2199
+ // Detect direction based on content
2200
+ detectAndSetDirection()
2201
+
150
2202
  // Handle complete content deletion immediately
151
2203
  if (!doc.body.innerHTML.trim() || doc.body.innerHTML === '') {
152
- doc.body.innerHTML = '<p><br></p>'
153
- editor.state.content = doc.body.innerHTML
2204
+ if (props.placeholder) {
2205
+ addPlaceholder()
2206
+ } else {
2207
+ const direction = getCurrentDirection()
2208
+ doc.body.innerHTML = `<p dir="${direction}"><br></p>`
2209
+ }
2210
+ updateContentWithHistory(doc)
154
2211
  return
155
- }
156
-
157
- // Don't normalize during normal typing - only on paste/drop
2212
+ } // Don't normalize during normal typing - only on paste/drop
158
2213
  const inputEvent = e as InputEvent
159
2214
  const normalizeInputTypes = ['insertFromPaste', 'insertFromDrop']
160
2215
 
@@ -167,7 +2222,7 @@ function setupAutoWrapping(doc: Document) {
167
2222
  }
168
2223
 
169
2224
  // Always update content to keep state in sync
170
- editor.state.content = doc.body.innerHTML
2225
+ updateContentWithHistory(doc)
171
2226
  })
172
2227
 
173
2228
  // Handle Enter key
@@ -282,7 +2337,7 @@ function setupAutoWrapping(doc: Document) {
282
2337
  selection.addRange(newRange)
283
2338
 
284
2339
  // Update content immediately to reflect changes
285
- editor.state.content = doc.body.innerHTML
2340
+ updateContentWithHistory(doc)
286
2341
  }
287
2342
  })
288
2343
 
@@ -291,11 +2346,9 @@ function setupAutoWrapping(doc: Document) {
291
2346
  // Give the paste operation time to complete before normalizing
292
2347
  setTimeout(() => {
293
2348
  normalizeContent()
294
- editor.state.content = doc.body.innerHTML
2349
+ updateContentWithHistory(doc)
295
2350
  }, 150) // Longer timeout for reliable paste processing
296
- })
297
-
298
- // Add a MutationObserver to catch structural changes that might create loose text
2351
+ }) // Add a MutationObserver to catch structural changes that might create loose text
299
2352
  const observer = new MutationObserver((mutations) => {
300
2353
  for (const mutation of mutations) {
301
2354
  if (mutation.type === 'childList') {
@@ -309,7 +2362,39 @@ function setupAutoWrapping(doc: Document) {
309
2362
  p.dir = doc.body.dir || 'ltr'
310
2363
  p.textContent = addedNode.textContent
311
2364
  addedNode.parentNode.replaceChild(p, addedNode)
312
- editor.state.content = doc.body.innerHTML
2365
+ updateContentWithHistory(doc)
2366
+ }
2367
+ // Check if a table was added
2368
+ else if (addedNode.nodeType === Node.ELEMENT_NODE) {
2369
+ const element = addedNode as HTMLElement
2370
+ if (element.tagName === 'TABLE') {
2371
+ console.log('MutationObserver detected new table:', element)
2372
+ setTimeout(() => {
2373
+ console.log('Applying default table settings...')
2374
+ applyDefaultTableSettings(element as HTMLTableElement)
2375
+ console.log('Adding edit buttons to newly detected table...')
2376
+ setupTableEditButtons(doc)
2377
+ }, 100)
2378
+ }
2379
+ // Apply direction to blockquotes and lists
2380
+ else if (['BLOCKQUOTE', 'UL', 'OL'].includes(element.tagName)) {
2381
+ console.log('MutationObserver detected new', element.tagName, ':', element)
2382
+ if (!element.dir) {
2383
+ element.dir = doc.body.dir || 'ltr'
2384
+ console.log('Applied direction to', element.tagName, ':', element.dir)
2385
+ }
2386
+
2387
+ // Also apply direction to list items if it's a list
2388
+ if (element.tagName === 'UL' || element.tagName === 'OL') {
2389
+ const listItems = element.querySelectorAll('li')
2390
+ listItems.forEach(li => {
2391
+ if (!li.dir) {
2392
+ li.dir = element.dir
2393
+ console.log('Applied direction to new list item:', li.dir)
2394
+ }
2395
+ })
2396
+ }
2397
+ }
313
2398
  }
314
2399
  })
315
2400
  }
@@ -326,6 +2411,101 @@ function setupAutoWrapping(doc: Document) {
326
2411
  if (!doc.body.dataset.observers) {
327
2412
  doc.body.dataset.observers = 'mutation'
328
2413
  }
2414
+
2415
+ // Handle clicks on links to open link modal
2416
+ doc.addEventListener('click', (e) => {
2417
+ const target = e.target as HTMLElement
2418
+ const link = target.closest('a')
2419
+ const videoFigure = target.closest('.video-figure')
2420
+ const imageFigure = target.closest('.image-figure')
2421
+ const embedFigure = target.closest('.embed-figure')
2422
+ const table = target.closest('table')
2423
+
2424
+ if (table) {
2425
+ // Table clicks are handled only by edit button, not by direct clicks
2426
+ // No action needed for regular table clicks
2427
+ }
2428
+ else if (videoFigure) {
2429
+ e.preventDefault()
2430
+ e.stopPropagation()
2431
+
2432
+ // Open video modal for editing
2433
+ openVideoModal(videoFigure as HTMLElement)
2434
+ }
2435
+ else if (imageFigure) {
2436
+ e.preventDefault()
2437
+ e.stopPropagation()
2438
+
2439
+ // Open image modal for editing
2440
+ openImageModal(imageFigure as HTMLElement)
2441
+ }
2442
+ else if (embedFigure) {
2443
+ e.preventDefault()
2444
+ e.stopPropagation()
2445
+
2446
+ // Open embed modal for editing
2447
+ openEmbedModal(embedFigure as HTMLElement)
2448
+ }
2449
+ else if (link && link.href) {
2450
+ e.preventDefault()
2451
+ e.stopPropagation()
2452
+
2453
+ // Get current selection
2454
+ const selection = doc.getSelection()
2455
+ if (!selection) return
2456
+
2457
+ // Create a range that selects the entire link
2458
+ const range = doc.createRange()
2459
+ range.selectNodeContents(link)
2460
+ selection.removeAllRanges()
2461
+ selection.addRange(range)
2462
+
2463
+ // Populate the form with existing link data
2464
+ linkForm.value.url = link.href
2465
+ linkForm.value.openInNewTab = link.target === '_blank'
2466
+
2467
+ // Store the link data for editing
2468
+ pendingLinkData = {
2469
+ selection,
2470
+ range,
2471
+ existingLink: link
2472
+ }
2473
+
2474
+ showLinkModal.value = true
2475
+ }
2476
+ })
2477
+
2478
+ // Handle focus to remove placeholder when user starts interacting
2479
+ doc.addEventListener('focus', () => {
2480
+ // If content is just placeholder, remove it when focusing
2481
+ const placeholderElement = doc.querySelector('.placeholder')
2482
+ if (placeholderElement) {
2483
+ // Clear content and add empty paragraph for typing
2484
+ const direction = getCurrentDirection()
2485
+ doc.body.innerHTML = `<p dir="${direction}"><br></p>`
2486
+ // Set cursor in the paragraph
2487
+ const p = doc.body.querySelector('p')
2488
+ if (p) {
2489
+ const range = doc.createRange()
2490
+ range.selectNodeContents(p)
2491
+ range.collapse(true)
2492
+ const selection = doc.getSelection()
2493
+ if (selection) {
2494
+ selection.removeAllRanges()
2495
+ selection.addRange(range)
2496
+ }
2497
+ }
2498
+ updateContentWithHistory(doc)
2499
+ }
2500
+ }) // Handle blur to add placeholder back if content is empty
2501
+ doc.addEventListener('blur', () => {
2502
+ const direction = getCurrentDirection()
2503
+ const emptyContent = `<p dir="${direction}"><br></p>`
2504
+ if (props.placeholder && (!doc.body.innerHTML.trim() || doc.body.innerHTML === emptyContent)) {
2505
+ addPlaceholder()
2506
+ updateContentWithHistory(doc)
2507
+ }
2508
+ })
329
2509
  }
330
2510
 
331
2511
  const initEditor = async () => {
@@ -336,10 +2516,77 @@ const initEditor = async () => {
336
2516
  isInitializing.value = true
337
2517
 
338
2518
  try {
339
- // Load styles first
340
- const editorStyles = await import('./editor.css?inline')
2519
+ // Use basic embedded styles for better compatibility
2520
+ let editorStylesContent = `
2521
+ body {
2522
+ margin: 0;
2523
+ padding: 8px;
2524
+ min-height: 200px;
2525
+ font-family: sans-serif !important;
2526
+ line-height: 1.5;
2527
+ color: inherit;
2528
+ background: transparent;
2529
+ max-width: 1060px;
2530
+ margin: 0 auto;
2531
+ }
2532
+ table {
2533
+ border-collapse: collapse;
2534
+ margin-bottom: 1rem;
2535
+ }
2536
+ th, td {
2537
+ padding: 1rem;
2538
+ border: 1px solid #2a2a2a;
2539
+ line-height: 1.5;
2540
+ text-align: unset;
2541
+ }
2542
+ th {
2543
+ background-color: #f4f4f4;
2544
+ }
2545
+
2546
+ /* Table edit button styles */
2547
+ .table-edit-btn {
2548
+ position: absolute;
2549
+ top: -8px;
2550
+ right: -8px;
2551
+ background: #007bff;
2552
+ color: white;
2553
+ border: none;
2554
+ border-radius: 4px;
2555
+ padding: 4px 8px;
2556
+ font-size: 12px;
2557
+ cursor: pointer;
2558
+ opacity: 1;
2559
+ z-index: 99;
2560
+ font-family: sans-serif;
2561
+ pointer-events: auto;
2562
+ user-select: none;
2563
+ display: block;
2564
+ line-height: 1;
2565
+ /* Prevent content insertion */
2566
+ contenteditable: false !important;
2567
+ outline: none !important;
2568
+ }
341
2569
 
342
- // Create a complete HTML document with proper doctype and meta tags
2570
+ .table-edit-btn:hover {
2571
+ background: #0056b3;
2572
+ }
2573
+
2574
+ /* Prevent any focus or content insertion */
2575
+ .table-edit-btn:focus {
2576
+ outline: none !important;
2577
+ background: #007bff;
2578
+ }
2579
+
2580
+ .table-edit-btn:active {
2581
+ background: #0056b3;
2582
+ }
2583
+ .richtext-editor-content blockquote {
2584
+ padding: 8px !important;
2585
+ background: #f4f4f4 !important;
2586
+ border-inline-start: 4px solid #ccc !important;
2587
+ }
2588
+ ` // Create a complete HTML document with proper doctype and meta tags
2589
+ const initialContent = props.modelValue || (props.placeholder ? `<p class="placeholder">${props.placeholder}</p>` : '')
343
2590
  const htmlContent = `
344
2591
  <!DOCTYPE html>
345
2592
  <html>
@@ -356,9 +2603,17 @@ const initEditor = async () => {
356
2603
  media-src *;
357
2604
  ">
358
2605
  <base target="_blank">
359
- <style id="editor-styles">${editorStyles.default}</style>
2606
+ <style id="editor-styles">
2607
+ ${editorStylesContent}
2608
+ .placeholder {
2609
+ color: #9ca3af;
2610
+ font-style: italic;
2611
+ pointer-events: none;
2612
+ user-select: none;
2613
+ }
2614
+ </style>
360
2615
  </head>
361
- <body>${props.modelValue || ''}</body>
2616
+ <body class="richtext-editor-content">${initialContent}</body>
362
2617
  </html>
363
2618
  `
364
2619
 
@@ -381,12 +2636,96 @@ const initEditor = async () => {
381
2636
  // Set default direction based on content
382
2637
  doc.body.dir = hasRTL ? 'rtl' : 'ltr'
383
2638
 
2639
+ // Apply direction to existing blockquotes and lists
2640
+ const blockElements = doc.body.querySelectorAll('blockquote, ul, ol')
2641
+ console.log('Found existing block elements:', blockElements.length)
2642
+ blockElements.forEach(element => {
2643
+ const htmlElement = element as HTMLElement
2644
+ console.log('Processing element:', htmlElement.tagName, 'current dir:', htmlElement.dir)
2645
+ if (!htmlElement.dir) {
2646
+ htmlElement.dir = doc.body.dir || 'ltr'
2647
+ console.log('Applied direction to existing', htmlElement.tagName, ':', htmlElement.dir)
2648
+ }
2649
+
2650
+ // Also apply direction to list items
2651
+ if (htmlElement.tagName === 'UL' || htmlElement.tagName === 'OL') {
2652
+ const listItems = htmlElement.querySelectorAll('li')
2653
+ listItems.forEach(li => {
2654
+ const listItem = li as HTMLElement
2655
+ if (!listItem.dir) {
2656
+ listItem.dir = htmlElement.dir
2657
+ console.log('Applied direction to list item:', listItem.dir)
2658
+ }
2659
+ })
2660
+ }
2661
+ })
2662
+
384
2663
  // Ensure editor.state.content is set to the current HTML content
385
2664
  editor.state.content = doc.body.innerHTML
386
2665
 
387
2666
  editor.init(doc)
388
2667
  useEditorKeyboard(doc, commands)
389
2668
 
2669
+ // Auto clear format for basic mode
2670
+ if (props.basic && props.modelValue) {
2671
+ setTimeout(() => {
2672
+ // Clear all formatting by removing styles and replacing with plain text
2673
+ const elements = doc.body.querySelectorAll('*')
2674
+ elements.forEach(el => {
2675
+ if (el.tagName !== 'P' && el.tagName !== 'BR') {
2676
+ const textContent = el.textContent || ''
2677
+ if (textContent.trim()) {
2678
+ const p = doc.createElement('p')
2679
+ p.textContent = textContent
2680
+ el.parentNode?.replaceChild(p, el)
2681
+ } else {
2682
+ el.remove()
2683
+ }
2684
+ } else if (el.tagName === 'P') {
2685
+ // Remove all attributes from paragraphs
2686
+ Array.from(el.attributes).forEach(attr => {
2687
+ if (attr.name !== 'dir') {
2688
+ el.removeAttribute(attr.name)
2689
+ }
2690
+ })
2691
+ }
2692
+ })
2693
+ updateContentWithHistory(doc)
2694
+ }, 100)
2695
+ }
2696
+
2697
+ // Add inline toolbar selection listener
2698
+ doc.addEventListener('selectionchange', () => {
2699
+ setTimeout(() => showInlineToolbarForSelection(), 10)
2700
+ })
2701
+
2702
+ // Also listen for mouseup to handle manual selections
2703
+ doc.addEventListener('mouseup', () => {
2704
+ setTimeout(() => showInlineToolbarForSelection(), 10)
2705
+ })
2706
+
2707
+ // Hide inline toolbar when clicking outside or when selection is lost
2708
+ doc.addEventListener('click', (e) => {
2709
+ setTimeout(() => {
2710
+ const selection = doc.getSelection()
2711
+ if (!selection || selection.rangeCount === 0 || selection.getRangeAt(0).collapsed) {
2712
+ hideInlineToolbar()
2713
+ }
2714
+ }, 10)
2715
+ })
2716
+
2717
+ // Also hide when pressing keyboard arrows or escape
2718
+ doc.addEventListener('keydown', (e) => {
2719
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Escape'].includes(e.key)) {
2720
+ setTimeout(() => {
2721
+ const selection = doc.getSelection()
2722
+ if (!selection || selection.rangeCount === 0 || selection.getRangeAt(0).collapsed) {
2723
+ hideInlineToolbar()
2724
+ }
2725
+ }, 10)
2726
+ }
2727
+ })
2728
+
390
2729
  // Clean up any existing content and convert direct text nodes to paragraphs
391
2730
  const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT)
392
2731
  const textNodes: Text[] = []
@@ -407,11 +2746,52 @@ const initEditor = async () => {
407
2746
  }
408
2747
  })
409
2748
 
2749
+ // Add table context menu listener
2750
+ doc.addEventListener('contextmenu', (e) => {
2751
+ const target = e.target as HTMLElement
2752
+ const cell = target.closest('td, th') as HTMLTableCellElement
2753
+
2754
+ if (cell && cell.closest('table')) {
2755
+ e.preventDefault()
2756
+ contextMenuCell.value = cell
2757
+
2758
+ // Get iframe position relative to viewport
2759
+ const iframeRect = iframe.value!.getBoundingClientRect()
2760
+ contextMenuPosition.value = {
2761
+ x: iframeRect.left + e.clientX,
2762
+ y: iframeRect.top + e.clientY
2763
+ }
2764
+ showTableContextMenu.value = true
2765
+ }
2766
+ })
2767
+
2768
+ // Add click listener to highlight selected table cell
2769
+ doc.addEventListener('click', (e) => {
2770
+ // Remove previous highlights
2771
+ doc.querySelectorAll('.table-cell-selected').forEach(cell => {
2772
+ cell.classList.remove('table-cell-selected')
2773
+ })
2774
+
2775
+ const target = e.target as HTMLElement
2776
+ const cell = target.closest('td, th') as HTMLTableCellElement
2777
+
2778
+ if (cell && cell.closest('table')) {
2779
+ // Highlight the selected cell
2780
+ cell.classList.add('table-cell-selected')
2781
+ contextMenuCell.value = cell
2782
+ }
2783
+ })
2784
+
410
2785
  // Setup auto-wrapping for typed content
411
2786
  setupAutoWrapping(doc)
412
2787
 
2788
+ // Setup table edit buttons
2789
+ console.log('About to setup table edit buttons...')
2790
+ setupTableEditButtons(doc)
2791
+ console.log('Table edit buttons setup completed')
2792
+
413
2793
  // Update state.content after cleanup
414
- editor.state.content = doc.body.innerHTML
2794
+ updateContentWithHistory(doc)
415
2795
 
416
2796
  // If editor is empty, add an initial paragraph
417
2797
  if (!doc.body.innerHTML.trim() || !doc.body.querySelector('p,h1,h2,h3,h4,h5,h6,blockquote,ul,ol,table')) {
@@ -429,10 +2809,14 @@ const initEditor = async () => {
429
2809
  selection.addRange(range)
430
2810
  }
431
2811
 
432
- editor.state.content = doc.body.innerHTML
2812
+ updateContentWithHistory(doc)
2813
+ }
2814
+
2815
+ // Only focus if autofocus is explicitly set to true
2816
+ if (props.autofocus === true) {
2817
+ doc.body.focus()
433
2818
  }
434
2819
 
435
- doc.body.focus()
436
2820
  hasInitialized.value = true
437
2821
  } catch (error) {
438
2822
  // Keep only this error log for debugging critical issues
@@ -448,8 +2832,24 @@ watch(() => props.modelValue, (newValue, oldValue) => {
448
2832
  // Only reset if content change is significant (not just minor edits)
449
2833
  if (!oldValue || Math.abs(newValue.length - oldValue.length) > 50) {
450
2834
  hasInitialized.value = false
2835
+ // For external changes, update content directly but then push to history
451
2836
  editor.state.content = newValue
452
2837
  editor.updateState.content('html')
2838
+ // Add this external change to history after a brief delay
2839
+ setTimeout(() => {
2840
+ if (editor.state.doc) {
2841
+ updateContentWithHistory(editor.state.doc)
2842
+ // Also setup table edit buttons for any new tables
2843
+ setupTableEditButtons(editor.state.doc)
2844
+ }
2845
+ }, 100)
2846
+ } else {
2847
+ // For minor changes, still check for new tables
2848
+ setTimeout(() => {
2849
+ if (editor.state.doc) {
2850
+ setupTableEditButtons(editor.state.doc)
2851
+ }
2852
+ }, 50)
453
2853
  }
454
2854
  }
455
2855
  })
@@ -461,6 +2861,45 @@ watch(() => editor.state.content, (newValue) => {
461
2861
  emit('update:modelValue', newValue)
462
2862
  })
463
2863
 
2864
+ // Watch table alignment changes and update the table immediately
2865
+ watch(() => tableForm.value.alignment, (newAlignment) => {
2866
+ if (pendingTableData?.existingTable) {
2867
+ const table = pendingTableData.existingTable
2868
+ if (newAlignment === 'center') {
2869
+ table.style.marginLeft = 'auto'
2870
+ table.style.marginRight = 'auto'
2871
+ } else if (newAlignment === 'right') {
2872
+ table.style.marginLeft = 'auto'
2873
+ table.style.marginRight = '0'
2874
+ } else {
2875
+ table.style.marginLeft = '0'
2876
+ table.style.marginRight = 'auto'
2877
+ }
2878
+
2879
+ // Update content with history
2880
+ if (editor.state.doc) {
2881
+ updateContentWithHistory(editor.state.doc)
2882
+ }
2883
+ }
2884
+ })
2885
+
2886
+ // Close context menu when clicking outside
2887
+ const handleGlobalClick = () => {
2888
+ showTableContextMenu.value = false
2889
+ }
2890
+
2891
+ // Add event listener when component mounts
2892
+ if (typeof window !== 'undefined') {
2893
+ window.addEventListener('click', handleGlobalClick)
2894
+ }
2895
+
2896
+ // Cleanup event listener
2897
+ onBeforeUnmount(() => {
2898
+ if (typeof window !== 'undefined') {
2899
+ window.removeEventListener('click', handleGlobalClick)
2900
+ }
2901
+ })
2902
+
464
2903
  // Expose for testing
465
2904
  defineExpose({
466
2905
  editor,
@@ -469,56 +2908,324 @@ defineExpose({
469
2908
  </script>
470
2909
 
471
2910
  <template>
472
- <div class="bagel-input">
473
- <label>{{ label }}</label>
474
-
475
- <div
476
- class="rich-text-editor rounded pt-05 px-05 pb-075"
477
- :class="{ 'fullscreen-mode': editor.state.isFullscreen }"
478
- >
479
- <EditorToolbar
480
- v-if="editor.state.hasInit" :config="toolbarConfig"
481
- :selectedStyles="editor.state.selectedStyles" @action="commands.execute"
482
- />
483
- <div class="editor-container" :class="{ 'split-view': editor.state.isSplitView }">
484
- <div
485
- class="content-area radius-05"
486
- :style="{ height: editor.state.isFullscreen ? 'calc(100vh - 4rem)' : editorHeight }"
487
- >
488
- <iframe
489
- id="rich-text-iframe" ref="iframe" class="editableContent" title="Editor" srcdoc=""
490
- 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"
491
- @load="initEditor"
492
- />
493
- </div>
494
- <CodeEditor
495
- v-if="editor.state.isSplitView" v-model="editor.state.content" language="html"
496
- :height="editor.state.isFullscreen ? 'calc(100vh - 4rem)' : editorHeight"
497
- @update:modelValue="editor.updateState.content('html')"
498
- />
2911
+ <div class="bagel-input" v-bind="attrs">
2912
+ <label v-if="label">{{ label }}</label>
2913
+
2914
+ <div :class="[
2915
+ {
2916
+ 'rich-text-editor pt-05 px-05 pb-075': !basic,
2917
+ 'rich-text-editor--basic': basic,
2918
+ 'fullscreen-mode': editor.state.isFullscreen
2919
+ },
2920
+ 'rounded'
2921
+ ]">
2922
+ <EditorToolbar v-if="editor.state.hasInit && shouldShowToolbar" :config="effectiveToolbarConfig" :selectedStyles="editor.state.selectedStyles" :hide-images="hideImages"
2923
+ :hide-videos="hideVideos" :hide-embeds="hideEmbed" :hide-tables="hideTables" :hide-alignment="hideAlignment" :hide-directions="hideDirections" :hide-h5-h6="hideH5H6"
2924
+ :hide="effectiveHideArray" @action="commands.execute" />
2925
+ <div class="editor-container" :class="{ 'split-view': editor.state.isSplitView, }">
2926
+ <div class="content-area radius-1" :style="{ height: editor.state.isFullscreen ? 'calc(100vh - 4rem)' : editorHeight }">
2927
+ <iframe id="rich-text-iframe" ref="iframe" class="editableContent" title="Editor" srcdoc=""
2928
+ 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"
2929
+ @load="initEditor" @contextmenu="handleTableContextMenu" />
2930
+ </div>
2931
+ <CodeEditor v-if="editor.state.isSplitView" v-model="editor.state.content" language="html" :height="editor.state.isFullscreen ? 'calc(100vh - 4rem)' : editorHeight"
2932
+ @update:modelValue="editor.updateState.content('html')" />
2933
+ </div>
2934
+ <div v-if="debug" class="flex pt-05">
2935
+ <p class="txt12 txt-gray mb-0 p-0">
2936
+ Debug
2937
+ </p>
2938
+ <Btn thin color="gray" icon="visibility" @click="debugShowContent">
2939
+ Show Content
2940
+ </Btn>
2941
+ <Btn thin color="gray" icon="delete" @click="debugMethods?.clearSession">
2942
+ Clear Session
2943
+ </Btn>
2944
+ <Btn thin color="gray" icon="download" @click="debugMethods?.downloadSession">
2945
+ Download Log
2946
+ </Btn>
2947
+ <Btn thin color="gray" icon="content_copy" @click="copyText(debugMethods?.exportDebugWithPrompt() || '')">
2948
+ Copy Log
2949
+ </Btn>
2950
+ </div>
2951
+ </div>
2952
+
2953
+ <!-- Inline Toolbar -->
2954
+ <div v-if="showInlineToolbar" class="inline-toolbar" :style="{
2955
+ position: 'fixed',
2956
+ top: inlineToolbarPosition.top + 'px',
2957
+ left: inlineToolbarPosition.left + 'px',
2958
+ zIndex: 99,
2959
+ }">
2960
+ <div class="inline-toolbar-content">
2961
+ <Btn thin flat icon="format_bold" @click="runInlineAction('bold')" :class="{ active: editor.state.selectedStyles.has('bold') }" />
2962
+ <Btn thin flat icon="format_italic" @click="runInlineAction('italic')" :class="{ active: editor.state.selectedStyles.has('italic') }" />
2963
+ <Btn thin flat icon="format_underlined" @click="runInlineAction('underline')" :class="{ active: editor.state.selectedStyles.has('underline') }" />
2964
+ <span class="separator">|</span>
2965
+ <Btn thin flat icon="add_link" @click="runInlineAction('link')" />
2966
+ </div>
2967
+ </div>
2968
+ <!-- Table Context Menu -->
2969
+ <div v-if="showTableContextMenu" class="table-context-menu" :style="{
2970
+ position: 'fixed',
2971
+ top: contextMenuPosition.y + 'px',
2972
+ left: contextMenuPosition.x + 'px',
2973
+ zIndex: 1001
2974
+ }">
2975
+ <div class="context-menu-content" @click.stop>
2976
+ <div class="menu-header">
2977
+ <span>Table Actions</span>
2978
+ <button class="close-btn" @click="showTableContextMenu = false">×</button>
499
2979
  </div>
500
- <div v-if="debug" class="flex">
501
- <p class="text12 txt-gray mb-0 p-0">
502
- Debug
503
- </p>
504
- <Btn thin color="gray" icon="delete" @click="debugMethods?.clearSession">
505
- Clear Session
2980
+ <div class="px-025">
2981
+ <Btn v-if="canMergeRight" full-width align-txt="start" thin flat icon="start" @click="mergeCellRight(); showTableContextMenu = false" class="context-menu-btn">
2982
+ Merge Right
2983
+ </Btn>
2984
+ <Btn v-if="canMergeDown" full-width align-txt="start" thin flat icon="text_select_move_down" @click="mergeCellDown(); showTableContextMenu = false" class="context-menu-btn">
2985
+ Merge Down
2986
+ </Btn>
2987
+ <Btn v-if="canSplit" full-width align-txt="start" thin flat icon="call_split" @click="splitCell(); showTableContextMenu = false" class="context-menu-btn">
2988
+ Split Cell
2989
+ </Btn>
2990
+ <div class="context-menu-separator" v-if="canMergeRight || canMergeDown || canSplit"></div>
2991
+ <Btn full-width align-txt="start" thin flat icon="add_row_above" @click="insertRowAbove(); showTableContextMenu = false" class="context-menu-btn">
2992
+ Insert Row Above
2993
+ </Btn>
2994
+ <Btn full-width align-txt="start" thin flat icon="add_row_below" @click="insertRowBelow(); showTableContextMenu = false" class="context-menu-btn">
2995
+ Insert Row Below
2996
+ </Btn>
2997
+ <Btn full-width align-txt="start" thin flat icon="remove" @click="deleteRow(); showTableContextMenu = false" class="context-menu-btn">
2998
+ Delete Row
2999
+ </Btn>
3000
+ <div class="context-menu-separator"></div>
3001
+ <Btn full-width align-txt="start" thin flat icon="add_column_left" @click="insertColumnLeft(); showTableContextMenu = false" class="context-menu-btn">
3002
+ Insert Column Left
506
3003
  </Btn>
507
- <Btn thin color="gray" icon="download" @click="debugMethods?.downloadSession">
508
- Download Log
3004
+ <Btn full-width align-txt="start" thin flat icon="add_column_right" @click="insertColumnRight(); showTableContextMenu = false" class="context-menu-btn">
3005
+ Insert Column Right
509
3006
  </Btn>
510
- <Btn
511
- thin color="gray" icon="content_copy"
512
- @click="copyText(debugMethods?.exportDebugWithPrompt() || '')"
513
- >
514
- Copy Log
3007
+ <Btn full-width align-txt="start" thin flat icon="remove" @click="deleteColumn(); showTableContextMenu = false" class="context-menu-btn">
3008
+ Delete Column
515
3009
  </Btn>
516
3010
  </div>
517
3011
  </div>
518
3012
  </div>
3013
+ </div>
3014
+ <!-- Link Modal -->
3015
+ <Modal v-model:visible="showLinkModal" title="Add Link" width="400">
3016
+ <div class="flex gap-05 align-items-end">
3017
+ <TextInput label="URL" v-model="linkForm.url" type="url" placeholder="https://example.com" @keydown.enter="submitLink" />
3018
+ <Btn @click="visitLink" icon="open_in_new" class="mb-05 radius-1" flat :disabled="!isValidUrl(linkForm.url)" />
3019
+ </div>
3020
+ <CheckInput label="Open in new tab" v-model="linkForm.openInNewTab" type="checkbox" class="mb-2 mt-05" />
3021
+ <template #footer>
3022
+ <Btn @click="showLinkModal = false" value="Cancel" flat thin />
3023
+ <Btn @click="submitLink" value="Add Link" :disabled="!isValidUrl(linkForm.url)" />
3024
+ </template>
3025
+ </Modal>
3026
+
3027
+ <!-- Tooltip -->
3028
+ <div v-if="showTooltip" class="editor-tooltip" :style="{ left: tooltipData.x + 'px', top: tooltipData.y + 'px' }">
3029
+ {{ tooltipData.message }}
3030
+ </div>
3031
+
3032
+ <!-- Image Modal -->
3033
+ <Modal v-model:visible="showImageModal" :title="pendingImageData?.existingImage ? 'Edit Image' : 'Insert Image'" width="500">
3034
+ <TextInput label="Image URL" v-model="imageForm.src" type="url" placeholder="https://example.com/image.jpg" @keydown.enter="submitImage" />
3035
+ <TextInput label="Alt Text" v-model="imageForm.alt" placeholder="Describe the image" />
3036
+ <div class="flex gap-1">
3037
+ <TextInput label="Width" v-model="imageForm.width" placeholder="100% or 500px or auto" help="Examples: 100%, 500px, auto" />
3038
+ <TextInput label="Height" v-model="imageForm.height" placeholder="auto or 300px" help="Examples: auto, 300px" />
3039
+ </div>
3040
+ <TextInput label="Photo Credit" v-model="imageForm.credit" placeholder="Photographer name (optional)" />
3041
+ <CheckInput label="Show alt text as caption" v-model="imageForm.figcaption" help="Photo credit will always be shown if provided" />
3042
+ <template #footer>
3043
+ <div class="flex gap-05 w-100 ">
3044
+ <Btn @click="showImageModal = false" value="Cancel" flat thin />
3045
+ <Btn v-if="pendingImageData?.existingImage" @click="deleteImage" value="Delete Image" color="red" flat thin icon="delete" />
3046
+ <Btn class="ms-auto" @click="submitImage" :value="pendingImageData?.existingImage ? 'Save Changes' : 'Insert Image'" :disabled="!imageForm.src" />
3047
+ </div>
3048
+ </template>
3049
+ </Modal>
3050
+
3051
+ <!-- Embed Modal -->
3052
+ <Modal v-model:visible="showEmbedModal" :title="pendingEmbedData?.existingEmbed ? 'Edit Embed' : 'Insert Embed'" width="500">
3053
+ <TextInput label="Embed URL or Code" v-model="embedForm.src" type="url" placeholder="https://www.youtube.com/embed/... or paste iframe code" @keydown.enter="submitEmbed" />
3054
+ <div class="flex gap-1">
3055
+ <TextInput label="Width" v-model="embedForm.width" placeholder="560" />
3056
+ <TextInput label="Height" v-model="embedForm.height" placeholder="315" />
3057
+ </div>
3058
+ <TextInput label="Caption" v-model="embedForm.alt" placeholder="Enter caption (optional)" />
3059
+ <template #footer>
3060
+ <div class="flex gap-05 w-100">
3061
+ <Btn @click="showEmbedModal = false" value="Cancel" flat thin />
3062
+ <Btn v-if="pendingEmbedData?.existingEmbed" @click="deleteEmbed" value="Delete Embed" color="red" flat thin icon="delete" />
3063
+ <Btn @click="submitEmbed" :value="pendingEmbedData?.existingEmbed ? 'Save Changes' : 'Insert Embed'" :disabled="!embedForm.src" class="ms-auto" />
3064
+ </div>
3065
+ </template>
3066
+ </Modal>
3067
+ <!-- Video Modal -->
3068
+ <Modal v-model:visible="showVideoModal" title="Insert Video" width="500">
3069
+ <div class="grid gap-0">
3070
+ <TextInput label="Video URL or Embed Code" v-model="videoForm.src" type="url" placeholder="Paste YouTube URL, video file URL, or iframe embed code..." @keydown.enter="submitVideo"
3071
+ :class="{ 'error': videoForm.src && !isValidVideoUrl(videoForm.src) }" />
3072
+
3073
+ <div v-if="videoForm.src && !isValidVideoUrl(videoForm.src)" class="flex gap-025 opacity-5 -mt-05">
3074
+ <Icon name="warning" />
3075
+ <p class="txt12">Please enter a valid video URL or iframe embed code</p>
3076
+ </div>
3077
+
3078
+ <TextInput label="Width" v-model="videoForm.width" placeholder="100% or 500px or 50vw" help="Examples: 100%, 500px, 50vw, 300" />
3079
+
3080
+ <SelectInput label="Aspect Ratio" v-model="videoForm.aspectRatio" :options="[
3081
+ { value: '16:9', label: '16:9 (Standard)' },
3082
+ { value: '4:3', label: '4:3 (Classic)' },
3083
+ { value: '9:16', label: '9:16 (Vertical/Shorts)' },
3084
+ { value: '21:9', label: '21:9 (Cinematic)' },
3085
+ { value: '1:1', label: '1:1 (Square)' },
3086
+ { value: 'custom', label: 'Custom' }
3087
+ ]" />
3088
+
3089
+ <div v-if="videoForm.aspectRatio === 'custom'" class="grid grid-wrap-2 gap-05">
3090
+ <TextInput label="Width ratio" v-model="videoForm.customWidth" placeholder="16" type="number" />
3091
+ <TextInput label="Height ratio" v-model="videoForm.customHeight" placeholder="9" type="number" />
3092
+ </div>
3093
+
3094
+ <div class="grid grid-wrap-2 gap-05 py-1">
3095
+ <CheckInput label="Show controls" v-model="videoForm.controls" />
3096
+ <CheckInput label="Autoplay" v-model="videoForm.autoplay" />
3097
+ <CheckInput label="Mute" v-model="videoForm.mute" />
3098
+ <CheckInput label="Loop" v-model="videoForm.loop" />
3099
+ <CheckInput class="grid-span-2" label="Show caption below video" v-model="videoForm.showCaption" />
3100
+ <TextInput v-if="videoForm.showCaption" label="Caption" class="grid-span-2" v-model="videoForm.caption" placeholder="Describe the video content" />
3101
+ </div>
3102
+
3103
+ <!-- Video Preview -->
3104
+ <Card v-if="videoForm.src && isValidVideoUrl(videoForm.src)" frame thin class="bg-gray-20">
3105
+ <p class="label">Preview:</p>
3106
+ <div class="overflow-hidden flex justify-content-center">
3107
+ <BglVideo :src="videoForm.src" class=""
3108
+ :aspect-ratio="videoForm.aspectRatio === 'custom' && videoForm.customWidth && videoForm.customHeight ? `${videoForm.customWidth}:${videoForm.customHeight}` : videoForm.aspectRatio"
3109
+ :autoplay="false" :mute="videoForm.mute" :controls="videoForm.controls" :loop="videoForm.loop" />
3110
+ </div>
3111
+ </Card>
3112
+ </div>
3113
+ <template #footer>
3114
+ <div class="flex gap-05 w-100 ">
3115
+ <Btn @click="showVideoModal = false" value="Cancel" flat thin />
3116
+ <Btn v-if="pendingVideoData?.existingVideo" @click="deleteVideo" value="Delete Video" color="red" flat thin icon="delete" />
3117
+ <Btn @click="submitVideo" value="Insert Video" class="ms-auto" :disabled="!videoForm.src || !isValidVideoUrl(videoForm.src)" />
3118
+ </div>
3119
+ </template>
3120
+ </Modal>
3121
+
3122
+ <!-- Table Editor Modal -->
3123
+ <Modal v-model:visible="showTableEditor" :title="pendingTableData?.existingTable ? 'Edit Table' : 'Insert Table'" width="700">
3124
+ <template #default>
3125
+ <div class="grid grid-wrap-4 m_grid-wrap-2 gap-col-1 table-editor testMe1">
3126
+ <!-- Structure Section -->
3127
+ <div class="flex gap-05 white-space mt-0 grid-span-4 m_grid-span-2 pt-1 pb-05">
3128
+ <div class="line"></div>
3129
+ <p class="label grid-span-4 m_grid-span-2">Table Structure</p>
3130
+ <div class="line"></div>
3131
+ </div>
3132
+ <NumberInput v-model="tableForm.rows" :min="1" :max="20" label="Rows" />
3133
+ <NumberInput v-model="tableForm.cols" :min="1" :max="10" label="Columns" />
3134
+ <!-- Cell Text Alignment -->
3135
+ <div class="grid-span-1">
3136
+ <label class="label">Cell Text Alignment</label>
3137
+ <div class="flex gap-025 mt-025 radius-1 p-05 w-fit" style="height: var(--input-height); background: var(--input-bg);">
3138
+ <Btn :class="{ 'activeBtn': tableForm.alignment === 'left' }" @click="tableForm.alignment = 'left'" flat thin icon="format_align_left" title="Align Left" />
3139
+ <Btn :class="{ 'activeBtn': tableForm.alignment === 'center' }" @click="tableForm.alignment = 'center'" flat thin icon="format_align_center" title="Align Center" />
3140
+ <Btn :class="{ 'activeBtn': tableForm.alignment === 'right' }" @click="tableForm.alignment = 'right'" flat thin icon="format_align_right" title="Align Right" />
3141
+ </div>
3142
+ </div>
3143
+
3144
+ <!-- Text Direction -->
3145
+ <div class="grid-span-1">
3146
+ <label class="label">Text Direction</label>
3147
+ <div class="flex gap-025 mt-025 radius-1 p-025 w-fit" style="height: var(--input-height); background: var(--input-bg);">
3148
+ <Btn :class="{ 'activeBtn': tableForm.direction === 'ltr' }" @click="tableForm.direction = 'ltr'" flat thin value="LTR" title="Left to Right" />
3149
+ <Btn :class="{ 'activeBtn': tableForm.direction === 'rtl' }" @click="tableForm.direction = 'rtl'" flat thin value="RTL" title="Right to Left" />
3150
+ </div>
3151
+ </div>
3152
+ <!-- Style Section -->
3153
+ <div class="flex gap-05 white-space mt-0 grid-span-4 m_grid-span-2 pt-1 pb-05">
3154
+ <div class="line"></div>
3155
+ <p class="label grid-span-4 m_grid-span-2">Table Style</p>
3156
+ <div class="line"></div>
3157
+ </div>
3158
+ <NumberInput v-model="tableForm.width" label="Width (%)" :min="25" :max="100" />
3159
+ <NumberInput v-model="tableForm.cellPadding" label="Cell Padding (px)" :min="2" :max="20" />
3160
+ <NumberInput v-model="tableForm.borderWidth" label="Border Width (px)" :min="0" :max="5" />
3161
+ <ColorInput v-model="tableForm.borderColor" label="Border Color" />
3162
+ <CheckInput v-model="tableForm.fixedLayout" label="Fixed cell width (doesn't change by content)" class="grid-span-4 m_grid-span-2" />
3163
+
3164
+ <!-- Cell Colors -->
3165
+ <div class="flex gap-05 white-space mt-0 grid-span-4 m_grid-span-2 pt-1 pb-05">
3166
+ <div class="line"></div>
3167
+ <p class="label grid-span-4 m_grid-span-2">Cell Colors:</p>
3168
+ <div class="line"></div>
3169
+ </div>
3170
+ <ColorInput class="grid-span-2 m_grid-span-1" label="Background Color" v-model="tableForm.cellBgColor" />
3171
+ <ColorInput class="grid-span-2 m_grid-span-1" label="Text Color" v-model="tableForm.cellTextColor" />
3172
+ <!-- Alternating Rows -->
3173
+ <div class="flex gap-05 white-space mt-0 grid-span-4 m_grid-span-2 pt-1 pb-05">
3174
+ <div class="line"></div>
3175
+ <p class="label grid-span-4 m_grid-span-2">Row Styles:</p>
3176
+ <div class="line"></div>
3177
+ </div>
3178
+ <CheckInput v-model="tableForm.showHeaders" label="Show header row" class="grid-span-4 m_grid-span-2" />
3179
+ <!-- Header Colors -->
3180
+ <div v-if="tableForm.showHeaders" class="grid-span-4 m_grid-span-2 grid-wrap-4 m_grid-wrap-2 grid gap-col-1 border-bottom pb-05 mb-05">
3181
+ <ColorInput class="grid-span-2 m_grid-span-1" v-model="tableForm.headerBgColor" label="Header Background Color" />
3182
+ <ColorInput class="grid-span-2 m_grid-span-1" v-model="tableForm.headerTextColor" label="Header Text Color" />
3183
+ </div>
3184
+ <CheckInput v-model="tableForm.alternateRows" label="Alternating Row Colors" class="grid-span-4 m_grid-span-2" />
3185
+ <div v-if="tableForm.alternateRows" class="grid grid-wrap-4 m_grid-wrap-2 gap-col-1 grid-span-4 m_grid-span-2 border-bottom pb-05 mb-05">
3186
+ <ColorInput class="grid-span-2 m_grid-span-1" v-model="tableForm.alternateRowBgColor" label="Alternate Row Background:" />
3187
+ <ColorInput class="grid-span-2 m_grid-span-1" v-model="tableForm.alternateRowTextColor" label="Alternate Row Text:" />
3188
+ </div>
3189
+
3190
+ </div>
3191
+ <!-- Table Preview -->
3192
+ <div class="flex gap-05 white-space mt-0 grid-span-4 m_grid-span-2 pt-1 pb-05">
3193
+ <div class="line"></div>
3194
+ <p class="label grid-span-4 m_grid-span-2">Preview</p>
3195
+ <div class="line"></div>
3196
+ </div>
3197
+ <div v-html="tablePreviewHtml" style="zoom: 0.8;" class="opacity-7 user-select-none pointer-events-none"></div>
3198
+ </template>
3199
+
3200
+ <template #footer>
3201
+ <Btn @click="showTableEditor = false" value="Cancel" flat thin />
3202
+ <Btn v-if="pendingTableData?.existingTable" @click="deleteTable" value="Delete Table" color="red" flat thin icon="delete" />
3203
+ <Btn @click="submitTable" :value="pendingTableData?.existingTable ? 'Save' : 'Insert Table'" class="ms-auto" />
3204
+ </template>
3205
+ </Modal>
519
3206
  </template>
520
3207
 
521
3208
  <style>
3209
+ .table-editor .colorInputPickWrap {
3210
+ background: var(--input-bg);
3211
+ }
3212
+
3213
+ .rich-text-editor--basic .content-area {
3214
+ background: var(--input-bg) !important;
3215
+ border: none;
3216
+ padding: 0 0.7rem;
3217
+ border-radius: var(--input-border-radius);
3218
+ color: var(--input-color);
3219
+ min-width: calc(var(--input-height) * 3);
3220
+ width: 100%;
3221
+ }
3222
+
3223
+ .rich-text-editor--basic .content-area:hover {
3224
+ outline-color: rgba(0, 0, 0, 0.05);
3225
+ box-shadow: inset 0 0 8px #00000018;
3226
+ outline-color: var(--input-bg);
3227
+ }
3228
+
522
3229
  .content-area p,
523
3230
  .content-area span,
524
3231
  .content-area li {
@@ -527,6 +3234,17 @@ defineExpose({
527
3234
  </style>
528
3235
 
529
3236
  <style scoped>
3237
+ /* Table hover and edit button styles */
3238
+ .content-area table {
3239
+ position: relative;
3240
+ }
3241
+
3242
+
3243
+ .content-area table:hover {
3244
+ outline: 2px solid rgba(0, 123, 204, 0.3);
3245
+ outline-offset: 2px;
3246
+ }
3247
+
530
3248
  .rich-text-editor {
531
3249
  background: var(--input-bg);
532
3250
  border: 1px solid var(--border-color);
@@ -581,7 +3299,7 @@ defineExpose({
581
3299
  left: 0;
582
3300
  width: 100vw;
583
3301
  height: 100vh;
584
- z-index: 9999;
3302
+ z-index: 99;
585
3303
  padding: 2rem;
586
3304
  }
587
3305
 
@@ -600,4 +3318,225 @@ defineExpose({
600
3318
  gap: 0.5rem;
601
3319
  justify-content: flex-end;
602
3320
  }
3321
+
3322
+ /* Inline Toolbar Styles */
3323
+ .inline-toolbar {
3324
+ pointer-events: all;
3325
+ }
3326
+
3327
+ .inline-toolbar-content {
3328
+ background: var(--bgl-surface, white);
3329
+ border: 1px solid var(--border-color, #dddddd);
3330
+ border-radius: 8px;
3331
+ padding: 0.25rem;
3332
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
3333
+ display: flex;
3334
+ align-items: center;
3335
+ gap: 0.125rem;
3336
+ backdrop-filter: blur(8px);
3337
+ }
3338
+
3339
+ .inline-toolbar-content .btn {
3340
+ min-width: 32px;
3341
+ height: 32px;
3342
+ padding: 0;
3343
+ }
3344
+
3345
+ .inline-toolbar-content .btn.active {
3346
+ background: var(--bgl-primary);
3347
+ color: white;
3348
+ }
3349
+
3350
+ .inline-toolbar-content .separator {
3351
+ color: var(--border-color, #dddddd);
3352
+ margin: 0 0.25rem;
3353
+ opacity: 0.5;
3354
+ }
3355
+
3356
+ /* Table Context Menu */
3357
+ .table-context-menu {
3358
+ pointer-events: auto;
3359
+ }
3360
+
3361
+ .context-menu-content {
3362
+ background: white;
3363
+ border: 1px solid var(--border-color, #dddddd);
3364
+ border-radius: var(--btn-border-radius) !important;
3365
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
3366
+ padding: 0;
3367
+ min-width: 140px;
3368
+ max-width: 180px;
3369
+ overflow: hidden;
3370
+ --input-font-size: 12px;
3371
+ }
3372
+
3373
+ .menu-header {
3374
+ display: flex;
3375
+ justify-content: space-between;
3376
+ align-items: center;
3377
+ padding: 8px 12px;
3378
+ background-color: var(--bg-light, #f8f9fa);
3379
+ border-bottom: 1px solid var(--border-color, #dddddd);
3380
+ font-size: 12px;
3381
+ font-weight: 500;
3382
+ color: var(--text-secondary, #666);
3383
+ margin-bottom: 5px;
3384
+ }
3385
+
3386
+ .close-btn {
3387
+ background: none;
3388
+ border: none;
3389
+ font-size: 16px;
3390
+ cursor: pointer;
3391
+ padding: 0;
3392
+ width: 20px;
3393
+ height: 20px;
3394
+ display: flex;
3395
+ align-items: center;
3396
+ justify-content: center;
3397
+ border-radius: 3px;
3398
+ color: var(--text-secondary, #666);
3399
+ transition: all 0.2s;
3400
+ }
3401
+
3402
+ .close-btn:hover {
3403
+ background-color: var(--hover-bg, #e9ecef);
3404
+ color: var(--text-primary, #333);
3405
+ }
3406
+
3407
+ .context-menu-content>.btn {
3408
+ margin: 2px 4px;
3409
+ }
3410
+
3411
+ .context-menu-item {
3412
+ padding: 8px 16px;
3413
+ cursor: pointer;
3414
+ transition: background-color 0.2s;
3415
+ font-size: 14px;
3416
+ }
3417
+
3418
+ .context-menu-item:hover {
3419
+ background-color: var(--hover-bg, #f5f5f5);
3420
+ }
3421
+
3422
+ .context-menu-separator {
3423
+ height: 1px;
3424
+ background-color: var(--border-color, #dddddd);
3425
+ margin: 4px 0;
3426
+ }
3427
+
3428
+ .context-menu-btn:hover {
3429
+ background-color: var(--hover-bg, #f5f5f5) !important;
3430
+ }
3431
+
3432
+ /* Table Cell Selection */
3433
+ .table-cell-selected {
3434
+ background-color: rgba(0, 123, 255, 0.1) !important;
3435
+ border: 2px solid #007bff !important;
3436
+ position: relative;
3437
+ }
3438
+
3439
+ .table-cell-selected::after {
3440
+ content: '';
3441
+ position: absolute;
3442
+ top: -2px;
3443
+ left: -2px;
3444
+ right: -2px;
3445
+ bottom: -2px;
3446
+ border: 2px solid #007bff;
3447
+ pointer-events: none;
3448
+ z-index: 1;
3449
+ }
3450
+
3451
+ /* Table Modal Button Active States */
3452
+ .activeBtn {
3453
+ background: var(--bgl-primary) !important;
3454
+ color: white !important;
3455
+ border-color: var(--bgl-primary) !important;
3456
+ }
3457
+
3458
+ /* Table Edit Button Protection */
3459
+ .table-edit-btn {
3460
+ position: absolute !important;
3461
+ top: 5px !important;
3462
+ right: 5px !important;
3463
+ background: rgba(0, 0, 0, 0.7) !important;
3464
+ color: white !important;
3465
+ padding: 4px 8px !important;
3466
+ border-radius: 4px !important;
3467
+ font-size: 12px !important;
3468
+ cursor: pointer !important;
3469
+ z-index: 100 !important;
3470
+ pointer-events: auto !important;
3471
+ user-select: none !important;
3472
+ -webkit-user-select: none !important;
3473
+ -moz-user-select: none !important;
3474
+ -ms-user-select: none !important;
3475
+ outline: none !important;
3476
+ border: none !important;
3477
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2) !important;
3478
+ white-space: nowrap !important;
3479
+ overflow: hidden !important;
3480
+ text-overflow: clip !important;
3481
+ min-width: unset !important;
3482
+ max-width: unset !important;
3483
+ width: auto !important;
3484
+ height: auto !important;
3485
+ line-height: normal !important;
3486
+ font-family: inherit !important;
3487
+ font-weight: normal !important;
3488
+ text-align: center !important;
3489
+ vertical-align: baseline !important;
3490
+ text-decoration: none !important;
3491
+ text-transform: none !important;
3492
+ letter-spacing: normal !important;
3493
+ word-spacing: normal !important;
3494
+ text-indent: 0 !important;
3495
+ text-shadow: none !important;
3496
+ direction: ltr !important;
3497
+ unicode-bidi: normal !important;
3498
+ }
3499
+
3500
+ .table-edit-btn:hover {
3501
+ background: rgba(0, 0, 0, 0.9) !important;
3502
+ }
3503
+
3504
+ /* Prevent any content insertion into table edit buttons */
3505
+ .table-edit-btn * {
3506
+ display: none !important;
3507
+ }
3508
+
3509
+ .table-edit-btn::before,
3510
+ .table-edit-btn::after {
3511
+ content: none !important;
3512
+ }
3513
+
3514
+ /* Editor Tooltip */
3515
+ .editor-tooltip {
3516
+ position: fixed;
3517
+ background: rgba(0, 0, 0, 0.9);
3518
+ color: white;
3519
+ padding: 8px 12px;
3520
+ border-radius: 6px;
3521
+ font-size: 14px;
3522
+ line-height: 1.4;
3523
+ max-width: 300px;
3524
+ word-wrap: break-word;
3525
+ z-index: 9999;
3526
+ pointer-events: none;
3527
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
3528
+ animation: tooltipFadeIn 0.2s ease-out;
3529
+ }
3530
+
3531
+ @keyframes tooltipFadeIn {
3532
+ from {
3533
+ opacity: 0;
3534
+ transform: translateY(-5px);
3535
+ }
3536
+
3537
+ to {
3538
+ opacity: 1;
3539
+ transform: translateY(0);
3540
+ }
3541
+ }
603
3542
  </style>