Skip to content

Commit c4cc566

Browse files
authored
Support export options (#61)
* Support export options * Fix padding and tapping area of tool bar buttons * Fix styles are added and removed on selecting text
1 parent 1136518 commit c4cc566

32 files changed

+1435
-35
lines changed

RichEditorDemo/RichEditorDemo/ContentView.swift

+102-11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ struct ContentView: View {
1313

1414
@ObservedObject var state: RichEditorState
1515
@State private var isInspectorPresented = false
16+
@State private var fileName: String = ""
17+
@State private var exportFormat: RichTextDataFormat? = nil
18+
@State private var otherExportFormat: RichTextExportOption? = nil
19+
@State private var exportService: StandardRichTextExportService = .init()
1620

1721
init(state: RichEditorState? = nil) {
1822
if let state {
@@ -61,22 +65,109 @@ struct ContentView: View {
6165
}
6266
.padding(10)
6367
.toolbar {
64-
ToolbarItem(placement: .automatic) {
65-
Button(
66-
action: {
67-
print("Exported JSON == \(state.output())")
68-
},
69-
label: {
70-
Image(systemName: "printer.inverse")
71-
.padding()
72-
})
68+
ToolbarItemGroup(placement: .automatic) {
69+
toolBarGroup
7370
}
7471
}
7572
.background(colorScheme == .dark ? .black : .gray.opacity(0.07))
7673
.navigationTitle("Rich Editor")
77-
#if os(iOS)
78-
.navigationBarTitleDisplayMode(.inline)
74+
.alert("Enter file name", isPresented: getBindingAlert()) {
75+
TextField("Enter file name", text: $fileName)
76+
Button("OK", action: submit)
77+
} message: {
78+
Text("Please enter file name")
79+
}
80+
.focusedValue(\.richEditorState, state)
81+
.toolbarRole(.automatic)
82+
.richTextFormatSheetConfig(.init(colorPickers: colorPickers))
83+
.richTextFormatSidebarConfig(
84+
.init(
85+
colorPickers: colorPickers,
86+
fontPicker: isMac
87+
)
88+
)
89+
.richTextFormatToolbarConfig(.init(colorPickers: []))
90+
}
91+
}
92+
93+
var toolBarGroup: some View {
94+
return Group {
95+
RichTextExportMenu.init(
96+
formatAction: { format in
97+
exportFormat = format
98+
},
99+
otherOptionAction: { format in
100+
otherExportFormat = format
101+
}
102+
)
103+
#if !os(macOS)
104+
.frame(width: 25, alignment: .center)
79105
#endif
106+
Button(
107+
action: {
108+
print("Exported JSON == \(state.outputAsString())")
109+
},
110+
label: {
111+
Image(systemName: "printer.inverse")
112+
}
113+
)
114+
#if !os(macOS)
115+
.frame(width: 25, alignment: .center)
116+
#endif
117+
Toggle(isOn: $isInspectorPresented) {
118+
Image.richTextFormatBrush
119+
.resizable()
120+
.aspectRatio(1, contentMode: .fit)
121+
}
122+
#if !os(macOS)
123+
.frame(width: 25, alignment: .center)
124+
#endif
125+
}
126+
}
127+
128+
func getBindingAlert() -> Binding<Bool> {
129+
.init(get: { exportFormat != nil || otherExportFormat != nil }, set: { newValue in
130+
exportFormat = nil
131+
otherExportFormat = nil
132+
})
133+
}
134+
135+
func submit() {
136+
guard !fileName.isEmpty else { return }
137+
var path: URL?
138+
139+
if let exportFormat {
140+
path = try? exportService.generateExportFile(withName: fileName, content: state.attributedString, format: exportFormat)
141+
}
142+
if let otherExportFormat {
143+
switch otherExportFormat {
144+
case .pdf:
145+
path = try? exportService.generatePdfExportFile(withName: fileName, content: state.attributedString)
146+
case .json:
147+
path = try? exportService.generateJsonExportFile(withName: fileName, content: state.richText)
148+
}
80149
}
150+
if let path {
151+
print("Exported at path == \(path)")
152+
}
153+
}
154+
}
155+
156+
private extension ContentView {
157+
158+
var isMac: Bool {
159+
#if os(macOS)
160+
true
161+
#else
162+
false
163+
#endif
164+
}
165+
166+
var colorPickers: [RichTextColor] {
167+
[.foreground, .background]
168+
}
169+
170+
var formatToolbarEdge: VerticalEdge {
171+
isMac ? .top : .bottom
81172
}
82173
}

Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Actions.swift

-2
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,9 @@ extension RichTextCoordinator {
4343
textView.highlightingStyle = style
4444
case .setStyle(let style, let newValue):
4545
setStyle(style, to: newValue)
46-
return
4746
case .stepFontSize(let points):
4847
textView.stepRichTextFontSize(points: points)
4948
syncContextWithTextView()
50-
return
5149
case .stepIndent(_):
5250
// textView.stepRichTextIndent(points: points)
5351
return

Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ open class RichTextCoordinator: NSObject {
6262
- Parameters:
6363
- text: The rich text to edit.
6464
- textView: The rich text view to keep in sync.
65-
- richTextContext: The context to keep in sync.
65+
- richEditorState: The context to keep in sync.
6666
*/
6767
public init(
6868
text: Binding<NSAttributedString>,

Sources/RichEditorSwiftUI/Data/Models/HeaderType.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// HeaderType.swift
3-
//
3+
// RichEditorSwiftUI
44
//
55
// Created by Divyesh Vekariya on 29/04/24.
66
//

Sources/RichEditorSwiftUI/Data/Models/RichAttributes.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//
22
// RichAttributes.swift
3-
//
3+
// RichEditorSwiftUI
44
//
55
// Created by Divyesh Vekariya on 04/04/24.
66
//

Sources/RichEditorSwiftUI/Data/Models/RichText.swift

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ public struct RichText: Codable {
1515
public init(spans: [RichTextSpan] = []) {
1616
self.spans = spans
1717
}
18+
19+
func encodeToData() throws -> Data {
20+
return try JSONEncoder().encode(self)
21+
}
1822
}
1923

2024
public struct RichTextSpan: Codable {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// NSAttributedString+Export.swift
3+
// RichEditorSwiftUI
4+
//
5+
// Created by Divyesh Vekariya on 26/11/24.
6+
//
7+
8+
import Foundation
9+
10+
@MainActor
11+
extension NSAttributedString {
12+
13+
/// Make all text black to account for dark mode.
14+
func withBlackText() -> NSAttributedString {
15+
let mutable = NSMutableAttributedString(attributedString: self)
16+
let range = mutable.safeRange(for: NSRange(location: 0, length: mutable.length))
17+
mutable.setRichTextAttribute(.foregroundColor, to: ColorRepresentable.black, at: range)
18+
return mutable
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//
2+
// RichTextExportError.swift
3+
// RichEditorSwiftUI
4+
//
5+
// Created by Divyesh Vekariya on 26/11/24.
6+
//
7+
8+
import Foundation
9+
10+
/**
11+
This enum defines errors that can be thrown when failing to
12+
export rich text.
13+
*/
14+
public enum RichTextExportError: Error {
15+
16+
/// This error occurs when no file could be generated at a certain url.
17+
case cantCreateFile(at: URL)
18+
19+
/// This error occurs when no file could be generated in a certain directory.
20+
case cantCreateFileUrl(in: FileManager.SearchPathDirectory)
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// RichTextExportMenu.swift
3+
// RichEditorSwiftUI
4+
//
5+
// Created by Divyesh Vekariya on 26/11/24.
6+
//
7+
8+
#if iOS || macOS || os(visionOS)
9+
import SwiftUI
10+
11+
/**
12+
This menu can be used to trigger various export actions for
13+
a list of ``RichTextDataFormat`` values.
14+
15+
This menu uses a ``RichTextDataFormat/Menu`` configured for
16+
exporting, with customizable actions and data formats.
17+
*/
18+
public struct RichTextExportMenu: View {
19+
20+
public init(
21+
title: String = RTEL10n.menuExportAs.text,
22+
icon: Image = .richTextExport,
23+
formats: [RichTextDataFormat] = RichTextDataFormat.libraryFormats,
24+
otherFormats: [RichTextExportOption] = .all,
25+
formatAction: @escaping (RichTextDataFormat) -> Void,
26+
otherOptionAction: ((RichTextExportOption) -> Void)? = nil
27+
) {
28+
self.menu = RichTextDataFormat.Menu(
29+
title: title,
30+
icon: icon,
31+
formats: formats,
32+
otherFormats: otherFormats,
33+
formatAction: formatAction,
34+
otherOptionAction: otherOptionAction
35+
)
36+
}
37+
38+
private let menu: RichTextDataFormat.Menu
39+
40+
public var body: some View {
41+
menu
42+
}
43+
}
44+
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// RichTextExportOption.swift
3+
// RichEditorSwiftUI
4+
//
5+
// Created by Divyesh Vekariya on 27/11/24.
6+
//
7+
8+
import Foundation
9+
10+
public enum RichTextExportOption: CaseIterable, Equatable, Identifiable {
11+
case pdf
12+
case json
13+
}
14+
15+
public extension RichTextExportOption {
16+
/// The format's unique identifier.
17+
var id: String {
18+
switch self {
19+
case .pdf: "pdf"
20+
case .json: "json"
21+
}
22+
}
23+
24+
/// The format's file format display text.
25+
var fileFormatText: String {
26+
switch self {
27+
case .pdf: RTEL10n.fileFormatPdf.text
28+
case .json: RTEL10n.fileFormatJson.text
29+
}
30+
}
31+
}
32+
33+
public extension Collection where Element == RichTextExportOption {
34+
35+
static var all: [Element] { RichTextExportOption.allCases }
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//
2+
// RichTextExportService.swift
3+
// RichEditorSwiftUI
4+
//
5+
// Created by Divyesh Vekariya on 26/11/24.
6+
//
7+
8+
import Foundation
9+
10+
/**
11+
This protocol can be implemented by any classes that can be
12+
used to export rich text to files.
13+
*/
14+
@preconcurrency @MainActor
15+
public protocol RichTextExportService: AnyObject {
16+
17+
/**
18+
Generate an export file with a certain name and content,
19+
that uses a certain rich text data format.
20+
21+
- Parameters:
22+
- fileName: The preferred file name.
23+
- content: The rich text content to export.
24+
- format: The rich text format to use when exporting.
25+
*/
26+
func generateExportFile(
27+
withName fileName: String,
28+
content: NSAttributedString,
29+
format: RichTextDataFormat
30+
) throws -> URL
31+
32+
/**
33+
Generate a PDF export file with a certain name and rich
34+
text content.
35+
36+
- Parameters:
37+
- fileName: The preferred file name.
38+
- content: The rich text content to export.
39+
*/
40+
func generatePdfExportFile(
41+
withName fileName: String,
42+
content: NSAttributedString
43+
) throws -> URL
44+
45+
/**
46+
Generate a JSON export file with a certain name and rich
47+
text content.
48+
49+
- Parameters:
50+
- fileName: The preferred file name.
51+
- content: The rich text (`RichText`) content to export.
52+
*/
53+
func generateJsonExportFile(
54+
withName fileName: String,
55+
content: RichText
56+
) throws -> URL
57+
58+
/**
59+
Get `Data` for with provided `RichTextDataFormat`
60+
*/
61+
func getDataFor(_ string: NSAttributedString, format: RichTextDataFormat) throws -> Data
62+
63+
/**
64+
Get `Data` for `PDF` format.
65+
*/
66+
func getDataForPdfFormat(_ string: NSAttributedString) throws -> Data
67+
/**
68+
Get `Data` for `JSON` format.
69+
*/
70+
func getDataForJsonFormat(_ richText: RichText) throws -> Data
71+
}

0 commit comments

Comments
 (0)