@bagelink/vue 1.4.149 → 1.4.151

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.
@@ -2,53 +2,54 @@
2
2
  import type { ToolbarConfig } from './richTextTypes'
3
3
  import { CodeEditor, copyText, Btn, Modal, BglVideo, Icon, Card, ColorInput } from '@bagelink/vue'
4
4
  import { watch, onUnmounted, onBeforeUnmount, ref, computed, useAttrs } from 'vue'
5
+ import CheckInput from '../CheckInput.vue'
6
+ import NumberInput from '../NumberInput.vue'
7
+ import SelectInput from '../SelectInput.vue'
8
+ import TextInput from '../TextInput.vue'
5
9
  import EditorToolbar from './components/EditorToolbar.vue'
6
10
  import { useCommands } from './composables/useCommands'
7
11
  import { useEditor } from './composables/useEditor'
8
12
  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
13
 
14
14
  // Disable automatic inheritance of non-prop attributes
15
15
  defineOptions({
16
16
  inheritAttrs: false
17
17
  })
18
18
 
19
- const attrs = useAttrs()
20
-
21
19
  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,
20
+ modelValue: string
21
+ toolbarConfig?: ToolbarConfig
22
+ debug?: boolean
23
+ label?: string
24
+ height?: number | string
25
+ basic?: boolean
26
+ simple?: boolean
27
+ placeholder?: string
30
28
  // 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,
29
+ hideToolbar?: boolean
30
+ hideInlineToolbar?: boolean
31
+ hideImages?: boolean
32
+ hideVideos?: boolean
33
+ hideEmbed?: boolean
34
+ hideTables?: boolean
35
+ hideAlignment?: boolean
36
+ hideDirections?: boolean
37
+ hideH5H6?: boolean
40
38
  // Simple array-based hide option
41
- hide?: string[],
39
+ hide?: string[]
42
40
  // Control autofocus behavior
43
41
  autofocus?: boolean
44
42
  }>()
43
+
45
44
  const emit = defineEmits(['update:modelValue'])
46
45
 
46
+ const attrs = useAttrs()
47
+
47
48
  const iframe = ref<HTMLIFrameElement>()
48
49
  const editor = useEditor()
49
50
  const isInitializing = ref(false)
50
51
  const hasInitialized = ref(false)
51
-
52
+ const commands = useCommands(editor.state, editor.state.debug)
52
53
  // Computed properties for UI control
