@bagelink/vue 0.0.992 → 0.0.996

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.
Files changed (24) hide show
  1. package/dist/components/form/inputs/CodeEditor/Index.vue.d.ts +7 -11
  2. package/dist/components/form/inputs/CodeEditor/Index.vue.d.ts.map +1 -1
  3. package/dist/components/form/inputs/RichText/components/Toolbar.vue.d.ts.map +1 -1
  4. package/dist/components/form/inputs/RichText/composables/useEditor.d.ts.map +1 -1
  5. package/dist/components/form/inputs/RichText/config.d.ts.map +1 -1
  6. package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
  7. package/dist/components/form/inputs/RichText/utils/formatting.d.ts +1 -0
  8. package/dist/components/form/inputs/RichText/utils/formatting.d.ts.map +1 -1
  9. package/dist/components/form/inputs/RichText/utils/media.d.ts +3 -3
  10. package/dist/components/form/inputs/RichText/utils/media.d.ts.map +1 -1
  11. package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
  12. package/dist/index.cjs +285 -102
  13. package/dist/index.mjs +285 -102
  14. package/dist/style.css +16 -14
  15. package/package.json +2 -1
  16. package/src/components/form/inputs/CodeEditor/Index.vue +26 -10
  17. package/src/components/form/inputs/RichText/components/Toolbar.vue +1 -1
  18. package/src/components/form/inputs/RichText/composables/useEditor.ts +17 -4
  19. package/src/components/form/inputs/RichText/config.ts +5 -1
  20. package/src/components/form/inputs/RichText/index.vue +1 -0
  21. package/src/components/form/inputs/RichText/richTextTypes.d.ts +2 -0
  22. package/src/components/form/inputs/RichText/utils/formatting.ts +281 -34
  23. package/src/components/form/inputs/RichText/utils/media.ts +9 -6
  24. package/src/components/form/inputs/RichText/utils/selection.ts +6 -22
@@ -1,9 +1,113 @@
1
1
  import type { EditorState } from '../richTextTypes'
2
2
 
