@bagelink/vue 1.19.1 → 1.20.3
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/Dropdown.vue.d.ts +73 -48
- package/dist/components/Dropdown.vue.d.ts.map +1 -1
- package/dist/components/Icon/Icon.vue.d.ts.map +1 -1
- package/dist/components/dataTable/DataTable.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts +1 -0
- package/dist/components/form/inputs/RichText/components/EditorToolbar.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/useCommands.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/composables/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 +6 -0
- package/dist/components/form/inputs/RichText/index.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/richTextTypes.d.ts +1 -0
- package/dist/components/form/inputs/RichText/richTextTypes.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/contentMetrics.d.ts +7 -0
- package/dist/components/form/inputs/RichText/utils/contentMetrics.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/image.d.ts +17 -0
- package/dist/components/form/inputs/RichText/utils/image.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/sanitizePaste.d.ts +15 -0
- package/dist/components/form/inputs/RichText/utils/sanitizePaste.d.ts.map +1 -0
- package/dist/components/form/inputs/RichText/utils/selection.d.ts +1 -1
- package/dist/components/form/inputs/RichText/utils/selection.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/table.d.ts.map +1 -1
- package/dist/components/form/inputs/RichText/utils/video.d.ts +23 -0
- package/dist/components/form/inputs/RichText/utils/video.d.ts.map +1 -0
- package/dist/components/form/inputs/SelectBtn.vue.d.ts +28 -1
- package/dist/components/form/inputs/SelectBtn.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts +2 -0
- package/dist/components/form/inputs/Upload/UploadInput.vue.d.ts.map +1 -1
- package/dist/components/form/inputs/Upload/useFileUpload.d.ts +1 -0
- package/dist/components/form/inputs/Upload/useFileUpload.d.ts.map +1 -1
- package/dist/core.css +2 -2
- package/dist/index.cjs +218 -130
- package/dist/index.css +1 -1
- package/dist/index.mjs +18604 -17549
- package/dist/style.css +2 -2
- package/package.json +1 -1
- package/src/components/Dropdown.vue +48 -7
- package/src/components/FilterQuery.vue +1 -1
- package/src/components/Icon/Icon.vue +8 -0
- package/src/components/dataTable/DataTable.vue +6 -3
- package/src/components/form/inputs/RichText/components/EditorToolbar.vue +238 -8
- package/src/components/form/inputs/RichText/composables/useCommands.ts +23 -5
- package/src/components/form/inputs/RichText/composables/useEditorKeyboard.ts +28 -0
- package/src/components/form/inputs/RichText/config.ts +17 -14
- package/src/components/form/inputs/RichText/index.vue +354 -463
- package/src/components/form/inputs/RichText/richTextTypes.ts +1 -0
- package/src/components/form/inputs/RichText/utils/commands.ts +437 -24
- package/src/components/form/inputs/RichText/utils/contentMetrics.ts +24 -0
- package/src/components/form/inputs/RichText/utils/image.ts +136 -0
- package/src/components/form/inputs/RichText/utils/sanitizePaste.ts +110 -0
- package/src/components/form/inputs/RichText/utils/selection.ts +32 -0
- package/src/components/form/inputs/RichText/utils/table.ts +77 -8
- package/src/components/form/inputs/RichText/utils/video.ts +270 -0
- package/src/components/form/inputs/SelectBtn.vue +126 -15
- package/src/components/form/inputs/TelInput.vue +1 -1
- package/src/components/form/inputs/Upload/UploadInput.vue +3 -2
- package/src/components/form/inputs/Upload/useFileUpload.ts +2 -0
- package/src/styles/colors.css +5 -2
- package/src/styles/core.css +1 -0
- package/src/styles/rich-content.css +109 -0
|
@@ -42,6 +42,99 @@ function getCurrentSelection(state: EditorState): { selection: Selection, range:
|
|
|
42
42
|
return { selection, range }
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function rangeOverlapsNodeContent(range: Range, node: Node): boolean {
|
|
46
|
+
const nodeRange = node.ownerDocument?.createRange()
|
|
47
|
+
if (!nodeRange) { return false }
|
|
48
|
+
nodeRange.selectNodeContents(node)
|
|
49
|
+
|
|
50
|
+
// `intersectsNode()` treats touching boundaries as an intersection. A browser
|
|
51
|
+
// selection that ends at offset 0 of the next paragraph must not format it.
|
|
52
|
+
return range.compareBoundaryPoints(Range.END_TO_START, nodeRange) < 0
|
|
53
|
+
&& range.compareBoundaryPoints(Range.START_TO_END, nodeRange) > 0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getSelectedListItems(doc: Document, range: Range): HTMLLIElement[] {
|
|
57
|
+
const items = Array.from(doc.body.querySelectorAll<HTMLLIElement>('li'))
|
|
58
|
+
.filter(item => rangeOverlapsNodeContent(range, item))
|
|
59
|
+
const itemSet = new Set(items)
|
|
60
|
+
const deepestItems = items.filter(item =>
|
|
61
|
+
!Array.from(item.querySelectorAll<HTMLLIElement>('li')).some(descendant => itemSet.has(descendant))
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if (deepestItems.length > 0) { return deepestItems }
|
|
65
|
+
|
|
66
|
+
const container = range.commonAncestorContainer.nodeType === Node.TEXT_NODE
|
|
67
|
+
? range.commonAncestorContainer.parentElement
|
|
68
|
+
: range.commonAncestorContainer as Element
|
|
69
|
+
const currentItem = container?.closest('li') as HTMLLIElement | null
|
|
70
|
+
return currentItem ? [currentItem] : []
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function selectNodes(doc: Document, selection: Selection, nodes: Node[]) {
|
|
74
|
+
if (!nodes.length) { return }
|
|
75
|
+
const nextRange = doc.createRange()
|
|
76
|
+
nextRange.setStartBefore(nodes[0])
|
|
77
|
+
nextRange.setEndAfter(nodes[nodes.length - 1])
|
|
78
|
+
selection.removeAllRanges()
|
|
79
|
+
selection.addRange(nextRange)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function indentSelectedListItems(doc: Document, selection: Selection, range: Range): boolean {
|
|
83
|
+
const items = getSelectedListItems(doc, range)
|
|
84
|
+
if (!items.length) { return false }
|
|
85
|
+
|
|
86
|
+
const parentList = items[0].parentElement
|
|
87
|
+
if (!parentList || !['UL', 'OL'].includes(parentList.tagName)
|
|
88
|
+
|| !items.every(item => item.parentElement === parentList)) {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const siblings = Array.from(parentList.children)
|
|
93
|
+
.filter((element): element is HTMLLIElement => element.tagName === 'LI')
|
|
94
|
+
const firstIndex = siblings.indexOf(items[0])
|
|
95
|
+
if (firstIndex <= 0) { return true }
|
|
96
|
+
|
|
97
|
+
const previousItem = siblings[firstIndex - 1]
|
|
98
|
+
const listTag = parentList.tagName.toLowerCase()
|
|
99
|
+
let nestedList = Array.from(previousItem.children)
|
|
100
|
+
.find(child => child.tagName.toLowerCase() === listTag) as HTMLElement | undefined
|
|
101
|
+
if (!nestedList) {
|
|
102
|
+
nestedList = doc.createElement(listTag)
|
|
103
|
+
if (parentList.dir) { nestedList.dir = parentList.dir }
|
|
104
|
+
previousItem.appendChild(nestedList)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
items.forEach(item => nestedList!.appendChild(item))
|
|
108
|
+
selectNodes(doc, selection, items)
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function outdentSelectedListItems(doc: Document, selection: Selection, range: Range): boolean {
|
|
113
|
+
const items = getSelectedListItems(doc, range)
|
|
114
|
+
if (!items.length) { return false }
|
|
115
|
+
|
|
116
|
+
const parentList = items[0].parentElement
|
|
117
|
+
const ownerItem = parentList?.parentElement?.tagName === 'LI'
|
|
118
|
+
? parentList.parentElement as HTMLLIElement
|
|
119
|
+
: parentList?.parentElement?.closest('li') as HTMLLIElement | null
|
|
120
|
+
const outerList = ownerItem?.parentElement
|
|
121
|
+
if (!parentList || !ownerItem || !outerList || !['UL', 'OL'].includes(outerList.tagName)
|
|
122
|
+
|| !items.every(item => item.parentElement === parentList)) {
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let insertAfter: Element = ownerItem
|
|
127
|
+
items.forEach((item) => {
|
|
128
|
+
insertAfter.insertAdjacentElement('afterend', item)
|
|
129
|
+
insertAfter = item
|
|
130
|
+
})
|
|
131
|
+
if (!parentList.querySelector(':scope > li')) {
|
|
132
|
+
parentList.remove()
|
|
133
|
+
}
|
|
134
|
+
selectNodes(doc, selection, items)
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
|
|
45
138
|
function applyFormattingToWordAtCursor(doc: Document, range: Range, tagSelector: string, createTag: string): boolean {
|
|
46
139
|
if (!range.collapsed) { return false }
|
|
47
140
|
|
|
@@ -236,16 +329,144 @@ function toggleInlineFormatting(
|
|
|
236
329
|
// Check if selection spans across multiple block elements
|
|
237
330
|
const blockSelector = 'p,h1,h2,h3,h4,h5,h6,blockquote,li,td,th'
|
|
238
331
|
const blocks = Array.from(doc.body.querySelectorAll<HTMLElement>(blockSelector))
|
|
239
|
-
.filter(
|
|
332
|
+
.filter(block => {
|
|
333
|
+
if (!rangeOverlapsNodeContent(range, block)) { return false }
|
|
334
|
+
return block.tagName === 'LI' || !block.querySelector(blockSelector)
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
const fullySelectedBlocks = blocks.filter((block) => {
|
|
338
|
+
const blockRange = doc.createRange()
|
|
339
|
+
blockRange.selectNodeContents(block)
|
|
340
|
+
return range.compareBoundaryPoints(Range.START_TO_START, blockRange) <= 0
|
|
341
|
+
&& range.compareBoundaryPoints(Range.END_TO_END, blockRange) >= 0
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
// Browser line selections can extend just beyond an <li> boundary. Never pass
|
|
345
|
+
// such a range to extractContents(), because that can split the surrounding list
|
|
346
|
+
// and leave empty blocks before and after the selected item.
|
|
347
|
+
if (blocks.length > 0 && blocks.every(block => block.tagName === 'LI')) {
|
|
348
|
+
const allFormatted = blocks.every((block) => {
|
|
349
|
+
const meaningfulChildren = Array.from(block.childNodes)
|
|
350
|
+
.filter(node => node.nodeType !== Node.ELEMENT_NODE || !['UL', 'OL'].includes((node as Element).tagName))
|
|
351
|
+
.filter(node => node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()))
|
|
352
|
+
return meaningfulChildren.length > 0 && meaningfulChildren.every((node) => {
|
|
353
|
+
const element = node.nodeType === Node.ELEMENT_NODE ? node as Element : node.parentElement
|
|
354
|
+
return Boolean(element?.closest(matchSelector))
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
const formattedNodes: HTMLElement[] = []
|
|
240
358
|
|
|
241
|
-
if (blocks.length > 1) {
|
|
242
359
|
blocks.forEach((block) => {
|
|
360
|
+
if (allFormatted) {
|
|
361
|
+
block.querySelectorAll<HTMLElement>(matchSelector).forEach((formatted) => {
|
|
362
|
+
const parent = formatted.parentNode
|
|
363
|
+
while (formatted.firstChild) {
|
|
364
|
+
parent?.insertBefore(formatted.firstChild, formatted)
|
|
365
|
+
}
|
|
366
|
+
formatted.remove()
|
|
367
|
+
})
|
|
368
|
+
formattedNodes.push(block)
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const blockRange = doc.createRange()
|
|
373
|
+
const nestedListIndex = Array.from(block.childNodes)
|
|
374
|
+
.findIndex(node => node.nodeType === Node.ELEMENT_NODE && ['UL', 'OL'].includes((node as Element).tagName))
|
|
375
|
+
blockRange.setStart(block, 0)
|
|
376
|
+
blockRange.setEnd(block, nestedListIndex >= 0 ? nestedListIndex : block.childNodes.length)
|
|
377
|
+
|
|
378
|
+
const startItem = (range.startContainer.nodeType === Node.TEXT_NODE
|
|
379
|
+
? range.startContainer.parentElement
|
|
380
|
+
: range.startContainer as Element)?.closest('li')
|
|
381
|
+
const endItem = (range.endContainer.nodeType === Node.TEXT_NODE
|
|
382
|
+
? range.endContainer.parentElement
|
|
383
|
+
: range.endContainer as Element)?.closest('li')
|
|
384
|
+
if (startItem === block) {
|
|
385
|
+
blockRange.setStart(range.startContainer, range.startOffset)
|
|
386
|
+
}
|
|
387
|
+
if (endItem === block) {
|
|
388
|
+
blockRange.setEnd(range.endContainer, range.endOffset)
|
|
389
|
+
}
|
|
390
|
+
if (blockRange.collapsed || !blockRange.toString()) { return }
|
|
391
|
+
|
|
243
392
|
const wrapper = doc.createElement(tagName)
|
|
244
|
-
|
|
245
|
-
|
|
393
|
+
try {
|
|
394
|
+
blockRange.surroundContents(wrapper)
|
|
395
|
+
} catch {
|
|
396
|
+
const fragment = blockRange.extractContents()
|
|
397
|
+
fragment.querySelectorAll(matchSelector).forEach((nested) => {
|
|
398
|
+
const parent = nested.parentNode
|
|
399
|
+
while (nested.firstChild) {
|
|
400
|
+
parent?.insertBefore(nested.firstChild, nested)
|
|
401
|
+
}
|
|
402
|
+
nested.remove()
|
|
403
|
+
})
|
|
404
|
+
wrapper.appendChild(fragment)
|
|
405
|
+
blockRange.insertNode(wrapper)
|
|
246
406
|
}
|
|
247
|
-
|
|
407
|
+
wrapper.querySelectorAll(matchSelector).forEach((nested) => {
|
|
408
|
+
const parent = nested.parentNode
|
|
409
|
+
while (nested.firstChild) {
|
|
410
|
+
parent?.insertBefore(nested.firstChild, nested)
|
|
411
|
+
}
|
|
412
|
+
nested.remove()
|
|
413
|
+
})
|
|
414
|
+
formattedNodes.push(wrapper)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
if (formattedNodes.length > 0) {
|
|
418
|
+
const nextRange = doc.createRange()
|
|
419
|
+
nextRange.setStartBefore(formattedNodes[0])
|
|
420
|
+
nextRange.setEndAfter(formattedNodes[formattedNodes.length - 1])
|
|
421
|
+
selection.removeAllRanges()
|
|
422
|
+
selection.addRange(nextRange)
|
|
423
|
+
}
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (fullySelectedBlocks.length > 0 && fullySelectedBlocks.length === blocks.length) {
|
|
428
|
+
const allFormatted = fullySelectedBlocks.every((block) => {
|
|
429
|
+
const meaningfulChildren = Array.from(block.childNodes)
|
|
430
|
+
.filter(node => node.nodeType !== Node.TEXT_NODE || Boolean(node.textContent?.trim()))
|
|
431
|
+
return meaningfulChildren.length > 0 && meaningfulChildren.every((node) => {
|
|
432
|
+
const element = node.nodeType === Node.ELEMENT_NODE ? node as Element : node.parentElement
|
|
433
|
+
return Boolean(element?.closest(matchSelector))
|
|
434
|
+
})
|
|
248
435
|
})
|
|
436
|
+
|
|
437
|
+
if (allFormatted) {
|
|
438
|
+
fullySelectedBlocks.forEach((block) => {
|
|
439
|
+
block.querySelectorAll<HTMLElement>(matchSelector).forEach((formatted) => {
|
|
440
|
+
const parent = formatted.parentNode
|
|
441
|
+
while (formatted.firstChild) {
|
|
442
|
+
parent?.insertBefore(formatted.firstChild, formatted)
|
|
443
|
+
}
|
|
444
|
+
formatted.remove()
|
|
445
|
+
})
|
|
446
|
+
})
|
|
447
|
+
} else {
|
|
448
|
+
fullySelectedBlocks.forEach((block) => {
|
|
449
|
+
const wrapper = doc.createElement(tagName)
|
|
450
|
+
const children = Array.from(block.childNodes)
|
|
451
|
+
children.forEach((child) => {
|
|
452
|
+
if (child.nodeType === Node.ELEMENT_NODE && (child as Element).matches(matchSelector)) {
|
|
453
|
+
while (child.firstChild) {
|
|
454
|
+
wrapper.appendChild(child.firstChild)
|
|
455
|
+
}
|
|
456
|
+
child.remove()
|
|
457
|
+
} else {
|
|
458
|
+
wrapper.appendChild(child)
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
block.appendChild(wrapper)
|
|
462
|
+
})
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const nextRange = doc.createRange()
|
|
466
|
+
nextRange.setStart(fullySelectedBlocks[0], 0)
|
|
467
|
+
nextRange.setEnd(fullySelectedBlocks[fullySelectedBlocks.length - 1], fullySelectedBlocks[fullySelectedBlocks.length - 1].childNodes.length)
|
|
468
|
+
selection.removeAllRanges()
|
|
469
|
+
selection.addRange(nextRange)
|
|
249
470
|
return
|
|
250
471
|
}
|
|
251
472
|
|
|
@@ -275,6 +496,66 @@ function toggleInlineFormatting(
|
|
|
275
496
|
}
|
|
276
497
|
}
|
|
277
498
|
|
|
499
|
+
function applyInlineStyle(state: EditorState, property: string, value: string) {
|
|
500
|
+
const selectionInfo = getCurrentSelection(state)
|
|
501
|
+
if (!selectionInfo || !state.doc || !value) { return }
|
|
502
|
+
|
|
503
|
+
const { doc } = state
|
|
504
|
+
const { range, selection } = selectionInfo
|
|
505
|
+
const styleSelector = `span[style*="${property}"]`
|
|
506
|
+
|
|
507
|
+
if (range.collapsed) {
|
|
508
|
+
const span = doc.createElement('span')
|
|
509
|
+
span.style.setProperty(property, value)
|
|
510
|
+
const marker = doc.createTextNode('\u200B')
|
|
511
|
+
span.appendChild(marker)
|
|
512
|
+
range.insertNode(span)
|
|
513
|
+
|
|
514
|
+
const nextRange = doc.createRange()
|
|
515
|
+
nextRange.setStart(marker, 1)
|
|
516
|
+
nextRange.collapse(true)
|
|
517
|
+
selection.removeAllRanges()
|
|
518
|
+
selection.addRange(nextRange)
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const commonAncestor = range.commonAncestorContainer
|
|
523
|
+
const commonElement = commonAncestor.nodeType === Node.TEXT_NODE
|
|
524
|
+
? commonAncestor.parentElement
|
|
525
|
+
: commonAncestor as Element
|
|
526
|
+
const activeStyleSpan = commonElement?.closest(styleSelector) as HTMLSpanElement | null
|
|
527
|
+
if (activeStyleSpan && activeStyleSpan.contains(range.startContainer) && activeStyleSpan.contains(range.endContainer)) {
|
|
528
|
+
activeStyleSpan.style.setProperty(property, value)
|
|
529
|
+
const nextRange = doc.createRange()
|
|
530
|
+
nextRange.selectNodeContents(activeStyleSpan)
|
|
531
|
+
selection.removeAllRanges()
|
|
532
|
+
selection.addRange(nextRange)
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const span = doc.createElement('span')
|
|
537
|
+
span.style.setProperty(property, value)
|
|
538
|
+
try {
|
|
539
|
+
range.surroundContents(span)
|
|
540
|
+
} catch {
|
|
541
|
+
const fragment = range.extractContents()
|
|
542
|
+
fragment.querySelectorAll<HTMLSpanElement>(styleSelector).forEach((nested) => {
|
|
543
|
+
const parent = nested.parentNode
|
|
544
|
+
while (nested.firstChild) {
|
|
545
|
+
parent?.insertBefore(nested.firstChild, nested)
|
|
546
|
+
}
|
|
547
|
+
nested.remove()
|
|
548
|
+
})
|
|
549
|
+
span.appendChild(fragment)
|
|
550
|
+
range.insertNode(span)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const nextRange = doc.createRange()
|
|
554
|
+
nextRange.selectNodeContents(span)
|
|
555
|
+
selection.removeAllRanges()
|
|
556
|
+
selection.addRange(nextRange)
|
|
557
|
+
}
|
|
558
|
+
|
|
278
559
|
function updateStateAfterCommand(state: EditorState, preCommandContent?: string) {
|
|
279
560
|
if (!state.doc) { return }
|
|
280
561
|
|
|
@@ -308,7 +589,7 @@ const CONVERTIBLE_BLOCK_SELECTOR = 'p,h1,h2,h3,h4,h5,h6,blockquote'
|
|
|
308
589
|
function getSelectedBlocks(doc: Document, range: Range): HTMLElement[] {
|
|
309
590
|
if (!range.collapsed) {
|
|
310
591
|
const blocks = Array.from(doc.body.querySelectorAll<HTMLElement>(CONVERTIBLE_BLOCK_SELECTOR))
|
|
311
|
-
.filter(block => !block.closest('li') && range
|
|
592
|
+
.filter(block => !block.closest('li') && rangeOverlapsNodeContent(range, block))
|
|
312
593
|
.filter(block => !block.parentElement?.closest(CONVERTIBLE_BLOCK_SELECTOR))
|
|
313
594
|
if (blocks.length > 0) {
|
|
314
595
|
return blocks
|
|
@@ -348,7 +629,7 @@ function convertSelectedBlocksToList(
|
|
|
348
629
|
if (!state.doc || range.collapsed) { return false }
|
|
349
630
|
|
|
350
631
|
const blocks = Array.from(state.doc.body.querySelectorAll<HTMLElement>(CONVERTIBLE_BLOCK_SELECTOR))
|
|
351
|
-
.filter(block => !block.closest('li') && range
|
|
632
|
+
.filter(block => !block.closest('li') && rangeOverlapsNodeContent(range, block))
|
|
352
633
|
.filter(block => !block.parentElement?.closest(CONVERTIBLE_BLOCK_SELECTOR))
|
|
353
634
|
|
|
354
635
|
if (!blocks.length) { return false }
|
|
@@ -376,12 +657,124 @@ function convertSelectedBlocksToList(
|
|
|
376
657
|
return true
|
|
377
658
|
}
|
|
378
659
|
|
|
660
|
+
function replaceListTag(doc: Document, list: HTMLElement, listTag: 'ol' | 'ul'): HTMLElement {
|
|
661
|
+
const convertedList = doc.createElement(listTag)
|
|
662
|
+
for (const attribute of Array.from(list.attributes)) {
|
|
663
|
+
convertedList.setAttribute(attribute.name, attribute.value)
|
|
664
|
+
}
|
|
665
|
+
while (list.firstChild) {
|
|
666
|
+
convertedList.appendChild(list.firstChild)
|
|
667
|
+
}
|
|
668
|
+
list.replaceWith(convertedList)
|
|
669
|
+
return convertedList
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function convertSelectedListItems(
|
|
673
|
+
doc: Document,
|
|
674
|
+
selection: Selection,
|
|
675
|
+
range: Range,
|
|
676
|
+
listTag: 'ol' | 'ul'
|
|
677
|
+
): boolean {
|
|
678
|
+
const selectedItems = getSelectedListItems(doc, range)
|
|
679
|
+
if (!selectedItems.length) { return false }
|
|
680
|
+
|
|
681
|
+
const listParent = selectedItems[0].parentElement
|
|
682
|
+
if (!listParent || !['UL', 'OL'].includes(listParent.tagName)
|
|
683
|
+
|| !selectedItems.every(item => item.parentElement === listParent)) {
|
|
684
|
+
return false
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const allItems = Array.from(listParent.children)
|
|
688
|
+
.filter((element): element is HTMLLIElement => element.tagName === 'LI')
|
|
689
|
+
const selectedSet = new Set(selectedItems)
|
|
690
|
+
const selectedIndexes = allItems
|
|
691
|
+
.map((item, index) => selectedSet.has(item) ? index : -1)
|
|
692
|
+
.filter(index => index >= 0)
|
|
693
|
+
if (!selectedIndexes.length) { return false }
|
|
694
|
+
|
|
695
|
+
const firstIndex = selectedIndexes[0]
|
|
696
|
+
const lastIndex = selectedIndexes[selectedIndexes.length - 1]
|
|
697
|
+
const selectedAreConsecutive = selectedIndexes.every((index, offset) => index === firstIndex + offset)
|
|
698
|
+
if (!selectedAreConsecutive) { return false }
|
|
699
|
+
|
|
700
|
+
const parentTag = listParent.tagName.toLowerCase()
|
|
701
|
+
if (firstIndex === 0 && lastIndex === allItems.length - 1) {
|
|
702
|
+
if (parentTag === listTag) {
|
|
703
|
+
const paragraphs = selectedItems.map((item) => {
|
|
704
|
+
const paragraph = doc.createElement('p')
|
|
705
|
+
if (item.dir || listParent.dir) { paragraph.dir = item.dir || listParent.dir }
|
|
706
|
+
while (item.firstChild) { paragraph.appendChild(item.firstChild) }
|
|
707
|
+
return paragraph
|
|
708
|
+
})
|
|
709
|
+
paragraphs.forEach(paragraph => listParent.parentNode?.insertBefore(paragraph, listParent))
|
|
710
|
+
listParent.remove()
|
|
711
|
+
const nextRange = doc.createRange()
|
|
712
|
+
nextRange.setStartBefore(paragraphs[0])
|
|
713
|
+
nextRange.setEndAfter(paragraphs[paragraphs.length - 1])
|
|
714
|
+
selection.removeAllRanges()
|
|
715
|
+
selection.addRange(nextRange)
|
|
716
|
+
return true
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const convertedList = replaceListTag(doc, listParent, listTag)
|
|
720
|
+
const nextRange = doc.createRange()
|
|
721
|
+
nextRange.selectNodeContents(convertedList)
|
|
722
|
+
selection.removeAllRanges()
|
|
723
|
+
selection.addRange(nextRange)
|
|
724
|
+
return true
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
const beforeItems = allItems.slice(0, firstIndex)
|
|
728
|
+
const convertedItems = allItems.slice(firstIndex, lastIndex + 1)
|
|
729
|
+
const afterItems = allItems.slice(lastIndex + 1)
|
|
730
|
+
const fragment = doc.createDocumentFragment()
|
|
731
|
+
const createList = (tag: 'ol' | 'ul', items: HTMLLIElement[]) => {
|
|
732
|
+
const nextList = doc.createElement(tag)
|
|
733
|
+
for (const attribute of Array.from(listParent.attributes)) {
|
|
734
|
+
nextList.setAttribute(attribute.name, attribute.value)
|
|
735
|
+
}
|
|
736
|
+
items.forEach(item => nextList.appendChild(item))
|
|
737
|
+
return nextList
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
if (beforeItems.length) { fragment.appendChild(createList(parentTag as 'ol' | 'ul', beforeItems)) }
|
|
741
|
+
if (parentTag === listTag) {
|
|
742
|
+
convertedItems.forEach((item) => {
|
|
743
|
+
const paragraph = doc.createElement('p')
|
|
744
|
+
if (item.dir || listParent.dir) { paragraph.dir = item.dir || listParent.dir }
|
|
745
|
+
while (item.firstChild) { paragraph.appendChild(item.firstChild) }
|
|
746
|
+
fragment.appendChild(paragraph)
|
|
747
|
+
})
|
|
748
|
+
} else {
|
|
749
|
+
fragment.appendChild(createList(listTag, convertedItems))
|
|
750
|
+
}
|
|
751
|
+
if (afterItems.length) { fragment.appendChild(createList(parentTag as 'ol' | 'ul', afterItems)) }
|
|
752
|
+
|
|
753
|
+
const firstSelectedNode = parentTag === listTag
|
|
754
|
+
? fragment.childNodes[beforeItems.length ? 1 : 0]
|
|
755
|
+
: convertedItems[0]
|
|
756
|
+
const lastSelectedNode = parentTag === listTag
|
|
757
|
+
? fragment.childNodes[(beforeItems.length ? 1 : 0) + convertedItems.length - 1]
|
|
758
|
+
: convertedItems[convertedItems.length - 1]
|
|
759
|
+
listParent.replaceWith(fragment)
|
|
760
|
+
|
|
761
|
+
if (firstSelectedNode && lastSelectedNode) {
|
|
762
|
+
const nextRange = doc.createRange()
|
|
763
|
+
nextRange.setStartBefore(firstSelectedNode)
|
|
764
|
+
nextRange.setEndAfter(lastSelectedNode)
|
|
765
|
+
selection.removeAllRanges()
|
|
766
|
+
selection.addRange(nextRange)
|
|
767
|
+
}
|
|
768
|
+
return true
|
|
769
|
+
}
|
|
770
|
+
|
|
379
771
|
function toggleListCommand(state: EditorState, listTag: 'ol' | 'ul') {
|
|
380
772
|
const selectionInfo = getCurrentSelection(state)
|
|
381
773
|
if (!selectionInfo || !state.doc) { return }
|
|
382
774
|
|
|
383
775
|
const { doc } = state
|
|
384
776
|
const { range, selection } = selectionInfo
|
|
777
|
+
if (!range.collapsed && convertSelectedListItems(doc, selection, range, listTag)) { return }
|
|
385
778
|
if (convertSelectedBlocksToList(state, selection, range, listTag)) { return }
|
|
386
779
|
|
|
387
780
|
const currentBlock = findBlockElement(range.commonAncestorContainer)
|
|
@@ -632,25 +1025,35 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
632
1025
|
|
|
633
1026
|
indent: createCommand('Indent', (s) => {
|
|
634
1027
|
const selectionInfo = getCurrentSelection(s)
|
|
635
|
-
if (!selectionInfo) { return }
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
block
|
|
1028
|
+
if (!selectionInfo || !s.doc) { return }
|
|
1029
|
+
if (indentSelectedListItems(s.doc, selectionInfo.selection, selectionInfo.range)) { return }
|
|
1030
|
+
const listItems = getSelectedListItems(s.doc, selectionInfo.range)
|
|
1031
|
+
const blocks = listItems.length
|
|
1032
|
+
? listItems
|
|
1033
|
+
: [findBlockElement(selectionInfo.range.commonAncestorContainer)].filter(Boolean) as HTMLElement[]
|
|
1034
|
+
blocks.forEach((block) => {
|
|
1035
|
+
const isRTL = (block.dir || s.doc?.body.dir) === 'rtl'
|
|
1036
|
+
const prop = isRTL ? 'marginRight' : 'marginLeft'
|
|
1037
|
+
const current = Number.parseInt(block.style[prop] || '0', 10) || 0
|
|
1038
|
+
block.style[prop] = `${current + 24}px`
|
|
1039
|
+
})
|
|
642
1040
|
}),
|
|
643
1041
|
|
|
644
1042
|
outdent: createCommand('Outdent', (s) => {
|
|
645
1043
|
const selectionInfo = getCurrentSelection(s)
|
|
646
|
-
if (!selectionInfo) { return }
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
1044
|
+
if (!selectionInfo || !s.doc) { return }
|
|
1045
|
+
if (outdentSelectedListItems(s.doc, selectionInfo.selection, selectionInfo.range)) { return }
|
|
1046
|
+
const listItems = getSelectedListItems(s.doc, selectionInfo.range)
|
|
1047
|
+
const blocks = listItems.length
|
|
1048
|
+
? listItems
|
|
1049
|
+
: [findBlockElement(selectionInfo.range.commonAncestorContainer)].filter(Boolean) as HTMLElement[]
|
|
1050
|
+
blocks.forEach((block) => {
|
|
1051
|
+
const isRTL = (block.dir || s.doc?.body.dir) === 'rtl'
|
|
1052
|
+
const prop = isRTL ? 'marginRight' : 'marginLeft'
|
|
1053
|
+
const current = Number.parseInt(block.style[prop] || '0', 10) || 0
|
|
1054
|
+
const next = Math.max(0, current - 24)
|
|
1055
|
+
block.style[prop] = next > 0 ? `${next}px` : ''
|
|
1056
|
+
})
|
|
654
1057
|
})
|
|
655
1058
|
}
|
|
656
1059
|
|
|
@@ -668,6 +1071,16 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
668
1071
|
}
|
|
669
1072
|
|
|
670
1073
|
const formatCommands: CommandRegistry = {
|
|
1074
|
+
fontColor: createCommand('Font Color', (s, value) => {
|
|
1075
|
+
if (value) {
|
|
1076
|
+
applyInlineStyle(s, 'color', value)
|
|
1077
|
+
}
|
|
1078
|
+
}),
|
|
1079
|
+
bgColor: createCommand('Background Color', (s, value) => {
|
|
1080
|
+
if (value) {
|
|
1081
|
+
applyInlineStyle(s, 'background-color', value)
|
|
1082
|
+
}
|
|
1083
|
+
}),
|
|
671
1084
|
clear: createCommand('Clear Formatting', (s) => {
|
|
672
1085
|
const selectionInfo = getCurrentSelection(s)
|
|
673
1086
|
if (!selectionInfo || !s.doc) { return }
|
|
@@ -889,7 +1302,7 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
889
1302
|
|
|
890
1303
|
if (!range.collapsed) {
|
|
891
1304
|
const blocks = Array.from(s.doc.body.querySelectorAll<HTMLElement>(ALIGN_TARGET_SELECTOR))
|
|
892
|
-
.filter(el => range
|
|
1305
|
+
.filter(el => rangeOverlapsNodeContent(range, el) && !el.querySelector(ALIGN_TARGET_SELECTOR))
|
|
893
1306
|
if (blocks.length > 0) {
|
|
894
1307
|
blocks.forEach((b) => { b.style.textAlign = value })
|
|
895
1308
|
return
|
|
@@ -924,7 +1337,7 @@ export function createCommandRegistry(state: EditorState): CommandRegistry {
|
|
|
924
1337
|
|
|
925
1338
|
if (!range.collapsed) {
|
|
926
1339
|
const blocks = Array.from(s.doc.body.querySelectorAll<HTMLElement>(ALIGN_TARGET_SELECTOR))
|
|
927
|
-
.filter(el => range
|
|
1340
|
+
.filter(el => rangeOverlapsNodeContent(range, el) && !el.querySelector(ALIGN_TARGET_SELECTOR))
|
|
928
1341
|
if (blocks.length > 0) {
|
|
929
1342
|
blocks.forEach(setDirOnEl)
|
|
930
1343
|
return
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface RichTextMetrics {
|
|
2
|
+
text: string
|
|
3
|
+
characters: number
|
|
4
|
+
words: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function getRichTextMetrics(html: string, doc?: Document): RichTextMetrics {
|
|
8
|
+
let text = ''
|
|
9
|
+
if (doc?.body) {
|
|
10
|
+
text = doc.body.innerText || doc.body.textContent || ''
|
|
11
|
+
} else if (typeof DOMParser !== 'undefined') {
|
|
12
|
+
const body = new DOMParser().parseFromString(html, 'text/html').body
|
|
13
|
+
text = body.innerText || body.textContent || ''
|
|
14
|
+
} else {
|
|
15
|
+
text = html.replace(/<[^>]*>/g, ' ')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const normalizedText = text.replace(/\u200B/g, '').replace(/\s+/g, ' ').trim()
|
|
19
|
+
return {
|
|
20
|
+
text: normalizedText,
|
|
21
|
+
characters: normalizedText.length,
|
|
22
|
+
words: normalizedText ? normalizedText.split(/\s+/).length : 0,
|
|
23
|
+
}
|
|
24
|
+
}
|