53
54
  const shouldShowToolbar = computed(() => {
54
55
  if (props.hideToolbar) return false
@@ -73,7 +74,7 @@ const effectiveHideArray = computed(() => {
73
74
  // If simple mode is enabled, add the simple mode hide items
74
75
  if (props.simple) {
75
76
  const simpleHideItems = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'image', 'video', 'embed', 'ul', 'ol', 'blockquote', 'direction', 'table', 'alignment']
76
- simpleHideItems.forEach(item => {
77
+ simpleHideItems.forEach((item) => {
77
78
  if (!hideArray.includes(item)) {
78
79
  hideArray.push(item)
79
80
  }
@@ -90,7 +91,7 @@ const linkForm = ref({
90
91
  openInNewTab: true
91
92
  })
92
93
 
93
- // Tooltip state
94
+ // Tooltip state
94
95
  const showTooltip = ref(false)
95
96
  const tooltipData = ref({
96
97
  message: '',
@@ -99,8 +100,8 @@ const tooltipData = ref({
99
100
  })
100
101
 
101
102
  let pendingLinkData: {
102
- selection: Selection,
103
- range: Range,
103
+ selection: Selection
104
+ range: Range
104
105
  existingLink: HTMLAnchorElement | null
105
106
  } | null = null
106
107
 
@@ -115,8 +116,8 @@ const imageForm = ref({
115
116
  figcaption: false
116
117
  })
117
118
  let pendingImageData: {
118
- selection: Selection,
119
- range: Range,
119
+ selection: Selection
120
+ range: Range
120
121
  existingImage: HTMLElement | null
121
122
  } | null = null
122
123
 
@@ -129,8 +130,8 @@ const embedForm = ref({
129
130
  alt: ''
130
131
  })
131
132
  let pendingEmbedData: {
132
- selection: Selection,
133
- range: Range,
133
+ selection: Selection
134
+ range: Range
134
135
  existingEmbed: HTMLElement | null
135
136
  } | null = null
136
137
 
@@ -155,8 +156,8 @@ const videoForm = ref({
155
156
  showCaption: false
156
157
  })
157
158
  let pendingVideoData: {
158
- selection: Selection,
159
- range: Range,
159
+ selection: Selection
160
+ range: Range
160
161
  existingVideo: HTMLElement | null
161
162
  } | null = null
162
163
 
@@ -188,8 +189,8 @@ const contextMenuPosition = ref({ x: 0, y: 0 })
188
189
  const contextMenuCell = ref<HTMLTableCellElement | null>(null)
189
190
 
190
191
  let pendingTableData: {
191
- selection: Selection,
192
- range: Range,
192
+ selection: Selection
193
+ range: Range
193
194
  existingTable: HTMLTableElement | null
194
195
  } | null = null
195
196
 
@@ -197,7 +198,7 @@ let pendingTableData: {
197
198
  const tablePreviewHtml = computed(() => {
198
199
  const form = tableForm.value
199
200
 
200
- let tableStyle = `
201
+ const tableStyle = `
201
202
  width: ${form.width}%;
202
203
  border-collapse: collapse;
203
204
  margin-bottom: 1rem;
@@ -236,9 +237,9 @@ const tablePreviewHtml = computed(() => {
236
237
  border: ${form.borderWidth}px solid ${form.borderColor};
237
238
  text-align: ${form.alignment};
238
239
  ${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};`}
240
+ ${form.alternateRows && i % 2 === 1
241
+ ? `background-color: ${form.alternateRowBgColor}; color: ${form.alternateRowTextColor};`
242
+ : `background-color: ${form.cellBgColor}; color: ${form.cellTextColor};`}
242
243
  `
243
244
  html += `<td style="${tdStyle}">Cell ${i + 1},${j + 1}</td>`
244
245
  }
@@ -254,7 +255,7 @@ editor.state.content = props.modelValue
254
255
 
255
256
  // Function to detect Hebrew text and set direction
256
257
  function detectAndSetDirection() {
257
- const doc = editor.state.doc
258
+ const { doc } = editor.state
258
259
  if (!doc?.body) {
259
260
  console.log('No doc.body found')
260
261
  return
@@ -285,7 +286,7 @@ function detectAndSetDirection() {
285
286
 
286
287
  // Update all paragraphs to match the new direction
287
288
  const paragraphs = doc.querySelectorAll('p')
288
- paragraphs.forEach(p => {
289
+ paragraphs.forEach((p) => {
289
290
  if (!p.classList.contains('placeholder')) {
290
291
  p.setAttribute('dir', newDirection)
291
292
  }
@@ -297,7 +298,7 @@ function detectAndSetDirection() {
297
298
 
298
299
  // Function to get current direction based on content
299
300
  function getCurrentDirection() {
300
- const doc = editor.state.doc
301
+ const { doc } = editor.state
301
302
  if (!doc?.body) return 'ltr'
302
303
 
303
304
  const allText = doc.body.textContent || ''
@@ -334,7 +335,7 @@ function updateContentWithHistory(doc: Document, skipHistory = false) {
334
335
 
335
336
  // Debug function to show current content
336
337
  function debugShowContent() {
337
- const doc = editor.state.doc
338
+ const { doc } = editor.state
338
339
  if (doc) {
339
340
  console.log('=== Current Editor Content ===')
340
341
  console.log('HTML:', doc.body.innerHTML)
@@ -370,7 +371,7 @@ function showInlineToolbarForSelection() {
370
371
  return
371
372
  }
372
373
 
373
- const doc = editor.state.doc
374
+ const { doc } = editor.state
374
375
  if (!doc) return
375
376
 
376
377
  const selection = doc.getSelection()
@@ -413,8 +414,8 @@ function showInlineToolbarForSelection() {
413
414
  const rangeClone = range.cloneRange()
414
415
  inlineToolbarSelection.value = selection
415
416
 
416
- // Store the cloned range for later use
417
- ; (inlineToolbarSelection.value as any)._storedRange = rangeClone
417
+ // Store the cloned range for later use
418
+ ; (inlineToolbarSelection.value as any)._storedRange = rangeClone
418
419
 
419
420
  showInlineToolbar.value = true
420
421
  }
@@ -430,7 +431,7 @@ function hideInlineToolbar() {
430
431
  function runInlineAction(actionName: string) {
431
432
  if (!inlineToolbarSelection.value) return
432
433
 
433
- const doc = editor.state.doc
434
+ const { doc } = editor.state
434
435
  if (!doc) return
435
436
 
436
437
  // Get the stored range
@@ -518,7 +519,7 @@ function submitLink() {
518
519
 
519
520
  const { selection, range, existingLink } = pendingLinkData
520
521
  const { url, openInNewTab } = linkForm.value
521
- const doc = editor.state.doc
522
+ const { doc } = editor.state
522
523
 
523
524
  if (!doc) return
524
525
 
@@ -573,7 +574,7 @@ function visitLink() {
573
574
 
574
575
  // Add https:// if no protocol is specified
575
576
  if (!url.match(/^https?:\/\//)) {
576
- url = 'https://' + url
577
+ url = `https://${url}`
577
578
  }
578
579
 
579
580
  if (linkForm.value.openInNewTab) {
@@ -588,12 +589,12 @@ function isValidUrl(url: string): boolean {
588
589
  if (!url || url.trim() === '') return false
589
590
 
590
591
  try {
591
- new URL(url)
592
+ URL.canParse(url)
592
593
  return true
593
594
  } catch {
594
595
  // Try with https:// prefix if it's missing
595
596
  try {
596
- new URL('https://' + url)
597
+ URL.canParse(`https://${url}`)
597
598
  return true
598
599
  } catch {
599
600
  return false
@@ -603,7 +604,7 @@ function isValidUrl(url: string): boolean {
603
604
 
604
605
  // Function to open image modal
605
606
  function openImageModal(existingImage: HTMLElement | null = null) {
606
- const doc = editor.state.doc
607
+ const { doc } = editor.state
607
608
  if (!doc) return
608
609
 
609
610
  // Get current selection for new images
@@ -633,7 +634,7 @@ function openImageModal(existingImage: HTMLElement | null = null) {
633
634
  alt: img?.alt || '',
634
635
  width: img?.getAttribute('data-width') || img?.style.width || '',
635
636
  height: img?.getAttribute('data-height') || img?.style.height || '',
636
- credit: credit,
637
+ credit,
637
638
  figcaption: !!figcaption
638
639
  }
639
640
  } else {
@@ -655,7 +656,7 @@ function openImageModal(existingImage: HTMLElement | null = null) {
655
656
  function submitImage() {
656
657
  if (!imageForm.value.src || !pendingImageData) return
657
658
 
658
- const doc = editor.state.doc
659
+ const { doc } = editor.state
659
660
  if (!doc) return
660
661
 
661
662
  const { existingImage } = pendingImageData
@@ -671,7 +672,7 @@ function submitImage() {
671
672
  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
673
  img.style.width = imageForm.value.width
673
674
  } else {
674
- img.style.width = imageForm.value.width + 'px'
675
+ img.style.width = `${imageForm.value.width}px`
675
676
  }
676
677
  } else {
677
678
  img.style.width = '100%'
@@ -682,7 +683,7 @@ function submitImage() {
682
683
  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
684
  img.style.height = imageForm.value.height
684
685
  } else {
685
- img.style.height = imageForm.value.height + 'px'
686
+ img.style.height = `${imageForm.value.height}px`
686
687
  }
687
688
  } else {
688
689
  img.style.height = 'auto'
@@ -700,7 +701,7 @@ function submitImage() {
700
701
 
701
702
  // Always add credit if it exists (with lighter styling)
702
703
  if (imageForm.value.credit) {
703
- captionHTML += (captionHTML ? ' • ' : '') + `<span class="photo-credit">${imageForm.value.credit}</span>`
704
+ captionHTML += `${captionHTML ? ' • ' : ''}<span class="photo-credit">${imageForm.value.credit}</span>`
704
705
  }
705
706
 
706
707
  figcaption.innerHTML = captionHTML
@@ -774,7 +775,7 @@ function submitImage() {
774
775
 
775
776
  // Function to open embed modal
776
777
  function openEmbedModal(existingEmbed: HTMLElement | null = null) {
777
- const doc = editor.state.doc
778
+ const { doc } = editor.state
778
779
  if (!doc) return
779
780
 
780
781
  // Get current selection for new embeds
@@ -836,7 +837,7 @@ function extractEmbedUrl(input: string): string {
836
837
  function submitEmbed() {
837
838
  if (!embedForm.value.src || !pendingEmbedData) return
838
839
 
839
- const doc = editor.state.doc
840
+ const { doc } = editor.state
840
841
  if (!doc) return
841
842
 
842
843
  const { existingEmbed } = pendingEmbedData
@@ -853,7 +854,7 @@ function submitEmbed() {
853
854
  // Debug: Log the iframe details
854
855
  console.log('Creating embed iframe:', {
855
856
  originalSrc: embedForm.value.src,
856
- cleanUrl: cleanUrl,
857
+ cleanUrl,
857
858
  width: embedForm.value.width,
858
859
  height: embedForm.value.height
859
860
  })
@@ -861,8 +862,8 @@ function submitEmbed() {
861
862
  // Set width and height - always set them
862
863
  iframe.width = embedForm.value.width || '560'
863
864
  iframe.height = embedForm.value.height || '315'
864
- iframe.style.width = (embedForm.value.width || '560') + 'px'
865
- iframe.style.height = (embedForm.value.height || '315') + 'px'
865
+ iframe.style.width = `${embedForm.value.width || '560'}px`
866
+ iframe.style.height = `${embedForm.value.height || '315'}px`
866
867
 
867
868
  // Create or update figure
868
869
  let figure: HTMLElement
@@ -913,7 +914,7 @@ function submitEmbed() {
913
914
 
914
915
  // Function to open video modal
915
916
  function openVideoModal(existingVideo: HTMLElement | null = null) {
916
- const doc = editor.state.doc
917
+ const { doc } = editor.state
917
918
  if (!doc) return
918
919
 
919
920
  // Get current selection for new videos
@@ -998,7 +999,7 @@ function isValidVideoUrl(url: string): boolean {
998
999
  }
999
1000
 
1000
1001
  // Basic URL validation
1001
- new URL(url)
1002
+ URL.canParse(url)
1002
1003
  return true
1003
1004
  } catch {
1004
1005
  return false
@@ -1007,13 +1008,13 @@ function isValidVideoUrl(url: string): boolean {
1007
1008
  function submitVideo() {
1008
1009
  if (!videoForm.value.src || !pendingVideoData) return
1009
1010
 
1010
- const doc = editor.state.doc
1011
+ const { doc } = editor.state
1011
1012
  if (!doc) return
1012
1013
 
1013
1014
  const { existingVideo } = pendingVideoData
1014
1015
 
1015
1016
  // Calculate aspect ratio
1016
- let aspectRatio = videoForm.value.aspectRatio
1017
+ let { aspectRatio } = videoForm.value
1017
1018
  const isCustom = videoForm.value.aspectRatio === 'custom'
1018
1019
  if (isCustom && videoForm.value.customWidth && videoForm.value.customHeight) {
1019
1020
  aspectRatio = `${videoForm.value.customWidth}:${videoForm.value.customHeight}`
@@ -1045,7 +1046,7 @@ function submitVideo() {
1045
1046
  if (videoForm.value.width) {
1046
1047
  videoContainer.style.width = videoForm.value.width.includes('%') || videoForm.value.width.includes('px')
1047
1048
  ? videoForm.value.width
1048
- : videoForm.value.width + 'px'
1049
+ : `${videoForm.value.width}px`
1049
1050
  }
1050
1051
 
1051
1052
  // Create the video placeholder/thumbnail instead of actual video
@@ -1173,8 +1174,8 @@ function deleteVideo() {
1173
1174
  // Function to detect table alignment from table or wrapper
1174
1175
  function detectTableAlignment(table: HTMLTableElement): 'left' | 'center' | 'right' {
1175
1176
  // Check table margins to determine alignment
1176
- const marginLeft = table.style.marginLeft
1177
- const marginRight = table.style.marginRight
1177
+ const { marginLeft } = table.style
1178
+ const { marginRight } = table.style
1178
1179
 
1179
1180
  console.log('Table margins:', { marginLeft, marginRight })
1180
1181
 
@@ -1188,7 +1189,7 @@ function detectTableAlignment(table: HTMLTableElement): 'left' | 'center' | 'rig
1188
1189
  }
1189
1190
 
1190
1191
  function openTableEditor(existingTable: HTMLTableElement | null = null) {
1191
- const doc = editor.state.doc
1192
+ const { doc } = editor.state
1192
1193
  if (!doc) return
1193
1194
 
1194
1195
  // Get current selection for new tables
@@ -1239,8 +1240,8 @@ function openTableEditor(existingTable: HTMLTableElement | null = null) {
1239
1240
  tableForm.value = {
1240
1241
  rows: tbody ? tbody.rows.length : 3,
1241
1242
  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,
1243
+ width: Number.parseInt(existingTable.style.width) || 100,
1244
+ borderWidth: Number.parseInt(existingTable.style.borderWidth) || 1,
1244
1245
  borderColor: existingTable.style.borderColor || '#dddddd',
1245
1246
  cellPadding: 8, // default, hard to extract
1246
1247
  showHeaders: !!thead,
@@ -1250,7 +1251,7 @@ function openTableEditor(existingTable: HTMLTableElement | null = null) {
1250
1251
  cellTextColor: tbody ? getComputedStyle(tbody.querySelector('td') || tbody).color || '#333333' : '#333333',
1251
1252
  alternateRows: hasAlternatingRows,
1252
1253
  alternateRowBgColor: alternateRowBg,
1253
- alternateRowTextColor: alternateRowTextColor,
1254
+ alternateRowTextColor,
1254
1255
  fixedLayout: existingTable.style.tableLayout === 'fixed' || true, // ברירת מחדל true
1255
1256
  alignment: detectTableAlignment(existingTable),
1256
1257
  direction: existingTable.dir || 'ltr'
@@ -1283,7 +1284,7 @@ function openTableEditor(existingTable: HTMLTableElement | null = null) {
1283
1284
  function submitTable() {
1284
1285
  if (!pendingTableData) return
1285
1286
 
1286
- const doc = editor.state.doc
1287
+ const { doc } = editor.state
1287
1288
  if (!doc) return
1288
1289
 
1289
1290
  if (pendingTableData.existingTable) {
@@ -1316,7 +1317,7 @@ function submitTable() {
1316
1317
 
1317
1318
  // Update border and padding for all cells
1318
1319
  const allCells = table.querySelectorAll('td, th')
1319
- allCells.forEach(cell => {
1320
+ allCells.forEach((cell) => {
1320
1321
  const cellEl = cell as HTMLElement
1321
1322
  cellEl.style.padding = `${tableForm.value.cellPadding}px`
1322
1323
  cellEl.style.border = `${tableForm.value.borderWidth}px solid ${tableForm.value.borderColor}`
@@ -1330,7 +1331,7 @@ function submitTable() {
1330
1331
 
1331
1332
  // Update header styles if headers exist
1332
1333
  const headers = table.querySelectorAll('th')
1333
- headers.forEach(th => {
1334
+ headers.forEach((th) => {
1334
1335
  const thEl = th as HTMLElement
1335
1336
  thEl.style.backgroundColor = tableForm.value.headerBgColor
1336
1337
  thEl.style.color = tableForm.value.headerTextColor
@@ -1342,7 +1343,7 @@ function submitTable() {
1342
1343
  const rows = Array.from(tbody.querySelectorAll('tr'))
1343
1344
  rows.forEach((row, i) => {
1344
1345
  const cells = row.querySelectorAll('td')
1345
- cells.forEach(cell => {
1346
+ cells.forEach((cell) => {
1346
1347
  const cellEl = cell as HTMLElement
1347
1348
  if (tableForm.value.alternateRows && i % 2 === 1) {
1348
1349
  // Alternate rows: use special colors
@@ -1479,7 +1480,7 @@ function createNewTable(doc: Document) {
1479
1480
  console.log('Trying to add edit button to new table...')
1480
1481
  if (doc && (doc as any).__addEditButtonsToTables) {
1481
1482
  console.log('Calling __addEditButtonsToTables...')
1482
- ; (doc as any).__addEditButtonsToTables()
1483
+ ; (doc as any).__addEditButtonsToTables()
1483
1484
  } else {
1484
1485
  console.log('__addEditButtonsToTables not found, calling setupTableEditButtons...')
1485
1486
  setupTableEditButtons(doc)
@@ -1520,7 +1521,7 @@ function applyDefaultTableSettings(table: HTMLTableElement) {
1520
1521
 
1521
1522
  if (headers.length > 0) {
1522
1523
  const colWidth = `${100 / headers.length}%`
1523
- headers.forEach(th => {
1524
+ headers.forEach((th) => {
1524
1525
  if (!(th as HTMLElement).style.width) {
1525
1526
  (th as HTMLElement).style.width = colWidth
1526
1527
  }
@@ -1533,7 +1534,7 @@ function applyDefaultTableSettings(table: HTMLTableElement) {
1533
1534
  if (firstRow) {
1534
1535
  const colCount = firstRow.querySelectorAll('td').length
1535
1536
  const colWidth = `${100 / colCount}%`
1536
- cells.forEach(td => {
1537
+ cells.forEach((td) => {
1537
1538
  if (!(td as HTMLElement).style.width) {
1538
1539
  (td as HTMLElement).style.width = colWidth
1539
1540
  }
@@ -1544,7 +1545,7 @@ function applyDefaultTableSettings(table: HTMLTableElement) {
1544
1545
 
1545
1546
  // Set cell text alignment
1546
1547
  const allCells = table.querySelectorAll('td, th')
1547
- allCells.forEach(cell => {
1548
+ allCells.forEach((cell) => {
1548
1549
  const cellEl = cell as HTMLElement
1549
1550
  cellEl.style.textAlign = defaultSettings.alignment
1550
1551
  })
@@ -1581,17 +1582,17 @@ function deleteEmbed() {
1581
1582
 
1582
1583
  // Expose openLinkModal to editor state
1583
1584
  ; (editor.state as any).openLinkModal = openLinkModal
1584
- ; (editor.state as any).showTooltipMessage = showTooltipMessage
1585
+ ; (editor.state as any).showTooltipMessage = showTooltipMessage
1585
1586
 
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
1587
+ // Expose openTableEditor to editor state
1588
+ ; (editor.state as any).openTableEditor = openTableEditor
1589
+ ; (editor.state as any).openImageModal = openImageModal
1590
+ ; (editor.state as any).openEmbedModal = openEmbedModal
1591
+ ; (editor.state as any).openVideoModal = openVideoModal
1591
1592
 
1592
1593
  // Table manipulation functions
1593
1594
  function mergeCellRight() {
1594
- const doc = editor.state.doc
1595
+ const { doc } = editor.state
1595
1596
  if (!doc || !contextMenuCell.value) return
1596
1597
 
1597
1598
  const cell = contextMenuCell.value
@@ -1607,12 +1608,12 @@ function mergeCellRight() {
1607
1608
  if (targetCell && targetIndex >= 0 && targetIndex < row.cells.length) {
1608
1609
  // Combine content
1609
1610
  if (targetCell.innerHTML.trim()) {
1610
- cell.innerHTML += ' ' + targetCell.innerHTML
1611
+ cell.innerHTML += ` ${targetCell.innerHTML}`
1611
1612
  }
1612
1613
 
1613
1614
  // Update colspan
1614
- const currentColspan = parseInt(cell.getAttribute('colspan') || '1')
1615
- const targetColspan = parseInt(targetCell.getAttribute('colspan') || '1')
1615
+ const currentColspan = Number.parseInt(cell.getAttribute('colspan') || '1')
1616
+ const targetColspan = Number.parseInt(targetCell.getAttribute('colspan') || '1')
1616
1617
  cell.setAttribute('colspan', (currentColspan + targetColspan).toString())
1617
1618
 
1618
1619
  // Remove the target cell
@@ -1623,7 +1624,7 @@ function mergeCellRight() {
1623
1624
  }
1624
1625
 
1625
1626
  function mergeCellDown() {
1626
- const doc = editor.state.doc
1627
+ const { doc } = editor.state
1627
1628
  if (!doc || !contextMenuCell.value) return
1628
1629
 
1629
1630
  const cell = contextMenuCell.value
@@ -1642,12 +1643,12 @@ function mergeCellDown() {
1642
1643
  if (targetCell) {
1643
1644
  // Combine content
1644
1645
  if (targetCell.innerHTML.trim()) {
1645
- cell.innerHTML += '<br>' + targetCell.innerHTML
1646
+ cell.innerHTML += `<br>${targetCell.innerHTML}`
1646
1647
  }
1647
1648
 
1648
1649
  // Update rowspan
1649
- const currentRowspan = parseInt(cell.getAttribute('rowspan') || '1')
1650
- const targetRowspan = parseInt(targetCell.getAttribute('rowspan') || '1')
1650
+ const currentRowspan = Number.parseInt(cell.getAttribute('rowspan') || '1')
1651
+ const targetRowspan = Number.parseInt(targetCell.getAttribute('rowspan') || '1')
1651
1652
  cell.setAttribute('rowspan', (currentRowspan + targetRowspan).toString())
1652
1653
 
1653
1654
  // Remove the target cell
@@ -1659,20 +1660,19 @@ function mergeCellDown() {
1659
1660
  }
1660
1661
 
1661
1662
  function splitCell() {
1662
- const doc = editor.state.doc
1663
+ const { doc } = editor.state
1663
1664
  if (!doc || !contextMenuCell.value) return
1664
1665
 
1665
1666
  const cell = contextMenuCell.value
1666
1667
  const table = cell.closest('table') as HTMLTableElement
1667
1668
  const isRTL = table?.dir === 'rtl'
1668
- const colspan = parseInt(cell.getAttribute('colspan') || '1')
1669
- const rowspan = parseInt(cell.getAttribute('rowspan') || '1')
1669
+ const colspan = Number.parseInt(cell.getAttribute('colspan') || '1')
1670
+ const rowspan = Number.parseInt(cell.getAttribute('rowspan') || '1')
1670
1671
 
1671
1672
  if (colspan > 1) {
1672
1673
  // Split horizontally
1673
1674
  cell.setAttribute('colspan', '1')
1674
1675
  const row = cell.parentElement as HTMLTableRowElement
1675
- const cellIndex = Array.from(row.cells).indexOf(cell)
1676
1676
 
1677
1677
  // Add new cells to the right (or left in RTL)
1678
1678
  for (let i = 1; i < colspan; i++) {
@@ -1721,7 +1721,7 @@ function splitCell() {
1721
1721
  }
1722
1722
 
1723
1723
  function insertRowAbove() {
1724
- const doc = editor.state.doc
1724
+ const { doc } = editor.state
1725
1725
  if (!doc || !contextMenuCell.value) return
1726
1726
 
1727
1727
  const cell = contextMenuCell.value
@@ -1730,7 +1730,7 @@ function insertRowAbove() {
1730
1730
  // Create new row with same number of columns
1731
1731
  const newRow = doc.createElement('tr')
1732
1732
  const columnCount = Array.from(row.cells).reduce((total, cell) => {
1733
- return total + parseInt(cell.getAttribute('colspan') || '1')
1733
+ return total + Number.parseInt(cell.getAttribute('colspan') || '1')
1734
1734
  }, 0)
1735
1735
 
1736
1736
  for (let i = 0; i < columnCount; i++) {
@@ -1748,7 +1748,7 @@ function insertRowAbove() {
1748
1748
  }
1749
1749
 
1750
1750
  function insertRowBelow() {
1751
- const doc = editor.state.doc
1751
+ const { doc } = editor.state
1752
1752
  if (!doc || !contextMenuCell.value) return
1753
1753
 
1754
1754
  const cell = contextMenuCell.value
@@ -1757,7 +1757,7 @@ function insertRowBelow() {
1757
1757
  // Create new row with same number of columns
1758
1758
  const newRow = doc.createElement('tr')
1759
1759
  const columnCount = Array.from(row.cells).reduce((total, cell) => {
1760
- return total + parseInt(cell.getAttribute('colspan') || '1')
1760
+ return total + Number.parseInt(cell.getAttribute('colspan') || '1')
1761
1761
  }, 0)
1762
1762
 
1763
1763
  for (let i = 0; i < columnCount; i++) {
@@ -1775,7 +1775,7 @@ function insertRowBelow() {
1775
1775
  }
1776
1776
 
1777
1777
  function deleteRow() {
1778
- const doc = editor.state.doc
1778
+ const { doc } = editor.state
1779
1779
  if (!doc || !contextMenuCell.value) return
1780
1780
 
1781
1781
  const cell = contextMenuCell.value
@@ -1796,7 +1796,7 @@ function deleteRow() {
1796
1796
  }
1797
1797
 
1798
1798
  function insertColumnLeft() {
1799
- const doc = editor.state.doc
1799
+ const { doc } = editor.state
1800
1800
  if (!doc || !contextMenuCell.value) return
1801
1801
 
1802
1802
  const cell = contextMenuCell.value
@@ -1805,7 +1805,7 @@ function insertColumnLeft() {
1805
1805
 
1806
1806
  // Add new cell to each row at the same index
1807
1807
  const rows = table.querySelectorAll('tr')
1808
- rows.forEach(row => {
1808
+ rows.forEach((row) => {
1809
1809
  const newCell = doc.createElement(row.parentElement?.tagName === 'THEAD' ? 'th' : 'td')
1810
1810
  newCell.innerHTML = '&nbsp;'
1811
1811
  newCell.style.padding = '8px'
@@ -1822,7 +1822,7 @@ function insertColumnLeft() {
1822
1822
  }
1823
1823
 
1824
1824
  function insertColumnRight() {
1825
- const doc = editor.state.doc
1825
+ const { doc } = editor.state
1826
1826
  if (!doc || !contextMenuCell.value) return
1827
1827
 
1828
1828
  const cell = contextMenuCell.value
@@ -1831,7 +1831,7 @@ function insertColumnRight() {
1831
1831
 
1832
1832
  // Add new cell to each row after the current index
1833
1833
  const rows = table.querySelectorAll('tr')
1834
- rows.forEach(row => {
1834
+ rows.forEach((row) => {
1835
1835
  const newCell = doc.createElement(row.parentElement?.tagName === 'THEAD' ? 'th' : 'td')
1836
1836
  newCell.innerHTML = '&nbsp;'
1837
1837
  newCell.style.padding = '8px'
@@ -1849,7 +1849,7 @@ function insertColumnRight() {
1849
1849
  }
1850
1850
 
1851
1851
  function deleteColumn() {
1852
- const doc = editor.state.doc
1852
+ const { doc } = editor.state
1853
1853
  if (!doc || !contextMenuCell.value) return
1854
1854
 
1855
1855
  const cell = contextMenuCell.value
@@ -1865,7 +1865,7 @@ function deleteColumn() {
1865
1865
 
1866
1866
  // Remove cell from each row at the same index
1867
1867
  const rows = table.querySelectorAll('tr')
1868
- rows.forEach(row => {
1868
+ rows.forEach((row) => {
1869
1869
  if (row.cells[cellIndex]) {
1870
1870
  row.cells[cellIndex].remove()
1871
1871
  }
@@ -1899,8 +1899,8 @@ const canMergeDown = computed(() => {
1899
1899
  const canSplit = computed(() => {
1900
1900
  if (!contextMenuCell.value) return false
1901
1901
  const cell = contextMenuCell.value
1902
- const colspan = parseInt(cell.getAttribute('colspan') || '1')
1903
- const rowspan = parseInt(cell.getAttribute('rowspan') || '1')
1902
+ const colspan = Number.parseInt(cell.getAttribute('colspan') || '1')
1903
+ const rowspan = Number.parseInt(cell.getAttribute('rowspan') || '1')
1904
1904
  return colspan > 1 || rowspan > 1
1905
1905
  })
1906
1906
 
@@ -1923,17 +1923,15 @@ function closeTableContextMenu() {
1923
1923
  }
1924
1924
 
1925
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
-
1936
- const commands = useCommands(editor.state, editor.state.debug)
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
+ // }
1937
1935
 
1938
1936
  // Expose debug methods if debug mode is enabled
1939
1937
  const debugMethods = $computed(() => editor.state.debug)
@@ -2115,14 +2113,15 @@ function setupAutoWrapping(doc: Document) {
2115
2113
  const walker = doc.createTreeWalker(
2116
2114
  doc.body,
2117
2115
  NodeFilter.SHOW_TEXT,
2118
- node => {
2116
+ (node) => {
2119
2117
  // Only accept text nodes that are direct children of body
2120
2118
  // AND have meaningful content
2121
2119
  const parent = node.parentNode as HTMLElement
2122
- return (parent === doc.body &&
2123
- node.textContent?.trim() &&
2124
- node.textContent.trim().length > 0) ?
2125
- NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
2120
+ return (parent === doc.body
2121
+ && node.textContent?.trim()
2122
+ && node.textContent.trim().length > 0)
2123
+ ? NodeFilter.FILTER_ACCEPT
2124
+ : NodeFilter.FILTER_REJECT
2126
2125
  }
2127
2126
  )
2128
2127
 
@@ -2133,17 +2132,16 @@ function setupAutoWrapping(doc: Document) {
2133
2132
  }
2134
2133
 
2135
2134
  // Wrap loose text nodes very carefully, preserving existing structure
2136
- textNodes.forEach(textNode => {
2135
+ textNodes.forEach((textNode) => {
2137
2136
  // Triple check the node is still valid and not corrupted
2138
- if (textNode.parentNode === doc.body &&
2139
- textNode.textContent?.trim() &&
2140
- textNode.nodeType === Node.TEXT_NODE) {
2141
-
2137
+ if (textNode.parentNode === doc.body
2138
+ && textNode.textContent?.trim()
2139
+ && textNode.nodeType === Node.TEXT_NODE) {
2142
2140
  const p = doc.createElement('p')
2143
2141
  p.dir = doc.body.dir || 'ltr'
2144
2142
 
2145
2143
  // Clone the text content to avoid any reference issues
2146
- const textContent = textNode.textContent
2144
+ const { textContent } = textNode
2147
2145
  p.textContent = textContent
2148
2146
 
2149
2147
  // Replace the text node with the paragraph
@@ -2184,7 +2182,6 @@ function setupAutoWrapping(doc: Document) {
2184
2182
  }
2185
2183
  }
2186
2184
  }
2187
-
2188
2185
  } finally {
2189
2186
  // Always clean up the processing flag
2190
2187
  delete doc.body.dataset.normalizing
@@ -2209,7 +2206,7 @@ function setupAutoWrapping(doc: Document) {
2209
2206
  }
2210
2207
  updateContentWithHistory(doc)
2211
2208
  return
2212
- } // Don't normalize during normal typing - only on paste/drop
2209
+ } // Don't normalize during normal typing - only on paste/drop
2213
2210
  const inputEvent = e as InputEvent
2214
2211
  const normalizeInputTypes = ['insertFromPaste', 'insertFromDrop']
2215
2212
 
@@ -2248,7 +2245,7 @@ function setupAutoWrapping(doc: Document) {
2248
2245
  if (li || listContainer) {
2249
2246
  // We're in a list context - let browser handle everything
2250
2247
  return
2251
- } // Find the paragraph we're in (only for confirmed non-list content)
2248
+ } // Find the paragraph we're in (only for confirmed non-list content)
2252
2249
  let paragraph = range.startContainer
2253
2250
  if (paragraph.nodeType === Node.TEXT_NODE) {
2254
2251
  paragraph = paragraph.parentElement!
@@ -2260,9 +2257,9 @@ function setupAutoWrapping(doc: Document) {
2260
2257
  // Check if current paragraph is empty and previous paragraph is also empty
2261
2258
  const currentIsEmpty = !paragraph.textContent?.trim()
2262
2259
  const prevSibling = (paragraph as Element).previousElementSibling as HTMLElement
2263
- const prevIsEmpty = prevSibling &&
2264
- (prevSibling.tagName === 'P') &&
2265
- !prevSibling.textContent?.trim()
2260
+ const prevIsEmpty = prevSibling
2261
+ && (prevSibling.tagName === 'P')
2262
+ && !prevSibling.textContent?.trim()
2266
2263
 
2267
2264
  // If both current and previous paragraphs are empty, don't create another empty paragraph
2268
2265
  if (currentIsEmpty && prevIsEmpty) {
@@ -2303,7 +2300,7 @@ function setupAutoWrapping(doc: Document) {
2303
2300
  const textNode = range.startContainer as Text
2304
2301
 
2305
2302
  // Store the original parent element for formatting preservation
2306
- const parentElement = textNode.parentElement
2303
+ const { parentElement } = textNode
2307
2304
 
2308
2305
  // Keep text before cursor in current paragraph
2309
2306
  textNode.textContent = textBeforeCursor
@@ -2353,10 +2350,10 @@ function setupAutoWrapping(doc: Document) {
2353
2350
  for (const mutation of mutations) {
2354
2351
  if (mutation.type === 'childList') {
2355
2352
  // Check if any loose text nodes were added to body
2356
- Array.from(mutation.addedNodes).forEach(addedNode => {
2357
- if (addedNode.nodeType === Node.TEXT_NODE &&
2358
- addedNode.parentNode === doc.body &&
2359
- addedNode.textContent?.trim()) {
2353
+ Array.from(mutation.addedNodes).forEach((addedNode) => {
2354
+ if (addedNode.nodeType === Node.TEXT_NODE
2355
+ && addedNode.parentNode === doc.body
2356
+ && addedNode.textContent?.trim()) {
2360
2357
  // Wrap loose text node immediately
2361
2358
  const p = doc.createElement('p')
2362
2359
  p.dir = doc.body.dir || 'ltr'
@@ -2387,7 +2384,7 @@ function setupAutoWrapping(doc: Document) {
2387
2384
  // Also apply direction to list items if it's a list
2388
2385
  if (element.tagName === 'UL' || element.tagName === 'OL') {
2389
2386
  const listItems = element.querySelectorAll('li')
2390
- listItems.forEach(li => {
2387
+ listItems.forEach((li) => {
2391
2388
  if (!li.dir) {
2392
2389
  li.dir = element.dir
2393
2390
  console.log('Applied direction to new list item:', li.dir)
@@ -2508,7 +2505,7 @@ function setupAutoWrapping(doc: Document) {
2508
2505
  })
2509
2506
  }
2510
2507
 
2511
- const initEditor = async () => {
2508
+ async function initEditor() {
2512
2509
  if (isInitializing.value || !iframe.value || hasInitialized.value) {
2513
2510
  return
2514
2511
  }
@@ -2517,7 +2514,7 @@ const initEditor = async () => {
2517
2514
 
2518
2515
  try {
2519
2516
  // Use basic embedded styles for better compatibility
2520
- let editorStylesContent = `
2517
+ const editorStylesContent = `
2521
2518
  body {
2522
2519
  margin: 0;
2523
2520
  padding: 8px;
@@ -2585,7 +2582,7 @@ const initEditor = async () => {
2585
2582
  background: #f4f4f4 !important;
2586
2583
  border-inline-start: 4px solid #ccc !important;
2587
2584
  }
2588
- ` // Create a complete HTML document with proper doctype and meta tags
2585
+ ` // Create a complete HTML document with proper doctype and meta tags
2589
2586
  const initialContent = props.modelValue || (props.placeholder ? `<p class="placeholder">${props.placeholder}</p>` : '')
2590
2587
  const htmlContent = `
2591
2588
  <!DOCTYPE html>
@@ -2639,7 +2636,7 @@ const initEditor = async () => {
2639
2636
  // Apply direction to existing blockquotes and lists
2640
2637
  const blockElements = doc.body.querySelectorAll('blockquote, ul, ol')
2641
2638
  console.log('Found existing block elements:', blockElements.length)
2642
- blockElements.forEach(element => {
2639
+ blockElements.forEach((element) => {
2643
2640
  const htmlElement = element as HTMLElement
2644
2641
  console.log('Processing element:', htmlElement.tagName, 'current dir:', htmlElement.dir)
2645
2642
  if (!htmlElement.dir) {
@@ -2650,7 +2647,7 @@ const initEditor = async () => {
2650
2647
  // Also apply direction to list items
2651
2648
  if (htmlElement.tagName === 'UL' || htmlElement.tagName === 'OL') {
2652
2649
  const listItems = htmlElement.querySelectorAll('li')
2653
- listItems.forEach(li => {
2650
+ listItems.forEach((li) => {
2654
2651
  const listItem = li as HTMLElement
2655
2652
  if (!listItem.dir) {
2656
2653
  listItem.dir = htmlElement.dir
@@ -2671,7 +2668,7 @@ const initEditor = async () => {
2671
2668
  setTimeout(() => {
2672
2669
  // Clear all formatting by removing styles and replacing with plain text
2673
2670
  const elements = doc.body.querySelectorAll('*')
2674
- elements.forEach(el => {
2671
+ elements.forEach((el) => {
2675
2672
  if (el.tagName !== 'P' && el.tagName !== 'BR') {
2676
2673
  const textContent = el.textContent || ''
2677
2674
  if (textContent.trim()) {
@@ -2683,7 +2680,7 @@ const initEditor = async () => {
2683
2680
  }
2684
2681
  } else if (el.tagName === 'P') {
2685
2682
  // Remove all attributes from paragraphs
2686
- Array.from(el.attributes).forEach(attr => {
2683
+ Array.from(el.attributes).forEach((attr) => {
2687
2684
  if (attr.name !== 'dir') {
2688
2685
  el.removeAttribute(attr.name)
2689
2686
  }
@@ -2696,12 +2693,12 @@ const initEditor = async () => {
2696
2693
 
2697
2694
  // Add inline toolbar selection listener
2698
2695
  doc.addEventListener('selectionchange', () => {
2699
- setTimeout(() => showInlineToolbarForSelection(), 10)
2696
+ setTimeout(() => { showInlineToolbarForSelection() }, 10)
2700
2697
  })
2701
2698
 
2702
2699
  // Also listen for mouseup to handle manual selections
2703
2700
  doc.addEventListener('mouseup', () => {
2704
- setTimeout(() => showInlineToolbarForSelection(), 10)
2701
+ setTimeout(() => { showInlineToolbarForSelection() }, 10)
2705
2702
  })
2706
2703
 
2707
2704
  // Hide inline toolbar when clicking outside or when selection is lost
@@ -2768,7 +2765,7 @@ const initEditor = async () => {
2768
2765
  // Add click listener to highlight selected table cell
2769
2766
  doc.addEventListener('click', (e) => {
2770
2767
  // Remove previous highlights
2771
- doc.querySelectorAll('.table-cell-selected').forEach(cell => {
2768
+ doc.querySelectorAll('.table-cell-selected').forEach((cell) => {
2772
2769
  cell.classList.remove('table-cell-selected')
2773
2770
  })
2774
2771
 
@@ -2884,7 +2881,7 @@ watch(() => tableForm.value.alignment, (newAlignment) => {
2884
2881
  })
2885
2882
 
2886
2883
  // Close context menu when clicking outside
2887
- const handleGlobalClick = () => {
2884
+ function handleGlobalClick() {
2888
2885
  showTableContextMenu.value = false
2889
2886
  }
2890
2887
 
@@ -2908,301 +2905,489 @@ defineExpose({
2908
2905
  </script>
2909
2906
 
2910
2907
  <template>
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>
2908
+ <div class="bagel-input" v-bind="attrs">
2909
+ <label v-if="label">{{ label }}</label>
2910
+
2911
+ <div
2912
+ class="rounded" :class="[
2913
+ {
2914
+ 'rich-text-editor pt-05 px-05 pb-075': !basic,
2915
+ 'rich-text-editor--basic': basic,
2916
+ 'fullscreen-mode': editor.state.isFullscreen,
2917
+ },
2918
+ ]"
2919
+ >
2920
+ <EditorToolbar
2921
+ v-if="editor.state.hasInit && shouldShowToolbar" :config="effectiveToolbarConfig"
2922
+ :selectedStyles="editor.state.selectedStyles" :hide-images="hideImages" :hide-videos="hideVideos"
2923
+ :hide-embeds="hideEmbed" :hide-tables="hideTables" :hide-alignment="hideAlignment"
2924
+ :hide-directions="hideDirections" :hide-h5-h6="hideH5H6" :hide="effectiveHideArray"
2925
+ @action="commands.execute"
2926
+ />
2927
+ <div class="editor-container" :class="{ 'split-view': editor.state.isSplitView }">
2928
+ <div
2929
+ class="content-area radius-1"
2930
+ :style="{ height: editor.state.isFullscreen ? 'calc(100vh - 4rem)' : editorHeight }"
2931
+ >
2932
+ <iframe
2933
+ id="rich-text-iframe" ref="iframe" class="editableContent" title="Editor" srcdoc=""
2934
+ 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"
2935
+ @load="initEditor" @contextmenu="handleTableContextMenu"
2936
+ />
2937
+ </div>
2938
+ <CodeEditor
2939
+ v-if="editor.state.isSplitView" v-model="editor.state.content" language="html"
2940
+ :height="editor.state.isFullscreen ? 'calc(100vh - 4rem)' : editorHeight"
2941
+ @update:modelValue="editor.updateState.content('html')"
2942
+ />
2979
2943
  </div>
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
2944
+ <div v-if="debug" class="flex pt-05">
2945
+ <p class="txt12 txt-gray mb-0 p-0">
2946
+ Debug
2947
+ </p>
2948
+ <Btn thin color="gray" icon="visibility" @click="debugShowContent">
2949
+ Show Content
2996
2950
  </Btn>
2997
- <Btn full-width align-txt="start" thin flat icon="remove" @click="deleteRow(); showTableContextMenu = false" class="context-menu-btn">
2998
- Delete Row
2951
+ <Btn thin color="gray" icon="delete" @click="debugMethods?.clearSession">
2952
+ Clear Session
2999
2953
  </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
2954
+ <Btn thin color="gray" icon="download" @click="debugMethods?.downloadSession">
2955
+ Download Log
3003
2956
  </Btn>
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
3006
- </Btn>
3007
- <Btn full-width align-txt="start" thin flat icon="remove" @click="deleteColumn(); showTableContextMenu = false" class="context-menu-btn">
3008
- Delete Column
2957
+ <Btn
2958
+ thin color="gray" icon="content_copy"
2959
+ @click="copyText(debugMethods?.exportDebugWithPrompt() || '')"
2960
+ >
2961
+ Copy Log
3009
2962
  </Btn>
3010
2963
  </div>
3011
2964
  </div>
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" />
2965
+
2966
+ <!-- Inline Toolbar -->
2967
+ <div
2968
+ v-if="showInlineToolbar" class="inline-toolbar" :style="{
2969
+ position: 'fixed',
2970
+ top: `${inlineToolbarPosition.top}px`,
2971
+ left: `${inlineToolbarPosition.left}px`,
2972
+ zIndex: 99,
2973
+ }"
2974
+ >
2975
+ <div class="inline-toolbar-content">
2976
+ <Btn
2977
+ thin flat icon="format_bold" :class="{ active: editor.state.selectedStyles.has('bold') }"
2978
+ @click="runInlineAction('bold')"
2979
+ />
2980
+ <Btn
2981
+ thin flat icon="format_italic" :class="{ active: editor.state.selectedStyles.has('italic') }"
2982
+ @click="runInlineAction('italic')"
2983
+ />
2984
+ <Btn
2985
+ thin flat icon="format_underlined"
2986
+ :class="{ active: editor.state.selectedStyles.has('underline') }"
2987
+ @click="runInlineAction('underline')"
2988
+ />
2989
+ <span class="separator">|</span>
2990
+ <Btn thin flat icon="add_link" @click="runInlineAction('link')" />
2991
+ </div>
3064
2992
  </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>
2993
+ <!-- Table Context Menu -->
2994
+ <div
2995
+ v-if="showTableContextMenu" class="table-context-menu" :style="{
2996
+ position: 'fixed',
2997
+ top: `${contextMenuPosition.y}px`,
2998
+ left: `${contextMenuPosition.x}px`,
2999
+ zIndex: 1001,
3000
+ }"
3001
+ >
3002
+ <div class="context-menu-content" @click.stop>
3003
+ <div class="menu-header">
3004
+ <span>Table Actions</span>
3005
+ <button class="close-btn" @click="showTableContextMenu = false">
3006
+ ×
3007
+ </button>
3008
+ </div>
3009
+ <div class="px-025">
3010
+ <Btn
3011
+ v-if="canMergeRight" full-width align-txt="start" thin flat icon="start"
3012
+ class="context-menu-btn" @click="mergeCellRight(); showTableContextMenu = false"
3013
+ >
3014
+ Merge Right
3015
+ </Btn>
3016
+ <Btn
3017
+ v-if="canMergeDown" full-width align-txt="start" thin flat icon="text_select_move_down"
3018
+ class="context-menu-btn" @click="mergeCellDown(); showTableContextMenu = false"
3019
+ >
3020
+ Merge Down
3021
+ </Btn>
3022
+ <Btn
3023
+ v-if="canSplit" full-width align-txt="start" thin flat icon="call_split"
3024
+ class="context-menu-btn" @click="splitCell(); showTableContextMenu = false"
3025
+ >
3026
+ Split Cell
3027
+ </Btn>
3028
+ <div v-if="canMergeRight || canMergeDown || canSplit" class="context-menu-separator" />
3029
+ <Btn
3030
+ full-width align-txt="start" thin flat icon="add_row_above" class="context-menu-btn"
3031
+ @click="insertRowAbove(); showTableContextMenu = false"
3032
+ >
3033
+ Insert Row Above
3034
+ </Btn>
3035
+ <Btn
3036
+ full-width align-txt="start" thin flat icon="add_row_below" class="context-menu-btn"
3037
+ @click="insertRowBelow(); showTableContextMenu = false"
3038
+ >
3039
+ Insert Row Below
3040
+ </Btn>
3041
+ <Btn
3042
+ full-width align-txt="start" thin flat icon="remove" class="context-menu-btn"
3043
+ @click="deleteRow(); showTableContextMenu = false"
3044
+ >
3045
+ Delete Row
3046
+ </Btn>
3047
+ <div class="context-menu-separator" />
3048
+ <Btn
3049
+ full-width align-txt="start" thin flat icon="add_column_left" class="context-menu-btn"
3050
+ @click="insertColumnLeft(); showTableContextMenu = false"
3051
+ >
3052
+ Insert Column Left
3053
+ </Btn>
3054
+ <Btn
3055
+ full-width align-txt="start" thin flat icon="add_column_right" class="context-menu-btn"
3056
+ @click="insertColumnRight(); showTableContextMenu = false"
3057
+ >
3058
+ Insert Column Right
3059
+ </Btn>
3060
+ <Btn
3061
+ full-width align-txt="start" thin flat icon="remove" class="context-menu-btn"
3062
+ @click="deleteColumn(); showTableContextMenu = false"
3063
+ >
3064
+ Delete Column
3065
+ </Btn>
3066
+ </div>
3067
+ </div>
3076
3068
  </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" />
3069
+ </div>
3070
+ <!-- Link Modal -->
3071
+ <Modal v-model:visible="showLinkModal" title="Add Link" width="400">
3072
+ <div class="flex gap-05 align-items-end">
3073
+ <TextInput
3074
+ v-model="linkForm.url" label="URL" type="url" placeholder="https://example.com"
3075
+ @keydown.enter="submitLink"
3076
+ />
3077
+ <Btn
3078
+ icon="open_in_new" class="mb-05 radius-1" flat :disabled="!isValidUrl(linkForm.url)"
3079
+ @click="visitLink"
3080
+ />
3092
3081
  </div>
3082
+ <CheckInput v-model="linkForm.openInNewTab" label="Open in new tab" type="checkbox" class="mb-2 mt-05" />
3083
+ <template #footer>
3084
+ <Btn value="Cancel" flat thin @click="showLinkModal = false" />
3085
+ <Btn value="Add Link" :disabled="!isValidUrl(linkForm.url)" @click="submitLink" />
3086
+ </template>
3087
+ </Modal>
3088
+
3089
+ <!-- Tooltip -->
3090
+ <div v-if="showTooltip" class="editor-tooltip" :style="{ left: `${tooltipData.x}px`, top: `${tooltipData.y}px` }">
3091
+ {{ tooltipData.message }}
3092
+ </div>
3093
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" />
3094
+ <!-- Image Modal -->
3095
+ <Modal
3096
+ v-model:visible="showImageModal" :title="pendingImageData?.existingImage ? 'Edit Image' : 'Insert Image'"
3097
+ width="500"
3098
+ >
3099
+ <TextInput
3100
+ v-model="imageForm.src" label="Image URL" type="url" placeholder="https://example.com/image.jpg"
3101
+ @keydown.enter="submitImage"
3102
+ />
3103
+ <TextInput v-model="imageForm.alt" label="Alt Text" placeholder="Describe the image" />
3104
+ <div class="flex gap-1">
3105
+ <TextInput
3106
+ v-model="imageForm.width" label="Width" placeholder="100% or 500px or auto"
3107
+ help="Examples: 100%, 500px, auto"
3108
+ />
3109
+ <TextInput
3110
+ v-model="imageForm.height" label="Height" placeholder="auto or 300px"
3111
+ help="Examples: auto, 300px"
3112
+ />
3101
3113
  </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" />
3114
+ <TextInput v-model="imageForm.credit" label="Photo Credit" placeholder="Photographer name (optional)" />
3115
+ <CheckInput
3116
+ v-model="imageForm.figcaption" label="Show alt text as caption"
3117
+ help="Photo credit will always be shown if provided"
3118
+ />
3119
+ <template #footer>
3120
+ <div class="flex gap-05 w-100 ">
3121
+ <Btn value="Cancel" flat thin @click="showImageModal = false" />
3122
+ <Btn
3123
+ v-if="pendingImageData?.existingImage" value="Delete Image" color="red" flat thin icon="delete"
3124
+ @click="deleteImage"
3125
+ />
3126
+ <Btn
3127
+ class="ms-auto" :value="pendingImageData?.existingImage ? 'Save Changes' : 'Insert Image'"
3128
+ :disabled="!imageForm.src" @click="submitImage"
3129
+ />
3110
3130
  </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)" />
3131
+ </template>
3132
+ </Modal>
3133
+
3134
+ <!-- Embed Modal -->
3135
+ <Modal
3136
+ v-model:visible="showEmbedModal" :title="pendingEmbedData?.existingEmbed ? 'Edit Embed' : 'Insert Embed'"
3137
+ width="500"
3138
+ >
3139
+ <TextInput
3140
+ v-model="embedForm.src" label="Embed URL or Code" type="url"
3141
+ placeholder="https://www.youtube.com/embed/... or paste iframe code" @keydown.enter="submitEmbed"
3142
+ />
3143
+ <div class="flex gap-1">
3144
+ <TextInput v-model="embedForm.width" label="Width" placeholder="560" />
3145
+ <TextInput v-model="embedForm.height" label="Height" placeholder="315" />
3118
3146
  </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>
3147
+ <TextInput v-model="embedForm.alt" label="Caption" placeholder="Enter caption (optional)" />
3148
+ <template #footer>
3149
+ <div class="flex gap-05 w-100">
3150
+ <Btn value="Cancel" flat thin @click="showEmbedModal = false" />
3151
+ <Btn
3152
+ v-if="pendingEmbedData?.existingEmbed" value="Delete Embed" color="red" flat thin icon="delete"
3153
+ @click="deleteEmbed"
3154
+ />
3155
+ <Btn
3156
+ :value="pendingEmbedData?.existingEmbed ? 'Save Changes' : 'Insert Embed'"
3157
+ :disabled="!embedForm.src" class="ms-auto" @click="submitEmbed"
3158
+ />
3131
3159
  </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>
3160
+ </template>
3161
+ </Modal>
3162
+ <!-- Video Modal -->
3163
+ <Modal v-model:visible="showVideoModal" title="Insert Video" width="500">
3164
+ <div class="grid gap-0">
3165
+ <TextInput
3166
+ v-model="videoForm.src" label="Video URL or Embed Code" type="url"
3167
+ placeholder="Paste YouTube URL, video file URL, or iframe embed code..."
3168
+ :class="{ error: videoForm.src && !isValidVideoUrl(videoForm.src) }" @keydown.enter="submitVideo"
3169
+ />
3170
+
3171
+ <div v-if="videoForm.src && !isValidVideoUrl(videoForm.src)" class="flex gap-025 opacity-5 -mt-05">
3172
+ <Icon name="warning" />
3173
+ <p class="txt12">
3174
+ Please enter a valid video URL or iframe embed code
3175
+ </p>
3142
3176
  </div>
3143
3177
 
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>
3178
+ <TextInput
3179
+ v-model="videoForm.width" label="Width" placeholder="100% or 500px or 50vw"
3180
+ help="Examples: 100%, 500px, 50vw, 300"
3181
+ />
3182
+
3183
+ <SelectInput
3184
+ v-model="videoForm.aspectRatio" label="Aspect Ratio" :options="[
3185
+ { value: '16:9', label: '16:9 (Standard)' },
3186
+ { value: '4:3', label: '4:3 (Classic)' },
3187
+ { value: '9:16', label: '9:16 (Vertical/Shorts)' },
3188
+ { value: '21:9', label: '21:9 (Cinematic)' },
3189
+ { value: '1:1', label: '1:1 (Square)' },
3190
+ { value: 'custom', label: 'Custom' },
3191
+ ]"
3192
+ />
3193
+
3194
+ <div v-if="videoForm.aspectRatio === 'custom'" class="grid grid-wrap-2 gap-05">
3195
+ <TextInput v-model="videoForm.customWidth" label="Width ratio" placeholder="16" type="number" />
3196
+ <TextInput v-model="videoForm.customHeight" label="Height ratio" placeholder="9" type="number" />
3157
3197
  </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
3198
 
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>
3199
+ <div class="grid grid-wrap-2 gap-05 py-1">
3200
+ <CheckInput v-model="videoForm.controls" label="Show controls" />
3201
+ <CheckInput v-model="videoForm.autoplay" label="Autoplay" />
3202
+ <CheckInput v-model="videoForm.mute" label="Mute" />
3203
+ <CheckInput v-model="videoForm.loop" label="Loop" />
3204
+ <CheckInput v-model="videoForm.showCaption" class="grid-span-2" label="Show caption below video" />
3205
+ <TextInput
3206
+ v-if="videoForm.showCaption" v-model="videoForm.caption" label="Caption" class="grid-span-2"
3207
+ placeholder="Describe the video content"
3208
+ />
3169
3209
  </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>
3210
+
3211
+ <!-- Video Preview -->
3212
+ <Card v-if="videoForm.src && isValidVideoUrl(videoForm.src)" frame thin class="bg-gray-20">
3213
+ <p class="label">
3214
+ Preview:
3215
+ </p>
3216
+ <div class="overflow-hidden flex justify-content-center">
3217
+ <BglVideo
3218
+ :src="videoForm.src" class=""
3219
+ :aspect-ratio="videoForm.aspectRatio === 'custom' && videoForm.customWidth && videoForm.customHeight ? `${videoForm.customWidth}:${videoForm.customHeight}` : videoForm.aspectRatio"
3220
+ :autoplay="false" :mute="videoForm.mute" :controls="videoForm.controls"
3221
+ :loop="videoForm.loop"
3222
+ />
3223
+ </div>
3224
+ </Card>
3225
+ </div>
3226
+ <template #footer>
3227
+ <div class="flex gap-05 w-100 ">
3228
+ <Btn value="Cancel" flat thin @click="showVideoModal = false" />
3229
+ <Btn
3230
+ v-if="pendingVideoData?.existingVideo" value="Delete Video" color="red" flat thin icon="delete"
3231
+ @click="deleteVideo"
3232
+ />
3233
+ <Btn
3234
+ value="Insert Video" class="ms-auto" :disabled="!videoForm.src || !isValidVideoUrl(videoForm.src)"
3235
+ @click="submitVideo"
3236
+ />
3177
3237
  </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" />
3238
+ </template>
3239
+ </Modal>
3240
+
3241
+ <!-- Table Editor Modal -->
3242
+ <Modal
3243
+ v-model:visible="showTableEditor" :title="pendingTableData?.existingTable ? 'Edit Table' : 'Insert Table'"
3244
+ width="700"
3245
+ >
3246
+ <template #default>
3247
+ <div class="grid grid-wrap-4 m_grid-wrap-2 gap-col-1 table-editor testMe1">
3248
+ <!-- Structure Section -->
3249
+ <div class="flex gap-05 white-space mt-0 grid-span-4 m_grid-span-2 pt-1 pb-05">
3250
+ <div class="line" />
3251
+ <p class="label grid-span-4 m_grid-span-2">
3252
+ Table Structure
3253
+ </p>
3254
+ <div class="line" />
3255
+ </div>
3256
+ <NumberInput v-model="tableForm.rows" :min="1" :max="20" label="Rows" />
3257
+ <NumberInput v-model="tableForm.cols" :min="1" :max="10" label="Columns" />
3258
+ <!-- Cell Text Alignment -->
3259
+ <div class="grid-span-1">
3260
+ <label class="label">Cell Text Alignment</label>
3261
+ <div
3262
+ class="flex gap-025 mt-025 radius-1 p-05 w-fit"
3263
+ style="height: var(--input-height); background: var(--input-bg);"
3264
+ >
3265
+ <Btn
3266
+ :class="{ activeBtn: tableForm.alignment === 'left' }" flat thin icon="format_align_left"
3267
+ title="Align Left" @click="tableForm.alignment = 'left'"
3268
+ />
3269
+ <Btn
3270
+ :class="{ activeBtn: tableForm.alignment === 'center' }" flat thin
3271
+ icon="format_align_center" title="Align Center" @click="tableForm.alignment = 'center'"
3272
+ />
3273
+ <Btn
3274
+ :class="{ activeBtn: tableForm.alignment === 'right' }" flat thin icon="format_align_right"
3275
+ title="Align Right" @click="tableForm.alignment = 'right'"
3276
+ />
3277
+ </div>
3278
+ </div>
3279
+
3280
+ <!-- Text Direction -->
3281
+ <div class="grid-span-1">
3282
+ <label class="label">Text Direction</label>
3283
+ <div
3284
+ class="flex gap-025 mt-025 radius-1 p-025 w-fit"
3285
+ style="height: var(--input-height); background: var(--input-bg);"
3286
+ >
3287
+ <Btn
3288
+ :class="{ activeBtn: tableForm.direction === 'ltr' }" flat thin value="LTR"
3289
+ title="Left to Right" @click="tableForm.direction = 'ltr'"
3290
+ />
3291
+ <Btn
3292
+ :class="{ activeBtn: tableForm.direction === 'rtl' }" flat thin value="RTL"
3293
+ title="Right to Left" @click="tableForm.direction = 'rtl'"
3294
+ />
3295
+ </div>
3296
+ </div>
3297
+ <!-- Style Section -->
3298
+ <div class="flex gap-05 white-space mt-0 grid-span-4 m_grid-span-2 pt-1 pb-05">
3299
+ <div class="line" />
3300
+ <p class="label grid-span-4 m_grid-span-2">
3301
+ Table Style
3302
+ </p>
3303
+ <div class="line" />
3304
+ </div>
3305
+ <NumberInput v-model="tableForm.width" label="Width (%)" :min="25" :max="100" />
3306
+ <NumberInput v-model="tableForm.cellPadding" label="Cell Padding (px)" :min="2" :max="20" />
3307
+ <NumberInput v-model="tableForm.borderWidth" label="Border Width (px)" :min="0" :max="5" />
3308
+ <ColorInput v-model="tableForm.borderColor" label="Border Color" />
3309
+ <CheckInput
3310
+ v-model="tableForm.fixedLayout" label="Fixed cell width (doesn't change by content)"
3311
+ class="grid-span-4 m_grid-span-2"
3312
+ />
3313
+
3314
+ <!-- Cell Colors -->
3315
+ <div class="flex gap-05 white-space mt-0 grid-span-4 m_grid-span-2 pt-1 pb-05">
3316
+ <div class="line" />
3317
+ <p class="label grid-span-4 m_grid-span-2">
3318
+ Cell Colors:
3319
+ </p>
3320
+ <div class="line" />
3321
+ </div>
3322
+ <ColorInput
3323
+ v-model="tableForm.cellBgColor" class="grid-span-2 m_grid-span-1"
3324
+ label="Background Color"
3325
+ />
3326
+ <ColorInput v-model="tableForm.cellTextColor" class="grid-span-2 m_grid-span-1" label="Text Color" />
3327
+ <!-- Alternating Rows -->
3328
+ <div class="flex gap-05 white-space mt-0 grid-span-4 m_grid-span-2 pt-1 pb-05">
3329
+ <div class="line" />
3330
+ <p class="label grid-span-4 m_grid-span-2">
3331
+ Row Styles:
3332
+ </p>
3333
+ <div class="line" />
3334
+ </div>
3335
+ <CheckInput v-model="tableForm.showHeaders" label="Show header row" class="grid-span-4 m_grid-span-2" />
3336
+ <!-- Header Colors -->
3337
+ <div
3338
+ v-if="tableForm.showHeaders"
3339
+ 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"
3340
+ >
3341
+ <ColorInput
3342
+ v-model="tableForm.headerBgColor" class="grid-span-2 m_grid-span-1"
3343
+ label="Header Background Color"
3344
+ />
3345
+ <ColorInput
3346
+ v-model="tableForm.headerTextColor" class="grid-span-2 m_grid-span-1"
3347
+ label="Header Text Color"
3348
+ />
3349
+ </div>
3350
+ <CheckInput
3351
+ v-model="tableForm.alternateRows" label="Alternating Row Colors"
3352
+ class="grid-span-4 m_grid-span-2"
3353
+ />
3354
+ <div
3355
+ v-if="tableForm.alternateRows"
3356
+ 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"
3357
+ >
3358
+ <ColorInput
3359
+ v-model="tableForm.alternateRowBgColor" class="grid-span-2 m_grid-span-1"
3360
+ label="Alternate Row Background:"
3361
+ />
3362
+ <ColorInput
3363
+ v-model="tableForm.alternateRowTextColor" class="grid-span-2 m_grid-span-1"
3364
+ label="Alternate Row Text:"
3365
+ />
3366
+ </div>
3183
3367
  </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:" />
3368
+ <!-- Table Preview -->
3369
+ <div class="flex gap-05 white-space mt-0 grid-span-4 m_grid-span-2 pt-1 pb-05">
3370
+ <div class="line" />
3371
+ <p class="label grid-span-4 m_grid-span-2">
3372
+ Preview
3373
+ </p>
3374
+ <div class="line" />
3188
3375
  </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>
3376
+ <div style="zoom: 0.8;" class="opacity-7 user-select-none pointer-events-none" v-html="tablePreviewHtml" />
3377
+ </template>
3378
+
3379
+ <template #footer>
3380
+ <Btn value="Cancel" flat thin @click="showTableEditor = false" />
3381
+ <Btn
3382
+ v-if="pendingTableData?.existingTable" value="Delete Table" color="red" flat thin icon="delete"
3383
+ @click="deleteTable"
3384
+ />
3385
+ <Btn
3386
+ :value="pendingTableData?.existingTable ? 'Save' : 'Insert Table'" class="ms-auto"
3387
+ @click="submitTable"
3388
+ />
3389
+ </template>
3390
+ </Modal>
3206
3391
  </template>
3207
3392
 
3208
3393
  <style>
@@ -3239,7 +3424,6 @@ defineExpose({
3239
3424
  position: relative;
3240
3425
  }
3241
3426
 
3242
-
3243
3427
  .content-area table:hover {
3244
3428
  outline: 2px solid rgba(0, 123, 204, 0.3);
3245
3429
  outline-offset: 2px;