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