Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add image support #75

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,19 @@ The editor offers the following <b>options</b>:
- [x] Font family
- [x] Background color
- [x] Export with .txt, .rtf, .pdf, .json
- [x] Link
- [ ] Link
- [ ] Image Attachment
- [ ] Undo/Redo

## What’s Coming Next for RichEditorSwiftUI?🚀

We’re thrilled about the future of **RichEditorSwiftUI!** 🎉 Check out the exciting features currently in development:

- **Link Support:** Easily add hyperlinks to your rich text content.
- **Image Drop:** Drag and drop images directly into your editor for seamless integration.
- **Undo & Redo:** Effortlessly step forward or backward in your edits for greater control.

Thank you for your support and feedback—it fuels our journey. Stay tuned for these enhancements and more! 🙌

## Screenshots

Expand Down
355 changes: 190 additions & 165 deletions RichEditorDemo/RichEditorDemo/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,183 +9,208 @@ import RichEditorSwiftUI
import SwiftUI

struct ContentView: View {
@Environment(\.colorScheme) var colorScheme

@ObservedObject var state: RichEditorState
@State private var isInspectorPresented = false
@State private var fileName: String = ""
@State private var exportFormat: RichTextDataFormat? = nil
@State private var otherExportFormat: RichTextExportOption? = nil
@State private var exportService: StandardRichTextExportService = .init()

init(state: RichEditorState? = nil) {
if let state {
self.state = state
} else {
if let richText = readJSONFromFile(
fileName: "Sample_json",
type: RichText.self)
{
self.state = .init(richText: richText)
} else {
self.state = .init(input: "Hello World!")
}
}
}
@Environment(\.colorScheme) var colorScheme

@ObservedObject var state: RichEditorState
@State private var isInspectorPresented = false
@State private var fileName: String = ""
@State private var exportFormat: RichTextDataFormat? = nil
@State private var otherExportFormat: RichTextExportOption? = nil
@State private var exportService: StandardRichTextExportService = .init()

init(state: RichEditorState? = nil) {
if let state {
self.state = state
} else {
if let richText = readJSONFromFile(
fileName: "Sample_json",
type: RichText.self)
{
self.state = .init(richText: richText, handleImageDropAction: handleImages(_:_:))
} else {
self.state = .init(
input: "Hello World!",
handleImageDropAction: handleImages(_:_:))
}

var body: some View {
NavigationStack {
VStack {
#if os(macOS)
RichTextFormat.Toolbar(context: state)
#endif

RichTextEditor(
context: _state,
viewConfiguration: { _ in

}
)
.background(
colorScheme == .dark ? .gray.opacity(0.3) : Color.white
)
.cornerRadius(10)

#if os(iOS)
RichTextKeyboardToolbar(
context: state,
leadingButtons: { $0 },
trailingButtons: { $0 },
formatSheet: { $0 }
)
#endif
}
#if os(iOS) || os(macOS)
.inspector(isPresented: $isInspectorPresented) {
RichTextFormat.Sidebar(context: state)
#if os(macOS)
.inspectorColumnWidth(
min: 200, ideal: 200, max: 315)
#endif
}
#endif
.padding(10)
#if os(iOS) || os(macOS)
.toolbar {
ToolbarItemGroup(placement: .automatic) {
toolBarGroup
}
}
#endif
.background(colorScheme == .dark ? .black : .gray.opacity(0.07))
.navigationTitle("Rich Editor")
.alert("Enter file name", isPresented: getBindingAlert()) {
TextField("Enter file name", text: $fileName)
Button("OK", action: submit)
} message: {
Text("Please enter file name")
}
.focusedValue(\.richEditorState, state)
.toolbarRole(.automatic)
#if os(iOS) || os(macOS) || os(visionOS)
.richTextFormatSheetConfig(.init(colorPickers: colorPickers))
.richTextFormatSidebarConfig(
.init(
colorPickers: colorPickers,
fontPicker: isMac
)
)
.richTextFormatToolbarConfig(.init(colorPickers: []))
#endif
}
}
}

#if os(iOS) || os(macOS)
var toolBarGroup: some View {
return Group {
RichTextExportMenu.init(
formatAction: { format in
exportFormat = format
},
otherOptionAction: { format in
otherExportFormat = format
}
)
#if !os(macOS)
.frame(width: 25, alignment: .center)
#endif
Button(
action: {
print("Exported JSON == \(state.outputAsString())")
},
label: {
Image(systemName: "printer.inverse")
}
)
#if !os(macOS)
.frame(width: 25, alignment: .center)
#endif
Toggle(isOn: $isInspectorPresented) {
Image.richTextFormatBrush
.resizable()
.aspectRatio(1, contentMode: .fit)
}
#if !os(macOS)
.frame(width: 25, alignment: .center)
#endif
}
}
#endif

func getBindingAlert() -> Binding<Bool> {
.init(
get: { exportFormat != nil || otherExportFormat != nil },
set: { newValue in
exportFormat = nil
otherExportFormat = nil
})
}

func submit() {
guard !fileName.isEmpty else { return }
var path: URL?
var body: some View {
NavigationStack {
VStack {
#if os(macOS)
RichTextFormat.Toolbar(context: state)
#endif

if let exportFormat {
path = try? exportService.generateExportFile(
withName: fileName, content: state.attributedString,
format: exportFormat)
RichTextEditor(
context: _state,
viewConfiguration: { _ in

}
)
.background(
colorScheme == .dark ? .gray.opacity(0.3) : Color.white
)
.cornerRadius(10)

#if os(iOS)
RichTextKeyboardToolbar(
context: state,
leadingButtons: { $0 },
trailingButtons: { $0 },
formatSheet: { $0 }
)
#endif
}
#if os(iOS) || os(macOS)
.inspector(isPresented: $isInspectorPresented) {
RichTextFormat.Sidebar(context: state)
#if os(macOS)
.inspectorColumnWidth(
min: 200, ideal: 200, max: 315)
#endif
}
if let otherExportFormat {
switch otherExportFormat {
case .pdf:
path = try? exportService.generatePdfExportFile(
withName: fileName, content: state.attributedString)
case .json:
path = try? exportService.generateJsonExportFile(
withName: fileName, content: state.richText)
}
#endif
.padding(10)
#if os(iOS) || os(macOS)
.toolbar {
ToolbarItemGroup(placement: .automatic) {
toolBarGroup
}
}
if let path {
print("Exported at path == \(path)")
#endif
.background(colorScheme == .dark ? .black : .gray.opacity(0.07))
.navigationTitle("Rich Editor")
.alert("Enter file name", isPresented: getBindingAlert()) {
TextField("Enter file name", text: $fileName)
Button("OK", action: submit)
} message: {
Text("Please enter file name")
}
.focusedValue(\.richEditorState, state)
.toolbarRole(.automatic)
#if os(iOS) || os(macOS) || os(visionOS)
.richTextFormatSheetConfig(.init(colorPickers: colorPickers))
.richTextFormatSidebarConfig(
.init(
colorPickers: colorPickers,
fontPicker: isMac
)
)
.richTextFormatToolbarConfig(.init(colorPickers: []))
#endif
}
}

#if os(iOS) || os(macOS)
var toolBarGroup: some View {
return Group {
RichTextExportMenu.init(
formatAction: { format in
exportFormat = format
},
otherOptionAction: { format in
otherExportFormat = format
}
)
#if !os(macOS)
.frame(width: 25, alignment: .center)
#endif
Button(
action: {
print("Exported JSON == \(state.outputAsString())")
},
label: {
Image(systemName: "printer.inverse")
}
)
#if !os(macOS)
.frame(width: 25, alignment: .center)
#endif
Toggle(isOn: $isInspectorPresented) {
Image.richTextFormatBrush
.resizable()
.aspectRatio(1, contentMode: .fit)
}
#if !os(macOS)
.frame(width: 25, alignment: .center)
#endif
}
}
#endif

func getBindingAlert() -> Binding<Bool> {
.init(
get: { exportFormat != nil || otherExportFormat != nil },
set: { newValue in
exportFormat = nil
otherExportFormat = nil
})
}

func submit() {
guard !fileName.isEmpty else { return }
var path: URL?

if let exportFormat {
path = try? exportService.generateExportFile(
withName: fileName, content: state.attributedString,
format: exportFormat)
}
if let otherExportFormat {
switch otherExportFormat {
case .pdf:
path = try? exportService.generatePdfExportFile(
withName: fileName, content: state.attributedString)
case .json:
path = try? exportService.generateJsonExportFile(
withName: fileName, content: state.richText)
}
}
if let path {
print("Exported at path == \(path)")
}
}
}

extension ContentView {

var isMac: Bool {
#if os(macOS)
true
#else
false
#endif
}
var isMac: Bool {
#if os(macOS)
true
#else
false
#endif
}

var colorPickers: [RichTextColor] {
[.foreground, .background]
}
var colorPickers: [RichTextColor] {
[.foreground, .background]
}

var formatToolbarEdge: VerticalEdge {
isMac ? .top : .bottom
}
var formatToolbarEdge: VerticalEdge {
isMac ? .top : .bottom
}
}

private func handleImages(
_ action: ImageAttachmentAction,
_ onCompletion: ((ImageAttachmentCompleteAction) -> Void)?
) {
switch action {
case .save(let images):
images.forEach({
$0.updateUrl(with: "https://example.com/image/\($0.id).jpg")
})
onCompletion?(.saved(images))
case .delete(_):
onCompletion?(.deleted)
return
case .getImage(_):
onCompletion?(.getImage(nil))
return
case .getImages(_):
onCompletion?(.getImages([]))
return
}
}
Loading