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