@bagelink/vue 0.0.1025 → 0.0.1031
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/README.md +1 -0
- package/dist/components/Image.vue.d.ts.map +1 -1
- package/dist/components/ToolBar.vue.d.ts +3 -0
- package/dist/components/ToolBar.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/NumberInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RangeInput.vue.d.ts +2 -2
- package/dist/components/form/inputs/RangeInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts +12 -0
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts +9 -0
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/composables/useEditor.d.ts +40 -20
- 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 +1 -0
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/commands.d.ts +17 -0
- package/dist/components/form/inputs/RichText/utils/commands.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/debug.d.ts +48 -0
- package/dist/components/form/inputs/RichText/utils/debug.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts +3 -1
- package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts +13 -0
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts +4 -0
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/form/inputs/SignaturePad.vue.d.ts +2 -2
- package/dist/components/form/inputs/SignaturePad.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/Upload/UploadFile.vue.d.ts +86 -0
- package/dist/components/form/inputs/Upload/UploadFile.vue.d.ts.map +1 -0
- package/dist/components/form/inputs/Upload/upload.d.ts +13 -0
- package/dist/components/form/inputs/Upload/upload.d.ts.map +1 -0
- package/dist/components/form/inputs/index.d.ts +1 -0
- package/dist/components/form/inputs/index.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/editor-B3mMCQSg.cjs +4 -0
- package/dist/editor-BKPRpAjr.js +4 -0
- package/dist/index.cjs +10059 -6970
- package/dist/index.mjs +10065 -6976
- package/dist/plugins/bagel.d.ts +1 -0
- package/dist/plugins/bagel.d.ts.map +1 -1
- package/dist/style.css +666 -458
- package/package.json +1 -1
- package/src/components/Image.vue +1 -2
- package/src/components/ToolBar.vue +9 -0
- package/src/components/form/inputs/NumberInput.vue +8 -1
- package/src/components/form/inputs/RangeInput.vue +6 -5
- package/src/components/form/inputs/RichText/composables/useCommands.ts +114 -0
- package/src/components/form/inputs/RichText/composables/useEditor.ts +257 -129
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +64 -19
- package/src/components/form/inputs/RichText/config.ts +18 -2
- package/src/components/form/inputs/RichText/editor.css +17 -15
- package/src/components/form/inputs/RichText/index.vue +67 -13
- package/src/components/form/inputs/RichText/richTextTypes.d.ts +2 -0
- package/src/components/form/inputs/RichText/utils/commands.ts +37 -0
- package/src/components/form/inputs/RichText/utils/debug.ts +196 -0
- package/src/components/form/inputs/RichText/utils/formatting.ts +168 -288
- package/src/components/form/inputs/RichText/utils/selection.ts +77 -0
- package/src/components/form/inputs/RichText/utils/table.ts +66 -0
- package/src/components/form/inputs/SignaturePad.vue +2 -2
- package/src/components/form/inputs/Upload/UploadFile.vue +357 -0
- package/src/components/form/inputs/Upload/upload.css +232 -0
- package/src/components/form/inputs/Upload/upload.ts +22 -0
- package/src/components/form/inputs/Upload/upload.types.d.ts +43 -0
- package/src/components/form/inputs/index.ts +1 -0
- package/src/components/index.ts +2 -0
- package/src/plugins/bagel.ts +2 -2
- /package/src/components/form/inputs/RichText/components/{Toolbar.vue → EditorToolbar.vue} +0 -0
|
@@ -1,345 +1,225 @@
|
|
|
1
1
|
import type { EditorState } from '../richTextTypes'
|
|
2
|
+
import { analyzeSelection, restoreSelection, getBlocksBetween } from './selection'
|
|
2
3
|
|
|
3
4
|
export function formatting(state: EditorState) {
|
|
4
|
-
const { doc
|
|
5
|
-
|
|
6
|
-
if (!range) return
|
|
7
|
-
range.setStart(node, start)
|
|
8
|
-
range.setEnd(node, end)
|
|
9
|
-
}
|
|
10
|
-
const wrapWithTag = (content: string, tag: string): HTMLElement => {
|
|
11
|
-
const wrapper = doc!.createElement(tag)
|
|
12
|
-
wrapper.textContent = content
|
|
13
|
-
return wrapper
|
|
14
|
-
}
|
|
5
|
+
const { doc } = state
|
|
6
|
+
if (!doc) return { text: () => {}, block: () => {}, list: () => {}, clear: () => {} }
|
|
15
7
|
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
return block
|
|
20
|
-
}
|
|
21
|
-
const clear = () => {
|
|
22
|
-
if (!doc || !range || !selection) return
|
|
8
|
+
const text = (command: string) => {
|
|
9
|
+
const selection = doc.getSelection()
|
|
10
|
+
if (!selection || !selection.rangeCount) return
|
|
23
11
|
|
|
24
|
-
const
|
|
12
|
+
const range = selection.getRangeAt(0)
|
|
13
|
+
const container = range.commonAncestorContainer
|
|
14
|
+
const parentBlock = container.nodeType === 3 ? container.parentElement : container as HTMLElement
|
|
25
15
|
|
|
26
|
-
|
|
16
|
+
// Preserve RTL direction when applying text formatting
|
|
17
|
+
const isRTL = parentBlock?.closest('[dir="rtl"]') !== null
|
|
27
18
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
// Don't apply inline styles directly to block elements
|
|
20
|
+
if (parentBlock?.tagName.match(/^H[1-6]|P|BLOCKQUOTE|LI$/)) {
|
|
21
|
+
if (!range.collapsed && range.toString().trim()) {
|
|
22
|
+
const span = doc.createElement('span') as HTMLSpanElement
|
|
23
|
+
if (command === 'underline') span.style.textDecoration = 'underline'
|
|
24
|
+
else if (command === 'bold') span.style.fontWeight = 'bold'
|
|
25
|
+
else if (command === 'italic') span.style.fontStyle = 'italic'
|
|
32
26
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Helper function to recursively clean inline tags
|
|
37
|
-
const cleanInlineTags = (node: Node): Node => {
|
|
38
|
-
if (node.nodeType === 1 && isInlineTag(node as HTMLElement)) {
|
|
39
|
-
// Inline element: replace it with its children
|
|
40
|
-
const fragment = doc.createDocumentFragment()
|
|
41
|
-
let child = node.firstChild // Start with the first child
|
|
42
|
-
while (child) {
|
|
43
|
-
const { nextSibling } = child // Store the next sibling before appending
|
|
44
|
-
fragment.appendChild(cleanInlineTags(child))
|
|
45
|
-
child = nextSibling // Move to the next sibling
|
|
46
|
-
}
|
|
47
|
-
return fragment
|
|
48
|
-
} else if (node.nodeType === 1) {
|
|
49
|
-
// Non-inline element: clean its children
|
|
50
|
-
const element = node.cloneNode(false) as HTMLElement
|
|
51
|
-
let child = node.firstChild // Start with the first child
|
|
52
|
-
while (child) {
|
|
53
|
-
const { nextSibling } = child // Store the next sibling before appending
|
|
54
|
-
element.appendChild(cleanInlineTags(child))
|
|
55
|
-
child = nextSibling // Move to the next sibling
|
|
56
|
-
}
|
|
57
|
-
return element
|
|
58
|
-
} else if (node.nodeType === 3) {
|
|
59
|
-
// Text node: return as is
|
|
60
|
-
return node
|
|
27
|
+
if (isRTL) span.dir = 'rtl'
|
|
28
|
+
range.surroundContents(span)
|
|
61
29
|
}
|
|
62
|
-
|
|
30
|
+
} else {
|
|
31
|
+
doc.execCommand(command, false)
|
|
63
32
|
}
|
|
33
|
+
}
|
|
64
34
|
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const cleanedChild = cleanInlineTags(child)
|
|
69
|
-
if (cleanedChild.nodeType === 11) {
|
|
70
|
-
// If it's a DocumentFragment, append its children directly
|
|
71
|
-
fragment.appendChild(cleanedChild)
|
|
72
|
-
} else {
|
|
73
|
-
fragment.appendChild(cleanedChild)
|
|
74
|
-
}
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
// Replace the range content with the cleaned content
|
|
78
|
-
range.deleteContents()
|
|
79
|
-
range.insertNode(fragment)
|
|
35
|
+
const block = (command: string, tag: string) => {
|
|
36
|
+
const selection = doc.getSelection()
|
|
37
|
+
if (!selection || !selection.rangeCount) return
|
|
80
38
|
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
39
|
+
const range = selection.getRangeAt(0)
|
|
40
|
+
const container = range.commonAncestorContainer
|
|
41
|
+
const parentBlock = container.nodeType === 3 ? container.parentElement : container as HTMLElement
|
|
42
|
+
const isRTL = parentBlock?.closest('[dir="rtl"]') !== null
|
|
43
|
+
|
|
44
|
+
// Remove any invalid inline style wrapping
|
|
45
|
+
if (parentBlock?.closest('u, b, i')) {
|
|
46
|
+
const wrapper = parentBlock.closest('u, b, i')
|
|
47
|
+
if (wrapper) {
|
|
48
|
+
const parent = wrapper.parentNode
|
|
49
|
+
while (wrapper.firstChild) {
|
|
50
|
+
parent?.insertBefore(wrapper.firstChild, wrapper)
|
|
87
51
|
}
|
|
88
|
-
|
|
89
|
-
} else if (node.nodeType === 1) {
|
|
90
|
-
// Recursively normalize child nodes
|
|
91
|
-
Array.from(node.childNodes).forEach((child) => {
|
|
92
|
-
normalizeDom(child)
|
|
93
|
-
})
|
|
52
|
+
wrapper.remove()
|
|
94
53
|
}
|
|
95
54
|
}
|
|
96
55
|
|
|
97
|
-
|
|
98
|
-
normalizeDom(child)
|
|
99
|
-
})
|
|
56
|
+
doc.execCommand('formatBlock', false, tag)
|
|
100
57
|
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
58
|
+
// Preserve RTL direction after block formatting
|
|
59
|
+
if (isRTL) {
|
|
60
|
+
const newBlock = selection.anchorNode?.parentElement?.closest(tag.toLowerCase()) as HTMLElement
|
|
61
|
+
if (newBlock) newBlock.dir = 'rtl'
|
|
62
|
+
}
|
|
105
63
|
}
|
|
106
64
|
|
|
107
|
-
const
|
|
108
|
-
if (!doc || !range || !selection) return
|
|
109
|
-
const { startOffset, endOffset } = range
|
|
110
|
-
|
|
111
|
-
let tag = ''
|
|
112
|
-
if (command === 'bold') tag = 'b'
|
|
113
|
-
if (command === 'italic') tag = 'i'
|
|
114
|
-
if (command === 'underline') tag = 'u'
|
|
115
|
-
if (command === 'emphasis') tag = 'em'
|
|
116
|
-
if (command === 'strong') tag = 'strong'
|
|
117
|
-
|
|
118
|
-
const relatedTags: { [key: string]: string[] } = {
|
|
119
|
-
b: ['strong'],
|
|
120
|
-
i: ['em'],
|
|
121
|
-
u: ['u'],
|
|
122
|
-
em: ['i'],
|
|
123
|
-
strong: ['b'],
|
|
124
|
-
}
|
|
65
|
+
const list = (command: string) => {
|
|
66
|
+
if (!state.doc || !state.range || !state.selection) return
|
|
125
67
|
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (range.collapsed) {
|
|
130
|
-
// Handle collapsed range
|
|
131
|
-
const container = range.commonAncestorContainer
|
|
132
|
-
const parent = container.parentElement
|
|
133
|
-
if (parent && isTagMatch(parent)) {
|
|
134
|
-
const textNode = doc.createTextNode(parent.textContent || '')
|
|
135
|
-
parent.parentNode?.replaceChild(textNode, parent)
|
|
136
|
-
range.selectNodeContents(textNode)
|
|
137
|
-
selection.removeAllRanges()
|
|
138
|
-
setRangeAndSelect(textNode, startOffset, endOffset)
|
|
139
|
-
selection.addRange(range)
|
|
140
|
-
}
|
|
141
|
-
return
|
|
142
|
-
}
|
|
68
|
+
const listTag = command === 'orderedList' ? 'ol' : 'ul'
|
|
69
|
+
const selectionInfo = analyzeSelection(state.doc, state.range)
|
|
70
|
+
if (!selectionInfo) return
|
|
143
71
|
|
|
144
|
-
const
|
|
145
|
-
const parent = container.parentElement
|
|
146
|
-
|
|
147
|
-
if (parent && isTagMatch(parent)) {
|
|
148
|
-
// Split text content into three parts: before, selected, and after
|
|
149
|
-
const parentText = parent.textContent || ''
|
|
150
|
-
const beforeText = parentText.substring(0, range.startOffset - parentText.indexOf(parent.textContent || ''))
|
|
151
|
-
const selectedText = parentText.substring(range.startOffset, range.endOffset)
|
|
152
|
-
const afterText = parentText.substring(range.endOffset)
|
|
153
|
-
|
|
154
|
-
// Create text nodes for before, selected, and after
|
|
155
|
-
if (beforeText) {
|
|
156
|
-
const beforeNode = wrapWithTag(beforeText, tag)
|
|
157
|
-
parent.parentNode?.insertBefore(beforeNode, parent)
|
|
158
|
-
}
|
|
72
|
+
const isRTL = selectionInfo.parent.closest('[dir="rtl"]') !== null
|
|
159
73
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
74
|
+
// If we're inside a list item, handle toggling off or switching list type
|
|
75
|
+
const listItem = selectionInfo.parent.closest('li')
|
|
76
|
+
if (listItem) {
|
|
77
|
+
const listParent = listItem.parentElement
|
|
78
|
+
if (!listParent) return
|
|
163
79
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
80
|
+
// If it's the same list type, toggle off
|
|
81
|
+
if (listParent.tagName.toLowerCase() === listTag) {
|
|
82
|
+
// Create a paragraph from the list item content
|
|
83
|
+
const p = state.doc.createElement('p') as HTMLParagraphElement
|
|
84
|
+
if (isRTL) p.dir = 'rtl'
|
|
85
|
+
while (listItem.firstChild) {
|
|
86
|
+
p.appendChild(listItem.firstChild)
|
|
87
|
+
}
|
|
168
88
|
|
|
169
|
-
|
|
170
|
-
|
|
89
|
+
listParent.parentNode?.insertBefore(p, listParent)
|
|
90
|
+
listItem.remove()
|
|
171
91
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
} else {
|
|
175
|
-
// Standard case: wrap the selection
|
|
176
|
-
const selectedContent = range.extractContents()
|
|
177
|
-
const wrapper = doc.createElement(tag)
|
|
178
|
-
wrapper.appendChild(selectedContent)
|
|
179
|
-
range.insertNode(wrapper)
|
|
180
|
-
|
|
181
|
-
// Clean up nested tags
|
|
182
|
-
const nestedTags = wrapper.querySelectorAll(`${tag},${(relatedTags[tag] || []).join(',')}`)
|
|
183
|
-
nestedTags.forEach((nestedTag) => {
|
|
184
|
-
while (nestedTag.firstChild) {
|
|
185
|
-
nestedTag.parentNode?.insertBefore(nestedTag.firstChild, nestedTag)
|
|
92
|
+
if (!listParent.querySelector('li')) {
|
|
93
|
+
listParent.remove()
|
|
186
94
|
}
|
|
187
|
-
nestedTag.parentNode?.removeChild(nestedTag)
|
|
188
|
-
})
|
|
189
95
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
96
|
+
restoreSelection(state.doc, state.range, state.selection, selectionInfo, p)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
193
99
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
100
|
+
// Convert to the other list type
|
|
101
|
+
const newList = state.doc.createElement(listTag) as HTMLElement
|
|
102
|
+
if (isRTL) newList.dir = 'rtl'
|
|
103
|
+
const allItems = Array.from(listParent.children)
|
|
104
|
+
allItems.forEach(item => newList.appendChild(item.cloneNode(true)))
|
|
105
|
+
listParent.parentNode?.replaceChild(newList, listParent)
|
|
106
|
+
|
|
107
|
+
const newListItem = newList.children[Array.from(allItems).indexOf(listItem)]
|
|
108
|
+
restoreSelection(state.doc, state.range, state.selection, selectionInfo, newListItem)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
202
111
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
112
|
+
// For single block conversion
|
|
113
|
+
if (!selectionInfo.isMultiBlock) {
|
|
114
|
+
const li = state.doc.createElement('li')
|
|
115
|
+
const listEl = state.doc.createElement(listTag) as HTMLElement
|
|
116
|
+
if (isRTL) listEl.dir = 'rtl'
|
|
207
117
|
|
|
208
|
-
|
|
209
|
-
|
|
118
|
+
while (selectionInfo.startBlock.firstChild) {
|
|
119
|
+
li.appendChild(selectionInfo.startBlock.firstChild)
|
|
120
|
+
}
|
|
210
121
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
122
|
+
listEl.appendChild(li)
|
|
123
|
+
selectionInfo.startBlock.parentNode?.replaceChild(listEl, selectionInfo.startBlock)
|
|
124
|
+
restoreSelection(state.doc, state.range, state.selection, selectionInfo, li)
|
|
125
|
+
return
|
|
215
126
|
}
|
|
216
127
|
|
|
217
|
-
selection
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const list = (command: string) => {
|
|
222
|
-
if (!doc || !range || !selection) return
|
|
223
|
-
|
|
224
|
-
const listTag = command === 'orderedList' ? 'ol' : 'ul'
|
|
225
|
-
const container = range.commonAncestorContainer
|
|
226
|
-
const parent = container.nodeType === 3 ? container.parentElement : (container as Element)
|
|
227
|
-
const currentList = parent?.closest('ul,ol')
|
|
128
|
+
// Handle multi-block selection
|
|
129
|
+
const blocks = getBlocksBetween(selectionInfo.startBlock, selectionInfo.endBlock)
|
|
130
|
+
const listEl = state.doc.createElement(listTag) as HTMLElement
|
|
131
|
+
if (isRTL) listEl.dir = 'rtl'
|
|
228
132
|
|
|
229
|
-
|
|
133
|
+
blocks.forEach((block) => {
|
|
134
|
+
if (!state.doc) return
|
|
135
|
+
const li = state.doc.createElement('li')
|
|
136
|
+
while (block.firstChild) {
|
|
137
|
+
li.appendChild(block.firstChild)
|
|
138
|
+
}
|
|
139
|
+
listEl.appendChild(li)
|
|
140
|
+
block.remove()
|
|
141
|
+
})
|
|
230
142
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const listParent = listItem.parentElement // The <ol> or <ul>
|
|
235
|
-
if (!listParent) return
|
|
143
|
+
selectionInfo.startBlock.parentNode?.insertBefore(listEl, selectionInfo.startBlock)
|
|
144
|
+
restoreSelection(state.doc, state.range, state.selection, selectionInfo, listEl)
|
|
145
|
+
}
|
|
236
146
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const afterFragment = doc.createDocumentFragment()
|
|
147
|
+
const clear = () => {
|
|
148
|
+
if (!state.doc || !state.range || !state.selection) return
|
|
240
149
|
|
|
241
|
-
|
|
242
|
-
|
|
150
|
+
const selectionInfo = analyzeSelection(state.doc, state.range)
|
|
151
|
+
if (!selectionInfo) return
|
|
243
152
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
beforeFragment.appendChild(currentNode.cloneNode(true))
|
|
249
|
-
} else {
|
|
250
|
-
afterFragment.appendChild(currentNode.cloneNode(true))
|
|
251
|
-
}
|
|
252
|
-
currentNode = currentNode.nextSibling
|
|
253
|
-
}
|
|
153
|
+
const inlineTags = ['b', 'i', 'u', 'strong', 'em', 'span']
|
|
154
|
+
const fragment = state.range.cloneContents()
|
|
155
|
+
const tempDiv = state.doc.createElement('div')
|
|
156
|
+
tempDiv.appendChild(fragment)
|
|
254
157
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
158
|
+
// Function to clean a node and its children
|
|
159
|
+
const cleanNode = (node: Node): Node => {
|
|
160
|
+
if (!state.doc) return node
|
|
161
|
+
if (node.nodeType === 3) { // Text node
|
|
162
|
+
return node.cloneNode(true)
|
|
259
163
|
}
|
|
260
164
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
165
|
+
if (node.nodeType === 1) { // Element node
|
|
166
|
+
const el = node as HTMLElement
|
|
167
|
+
const nodeName = el.nodeName.toLowerCase()
|
|
264
168
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
169
|
+
// If it's an inline formatting element, just return its text content
|
|
170
|
+
if (inlineTags.includes(nodeName)) {
|
|
171
|
+
return state.doc.createTextNode(el.textContent || '')
|
|
172
|
+
}
|
|
270
173
|
|
|
271
|
-
|
|
174
|
+
// For block elements, create a new clean element
|
|
175
|
+
const newEl = state.doc.createElement(nodeName)
|
|
272
176
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
grandParent.insertBefore(newListAfter, listParent.nextSibling)
|
|
277
|
-
}
|
|
177
|
+
// Remove all styles and classes
|
|
178
|
+
newEl.removeAttribute('style')
|
|
179
|
+
newEl.removeAttribute('class')
|
|
278
180
|
|
|
279
|
-
|
|
280
|
-
|
|
181
|
+
// Clean and append all child nodes
|
|
182
|
+
Array.from(el.childNodes).forEach((child) => {
|
|
183
|
+
newEl.appendChild(cleanNode(child))
|
|
184
|
+
})
|
|
281
185
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
range.selectNodeContents(standaloneBlock)
|
|
285
|
-
selection.addRange(range)
|
|
186
|
+
return newEl
|
|
187
|
+
}
|
|
286
188
|
|
|
287
|
-
return
|
|
189
|
+
return node.cloneNode(true)
|
|
288
190
|
}
|
|
289
191
|
|
|
290
|
-
//
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const blocks = []
|
|
297
|
-
let currentBlock = startBlock
|
|
298
|
-
while (currentBlock) {
|
|
299
|
-
blocks.push(currentBlock)
|
|
300
|
-
if (currentBlock === endBlock) break
|
|
301
|
-
currentBlock = currentBlock.nextElementSibling as Element
|
|
302
|
-
}
|
|
192
|
+
// Clean all nodes in the fragment
|
|
193
|
+
const cleanedFragment = state.doc.createDocumentFragment()
|
|
194
|
+
Array.from(tempDiv.childNodes).forEach((node) => {
|
|
195
|
+
cleanedFragment.appendChild(cleanNode(node))
|
|
196
|
+
})
|
|
303
197
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
paragraph.appendChild(block.firstChild)
|
|
310
|
-
}
|
|
311
|
-
fragment.appendChild(paragraph)
|
|
312
|
-
})
|
|
198
|
+
// Replace the content
|
|
199
|
+
state.range.deleteContents()
|
|
200
|
+
state.range.insertNode(cleanedFragment)
|
|
201
|
+
restoreSelection(state.doc, state.range, state.selection, selectionInfo)
|
|
202
|
+
}
|
|
313
203
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
listParent.insertBefore(fragment, currentList.nextSibling)
|
|
204
|
+
return { text, block, list, clear }
|
|
205
|
+
}
|
|
317
206
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
207
|
+
export function getBlockElement(node: Node): HTMLElement | null {
|
|
208
|
+
const element = node.nodeType === 3 ? node.parentElement : node as HTMLElement
|
|
209
|
+
if (!element) return null
|
|
210
|
+
return element.closest('p,h1,h2,h3,h4,h5,h6,blockquote,li') || element
|
|
211
|
+
}
|
|
322
212
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
} else {
|
|
327
|
-
const listEl = doc.createElement(listTag)
|
|
213
|
+
export function adjustIndentation(state: EditorState, increase: boolean) {
|
|
214
|
+
const { doc, range } = state
|
|
215
|
+
if (!doc || !range) return
|
|
328
216
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
while (block.firstChild) {
|
|
332
|
-
li.appendChild(block.firstChild)
|
|
333
|
-
}
|
|
334
|
-
listEl.appendChild(li)
|
|
335
|
-
})
|
|
217
|
+
const blockElement = getBlockElement(range.commonAncestorContainer)
|
|
218
|
+
if (!blockElement) return
|
|
336
219
|
|
|
337
|
-
|
|
220
|
+
const currentMargin = Number.parseInt(blockElement.style.marginLeft || '0', 10)
|
|
221
|
+
const step = 20 // pixels to indent/outdent
|
|
222
|
+
const newMargin = increase ? currentMargin + step : Math.max(0, currentMargin - step)
|
|
338
223
|
|
|
339
|
-
|
|
340
|
-
range.selectNodeContents(listEl)
|
|
341
|
-
selection.addRange(range)
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
return { text, block, list, clear }
|
|
224
|
+
blockElement.style.marginLeft = `${newMargin}px`
|
|
345
225
|
}
|
|
@@ -30,3 +30,80 @@ export function isStyleActive(style: string, doc: Document) {
|
|
|
30
30
|
|
|
31
31
|
return checkParent(parent, styleTags[style] ?? [style])
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
export interface SelectionInfo {
|
|
35
|
+
startBlock: Element
|
|
36
|
+
endBlock: Element
|
|
37
|
+
isMultiBlock: boolean
|
|
38
|
+
originalStart: Node
|
|
39
|
+
originalEnd: Node
|
|
40
|
+
originalStartOffset: number
|
|
41
|
+
originalEndOffset: number
|
|
42
|
+
parent: Element
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function analyzeSelection(doc: Document, range: Range): SelectionInfo | null {
|
|
46
|
+
const container = range.commonAncestorContainer
|
|
47
|
+
const parent = container.nodeType === 3 ? container.parentElement : container as Element
|
|
48
|
+
if (!parent) return null
|
|
49
|
+
|
|
50
|
+
const startBlock = parent.closest('p,h1,h2,h3,h4,h5,h6,blockquote,li') || parent
|
|
51
|
+
|
|
52
|
+
// Analyze if selection spans multiple blocks
|
|
53
|
+
const isMultiBlock = (() => {
|
|
54
|
+
if (!range.collapsed) {
|
|
55
|
+
const { endContainer } = range
|
|
56
|
+
const endParent = endContainer.nodeType === 3 ? endContainer.parentElement : endContainer as Element
|
|
57
|
+
const endBlock = endParent?.closest('p,h1,h2,h3,h4,h5,h6,blockquote,li') || endParent
|
|
58
|
+
return startBlock !== endBlock
|
|
59
|
+
}
|
|
60
|
+
return false
|
|
61
|
+
})()
|
|
62
|
+
|
|
63
|
+
const { endContainer } = range
|
|
64
|
+
const endParent = endContainer.nodeType === 3 ? endContainer.parentElement : endContainer as Element
|
|
65
|
+
const endBlock = endParent?.closest('p,h1,h2,h3,h4,h5,h6,blockquote,li') || endParent
|
|
66
|
+
|
|
67
|
+
if (!endBlock) return null
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
startBlock,
|
|
71
|
+
endBlock,
|
|
72
|
+
isMultiBlock,
|
|
73
|
+
originalStart: range.startContainer,
|
|
74
|
+
originalEnd: range.endContainer,
|
|
75
|
+
originalStartOffset: range.startOffset,
|
|
76
|
+
originalEndOffset: range.endOffset,
|
|
77
|
+
parent
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function restoreSelection(
|
|
82
|
+
doc: Document,
|
|
83
|
+
range: Range,
|
|
84
|
+
selection: Selection,
|
|
85
|
+
info: SelectionInfo,
|
|
86
|
+
fallbackNode?: Node
|
|
87
|
+
) {
|
|
88
|
+
try {
|
|
89
|
+
range.setStart(info.originalStart, info.originalStartOffset)
|
|
90
|
+
range.setEnd(info.originalEnd, info.originalEndOffset)
|
|
91
|
+
} catch (e) {
|
|
92
|
+
if (fallbackNode) {
|
|
93
|
+
range.selectNodeContents(fallbackNode)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
selection.removeAllRanges()
|
|
97
|
+
selection.addRange(range)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getBlocksBetween(startBlock: Element, endBlock: Element): Element[] {
|
|
101
|
+
const blocks = []
|
|
102
|
+
let currentBlock = startBlock
|
|
103
|
+
while (currentBlock) {
|
|
104
|
+
blocks.push(currentBlock)
|
|
105
|
+
if (currentBlock === endBlock) break
|
|
106
|
+
currentBlock = currentBlock.nextElementSibling as Element
|
|
107
|
+
}
|
|
108
|
+
return blocks
|
|
109
|
+
}
|
|
@@ -79,3 +79,69 @@ export function deleteRow(range: Range) {
|
|
|
79
79
|
|
|
80
80
|
row.remove()
|
|
81
81
|
}
|
|
82
|
+
|
|
83
|
+
export function deleteTable(range: Range) {
|
|
84
|
+
const cell = range.startContainer.parentElement?.closest('td')
|
|
85
|
+
if (!cell) return
|
|
86
|
+
|
|
87
|
+
const table = cell.closest('table')
|
|
88
|
+
if (!table) return
|
|
89
|
+
|
|
90
|
+
table.remove()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function insertColumn(position: 'before' | 'after', range: Range) {
|
|
94
|
+
const cell = range.startContainer.parentElement?.closest('td')
|
|
95
|
+
if (!cell) return
|
|
96
|
+
|
|
97
|
+
const table = cell.closest('table')
|
|
98
|
+
if (!table) return
|
|
99
|
+
|
|
100
|
+
const columnIndex = cell.cellIndex
|
|
101
|
+
const { rows } = table
|
|
102
|
+
|
|
103
|
+
for (let i = 0; i < rows.length; i++) {
|
|
104
|
+
const newCell = rows[i].insertCell(position === 'after' ? columnIndex + 1 : columnIndex)
|
|
105
|
+
newCell.innerHTML = ' '
|
|
106
|
+
newCell.style.border = '1px solid var(--border-color)'
|
|
107
|
+
newCell.style.padding = '8px'
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function deleteColumn(range: Range) {
|
|
112
|
+
const cell = range.startContainer.parentElement?.closest('td')
|
|
113
|
+
if (!cell) return
|
|
114
|
+
|
|
115
|
+
const table = cell.closest('table')
|
|
116
|
+
if (!table) return
|
|
117
|
+
|
|
118
|
+
const columnIndex = cell.cellIndex
|
|
119
|
+
const { rows } = table
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < rows.length; i++) {
|
|
122
|
+
rows[i].deleteCell(columnIndex)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// If no columns left, remove the table
|
|
126
|
+
if (rows[0].cells.length === 0) {
|
|
127
|
+
table.remove()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function alignColumn(range: Range, alignment: 'left' | 'center' | 'right' | 'justify') {
|
|
132
|
+
const cell = range.startContainer.parentElement?.closest('td')
|
|
133
|
+
if (!cell) return
|
|
134
|
+
|
|
135
|
+
const table = cell.closest('table')
|
|
136
|
+
if (!table) return
|
|
137
|
+
|
|
138
|
+
const columnIndex = cell.cellIndex
|
|
139
|
+
const { rows } = table
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < rows.length; i++) {
|
|
142
|
+
const cell = rows[i].cells[columnIndex]
|
|
143
|
+
if (cell) {
|
|
144
|
+
cell.style.textAlign = alignment
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|