3
3
  export function formatting(state: EditorState) {
4
+ const { doc, range, selection } = state
5
+ function setRangeAndSelect(node: Node, start: number, end: number) {
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
+ }
15
+
16
+ const createBlockElement = (tag: string, content: DocumentFragment): HTMLElement => {
17
+ const block = doc!.createElement(tag)
18
+ block.appendChild(content)
19
+ return block
20
+ }
21
+ const clear = () => {
22
+ if (!doc || !range || !selection) return
23
+
24
+ const inlineTags = ['b', 'i', 'u', 'strong', 'em']
25
+
26
+ const isInlineTag = (el: HTMLElement | null) => el && inlineTags.includes(el.tagName.toLowerCase())
27
+
28
+ if (range.collapsed) {
29
+ // No action if the selection is collapsed
30
+ return
31
+ }
32
+
33
+ // Extract the selected content
34
+ const selectedContent = range.extractContents()
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
61
+ }
62
+ return doc.createTextNode('') // Ignore other node types
63
+ }
64
+
65
+ // Clean the extracted content
66
+ const fragment = doc.createDocumentFragment()
67
+ Array.from(selectedContent.childNodes).forEach((child) => {
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)
80
+
81
+ // Normalize the DOM (merge adjacent text nodes)
82
+ const normalizeDom = (node: Node) => {
83
+ if (node.nodeType === 3 && node.nextSibling?.nodeType === 3) {
84
+ // Merge adjacent text nodes
85
+ if (node.textContent && node.nextSibling.textContent) {
86
+ node.textContent += node.nextSibling.textContent
87
+ }
88
+ node.parentNode?.removeChild(node.nextSibling)
89
+ } else if (node.nodeType === 1) {
90
+ // Recursively normalize child nodes
91
+ Array.from(node.childNodes).forEach((child) => {
92
+ normalizeDom(child)
93
+ })
94
+ }
95
+ }
96
+
97
+ Array.from(fragment.childNodes).forEach((child) => {
98
+ normalizeDom(child)
99
+ })
100
+
101
+ // Update the selection to cover the cleaned content
102
+ selection.removeAllRanges()
103
+ range.selectNodeContents(fragment)
104
+ selection.addRange(range)
105
+ }
106
+
4
107
  const text = (command: string) => {
5
- const { doc, range, selection } = state
6
108
  if (!doc || !range || !selection) return
109
+ const { startOffset, endOffset } = range
110
+
7
111
  let tag = ''
8
112
  if (command === 'bold') tag = 'b'
9
113
  if (command === 'italic') tag = 'i'
@@ -14,52 +118,95 @@ export function formatting(state: EditorState) {
14
118
  const relatedTags: { [key: string]: string[] } = {
15
119
  b: ['strong'],
16
120
  i: ['em'],
17
- u: [],
121
+ u: ['u'],
18
122
  em: ['i'],
19
- strong: ['b']
123
+ strong: ['b'],
124
+ }
125
+
126
+ const isTagMatch = (el: HTMLElement | null) => el
127
+ && (el.tagName.toLowerCase() === tag || relatedTags[tag].includes(el.tagName.toLowerCase()))
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
20
142
  }
21
143
 
22
- const selectedContent = range.extractContents()
23
- const wrapper = doc.createElement(tag)
24
- wrapper.appendChild(selectedContent)
25
144
  const container = range.commonAncestorContainer
26
145
  const parent = container.parentElement
27
146
 
28
- if (parent && (parent.tagName.toLowerCase() === tag || relatedTags[tag].includes(parent.tagName.toLowerCase()))) {
29
- while (wrapper.firstChild) {
30
- parent.parentNode?.insertBefore(wrapper.firstChild, parent)
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
+ }
159
+
160
+ // Add unwrapped selected text
161
+ const unwrappedTextNode = doc.createTextNode(selectedText)
162
+ parent.parentNode?.insertBefore(unwrappedTextNode, parent)
163
+
164
+ if (afterText) {
165
+ const afterNode = wrapWithTag(afterText, tag)
166
+ parent.parentNode?.insertBefore(afterNode, parent)
31
167
  }
168
+
169
+ // Remove the original parent node
32
170
  parent.parentNode?.removeChild(parent)
171
+
172
+ // Reselect the unwrapped text
173
+ range.selectNode(unwrappedTextNode)
33
174
  } else {
175
+ // Standard case: wrap the selection
176
+ const selectedContent = range.extractContents()
177
+ const wrapper = doc.createElement(tag)
178
+ wrapper.appendChild(selectedContent)
34
179
  range.insertNode(wrapper)
35
- }
36
180
 
37
- const nestedTags = wrapper.querySelectorAll(`${tag},${(relatedTags[tag] || []).join(',')}`)
38
- nestedTags.forEach((nestedTag) => {
39
- while (nestedTag.firstChild) {
40
- nestedTag.parentNode?.insertBefore(nestedTag.firstChild, nestedTag)
41
- }
42
- nestedTag.parentNode?.removeChild(nestedTag)
43
- })
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)
186
+ }
187
+ nestedTag.parentNode?.removeChild(nestedTag)
188
+ })
189
+
190
+ // Reselect the wrapped content
191
+ range.selectNodeContents(wrapper)
192
+ }
44
193
 
45
194
  selection.removeAllRanges()
46
195
  selection.addRange(range)
47
196
  }
