@bagelink/vue 1.4.139 → 1.4.145
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Btn.vue.d.ts.map +1 -1
- package/dist/components/Carousel.vue.d.ts +1 -1
- package/dist/components/Modal.vue.d.ts +3 -0
- package/dist/components/Modal.vue.d.ts.map +1 -1
- package/dist/components/Slider.vue.d.ts +1 -1
- package/dist/components/Slider.vue.d.ts.map +1 -1
- package/dist/components/analytics/BarChart.vue.d.ts +11 -3
- package/dist/components/analytics/BarChart.vue.d.ts.map +1 -1
- package/dist/components/analytics/LineChart.vue.d.ts +9 -0
- package/dist/components/analytics/LineChart.vue.d.ts.map +1 -1
- package/dist/components/analytics/PieChart.vue.d.ts +30 -2
- package/dist/components/analytics/PieChart.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts +8 -0
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/TableGridSelector.vue.d.ts +9 -0
- package/dist/components/form/inputs/RichText/components/TableGridSelector.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +0 -14
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts +15 -15
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts +1 -3
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/media-clean.d.ts +2 -0
- package/dist/components/form/inputs/RichText/utils/media-clean.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/media.d.ts +4 -4
- package/dist/components/form/inputs/RichText/utils/media.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/layout/AppContent.vue.d.ts.map +1 -1
- package/dist/components/layout/AppLayout.vue.d.ts.map +1 -1
- package/dist/components/layout/AppSidebar.vue.d.ts.map +1 -1
- package/dist/index.cjs +123 -22
- package/dist/index.mjs +123 -22
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/Btn.vue +50 -42
- package/src/components/Modal.vue +49 -50
- package/src/components/analytics/BarChart.vue +118 -7
- package/src/components/analytics/KpiCard.vue +2 -2
- package/src/components/analytics/LineChart.vue +189 -105
- package/src/components/analytics/PieChart.vue +392 -49
- package/src/components/dataTable/DataTable.vue +1 -1
- package/src/components/form/inputs/RichText/CheckList.md +23 -0
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +243 -27
- package/src/components/form/inputs/RichText/components/TableGridSelector.vue +94 -0
- package/src/components/form/inputs/RichText/composables/useCommands.ts +45 -0
- package/src/components/form/inputs/RichText/composables/useEditor.ts +13 -10
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +3 -128
- package/src/components/form/inputs/RichText/config.ts +33 -10
- package/src/components/form/inputs/RichText/editor.css +300 -33
- package/src/components/form/inputs/RichText/index.vue +3271 -130
- package/src/components/form/inputs/RichText/richTextTypes.ts +7 -3
- package/src/components/form/inputs/RichText/utils/commands.ts +851 -90
- package/src/components/form/inputs/RichText/utils/formatting.ts +17 -15
- package/src/components/form/inputs/RichText/utils/media-clean.ts +0 -0
- package/src/components/form/inputs/RichText/utils/media.ts +133 -67
- package/src/components/form/inputs/RichText/utils/selection.ts +40 -11
- package/src/components/form/inputs/RichText/utils/table.ts +1 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/AppContent.vue +26 -26
- package/src/components/layout/AppLayout.vue +21 -3
- package/src/components/layout/AppSidebar.vue +5 -2
- package/src/styles/layout.css +267 -0
- package/src/styles/mobilLayout.css +266 -0
- package/src/styles/modal.css +3 -17
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { EditorState } from '../richTextTypes'
|
|
2
|
-
import {
|
|
2
|
+
import { insertLink } from './media'
|
|
3
3
|
import { addRow, deleteRow, mergeCells, splitCell, insertTable, deleteTable, insertColumn, deleteColumn, alignColumn } from './table'
|
|
4
4
|
|
|
5
5
|
export interface Command {
|
|
@@ -30,6 +30,142 @@ function getCurrentSelection(state: EditorState): { selection: Selection, range:
|
|
|
30
30
|
return { selection, range }
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Alternative helper function to apply formatting to word at cursor
|
|
34
|
+
function applyFormattingToWordAtCursor(doc: Document, range: Range, tagName: string): boolean {
|
|
35
|
+
if (!range.collapsed) return false
|
|
36
|
+
|
|
37
|
+
let textNode = range.startContainer
|
|
38
|
+
let offset = range.startOffset
|
|
39
|
+
|
|
40
|
+
// If we're in an element node, try to find a text node
|
|
41
|
+
if (textNode.nodeType !== Node.TEXT_NODE) {
|
|
42
|
+
const element = textNode as Element
|
|
43
|
+
if (element.childNodes.length > 0 && offset < element.childNodes.length) {
|
|
44
|
+
const childNode = element.childNodes[offset]
|
|
45
|
+
if (childNode.nodeType === Node.TEXT_NODE) {
|
|
46
|
+
textNode = childNode
|
|
47
|
+
offset = 0
|
|
48
|
+
} else {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if we're already inside a formatting element
|
|
57
|
+
const parentElement = textNode.parentElement
|
|
58
|
+
const existingFormatElement = parentElement?.closest(tagName.toLowerCase())
|
|
59
|
+
|
|
60
|
+
if (existingFormatElement) {
|
|
61
|
+
// Remove existing formatting
|
|
62
|
+
try {
|
|
63
|
+
const parent = existingFormatElement.parentNode
|
|
64
|
+
if (parent) {
|
|
65
|
+
// Move all children of the format element to its parent
|
|
66
|
+
while (existingFormatElement.firstChild) {
|
|
67
|
+
parent.insertBefore(existingFormatElement.firstChild, existingFormatElement)
|
|
68
|
+
}
|
|
69
|
+
parent.removeChild(existingFormatElement)
|
|
70
|
+
|
|
71
|
+
// Position cursor at the same relative position
|
|
72
|
+
const newRange = doc.createRange()
|
|
73
|
+
newRange.setStart(textNode, offset)
|
|
74
|
+
newRange.collapse(true)
|
|
75
|
+
|
|
76
|
+
const selection = doc.getSelection()
|
|
77
|
+
if (selection) {
|
|
78
|
+
selection.removeAllRanges()
|
|
79
|
+
selection.addRange(newRange)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('Error removing formatting from word:', error)
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const text = textNode.textContent || ''
|
|
91
|
+
if (!text) return false
|
|
92
|
+
|
|
93
|
+
// Find word boundaries - support Hebrew, English, and numbers
|
|
94
|
+
let start = offset
|
|
95
|
+
let end = offset
|
|
96
|
+
|
|
97
|
+
// Regular expression for word characters (Hebrew, English, numbers)
|
|
98
|
+
const wordChar = /[\u0590-\u05FF\u0600-\u06FF\w]/
|
|
99
|
+
|
|
100
|
+
// If we're at the end of a word (cursor right after the last character)
|
|
101
|
+
// move back one position to include that word
|
|
102
|
+
if (offset > 0 && wordChar.test(text[offset - 1]) &&
|
|
103
|
+
(offset >= text.length || !wordChar.test(text[offset]))) {
|
|
104
|
+
start = offset - 1
|
|
105
|
+
end = offset - 1
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Move start backwards to find word beginning
|
|
109
|
+
while (start > 0 && wordChar.test(text[start - 1])) {
|
|
110
|
+
start--
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Move end forwards to find word ending
|
|
114
|
+
while (end < text.length && wordChar.test(text[end])) {
|
|
115
|
+
end++
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// If we found a word, apply formatting
|
|
119
|
+
if (start < end && end > start) {
|
|
120
|
+
try {
|
|
121
|
+
// Split the text node into three parts: before, word, after
|
|
122
|
+
const beforeText = text.substring(0, start)
|
|
123
|
+
const wordText = text.substring(start, end)
|
|
124
|
+
const afterText = text.substring(end)
|
|
125
|
+
|
|
126
|
+
// Create the formatting element
|
|
127
|
+
const formatElement = doc.createElement(tagName)
|
|
128
|
+
formatElement.textContent = wordText
|
|
129
|
+
|
|
130
|
+
// Replace the text node with the new structure
|
|
131
|
+
const parent = textNode.parentNode
|
|
132
|
+
if (parent) {
|
|
133
|
+
// Create text nodes for before and after
|
|
134
|
+
if (beforeText) {
|
|
135
|
+
const beforeNode = doc.createTextNode(beforeText)
|
|
136
|
+
parent.insertBefore(beforeNode, textNode)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
parent.insertBefore(formatElement, textNode)
|
|
140
|
+
|
|
141
|
+
if (afterText) {
|
|
142
|
+
const afterNode = doc.createTextNode(afterText)
|
|
143
|
+
parent.insertBefore(afterNode, textNode)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
parent.removeChild(textNode)
|
|
147
|
+
|
|
148
|
+
// Position cursor after the formatted word
|
|
149
|
+
const newRange = doc.createRange()
|
|
150
|
+
newRange.setStartAfter(formatElement)
|
|
151
|
+
newRange.collapse(true)
|
|
152
|
+
|
|
153
|
+
const selection = doc.getSelection()
|
|
154
|
+
if (selection) {
|
|
155
|
+
selection.removeAllRanges()
|
|
156
|
+
selection.addRange(newRange)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.error('Error applying formatting to word:', error)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return false
|
|
167
|
+
}
|
|
168
|
+
|
|
33
169
|
function updateStateAfterCommand(state: EditorState) {
|
|
34
170
|
if (!state.doc) return
|
|
35
171
|
|
|
@@ -94,81 +230,231 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
94
230
|
})
|
|
95
231
|
}
|
|
96
232
|
|
|
97
|
-
// Basic text formatting commands
|
|
233
|
+
// Basic text formatting commands with toggle functionality
|
|
98
234
|
const textCommands = {
|
|
99
235
|
bold: createCommand('Bold', (state) => {
|
|
100
236
|
const selectionInfo = getCurrentSelection(state)
|
|
101
|
-
if (!selectionInfo
|
|
237
|
+
if (!selectionInfo) return
|
|
102
238
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
239
|
+
let { range, selection } = selectionInfo
|
|
240
|
+
|
|
241
|
+
// If no text is selected, try to apply formatting to word at cursor
|
|
242
|
+
if (range.collapsed) {
|
|
243
|
+
const success = applyFormattingToWordAtCursor(state.doc!, range, 'b')
|
|
244
|
+
if (success) return
|
|
245
|
+
else return
|
|
246
|
+
}
|
|
107
247
|
|
|
248
|
+
// Check if the selection is already bold
|
|
249
|
+
const commonAncestor = range.commonAncestorContainer
|
|
250
|
+
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
|
|
251
|
+
? commonAncestor.parentElement
|
|
252
|
+
: commonAncestor as Element
|
|
253
|
+
|
|
254
|
+
const boldElement = parentElement?.closest('b, strong')
|
|
255
|
+
|
|
256
|
+
if (boldElement) {
|
|
257
|
+
// Remove bold formatting
|
|
108
258
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
259
|
+
const parent = boldElement.parentNode
|
|
260
|
+
while (boldElement.firstChild) {
|
|
261
|
+
parent?.insertBefore(boldElement.firstChild, boldElement)
|
|
262
|
+
}
|
|
263
|
+
parent?.removeChild(boldElement)
|
|
264
|
+
|
|
265
|
+
// Restore selection
|
|
266
|
+
selection.removeAllRanges()
|
|
267
|
+
selection.addRange(range)
|
|
268
|
+
} catch (error) {
|
|
269
|
+
console.error('Error removing bold:', error)
|
|
114
270
|
}
|
|
271
|
+
} else {
|
|
272
|
+
// Apply bold formatting
|
|
273
|
+
try {
|
|
274
|
+
const b = state.doc!.createElement('b')
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
range.surroundContents(b)
|
|
278
|
+
} catch {
|
|
279
|
+
const fragment = range.extractContents()
|
|
280
|
+
b.appendChild(fragment)
|
|
281
|
+
range.insertNode(b)
|
|
282
|
+
}
|
|
115
283
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
284
|
+
range.selectNodeContents(b)
|
|
285
|
+
selection.removeAllRanges()
|
|
286
|
+
selection.addRange(range)
|
|
287
|
+
} catch (error) {
|
|
288
|
+
console.error('Error applying bold:', error)
|
|
289
|
+
}
|
|
121
290
|
}
|
|
291
|
+
}, (state) => {
|
|
292
|
+
// isActive function for bold
|
|
293
|
+
const selectionInfo = getCurrentSelection(state)
|
|
294
|
+
if (!selectionInfo) return false
|
|
295
|
+
|
|
296
|
+
const commonAncestor = selectionInfo.range.commonAncestorContainer
|
|
297
|
+
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
|
|
298
|
+
? commonAncestor.parentElement
|
|
299
|
+
: commonAncestor as Element
|
|
300
|
+
|
|
301
|
+
return !!parentElement?.closest('b, strong')
|
|
122
302
|
}),
|
|
123
303
|
|
|
124
304
|
italic: createCommand('Italic', (state) => {
|
|
125
305
|
const selectionInfo = getCurrentSelection(state)
|
|
126
|
-
if (!selectionInfo
|
|
306
|
+
if (!selectionInfo) return
|
|
127
307
|
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
const span = state.doc!.createElement('span')
|
|
131
|
-
span.style.fontStyle = 'italic'
|
|
308
|
+
let { range, selection } = selectionInfo
|
|
132
309
|
|
|
310
|
+
// If no text is selected, try to apply formatting to word at cursor
|
|
311
|
+
if (range.collapsed) {
|
|
312
|
+
const success = applyFormattingToWordAtCursor(state.doc!, range, 'i')
|
|
313
|
+
if (success) return
|
|
314
|
+
else return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Check if the selection is already italic
|
|
318
|
+
const commonAncestor = range.commonAncestorContainer
|
|
319
|
+
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
|
|
320
|
+
? commonAncestor.parentElement
|
|
321
|
+
: commonAncestor as Element
|
|
322
|
+
|
|
323
|
+
const italicElement = parentElement?.closest('i, em')
|
|
324
|
+
|
|
325
|
+
if (italicElement) {
|
|
326
|
+
// Remove italic formatting
|
|
133
327
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
328
|
+
const parent = italicElement.parentNode
|
|
329
|
+
while (italicElement.firstChild) {
|
|
330
|
+
parent?.insertBefore(italicElement.firstChild, italicElement)
|
|
331
|
+
}
|
|
332
|
+
parent?.removeChild(italicElement)
|
|
333
|
+
|
|
334
|
+
// Restore selection
|
|
335
|
+
selection.removeAllRanges()
|
|
336
|
+
selection.addRange(range)
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.error('Error removing italic:', error)
|
|
139
339
|
}
|
|
340
|
+
} else {
|
|
341
|
+
// Apply italic formatting
|
|
342
|
+
try {
|
|
343
|
+
const i = state.doc!.createElement('i')
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
range.surroundContents(i)
|
|
347
|
+
} catch {
|
|
348
|
+
const fragment = range.extractContents()
|
|
349
|
+
i.appendChild(fragment)
|
|
350
|
+
range.insertNode(i)
|
|
351
|
+
}
|
|
140
352
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
353
|
+
range.selectNodeContents(i)
|
|
354
|
+
selection.removeAllRanges()
|
|
355
|
+
selection.addRange(range)
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error('Error applying italic:', error)
|
|
358
|
+
}
|
|
146
359
|
}
|
|
360
|
+
}, (state) => {
|
|
361
|
+
// isActive function for italic
|
|
362
|
+
const selectionInfo = getCurrentSelection(state)
|
|
363
|
+
if (!selectionInfo) return false
|
|
364
|
+
|
|
365
|
+
const commonAncestor = selectionInfo.range.commonAncestorContainer
|
|
366
|
+
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
|
|
367
|
+
? commonAncestor.parentElement
|
|
368
|
+
: commonAncestor as Element
|
|
369
|
+
|
|
370
|
+
return !!parentElement?.closest('i, em')
|
|
147
371
|
}),
|
|
148
372
|
|
|
149
373
|
underline: createCommand('Underline', (state) => {
|
|
150
374
|
const selectionInfo = getCurrentSelection(state)
|
|
151
|
-
if (!selectionInfo
|
|
375
|
+
if (!selectionInfo) return
|
|
152
376
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
377
|
+
let { range, selection } = selectionInfo
|
|
378
|
+
|
|
379
|
+
// If no text is selected, try to apply formatting to word at cursor
|
|
380
|
+
if (range.collapsed) {
|
|
381
|
+
const success = applyFormattingToWordAtCursor(state.doc!, range, 'u')
|
|
382
|
+
if (success) return
|
|
383
|
+
else return
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Check if the selection is already underlined
|
|
387
|
+
const commonAncestor = range.commonAncestorContainer
|
|
388
|
+
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
|
|
389
|
+
? commonAncestor.parentElement
|
|
390
|
+
: commonAncestor as Element
|
|
157
391
|
|
|
392
|
+
const underlineElement = parentElement?.closest('u')
|
|
393
|
+
|
|
394
|
+
if (underlineElement) {
|
|
395
|
+
// Remove underline formatting
|
|
158
396
|
try {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
397
|
+
const parent = underlineElement.parentNode
|
|
398
|
+
while (underlineElement.firstChild) {
|
|
399
|
+
parent?.insertBefore(underlineElement.firstChild, underlineElement)
|
|
400
|
+
}
|
|
401
|
+
parent?.removeChild(underlineElement)
|
|
402
|
+
|
|
403
|
+
// Restore selection
|
|
404
|
+
selection.removeAllRanges()
|
|
405
|
+
selection.addRange(range)
|
|
406
|
+
} catch (error) {
|
|
407
|
+
console.error('Error removing underline:', error)
|
|
164
408
|
}
|
|
409
|
+
} else {
|
|
410
|
+
// Apply underline formatting
|
|
411
|
+
try {
|
|
412
|
+
const u = state.doc!.createElement('u')
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
range.surroundContents(u)
|
|
416
|
+
} catch {
|
|
417
|
+
const fragment = range.extractContents()
|
|
418
|
+
u.appendChild(fragment)
|
|
419
|
+
range.insertNode(u)
|
|
420
|
+
}
|
|
165
421
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
422
|
+
range.selectNodeContents(u)
|
|
423
|
+
selection.removeAllRanges()
|
|
424
|
+
selection.addRange(range)
|
|
425
|
+
} catch (error) {
|
|
426
|
+
console.error('Error applying underline:', error)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}, (state) => {
|
|
430
|
+
// isActive function for underline
|
|
431
|
+
const selectionInfo = getCurrentSelection(state)
|
|
432
|
+
if (!selectionInfo) return false
|
|
433
|
+
|
|
434
|
+
const commonAncestor = selectionInfo.range.commonAncestorContainer
|
|
435
|
+
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
|
|
436
|
+
? commonAncestor.parentElement
|
|
437
|
+
: commonAncestor as Element
|
|
438
|
+
|
|
439
|
+
return !!parentElement?.closest('u')
|
|
440
|
+
}),
|
|
441
|
+
|
|
442
|
+
link: createCommand('Link', (state) => {
|
|
443
|
+
const openLinkModal = (state as any).openLinkModal
|
|
444
|
+
if (openLinkModal) {
|
|
445
|
+
insertLink(openLinkModal, state)
|
|
171
446
|
}
|
|
447
|
+
}, (state) => {
|
|
448
|
+
// isActive function for link
|
|
449
|
+
const selectionInfo = getCurrentSelection(state)
|
|
450
|
+
if (!selectionInfo) return false
|
|
451
|
+
|
|
452
|
+
const commonAncestor = selectionInfo.range.commonAncestorContainer
|
|
453
|
+
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
|
|
454
|
+
? commonAncestor.parentElement
|
|
455
|
+
: commonAncestor as Element
|
|
456
|
+
|
|
457
|
+
return !!parentElement?.closest('a')
|
|
172
458
|
})
|
|
173
459
|
}
|
|
174
460
|
|
|
@@ -179,10 +465,17 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
179
465
|
const selectionInfo = getCurrentSelection(state)
|
|
180
466
|
if (!selectionInfo) return
|
|
181
467
|
|
|
182
|
-
const { range } = selectionInfo
|
|
468
|
+
const { range, selection } = selectionInfo
|
|
183
469
|
const currentBlock = findBlockElement(range.commonAncestorContainer)
|
|
184
470
|
if (!currentBlock) return
|
|
185
471
|
|
|
472
|
+
// Store the original selection information before modification
|
|
473
|
+
const startContainer = range.startContainer
|
|
474
|
+
const endContainer = range.endContainer
|
|
475
|
+
const startOffset = range.startOffset
|
|
476
|
+
const endOffset = range.endOffset
|
|
477
|
+
const isCollapsed = range.collapsed
|
|
478
|
+
|
|
186
479
|
// Check if we need to toggle off (already in the same heading)
|
|
187
480
|
const isToggleOff = currentBlock.tagName.toLowerCase() === cmd.toLowerCase()
|
|
188
481
|
const newTag = isToggleOff ? 'p' : cmd
|
|
@@ -198,11 +491,105 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
198
491
|
// Replace the old block with the new one
|
|
199
492
|
currentBlock.parentNode?.replaceChild(newBlock, currentBlock)
|
|
200
493
|
|
|
201
|
-
//
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
494
|
+
// Restore selection within the new block
|
|
495
|
+
try {
|
|
496
|
+
const newRange = state.doc!.createRange()
|
|
497
|
+
|
|
498
|
+
// Find corresponding nodes in the new block
|
|
499
|
+
const findCorrespondingNode = (originalNode: Node, originalBlock: Element, newBlock: Element): Node | null => {
|
|
500
|
+
if (originalNode === originalBlock) return newBlock
|
|
501
|
+
|
|
502
|
+
// If it's a text node, find it by traversing the tree
|
|
503
|
+
if (originalNode.nodeType === Node.TEXT_NODE) {
|
|
504
|
+
const walker = state.doc!.createTreeWalker(
|
|
505
|
+
newBlock,
|
|
506
|
+
NodeFilter.SHOW_TEXT,
|
|
507
|
+
null
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
let textNodeIndex = 0
|
|
511
|
+
const originalWalker = state.doc!.createTreeWalker(
|
|
512
|
+
originalBlock,
|
|
513
|
+
NodeFilter.SHOW_TEXT,
|
|
514
|
+
null
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
let currentOriginalNode = originalWalker.nextNode()
|
|
518
|
+
while (currentOriginalNode && currentOriginalNode !== originalNode) {
|
|
519
|
+
textNodeIndex++
|
|
520
|
+
currentOriginalNode = originalWalker.nextNode()
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
let currentNewNode = walker.nextNode()
|
|
524
|
+
let currentIndex = 0
|
|
525
|
+
while (currentNewNode && currentIndex < textNodeIndex) {
|
|
526
|
+
currentIndex++
|
|
527
|
+
currentNewNode = walker.nextNode()
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return currentNewNode
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return newBlock.firstChild
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const newStartContainer = findCorrespondingNode(startContainer, currentBlock, newBlock)
|
|
537
|
+
const newEndContainer = findCorrespondingNode(endContainer, currentBlock, newBlock)
|
|
538
|
+
|
|
539
|
+
if (newStartContainer && newEndContainer) {
|
|
540
|
+
// Set start position
|
|
541
|
+
if (newStartContainer.nodeType === Node.TEXT_NODE) {
|
|
542
|
+
const maxOffset = Math.min(startOffset, newStartContainer.textContent?.length || 0)
|
|
543
|
+
newRange.setStart(newStartContainer, maxOffset)
|
|
544
|
+
} else {
|
|
545
|
+
newRange.setStart(newStartContainer, 0)
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Set end position
|
|
549
|
+
if (!isCollapsed) {
|
|
550
|
+
if (newEndContainer.nodeType === Node.TEXT_NODE) {
|
|
551
|
+
const maxOffset = Math.min(endOffset, newEndContainer.textContent?.length || 0)
|
|
552
|
+
newRange.setEnd(newEndContainer, maxOffset)
|
|
553
|
+
} else {
|
|
554
|
+
newRange.setEnd(newEndContainer, 0)
|
|
555
|
+
}
|
|
556
|
+
} else {
|
|
557
|
+
newRange.collapse(true)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Apply the restored selection
|
|
561
|
+
selection.removeAllRanges()
|
|
562
|
+
selection.addRange(newRange)
|
|
563
|
+
} else {
|
|
564
|
+
// Fallback: select all content in the new block
|
|
565
|
+
newRange.selectNodeContents(newBlock)
|
|
566
|
+
if (isCollapsed) {
|
|
567
|
+
newRange.collapse(true)
|
|
568
|
+
}
|
|
569
|
+
selection.removeAllRanges()
|
|
570
|
+
selection.addRange(newRange)
|
|
571
|
+
}
|
|
572
|
+
} catch (selectionError) {
|
|
573
|
+
console.warn('Error restoring selection after heading change:', selectionError)
|
|
574
|
+
// Fallback: place cursor at start of the new block
|
|
575
|
+
const fallbackRange = state.doc!.createRange()
|
|
576
|
+
fallbackRange.selectNodeContents(newBlock)
|
|
577
|
+
fallbackRange.collapse(true)
|
|
578
|
+
selection.removeAllRanges()
|
|
579
|
+
selection.addRange(fallbackRange)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// If we created a heading (not toggling off), ensure there's a paragraph after it
|
|
583
|
+
if (!isToggleOff) {
|
|
584
|
+
const nextSibling = newBlock.nextElementSibling
|
|
585
|
+
if (!nextSibling) {
|
|
586
|
+
// No element after the heading, create a paragraph
|
|
587
|
+
const p = state.doc!.createElement('p')
|
|
588
|
+
p.dir = newBlock.dir || state.doc!.body.dir
|
|
589
|
+
newBlock.parentNode?.insertBefore(p, newBlock.nextSibling)
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
206
593
|
} catch (error) {
|
|
207
594
|
console.error(`Error applying ${cmd} heading:`, error)
|
|
208
595
|
}
|
|
@@ -318,10 +705,34 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
318
705
|
// Convert unordered list to ordered list
|
|
319
706
|
if (listParent.tagName.toLowerCase() === 'ul') {
|
|
320
707
|
const ol = state.doc!.createElement('ol')
|
|
708
|
+
|
|
709
|
+
// Store current cursor position before conversion
|
|
710
|
+
const currentOffset = range.startOffset
|
|
711
|
+
const currentContainer = range.startContainer
|
|
712
|
+
|
|
321
713
|
while (listParent.firstChild) {
|
|
322
714
|
ol.appendChild(listParent.firstChild)
|
|
323
715
|
}
|
|
324
716
|
listParent.parentNode?.replaceChild(ol, listParent)
|
|
717
|
+
|
|
718
|
+
// Restore cursor position
|
|
719
|
+
try {
|
|
720
|
+
const newRange = state.doc!.createRange()
|
|
721
|
+
newRange.setStart(currentContainer, currentOffset)
|
|
722
|
+
newRange.collapse(true)
|
|
723
|
+
selectionInfo.selection.removeAllRanges()
|
|
724
|
+
selectionInfo.selection.addRange(newRange)
|
|
725
|
+
} catch {
|
|
726
|
+
// Fallback: put cursor at beginning of first list item
|
|
727
|
+
const firstLi = ol.querySelector('li')
|
|
728
|
+
if (firstLi) {
|
|
729
|
+
const newRange = state.doc!.createRange()
|
|
730
|
+
newRange.selectNodeContents(firstLi)
|
|
731
|
+
newRange.collapse(true)
|
|
732
|
+
selectionInfo.selection.removeAllRanges()
|
|
733
|
+
selectionInfo.selection.addRange(newRange)
|
|
734
|
+
}
|
|
735
|
+
}
|
|
325
736
|
return
|
|
326
737
|
}
|
|
327
738
|
}
|
|
@@ -386,10 +797,35 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
386
797
|
// Convert ordered list to unordered list
|
|
387
798
|
if (listParent.tagName.toLowerCase() === 'ol') {
|
|
388
799
|
const ul = state.doc!.createElement('ul')
|
|
800
|
+
|
|
801
|
+
// Store current cursor position before conversion
|
|
802
|
+
const currentOffset = range.startOffset
|
|
803
|
+
const currentContainer = range.startContainer
|
|
804
|
+
|
|
389
805
|
while (listParent.firstChild) {
|
|
390
806
|
ul.appendChild(listParent.firstChild)
|
|
391
807
|
}
|
|
392
808
|
listParent.parentNode?.replaceChild(ul, listParent)
|
|
809
|
+
|
|
810
|
+
// Restore cursor position
|
|
811
|
+
try {
|
|
812
|
+
const newRange = state.doc!.createRange()
|
|
813
|
+
newRange.setStart(currentContainer, currentOffset)
|
|
814
|
+
newRange.collapse(true)
|
|
815
|
+
selectionInfo.selection.removeAllRanges()
|
|
816
|
+
selectionInfo.selection.addRange(newRange)
|
|
817
|
+
} catch (e) {
|
|
818
|
+
// Fallback: put cursor at beginning of first list item
|
|
819
|
+
console.warn('Could not restore cursor position:', e)
|
|
820
|
+
const firstLi = ul.querySelector('li')
|
|
821
|
+
if (firstLi) {
|
|
822
|
+
const newRange = state.doc!.createRange()
|
|
823
|
+
newRange.selectNodeContents(firstLi)
|
|
824
|
+
newRange.collapse(true)
|
|
825
|
+
selectionInfo.selection.removeAllRanges()
|
|
826
|
+
selectionInfo.selection.addRange(newRange)
|
|
827
|
+
}
|
|
828
|
+
}
|
|
393
829
|
return
|
|
394
830
|
}
|
|
395
831
|
}
|
|
@@ -418,78 +854,145 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
418
854
|
})
|
|
419
855
|
}
|
|
420
856
|
|
|
421
|
-
// Clear formatting command -
|
|
857
|
+
// Clear formatting command - enhanced
|
|
422
858
|
const formatCommands = {
|
|
423
859
|
clear: createCommand('Clear Formatting', (state) => {
|
|
424
860
|
const selectionInfo = getCurrentSelection(state)
|
|
425
|
-
if (!selectionInfo
|
|
861
|
+
if (!selectionInfo) return
|
|
862
|
+
|
|
863
|
+
const { range, selection } = selectionInfo
|
|
426
864
|
|
|
427
|
-
const { range } = selectionInfo
|
|
428
865
|
try {
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
866
|
+
// Function to clean text from unwanted characters
|
|
867
|
+
function cleanText(text: string): string {
|
|
868
|
+
return text
|
|
869
|
+
// Remove zero-width characters
|
|
870
|
+
.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
|
871
|
+
// Remove extra whitespace
|
|
872
|
+
.replace(/\s+/g, ' ')
|
|
873
|
+
// Remove invisible characters
|
|
874
|
+
.replace(/[\u00A0]/g, ' ') // non-breaking space to regular space
|
|
875
|
+
.trim()
|
|
876
|
+
}
|
|
433
877
|
|
|
434
878
|
// Function to clean a node recursively
|
|
435
879
|
function cleanNode(node: Node): Node {
|
|
436
880
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
437
|
-
|
|
881
|
+
const cleanedText = cleanText(node.textContent || '')
|
|
882
|
+
return state.doc!.createTextNode(cleanedText)
|
|
438
883
|
}
|
|
439
884
|
|
|
440
885
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
441
886
|
const element = node as Element
|
|
442
887
|
const tagName = element.tagName.toLowerCase()
|
|
443
888
|
|
|
444
|
-
// Preserve structural elements
|
|
445
|
-
const structuralTags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'div', 'table', 'tr', 'td', 'th']
|
|
446
|
-
// Remove formatting elements
|
|
447
|
-
const formattingTags = ['
|
|
889
|
+
// Preserve structural elements, links, and media
|
|
890
|
+
const structuralTags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'div', 'table', 'tr', 'td', 'th', 'thead', 'tbody', 'b', 'i', 'u', 'a', 'img', 'iframe', 'video', 'audio', 'figure', 'figcaption']
|
|
891
|
+
// Remove formatting elements (excluding basic HTML formatting tags)
|
|
892
|
+
const formattingTags = ['strong', 'em', 'span', 'font', 'strike', 'sub', 'sup', 'mark', 'del', 'ins', 'small', 'big']
|
|
448
893
|
|
|
449
894
|
if (formattingTags.includes(tagName)) {
|
|
450
|
-
// For formatting elements, just return the text content
|
|
451
|
-
|
|
895
|
+
// For formatting elements, just return the cleaned text content
|
|
896
|
+
const cleanedText = cleanText(element.textContent || '')
|
|
897
|
+
return state.doc!.createTextNode(cleanedText)
|
|
452
898
|
} else if (structuralTags.includes(tagName)) {
|
|
453
|
-
// For structural elements, keep the element but remove attributes
|
|
899
|
+
// For structural elements, keep the element but remove all styling attributes
|
|
454
900
|
const cleanElement = state.doc!.createElement(tagName)
|
|
455
901
|
|
|
456
|
-
//
|
|
902
|
+
// Only preserve essential attributes
|
|
457
903
|
if (tagName === 'a' && element.hasAttribute('href')) {
|
|
458
904
|
cleanElement.setAttribute('href', element.getAttribute('href') || '')
|
|
459
905
|
if (element.hasAttribute('target')) {
|
|
460
906
|
cleanElement.setAttribute('target', element.getAttribute('target') || '')
|
|
461
907
|
}
|
|
908
|
+
if (element.hasAttribute('rel')) {
|
|
909
|
+
cleanElement.setAttribute('rel', element.getAttribute('rel') || '')
|
|
910
|
+
}
|
|
462
911
|
} else if (tagName === 'img') {
|
|
463
912
|
if (element.hasAttribute('src')) cleanElement.setAttribute('src', element.getAttribute('src') || '')
|
|
464
913
|
if (element.hasAttribute('alt')) cleanElement.setAttribute('alt', element.getAttribute('alt') || '')
|
|
914
|
+
if (element.hasAttribute('width')) cleanElement.setAttribute('width', element.getAttribute('width') || '')
|
|
915
|
+
if (element.hasAttribute('height')) cleanElement.setAttribute('height', element.getAttribute('height') || '')
|
|
916
|
+
} else if (tagName === 'iframe') {
|
|
917
|
+
if (element.hasAttribute('src')) cleanElement.setAttribute('src', element.getAttribute('src') || '')
|
|
918
|
+
if (element.hasAttribute('width')) cleanElement.setAttribute('width', element.getAttribute('width') || '')
|
|
919
|
+
if (element.hasAttribute('height')) cleanElement.setAttribute('height', element.getAttribute('height') || '')
|
|
920
|
+
if (element.hasAttribute('frameborder')) cleanElement.setAttribute('frameborder', element.getAttribute('frameborder') || '')
|
|
921
|
+
if (element.hasAttribute('allowfullscreen')) cleanElement.setAttribute('allowfullscreen', element.getAttribute('allowfullscreen') || '')
|
|
922
|
+
} else if (tagName === 'video' || tagName === 'audio') {
|
|
923
|
+
if (element.hasAttribute('src')) cleanElement.setAttribute('src', element.getAttribute('src') || '')
|
|
924
|
+
if (element.hasAttribute('controls')) cleanElement.setAttribute('controls', element.getAttribute('controls') || '')
|
|
925
|
+
if (element.hasAttribute('width')) cleanElement.setAttribute('width', element.getAttribute('width') || '')
|
|
926
|
+
if (element.hasAttribute('height')) cleanElement.setAttribute('height', element.getAttribute('height') || '')
|
|
465
927
|
}
|
|
466
928
|
|
|
467
929
|
// Clean and append child nodes
|
|
468
930
|
Array.from(element.childNodes).forEach((child) => {
|
|
469
|
-
|
|
931
|
+
const cleanedChild = cleanNode(child)
|
|
932
|
+
if (cleanedChild.textContent?.trim() || cleanedChild.nodeType === Node.ELEMENT_NODE) {
|
|
933
|
+
cleanElement.appendChild(cleanedChild)
|
|
934
|
+
}
|
|
470
935
|
})
|
|
471
936
|
|
|
472
937
|
return cleanElement
|
|
473
938
|
}
|
|
474
939
|
}
|
|
475
940
|
|
|
476
|
-
return
|
|
941
|
+
return state.doc!.createTextNode('')
|
|
477
942
|
}
|
|
478
943
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
cleanedFragment.
|
|
483
|
-
})
|
|
944
|
+
if (range.collapsed) {
|
|
945
|
+
// If no selection, clean the entire document
|
|
946
|
+
const body = state.doc!.body
|
|
947
|
+
const cleanedFragment = state.doc!.createDocumentFragment()
|
|
484
948
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
949
|
+
Array.from(body.childNodes).forEach((node) => {
|
|
950
|
+
const cleanedNode = cleanNode(node)
|
|
951
|
+
if (cleanedNode.textContent?.trim() || cleanedNode.nodeType === Node.ELEMENT_NODE) {
|
|
952
|
+
cleanedFragment.appendChild(cleanedNode)
|
|
953
|
+
}
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
// Clear body and append cleaned content
|
|
957
|
+
body.innerHTML = ''
|
|
958
|
+
body.appendChild(cleanedFragment)
|
|
959
|
+
|
|
960
|
+
// If body is empty, add a paragraph
|
|
961
|
+
if (!body.firstElementChild) {
|
|
962
|
+
const p = state.doc!.createElement('p')
|
|
963
|
+
body.appendChild(p)
|
|
964
|
+
|
|
965
|
+
// Set cursor in the paragraph
|
|
966
|
+
const newRange = state.doc!.createRange()
|
|
967
|
+
newRange.selectNodeContents(p)
|
|
968
|
+
newRange.collapse(true)
|
|
969
|
+
selection.removeAllRanges()
|
|
970
|
+
selection.addRange(newRange)
|
|
971
|
+
}
|
|
972
|
+
} else {
|
|
973
|
+
// If there's a selection, clean only the selected content
|
|
974
|
+
const fragment = range.cloneContents()
|
|
975
|
+
const tempDiv = state.doc!.createElement('div')
|
|
976
|
+
tempDiv.appendChild(fragment)
|
|
977
|
+
|
|
978
|
+
// Clean all nodes in the temp div
|
|
979
|
+
const cleanedFragment = state.doc!.createDocumentFragment()
|
|
980
|
+
Array.from(tempDiv.childNodes).forEach((node) => {
|
|
981
|
+
const cleanedNode = cleanNode(node)
|
|
982
|
+
if (cleanedNode.textContent?.trim() || cleanedNode.nodeType === Node.ELEMENT_NODE) {
|
|
983
|
+
cleanedFragment.appendChild(cleanedNode)
|
|
984
|
+
}
|
|
985
|
+
})
|
|
488
986
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
987
|
+
// Replace the selection with cleaned content
|
|
988
|
+
range.deleteContents()
|
|
989
|
+
range.insertNode(cleanedFragment)
|
|
990
|
+
|
|
991
|
+
// Restore selection
|
|
992
|
+
range.collapse(false)
|
|
993
|
+
selection.removeAllRanges()
|
|
994
|
+
selection.addRange(range)
|
|
995
|
+
}
|
|
493
996
|
} catch (error) {
|
|
494
997
|
console.error('Error clearing formatting:', error)
|
|
495
998
|
}
|
|
@@ -499,8 +1002,67 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
499
1002
|
// Table commands
|
|
500
1003
|
const tableCommands = {
|
|
501
1004
|
insertTable: createCommand('Insert Table', (state, value) => {
|
|
502
|
-
|
|
503
|
-
|
|
1005
|
+
console.log('insertTable command called with value:', value)
|
|
1006
|
+
|
|
1007
|
+
// If we have an openTableEditor function in state, use it for new tables
|
|
1008
|
+
if ((state as any).openTableEditor && !value) {
|
|
1009
|
+
console.log('Opening table editor modal for new table')
|
|
1010
|
+
; (state as any).openTableEditor(null)
|
|
1011
|
+
return
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
if (!value) {
|
|
1015
|
+
// Default fallback
|
|
1016
|
+
const [rows, cols] = [3, 3]
|
|
1017
|
+
insertTable(rows, cols, state)
|
|
1018
|
+
return
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Check if value is HTML string (from our advanced table editor)
|
|
1022
|
+
if (value.includes('<table')) {
|
|
1023
|
+
console.log('Inserting HTML table')
|
|
1024
|
+
// Insert HTML table directly
|
|
1025
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1026
|
+
if (!selectionInfo || !state.doc) {
|
|
1027
|
+
console.error('No selection info or document')
|
|
1028
|
+
return
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const { range } = selectionInfo
|
|
1032
|
+
|
|
1033
|
+
try {
|
|
1034
|
+
// Create a temporary div to parse the HTML
|
|
1035
|
+
const tempDiv = state.doc.createElement('div')
|
|
1036
|
+
tempDiv.innerHTML = value
|
|
1037
|
+
const table = tempDiv.querySelector('table')
|
|
1038
|
+
|
|
1039
|
+
if (table) {
|
|
1040
|
+
console.log('Table parsed successfully, inserting...')
|
|
1041
|
+
// Insert the table at the current selection
|
|
1042
|
+
const insertedTable = table.cloneNode(true) as HTMLTableElement
|
|
1043
|
+
range.insertNode(insertedTable)
|
|
1044
|
+
|
|
1045
|
+
// Move cursor after the table
|
|
1046
|
+
range.setStartAfter(insertedTable)
|
|
1047
|
+
range.collapse(true)
|
|
1048
|
+
|
|
1049
|
+
const selection = state.doc.getSelection()
|
|
1050
|
+
if (selection) {
|
|
1051
|
+
selection.removeAllRanges()
|
|
1052
|
+
selection.addRange(range)
|
|
1053
|
+
}
|
|
1054
|
+
console.log('Table inserted successfully')
|
|
1055
|
+
} else {
|
|
1056
|
+
console.error('Could not parse table from HTML')
|
|
1057
|
+
}
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
console.error('Error inserting HTML table:', error)
|
|
1060
|
+
}
|
|
1061
|
+
} else {
|
|
1062
|
+
// Original grid format (e.g., "3x3")
|
|
1063
|
+
const [rows, cols] = value.split('x').map(Number) || [3, 3]
|
|
1064
|
+
insertTable(rows, cols, state)
|
|
1065
|
+
}
|
|
504
1066
|
}),
|
|
505
1067
|
deleteTable: createCommand('Delete Table', state => state.range && deleteTable(state.range)),
|
|
506
1068
|
mergeCells: createCommand('Merge Cells', state => state.range && state.doc && mergeCells(state.range, state.doc)),
|
|
@@ -513,12 +1075,189 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
513
1075
|
deleteColumn: createCommand('Delete Column', state => state.range && deleteColumn(state.range))
|
|
514
1076
|
}
|
|
515
1077
|
|
|
516
|
-
//
|
|
1078
|
+
// Table alignment commands
|
|
517
1079
|
const alignmentCommands = ['Left', 'Center', 'Right', 'Justify'].reduce((acc, align) => ({
|
|
518
1080
|
...acc,
|
|
519
|
-
[`
|
|
1081
|
+
[`tableAlign${align}`]: createCommand(`Table Align ${align}`, state => {
|
|
1082
|
+
if (state.range) {
|
|
1083
|
+
const alignment = align === 'Left' ? 'start' : align === 'Right' ? 'end' : align.toLowerCase() as 'center' | 'justify'
|
|
1084
|
+
return alignColumn(state.range, alignment)
|
|
1085
|
+
}
|
|
1086
|
+
})
|
|
520
1087
|
}), {})
|
|
521
1088
|
|
|
1089
|
+
// Text alignment commands (for paragraphs)
|
|
1090
|
+
const textAlignmentCommands = {
|
|
1091
|
+
alignLeft: createCommand('Align Left', (state) => {
|
|
1092
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1093
|
+
if (!selectionInfo) return
|
|
1094
|
+
|
|
1095
|
+
const { range } = selectionInfo
|
|
1096
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1097
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1098
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1099
|
+
|
|
1100
|
+
if (paragraph) {
|
|
1101
|
+
(paragraph as HTMLElement).style.textAlign = 'left'
|
|
1102
|
+
}
|
|
1103
|
+
}, (state) => {
|
|
1104
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1105
|
+
if (!selectionInfo) return false
|
|
1106
|
+
|
|
1107
|
+
const { range } = selectionInfo
|
|
1108
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1109
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1110
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1111
|
+
|
|
1112
|
+
return (paragraph as HTMLElement)?.style.textAlign === 'left'
|
|
1113
|
+
}),
|
|
1114
|
+
|
|
1115
|
+
alignCenter: createCommand('Align Center', (state) => {
|
|
1116
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1117
|
+
if (!selectionInfo) return
|
|
1118
|
+
|
|
1119
|
+
const { range } = selectionInfo
|
|
1120
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1121
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1122
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1123
|
+
|
|
1124
|
+
if (paragraph) {
|
|
1125
|
+
(paragraph as HTMLElement).style.textAlign = 'center'
|
|
1126
|
+
}
|
|
1127
|
+
}, (state) => {
|
|
1128
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1129
|
+
if (!selectionInfo) return false
|
|
1130
|
+
|
|
1131
|
+
const { range } = selectionInfo
|
|
1132
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1133
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1134
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1135
|
+
|
|
1136
|
+
return (paragraph as HTMLElement)?.style.textAlign === 'center'
|
|
1137
|
+
}),
|
|
1138
|
+
|
|
1139
|
+
alignRight: createCommand('Align Right', (state) => {
|
|
1140
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1141
|
+
if (!selectionInfo) return
|
|
1142
|
+
|
|
1143
|
+
const { range } = selectionInfo
|
|
1144
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1145
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1146
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1147
|
+
|
|
1148
|
+
if (paragraph) {
|
|
1149
|
+
(paragraph as HTMLElement).style.textAlign = 'right'
|
|
1150
|
+
}
|
|
1151
|
+
}, (state) => {
|
|
1152
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1153
|
+
if (!selectionInfo) return false
|
|
1154
|
+
|
|
1155
|
+
const { range } = selectionInfo
|
|
1156
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1157
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1158
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1159
|
+
|
|
1160
|
+
return (paragraph as HTMLElement)?.style.textAlign === 'right'
|
|
1161
|
+
}),
|
|
1162
|
+
|
|
1163
|
+
alignJustify: createCommand('Align Justify', (state) => {
|
|
1164
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1165
|
+
if (!selectionInfo) return
|
|
1166
|
+
|
|
1167
|
+
const { range } = selectionInfo
|
|
1168
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1169
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1170
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1171
|
+
|
|
1172
|
+
if (paragraph) {
|
|
1173
|
+
(paragraph as HTMLElement).style.textAlign = 'justify'
|
|
1174
|
+
}
|
|
1175
|
+
}, (state) => {
|
|
1176
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1177
|
+
if (!selectionInfo) return false
|
|
1178
|
+
|
|
1179
|
+
const { range } = selectionInfo
|
|
1180
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1181
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1182
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1183
|
+
|
|
1184
|
+
return (paragraph as HTMLElement)?.style.textAlign === 'justify'
|
|
1185
|
+
}),
|
|
1186
|
+
|
|
1187
|
+
textDirection: createCommand('Toggle Text Direction', (state) => {
|
|
1188
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1189
|
+
if (!selectionInfo) return
|
|
1190
|
+
|
|
1191
|
+
const { range } = selectionInfo
|
|
1192
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1193
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1194
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1195
|
+
|
|
1196
|
+
if (paragraph) {
|
|
1197
|
+
const currentDir = (paragraph as HTMLElement).dir || 'ltr'
|
|
1198
|
+
; (paragraph as HTMLElement).dir = currentDir === 'ltr' ? 'rtl' : 'ltr'
|
|
1199
|
+
}
|
|
1200
|
+
}, (state) => {
|
|
1201
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1202
|
+
if (!selectionInfo) return false
|
|
1203
|
+
|
|
1204
|
+
const { range } = selectionInfo
|
|
1205
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1206
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1207
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1208
|
+
|
|
1209
|
+
return (paragraph as HTMLElement)?.dir === 'rtl'
|
|
1210
|
+
}),
|
|
1211
|
+
|
|
1212
|
+
ltrDirection: createCommand('Set Left to Right Direction', (state) => {
|
|
1213
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1214
|
+
if (!selectionInfo) return
|
|
1215
|
+
|
|
1216
|
+
const { range } = selectionInfo
|
|
1217
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1218
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1219
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1220
|
+
|
|
1221
|
+
if (paragraph) {
|
|
1222
|
+
(paragraph as HTMLElement).dir = 'ltr'
|
|
1223
|
+
}
|
|
1224
|
+
}, (state) => {
|
|
1225
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1226
|
+
if (!selectionInfo) return false
|
|
1227
|
+
|
|
1228
|
+
const { range } = selectionInfo
|
|
1229
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1230
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1231
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1232
|
+
|
|
1233
|
+
return (paragraph as HTMLElement)?.dir === 'ltr'
|
|
1234
|
+
}),
|
|
1235
|
+
|
|
1236
|
+
rtlDirection: createCommand('Set Right to Left Direction', (state) => {
|
|
1237
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1238
|
+
if (!selectionInfo) return
|
|
1239
|
+
|
|
1240
|
+
const { range } = selectionInfo
|
|
1241
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1242
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1243
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1244
|
+
|
|
1245
|
+
if (paragraph) {
|
|
1246
|
+
(paragraph as HTMLElement).dir = 'rtl'
|
|
1247
|
+
}
|
|
1248
|
+
}, (state) => {
|
|
1249
|
+
const selectionInfo = getCurrentSelection(state)
|
|
1250
|
+
if (!selectionInfo) return false
|
|
1251
|
+
|
|
1252
|
+
const { range } = selectionInfo
|
|
1253
|
+
const paragraph = range.startContainer.nodeType === Node.TEXT_NODE
|
|
1254
|
+
? range.startContainer.parentElement?.closest('p, h1, h2, h3, h4, h5, h6')
|
|
1255
|
+
: (range.startContainer as Element).closest('p, h1, h2, h3, h4, h5, h6')
|
|
1256
|
+
|
|
1257
|
+
return (paragraph as HTMLElement)?.dir === 'rtl'
|
|
1258
|
+
})
|
|
1259
|
+
}
|
|
1260
|
+
|
|
522
1261
|
// View state commands
|
|
523
1262
|
const viewCommands = {
|
|
524
1263
|
fullScreen: createCommand('Full Screen', (state) => { state.isFullscreen = !state.isFullscreen }, state => state.isFullscreen),
|
|
@@ -528,9 +1267,30 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
528
1267
|
|
|
529
1268
|
// Media commands
|
|
530
1269
|
const mediaCommands = {
|
|
531
|
-
image: createCommand('Insert Image', (state) => {
|
|
532
|
-
|
|
533
|
-
|
|
1270
|
+
image: createCommand('Insert Image', (state) => {
|
|
1271
|
+
const openImageModal = (state as any).openImageModal
|
|
1272
|
+
if (openImageModal) {
|
|
1273
|
+
openImageModal()
|
|
1274
|
+
} else {
|
|
1275
|
+
console.warn('Image insertion requires modal implementation')
|
|
1276
|
+
}
|
|
1277
|
+
}),
|
|
1278
|
+
video: createCommand('Insert Video', (state) => {
|
|
1279
|
+
const openVideoModal = (state as any).openVideoModal
|
|
1280
|
+
if (openVideoModal) {
|
|
1281
|
+
openVideoModal()
|
|
1282
|
+
} else {
|
|
1283
|
+
console.warn('Video insertion requires modal implementation')
|
|
1284
|
+
}
|
|
1285
|
+
}),
|
|
1286
|
+
embed: createCommand('Insert Embed', (state) => {
|
|
1287
|
+
const openEmbedModal = (state as any).openEmbedModal
|
|
1288
|
+
if (openEmbedModal) {
|
|
1289
|
+
openEmbedModal()
|
|
1290
|
+
} else {
|
|
1291
|
+
console.warn('Embed insertion requires modal implementation')
|
|
1292
|
+
}
|
|
1293
|
+
})
|
|
534
1294
|
}
|
|
535
1295
|
|
|
536
1296
|
return {
|
|
@@ -542,6 +1302,7 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
542
1302
|
...formatCommands,
|
|
543
1303
|
...tableCommands,
|
|
544
1304
|
...alignmentCommands,
|
|
1305
|
+
...textAlignmentCommands,
|
|
545
1306
|
...viewCommands,
|
|
546
1307
|
...mediaCommands
|
|
547
1308
|
}
|