Skip to content

Commit cb357b9

Browse files
authored
Support list style (#42)
* Added bullet list style in TextSpanStyle * Removing bullet from final out put string * Apply list style to selected text * Supporting list style with bullet with disc type and single indent * End list style on adding new line twice * fix not able to add more then one new line * Supporting list style
1 parent de378dd commit cb357b9

17 files changed

+663
-139
lines changed

RichEditorDemo/RichEditorDemo/ContentView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ struct ContentView: View {
4040
ToolbarItem(placement: .topBarTrailing) {
4141
Button(action: {
4242

43-
print("Export JSON == \(state.output())")
43+
print("Exported JSON == \(state.output())")
4444
}, label: {
4545
Image(systemName: "checkmark")
4646
.padding()
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//
2+
// RichTextAttributeWriter+List.swift
3+
//
4+
//
5+
// Created by Divyesh Vekariya on 09/05/24.
6+
//
7+
8+
import Foundation
9+
10+
#if canImport(UIKit)
11+
import UIKit
12+
#endif
13+
14+
#if canImport(AppKit) && !targetEnvironment(macCatalyst)
15+
import AppKit
16+
#endif
17+
18+
public extension RichTextAttributeWriter {
19+
20+
/**
21+
Set the text alignment at a certain range.
22+
23+
Unlike some other attributes, this value applies to the
24+
entire paragraph, not just the selected range.
25+
*/
26+
func setRichTextListStyle(
27+
_ listType: ListType,
28+
to newValue: Bool,
29+
at range: NSRange
30+
) {
31+
setListStyle(listType, to: newValue, at: range)
32+
}
33+
}
34+
35+
private extension RichTextAttributeWriter {
36+
37+
func setListStyle(
38+
_ listType: ListType,
39+
to newValue: Bool,
40+
at range: NSRange
41+
) {
42+
guard let string = mutableRichText else { return }
43+
let safeRange = safeRange(for: range)
44+
45+
let searchRange = NSRange(location: max(0, (range.location - 1)), length: min(string.string.utf16Length, (range.length + 1)))
46+
var previousRang: NSRange? = nil
47+
48+
var attributesWithRange: [Int: (range: NSRange, paragraphStyle: NSMutableParagraphStyle)] = [:]
49+
string.beginEditing()
50+
var previousStyle: NSMutableParagraphStyle? = nil
51+
string.enumerateAttribute(.paragraphStyle, in: searchRange) { (attribute, range, _) in
52+
53+
if let style = attribute as? NSMutableParagraphStyle, !style.textLists.isEmpty {
54+
if newValue {
55+
/// For add style
56+
attributesWithRange[attributesWithRange.count] = (range: range, paragraphStyle: style)
57+
58+
if safeRange.location <= range.location && safeRange.upperBound >= range.upperBound {
59+
string.removeAttribute(.paragraphStyle, range: range)
60+
}
61+
62+
if let oldRange = previousRang, let previousStyle = previousStyle, previousStyle.textLists.count == listType.getIndent() {
63+
let location = min(oldRange.location, range.location)
64+
let length = max(oldRange.upperBound, range.upperBound) - location
65+
let combinedRange = NSRange(location: location, length: length)
66+
67+
string.addAttribute(.paragraphStyle, value: previousStyle, range: combinedRange)
68+
previousRang = combinedRange
69+
} else {
70+
let location = min(safeRange.location, range.location)
71+
let length = max(safeRange.upperBound, range.upperBound) - location
72+
let combinedRange = NSRange(location: location, length: length)
73+
74+
string.addAttribute(.paragraphStyle, value: style, range: combinedRange)
75+
previousRang = combinedRange
76+
}
77+
previousStyle = style
78+
} else {
79+
/// Fore Remove Style
80+
if safeRange.closedRange.overlaps(range.closedRange) {
81+
if style.textLists.count == listType.getIndent() {
82+
string.removeAttribute(.paragraphStyle, range: safeRange)
83+
previousRang = nil
84+
previousStyle = nil
85+
}
86+
}
87+
}
88+
}
89+
}
90+
91+
///Add style if not already added
92+
if attributesWithRange.isEmpty {
93+
94+
let paragraphStyle = NSMutableParagraphStyle()
95+
96+
paragraphStyle.alignment = .left
97+
let listItem = NSTextList(markerFormat: listType.getMarkerFormat(), options: 0)
98+
99+
if paragraphStyle.textLists.isEmpty && newValue {
100+
paragraphStyle.textLists.append(listItem)
101+
} else {
102+
paragraphStyle.textLists.removeAll()
103+
}
104+
105+
if !paragraphStyle.textLists.isEmpty {
106+
string.addAttributes([.paragraphStyle: paragraphStyle], range: safeRange)
107+
} else {
108+
string.removeAttribute(.paragraphStyle, range: safeRange)
109+
}
110+
}
111+
112+
string.fixAttributes(in: range)
113+
string.endEditing()
114+
}
115+
}

Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+Style.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ public extension NSMutableAttributedString {
2727
) {
2828
let rangeValue = range ?? richTextRange
2929
let range = safeRange(for: rangeValue)
30+
31+
if style.isList, let style = style.listType {
32+
setRichTextListStyle(style, to: newValue, at: range)
33+
}
34+
35+
guard !style.isList else { return }
36+
3037
let attributeValue = newValue ? 1 : 0
3138
if style == .underline { return setRichTextAttribute(.underlineStyle, to: attributeValue, at: range) }
3239
guard let font = richTextFont(at: range) else { return }

Sources/RichEditorSwiftUI/Data/Models/RichAttributes.swift

Lines changed: 41 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,34 @@ public struct RichAttributes: Codable {
1414
public let italic: Bool?
1515
public let underline: Bool?
1616
public let header: HeaderType?
17-
// public let list: ListType?
17+
public let list: ListType?
18+
public let indent: Int?
1819

1920
public init(
2021
// id: String = UUID().uuidString,
2122
bold: Bool? = nil,
2223
italic: Bool? = nil,
2324
underline: Bool? = nil,
24-
header: HeaderType? = nil
25-
// list: ListType? = nil
25+
header: HeaderType? = nil,
26+
list: ListType? = nil,
27+
indent: Int? = nil
2628
) {
2729
// self.id = id
2830
self.bold = bold
2931
self.italic = italic
3032
self.underline = underline
3133
self.header = header
32-
// self.list = list
34+
self.list = list
35+
self.indent = indent
3336
}
3437

3538
enum CodingKeys: String, CodingKey {
3639
case bold = "bold"
3740
case italic = "italic"
3841
case underline = "underline"
3942
case header = "header"
40-
// case list = "list"
43+
case list = "list"
44+
case indent = "indent"
4145
}
4246

4347
public init(from decoder: Decoder) throws {
@@ -47,7 +51,8 @@ public struct RichAttributes: Codable {
4751
self.italic = try values.decodeIfPresent(Bool.self, forKey: .italic)
4852
self.underline = try values.decodeIfPresent(Bool.self, forKey: .underline)
4953
self.header = try values.decodeIfPresent(HeaderType.self, forKey: .header)
50-
// self.list = try values.decodeIfPresent(ListType.self, forKey: .list)
54+
self.list = try values.decodeIfPresent(ListType.self, forKey: .list)
55+
self.indent = try values.decodeIfPresent(Int.self, forKey: .indent)
5156
}
5257
}
5358

@@ -58,7 +63,8 @@ extension RichAttributes: Hashable {
5863
hasher.combine(italic)
5964
hasher.combine(underline)
6065
hasher.combine(header)
61-
// hasher.combine(list)
66+
hasher.combine(list)
67+
hasher.combine(indent)
6268
}
6369
}
6470

@@ -71,7 +77,8 @@ extension RichAttributes: Equatable {
7177
&& lhs.italic == rhs.italic
7278
&& lhs.underline == rhs.underline
7379
&& lhs.header == rhs.header
74-
// && lhs.list == rhs.list
80+
&& lhs.list == rhs.list
81+
&& lhs.indent == rhs.indent
7582
)
7683
}
7784
}
@@ -80,15 +87,17 @@ extension RichAttributes {
8087
public func copy(bold: Bool? = nil,
8188
header: HeaderType? = nil,
8289
italic: Bool? = nil,
83-
underline: Bool? = nil
84-
// list: ListType? = nil
90+
underline: Bool? = nil,
91+
list: ListType? = nil,
92+
indent: Int? = nil
8593
) -> RichAttributes {
8694
return RichAttributes(
8795
bold: (bold != nil ? bold! : self.bold),
8896
italic: (italic != nil ? italic! : self.italic),
8997
underline: (underline != nil ? underline! : self.underline),
90-
header: (header != nil ? header! : self.header)
91-
// list: (list != nil ? list! : self.list)
98+
header: (header != nil ? header! : self.header),
99+
list: (list != nil ? list! : self.list),
100+
indent: (indent != nil ? indent! : self.indent)
92101
)
93102
}
94103

@@ -102,8 +111,9 @@ extension RichAttributes {
102111
bold: (att.bold != nil ? (byAdding ? att.bold! : nil) : self.bold),
103112
italic: (att.italic != nil ? (byAdding ? att.italic! : nil) : self.italic),
104113
underline: (att.underline != nil ? (byAdding ? att.underline! : nil) : self.underline),
105-
header: (att.header != nil ? (byAdding ? att.header! : nil) : self.header)
106-
// list: (att.list != nil ? (byAdding ? att.list! : nil) : self.list)
114+
header: (att.header != nil ? (byAdding ? att.header! : nil) : self.header),
115+
list: (att.list != nil ? (byAdding ? att.list! : nil) : self.list),
116+
indent: (att.indent != nil ? (byAdding ? att.indent! : nil) : self.indent)
107117
)
108118
}
109119
}
@@ -123,9 +133,9 @@ extension RichAttributes {
123133
if let header = header {
124134
styles.append(header.getTextSpanStyle())
125135
}
126-
// if let list = list {
127-
// styles.append(.list(list))
128-
// }
136+
if let list = list {
137+
styles.append(list.getTextSpanStyle())
138+
}
129139
return styles
130140
}
131141

@@ -143,9 +153,9 @@ extension RichAttributes {
143153
if let header = header {
144154
styles.insert(header.getTextSpanStyle())
145155
}
146-
// if let list = list {
147-
// styles.insert(.list(list))
148-
// }
156+
if let list = list {
157+
styles.insert(list.getTextSpanStyle())
158+
}
149159
return styles
150160
}
151161
}
@@ -173,11 +183,8 @@ extension RichAttributes {
173183
return header == .h5
174184
case .h6:
175185
return header == .h6
176-
// case .bullet:
177-
// return list == .bullet
178-
// case .ordered:
179-
// return list == .ordered
180-
186+
case .bullet:
187+
return list == .bullet(indent)
181188
}
182189
}
183190
}
@@ -192,7 +199,9 @@ internal func getRichAttributesFor(styles: [RichTextStyle]) -> RichAttributes {
192199
var italic: Bool? = nil
193200
var underline: Bool? = nil
194201
var header: HeaderType? = nil
195-
// var list: ListType? = nil
202+
var list: ListType? = nil
203+
var indent: Int? = nil
204+
196205
for style in styles {
197206
switch style {
198207
case .bold:
@@ -213,16 +222,18 @@ internal func getRichAttributesFor(styles: [RichTextStyle]) -> RichAttributes {
213222
header = .h5
214223
case .h6:
215224
header = .h6
216-
// case .list(let listType):
217-
// list = listType
225+
case .bullet(let indentIndex):
226+
list = .bullet(indentIndex)
227+
indent = indentIndex
218228
case .default:
219229
header = .default
220230
}
221231
}
222232
return RichAttributes(bold: bold,
223233
italic: italic,
224234
underline: underline,
225-
header: header
226-
// list: list
235+
header: header,
236+
list: list,
237+
indent: indent
227238
)
228239
}

Sources/RichEditorSwiftUI/Data/Models/RichText.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,3 @@ public struct RichTextSpan: Codable {
3030
}
3131
}
3232