48
197
  const block = (command: string, value: string) => {
49
- const { doc, range, selection } = state
50
198
  if (!doc || !range || !selection) return
51
199
  const container = range.commonAncestorContainer
52
200
  const parent = container.nodeType === 3 ? container.parentElement : (container as Element)
53
201
  if (!parent) return
54
202
 
55
- const currentHeading = parent.closest('p,h1,h2,h3,h4,h5,h6')
203
+ const currentHeading = parent.closest('p,h1,h2,h3,h4,h5,h6,blockquote')
56
204
  const { startOffset, endOffset } = range
57
205
  const blockEl = currentHeading || parent.closest('p,h1,h2,h3,h4,h5,h6') || parent
58
206
  range.selectNodeContents(blockEl)
59
207
 
60
208
  const content = range.extractContents()
61
- const newElement = doc.createElement(currentHeading && currentHeading.tagName.toLowerCase() === value ? 'p' : value)
62
- newElement.appendChild(content)
209
+ const newElement = createBlockElement(value, content)
63
210
 
64
211
  if (currentHeading) {
65
212
  currentHeading.parentNode?.replaceChild(newElement, currentHeading)
@@ -68,31 +215,131 @@ export function formatting(state: EditorState) {
68
215
  }
69
216
 
70
217
  selection.removeAllRanges()
71
- range.setStart(container, startOffset)
72
- range.setEnd(container, endOffset)
218
+ setRangeAndSelect(container, startOffset, endOffset)
73
219
  selection.addRange(range)
74
220
  }
75
221
  const list = (command: string) => {
76
- const { doc, range } = state
77
- if (!doc || !range) return
222
+ if (!doc || !range || !selection) return
223
+
78
224
  const listTag = command === 'orderedList' ? 'ol' : 'ul'
79
225
  const container = range.commonAncestorContainer
80
226
  const parent = container.nodeType === 3 ? container.parentElement : (container as Element)
81
227
  const currentList = parent?.closest('ul,ol')
82
228
 
229
+ if (!parent) return
230
+
231
+ // Expand range to include the entire list item if the caret is inside one
232
+ const listItem = parent.closest('li')
233
+ if (listItem) {
234
+ const listParent = listItem.parentElement // The <ol> or <ul>
235
+ if (!listParent) return
236
+
237
+ // Create new fragments for before and after the selected list item
238
+ const beforeFragment = doc.createDocumentFragment()
239
+ const afterFragment = doc.createDocumentFragment()
240
+
241
+ let currentNode = listParent.firstChild
242
+ let isBefore = true
243
+
244
+ while (currentNode) {
245
+ if (currentNode === listItem) {
246
+ isBefore = false // Mark as "after" once we reach the selected item
247
+ } else if (isBefore) {
248
+ beforeFragment.appendChild(currentNode.cloneNode(true))
249
+ } else {
250
+ afterFragment.appendChild(currentNode.cloneNode(true))
251
+ }
252
+ currentNode = currentNode.nextSibling
253
+ }
254
+
255
+ // Convert the selected list item to a standalone block (e.g., <p>)
256
+ const standaloneBlock = doc.createElement('p')
257
+ while (listItem.firstChild) {
258
+ standaloneBlock.appendChild(listItem.firstChild)
259
+ }
260
+
261
+ // Replace the original list with the split fragments and the standalone block
262
+ const grandParent = listParent.parentNode
263
+ if (!grandParent) return
264
+
265
+ if (beforeFragment.childNodes.length > 0) {
266
+ const newListBefore = doc.createElement(listParent.tagName.toLowerCase())
267
+ newListBefore.appendChild(beforeFragment)
268
+ grandParent.insertBefore(newListBefore, listParent)
269
+ }
270
+
271
+ grandParent.insertBefore(standaloneBlock, listParent)
272
+
273
+ if (afterFragment.childNodes.length > 0) {
274
+ const newListAfter = doc.createElement(listParent.tagName.toLowerCase())
275
+ newListAfter.appendChild(afterFragment)
276
+ grandParent.insertBefore(newListAfter, listParent.nextSibling)
277
+ }
278
+
279
+ // Remove the original list
280
+ grandParent.removeChild(listParent)
281
+
282
+ // Update the selection
283
+ selection.removeAllRanges()
284
+ range.selectNodeContents(standaloneBlock)
285
+ selection.addRange(range)
286
+
287
+ return
288
+ }
289
+
290
+ // Fallback for toggling list behavior (same as previous implementation)
291
+ const startBlock = parent.closest('p,h1,h2,h3,h4,h5,h6,blockquote') || parent
292
+ const endBlock = range.endContainer.nodeType === 3
293
+ ? (range.endContainer.parentElement?.closest('p,h1,h2,h3,h4,h5,h6,blockquote') || range.endContainer.parentElement)
294
+ : (range.endContainer as Element)
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
+ }
303
+
83
304
  if (currentList) {
84
- const content = range.extractContents()
85
- const p = doc.createElement('p')
86
- p.appendChild(content)
87
- currentList.parentNode?.replaceChild(p, currentList)
305
+ const fragment = doc.createDocumentFragment()
306
+ blocks.forEach((block) => {
307
+ const paragraph = doc.createElement('p')
308
+ while (block.firstChild) {
309
+ paragraph.appendChild(block.firstChild)
310
+ }
311
+ fragment.appendChild(paragraph)
312
+ })
313
+
314
+ const listParent = currentList.parentNode
315
+ if (listParent) {
316
+ listParent.insertBefore(fragment, currentList.nextSibling)
317
+
318
+ if (!currentList.querySelector('li')) {
319
+ listParent.removeChild(currentList)
320
+ }
321
+ }
322
+
323
+ selection.removeAllRanges()
324
+ range.selectNodeContents(fragment)
325
+ selection.addRange(range)
88
326
  } else {
89
327
  const listEl = doc.createElement(listTag)
90
- const li = doc.createElement('li')
91
- const content = range.extractContents()
92
- li.appendChild(content)
93
- listEl.appendChild(li)
328
+
329
+ blocks.forEach((block) => {
330
+ const li = doc.createElement('li')
331
+ while (block.firstChild) {
332
+ li.appendChild(block.firstChild)
333
+ }
334
+ listEl.appendChild(li)
335
+ })
336
+
94
337
  range.insertNode(listEl)
338
+
339
+ selection.removeAllRanges()
340
+ range.selectNodeContents(listEl)
341
+ selection.addRange(range)
95
342
  }
96
343
  }
