@bagelink/vue 1.16.0 → 1.17.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assetTransport.d.ts +28 -0
- package/dist/assetTransport.d.ts.map +1 -0
- package/dist/assetTransport.test.d.ts +2 -0
- package/dist/assetTransport.test.d.ts.map +1 -0
- package/dist/assets.d.ts +65 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.test.d.ts +2 -0
- package/dist/assets.test.d.ts.map +1 -0
- package/dist/components/AddressSearch.vue.d.ts +40 -36
- package/dist/components/AddressSearch.vue.d.ts.map +1 -1
- package/dist/components/ContextMenu.vue.d.ts +518 -0
- package/dist/components/ContextMenu.vue.d.ts.map +1 -0
- package/dist/components/Dropdown.types.d.ts +25 -0
- package/dist/components/Dropdown.types.d.ts.map +1 -0
- package/dist/components/Dropdown.vue.d.ts +20 -15
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/FilterQuery.vue.d.ts +1 -1
- package/dist/components/FilterQuery.vue.d.ts.map +1 -1
- package/dist/components/Icon/Icon.vue.d.ts +0 -1
- package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
- package/dist/components/Icon/constants.d.ts +0 -1
- package/dist/components/Icon/constants.d.ts.map +1 -1
- package/dist/components/Icon/types.d.ts +2 -3
- package/dist/components/Icon/types.d.ts.map +1 -1
- package/dist/components/Image.vue.d.ts +7 -1
- package/dist/components/Image.vue.d.ts.map +1 -1
- package/dist/components/Swiper.vue.d.ts +1 -1
- package/dist/components/draggable/useDraggable.d.ts.map +1 -1
- package/dist/components/form/inputs/IconPickerInput.vue.d.ts +48 -37
- package/dist/components/form/inputs/IconPickerInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +3 -0
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts +2 -0
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts +34 -8
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/form/inputs/SelectInput.vue.d.ts +71 -63
- package/dist/components/form/inputs/SelectInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/TelInput.vue.d.ts +2 -2
- package/dist/components/form/inputs/Upload/AssetUploadInput.vue.d.ts +11 -0
- package/dist/components/form/inputs/Upload/AssetUploadInput.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts +2 -2
- package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts.map +1 -1
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/lightbox/AssetPreview.vue.d.ts +9 -0
- package/dist/components/lightbox/AssetPreview.vue.d.ts.map +1 -0
- package/dist/components/lightbox/Lightbox.vue.d.ts.map +1 -1
- package/dist/components/lightbox/LightboxImage.vue.d.ts +2 -1
- package/dist/components/lightbox/LightboxImage.vue.d.ts.map +1 -1
- package/dist/components/lightbox/index.d.ts.map +1 -1
- package/dist/components/lightbox/lightbox.types.d.ts +7 -0
- package/dist/components/lightbox/lightbox.types.d.ts.map +1 -1
- package/dist/composables/index.d.ts +1 -1
- package/dist/composables/index.d.ts.map +1 -1
- package/dist/composables/useBoundedRectDrag.d.ts.map +1 -1
- package/dist/composables/useImageSrc.d.ts +8 -4
- package/dist/composables/useImageSrc.d.ts.map +1 -1
- package/dist/composables/usePointerSession.d.ts +14 -3
- package/dist/composables/usePointerSession.d.ts.map +1 -1
- package/dist/core.css +3 -3
- package/dist/directives/vResize.d.ts.map +1 -1
- package/dist/form-flow/FormFlowPlayer.vue.d.ts +3 -3
- package/dist/form-flow/FormFlowPlayer.vue.d.ts.map +1 -1
- package/dist/form-flow/form-flow.d.ts +13 -6
- package/dist/form-flow/form-flow.d.ts.map +1 -1
- package/dist/form-flow/schema-fields.d.ts +6 -0
- package/dist/form-flow/schema-fields.d.ts.map +1 -1
- package/dist/index.cjs +240 -153
- package/dist/index.css +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +24167 -23417
- package/dist/style.css +3 -3
- package/dist/types/BagelForm.d.ts +5 -1
- package/dist/types/BagelForm.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/assetTransport.test.ts +75 -0
- package/src/assetTransport.ts +98 -0
- package/src/assets.test.ts +82 -0
- package/src/assets.ts +162 -0
- package/src/components/ContextMenu.vue +113 -0
- package/src/components/Dropdown.types.ts +27 -0
- package/src/components/Dropdown.vue +129 -64
- package/src/components/FilterQuery.vue +1 -1
- package/src/components/Icon/Icon.vue +8 -30
- package/src/components/Icon/constants.ts +0 -166
- package/src/components/Icon/types.ts +2 -3
- package/src/components/Image.vue +19 -3
- package/src/components/draggable/useDraggable.ts +67 -100
- package/src/components/form/inputs/IconPickerInput.vue +14 -9
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +43 -111
- package/src/components/form/inputs/RichText/composables/useCommands.ts +32 -54
- package/src/components/form/inputs/RichText/composables/useEditor.ts +45 -171
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +90 -14
- package/src/components/form/inputs/RichText/index.vue +1195 -1210
- package/src/components/form/inputs/RichText/utils/commands.ts +689 -1028
- package/src/components/form/inputs/RichText/utils/selection.ts +66 -41
- package/src/components/form/inputs/RichText/utils/table.ts +798 -136
- package/src/components/form/inputs/SelectInput.vue +1 -1
- package/src/components/form/inputs/Upload/AssetUploadInput.vue +67 -0
- package/src/components/form/inputs/Upload/UploadInput.vue +5 -1
- package/src/components/index.ts +3 -1
- package/src/components/lightbox/AssetPreview.vue +16 -0
- package/src/components/lightbox/Lightbox.vue +8 -3
- package/src/components/lightbox/LightboxImage.vue +4 -3
- package/src/components/lightbox/index.ts +29 -4
- package/src/components/lightbox/lightbox.types.ts +5 -1
- package/src/composables/index.ts +1 -1
- package/src/composables/useBoundedRectDrag.ts +9 -12
- package/src/composables/useImageSrc.ts +47 -37
- package/src/composables/usePointerSession.ts +57 -12
- package/src/directives/vResize.ts +6 -7
- package/src/form-flow/form-flow.ts +31 -7
- package/src/form-flow/schema-fields.ts +28 -1
- package/src/index.ts +3 -0
- package/src/styles/core.css +1 -18
- package/src/types/BagelForm.ts +5 -1
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts +0 -10
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +0 -1
- package/src/components/form/inputs/RichText/components/gridBox.vue +0 -58
- package/src/components/form/inputs/RichText/utils/formatting.ts +0 -331
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<!-- eslint-disable vue/multi-word-component-names -->
|
|
2
2
|
<script setup lang="ts">
|
|
3
3
|
import type { CustomToolbarButton, RichTextExtension, RichTextToolbarApi, ToolbarConfig } from './richTextTypes'
|
|
4
|
-
import {
|
|
4
|
+
import type { DropdownItem } from '../../../../index'
|
|
5
|
+
import { CodeEditor, copyText, Btn, ContextMenu, Dialog, Divider, BglVideo, Icon, Card, ColorInput, pathKeyToURL, usePointerSession } from '../../../../index'
|
|
5
6
|
import { watch, onUnmounted, onBeforeUnmount, onMounted, ref, computed, useAttrs, inject } from 'vue'
|
|
6
7
|
import CheckInput from '../CheckInput.vue'
|
|
7
8
|
import NumberInput from '../NumberInput.vue'
|
|
@@ -11,8 +12,21 @@ import TextInput from '../TextInput.vue'
|
|
|
11
12
|
import UploadInput from '../Upload/UploadInput.vue'
|
|
12
13
|
import EditorToolbar from './components/EditorToolbar.vue'
|
|
13
14
|
import { useCommands } from './composables/useCommands'
|
|
14
|
-
import { useEditor } from './composables/useEditor'
|
|
15
|
+
import { pushHistorySnapshot, useEditor } from './composables/useEditor'
|
|
15
16
|
import { useEditorKeyboard } from './composables/useEditorKeyboard'
|
|
17
|
+
import {
|
|
18
|
+
addRow as tableAddRow,
|
|
19
|
+
buildTableGrid,
|
|
20
|
+
deleteColumn as tableDeleteColumn,
|
|
21
|
+
deleteRow as tableDeleteRow,
|
|
22
|
+
findCellCoords,
|
|
23
|
+
insertColumn as tableInsertColumn,
|
|
24
|
+
mergeCells as tableMergeCells,
|
|
25
|
+
moveColumn as tableMoveColumn,
|
|
26
|
+
removeTableElement,
|
|
27
|
+
splitCell as tableSplitCell,
|
|
28
|
+
syncTableColgroup,
|
|
29
|
+
} from './utils/table'
|
|
16
30
|
import type { BagelInputShellProps } from '../bagelInputShell'
|
|
17
31
|
import { useBagelInputShell } from '../bagelInputShell'
|
|
18
32
|
import { RICH_TEXT_EXTENSION_REGISTRY_KEY } from './richTextExtensions'
|
|
@@ -452,63 +466,6 @@ let pendingTableData: {
|
|
|
452
466
|
existingTable: HTMLTableElement | null
|
|
453
467
|
} | null = null
|
|
454
468
|
|
|
455
|
-
// Computed property for table preview
|
|
456
|
-
const tablePreviewHtml = computed(() => {
|
|
457
|
-
const form = tableForm.value
|
|
458
|
-
|
|
459
|
-
const tableStyle = `
|
|
460
|
-
width: ${form.width}%;
|
|
461
|
-
border-collapse: collapse;
|
|
462
|
-
margin-bottom: 1rem;
|
|
463
|
-
border: ${form.borderWidth}px solid ${form.borderColor};
|
|
464
|
-
table-layout: ${form.fixedLayout ? 'fixed' : 'auto'};
|
|
465
|
-
`
|
|
466
|
-
|
|
467
|
-
// For cell text alignment, we'll apply it to individual cells, not the table
|
|
468
|
-
|
|
469
|
-
let html = `<table style="${tableStyle}" dir="${form.direction}">`
|
|
470
|
-
html += `<colgroup>${form.columnWidths.slice(0, form.cols).map(width => `<col style="width: ${width}%;">`).join('')}</colgroup>`
|
|
471
|
-
|
|
472
|
-
// Add header row if enabled
|
|
473
|
-
if (form.showHeaders) {
|
|
474
|
-
html += '<thead><tr>'
|
|
475
|
-
for (let j = 0; j < form.cols; j++) {
|
|
476
|
-
const thStyle = `
|
|
477
|
-
padding: ${form.cellPadding}px;
|
|
478
|
-
border: ${form.borderWidth}px solid ${form.borderColor};
|
|
479
|
-
background-color: ${form.headerBgColor};
|
|
480
|
-
color: ${form.headerTextColor};
|
|
481
|
-
text-align: ${form.alignment};
|
|
482
|
-
${form.fixedLayout ? `width: ${form.columnWidths[j] ?? (100 / form.cols)}%;` : ''}
|
|
483
|
-
`
|
|
484
|
-
html += `<th style="${thStyle}">Header ${j + 1}</th>`
|
|
485
|
-
}
|
|
486
|
-
html += '</tr></thead>'
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Add body rows
|
|
490
|
-
html += '<tbody>'
|
|
491
|
-
for (let i = 0; i < form.rows; i++) {
|
|
492
|
-
html += '<tr>'
|
|
493
|
-
for (let j = 0; j < form.cols; j++) {
|
|
494
|
-
const tdStyle = `
|
|
495
|
-
padding: ${form.cellPadding}px;
|
|
496
|
-
border: ${form.borderWidth}px solid ${form.borderColor};
|
|
497
|
-
text-align: ${form.alignment};
|
|
498
|
-
${form.fixedLayout ? `width: ${form.columnWidths[j] ?? (100 / form.cols)}%;` : ''}
|
|
499
|
-
${form.alternateRows && i % 2 === 1
|
|
500
|
-
? `background-color: ${form.alternateRowBgColor}; color: ${form.alternateRowTextColor};`
|
|
501
|
-
: `background-color: ${form.cellBgColor}; color: ${form.cellTextColor};`}
|
|
502
|
-
`
|
|
503
|
-
html += `<td style="${tdStyle}">Cell ${i + 1},${j + 1}</td>`
|
|
504
|
-
}
|
|
505
|
-
html += '</tr>'
|
|
506
|
-
}
|
|
507
|
-
html += '</tbody></table>'
|
|
508
|
-
|
|
509
|
-
return html
|
|
510
|
-
})
|
|
511
|
-
|
|
512
469
|
// Initialize content from modelValue
|
|
513
470
|
editor.state.content = normalizedModelValue.value
|
|
514
471
|
|
|
@@ -526,7 +483,9 @@ function getTextDirection(text: string, fallback: 'ltr' | 'rtl' = 'ltr'): 'ltr'
|
|
|
526
483
|
|
|
527
484
|
function getBlockDirection(element: Element, fallback: 'ltr' | 'rtl', preserveExplicit = true): 'ltr' | 'rtl' {
|
|
528
485
|
const explicitDirection = element.getAttribute('dir')
|
|
529
|
-
if (preserveExplicit && (explicitDirection === 'rtl' || explicitDirection === 'ltr')) {
|
|
486
|
+
if ((preserveExplicit || element.hasAttribute('data-dir-explicit')) && (explicitDirection === 'rtl' || explicitDirection === 'ltr')) {
|
|
487
|
+
return explicitDirection
|
|
488
|
+
}
|
|
530
489
|
return getTextDirection(element.textContent || '', fallback)
|
|
531
490
|
}
|
|
532
491
|
|
|
@@ -540,7 +499,7 @@ function detectAndSetDirection() {
|
|
|
540
499
|
|
|
541
500
|
doc.querySelectorAll('p,h1,h2,h3,h4,h5,h6,blockquote,li').forEach((block) => {
|
|
542
501
|
if (!block.classList.contains('placeholder')) {
|
|
543
|
-
block.setAttribute('dir', getBlockDirection(block, fallback,
|
|
502
|
+
block.setAttribute('dir', getBlockDirection(block, fallback, true))
|
|
544
503
|
}
|
|
545
504
|
})
|
|
546
505
|
}
|
|
@@ -946,22 +905,27 @@ function submitImage() {
|
|
|
946
905
|
img.style.height = 'auto'
|
|
947
906
|
}
|
|
948
907
|
|
|
949
|
-
// Helper function to create figcaption with alt text and credit
|
|
908
|
+
// Helper function to create figcaption with alt text and credit (using safe DOM nodes)
|
|
950
909
|
function createFigcaption() {
|
|
951
910
|
const figcaption = doc!.createElement('figcaption')
|
|
952
|
-
let captionHTML = ''
|
|
953
911
|
|
|
954
|
-
// Add alt text only if figcaption checkbox is checked (with bold styling)
|
|
955
912
|
if (imageForm.value.figcaption && imageForm.value.alt) {
|
|
956
|
-
|
|
913
|
+
const altStrong = doc!.createElement('strong')
|
|
914
|
+
altStrong.className = 'alt-text'
|
|
915
|
+
altStrong.textContent = imageForm.value.alt
|
|
916
|
+
figcaption.appendChild(altStrong)
|
|
957
917
|
}
|
|
958
918
|
|
|
959
|
-
// Always add credit if it exists (with lighter styling)
|
|
960
919
|
if (imageForm.value.credit) {
|
|
961
|
-
|
|
920
|
+
if (figcaption.childNodes.length > 0) {
|
|
921
|
+
figcaption.appendChild(doc!.createTextNode(' • '))
|
|
922
|
+
}
|
|
923
|
+
const creditSpan = doc!.createElement('span')
|
|
924
|
+
creditSpan.className = 'photo-credit'
|
|
925
|
+
creditSpan.textContent = imageForm.value.credit
|
|
926
|
+
figcaption.appendChild(creditSpan)
|
|
962
927
|
}
|
|
963
928
|
|
|
964
|
-
figcaption.innerHTML = captionHTML
|
|
965
929
|
return figcaption
|
|
966
930
|
}
|
|
967
931
|
|
|
@@ -1461,38 +1425,62 @@ function deleteVideo() {
|
|
|
1461
1425
|
// Function to open table editor
|
|
1462
1426
|
// Function to detect table alignment from table or wrapper
|
|
1463
1427
|
function detectTableAlignment(table: HTMLTableElement): 'left' | 'center' | 'right' {
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1428
|
+
const wrapper = table.closest('.table-wrapper') as HTMLElement | null
|
|
1429
|
+
if (wrapper) {
|
|
1430
|
+
const { alignItems } = wrapper.style
|
|
1431
|
+
if (alignItems === 'center') { return 'center' }
|
|
1432
|
+
if (alignItems === 'flex-end') { return 'right' }
|
|
1433
|
+
if (alignItems === 'flex-start') { return 'left' }
|
|
1434
|
+
}
|
|
1467
1435
|
|
|
1436
|
+
const { marginLeft, marginRight } = table.style
|
|
1468
1437
|
if (marginLeft === 'auto' && marginRight === 'auto') {
|
|
1469
1438
|
return 'center'
|
|
1470
|
-
}
|
|
1439
|
+
}
|
|
1440
|
+
if (marginLeft === 'auto' && marginRight === '0') {
|
|
1471
1441
|
return 'right'
|
|
1442
|
+
}
|
|
1443
|
+
return 'left'
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function applyTableAlignmentToDOM(table: HTMLTableElement, alignment: 'left' | 'center' | 'right') {
|
|
1447
|
+
const wrapper = table.closest('.table-wrapper') as HTMLElement | null
|
|
1448
|
+
if (wrapper) {
|
|
1449
|
+
if (alignment === 'center') {
|
|
1450
|
+
wrapper.style.alignItems = 'center'
|
|
1451
|
+
} else if (alignment === 'right') {
|
|
1452
|
+
wrapper.style.alignItems = 'flex-end'
|
|
1453
|
+
} else {
|
|
1454
|
+
wrapper.style.alignItems = 'flex-start'
|
|
1455
|
+
}
|
|
1456
|
+
table.style.marginLeft = '0'
|
|
1457
|
+
table.style.marginRight = '0'
|
|
1472
1458
|
} else {
|
|
1473
|
-
|
|
1459
|
+
if (alignment === 'center') {
|
|
1460
|
+
table.style.marginLeft = 'auto'
|
|
1461
|
+
table.style.marginRight = 'auto'
|
|
1462
|
+
} else if (alignment === 'right') {
|
|
1463
|
+
table.style.marginLeft = 'auto'
|
|
1464
|
+
table.style.marginRight = '0'
|
|
1465
|
+
} else {
|
|
1466
|
+
table.style.marginLeft = '0'
|
|
1467
|
+
table.style.marginRight = 'auto'
|
|
1468
|
+
}
|
|
1474
1469
|
}
|
|
1475
1470
|
}
|
|
1476
1471
|
|
|
1477
1472
|
function openTableEditor(existingTable: HTMLTableElement | null = null) {
|
|
1478
|
-
console.log('[RichText table editor] open requested', {
|
|
1479
|
-
hasTable: Boolean(existingTable),
|
|
1480
|
-
isFullscreen: editor.state.isFullscreen,
|
|
1481
|
-
})
|
|
1482
1473
|
const { doc } = editor.state
|
|
1483
1474
|
if (!doc) {
|
|
1484
|
-
console.warn('[RichText table editor] aborted: editor document is unavailable')
|
|
1485
1475
|
return
|
|
1486
1476
|
}
|
|
1487
1477
|
|
|
1488
|
-
// Get current selection for new tables
|
|
1489
1478
|
if (!existingTable) {
|
|
1490
1479
|
const selection = doc.getSelection()
|
|
1491
1480
|
if (!selection || !selection.rangeCount) { return }
|
|
1492
1481
|
const range = selection.getRangeAt(0)
|
|
1493
1482
|
pendingTableData = { selection, range, existingTable: null }
|
|
1494
1483
|
} else {
|
|
1495
|
-
// For existing tables, set pendingTableData with the existing element
|
|
1496
1484
|
pendingTableData = {
|
|
1497
1485
|
selection: doc.getSelection()!,
|
|
1498
1486
|
range: doc.createRange(),
|
|
@@ -1501,13 +1489,11 @@ function openTableEditor(existingTable: HTMLTableElement | null = null) {
|
|
|
1501
1489
|
}
|
|
1502
1490
|
|
|
1503
1491
|
if (existingTable) {
|
|
1504
|
-
// Populate form with existing table data
|
|
1505
1492
|
const tbody = existingTable.querySelector('tbody')
|
|
1506
1493
|
const thead = existingTable.querySelector('thead')
|
|
1507
1494
|
const editorWindow = existingTable.ownerDocument.defaultView
|
|
1508
1495
|
const getEditorStyle = (element: Element) => editorWindow?.getComputedStyle(element) ?? null
|
|
1509
1496
|
|
|
1510
|
-
// Detect alternating rows by comparing background colors
|
|
1511
1497
|
let hasAlternatingRows = false
|
|
1512
1498
|
let alternateRowBg = '#f9f9f9'
|
|
1513
1499
|
let alternateRowTextColor = '#333333'
|
|
@@ -1523,7 +1509,6 @@ function openTableEditor(existingTable: HTMLTableElement | null = null) {
|
|
|
1523
1509
|
const firstBgColor = getEditorStyle(firstCell)?.backgroundColor || ''
|
|
1524
1510
|
const secondBgColor = getEditorStyle(secondCell)?.backgroundColor || ''
|
|
1525
1511
|
|
|
1526
|
-
// Check if colors are different (indicating alternating rows)
|
|
1527
1512
|
if (firstBgColor !== secondBgColor) {
|
|
1528
1513
|
hasAlternatingRows = true
|
|
1529
1514
|
alternateRowBg = secondBgColor
|
|
@@ -1532,14 +1517,19 @@ function openTableEditor(existingTable: HTMLTableElement | null = null) {
|
|
|
1532
1517
|
}
|
|
1533
1518
|
}
|
|
1534
1519
|
|
|
1520
|
+
const gridMap = buildTableGrid(existingTable)
|
|
1521
|
+
const cols = gridMap.colCount || (tbody && tbody.rows.length > 0 ? tbody.rows[0].cells.length : 3)
|
|
1522
|
+
const firstCellEl = existingTable.querySelector('td, th') as HTMLElement | null
|
|
1523
|
+
const parsedPadding = firstCellEl ? Number.parseInt(firstCellEl.style.padding, 10) : Number.NaN
|
|
1524
|
+
|
|
1535
1525
|
tableForm.value = {
|
|
1536
|
-
rows: tbody ? tbody.rows.length :
|
|
1537
|
-
cols
|
|
1538
|
-
width: Number.parseInt(existingTable.style.width) || 100,
|
|
1539
|
-
borderWidth: Number.parseInt(existingTable.style.borderWidth)
|
|
1526
|
+
rows: tbody ? tbody.rows.length : Math.max(1, existingTable.rows.length - (thead ? thead.rows.length : 0)),
|
|
1527
|
+
cols,
|
|
1528
|
+
width: Number.parseInt(existingTable.style.width, 10) || 100,
|
|
1529
|
+
borderWidth: Number.isNaN(Number.parseInt(existingTable.style.borderWidth, 10)) ? 1 : Number.parseInt(existingTable.style.borderWidth, 10),
|
|
1540
1530
|
borderColor: existingTable.style.borderColor || '#dddddd',
|
|
1541
|
-
cellPadding: 8
|
|
1542
|
-
showHeaders: !!thead,
|
|
1531
|
+
cellPadding: Number.isNaN(parsedPadding) ? 8 : parsedPadding,
|
|
1532
|
+
showHeaders: !!thead && thead.rows.length > 0,
|
|
1543
1533
|
headerBgColor: thead ? getEditorStyle(thead.querySelector('th') || thead)?.backgroundColor || '#f4f4f4' : '#f4f4f4',
|
|
1544
1534
|
headerTextColor: thead ? getEditorStyle(thead.querySelector('th') || thead)?.color || '#333333' : '#333333',
|
|
1545
1535
|
cellBgColor: tbody ? getEditorStyle(tbody.querySelector('td') || tbody)?.backgroundColor || '#ffffff' : '#ffffff',
|
|
@@ -1547,25 +1537,21 @@ function openTableEditor(existingTable: HTMLTableElement | null = null) {
|
|
|
1547
1537
|
alternateRows: hasAlternatingRows,
|
|
1548
1538
|
alternateRowBgColor: alternateRowBg,
|
|
1549
1539
|
alternateRowTextColor,
|
|
1550
|
-
fixedLayout: existingTable.style.tableLayout === 'fixed'
|
|
1540
|
+
fixedLayout: existingTable.style.tableLayout ? existingTable.style.tableLayout === 'fixed' : true,
|
|
1551
1541
|
columnWidths: (() => {
|
|
1552
|
-
const cols = tbody && tbody.rows.length > 0 ? tbody.rows[0].cells.length : 3
|
|
1553
1542
|
const columnElements = Array.from(existingTable.querySelectorAll(':scope > colgroup > col')) as HTMLElement[]
|
|
1554
1543
|
if (columnElements.length === cols) {
|
|
1555
1544
|
return columnElements.map((column, index) =>
|
|
1556
1545
|
Number.parseFloat(column.style.width)
|
|
1557
1546
|
|| Number.parseFloat((existingTable.rows[0]?.cells[index] as HTMLElement | undefined)?.style.width || '')
|
|
1558
|
-
|| (100 / cols))
|
|
1547
|
+
|| Number((100 / cols).toFixed(2)))
|
|
1559
1548
|
}
|
|
1560
|
-
return
|
|
1561
|
-
Number.parseFloat((existingTable.rows[0]?.cells[index] as HTMLElement | undefined)?.style.width || '')
|
|
1562
|
-
|| (100 / cols))
|
|
1549
|
+
return equalColumnWidths(cols)
|
|
1563
1550
|
})(),
|
|
1564
1551
|
alignment: detectTableAlignment(existingTable),
|
|
1565
|
-
direction: existingTable.dir || 'ltr'
|
|
1552
|
+
direction: (existingTable.dir || doc.body.dir || 'ltr') as 'ltr' | 'rtl'
|
|
1566
1553
|
}
|
|
1567
1554
|
} else {
|
|
1568
|
-
// Reset form for new table
|
|
1569
1555
|
tableForm.value = {
|
|
1570
1556
|
rows: 3,
|
|
1571
1557
|
cols: 3,
|
|
@@ -1584,14 +1570,10 @@ function openTableEditor(existingTable: HTMLTableElement | null = null) {
|
|
|
1584
1570
|
fixedLayout: true,
|
|
1585
1571
|
columnWidths: equalColumnWidths(3),
|
|
1586
1572
|
alignment: 'left',
|
|
1587
|
-
direction: 'ltr'
|
|
1573
|
+
direction: (doc.body.dir || 'ltr') as 'ltr' | 'rtl'
|
|
1588
1574
|
}
|
|
1589
1575
|
}
|
|
1590
1576
|
showTableEditor.value = true
|
|
1591
|
-
console.log('[RichText table editor] dialog state opened', {
|
|
1592
|
-
open: showTableEditor.value,
|
|
1593
|
-
hasPendingTable: Boolean(pendingTableData?.existingTable),
|
|
1594
|
-
})
|
|
1595
1577
|
}
|
|
1596
1578
|
|
|
1597
1579
|
function openExistingTableAdvancedSettings() {
|
|
@@ -1602,67 +1584,129 @@ function openExistingTableAdvancedSettings() {
|
|
|
1602
1584
|
showTableContextMenu.value = false
|
|
1603
1585
|
}
|
|
1604
1586
|
|
|
1605
|
-
// Function to submit table changes
|
|
1606
1587
|
function submitTable() {
|
|
1607
1588
|
if (!pendingTableData) { return }
|
|
1608
1589
|
|
|
1609
1590
|
const { doc } = editor.state
|
|
1610
1591
|
if (!doc) { return }
|
|
1611
1592
|
|
|
1593
|
+
const targetRows = Math.max(1, Number(tableForm.value.rows) || 1)
|
|
1594
|
+
const targetCols = Math.max(1, Number(tableForm.value.cols) || 1)
|
|
1595
|
+
|
|
1612
1596
|
if (pendingTableData.existingTable) {
|
|
1613
|
-
// Update existing table instead of creating new one
|
|
1614
1597
|
const table = pendingTableData.existingTable
|
|
1615
1598
|
|
|
1616
|
-
//
|
|
1599
|
+
// 1. Reconcile header row (<thead>)
|
|
1600
|
+
let thead = table.querySelector('thead')
|
|
1601
|
+
if (tableForm.value.showHeaders) {
|
|
1602
|
+
if (!thead) {
|
|
1603
|
+
thead = doc.createElement('thead')
|
|
1604
|
+
const headerRow = thead.insertRow()
|
|
1605
|
+
for (let j = 0; j < targetCols; j++) {
|
|
1606
|
+
const th = doc.createElement('th')
|
|
1607
|
+
th.textContent = `Header ${j + 1}`
|
|
1608
|
+
headerRow.appendChild(th)
|
|
1609
|
+
}
|
|
1610
|
+
const colgroupEl = table.querySelector(':scope > colgroup')
|
|
1611
|
+
if (colgroupEl && colgroupEl.nextSibling) {
|
|
1612
|
+
table.insertBefore(thead, colgroupEl.nextSibling)
|
|
1613
|
+
} else {
|
|
1614
|
+
table.insertBefore(thead, table.firstChild)
|
|
1615
|
+
}
|
|
1616
|
+
} else if (thead.rows.length === 0) {
|
|
1617
|
+
const headerRow = thead.insertRow()
|
|
1618
|
+
for (let j = 0; j < targetCols; j++) {
|
|
1619
|
+
const th = doc.createElement('th')
|
|
1620
|
+
th.textContent = `Header ${j + 1}`
|
|
1621
|
+
headerRow.appendChild(th)
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
} else if (thead) {
|
|
1625
|
+
thead.remove()
|
|
1626
|
+
thead = null
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// 2. Reconcile body rows (<tbody>)
|
|
1630
|
+
let tbody = table.querySelector('tbody')
|
|
1631
|
+
if (!tbody) {
|
|
1632
|
+
tbody = doc.createElement('tbody')
|
|
1633
|
+
Array.from(table.querySelectorAll(':scope > tr')).forEach(tr => tbody!.appendChild(tr))
|
|
1634
|
+
table.appendChild(tbody)
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
while (tbody.rows.length < targetRows) {
|
|
1638
|
+
const newRow = tbody.insertRow()
|
|
1639
|
+
for (let j = 0; j < targetCols; j++) {
|
|
1640
|
+
const td = newRow.insertCell()
|
|
1641
|
+
td.innerHTML = '<br>'
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
while (tbody.rows.length > targetRows) {
|
|
1645
|
+
tbody.deleteRow(tbody.rows.length - 1)
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
// 3. Reconcile column count across all rows (thead + tbody)
|
|
1649
|
+
Array.from(table.rows).forEach((row) => {
|
|
1650
|
+
const isHeader = row.parentElement?.tagName === 'THEAD'
|
|
1651
|
+
const currentSpanCols = () => Array.from(row.cells).reduce(
|
|
1652
|
+
(sum, c) => sum + (Math.max(1, Number.parseInt(c.getAttribute('colspan') || '1', 10) || 1)),
|
|
1653
|
+
0
|
|
1654
|
+
)
|
|
1655
|
+
while (currentSpanCols() < targetCols) {
|
|
1656
|
+
const cell = doc.createElement(isHeader ? 'th' : 'td')
|
|
1657
|
+
cell.innerHTML = isHeader ? `Header ${row.cells.length + 1}` : '<br>'
|
|
1658
|
+
row.appendChild(cell)
|
|
1659
|
+
}
|
|
1660
|
+
while (currentSpanCols() > targetCols && row.cells.length > 0) {
|
|
1661
|
+
const lastCell = row.cells[row.cells.length - 1]
|
|
1662
|
+
const span = Math.max(1, Number.parseInt(lastCell.getAttribute('colspan') || '1', 10) || 1)
|
|
1663
|
+
const excess = currentSpanCols() - targetCols
|
|
1664
|
+
if (span > excess) {
|
|
1665
|
+
lastCell.setAttribute('colspan', String(span - excess))
|
|
1666
|
+
} else {
|
|
1667
|
+
lastCell.remove()
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
})
|
|
1671
|
+
|
|
1672
|
+
// 4. Update table-level styles, alignment, direction, and <colgroup>
|
|
1617
1673
|
table.style.width = `${tableForm.value.width}%`
|
|
1618
1674
|
table.style.borderCollapse = 'collapse'
|
|
1619
|
-
table.style.marginBottom = '1rem'
|
|
1620
1675
|
table.style.border = `${tableForm.value.borderWidth}px solid ${tableForm.value.borderColor}`
|
|
1676
|
+
table.style.tableLayout = tableForm.value.fixedLayout ? 'fixed' : 'auto'
|
|
1677
|
+
table.dir = tableForm.value.direction
|
|
1621
1678
|
|
|
1622
|
-
|
|
1623
|
-
if (tableForm.value.fixedLayout) {
|
|
1624
|
-
table.style.tableLayout = 'fixed'
|
|
1625
|
-
} else {
|
|
1626
|
-
table.style.tableLayout = 'auto'
|
|
1627
|
-
}
|
|
1679
|
+
applyTableAlignmentToDOM(table, tableForm.value.alignment as 'left' | 'center' | 'right')
|
|
1628
1680
|
|
|
1629
1681
|
let colgroup = table.querySelector(':scope > colgroup')
|
|
1630
1682
|
if (!colgroup) {
|
|
1631
1683
|
colgroup = doc.createElement('colgroup')
|
|
1632
1684
|
table.insertBefore(colgroup, table.firstChild)
|
|
1633
1685
|
}
|
|
1634
|
-
|
|
1686
|
+
const widths = tableForm.value.columnWidths.length === targetCols
|
|
1687
|
+
? tableForm.value.columnWidths
|
|
1688
|
+
: equalColumnWidths(targetCols)
|
|
1689
|
+
colgroup.replaceChildren(...widths.slice(0, targetCols).map((width) => {
|
|
1635
1690
|
const column = doc.createElement('col')
|
|
1636
1691
|
column.style.width = `${width}%`
|
|
1637
1692
|
return column
|
|
1638
1693
|
}))
|
|
1639
1694
|
|
|
1640
|
-
//
|
|
1641
|
-
// console.log('Setting cell text alignment:', tableForm.value.alignment)
|
|
1642
|
-
// const allCells = table.querySelectorAll('td, th')
|
|
1643
|
-
// allCells.forEach(cell => {
|
|
1644
|
-
// const cellEl = cell as HTMLElement
|
|
1645
|
-
// cellEl.style.textAlign = tableForm.value.alignment
|
|
1646
|
-
// })
|
|
1647
|
-
|
|
1648
|
-
// Set table direction
|
|
1649
|
-
table.dir = tableForm.value.direction
|
|
1650
|
-
|
|
1651
|
-
// Update border and padding for all cells
|
|
1695
|
+
// 5. Update cell styles
|
|
1652
1696
|
const allCells = table.querySelectorAll('td, th')
|
|
1653
1697
|
allCells.forEach((cell) => {
|
|
1654
|
-
const cellEl = cell as
|
|
1698
|
+
const cellEl = cell as HTMLTableCellElement
|
|
1655
1699
|
cellEl.style.padding = `${tableForm.value.cellPadding}px`
|
|
1656
1700
|
cellEl.style.border = `${tableForm.value.borderWidth}px solid ${tableForm.value.borderColor}`
|
|
1657
1701
|
|
|
1658
|
-
// Set fixed width for fixed layout
|
|
1659
1702
|
if (tableForm.value.fixedLayout) {
|
|
1660
|
-
const cellIndex =
|
|
1661
|
-
cellEl.style.width = `${
|
|
1703
|
+
const cellIndex = cellEl.cellIndex
|
|
1704
|
+
cellEl.style.width = `${widths[cellIndex] ?? (100 / targetCols)}%`
|
|
1705
|
+
} else {
|
|
1706
|
+
cellEl.style.removeProperty('width')
|
|
1662
1707
|
}
|
|
1663
1708
|
})
|
|
1664
1709
|
|
|
1665
|
-
// Update header styles if headers exist
|
|
1666
1710
|
const headers = table.querySelectorAll('th')
|
|
1667
1711
|
headers.forEach((th) => {
|
|
1668
1712
|
const thEl = th as HTMLElement
|
|
@@ -1670,8 +1714,6 @@ function submitTable() {
|
|
|
1670
1714
|
thEl.style.color = tableForm.value.headerTextColor
|
|
1671
1715
|
})
|
|
1672
1716
|
|
|
1673
|
-
// Update body cell styles
|
|
1674
|
-
const tbody = table.querySelector('tbody')
|
|
1675
1717
|
if (tbody) {
|
|
1676
1718
|
const rows = Array.from(tbody.querySelectorAll('tr'))
|
|
1677
1719
|
rows.forEach((row, i) => {
|
|
@@ -1679,11 +1721,9 @@ function submitTable() {
|
|
|
1679
1721
|
cells.forEach((cell) => {
|
|
1680
1722
|
const cellEl = cell as HTMLElement
|
|
1681
1723
|
if (tableForm.value.alternateRows && i % 2 === 1) {
|
|
1682
|
-
// Alternate rows: use special colors
|
|
1683
1724
|
cellEl.style.backgroundColor = tableForm.value.alternateRowBgColor
|
|
1684
1725
|
cellEl.style.color = tableForm.value.alternateRowTextColor
|
|
1685
1726
|
} else {
|
|
1686
|
-
// Regular rows: use cell colors
|
|
1687
1727
|
cellEl.style.backgroundColor = tableForm.value.cellBgColor
|
|
1688
1728
|
cellEl.style.color = tableForm.value.cellTextColor
|
|
1689
1729
|
}
|
|
@@ -1693,7 +1733,6 @@ function submitTable() {
|
|
|
1693
1733
|
|
|
1694
1734
|
updateContentWithHistory(doc)
|
|
1695
1735
|
} else {
|
|
1696
|
-
// Create new table (existing logic)
|
|
1697
1736
|
createNewTable(doc)
|
|
1698
1737
|
}
|
|
1699
1738
|
|
|
@@ -1701,40 +1740,17 @@ function submitTable() {
|
|
|
1701
1740
|
pendingTableData = null
|
|
1702
1741
|
}
|
|
1703
1742
|
|
|
1704
|
-
// Function to create new table (moved from submitTable)
|
|
1705
1743
|
function createNewTable(doc: Document) {
|
|
1706
1744
|
if (!pendingTableData) { return }
|
|
1707
1745
|
|
|
1708
|
-
// Create new table with current settings
|
|
1709
1746
|
const table = doc.createElement('table')
|
|
1710
1747
|
table.style.width = `${tableForm.value.width}%`
|
|
1711
1748
|
table.style.borderCollapse = 'collapse'
|
|
1712
1749
|
table.style.marginBottom = '1rem'
|
|
1713
1750
|
table.style.border = `${tableForm.value.borderWidth}px solid ${tableForm.value.borderColor}`
|
|
1751
|
+
table.style.tableLayout = tableForm.value.fixedLayout ? 'fixed' : 'auto'
|
|
1714
1752
|
|
|
1715
|
-
|
|
1716
|
-
if (tableForm.value.fixedLayout) {
|
|
1717
|
-
table.style.tableLayout = 'fixed'
|
|
1718
|
-
} else {
|
|
1719
|
-
table.style.tableLayout = 'auto'
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
// Set table alignment (will be applied directly to table since no wrapper yet)
|
|
1723
|
-
// Don't set textAlign on table - let it inherit from document direction
|
|
1724
|
-
// table.style.textAlign = tableForm.value.alignment
|
|
1725
|
-
|
|
1726
|
-
if (tableForm.value.alignment === 'center') {
|
|
1727
|
-
table.style.marginLeft = 'auto'
|
|
1728
|
-
table.style.marginRight = 'auto'
|
|
1729
|
-
} else if (tableForm.value.alignment === 'right') {
|
|
1730
|
-
table.style.marginLeft = 'auto'
|
|
1731
|
-
table.style.marginRight = '0'
|
|
1732
|
-
} else {
|
|
1733
|
-
table.style.marginLeft = '0'
|
|
1734
|
-
table.style.marginRight = 'auto'
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
// Set table direction
|
|
1753
|
+
applyTableAlignmentToDOM(table, tableForm.value.alignment as 'left' | 'center' | 'right')
|
|
1738
1754
|
table.dir = tableForm.value.direction
|
|
1739
1755
|
|
|
1740
1756
|
const colgroup = doc.createElement('colgroup')
|
|
@@ -1745,54 +1761,39 @@ function createNewTable(doc: Document) {
|
|
|
1745
1761
|
})
|
|
1746
1762
|
table.appendChild(colgroup)
|
|
1747
1763
|
|
|
1748
|
-
// Add header if enabled
|
|
1749
1764
|
if (tableForm.value.showHeaders) {
|
|
1750
1765
|
const thead = doc.createElement('thead')
|
|
1751
1766
|
const headerRow = thead.insertRow()
|
|
1752
1767
|
for (let j = 0; j < tableForm.value.cols; j++) {
|
|
1753
1768
|
const th = doc.createElement('th')
|
|
1754
|
-
th.
|
|
1769
|
+
th.textContent = `Header ${j + 1}`
|
|
1755
1770
|
th.style.padding = `${tableForm.value.cellPadding}px`
|
|
1756
1771
|
th.style.border = `${tableForm.value.borderWidth}px solid ${tableForm.value.borderColor}`
|
|
1757
1772
|
th.style.backgroundColor = tableForm.value.headerBgColor
|
|
1758
1773
|
th.style.color = tableForm.value.headerTextColor
|
|
1759
|
-
// Don't set textAlign - let it inherit from table direction
|
|
1760
|
-
// th.style.textAlign = tableForm.value.alignment
|
|
1761
|
-
|
|
1762
|
-
// Set fixed width for fixed layout
|
|
1763
1774
|
if (tableForm.value.fixedLayout) {
|
|
1764
1775
|
th.style.width = `${tableForm.value.columnWidths[j] ?? (100 / tableForm.value.cols)}%`
|
|
1765
1776
|
}
|
|
1766
|
-
|
|
1767
1777
|
headerRow.appendChild(th)
|
|
1768
1778
|
}
|
|
1769
1779
|
table.appendChild(thead)
|
|
1770
1780
|
}
|
|
1771
1781
|
|
|
1772
|
-
// Add body
|
|
1773
1782
|
const tbody = doc.createElement('tbody')
|
|
1774
1783
|
for (let i = 0; i < tableForm.value.rows; i++) {
|
|
1775
1784
|
const row = tbody.insertRow()
|
|
1776
1785
|
for (let j = 0; j < tableForm.value.cols; j++) {
|
|
1777
1786
|
const cell = row.insertCell()
|
|
1778
|
-
cell.innerHTML = '
|
|
1787
|
+
cell.innerHTML = '<br>'
|
|
1779
1788
|
cell.style.padding = `${tableForm.value.cellPadding}px`
|
|
1780
1789
|
cell.style.border = `${tableForm.value.borderWidth}px solid ${tableForm.value.borderColor}`
|
|
1781
|
-
// Don't set textAlign - let it inherit from table direction
|
|
1782
|
-
// cell.style.textAlign = tableForm.value.alignment
|
|
1783
|
-
|
|
1784
|
-
// Set fixed width for fixed layout
|
|
1785
1790
|
if (tableForm.value.fixedLayout) {
|
|
1786
1791
|
cell.style.width = `${tableForm.value.columnWidths[j] ?? (100 / tableForm.value.cols)}%`
|
|
1787
1792
|
}
|
|
1788
|
-
|
|
1789
|
-
// Apply background and text colors
|
|
1790
1793
|
if (tableForm.value.alternateRows && i % 2 === 1) {
|
|
1791
|
-
// Alternate rows: use special colors
|
|
1792
1794
|
cell.style.backgroundColor = tableForm.value.alternateRowBgColor
|
|
1793
1795
|
cell.style.color = tableForm.value.alternateRowTextColor
|
|
1794
1796
|
} else {
|
|
1795
|
-
// Regular rows: use cell colors
|
|
1796
1797
|
cell.style.backgroundColor = tableForm.value.cellBgColor
|
|
1797
1798
|
cell.style.color = tableForm.value.cellTextColor
|
|
1798
1799
|
}
|
|
@@ -1800,9 +1801,14 @@ function createNewTable(doc: Document) {
|
|
|
1800
1801
|
}
|
|
1801
1802
|
table.appendChild(tbody)
|
|
1802
1803
|
|
|
1803
|
-
// Insert new table
|
|
1804
1804
|
const { range } = pendingTableData
|
|
1805
1805
|
range.insertNode(table)
|
|
1806
|
+
if (!table.nextElementSibling) {
|
|
1807
|
+
const trailingP = doc.createElement('p')
|
|
1808
|
+
trailingP.dir = doc.body.dir || 'ltr'
|
|
1809
|
+
trailingP.innerHTML = '<br>'
|
|
1810
|
+
table.insertAdjacentElement('afterend', trailingP)
|
|
1811
|
+
}
|
|
1806
1812
|
range.setStartAfter(table)
|
|
1807
1813
|
range.collapse(true)
|
|
1808
1814
|
const selection = doc.getSelection()
|
|
@@ -1811,72 +1817,33 @@ function createNewTable(doc: Document) {
|
|
|
1811
1817
|
selection.addRange(range)
|
|
1812
1818
|
}
|
|
1813
1819
|
|
|
1814
|
-
updateContentWithHistory(doc)
|
|
1815
1820
|
setupTableEditButtons(doc)
|
|
1821
|
+
updateContentWithHistory(doc)
|
|
1816
1822
|
}
|
|
1817
1823
|
|
|
1818
|
-
// Function to delete table
|
|
1819
1824
|
function deleteTable() {
|
|
1820
1825
|
if (!pendingTableData?.existingTable) { return }
|
|
1821
1826
|
|
|
1822
|
-
|
|
1823
|
-
existingTable.remove()
|
|
1824
|
-
|
|
1825
|
-
updateContentWithHistory(editor.state.doc!)
|
|
1827
|
+
removeTableElement(pendingTableData.existingTable)
|
|
1826
1828
|
showTableEditor.value = false
|
|
1827
1829
|
pendingTableData = null
|
|
1828
1830
|
}
|
|
1829
1831
|
|
|
1830
|
-
// Function to apply default table settings to externally created tables
|
|
1831
1832
|
function applyDefaultTableSettings(table: HTMLTableElement) {
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
fixedLayout: true,
|
|
1835
|
-
alignment: 'start',
|
|
1836
|
-
direction: 'ltr'
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
// Set table layout
|
|
1840
|
-
if (defaultSettings.fixedLayout) {
|
|
1833
|
+
const doc = table.ownerDocument
|
|
1834
|
+
if (!table.style.tableLayout) {
|
|
1841
1835
|
table.style.tableLayout = 'fixed'
|
|
1842
|
-
|
|
1843
|
-
// Set equal width for all columns if not already set
|
|
1844
|
-
const headers = table.querySelectorAll('th')
|
|
1845
|
-
const cells = table.querySelectorAll('td')
|
|
1846
|
-
|
|
1847
|
-
if (headers.length > 0) {
|
|
1848
|
-
const colWidth = `${100 / headers.length}%`
|
|
1849
|
-
headers.forEach((th) => {
|
|
1850
|
-
if (!(th as HTMLElement).style.width) {
|
|
1851
|
-
(th as HTMLElement).style.width = colWidth
|
|
1852
|
-
}
|
|
1853
|
-
})
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
if (cells.length > 0 && headers.length === 0) {
|
|
1857
|
-
// If no headers, use first row to determine column count
|
|
1858
|
-
const firstRow = table.querySelector('tr')
|
|
1859
|
-
if (firstRow) {
|
|
1860
|
-
const colCount = firstRow.querySelectorAll('td').length
|
|
1861
|
-
const colWidth = `${100 / colCount}%`
|
|
1862
|
-
cells.forEach((td) => {
|
|
1863
|
-
if (!(td as HTMLElement).style.width) {
|
|
1864
|
-
(td as HTMLElement).style.width = colWidth
|
|
1865
|
-
}
|
|
1866
|
-
})
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
1836
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
table
|
|
1837
|
+
if (!table.style.width) {
|
|
1838
|
+
table.style.width = '100%'
|
|
1839
|
+
}
|
|
1840
|
+
if (!table.style.borderCollapse) {
|
|
1841
|
+
table.style.borderCollapse = 'collapse'
|
|
1842
|
+
}
|
|
1843
|
+
if (!table.dir) {
|
|
1844
|
+
table.dir = doc?.body?.dir || 'ltr'
|
|
1845
|
+
}
|
|
1846
|
+
syncTableColgroup(table)
|
|
1880
1847
|
}
|
|
1881
1848
|
|
|
1882
1849
|
// Function to delete image
|
|
@@ -1911,9 +1878,11 @@ function getCleanHTML(doc: Document | null): string {
|
|
|
1911
1878
|
const clone = doc.body.cloneNode(true) as HTMLElement
|
|
1912
1879
|
|
|
1913
1880
|
// Strip editor-UI elements that must never appear in output
|
|
1914
|
-
clone.querySelectorAll('.
|
|
1881
|
+
clone.querySelectorAll('.placeholder').forEach((el) => { el.remove() })
|
|
1882
|
+
clone.querySelectorAll('.table-edit-btn, .table-inline-controls').forEach((btn) => { btn.remove() })
|
|
1915
1883
|
clone.querySelectorAll('.media-selected').forEach((el) => { el.classList.remove('media-selected') })
|
|
1916
1884
|
clone.querySelectorAll('.table-cell-selected').forEach((el) => { el.classList.remove('table-cell-selected') })
|
|
1885
|
+
clone.querySelectorAll('[data-dir-explicit]').forEach((el) => { el.removeAttribute('data-dir-explicit') })
|
|
1917
1886
|
|
|
1918
1887
|
// Unwrap .table-wrapper divs: restore original table margin-based alignment
|
|
1919
1888
|
clone.querySelectorAll('.table-wrapper').forEach((wrapper) => {
|
|
@@ -2032,7 +2001,12 @@ function getCleanHTML(doc: Document | null): string {
|
|
|
2032
2001
|
if (fig.classList.length === 0) { fig.removeAttribute('class') }
|
|
2033
2002
|
})
|
|
2034
2003
|
|
|
2035
|
-
|
|
2004
|
+
const html = clone.innerHTML
|
|
2005
|
+
if (!clone.textContent?.trim() && !clone.querySelector('img, video, iframe, table, hr, figure')) {
|
|
2006
|
+
return ''
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
return html
|
|
2036
2010
|
}
|
|
2037
2011
|
|
|
2038
2012
|
// Expose openLinkModal to editor state
|
|
@@ -2045,336 +2019,135 @@ function getCleanHTML(doc: Document | null): string {
|
|
|
2045
2019
|
; (editor.state as any).openEmbedModal = openEmbedModal
|
|
2046
2020
|
; (editor.state as any).openVideoModal = openVideoModal
|
|
2047
2021
|
|
|
2048
|
-
// Table manipulation functions
|
|
2022
|
+
// Table manipulation functions (delegating to unified 2D grid table utilities)
|
|
2049
2023
|
function mergeCellRight() {
|
|
2050
2024
|
const { doc } = editor.state
|
|
2051
2025
|
if (!doc || !contextMenuCell.value) { return }
|
|
2052
|
-
|
|
2053
|
-
const cell = contextMenuCell.value
|
|
2054
|
-
const row = cell.parentElement as HTMLTableRowElement
|
|
2055
|
-
const cellIndex = Array.from(row.cells).indexOf(cell)
|
|
2056
|
-
const table = cell.closest('table') as HTMLTableElement
|
|
2057
|
-
const isRTL = table?.dir === 'rtl'
|
|
2058
|
-
|
|
2059
|
-
// In RTL, "right" means the previous cell, in LTR it means the next cell
|
|
2060
|
-
const targetIndex = isRTL ? cellIndex - 1 : cellIndex + 1
|
|
2061
|
-
const targetCell = row.cells[targetIndex]
|
|
2062
|
-
|
|
2063
|
-
if (targetCell && targetIndex >= 0 && targetIndex < row.cells.length) {
|
|
2064
|
-
// Combine content
|
|
2065
|
-
if (targetCell.innerHTML.trim()) {
|
|
2066
|
-
cell.innerHTML += ` ${targetCell.innerHTML}`
|
|
2067
|
-
}
|
|
2068
|
-
|
|
2069
|
-
// Update colspan
|
|
2070
|
-
const currentColspan = Number.parseInt(cell.getAttribute('colspan') || '1')
|
|
2071
|
-
const targetColspan = Number.parseInt(targetCell.getAttribute('colspan') || '1')
|
|
2072
|
-
cell.setAttribute('colspan', (currentColspan + targetColspan).toString())
|
|
2073
|
-
|
|
2074
|
-
// Remove the target cell
|
|
2075
|
-
targetCell.remove()
|
|
2076
|
-
|
|
2077
|
-
updateContentWithHistory(doc)
|
|
2078
|
-
}
|
|
2026
|
+
tableMergeCells(editor.state.range, doc, contextMenuCell.value, 'right')
|
|
2079
2027
|
}
|
|
2080
2028
|
|
|
2081
2029
|
function mergeCellDown() {
|
|
2082
2030
|
const { doc } = editor.state
|
|
2083
2031
|
if (!doc || !contextMenuCell.value) { return }
|
|
2084
|
-
|
|
2085
|
-
const cell = contextMenuCell.value
|
|
2086
|
-
const table = cell.closest('table') as HTMLTableElement
|
|
2087
|
-
const row = cell.parentElement as HTMLTableRowElement
|
|
2088
|
-
const cellIndex = Array.from(row.cells).indexOf(cell)
|
|
2089
|
-
|
|
2090
|
-
// Find the row below
|
|
2091
|
-
const tbody = table.querySelector('tbody') || table
|
|
2092
|
-
const rows = Array.from(tbody.querySelectorAll('tr'))
|
|
2093
|
-
const currentRowIndex = rows.indexOf(row)
|
|
2094
|
-
const nextRow = rows[currentRowIndex + 1]
|
|
2095
|
-
|
|
2096
|
-
if (nextRow) {
|
|
2097
|
-
const targetCell = nextRow.cells[cellIndex]
|
|
2098
|
-
if (targetCell) {
|
|
2099
|
-
// Combine content
|
|
2100
|
-
if (targetCell.innerHTML.trim()) {
|
|
2101
|
-
cell.innerHTML += `<br>${targetCell.innerHTML}`
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
// Update rowspan
|
|
2105
|
-
const currentRowspan = Number.parseInt(cell.getAttribute('rowspan') || '1')
|
|
2106
|
-
const targetRowspan = Number.parseInt(targetCell.getAttribute('rowspan') || '1')
|
|
2107
|
-
cell.setAttribute('rowspan', (currentRowspan + targetRowspan).toString())
|
|
2108
|
-
|
|
2109
|
-
// Remove the target cell
|
|
2110
|
-
targetCell.remove()
|
|
2111
|
-
|
|
2112
|
-
updateContentWithHistory(doc)
|
|
2113
|
-
}
|
|
2114
|
-
}
|
|
2032
|
+
tableMergeCells(editor.state.range, doc, contextMenuCell.value, 'down')
|
|
2115
2033
|
}
|
|
2116
2034
|
|
|
2117
2035
|
function splitCell() {
|
|
2118
2036
|
const { doc } = editor.state
|
|
2119
2037
|
if (!doc || !contextMenuCell.value) { return }
|
|
2120
|
-
|
|
2121
|
-
const cell = contextMenuCell.value
|
|
2122
|
-
const table = cell.closest('table') as HTMLTableElement
|
|
2123
|
-
const isRTL = table?.dir === 'rtl'
|
|
2124
|
-
const colspan = Number.parseInt(cell.getAttribute('colspan') || '1')
|
|
2125
|
-
const rowspan = Number.parseInt(cell.getAttribute('rowspan') || '1')
|
|
2126
|
-
|
|
2127
|
-
if (colspan > 1) {
|
|
2128
|
-
// Split horizontally
|
|
2129
|
-
cell.setAttribute('colspan', '1')
|
|
2130
|
-
const row = cell.parentElement as HTMLTableRowElement
|
|
2131
|
-
|
|
2132
|
-
// Add new cells to the right (or left in RTL)
|
|
2133
|
-
for (let i = 1; i < colspan; i++) {
|
|
2134
|
-
const newCell = doc.createElement(cell.tagName.toLowerCase() as 'td' | 'th')
|
|
2135
|
-
newCell.innerHTML = ' '
|
|
2136
|
-
newCell.style.cssText = cell.style.cssText
|
|
2137
|
-
|
|
2138
|
-
if (isRTL) {
|
|
2139
|
-
row.insertBefore(newCell, cell)
|
|
2140
|
-
} else {
|
|
2141
|
-
if (cell.nextSibling) {
|
|
2142
|
-
row.insertBefore(newCell, cell.nextSibling)
|
|
2143
|
-
} else {
|
|
2144
|
-
row.appendChild(newCell)
|
|
2145
|
-
}
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
} else if (rowspan > 1) {
|
|
2149
|
-
// Split vertically
|
|
2150
|
-
cell.setAttribute('rowspan', '1')
|
|
2151
|
-
const row = cell.parentElement as HTMLTableRowElement
|
|
2152
|
-
const cellIndex = Array.from(row.cells).indexOf(cell)
|
|
2153
|
-
const tbody = table.querySelector('tbody') || table
|
|
2154
|
-
const rows = Array.from(tbody.querySelectorAll('tr'))
|
|
2155
|
-
const currentRowIndex = rows.indexOf(row)
|
|
2156
|
-
|
|
2157
|
-
// Add new cells to rows below
|
|
2158
|
-
for (let i = 1; i < rowspan; i++) {
|
|
2159
|
-
const targetRow = rows[currentRowIndex + i]
|
|
2160
|
-
if (targetRow) {
|
|
2161
|
-
const newCell = doc.createElement(cell.tagName.toLowerCase() as 'td' | 'th')
|
|
2162
|
-
newCell.innerHTML = ' '
|
|
2163
|
-
newCell.style.cssText = cell.style.cssText
|
|
2164
|
-
|
|
2165
|
-
// Insert at the correct position
|
|
2166
|
-
if (cellIndex < targetRow.cells.length) {
|
|
2167
|
-
targetRow.insertBefore(newCell, targetRow.cells[cellIndex])
|
|
2168
|
-
} else {
|
|
2169
|
-
targetRow.appendChild(newCell)
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
updateContentWithHistory(doc)
|
|
2038
|
+
tableSplitCell(editor.state.range, doc, contextMenuCell.value)
|
|
2176
2039
|
}
|
|
2177
2040
|
|
|
2178
2041
|
function insertRowAbove() {
|
|
2179
2042
|
const { doc } = editor.state
|
|
2180
2043
|
if (!doc || !contextMenuCell.value) { return }
|
|
2181
|
-
|
|
2182
|
-
const cell = contextMenuCell.value
|
|
2183
|
-
const row = cell.parentElement as HTMLTableRowElement
|
|
2184
|
-
|
|
2185
|
-
// Create new row with same number of columns
|
|
2186
|
-
const newRow = doc.createElement('tr')
|
|
2187
|
-
const columnCount = Array.from(row.cells).reduce((total, cell) => {
|
|
2188
|
-
return total + Number.parseInt(cell.getAttribute('colspan') || '1')
|
|
2189
|
-
}, 0)
|
|
2190
|
-
|
|
2191
|
-
for (let i = 0; i < columnCount; i++) {
|
|
2192
|
-
const newCell = doc.createElement('td')
|
|
2193
|
-
newCell.innerHTML = ' '
|
|
2194
|
-
newCell.style.padding = '8px'
|
|
2195
|
-
newCell.style.border = '1px solid #ddd'
|
|
2196
|
-
newRow.appendChild(newCell)
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
// Insert before current row
|
|
2200
|
-
row.insertAdjacentElement('beforebegin', newRow)
|
|
2201
|
-
|
|
2202
|
-
updateContentWithHistory(doc)
|
|
2044
|
+
tableAddRow('before', editor.state.range, doc, contextMenuCell.value)
|
|
2203
2045
|
}
|
|
2204
2046
|
|
|
2205
2047
|
function insertRowBelow() {
|
|
2206
2048
|
const { doc } = editor.state
|
|
2207
2049
|
if (!doc || !contextMenuCell.value) { return }
|
|
2208
|
-
|
|
2209
|
-
const cell = contextMenuCell.value
|
|
2210
|
-
const row = cell.parentElement as HTMLTableRowElement
|
|
2211
|
-
|
|
2212
|
-
// Create new row with same number of columns
|
|
2213
|
-
const newRow = doc.createElement('tr')
|
|
2214
|
-
const columnCount = Array.from(row.cells).reduce((total, cell) => {
|
|
2215
|
-
return total + Number.parseInt(cell.getAttribute('colspan') || '1')
|
|
2216
|
-
}, 0)
|
|
2217
|
-
|
|
2218
|
-
for (let i = 0; i < columnCount; i++) {
|
|
2219
|
-
const newCell = doc.createElement('td')
|
|
2220
|
-
newCell.innerHTML = ' '
|
|
2221
|
-
newCell.style.padding = '8px'
|
|
2222
|
-
newCell.style.border = '1px solid #ddd'
|
|
2223
|
-
newRow.appendChild(newCell)
|
|
2224
|
-
}
|
|
2225
|
-
|
|
2226
|
-
// Insert after current row
|
|
2227
|
-
row.insertAdjacentElement('afterend', newRow)
|
|
2228
|
-
|
|
2229
|
-
updateContentWithHistory(doc)
|
|
2050
|
+
tableAddRow('after', editor.state.range, doc, contextMenuCell.value)
|
|
2230
2051
|
}
|
|
2231
2052
|
|
|
2232
2053
|
function deleteRow() {
|
|
2233
2054
|
const { doc } = editor.state
|
|
2234
2055
|
if (!doc || !contextMenuCell.value) { return }
|
|
2235
|
-
|
|
2236
|
-
const cell = contextMenuCell.value
|
|
2237
|
-
const row = cell.parentElement as HTMLTableRowElement
|
|
2238
|
-
const table = cell.closest('table') as HTMLTableElement
|
|
2239
|
-
|
|
2240
|
-
// Don't delete if it's the only row
|
|
2241
|
-
const tbody = table.querySelector('tbody') || table
|
|
2242
|
-
const rows = tbody.querySelectorAll('tr')
|
|
2243
|
-
|
|
2244
|
-
if (rows.length <= 1) { return }
|
|
2245
|
-
|
|
2246
|
-
row.remove()
|
|
2247
|
-
updateContentWithHistory(doc)
|
|
2056
|
+
tableDeleteRow(editor.state.range, contextMenuCell.value)
|
|
2248
2057
|
}
|
|
2249
2058
|
|
|
2250
2059
|
function insertColumnLeft() {
|
|
2251
2060
|
const { doc } = editor.state
|
|
2252
2061
|
if (!doc || !contextMenuCell.value) { return }
|
|
2253
|
-
|
|
2254
|
-
const cell = contextMenuCell.value
|
|
2255
|
-
const table = cell.closest('table') as HTMLTableElement
|
|
2256
|
-
const cellIndex = Array.from((cell.parentElement as HTMLTableRowElement).cells).indexOf(cell)
|
|
2257
|
-
|
|
2258
|
-
// Add new cell to each row at the same index
|
|
2259
|
-
const rows = table.querySelectorAll('tr')
|
|
2260
|
-
rows.forEach((row) => {
|
|
2261
|
-
const newCell = doc.createElement(row.parentElement?.tagName === 'THEAD' ? 'th' : 'td')
|
|
2262
|
-
newCell.innerHTML = ' '
|
|
2263
|
-
newCell.style.padding = '8px'
|
|
2264
|
-
newCell.style.border = '1px solid #ddd'
|
|
2265
|
-
|
|
2266
|
-
if (cellIndex < row.cells.length) {
|
|
2267
|
-
row.insertBefore(newCell, row.cells[cellIndex])
|
|
2268
|
-
} else {
|
|
2269
|
-
row.appendChild(newCell)
|
|
2270
|
-
}
|
|
2271
|
-
})
|
|
2272
|
-
|
|
2273
|
-
updateContentWithHistory(doc)
|
|
2062
|
+
tableInsertColumn('before', editor.state.range, contextMenuCell.value)
|
|
2274
2063
|
}
|
|
2275
2064
|
|
|
2276
2065
|
function insertColumnRight() {
|
|
2277
2066
|
const { doc } = editor.state
|
|
2278
2067
|
if (!doc || !contextMenuCell.value) { return }
|
|
2279
|
-
|
|
2280
|
-
const cell = contextMenuCell.value
|
|
2281
|
-
const table = cell.closest('table') as HTMLTableElement
|
|
2282
|
-
const cellIndex = Array.from((cell.parentElement as HTMLTableRowElement).cells).indexOf(cell)
|
|
2283
|
-
|
|
2284
|
-
// Add new cell to each row after the current index
|
|
2285
|
-
const rows = table.querySelectorAll('tr')
|
|
2286
|
-
rows.forEach((row) => {
|
|
2287
|
-
const newCell = doc.createElement(row.parentElement?.tagName === 'THEAD' ? 'th' : 'td')
|
|
2288
|
-
newCell.innerHTML = ' '
|
|
2289
|
-
newCell.style.padding = '8px'
|
|
2290
|
-
newCell.style.border = '1px solid #ddd'
|
|
2291
|
-
|
|
2292
|
-
const targetIndex = cellIndex + 1
|
|
2293
|
-
if (targetIndex < row.cells.length) {
|
|
2294
|
-
row.insertBefore(newCell, row.cells[targetIndex])
|
|
2295
|
-
} else {
|
|
2296
|
-
row.appendChild(newCell)
|
|
2297
|
-
}
|
|
2298
|
-
})
|
|
2299
|
-
|
|
2300
|
-
updateContentWithHistory(doc)
|
|
2068
|
+
tableInsertColumn('after', editor.state.range, contextMenuCell.value)
|
|
2301
2069
|
}
|
|
2302
2070
|
|
|
2303
2071
|
function deleteColumn() {
|
|
2304
2072
|
const { doc } = editor.state
|
|
2305
2073
|
if (!doc || !contextMenuCell.value) { return }
|
|
2306
|
-
|
|
2307
|
-
const cell = contextMenuCell.value
|
|
2308
|
-
const table = cell.closest('table') as HTMLTableElement
|
|
2309
|
-
const cellIndex = Array.from((cell.parentElement as HTMLTableRowElement).cells).indexOf(cell)
|
|
2310
|
-
|
|
2311
|
-
// Check if this is the only column
|
|
2312
|
-
const firstRow = table.querySelector('tr')
|
|
2313
|
-
if (firstRow && firstRow.cells.length <= 1) { return }
|
|
2314
|
-
|
|
2315
|
-
// Remove cell from each row at the same index
|
|
2316
|
-
const rows = table.querySelectorAll('tr')
|
|
2317
|
-
rows.forEach((row) => {
|
|
2318
|
-
if (row.cells[cellIndex]) {
|
|
2319
|
-
row.cells[cellIndex].remove()
|
|
2320
|
-
}
|
|
2321
|
-
})
|
|
2322
|
-
|
|
2323
|
-
updateContentWithHistory(doc)
|
|
2074
|
+
tableDeleteColumn(editor.state.range, contextMenuCell.value)
|
|
2324
2075
|
}
|
|
2325
2076
|
|
|
2326
|
-
// Table context menu functions
|
|
2327
2077
|
const canMergeRight = computed(() => {
|
|
2328
2078
|
if (!contextMenuCell.value) { return false }
|
|
2329
2079
|
const cell = contextMenuCell.value
|
|
2330
|
-
const
|
|
2331
|
-
|
|
2332
|
-
const
|
|
2333
|
-
const
|
|
2334
|
-
|
|
2335
|
-
|
|
2080
|
+
const table = cell.closest('table') as HTMLTableElement | null
|
|
2081
|
+
if (!table) { return false }
|
|
2082
|
+
const map = buildTableGrid(table)
|
|
2083
|
+
const coords = findCellCoords(map, cell)
|
|
2084
|
+
if (!coords) { return false }
|
|
2085
|
+
const isRTL = table.dir === 'rtl'
|
|
2086
|
+
const targetCol = isRTL ? coords.col - 1 : coords.col + coords.colspan
|
|
2087
|
+
const neighbor = map.grid[coords.row]?.[targetCol]
|
|
2088
|
+
return Boolean(neighbor && neighbor.cell !== cell && neighbor.cell.parentElement === cell.parentElement)
|
|
2336
2089
|
})
|
|
2337
2090
|
|
|
2338
2091
|
const canMergeDown = computed(() => {
|
|
2339
2092
|
if (!contextMenuCell.value) { return false }
|
|
2340
2093
|
const cell = contextMenuCell.value
|
|
2341
|
-
const table = cell.closest('table') as HTMLTableElement
|
|
2342
|
-
|
|
2343
|
-
const
|
|
2344
|
-
const
|
|
2345
|
-
|
|
2094
|
+
const table = cell.closest('table') as HTMLTableElement | null
|
|
2095
|
+
if (!table) { return false }
|
|
2096
|
+
const map = buildTableGrid(table)
|
|
2097
|
+
const coords = findCellCoords(map, cell)
|
|
2098
|
+
if (!coords) { return false }
|
|
2099
|
+
const targetRow = coords.row + coords.rowspan
|
|
2100
|
+
const neighbor = map.grid[targetRow]?.[coords.col]
|
|
2101
|
+
return Boolean(
|
|
2102
|
+
neighbor
|
|
2103
|
+
&& neighbor.cell !== cell
|
|
2104
|
+
&& neighbor.cell.parentElement?.parentElement === cell.parentElement?.parentElement
|
|
2105
|
+
)
|
|
2346
2106
|
})
|
|
2347
2107
|
|
|
2348
2108
|
const canSplit = computed(() => {
|
|
2349
2109
|
if (!contextMenuCell.value) { return false }
|
|
2350
2110
|
const cell = contextMenuCell.value
|
|
2351
|
-
const colspan = Number.parseInt(cell.getAttribute('colspan') || '1')
|
|
2352
|
-
const rowspan = Number.parseInt(cell.getAttribute('rowspan') || '1')
|
|
2111
|
+
const colspan = Number.parseInt(cell.getAttribute('colspan') || '1', 10)
|
|
2112
|
+
const rowspan = Number.parseInt(cell.getAttribute('rowspan') || '1', 10)
|
|
2353
2113
|
return colspan > 1 || rowspan > 1
|
|
2354
2114
|
})
|
|
2355
2115
|
|
|
2356
|
-
|
|
2357
|
-
const
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2116
|
+
const tableContextMenuItems = computed<DropdownItem[]>(() => {
|
|
2117
|
+
const items: DropdownItem[] = [
|
|
2118
|
+
{
|
|
2119
|
+
label: 'Advanced Settings',
|
|
2120
|
+
icon: 'settings',
|
|
2121
|
+
action: openExistingTableAdvancedSettings,
|
|
2122
|
+
},
|
|
2123
|
+
{ divider: true },
|
|
2124
|
+
]
|
|
2125
|
+
|
|
2126
|
+
if (canMergeRight.value) {
|
|
2127
|
+
items.push({ label: 'Merge Right', icon: 'start', action: mergeCellRight })
|
|
2128
|
+
}
|
|
2129
|
+
if (canMergeDown.value) {
|
|
2130
|
+
items.push({ label: 'Merge Down', icon: 'text_select_move_down', action: mergeCellDown })
|
|
2131
|
+
}
|
|
2132
|
+
if (canSplit.value) {
|
|
2133
|
+
items.push({ label: 'Split Cell', icon: 'call_split', action: splitCell })
|
|
2134
|
+
}
|
|
2135
|
+
if (canMergeRight.value || canMergeDown.value || canSplit.value) {
|
|
2136
|
+
items.push({ divider: true })
|
|
2365
2137
|
}
|
|
2366
|
-
}
|
|
2367
2138
|
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2139
|
+
items.push(
|
|
2140
|
+
{ label: 'Insert Row Above', icon: 'add_row_above', action: insertRowAbove },
|
|
2141
|
+
{ label: 'Insert Row Below', icon: 'add_row_below', action: insertRowBelow },
|
|
2142
|
+
{ label: 'Delete Row', icon: 'remove', danger: true, action: deleteRow },
|
|
2143
|
+
{ divider: true },
|
|
2144
|
+
{ label: 'Insert Column Left', icon: 'add_column_left', action: insertColumnLeft },
|
|
2145
|
+
{ label: 'Insert Column Right', icon: 'add_column_right', action: insertColumnRight },
|
|
2146
|
+
{ label: 'Delete Column', icon: 'delete', danger: true, action: deleteColumn },
|
|
2147
|
+
)
|
|
2148
|
+
|
|
2149
|
+
return items
|
|
2150
|
+
})
|
|
2378
2151
|
|
|
2379
2152
|
// Expose debug methods if debug mode is enabled
|
|
2380
2153
|
const debugMethods = computed(() => editor.state.debug)
|
|
@@ -2398,87 +2171,473 @@ const editorHeight = computed(() => {
|
|
|
2398
2171
|
return '240px' // default height
|
|
2399
2172
|
})
|
|
2400
2173
|
|
|
2174
|
+
let bodyMutationObserver: MutationObserver | null = null
|
|
2175
|
+
let activeResizeCleanup: (() => void) | null = null
|
|
2176
|
+
const tablePointerSession = usePointerSession()
|
|
2177
|
+
|
|
2401
2178
|
// Cleanup on component unmount
|
|
2402
2179
|
onUnmounted(() => {
|
|
2180
|
+
bodyMutationObserver?.disconnect()
|
|
2181
|
+
bodyMutationObserver = null
|
|
2182
|
+
tablePointerSession.cancel()
|
|
2183
|
+
activeResizeCleanup?.()
|
|
2184
|
+
activeResizeCleanup = null
|
|
2403
2185
|
editor.cleanup()
|
|
2404
2186
|
})
|
|
2405
2187
|
|
|
2406
|
-
function
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2188
|
+
function setupTableColumnResizing(doc: Document) {
|
|
2189
|
+
if (activeResizeCleanup) {
|
|
2190
|
+
activeResizeCleanup()
|
|
2191
|
+
activeResizeCleanup = null
|
|
2410
2192
|
}
|
|
2411
2193
|
|
|
2412
|
-
const
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2194
|
+
const EDGE_THRESHOLD = 5
|
|
2195
|
+
const ADD_TRIGGER_ZONE = 28
|
|
2196
|
+
const DRAG_THRESHOLD = 5
|
|
2197
|
+
let isSessionActive = false
|
|
2416
2198
|
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2199
|
+
let activeHoverState: {
|
|
2200
|
+
table: HTMLTableElement
|
|
2201
|
+
cell: HTMLTableCellElement
|
|
2202
|
+
insertPosition: 'before' | 'after'
|
|
2203
|
+
} | null = null
|
|
2204
|
+
|
|
2205
|
+
const hideAllInlineControls = () => {
|
|
2206
|
+
doc.querySelectorAll<HTMLElement>('.table-inline-controls').forEach((ctrl) => {
|
|
2207
|
+
ctrl.classList.remove('is-visible', 'show-col-add', 'show-col-thumb', 'is-dragging-col')
|
|
2421
2208
|
})
|
|
2422
|
-
|
|
2209
|
+
}
|
|
2423
2210
|
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
} catch (error) {
|
|
2431
|
-
console.error('[RichText table editor] delegated open failed', error)
|
|
2432
|
-
}
|
|
2433
|
-
}, 0)
|
|
2211
|
+
const highlightTableColumn = (table: HTMLTableElement, colIndex: number) => {
|
|
2212
|
+
const map = buildTableGrid(table)
|
|
2213
|
+
doc.querySelectorAll('.table-cell-selected').forEach(c => c.classList.remove('table-cell-selected'))
|
|
2214
|
+
for (let r = 0; r < map.rowCount; r++) {
|
|
2215
|
+
map.grid[r]?.[colIndex]?.cell.classList.add('table-cell-selected')
|
|
2216
|
+
}
|
|
2434
2217
|
}
|
|
2435
2218
|
|
|
2436
|
-
|
|
2437
|
-
|
|
2219
|
+
const applyColumnWidthsToDOM = (table: HTMLTableElement, widths: number[]) => {
|
|
2220
|
+
let colgroup = table.querySelector(':scope > colgroup')
|
|
2221
|
+
if (!colgroup) {
|
|
2222
|
+
colgroup = doc.createElement('colgroup')
|
|
2223
|
+
table.insertBefore(colgroup, table.firstChild)
|
|
2224
|
+
}
|
|
2225
|
+
colgroup.replaceChildren(...widths.map((w) => {
|
|
2226
|
+
const col = doc.createElement('col')
|
|
2227
|
+
col.style.width = `${w}%`
|
|
2228
|
+
return col
|
|
2229
|
+
}))
|
|
2438
2230
|
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2231
|
+
table.style.tableLayout = 'fixed'
|
|
2232
|
+
Array.from(table.rows).forEach((row) => {
|
|
2233
|
+
Array.from(row.cells).forEach((c, idx) => {
|
|
2234
|
+
if (widths[idx] !== undefined && (c.colSpan || 1) === 1) {
|
|
2235
|
+
c.style.width = `${widths[idx]}%`
|
|
2236
|
+
}
|
|
2237
|
+
})
|
|
2238
|
+
})
|
|
2239
|
+
}
|
|
2442
2240
|
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2241
|
+
const getHoveredResizeCell = (e: PointerEvent): { cell: HTMLTableCellElement, table: HTMLTableElement } | null => {
|
|
2242
|
+
const target = e.target as HTMLElement | null
|
|
2243
|
+
if (target?.closest('.table-inline-controls, .table-edit-btn')) { return null }
|
|
2244
|
+
const cell = target?.closest('td, th') as HTMLTableCellElement | null
|
|
2245
|
+
if (!cell) { return null }
|
|
2246
|
+
const table = cell.closest('table') as HTMLTableElement | null
|
|
2247
|
+
if (!table) { return null }
|
|
2248
|
+
const rect = cell.getBoundingClientRect()
|
|
2249
|
+
const isRTL = table.dir === 'rtl'
|
|
2250
|
+
const nearTrailingEdge = isRTL
|
|
2251
|
+
? (e.clientX - rect.left <= EDGE_THRESHOLD)
|
|
2252
|
+
: (rect.right - e.clientX <= EDGE_THRESHOLD)
|
|
2253
|
+
if (!nearTrailingEdge) { return null }
|
|
2254
|
+
return { cell, table }
|
|
2255
|
+
}
|
|
2450
2256
|
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2257
|
+
const updateInlineColumnControls = (e: PointerEvent) => {
|
|
2258
|
+
const target = e.target as HTMLElement | null
|
|
2259
|
+
if (!target) {
|
|
2260
|
+
hideAllInlineControls()
|
|
2261
|
+
return
|
|
2262
|
+
}
|
|
2456
2263
|
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
e.stopImmediatePropagation()
|
|
2462
|
-
})
|
|
2264
|
+
// Keep current handle position locked while pointer is over the inline control buttons themselves
|
|
2265
|
+
if (target.closest('.table-inline-controls')) {
|
|
2266
|
+
return
|
|
2267
|
+
}
|
|
2463
2268
|
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2269
|
+
const cell = target.closest('td, th') as HTMLTableCellElement | null
|
|
2270
|
+
const table = cell?.closest('table') as HTMLTableElement | null
|
|
2271
|
+
const wrapper = table?.closest('.table-wrapper') as HTMLElement | null
|
|
2272
|
+
if (!cell || !table || !wrapper) {
|
|
2273
|
+
hideAllInlineControls()
|
|
2274
|
+
return
|
|
2275
|
+
}
|
|
2470
2276
|
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2277
|
+
const controls = wrapper.querySelector<HTMLElement>('.table-inline-controls')
|
|
2278
|
+
const addBtn = controls?.querySelector<HTMLElement>('.table-col-add-btn')
|
|
2279
|
+
const addLine = controls?.querySelector<HTMLElement>('.table-col-add-line')
|
|
2280
|
+
const thumbBtn = controls?.querySelector<HTMLElement>('.table-col-thumb')
|
|
2281
|
+
if (!controls || !addBtn || !addLine || !thumbBtn) { return }
|
|
2282
|
+
|
|
2283
|
+
doc.querySelectorAll<HTMLElement>('.table-inline-controls').forEach((other) => {
|
|
2284
|
+
if (other !== controls) {
|
|
2285
|
+
other.classList.remove('is-visible', 'show-col-add', 'show-col-thumb', 'is-dragging-col')
|
|
2286
|
+
}
|
|
2287
|
+
})
|
|
2288
|
+
|
|
2289
|
+
const wrapperRect = wrapper.getBoundingClientRect()
|
|
2290
|
+
const tableRect = table.getBoundingClientRect()
|
|
2291
|
+
const cellRect = cell.getBoundingClientRect()
|
|
2292
|
+
|
|
2293
|
+
const tableTop = tableRect.top - wrapperRect.top
|
|
2294
|
+
const tableHeight = tableRect.height
|
|
2295
|
+
const cellLeft = cellRect.left - wrapperRect.left
|
|
2296
|
+
const cellRight = cellRect.right - wrapperRect.left
|
|
2297
|
+
const cellCenter = cellLeft + cellRect.width / 2
|
|
2298
|
+
|
|
2299
|
+
const isRTL = table.dir === 'rtl'
|
|
2300
|
+
|
|
2301
|
+
const distLeft = Math.abs(e.clientX - cellRect.left)
|
|
2302
|
+
const distRight = Math.abs(e.clientX - cellRect.right)
|
|
2303
|
+
const nearLeft = distLeft <= distRight
|
|
2304
|
+
const minEdgeDist = Math.min(distLeft, distRight)
|
|
2305
|
+
const nearTopBand = (e.clientY - tableRect.top) <= 36
|
|
2306
|
+
|
|
2307
|
+
// In LTR: left edge inserts 'before', right edge inserts 'after'. In RTL: reversed.
|
|
2308
|
+
const insertPosition: 'before' | 'after' = nearLeft
|
|
2309
|
+
? (isRTL ? 'after' : 'before')
|
|
2310
|
+
: (isRTL ? 'before' : 'after')
|
|
2311
|
+
const boundaryX = nearLeft ? cellLeft : cellRight
|
|
2312
|
+
|
|
2313
|
+
activeHoverState = {
|
|
2314
|
+
table,
|
|
2315
|
+
cell,
|
|
2316
|
+
insertPosition,
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
controls.classList.add('is-visible', 'show-col-thumb')
|
|
2320
|
+
thumbBtn.style.left = `${cellCenter}px`
|
|
2321
|
+
thumbBtn.style.top = `${tableTop}px`
|
|
2322
|
+
|
|
2323
|
+
// Show '+' between columns when near a column divider or near the top header band
|
|
2324
|
+
if (minEdgeDist <= ADD_TRIGGER_ZONE || nearTopBand) {
|
|
2325
|
+
addBtn.style.left = `${boundaryX}px`
|
|
2326
|
+
addBtn.style.top = `${tableTop}px`
|
|
2327
|
+
addLine.style.left = `${boundaryX}px`
|
|
2328
|
+
addLine.style.top = `${tableTop}px`
|
|
2329
|
+
addLine.style.height = `${tableHeight}px`
|
|
2330
|
+
controls.classList.add('show-col-add')
|
|
2331
|
+
} else {
|
|
2332
|
+
controls.classList.remove('show-col-add')
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
const onHoverPointerMove = (e: PointerEvent) => {
|
|
2337
|
+
if (isSessionActive) { return }
|
|
2338
|
+
updateInlineColumnControls(e)
|
|
2339
|
+
|
|
2340
|
+
const hovered = getHoveredResizeCell(e)
|
|
2341
|
+
if (hovered && hovered.cell.nextElementSibling) {
|
|
2342
|
+
doc.body.style.cursor = 'col-resize'
|
|
2343
|
+
} else if (doc.body.style.cursor === 'col-resize') {
|
|
2344
|
+
doc.body.style.cursor = ''
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
2349
|
+
const target = e.target as HTMLElement | null
|
|
2350
|
+
|
|
2351
|
+
// 1. Handle native inline '+' (Add Column) click
|
|
2352
|
+
const addColBtn = target?.closest('.table-col-add-btn') as HTMLElement | null
|
|
2353
|
+
if (addColBtn) {
|
|
2354
|
+
e.preventDefault()
|
|
2355
|
+
e.stopPropagation()
|
|
2356
|
+
e.stopImmediatePropagation()
|
|
2357
|
+
const wrapper = addColBtn.closest('.table-wrapper')
|
|
2358
|
+
const table = wrapper?.querySelector('table') as HTMLTableElement | null
|
|
2359
|
+
const fallbackCell = table?.querySelector('th, td') as HTMLTableCellElement | null
|
|
2360
|
+
const targetCell = (activeHoverState?.table === table && activeHoverState?.cell.isConnected)
|
|
2361
|
+
? activeHoverState.cell
|
|
2362
|
+
: (contextMenuCell.value?.closest('table') === table ? contextMenuCell.value : fallbackCell)
|
|
2363
|
+
const pos = activeHoverState?.table === table
|
|
2364
|
+
? activeHoverState.insertPosition
|
|
2365
|
+
: ((addColBtn.getAttribute('data-insert-pos') as 'before' | 'after') || 'after')
|
|
2366
|
+
|
|
2367
|
+
if (targetCell) {
|
|
2368
|
+
tableInsertColumn(pos, editor.state.range, targetCell)
|
|
2369
|
+
updateContentWithHistory(doc)
|
|
2370
|
+
}
|
|
2371
|
+
return
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2374
|
+
// 2. Handle native inline '⋯' (Column Thumb: drag via usePointerSession to reorder, or click to open ContextMenu)
|
|
2375
|
+
const thumbColBtn = target?.closest('.table-col-thumb') as HTMLElement | null
|
|
2376
|
+
if (thumbColBtn) {
|
|
2377
|
+
e.stopPropagation()
|
|
2378
|
+
e.stopImmediatePropagation()
|
|
2379
|
+
const wrapper = thumbColBtn.closest('.table-wrapper') as HTMLElement | null
|
|
2380
|
+
const table = wrapper?.querySelector('table') as HTMLTableElement | null
|
|
2381
|
+
const controls = wrapper?.querySelector('.table-inline-controls') as HTMLElement | null
|
|
2382
|
+
const addLine = wrapper?.querySelector('.table-col-add-line') as HTMLElement | null
|
|
2383
|
+
const fallbackCell = table?.querySelector('th, td') as HTMLTableCellElement | null
|
|
2384
|
+
const targetCell = (activeHoverState?.table === table && activeHoverState?.cell.isConnected)
|
|
2385
|
+
? activeHoverState.cell
|
|
2386
|
+
: (contextMenuCell.value?.closest('table') === table ? contextMenuCell.value : fallbackCell)
|
|
2387
|
+
|
|
2388
|
+
if (table && wrapper && controls && addLine && targetCell) {
|
|
2389
|
+
const map = buildTableGrid(table)
|
|
2390
|
+
const coords = findCellCoords(map, targetCell)
|
|
2391
|
+
const fromCol = coords?.col ?? 0
|
|
2392
|
+
let toCol = fromCol
|
|
2393
|
+
const initialThumbLeft = thumbColBtn.style.left
|
|
2394
|
+
|
|
2395
|
+
isSessionActive = true
|
|
2396
|
+
tablePointerSession.start(e, {
|
|
2397
|
+
target: thumbColBtn,
|
|
2398
|
+
threshold: DRAG_THRESHOLD,
|
|
2399
|
+
cursor: 'grabbing',
|
|
2400
|
+
onThreshold: () => {
|
|
2401
|
+
controls.classList.add('is-dragging-col')
|
|
2402
|
+
controls.classList.remove('show-col-add')
|
|
2403
|
+
highlightTableColumn(table, fromCol)
|
|
2404
|
+
},
|
|
2405
|
+
onMove: (moveEvent, { exceededThreshold }) => {
|
|
2406
|
+
if (!exceededThreshold) { return }
|
|
2407
|
+
const wrapperRect = wrapper.getBoundingClientRect()
|
|
2408
|
+
const tableRect = table.getBoundingClientRect()
|
|
2409
|
+
const clampedX = Math.max(tableRect.left, Math.min(tableRect.right, moveEvent.clientX))
|
|
2410
|
+
thumbColBtn.style.left = `${clampedX - wrapperRect.left}px`
|
|
2411
|
+
|
|
2412
|
+
const firstRow = table.rows[0]
|
|
2413
|
+
if (firstRow) {
|
|
2414
|
+
let targetIdx = fromCol
|
|
2415
|
+
let indicatorX = clampedX - wrapperRect.left
|
|
2416
|
+
const cells = Array.from(firstRow.cells)
|
|
2417
|
+
for (let i = 0; i < cells.length; i++) {
|
|
2418
|
+
const r = cells[i].getBoundingClientRect()
|
|
2419
|
+
if (clampedX >= r.left && clampedX <= r.right) {
|
|
2420
|
+
targetIdx = i
|
|
2421
|
+
const isRTL = table.dir === 'rtl'
|
|
2422
|
+
const dropEdge = i > fromCol
|
|
2423
|
+
? (isRTL ? r.left : r.right)
|
|
2424
|
+
: (isRTL ? r.right : r.left)
|
|
2425
|
+
indicatorX = dropEdge - wrapperRect.left
|
|
2426
|
+
break
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
toCol = targetIdx
|
|
2430
|
+
addLine.style.left = `${indicatorX}px`
|
|
2431
|
+
addLine.style.top = `${tableRect.top - wrapperRect.top}px`
|
|
2432
|
+
addLine.style.height = `${tableRect.height}px`
|
|
2433
|
+
}
|
|
2434
|
+
},
|
|
2435
|
+
onEnd: (_endEvent, { exceededThreshold }) => {
|
|
2436
|
+
isSessionActive = false
|
|
2437
|
+
controls.classList.remove('is-dragging-col')
|
|
2438
|
+
|
|
2439
|
+
if (exceededThreshold) {
|
|
2440
|
+
if (fromCol !== toCol) {
|
|
2441
|
+
tableMoveColumn(table, fromCol, toCol)
|
|
2442
|
+
highlightTableColumn(table, toCol)
|
|
2443
|
+
updateContentWithHistory(doc)
|
|
2444
|
+
} else {
|
|
2445
|
+
thumbColBtn.style.left = initialThumbLeft
|
|
2446
|
+
}
|
|
2447
|
+
} else {
|
|
2448
|
+
// Clicked without dragging: highlight column and open ContextMenu below the thumb
|
|
2449
|
+
contextMenuCell.value = targetCell
|
|
2450
|
+
highlightTableColumn(table, fromCol)
|
|
2451
|
+
const iframeRect = iframe.value?.getBoundingClientRect()
|
|
2452
|
+
const thumbRect = thumbColBtn.getBoundingClientRect()
|
|
2453
|
+
if (iframeRect) {
|
|
2454
|
+
contextMenuPosition.value = {
|
|
2455
|
+
x: iframeRect.left + thumbRect.left,
|
|
2456
|
+
y: iframeRect.top + thumbRect.bottom + 6,
|
|
2457
|
+
}
|
|
2458
|
+
showTableContextMenu.value = true
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
},
|
|
2462
|
+
onCancel: () => {
|
|
2463
|
+
isSessionActive = false
|
|
2464
|
+
controls.classList.remove('is-dragging-col')
|
|
2465
|
+
thumbColBtn.style.left = initialThumbLeft
|
|
2466
|
+
},
|
|
2467
|
+
})
|
|
2468
|
+
}
|
|
2469
|
+
return
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// 3. Handle column border drag-resizing via usePointerSession (with Escape rollback)
|
|
2473
|
+
const hovered = getHoveredResizeCell(e)
|
|
2474
|
+
if (!hovered || !hovered.cell.nextElementSibling) { return }
|
|
2475
|
+
|
|
2476
|
+
const { cell, table } = hovered
|
|
2477
|
+
const map = buildTableGrid(table)
|
|
2478
|
+
const coords = findCellCoords(map, cell)
|
|
2479
|
+
if (!coords) { return }
|
|
2480
|
+
const colIndex = coords.col + coords.colspan - 1
|
|
2481
|
+
if (colIndex >= map.colCount - 1) { return }
|
|
2482
|
+
|
|
2483
|
+
const tableRect = table.getBoundingClientRect()
|
|
2484
|
+
const cols = Array.from(table.querySelectorAll<HTMLElement>(':scope > colgroup > col'))
|
|
2485
|
+
const startWidths = cols.length === map.colCount
|
|
2486
|
+
? cols.map(c => Number.parseFloat(c.style.width) || (100 / map.colCount))
|
|
2487
|
+
: Array.from({ length: map.colCount }, () => Number((100 / map.colCount).toFixed(2)))
|
|
2488
|
+
|
|
2489
|
+
isSessionActive = true
|
|
2490
|
+
hideAllInlineControls()
|
|
2491
|
+
tablePointerSession.start(e, {
|
|
2492
|
+
target: cell,
|
|
2493
|
+
cursor: 'col-resize',
|
|
2494
|
+
onMove: (_moveEvent, { dx }) => {
|
|
2495
|
+
if (tableRect.width <= 0) { return }
|
|
2496
|
+
const isRTL = table.dir === 'rtl'
|
|
2497
|
+
const deltaPx = isRTL ? -dx : dx
|
|
2498
|
+
const deltaPct = (deltaPx / tableRect.width) * 100
|
|
2499
|
+
|
|
2500
|
+
const nextIdx = colIndex + 1
|
|
2501
|
+
if (nextIdx >= startWidths.length) { return }
|
|
2502
|
+
|
|
2503
|
+
const minPct = 5
|
|
2504
|
+
const pairTotal = startWidths[colIndex] + startWidths[nextIdx]
|
|
2505
|
+
const newCurrent = Math.max(minPct, Math.min(pairTotal - minPct, startWidths[colIndex] + deltaPct))
|
|
2506
|
+
const newNext = pairTotal - newCurrent
|
|
2507
|
+
|
|
2508
|
+
const updatedWidths = [...startWidths]
|
|
2509
|
+
updatedWidths[colIndex] = Number(newCurrent.toFixed(2))
|
|
2510
|
+
updatedWidths[nextIdx] = Number(newNext.toFixed(2))
|
|
2511
|
+
applyColumnWidthsToDOM(table, updatedWidths)
|
|
2512
|
+
},
|
|
2513
|
+
onEnd: () => {
|
|
2514
|
+
isSessionActive = false
|
|
2515
|
+
updateContentWithHistory(doc)
|
|
2516
|
+
},
|
|
2517
|
+
onCancel: () => {
|
|
2518
|
+
isSessionActive = false
|
|
2519
|
+
applyColumnWidthsToDOM(table, startWidths)
|
|
2520
|
+
},
|
|
2521
|
+
})
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
doc.addEventListener('pointermove', onHoverPointerMove)
|
|
2525
|
+
doc.addEventListener('pointerdown', onPointerDown)
|
|
2526
|
+
|
|
2527
|
+
activeResizeCleanup = () => {
|
|
2528
|
+
tablePointerSession.cancel()
|
|
2529
|
+
doc.removeEventListener('pointermove', onHoverPointerMove)
|
|
2530
|
+
doc.removeEventListener('pointerdown', onPointerDown)
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
function setupTableEditButtons(doc: Document) {
|
|
2535
|
+
const previousEditHandler = (doc as any).__tableEditPointerHandler as ((event: PointerEvent) => void) | undefined
|
|
2536
|
+
if (previousEditHandler) {
|
|
2537
|
+
doc.removeEventListener('pointerdown', previousEditHandler, true)
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
const tableEditPointerHandler = (event: PointerEvent) => {
|
|
2541
|
+
const target = event.target as Element | null
|
|
2542
|
+
const editButton = target?.closest('.table-edit-btn')
|
|
2543
|
+
if (!editButton) { return }
|
|
2544
|
+
|
|
2545
|
+
const table = editButton.closest('.table-wrapper')?.querySelector('table') as HTMLTableElement | null
|
|
2546
|
+
if (!table) { return }
|
|
2547
|
+
|
|
2548
|
+
event.preventDefault()
|
|
2549
|
+
event.stopPropagation()
|
|
2550
|
+
event.stopImmediatePropagation()
|
|
2551
|
+
globalThis.setTimeout(() => {
|
|
2552
|
+
try {
|
|
2553
|
+
openTableEditor(table)
|
|
2554
|
+
} catch (error) {
|
|
2555
|
+
console.error('[RichText table editor] delegated open failed', error)
|
|
2556
|
+
}
|
|
2557
|
+
}, 0)
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
;(doc as any).__tableEditPointerHandler = tableEditPointerHandler
|
|
2561
|
+
doc.addEventListener('pointerdown', tableEditPointerHandler, true)
|
|
2562
|
+
setupTableColumnResizing(doc)
|
|
2563
|
+
|
|
2564
|
+
function createInlineColumnControls(): HTMLElement {
|
|
2565
|
+
const controls = doc.createElement('div')
|
|
2566
|
+
controls.className = 'table-inline-controls'
|
|
2567
|
+
controls.setAttribute('contenteditable', 'false')
|
|
2568
|
+
|
|
2569
|
+
const addLine = doc.createElement('span')
|
|
2570
|
+
addLine.className = 'table-col-add-line'
|
|
2571
|
+
addLine.setAttribute('contenteditable', 'false')
|
|
2572
|
+
|
|
2573
|
+
const addBtn = doc.createElement('span')
|
|
2574
|
+
addBtn.className = 'table-col-add-btn'
|
|
2575
|
+
addBtn.textContent = '+'
|
|
2576
|
+
addBtn.title = 'Insert column here'
|
|
2577
|
+
addBtn.setAttribute('aria-label', 'Insert column here')
|
|
2578
|
+
addBtn.setAttribute('contenteditable', 'false')
|
|
2579
|
+
addBtn.setAttribute('tabindex', '-1')
|
|
2580
|
+
|
|
2581
|
+
const thumbBtn = doc.createElement('span')
|
|
2582
|
+
thumbBtn.className = 'table-col-thumb'
|
|
2583
|
+
thumbBtn.textContent = '⋯'
|
|
2584
|
+
thumbBtn.title = 'Click for column options • Drag to reorder column'
|
|
2585
|
+
thumbBtn.setAttribute('aria-label', 'Column options')
|
|
2586
|
+
thumbBtn.setAttribute('contenteditable', 'false')
|
|
2587
|
+
thumbBtn.setAttribute('tabindex', '-1')
|
|
2588
|
+
|
|
2589
|
+
controls.appendChild(addLine)
|
|
2590
|
+
controls.appendChild(addBtn)
|
|
2591
|
+
controls.appendChild(thumbBtn)
|
|
2592
|
+
return controls
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// Simple function to add edit buttons to all tables
|
|
2596
|
+
function addEditButtonsToTables() {
|
|
2597
|
+
const tables = doc.querySelectorAll<HTMLTableElement>('table:not([data-edit-button-added])')
|
|
2598
|
+
|
|
2599
|
+
tables.forEach((table) => {
|
|
2600
|
+
const editBtn = doc.createElement('span')
|
|
2601
|
+
editBtn.className = 'table-edit-btn'
|
|
2602
|
+
editBtn.textContent = '✎'
|
|
2603
|
+
editBtn.title = 'Edit Table'
|
|
2604
|
+
editBtn.setAttribute('aria-label', 'Edit Table')
|
|
2605
|
+
|
|
2606
|
+
editBtn.setAttribute('contenteditable', 'false')
|
|
2607
|
+
editBtn.setAttribute('tabindex', '-1')
|
|
2608
|
+
editBtn.style.pointerEvents = 'auto'
|
|
2609
|
+
editBtn.style.userSelect = 'none'
|
|
2610
|
+
|
|
2611
|
+
editBtn.addEventListener('click', (e) => {
|
|
2612
|
+
e.preventDefault()
|
|
2613
|
+
e.stopPropagation()
|
|
2614
|
+
e.stopImmediatePropagation()
|
|
2615
|
+
})
|
|
2616
|
+
|
|
2617
|
+
editBtn.addEventListener('focus', (e) => {
|
|
2618
|
+
e.preventDefault()
|
|
2619
|
+
e.stopPropagation()
|
|
2620
|
+
editBtn.blur()
|
|
2621
|
+
})
|
|
2622
|
+
|
|
2623
|
+
editBtn.addEventListener('pointerdown', (e) => {
|
|
2624
|
+
e.preventDefault()
|
|
2625
|
+
e.stopPropagation()
|
|
2626
|
+
e.stopImmediatePropagation()
|
|
2627
|
+
})
|
|
2628
|
+
|
|
2629
|
+
const existingWrapper = table.closest('.table-wrapper') as HTMLElement | null
|
|
2630
|
+
if (existingWrapper) {
|
|
2631
|
+
if (!existingWrapper.querySelector('.table-edit-btn')) {
|
|
2632
|
+
existingWrapper.appendChild(editBtn)
|
|
2633
|
+
}
|
|
2634
|
+
if (!existingWrapper.querySelector('.table-inline-controls')) {
|
|
2635
|
+
existingWrapper.appendChild(createInlineColumnControls())
|
|
2636
|
+
}
|
|
2637
|
+
table.setAttribute('data-edit-button-added', 'true')
|
|
2638
|
+
return
|
|
2639
|
+
}
|
|
2480
2640
|
|
|
2481
|
-
// Create a wrapper div to contain both table and button
|
|
2482
2641
|
const wrapper = doc.createElement('div')
|
|
2483
2642
|
wrapper.className = 'table-wrapper'
|
|
2484
2643
|
wrapper.style.position = 'relative'
|
|
@@ -2487,9 +2646,7 @@ function setupTableEditButtons(doc: Document) {
|
|
|
2487
2646
|
wrapper.style.width = '100%'
|
|
2488
2647
|
wrapper.style.marginBottom = '1rem'
|
|
2489
2648
|
|
|
2490
|
-
// Copy table alignment to wrapper and convert to flex
|
|
2491
2649
|
if (table.style.marginLeft && table.style.marginRight) {
|
|
2492
|
-
// Detect current alignment
|
|
2493
2650
|
let alignment = 'left'
|
|
2494
2651
|
if (table.style.marginLeft === 'auto' && table.style.marginRight === 'auto') {
|
|
2495
2652
|
alignment = 'center'
|
|
@@ -2497,7 +2654,6 @@ function setupTableEditButtons(doc: Document) {
|
|
|
2497
2654
|
alignment = 'right'
|
|
2498
2655
|
}
|
|
2499
2656
|
|
|
2500
|
-
// Apply flex alignment to wrapper
|
|
2501
2657
|
if (alignment === 'center') {
|
|
2502
2658
|
wrapper.style.alignItems = 'center'
|
|
2503
2659
|
} else if (alignment === 'right') {
|
|
@@ -2506,42 +2662,38 @@ function setupTableEditButtons(doc: Document) {
|
|
|
2506
2662
|
wrapper.style.alignItems = 'flex-start'
|
|
2507
2663
|
}
|
|
2508
2664
|
|
|
2509
|
-
// Reset table margins since wrapper handles alignment now
|
|
2510
2665
|
table.style.marginLeft = '0'
|
|
2511
2666
|
table.style.marginRight = '0'
|
|
2512
2667
|
} else {
|
|
2513
|
-
// Default to left alignment
|
|
2514
2668
|
wrapper.style.alignItems = 'flex-start'
|
|
2515
2669
|
}
|
|
2516
2670
|
|
|
2517
|
-
// Insert wrapper before table
|
|
2518
2671
|
table.parentNode?.insertBefore(wrapper, table)
|
|
2519
|
-
// Move table into wrapper
|
|
2520
2672
|
wrapper.appendChild(table)
|
|
2521
|
-
// Add button to wrapper (not to table)
|
|
2522
2673
|
wrapper.appendChild(editBtn)
|
|
2674
|
+
wrapper.appendChild(createInlineColumnControls())
|
|
2523
2675
|
|
|
2524
|
-
// Remove margin from table since wrapper handles it
|
|
2525
2676
|
table.style.marginBottom = '0'
|
|
2526
|
-
|
|
2527
2677
|
table.setAttribute('data-edit-button-added', 'true')
|
|
2528
2678
|
})
|
|
2529
2679
|
}
|
|
2530
2680
|
|
|
2531
|
-
|
|
2532
|
-
; (doc as any).__addEditButtonsToTables = addEditButtonsToTables
|
|
2533
|
-
|
|
2534
|
-
// Add buttons immediately
|
|
2681
|
+
;(doc as any).__addEditButtonsToTables = addEditButtonsToTables
|
|
2535
2682
|
addEditButtonsToTables()
|
|
2536
2683
|
}
|
|
2537
2684
|
|
|
2538
2685
|
function setupAutoWrapping(doc: Document) {
|
|
2539
|
-
|
|
2686
|
+
function createPlaceholderElement(): HTMLParagraphElement {
|
|
2687
|
+
const p = doc.createElement('p')
|
|
2688
|
+
p.className = 'placeholder'
|
|
2689
|
+
p.textContent = props.placeholder || ''
|
|
2690
|
+
return p
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2540
2693
|
function removePlaceholder() {
|
|
2541
2694
|
const placeholderElement = doc.querySelector('.placeholder')
|
|
2542
2695
|
if (placeholderElement) {
|
|
2543
2696
|
placeholderElement.remove()
|
|
2544
|
-
// Add empty paragraph if body becomes empty
|
|
2545
2697
|
if (!doc.body.innerHTML.trim()) {
|
|
2546
2698
|
const direction = getCurrentDirection()
|
|
2547
2699
|
doc.body.innerHTML = `<p dir="${direction}"><br></p>`
|
|
@@ -2549,16 +2701,14 @@ function setupAutoWrapping(doc: Document) {
|
|
|
2549
2701
|
}
|
|
2550
2702
|
}
|
|
2551
2703
|
|
|
2552
|
-
// Function to add placeholder if content is empty
|
|
2553
2704
|
function addPlaceholder() {
|
|
2554
2705
|
const direction = getCurrentDirection()
|
|
2555
2706
|
const emptyContent = `<p dir="${direction}"><br></p>`
|
|
2556
2707
|
if (props.placeholder && (!doc.body.innerHTML.trim() || doc.body.innerHTML === emptyContent)) {
|
|
2557
|
-
doc.body.
|
|
2708
|
+
doc.body.replaceChildren(createPlaceholderElement())
|
|
2558
2709
|
}
|
|
2559
2710
|
}
|
|
2560
2711
|
|
|
2561
|
-
// Initialize editor with paragraph or placeholder
|
|
2562
2712
|
if (!doc.body.innerHTML.trim()) {
|
|
2563
2713
|
if (props.placeholder) {
|
|
2564
2714
|
addPlaceholder()
|
|
@@ -2568,34 +2718,27 @@ function setupAutoWrapping(doc: Document) {
|
|
|
2568
2718
|
}
|
|
2569
2719
|
}
|
|
2570
2720
|
|
|
2571
|
-
// After any change, ensure proper structure
|
|
2572
2721
|
function normalizeContent() {
|
|
2573
|
-
// Only proceed if the body exists
|
|
2574
2722
|
if (!doc.body) {
|
|
2575
2723
|
return
|
|
2576
2724
|
}
|
|
2577
2725
|
|
|
2578
|
-
// If body is completely empty, add a paragraph and return
|
|
2579
2726
|
if (!doc.body.innerHTML.trim() || doc.body.innerHTML === '') {
|
|
2580
2727
|
const direction = getCurrentDirection()
|
|
2581
2728
|
doc.body.innerHTML = `<p dir="${direction}"><br></p>`
|
|
2582
2729
|
return
|
|
2583
2730
|
}
|
|
2584
2731
|
|
|
2585
|
-
// Mark body as being normalized to prevent recursive processing
|
|
2586
2732
|
if (doc.body.dataset.normalizing === 'true') {
|
|
2587
2733
|
return
|
|
2588
2734
|
}
|
|
2589
2735
|
doc.body.dataset.normalizing = 'true'
|
|
2590
2736
|
|
|
2591
2737
|
try {
|
|
2592
|
-
// Only wrap loose text nodes that are direct children of body
|
|
2593
2738
|
const walker = doc.createTreeWalker(
|
|
2594
2739
|
doc.body,
|
|
2595
2740
|
NodeFilter.SHOW_TEXT,
|
|
2596
2741
|
(node) => {
|
|
2597
|
-
// Only accept text nodes that are direct children of body
|
|
2598
|
-
// AND have meaningful content
|
|
2599
2742
|
const parent = node.parentNode as HTMLElement
|
|
2600
2743
|
return (parent === doc.body
|
|
2601
2744
|
&& node.textContent?.trim()
|
|
@@ -2611,30 +2754,22 @@ function setupAutoWrapping(doc: Document) {
|
|
|
2611
2754
|
textNodes.push(node as Text)
|
|
2612
2755
|
}
|
|
2613
2756
|
|
|
2614
|
-
// Wrap loose text nodes very carefully, preserving existing structure
|
|
2615
2757
|
textNodes.forEach((textNode) => {
|
|
2616
|
-
// Triple check the node is still valid and not corrupted
|
|
2617
2758
|
if (textNode.parentNode === doc.body
|
|
2618
2759
|
&& textNode.textContent?.trim()
|
|
2619
2760
|
&& textNode.nodeType === Node.TEXT_NODE) {
|
|
2620
2761
|
const p = doc.createElement('p')
|
|
2621
2762
|
p.dir = doc.body.dir || 'ltr'
|
|
2763
|
+
p.textContent = textNode.textContent
|
|
2622
2764
|
|
|
2623
|
-
// Clone the text content to avoid any reference issues
|
|
2624
|
-
const { textContent } = textNode
|
|
2625
|
-
p.textContent = textContent
|
|
2626
|
-
|
|
2627
|
-
// Replace the text node with the paragraph
|
|
2628
2765
|
try {
|
|
2629
2766
|
textNode.parentNode.replaceChild(p, textNode)
|
|
2630
|
-
} catch
|
|
2631
|
-
// If replacement fails, just remove the problematic text node
|
|
2767
|
+
} catch {
|
|
2632
2768
|
textNode.parentNode.removeChild(textNode)
|
|
2633
2769
|
}
|
|
2634
2770
|
}
|
|
2635
2771
|
})
|
|
2636
2772
|
|
|
2637
|
-
// Ensure empty body has a paragraph or placeholder
|
|
2638
2773
|
if (!doc.body.children.length || !doc.body.innerHTML.trim()) {
|
|
2639
2774
|
if (props.placeholder) {
|
|
2640
2775
|
addPlaceholder()
|
|
@@ -2644,39 +2779,36 @@ function setupAutoWrapping(doc: Document) {
|
|
|
2644
2779
|
}
|
|
2645
2780
|
}
|
|
2646
2781
|
|
|
2647
|
-
|
|
2648
|
-
const paras = Array.from(doc.body.querySelectorAll('p'))
|
|
2782
|
+
const paras = Array.from(doc.body.querySelectorAll(':scope > p'))
|
|
2649
2783
|
for (let i = 0; i < paras.length; i++) {
|
|
2650
2784
|
const current = paras[i]
|
|
2651
|
-
const isEmpty = !current.textContent?.trim()
|
|
2785
|
+
const isEmpty = !current.textContent?.trim() && !current.querySelector('img, video, iframe')
|
|
2652
2786
|
|
|
2653
2787
|
if (isEmpty) {
|
|
2654
|
-
// Only remove if we have consecutive empty paragraphs at the end
|
|
2655
2788
|
const isLastPara = i === paras.length - 1
|
|
2656
2789
|
const prevPara = i > 0 ? paras[i - 1] : null
|
|
2657
|
-
const isPrevEmpty = prevPara &&
|
|
2790
|
+
const isPrevEmpty = prevPara && !prevPara.textContent?.trim() && !prevPara.querySelector('img, video, iframe')
|
|
2658
2791
|
|
|
2659
|
-
// Keep at least one empty paragraph for cursor placement
|
|
2660
2792
|
if (isPrevEmpty && !isLastPara) {
|
|
2661
2793
|
current.remove()
|
|
2662
2794
|
}
|
|
2663
2795
|
}
|
|
2664
2796
|
}
|
|
2665
2797
|
} finally {
|
|
2666
|
-
// Always clean up the processing flag
|
|
2667
2798
|
delete doc.body.dataset.normalizing
|
|
2668
2799
|
}
|
|
2669
2800
|
}
|
|
2670
2801
|
|
|
2671
2802
|
// Handle input events
|
|
2672
2803
|
doc.addEventListener('input', (e) => {
|
|
2673
|
-
|
|
2674
|
-
|
|
2804
|
+
const inputEvent = e as InputEvent
|
|
2805
|
+
if (inputEvent.isComposing) {
|
|
2806
|
+
return
|
|
2807
|
+
}
|
|
2675
2808
|
|
|
2676
|
-
|
|
2809
|
+
removePlaceholder()
|
|
2677
2810
|
detectAndSetDirection()
|
|
2678
2811
|
|
|
2679
|
-
// Handle complete content deletion immediately
|
|
2680
2812
|
if (!doc.body.innerHTML.trim() || doc.body.innerHTML === '') {
|
|
2681
2813
|
if (props.placeholder) {
|
|
2682
2814
|
addPlaceholder()
|
|
@@ -2686,175 +2818,146 @@ function setupAutoWrapping(doc: Document) {
|
|
|
2686
2818
|
}
|
|
2687
2819
|
updateContentWithHistory(doc)
|
|
2688
2820
|
return
|
|
2689
|
-
}
|
|
2690
|
-
const inputEvent = e as InputEvent
|
|
2691
|
-
const normalizeInputTypes = ['insertFromPaste', 'insertFromDrop']
|
|
2821
|
+
}
|
|
2692
2822
|
|
|
2693
|
-
|
|
2823
|
+
const normalizeInputTypes = ['insertFromPaste', 'insertFromDrop']
|
|
2694
2824
|
if (inputEvent.inputType && normalizeInputTypes.includes(inputEvent.inputType)) {
|
|
2695
|
-
// Add a delay to let the browser finish processing
|
|
2696
2825
|
setTimeout(() => {
|
|
2697
2826
|
normalizeContent()
|
|
2698
|
-
|
|
2827
|
+
setupTableEditButtons(doc)
|
|
2828
|
+
updateContentWithHistory(doc)
|
|
2829
|
+
}, 50)
|
|
2830
|
+
return
|
|
2699
2831
|
}
|
|
2700
2832
|
|
|
2701
|
-
// Always update content to keep state in sync
|
|
2702
2833
|
updateContentWithHistory(doc)
|
|
2703
2834
|
})
|
|
2704
2835
|
|
|
2705
|
-
// Handle Enter key
|
|
2836
|
+
// Handle Enter key safely across inline trees, selections, IME, and tables
|
|
2706
2837
|
doc.addEventListener('keydown', (e) => {
|
|
2707
|
-
if (e.key
|
|
2708
|
-
|
|
2709
|
-
if (!selection || !selection.rangeCount) { return }
|
|
2710
|
-
|
|
2711
|
-
const range = selection.getRangeAt(0)
|
|
2712
|
-
|
|
2713
|
-
// Check if we're in any list context - let browser handle lists
|
|
2714
|
-
let currentElement = range.startContainer
|
|
2715
|
-
|
|
2716
|
-
// If it's a text node, get its parent
|
|
2717
|
-
if (currentElement.nodeType === Node.TEXT_NODE) {
|
|
2718
|
-
currentElement = currentElement.parentElement!
|
|
2719
|
-
}
|
|
2838
|
+
if (e.key !== 'Enter') { return }
|
|
2839
|
+
if (e.isComposing || e.keyCode === 229 || e.shiftKey) { return }
|
|
2720
2840
|
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
const listContainer = (currentElement as Element).closest('ul, ol')
|
|
2841
|
+
const selection = doc.getSelection()
|
|
2842
|
+
if (!selection || !selection.rangeCount) { return }
|
|
2724
2843
|
|
|
2725
|
-
|
|
2726
|
-
// We're in a list context - let browser handle everything
|
|
2727
|
-
return
|
|
2728
|
-
} // Find the paragraph we're in (only for confirmed non-list content)
|
|
2729
|
-
let paragraph = range.startContainer
|
|
2730
|
-
if (paragraph.nodeType === Node.TEXT_NODE) {
|
|
2731
|
-
paragraph = paragraph.parentElement!
|
|
2732
|
-
}
|
|
2733
|
-
paragraph = (paragraph as Element).closest('p,h1,h2,h3,h4,h5,h6') as HTMLElement
|
|
2844
|
+
const range = selection.getRangeAt(0)
|
|
2734
2845
|
|
|
2735
|
-
|
|
2846
|
+
let currentElement = range.startContainer
|
|
2847
|
+
if (currentElement.nodeType === Node.TEXT_NODE) {
|
|
2848
|
+
currentElement = currentElement.parentElement!
|
|
2849
|
+
}
|
|
2736
2850
|
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
&& (prevSibling.tagName === 'P')
|
|
2742
|
-
&& !prevSibling.textContent?.trim()
|
|
2851
|
+
// Let browser handle lists and table cells natively
|
|
2852
|
+
if ((currentElement as Element)?.closest('li, ul, ol, td, th')) {
|
|
2853
|
+
return
|
|
2854
|
+
}
|
|
2743
2855
|
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
e.preventDefault()
|
|
2747
|
-
return
|
|
2748
|
-
}
|
|
2856
|
+
const paragraph = (currentElement as Element)?.closest('p,h1,h2,h3,h4,h5,h6') as HTMLElement | null
|
|
2857
|
+
if (!paragraph) { return }
|
|
2749
2858
|
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
)
|
|
2859
|
+
const currentIsEmpty = !paragraph.textContent?.trim() && !paragraph.querySelector('img, video, iframe')
|
|
2860
|
+
const prevSibling = paragraph.previousElementSibling as HTMLElement | null
|
|
2861
|
+
const prevIsEmpty = prevSibling
|
|
2862
|
+
&& prevSibling.tagName === 'P'
|
|
2863
|
+
&& !prevSibling.textContent?.trim()
|
|
2864
|
+
&& !prevSibling.querySelector('img, video, iframe')
|
|
2755
2865
|
|
|
2756
|
-
|
|
2866
|
+
if (currentIsEmpty && prevIsEmpty) {
|
|
2757
2867
|
e.preventDefault()
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
// Get the text content before and after cursor
|
|
2761
|
-
const textBeforeCursor = range.startContainer.textContent?.substring(0, range.startOffset) || ''
|
|
2762
|
-
const textAfterCursor = range.startContainer.textContent?.substring(range.startOffset) || ''
|
|
2868
|
+
return
|
|
2869
|
+
}
|
|
2763
2870
|
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
if (props.debug) { /* reserved for future debug output */ }
|
|
2871
|
+
e.preventDefault()
|
|
2872
|
+
e.stopPropagation()
|
|
2767
2873
|
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2874
|
+
// Delete selected content first if selection is not collapsed
|
|
2875
|
+
if (!range.collapsed) {
|
|
2876
|
+
range.deleteContents()
|
|
2877
|
+
}
|
|
2771
2878
|
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
// We're in the middle of text - split properly
|
|
2775
|
-
const textNode = range.startContainer as Text
|
|
2879
|
+
const paragraphDirection = paragraph.getAttribute('dir') as 'ltr' | 'rtl'
|
|
2880
|
+
|| getTextDirection(paragraph.textContent || '', (doc.body.dir || 'ltr') as 'ltr' | 'rtl')
|
|
2776
2881
|
|
|
2777
|
-
|
|
2778
|
-
|
|
2882
|
+
const newParagraph = doc.createElement('p')
|
|
2883
|
+
newParagraph.dir = paragraphDirection
|
|
2884
|
+
if (paragraph.hasAttribute('data-dir-explicit')) {
|
|
2885
|
+
newParagraph.setAttribute('data-dir-explicit', 'true')
|
|
2886
|
+
}
|
|
2887
|
+
if (paragraph.style.textAlign) {
|
|
2888
|
+
newParagraph.style.textAlign = paragraph.style.textAlign
|
|
2889
|
+
}
|
|
2779
2890
|
|
|
2780
|
-
|
|
2781
|
-
|
|
2891
|
+
// Extract all DOM nodes from the caret position to the end of `paragraph`
|
|
2892
|
+
const tailRange = doc.createRange()
|
|
2893
|
+
tailRange.setStart(range.startContainer, range.startOffset)
|
|
2894
|
+
tailRange.setEnd(paragraph, paragraph.childNodes.length)
|
|
2895
|
+
const trailingFragment = tailRange.extractContents()
|
|
2782
2896
|
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
const newFormattedElement = parentElement.cloneNode(false) as HTMLElement
|
|
2788
|
-
newFormattedElement.textContent = textAfterCursor
|
|
2789
|
-
newParagraph.appendChild(newFormattedElement)
|
|
2790
|
-
} else {
|
|
2791
|
-
newParagraph.textContent = textAfterCursor
|
|
2792
|
-
}
|
|
2793
|
-
} else {
|
|
2794
|
-
newParagraph.innerHTML = '<br>'
|
|
2795
|
-
}
|
|
2796
|
-
} else {
|
|
2797
|
-
// We're at the end of text or in empty space
|
|
2798
|
-
newParagraph.innerHTML = '<br>'
|
|
2897
|
+
// Strip empty inline wrappers left behind at the split boundary
|
|
2898
|
+
Array.from(paragraph.querySelectorAll('b, i, u, strong, em, span, a')).forEach((el) => {
|
|
2899
|
+
if (!el.textContent && !el.querySelector('img, br')) {
|
|
2900
|
+
el.remove()
|
|
2799
2901
|
}
|
|
2902
|
+
})
|
|
2903
|
+
Array.from(trailingFragment.querySelectorAll('b, i, u, strong, em, span, a')).forEach((el) => {
|
|
2904
|
+
if (!el.textContent && !el.querySelector('img, br')) {
|
|
2905
|
+
el.remove()
|
|
2906
|
+
}
|
|
2907
|
+
})
|
|
2800
2908
|
|
|
2801
|
-
|
|
2802
|
-
paragraph.
|
|
2803
|
-
|
|
2804
|
-
// Set cursor at the beginning of the new paragraph
|
|
2805
|
-
const newRange = doc.createRange()
|
|
2806
|
-
newRange.selectNodeContents(newParagraph)
|
|
2807
|
-
newRange.collapse(true)
|
|
2808
|
-
selection.removeAllRanges()
|
|
2809
|
-
selection.addRange(newRange)
|
|
2909
|
+
if (!paragraph.textContent?.trim() && !paragraph.querySelector('img, br, video, iframe')) {
|
|
2910
|
+
paragraph.innerHTML = '<br>'
|
|
2911
|
+
}
|
|
2810
2912
|
|
|
2811
|
-
|
|
2812
|
-
|
|
2913
|
+
if (trailingFragment.textContent?.trim() || trailingFragment.querySelector('img, video, iframe')) {
|
|
2914
|
+
newParagraph.appendChild(trailingFragment)
|
|
2915
|
+
} else {
|
|
2916
|
+
newParagraph.innerHTML = '<br>'
|
|
2813
2917
|
}
|
|
2918
|
+
|
|
2919
|
+
paragraph.parentNode?.insertBefore(newParagraph, paragraph.nextSibling)
|
|
2920
|
+
|
|
2921
|
+
const newRange = doc.createRange()
|
|
2922
|
+
newRange.selectNodeContents(newParagraph)
|
|
2923
|
+
newRange.collapse(true)
|
|
2924
|
+
selection.removeAllRanges()
|
|
2925
|
+
selection.addRange(newRange)
|
|
2926
|
+
|
|
2927
|
+
updateContentWithHistory(doc)
|
|
2814
2928
|
})
|
|
2815
2929
|
|
|
2816
|
-
//
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
setTimeout(() => {
|
|
2820
|
-
normalizeContent()
|
|
2821
|
-
updateContentWithHistory(doc)
|
|
2822
|
-
}, 150) // Longer timeout for reliable paste processing
|
|
2823
|
-
}) // Add a MutationObserver to catch structural changes that might create loose text
|
|
2824
|
-
const observer = new MutationObserver((mutations) => {
|
|
2930
|
+
// Disconnect any previous observer before attaching a new one
|
|
2931
|
+
bodyMutationObserver?.disconnect()
|
|
2932
|
+
bodyMutationObserver = new MutationObserver((mutations) => {
|
|
2825
2933
|
for (const mutation of mutations) {
|
|
2826
2934
|
if (mutation.type === 'childList') {
|
|
2827
|
-
// Check if any loose text nodes were added to body
|
|
2828
2935
|
Array.from(mutation.addedNodes).forEach((addedNode) => {
|
|
2829
2936
|
if (addedNode.nodeType === Node.TEXT_NODE
|
|
2830
2937
|
&& addedNode.parentNode === doc.body
|
|
2831
2938
|
&& addedNode.textContent?.trim()) {
|
|
2832
|
-
// Wrap loose text node immediately
|
|
2833
2939
|
const p = doc.createElement('p')
|
|
2834
2940
|
p.dir = doc.body.dir || 'ltr'
|
|
2835
2941
|
p.textContent = addedNode.textContent
|
|
2836
2942
|
addedNode.parentNode.replaceChild(p, addedNode)
|
|
2837
2943
|
updateContentWithHistory(doc)
|
|
2838
|
-
}
|
|
2839
|
-
// Check if a table was added
|
|
2840
|
-
else if (addedNode.nodeType === Node.ELEMENT_NODE) {
|
|
2944
|
+
} else if (addedNode.nodeType === Node.ELEMENT_NODE) {
|
|
2841
2945
|
const element = addedNode as HTMLElement
|
|
2842
|
-
|
|
2946
|
+
const tables = element.tagName === 'TABLE'
|
|
2947
|
+
? [element as HTMLTableElement]
|
|
2948
|
+
: Array.from(element.querySelectorAll<HTMLTableElement>('table'))
|
|
2949
|
+
if (tables.length > 0) {
|
|
2843
2950
|
setTimeout(() => {
|
|
2844
|
-
|
|
2951
|
+
tables.forEach(t => applyDefaultTableSettings(t))
|
|
2845
2952
|
setupTableEditButtons(doc)
|
|
2846
|
-
},
|
|
2953
|
+
}, 20)
|
|
2847
2954
|
}
|
|
2848
|
-
|
|
2849
|
-
else if (['BLOCKQUOTE', 'UL', 'OL'].includes(element.tagName)) {
|
|
2955
|
+
if (['BLOCKQUOTE', 'UL', 'OL'].includes(element.tagName)) {
|
|
2850
2956
|
if (!element.dir) {
|
|
2851
2957
|
element.dir = doc.body.dir || 'ltr'
|
|
2852
2958
|
}
|
|
2853
|
-
|
|
2854
|
-
// Also apply direction to list items if it's a list
|
|
2855
2959
|
if (element.tagName === 'UL' || element.tagName === 'OL') {
|
|
2856
|
-
|
|
2857
|
-
listItems.forEach((li) => {
|
|
2960
|
+
element.querySelectorAll('li').forEach((li) => {
|
|
2858
2961
|
if (!li.dir) {
|
|
2859
2962
|
li.dir = element.dir
|
|
2860
2963
|
}
|
|
@@ -2867,17 +2970,11 @@ function setupAutoWrapping(doc: Document) {
|
|
|
2867
2970
|
}
|
|
2868
2971
|
})
|
|
2869
2972
|
|
|
2870
|
-
|
|
2871
|
-
observer.observe(doc.body, {
|
|
2973
|
+
bodyMutationObserver.observe(doc.body, {
|
|
2872
2974
|
childList: true,
|
|
2873
|
-
subtree:
|
|
2975
|
+
subtree: true
|
|
2874
2976
|
})
|
|
2875
2977
|
|
|
2876
|
-
// Store observer for cleanup
|
|
2877
|
-
if (!doc.body.dataset.observers) {
|
|
2878
|
-
doc.body.dataset.observers = 'mutation'
|
|
2879
|
-
}
|
|
2880
|
-
|
|
2881
2978
|
// Handle clicks on links to open link modal
|
|
2882
2979
|
doc.addEventListener('click', (e) => {
|
|
2883
2980
|
const target = e.target as HTMLElement
|
|
@@ -3052,7 +3149,7 @@ async function initEditor() {
|
|
|
3052
3149
|
}
|
|
3053
3150
|
body {
|
|
3054
3151
|
margin: 0;
|
|
3055
|
-
padding: 8px;
|
|
3152
|
+
padding: 14px 8px 8px 8px;
|
|
3056
3153
|
min-height: 200px;
|
|
3057
3154
|
font-family: ${currentFontFamily.value};
|
|
3058
3155
|
line-height: 1.5;
|
|
@@ -3140,6 +3237,116 @@ async function initEditor() {
|
|
|
3140
3237
|
.table-edit-btn:active {
|
|
3141
3238
|
background: #1f1f1f;
|
|
3142
3239
|
}
|
|
3240
|
+
|
|
3241
|
+
/* Native inline column add (+) and draggable column thumb (⋯) handles */
|
|
3242
|
+
.table-inline-controls {
|
|
3243
|
+
position: absolute;
|
|
3244
|
+
inset: 0;
|
|
3245
|
+
pointer-events: none;
|
|
3246
|
+
user-select: none;
|
|
3247
|
+
z-index: 95;
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
.table-col-add-btn,
|
|
3251
|
+
.table-col-thumb {
|
|
3252
|
+
position: absolute;
|
|
3253
|
+
display: flex;
|
|
3254
|
+
align-items: center;
|
|
3255
|
+
justify-content: center;
|
|
3256
|
+
font-family: sans-serif;
|
|
3257
|
+
font-weight: 600;
|
|
3258
|
+
line-height: 1;
|
|
3259
|
+
pointer-events: auto;
|
|
3260
|
+
user-select: none;
|
|
3261
|
+
opacity: 0;
|
|
3262
|
+
visibility: hidden;
|
|
3263
|
+
transform: translate(-50%, -50%) scale(0.9);
|
|
3264
|
+
transition: opacity 0.14s ease, transform 0.14s ease, background-color 0.14s ease, border-color 0.14s ease, visibility 0.14s ease;
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
.table-col-add-btn {
|
|
3268
|
+
width: 20px;
|
|
3269
|
+
height: 20px;
|
|
3270
|
+
border-radius: 999px;
|
|
3271
|
+
background: #007bff;
|
|
3272
|
+
color: #fff !important;
|
|
3273
|
+
border: 2px solid #fff;
|
|
3274
|
+
font-size: 14px;
|
|
3275
|
+
cursor: pointer;
|
|
3276
|
+
box-shadow: 0 2px 6px rgba(0, 123, 255, 0.35);
|
|
3277
|
+
z-index: 98;
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
.table-col-add-btn:hover {
|
|
3281
|
+
background: #0056b3;
|
|
3282
|
+
transform: translate(-50%, -50%) scale(1.08) !important;
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
.table-col-add-line {
|
|
3286
|
+
position: absolute;
|
|
3287
|
+
width: 2px;
|
|
3288
|
+
background: #007bff;
|
|
3289
|
+
pointer-events: none;
|
|
3290
|
+
opacity: 0;
|
|
3291
|
+
visibility: hidden;
|
|
3292
|
+
transform: translateX(-50%);
|
|
3293
|
+
transition: opacity 0.14s ease, visibility 0.14s ease;
|
|
3294
|
+
z-index: 94;
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
.table-col-thumb {
|
|
3298
|
+
width: 28px;
|
|
3299
|
+
height: 14px;
|
|
3300
|
+
border-radius: 999px;
|
|
3301
|
+
background: #f3f4f6;
|
|
3302
|
+
color: #4b5563 !important;
|
|
3303
|
+
border: 1px solid #d1d5db;
|
|
3304
|
+
font-size: 13px;
|
|
3305
|
+
letter-spacing: -1px;
|
|
3306
|
+
cursor: grab;
|
|
3307
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
|
|
3308
|
+
z-index: 96;
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
.table-col-thumb:hover {
|
|
3312
|
+
background: #007bff;
|
|
3313
|
+
color: #fff !important;
|
|
3314
|
+
border-color: #007bff;
|
|
3315
|
+
transform: translate(-50%, -50%) scale(1.05) !important;
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
.table-col-thumb:active,
|
|
3319
|
+
.table-inline-controls.is-dragging-col .table-col-thumb {
|
|
3320
|
+
cursor: grabbing;
|
|
3321
|
+
background: #0056b3;
|
|
3322
|
+
color: #fff !important;
|
|
3323
|
+
border-color: #0056b3;
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
.table-wrapper:hover .table-inline-controls.is-visible.show-col-add .table-col-add-btn {
|
|
3327
|
+
opacity: 1;
|
|
3328
|
+
visibility: visible;
|
|
3329
|
+
transform: translate(-50%, -50%) scale(1);
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
.table-wrapper:hover .table-inline-controls.is-visible.show-col-add .table-col-add-line,
|
|
3333
|
+
.table-inline-controls.is-dragging-col .table-col-add-line {
|
|
3334
|
+
opacity: 0.45;
|
|
3335
|
+
visibility: visible;
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
.table-wrapper:hover .table-inline-controls.is-visible.show-col-add:has(.table-col-add-btn:hover) .table-col-add-line,
|
|
3339
|
+
.table-inline-controls.is-dragging-col .table-col-add-line {
|
|
3340
|
+
opacity: 0.9;
|
|
3341
|
+
visibility: visible;
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
.table-wrapper:hover .table-inline-controls.is-visible.show-col-thumb .table-col-thumb,
|
|
3345
|
+
.table-inline-controls.is-dragging-col .table-col-thumb {
|
|
3346
|
+
opacity: 1;
|
|
3347
|
+
visibility: visible;
|
|
3348
|
+
transform: translate(-50%, -50%) scale(1);
|
|
3349
|
+
}
|
|
3143
3350
|
.richtext-editor-content blockquote {
|
|
3144
3351
|
padding: 8px !important;
|
|
3145
3352
|
background: #f4f4f4 !important;
|
|
@@ -3257,50 +3464,52 @@ async function initEditor() {
|
|
|
3257
3464
|
opacity: 1 !important;
|
|
3258
3465
|
background: #007bff !important;
|
|
3259
3466
|
}
|
|
3467
|
+
|
|
3468
|
+
.table-cell-selected {
|
|
3469
|
+
background-color: rgba(0, 123, 255, 0.08) !important;
|
|
3470
|
+
outline: 2px solid #007bff !important;
|
|
3471
|
+
outline-offset: -2px !important;
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
.placeholder {
|
|
3475
|
+
color: #9ca3af;
|
|
3476
|
+
font-style: italic;
|
|
3477
|
+
pointer-events: none;
|
|
3478
|
+
user-select: none;
|
|
3479
|
+
}
|
|
3260
3480
|
` // Create a complete HTML document with proper doctype and meta tags
|
|
3261
|
-
const initialContent = normalizedModelValue.value || (props.placeholder ? `<p class="placeholder">${props.placeholder}</p>` : '')
|
|
3262
3481
|
const htmlContent = `
|
|
3263
3482
|
<!DOCTYPE html>
|
|
3264
3483
|
<html>
|
|
3265
3484
|
<head>
|
|
3266
3485
|
<meta charset="UTF-8">
|
|
3267
3486
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3268
|
-
<meta http-equiv="Content-Security-Policy" content="
|
|
3269
|
-
default-src * data: blob: 'unsafe-inline' 'unsafe-eval';
|
|
3270
|
-
img-src * data: blob:;
|
|
3271
|
-
style-src * 'unsafe-inline';
|
|
3272
|
-
script-src * 'unsafe-inline' 'unsafe-eval';
|
|
3273
|
-
frame-src * data: blob:;
|
|
3274
|
-
connect-src *;
|
|
3275
|
-
media-src *;
|
|
3276
|
-
">
|
|
3277
3487
|
<base target="_blank">
|
|
3278
3488
|
<style id="editor-styles">
|
|
3279
|
-
|
|
3280
3489
|
${editorStylesContent}
|
|
3281
|
-
.placeholder {
|
|
3282
|
-
color: #9ca3af;
|
|
3283
|
-
font-style: italic;
|
|
3284
|
-
pointer-events: none;
|
|
3285
|
-
user-select: none;
|
|
3286
|
-
}
|
|
3287
|
-
|
|
3288
3490
|
</style>
|
|
3289
3491
|
</head>
|
|
3290
|
-
<body class="richtext-editor-content"
|
|
3492
|
+
<body class="richtext-editor-content"></body>
|
|
3291
3493
|
</html>
|
|
3292
3494
|
`
|
|
3293
3495
|
|
|
3294
|
-
// Write the
|
|
3496
|
+
// Write the HTML shell to the iframe, then populate body via DOM
|
|
3295
3497
|
const doc = iframe.value.contentDocument || iframe.value.contentWindow?.document
|
|
3296
3498
|
if (!doc) { return }
|
|
3297
3499
|
|
|
3298
|
-
// First write the content
|
|
3299
3500
|
doc.open()
|
|
3300
|
-
// ! @deprecated — MDN Reference[https://developer.mozilla.org/en-US/docs/Web/API/Document/write]
|
|
3301
3501
|
doc.write(htmlContent)
|
|
3302
3502
|
doc.close()
|
|
3303
3503
|
|
|
3504
|
+
if (normalizedModelValue.value) {
|
|
3505
|
+
doc.body.innerHTML = normalizedModelValue.value
|
|
3506
|
+
} else if (props.placeholder) {
|
|
3507
|
+
const placeholderEl = doc.createElement('p')
|
|
3508
|
+
placeholderEl.className = 'placeholder'
|
|
3509
|
+
placeholderEl.textContent = props.placeholder
|
|
3510
|
+
doc.body.appendChild(placeholderEl)
|
|
3511
|
+
}
|
|
3512
|
+
|
|
3304
3513
|
// Then make it editable after a short delay
|
|
3305
3514
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
3306
3515
|
|
|
@@ -3311,16 +3520,12 @@ async function initEditor() {
|
|
|
3311
3520
|
doc.body.dir = hasRTL.value ? 'rtl' : 'ltr'
|
|
3312
3521
|
doc.body.style.direction = doc.body.dir
|
|
3313
3522
|
|
|
3314
|
-
// Resolve bidirectional text independently for each block. Using the
|
|
3315
|
-
// first strong character keeps English fragments stable inside Hebrew
|
|
3316
|
-
// text (and vice versa) without forcing one direction on the whole document.
|
|
3317
3523
|
doc.body.querySelectorAll('p,h1,h2,h3,h4,h5,h6,blockquote,li').forEach((block) => {
|
|
3318
3524
|
if (!block.classList.contains('placeholder')) {
|
|
3319
3525
|
block.setAttribute('dir', getBlockDirection(block, doc.body.dir as 'ltr' | 'rtl'))
|
|
3320
3526
|
}
|
|
3321
3527
|
})
|
|
3322
3528
|
|
|
3323
|
-
// Apply direction to existing blockquotes and lists
|
|
3324
3529
|
const blockElements = doc.body.querySelectorAll('blockquote, ul, ol')
|
|
3325
3530
|
blockElements.forEach((element) => {
|
|
3326
3531
|
const htmlElement = element as HTMLElement
|
|
@@ -3328,7 +3533,6 @@ async function initEditor() {
|
|
|
3328
3533
|
htmlElement.dir = doc.body.dir || 'ltr'
|
|
3329
3534
|
}
|
|
3330
3535
|
|
|
3331
|
-
// Also apply direction to list items
|
|
3332
3536
|
if (htmlElement.tagName === 'UL' || htmlElement.tagName === 'OL') {
|
|
3333
3537
|
const listItems = htmlElement.querySelectorAll('li')
|
|
3334
3538
|
listItems.forEach((li) => {
|
|
@@ -3340,40 +3544,33 @@ async function initEditor() {
|
|
|
3340
3544
|
}
|
|
3341
3545
|
})
|
|
3342
3546
|
|
|
3547
|
+
// Auto clear format for basic mode (convert top-level blocks to plain <p> without nesting <p> inside <p>)
|
|
3548
|
+
if (props.basic && normalizedModelValue.value) {
|
|
3549
|
+
const lines = Array.from(doc.body.childNodes)
|
|
3550
|
+
.map(node => (node.textContent || '').trim())
|
|
3551
|
+
.filter(Boolean)
|
|
3552
|
+
doc.body.innerHTML = ''
|
|
3553
|
+
if (lines.length === 0) {
|
|
3554
|
+
const p = doc.createElement('p')
|
|
3555
|
+
p.dir = doc.body.dir || 'ltr'
|
|
3556
|
+
p.innerHTML = '<br>'
|
|
3557
|
+
doc.body.appendChild(p)
|
|
3558
|
+
} else {
|
|
3559
|
+
lines.forEach((line) => {
|
|
3560
|
+
const p = doc.createElement('p')
|
|
3561
|
+
p.dir = getTextDirection(line, (doc.body.dir || 'ltr') as 'ltr' | 'rtl')
|
|
3562
|
+
p.textContent = line
|
|
3563
|
+
doc.body.appendChild(p)
|
|
3564
|
+
})
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3343
3568
|
// Ensure editor.state.content is set to the current HTML content
|
|
3344
3569
|
editor.state.content = doc.body.innerHTML
|
|
3345
3570
|
|
|
3346
3571
|
editor.init(doc)
|
|
3347
3572
|
useEditorKeyboard(doc, commands)
|
|
3348
3573
|
|
|
3349
|
-
// Auto clear format for basic mode
|
|
3350
|
-
if (props.basic && normalizedModelValue.value) {
|
|
3351
|
-
setTimeout(() => {
|
|
3352
|
-
// Clear all formatting by removing styles and replacing with plain text
|
|
3353
|
-
const elements = doc.body.querySelectorAll('*')
|
|
3354
|
-
elements.forEach((el) => {
|
|
3355
|
-
if (el.tagName !== 'P' && el.tagName !== 'BR') {
|
|
3356
|
-
const textContent = el.textContent || ''
|
|
3357
|
-
if (textContent.trim()) {
|
|
3358
|
-
const p = doc.createElement('p')
|
|
3359
|
-
p.textContent = textContent
|
|
3360
|
-
el.parentNode?.replaceChild(p, el)
|
|
3361
|
-
} else {
|
|
3362
|
-
el.remove()
|
|
3363
|
-
}
|
|
3364
|
-
} else if (el.tagName === 'P') {
|
|
3365
|
-
// Remove all attributes from paragraphs
|
|
3366
|
-
Array.from(el.attributes).forEach((attr) => {
|
|
3367
|
-
if (attr.name !== 'dir') {
|
|
3368
|
-
el.removeAttribute(attr.name)
|
|
3369
|
-
}
|
|
3370
|
-
})
|
|
3371
|
-
}
|
|
3372
|
-
})
|
|
3373
|
-
updateContentWithHistory(doc)
|
|
3374
|
-
}, 100)
|
|
3375
|
-
}
|
|
3376
|
-
|
|
3377
3574
|
// Add inline toolbar selection listener
|
|
3378
3575
|
doc.addEventListener('selectionchange', () => {
|
|
3379
3576
|
editor.updateState.selection()
|
|
@@ -3386,17 +3583,61 @@ async function initEditor() {
|
|
|
3386
3583
|
setTimeout(() => { showInlineToolbarForSelection() }, 10)
|
|
3387
3584
|
})
|
|
3388
3585
|
|
|
3389
|
-
// Hide inline toolbar
|
|
3586
|
+
// Hide inline toolbar & table context menu when clicking inside the iframe
|
|
3390
3587
|
doc.addEventListener('click', (e) => {
|
|
3588
|
+
const target = e.target as HTMLElement
|
|
3589
|
+
if (target.closest('.table-inline-controls')) {
|
|
3590
|
+
return
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
showTableContextMenu.value = false
|
|
3391
3594
|
setTimeout(() => {
|
|
3392
3595
|
const selection = doc.getSelection()
|
|
3393
3596
|
if (!selection || selection.rangeCount === 0 || selection.getRangeAt(0).collapsed) {
|
|
3394
3597
|
closeInlineToolbar()
|
|
3395
3598
|
}
|
|
3396
3599
|
}, 10)
|
|
3600
|
+
|
|
3601
|
+
const cell = target.closest('td, th') as HTMLTableCellElement | null
|
|
3602
|
+
|
|
3603
|
+
if (cell && cell.closest('table')) {
|
|
3604
|
+
const table = cell.closest('table')!
|
|
3605
|
+
if (e.shiftKey && contextMenuCell.value && contextMenuCell.value.closest('table') === table) {
|
|
3606
|
+
// Rectangular Shift+Click cell selection
|
|
3607
|
+
const map = buildTableGrid(table)
|
|
3608
|
+
const anchorCoords = findCellCoords(map, contextMenuCell.value)
|
|
3609
|
+
const targetCoords = findCellCoords(map, cell)
|
|
3610
|
+
doc.querySelectorAll('.table-cell-selected').forEach(c => c.classList.remove('table-cell-selected'))
|
|
3611
|
+
if (anchorCoords && targetCoords) {
|
|
3612
|
+
const minR = Math.min(anchorCoords.row, targetCoords.row)
|
|
3613
|
+
const maxR = Math.max(anchorCoords.row + anchorCoords.rowspan - 1, targetCoords.row + targetCoords.rowspan - 1)
|
|
3614
|
+
const minC = Math.min(anchorCoords.col, targetCoords.col)
|
|
3615
|
+
const maxC = Math.max(anchorCoords.col + anchorCoords.colspan - 1, targetCoords.col + targetCoords.colspan - 1)
|
|
3616
|
+
for (let r = minR; r <= maxR; r++) {
|
|
3617
|
+
for (let c = minC; c <= maxC; c++) {
|
|
3618
|
+
map.grid[r]?.[c]?.cell.classList.add('table-cell-selected')
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
} else {
|
|
3622
|
+
cell.classList.add('table-cell-selected')
|
|
3623
|
+
contextMenuCell.value = cell
|
|
3624
|
+
}
|
|
3625
|
+
} else {
|
|
3626
|
+
doc.querySelectorAll('.table-cell-selected').forEach(c => c.classList.remove('table-cell-selected'))
|
|
3627
|
+
cell.classList.add('table-cell-selected')
|
|
3628
|
+
contextMenuCell.value = cell
|
|
3629
|
+
}
|
|
3630
|
+
} else {
|
|
3631
|
+
doc.querySelectorAll('.table-cell-selected').forEach(c => c.classList.remove('table-cell-selected'))
|
|
3632
|
+
}
|
|
3397
3633
|
})
|
|
3398
3634
|
|
|
3399
|
-
// Also hide when pressing keyboard arrows or escape
|
|
3635
|
+
// Also hide inline toolbar on scroll or when pressing keyboard arrows or escape
|
|
3636
|
+
doc.addEventListener('scroll', () => {
|
|
3637
|
+
closeInlineToolbar()
|
|
3638
|
+
showTableContextMenu.value = false
|
|
3639
|
+
}, { passive: true })
|
|
3640
|
+
|
|
3400
3641
|
doc.addEventListener('keydown', (e) => {
|
|
3401
3642
|
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Escape'].includes(e.key)) {
|
|
3402
3643
|
setTimeout(() => {
|
|
@@ -3428,7 +3669,7 @@ async function initEditor() {
|
|
|
3428
3669
|
}
|
|
3429
3670
|
})
|
|
3430
3671
|
|
|
3431
|
-
// Add table context menu listener
|
|
3672
|
+
// Add table context menu listener with viewport boundary clamping
|
|
3432
3673
|
doc.addEventListener('contextmenu', (e) => {
|
|
3433
3674
|
const target = e.target as HTMLElement
|
|
3434
3675
|
const cell = target.closest('td, th') as HTMLTableCellElement
|
|
@@ -3437,49 +3678,35 @@ async function initEditor() {
|
|
|
3437
3678
|
e.preventDefault()
|
|
3438
3679
|
contextMenuCell.value = cell
|
|
3439
3680
|
|
|
3440
|
-
// Get iframe position relative to viewport
|
|
3441
3681
|
const iframeRect = iframe.value!.getBoundingClientRect()
|
|
3682
|
+
const menuWidth = 180
|
|
3683
|
+
const menuHeight = 280
|
|
3684
|
+
const rawX = iframeRect.left + e.clientX
|
|
3685
|
+
const rawY = iframeRect.top + e.clientY
|
|
3442
3686
|
contextMenuPosition.value = {
|
|
3443
|
-
x:
|
|
3444
|
-
y:
|
|
3687
|
+
x: Math.max(8, Math.min(window.innerWidth - menuWidth - 8, rawX)),
|
|
3688
|
+
y: Math.max(8, Math.min(window.innerHeight - menuHeight - 8, rawY))
|
|
3445
3689
|
}
|
|
3446
3690
|
showTableContextMenu.value = true
|
|
3447
3691
|
}
|
|
3448
3692
|
})
|
|
3449
3693
|
|
|
3450
|
-
// Add click listener to highlight selected table cell
|
|
3451
|
-
doc.addEventListener('click', (e) => {
|
|
3452
|
-
// Remove previous highlights
|
|
3453
|
-
doc.querySelectorAll('.table-cell-selected').forEach((cell) => {
|
|
3454
|
-
cell.classList.remove('table-cell-selected')
|
|
3455
|
-
})
|
|
3456
|
-
|
|
3457
|
-
const target = e.target as HTMLElement
|
|
3458
|
-
const cell = target.closest('td, th') as HTMLTableCellElement
|
|
3459
|
-
|
|
3460
|
-
if (cell && cell.closest('table')) {
|
|
3461
|
-
// Highlight the selected cell
|
|
3462
|
-
cell.classList.add('table-cell-selected')
|
|
3463
|
-
contextMenuCell.value = cell
|
|
3464
|
-
}
|
|
3465
|
-
})
|
|
3466
|
-
|
|
3467
3694
|
// Setup auto-wrapping for typed content
|
|
3468
3695
|
setupAutoWrapping(doc)
|
|
3469
3696
|
|
|
3470
3697
|
// Setup table edit buttons
|
|
3471
3698
|
setupTableEditButtons(doc)
|
|
3472
3699
|
|
|
3473
|
-
// Update state.content after cleanup
|
|
3474
|
-
updateContentWithHistory(doc)
|
|
3700
|
+
// Update state.content after cleanup (skip pushing initial setup into undoStack)
|
|
3701
|
+
updateContentWithHistory(doc, true)
|
|
3475
3702
|
|
|
3476
3703
|
// If editor is empty, add an initial paragraph
|
|
3477
|
-
if (!doc.body.innerHTML.trim() || !doc.body.querySelector('p,h1,h2,h3,h4,h5,h6,blockquote,ul,ol,table')) {
|
|
3704
|
+
if (!doc.body.innerHTML.trim() || !doc.body.querySelector('p,h1,h2,h3,h4,h5,h6,blockquote,ul,ol,table,figure')) {
|
|
3478
3705
|
const p = doc.createElement('p')
|
|
3479
3706
|
p.dir = doc.body.dir
|
|
3707
|
+
p.innerHTML = '<br>'
|
|
3480
3708
|
doc.body.appendChild(p)
|
|
3481
3709
|
|
|
3482
|
-
// Set cursor in the new paragraph
|
|
3483
3710
|
const range = doc.createRange()
|
|
3484
3711
|
range.selectNodeContents(p)
|
|
3485
3712
|
range.collapse(true)
|
|
@@ -3489,7 +3716,7 @@ async function initEditor() {
|
|
|
3489
3716
|
selection.addRange(range)
|
|
3490
3717
|
}
|
|
3491
3718
|
|
|
3492
|
-
updateContentWithHistory(doc)
|
|
3719
|
+
updateContentWithHistory(doc, true)
|
|
3493
3720
|
}
|
|
3494
3721
|
|
|
3495
3722
|
// Only focus if autofocus is explicitly set to true
|
|
@@ -3555,29 +3782,55 @@ watch([currentInputColor, currentTextColor, currentFontFamily, effectiveContentS
|
|
|
3555
3782
|
}
|
|
3556
3783
|
}, { flush: 'post' })
|
|
3557
3784
|
|
|
3558
|
-
//
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3785
|
+
// Synchronize external modelValue changes cleanly without hiding the iframe or dropping small (<50 char) updates
|
|
3786
|
+
watch(normalizedModelValue, (newValue) => {
|
|
3787
|
+
const doc = editor.state.doc
|
|
3788
|
+
if (!doc?.body) {
|
|
3789
|
+
editor.state.content = newValue
|
|
3790
|
+
return
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
const cleanCurrent = getCleanHTML(doc)
|
|
3794
|
+
if (newValue === cleanCurrent) {
|
|
3795
|
+
return
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
pushHistorySnapshot(editor.state, editor.state.content)
|
|
3799
|
+
if (!newValue.trim()) {
|
|
3800
|
+
if (props.placeholder) {
|
|
3801
|
+
const placeholderEl = doc.createElement('p')
|
|
3802
|
+
placeholderEl.className = 'placeholder'
|
|
3803
|
+
placeholderEl.textContent = props.placeholder
|
|
3804
|
+
doc.body.replaceChildren(placeholderEl)
|
|
3574
3805
|
} else {
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3806
|
+
const direction = getCurrentDirection()
|
|
3807
|
+
doc.body.innerHTML = `<p dir="${direction}"><br></p>`
|
|
3808
|
+
}
|
|
3809
|
+
} else {
|
|
3810
|
+
doc.body.innerHTML = newValue
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
detectAndSetDirection()
|
|
3814
|
+
setupTableEditButtons(doc)
|
|
3815
|
+
editor.state.content = doc.body.innerHTML
|
|
3816
|
+
})
|
|
3817
|
+
|
|
3818
|
+
// Clean HTML model for Split View (CodeEditor) so internal editor chrome is never exposed
|
|
3819
|
+
const splitViewHtml = computed({
|
|
3820
|
+
get: () => {
|
|
3821
|
+
void editor.state.content
|
|
3822
|
+
return editor.state.doc ? getCleanHTML(editor.state.doc) : editor.state.content
|
|
3823
|
+
},
|
|
3824
|
+
set: (val: string) => {
|
|
3825
|
+
const doc = editor.state.doc
|
|
3826
|
+
if (!doc?.body) {
|
|
3827
|
+
editor.state.content = val
|
|
3828
|
+
return
|
|
3580
3829
|
}
|
|
3830
|
+
pushHistorySnapshot(editor.state, editor.state.content)
|
|
3831
|
+
doc.body.innerHTML = val
|
|
3832
|
+
setupTableEditButtons(doc)
|
|
3833
|
+
editor.state.content = doc.body.innerHTML
|
|
3581
3834
|
}
|
|
3582
3835
|
})
|
|
3583
3836
|
|
|
@@ -3589,36 +3842,23 @@ watch(() => editor.state.content, () => {
|
|
|
3589
3842
|
// Watch table alignment changes and update the table immediately
|
|
3590
3843
|
watch(() => tableForm.value.alignment, (newAlignment) => {
|
|
3591
3844
|
if (pendingTableData?.existingTable) {
|
|
3592
|
-
|
|
3593
|
-
if (newAlignment === 'center') {
|
|
3594
|
-
table.style.marginLeft = 'auto'
|
|
3595
|
-
table.style.marginRight = 'auto'
|
|
3596
|
-
} else if (newAlignment === 'right') {
|
|
3597
|
-
table.style.marginLeft = 'auto'
|
|
3598
|
-
table.style.marginRight = '0'
|
|
3599
|
-
} else {
|
|
3600
|
-
table.style.marginLeft = '0'
|
|
3601
|
-
table.style.marginRight = 'auto'
|
|
3602
|
-
}
|
|
3603
|
-
|
|
3604
|
-
// Update content with history
|
|
3845
|
+
applyTableAlignmentToDOM(pendingTableData.existingTable, newAlignment as 'left' | 'center' | 'right')
|
|
3605
3846
|
if (editor.state.doc) {
|
|
3606
3847
|
updateContentWithHistory(editor.state.doc)
|
|
3607
3848
|
}
|
|
3608
3849
|
}
|
|
3609
3850
|
})
|
|
3610
3851
|
|
|
3611
|
-
// Close context menu when clicking outside
|
|
3612
3852
|
function handleGlobalClick() {
|
|
3613
3853
|
showTableContextMenu.value = false
|
|
3614
3854
|
}
|
|
3615
3855
|
|
|
3616
|
-
|
|
3617
|
-
if (typeof window !== 'undefined') {
|
|
3618
|
-
|
|
3619
|
-
}
|
|
3856
|
+
onMounted(() => {
|
|
3857
|
+
if (typeof window !== 'undefined') {
|
|
3858
|
+
window.addEventListener('click', handleGlobalClick)
|
|
3859
|
+
}
|
|
3860
|
+
})
|
|
3620
3861
|
|
|
3621
|
-
// Cleanup event listener
|
|
3622
3862
|
onBeforeUnmount(() => {
|
|
3623
3863
|
if (typeof window !== 'undefined') {
|
|
3624
3864
|
window.removeEventListener('click', handleGlobalClick)
|
|
@@ -3693,14 +3933,13 @@ const imgTransform = computed({
|
|
|
3693
3933
|
>
|
|
3694
3934
|
<iframe
|
|
3695
3935
|
id="rich-text-iframe" ref="iframe" class="editableContent" :class="{ 'editor-ready': hasInitialized }" title="Editor" srcdoc=""
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3936
|
+
sandbox="allow-same-origin allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation"
|
|
3937
|
+
@load="initEditor"
|
|
3938
|
+
/>
|
|
3699
3939
|
</div>
|
|
3700
3940
|
<CodeEditor
|
|
3701
|
-
v-if="editor.state.isSplitView" v-model="
|
|
3941
|
+
v-if="editor.state.isSplitView" v-model="splitViewHtml" language="html" wrap
|
|
3702
3942
|
:height="editor.state.isFullscreen ? 'calc(100vh - 4rem)' : editorHeight"
|
|
3703
|
-
@update:modelValue="editor.updateState.content('html')"
|
|
3704
3943
|
/>
|
|
3705
3944
|
</div>
|
|
3706
3945
|
<div v-if="debug" class="flex pt-05">
|
|
@@ -3748,92 +3987,18 @@ const imgTransform = computed({
|
|
|
3748
3987
|
:class="{ active: editor.state.selectedStyles.has('underline') }"
|
|
3749
3988
|
@click="runInlineAction('underline')"
|
|
3750
3989
|
/>
|
|
3751
|
-
<
|
|
3990
|
+
<Divider vertical size="1.25rem" class="mx-0125" />
|
|
3752
3991
|
<Btn thin flat icon="add_link" @click="runInlineAction('link')" />
|
|
3753
3992
|
</div>
|
|
3754
3993
|
</div>
|
|
3755
|
-
<!-- Table Context Menu -->
|
|
3756
|
-
<
|
|
3757
|
-
v-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
>
|
|
3764
|
-
<div class="context-menu-content" @click.stop>
|
|
3765
|
-
<div class="menu-header">
|
|
3766
|
-
<span>Table Actions</span>
|
|
3767
|
-
<button class="close-btn" @click="showTableContextMenu = false">
|
|
3768
|
-
×
|
|
3769
|
-
</button>
|
|
3770
|
-
</div>
|
|
3771
|
-
<div class="px-025">
|
|
3772
|
-
<Btn
|
|
3773
|
-
full-width align-txt="start" thin icon="settings"
|
|
3774
|
-
class="context-menu-btn context-menu-btn--advanced" value="Advanced Settings"
|
|
3775
|
-
@mousedown.stop @click.stop="openExistingTableAdvancedSettings"
|
|
3776
|
-
/>
|
|
3777
|
-
<div class="context-menu-separator" />
|
|
3778
|
-
<Btn
|
|
3779
|
-
v-if="canMergeRight" full-width align-txt="start" thin flat icon="start"
|
|
3780
|
-
class="context-menu-btn" @click="mergeCellRight(); showTableContextMenu = false"
|
|
3781
|
-
>
|
|
3782
|
-
Merge Right
|
|
3783
|
-
</Btn>
|
|
3784
|
-
<Btn
|
|
3785
|
-
v-if="canMergeDown" full-width align-txt="start" thin flat icon="text_select_move_down"
|
|
3786
|
-
class="context-menu-btn" @click="mergeCellDown(); showTableContextMenu = false"
|
|
3787
|
-
>
|
|
3788
|
-
Merge Down
|
|
3789
|
-
</Btn>
|
|
3790
|
-
<Btn
|
|
3791
|
-
v-if="canSplit" full-width align-txt="start" thin flat icon="call_split"
|
|
3792
|
-
class="context-menu-btn" @click="splitCell(); showTableContextMenu = false"
|
|
3793
|
-
>
|
|
3794
|
-
Split Cell
|
|
3795
|
-
</Btn>
|
|
3796
|
-
<div v-if="canMergeRight || canMergeDown || canSplit" class="context-menu-separator" />
|
|
3797
|
-
<Btn
|
|
3798
|
-
full-width align-txt="start" thin flat icon="add_row_above" class="context-menu-btn"
|
|
3799
|
-
@click="insertRowAbove(); showTableContextMenu = false"
|
|
3800
|
-
>
|
|
3801
|
-
Insert Row Above
|
|
3802
|
-
</Btn>
|
|
3803
|
-
<Btn
|
|
3804
|
-
full-width align-txt="start" thin flat icon="add_row_below" class="context-menu-btn"
|
|
3805
|
-
@click="insertRowBelow(); showTableContextMenu = false"
|
|
3806
|
-
>
|
|
3807
|
-
Insert Row Below
|
|
3808
|
-
</Btn>
|
|
3809
|
-
<Btn
|
|
3810
|
-
full-width align-txt="start" thin flat icon="remove" class="context-menu-btn"
|
|
3811
|
-
@click="deleteRow(); showTableContextMenu = false"
|
|
3812
|
-
>
|
|
3813
|
-
Delete Row
|
|
3814
|
-
</Btn>
|
|
3815
|
-
<div class="context-menu-separator" />
|
|
3816
|
-
<Btn
|
|
3817
|
-
full-width align-txt="start" thin flat icon="add_column_left" class="context-menu-btn"
|
|
3818
|
-
@click="insertColumnLeft(); showTableContextMenu = false"
|
|
3819
|
-
>
|
|
3820
|
-
Insert Column Left
|
|
3821
|
-
</Btn>
|
|
3822
|
-
<Btn
|
|
3823
|
-
full-width align-txt="start" thin flat icon="add_column_right" class="context-menu-btn"
|
|
3824
|
-
@click="insertColumnRight(); showTableContextMenu = false"
|
|
3825
|
-
>
|
|
3826
|
-
Insert Column Right
|
|
3827
|
-
</Btn>
|
|
3828
|
-
<Btn
|
|
3829
|
-
full-width align-txt="start" thin flat icon="remove" class="context-menu-btn"
|
|
3830
|
-
@click="deleteColumn(); showTableContextMenu = false"
|
|
3831
|
-
>
|
|
3832
|
-
Delete Column
|
|
3833
|
-
</Btn>
|
|
3834
|
-
</div>
|
|
3835
|
-
</div>
|
|
3836
|
-
</div>
|
|
3994
|
+
<!-- Table Context Menu (built on shared ContextMenu / Dropdown) -->
|
|
3995
|
+
<ContextMenu
|
|
3996
|
+
v-model:shown="showTableContextMenu"
|
|
3997
|
+
:anchor-point="contextMenuPosition"
|
|
3998
|
+
title="Table Actions"
|
|
3999
|
+
:items="tableContextMenuItems"
|
|
4000
|
+
popup-class="rich-text-light-theme"
|
|
4001
|
+
/>
|
|
3837
4002
|
</div>
|
|
3838
4003
|
<!-- Link Modal -->
|
|
3839
4004
|
<Dialog
|
|
@@ -4045,13 +4210,7 @@ const imgTransform = computed({
|
|
|
4045
4210
|
<template #default>
|
|
4046
4211
|
<div class="grid grid-wrap-4 m_grid-wrap-2 gap-col-1 table-editor testMe1">
|
|
4047
4212
|
<!-- Structure Section -->
|
|
4048
|
-
<
|
|
4049
|
-
<div class="line" />
|
|
4050
|
-
<p class="label grid-span-4 m_grid-span-2">
|
|
4051
|
-
Table Structure
|
|
4052
|
-
</p>
|
|
4053
|
-
<div class="line" />
|
|
4054
|
-
</div>
|
|
4213
|
+
<Divider label="Table Structure" class="grid-span-4 m_grid-span-2 pt-05 pb-025" />
|
|
4055
4214
|
<NumberInput v-model="tableForm.rows" :min="1" :max="20" label="Rows" />
|
|
4056
4215
|
<NumberInput v-model="tableForm.cols" :min="1" :max="10" label="Columns" />
|
|
4057
4216
|
<div class="grid-span-4 m_grid-span-2">
|
|
@@ -4071,52 +4230,31 @@ const imgTransform = computed({
|
|
|
4071
4230
|
</p>
|
|
4072
4231
|
</div>
|
|
4073
4232
|
<!-- Cell Text Alignment -->
|
|
4074
|
-
<
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
:class="{ activeBtn: tableForm.alignment === 'center' }" flat thin
|
|
4086
|
-
icon="format_align_center" title="Align Center" @click="tableForm.alignment = 'center'"
|
|
4087
|
-
/>
|
|
4088
|
-
<Btn
|
|
4089
|
-
:class="{ activeBtn: tableForm.alignment === 'right' }" flat thin icon="format_align_right"
|
|
4090
|
-
title="Align Right" @click="tableForm.alignment = 'right'"
|
|
4091
|
-
/>
|
|
4092
|
-
</div>
|
|
4093
|
-
</div>
|
|
4233
|
+
<SelectBtn
|
|
4234
|
+
v-model="tableForm.alignment"
|
|
4235
|
+
label="Cell Text Alignment"
|
|
4236
|
+
class="grid-span-1"
|
|
4237
|
+
icon-only
|
|
4238
|
+
:options="[
|
|
4239
|
+
{ value: 'left', label: 'Align Left', icon: 'format_align_left' },
|
|
4240
|
+
{ value: 'center', label: 'Align Center', icon: 'format_align_center' },
|
|
4241
|
+
{ value: 'right', label: 'Align Right', icon: 'format_align_right' },
|
|
4242
|
+
]"
|
|
4243
|
+
/>
|
|
4094
4244
|
|
|
4095
4245
|
<!-- Text Direction -->
|
|
4096
|
-
<
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
<Btn
|
|
4107
|
-
:class="{ activeBtn: tableForm.direction === 'rtl' }" flat thin value="RTL"
|
|
4108
|
-
title="Right to Left" @click="tableForm.direction = 'rtl'"
|
|
4109
|
-
/>
|
|
4110
|
-
</div>
|
|
4111
|
-
</div>
|
|
4246
|
+
<SelectBtn
|
|
4247
|
+
v-model="tableForm.direction"
|
|
4248
|
+
label="Text Direction"
|
|
4249
|
+
class="grid-span-1"
|
|
4250
|
+
:options="[
|
|
4251
|
+
{ value: 'ltr', label: 'LTR' },
|
|
4252
|
+
{ value: 'rtl', label: 'RTL' },
|
|
4253
|
+
]"
|
|
4254
|
+
/>
|
|
4255
|
+
|
|
4112
4256
|
<!-- Style Section -->
|
|
4113
|
-
<
|
|
4114
|
-
<div class="line" />
|
|
4115
|
-
<p class="label grid-span-4 m_grid-span-2">
|
|
4116
|
-
Table Style
|
|
4117
|
-
</p>
|
|
4118
|
-
<div class="line" />
|
|
4119
|
-
</div>
|
|
4257
|
+
<Divider label="Table Style" class="grid-span-4 m_grid-span-2 pt-05 pb-025" />
|
|
4120
4258
|
<NumberInput v-model="tableForm.width" label="Width (%)" :min="25" :max="100" />
|
|
4121
4259
|
<NumberInput v-model="tableForm.cellPadding" label="Cell Padding (px)" :min="2" :max="20" />
|
|
4122
4260
|
<NumberInput v-model="tableForm.borderWidth" label="Border Width (px)" :min="0" :max="5" />
|
|
@@ -4127,26 +4265,14 @@ const imgTransform = computed({
|
|
|
4127
4265
|
/>
|
|
4128
4266
|
|
|
4129
4267
|
<!-- Cell Colors -->
|
|
4130
|
-
<
|
|
4131
|
-
<div class="line" />
|
|
4132
|
-
<p class="label grid-span-4 m_grid-span-2">
|
|
4133
|
-
Cell Colors:
|
|
4134
|
-
</p>
|
|
4135
|
-
<div class="line" />
|
|
4136
|
-
</div>
|
|
4268
|
+
<Divider label="Cell Colors" class="grid-span-4 m_grid-span-2 pt-05 pb-025" />
|
|
4137
4269
|
<ColorInput
|
|
4138
4270
|
v-model="tableForm.cellBgColor" class="grid-span-2 m_grid-span-1"
|
|
4139
4271
|
label="Background Color"
|
|
4140
4272
|
/>
|
|
4141
4273
|
<ColorInput v-model="tableForm.cellTextColor" class="grid-span-2 m_grid-span-1" label="Text Color" />
|
|
4142
4274
|
<!-- Alternating Rows -->
|
|
4143
|
-
<
|
|
4144
|
-
<div class="line" />
|
|
4145
|
-
<p class="label grid-span-4 m_grid-span-2">
|
|
4146
|
-
Row Styles:
|
|
4147
|
-
</p>
|
|
4148
|
-
<div class="line" />
|
|
4149
|
-
</div>
|
|
4275
|
+
<Divider label="Row Styles" class="grid-span-4 m_grid-span-2 pt-05 pb-025" />
|
|
4150
4276
|
<CheckInput v-model="tableForm.showHeaders" label="Show header row" class="grid-span-4 m_grid-span-2" />
|
|
4151
4277
|
<!-- Header Colors -->
|
|
4152
4278
|
<div
|
|
@@ -4181,14 +4307,63 @@ const imgTransform = computed({
|
|
|
4181
4307
|
</div>
|
|
4182
4308
|
</div>
|
|
4183
4309
|
<!-- Table Preview -->
|
|
4184
|
-
<
|
|
4185
|
-
|
|
4186
|
-
<
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4310
|
+
<Divider label="Preview" class="grid-span-4 m_grid-span-2 pt-05 pb-025" />
|
|
4311
|
+
<div style="zoom: 0.8;" class="opacity-7 user-select-none pointer-events-none">
|
|
4312
|
+
<table
|
|
4313
|
+
:dir="tableForm.direction"
|
|
4314
|
+
:style="{
|
|
4315
|
+
width: `${tableForm.width}%`,
|
|
4316
|
+
borderCollapse: 'collapse',
|
|
4317
|
+
marginBottom: '1rem',
|
|
4318
|
+
border: `${tableForm.borderWidth}px solid ${tableForm.borderColor}`,
|
|
4319
|
+
tableLayout: tableForm.fixedLayout ? 'fixed' : 'auto',
|
|
4320
|
+
}"
|
|
4321
|
+
>
|
|
4322
|
+
<colgroup>
|
|
4323
|
+
<col
|
|
4324
|
+
v-for="(colWidth, colIdx) in tableForm.columnWidths.slice(0, tableForm.cols)"
|
|
4325
|
+
:key="`col-${colIdx}`"
|
|
4326
|
+
:style="{ width: `${colWidth}%` }"
|
|
4327
|
+
>
|
|
4328
|
+
</colgroup>
|
|
4329
|
+
<thead v-if="tableForm.showHeaders">
|
|
4330
|
+
<tr>
|
|
4331
|
+
<th
|
|
4332
|
+
v-for="colIdx in tableForm.cols"
|
|
4333
|
+
:key="`th-${colIdx}`"
|
|
4334
|
+
:style="{
|
|
4335
|
+
padding: `${tableForm.cellPadding}px`,
|
|
4336
|
+
border: `${tableForm.borderWidth}px solid ${tableForm.borderColor}`,
|
|
4337
|
+
backgroundColor: tableForm.headerBgColor,
|
|
4338
|
+
color: tableForm.headerTextColor,
|
|
4339
|
+
textAlign: tableForm.alignment as any,
|
|
4340
|
+
width: tableForm.fixedLayout ? `${tableForm.columnWidths[colIdx - 1] ?? (100 / tableForm.cols)}%` : undefined,
|
|
4341
|
+
}"
|
|
4342
|
+
>
|
|
4343
|
+
Header {{ colIdx }}
|
|
4344
|
+
</th>
|
|
4345
|
+
</tr>
|
|
4346
|
+
</thead>
|
|
4347
|
+
<tbody>
|
|
4348
|
+
<tr v-for="rowIdx in tableForm.rows" :key="`tr-${rowIdx}`">
|
|
4349
|
+
<td
|
|
4350
|
+
v-for="colIdx in tableForm.cols"
|
|
4351
|
+
:key="`td-${rowIdx}-${colIdx}`"
|
|
4352
|
+
:style="{
|
|
4353
|
+
padding: `${tableForm.cellPadding}px`,
|
|
4354
|
+
border: `${tableForm.borderWidth}px solid ${tableForm.borderColor}`,
|
|
4355
|
+
textAlign: tableForm.alignment as any,
|
|
4356
|
+
width: tableForm.fixedLayout ? `${tableForm.columnWidths[colIdx - 1] ?? (100 / tableForm.cols)}%` : undefined,
|
|
4357
|
+
backgroundColor: tableForm.alternateRows && (rowIdx - 1) % 2 === 1 ? tableForm.alternateRowBgColor : tableForm.cellBgColor,
|
|
4358
|
+
color: tableForm.alternateRows && (rowIdx - 1) % 2 === 1 ? tableForm.alternateRowTextColor : tableForm.cellTextColor,
|
|
4359
|
+
}"
|
|
4360
|
+
>
|
|
4361
|
+
Cell {{ rowIdx }},{{ colIdx }}
|
|
4362
|
+
</td>
|
|
4363
|
+
</tr>
|
|
4364
|
+
</tbody>
|
|
4365
|
+
</table>
|
|
4190
4366
|
</div>
|
|
4191
|
-
<div style="zoom: 0.8;" class="opacity-7 user-select-none pointer-events-none" v-html="tablePreviewHtml" />
|
|
4192
4367
|
</template>
|
|
4193
4368
|
|
|
4194
4369
|
<template #footer>
|
|
@@ -4460,196 +4635,6 @@ background: var(--bgl-primary);
|
|
|
4460
4635
|
color: white;
|
|
4461
4636
|
}
|
|
4462
4637
|
|
|
4463
|
-
.inline-toolbar-content .separator {
|
|
4464
|
-
color: var(--bgl-border-color, #dddddd);
|
|
4465
|
-
margin: 0 0.25rem;
|
|
4466
|
-
opacity: 0.5;
|
|
4467
|
-
}
|
|
4468
|
-
|
|
4469
|
-
/* Table Context Menu */
|
|
4470
|
-
.table-context-menu {
|
|
4471
|
-
pointer-events: auto;
|
|
4472
|
-
}
|
|
4473
|
-
|
|
4474
|
-
.context-menu-content {
|
|
4475
|
-
background: var(--bgl-popup-bg, white);
|
|
4476
|
-
color: var(--bgl-popup-text);
|
|
4477
|
-
border: 1px solid var(--bgl-border-color, #dddddd);
|
|
4478
|
-
border-radius: var(--bgl-btn-border-radius) !important;
|
|
4479
|
-
box-shadow: 0 2px 8px var(--bgl-shadow, rgba(0, 0, 0, 0.15));
|
|
4480
|
-
padding: 0;
|
|
4481
|
-
min-width: 140px;
|
|
4482
|
-
max-width: 180px;
|
|
4483
|
-
overflow: hidden;
|
|
4484
|
-
--bgl-input-font-size: 12px;
|
|
4485
|
-
}
|
|
4486
|
-
|
|
4487
|
-
.menu-header {
|
|
4488
|
-
display: flex;
|
|
4489
|
-
justify-content: space-between;
|
|
4490
|
-
align-items: center;
|
|
4491
|
-
padding: 8px 12px;
|
|
4492
|
-
background-color: var(--bg-light, #f8f9fa);
|
|
4493
|
-
border-bottom: 1px solid var(--bgl-border-color, #dddddd);
|
|
4494
|
-
font-size: 12px;
|
|
4495
|
-
font-weight: 500;
|
|
4496
|
-
color: var(--text-secondary, #666);
|
|
4497
|
-
margin-bottom: 5px;
|
|
4498
|
-
}
|
|
4499
|
-
|
|
4500
|
-
.close-btn {
|
|
4501
|
-
background: none;
|
|
4502
|
-
border: none;
|
|
4503
|
-
font-size: 16px;
|
|
4504
|
-
cursor: pointer;
|
|
4505
|
-
padding: 0;
|
|
4506
|
-
width: 20px;
|
|
4507
|
-
height: 20px;
|
|
4508
|
-
display: flex;
|
|
4509
|
-
align-items: center;
|
|
4510
|
-
justify-content: center;
|
|
4511
|
-
border-radius: 3px;
|
|
4512
|
-
color: var(--text-secondary, #666);
|
|
4513
|
-
transition: all 0.2s;
|
|
4514
|
-
}
|
|
4515
|
-
|
|
4516
|
-
.close-btn:hover {
|
|
4517
|
-
background-color: var(--hover-bg, #e9ecef);
|
|
4518
|
-
color: var(--text-primary, #333);
|
|
4519
|
-
}
|
|
4520
|
-
|
|
4521
|
-
.context-menu-content>.btn {
|
|
4522
|
-
margin: 2px 4px;
|
|
4523
|
-
}
|
|
4524
|
-
|
|
4525
|
-
.context-menu-item {
|
|
4526
|
-
padding: 8px 16px;
|
|
4527
|
-
cursor: pointer;
|
|
4528
|
-
transition: background-color 0.2s;
|
|
4529
|
-
font-size: 14px;
|
|
4530
|
-
}
|
|
4531
|
-
|
|
4532
|
-
.context-menu-item:hover {
|
|
4533
|
-
background-color: var(--hover-bg, #f5f5f5);
|
|
4534
|
-
}
|
|
4535
|
-
|
|
4536
|
-
.context-menu-separator {
|
|
4537
|
-
height: 1px;
|
|
4538
|
-
background-color: var(--bgl-border-color, #dddddd);
|
|
4539
|
-
margin: 4px 0;
|
|
4540
|
-
}
|
|
4541
|
-
|
|
4542
|
-
.context-menu-btn:hover {
|
|
4543
|
-
background-color: var(--hover-bg, #f5f5f5) !important;
|
|
4544
|
-
}
|
|
4545
|
-
|
|
4546
|
-
.context-menu-btn--advanced,
|
|
4547
|
-
.context-menu-btn--advanced:hover,
|
|
4548
|
-
.context-menu-btn--advanced:focus-visible {
|
|
4549
|
-
background: #000 !important;
|
|
4550
|
-
border: 1px solid #fff !important;
|
|
4551
|
-
color: #fff !important;
|
|
4552
|
-
}
|
|
4553
|
-
|
|
4554
|
-
.context-menu-btn--advanced:hover,
|
|
4555
|
-
.context-menu-btn--advanced:focus-visible {
|
|
4556
|
-
background: #1f1f1f !important;
|
|
4557
|
-
}
|
|
4558
|
-
|
|
4559
|
-
/* Table Cell Selection */
|
|
4560
|
-
.table-cell-selected {
|
|
4561
|
-
background-color: rgba(0, 123, 255, 0.1) !important;
|
|
4562
|
-
border: 2px solid #007bff !important;
|
|
4563
|
-
position: relative;
|
|
4564
|
-
}
|
|
4565
|
-
|
|
4566
|
-
.table-cell-selected::after {
|
|
4567
|
-
content: '';
|
|
4568
|
-
position: absolute;
|
|
4569
|
-
top: -2px;
|
|
4570
|
-
left: -2px;
|
|
4571
|
-
right: -2px;
|
|
4572
|
-
bottom: -2px;
|
|
4573
|
-
border: 2px solid #007bff;
|
|
4574
|
-
pointer-events: none;
|
|
4575
|
-
z-index: 1;
|
|
4576
|
-
}
|
|
4577
|
-
|
|
4578
|
-
/* Table Modal Button Active States */
|
|
4579
|
-
.activeBtn {
|
|
4580
|
-
background: var(--bgl-primary) !important;
|
|
4581
|
-
color: white !important;
|
|
4582
|
-
border-color: var(--bgl-primary) !important;
|
|
4583
|
-
}
|
|
4584
|
-
|
|
4585
|
-
/* Table Edit Button Protection */
|
|
4586
|
-
.table-edit-btn {
|
|
4587
|
-
position: absolute !important;
|
|
4588
|
-
top: 5px !important;
|
|
4589
|
-
right: 5px !important;
|
|
4590
|
-
background: rgba(0, 0, 0, 0.7) !important;
|
|
4591
|
-
color: white !important;
|
|
4592
|
-
-webkit-text-fill-color: white !important;
|
|
4593
|
-
padding: 0 !important;
|
|
4594
|
-
border: 1px solid white !important;
|
|
4595
|
-
border-radius: 4px !important;
|
|
4596
|
-
font-size: 12px !important;
|
|
4597
|
-
cursor: pointer !important;
|
|
4598
|
-
opacity: 0 !important;
|
|
4599
|
-
visibility: hidden !important;
|
|
4600
|
-
z-index: 100 !important;
|
|
4601
|
-
pointer-events: auto !important;
|
|
4602
|
-
user-select: none !important;
|
|
4603
|
-
-webkit-user-select: none !important;
|
|
4604
|
-
-moz-user-select: none !important;
|
|
4605
|
-
-ms-user-select: none !important;
|
|
4606
|
-
outline: none !important;
|
|
4607
|
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2) !important;
|
|
4608
|
-
white-space: nowrap !important;
|
|
4609
|
-
overflow: hidden !important;
|
|
4610
|
-
text-overflow: clip !important;
|
|
4611
|
-
min-width: unset !important;
|
|
4612
|
-
max-width: unset !important;
|
|
4613
|
-
width: 28px !important;
|
|
4614
|
-
height: 28px !important;
|
|
4615
|
-
line-height: 26px !important;
|
|
4616
|
-
font-family: inherit !important;
|
|
4617
|
-
font-weight: normal !important;
|
|
4618
|
-
text-align: center !important;
|
|
4619
|
-
vertical-align: baseline !important;
|
|
4620
|
-
text-decoration: none !important;
|
|
4621
|
-
text-transform: none !important;
|
|
4622
|
-
letter-spacing: normal !important;
|
|
4623
|
-
word-spacing: normal !important;
|
|
4624
|
-
text-indent: 0 !important;
|
|
4625
|
-
text-shadow: none !important;
|
|
4626
|
-
direction: ltr !important;
|
|
4627
|
-
unicode-bidi: normal !important;
|
|
4628
|
-
transition: opacity 0.15s ease, visibility 0.15s ease, background 0.15s ease !important;
|
|
4629
|
-
}
|
|
4630
|
-
|
|
4631
|
-
.table-wrapper:hover > .table-edit-btn,
|
|
4632
|
-
.table-wrapper:focus-within > .table-edit-btn,
|
|
4633
|
-
.table-edit-btn:hover,
|
|
4634
|
-
.table-edit-btn:focus {
|
|
4635
|
-
opacity: 1 !important;
|
|
4636
|
-
visibility: visible !important;
|
|
4637
|
-
}
|
|
4638
|
-
|
|
4639
|
-
.table-edit-btn:hover {
|
|
4640
|
-
background: rgba(0, 0, 0, 0.9) !important;
|
|
4641
|
-
}
|
|
4642
|
-
|
|
4643
|
-
/* Prevent any content insertion into table edit buttons */
|
|
4644
|
-
.table-edit-btn * {
|
|
4645
|
-
display: none !important;
|
|
4646
|
-
}
|
|
4647
|
-
|
|
4648
|
-
.table-edit-btn::before,
|
|
4649
|
-
.table-edit-btn::after {
|
|
4650
|
-
content: none !important;
|
|
4651
|
-
}
|
|
4652
|
-
|
|
4653
4638
|
/* Editor Tooltip */
|
|
4654
4639
|
.editor-tooltip {
|
|
4655
4640
|
position: fixed;
|