@bagelink/vue 1.2.13 → 1.2.15
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/form/FieldArray.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/gridBox.vue.d.ts +6 -3
- package/dist/components/form/inputs/RichText/components/gridBox.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/config.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/table.d.ts.map +1 -1
- package/dist/index.cjs +355 -56
- package/dist/index.mjs +355 -56
- package/dist/style.css +3 -3
- package/package.json +1 -1
- package/src/components/form/FieldArray.vue +1 -1
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +3 -4
- package/src/components/form/inputs/RichText/components/gridBox.vue +4 -1
- package/src/components/form/inputs/RichText/composables/useCommands.ts +22 -3
- package/src/components/form/inputs/RichText/config.ts +9 -7
- package/src/components/form/inputs/RichText/utils/commands.ts +307 -23
- package/src/components/form/inputs/RichText/utils/formatting.ts +117 -17
- package/src/components/form/inputs/RichText/utils/table.ts +36 -1
|
@@ -7,13 +7,19 @@ export function useCommands(state: EditorState, debug?: { logCommand: (command:
|
|
|
7
7
|
|
|
8
8
|
return {
|
|
9
9
|
execute: (command: string, value?: string) => {
|
|
10
|
-
|
|
10
|
+
console.log(`[useCommands] Executing command: ${command}`, value ? `with value: ${value}` : '')
|
|
11
|
+
|
|
12
|
+
if (!state.doc) {
|
|
13
|
+
console.log('[useCommands] No document available, skipping command')
|
|
14
|
+
return
|
|
15
|
+
}
|
|
11
16
|
|
|
12
17
|
// Log command if debug is enabled
|
|
13
18
|
debug?.logCommand(command, value)
|
|
14
19
|
|
|
15
20
|
// Handle view state commands directly
|
|
16
21
|
if (['splitView', 'codeView', 'fullScreen'].includes(command)) {
|
|
22
|
+
console.log(`[useCommands] Handling view state command: ${command}`)
|
|
17
23
|
switch (command) {
|
|
18
24
|
case 'splitView':
|
|
19
25
|
state.isSplitView = !state.isSplitView
|
|
@@ -28,14 +34,26 @@ export function useCommands(state: EditorState, debug?: { logCommand: (command:
|
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
// Focus the editor before executing command
|
|
31
|
-
|
|
37
|
+
try {
|
|
38
|
+
state.doc.body.focus()
|
|
39
|
+
console.log('[useCommands] Editor focused')
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error('[useCommands] Error focusing editor:', e)
|
|
42
|
+
}
|
|
32
43
|
|
|
33
44
|
// Execute the command
|
|
34
|
-
|
|
45
|
+
try {
|
|
46
|
+
console.log('[useCommands] Executing command via executor')
|
|
47
|
+
executor.execute(command, value)
|
|
48
|
+
console.log('[useCommands] Command execution completed')
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error('[useCommands] Error during command execution:', e)
|
|
51
|
+
}
|
|
35
52
|
|
|
36
53
|
// Update content state only if it has changed
|
|
37
54
|
const newContent = state.doc.body.innerHTML
|
|
38
55
|
if (newContent !== state.content) {
|
|
56
|
+
console.log('[useCommands] Content changed, updating state')
|
|
39
57
|
state.content = newContent
|
|
40
58
|
}
|
|
41
59
|
|
|
@@ -47,6 +65,7 @@ export function useCommands(state: EditorState, debug?: { logCommand: (command:
|
|
|
47
65
|
|| state.rangeCount !== selection.rangeCount
|
|
48
66
|
|
|
49
67
|
if (hasSelectionChanged) {
|
|
68
|
+
console.log('[useCommands] Selection changed, updating state')
|
|
50
69
|
state.selection = selection
|
|
51
70
|
state.range = selection.getRangeAt(0).cloneRange()
|
|
52
71
|
state.rangeCount = selection.rangeCount
|
|
@@ -102,13 +102,15 @@ export const toolbarOptions: ToolbarOption[] = [
|
|
|
102
102
|
{ name: 'fontColor', label: 'Font Color', icon: 'format_color_text' },
|
|
103
103
|
{ name: 'bgColor', label: 'Background Color', icon: 'format_color_fill' },
|
|
104
104
|
{ name: 'insertTable', label: 'Insert Table', icon: 'table' },
|
|
105
|
-
{ name: 'deleteTable', label: 'Delete Table', icon: '
|
|
106
|
-
{ name: '
|
|
107
|
-
{ name: '
|
|
108
|
-
{ name: '
|
|
109
|
-
{ name: '
|
|
110
|
-
{ name: '
|
|
111
|
-
{ name: '
|
|
105
|
+
{ name: 'deleteTable', label: 'Delete Table', icon: 'delete_sweep' },
|
|
106
|
+
{ name: 'mergeCells', label: 'Merge Cells', icon: 'table_chart' },
|
|
107
|
+
{ name: 'splitCells', label: 'Split Cells', icon: 'dashboard' },
|
|
108
|
+
{ name: 'addRowBefore', label: 'Insert Row Above', icon: 'add_box' },
|
|
109
|
+
{ name: 'addRowAfter', label: 'Insert Row Below', icon: 'vertical_align_bottom' },
|
|
110
|
+
{ name: 'deleteRow', label: 'Delete Row', icon: 'remove' },
|
|
111
|
+
{ name: 'insertColumnLeft', label: 'Insert Column Left', icon: 'format_indent_decrease' },
|
|
112
|
+
{ name: 'insertColumnRight', label: 'Insert Column Right', icon: 'format_indent_increase' },
|
|
113
|
+
{ name: 'deleteColumn', label: 'Delete Column', icon: 'view_week' },
|
|
112
114
|
{ name: 'separator' },
|
|
113
115
|
{ name: 'undo', label: 'Undo', icon: 'undo' },
|
|
114
116
|
{ name: 'redo', label: 'Redo', icon: 'redo' },
|
|
@@ -41,13 +41,10 @@ function createFormattingCommand(state: EditorState, type: 'text' | 'block' | 'l
|
|
|
41
41
|
if (!state.doc) return
|
|
42
42
|
|
|
43
43
|
if (type === 'text') {
|
|
44
|
-
|
|
45
|
-
else if (command === 'italic') state.doc.execCommand('italic', false)
|
|
46
|
-
else if (command === 'underline') state.doc.execCommand('underline', false)
|
|
47
|
-
else format.text(command)
|
|
44
|
+
format.text(command)
|
|
48
45
|
}
|
|
49
46
|
else if (type === 'block') {
|
|
50
|
-
|
|
47
|
+
format.block(command, tag || command)
|
|
51
48
|
}
|
|
52
49
|
else if (type === 'list') {
|
|
53
50
|
const selection = state.doc.getSelection()
|
|
@@ -82,11 +79,7 @@ function createFormattingCommand(state: EditorState, type: 'text' | 'block' | 'l
|
|
|
82
79
|
selection.removeAllRanges()
|
|
83
80
|
selection.addRange(range)
|
|
84
81
|
} else {
|
|
85
|
-
|
|
86
|
-
state.doc.execCommand(
|
|
87
|
-
command === 'orderedList' ? 'insertOrderedList' : 'insertUnorderedList',
|
|
88
|
-
false
|
|
89
|
-
)
|
|
82
|
+
format.list(command)
|
|
90
83
|
}
|
|
91
84
|
}
|
|
92
85
|
},
|
|
@@ -96,15 +89,16 @@ function createFormattingCommand(state: EditorState, type: 'text' | 'block' | 'l
|
|
|
96
89
|
|
|
97
90
|
// Helper function to check if a node is empty (contains only whitespace or <br>)
|
|
98
91
|
function isNodeEmpty(node: Node): boolean {
|
|
99
|
-
|
|
92
|
+
// Check for text content after removing whitespace and nonbreaking spaces
|
|
93
|
+
const text = node.textContent?.replace(/\s/g, '') || ''
|
|
100
94
|
if (text) return false
|
|
101
95
|
|
|
102
96
|
// Check for <br> tags
|
|
103
|
-
const brElements = (node as Element).getElementsByTagName('br')
|
|
97
|
+
const brElements = (node as Element).getElementsByTagName ? (node as Element).getElementsByTagName('br') : []
|
|
104
98
|
if (brElements.length === 0) return true
|
|
105
99
|
|
|
106
|
-
// If there's only one <br> and it's the only content, consider it empty
|
|
107
|
-
return brElements.length === 1 && node.childNodes.length
|
|
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>
|
|
108
102
|
}
|
|
109
103
|
|
|
110
104
|
export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
@@ -112,25 +106,226 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
112
106
|
|
|
113
107
|
// History commands
|
|
114
108
|
const historyCommands = {
|
|
115
|
-
undo: createCommand('Undo', () =>
|
|
116
|
-
|
|
109
|
+
undo: createCommand('Undo', () => {
|
|
110
|
+
if (state.undoStack.length > 0 && state.doc) {
|
|
111
|
+
const lastContent = state.undoStack.pop()
|
|
112
|
+
if (lastContent !== undefined) {
|
|
113
|
+
state.redoStack.push(state.content)
|
|
114
|
+
state.content = lastContent
|
|
115
|
+
if (state.doc) state.doc.body.innerHTML = lastContent
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}),
|
|
119
|
+
redo: createCommand('Redo', () => {
|
|
120
|
+
if (state.redoStack.length > 0 && state.doc) {
|
|
121
|
+
const nextContent = state.redoStack.pop()
|
|
122
|
+
if (nextContent !== undefined) {
|
|
123
|
+
state.undoStack.push(state.content)
|
|
124
|
+
state.content = nextContent
|
|
125
|
+
if (state.doc) state.doc.body.innerHTML = nextContent
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
})
|
|
117
129
|
}
|
|
118
130
|
|
|
119
131
|
// Basic text formatting commands
|
|
120
|
-
const textCommands =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
132
|
+
const textCommands = {
|
|
133
|
+
bold: createCommand('Bold', (state) => {
|
|
134
|
+
console.log('[Command] Bold called')
|
|
135
|
+
if (!state.doc || !state.range || !state.selection) return
|
|
136
|
+
|
|
137
|
+
const { range } = state
|
|
138
|
+
if (range.collapsed) return
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Create a span with bold style
|
|
142
|
+
const span = state.doc.createElement('span')
|
|
143
|
+
span.style.fontWeight = 'bold'
|
|
144
|
+
|
|
145
|
+
// Try to surround contents, if that fails use extract and insert
|
|
146
|
+
try {
|
|
147
|
+
range.surroundContents(span)
|
|
148
|
+
} catch (e) {
|
|
149
|
+
// Handle selections that cross node boundaries
|
|
150
|
+
const fragment = range.extractContents()
|
|
151
|
+
span.appendChild(fragment)
|
|
152
|
+
range.insertNode(span)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Select the new formatted content
|
|
156
|
+
range.selectNodeContents(span)
|
|
157
|
+
state.selection.removeAllRanges()
|
|
158
|
+
state.selection.addRange(range)
|
|
159
|
+
|
|
160
|
+
// Update content
|
|
161
|
+
state.content = state.doc.body.innerHTML
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.error('[Command] Error applying bold:', error)
|
|
164
|
+
}
|
|
165
|
+
}),
|
|
166
|
+
|
|
167
|
+
italic: createCommand('Italic', (state) => {
|
|
168
|
+
console.log('[Command] Italic called')
|
|
169
|
+
if (!state.doc || !state.range || !state.selection) return
|
|
170
|
+
|
|
171
|
+
const { range } = state
|
|
172
|
+
if (range.collapsed) return
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
// Create a span with italic style
|
|
176
|
+
const span = state.doc.createElement('span')
|
|
177
|
+
span.style.fontStyle = 'italic'
|
|
178
|
+
|
|
179
|
+
// Try to surround contents, if that fails use extract and insert
|
|
180
|
+
try {
|
|
181
|
+
range.surroundContents(span)
|
|
182
|
+
} catch (e) {
|
|
183
|
+
// Handle selections that cross node boundaries
|
|
184
|
+
const fragment = range.extractContents()
|
|
185
|
+
span.appendChild(fragment)
|
|
186
|
+
range.insertNode(span)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Select the new formatted content
|
|
190
|
+
range.selectNodeContents(span)
|
|
191
|
+
state.selection.removeAllRanges()
|
|
192
|
+
state.selection.addRange(range)
|
|
193
|
+
|
|
194
|
+
// Update content
|
|
195
|
+
state.content = state.doc.body.innerHTML
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error('[Command] Error applying italic:', error)
|
|
198
|
+
}
|
|
199
|
+
}),
|
|
200
|
+
|
|
201
|
+
underline: createCommand('Underline', (state) => {
|
|
202
|
+
console.log('[Command] Underline called')
|
|
203
|
+
if (!state.doc || !state.range || !state.selection) return
|
|
204
|
+
|
|
205
|
+
const { range } = state
|
|
206
|
+
if (range.collapsed) return
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
// Create a span with underline style
|
|
210
|
+
const span = state.doc.createElement('span')
|
|
211
|
+
span.style.textDecoration = 'underline'
|
|
212
|
+
|
|
213
|
+
// Try to surround contents, if that fails use extract and insert
|
|
214
|
+
try {
|
|
215
|
+
range.surroundContents(span)
|
|
216
|
+
} catch (e) {
|
|
217
|
+
// Handle selections that cross node boundaries
|
|
218
|
+
const fragment = range.extractContents()
|
|
219
|
+
span.appendChild(fragment)
|
|
220
|
+
range.insertNode(span)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Select the new formatted content
|
|
224
|
+
range.selectNodeContents(span)
|
|
225
|
+
state.selection.removeAllRanges()
|
|
226
|
+
state.selection.addRange(range)
|
|
227
|
+
|
|
228
|
+
// Update content
|
|
229
|
+
state.content = state.doc.body.innerHTML
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error('[Command] Error applying underline:', error)
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
}
|
|
124
235
|
|
|
125
236
|
// Heading commands
|
|
126
237
|
const headingCommands = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].reduce((acc, cmd) => ({
|
|
127
238
|
...acc,
|
|
128
|
-
[cmd]:
|
|
239
|
+
[cmd]: createCommand(`Heading ${cmd}`, (state) => {
|
|
240
|
+
console.log(`[Command] ${cmd} heading called`)
|
|
241
|
+
if (!state.doc || !state.range || !state.selection) return
|
|
242
|
+
|
|
243
|
+
const { range, doc, selection } = state
|
|
244
|
+
const container = range.commonAncestorContainer
|
|
245
|
+
const parentBlock = container.nodeType === 3 ? container.parentElement : container as HTMLElement
|
|
246
|
+
|
|
247
|
+
// Find current block element
|
|
248
|
+
const currentBlock = parentBlock?.closest('p,h1,h2,h3,h4,h5,h6,blockquote,div') || parentBlock
|
|
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
|
|
254
|
+
const newTag = isToggleOff ? 'p' : cmd
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
// Create a new block element of the proper type
|
|
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)
|
|
268
|
+
|
|
269
|
+
// Move cursor into the new block
|
|
270
|
+
range.selectNodeContents(newBlock)
|
|
271
|
+
range.collapse(false) // Move to end
|
|
272
|
+
selection.removeAllRanges()
|
|
273
|
+
selection.addRange(range)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Update content
|
|
277
|
+
state.content = doc.body.innerHTML
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.error(`[Command] Error applying ${cmd} heading:`, error)
|
|
280
|
+
}
|
|
281
|
+
}),
|
|
129
282
|
}), {})
|
|
130
283
|
|
|
131
284
|
// Block commands
|
|
132
285
|
const blockCommands = {
|
|
133
|
-
p:
|
|
286
|
+
p: createCommand('Paragraph', (state) => {
|
|
287
|
+
console.log('[Command] Paragraph called')
|
|
288
|
+
if (!state.doc || !state.range || !state.selection) return
|
|
289
|
+
|
|
290
|
+
const { range, doc, selection } = state
|
|
291
|
+
const container = range.commonAncestorContainer
|
|
292
|
+
const parentBlock = container.nodeType === 3 ? container.parentElement : container as HTMLElement
|
|
293
|
+
|
|
294
|
+
// Find current block element
|
|
295
|
+
const currentBlock = parentBlock?.closest('p,h1,h2,h3,h4,h5,h6,blockquote,div') || parentBlock
|
|
296
|
+
|
|
297
|
+
// Check if already a paragraph - if so, nothing to do
|
|
298
|
+
if (currentBlock?.tagName.toLowerCase() === 'p') {
|
|
299
|
+
console.log('[Command] Already a paragraph')
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
// Create a new paragraph
|
|
305
|
+
const newParagraph = doc.createElement('p')
|
|
306
|
+
|
|
307
|
+
if (currentBlock) {
|
|
308
|
+
// Copy content from current block to paragraph
|
|
309
|
+
while (currentBlock.firstChild) {
|
|
310
|
+
newParagraph.appendChild(currentBlock.firstChild)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Replace the old block with the paragraph
|
|
314
|
+
currentBlock.parentNode?.replaceChild(newParagraph, currentBlock)
|
|
315
|
+
|
|
316
|
+
// Move cursor into the new paragraph
|
|
317
|
+
range.selectNodeContents(newParagraph)
|
|
318
|
+
range.collapse(false) // Move to end
|
|
319
|
+
selection.removeAllRanges()
|
|
320
|
+
selection.addRange(range)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Update content
|
|
324
|
+
state.content = doc.body.innerHTML
|
|
325
|
+
} catch (error) {
|
|
326
|
+
console.error('[Command] Error applying paragraph:', error)
|
|
327
|
+
}
|
|
328
|
+
}),
|
|
134
329
|
blockquote: createFormattingCommand(state, 'block', 'blockquote')
|
|
135
330
|
}
|
|
136
331
|
|
|
@@ -179,7 +374,96 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
179
374
|
|
|
180
375
|
// Other formatting commands
|
|
181
376
|
const otherCommands = {
|
|
182
|
-
clear: createCommand('Clear Formatting', () => {
|
|
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
|
+
}),
|
|
183
467
|
indent: createCommand('Indent', () => { format.text('indent') }),
|
|
184
468
|
outdent: createCommand('Outdent', () => { format.text('outdent') })
|
|
185
469
|
}
|
|
@@ -28,7 +28,22 @@ export function formatting(state: EditorState) {
|
|
|
28
28
|
range.surroundContents(span)
|
|
29
29
|
}
|
|
30
30
|
} else {
|
|
31
|
-
|
|
31
|
+
if (range.collapsed) return // No selection, nothing to format
|
|
32
|
+
|
|
33
|
+
const span = doc.createElement('span')
|
|
34
|
+
if (command === 'bold') span.style.fontWeight = 'bold'
|
|
35
|
+
else if (command === 'italic') span.style.fontStyle = 'italic'
|
|
36
|
+
else if (command === 'underline') span.style.textDecoration = 'underline'
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
range.surroundContents(span)
|
|
40
|
+
} catch (e) {
|
|
41
|
+
// If surroundContents fails (e.g., for selections across multiple nodes)
|
|
42
|
+
// Extract the fragment, wrap it, and insert it back
|
|
43
|
+
const fragment = range.extractContents()
|
|
44
|
+
span.appendChild(fragment)
|
|
45
|
+
range.insertNode(span)
|
|
46
|
+
}
|
|
32
47
|
}
|
|
33
48
|
}
|
|
34
49
|
|
|
@@ -53,12 +68,27 @@ export function formatting(state: EditorState) {
|
|
|
53
68
|
}
|
|
54
69
|
}
|
|
55
70
|
|
|
56
|
-
|
|
71
|
+
// Create a new block element of the desired type
|
|
72
|
+
const newBlock = doc.createElement(tag)
|
|
73
|
+
if (isRTL) newBlock.dir = 'rtl'
|
|
74
|
+
|
|
75
|
+
// Find the current block to replace
|
|
76
|
+
const currentBlock = parentBlock?.closest('p,h1,h2,h3,h4,h5,h6,blockquote,div') || parentBlock
|
|
77
|
+
|
|
78
|
+
if (currentBlock) {
|
|
79
|
+
// Copy content to the new block
|
|
80
|
+
while (currentBlock.firstChild) {
|
|
81
|
+
newBlock.appendChild(currentBlock.firstChild)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Replace the current block with the new one
|
|
85
|
+
currentBlock.parentNode?.replaceChild(newBlock, currentBlock)
|
|
57
86
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
87
|
+
// Move cursor into the new block
|
|
88
|
+
range.selectNodeContents(newBlock)
|
|
89
|
+
range.collapse(false)
|
|
90
|
+
selection.removeAllRanges()
|
|
91
|
+
selection.addRange(range)
|
|
62
92
|
}
|
|
63
93
|
}
|
|
64
94
|
|
|
@@ -145,38 +175,97 @@ export function formatting(state: EditorState) {
|
|
|
145
175
|
}
|
|
146
176
|
|
|
147
177
|
const clear = () => {
|
|
148
|
-
|
|
178
|
+
console.log('[Clear Format] Starting clear format process', state)
|
|
179
|
+
console.assert(state, '[Clear Format] State must exist')
|
|
180
|
+
console.assert(state.doc, '[Clear Format] Document must exist')
|
|
181
|
+
console.assert(state.range, '[Clear Format] Range must exist')
|
|
182
|
+
console.assert(state.selection, '[Clear Format] Selection must exist')
|
|
183
|
+
|
|
184
|
+
if (!state.doc || !state.range || !state.selection) {
|
|
185
|
+
console.log('[Clear Format] No document or selection')
|
|
186
|
+
return
|
|
187
|
+
}
|
|
149
188
|
|
|
150
189
|
const selectionInfo = analyzeSelection(state.doc, state.range)
|
|
151
|
-
|
|
190
|
+
console.log('[Clear Format] Selection info:', selectionInfo)
|
|
191
|
+
if (!selectionInfo) {
|
|
192
|
+
console.log('[Clear Format] No valid selection info')
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// If selection is collapsed (just a cursor), return
|
|
197
|
+
if (state.range.collapsed) {
|
|
198
|
+
console.log('[Clear Format] Selection is collapsed, nothing to clear')
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
console.log('[Clear Format] Processing selection:', {
|
|
203
|
+
startContainer: state.range.startContainer,
|
|
204
|
+
endContainer: state.range.endContainer,
|
|
205
|
+
selectedText: state.range.toString()
|
|
206
|
+
})
|
|
152
207
|
|
|
153
|
-
|
|
208
|
+
// Get the selected content
|
|
154
209
|
const fragment = state.range.cloneContents()
|
|
155
210
|
const tempDiv = state.doc.createElement('div')
|
|
156
211
|
tempDiv.appendChild(fragment)
|
|
157
212
|
|
|
158
|
-
|
|
213
|
+
console.log('[Clear Format] Original HTML:', tempDiv.innerHTML)
|
|
214
|
+
|
|
215
|
+
// Function to recursively clean a node and its children
|
|
159
216
|
const cleanNode = (node: Node): Node => {
|
|
160
217
|
if (!state.doc) return node
|
|
161
|
-
|
|
218
|
+
|
|
219
|
+
// Text nodes can be returned as-is
|
|
220
|
+
if (node.nodeType === 3) {
|
|
162
221
|
return node.cloneNode(true)
|
|
163
222
|
}
|
|
164
223
|
|
|
165
224
|
if (node.nodeType === 1) { // Element node
|
|
166
225
|
const el = node as HTMLElement
|
|
167
226
|
const nodeName = el.nodeName.toLowerCase()
|
|
227
|
+
const inlineTags = ['b', 'i', 'u', 'strong', 'em', 'span', 'font', 'strike', 'sub', 'sup']
|
|
228
|
+
|
|
229
|
+
console.log('[Clear Format] Processing element:', nodeName, {
|
|
230
|
+
hasStyle: el.hasAttribute('style'),
|
|
231
|
+
style: el.getAttribute('style'),
|
|
232
|
+
className: el.className
|
|
233
|
+
})
|
|
168
234
|
|
|
169
|
-
//
|
|
235
|
+
// For inline formatting elements, just extract the text content
|
|
170
236
|
if (inlineTags.includes(nodeName)) {
|
|
171
|
-
|
|
237
|
+
// Create a text node with the element's content
|
|
238
|
+
const textContent = el.textContent || ''
|
|
239
|
+
console.log('[Clear Format] Extracting text from inline element:', textContent)
|
|
240
|
+
return state.doc.createTextNode(textContent)
|
|
172
241
|
}
|
|
173
242
|
|
|
174
|
-
// For block elements,
|
|
243
|
+
// For block elements, preserve the element type but remove formatting
|
|
175
244
|
const newEl = state.doc.createElement(nodeName)
|
|
176
245
|
|
|
177
|
-
// Remove
|
|
178
|
-
|
|
179
|
-
|
|
246
|
+
// Remove any style and class attributes
|
|
247
|
+
if (el.hasAttribute('style')) {
|
|
248
|
+
console.log('[Clear Format] Removing style attribute:', el.getAttribute('style'))
|
|
249
|
+
}
|
|
250
|
+
if (el.className) {
|
|
251
|
+
console.log('[Clear Format] Removing class:', el.className)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Transfer only specific attributes we want to keep (like href for links)
|
|
255
|
+
if (nodeName === 'a' && el.hasAttribute('href')) {
|
|
256
|
+
newEl.setAttribute('href', el.getAttribute('href') || '')
|
|
257
|
+
if (el.hasAttribute('target')) {
|
|
258
|
+
newEl.setAttribute('target', el.getAttribute('target') || '')
|
|
259
|
+
}
|
|
260
|
+
console.log('[Clear Format] Preserving link attributes')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// For images, preserve src and alt
|
|
264
|
+
if (nodeName === 'img') {
|
|
265
|
+
if (el.hasAttribute('src')) newEl.setAttribute('src', el.getAttribute('src') || '')
|
|
266
|
+
if (el.hasAttribute('alt')) newEl.setAttribute('alt', el.getAttribute('alt') || '')
|
|
267
|
+
console.log('[Clear Format] Preserving image attributes')
|
|
268
|
+
}
|
|
180
269
|
|
|
181
270
|
// Clean and append all child nodes
|
|
182
271
|
Array.from(el.childNodes).forEach((child) => {
|
|
@@ -195,10 +284,21 @@ export function formatting(state: EditorState) {
|
|
|
195
284
|
cleanedFragment.appendChild(cleanNode(node))
|
|
196
285
|
})
|
|
197
286
|
|
|
287
|
+
// For debugging: check what the cleaned HTML looks like
|
|
288
|
+
const debugDiv = state.doc.createElement('div')
|
|
289
|
+
debugDiv.appendChild(cleanedFragment.cloneNode(true))
|
|
290
|
+
console.log('[Clear Format] Cleaned HTML:', debugDiv.innerHTML)
|
|
291
|
+
|
|
198
292
|
// Replace the content
|
|
199
293
|
state.range.deleteContents()
|
|
200
294
|
state.range.insertNode(cleanedFragment)
|
|
295
|
+
|
|
296
|
+
// Restore selection
|
|
201
297
|
restoreSelection(state.doc, state.range, state.selection, selectionInfo)
|
|
298
|
+
|
|
299
|
+
// Update content state
|
|
300
|
+
state.content = state.doc.body.innerHTML
|
|
301
|
+
console.log('[Clear Format] Updated document HTML:', state.content)
|
|
202
302
|
}
|
|
203
303
|
|
|
204
304
|
return { text, block, list, clear }
|