@bagelink/vue 1.4.64 → 1.4.69
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/NavBar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useEditorKeyboard.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts +128 -1
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/index.cjs +20 -20
- package/dist/index.mjs +20 -20
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/NavBar.vue +17 -5
- package/src/components/form/inputs/RichText/composables/useCommands.ts +9 -29
- package/src/components/form/inputs/RichText/composables/useEditor.ts +45 -46
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +52 -3
- package/src/components/form/inputs/RichText/index.vue +148 -43
- package/src/components/form/inputs/RichText/utils/commands.ts +347 -282
- package/src/components/form/inputs/RichText/utils/selection.ts +55 -37
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { EditorState } from '../richTextTypes'
|
|
2
|
-
import { formatting } from './formatting'
|
|
3
2
|
import { insertImage, insertLink, insertEmbed } from './media'
|
|
4
3
|
import { addRow, deleteRow, mergeCells, splitCell, insertTable, deleteTable, insertColumn, deleteColumn, alignColumn } from './table'
|
|
5
4
|
|
|
@@ -20,6 +19,43 @@ export interface CommandExecutor {
|
|
|
20
19
|
getValue: (command: string) => string | null
|
|
21
20
|
}
|
|
22
21
|
|
|
22
|
+
// Helper functions
|
|
23
|
+
function getCurrentSelection(state: EditorState): { selection: Selection, range: Range } | null {
|
|
24
|
+
if (!state.doc) return null
|
|
25
|
+
|
|
26
|
+
const selection = state.doc.getSelection()
|
|
27
|
+
if (!selection || !selection.rangeCount) return null
|
|
28
|
+
|
|
29
|
+
const range = selection.getRangeAt(0)
|
|
30
|
+
return { selection, range }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function updateStateAfterCommand(state: EditorState) {
|
|
34
|
+
if (!state.doc) return
|
|
35
|
+
|
|
36
|
+
// Update content
|
|
37
|
+
state.content = state.doc.body.innerHTML
|
|
38
|
+
|
|
39
|
+
// Update selection state
|
|
40
|
+
const selection = state.doc.getSelection()
|
|
41
|
+
if (selection && selection.rangeCount > 0) {
|
|
42
|
+
state.selection = selection
|
|
43
|
+
state.range = selection.getRangeAt(0).cloneRange()
|
|
44
|
+
state.rangeCount = selection.rangeCount
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function findBlockElement(node: Node): Element | null {
|
|
49
|
+
let current = node.nodeType === Node.TEXT_NODE ? node.parentElement : node as Element
|
|
50
|
+
while (current) {
|
|
51
|
+
if (current.matches && current.matches('p,h1,h2,h3,h4,h5,h6,blockquote,li,div')) {
|
|
52
|
+
return current
|
|
53
|
+
}
|
|
54
|
+
current = current.parentElement
|
|
55
|
+
}
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
23
59
|
// Centralized command creation helper
|
|
24
60
|
function createCommand(name: string, execute: Command['execute'], isActive?: Command['isActive']): Command {
|
|
25
61
|
return {
|
|
@@ -27,83 +63,13 @@ function createCommand(name: string, execute: Command['execute'], isActive?: Com
|
|
|
27
63
|
execute: (state: EditorState, value?: string) => {
|
|
28
64
|
if (!state.doc) return
|
|
29
65
|
execute(state, value)
|
|
66
|
+
updateStateAfterCommand(state)
|
|
30
67
|
},
|
|
31
68
|
isActive
|
|
32
69
|
}
|
|
33
70
|
}
|
|
34
71
|
|
|
35
|
-
// Create formatting commands helper
|
|
36
|
-
function createFormattingCommand(state: EditorState, type: 'text' | 'block' | 'list', command: string, tag?: string): Command {
|
|
37
|
-
const format = formatting(state)
|
|
38
|
-
return createCommand(
|
|
39
|
-
command,
|
|
40
|
-
() => {
|
|
41
|
-
if (!state.doc) return
|
|
42
|
-
|
|
43
|
-
if (type === 'text') {
|
|
44
|
-
format.text(command)
|
|
45
|
-
}
|
|
46
|
-
else if (type === 'block') {
|
|
47
|
-
format.block(command, tag || command)
|
|
48
|
-
}
|
|
49
|
-
else if (type === 'list') {
|
|
50
|
-
const selection = state.doc.getSelection()
|
|
51
|
-
if (!selection || !selection.rangeCount) return
|
|
52
|
-
|
|
53
|
-
const range = selection.getRangeAt(0)
|
|
54
|
-
|
|
55
|
-
// If there's no content or the selection is collapsed
|
|
56
|
-
if (range.collapsed && (!range.startContainer.textContent?.trim() || range.startContainer === state.doc.body)) {
|
|
57
|
-
// Create a new list with an empty item
|
|
58
|
-
const list = state.doc.createElement(command === 'orderedList' ? 'ol' : 'ul')
|
|
59
|
-
const li = state.doc.createElement('li')
|
|
60
|
-
// Use a non-breaking space with br to ensure proper rendering
|
|
61
|
-
li.innerHTML = ' <br>'
|
|
62
|
-
list.appendChild(li)
|
|
63
|
-
|
|
64
|
-
// If we're in an empty paragraph, replace it
|
|
65
|
-
const currentBlock = range.startContainer.nodeType === 1
|
|
66
|
-
? range.startContainer as Element
|
|
67
|
-
: range.startContainer.parentElement
|
|
68
|
-
|
|
69
|
-
if (currentBlock?.tagName.toLowerCase() === 'p' && isNodeEmpty(currentBlock)) {
|
|
70
|
-
currentBlock.parentNode?.replaceChild(list, currentBlock)
|
|
71
|
-
} else {
|
|
72
|
-
// Otherwise insert at cursor
|
|
73
|
-
range.insertNode(list)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Move cursor into the list item
|
|
77
|
-
range.selectNodeContents(li)
|
|
78
|
-
range.collapse(true)
|
|
79
|
-
selection.removeAllRanges()
|
|
80
|
-
selection.addRange(range)
|
|
81
|
-
} else {
|
|
82
|
-
format.list(command)
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
() => state.selectedStyles.has(command)
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Helper function to check if a node is empty (contains only whitespace or <br>)
|
|
91
|
-
function isNodeEmpty(node: Node): boolean {
|
|
92
|
-
// Check for text content after removing whitespace and nonbreaking spaces
|
|
93
|
-
const text = node.textContent?.replace(/\s/g, '') || ''
|
|
94
|
-
if (text) return false
|
|
95
|
-
|
|
96
|
-
// Check for <br> tags
|
|
97
|
-
const brElements = (node as Element).getElementsByTagName ? (node as Element).getElementsByTagName('br') : []
|
|
98
|
-
if (brElements.length === 0) return true
|
|
99
|
-
|
|
100
|
-
// If there's only one <br> and it's the only content (besides potential ), consider it empty
|
|
101
|
-
return brElements.length === 1 && node.childNodes.length <= 2 // Allow for + <br>
|
|
102
|
-
}
|
|
103
|
-
|
|
104
72
|
export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
105
|
-
const format = formatting(state)
|
|
106
|
-
|
|
107
73
|
// History commands
|
|
108
74
|
const historyCommands = {
|
|
109
75
|
undo: createCommand('Undo', () => {
|
|
@@ -112,7 +78,7 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
112
78
|
if (lastContent !== undefined) {
|
|
113
79
|
state.redoStack.push(state.content)
|
|
114
80
|
state.content = lastContent
|
|
115
|
-
|
|
81
|
+
state.doc.body.innerHTML = lastContent
|
|
116
82
|
}
|
|
117
83
|
}
|
|
118
84
|
}),
|
|
@@ -122,7 +88,7 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
122
88
|
if (nextContent !== undefined) {
|
|
123
89
|
state.undoStack.push(state.content)
|
|
124
90
|
state.content = nextContent
|
|
125
|
-
|
|
91
|
+
state.doc.body.innerHTML = nextContent
|
|
126
92
|
}
|
|
127
93
|
}
|
|
128
94
|
})
|
|
@@ -131,104 +97,77 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
131
97
|
// Basic text formatting commands
|
|
132
98
|
const textCommands = {
|
|
133
99
|
bold: createCommand('Bold', (state) => {
|
|
134
|
-
|
|
135
|
-
if (!
|
|
136
|
-
|
|
137
|
-
const { range } = state
|
|
138
|
-
if (range.collapsed) return
|
|
100
|
+
const selectionInfo = getCurrentSelection(state)
|
|
101
|
+
if (!selectionInfo || selectionInfo.range.collapsed) return
|
|
139
102
|
|
|
103
|
+
const { range } = selectionInfo
|
|
140
104
|
try {
|
|
141
|
-
|
|
142
|
-
const span = state.doc.createElement('span')
|
|
105
|
+
const span = state.doc!.createElement('span')
|
|
143
106
|
span.style.fontWeight = 'bold'
|
|
144
107
|
|
|
145
|
-
// Try to surround contents, if that fails use extract and insert
|
|
146
108
|
try {
|
|
147
109
|
range.surroundContents(span)
|
|
148
110
|
} catch {
|
|
149
|
-
// Handle selections that cross node boundaries
|
|
150
111
|
const fragment = range.extractContents()
|
|
151
112
|
span.appendChild(fragment)
|
|
152
113
|
range.insertNode(span)
|
|
153
114
|
}
|
|
154
115
|
|
|
155
|
-
// Select the new formatted content
|
|
156
116
|
range.selectNodeContents(span)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// Update content
|
|
161
|
-
state.content = state.doc.body.innerHTML
|
|
117
|
+
selectionInfo.selection.removeAllRanges()
|
|
118
|
+
selectionInfo.selection.addRange(range)
|
|
162
119
|
} catch (error) {
|
|
163
|
-
console.error('
|
|
120
|
+
console.error('Error applying bold:', error)
|
|
164
121
|
}
|
|
165
122
|
}),
|
|
166
123
|
|
|
167
124
|
italic: createCommand('Italic', (state) => {
|
|
168
|
-
|
|
169
|
-
if (!
|
|
170
|
-
|
|
171
|
-
const { range } = state
|
|
172
|
-
if (range.collapsed) return
|
|
125
|
+
const selectionInfo = getCurrentSelection(state)
|
|
126
|
+
if (!selectionInfo || selectionInfo.range.collapsed) return
|
|
173
127
|
|
|
128
|
+
const { range } = selectionInfo
|
|
174
129
|
try {
|
|
175
|
-
|
|
176
|
-
const span = state.doc.createElement('span')
|
|
130
|
+
const span = state.doc!.createElement('span')
|
|
177
131
|
span.style.fontStyle = 'italic'
|
|
178
132
|
|
|
179
|
-
// Try to surround contents, if that fails use extract and insert
|
|
180
133
|
try {
|
|
181
134
|
range.surroundContents(span)
|
|
182
135
|
} catch {
|
|
183
|
-
// Handle selections that cross node boundaries
|
|
184
136
|
const fragment = range.extractContents()
|
|
185
137
|
span.appendChild(fragment)
|
|
186
138
|
range.insertNode(span)
|
|
187
139
|
}
|
|
188
140
|
|
|
189
|
-
// Select the new formatted content
|
|
190
141
|
range.selectNodeContents(span)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// Update content
|
|
195
|
-
state.content = state.doc.body.innerHTML
|
|
142
|
+
selectionInfo.selection.removeAllRanges()
|
|
143
|
+
selectionInfo.selection.addRange(range)
|
|
196
144
|
} catch (error) {
|
|
197
|
-
console.error('
|
|
145
|
+
console.error('Error applying italic:', error)
|
|
198
146
|
}
|
|
199
147
|
}),
|
|
200
148
|
|
|
201
149
|
underline: createCommand('Underline', (state) => {
|
|
202
|
-
|
|
203
|
-
if (!
|
|
204
|
-
|
|
205
|
-
const { range } = state
|
|
206
|
-
if (range.collapsed) return
|
|
150
|
+
const selectionInfo = getCurrentSelection(state)
|
|
151
|
+
if (!selectionInfo || selectionInfo.range.collapsed) return
|
|
207
152
|
|
|
153
|
+
const { range } = selectionInfo
|
|
208
154
|
try {
|
|
209
|
-
|
|
210
|
-
const span = state.doc.createElement('span')
|
|
155
|
+
const span = state.doc!.createElement('span')
|
|
211
156
|
span.style.textDecoration = 'underline'
|
|
212
157
|
|
|
213
|
-
// Try to surround contents, if that fails use extract and insert
|
|
214
158
|
try {
|
|
215
159
|
range.surroundContents(span)
|
|
216
160
|
} catch {
|
|
217
|
-
// Handle selections that cross node boundaries
|
|
218
161
|
const fragment = range.extractContents()
|
|
219
162
|
span.appendChild(fragment)
|
|
220
163
|
range.insertNode(span)
|
|
221
164
|
}
|
|
222
165
|
|
|
223
|
-
// Select the new formatted content
|
|
224
166
|
range.selectNodeContents(span)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
// Update content
|
|
229
|
-
state.content = state.doc.body.innerHTML
|
|
167
|
+
selectionInfo.selection.removeAllRanges()
|
|
168
|
+
selectionInfo.selection.addRange(range)
|
|
230
169
|
} catch (error) {
|
|
231
|
-
console.error('
|
|
170
|
+
console.error('Error applying underline:', error)
|
|
232
171
|
}
|
|
233
172
|
})
|
|
234
173
|
}
|
|
@@ -237,46 +176,35 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
237
176
|
const headingCommands = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].reduce((acc, cmd) => ({
|
|
238
177
|
...acc,
|
|
239
178
|
[cmd]: createCommand(`Heading ${cmd}`, (state) => {
|
|
240
|
-
|
|
241
|
-
if (!
|
|
179
|
+
const selectionInfo = getCurrentSelection(state)
|
|
180
|
+
if (!selectionInfo) return
|
|
242
181
|
|
|
243
|
-
const { range
|
|
244
|
-
const
|
|
245
|
-
|
|
182
|
+
const { range } = selectionInfo
|
|
183
|
+
const currentBlock = findBlockElement(range.commonAncestorContainer)
|
|
184
|
+
if (!currentBlock) return
|
|
246
185
|
|
|
247
|
-
//
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
// Check if we need to toggle off (already in a heading with the same tag)
|
|
251
|
-
const isToggleOff = currentBlock?.tagName.toLowerCase() === cmd.toLowerCase()
|
|
252
|
-
|
|
253
|
-
// Determine which tag to apply
|
|
186
|
+
// Check if we need to toggle off (already in the same heading)
|
|
187
|
+
const isToggleOff = currentBlock.tagName.toLowerCase() === cmd.toLowerCase()
|
|
254
188
|
const newTag = isToggleOff ? 'p' : cmd
|
|
255
189
|
|
|
256
190
|
try {
|
|
257
|
-
|
|
258
|
-
const newBlock = doc.createElement(newTag)
|
|
259
|
-
|
|
260
|
-
if (currentBlock) {
|
|
261
|
-
// Copy content from current block to new block
|
|
262
|
-
while (currentBlock.firstChild) {
|
|
263
|
-
newBlock.appendChild(currentBlock.firstChild)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Replace the old block with the new one
|
|
267
|
-
currentBlock.parentNode?.replaceChild(newBlock, currentBlock)
|
|
191
|
+
const newBlock = state.doc!.createElement(newTag)
|
|
268
192
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
selection.removeAllRanges()
|
|
273
|
-
selection.addRange(range)
|
|
193
|
+
// Copy content from current block to new block
|
|
194
|
+
while (currentBlock.firstChild) {
|
|
195
|
+
newBlock.appendChild(currentBlock.firstChild)
|
|
274
196
|
}
|
|
275
197
|
|
|
276
|
-
//
|
|
277
|
-
|
|
198
|
+
// Replace the old block with the new one
|
|
199
|
+
currentBlock.parentNode?.replaceChild(newBlock, currentBlock)
|
|
200
|
+
|
|
201
|
+
// Move cursor into the new block
|
|
202
|
+
range.selectNodeContents(newBlock)
|
|
203
|
+
range.collapse(false)
|
|
204
|
+
selectionInfo.selection.removeAllRanges()
|
|
205
|
+
selectionInfo.selection.addRange(range)
|
|
278
206
|
} catch (error) {
|
|
279
|
-
console.error(`
|
|
207
|
+
console.error(`Error applying ${cmd} heading:`, error)
|
|
280
208
|
}
|
|
281
209
|
}),
|
|
282
210
|
}), {})
|
|
@@ -284,55 +212,288 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
284
212
|
// Block commands
|
|
285
213
|
const blockCommands = {
|
|
286
214
|
p: createCommand('Paragraph', (state) => {
|
|
287
|
-
|
|
288
|
-
if (!
|
|
289
|
-
|
|
290
|
-
const { range, doc, selection } = state
|
|
291
|
-
const container = range.commonAncestorContainer
|
|
292
|
-
const parentBlock = container.nodeType === 3 ? container.parentElement : container as HTMLElement
|
|
215
|
+
const selectionInfo = getCurrentSelection(state)
|
|
216
|
+
if (!selectionInfo) return
|
|
293
217
|
|
|
294
|
-
|
|
295
|
-
const currentBlock =
|
|
218
|
+
const { range } = selectionInfo
|
|
219
|
+
const currentBlock = findBlockElement(range.commonAncestorContainer)
|
|
220
|
+
if (!currentBlock) return
|
|
296
221
|
|
|
297
222
|
// Check if already a paragraph - if so, nothing to do
|
|
298
|
-
if (currentBlock
|
|
299
|
-
console.log('[Command] Already a paragraph')
|
|
223
|
+
if (currentBlock.tagName.toLowerCase() === 'p') {
|
|
300
224
|
return
|
|
301
225
|
}
|
|
302
226
|
|
|
303
227
|
try {
|
|
304
|
-
|
|
305
|
-
|
|
228
|
+
const newParagraph = state.doc!.createElement('p')
|
|
229
|
+
|
|
230
|
+
// Copy content from current block to paragraph
|
|
231
|
+
while (currentBlock.firstChild) {
|
|
232
|
+
newParagraph.appendChild(currentBlock.firstChild)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Replace the old block with the paragraph
|
|
236
|
+
currentBlock.parentNode?.replaceChild(newParagraph, currentBlock)
|
|
237
|
+
|
|
238
|
+
// Move cursor into the new paragraph
|
|
239
|
+
range.selectNodeContents(newParagraph)
|
|
240
|
+
range.collapse(false)
|
|
241
|
+
selectionInfo.selection.removeAllRanges()
|
|
242
|
+
selectionInfo.selection.addRange(range)
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error('Error applying paragraph:', error)
|
|
245
|
+
}
|
|
246
|
+
}),
|
|
247
|
+
|
|
248
|
+
blockquote: createCommand('Blockquote', (state) => {
|
|
249
|
+
const selectionInfo = getCurrentSelection(state)
|
|
250
|
+
if (!selectionInfo) return
|
|
251
|
+
|
|
252
|
+
const { range } = selectionInfo
|
|
253
|
+
const currentBlock = findBlockElement(range.commonAncestorContainer)
|
|
254
|
+
if (!currentBlock) return
|
|
255
|
+
|
|
256
|
+
// Check if already a blockquote - if so, toggle to paragraph
|
|
257
|
+
const isToggleOff = currentBlock.tagName.toLowerCase() === 'blockquote'
|
|
258
|
+
const newTag = isToggleOff ? 'p' : 'blockquote'
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
const newBlock = state.doc!.createElement(newTag)
|
|
262
|
+
|
|
263
|
+
// Copy content from current block to new block
|
|
264
|
+
while (currentBlock.firstChild) {
|
|
265
|
+
newBlock.appendChild(currentBlock.firstChild)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Replace the old block with the new one
|
|
269
|
+
currentBlock.parentNode?.replaceChild(newBlock, currentBlock)
|
|
270
|
+
|
|
271
|
+
// Move cursor into the new block
|
|
272
|
+
range.selectNodeContents(newBlock)
|
|
273
|
+
range.collapse(false)
|
|
274
|
+
selectionInfo.selection.removeAllRanges()
|
|
275
|
+
selectionInfo.selection.addRange(range)
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error('Error applying blockquote:', error)
|
|
278
|
+
}
|
|
279
|
+
})
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// List commands - simplified implementation
|
|
283
|
+
const listCommands = {
|
|
284
|
+
orderedList: createCommand('Ordered List', (state) => {
|
|
285
|
+
const selectionInfo = getCurrentSelection(state)
|
|
286
|
+
if (!selectionInfo) return
|
|
287
|
+
|
|
288
|
+
const { range } = selectionInfo
|
|
289
|
+
const currentBlock = findBlockElement(range.commonAncestorContainer)
|
|
290
|
+
if (!currentBlock) return
|
|
291
|
+
|
|
292
|
+
// Check if we're inside a list item
|
|
293
|
+
const listItem = currentBlock.closest('li')
|
|
294
|
+
if (listItem) {
|
|
295
|
+
const listParent = listItem.parentElement as HTMLElement
|
|
296
|
+
if (!listParent) return
|
|
297
|
+
|
|
298
|
+
// If it's already an ordered list, toggle off to paragraph
|
|
299
|
+
if (listParent.tagName.toLowerCase() === 'ol') {
|
|
300
|
+
const p = state.doc!.createElement('p')
|
|
301
|
+
while (listItem.firstChild) {
|
|
302
|
+
p.appendChild(listItem.firstChild)
|
|
303
|
+
}
|
|
304
|
+
listParent.parentNode?.insertBefore(p, listParent)
|
|
305
|
+
listItem.remove()
|
|
306
|
+
|
|
307
|
+
if (!listParent.querySelector('li')) {
|
|
308
|
+
listParent.remove()
|
|
309
|
+
}
|
|
306
310
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
+
range.selectNodeContents(p)
|
|
312
|
+
range.collapse(false)
|
|
313
|
+
selectionInfo.selection.removeAllRanges()
|
|
314
|
+
selectionInfo.selection.addRange(range)
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Convert unordered list to ordered list
|
|
319
|
+
if (listParent.tagName.toLowerCase() === 'ul') {
|
|
320
|
+
const ol = state.doc!.createElement('ol')
|
|
321
|
+
while (listParent.firstChild) {
|
|
322
|
+
ol.appendChild(listParent.firstChild)
|
|
311
323
|
}
|
|
324
|
+
listParent.parentNode?.replaceChild(ol, listParent)
|
|
325
|
+
return
|
|
326
|
+
}
|
|
327
|
+
}
|
|
312
328
|
|
|
313
|
-
|
|
314
|
-
|
|
329
|
+
// Convert current block to ordered list
|
|
330
|
+
try {
|
|
331
|
+
const ol = state.doc!.createElement('ol')
|
|
332
|
+
const li = state.doc!.createElement('li')
|
|
315
333
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
selection.removeAllRanges()
|
|
320
|
-
selection.addRange(range)
|
|
334
|
+
// Copy content to list item
|
|
335
|
+
while (currentBlock.firstChild) {
|
|
336
|
+
li.appendChild(currentBlock.firstChild)
|
|
321
337
|
}
|
|
322
338
|
|
|
323
|
-
|
|
324
|
-
|
|
339
|
+
ol.appendChild(li)
|
|
340
|
+
currentBlock.parentNode?.replaceChild(ol, currentBlock)
|
|
341
|
+
|
|
342
|
+
// Move cursor into the list item
|
|
343
|
+
range.selectNodeContents(li)
|
|
344
|
+
range.collapse(false)
|
|
345
|
+
selectionInfo.selection.removeAllRanges()
|
|
346
|
+
selectionInfo.selection.addRange(range)
|
|
325
347
|
} catch (error) {
|
|
326
|
-
console.error('
|
|
348
|
+
console.error('Error creating ordered list:', error)
|
|
327
349
|
}
|
|
328
350
|
}),
|
|
329
|
-
|
|
351
|
+
|
|
352
|
+
unorderedList: createCommand('Unordered List', (state) => {
|
|
353
|
+
const selectionInfo = getCurrentSelection(state)
|
|
354
|
+
if (!selectionInfo) return
|
|
355
|
+
|
|
356
|
+
const { range } = selectionInfo
|
|
357
|
+
const currentBlock = findBlockElement(range.commonAncestorContainer)
|
|
358
|
+
if (!currentBlock) return
|
|
359
|
+
|
|
360
|
+
// Check if we're inside a list item
|
|
361
|
+
const listItem = currentBlock.closest('li')
|
|
362
|
+
if (listItem) {
|
|
363
|
+
const listParent = listItem.parentElement as HTMLElement
|
|
364
|
+
if (!listParent) return
|
|
365
|
+
|
|
366
|
+
// If it's already an unordered list, toggle off to paragraph
|
|
367
|
+
if (listParent.tagName.toLowerCase() === 'ul') {
|
|
368
|
+
const p = state.doc!.createElement('p')
|
|
369
|
+
while (listItem.firstChild) {
|
|
370
|
+
p.appendChild(listItem.firstChild)
|
|
371
|
+
}
|
|
372
|
+
listParent.parentNode?.insertBefore(p, listParent)
|
|
373
|
+
listItem.remove()
|
|
374
|
+
|
|
375
|
+
if (!listParent.querySelector('li')) {
|
|
376
|
+
listParent.remove()
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
range.selectNodeContents(p)
|
|
380
|
+
range.collapse(false)
|
|
381
|
+
selectionInfo.selection.removeAllRanges()
|
|
382
|
+
selectionInfo.selection.addRange(range)
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Convert ordered list to unordered list
|
|
387
|
+
if (listParent.tagName.toLowerCase() === 'ol') {
|
|
388
|
+
const ul = state.doc!.createElement('ul')
|
|
389
|
+
while (listParent.firstChild) {
|
|
390
|
+
ul.appendChild(listParent.firstChild)
|
|
391
|
+
}
|
|
392
|
+
listParent.parentNode?.replaceChild(ul, listParent)
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Convert current block to unordered list
|
|
398
|
+
try {
|
|
399
|
+
const ul = state.doc!.createElement('ul')
|
|
400
|
+
const li = state.doc!.createElement('li')
|
|
401
|
+
|
|
402
|
+
// Copy content to list item
|
|
403
|
+
while (currentBlock.firstChild) {
|
|
404
|
+
li.appendChild(currentBlock.firstChild)
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
ul.appendChild(li)
|
|
408
|
+
currentBlock.parentNode?.replaceChild(ul, currentBlock)
|
|
409
|
+
|
|
410
|
+
// Move cursor into the list item
|
|
411
|
+
range.selectNodeContents(li)
|
|
412
|
+
range.collapse(false)
|
|
413
|
+
selectionInfo.selection.removeAllRanges()
|
|
414
|
+
selectionInfo.selection.addRange(range)
|
|
415
|
+
} catch (error) {
|
|
416
|
+
console.error('Error creating unordered list:', error)
|
|
417
|
+
}
|
|
418
|
+
})
|
|
330
419
|
}
|
|
331
420
|
|
|
332
|
-
//
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
421
|
+
// Clear formatting command - simplified
|
|
422
|
+
const formatCommands = {
|
|
423
|
+
clear: createCommand('Clear Formatting', (state) => {
|
|
424
|
+
const selectionInfo = getCurrentSelection(state)
|
|
425
|
+
if (!selectionInfo || selectionInfo.range.collapsed) return
|
|
426
|
+
|
|
427
|
+
const { range } = selectionInfo
|
|
428
|
+
try {
|
|
429
|
+
// Get the selected content
|
|
430
|
+
const fragment = range.cloneContents()
|
|
431
|
+
const tempDiv = state.doc!.createElement('div')
|
|
432
|
+
tempDiv.appendChild(fragment)
|
|
433
|
+
|
|
434
|
+
// Function to clean a node recursively
|
|
435
|
+
function cleanNode(node: Node): Node {
|
|
436
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
437
|
+
return node.cloneNode(true)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
441
|
+
const element = node as Element
|
|
442
|
+
const tagName = element.tagName.toLowerCase()
|
|
443
|
+
|
|
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 = ['b', 'i', 'u', 'strong', 'em', 'span', 'font', 'strike', 'sub', 'sup']
|
|
448
|
+
|
|
449
|
+
if (formattingTags.includes(tagName)) {
|
|
450
|
+
// For formatting elements, just return the text content
|
|
451
|
+
return state.doc!.createTextNode(element.textContent || '')
|
|
452
|
+
} else if (structuralTags.includes(tagName)) {
|
|
453
|
+
// For structural elements, keep the element but remove attributes
|
|
454
|
+
const cleanElement = state.doc!.createElement(tagName)
|
|
455
|
+
|
|
456
|
+
// Preserve certain attributes for links and images
|
|
457
|
+
if (tagName === 'a' && element.hasAttribute('href')) {
|
|
458
|
+
cleanElement.setAttribute('href', element.getAttribute('href') || '')
|
|
459
|
+
if (element.hasAttribute('target')) {
|
|
460
|
+
cleanElement.setAttribute('target', element.getAttribute('target') || '')
|
|
461
|
+
}
|
|
462
|
+
} else if (tagName === 'img') {
|
|
463
|
+
if (element.hasAttribute('src')) cleanElement.setAttribute('src', element.getAttribute('src') || '')
|
|
464
|
+
if (element.hasAttribute('alt')) cleanElement.setAttribute('alt', element.getAttribute('alt') || '')
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Clean and append child nodes
|
|
468
|
+
Array.from(element.childNodes).forEach((child) => {
|
|
469
|
+
cleanElement.appendChild(cleanNode(child))
|
|
470
|
+
})
|
|
471
|
+
|
|
472
|
+
return cleanElement
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return node.cloneNode(true)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Clean all nodes in the temp div
|
|
480
|
+
const cleanedFragment = state.doc!.createDocumentFragment()
|
|
481
|
+
Array.from(tempDiv.childNodes).forEach((node) => {
|
|
482
|
+
cleanedFragment.appendChild(cleanNode(node))
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
// Replace the selection with cleaned content
|
|
486
|
+
range.deleteContents()
|
|
487
|
+
range.insertNode(cleanedFragment)
|
|
488
|
+
|
|
489
|
+
// Restore selection
|
|
490
|
+
range.collapse(false)
|
|
491
|
+
selectionInfo.selection.removeAllRanges()
|
|
492
|
+
selectionInfo.selection.addRange(range)
|
|
493
|
+
} catch (error) {
|
|
494
|
+
console.error('Error clearing formatting:', error)
|
|
495
|
+
}
|
|
496
|
+
})
|
|
336
497
|
}
|
|
337
498
|
|
|
338
499
|
// Table commands
|
|
@@ -372,113 +533,17 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
372
533
|
embed: createCommand('Insert Embed', (state) => { state.modal && insertEmbed(state.modal, state) })
|
|
373
534
|
}
|
|
374
535
|
|
|
375
|
-
// Other formatting commands
|
|
376
|
-
const otherCommands = {
|
|
377
|
-
clear: createCommand('Clear Formatting', (state) => {
|
|
378
|
-
console.log('[Command] Clear formatting called')
|
|
379
|
-
console.log('[Command] Current selection:', state.selection?.toString())
|
|
380
|
-
console.log('[Command] Range exists:', !!state.range)
|
|
381
|
-
console.log('[Command] Document exists:', !!state.doc)
|
|
382
|
-
|
|
383
|
-
try {
|
|
384
|
-
if (!state.doc || !state.range || !state.selection) {
|
|
385
|
-
console.log('[Command] Missing required state for clear formatting')
|
|
386
|
-
return
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
const { selection, range, doc } = state
|
|
390
|
-
|
|
391
|
-
if (range.collapsed) {
|
|
392
|
-
console.log('[Command] Selection is collapsed, nothing to clear')
|
|
393
|
-
return
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Clone the contents to work with
|
|
397
|
-
const fragment = range.cloneContents()
|
|
398
|
-
const tempDiv = doc.createElement('div')
|
|
399
|
-
tempDiv.appendChild(fragment)
|
|
400
|
-
|
|
401
|
-
console.log('[Command] Original HTML:', tempDiv.innerHTML)
|
|
402
|
-
|
|
403
|
-
// Process all elements to remove styling but keep structure
|
|
404
|
-
const structuralTags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'div', 'table', 'tr', 'td', 'th']
|
|
405
|
-
const inlineTags = ['b', 'i', 'u', 'strong', 'em', 'span', 'font', 'strike', 'sub', 'sup']
|
|
406
|
-
|
|
407
|
-
// Function to clean an element of styling
|
|
408
|
-
const cleanElement = (element: Element) => {
|
|
409
|
-
// Remove style and class attributes
|
|
410
|
-
element.removeAttribute('style')
|
|
411
|
-
element.removeAttribute('class')
|
|
412
|
-
|
|
413
|
-
// Process child elements
|
|
414
|
-
Array.from(element.children).forEach((child) => {
|
|
415
|
-
if (inlineTags.includes(child.tagName.toLowerCase())) {
|
|
416
|
-
// For inline formatting elements, replace with their text content
|
|
417
|
-
const textContent = child.textContent || ''
|
|
418
|
-
const textNode = doc.createTextNode(textContent)
|
|
419
|
-
child.replaceWith(textNode)
|
|
420
|
-
} else {
|
|
421
|
-
// For structural elements, keep them but clean styling
|
|
422
|
-
cleanElement(child)
|
|
423
|
-
}
|
|
424
|
-
})
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Clean all elements
|
|
428
|
-
Array.from(tempDiv.querySelectorAll('*')).forEach((element) => {
|
|
429
|
-
const tagName = element.tagName.toLowerCase()
|
|
430
|
-
|
|
431
|
-
// For inline formatting tags, we'll remove and keep only their content
|
|
432
|
-
// which happens in cleanElement when processing children
|
|
433
|
-
|
|
434
|
-
// For structural tags, keep them but remove styling
|
|
435
|
-
if (structuralTags.includes(tagName)) {
|
|
436
|
-
cleanElement(element)
|
|
437
|
-
}
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
console.log('[Command] Cleaned HTML:', tempDiv.innerHTML)
|
|
441
|
-
|
|
442
|
-
// Replace the selection with the cleaned content
|
|
443
|
-
range.deleteContents()
|
|
444
|
-
|
|
445
|
-
// Create a new document fragment to hold the cleaned content
|
|
446
|
-
const cleanedFragment = doc.createDocumentFragment()
|
|
447
|
-
|
|
448
|
-
// Append all nodes from the tempDiv
|
|
449
|
-
while (tempDiv.firstChild) {
|
|
450
|
-
cleanedFragment.appendChild(tempDiv.firstChild)
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
range.insertNode(cleanedFragment)
|
|
454
|
-
|
|
455
|
-
// Try to restore a reasonable selection
|
|
456
|
-
range.collapse(false)
|
|
457
|
-
selection.removeAllRanges()
|
|
458
|
-
selection.addRange(range)
|
|
459
|
-
|
|
460
|
-
// Update content
|
|
461
|
-
state.content = doc.body.innerHTML
|
|
462
|
-
console.log('[Command] Clear formatting completed')
|
|
463
|
-
} catch (error) {
|
|
464
|
-
console.error('[Command] Error in clear formatting:', error)
|
|
465
|
-
}
|
|
466
|
-
}),
|
|
467
|
-
indent: createCommand('Indent', () => { format.text('indent') }),
|
|
468
|
-
outdent: createCommand('Outdent', () => { format.text('outdent') })
|
|
469
|
-
}
|
|
470
|
-
|
|
471
536
|
return {
|
|
472
537
|
...historyCommands,
|
|
473
538
|
...textCommands,
|
|
474
539
|
...headingCommands,
|
|
475
540
|
...blockCommands,
|
|
476
541
|
...listCommands,
|
|
542
|
+
...formatCommands,
|
|
477
543
|
...tableCommands,
|
|
478
544
|
...alignmentCommands,
|
|
479
545
|
...viewCommands,
|
|
480
|
-
...mediaCommands
|
|
481
|
-
...otherCommands
|
|
546
|
+
...mediaCommands
|
|
482
547
|
}
|
|
483
548
|
}
|
|
484
549
|
|