Skip to content

Commit 448205a

Browse files
committed
Fixed overlaping style will gnerate duplicate and incorect span
1 parent bc7b8a9 commit 448205a

File tree

2 files changed

+84
-190
lines changed

2 files changed

+84
-190
lines changed

RichEditorDemo/RichEditorDemo/ContentView.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ struct ContentView: View {
2828
{
2929
self.state = .init(richText: richText)
3030
} else {
31-
self.state = .init(input: "Bold \n Italic \n Underline \n Strikethrough \n ")
31+
self.state = .init(input: "Hello World!")
3232
}
3333
}
3434
}

Sources/RichEditorSwiftUI/UI/Editor/RichEditorState+Spans.swift

+83-189
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,9 @@ extension RichEditorState {
6767
- style: is of type RichTextSpanStyle
6868
*/
6969
public func updateStyle(style: RichTextSpanStyle, shouldRegisterUndo: Bool = true) {
70+
setInternalStyles(style: style)
7071
setStyle(style, shouldRegisterUndo: shouldRegisterUndo)
7172
/// Don't change order of function call as it is comparing active attributes with new one so updating it before applying attribute will break the behavior of undo and redo
72-
setInternalStyles(style: style)
7373
}
7474
}
7575

@@ -150,8 +150,9 @@ extension RichEditorState {
150150
if style.isHeaderStyle || style.isDefault //|| style.isList
151151
|| style.isAlignmentStyle
152152
{
153+
let shouldAdd = style.isHeaderStyle ? style.headerType != .default : !style.isDefault
153154
handleAddOrRemoveStyleToLine(
154-
in: selectedRange, style: style, byAdding: !style.isDefault)
155+
in: selectedRange, style: style, byAdding: shouldAdd)
155156
if shouldRegisterUndo {
156157
registerUndoForSetStyle(newStyle: style)
157158
}
@@ -176,16 +177,17 @@ extension RichEditorState {
176177
addStyle = CGFloat(size) != CGFloat.standardRichTextFontSize
177178
}
178179
case .font(let fontName):
179-
if let fontName {
180-
addStyle = fontName == self.fontName
180+
let defaultName = RichTextFont.PickerFont.standardSystemFontDisplayName
181+
if let fontName, fontName != defaultName {
182+
addStyle = fontName != defaultName
183+
} else {
184+
addStyle = false
181185
}
182186
case .color(let color):
183187
let defaultColor = RichTextColor.foreground.adjust(nil, for: colorScheme)
184188
if let color, color.toHex() != defaultColor.toHex() {
185189
if let internalColor = self.color(for: .foreground) {
186-
addStyle = (Color(internalColor) != color)
187-
} else {
188-
addStyle = true
190+
addStyle = (Color(internalColor) == color && Color(internalColor) != defaultColor)
189191
}
190192
} else {
191193
addStyle = false
@@ -194,9 +196,7 @@ extension RichEditorState {
194196
let defaultColor = RichTextColor.background.adjust(nil, for: colorScheme)
195197
if let color = bgColor, color.toHex() != defaultColor.toHex() {
196198
if let internalColor = self.color(for: .background) {
197-
addStyle = (Color(internalColor) == color)
198-
} else {
199-
addStyle = true
199+
addStyle = (Color(internalColor) == color && Color(internalColor) != defaultColor)
200200
}
201201
} else {
202202
addStyle = false
@@ -207,6 +207,7 @@ extension RichEditorState {
207207
}
208208
case .link(let link):
209209
addStyle = link != nil
210+
210211
default:
211212
return addStyle
212213
}
@@ -587,8 +588,10 @@ extension RichEditorState {
587588

588589
internalSpans.removeAll(where: { selectedParts.contains($0) })
589590
}
591+
}
590592

591-
//MARK: - Add Header style
593+
//MARK: - Process Spans for style
594+
extension RichEditorState {
592595
/**
593596
This will create span for selected text with provided style
594597
- Parameters:
@@ -604,151 +607,97 @@ extension RichEditorState {
604607

605608
var processedSpans: [RichTextSpanInternal] = []
606609

607-
let completeOverlap = getCompleteOverlappingSpans(for: range)
608-
var partialOverlap = getPartialOverlappingSpans(for: range)
609-
var sameSpans = getSameSpans(for: range)
610-
611-
partialOverlap.removeAll(where: { completeOverlap.contains($0) })
612-
sameSpans.removeAll(where: { completeOverlap.contains($0) })
613-
614-
let partialOverlapSpan = processPartialOverlappingSpans(
615-
partialOverlap, range: range, style: style, addStyle: addStyle)
616-
let completeOverlapSpan = processCompleteOverlappingSpans(
617-
completeOverlap, range: range, style: style, addStyle: addStyle)
618-
let sameSpan = processSameSpans(
619-
sameSpans, range: range, style: style, addStyle: addStyle)
620-
621-
processedSpans.append(contentsOf: partialOverlapSpan)
622-
processedSpans.append(contentsOf: completeOverlapSpan)
623-
processedSpans.append(contentsOf: sameSpan)
624-
625-
processedSpans = mergeSameStyledSpans(processedSpans)
626-
627-
internalSpans.removeAll(where: {
628-
$0.closedRange.overlaps(range.closedRange)
629-
})
630-
internalSpans.append(contentsOf: processedSpans)
631-
internalSpans = mergeSameStyledSpans(internalSpans)
632-
internalSpans.sort(by: { $0.from < $1.from })
633-
}
634-
635-
private func processCompleteOverlappingSpans(
636-
_ spans: [RichTextSpanInternal], range: NSRange,
637-
style: RichTextSpanStyle, addStyle: Bool = true
638-
) -> [RichTextSpanInternal] {
639-
var processedSpans: [RichTextSpanInternal] = []
640-
641-
for span in spans {
642-
if span.closedRange.isInRange(range.closedRange) {
643-
processedSpans.append(
644-
span.copy(
645-
attributes: span.attributes?.copy(
646-
with: style, byAdding: addStyle)))
647-
} else {
648-
if span.from < range.lowerBound {
649-
let leftPart = span.copy(to: range.lowerBound - 1)
650-
processedSpans.append(leftPart)
651-
}
652-
653-
if span.from <= (range.lowerBound)
654-
&& span.to >= (range.upperBound - 1)
655-
{
656-
let centerPart = span.copy(
657-
from: range.lowerBound, to: range.upperBound - 1,
658-
attributes: span.attributes?.copy(
659-
with: style, byAdding: addStyle))
660-
processedSpans.append(centerPart)
661-
}
662-
663-
if span.to > (range.upperBound - 1) {
664-
let rightPart = span.copy(from: range.upperBound)
665-
processedSpans.append(rightPart)
610+
// First split existing spans at selection boundaries
611+
let splitPoints = Set([range.lowerBound, range.upperBound])
612+
var currentSpans = internalSpans
613+
614+
// Split spans at boundaries
615+
for splitPoint in splitPoints {
616+
currentSpans = currentSpans.flatMap { span -> [RichTextSpanInternal] in
617+
if span.from < splitPoint && span.to >= splitPoint {
618+
return [
619+
span.copy(to: splitPoint - 1),
620+
span.copy(from: splitPoint),
621+
]
666622
}
623+
return [span]
667624
}
668625
}
669626

670-
processedSpans = mergeSameStyledSpans(processedSpans)
671-
672-
return processedSpans
673-
}
674-
675-
private func processPartialOverlappingSpans(
676-
_ spans: [RichTextSpanInternal], range: NSRange,
677-
style: RichTextSpanStyle, addStyle: Bool = true
678-
) -> [RichTextSpanInternal] {
679-
var processedSpans: [RichTextSpanInternal] = []
680-
681-
for span in spans {
682-
if span.from < range.location {
683-
let leftPart = span.copy(to: range.lowerBound - 1)
684-
let rightPart = span.copy(
685-
from: range.lowerBound,
686-
attributes: span.attributes?.copy(
687-
with: style, byAdding: addStyle))
688-
processedSpans.append(leftPart)
689-
processedSpans.append(rightPart)
627+
// Process spans in selection range
628+
for span in currentSpans {
629+
if span.closedRange.overlaps(range.closedRange) {
630+
// Span is within selection - apply new style
631+
if span.closedRange.isInRange(range.closedRange) {
632+
let newAttributes = span.attributes?.copy(with: style, byAdding: addStyle)
633+
processedSpans.append(span.copy(attributes: newAttributes))
634+
}
635+
// Span partially overlaps - split and apply style only to overlapping part
636+
else {
637+
if span.from < range.lowerBound {
638+
processedSpans.append(span.copy(to: range.lowerBound - 1))
639+
}
640+
641+
let overlapStart = max(span.from, range.lowerBound)
642+
let overlapEnd = min(span.to, range.upperBound - 1)
643+
644+
if overlapStart <= overlapEnd {
645+
let newAttributes = span.attributes?.copy(with: style, byAdding: addStyle)
646+
processedSpans.append(
647+
span.copy(
648+
from: overlapStart,
649+
to: overlapEnd,
650+
attributes: newAttributes
651+
)
652+
)
653+
}
654+
655+
if span.to >= range.upperBound {
656+
processedSpans.append(span.copy(from: range.upperBound))
657+
}
658+
}
690659
} else {
691-
let leftPart = span.copy(
692-
to: min(span.to, range.upperBound),
693-
attributes: span.attributes?.copy(
694-
with: style, byAdding: addStyle))
695-
let rightPart = span.copy(from: range.location)
696-
processedSpans.append(leftPart)
697-
processedSpans.append(rightPart)
660+
// Span outside selection - keep unchanged
661+
processedSpans.append(span)
698662
}
699663
}
700664

665+
// Merge adjacent spans with identical styles
701666
processedSpans = mergeSameStyledSpans(processedSpans)
702-
return processedSpans
703-
}
704667

705-
private func processSameSpans(
706-
_ spans: [RichTextSpanInternal], range: NSRange,
707-
style: RichTextSpanStyle, addStyle: Bool = true
708-
) -> [RichTextSpanInternal] {
709-
var processedSpans: [RichTextSpanInternal] = []
710-
711-
processedSpans = spans.map({
712-
$0.copy(
713-
attributes: $0.attributes?.copy(with: style, byAdding: addStyle)
714-
)
715-
})
716-
717-
processedSpans = mergeSameStyledSpans(processedSpans)
718-
return processedSpans
668+
// Update internal spans
669+
internalSpans = processedSpans.sorted(by: { $0.from < $1.from })
719670
}
720671

721-
// merge adjacent spans with same style
722-
private func mergeSameStyledSpans(_ spans: [RichTextSpanInternal])
723-
-> [RichTextSpanInternal]
724-
{
672+
///Merge adjacent spans with same style
673+
private func mergeSameStyledSpans(_ spans: [RichTextSpanInternal]) -> [RichTextSpanInternal] {
725674
guard !spans.isEmpty else { return [] }
675+
726676
var mergedSpans: [RichTextSpanInternal] = []
727-
var previousSpan: RichTextSpanInternal?
677+
var currentSpan: RichTextSpanInternal? = spans[0]
728678

729-
for span in spans.sorted(by: { $0.from < $1.from }) {
730-
if let current = previousSpan {
731-
if span.attributes?.stylesSet()
732-
== current.attributes?.stylesSet()
733-
{
734-
// Merge overlapping spans
735-
previousSpan = current.copy(to: max(current.to, span.to))
736-
} else {
737-
// Add merged span and start a new span
738-
mergedSpans.append(current)
739-
previousSpan = span
740-
}
679+
for nextSpan in spans.dropFirst() {
680+
guard let current = currentSpan else {
681+
currentSpan = nextSpan
682+
continue
683+
}
684+
685+
// Only merge if styles exactly match and spans are adjacent
686+
if current.attributes?.stylesSet() == nextSpan.attributes?.stylesSet()
687+
&& current.to + 1 == nextSpan.from
688+
{
689+
currentSpan = current.copy(to: nextSpan.to)
741690
} else {
742-
previousSpan = span
691+
mergedSpans.append(current)
692+
currentSpan = nextSpan
743693
}
744694
}
745695

746-
// Append the last current span
747-
if let lastSpan = previousSpan {
696+
if let lastSpan = currentSpan {
748697
mergedSpans.append(lastSpan)
749698
}
750699

751-
return mergedSpans.sorted(by: { $0.from < $1.from })
700+
return mergedSpans
752701
}
753702
}
754703

@@ -783,61 +732,6 @@ extension RichEditorState {
783732
}
784733
}
785734

786-
//MARK: - RichTextSpanInternal Helper
787-
extension RichEditorState {
788-
/**
789-
This will provide overlapping span for range
790-
- Parameters:
791-
- selectedRange: is of type NSRange
792-
*/
793-
private func getOverlappingSpans(for selectedRange: NSRange)
794-
-> [RichTextSpanInternal]
795-
{
796-
return internalSpans.filter {
797-
$0.closedRange.overlaps(selectedRange.closedRange)
798-
}
799-
}
800-
801-
/**
802-
This will provide partial overlapping span for range
803-
- Parameters:
804-
- selectedRange: selectedRange is of type NSRange
805-
*/
806-
func getPartialOverlappingSpans(for selectedRange: NSRange)
807-
-> [RichTextSpanInternal]
808-
{
809-
return getOverlappingSpans(for: selectedRange).filter({
810-
$0.closedRange.isPartialOverlap(selectedRange.closedRange)
811-
})
812-
}
813-
814-
/**
815-
This will provide complete overlapping span for range
816-
- Parameters:
817-
- selectedRange: selectedRange is of type NSRange
818-
*/
819-
func getCompleteOverlappingSpans(for selectedRange: NSRange)
820-
-> [RichTextSpanInternal]
821-
{
822-
return getOverlappingSpans(for: selectedRange).filter({
823-
$0.closedRange.isInRange(selectedRange.closedRange)
824-
|| selectedRange.closedRange.isInRange($0.closedRange)
825-
})
826-
}
827-
828-
/**
829-
This will provide same span for range
830-
- Parameters:
831-
- selectedRange: selectedRange is of type NSRange
832-
*/
833-
834-
func getSameSpans(for selectedRange: NSRange) -> [RichTextSpanInternal] {
835-
return getOverlappingSpans(for: selectedRange).filter({
836-
$0.closedRange.isSameAs(selectedRange.closedRange)
837-
})
838-
}
839-
}
840-
841735
//MARK: - Helper Methods
842736
extension RichEditorState {
843737
/**

0 commit comments

Comments
 (0)