97
- return { text, block, list }
344
+ return { text, block, list, clear }
98
345
  }
@@ -1,7 +1,8 @@
1
- import type { Modal } from '../richTextTypes'
1
+ import type { EditorState, Modal } from '../richTextTypes'
2
2
 
3
- export function insertImage(modal: Modal, doc: Document, range: Range | null) {
4
- if (!range) return
3
+ export function insertImage(modal: Modal, state: EditorState) {
4
+ const { range, doc } = state
5
+ if (!range || !doc) return
5
6
 
6
7
  modal.showModalForm({
7
8
  title: 'Upload Image',
@@ -14,15 +15,17 @@ export function insertImage(modal: Modal, doc: Document, range: Range | null) {
14
15
  const img = doc.createElement('img')
15
16
  img.src = data.src
16
17
  img.alt = data.alt
17
- range.deleteContents()
18
+ // range.deleteContents()
19
+ range.collapse(false)
18
20
  range.insertNode(img)
19
21
  }
20
22
  }
21
23
  })
22
24
  }
23
25
 
24
- export function insertLink(modal: Modal, doc: Document, range: Range | null) {
25
- if (!range) return
26
+ export function insertLink(modal: Modal, state: EditorState) {
27
+ const { range, doc } = state
28
+ if (!range || !doc) return
26
29
 
27
30
  modal.showModalForm({
28
31
  title: 'Insert Link',
@@ -1,4 +1,3 @@
1
-
2
1
  export function getSelection(state: any) {
3
2
  return state.selection
4
3
  }
@@ -23,26 +22,11 @@ export function isStyleActive(style: string, doc: Document) {
23
22
  return checkParent(element.parentElement, tags)
24
23
  }
25
24
 
26
- switch (style) {
27
- case 'bold':
28
- return checkParent(parent, ['strong', 'b'])
29
- case 'italic':
30
- return checkParent(parent, ['em', 'i'])
31
- case 'underline':
32
- return checkParent(parent, ['u'])
33
- case 'h1':
34
- return checkParent(parent, ['h1'])
35
- case 'h2':
36
- return checkParent(parent, ['h2'])
37
- case 'h3':
38
- return checkParent(parent, ['h3'])
39
- case 'h4':
40
- return checkParent(parent, ['h4'])
41
- case 'h5':
42
- return checkParent(parent, ['h5'])
43
- case 'h6':
44
- return checkParent(parent, ['h6'])
45
- default:
46
- return false
25
+ const styleTags: { [key: string]: string[] } = {
26
+ bold: ['strong', 'b'],
27
+ italic: ['em', 'i'],
28
+ underline: ['u'],
47
29
  }
30
+
31
+ return checkParent(parent, styleTags[style] ?? [style])
48
32
  }