33-
public enum ListType: String, Codable {
34-
case bullet = "bullet"
35-
case ordered = "ordered"
36-
}

Sources/RichEditorSwiftUI/Data/Models/RichTextSpanInternal.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,7 @@ extension RichTextSpanInternal: Hashable {
4848

4949
extension RichTextSpanInternal {
5050
public var spanRange: NSRange {
51-
let range = NSRange(location: from, length: (to - from) + 1)
52-
53-
guard range.length > 0 else {
54-
return .init(location: range.location,
55-
length: 0)
56-
}
51+
let range = NSRange(location: from, length: max(((to - from) + 1), 0))
5752
return range
5853
}
5954

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//
2+
// RichEditorState+UITextView.swift
3+
//
4+
//
5+
// Created by Divyesh Vekariya on 07/06/24.
6+
//
7+
8+
import Foundation
9+
import UIKit
10+
11+
extension RichEditorState {
12+
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
13+
/// Handle newly added text according to our convenience
14+
15+
// If the user just hit enter/newline
16+
if text == .newLine {
17+
let previousRange = NSRange(location: range.location > 1 ? (range.location - 2) : range.location, length: 0)
18+
if textView.text[previousRange.closedRange].string() == .newLine {
19+
if activeStyles.contains(where: { $0.isList }) {
20+
endListStyle()
21+
return false
22+
}
23+
}
24+
}
25+
// else if text == .tab {
26+
// if activeStyles.contains(where: { $0.isList }) {
27+
// addSubList()
28+
// return false
29+
// }
30+
// }
31+
32+
return true
33+
}
34+
35+
func endListStyle() {
36+
if let itemToRemove = activeStyles.first(where: { $0.isList }), let listType = itemToRemove.listType {
37+
if listType.getIndent() <= 0 {
38+
toggleStyle(style: itemToRemove)
39+
}
40+
// else {
41+
// activeStyles.remove(itemToRemove)
42+
// toggleStyle(style: listType.moveIndentBackward().getTextSpanStyle())
43+
// }
44+
}
45+
}
46+
47+
// func addSubList() {
48+
// if let listStyle = activeStyles.first(where: { $0.isList }) {
49+
// if let listType = listStyle.listType {
50+
// let newListStyle = listType.moveIndentForward().getTextSpanStyle()
51+
// activeStyles.remove(listStyle)
52+
// toggleStyle(style: newListStyle)
53+
// }
54+
// }
55+
// }
56+
}

0 commit comments

Comments
 (0)