@bagelink/vue 1.4.62 → 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.
@@ -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 = '&nbsp;<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 &nbsp;), consider it empty
101
- return brElements.length === 1 && node.childNodes.length <= 2 // Allow for &nbsp; + <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
- if (state.doc) state.doc.body.innerHTML = lastContent
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
- if (state.doc) state.doc.body.innerHTML = nextContent
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
- console.log('[Command] Bold called')
135
- if (!state.doc || !state.range || !state.selection) return
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
- // Create a span with bold style
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
- state.selection.removeAllRanges()
158
- state.selection.addRange(range)
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('[Command] Error applying bold:', error)
120
+ console.error('Error applying bold:', error)
164
121
  }
165
122
  }),
166
123
 
167
124
  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
125
+ const selectionInfo = getCurrentSelection(state)
126
+ if (!selectionInfo || selectionInfo.range.collapsed) return
173
127
 
128
+ const { range } = selectionInfo
174
129
  try {
175
- // Create a span with italic style
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
- state.selection.removeAllRanges()
192
- state.selection.addRange(range)
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('[Command] Error applying italic:', error)
145
+ console.error('Error applying italic:', error)
198
146
  }
199
147
  }),
200
148
 
201
149
  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
150
+ const selectionInfo = getCurrentSelection(state)
151
+ if (!selectionInfo || selectionInfo.range.collapsed) return
207
152
 
153
+ const { range } = selectionInfo
208
154
  try {
209
- // Create a span with underline style
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
- state.selection.removeAllRanges()
226
- state.selection.addRange(range)
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('[Command] Error applying underline:', 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
- console.log(`[Command] ${cmd} heading called`)
241
- if (!state.doc || !state.range || !state.selection) return
179
+ const selectionInfo = getCurrentSelection(state)
180
+ if (!selectionInfo) return
242
181
 
243
- const { range, doc, selection } = state
244
- const container = range.commonAncestorContainer
245
- const parentBlock = container.nodeType === 3 ? container.parentElement : container as HTMLElement
182
+ const { range } = selectionInfo
183
+ const currentBlock = findBlockElement(range.commonAncestorContainer)
184
+ if (!currentBlock) return
246
185
 
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
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
- // 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)
191
+ const newBlock = state.doc!.createElement(newTag)
268
192
 
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)
193
+ // Copy content from current block to new block
194
+ while (currentBlock.firstChild) {
195
+ newBlock.appendChild(currentBlock.firstChild)
274
196
  }
275
197
 
276
- // Update content
277
- state.content = doc.body.innerHTML
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(`[Command] Error applying ${cmd} heading:`, 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
- 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
215
+ const selectionInfo = getCurrentSelection(state)
216
+ if (!selectionInfo) return
293
217
 
294
- // Find current block element
295
- const currentBlock = parentBlock?.closest('p,h1,h2,h3,h4,h5,h6,blockquote,div') || parentBlock
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?.tagName.toLowerCase() === 'p') {
299
- console.log('[Command] Already a paragraph')
223
+ if (currentBlock.tagName.toLowerCase() === 'p') {
300
224
  return
301
225
  }
302
226
 
303
227
  try {
304
- // Create a new paragraph
305
- const newParagraph = doc.createElement('p')
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
- if (currentBlock) {
308
- // Copy content from current block to paragraph
309
- while (currentBlock.firstChild) {
310
- newParagraph.appendChild(currentBlock.firstChild)
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
- // Replace the old block with the paragraph
314
- currentBlock.parentNode?.replaceChild(newParagraph, currentBlock)
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
- // Move cursor into the new paragraph
317
- range.selectNodeContents(newParagraph)
318
- range.collapse(false) // Move to end
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
- // Update content
324
- state.content = doc.body.innerHTML
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('[Command] Error applying paragraph:', error)
348
+ console.error('Error creating ordered list:', error)
327
349
  }
328
350
  }),
329
- blockquote: createFormattingCommand(state, 'block', 'blockquote')
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
- // List commands
333
- const listCommands = {
334
- orderedList: createFormattingCommand(state, 'list', 'orderedList'),
335
- unorderedList: createFormattingCommand(state, 'list', 'unorderedList')
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