diff --git a/RichEditorDemo/RichEditorDemo/ContentView.swift b/RichEditorDemo/RichEditorDemo/ContentView.swift index 9ed1575..336e449 100644 --- a/RichEditorDemo/RichEditorDemo/ContentView.swift +++ b/RichEditorDemo/RichEditorDemo/ContentView.swift @@ -46,7 +46,9 @@ struct ContentView: View { } ) - .background(colorScheme == .dark ? .gray.opacity(0.3) : Color.white) + .background( + colorScheme == .dark ? .gray.opacity(0.3) : Color.white + ) .cornerRadius(10) #if os(iOS) @@ -58,21 +60,22 @@ struct ContentView: View { ) #endif } - #if iOS || macOS - .inspector(isPresented: $isInspectorPresented) { - RichTextFormat.Sidebar(context: state) - #if os(macOS) - .inspectorColumnWidth(min: 200, ideal: 200, max: 315) - #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 iOS || os(macOS) - .toolbar { - ToolbarItemGroup(placement: .automatic) { - toolBarGroup + #if os(iOS) || os(macOS) + .toolbar { + ToolbarItemGroup(placement: .automatic) { + toolBarGroup + } } - } #endif .background(colorScheme == .dark ? .black : .gray.opacity(0.07)) .navigationTitle("Rich Editor") @@ -84,54 +87,54 @@ struct ContentView: View { } .focusedValue(\.richEditorState, state) .toolbarRole(.automatic) -#if iOS || macOS || os(visionOS) - .richTextFormatSheetConfig(.init(colorPickers: colorPickers)) - .richTextFormatSidebarConfig( - .init( - colorPickers: colorPickers, - fontPicker: isMac + #if os(iOS) || os(macOS) || os(visionOS) + .richTextFormatSheetConfig(.init(colorPickers: colorPickers)) + .richTextFormatSidebarConfig( + .init( + colorPickers: colorPickers, + fontPicker: isMac + ) ) - ) - .richTextFormatToolbarConfig(.init(colorPickers: [])) + .richTextFormatToolbarConfig(.init(colorPickers: [])) #endif } } - #if 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(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 - Toggle(isOn: $isInspectorPresented) { - Image.richTextFormatBrush - .resizable() - .aspectRatio(1, contentMode: .fit) + #if !os(macOS) + .frame(width: 25, alignment: .center) + #endif } - #if !os(macOS) - .frame(width: 25, alignment: .center) - #endif } - } #endif func getBindingAlert() -> Binding { diff --git a/RichEditorDemo/RichEditorDemo/JsonUtils.swift b/RichEditorDemo/RichEditorDemo/JsonUtils.swift index 40d8f2e..bcd1d40 100644 --- a/RichEditorDemo/RichEditorDemo/JsonUtils.swift +++ b/RichEditorDemo/RichEditorDemo/JsonUtils.swift @@ -7,12 +7,14 @@ import Foundation - -internal func readJSONFromFile(fileName: String, - type: T.Type, - bundle: Bundle? = nil) -> T? { +internal func readJSONFromFile( + fileName: String, + type: T.Type, + bundle: Bundle? = nil +) -> T? { if let url = (bundle ?? Bundle.main) - .url(forResource: fileName, withExtension: "json") { + .url(forResource: fileName, withExtension: "json") + { do { let data = try Data(contentsOf: url) let decoder = JSONDecoder() @@ -25,16 +27,14 @@ internal func readJSONFromFile(fileName: String, return nil } - internal class RichBundleFakeClass {} -internal extension Bundle { +extension Bundle { static var richBundle: Bundle { return Bundle(for: RichBundleFakeClass.self) } } - func encode(model: T?) throws -> String? { guard let model else { return nil } do { diff --git a/RichEditorDemo/RichEditorDemo/RichEditorDemoApp.swift b/RichEditorDemo/RichEditorDemo/RichEditorDemoApp.swift index 3d83771..c0e2c95 100644 --- a/RichEditorDemo/RichEditorDemo/RichEditorDemoApp.swift +++ b/RichEditorDemo/RichEditorDemo/RichEditorDemoApp.swift @@ -11,7 +11,7 @@ import SwiftUI struct RichEditorDemoApp: App { var body: some Scene { WindowGroup { - ContentView() + ContentView() } } } diff --git a/Sources/RichEditorSwiftUI/Actions/RichTextAction+KeyboardShortcutModifier.swift b/Sources/RichEditorSwiftUI/Actions/RichTextAction+KeyboardShortcutModifier.swift index 0b439f0..7b8925e 100644 --- a/Sources/RichEditorSwiftUI/Actions/RichTextAction+KeyboardShortcutModifier.swift +++ b/Sources/RichEditorSwiftUI/Actions/RichTextAction+KeyboardShortcutModifier.swift @@ -7,7 +7,7 @@ import SwiftUI -public extension RichTextAction { +extension RichTextAction { /** This view modifier can apply keyboard shortcuts for any @@ -16,7 +16,7 @@ public extension RichTextAction { You can also apply it with the `.keyboardShortcut(for:)` view modifier. */ - struct KeyboardShortcutModifier: ViewModifier { + public struct KeyboardShortcutModifier: ViewModifier { public init(_ action: RichTextAction) { self.action = action @@ -30,28 +30,30 @@ public extension RichTextAction { } } -public extension View { +extension View { /// Apply a ``RichTextAction/KeyboardShortcutModifier``. @ViewBuilder - func keyboardShortcut(for action: RichTextAction) -> some View { -#if iOS || macOS || os(visionOS) - switch action { - case .copy: keyboardShortcut("c", modifiers: .command) - case .dismissKeyboard: self - case .print: keyboardShortcut("p", modifiers: .command) - case .redoLatestChange: keyboardShortcut("z", modifiers: [.command, .shift]) -// case .setAlignment(let align): keyboardShortcut(for: align) - case .stepFontSize(let points): keyboardShortcut(points < 0 ? "-" : "+", modifiers: .command) - case .stepIndent(let steps): keyboardShortcut(steps < 0 ? "Ö" : "Ä", modifiers: .command) - case .stepSuperscript: self -// case .toggleStyle(let style): keyboardShortcut(for: style) - case .undoLatestChange: keyboardShortcut("z", modifiers: .command) - default: self // TODO: Probably not defined, object to discuss. - } -#else - self -#endif + public func keyboardShortcut(for action: RichTextAction) -> some View { + #if os(iOS) || os(macOS) || os(visionOS) + switch action { + case .copy: keyboardShortcut("c", modifiers: .command) + case .dismissKeyboard: self + case .print: keyboardShortcut("p", modifiers: .command) + case .redoLatestChange: + keyboardShortcut("z", modifiers: [.command, .shift]) + // case .setAlignment(let align): keyboardShortcut(for: align) + case .stepFontSize(let points): + keyboardShortcut(points < 0 ? "-" : "+", modifiers: .command) + case .stepIndent(let steps): + keyboardShortcut(steps < 0 ? "Ö" : "Ä", modifiers: .command) + case .stepSuperscript: self + // case .toggleStyle(let style): keyboardShortcut(for: style) + case .undoLatestChange: keyboardShortcut("z", modifiers: .command) + default: self // TODO: Probably not defined, object to discuss. + } + #else + self + #endif } } - diff --git a/Sources/RichEditorSwiftUI/Actions/RichTextAction.swift b/Sources/RichEditorSwiftUI/Actions/RichTextAction.swift index 4bbee89..532080c 100644 --- a/Sources/RichEditorSwiftUI/Actions/RichTextAction.swift +++ b/Sources/RichEditorSwiftUI/Actions/RichTextAction.swift @@ -5,16 +5,14 @@ // Created by Divyesh Vekariya on 21/10/24. // -import SwiftUI import Combine +import SwiftUI -/** - This enum defines rich text actions that can be executed on - a rich text editor. - - This type also serves as a type namespace for other related - types and views, like ``RichTextAction/Button``. - */ +/// This enum defines rich text actions that can be executed on +/// a rich text editor. +/// +/// This type also serves as a type namespace for other related +/// types and views, like ``RichTextAction/Button``. public enum RichTextAction: Identifiable, Equatable { /// Copy the currently selected text, if any. @@ -24,13 +22,13 @@ public enum RichTextAction: Identifiable, Equatable { case dismissKeyboard /// Paste a single image. -// case pasteImage(RichTextInsertion) -// -// /// Paste multiple images. -// case pasteImages(RichTextInsertion<[ImageRepresentable]>) -// -// /// Paste plain text. -// case pasteText(RichTextInsertion) + // case pasteImage(RichTextInsertion) + // + // /// Paste multiple images. + // case pasteImages(RichTextInsertion<[ImageRepresentable]>) + // + // /// Paste plain text. + // case pasteText(RichTextInsertion) /// A print command. case print @@ -79,23 +77,26 @@ public enum RichTextAction: Identifiable, Equatable { /// Set HeaderStyle. case setHeaderStyle(_ style: RichTextSpanStyle) + + /// Set link + case setLink(String? = nil) } -public extension RichTextAction { +extension RichTextAction { - typealias Publisher = PassthroughSubject + public typealias Publisher = PassthroughSubject /// The action's unique identifier. - var id: String { title } + public var id: String { title } /// The action's standard icon. - var icon: Image { + public var icon: Image { switch self { case .copy: .richTextCopy case .dismissKeyboard: .richTextDismissKeyboard -// case .pasteImage: .richTextDocuments -// case .pasteImages: .richTextDocuments -// case .pasteText: .richTextDocuments + // case .pasteImage: .richTextDocuments + // case .pasteImages: .richTextDocuments + // case .pasteText: .richTextDocuments case .print: .richTextPrint case .redoLatestChange: .richTextRedo case .selectRange: .richTextSelection @@ -112,21 +113,22 @@ public extension RichTextAction { case .toggleStyle(let val): val.icon case .undoLatestChange: .richTextUndo case .setHeaderStyle: .richTextIgnoreIt + case .setLink: .richTextLink } } /// The localized label to use for the action. - var label: some View { + public var label: some View { icon.label(title) } /// The localized title to use in the main menu. - var menuTitle: String { + public var menuTitle: String { menuTitleKey.text } /// The localized title key to use in the main menu. - var menuTitleKey: RTEL10n { + public var menuTitleKey: RTEL10n { switch self { case .stepIndent(let points): .menuIndent(points) default: titleKey @@ -134,18 +136,18 @@ public extension RichTextAction { } /// The localized action title. - var title: String { + public var title: String { titleKey.text } /// The localized action title key. - var titleKey: RTEL10n { + public var titleKey: RTEL10n { switch self { case .copy: .actionCopy case .dismissKeyboard: .actionDismissKeyboard -// case .pasteImage: .pasteImage -// case .pasteImages: .pasteImages -// case .pasteText: .pasteText + // case .pasteImage: .pasteImage + // case .pasteImages: .pasteImages + // case .pasteText: .pasteText case .print: .actionPrint case .redoLatestChange: .actionRedoLatestChange case .selectRange: .selectRange @@ -161,6 +163,7 @@ public extension RichTextAction { case .stepSuperscript(let steps): .actionStepSuperscript(steps) case .toggleStyle(let style): style.titleKey case .undoLatestChange: .actionUndoLatestChange + case .setLink: .link case .setHeaderStyle: .ignoreIt } } @@ -168,24 +171,23 @@ public extension RichTextAction { // MARK: - Aliases -public extension RichTextAction { +extension RichTextAction { /// A name alias for `.redoLatestChange`. - static var redo: RichTextAction { .redoLatestChange } + public static var redo: RichTextAction { .redoLatestChange } /// A name alias for `.undoLatestChange`. - static var undo: RichTextAction { .undoLatestChange } + public static var undo: RichTextAction { .undoLatestChange } } -public extension CGFloat { +extension CGFloat { /// The default rich text indent step size. - static var defaultRichTextIntentStepSize: CGFloat = 30.0 + public static var defaultRichTextIntentStepSize: CGFloat = 30.0 } -public extension UInt { +extension UInt { /// The default rich text indent step size. - static var defaultRichTextIntentStepSize: UInt = 30 + public static var defaultRichTextIntentStepSize: UInt = 30 } - diff --git a/Sources/RichEditorSwiftUI/Actions/RichTextActionButton.swift b/Sources/RichEditorSwiftUI/Actions/RichTextActionButton.swift index 2cc50a7..fb190b6 100644 --- a/Sources/RichEditorSwiftUI/Actions/RichTextActionButton.swift +++ b/Sources/RichEditorSwiftUI/Actions/RichTextActionButton.swift @@ -7,7 +7,7 @@ import SwiftUI -public extension RichTextAction { +extension RichTextAction { /** This button can be used to trigger a ``RichTextAction``. @@ -15,7 +15,7 @@ public extension RichTextAction { This renders a plain `Button`, which means that you can use and configure it as a normal button. */ - struct Button: View { + public struct Button: View { /** Create a rich text action button. @@ -53,9 +53,9 @@ public extension RichTextAction { } } -private extension RichTextAction.Button { +extension RichTextAction.Button { - func triggerAction() { + fileprivate func triggerAction() { context.handle(action) } } diff --git a/Sources/RichEditorSwiftUI/Alignment/RichTextAlignment+Picker.swift b/Sources/RichEditorSwiftUI/Alignment/RichTextAlignment+Picker.swift index dc85c02..d264e1c 100644 --- a/Sources/RichEditorSwiftUI/Alignment/RichTextAlignment+Picker.swift +++ b/Sources/RichEditorSwiftUI/Alignment/RichTextAlignment+Picker.swift @@ -7,13 +7,13 @@ import SwiftUI -public extension RichTextAlignment { +extension RichTextAlignment { /// This picker can be used to pick a text alignment. /// /// This view returns a plain SwiftUI `Picker` view that /// can be styled and configured with a `PickerStyle`. - struct Picker: View { + public struct Picker: View { /// Create a rich text alignment picker. /// diff --git a/Sources/RichEditorSwiftUI/Alignment/RichTextAlignment.swift b/Sources/RichEditorSwiftUI/Alignment/RichTextAlignment.swift index e22c763..a64f22e 100644 --- a/Sources/RichEditorSwiftUI/Alignment/RichTextAlignment.swift +++ b/Sources/RichEditorSwiftUI/Alignment/RichTextAlignment.swift @@ -7,11 +7,11 @@ import SwiftUI -/** - This enum defines supported rich text alignments, like left, - right, center, and justified. - */ -public enum RichTextAlignment: String, CaseIterable, Codable, Equatable, Identifiable, RichTextLabelValue { +/// This enum defines supported rich text alignments, like left, +/// right, center, and justified. +public enum RichTextAlignment: String, CaseIterable, Codable, Equatable, + Identifiable, RichTextLabelValue +{ /** Initialize a rich text alignment with a native alignment. @@ -21,11 +21,11 @@ public enum RichTextAlignment: String, CaseIterable, Codable, Equatable, Identif */ public init(_ alignment: NSTextAlignment) { switch alignment { - case .left: self = .left - case .right: self = .right - case .center: self = .center - case .justified: self = .justify - default: self = .left + case .left: self = .left + case .right: self = .right + case .center: self = .center + case .justified: self = .justify + default: self = .left } } @@ -42,33 +42,33 @@ public enum RichTextAlignment: String, CaseIterable, Codable, Equatable, Identif case right } -public extension RichTextAlignment { - func getTextSpanStyle() -> RichTextSpanStyle { +extension RichTextAlignment { + public func getTextSpanStyle() -> RichTextSpanStyle { return .align(self) } } -public extension Collection where Element == RichTextAlignment { +extension Collection where Element == RichTextAlignment { - static var all: [Element] { RichTextAlignment.allCases } + public static var all: [Element] { RichTextAlignment.allCases } } -public extension RichTextAlignment { +extension RichTextAlignment { /// The unique alignment ID. - var id: String { rawValue } + public var id: String { rawValue } /// The standard icon to use for the alignment. - var icon: Image { nativeAlignment.icon } + public var icon: Image { nativeAlignment.icon } /// The standard title to use for the alignment. - var title: String { nativeAlignment.title } + public var title: String { nativeAlignment.title } /// The standard title key to use for the alignment. - var titleKey: RTEL10n { nativeAlignment.titleKey } + public var titleKey: RTEL10n { nativeAlignment.titleKey } /// The native alignment of the alignment. - var nativeAlignment: NSTextAlignment { + public var nativeAlignment: NSTextAlignment { switch self { case .left: .left case .right: .right @@ -80,10 +80,10 @@ public extension RichTextAlignment { extension NSTextAlignment: RichTextLabelValue {} -public extension NSTextAlignment { +extension NSTextAlignment { /// The standard icon to use for the alignment. - var icon: Image { + public var icon: Image { switch self { case .left: .richTextAlignmentLeft case .right: .richTextAlignmentRight @@ -94,12 +94,12 @@ public extension NSTextAlignment { } /// The standard title to use for the alignment. - var title: String { + public var title: String { titleKey.text } /// The standard title key to use for the alignment. - var titleKey: RTEL10n { + public var titleKey: RTEL10n { switch self { case .left: .textAlignmentLeft case .right: .textAlignmentRight @@ -109,4 +109,3 @@ public extension NSTextAlignment { } } } - diff --git a/Sources/RichEditorSwiftUI/Attributes/RichTextAttribute.swift b/Sources/RichEditorSwiftUI/Attributes/RichTextAttribute.swift index c461032..e941dce 100644 --- a/Sources/RichEditorSwiftUI/Attributes/RichTextAttribute.swift +++ b/Sources/RichEditorSwiftUI/Attributes/RichTextAttribute.swift @@ -13,8 +13,6 @@ import Foundation public typealias RichTextAttribute = NSAttributedString.Key -/** - This typealias represents a ``RichTextAttribute`` keyed and - `Any` valued dictionary. - */ +/// This typealias represents a ``RichTextAttribute`` keyed and +/// `Any` valued dictionary. public typealias RichTextAttributes = [RichTextAttribute: Any] diff --git a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeReader.swift b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeReader.swift index d033492..d1c98b7 100644 --- a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeReader.swift +++ b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeReader.swift @@ -7,29 +7,27 @@ import Foundation -/** - This protocol extends ``RichTextReader`` with functionality - for reading rich text attributes for the current rich text. - - The protocol is implemented by `NSAttributedString` as well - as other types in the library. - */ +/// This protocol extends ``RichTextReader`` with functionality +/// for reading rich text attributes for the current rich text. +/// +/// The protocol is implemented by `NSAttributedString` as well +/// as other types in the library. public protocol RichTextAttributeReader: RichTextReader {} extension NSAttributedString: RichTextAttributeReader {} -public extension RichTextAttributeReader { - +extension RichTextAttributeReader { + /// Get a rich text attribute at a certain range. - func richTextAttribute( + public func richTextAttribute( _ attribute: RichTextAttribute, at range: NSRange ) -> Value? { richTextAttributes(at: range)[attribute] as? Value } - + /// Get all rich text attributes at a certain range. - func richTextAttributes( + public func richTextAttributes( at range: NSRange ) -> RichTextAttributes { if richText.string.utf16Length == 0 { return [:] } @@ -39,24 +37,24 @@ public extension RichTextAttributeReader { } // RichTextAttributeReader+Font -public extension RichTextAttributeReader { - +extension RichTextAttributeReader { + /// Get the font at a certain range. - func richTextFont(at range: NSRange) -> FontRepresentable? { + public func richTextFont(at range: NSRange) -> FontRepresentable? { richTextAttribute(.font, at: range) } - + /// Get the font size (in points) at a certain range. - func richTextFontSize(at range: NSRange) -> CGFloat? { + public func richTextFontSize(at range: NSRange) -> CGFloat? { richTextFont(at: range)?.pointSize } } // RichTextAttributeReader+Style -public extension RichTextAttributeReader { - +extension RichTextAttributeReader { + /// Get the text styles at a certain range. - func richTextStyles(at range: NSRange) -> [RichTextSpanStyle] { + public func richTextStyles(at range: NSRange) -> [RichTextSpanStyle] { let attributes = richTextAttributes(at: range) let traits = richTextFont(at: range)?.fontDescriptor.symbolicTraits var styles = traits?.enabledRichTextStyles ?? [] diff --git a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+List.swift b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+List.swift index cba7956..948bbdc 100644 --- a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+List.swift +++ b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+List.swift @@ -8,14 +8,14 @@ import Foundation #if canImport(UIKit) -import UIKit + import UIKit #endif #if canImport(AppKit) && !targetEnvironment(macCatalyst) -import AppKit + import AppKit #endif -public extension RichTextAttributeWriter { +extension RichTextAttributeWriter { /** Set the text alignment at a certain range. @@ -23,7 +23,7 @@ public extension RichTextAttributeWriter { Unlike some other attributes, this value applies to the entire paragraph, not just the selected range. */ - func setRichTextListStyle( + public func setRichTextListStyle( _ listType: ListType, to newValue: Bool, at range: NSRange @@ -32,9 +32,9 @@ public extension RichTextAttributeWriter { } } -private extension RichTextAttributeWriter { +extension RichTextAttributeWriter { - func setListStyle( + fileprivate func setListStyle( _ listType: ListType, to newValue: Bool, at range: NSRange @@ -42,36 +42,59 @@ private extension RichTextAttributeWriter { guard let string = mutableRichText else { return } let safeRange = safeRange(for: range) - let searchRange = NSRange(location: max(0, (range.location - 1)), length: min(string.string.utf16Length, (range.length + 1))) + let searchRange = NSRange( + location: max(0, (range.location - 1)), + length: min(string.string.utf16Length, (range.length + 1))) var previousRang: NSRange? = nil - var attributesWithRange: [Int: (range: NSRange, paragraphStyle: NSMutableParagraphStyle)] = [:] + var attributesWithRange: + [Int: (range: NSRange, paragraphStyle: NSMutableParagraphStyle)] = + [:] string.beginEditing() var previousStyle: NSMutableParagraphStyle? = nil - string.enumerateAttribute(.paragraphStyle, in: searchRange) { (attribute, range, _) in + string.enumerateAttribute(.paragraphStyle, in: searchRange) { + (attribute, range, _) in - if let style = attribute as? NSMutableParagraphStyle, !style.textLists.isEmpty { + if let style = attribute as? NSMutableParagraphStyle, + !style.textLists.isEmpty + { if newValue { /// For add style - attributesWithRange[attributesWithRange.count] = (range: range, paragraphStyle: style) + attributesWithRange[attributesWithRange.count] = ( + range: range, paragraphStyle: style + ) - if safeRange.location <= range.location && safeRange.upperBound >= range.upperBound { + if safeRange.location <= range.location + && safeRange.upperBound >= range.upperBound + { string.removeAttribute(.paragraphStyle, range: range) } - if let oldRange = previousRang, let previousStyle = previousStyle, previousStyle.textLists.count == listType.getIndent() { + if let oldRange = previousRang, + let previousStyle = previousStyle, + previousStyle.textLists.count == listType.getIndent() + { let location = min(oldRange.location, range.location) - let length = max(oldRange.upperBound, range.upperBound) - location - let combinedRange = NSRange(location: location, length: length) - - string.addAttribute(.paragraphStyle, value: previousStyle, range: combinedRange) + let length = + max(oldRange.upperBound, range.upperBound) + - location + let combinedRange = NSRange( + location: location, length: length) + + string.addAttribute( + .paragraphStyle, value: previousStyle, + range: combinedRange) previousRang = combinedRange } else { let location = min(safeRange.location, range.location) - let length = max(safeRange.upperBound, range.upperBound) - location - let combinedRange = NSRange(location: location, length: length) - - string.addAttribute(.paragraphStyle, value: style, range: combinedRange) + let length = + max(safeRange.upperBound, range.upperBound) + - location + let combinedRange = NSRange( + location: location, length: length) + + string.addAttribute( + .paragraphStyle, value: style, range: combinedRange) previousRang = combinedRange } previousStyle = style @@ -79,7 +102,8 @@ private extension RichTextAttributeWriter { /// Fore Remove Style if safeRange.closedRange.overlaps(range.closedRange) { if style.textLists.count == listType.getIndent() { - string.removeAttribute(.paragraphStyle, range: safeRange) + string.removeAttribute( + .paragraphStyle, range: safeRange) previousRang = nil previousStyle = nil } @@ -90,11 +114,12 @@ private extension RichTextAttributeWriter { ///Add style if not already added if attributesWithRange.isEmpty { - + let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = .left - let listItem = TextList(markerFormat: listType.getMarkerFormat(), options: 0) + let listItem = TextList( + markerFormat: listType.getMarkerFormat(), options: 0) if paragraphStyle.textLists.isEmpty && newValue { paragraphStyle.textLists.append(listItem) @@ -103,7 +128,8 @@ private extension RichTextAttributeWriter { } if !paragraphStyle.textLists.isEmpty { - string.addAttributes([.paragraphStyle: paragraphStyle], range: safeRange) + string.addAttributes( + [.paragraphStyle: paragraphStyle], range: safeRange) } else { string.removeAttribute(.paragraphStyle, range: safeRange) } diff --git a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+Style.swift b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+Style.swift index 027279c..b1198f2 100644 --- a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+Style.swift +++ b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter+Style.swift @@ -7,20 +7,20 @@ import Foundation -public extension NSMutableAttributedString { +extension NSMutableAttributedString { /** Set a rich text style at a certain range. - + The function uses `safeRange(for:)` to handle incorrect ranges, which is not handled by the native functions. - + - Parameters: - style: The style to set. - newValue: The new value to set the attribute to. - range: The range to affect, by default the entire text. */ - func setRichTextStyle( + public func setRichTextStyle( _ style: RichTextSpanStyle, to newValue: Bool, at range: NSRange? = nil @@ -35,30 +35,43 @@ public extension NSMutableAttributedString { guard !style.isList else { return } let attributeValue = newValue ? 1 : 0 - if style == .underline { return setRichTextAttribute(.underlineStyle, to: attributeValue, at: range) } - if style == .strikethrough { return setRichTextAttribute(.strikethroughStyle, to: attributeValue, at: range) } + if style == .underline { + return setRichTextAttribute( + .underlineStyle, to: attributeValue, at: range) + } + if style == .strikethrough { + return setRichTextAttribute( + .strikethroughStyle, to: attributeValue, at: range) + } let font = richTextFont(at: range) ?? .standardRichTextFont let styles = richTextStyles(at: range) let shouldAdd = newValue && !styles.hasStyle(style) let shouldRemove = !newValue && styles.hasStyle(style) guard shouldAdd || shouldRemove || style.isHeaderStyle else { return } var descriptor = font.fontDescriptor - if let richTextStyle = style.richTextStyle, !style.isDefault && !style.isHeaderStyle { + if let richTextStyle = style.richTextStyle, + !style.isDefault && !style.isHeaderStyle + { descriptor = descriptor.byTogglingStyle(richTextStyle) } let newFont: FontRepresentable? = FontRepresentable( descriptor: descriptor, - size: byTogglingFontSizeFor(style: style, font: font, shouldAdd: newValue)) + size: byTogglingFontSizeFor( + style: style, font: font, shouldAdd: newValue)) guard let newFont = newFont else { return } setRichTextFont(newFont, at: range) } - + /** This will reset font size before multiplying new size */ - private func byTogglingFontSizeFor(style: RichTextSpanStyle, font: FontRepresentable, shouldAdd: Bool) -> CGFloat { - guard style.isHeaderStyle || style.isDefault else { return font.pointSize } - + private func byTogglingFontSizeFor( + style: RichTextSpanStyle, font: FontRepresentable, shouldAdd: Bool + ) -> CGFloat { + guard style.isHeaderStyle || style.isDefault else { + return font.pointSize + } + let cleanFont = style.getFontAfterRemovingStyle(font: font) if shouldAdd { return cleanFont.pointSize * style.fontSizeMultiplier diff --git a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter.swift b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter.swift index be096cb..011d130 100644 --- a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter.swift +++ b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributeWriter.swift @@ -7,50 +7,49 @@ import Foundation -/** - This protocol extends ``RichTextWriter`` with functionality - for writing rich text attributes to the current rich text. - - This protocol is implemented by `NSMutableAttributedString` - as well as other types in the library. - */ -public protocol RichTextAttributeWriter: RichTextWriter, RichTextAttributeReader {} +/// This protocol extends ``RichTextWriter`` with functionality +/// for writing rich text attributes to the current rich text. +/// +/// This protocol is implemented by `NSMutableAttributedString` +/// as well as other types in the library. +public protocol RichTextAttributeWriter: RichTextWriter, RichTextAttributeReader +{} extension NSMutableAttributedString: RichTextAttributeWriter {} -public extension RichTextAttributeWriter { - +extension RichTextAttributeWriter { + /** Set a certain rich text attribute to a certain value at the provided range. - + The function uses `safeRange(for:)` to handle incorrect ranges, which is not handled by the native functions. - + - Parameters: - attribute: The attribute to set. - newValue: The new value to set the attribute to. - range: The range to affect, by default the entire text. */ - func setRichTextAttribute( + public func setRichTextAttribute( _ attribute: RichTextAttribute, to newValue: Any, at range: NSRange? = nil ) { setRichTextAttributes([attribute: newValue], at: range) } - + /** Set a set of rich text attributes at the provided range. - + The function uses `safeRange(for:)` to handle incorrect ranges, which is not handled by the native functions. - + - Parameters: - attributes: The attributes to set. - range: The range to affect, by default the entire text. */ - func setRichTextAttributes( + public func setRichTextAttributes( _ attributes: RichTextAttributes, at range: NSRange? = nil ) { @@ -59,7 +58,8 @@ public extension RichTextAttributeWriter { guard let string = mutableRichText else { return } string.beginEditing() attributes.forEach { attribute, newValue in - string.enumerateAttribute(attribute, in: range, options: .init()) { _, range, _ in + string.enumerateAttribute(attribute, in: range, options: .init()) { + _, range, _ in string.removeAttribute(attribute, range: range) string.addAttribute(attribute, value: newValue, range: range) string.fixAttributes(in: range) @@ -70,13 +70,63 @@ public extension RichTextAttributeWriter { } // RichTextAttributeWriter+Font -public extension RichTextAttributeWriter { - +extension RichTextAttributeWriter { + /// Set the font at a certain range. - func setRichTextFont( + public func setRichTextFont( _ font: FontRepresentable, at range: NSRange? = nil ) { setRichTextAttribute(.font, to: font, at: range) } } + +//MARK: - Remove Attributes +extension RichTextAttributeWriter { + /** + Remove a certain rich text attribute to a certain value at + the provided range. + + The function uses `safeRange(for:)` to handle incorrect + ranges, which is not handled by the native functions. + + - Parameters: + - attribute: The attribute to set. + - newValue: The new value to set the attribute to. + - range: The range to affect, by default the entire text. + */ + public func removeRichTextAttribute( + _ attribute: RichTextAttribute, + at range: NSRange? = nil + ) { + removeRichTextAttributes([attribute], at: range) + } + + /** + Remove a set of rich text attributes at the provided range. + + The function uses `safeRange(for:)` to handle incorrect + ranges, which is not handled by the native functions. + + - Parameters: + - attributes: The attributes to remove. + - range: The range to affect, by default the entire text. + */ + public func removeRichTextAttributes( + _ attributes: [RichTextAttribute], + at range: NSRange? = nil + ) { + let rangeValue = range ?? richTextRange + let range = safeRange(for: rangeValue) + guard let string = mutableRichText else { return } + string.beginEditing() + attributes.forEach { attribute in + string.enumerateAttribute(attribute, in: range, options: .init()) { + _, range, _ in + string.removeAttribute(attribute, range: range) + string.fixAttributes(in: range) + } + } + string.endEditing() + } +} diff --git a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributes+RichTextStyle.swift b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributes+RichTextStyle.swift index 90b6df6..de5104b 100644 --- a/Sources/RichEditorSwiftUI/Attributes/RichTextAttributes+RichTextStyle.swift +++ b/Sources/RichEditorSwiftUI/Attributes/RichTextAttributes+RichTextStyle.swift @@ -7,19 +7,19 @@ import Foundation -public extension RichTextAttributes { +extension RichTextAttributes { /** Whether or not the attributes has a strikethrough style. */ - var isStrikethrough: Bool { + public var isStrikethrough: Bool { get { self[.strikethroughStyle] as? Int == 1 } set { self[.strikethroughStyle] = newValue ? 1 : 0 } } - + /** Whether or not the attributes has an underline style. */ - var isUnderlined: Bool { + public var isUnderlined: Bool { get { self[.underlineStyle] as? Int == 1 } set { self[.underlineStyle] = newValue ? 1 : 0 } } diff --git a/Sources/RichEditorSwiftUI/Attributes/RichTextWriter.swift b/Sources/RichEditorSwiftUI/Attributes/RichTextWriter.swift index 2cdc2c1..291ab2f 100644 --- a/Sources/RichEditorSwiftUI/Attributes/RichTextWriter.swift +++ b/Sources/RichEditorSwiftUI/Attributes/RichTextWriter.swift @@ -7,60 +7,58 @@ import Foundation -/** - This protocol extends ``RichTextReader`` and is implemented - by types that can provide a writable rich text string. - - This protocol is implemented by `NSMutableAttributedString` - as well as other types in the library. - */ +/// This protocol extends ``RichTextReader`` and is implemented +/// by types that can provide a writable rich text string. +/// +/// This protocol is implemented by `NSMutableAttributedString` +/// as well as other types in the library. public protocol RichTextWriter: RichTextReader { - + /// Get the writable attributed string for the type. var mutableAttributedString: NSMutableAttributedString? { get } } extension NSMutableAttributedString: RichTextWriter { - + /// This type returns itself as the attributed string. public var mutableAttributedString: NSMutableAttributedString? { self } } -public extension RichTextWriter { - +extension RichTextWriter { + /** Get the writable rich text provided by the implementing type. - + This is an alias for ``mutableAttributedString`` and is used to get a property that uses the rich text naming. */ - var mutableRichText: NSMutableAttributedString? { + public var mutableRichText: NSMutableAttributedString? { mutableAttributedString } - + /** Replace the text in a certain range with a new string. - + - Parameters: - range: The range to replace text in. - string: The string to replace the current text with. */ - func replaceText(in range: NSRange, with string: String) { + public func replaceText(in range: NSRange, with string: String) { mutableRichText?.replaceCharacters(in: range, with: string) } - + /** Replace the text in a certain range with a new string. - + - Parameters: - range: The range to replace text in. - string: The string to replace the current text with. */ - func replaceText(in range: NSRange, with string: NSAttributedString) { + public func replaceText(in range: NSRange, with string: NSAttributedString) + { mutableRichText?.replaceCharacters(in: range, with: string) } } - diff --git a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Actions.swift b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Actions.swift index 6e861de..89b7711 100644 --- a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Actions.swift +++ b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Actions.swift @@ -7,181 +7,206 @@ import Foundation -#if iOS || macOS || os(tvOS) || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) + import SwiftUI -extension RichTextCoordinator { + extension RichTextCoordinator { - func handle(_ action: RichTextAction?) { - guard let action else { return } - switch action { - case .copy: textView.copySelection() - case .dismissKeyboard: - textView.resignFirstResponder() + func handle(_ action: RichTextAction?) { + guard let action else { return } + switch action { + case .copy: textView.copySelection() + case .dismissKeyboard: + textView.resignFirstResponder() // case .pasteImage(let image): // pasteImage(image) // case .pasteImages(let images): // pasteImages(images) // case .pasteText(let text): // pasteText(text) - case .print: - break - case .redoLatestChange: - textView.redoLatestChange() - syncContextWithTextView() - case .selectRange(let range): - setSelectedRange(to: range) - case .setAlignment(let alignment): - textView.setRichTextAlignment(alignment) - case .setAttributedString(let string): - setAttributedString(to: string) - case .setColor(let color, let newValue): - setColor(color, to: newValue) - case .setHighlightedRange(let range): - setHighlightedRange(to: range) - case .setHighlightingStyle(let style): - textView.highlightingStyle = style - case .setStyle(let style, let newValue): - setStyle(style, to: newValue) - case .stepFontSize(let points): - textView.stepRichTextFontSize(points: points) - syncContextWithTextView() - case .stepIndent(_): - // textView.stepRichTextIndent(points: points) - return - case .stepLineSpacing(_): - // textView.stepRichTextLineSpacing(points: points) - return - case .stepSuperscript(_): - // textView.stepRichTextSuperscriptLevel(points: points) - return - case .toggleStyle(_): - // textView.toggleRichTextStyle(style) - return - case .undoLatestChange: - textView.undoLatestChange() - syncContextWithTextView() - case .setHeaderStyle(let style): - let size = style.fontSizeMultiplier * .standardRichTextFontSize - let range = textView.textString.getHeaderRangeFor( - textView.selectedRange - ) - var font = textView.richTextFont(at: range) - font = font?.withSize(size) - textView - .setRichTextFont(font ?? .standardRichTextFont, at: range) + case .print: + break + case .redoLatestChange: + textView.redoLatestChange() + syncContextWithTextView() + case .selectRange(let range): + setSelectedRange(to: range) + case .setAlignment(let alignment): + textView.setRichTextAlignment(alignment) + case .setAttributedString(let string): + setAttributedString(to: string) + case .setColor(let color, let newValue): + setColor(color, to: newValue) + case .setHighlightedRange(let range): + setHighlightedRange(to: range) + case .setHighlightingStyle(let style): + textView.highlightingStyle = style + case .setStyle(let style, let newValue): + setStyle(style, to: newValue) + case .stepFontSize(let points): + textView.stepRichTextFontSize(points: points) + syncContextWithTextView() + case .stepIndent(_): + // textView.stepRichTextIndent(points: points) + return + case .stepLineSpacing(_): + // textView.stepRichTextLineSpacing(points: points) + return + case .stepSuperscript(_): + // textView.stepRichTextSuperscriptLevel(points: points) + return + case .toggleStyle(_): + // textView.toggleRichTextStyle(style) + return + case .undoLatestChange: + textView.undoLatestChange() + syncContextWithTextView() + case .setHeaderStyle(let style): + let size = style.fontSizeMultiplier * .standardRichTextFontSize + let range = textView.textString.getHeaderRangeFor( + textView.selectedRange + ) + var font = textView.richTextFont(at: range) + font = font?.withSize(size) + textView + .setRichTextFont(font ?? .standardRichTextFont, at: range) + case .setLink(let link): + if let link, link != self.context.link { + setLink(link) + } else { + removeLink() + } + } } } -} - -extension RichTextCoordinator { - - // func paste(_ data: RichTextInsertion) { - // if let data = data as? RichTextInsertion { - // pasteImage(data) - // } else if let data = data as? RichTextInsertion<[ImageRepresentable]> { - // pasteImages(data) - // } else if let data = data as? RichTextInsertion { - // pasteText(data) - // } else { - // print("Unsupported media type") - // } - // } - // - // func pasteImage(_ data: RichTextInsertion) { - // textView.pasteImage( - // data.content, - // at: data.index, - // moveCursorToPastedContent: data.moveCursor - // ) - // } - // - // func pasteImages(_ data: RichTextInsertion<[ImageRepresentable]>) { - // textView.pasteImages( - // data.content, - // at: data.index, - // moveCursorToPastedContent: data.moveCursor - // ) - // } - - // func pasteText(_ data: RichTextInsertion) { - // textView.pasteText( - // data.content, - // at: data.index, - // moveCursorToPastedContent: data.moveCursor - // ) - // } - - func setAttributedString(to newValue: NSAttributedString?) { - guard let newValue else { return } - textView.setRichText(newValue) - } - // TODO: This code should be handled by the component - func setColor(_ color: RichTextColor, to val: ColorRepresentable) { - var applyRange: NSRange? - if textView.hasSelectedRange { - applyRange = textView.selectedRange + extension RichTextCoordinator { + + // func paste(_ data: RichTextInsertion) { + // if let data = data as? RichTextInsertion { + // pasteImage(data) + // } else if let data = data as? RichTextInsertion<[ImageRepresentable]> { + // pasteImages(data) + // } else if let data = data as? RichTextInsertion { + // pasteText(data) + // } else { + // print("Unsupported media type") + // } + // } + // + // func pasteImage(_ data: RichTextInsertion) { + // textView.pasteImage( + // data.content, + // at: data.index, + // moveCursorToPastedContent: data.moveCursor + // ) + // } + // + // func pasteImages(_ data: RichTextInsertion<[ImageRepresentable]>) { + // textView.pasteImages( + // data.content, + // at: data.index, + // moveCursorToPastedContent: data.moveCursor + // ) + // } + + // func pasteText(_ data: RichTextInsertion) { + // textView.pasteText( + // data.content, + // at: data.index, + // moveCursorToPastedContent: data.moveCursor + // ) + // } + + func setAttributedString(to newValue: NSAttributedString?) { + guard let newValue else { return } + textView.setRichText(newValue) } - guard let attribute = color.attribute else { return } - if let applyRange { - textView.setRichTextColor(color, to: val, at: applyRange) - } else { - textView.setRichTextAttribute(attribute, to: val) + + // TODO: This code should be handled by the component + func setColor(_ color: RichTextColor, to val: ColorRepresentable) { + var applyRange: NSRange? + if textView.hasSelectedRange { + applyRange = textView.selectedRange + } + guard let attribute = color.attribute else { return } + if let applyRange { + textView.setRichTextColor(color, to: val, at: applyRange) + } else { + textView.setRichTextAttribute(attribute, to: val) + } } - } - func setHighlightedRange(to range: NSRange?) { - resetHighlightedRangeAppearance() - guard let range = range else { return } - setHighlightedRangeAppearance(for: range) - } + func setHighlightedRange(to range: NSRange?) { + resetHighlightedRangeAppearance() + guard let range = range else { return } + setHighlightedRangeAppearance(for: range) + } - func setHighlightedRangeAppearance(for range: NSRange) { - let back = textView.richTextColor(.background, at: range) ?? .clear - let fore = textView.richTextColor(.foreground, at: range) ?? .textColor - highlightedRangeOriginalBackgroundColor = back - highlightedRangeOriginalForegroundColor = fore - let style = textView.highlightingStyle - let background = ColorRepresentable(style.backgroundColor) - let foreground = ColorRepresentable(style.foregroundColor) - textView.setRichTextColor(.background, to: background, at: range) - textView.setRichTextColor(.foreground, to: foreground, at: range) - } + func setHighlightedRangeAppearance(for range: NSRange) { + let back = textView.richTextColor(.background, at: range) ?? .clear + let fore = + textView.richTextColor(.foreground, at: range) ?? .textColor + highlightedRangeOriginalBackgroundColor = back + highlightedRangeOriginalForegroundColor = fore + let style = textView.highlightingStyle + let background = ColorRepresentable(style.backgroundColor) + let foreground = ColorRepresentable(style.foregroundColor) + textView.setRichTextColor(.background, to: background, at: range) + textView.setRichTextColor(.foreground, to: foreground, at: range) + } - func setIsEditable(to newValue: Bool) { -#if iOS || macOS || os(visionOS) - if newValue == textView.isEditable { return } - textView.isEditable = newValue -#endif - } + func setIsEditable(to newValue: Bool) { + #if os(iOS) || os(macOS) || os(visionOS) + if newValue == textView.isEditable { return } + textView.isEditable = newValue + #endif + } - func setIsEditing(to newValue: Bool) { - if newValue == textView.isFirstResponder { return } - if newValue { -#if iOS || os(visionOS) - textView.becomeFirstResponder() -#else - print("macOS currently doesn't resign first responder.") -#endif - } else { -#if iOS || os(visionOS) - textView.resignFirstResponder() -#else - print("macOS currently doesn't resign first responder.") -#endif + func setIsEditing(to newValue: Bool) { + if newValue == textView.isFirstResponder { return } + if newValue { + #if os(iOS) || os(visionOS) + textView.becomeFirstResponder() + #else + print("macOS currently doesn't resign first responder.") + #endif + } else { + #if os(iOS) || os(visionOS) + textView.resignFirstResponder() + #else + print("macOS currently doesn't resign first responder.") + #endif + } } - } - func setSelectedRange(to range: NSRange) { - if range == textView.selectedRange { return } - textView.selectedRange = range - } + func setSelectedRange(to range: NSRange) { + if range == textView.selectedRange { return } + textView.selectedRange = range + } - func setStyle(_ style: RichTextStyle, to newValue: Bool) { - let hasStyle = textView.richTextStyles.hasStyle(style) - guard newValue != hasStyle else { return } - textView.setRichTextStyle(style, to: newValue) + func setStyle(_ style: RichTextStyle, to newValue: Bool) { + let hasStyle = textView.richTextStyles.hasStyle(style) + guard newValue != hasStyle else { return } + textView.setRichTextStyle(style, to: newValue) + } + + func setLink(_ link: String) { + let style: RichTextSpanStyle = .link(link) + var applyRange: NSRange? + if textView.hasSelectedRange { + applyRange = textView.selectedRange + } + textView.setRichTextLink(.link(link)) + if let applyRange { + textView.setRichTextLink(style, at: applyRange) + } else { + textView.setRichTextLink(style) + } + } + + func removeLink() { + textView.removeRichTextLink(.link()) + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Subscriptions.swift b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Subscriptions.swift index fd6692c..5a667cb 100644 --- a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Subscriptions.swift +++ b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator+Subscriptions.swift @@ -5,77 +5,77 @@ // Created by Divyesh Vekariya on 21/10/24. // -#if iOS || macOS || os(tvOS) || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) + import SwiftUI -extension RichTextCoordinator { + extension RichTextCoordinator { - /// Subscribe to observable context state changes. - /// - /// The coordinator subscribes to both actions triggered - /// by various buttons via the context, but also to some - /// context value that are changed through view bindings. - func subscribeToUserActions() { - context.actionPublisher.sink { [weak self] action in - self?.handle(action) - } - .store(in: &cancellables) + /// Subscribe to observable context state changes. + /// + /// The coordinator subscribes to both actions triggered + /// by various buttons via the context, but also to some + /// context value that are changed through view bindings. + func subscribeToUserActions() { + context.actionPublisher.sink { [weak self] action in + self?.handle(action) + } + .store(in: &cancellables) - subscribeToAlignment() - subscribeToFontName() - subscribeToFontSize() - subscribeToIsEditable() - subscribeToIsEditingText() - subscribeToLineSpacing() + subscribeToAlignment() + subscribeToFontName() + subscribeToFontSize() + subscribeToIsEditable() + subscribeToIsEditingText() + subscribeToLineSpacing() + } } -} -private extension RichTextCoordinator { + extension RichTextCoordinator { - func subscribe( - to publisher: Published.Publisher, - action: @escaping (T) -> Void - ) { - publisher - .sink(receiveValue: action) - .store(in: &cancellables) - } + fileprivate func subscribe( + to publisher: Published.Publisher, + action: @escaping (T) -> Void + ) { + publisher + .sink(receiveValue: action) + .store(in: &cancellables) + } - func subscribeToAlignment() { - subscribe(to: context.$textAlignment) { [weak self] in - self?.handle(.setAlignment($0)) + fileprivate func subscribeToAlignment() { + subscribe(to: context.$textAlignment) { [weak self] in + self?.handle(.setAlignment($0)) + } } - } - func subscribeToFontName() { - subscribe(to: context.$fontName) { [weak self] in - self?.textView.setRichTextFontName($0) + fileprivate func subscribeToFontName() { + subscribe(to: context.$fontName) { [weak self] in + self?.textView.setRichTextFontName($0) + } } - } - func subscribeToFontSize() { - subscribe(to: context.$fontSize) { [weak self] in - self?.textView.setRichTextFontSize($0) + fileprivate func subscribeToFontSize() { + subscribe(to: context.$fontSize) { [weak self] in + self?.textView.setRichTextFontSize($0) + } } - } - func subscribeToIsEditable() { - subscribe(to: context.$isEditable) { [weak self] in - self?.setIsEditable(to: $0) + fileprivate func subscribeToIsEditable() { + subscribe(to: context.$isEditable) { [weak self] in + self?.setIsEditable(to: $0) + } } - } - func subscribeToIsEditingText() { - subscribe(to: context.$isEditingText) { [weak self] in - self?.setIsEditing(to: $0) + fileprivate func subscribeToIsEditingText() { + subscribe(to: context.$isEditingText) { [weak self] in + self?.setIsEditing(to: $0) + } } - } - // TODO: Not done yet - func subscribeToLineSpacing() { - // subscribe(to: context.$lineSpacing) { [weak self] in - // self?.textView.setRichTextLineSpacing($0) - // } + // TODO: Not done yet + fileprivate func subscribeToLineSpacing() { + // subscribe(to: context.$lineSpacing) { [weak self] in + // self?.textView.setRichTextLineSpacing($0) + // } + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator.swift b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator.swift index 82b6ae7..c35f957 100644 --- a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator.swift +++ b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextCoordinator.swift @@ -4,59 +4,57 @@ // // Created by Divyesh Vekariya on 21/10/24. // -#if iOS || macOS || os(tvOS) || os(visionOS) -import Combine -import SwiftUI +#if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) + import Combine + import SwiftUI -/** - This coordinator is used to keep a ``RichTextView`` in sync - with a ``RichEditorState``. + /// This coordinator is used to keep a ``RichTextView`` in sync + /// with a ``RichEditorState``. + /// + /// This is used by ``RichTextEditor`` to coordinate changes in + /// its context and the underlying text view. + /// + /// The coordinator sets itself as the text view's delegate. It + /// updates the context when things change in the text view and + /// syncs to context changes to the text view. + open class RichTextCoordinator: NSObject { - This is used by ``RichTextEditor`` to coordinate changes in - its context and the underlying text view. + // MARK: - Properties - The coordinator sets itself as the text view's delegate. It - updates the context when things change in the text view and - syncs to context changes to the text view. - */ -open class RichTextCoordinator: NSObject { + /// The rich text context to coordinate with. + public let context: RichEditorState - // MARK: - Properties + /// The rich text to edit. + public var text: Binding - /// The rich text context to coordinate with. - public let context: RichEditorState + /// The text view for which the coordinator is used. + public private(set) var textView: RichTextView - /// The rich text to edit. - public var text: Binding + /// This set is used to store context observations. + public var cancellables = Set() - /// The text view for which the coordinator is used. - public private(set) var textView: RichTextView + /// This flag is used to avoid delaying context sync. + var shouldDelaySyncContextWithTextView = false - /// This set is used to store context observations. - public var cancellables = Set() + // MARK: - Internal Properties - /// This flag is used to avoid delaying context sync. - var shouldDelaySyncContextWithTextView = false - - // MARK: - Internal Properties - - /** + /** The background color that was used before the currently highlighted range was set. */ - var highlightedRangeOriginalBackgroundColor: ColorRepresentable? + var highlightedRangeOriginalBackgroundColor: ColorRepresentable? - /** + /** The foreground color that was used before the currently highlighted range was set. */ - var highlightedRangeOriginalForegroundColor: ColorRepresentable? + var highlightedRangeOriginalForegroundColor: ColorRepresentable? - private var cancellable: Set = [] + private var cancellable: Set = [] - // MARK: - Initialization + // MARK: - Initialization - /** + /** Create a rich text coordinator. - Parameters: @@ -64,203 +62,211 @@ open class RichTextCoordinator: NSObject { - textView: The rich text view to keep in sync. - richEditorState: The context to keep in sync. */ - public init( - text: Binding, - textView: RichTextView, - richTextContext: RichEditorState - ) { - textView.attributedString = text.wrappedValue - self.text = text - self.textView = textView - self.context = richTextContext - super.init() - self.textView.delegate = self - subscribeToUserActions() - } -#if canImport(UIKit) - - // MARK: - UITextViewDelegate - - open func textViewDidBeginEditing(_ textView: UITextView) { - context.onTextViewEvent( - .didBeginEditing( - selectedRange: textView.selectedRange, - text: textView.attributedText - ) - ) - context.isEditingText = true - } + public init( + text: Binding, + textView: RichTextView, + richTextContext: RichEditorState + ) { + textView.attributedString = text.wrappedValue + self.text = text + self.textView = textView + self.context = richTextContext + super.init() + self.textView.delegate = self + subscribeToUserActions() + } + #if canImport(UIKit) + + // MARK: - UITextViewDelegate + + open func textViewDidBeginEditing(_ textView: UITextView) { + context.onTextViewEvent( + .didBeginEditing( + selectedRange: textView.selectedRange, + text: textView.attributedText + ) + ) + context.isEditingText = true + } - open func textViewDidChange(_ textView: UITextView) { - syncWithTextView() - context.onTextViewEvent( - .didChange( - selectedRange: textView.selectedRange, - text: textView.attributedText - ) - ) - } + open func textViewDidChange(_ textView: UITextView) { + context.onTextViewEvent( + .didChange( + selectedRange: textView.selectedRange, + text: textView.attributedText + ) + ) + syncWithTextView() + } - open func textViewDidChangeSelection(_ textView: UITextView) { - syncWithTextView() - context.onTextViewEvent( - .didChangeSelection( - selectedRange: textView.selectedRange, - text: textView.attributedText - ) - ) - } + open func textViewDidChangeSelection(_ textView: UITextView) { + context.onTextViewEvent( + .didChangeSelection( + selectedRange: textView.selectedRange, + text: textView.attributedText + ) + ) + syncWithTextView() + } - open func textViewDidEndEditing(_ textView: UITextView) { - syncWithTextView() - context.onTextViewEvent( - .didEndEditing( - selectedRange: textView.selectedRange, - text: textView.attributedText - ) - ) - context.isEditingText = false - } -#endif + open func textViewDidEndEditing(_ textView: UITextView) { + context.onTextViewEvent( + .didEndEditing( + selectedRange: textView.selectedRange, + text: textView.attributedText + ) + ) + context.isEditingText = false + syncWithTextView() + } + #endif -#if canImport(AppKit) && !targetEnvironment(macCatalyst) + #if canImport(AppKit) && !targetEnvironment(macCatalyst) - // MARK: - NSTextViewDelegate + // MARK: - NSTextViewDelegate - open func textDidBeginEditing(_ notification: Notification) { - context.onTextViewEvent( - .didBeginEditing( - selectedRange: textView.selectedRange, - text: textView.attributedString() - ) - ) - context.isEditingText = true - } + open func textDidBeginEditing(_ notification: Notification) { + context.onTextViewEvent( + .didBeginEditing( + selectedRange: textView.selectedRange, + text: textView.attributedString() + ) + ) + context.isEditingText = true + } - open func textDidChange(_ notification: Notification) { - context.onTextViewEvent( - .didChange( - selectedRange: textView.selectedRange, - text: textView.attributedString() - ) - ) + open func textDidChange(_ notification: Notification) { + context.onTextViewEvent( + .didChange( + selectedRange: textView.selectedRange, + text: textView.attributedString() + ) + ) syncWithTextView() - } + } - open func textViewDidChangeSelection(_ notification: Notification) { - replaceCurrentAttributesIfNeeded() - context.onTextViewEvent( - .didChangeSelection( - selectedRange: textView.selectedRange, - text: textView.attributedString() - ) - ) + open func textViewDidChangeSelection(_ notification: Notification) { + replaceCurrentAttributesIfNeeded() + context.onTextViewEvent( + .didChangeSelection( + selectedRange: textView.selectedRange, + text: textView.attributedString() + ) + ) syncWithTextView() - } + } - open func textDidEndEditing(_ notification: Notification) { - context.onTextViewEvent( - .didEndEditing( - selectedRange: textView.selectedRange, - text: textView.attributedString() - ) - ) - context.isEditingText = false + open func textDidEndEditing(_ notification: Notification) { + context.onTextViewEvent( + .didEndEditing( + selectedRange: textView.selectedRange, + text: textView.attributedString() + ) + ) + context.isEditingText = false + } + #endif } -#endif -} -#if iOS || os(tvOS) || os(visionOS) -import UIKit + #if os(iOS) || os(tvOS) || os(visionOS) + import UIKit -extension RichTextCoordinator: UITextViewDelegate {} + extension RichTextCoordinator: UITextViewDelegate {} -#elseif macOS -import AppKit + #elseif macOS + import AppKit -extension RichTextCoordinator: NSTextViewDelegate {} -#endif + extension RichTextCoordinator: NSTextViewDelegate {} + #endif -// MARK: - Public Extensions + // MARK: - Public Extensions -public extension RichTextCoordinator { + extension RichTextCoordinator { - /// Reset appearance for the currently highlighted range. - func resetHighlightedRangeAppearance() { - guard - let range = context.highlightedRange, - let background = highlightedRangeOriginalBackgroundColor, - let foreground = highlightedRangeOriginalForegroundColor - else { return } - textView.setRichTextColor(.background, to: background, at: range) - textView.setRichTextColor(.foreground, to: foreground, at: range) + /// Reset appearance for the currently highlighted range. + public func resetHighlightedRangeAppearance() { + guard + let range = context.highlightedRange, + let background = highlightedRangeOriginalBackgroundColor, + let foreground = highlightedRangeOriginalForegroundColor + else { return } + textView.setRichTextColor(.background, to: background, at: range) + textView.setRichTextColor(.foreground, to: foreground, at: range) + } } -} + // MARK: - Internal Extensions -// MARK: - Internal Extensions + extension RichTextCoordinator { -extension RichTextCoordinator { - - /// Sync state from the text view's current state. - func syncWithTextView() { - syncContextWithTextView() - syncTextWithTextView() - } + /// Sync state from the text view's current state. + func syncWithTextView() { + syncContextWithTextView() + syncTextWithTextView() + } - /// Sync the rich text context with the text view. - func syncContextWithTextView() { - if shouldDelaySyncContextWithTextView { - DispatchQueue.main.async { - self.syncContextWithTextViewAfterDelay() + /// Sync the rich text context with the text view. + func syncContextWithTextView() { + if shouldDelaySyncContextWithTextView { + DispatchQueue.main.async { + self.syncContextWithTextViewAfterDelay() + } + } else { + syncContextWithTextViewAfterDelay() } - } else { - syncContextWithTextViewAfterDelay() } - } - func sync(_ prop: inout T, with value: T) { - if prop == value { return } - prop = value - } + func sync(_ prop: inout T, with value: T) { + if prop == value { return } + prop = value + } - /// Sync the rich text context with the text view. - func syncContextWithTextViewAfterDelay() { - let font = textView.richTextFont ?? .standardRichTextFont - sync(&context.attributedString, with: textView.attributedString) - sync(&context.selectedRange, with: textView.selectedRange) - sync(&context.canCopy, with: textView.hasSelectedRange) - sync(&context.canRedoLatestChange, with: textView.undoManager?.canRedo ?? false) - sync(&context.canUndoLatestChange, with: textView.undoManager?.canUndo ?? false) - sync(&context.fontName, with: font.fontName) - sync(&context.fontSize, with: font.pointSize) - sync(&context.isEditingText, with: textView.isFirstResponder) - sync(&context.paragraphStyle, with: textView.richTextParagraphStyle ?? .default) - sync(&context.textAlignment, with: textView.richTextAlignment ?? .left) - - RichTextColor.allCases.forEach { - if let color = textView.richTextColor($0) { - context.setColor($0, to: color) + /// Sync the rich text context with the text view. + func syncContextWithTextViewAfterDelay() { + let font = textView.richTextFont ?? .standardRichTextFont + sync(&context.attributedString, with: textView.attributedString) + sync(&context.selectedRange, with: textView.selectedRange) + sync(&context.canCopy, with: textView.hasSelectedRange) + sync( + &context.canRedoLatestChange, + with: textView.undoManager?.canRedo ?? false) + sync( + &context.canUndoLatestChange, + with: textView.undoManager?.canUndo ?? false) + sync(&context.fontName, with: font.fontName) + sync(&context.fontSize, with: font.pointSize) + sync(&context.isEditingText, with: textView.isFirstResponder) + sync( + &context.paragraphStyle, + with: textView.richTextParagraphStyle ?? .default) + sync( + &context.textAlignment, + with: textView.richTextAlignment ?? .left) + sync(&context.link, with: textView.richTextLink) + + RichTextColor.allCases.forEach { + if let color = textView.richTextColor($0) { + context.setColor($0, to: color) + } } - } - let styles = textView.richTextStyles - RichTextStyle.allCases.forEach { - let style = styles.hasStyle($0) - context.setStyleInternal($0, to: style) - } + let styles = textView.richTextStyles + RichTextStyle.allCases.forEach { + let style = styles.hasStyle($0) + context.setStyleInternal($0, to: style) + } - updateTextViewAttributesIfNeeded() - } + updateTextViewAttributesIfNeeded() + } - /// Sync the text binding with the text view. - func syncTextWithTextView() { - DispatchQueue.main.async { - self.text.wrappedValue = self.textView.attributedString + /// Sync the text binding with the text view. + func syncTextWithTextView() { + DispatchQueue.main.async { + self.text.wrappedValue = self.textView.attributedString + } } - } - /** + /** On macOS, we have to update the font and colors when we move the text input cursor and there's no selected text. @@ -274,25 +280,25 @@ extension RichTextCoordinator { the presented information will be correct, but when you type, the last selected font, colors etc. will be used. */ - func updateTextViewAttributesIfNeeded() { -#if macOS - if textView.hasSelectedRange { return } - let attributes = textView.richTextAttributes - textView.setRichTextAttributes(attributes) -#endif - } + func updateTextViewAttributesIfNeeded() { + #if macOS + if textView.hasSelectedRange { return } + let attributes = textView.richTextAttributes + textView.setRichTextAttributes(attributes) + #endif + } - /** + /** On macOS, we have to update the typingAttributes when we move the text input cursor and there's no selected text. So that the current attributes will set again for updated location. */ - func replaceCurrentAttributesIfNeeded() { -#if macOS - if textView.hasSelectedRange { return } - let attributes = textView.richTextAttributes - textView.setNewRichTextAttributes(attributes) -#endif + func replaceCurrentAttributesIfNeeded() { + #if macOS + if textView.hasSelectedRange { return } + let attributes = textView.richTextAttributes + textView.setNewRichTextAttributes(attributes) + #endif + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextPresenter.swift b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextPresenter.swift index 702015e..2d18da8 100644 --- a/Sources/RichEditorSwiftUI/BaseFoundation/RichTextPresenter.swift +++ b/Sources/RichEditorSwiftUI/BaseFoundation/RichTextPresenter.swift @@ -7,55 +7,52 @@ import Foundation -/** - This protocol can be implemented any types that can present - a rich text and provide a ``selectedRange``. - - This protocol is implemented by ``RichTextEditor`` since it - can both present and select text. It is also implemented by - the platform-specific ``RichTextView`` components. - */ +/// This protocol can be implemented any types that can present +/// a rich text and provide a ``selectedRange``. +/// +/// This protocol is implemented by ``RichTextEditor`` since it +/// can both present and select text. It is also implemented by +/// the platform-specific ``RichTextView`` components. public protocol RichTextPresenter: RichTextReader { /// Get the currently selected range. var selectedRange: NSRange { get } } -public extension RichTextPresenter { +extension RichTextPresenter { /// Whether or not the presenter has a selected range. - var hasSelectedRange: Bool { + public var hasSelectedRange: Bool { selectedRange.length > 0 } /// Whether or not the rich text contains trimmed text. - var hasTrimmedText: Bool { + public var hasTrimmedText: Bool { let string = richText.string let trimmed = string.trimmingCharacters(in: .whitespaces) return !trimmed.isEmpty } /// Get the range after the input cursor. - var rangeAfterInputCursor: NSRange { + public var rangeAfterInputCursor: NSRange { let location = selectedRange.location let length = richText.length - location return NSRange(location: location, length: length) } /// Get the range before the input cursor. - var rangeBeforeInputCursor: NSRange { + public var rangeBeforeInputCursor: NSRange { let location = selectedRange.location return NSRange(location: 0, length: location) } /// Get the rich text after the input cursor. - var richTextAfterInputCursor: NSAttributedString { + public var richTextAfterInputCursor: NSAttributedString { richText(at: rangeAfterInputCursor) } /// Get the rich text before the input cursor. - var richTextBeforeInputCursor: NSAttributedString { + public var richTextBeforeInputCursor: NSAttributedString { richText(at: rangeBeforeInputCursor) } } - diff --git a/Sources/RichEditorSwiftUI/Colors/ColorRepresentable.swift b/Sources/RichEditorSwiftUI/Colors/ColorRepresentable.swift index b21e98f..e517ee1 100644 --- a/Sources/RichEditorSwiftUI/Colors/ColorRepresentable.swift +++ b/Sources/RichEditorSwiftUI/Colors/ColorRepresentable.swift @@ -6,30 +6,26 @@ // #if macOS -import AppKit + import AppKit -/** - This typealias bridges platform-specific colors to simplify - multi-platform support. - */ -public typealias ColorRepresentable = NSColor + /// This typealias bridges platform-specific colors to simplify + /// multi-platform support. + public typealias ColorRepresentable = NSColor #endif -#if iOS || os(tvOS) || os(watchOS) || os(visionOS) -import UIKit +#if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) + import UIKit -/** - This typealias bridges platform-specific colors to simplify - multi-platform support. - */ -public typealias ColorRepresentable = UIColor + /// This typealias bridges platform-specific colors to simplify + /// multi-platform support. + public typealias ColorRepresentable = UIColor #endif -#if iOS || macOS || os(tvOS) || os(visionOS) -extension ColorRepresentable { +#if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) + extension ColorRepresentable { -#if iOS || os(tvOS) || os(visionOS) - public static var textColor: ColorRepresentable { .label } -#endif -} + #if os(iOS) || os(tvOS) || os(visionOS) + public static var textColor: ColorRepresentable { .label } + #endif + } #endif diff --git a/Sources/RichEditorSwiftUI/Colors/RichTextColor+Picker.swift b/Sources/RichEditorSwiftUI/Colors/RichTextColor+Picker.swift index cbbc098..35530c4 100644 --- a/Sources/RichEditorSwiftUI/Colors/RichTextColor+Picker.swift +++ b/Sources/RichEditorSwiftUI/Colors/RichTextColor+Picker.swift @@ -7,7 +7,7 @@ import SwiftUI -public extension RichTextColor { +extension RichTextColor { /** This picker can be used to select a rich text color. @@ -18,7 +18,7 @@ public extension RichTextColor { The quick color list is empty by default. You can add a custom set of colors, for instance `.quickPickerColors`. */ - struct Picker: View { + public struct Picker: View { /** Create a rich text color picker that binds to a color. @@ -71,53 +71,53 @@ public extension RichTextColor { } } -private extension RichTextColor.Picker { +extension RichTextColor.Picker { - var hasColors: Bool { + fileprivate var hasColors: Bool { !quickColors.isEmpty } } -public extension Color { +extension Color { /// Get a curated list of quick color picker colors. - static var quickPickerColors: [Self] { + public static var quickPickerColors: [Self] { [ .black, .gray, .white, .red, .pink, .orange, .yellow, .indigo, .purple, .blue, .cyan, .teal, .mint, - .green, .brown + .green, .brown, ] } } -public extension Collection where Element == Color { +extension Collection where Element == Color { /// Get a curated list of quick color picker colors. - static var quickPickerColors: [Element] { + public static var quickPickerColors: [Element] { Element.quickPickerColors } } -private extension RichTextColor.Picker { +extension RichTextColor.Picker { @ViewBuilder - var iconView: some View { + fileprivate var iconView: some View { if let icon { icon.frame(minWidth: 30) } } @ViewBuilder - var picker: some View { - #if iOS || macOS || os(visionOS) - ColorPicker("", selection: $value) - .fixedSize() - .padding(.horizontal, spacing) + fileprivate var picker: some View { + #if os(iOS) || os(macOS) || os(visionOS) + ColorPicker("", selection: $value) + .fixedSize() + .padding(.horizontal, spacing) #endif } - var quickPicker: some View { + fileprivate var quickPicker: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: spacing) { ForEach(Array(quickColors.enumerated()), id: \.offset) { @@ -129,7 +129,7 @@ private extension RichTextColor.Picker { }.frame(maxWidth: .infinity) } - func quickPickerButton(for color: Color?) -> some View { + fileprivate func quickPickerButton(for color: Color?) -> some View { Button { value = type.adjust(color, for: colorScheme) } label: { @@ -142,7 +142,7 @@ private extension RichTextColor.Picker { .buttonStyle(ColorButtonStyle()) } - var quickPickerDivider: some View { + fileprivate var quickPickerDivider: some View { Divider() .padding(0) .frame(maxHeight: 30) diff --git a/Sources/RichEditorSwiftUI/Colors/RichTextColor.swift b/Sources/RichEditorSwiftUI/Colors/RichTextColor.swift index 50b4d84..cb9f97f 100644 --- a/Sources/RichEditorSwiftUI/Colors/RichTextColor.swift +++ b/Sources/RichEditorSwiftUI/Colors/RichTextColor.swift @@ -7,12 +7,12 @@ import SwiftUI -/** - This enum defines supported rich text color types. - - The enum makes the colors identifiable and diffable. - */ -public enum RichTextColor: String, CaseIterable, Codable, Equatable, Identifiable { +/// This enum defines supported rich text color types. +/// +/// The enum makes the colors identifiable and diffable. +public enum RichTextColor: String, CaseIterable, Codable, Equatable, + Identifiable +{ /// Foreground color. case foreground @@ -30,13 +30,13 @@ public enum RichTextColor: String, CaseIterable, Codable, Equatable, Identifiabl case underline } -public extension RichTextColor { +extension RichTextColor { /// The unique color ID. - var id: String { rawValue } + public var id: String { rawValue } /// The corresponding rich text attribute, if any. - var attribute: NSAttributedString.Key? { + public var attribute: NSAttributedString.Key? { switch self { case .foreground: .foregroundColor case .background: .backgroundColor @@ -47,7 +47,7 @@ public extension RichTextColor { } /// The standard icon to use for the color. - var icon: Image { + public var icon: Image { switch self { case .foreground: .richTextColorForeground case .background: .richTextColorBackground @@ -58,7 +58,7 @@ public extension RichTextColor { } /// The localized color title key. - var titleKey: RTEL10n { + public var titleKey: RTEL10n { switch self { case .foreground: .foregroundColor case .background: .backgroundColor @@ -69,7 +69,7 @@ public extension RichTextColor { } /// Adjust a `color` for a certain `colorScheme`. - func adjust( + public func adjust( _ color: Color?, for scheme: ColorScheme ) -> Color { @@ -80,8 +80,7 @@ public extension RichTextColor { } } -public extension Collection where Element == RichTextColor { +extension Collection where Element == RichTextColor { - static var allCases: [RichTextColor] { Element.allCases } + public static var allCases: [RichTextColor] { Element.allCases } } - diff --git a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Alignment.swift b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Alignment.swift index 597bc3b..8be9b5b 100644 --- a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Alignment.swift +++ b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Alignment.swift @@ -8,22 +8,22 @@ import Foundation #if canImport(UIKit) -import UIKit + import UIKit #elseif canImport(AppKit) && !targetEnvironment(macCatalyst) -import AppKit + import AppKit #endif -public extension RichTextViewComponent { +extension RichTextViewComponent { /// Get the text alignment. - var richTextAlignment: RichTextAlignment? { + public var richTextAlignment: RichTextAlignment? { guard let style = richTextParagraphStyle else { return nil } return RichTextAlignment(style.alignment) } /// Set the text alignment. - func setRichTextAlignment(_ alignment: RichTextAlignment) { -// if richTextAlignment == alignment { return } + public func setRichTextAlignment(_ alignment: RichTextAlignment) { + // if richTextAlignment == alignment { return } let style = NSMutableParagraphStyle( from: richTextParagraphStyle, alignment: alignment diff --git a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Attributes.swift b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Attributes.swift index ea0c0a7..3c3f119 100644 --- a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Attributes.swift +++ b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Attributes.swift @@ -7,32 +7,32 @@ import Foundation -public extension RichTextViewComponent { +extension RichTextViewComponent { /// Get all attributes. - var richTextAttributes: RichTextAttributes { + public var richTextAttributes: RichTextAttributes { if hasSelectedRange { return richTextAttributes(at: selectedRange) } -#if macOS - let range = NSRange(location: selectedRange.location - 1, length: 1) - let safeRange = safeRange(for: range) - return richTextAttributes(at: safeRange) -#else - return typingAttributes -#endif + #if macOS + let range = NSRange(location: selectedRange.location - 1, length: 1) + let safeRange = safeRange(for: range) + return richTextAttributes(at: safeRange) + #else + return typingAttributes + #endif } /// Get a certain attribute. - func richTextAttribute( + public func richTextAttribute( _ attribute: RichTextAttribute ) -> Value? { richTextAttributes[attribute] as? Value } /// Set a certain attribute. - func setRichTextAttribute( + public func setRichTextAttribute( _ attribute: RichTextAttribute, to value: Any ) { @@ -44,7 +44,7 @@ public extension RichTextViewComponent { } /// Set certain attributes. - func setRichTextAttributes( + public func setRichTextAttributes( _ attributes: RichTextAttributes ) { attributes.forEach { attribute, value in @@ -52,10 +52,29 @@ public extension RichTextViewComponent { } } - func setNewRichTextAttributes( + public func setNewRichTextAttributes( _ attributes: RichTextAttributes ) { typingAttributes = attributes } -} + /// Remove a certain attribute. + public func removeRichTextAttribute( + _ attribute: RichTextAttribute + ) { + if hasSelectedRange { + removeRichTextAttribute(attribute, at: selectedRange) + } else { + typingAttributes[attribute] = nil + } + } + + /// Remove certain attributes. + public func removeRichTextAttributes( + _ attributes: RichTextAttributes + ) { + attributes.forEach { attribute, value in + removeRichTextAttribute(attribute) + } + } +} diff --git a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Colors.swift b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Colors.swift index 621af03..ee21b12 100644 --- a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Colors.swift +++ b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Colors.swift @@ -7,10 +7,10 @@ import Foundation -public extension RichTextViewComponent { +extension RichTextViewComponent { /// Get a certain color. - func richTextColor( + public func richTextColor( _ color: RichTextColor ) -> ColorRepresentable? { guard let attribute = color.attribute else { return nil } @@ -18,7 +18,7 @@ public extension RichTextViewComponent { } /// Get a certain color at a certain range. - func richTextColor( + public func richTextColor( _ color: RichTextColor, at range: NSRange ) -> ColorRepresentable? { @@ -27,7 +27,7 @@ public extension RichTextViewComponent { } /// Set a certain color. - func setRichTextColor( + public func setRichTextColor( _ color: RichTextColor, to val: ColorRepresentable ) { @@ -37,7 +37,7 @@ public extension RichTextViewComponent { } /// Set a certain colors at a certain range. - func setRichTextColor( + public func setRichTextColor( _ color: RichTextColor, to val: ColorRepresentable, at range: NSRange diff --git a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Link.swift b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Link.swift new file mode 100644 index 0000000..3a9bf75 --- /dev/null +++ b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Link.swift @@ -0,0 +1,63 @@ +// +// RichTextViewComponent+Link.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 18/12/24. +// + +import Foundation + +extension RichTextViewComponent { + /// Get the paragraph style. + public var richTextLink: String? { + richTextAttribute(.link) + } + + /// Get a certain link. + public func richTextLink( + _ style: RichTextSpanStyle + ) -> String? { + return richTextAttribute(style.attributedStringKey) + } + + /// Get a certain link at a certain range. + public func richTextLink( + _ style: RichTextSpanStyle, + at range: NSRange + ) -> String? { + return richTextAttribute(style.attributedStringKey, at: range) + } + + /// Set a certain link. + public func setRichTextLink( + _ style: RichTextSpanStyle + ) { + guard let val = style.getRichAttribute()?.link, + richTextLink(style) != val + else { return } + setRichTextAttribute(style.attributedStringKey, to: val) + + } + + /// Set a certain link at a certain range. + public func setRichTextLink( + _ style: RichTextSpanStyle, + at range: NSRange + ) { + let val = style.getRichAttribute()?.link + guard let val = val, richTextLink(style, at: range) != val else { + return + } + setRichTextAttribute(style.attributedStringKey, to: val, at: range) + } + + public func removeRichTextLink(_ style: RichTextSpanStyle) { + removeRichTextAttribute(style.attributedStringKey) + } + + public func removeRichTextLink( + _ style: RichTextSpanStyle, at range: NSRange + ) { + removeRichTextAttribute(style.attributedStringKey, at: range) + } +} diff --git a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Paragraph.swift b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Paragraph.swift index 16fdebd..5063351 100644 --- a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Paragraph.swift +++ b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Paragraph.swift @@ -8,17 +8,17 @@ import Foundation #if canImport(UIKit) -import UIKit + import UIKit #endif #if canImport(AppKit) && !targetEnvironment(macCatalyst) -import AppKit + import AppKit #endif -public extension RichTextViewComponent { +extension RichTextViewComponent { /// Get the paragraph style. - var richTextParagraphStyle: NSMutableParagraphStyle? { + public var richTextParagraphStyle: NSMutableParagraphStyle? { richTextAttribute(.paragraphStyle) } @@ -27,13 +27,14 @@ public extension RichTextViewComponent { /// > Todo: The function currently can't handle multiple /// selected paragraphs. If many paragraphs are selected, /// it will only affect the first one. - func setRichTextParagraphStyle(_ style: NSParagraphStyle) { + public func setRichTextParagraphStyle(_ style: NSParagraphStyle) { let range = lineRange(for: selectedRange) guard range.length > 0 else { return } #if os(watchOS) - setRichTextAttribute(.paragraphStyle, to: style, at: range) + setRichTextAttribute(.paragraphStyle, to: style, at: range) #else - textStorageWrapper?.addAttribute(.paragraphStyle, value: style, range: range) + textStorageWrapper?.addAttribute( + .paragraphStyle, value: style, range: range) #endif } } diff --git a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Pasting.swift b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Pasting.swift index 0285f0a..d109009 100644 --- a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Pasting.swift +++ b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Pasting.swift @@ -8,14 +8,14 @@ import Foundation #if canImport(UIKit) -import UIKit + import UIKit #endif #if canImport(AppKit) && !targetEnvironment(macCatalyst) -import AppKit + import AppKit #endif -public extension RichTextViewComponent { +extension RichTextViewComponent { /** Paste text into the text view, at a certain index. @@ -27,7 +27,7 @@ public extension RichTextViewComponent { cursor should be moved to the end of the pasted content, by default `false`. */ - func pasteText( + public func pasteText( _ text: String, at index: Int, moveCursorToPastedContent: Bool = false @@ -52,4 +52,3 @@ public extension RichTextViewComponent { } } } - diff --git a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Ranges.swift b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Ranges.swift index 68f283b..f2732af 100644 --- a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Ranges.swift +++ b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Ranges.swift @@ -16,43 +16,45 @@ extension RichTextViewComponent { /// Get the line range at a certain text location. func lineRange(at location: Int) -> NSRange { #if os(watchOS) - return notFoundRange + return notFoundRange #else - guard - let manager = layoutManagerWrapper, - let storage = textStorageWrapper - else { return NSRange(location: NSNotFound, length: 0) } - let string = storage.string as NSString - let locationRange = NSRange(location: location, length: 0) - let lineRange = string.lineRange(for: locationRange) - return manager.characterRange(forGlyphRange: lineRange, actualGlyphRange: nil) + guard + let manager = layoutManagerWrapper, + let storage = textStorageWrapper + else { return NSRange(location: NSNotFound, length: 0) } + let string = storage.string as NSString + let locationRange = NSRange(location: location, length: 0) + let lineRange = string.lineRange(for: locationRange) + return manager.characterRange( + forGlyphRange: lineRange, actualGlyphRange: nil) #endif } /// Get the line range for a certain text range. func lineRange(for range: NSRange) -> NSRange { #if os(watchOS) - return notFoundRange + return notFoundRange #else - // Use the location-based logic if range is empty - if range.length == 0 { - return lineRange(at: range.location) - } - - guard let manager = layoutManagerWrapper else { - return NSRange(location: NSNotFound, length: 0) - } - - var lineRange = NSRange(location: NSNotFound, length: 0) - manager.enumerateLineFragments( - forGlyphRange: range - ) { (_, _, _, glyphRange, stop) in - lineRange = glyphRange - stop.pointee = true - } - - // Convert glyph range to character range - return manager.characterRange(forGlyphRange: lineRange, actualGlyphRange: nil) + // Use the location-based logic if range is empty + if range.length == 0 { + return lineRange(at: range.location) + } + + guard let manager = layoutManagerWrapper else { + return NSRange(location: NSNotFound, length: 0) + } + + var lineRange = NSRange(location: NSNotFound, length: 0) + manager.enumerateLineFragments( + forGlyphRange: range + ) { (_, _, _, glyphRange, stop) in + lineRange = glyphRange + stop.pointee = true + } + + // Convert glyph range to character range + return manager.characterRange( + forGlyphRange: lineRange, actualGlyphRange: nil) #endif } } diff --git a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Styles.swift b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Styles.swift index c1b417a..d247775 100644 --- a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Styles.swift +++ b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent+Styles.swift @@ -7,10 +7,10 @@ import Foundation -public extension RichTextViewComponent { +extension RichTextViewComponent { /// Get all styles. - var richTextStyles: [RichTextStyle] { + public var richTextStyles: [RichTextStyle] { let attributes = richTextAttributes let traits = richTextFont?.fontDescriptor.symbolicTraits var styles = traits?.enabledRichTextStyles ?? [] @@ -20,36 +20,35 @@ public extension RichTextViewComponent { } /// Whether or not the current range has a certain style. - func hasRichTextStyle(_ style: RichTextStyle) -> Bool { + public func hasRichTextStyle(_ style: RichTextStyle) -> Bool { richTextStyles.contains(style) } /// Set a certain style. - func setRichTextStyle( + public func setRichTextStyle( _ style: RichTextStyle, to newValue: Bool ) { let value = newValue ? 1 : 0 switch style { - case .bold, .italic: - let styles = richTextStyles - guard styles.shouldAddOrRemove(style, newValue) else { return } - guard let font = richTextFont else { return } - guard let newFont = font.toggling(style) else { return } - setRichTextFont(newFont) - case .underline: - setRichTextAttribute(.underlineStyle, to: value) - case .strikethrough: - setRichTextAttribute(.strikethroughStyle, to: value) + case .bold, .italic: + let styles = richTextStyles + guard styles.shouldAddOrRemove(style, newValue) else { return } + guard let font = richTextFont else { return } + guard let newFont = font.toggling(style) else { return } + setRichTextFont(newFont) + case .underline: + setRichTextAttribute(.underlineStyle, to: value) + case .strikethrough: + setRichTextAttribute(.strikethroughStyle, to: value) } } /// Toggle a certain style. - func toggleRichTextStyle( + public func toggleRichTextStyle( _ style: RichTextStyle ) { let hasStyle = hasRichTextStyle(style) setRichTextStyle(style, to: !hasStyle) } } - diff --git a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent.swift b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent.swift index 990b8ef..46aebec 100644 --- a/Sources/RichEditorSwiftUI/Components/RichTextViewComponent.swift +++ b/Sources/RichEditorSwiftUI/Components/RichTextViewComponent.swift @@ -9,29 +9,27 @@ import CoreGraphics import Foundation #if canImport(UIKit) -import UIKit + import UIKit #elseif canImport(AppKit) && !targetEnvironment(macCatalyst) -import AppKit + import AppKit #endif -/** - This protocol provides a common interface for the UIKit and - AppKit ``RichTextView`` components. - - By implementing this protocol, the library does not have to - do a bunch of `#if` platform checks within the code. - - This component can read and write many different attributes - from and to its rich text, using the underlying features of - ``RichTextAttributeReader`` and ``RichTextAttributeWriter``. - - The protocol implements and extends many other protocols to - provide more features for components with more capabilities. - */ +/// This protocol provides a common interface for the UIKit and +/// AppKit ``RichTextView`` components. +/// +/// By implementing this protocol, the library does not have to +/// do a bunch of `#if` platform checks within the code. +/// +/// This component can read and write many different attributes +/// from and to its rich text, using the underlying features of +/// ``RichTextAttributeReader`` and ``RichTextAttributeWriter``. +/// +/// The protocol implements and extends many other protocols to +/// provide more features for components with more capabilities. public protocol RichTextViewComponent: AnyObject, - RichTextPresenter, - RichTextAttributeReader, - RichTextAttributeWriter + RichTextPresenter, + RichTextAttributeReader, + RichTextAttributeWriter { /// The text view's frame. @@ -43,13 +41,13 @@ public protocol RichTextViewComponent: AnyObject, /// Whether or not the text view is the first responder. var isFirstResponder: Bool { get } -#if iOS || macOS || os(tvOS) || os(visionOS) - /// The text view's layout manager, if any. - var layoutManagerWrapper: NSLayoutManager? { get } + #if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) + /// The text view's layout manager, if any. + var layoutManagerWrapper: NSLayoutManager? { get } - /// The text view's text storage, if any. - var textStorageWrapper: NSTextStorage? { get } -#endif + /// The text view's text storage, if any. + var textStorageWrapper: NSTextStorage? { get } + #endif /// The text view's mutable attributed string, if any. var mutableAttributedString: NSMutableAttributedString? { get } @@ -97,27 +95,27 @@ public protocol RichTextViewComponent: AnyObject, // MARK: - Public Extension -public extension RichTextViewComponent { +extension RichTextViewComponent { /// Show an alert with a title, message and OK button. - func alert(title: String, message: String) { + public func alert(title: String, message: String) { alert(title: title, message: message, buttonTitle: "OK") } /// Delete all characters in a certain range. - func deleteCharacters(in range: NSRange) { + public func deleteCharacters(in range: NSRange) { mutableAttributedString?.deleteCharacters(in: range) } /// Move the text cursor to a certain input index. - func moveInputCursor(to index: Int) { + public func moveInputCursor(to index: Int) { let newRange = NSRange(location: index, length: 0) let safeRange = safeRange(for: newRange) setSelectedRange(safeRange) } /// Setup the view with data and a data format. - func setup( + public func setup( with data: Data, format: RichTextDataFormat ) throws { @@ -126,20 +124,20 @@ public extension RichTextViewComponent { } /// Get the image configuration for a certain format. -// func standardImageConfiguration( -// for format: RichTextDataFormat -// ) -> RichTextImageConfiguration { -// let insertConfig = standardImageInsertConfiguration(for: format) -// return RichTextImageConfiguration( -// pasteConfiguration: insertConfig, -// dropConfiguration: insertConfig, -// maxImageSize: (width: .frame, height: .frame)) -// } + // func standardImageConfiguration( + // for format: RichTextDataFormat + // ) -> RichTextImageConfiguration { + // let insertConfig = standardImageInsertConfiguration(for: format) + // return RichTextImageConfiguration( + // pasteConfiguration: insertConfig, + // dropConfiguration: insertConfig, + // maxImageSize: (width: .frame, height: .frame)) + // } /// Get the image insert config for a certain format. -// func standardImageInsertConfiguration( -// for format: RichTextDataFormat -// ) -> RichTextImageInsertConfiguration { -// format.supportsImages ? .enabled : .disabled -// } + // func standardImageInsertConfiguration( + // for format: RichTextDataFormat + // ) -> RichTextImageInsertConfiguration { + // format.supportsImages ? .enabled : .disabled + // } } diff --git a/Sources/RichEditorSwiftUI/Data/Models/HeaderType.swift b/Sources/RichEditorSwiftUI/Data/Models/HeaderType.swift index ac820a4..003bd5d 100644 --- a/Sources/RichEditorSwiftUI/Data/Models/HeaderType.swift +++ b/Sources/RichEditorSwiftUI/Data/Models/HeaderType.swift @@ -8,7 +8,9 @@ import Foundation import SwiftUI -public enum HeaderType: Int, CaseIterable, Codable, Equatable, Identifiable, RichTextLabelValue { +public enum HeaderType: Int, CaseIterable, Codable, Equatable, Identifiable, + RichTextLabelValue +{ case `default` = 0 case h1 = 1 case h2 = 2 @@ -49,19 +51,18 @@ public enum HeaderType: Int, CaseIterable, Codable, Equatable, Identifiable, Ric } } +extension Collection where Element == HeaderType { -public extension Collection where Element == HeaderType { - - static var all: [Element] { HeaderType.allCases } + public static var all: [Element] { HeaderType.allCases } } -public extension HeaderType { +extension HeaderType { /// The unique header ID. - var id: String { "\(rawValue)" } + public var id: String { "\(rawValue)" } /// The standard icon to use for the header. - var icon: Image { + public var icon: Image { switch self { case .default: .richTextHeaderDefault case .h1: .richTextHeader1 @@ -74,10 +75,10 @@ public extension HeaderType { } /// standard title to use for the headers. - var title: String { titleKey.text } + public var title: String { titleKey.text } /// The standard title key to use for the header. - var titleKey: RTEL10n { + public var titleKey: RTEL10n { switch self { case .default: .headerDefault case .h1: .header1 diff --git a/Sources/RichEditorSwiftUI/Data/Models/RichAttributes+RichTextAttributes.swift b/Sources/RichEditorSwiftUI/Data/Models/RichAttributes+RichTextAttributes.swift index f23ca7d..b9ef384 100644 --- a/Sources/RichEditorSwiftUI/Data/Models/RichAttributes+RichTextAttributes.swift +++ b/Sources/RichEditorSwiftUI/Data/Models/RichAttributes+RichTextAttributes.swift @@ -5,14 +5,14 @@ // Created by Divyesh Vekariya on 24/10/24. // +import SwiftUI + #if canImport(UIKit) -import UIKit + import UIKit #elseif canImport(AppKit) -import AppKit + import AppKit #endif -import SwiftUI - extension RichAttributes { func toAttributes(font: FontRepresentable? = nil) -> RichTextAttributes { var attributes: RichTextAttributes = [:] @@ -24,7 +24,8 @@ extension RichAttributes { // Set the font size and handle headers var font = font ?? defaultFont if let headerType = self.header?.getTextSpanStyle() { - font = font + font = + font .updateFontSize( size: font.pointSize * headerType.fontSizeMultiplier ) @@ -68,7 +69,8 @@ extension RichAttributes { } if let background { - attributes[.backgroundColor] = ColorRepresentable(Color(hex: background)) + attributes[.backgroundColor] = ColorRepresentable( + Color(hex: background)) } if let align { @@ -77,11 +79,11 @@ extension RichAttributes { } // Handle indent and paragraph styles -// if let indentLevel = indent { -// let paragraphStyle = NSMutableParagraphStyle() -// paragraphStyle.headIndent = CGFloat(indentLevel * 10) // Adjust indentation as needed -// attributes[.paragraphStyle] = paragraphStyle -// } + // if let indentLevel = indent { + // let paragraphStyle = NSMutableParagraphStyle() + // paragraphStyle.headIndent = CGFloat(indentLevel * 10) // Adjust indentation as needed + // attributes[.paragraphStyle] = paragraphStyle + // } return attributes } diff --git a/Sources/RichEditorSwiftUI/Data/Models/RichAttributes.swift b/Sources/RichEditorSwiftUI/Data/Models/RichAttributes.swift index c545c88..484947f 100644 --- a/Sources/RichEditorSwiftUI/Data/Models/RichAttributes.swift +++ b/Sources/RichEditorSwiftUI/Data/Models/RichAttributes.swift @@ -22,6 +22,7 @@ public struct RichAttributes: Codable { public let color: String? public let background: String? public let align: RichTextAlignment? + public let link: String? public init( // id: String = UUID().uuidString, @@ -36,7 +37,8 @@ public struct RichAttributes: Codable { font: String? = nil, color: String? = nil, background: String? = nil, - align: RichTextAlignment? = nil + align: RichTextAlignment? = nil, + link: String? = nil ) { // self.id = id self.bold = bold @@ -51,6 +53,7 @@ public struct RichAttributes: Codable { self.color = color self.background = background self.align = align + self.link = link } enum CodingKeys: String, CodingKey { @@ -66,6 +69,7 @@ public struct RichAttributes: Codable { case color = "color" case background = "background" case align = "align" + case link = "link" } public init(from decoder: Decoder) throws { @@ -73,16 +77,21 @@ public struct RichAttributes: Codable { // self.id = UUID().uuidString self.bold = try values.decodeIfPresent(Bool.self, forKey: .bold) self.italic = try values.decodeIfPresent(Bool.self, forKey: .italic) - self.underline = try values.decodeIfPresent(Bool.self, forKey: .underline) + self.underline = try values.decodeIfPresent( + Bool.self, forKey: .underline) self.strike = try values.decodeIfPresent(Bool.self, forKey: .strike) - self.header = try values.decodeIfPresent(HeaderType.self, forKey: .header) + self.header = try values.decodeIfPresent( + HeaderType.self, forKey: .header) self.list = try values.decodeIfPresent(ListType.self, forKey: .list) self.indent = try values.decodeIfPresent(Int.self, forKey: .indent) self.size = try values.decodeIfPresent(Int.self, forKey: .size) self.font = try values.decodeIfPresent(String.self, forKey: .font) self.color = try values.decodeIfPresent(String.self, forKey: .color) - self.background = try values.decodeIfPresent(String.self, forKey: .background) - self.align = try values.decodeIfPresent(RichTextAlignment.self, forKey: .align) + self.background = try values.decodeIfPresent( + String.self, forKey: .background) + self.align = try values.decodeIfPresent( + RichTextAlignment.self, forKey: .align) + self.link = try values.decodeIfPresent(String.self, forKey: .link) } } @@ -101,43 +110,48 @@ extension RichAttributes: Hashable { hasher.combine(color) hasher.combine(background) hasher.combine(align) + hasher.combine(link) } } extension RichAttributes: Equatable { - public static func == (lhs: RichAttributes, - rhs: RichAttributes) -> Bool { - return( - // lhs.id == rhs.id - lhs.bold == rhs.bold - && lhs.italic == rhs.italic - && lhs.underline == rhs.underline - && lhs.strike == rhs.strike - && lhs.header == rhs.header - && lhs.list == rhs.list - && lhs.indent == rhs.indent - && lhs.size == rhs.size - && lhs.font == rhs.font - && lhs.color == rhs.color - && lhs.background == rhs.background - && lhs.align == rhs.align - ) + public static func == ( + lhs: RichAttributes, + rhs: RichAttributes + ) -> Bool { + return ( + // lhs.id == rhs.id + lhs.bold == rhs.bold + && lhs.italic == rhs.italic + && lhs.underline == rhs.underline + && lhs.strike == rhs.strike + && lhs.header == rhs.header + && lhs.list == rhs.list + && lhs.indent == rhs.indent + && lhs.size == rhs.size + && lhs.font == rhs.font + && lhs.color == rhs.color + && lhs.background == rhs.background + && lhs.align == rhs.align + && lhs.link == rhs.link) } } extension RichAttributes { - public func copy(bold: Bool? = nil, - header: HeaderType? = nil, - italic: Bool? = nil, - underline: Bool? = nil, - strike: Bool? = nil, - list: ListType? = nil, - indent: Int? = nil, - size: Int? = nil, - font: String? = nil, - color: String? = nil, - background: String? = nil, - align: RichTextAlignment? = nil + public func copy( + bold: Bool? = nil, + header: HeaderType? = nil, + italic: Bool? = nil, + underline: Bool? = nil, + strike: Bool? = nil, + list: ListType? = nil, + indent: Int? = nil, + size: Int? = nil, + font: String? = nil, + color: String? = nil, + background: String? = nil, + align: RichTextAlignment? = nil, + link: String? = nil ) -> RichAttributes { return RichAttributes( bold: (bold != nil ? bold! : self.bold), @@ -151,29 +165,44 @@ extension RichAttributes { font: (font != nil ? font! : self.font), color: (color != nil ? color! : self.color), background: (background != nil ? background! : self.background), - align: (align != nil ? align! : self.align) + align: (align != nil ? align! : self.align), + link: (link != nil ? link! : self.link) ) } - - public func copy(with style: RichTextSpanStyle, byAdding: Bool = true) -> RichAttributes { + + public func copy(with style: RichTextSpanStyle, byAdding: Bool = true) + -> RichAttributes + { return copy(with: [style], byAdding: byAdding) } - public func copy(with styles: [RichTextSpanStyle], byAdding: Bool = true) -> RichAttributes { + public func copy(with styles: [RichTextSpanStyle], byAdding: Bool = true) + -> RichAttributes + { let att = getRichAttributesFor(styles: styles) return RichAttributes( bold: (att.bold != nil ? (byAdding ? att.bold! : nil) : self.bold), - italic: (att.italic != nil ? (byAdding ? att.italic! : nil) : self.italic), - underline: (att.underline != nil ? (byAdding ? att.underline! : nil) : self.underline), - strike: (att.strike != nil ? (byAdding ? att.strike! : nil) : self.strike), - header: (att.header != nil ? (byAdding ? att.header! : nil) : self.header), + italic: (att.italic != nil + ? (byAdding ? att.italic! : nil) : self.italic), + underline: (att.underline != nil + ? (byAdding ? att.underline! : nil) : self.underline), + strike: (att.strike != nil + ? (byAdding ? att.strike! : nil) : self.strike), + header: (att.header != nil + ? (byAdding ? att.header! : nil) : self.header), list: (att.list != nil ? (byAdding ? att.list! : nil) : self.list), - indent: (att.indent != nil ? (byAdding ? att.indent! : nil) : self.indent), + indent: (att.indent != nil + ? (byAdding ? att.indent! : nil) : self.indent), size: (att.size != nil ? (byAdding ? att.size! : nil) : self.size), font: (att.font != nil ? (byAdding ? att.font! : nil) : self.font), - color: (att.color != nil ? (byAdding ? att.color! : nil) : self.color), - background: (att.background != nil ? (byAdding ? att.background! : nil) : self.background), - align: (att.align != nil ? (byAdding ? att.align! : nil) : self.align) + color: (att.color != nil + ? (byAdding ? att.color! : nil) : self.color), + background: (att.background != nil + ? (byAdding ? att.background! : nil) : self.background), + align: (att.align != nil + ? (byAdding ? att.align! : nil) : self.align), + ///nil link indicates removal as well so removing link if `byAdding == false && att.link == nil` + link: (att.link != nil ? (byAdding ? att.link! : nil) : (att.link == nil && !byAdding) ? nil : self.link) ) } } @@ -214,6 +243,9 @@ extension RichAttributes { if let align = align { styles.append(align.getTextSpanStyle()) } + if let link = link { + styles.append(.link(link)) + } return styles } @@ -252,6 +284,9 @@ extension RichAttributes { if let align = align { styles.insert(align.getTextSpanStyle()) } + if let link = link { + styles.insert(.link(link)) + } return styles } } @@ -293,6 +328,8 @@ extension RichAttributes { return background == color?.hexString case .align(let alignment): return align == alignment + case .link(let linkItem): + return link == linkItem } } } @@ -301,7 +338,9 @@ internal func getRichAttributesFor(style: RichTextSpanStyle) -> RichAttributes { return getRichAttributesFor(styles: [style]) } -internal func getRichAttributesFor(styles: [RichTextSpanStyle]) -> RichAttributes { +internal func getRichAttributesFor(styles: [RichTextSpanStyle]) + -> RichAttributes +{ guard !styles.isEmpty else { return RichAttributes() } var bold: Bool? = nil var italic: Bool? = nil @@ -315,6 +354,7 @@ internal func getRichAttributesFor(styles: [RichTextSpanStyle]) -> RichAttribute var color: String? = nil var background: String? = nil var align: RichTextAlignment? = nil + var link: String? = nil for style in styles { switch style { @@ -353,19 +393,23 @@ internal func getRichAttributesFor(styles: [RichTextSpanStyle]) -> RichAttribute background = backgroundColor?.hexString case .align(let alignment): align = alignment + case .link(let linkItem): + link = linkItem } } - return RichAttributes(bold: bold, - italic: italic, - underline: underline, - strike: strike, - header: header, - list: list, - indent: indent, - size: size, - font: font, - color: color, - background: background, - align: align + return RichAttributes( + bold: bold, + italic: italic, + underline: underline, + strike: strike, + header: header, + list: list, + indent: indent, + size: size, + font: font, + color: color, + background: background, + align: align, + link: link ) } diff --git a/Sources/RichEditorSwiftUI/Data/Models/RichText.swift b/Sources/RichEditorSwiftUI/Data/Models/RichText.swift index 18620a8..9d2f669 100644 --- a/Sources/RichEditorSwiftUI/Data/Models/RichText.swift +++ b/Sources/RichEditorSwiftUI/Data/Models/RichText.swift @@ -26,11 +26,12 @@ public struct RichTextSpan: Codable { public let insert: String public let attributes: RichAttributes? - public init(insert: String, - attributes: RichAttributes? = nil) { + public init( + insert: String, + attributes: RichAttributes? = nil + ) { // self.id = id self.insert = insert self.attributes = attributes } } - diff --git a/Sources/RichEditorSwiftUI/Data/Models/RichTextSpanInternal.swift b/Sources/RichEditorSwiftUI/Data/Models/RichTextSpanInternal.swift index 6ae8594..464b396 100644 --- a/Sources/RichEditorSwiftUI/Data/Models/RichTextSpanInternal.swift +++ b/Sources/RichEditorSwiftUI/Data/Models/RichTextSpanInternal.swift @@ -14,11 +14,13 @@ public struct RichTextSpanInternal { // public let insert: String public let attributes: RichAttributes? - public init(id: String = UUID().uuidString, - from: Int, - to: Int, - // insert: String, - attributes: RichAttributes? = RichAttributes()) { + public init( + id: String = UUID().uuidString, + from: Int, + to: Int, + // insert: String, + attributes: RichAttributes? = RichAttributes() + ) { self.id = id self.from = from self.to = to @@ -28,12 +30,14 @@ public struct RichTextSpanInternal { } extension RichTextSpanInternal: Equatable { - public static func == (lhs: RichTextSpanInternal, - rhs: RichTextSpanInternal) -> Bool { + public static func == ( + lhs: RichTextSpanInternal, + rhs: RichTextSpanInternal + ) -> Bool { return lhs.from == rhs.from - && lhs.to == rhs.to - // && lhs.insert == rhs.insert - && lhs.attributes == rhs.attributes + && lhs.to == rhs.to + // && lhs.insert == rhs.insert + && lhs.attributes == rhs.attributes } } @@ -62,10 +66,12 @@ extension RichTextSpanInternal { } extension RichTextSpanInternal { - public func copy(from: Int? = nil, - to: Int? = nil, - // insert: String? = nil, - attributes: RichAttributes? = nil) -> RichTextSpanInternal { + public func copy( + from: Int? = nil, + to: Int? = nil, + // insert: String? = nil, + attributes: RichAttributes? = nil + ) -> RichTextSpanInternal { return RichTextSpanInternal( from: (from != nil ? from! : self.from), to: (to != nil ? to! : self.to), diff --git a/Sources/RichEditorSwiftUI/Export/NSAttributedString+Export.swift b/Sources/RichEditorSwiftUI/Export/NSAttributedString+Export.swift index 8b1a0bd..b4ddd53 100644 --- a/Sources/RichEditorSwiftUI/Export/NSAttributedString+Export.swift +++ b/Sources/RichEditorSwiftUI/Export/NSAttributedString+Export.swift @@ -13,8 +13,10 @@ extension NSAttributedString { /// Make all text black to account for dark mode. func withBlackText() -> NSAttributedString { let mutable = NSMutableAttributedString(attributedString: self) - let range = mutable.safeRange(for: NSRange(location: 0, length: mutable.length)) - mutable.setRichTextAttribute(.foregroundColor, to: ColorRepresentable.black, at: range) + let range = mutable.safeRange( + for: NSRange(location: 0, length: mutable.length)) + mutable.setRichTextAttribute( + .foregroundColor, to: ColorRepresentable.black, at: range) return mutable } } diff --git a/Sources/RichEditorSwiftUI/Export/RichTextExportError.swift b/Sources/RichEditorSwiftUI/Export/RichTextExportError.swift index ca1c580..4bdaf56 100644 --- a/Sources/RichEditorSwiftUI/Export/RichTextExportError.swift +++ b/Sources/RichEditorSwiftUI/Export/RichTextExportError.swift @@ -7,10 +7,8 @@ import Foundation -/** - This enum defines errors that can be thrown when failing to - export rich text. - */ +/// This enum defines errors that can be thrown when failing to +/// export rich text. public enum RichTextExportError: Error { /// This error occurs when no file could be generated at a certain url. diff --git a/Sources/RichEditorSwiftUI/Export/RichTextExportMenu.swift b/Sources/RichEditorSwiftUI/Export/RichTextExportMenu.swift index 630dc44..528f9f0 100644 --- a/Sources/RichEditorSwiftUI/Export/RichTextExportMenu.swift +++ b/Sources/RichEditorSwiftUI/Export/RichTextExportMenu.swift @@ -5,40 +5,38 @@ // Created by Divyesh Vekariya on 26/11/24. // -#if iOS || macOS || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(visionOS) + import SwiftUI -/** - This menu can be used to trigger various export actions for - a list of ``RichTextDataFormat`` values. + /// This menu can be used to trigger various export actions for + /// a list of ``RichTextDataFormat`` values. + /// + /// This menu uses a ``RichTextDataFormat/Menu`` configured for + /// exporting, with customizable actions and data formats. + public struct RichTextExportMenu: View { - This menu uses a ``RichTextDataFormat/Menu`` configured for - exporting, with customizable actions and data formats. - */ -public struct RichTextExportMenu: View { + public init( + title: String = RTEL10n.menuExportAs.text, + icon: Image = .richTextExport, + formats: [RichTextDataFormat] = RichTextDataFormat.libraryFormats, + otherFormats: [RichTextExportOption] = .all, + formatAction: @escaping (RichTextDataFormat) -> Void, + otherOptionAction: ((RichTextExportOption) -> Void)? = nil + ) { + self.menu = RichTextDataFormat.Menu( + title: title, + icon: icon, + formats: formats, + otherFormats: otherFormats, + formatAction: formatAction, + otherOptionAction: otherOptionAction + ) + } - public init( - title: String = RTEL10n.menuExportAs.text, - icon: Image = .richTextExport, - formats: [RichTextDataFormat] = RichTextDataFormat.libraryFormats, - otherFormats: [RichTextExportOption] = .all, - formatAction: @escaping (RichTextDataFormat) -> Void, - otherOptionAction: ((RichTextExportOption) -> Void)? = nil - ) { - self.menu = RichTextDataFormat.Menu( - title: title, - icon: icon, - formats: formats, - otherFormats: otherFormats, - formatAction: formatAction, - otherOptionAction: otherOptionAction - ) - } - - private let menu: RichTextDataFormat.Menu + private let menu: RichTextDataFormat.Menu - public var body: some View { - menu + public var body: some View { + menu + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/Export/RichTextExportOption.swift b/Sources/RichEditorSwiftUI/Export/RichTextExportOption.swift index a210999..2d43c0a 100644 --- a/Sources/RichEditorSwiftUI/Export/RichTextExportOption.swift +++ b/Sources/RichEditorSwiftUI/Export/RichTextExportOption.swift @@ -12,9 +12,9 @@ public enum RichTextExportOption: CaseIterable, Equatable, Identifiable { case json } -public extension RichTextExportOption { +extension RichTextExportOption { /// The format's unique identifier. - var id: String { + public var id: String { switch self { case .pdf: "pdf" case .json: "json" @@ -22,7 +22,7 @@ public extension RichTextExportOption { } /// The format's file format display text. - var fileFormatText: String { + public var fileFormatText: String { switch self { case .pdf: RTEL10n.fileFormatPdf.text case .json: RTEL10n.fileFormatJson.text @@ -30,7 +30,7 @@ public extension RichTextExportOption { } } -public extension Collection where Element == RichTextExportOption { +extension Collection where Element == RichTextExportOption { - static var all: [Element] { RichTextExportOption.allCases } + public static var all: [Element] { RichTextExportOption.allCases } } diff --git a/Sources/RichEditorSwiftUI/Export/RichTextExportService.swift b/Sources/RichEditorSwiftUI/Export/RichTextExportService.swift index f0e976d..e8393d9 100644 --- a/Sources/RichEditorSwiftUI/Export/RichTextExportService.swift +++ b/Sources/RichEditorSwiftUI/Export/RichTextExportService.swift @@ -7,10 +7,8 @@ import Foundation -/** - This protocol can be implemented by any classes that can be - used to export rich text to files. - */ +/// This protocol can be implemented by any classes that can be +/// used to export rich text to files. @preconcurrency @MainActor public protocol RichTextExportService: AnyObject { @@ -58,7 +56,8 @@ public protocol RichTextExportService: AnyObject { /** Get `Data` for with provided `RichTextDataFormat` */ - func getDataFor(_ string: NSAttributedString, format: RichTextDataFormat) throws -> Data + func getDataFor(_ string: NSAttributedString, format: RichTextDataFormat) + throws -> Data /** Get `Data` for `PDF` format. diff --git a/Sources/RichEditorSwiftUI/Export/RichTextExportUrlResolver+FileManager.swift b/Sources/RichEditorSwiftUI/Export/RichTextExportUrlResolver+FileManager.swift index 1463a14..504612a 100644 --- a/Sources/RichEditorSwiftUI/Export/RichTextExportUrlResolver+FileManager.swift +++ b/Sources/RichEditorSwiftUI/Export/RichTextExportUrlResolver+FileManager.swift @@ -7,15 +7,13 @@ import Foundation -/** - This is a typealias for the `FileManager` class, since it's - the standard way to resolve export file urls. - */ +/// This is a typealias for the `FileManager` class, since it's +/// the standard way to resolve export file urls. public typealias StandardRichTextExportUrlResolver = FileManager extension FileManager: RichTextExportUrlResolver {} -public extension FileManager { +extension FileManager { /** Try to generate a file url in a certain directory. @@ -25,16 +23,19 @@ public extension FileManager { - extensions: The file extension. - directory: The directory in which to generate an url. */ - func fileUrl( + public func fileUrl( withName fileName: String, extension: String, in directory: FileManager.SearchPathDirectory ) throws -> URL { - let url = self + let url = + self .urls(for: directory, in: .userDomainMask).first? .appendingPathComponent(fileName) .appendingPathExtension(`extension`) - guard let fileUrl = url else { throw RichTextExportError.cantCreateFileUrl(in: directory) } + guard let fileUrl = url else { + throw RichTextExportError.cantCreateFileUrl(in: directory) + } return fileUrl } @@ -51,12 +52,13 @@ public extension FileManager { - extensions: The file extension. - directory: The directory in which to generate an url. */ - func uniqueFileUrl( + public func uniqueFileUrl( withName fileName: String, extension: String, in directory: FileManager.SearchPathDirectory ) throws -> URL { - let url = try fileUrl(withName: fileName, extension: `extension`, in: directory) + let url = try fileUrl( + withName: fileName, extension: `extension`, in: directory) let uniqueUrl = uniqueUrl(for: url) return uniqueUrl } @@ -73,14 +75,15 @@ public extension FileManager { - Parameters: - url: The url to generate a unique url for. */ - func uniqueUrl(for url: URL) -> URL { + public func uniqueUrl(for url: URL) -> URL { if !fileExists(atPath: url.path) { return url } let fileExtension = url.pathExtension let noExtension = url.deletingPathExtension() let fileName = noExtension.lastPathComponent var counter = 1 repeat { - let newUrl = noExtension + let newUrl = + noExtension .deletingLastPathComponent() .appendingPathComponent(fileName.appending("-\(counter)")) .appendingPathExtension(fileExtension) diff --git a/Sources/RichEditorSwiftUI/Export/RichTextExportUrlResolver.swift b/Sources/RichEditorSwiftUI/Export/RichTextExportUrlResolver.swift index aa0e62b..1fcd32e 100644 --- a/Sources/RichEditorSwiftUI/Export/RichTextExportUrlResolver.swift +++ b/Sources/RichEditorSwiftUI/Export/RichTextExportUrlResolver.swift @@ -7,13 +7,11 @@ import Foundation -/** - This protocol can be implemented by types that can generate - file urls, for instance when exporting rich text files. - - The protocol is implemented by `FileManager`, which is used - by default by the library. - */ +/// This protocol can be implemented by types that can generate +/// file urls, for instance when exporting rich text files. +/// +/// The protocol is implemented by `FileManager`, which is used +/// by default by the library. public protocol RichTextExportUrlResolver { /** diff --git a/Sources/RichEditorSwiftUI/Export/StandardRichTextExportService.swift b/Sources/RichEditorSwiftUI/Export/StandardRichTextExportService.swift index 8acadd5..77ce02e 100644 --- a/Sources/RichEditorSwiftUI/Export/StandardRichTextExportService.swift +++ b/Sources/RichEditorSwiftUI/Export/StandardRichTextExportService.swift @@ -7,16 +7,13 @@ import Foundation -/** - This export service can be used to export rich text content - to files with a certain format. - - Files are by default written to the app document folder. It - can be changed by providing another searchpath directory in - the initializer. - */ +/// This export service can be used to export rich text content +/// to files with a certain format. +/// +/// Files are by default written to the app document folder. It +/// can be changed by providing another searchpath directory in +/// the initializer. public class StandardRichTextExportService: RichTextExportService { - /** Create a standard rich text export service. @@ -125,14 +122,17 @@ public class StandardRichTextExportService: RichTextExportService { /** Get `Data` for with provided `RichTextDataFormat` */ - public func getDataFor(_ string: NSAttributedString, format: RichTextDataFormat) throws -> Data { + public func getDataFor( + _ string: NSAttributedString, format: RichTextDataFormat + ) throws -> Data { return try string.richTextData(for: format) } /** Get `Data` for `PDF` format. */ - public func getDataForPdfFormat(_ string: NSAttributedString) throws -> Data { + public func getDataForPdfFormat(_ string: NSAttributedString) throws -> Data + { return try string.richTextPdfData() } diff --git a/Sources/RichEditorSwiftUI/ExportData/NSAttributedString+Init.swift b/Sources/RichEditorSwiftUI/ExportData/NSAttributedString+Init.swift index 4259ac1..105fc7e 100644 --- a/Sources/RichEditorSwiftUI/ExportData/NSAttributedString+Init.swift +++ b/Sources/RichEditorSwiftUI/ExportData/NSAttributedString+Init.swift @@ -7,7 +7,7 @@ import Foundation -public extension NSAttributedString { +extension NSAttributedString { /** Try to parse ``RichTextDataFormat`` formatted data. @@ -16,7 +16,7 @@ public extension NSAttributedString { - data: The data to initialize the string with. - format: The data format to use. */ - convenience init( + public convenience init( data: Data, format: RichTextDataFormat ) throws { @@ -29,10 +29,10 @@ public extension NSAttributedString { } } -private extension NSAttributedString { +extension NSAttributedString { /// Try to parse ``RichTextDataFormat/archivedData``. - convenience init(archivedData data: Data) throws { + fileprivate convenience init(archivedData data: Data) throws { let unarchived = try NSKeyedUnarchiver.unarchivedObject( ofClass: NSAttributedString.self, from: data) @@ -43,7 +43,7 @@ private extension NSAttributedString { } /// Try to parse ``RichTextDataFormat/plainText`` data. - convenience init(plainTextData data: Data) throws { + fileprivate convenience init(plainTextData data: Data) throws { let decoded = String(data: data, encoding: .utf8) guard let string = decoded else { throw RichTextDataError.invalidPlainTextData(in: data) @@ -53,7 +53,7 @@ private extension NSAttributedString { } /// Try to parse ``RichTextDataFormat/rtf`` data. - convenience init(rtfData data: Data) throws { + fileprivate convenience init(rtfData data: Data) throws { var attributes = Self.rtfDataAttributes as NSDictionary? try self.init( data: data, @@ -63,7 +63,7 @@ private extension NSAttributedString { } /// Try to parse ``RichTextDataFormat/rtfd`` data. - convenience init(rtfdData data: Data) throws { + fileprivate convenience init(rtfdData data: Data) throws { var attributes = Self.rtfdDataAttributes as NSDictionary? try self.init( data: data, @@ -73,18 +73,18 @@ private extension NSAttributedString { } #if macOS - /// Try to parse ``RichTextDataFormat/word`` data. - convenience init(wordData data: Data) throws { - var attributes = Self.wordDataAttributes as NSDictionary? - try self.init( - data: data, - options: [.characterEncoding: Self.utf8], - documentAttributes: &attributes - ) - } + /// Try to parse ``RichTextDataFormat/word`` data. + convenience init(wordData data: Data) throws { + var attributes = Self.wordDataAttributes as NSDictionary? + try self.init( + data: data, + options: [.characterEncoding: Self.utf8], + documentAttributes: &attributes + ) + } #endif - convenience init(jsonData data: Data) throws { + fileprivate convenience init(jsonData data: Data) throws { let decoder = JSONDecoder() let richText = try? decoder.decode(RichText.self, from: data) guard let richText = richText else { @@ -94,9 +94,10 @@ private extension NSAttributedString { var tempSpans: [RichTextSpanInternal] = [] var text = "" richText.spans.forEach({ - let span = RichTextSpanInternal(from: text.utf16Length, - to: (text.utf16Length + $0.insert.utf16Length - 1), - attributes: $0.attributes) + let span = RichTextSpanInternal( + from: text.utf16Length, + to: (text.utf16Length + $0.insert.utf16Length - 1), + attributes: $0.attributes) tempSpans.append(span) text += $0.insert }) @@ -104,29 +105,30 @@ private extension NSAttributedString { let attributedString = NSMutableAttributedString(string: text) tempSpans.forEach { span in - attributedString.addAttributes(span.attributes?.toAttributes() ?? [:], range: span.spanRange) + attributedString.addAttributes( + span.attributes?.toAttributes() ?? [:], range: span.spanRange) } self.init(attributedString: attributedString) } } -private extension NSAttributedString { +extension NSAttributedString { - static var utf8: UInt { + fileprivate static var utf8: UInt { String.Encoding.utf8.rawValue } - static var rtfDataAttributes: [DocumentAttributeKey: Any] { + fileprivate static var rtfDataAttributes: [DocumentAttributeKey: Any] { [.documentType: NSAttributedString.DocumentType.rtf] } - static var rtfdDataAttributes: [DocumentAttributeKey: Any] { + fileprivate static var rtfdDataAttributes: [DocumentAttributeKey: Any] { [.documentType: NSAttributedString.DocumentType.rtfd] } #if macOS - static var wordDataAttributes: [DocumentAttributeKey: Any] { - [.documentType: NSAttributedString.DocumentType.docFormat] - } + static var wordDataAttributes: [DocumentAttributeKey: Any] { + [.documentType: NSAttributedString.DocumentType.docFormat] + } #endif } diff --git a/Sources/RichEditorSwiftUI/ExportData/RichTextDataError.swift b/Sources/RichEditorSwiftUI/ExportData/RichTextDataError.swift index c5d6f63..3b0615e 100644 --- a/Sources/RichEditorSwiftUI/ExportData/RichTextDataError.swift +++ b/Sources/RichEditorSwiftUI/ExportData/RichTextDataError.swift @@ -7,9 +7,7 @@ import Foundation -/** - This enum represents rich text data-related errors. - */ +/// This enum represents rich text data-related errors. public enum RichTextDataError: Error { case invalidArchivedData(in: Data) diff --git a/Sources/RichEditorSwiftUI/ExportData/RichTextDataFormat+Menu.swift b/Sources/RichEditorSwiftUI/ExportData/RichTextDataFormat+Menu.swift index 8e844c1..84a9500 100644 --- a/Sources/RichEditorSwiftUI/ExportData/RichTextDataFormat+Menu.swift +++ b/Sources/RichEditorSwiftUI/ExportData/RichTextDataFormat+Menu.swift @@ -5,12 +5,12 @@ // Created by Divyesh Vekariya on 26/11/24. // -#if iOS || macOS || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(visionOS) + import SwiftUI -public extension RichTextDataFormat { + extension RichTextDataFormat { - /** + /** This menu can be used to trigger custom actions for any list of ``RichTextDataFormat`` values. @@ -19,55 +19,55 @@ public extension RichTextDataFormat { has an optional `pdf` action, which for instance can be used when exporting or sharing rich text. */ - struct Menu: View { + public struct Menu: View { - public init( - title: String, - icon: Image, - formats: [Format] = Format.libraryFormats, - otherFormats: [RichTextExportOption] = .all, - formatAction: @escaping (Format) -> Void, - otherOptionAction: ((RichTextExportOption) -> Void)? = nil - ) { - self.title = title - self.icon = icon - self.formats = formats - self.otherFormats = otherFormats - self.formatAction = formatAction - self.otherOptionAction = otherOptionAction - } + public init( + title: String, + icon: Image, + formats: [Format] = Format.libraryFormats, + otherFormats: [RichTextExportOption] = .all, + formatAction: @escaping (Format) -> Void, + otherOptionAction: ((RichTextExportOption) -> Void)? = nil + ) { + self.title = title + self.icon = icon + self.formats = formats + self.otherFormats = otherFormats + self.formatAction = formatAction + self.otherOptionAction = otherOptionAction + } - public typealias Format = RichTextDataFormat + public typealias Format = RichTextDataFormat - private let title: String - private let icon: Image - private let formats: [Format] - private let otherFormats: [RichTextExportOption] - private let formatAction: (Format) -> Void - private let otherOptionAction: ((RichTextExportOption) -> Void)? + private let title: String + private let icon: Image + private let formats: [Format] + private let otherFormats: [RichTextExportOption] + private let formatAction: (Format) -> Void + private let otherOptionAction: ((RichTextExportOption) -> Void)? - public var body: some View { - SwiftUI.Menu { - ForEach(formats) { format in - Button { - formatAction(format) - } label: { - icon.label(format.fileFormatText) - } - } - if let action = otherOptionAction { - ForEach(otherFormats) { format in + public var body: some View { + SwiftUI.Menu { + ForEach(formats) { format in Button { - action(format) + formatAction(format) } label: { icon.label(format.fileFormatText) } } + if let action = otherOptionAction { + ForEach(otherFormats) { format in + Button { + action(format) + } label: { + icon.label(format.fileFormatText) + } + } + } + } label: { + icon.label(title) } - } label: { - icon.label(title) } } } -} #endif diff --git a/Sources/RichEditorSwiftUI/ExportData/RichTextDataFormat.swift b/Sources/RichEditorSwiftUI/ExportData/RichTextDataFormat.swift index 91bafaf..30c4cd5 100644 --- a/Sources/RichEditorSwiftUI/ExportData/RichTextDataFormat.swift +++ b/Sources/RichEditorSwiftUI/ExportData/RichTextDataFormat.swift @@ -8,25 +8,23 @@ import Foundation import UniformTypeIdentifiers -/** - This enum specifies rich text data formats. - - Different formats have different capabilities. For instance, - ``rtf`` supports rich text, styles, etc., while ``plainText`` - only handles text. ``archivedData`` can archive text, image - data and attachments in binary archives. This is convenient - when only targeting Apple platforms, but restricts how data - can be used elsewhere. - - ``archivedData`` uses an `rtk` file extension, as well as a - `UTType.archivedData` uniform type. You can create a custom - ``vendorArchivedData(id:fileExtension:fileFormatText:uniformType:)`` - value to specify a custom data format. - - Remember to configure your app for handling the UTTypes you - want to support, as well as the file extensions you want to - open with the app. Take a look at the demo app for examples. - */ +/// This enum specifies rich text data formats. +/// +/// Different formats have different capabilities. For instance, +/// ``rtf`` supports rich text, styles, etc., while ``plainText`` +/// only handles text. ``archivedData`` can archive text, image +/// data and attachments in binary archives. This is convenient +/// when only targeting Apple platforms, but restricts how data +/// can be used elsewhere. +/// +/// ``archivedData`` uses an `rtk` file extension, as well as a +/// `UTType.archivedData` uniform type. You can create a custom +/// ``vendorArchivedData(id:fileExtension:fileFormatText:uniformType:)`` +/// value to specify a custom data format. +/// +/// Remember to configure your app for handling the UTTypes you +/// want to support, as well as the file extensions you want to +/// open with the app. Take a look at the demo app for examples. public enum RichTextDataFormat: Equatable, Identifiable { /// Archived data that's persisted with a keyed archiver. @@ -47,23 +45,23 @@ public enum RichTextDataFormat: Equatable, Identifiable { ) } -public extension Collection where Element == RichTextDataFormat { +extension Collection where Element == RichTextDataFormat { /// Get all library supported data formats. - static var libraryFormats: [Element] { + public static var libraryFormats: [Element] { Element.libraryFormats } } -public extension RichTextDataFormat { +extension RichTextDataFormat { /// Get all library supported data formats. - static var libraryFormats: [Self] { + public static var libraryFormats: [Self] { [.archivedData, .plainText, .rtf] } /// The format's unique identifier. - var id: String { + public var id: String { switch self { case .archivedData: "archivedData" case .plainText: "plainText" @@ -73,7 +71,7 @@ public extension RichTextDataFormat { } /// The formats that a format can be converted to. - var convertibleFormats: [Self] { + public var convertibleFormats: [Self] { switch self { case .vendorArchivedData: Self.libraryFormats.removing(.archivedData) default: Self.libraryFormats.removing(self) @@ -81,7 +79,7 @@ public extension RichTextDataFormat { } /// The format's file format display text. - var fileFormatText: String { + public var fileFormatText: String { switch self { case .archivedData: RTEL10n.fileFormatRtk.text case .plainText: RTEL10n.fileFormatTxt.text @@ -91,7 +89,7 @@ public extension RichTextDataFormat { } /// Whether or not the format is an archived data type. - var isArchivedDataFormat: Bool { + public var isArchivedDataFormat: Bool { switch self { case .archivedData: true case .plainText: false @@ -101,7 +99,7 @@ public extension RichTextDataFormat { } /// The format's standard file extension. - var standardFileExtension: String { + public var standardFileExtension: String { switch self { case .archivedData: "rtk" case .plainText: "txt" @@ -111,7 +109,7 @@ public extension RichTextDataFormat { } /// Whether or not the format supports images. - var supportsImages: Bool { + public var supportsImages: Bool { switch self { case .archivedData: true case .plainText: false @@ -121,7 +119,7 @@ public extension RichTextDataFormat { } /// The format's uniform type. - var uniformType: UTType { + public var uniformType: UTType { switch self { case .archivedData: .archivedData case .plainText: .plainText @@ -131,9 +129,9 @@ public extension RichTextDataFormat { } } -private extension Collection where Element == RichTextDataFormat { +extension Collection where Element == RichTextDataFormat { - func removing(_ format: Element) -> [Element] { + fileprivate func removing(_ format: Element) -> [Element] { filter { $0 != format } } } diff --git a/Sources/RichEditorSwiftUI/ExportData/RichTextDataReader.swift b/Sources/RichEditorSwiftUI/ExportData/RichTextDataReader.swift index beee6c5..d4d341c 100644 --- a/Sources/RichEditorSwiftUI/ExportData/RichTextDataReader.swift +++ b/Sources/RichEditorSwiftUI/ExportData/RichTextDataReader.swift @@ -7,18 +7,16 @@ import Foundation -/** - This protocol extends ``RichTextReader`` with functionality - for reading rich text data for the current rich text. - - The protocol is implemented by `NSAttributedString` as well - as other types in the library. - */ +/// This protocol extends ``RichTextReader`` with functionality +/// for reading rich text data for the current rich text. +/// +/// The protocol is implemented by `NSAttributedString` as well +/// as other types in the library. public protocol RichTextDataReader: RichTextReader {} extension NSAttributedString: RichTextDataReader {} -public extension RichTextDataReader { +extension RichTextDataReader { /** Generate rich text data from the current rich text. @@ -26,7 +24,7 @@ public extension RichTextDataReader { - Parameters: - format: The data format to use. */ - func richTextData( + public func richTextData( for format: RichTextDataFormat ) throws -> Data { switch format { @@ -38,22 +36,22 @@ public extension RichTextDataReader { } } -private extension RichTextDataReader { +extension RichTextDataReader { /// The full text range. - var textRange: NSRange { + fileprivate var textRange: NSRange { NSRange(location: 0, length: richText.length) } /// The full text range. - func documentAttributes( + fileprivate func documentAttributes( for documentType: NSAttributedString.DocumentType ) -> [NSAttributedString.DocumentAttributeKey: Any] { [.documentType: documentType] } /// Generate archived formatted data. - func richTextArchivedData() throws -> Data { + fileprivate func richTextArchivedData() throws -> Data { try NSKeyedArchiver.archivedData( withRootObject: richText, requiringSecureCoding: false @@ -61,17 +59,18 @@ private extension RichTextDataReader { } /// Generate plain text formatted data. - func richTextPlainTextData() throws -> Data { + fileprivate func richTextPlainTextData() throws -> Data { let string = richText.string guard let data = string.data(using: .utf8) else { - throw RichTextDataError + throw + RichTextDataError .invalidData(in: string) } return data } /// Generate RTF formatted data. - func richTextRtfData() throws -> Data { + fileprivate func richTextRtfData() throws -> Data { try richText.data( from: textRange, documentAttributes: documentAttributes(for: .rtf) @@ -79,7 +78,7 @@ private extension RichTextDataReader { } /// Generate RTFD formatted data. - func richTextRtfdData() throws -> Data { + fileprivate func richTextRtfdData() throws -> Data { try richText.data( from: textRange, documentAttributes: documentAttributes(for: .rtfd) @@ -87,12 +86,12 @@ private extension RichTextDataReader { } #if macOS - /// Generate Word formatted data. - func richTextWordData() throws -> Data { - try richText.data( - from: textRange, - documentAttributes: documentAttributes(for: .docFormat) - ) - } + /// Generate Word formatted data. + func richTextWordData() throws -> Data { + try richText.data( + from: textRange, + documentAttributes: documentAttributes(for: .docFormat) + ) + } #endif } diff --git a/Sources/RichEditorSwiftUI/ExportData/UTType+RichText.swift b/Sources/RichEditorSwiftUI/ExportData/UTType+RichText.swift index 723ac19..6c80780 100644 --- a/Sources/RichEditorSwiftUI/ExportData/UTType+RichText.swift +++ b/Sources/RichEditorSwiftUI/ExportData/UTType+RichText.swift @@ -7,24 +7,24 @@ import UniformTypeIdentifiers -public extension UTType { +extension UTType { /// Uniform rich text types that RichTextKit supports. - static let richTextTypes: [UTType] = [ + public static let richTextTypes: [UTType] = [ .archivedData, .rtf, .text, .plainText, - .data + .data, ] /// The uniform type for ``RichTextDataFormat/archivedData``. - static let archivedData = UTType( + public static let archivedData = UTType( exportedAs: "com.richtextkit.archiveddata") } -public extension Collection where Element == UTType { +extension Collection where Element == UTType { /// The uniforum types that rich text documents support. - static var richTextTypes: [UTType] { UTType.richTextTypes } + public static var richTextTypes: [UTType] { UTType.richTextTypes } } diff --git a/Sources/RichEditorSwiftUI/Fonts/FontDescriptorRepresentable.swift b/Sources/RichEditorSwiftUI/Fonts/FontDescriptorRepresentable.swift index 59c5008..5cc104c 100644 --- a/Sources/RichEditorSwiftUI/Fonts/FontDescriptorRepresentable.swift +++ b/Sources/RichEditorSwiftUI/Fonts/FontDescriptorRepresentable.swift @@ -1,6 +1,6 @@ // // FontDescriptorRepresentable.swift -// +// // // Created by Divyesh Vekariya on 17/01/24. // @@ -8,51 +8,52 @@ import Foundation #if canImport(AppKit) && !targetEnvironment(macCatalyst) -import AppKit - -/** - This typealias bridges platform-specific font descriptors. - - The typealias also defines additional functionality as type - extensions for the platform-specific types. - */ -public typealias FontDescriptorRepresentable = NSFontDescriptor - -public extension FontDescriptorRepresentable { - - /// Get a new font descriptor by toggling a text style. - func byTogglingStyle(_ style: RichTextStyle) -> FontDescriptorRepresentable { - guard let traits = style.symbolicTraits else { return self } - if symbolicTraits.contains(traits) { - return withSymbolicTraits(symbolicTraits.subtracting(traits)) - } else { - return withSymbolicTraits(symbolicTraits.union(traits)) + import AppKit + + /// This typealias bridges platform-specific font descriptors. + /// + /// The typealias also defines additional functionality as type + /// extensions for the platform-specific types. + public typealias FontDescriptorRepresentable = NSFontDescriptor + + extension FontDescriptorRepresentable { + + /// Get a new font descriptor by toggling a text style. + public func byTogglingStyle(_ style: RichTextStyle) + -> FontDescriptorRepresentable + { + guard let traits = style.symbolicTraits else { return self } + if symbolicTraits.contains(traits) { + return withSymbolicTraits(symbolicTraits.subtracting(traits)) + } else { + return withSymbolicTraits(symbolicTraits.union(traits)) + } } } -} #endif #if canImport(UIKit) -import UIKit - -/** - This typealias bridges platform-specific font descriptors. - - The typealias also defines additional functionality as type - extensions for the platform-specific types. - */ -public typealias FontDescriptorRepresentable = UIFontDescriptor - -public extension FontDescriptorRepresentable { - - /// Get a new font descriptor by toggling a text style. - func byTogglingStyle(_ style: RichTextStyle) -> FontDescriptorRepresentable { - guard let traits = style.symbolicTraits else { return self } - if symbolicTraits.contains(traits) { - return withSymbolicTraits(symbolicTraits.subtracting(traits)) ?? self - } else { - return withSymbolicTraits(symbolicTraits.union(traits)) ?? self + import UIKit + + /// This typealias bridges platform-specific font descriptors. + /// + /// The typealias also defines additional functionality as type + /// extensions for the platform-specific types. + public typealias FontDescriptorRepresentable = UIFontDescriptor + + extension FontDescriptorRepresentable { + + /// Get a new font descriptor by toggling a text style. + public func byTogglingStyle(_ style: RichTextStyle) + -> FontDescriptorRepresentable + { + guard let traits = style.symbolicTraits else { return self } + if symbolicTraits.contains(traits) { + return withSymbolicTraits(symbolicTraits.subtracting(traits)) + ?? self + } else { + return withSymbolicTraits(symbolicTraits.union(traits)) ?? self + } } } -} #endif diff --git a/Sources/RichEditorSwiftUI/Fonts/FontRepresentable.swift b/Sources/RichEditorSwiftUI/Fonts/FontRepresentable.swift index 54cf0bc..e1bb1d6 100644 --- a/Sources/RichEditorSwiftUI/Fonts/FontRepresentable.swift +++ b/Sources/RichEditorSwiftUI/Fonts/FontRepresentable.swift @@ -6,37 +6,34 @@ // #if canImport(UIKit) -import UIKit + import UIKit -/** - This typealias bridges platform-specific fonts, to simplify - multi-platform support. - */ -public typealias FontRepresentable = UIFont + /// This typealias bridges platform-specific fonts, to simplify + /// multi-platform support. + public typealias FontRepresentable = UIFont #endif #if canImport(AppKit) && !targetEnvironment(macCatalyst) -import AppKit + import AppKit -/** - This typealias bridges platform-specific fonts, to simplify - multi-platform support. - */ -public typealias FontRepresentable = NSFont + /// This typealias bridges platform-specific fonts, to simplify + /// multi-platform support. + public typealias FontRepresentable = NSFont #endif -public extension FontRepresentable { - +extension FontRepresentable { + /** The standard font to use for rich text. - + You can change this value to affect all types that make use of the value. */ - static var standardRichTextFont = systemFont(ofSize: .standardRichTextFontSize) + public static var standardRichTextFont = systemFont( + ofSize: .standardRichTextFontSize) /// Create a new font by toggling a certain style. - func toggling( + public func toggling( _ style: RichTextStyle ) -> FontRepresentable? { .init( diff --git a/Sources/RichEditorSwiftUI/Fonts/FontTraitsRepresentable.swift b/Sources/RichEditorSwiftUI/Fonts/FontTraitsRepresentable.swift index dadf06a..3bfd9f7 100644 --- a/Sources/RichEditorSwiftUI/Fonts/FontTraitsRepresentable.swift +++ b/Sources/RichEditorSwiftUI/Fonts/FontTraitsRepresentable.swift @@ -1,45 +1,41 @@ // // FontTraitsRepresentable.swift -// +// // // Created by Divyesh Vekariya on 28/12/23. // #if canImport(AppKit) && !targetEnvironment(macCatalyst) -import AppKit + import AppKit -/** - This typealias bridges platform-specific symbolic traits to - simplify multi-platform support. - - The typealias also defines additional functionality as type - extensions for the platform-specific types. - */ -public typealias FontTraitsRepresentable = NSFontDescriptor.SymbolicTraits + /// This typealias bridges platform-specific symbolic traits to + /// simplify multi-platform support. + /// + /// The typealias also defines additional functionality as type + /// extensions for the platform-specific types. + public typealias FontTraitsRepresentable = NSFontDescriptor.SymbolicTraits #endif #if canImport(UIKit) -import UIKit + import UIKit -/** - This typealias bridges platform-specific symbolic traits to - simplify multi-platform support. - - The typealias also defines additional functionality as type - extensions for the platform-specific types. - */ -public typealias FontTraitsRepresentable = UIFontDescriptor.SymbolicTraits + /// This typealias bridges platform-specific symbolic traits to + /// simplify multi-platform support. + /// + /// The typealias also defines additional functionality as type + /// extensions for the platform-specific types. + public typealias FontTraitsRepresentable = UIFontDescriptor.SymbolicTraits #endif -public extension FontTraitsRepresentable { - +extension FontTraitsRepresentable { + /** Get the rich text styles that are enabled in the traits. - + Note that the traits only contain some of the available rich text styles. */ - var enabledRichTextStyles: [RichTextStyle] { + public var enabledRichTextStyles: [RichTextStyle] { RichTextStyle.allCases.filter { guard let trait = $0.symbolicTraits else { return false } return contains(trait) diff --git a/Sources/RichEditorSwiftUI/Fonts/RichTextFont+ListPicker.swift b/Sources/RichEditorSwiftUI/Fonts/RichTextFont+ListPicker.swift index 6127374..3699aae 100644 --- a/Sources/RichEditorSwiftUI/Fonts/RichTextFont+ListPicker.swift +++ b/Sources/RichEditorSwiftUI/Fonts/RichTextFont+ListPicker.swift @@ -7,7 +7,7 @@ import SwiftUI -public extension RichTextFont { +extension RichTextFont { /** This view uses a `List` to list a set of fonts of which @@ -28,7 +28,7 @@ public extension RichTextFont { .richTextFontPickerConfig(...) ``` */ - struct ListPicker: View { + public struct ListPicker: View { /** Create a font list picker. diff --git a/Sources/RichEditorSwiftUI/Fonts/RichTextFont+Picker.swift b/Sources/RichEditorSwiftUI/Fonts/RichTextFont+Picker.swift index 99c51cf..d6ef3fc 100644 --- a/Sources/RichEditorSwiftUI/Fonts/RichTextFont+Picker.swift +++ b/Sources/RichEditorSwiftUI/Fonts/RichTextFont+Picker.swift @@ -7,7 +7,7 @@ import SwiftUI -public extension RichTextFont { +extension RichTextFont { /** This font picker can be used to pick a font from a list, @@ -31,7 +31,7 @@ public extension RichTextFont { Note that this picker will not apply all configurations. */ - struct Picker: View { + public struct Picker: View { /** Create a font picker. @@ -76,32 +76,38 @@ public extension RichTextFont { } } -private extension RichTextFont.PickerFont { +extension RichTextFont.PickerFont { /** A system font has a font name that may be resolved to a different name when picked. We must thus try to pattern match, using the currently selected font name. */ - func matches(_ fontName: String) -> Bool { + fileprivate func matches(_ fontName: String) -> Bool { let compare = fontName.lowercased() let fontName = self.fontName.lowercased() if fontName == compare { return true } - if compare.hasPrefix(fontName.replacingOccurrences(of: " ", with: "")) { return true } - if compare.hasPrefix(fontName.replacingOccurrences(of: " ", with: "-")) { return true } + if compare.hasPrefix(fontName.replacingOccurrences(of: " ", with: "")) { + return true + } + if compare.hasPrefix(fontName.replacingOccurrences(of: " ", with: "-")) + { + return true + } return false } /** Use the selected font name as tag for the selected font. */ - func tag(for selectedFont: Self?, selectedName: String) -> String { + fileprivate func tag(for selectedFont: Self?, selectedName: String) + -> String + { let isSelected = fontName == selectedFont?.fontName return isSelected ? selectedName : fontName } } - //extension View { // // func withPreviewPickerStyles() -> some View { @@ -114,7 +120,7 @@ private extension RichTextFont.PickerFont { // self.label("Default") // self.pickerStyle(.automatic).label(".automatic") // self.pickerStyle(.inline).label(".inline") -// #if iOS || macOS +// #if os(iOS) || macOS // self.pickerStyle(.menu).label(".menu") // #endif // #if iOS @@ -122,12 +128,12 @@ private extension RichTextFont.PickerFont { // pickerStyle(.navigationLink).label(".navigationLink") // } // #endif -// #if iOS || macOS -// if #available(iOS 17.0, macOS 14.0, watchOS 10.0, *) { +// #if os(iOS) || macOS +// if #available(iOS 17.0, os(macOS) 14.0, watchOS 10.0, *) { // pickerStyle(.palette).label(".palette") // } // #endif -// #if iOS || macOS || os(tvOS) || os(visionOS) +// #if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) // self.pickerStyle(.segmented).label(".segmented") // #endif // #if iOS diff --git a/Sources/RichEditorSwiftUI/Fonts/RichTextFont+PickerConfig.swift b/Sources/RichEditorSwiftUI/Fonts/RichTextFont+PickerConfig.swift index 49af262..90da381 100644 --- a/Sources/RichEditorSwiftUI/Fonts/RichTextFont+PickerConfig.swift +++ b/Sources/RichEditorSwiftUI/Fonts/RichTextFont+PickerConfig.swift @@ -7,14 +7,14 @@ import SwiftUI -public extension RichTextFont { +extension RichTextFont { /// This type can configure a ``RichTextFont/Picker``. /// /// This configuration contains configuration properties /// for many different font pickers types. Some of these /// properties are not used in some pickers. - struct PickerConfig { + public struct PickerConfig { /// Create a custom font picker config. /// @@ -52,16 +52,16 @@ public extension RichTextFont { } } -public extension RichTextFont.PickerConfig { +extension RichTextFont.PickerConfig { /// The standard font picker configuration. - static var standard: Self { .init() } + public static var standard: Self { .init() } } -public extension RichTextFont.PickerConfig { +extension RichTextFont.PickerConfig { /// The fonts to list for a given selection. - func fontsToList(for selection: FontName) -> [Font] { + public func fontsToList(for selection: FontName) -> [Font] { if moveSelectionTopmost { return fonts.moveTopmost(selection) } else { @@ -70,19 +70,19 @@ public extension RichTextFont.PickerConfig { } } -public extension View { +extension View { /// Apply a ``RichTextFont`` picker configuration. - func richTextFontPickerConfig( + public func richTextFontPickerConfig( _ config: RichTextFont.PickerConfig ) -> some View { self.environment(\.richTextFontPickerConfig, config) } } -private extension RichTextFont.PickerConfig { +extension RichTextFont.PickerConfig { - struct Key: EnvironmentKey { + fileprivate struct Key: EnvironmentKey { public static var defaultValue: RichTextFont.PickerConfig { .standard @@ -90,11 +90,11 @@ private extension RichTextFont.PickerConfig { } } -public extension EnvironmentValues { +extension EnvironmentValues { /// This value can bind to a font picker config. - var richTextFontPickerConfig: RichTextFont.PickerConfig { - get { self [RichTextFont.PickerConfig.Key.self] } - set { self [RichTextFont.PickerConfig.Key.self] = newValue } + public var richTextFontPickerConfig: RichTextFont.PickerConfig { + get { self[RichTextFont.PickerConfig.Key.self] } + set { self[RichTextFont.PickerConfig.Key.self] = newValue } } } diff --git a/Sources/RichEditorSwiftUI/Fonts/RichTextFont+SizePicker.swift b/Sources/RichEditorSwiftUI/Fonts/RichTextFont+SizePicker.swift index bb2c122..b36f70d 100644 --- a/Sources/RichEditorSwiftUI/Fonts/RichTextFont+SizePicker.swift +++ b/Sources/RichEditorSwiftUI/Fonts/RichTextFont+SizePicker.swift @@ -7,7 +7,7 @@ import SwiftUI -public extension RichTextFont { +extension RichTextFont { /** This picker can be used to pick a font size. @@ -26,7 +26,7 @@ public extension RichTextFont { .richTextFontSizePickerConfig(...) ``` */ - struct SizePicker: View { + public struct SizePicker: View { /** Create a font size picker. @@ -48,10 +48,12 @@ public extension RichTextFont { public var body: some View { SwiftUI.Picker("", selection: $selection) { - ForEach(values( - for: config.values, - selection: selection - ), id: \.self) { + ForEach( + values( + for: config.values, + selection: selection + ), id: \.self + ) { text(for: $0) .tag($0) } @@ -60,10 +62,10 @@ public extension RichTextFont { } } -public extension RichTextFont.SizePicker { +extension RichTextFont.SizePicker { /// Get a list of values for a certain selection. - func values( + public func values( for values: [CGFloat], selection: CGFloat ) -> [CGFloat] { @@ -72,13 +74,12 @@ public extension RichTextFont.SizePicker { } } -private extension RichTextFont.SizePicker { +extension RichTextFont.SizePicker { - func text( + fileprivate func text( for fontSize: CGFloat ) -> some View { Text("\(Int(fontSize))") .fixedSize(horizontal: true, vertical: false) } } - diff --git a/Sources/RichEditorSwiftUI/Fonts/RichTextFont+SizePickerConfig.swift b/Sources/RichEditorSwiftUI/Fonts/RichTextFont+SizePickerConfig.swift index 311d40b..4ee2f29 100644 --- a/Sources/RichEditorSwiftUI/Fonts/RichTextFont+SizePickerConfig.swift +++ b/Sources/RichEditorSwiftUI/Fonts/RichTextFont+SizePickerConfig.swift @@ -7,17 +7,19 @@ import SwiftUI -public extension RichTextFont { +extension RichTextFont { /// This type can configure a ``RichTextFont/SizePicker``. - struct SizePickerConfig { + public struct SizePickerConfig { /// Create a custom font size picker config. /// /// - Parameters: /// - values: The values to display in the list, by default a standard list. public init( - values: [CGFloat] = [10, 12, 14, 16, 18, 20, 22, 24, 28, 36, 48, 64, 72, 96, 144] + values: [CGFloat] = [ + 10, 12, 14, 16, 18, 20, 22, 24, 28, 36, 48, 64, 72, 96, 144, + ] ) { self.values = values } @@ -27,37 +29,38 @@ public extension RichTextFont { } } -public extension RichTextFont.SizePickerConfig { +extension RichTextFont.SizePickerConfig { /// The standard font size picker configuration. /// /// You can set a new value to change the global default. - static var standard = Self() + public static var standard = Self() } -public extension View { +extension View { /// Apply a ``RichTextFont`` size picker configuration. - func richTextFontSizePickerConfig( + public func richTextFontSizePickerConfig( _ config: RichTextFont.SizePickerConfig ) -> some View { self.environment(\.richTextFontSizePickerConfig, config) } } -private extension RichTextFont.SizePickerConfig { +extension RichTextFont.SizePickerConfig { - struct Key: EnvironmentKey { + fileprivate struct Key: EnvironmentKey { - public static var defaultValue: RichTextFont.SizePickerConfig = .standard + public static var defaultValue: RichTextFont.SizePickerConfig = + .standard } } -public extension EnvironmentValues { +extension EnvironmentValues { /// This value can bind to a font size picker config. - var richTextFontSizePickerConfig: RichTextFont.SizePickerConfig { - get { self [RichTextFont.SizePickerConfig.Key.self] } - set { self [RichTextFont.SizePickerConfig.Key.self] = newValue } + public var richTextFontSizePickerConfig: RichTextFont.SizePickerConfig { + get { self[RichTextFont.SizePickerConfig.Key.self] } + set { self[RichTextFont.SizePickerConfig.Key.self] = newValue } } } diff --git a/Sources/RichEditorSwiftUI/Fonts/RichTextFontPickerFont.swift b/Sources/RichEditorSwiftUI/Fonts/RichTextFontPickerFont.swift index 1f6fc9a..98219d7 100644 --- a/Sources/RichEditorSwiftUI/Fonts/RichTextFontPickerFont.swift +++ b/Sources/RichEditorSwiftUI/Fonts/RichTextFontPickerFont.swift @@ -8,14 +8,14 @@ import Foundation #if canImport(AppKit) && !targetEnvironment(macCatalyst) -import AppKit + import AppKit #endif #if canImport(UIKit) -import UIKit + import UIKit #endif -public extension RichTextFont { +extension RichTextFont { /** This struct defines picker-specific fonts that are used @@ -34,7 +34,7 @@ public extension RichTextFont { to another value. To edit how fonts are detected by the system, use the ``systemFontNamePrefix``. */ - struct PickerFont: Identifiable, Equatable { + public struct PickerFont: Identifiable, Equatable { public init( fontName: String @@ -56,10 +56,10 @@ public extension RichTextFont { // MARK: - Static Properties -public extension RichTextFont.PickerFont { +extension RichTextFont.PickerFont { /// Get all available system fonts. - static var all: [Self] { + public static var all: [Self] { let all = systemFonts let system = Self.init( fontName: Self.systemFontNamePrefix @@ -70,37 +70,37 @@ public extension RichTextFont.PickerFont { } /// The display name for the standard system font. - static var standardSystemFontDisplayName: String { -#if macOS - return "Standard" -#else - return "San Francisco" -#endif + public static var standardSystemFontDisplayName: String { + #if macOS + return "Standard" + #else + return "San Francisco" + #endif } /// The font name prefix for the standard system font. - static var systemFontNamePrefix: String { -#if macOS - return ".AppleSystemUIFont" -#else - return ".SFUI" -#endif + public static var systemFontNamePrefix: String { + #if macOS + return ".AppleSystemUIFont" + #else + return ".SFUI" + #endif } } // MARK: - Public Properties -public extension RichTextFont.PickerFont { +extension RichTextFont.PickerFont { /// Get the font display name. - var displayName: String { + public var displayName: String { let isSystemFont = isStandardSystemFont let systemName = Self.standardSystemFontDisplayName return isSystemFont ? systemName : fontName } /// Check if the a font name represents the system font. - var isStandardSystemFont: Bool { + public var isStandardSystemFont: Bool { let name = fontName.trimmingCharacters(in: .whitespaces) let prefix = Self.systemFontNamePrefix return name.hasPrefix(prefix) @@ -109,19 +109,23 @@ public extension RichTextFont.PickerFont { // MARK: - Collection Extensions -public extension Collection where Element == RichTextFont.PickerFont { +extension Collection where Element == RichTextFont.PickerFont { /// Get all available system fonts. - static var all: [Element] { + public static var all: [Element] { Element.all } /// Move a certain font topmost in the list. - func moveTopmost(_ topmost: String) -> [Element] { + public func moveTopmost(_ topmost: String) -> [Element] { let topmost = topmost.trimmingCharacters(in: .whitespaces) - let exists = contains { $0.fontName.lowercased() == topmost.lowercased() } + let exists = contains { + $0.fontName.lowercased() == topmost.lowercased() + } guard exists else { return Array(self) } - var filtered = filter { $0.fontName.lowercased() != topmost.lowercased() } + var filtered = filter { + $0.fontName.lowercased() != topmost.lowercased() + } let new = Element(fontName: topmost) filtered.insert(new, at: 0) return filtered @@ -130,26 +134,25 @@ public extension Collection where Element == RichTextFont.PickerFont { // MARK: - System Fonts -private extension RichTextFont.PickerFont { +extension RichTextFont.PickerFont { /** Get all available font picker fonts. */ - static var systemFonts: [RichTextFont.PickerFont] { -#if canImport(AppKit) && !targetEnvironment(macCatalyst) - return NSFontManager.shared - .availableFontFamilies - .map { - RichTextFont.PickerFont(fontName: $0) - } -#endif - -#if canImport(UIKit) - return UIFont.familyNames - .map { - RichTextFont.PickerFont(fontName: $0) - } -#endif + fileprivate static var systemFonts: [RichTextFont.PickerFont] { + #if canImport(AppKit) && !targetEnvironment(macCatalyst) + return NSFontManager.shared + .availableFontFamilies + .map { + RichTextFont.PickerFont(fontName: $0) + } + #endif + + #if canImport(UIKit) + return UIFont.familyNames + .map { + RichTextFont.PickerFont(fontName: $0) + } + #endif } } - diff --git a/Sources/RichEditorSwiftUI/Fonts/RichTextFontSizePickerStack.swift b/Sources/RichEditorSwiftUI/Fonts/RichTextFontSizePickerStack.swift index 3a2af5d..a68800f 100644 --- a/Sources/RichEditorSwiftUI/Fonts/RichTextFontSizePickerStack.swift +++ b/Sources/RichEditorSwiftUI/Fonts/RichTextFontSizePickerStack.swift @@ -5,12 +5,12 @@ // Created by Divyesh Vekariya on 29/10/24. // -#if iOS || macOS || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(visionOS) + import SwiftUI -public extension RichTextFont { + extension RichTextFont { - /** + /** This view uses a ``RichTextFont/SizePicker`` and button steppers to increment and a decrement the font size. @@ -25,87 +25,87 @@ public extension RichTextFont { .richTextFontSizePickerConfig(...) ``` */ - struct SizePickerStack: View { + public struct SizePickerStack: View { - /** + /** Create a rich text font size picker stack. - Parameters: - context: The context to affect. */ - public init( - context: RichEditorState - ) { - self._context = ObservedObject(wrappedValue: context) - } - - private let step = 1 - - @ObservedObject - private var context: RichEditorState + public init( + context: RichEditorState + ) { + self._context = ObservedObject(wrappedValue: context) + } - public var body: some View { -#if iOS || os(visionOS) - stack - .fixedSize(horizontal: false, vertical: true) -#else - HStack(spacing: 3) { - picker - stepper + private let step = 1 + + @ObservedObject + private var context: RichEditorState + + public var body: some View { + #if os(iOS) || os(visionOS) + stack + .fixedSize(horizontal: false, vertical: true) + #else + HStack(spacing: 3) { + picker + stepper + } + .overlay(macShortcutOverlay) + #endif } - .overlay(macShortcutOverlay) -#endif } } -} -private extension RichTextFont.SizePickerStack { + extension RichTextFont.SizePickerStack { - var macShortcutOverlay: some View { - stack - .opacity(0) - .allowsHitTesting(false) - } + fileprivate var macShortcutOverlay: some View { + stack + .opacity(0) + .allowsHitTesting(false) + } - var stack: some View { - HStack(spacing: 2) { - stepButton(-step) - picker - stepButton(step) + fileprivate var stack: some View { + HStack(spacing: 2) { + stepButton(-step) + picker + stepButton(step) + } } - } - func stepButton(_ points: Int) -> some View { - RichTextAction.Button( - action: .stepFontSize(points: points), - context: context, - fillVertically: true - ) - } + fileprivate func stepButton(_ points: Int) -> some View { + RichTextAction.Button( + action: .stepFontSize(points: points), + context: context, + fillVertically: true + ) + } - var picker: some View { - RichTextFont.SizePicker( - selection: $context.fontSize - ) - .onChangeBackPort(of: context.fontSize) { newValue in - context.updateStyle(style: .size(Int(context.fontSize))) + fileprivate var picker: some View { + RichTextFont.SizePicker( + selection: $context.fontSize + ) + .onChangeBackPort(of: context.fontSize) { newValue in + context.updateStyle(style: .size(Int(context.fontSize))) + } } - } - var stepper: some View { - Stepper( - "", - onIncrement: increment, - onDecrement: decrement - ) - } + fileprivate var stepper: some View { + Stepper( + "", + onIncrement: increment, + onDecrement: decrement + ) + } - func decrement() { - context.fontSize -= CGFloat(step) - } + fileprivate func decrement() { + context.fontSize -= CGFloat(step) + } - func increment() { - context.fontSize += CGFloat(step) + fileprivate func increment() { + context.fontSize += CGFloat(step) + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/Fonts/RichTextViewComponent+Font.swift b/Sources/RichEditorSwiftUI/Fonts/RichTextViewComponent+Font.swift index 9315604..f63ab3a 100644 --- a/Sources/RichEditorSwiftUI/Fonts/RichTextViewComponent+Font.swift +++ b/Sources/RichEditorSwiftUI/Fonts/RichTextViewComponent+Font.swift @@ -24,20 +24,21 @@ import CoreGraphics import Foundation -public extension RichTextViewComponent { +extension RichTextViewComponent { /// Get the rich text font at current range. - var richTextFont: FontRepresentable? { - richTextAttributes[.font] as? FontRepresentable ?? typingAttributes[.font] as? FontRepresentable + public var richTextFont: FontRepresentable? { + richTextAttributes[.font] as? FontRepresentable ?? typingAttributes[ + .font] as? FontRepresentable } /// Set the rich text font at current range. - func setRichTextFont(_ font: FontRepresentable) { + public func setRichTextFont(_ font: FontRepresentable) { setRichTextAttribute(.font, to: font) } /// Set the rich text font name at current range. - func setRichTextFontName(_ name: String) { + public func setRichTextFontName(_ name: String) { if richTextFont?.fontName == name { return } if hasSelectedRange { setFontName(name, at: selectedRange) @@ -47,34 +48,35 @@ public extension RichTextViewComponent { } /// Set the rich text font size at current range. - func setRichTextFontSize(_ size: CGFloat) { + public func setRichTextFontSize(_ size: CGFloat) { if size == richTextFont?.pointSize { return } -#if macOS - setFontSize(size, at: selectedRange) - setFontSizeAtCurrentPosition(size) -#else - if hasSelectedRange { + #if macOS setFontSize(size, at: selectedRange) - } else { setFontSizeAtCurrentPosition(size) - } -#endif + #else + if hasSelectedRange { + setFontSize(size, at: selectedRange) + } else { + setFontSizeAtCurrentPosition(size) + } + #endif } /// Step the rich text font size at current range. - func stepRichTextFontSize(points: Int) { + public func stepRichTextFontSize(points: Int) { let old = richTextFont?.pointSize ?? .standardRichTextFontSize let new = max(0, old + CGFloat(points)) setRichTextFontSize(new) } } -private extension RichTextViewComponent { +extension RichTextViewComponent { /// Set the font at the current position. - func setFontNameAtCurrentPosition(to name: String) { + fileprivate func setFontNameAtCurrentPosition(to name: String) { var attributes = typingAttributes - let oldFont = attributes[.font] as? FontRepresentable ?? .standardRichTextFont + let oldFont = + attributes[.font] as? FontRepresentable ?? .standardRichTextFont let size = oldFont.pointSize let newFont = FontRepresentable(name: name, size: size) attributes[.font] = newFont @@ -82,24 +84,28 @@ private extension RichTextViewComponent { } /// Set the font size at the current position. - func setFontSizeAtCurrentPosition(_ size: CGFloat) { + fileprivate func setFontSizeAtCurrentPosition(_ size: CGFloat) { var attributes = typingAttributes - let oldFont = attributes[.font] as? FontRepresentable ?? .standardRichTextFont + let oldFont = + attributes[.font] as? FontRepresentable ?? .standardRichTextFont let newFont = oldFont.withSize(size) attributes[.font] = newFont typingAttributes = attributes } /// Set the font name at a certain range. - func setFontName(_ name: String, at range: NSRange) { + fileprivate func setFontName(_ name: String, at range: NSRange) { guard let text = mutableRichText else { return } guard text.length > 0 else { return } let fontName = settableFontName(for: name) text.beginEditing() - text.enumerateAttribute(.font, in: range, options: .init()) { value, range, _ in + text.enumerateAttribute(.font, in: range, options: .init()) { + value, range, _ in let oldFont = value as? FontRepresentable ?? .standardRichTextFont let size = oldFont.pointSize - let newFont = FontRepresentable(name: fontName, size: size) ?? .standardRichTextFont + let newFont = + FontRepresentable(name: fontName, size: size) + ?? .standardRichTextFont text.removeAttribute(.font, range: range) text.addAttribute(.font, value: newFont, range: range) text.fixAttributes(in: range) @@ -108,11 +114,12 @@ private extension RichTextViewComponent { } /// Set the font size at a certain range. - func setFontSize(_ size: CGFloat, at range: NSRange) { + fileprivate func setFontSize(_ size: CGFloat, at range: NSRange) { guard let text = mutableRichText else { return } guard text.length > 0 else { return } text.beginEditing() - text.enumerateAttribute(.font, in: range, options: .init()) { value, range, _ in + text.enumerateAttribute(.font, in: range, options: .init()) { + value, range, _ in let oldFont = value as? FontRepresentable ?? .standardRichTextFont let newFont = oldFont.withSize(size) text.removeAttribute(.font, range: range) @@ -123,14 +130,14 @@ private extension RichTextViewComponent { } } -private extension RichTextAttributeWriter { +extension RichTextAttributeWriter { /// We must adjust empty font names on some platforms. - func settableFontName(for fontName: String) -> String { -#if macOS - fontName -#else - fontName -#endif + fileprivate func settableFontName(for fontName: String) -> String { + #if macOS + fontName + #else + fontName + #endif } } diff --git a/Sources/RichEditorSwiftUI/Fonts/StandardFontSizeProvider.swift b/Sources/RichEditorSwiftUI/Fonts/StandardFontSizeProvider.swift index 48bde1f..72a5699 100644 --- a/Sources/RichEditorSwiftUI/Fonts/StandardFontSizeProvider.swift +++ b/Sources/RichEditorSwiftUI/Fonts/StandardFontSizeProvider.swift @@ -1,6 +1,6 @@ // // StandardFontSizeProvider.swift -// +// // // Created by Divyesh Vekariya on 12/01/24. // @@ -15,27 +15,29 @@ extension Double: StandardFontSizeProvider {} extension RichEditorState: StandardFontSizeProvider {} -#if iOS || macOS || os(tvOS) -extension RichTextEditor: StandardFontSizeProvider {} +#if os(iOS) || os(macOS) || os(tvOS) + extension RichTextEditor: StandardFontSizeProvider {} -extension RichTextView: StandardFontSizeProvider {} + extension RichTextView: StandardFontSizeProvider {} #endif -public extension StandardFontSizeProvider { - +extension StandardFontSizeProvider { + /** The standard font size to use for rich text. - + You can change this value to affect all types that make use of this value. */ - static var standardRichTextFontSize: CGFloat { + public static var standardRichTextFontSize: CGFloat { get { StandardFontSizeProviderStorage.standardRichTextFontSize } - set { StandardFontSizeProviderStorage.standardRichTextFontSize = newValue } + set { + StandardFontSizeProviderStorage.standardRichTextFontSize = newValue + } } } private class StandardFontSizeProviderStorage { - + static var standardRichTextFontSize: CGFloat = 16 } diff --git a/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sheet.swift b/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sheet.swift index 9fb4162..b9d44cc 100644 --- a/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sheet.swift +++ b/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sheet.swift @@ -5,12 +5,12 @@ // Created by Divyesh Vekariya on 18/11/24. // -#if iOS || macOS || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(visionOS) + import SwiftUI -public extension RichTextFormat { + extension RichTextFormat { - /** + /** This sheet contains a font picker and a bottom toolbar. You can configure and style the view by applying config @@ -24,119 +24,119 @@ public extension RichTextFormat { .richTextFormatSheetConfig(...) ``` */ - struct Sheet: RichTextFormatToolbarBase { + public struct Sheet: RichTextFormatToolbarBase { - /** + /** Create a rich text format sheet. - Parameters: - context: The context to apply changes to. */ - public init( - context: RichEditorState - ) { - self._context = ObservedObject(wrappedValue: context) - } + public init( + context: RichEditorState + ) { + self._context = ObservedObject(wrappedValue: context) + } - public typealias Config = RichTextFormat.ToolbarConfig - public typealias Style = RichTextFormat.ToolbarStyle + public typealias Config = RichTextFormat.ToolbarConfig + public typealias Style = RichTextFormat.ToolbarStyle - @ObservedObject - private var context: RichEditorState + @ObservedObject + private var context: RichEditorState - @Environment(\.richTextFormatSheetConfig) - var config + @Environment(\.richTextFormatSheetConfig) + var config - @Environment(\.richTextFormatSheetStyle) - var style + @Environment(\.richTextFormatSheetStyle) + var style - @Environment(\.dismiss) - private var dismiss + @Environment(\.dismiss) + private var dismiss - @Environment(\.horizontalSizeClass) - private var horizontalSizeClass + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass - public var body: some View { - NavigationView { - VStack(spacing: 0) { - RichTextFont.ListPicker( - selection: $context.fontName - ) - Divider() - RichTextFormat.Toolbar( - context: context - ) - .richTextFormatToolbarConfig(config) - } - .padding(.top, -35) - .toolbar { - ToolbarItem(placement: .confirmationAction) { - Button(RTEL10n.done.text) { - dismiss() + public var body: some View { + NavigationView { + VStack(spacing: 0) { + RichTextFont.ListPicker( + selection: $context.fontName + ) + Divider() + RichTextFormat.Toolbar( + context: context + ) + .richTextFormatToolbarConfig(config) + } + .padding(.top, -35) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button(RTEL10n.done.text) { + dismiss() + } } } + .navigationTitle("") + #if iOS + .navigationBarTitleDisplayMode(.inline) + #endif } - .navigationTitle("") #if iOS - .navigationBarTitleDisplayMode(.inline) + .navigationViewStyle(.stack) #endif } - #if iOS - .navigationViewStyle(.stack) - #endif } } -} -public extension View { + extension View { - /// Apply a rich text format sheet config. - func richTextFormatSheetConfig( - _ value: RichTextFormat.Sheet.Config - ) -> some View { - self.environment(\.richTextFormatSheetConfig, value) - } + /// Apply a rich text format sheet config. + public func richTextFormatSheetConfig( + _ value: RichTextFormat.Sheet.Config + ) -> some View { + self.environment(\.richTextFormatSheetConfig, value) + } - /// Apply a rich text format sheet style. - func richTextFormatSheetStyle( - _ value: RichTextFormat.Sheet.Style - ) -> some View { - self.environment(\.richTextFormatSheetStyle, value) + /// Apply a rich text format sheet style. + public func richTextFormatSheetStyle( + _ value: RichTextFormat.Sheet.Style + ) -> some View { + self.environment(\.richTextFormatSheetStyle, value) + } } -} -private extension RichTextFormat.Sheet.Config { + extension RichTextFormat.Sheet.Config { - struct Key: EnvironmentKey { + fileprivate struct Key: EnvironmentKey { - static var defaultValue: RichTextFormat.Sheet.Config { - .standard + static var defaultValue: RichTextFormat.Sheet.Config { + .standard + } } } -} -private extension RichTextFormat.Sheet.Style { + extension RichTextFormat.Sheet.Style { - struct Key: EnvironmentKey { + fileprivate struct Key: EnvironmentKey { - static var defaultValue: RichTextFormat.Sheet.Style { - .standard + static var defaultValue: RichTextFormat.Sheet.Style { + .standard + } } } -} -public extension EnvironmentValues { + extension EnvironmentValues { - /// This value can bind to a format sheet config. - var richTextFormatSheetConfig: RichTextFormat.Sheet.Config { - get { self [RichTextFormat.Sheet.Config.Key.self] } - set { self [RichTextFormat.Sheet.Config.Key.self] = newValue } - } + /// This value can bind to a format sheet config. + public var richTextFormatSheetConfig: RichTextFormat.Sheet.Config { + get { self[RichTextFormat.Sheet.Config.Key.self] } + set { self[RichTextFormat.Sheet.Config.Key.self] = newValue } + } - /// This value can bind to a format sheet style. - var richTextFormatSheetStyle: RichTextFormat.Sheet.Style { - get { self [RichTextFormat.Sheet.Style.Key.self] } - set { self [RichTextFormat.Sheet.Style.Key.self] = newValue } + /// This value can bind to a format sheet style. + public var richTextFormatSheetStyle: RichTextFormat.Sheet.Style { + get { self[RichTextFormat.Sheet.Style.Key.self] } + set { self[RichTextFormat.Sheet.Style.Key.self] = newValue } + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sidebar.swift b/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sidebar.swift index 610cbd8..cf6c454 100644 --- a/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sidebar.swift +++ b/Sources/RichEditorSwiftUI/Format/RichTextFormat+Sidebar.swift @@ -5,12 +5,12 @@ // Created by Divyesh Vekariya on 18/11/24. // -#if iOS || macOS || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(visionOS) + import SwiftUI -public extension RichTextFormat { + extension RichTextFormat { - /** + /** This sidebar view provides various text format options, and is meant to be used on macOS, in a trailing sidebar. @@ -29,146 +29,150 @@ public extension RichTextFormat { should also be made to look good on iPadOS in landscape, to let us use it instead of the ``RichTextFormat/Sheet``. */ - struct Sidebar: RichTextFormatToolbarBase { + public struct Sidebar: RichTextFormatToolbarBase { - /** + /** Create a rich text format sheet. - Parameters: - context: The context to apply changes to. */ - public init( - context: RichEditorState - ) { - self._context = ObservedObject(wrappedValue: context) - } + public init( + context: RichEditorState + ) { + self._context = ObservedObject(wrappedValue: context) + } - public typealias Config = RichTextFormat.ToolbarConfig - public typealias Style = RichTextFormat.ToolbarStyle + public typealias Config = RichTextFormat.ToolbarConfig + public typealias Style = RichTextFormat.ToolbarStyle - @ObservedObject - private var context: RichEditorState + @ObservedObject + private var context: RichEditorState - @Environment(\.richTextFormatSidebarConfig) - var config + @Environment(\.richTextFormatSidebarConfig) + var config - @Environment(\.richTextFormatSidebarStyle) - var style + @Environment(\.richTextFormatSidebarStyle) + var style - public var body: some View { - VStack(alignment: .leading, spacing: style.spacing) { - SidebarSection { - fontPicker(value: $context.fontName) - .onChangeBackPort(of: context.fontName) { newValue in - context.updateStyle(style: .font(newValue)) + public var body: some View { + VStack(alignment: .leading, spacing: style.spacing) { + SidebarSection { + fontPicker(value: $context.fontName) + .onChangeBackPort(of: context.fontName) { + newValue in + context.updateStyle(style: .font(newValue)) + } + HStack { + styleToggleGroup(for: context) + Spacer() + fontSizePicker(for: context) } - HStack { - styleToggleGroup(for: context) - Spacer() - fontSizePicker(for: context) - } - headerPicker(context: context) - } - - Divider() + otherMenuToggleGroup(for: context) - SidebarSection { - alignmentPicker(context: context) - .onChangeBackPort(of: context.textAlignment) { newValue in - context.updateStyle(style: .align(newValue)) - } -// HStack { -// lineSpacingPicker(for: context) -// } -// HStack { -// indentButtons(for: context, greedy: true) -// superscriptButtons(for: context, greedy: true) -// } - } + headerPicker(context: context) + } - Divider() + Divider() - if hasColorPickers { SidebarSection { - colorPickers(for: context) + alignmentPicker(context: context) + .onChangeBackPort(of: context.textAlignment) { + newValue in + context.updateStyle(style: .align(newValue)) + } + // HStack { + // lineSpacingPicker(for: context) + // } + // HStack { + // indentButtons(for: context, greedy: true) + // superscriptButtons(for: context, greedy: true) + // } } - .padding(.trailing, -8) + Divider() - } - Spacer() + if hasColorPickers { + SidebarSection { + colorPickers(for: context) + } + .padding(.trailing, -8) + Divider() + } + + Spacer() + } + .labelsHidden() + .padding(style.padding - 2) + .background(Color.white.opacity(0.05)) } - .labelsHidden() - .padding(style.padding - 2) - .background(Color.white.opacity(0.05)) } } -} -private struct SidebarSection: View { + private struct SidebarSection: View { - @ViewBuilder - let content: () -> Content + @ViewBuilder + let content: () -> Content - @Environment(\.richTextFormatToolbarStyle) - var style + @Environment(\.richTextFormatToolbarStyle) + var style - var body: some View { - VStack(alignment: .leading, spacing: style.spacing) { - content() + var body: some View { + VStack(alignment: .leading, spacing: style.spacing) { + content() + } } } -} -public extension View { + extension View { - /// Apply a rich text format sidebar config. - func richTextFormatSidebarConfig( - _ value: RichTextFormat.Sidebar.Config - ) -> some View { - self.environment(\.richTextFormatSidebarConfig, value) - } + /// Apply a rich text format sidebar config. + public func richTextFormatSidebarConfig( + _ value: RichTextFormat.Sidebar.Config + ) -> some View { + self.environment(\.richTextFormatSidebarConfig, value) + } - /// Apply a rich text format sidebar style. - func richTextFormatSidebarStyle( - _ value: RichTextFormat.Sidebar.Style - ) -> some View { - self.environment(\.richTextFormatSidebarStyle, value) + /// Apply a rich text format sidebar style. + public func richTextFormatSidebarStyle( + _ value: RichTextFormat.Sidebar.Style + ) -> some View { + self.environment(\.richTextFormatSidebarStyle, value) + } } -} -private extension RichTextFormat.Sidebar.Config { + extension RichTextFormat.Sidebar.Config { - struct Key: EnvironmentKey { + fileprivate struct Key: EnvironmentKey { - static var defaultValue: RichTextFormat.Sidebar.Config { - .standard + static var defaultValue: RichTextFormat.Sidebar.Config { + .standard + } } } -} -private extension RichTextFormat.Sidebar.Style { + extension RichTextFormat.Sidebar.Style { - struct Key: EnvironmentKey { + fileprivate struct Key: EnvironmentKey { - static var defaultValue: RichTextFormat.Sidebar.Style { - .standard + static var defaultValue: RichTextFormat.Sidebar.Style { + .standard + } } } -} -public extension EnvironmentValues { + extension EnvironmentValues { - /// This value can bind to a format sidebar config. - var richTextFormatSidebarConfig: RichTextFormat.Sidebar.Config { - get { self [RichTextFormat.Sidebar.Config.Key.self] } - set { self [RichTextFormat.Sidebar.Config.Key.self] = newValue } - } + /// This value can bind to a format sidebar config. + public var richTextFormatSidebarConfig: RichTextFormat.Sidebar.Config { + get { self[RichTextFormat.Sidebar.Config.Key.self] } + set { self[RichTextFormat.Sidebar.Config.Key.self] = newValue } + } - /// This value can bind to a format sidebar style. - var richTextFormatSidebarStyle: RichTextFormat.Sidebar.Style { - get { self [RichTextFormat.Sidebar.Style.Key.self] } - set { self [RichTextFormat.Sidebar.Style.Key.self] = newValue } + /// This value can bind to a format sidebar style. + public var richTextFormatSidebarStyle: RichTextFormat.Sidebar.Style { + get { self[RichTextFormat.Sidebar.Style.Key.self] } + set { self[RichTextFormat.Sidebar.Style.Key.self] = newValue } + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/Format/RichTextFormat+Toolbar.swift b/Sources/RichEditorSwiftUI/Format/RichTextFormat+Toolbar.swift index 3d0a097..25a23f7 100644 --- a/Sources/RichEditorSwiftUI/Format/RichTextFormat+Toolbar.swift +++ b/Sources/RichEditorSwiftUI/Format/RichTextFormat+Toolbar.swift @@ -5,17 +5,17 @@ // Created by Divyesh Vekariya on 18/11/24. // -#if iOS || macOS || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(visionOS) + import SwiftUI -public extension RichTextFormat { + extension RichTextFormat { - /** + /** This horizontal toolbar provides text format controls. This toolbar adapts the layout based on horizontal size class. The control row is split in two for compact size, - while macOS and regular sizes get a single row. + while os(macOS) and regular sizes get a single row. You can configure and style the view by applying config and style view modifiers to your view hierarchy: @@ -28,115 +28,116 @@ public extension RichTextFormat { .richTextFormatToolbarConfig(...) ``` */ - struct Toolbar: RichTextFormatToolbarBase { + public struct Toolbar: RichTextFormatToolbarBase { - /** + /** Create a rich text format sheet. - Parameters: - context: The context to apply changes to. */ - public init( - context: RichEditorState - ) { - self._context = ObservedObject(wrappedValue: context) - } + public init( + context: RichEditorState + ) { + self._context = ObservedObject(wrappedValue: context) + } - @ObservedObject - private var context: RichEditorState + @ObservedObject + private var context: RichEditorState - @Environment(\.richTextFormatToolbarConfig) - var config + @Environment(\.richTextFormatToolbarConfig) + var config - @Environment(\.richTextFormatToolbarStyle) - var style + @Environment(\.richTextFormatToolbarStyle) + var style - @Environment(\.horizontalSizeClass) - private var horizontalSizeClass + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass - public var body: some View { - VStack(spacing: style.spacing) { - controls - if hasColorPickers { - Divider() - colorPickers(for: context) + public var body: some View { + VStack(spacing: style.spacing) { + controls + if hasColorPickers { + Divider() + colorPickers(for: context) + } } + .labelsHidden() + .padding(.vertical, style.padding) + .environment(\.sizeCategory, .medium) + // .background(background) + #if macOS + .frame(minWidth: 650) + #endif } - .labelsHidden() - .padding(.vertical, style.padding) - .environment(\.sizeCategory, .medium) -// .background(background) - #if macOS - .frame(minWidth: 650) - #endif } } -} -// MARK: - Views + // MARK: - Views -private extension RichTextFormat.Toolbar { + extension RichTextFormat.Toolbar { - var useSingleLine: Bool { - #if macOS - true - #else - horizontalSizeClass == .regular - #endif + fileprivate var useSingleLine: Bool { + #if macOS + true + #else + horizontalSizeClass == .regular + #endif + } } -} -private extension RichTextFormat.Toolbar { + extension RichTextFormat.Toolbar { - var background: some View { - Color.clear - .overlay(Color.primary.opacity(0.1)) - .shadow(color: .black.opacity(0.1), radius: 5) - .edgesIgnoringSafeArea(.all) - } + fileprivate var background: some View { + Color.clear + .overlay(Color.primary.opacity(0.1)) + .shadow(color: .black.opacity(0.1), radius: 5) + .edgesIgnoringSafeArea(.all) + } - @ViewBuilder - var controls: some View { - if useSingleLine { - HStack { - controlsContent - } - .padding(.horizontal, style.padding) - } else { - VStack(spacing: style.spacing) { - controlsContent + @ViewBuilder + fileprivate var controls: some View { + if useSingleLine { + HStack { + controlsContent + } + .padding(.horizontal, style.padding) + } else { + VStack(spacing: style.spacing) { + controlsContent + } + .padding(.horizontal, style.padding) } - .padding(.horizontal, style.padding) } - } - @ViewBuilder - var controlsContent: some View { - HStack { - #if macOS - headerPicker(context: context) - fontPicker(value: $context.fontName) - .onChangeBackPort(of: context.fontName) { newValue in - context.updateStyle(style: .font(newValue)) + @ViewBuilder + fileprivate var controlsContent: some View { + HStack { + #if macOS + headerPicker(context: context) + fontPicker(value: $context.fontName) + .onChangeBackPort(of: context.fontName) { newValue in + context.updateStyle(style: .font(newValue)) + } + #endif + styleToggleGroup(for: context) + otherMenuToggleGroup(for: context) + if !useSingleLine { + Spacer() + } + fontSizePicker(for: context) + if horizontalSizeClass == .regular { + Spacer() } - #endif - styleToggleGroup(for: context) - if !useSingleLine { - Spacer() } - fontSizePicker(for: context) - if horizontalSizeClass == .regular { - Spacer() + HStack { + #if !macOS + headerPicker(context: context) + #endif + alignmentPicker(context: context) + // superscriptButtons(for: context, greedy: false) + // indentButtons(for: context, greedy: false) } } - HStack { - #if !macOS - headerPicker(context: context) - #endif - alignmentPicker(context: context) -// superscriptButtons(for: context, greedy: false) -// indentButtons(for: context, greedy: false) - } } -} #endif diff --git a/Sources/RichEditorSwiftUI/Format/RichTextFormat+ToolbarConfig.swift b/Sources/RichEditorSwiftUI/Format/RichTextFormat+ToolbarConfig.swift index f704f41..25ab92d 100644 --- a/Sources/RichEditorSwiftUI/Format/RichTextFormat+ToolbarConfig.swift +++ b/Sources/RichEditorSwiftUI/Format/RichTextFormat+ToolbarConfig.swift @@ -5,87 +5,90 @@ // Created by Divyesh Vekariya on 18/11/24. // -#if iOS || macOS || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(visionOS) + import SwiftUI -public extension RichTextFormat { + extension RichTextFormat { - /// This type can be used to configure a format toolbar. - struct ToolbarConfig { + /// This type can be used to configure a format toolbar. + public struct ToolbarConfig { - public init( - headers: [HeaderType] = .all, - alignments: [RichTextAlignment] = .all, - colorPickers: [RichTextColor] = [.foreground], - colorPickersDisclosed: [RichTextColor] = [], - fontPicker: Bool = true, - fontSizePicker: Bool = true, - indentButtons: Bool = true, - lineSpacingPicker: Bool = false, - styles: [RichTextStyle] = .all, - superscriptButtons: Bool = true - ) { - self.headers = headers - self.alignments = alignments - self.colorPickers = colorPickers - self.colorPickersDisclosed = colorPickersDisclosed - self.fontPicker = fontPicker - self.fontSizePicker = fontSizePicker - self.indentButtons = indentButtons - self.lineSpacingPicker = lineSpacingPicker - self.styles = styles - #if macOS - self.superscriptButtons = superscriptButtons - #else - self.superscriptButtons = false - #endif - } + public init( + headers: [HeaderType] = .all, + alignments: [RichTextAlignment] = .all, + colorPickers: [RichTextColor] = [.foreground], + colorPickersDisclosed: [RichTextColor] = [], + fontPicker: Bool = true, + fontSizePicker: Bool = true, + indentButtons: Bool = true, + lineSpacingPicker: Bool = false, + styles: [RichTextStyle] = .all, + otherMenu: [RichTextOtherMenu] = .all, + superscriptButtons: Bool = true + ) { + self.headers = headers + self.alignments = alignments + self.colorPickers = colorPickers + self.colorPickersDisclosed = colorPickersDisclosed + self.fontPicker = fontPicker + self.fontSizePicker = fontSizePicker + self.indentButtons = indentButtons + self.lineSpacingPicker = lineSpacingPicker + self.styles = styles + self.otherMenu = otherMenu + #if macOS + self.superscriptButtons = superscriptButtons + #else + self.superscriptButtons = false + #endif + } - public var headers: [HeaderType] - public var alignments: [RichTextAlignment] - public var colorPickers: [RichTextColor] - public var colorPickersDisclosed: [RichTextColor] - public var fontPicker: Bool - public var fontSizePicker: Bool - public var indentButtons: Bool - public var lineSpacingPicker: Bool - public var styles: [RichTextStyle] - public var superscriptButtons: Bool + public var headers: [HeaderType] + public var alignments: [RichTextAlignment] + public var colorPickers: [RichTextColor] + public var colorPickersDisclosed: [RichTextColor] + public var fontPicker: Bool + public var fontSizePicker: Bool + public var indentButtons: Bool + public var lineSpacingPicker: Bool + public var styles: [RichTextStyle] + public var otherMenu: [RichTextOtherMenu] + public var superscriptButtons: Bool + } } -} -public extension RichTextFormat.ToolbarConfig { + extension RichTextFormat.ToolbarConfig { - /// The standard rich text format toolbar configuration. - static var standard: Self { .init() } -} + /// The standard rich text format toolbar configuration. + public static var standard: Self { .init() } + } -public extension View { + extension View { - /// Apply a rich text format toolbar style. - func richTextFormatToolbarConfig( - _ value: RichTextFormat.ToolbarConfig - ) -> some View { - self.environment(\.richTextFormatToolbarConfig, value) + /// Apply a rich text format toolbar style. + public func richTextFormatToolbarConfig( + _ value: RichTextFormat.ToolbarConfig + ) -> some View { + self.environment(\.richTextFormatToolbarConfig, value) + } } -} -private extension RichTextFormat.ToolbarConfig { + extension RichTextFormat.ToolbarConfig { - struct Key: EnvironmentKey { + fileprivate struct Key: EnvironmentKey { - public static var defaultValue: RichTextFormat.ToolbarConfig { - .init() + public static var defaultValue: RichTextFormat.ToolbarConfig { + .init() + } } } -} -public extension EnvironmentValues { + extension EnvironmentValues { - /// This value can bind to a format toolbar config. - var richTextFormatToolbarConfig: RichTextFormat.ToolbarConfig { - get { self [RichTextFormat.ToolbarConfig.Key.self] } - set { self [RichTextFormat.ToolbarConfig.Key.self] = newValue } + /// This value can bind to a format toolbar config. + public var richTextFormatToolbarConfig: RichTextFormat.ToolbarConfig { + get { self[RichTextFormat.ToolbarConfig.Key.self] } + set { self[RichTextFormat.ToolbarConfig.Key.self] = newValue } + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/Format/RichTextFormat+ToolbarStyle.swift b/Sources/RichEditorSwiftUI/Format/RichTextFormat+ToolbarStyle.swift index 1e49a84..b21719f 100644 --- a/Sources/RichEditorSwiftUI/Format/RichTextFormat+ToolbarStyle.swift +++ b/Sources/RichEditorSwiftUI/Format/RichTextFormat+ToolbarStyle.swift @@ -5,59 +5,59 @@ // Created by Divyesh Vekariya on 18/11/24. // -#if iOS || macOS || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(visionOS) + import SwiftUI -public extension RichTextFormat { + extension RichTextFormat { - /// This type can be used to style a format toolbar. - struct ToolbarStyle { + /// This type can be used to style a format toolbar. + public struct ToolbarStyle { - public init( - padding: Double = 10, - spacing: Double = 10 - ) { - self.padding = padding - self.spacing = spacing - } + public init( + padding: Double = 10, + spacing: Double = 10 + ) { + self.padding = padding + self.spacing = spacing + } - public var padding: Double - public var spacing: Double + public var padding: Double + public var spacing: Double + } } -} -public extension RichTextFormat.ToolbarStyle { + extension RichTextFormat.ToolbarStyle { - /// The standard rich text format toolbar style. - static var standard: Self { .init() } -} + /// The standard rich text format toolbar style. + public static var standard: Self { .init() } + } -public extension View { + extension View { - /// Apply a rich text format toolbar style. - func richTextFormatToolbarStyle( - _ style: RichTextFormat.ToolbarStyle - ) -> some View { - self.environment(\.richTextFormatToolbarStyle, style) + /// Apply a rich text format toolbar style. + public func richTextFormatToolbarStyle( + _ style: RichTextFormat.ToolbarStyle + ) -> some View { + self.environment(\.richTextFormatToolbarStyle, style) + } } -} -private extension RichTextFormat.ToolbarStyle { + extension RichTextFormat.ToolbarStyle { - struct Key: EnvironmentKey { + fileprivate struct Key: EnvironmentKey { - public static var defaultValue: RichTextFormat.ToolbarStyle { - .standard + public static var defaultValue: RichTextFormat.ToolbarStyle { + .standard + } } } -} -public extension EnvironmentValues { + extension EnvironmentValues { - /// This value can bind to a format toolbar style. - var richTextFormatToolbarStyle: RichTextFormat.ToolbarStyle { - get { self [RichTextFormat.ToolbarStyle.Key.self] } - set { self [RichTextFormat.ToolbarStyle.Key.self] = newValue } + /// This value can bind to a format toolbar style. + public var richTextFormatToolbarStyle: RichTextFormat.ToolbarStyle { + get { self[RichTextFormat.ToolbarStyle.Key.self] } + set { self[RichTextFormat.ToolbarStyle.Key.self] = newValue } + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/Format/RichTextFormatToolbarBase.swift b/Sources/RichEditorSwiftUI/Format/RichTextFormatToolbarBase.swift index d21b271..b6a47a0 100644 --- a/Sources/RichEditorSwiftUI/Format/RichTextFormatToolbarBase.swift +++ b/Sources/RichEditorSwiftUI/Format/RichTextFormatToolbarBase.swift @@ -5,185 +5,197 @@ // Created by Divyesh Vekariya on 18/11/24. // -#if iOS || macOS || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(visionOS) + import SwiftUI -/// This internal protocol is used to share code between the -/// two toolbars, which should eventually become one. -protocol RichTextFormatToolbarBase: View { + /// This internal protocol is used to share code between the + /// two toolbars, which should eventually become one. + protocol RichTextFormatToolbarBase: View { - var config: RichTextFormat.ToolbarConfig { get } - var style: RichTextFormat.ToolbarStyle { get } -} + var config: RichTextFormat.ToolbarConfig { get } + var style: RichTextFormat.ToolbarStyle { get } + } -extension RichTextFormatToolbarBase { + extension RichTextFormatToolbarBase { - var hasColorPickers: Bool { - let colors = config.colorPickers - let disclosed = config.colorPickersDisclosed - return !colors.isEmpty || !disclosed.isEmpty - } -} - -extension RichTextFormatToolbarBase { - - @ViewBuilder - func alignmentPicker( - context: RichEditorState - ) -> some View { - if !config.alignments.isEmpty { - RichTextAlignment.Picker( - selection: context.textAlignmentBinding(), - values: config.alignments - ) - .pickerStyle(.segmented) + var hasColorPickers: Bool { + let colors = config.colorPickers + let disclosed = config.colorPickersDisclosed + return !colors.isEmpty || !disclosed.isEmpty } } - @ViewBuilder - func headerPicker( - context: RichEditorState - ) -> some View { - if !config.headers.isEmpty { - RichTextHeader.Picker( - context: context, - values: config.headers - ) - } - } + extension RichTextFormatToolbarBase { - @ViewBuilder - func colorPickers( - for context: RichEditorState - ) -> some View { - if hasColorPickers { - VStack(spacing: style.spacing) { - colorPickers( - for: config.colorPickers, - context: context + @ViewBuilder + func alignmentPicker( + context: RichEditorState + ) -> some View { + if !config.alignments.isEmpty { + RichTextAlignment.Picker( + selection: context.textAlignmentBinding(), + values: config.alignments ) - colorPickersDisclosureGroup( - for: config.colorPickersDisclosed, - context: context + .pickerStyle(.segmented) + } + } + + @ViewBuilder + func headerPicker( + context: RichEditorState + ) -> some View { + if !config.headers.isEmpty { + RichTextHeader.Picker( + context: context, + values: config.headers ) } } - } - @ViewBuilder - func colorPickers( - for colors: [RichTextColor], - context: RichEditorState - ) -> some View { - if !colors.isEmpty { - ForEach(colors) { - colorPicker(for: $0, context: context) + @ViewBuilder + func colorPickers( + for context: RichEditorState + ) -> some View { + if hasColorPickers { + VStack(spacing: style.spacing) { + colorPickers( + for: config.colorPickers, + context: context + ) + colorPickersDisclosureGroup( + for: config.colorPickersDisclosed, + context: context + ) + } } } - } - @ViewBuilder - func colorPickersDisclosureGroup( - for colors: [RichTextColor], - context: RichEditorState - ) -> some View { - if !colors.isEmpty { - DisclosureGroup { - colorPickers( - for: config.colorPickersDisclosed, - context: context - ) - } label: { - Image - .symbol("chevron.down") - .label(RTEL10n.more.text) - .labelStyle(.iconOnly) - .frame(minWidth: 30) + @ViewBuilder + func colorPickers( + for colors: [RichTextColor], + context: RichEditorState + ) -> some View { + if !colors.isEmpty { + ForEach(colors) { + colorPicker(for: $0, context: context) + } } } - } - func colorPicker( - for color: RichTextColor, - context: RichEditorState - ) -> some View { - RichTextColor.Picker( - type: color, - value: context.binding(for: color), - quickColors: .quickPickerColors - ) - } + @ViewBuilder + func colorPickersDisclosureGroup( + for colors: [RichTextColor], + context: RichEditorState + ) -> some View { + if !colors.isEmpty { + DisclosureGroup { + colorPickers( + for: config.colorPickersDisclosed, + context: context + ) + } label: { + Image + .symbol("chevron.down") + .label(RTEL10n.more.text) + .labelStyle(.iconOnly) + .frame(minWidth: 30) + } + } + } - @ViewBuilder - func fontPicker( - value: Binding - ) -> some View { - if config.fontPicker { - RichTextFont.Picker( - selection: value + func colorPicker( + for color: RichTextColor, + context: RichEditorState + ) -> some View { + RichTextColor.Picker( + type: color, + value: context.binding(for: color), + quickColors: .quickPickerColors ) - .richTextFontPickerConfig(.init(fontSize: 12)) } - } - @ViewBuilder - func fontSizePicker( - for context: RichEditorState - ) -> some View { - if config.fontSizePicker { - RichTextFont.SizePickerStack(context: context) - .buttonStyle(.bordered) + @ViewBuilder + func fontPicker( + value: Binding + ) -> some View { + if config.fontPicker { + RichTextFont.Picker( + selection: value + ) + .richTextFontPickerConfig(.init(fontSize: 12)) + } } - } -// @ViewBuilder -// func indentButtons( -// for context: RichEditorState, -// greedy: Bool -// ) -> some View { -// if config.indentButtons { -// RichTextAction.ButtonGroup( -// context: context, -// actions: [.stepIndent(points: -30), .stepIndent(points: 30)], -// greedy: greedy -// ) -// } -// } - -// @ViewBuilder -// func lineSpacingPicker( -// for context: RichEditorState -// ) -> some View { -// if config.lineSpacingPicker { -// RichTextLine.SpacingPickerStack(context: context) -// .buttonStyle(.bordered) -// } -// } - - @ViewBuilder - func styleToggleGroup( - for context: RichEditorState - ) -> some View { - if !config.styles.isEmpty { - RichTextStyle.ToggleGroup( - context: context, - styles: config.styles - ) + @ViewBuilder + func fontSizePicker( + for context: RichEditorState + ) -> some View { + if config.fontSizePicker { + RichTextFont.SizePickerStack(context: context) + .buttonStyle(.bordered) + } + } + + // @ViewBuilder + // func indentButtons( + // for context: RichEditorState, + // greedy: Bool + // ) -> some View { + // if config.indentButtons { + // RichTextAction.ButtonGroup( + // context: context, + // actions: [.stepIndent(points: -30), .stepIndent(points: 30)], + // greedy: greedy + // ) + // } + // } + + // @ViewBuilder + // func lineSpacingPicker( + // for context: RichEditorState + // ) -> some View { + // if config.lineSpacingPicker { + // RichTextLine.SpacingPickerStack(context: context) + // .buttonStyle(.bordered) + // } + // } + + @ViewBuilder + func styleToggleGroup( + for context: RichEditorState + ) -> some View { + if !config.styles.isEmpty { + RichTextStyle.ToggleGroup( + context: context, + styles: config.styles + ) + } + } + + @ViewBuilder + func otherMenuToggleGroup( + for context: RichEditorState + ) -> some View { + if !config.otherMenu.isEmpty { + RichTextOtherMenu.ToggleGroup( + context: context, + styles: config.otherMenu + ) + } } - } -// @ViewBuilder -// func superscriptButtons( -// for context: RichEditorState, -// greedy: Bool -// ) -> some View { -// if config.superscriptButtons { -// RichTextAction.ButtonGroup( -// context: context, -// actions: [.stepSuperscript(steps: -1), .stepSuperscript(steps: 1)], -// greedy: greedy -// ) -// } -// } -} + // @ViewBuilder + // func superscriptButtons( + // for context: RichEditorState, + // greedy: Bool + // ) -> some View { + // if config.superscriptButtons { + // RichTextAction.ButtonGroup( + // context: context, + // actions: [.stepSuperscript(steps: -1), .stepSuperscript(steps: 1)], + // greedy: greedy + // ) + // } + // } + } #endif diff --git a/Sources/RichEditorSwiftUI/Headers/RichTextHeader+Picker.swift b/Sources/RichEditorSwiftUI/Headers/RichTextHeader+Picker.swift index 365a3e9..feeb580 100644 --- a/Sources/RichEditorSwiftUI/Headers/RichTextHeader+Picker.swift +++ b/Sources/RichEditorSwiftUI/Headers/RichTextHeader+Picker.swift @@ -7,7 +7,7 @@ import SwiftUI -public extension RichTextHeader { +extension RichTextHeader { /** This picker can be used to pick a Header type. @@ -25,7 +25,7 @@ public extension RichTextHeader { } ``` */ - struct Picker: View { + public struct Picker: View { /** Create a font size picker. @@ -48,8 +48,10 @@ public extension RichTextHeader { public var body: some View { SwiftUI.Picker("", selection: $selection) { - ForEach(values, - id: \.self) { + ForEach( + values, + id: \.self + ) { text(for: $0) .tag($0) } @@ -58,13 +60,12 @@ public extension RichTextHeader { } } -private extension RichTextHeader.Picker { +extension RichTextHeader.Picker { - func text( + fileprivate func text( for headerType: HeaderType ) -> some View { Text(headerType.titleLabel) .fixedSize(horizontal: true, vertical: false) } } - diff --git a/Sources/RichEditorSwiftUI/Images/Image+RichText.swift b/Sources/RichEditorSwiftUI/Images/Image+RichText.swift index a0cfcad..8078334 100644 --- a/Sources/RichEditorSwiftUI/Images/Image+RichText.swift +++ b/Sources/RichEditorSwiftUI/Images/Image+RichText.swift @@ -7,99 +7,96 @@ import SwiftUI -public extension Image { - - static let richTextCopy = symbol("doc.on.clipboard") - static let richTextDismissKeyboard = symbol("keyboard.chevron.compact.down") - static let richTextEdit = symbol("square.and.pencil") - static let richTextExport = symbol("square.and.arrow.up.on.square") - static let richTextPrint = symbol("printer") - static let richTextRedo = symbol("arrow.uturn.forward") - static let richTextShare = symbol("square.and.arrow.up") - static let richTextUndo = symbol("arrow.uturn.backward") - - static let richTextAlignmentCenter = symbol("text.aligncenter") - static let richTextAlignmentJustified = symbol("text.justify") - static let richTextAlignmentLeft = symbol("text.alignleft") - static let richTextAlignmentRight = symbol("text.alignright") - - static let richTextColorBackground = symbol("highlighter") - static let richTextColorForeground = symbol("character") - static let richTextColorReset = symbol("circle.slash") - static let richTextColorStroke = symbol("a.square") - static let richTextColorStrikethrough = symbol("strikethrough") - static let richTextColorUnderline = symbol("underline") - static let richTextColorUndefined = symbol("questionmark.app") - - static let richTextHeaderDefault = symbol("textformat") - static let richTextHeader1 = symbol("textformat") - static let richTextHeader2 = symbol("textformat") - static let richTextHeader3 = symbol("textformat") - static let richTextHeader4 = symbol("textformat") - static let richTextHeader5 = symbol("textformat") - static let richTextHeader6 = symbol("textformat") - - static let richTextDocument = symbol("doc.text") - static let richTextDocuments = symbol("doc.on.doc") - - static let richTextFont = symbol("textformat") - static let richTextFontSizeDecrease = symbol("minus") - static let richTextFontSizeIncrease = symbol("plus") - - static let richTextFormat = symbol("textformat") - static let richTextFormatBrush = symbol("paintbrush") - - static let richTextIndentDecrease = symbol("decrease.indent") - static let richTextIndentIncrease = symbol("increase.indent") - - static let richTextLineSpacing = symbol("arrow.up.and.down.text.horizontal") - static let richTextLineSpacingDecrease = symbol("minus") - static let richTextLineSpacingIncrease = symbol("plus") - - static let richTextSelection = symbol("123.rectangle.fill") - - static let richTextStyleBold = symbol("bold") - static let richTextStyleItalic = symbol("italic") - static let richTextStyleStrikethrough = symbol("strikethrough") - static let richTextStyleUnderline = symbol("underline") - - static let richTextSuperscriptDecrease = symbol("textformat.subscript") - static let richTextSuperscriptIncrease = symbol("textformat.superscript") - static let richTextIgnoreIt = symbol("") +extension Image { + + public static let richTextCopy = symbol("doc.on.clipboard") + public static let richTextDismissKeyboard = symbol( + "keyboard.chevron.compact.down") + public static let richTextEdit = symbol("square.and.pencil") + public static let richTextExport = symbol("square.and.arrow.up.on.square") + public static let richTextPrint = symbol("printer") + public static let richTextRedo = symbol("arrow.uturn.forward") + public static let richTextShare = symbol("square.and.arrow.up") + public static let richTextUndo = symbol("arrow.uturn.backward") + + public static let richTextAlignmentCenter = symbol("text.aligncenter") + public static let richTextAlignmentJustified = symbol("text.justify") + public static let richTextAlignmentLeft = symbol("text.alignleft") + public static let richTextAlignmentRight = symbol("text.alignright") + + public static let richTextColorBackground = symbol("highlighter") + public static let richTextColorForeground = symbol("character") + public static let richTextColorReset = symbol("circle.slash") + public static let richTextColorStroke = symbol("a.square") + public static let richTextColorStrikethrough = symbol("strikethrough") + public static let richTextColorUnderline = symbol("underline") + public static let richTextColorUndefined = symbol("questionmark.app") + + public static let richTextHeaderDefault = symbol("textformat") + public static let richTextHeader1 = symbol("textformat") + public static let richTextHeader2 = symbol("textformat") + public static let richTextHeader3 = symbol("textformat") + public static let richTextHeader4 = symbol("textformat") + public static let richTextHeader5 = symbol("textformat") + public static let richTextHeader6 = symbol("textformat") + + public static let richTextDocument = symbol("doc.text") + public static let richTextDocuments = symbol("doc.on.doc") + + public static let richTextFont = symbol("textformat") + public static let richTextFontSizeDecrease = symbol("minus") + public static let richTextFontSizeIncrease = symbol("plus") + + public static let richTextFormat = symbol("textformat") + public static let richTextFormatBrush = symbol("paintbrush") + + public static let richTextIndentDecrease = symbol("decrease.indent") + public static let richTextIndentIncrease = symbol("increase.indent") + + public static let richTextLineSpacing = symbol( + "arrow.up.and.down.text.horizontal") + public static let richTextLineSpacingDecrease = symbol("minus") + public static let richTextLineSpacingIncrease = symbol("plus") + + public static let richTextSelection = symbol("123.rectangle.fill") + + public static let richTextStyleBold = symbol("bold") + public static let richTextStyleItalic = symbol("italic") + public static let richTextStyleStrikethrough = symbol("strikethrough") + public static let richTextStyleUnderline = symbol("underline") + + public static let richTextSuperscriptDecrease = symbol( + "textformat.subscript") + public static let richTextSuperscriptIncrease = symbol( + "textformat.superscript") + public static let richTextLink = symbol("link") + public static let richTextIgnoreIt = symbol("") } -public extension Image { +extension Image { - static func richTextStepFontSize( + public static func richTextStepFontSize( _ points: Int ) -> Image { - points < 0 ? - .richTextFontSizeDecrease : - .richTextFontSizeIncrease + points < 0 ? .richTextFontSizeDecrease : .richTextFontSizeIncrease } - static func richTextStepIndent( + public static func richTextStepIndent( _ points: Double ) -> Image { - points < 0 ? - .richTextIndentDecrease : - .richTextIndentIncrease + points < 0 ? .richTextIndentDecrease : .richTextIndentIncrease } - static func richTextStepLineSpacing( + public static func richTextStepLineSpacing( _ points: Double ) -> Image { - points < 0 ? - .richTextLineSpacingDecrease : - .richTextLineSpacingIncrease + points < 0 ? .richTextLineSpacingDecrease : .richTextLineSpacingIncrease } - static func richTextStepSuperscript( + public static func richTextStepSuperscript( _ steps: Int ) -> Image { - steps < 0 ? - .richTextSuperscriptDecrease : - .richTextSuperscriptIncrease + steps < 0 ? .richTextSuperscriptDecrease : .richTextSuperscriptIncrease } } @@ -109,4 +106,3 @@ extension Image { .init(systemName: name) } } - diff --git a/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar+Config.swift b/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar+Config.swift index b870b8e..6a4c161 100644 --- a/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar+Config.swift +++ b/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar+Config.swift @@ -5,71 +5,72 @@ // Created by Divyesh Vekariya on 22/10/24. // -#if iOS || macOS || os(visionOS) -import SwiftUI - -/// This struct can configure a ``RichTextKeyboardToolbar``. -public struct RichTextKeyboardToolbarConfig { - - /// Create a custom keyboard toolbar configuration. - /// - /// - Parameters: - /// - alwaysDisplayToolbar: Whether or not to always show the toolbar, by default `false`. - /// - leadingActions: The leading actions, by default `.undo` and `.redo`. - /// - trailingActions: The trailing actions, by default `.dismissKeyboard`. - public init( - alwaysDisplayToolbar: Bool = false, - leadingActions: [RichTextAction] = [.undo, .redo], - trailingActions: [RichTextAction] = [.dismissKeyboard] - ) { - self.alwaysDisplayToolbar = alwaysDisplayToolbar - self.leadingActions = leadingActions - self.trailingActions = trailingActions +#if os(iOS) || os(macOS) || os(visionOS) + import SwiftUI + + /// This struct can configure a ``RichTextKeyboardToolbar``. + public struct RichTextKeyboardToolbarConfig { + + /// Create a custom keyboard toolbar configuration. + /// + /// - Parameters: + /// - alwaysDisplayToolbar: Whether or not to always show the toolbar, by default `false`. + /// - leadingActions: The leading actions, by default `.undo` and `.redo`. + /// - trailingActions: The trailing actions, by default `.dismissKeyboard`. + public init( + alwaysDisplayToolbar: Bool = false, + leadingActions: [RichTextAction] = [.undo, .redo], + trailingActions: [RichTextAction] = [.dismissKeyboard] + ) { + self.alwaysDisplayToolbar = alwaysDisplayToolbar + self.leadingActions = leadingActions + self.trailingActions = trailingActions + } + + /// Whether or not to always show the toolbar. + public var alwaysDisplayToolbar: Bool + + /// The leading toolbar actions. + public var leadingActions: [RichTextAction] + + /// The trailing toolbar actions. + public var trailingActions: [RichTextAction] } - /// Whether or not to always show the toolbar. - public var alwaysDisplayToolbar: Bool - - /// The leading toolbar actions. - public var leadingActions: [RichTextAction] - - /// The trailing toolbar actions. - public var trailingActions: [RichTextAction] -} + extension RichTextKeyboardToolbarConfig { -public extension RichTextKeyboardToolbarConfig { - - /// The standard rich text keyboard toolbar config. - /// - /// You can override this to change the global default. - static var standard = RichTextKeyboardToolbarConfig() -} + /// The standard rich text keyboard toolbar config. + /// + /// You can override this to change the global default. + public static var standard = RichTextKeyboardToolbarConfig() + } -public extension View { + extension View { - /// Apply a ``RichTextKeyboardToolbar`` configuration. - func richTextKeyboardToolbarConfig( - _ config: RichTextKeyboardToolbarConfig - ) -> some View { - self.environment(\.richTextKeyboardToolbarConfig, config) + /// Apply a ``RichTextKeyboardToolbar`` configuration. + public func richTextKeyboardToolbarConfig( + _ config: RichTextKeyboardToolbarConfig + ) -> some View { + self.environment(\.richTextKeyboardToolbarConfig, config) + } } -} -private extension RichTextKeyboardToolbarConfig { + extension RichTextKeyboardToolbarConfig { - struct Key: EnvironmentKey { + fileprivate struct Key: EnvironmentKey { - public static var defaultValue: RichTextKeyboardToolbarConfig = .standard + public static var defaultValue: RichTextKeyboardToolbarConfig = + .standard + } } -} -public extension EnvironmentValues { + extension EnvironmentValues { - /// This value can bind to a keyboard toolbar config. - var richTextKeyboardToolbarConfig: RichTextKeyboardToolbarConfig { - get { self [RichTextKeyboardToolbarConfig.Key.self] } - set { self [RichTextKeyboardToolbarConfig.Key.self] = newValue } + /// This value can bind to a keyboard toolbar config. + public var richTextKeyboardToolbarConfig: RichTextKeyboardToolbarConfig + { + get { self[RichTextKeyboardToolbarConfig.Key.self] } + set { self[RichTextKeyboardToolbarConfig.Key.self] = newValue } + } } -} #endif - diff --git a/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar+Style.swift b/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar+Style.swift index 88833b7..56f13f3 100644 --- a/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar+Style.swift +++ b/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar+Style.swift @@ -5,77 +5,77 @@ // Created by Divyesh Vekariya on 22/10/24. // -#if iOS || macOS || os(visionOS) -import SwiftUI - -/// This struct can style a ``RichTextKeyboardToolbar``. -public struct RichTextKeyboardToolbarStyle { - - /// Create a custom toolbar style - /// - /// - Parameters: - /// - toolbarHeight: The height of the toolbar, by default `50`. - /// - itemSpacing: The spacing between toolbar items, by default `15`. - /// - shadowColor: The toolbar's shadow color, by default transparent black. - /// - shadowRadius: The toolbar's shadow radius, by default `3`. - public init( - toolbarHeight: Double = 50, - itemSpacing: Double = 15, - shadowColor: Color = .black.opacity(0.1), - shadowRadius: Double = 3 - ) { - self.toolbarHeight = toolbarHeight - self.itemSpacing = itemSpacing - self.shadowColor = shadowColor - self.shadowRadius = shadowRadius +#if os(iOS) || os(macOS) || os(visionOS) + import SwiftUI + + /// This struct can style a ``RichTextKeyboardToolbar``. + public struct RichTextKeyboardToolbarStyle { + + /// Create a custom toolbar style + /// + /// - Parameters: + /// - toolbarHeight: The height of the toolbar, by default `50`. + /// - itemSpacing: The spacing between toolbar items, by default `15`. + /// - shadowColor: The toolbar's shadow color, by default transparent black. + /// - shadowRadius: The toolbar's shadow radius, by default `3`. + public init( + toolbarHeight: Double = 50, + itemSpacing: Double = 15, + shadowColor: Color = .black.opacity(0.1), + shadowRadius: Double = 3 + ) { + self.toolbarHeight = toolbarHeight + self.itemSpacing = itemSpacing + self.shadowColor = shadowColor + self.shadowRadius = shadowRadius + } + + /// The height of the toolbar. + public var toolbarHeight: Double + + /// The spacing between toolbar items. + public var itemSpacing: Double + + /// The toolbar's shadow color. + public var shadowColor: Color + + /// The toolbar's shadow radius. + public var shadowRadius: Double } - /// The height of the toolbar. - public var toolbarHeight: Double + extension RichTextKeyboardToolbarStyle { - /// The spacing between toolbar items. - public var itemSpacing: Double - - /// The toolbar's shadow color. - public var shadowColor: Color - - /// The toolbar's shadow radius. - public var shadowRadius: Double -} - -public extension RichTextKeyboardToolbarStyle { - - /// The standard rich text keyboard toolbar style. - /// - /// You can set a new value to change the global default. - static var standard = Self() -} + /// The standard rich text keyboard toolbar style. + /// + /// You can set a new value to change the global default. + public static var standard = Self() + } -public extension View { + extension View { - /// Apply a ``RichTextKeyboardToolbar`` style. - func richTextKeyboardToolbarStyle( - _ style: RichTextKeyboardToolbarStyle - ) -> some View { - self.environment(\.richTextKeyboardToolbarStyle, style) + /// Apply a ``RichTextKeyboardToolbar`` style. + public func richTextKeyboardToolbarStyle( + _ style: RichTextKeyboardToolbarStyle + ) -> some View { + self.environment(\.richTextKeyboardToolbarStyle, style) + } } -} -private extension RichTextKeyboardToolbarStyle { + extension RichTextKeyboardToolbarStyle { - struct Key: EnvironmentKey { + fileprivate struct Key: EnvironmentKey { - static var defaultValue: RichTextKeyboardToolbarStyle = .standard + static var defaultValue: RichTextKeyboardToolbarStyle = .standard + } } -} -public extension EnvironmentValues { + extension EnvironmentValues { - /// This value can bind to a keyboard toolbar style. - var richTextKeyboardToolbarStyle: RichTextKeyboardToolbarStyle { - get { self [RichTextKeyboardToolbarStyle.Key.self] } - set { self [RichTextKeyboardToolbarStyle.Key.self] = newValue } + /// This value can bind to a keyboard toolbar style. + public var richTextKeyboardToolbarStyle: RichTextKeyboardToolbarStyle { + get { self[RichTextKeyboardToolbarStyle.Key.self] } + set { self[RichTextKeyboardToolbarStyle.Key.self] = newValue } + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar.swift b/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar.swift index fbba983..41a016f 100644 --- a/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar.swift +++ b/Sources/RichEditorSwiftUI/Keyboard/RichTextKeyboardToolbar.swift @@ -6,66 +6,66 @@ // Copyright © 2022-2024 Daniel Saidi. All rights reserved. // -#if iOS || macOS || os(visionOS) -import SwiftUI - -/** - This toolbar can be added above an iOS keyboard, to provide - rich text formatting in a compact form. - - This toolbar is needed since the ``RichTextEditor`` can not - use a `toolbar` modifier with `.keyboard` placement: - - ```swift - RichTextEditor(text: $text, context: context) - .toolbar { - ToolbarItemGroup(placement: .keyboard) { - .... - } - } - ``` - - Instead, add this toolbar below a ``RichTextEditor`` to let - it automatically show when the text editor is edited in iOS. - - You can inject additional leading and trailing buttons, and - customize the format sheet that is presented when users tap - format button: - - ```swift - VStack { - RichTextEditor(...) - RichTextKeyboardToolbar( - context: context, - leadingButtons: {}, - trailingButtons: {}, - formatSheet: { $0 } - ) - } - ``` - - These view builders provide you with standard views. Return - `$0` to use these standard views, or return any custom view - that you want to use instead. - - You can configure and style the view by applying its config - and style view modifiers to your view hierarchy: - - ```swift - VStack { - RichTextEditor(...) - RichTextKeyboardToolbar(...) - } - .richTextKeyboardToolbarStyle(...) - .richTextKeyboardToolbarConfig(...) - ``` - - For more information, see ``RichTextKeyboardToolbarConfig`` - and ``RichTextKeyboardToolbarStyle``. - */ -public struct RichTextKeyboardToolbar: View { - - /** +#if os(iOS) || os(macOS) || os(visionOS) + import SwiftUI + + /// This toolbar can be added above an iOS keyboard, to provide + /// rich text formatting in a compact form. + /// + /// This toolbar is needed since the ``RichTextEditor`` can not + /// use a `toolbar` modifier with `.keyboard` placement: + /// + /// ```swift + /// RichTextEditor(text: $text, context: context) + /// .toolbar { + /// ToolbarItemGroup(placement: .keyboard) { + /// .... + /// } + /// } + /// ``` + /// + /// Instead, add this toolbar below a ``RichTextEditor`` to let + /// it automatically show when the text editor is edited in iOS. + /// + /// You can inject additional leading and trailing buttons, and + /// customize the format sheet that is presented when users tap + /// format button: + /// + /// ```swift + /// VStack { + /// RichTextEditor(...) + /// RichTextKeyboardToolbar( + /// context: context, + /// leadingButtons: {}, + /// trailingButtons: {}, + /// formatSheet: { $0 } + /// ) + /// } + /// ``` + /// + /// These view builders provide you with standard views. Return + /// `$0` to use these standard views, or return any custom view + /// that you want to use instead. + /// + /// You can configure and style the view by applying its config + /// and style view modifiers to your view hierarchy: + /// + /// ```swift + /// VStack { + /// RichTextEditor(...) + /// RichTextKeyboardToolbar(...) + /// } + /// .richTextKeyboardToolbarStyle(...) + /// .richTextKeyboardToolbarConfig(...) + /// ``` + /// + /// For more information, see ``RichTextKeyboardToolbarConfig`` + /// and ``RichTextKeyboardToolbarStyle``. + public struct RichTextKeyboardToolbar< + LeadingButtons: View, TrailingButtons: View, FormatSheet: View + >: View { + + /** Create a rich text keyboard toolbar. - Parameters: @@ -74,167 +74,175 @@ public struct RichTextKeyboardToolbar LeadingButtons, - @ViewBuilder trailingButtons: @escaping (StandardTrailingButtons) -> TrailingButtons, - @ViewBuilder formatSheet: @escaping (StandardFormatSheet) -> FormatSheet - ) { - self._context = ObservedObject(wrappedValue: context) - self.leadingButtons = leadingButtons - self.trailingButtons = trailingButtons - self.formatSheet = formatSheet - } + public init( + context: RichEditorState, + @ViewBuilder leadingButtons: @escaping (StandardLeadingButtons) -> + LeadingButtons, + @ViewBuilder trailingButtons: @escaping (StandardTrailingButtons) -> + TrailingButtons, + @ViewBuilder formatSheet: @escaping (StandardFormatSheet) -> + FormatSheet + ) { + self._context = ObservedObject(wrappedValue: context) + self.leadingButtons = leadingButtons + self.trailingButtons = trailingButtons + self.formatSheet = formatSheet + } - public typealias StandardLeadingButtons = EmptyView - public typealias StandardTrailingButtons = EmptyView - public typealias StandardFormatSheet = RichTextFormat.Sheet + public typealias StandardLeadingButtons = EmptyView + public typealias StandardTrailingButtons = EmptyView + public typealias StandardFormatSheet = RichTextFormat.Sheet - private let leadingButtons: (StandardLeadingButtons) -> LeadingButtons - private let trailingButtons: (StandardTrailingButtons) -> TrailingButtons - private let formatSheet: (StandardFormatSheet) -> FormatSheet + private let leadingButtons: (StandardLeadingButtons) -> LeadingButtons + private let trailingButtons: + (StandardTrailingButtons) -> TrailingButtons + private let formatSheet: (StandardFormatSheet) -> FormatSheet - @ObservedObject - private var context: RichEditorState + @ObservedObject + private var context: RichEditorState - @State - private var isFormatSheetPresented = false + @State + private var isFormatSheetPresented = false - @Environment(\.horizontalSizeClass) - private var horizontalSizeClass + @Environment(\.horizontalSizeClass) + private var horizontalSizeClass - @Environment(\.richTextKeyboardToolbarConfig) - private var config + @Environment(\.richTextKeyboardToolbarConfig) + private var config - @Environment(\.richTextKeyboardToolbarStyle) - private var style + @Environment(\.richTextKeyboardToolbarStyle) + private var style - public var body: some View { - VStack(spacing: 0) { - HStack(spacing: style.itemSpacing) { - leadingViews - Spacer() - .frame(minWidth: 0, maxWidth: .infinity) - trailingViews + public var body: some View { + VStack(spacing: 0) { + HStack(spacing: style.itemSpacing) { + leadingViews + Spacer() + .frame(minWidth: 0, maxWidth: .infinity) + trailingViews + } + .padding(10) } - .padding(10) - } - .environment(\.sizeCategory, .medium) - .frame(height: style.toolbarHeight) - .overlay(Divider(), alignment: .bottom) - .accentColor(.primary) - .background( - Color.primary.colorInvert() - .overlay(Color.white.opacity(0.2)) - .shadow(color: style.shadowColor, radius: style.shadowRadius, x: 0, y: 0) - ) - .opacity(shouldDisplayToolbar ? 1 : 0) - .offset(y: shouldDisplayToolbar ? 0 : style.toolbarHeight) - .frame(height: shouldDisplayToolbar ? nil : 0) - .sheet(isPresented: $isFormatSheetPresented) { - formatSheet( - .init(context: context) + .environment(\.sizeCategory, .medium) + .frame(height: style.toolbarHeight) + .overlay(Divider(), alignment: .bottom) + .accentColor(.primary) + .background( + Color.primary.colorInvert() + .overlay(Color.white.opacity(0.2)) + .shadow( + color: style.shadowColor, radius: style.shadowRadius, + x: 0, y: 0) ) - .prefersMediumSize() + .opacity(shouldDisplayToolbar ? 1 : 0) + .offset(y: shouldDisplayToolbar ? 0 : style.toolbarHeight) + .frame(height: shouldDisplayToolbar ? nil : 0) + .sheet(isPresented: $isFormatSheetPresented) { + formatSheet( + .init(context: context) + ) + .prefersMediumSize() + } } } -} - -private extension View { - - @ViewBuilder - func prefersMediumSize() -> some View { - #if macOS - self - #else - if #available(iOS 16, *) { - self.presentationDetents([.medium]) - } else { - self + + extension View { + + @ViewBuilder + fileprivate func prefersMediumSize() -> some View { + #if macOS + self + #else + if #available(iOS 16, *) { + self.presentationDetents([.medium]) + } else { + self + } + #endif } - #endif } -} -private extension RichTextKeyboardToolbar { + extension RichTextKeyboardToolbar { - var isCompact: Bool { - horizontalSizeClass == .compact + fileprivate var isCompact: Bool { + horizontalSizeClass == .compact + } } -} -private extension RichTextKeyboardToolbar { + extension RichTextKeyboardToolbar { - var divider: some View { - Divider() - .frame(height: 25) - } + fileprivate var divider: some View { + Divider() + .frame(height: 25) + } - @ViewBuilder - var leadingViews: some View { - RichTextAction.ButtonStack( - context: context, - actions: config.leadingActions, - spacing: style.itemSpacing - ) + @ViewBuilder + fileprivate var leadingViews: some View { + RichTextAction.ButtonStack( + context: context, + actions: config.leadingActions, + spacing: style.itemSpacing + ) - leadingButtons(StandardLeadingButtons()) + leadingButtons(StandardLeadingButtons()) - divider + divider - Button(action: presentFormatSheet) { - Image.richTextFormat - .contentShape(Rectangle()) - } + Button(action: presentFormatSheet) { + Image.richTextFormat + .contentShape(Rectangle()) + } - RichTextStyle.ToggleStack(context: context) - .keyboardShortcutsOnly(if: isCompact) + RichTextStyle.ToggleStack(context: context) + .keyboardShortcutsOnly(if: isCompact) - RichTextFont.SizePickerStack(context: context) - .keyboardShortcutsOnly() - } + RichTextFont.SizePickerStack(context: context) + .keyboardShortcutsOnly() + } - @ViewBuilder - var trailingViews: some View { - RichTextAlignment.Picker(selection: $context.textAlignment) - .pickerStyle(.segmented) - .frame(maxWidth: 200) - .keyboardShortcutsOnly(if: isCompact) + @ViewBuilder + fileprivate var trailingViews: some View { + RichTextAlignment.Picker(selection: $context.textAlignment) + .pickerStyle(.segmented) + .frame(maxWidth: 200) + .keyboardShortcutsOnly(if: isCompact) - trailingButtons(StandardTrailingButtons()) + trailingButtons(StandardTrailingButtons()) - RichTextAction.ButtonStack( - context: context, - actions: config.trailingActions, - spacing: style.itemSpacing - ) + RichTextAction.ButtonStack( + context: context, + actions: config.trailingActions, + spacing: style.itemSpacing + ) + } } -} - -private extension View { - - @ViewBuilder - func keyboardShortcutsOnly( - if condition: Bool = true - ) -> some View { - if condition { - self.hidden() - .frame(width: 0) - } else { - self + + extension View { + + @ViewBuilder + fileprivate func keyboardShortcutsOnly( + if condition: Bool = true + ) -> some View { + if condition { + self.hidden() + .frame(width: 0) + } else { + self + } } } -} -private extension RichTextKeyboardToolbar { + extension RichTextKeyboardToolbar { - var shouldDisplayToolbar: Bool { context.isEditingText || config.alwaysDisplayToolbar } -} + fileprivate var shouldDisplayToolbar: Bool { + context.isEditingText || config.alwaysDisplayToolbar + } + } -private extension RichTextKeyboardToolbar { + extension RichTextKeyboardToolbar { - func presentFormatSheet() { - isFormatSheetPresented = true + fileprivate func presentFormatSheet() { + isFormatSheetPresented = true + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/ListStyle/ListType.swift b/Sources/RichEditorSwiftUI/ListStyle/ListType.swift index 4c686fb..cb7d80c 100644 --- a/Sources/RichEditorSwiftUI/ListStyle/ListType.swift +++ b/Sources/RichEditorSwiftUI/ListStyle/ListType.swift @@ -1,6 +1,6 @@ // // ListType.swift -// +// // // Created by Divyesh Vekariya on 29/04/24. // @@ -20,19 +20,19 @@ public enum ListType: Codable, Identifiable, CaseIterable, Hashable { } case bullet(_ indent: Int? = nil) -// case ordered(_ indent: Int? = nil) + // case ordered(_ indent: Int? = nil) enum CodingKeys: String, CodingKey { case bullet = "bullet" -// case ordered = "ordered" + // case ordered = "ordered" } var key: String { switch self { case .bullet: return "bullet" -// case .ordered: -// return "ordered" + // case .ordered: + // return "ordered" } } } @@ -42,8 +42,8 @@ extension ListType { switch self { case .bullet(let indent): return .bullet(indent) -// case .ordered: -// return .ordered + // case .ordered: + // return .ordered } } @@ -51,8 +51,8 @@ extension ListType { switch self { case .bullet: return .disc -// case .ordered: -// return .decimal + // case .ordered: + // return .decimal } } @@ -60,37 +60,36 @@ extension ListType { switch self { case .bullet(let indent): return indent ?? 0 -// case .ordered(let indent): -// return indent ?? 0 + // case .ordered(let indent): + // return indent ?? 0 } } -// func moveIndentForward() -> ListType { -// switch self { -// case .bullet(let indent): -// let newIndent = (indent ?? 0) + 1 -// return .bullet(newIndent) -// } -// } -// -// func moveIndentBackward() -> ListType { -// switch self { -// case .bullet(let indent): -// let newIndent = max(0, ((indent ?? 0) - 1)) -// return .bullet(newIndent) -// } -// } + // func moveIndentForward() -> ListType { + // switch self { + // case .bullet(let indent): + // let newIndent = (indent ?? 0) + 1 + // return .bullet(newIndent) + // } + // } + // + // func moveIndentBackward() -> ListType { + // switch self { + // case .bullet(let indent): + // let newIndent = max(0, ((indent ?? 0) - 1)) + // return .bullet(newIndent) + // } + // } } - #if canImport(UIKit) -import UIKit + import UIKit -typealias TextList = NSTextList + typealias TextList = NSTextList #endif #if canImport(AppKit) -import AppKit + import AppKit -typealias TextList = NSTextList + typealias TextList = NSTextList #endif diff --git a/Sources/RichEditorSwiftUI/Localization/RTEL10n.swift b/Sources/RichEditorSwiftUI/Localization/RTEL10n.swift index a5e788c..2f84cb6 100644 --- a/Sources/RichEditorSwiftUI/Localization/RTEL10n.swift +++ b/Sources/RichEditorSwiftUI/Localization/RTEL10n.swift @@ -11,154 +11,148 @@ import SwiftUI public enum RTEL10n: String, CaseIterable, Identifiable { case - done, - more, - - font, - fontSize, - fontSizeIncrease, - fontSizeIncreaseDescription, - fontSizeDecrease, - fontSizeDecreaseDescription, - - setHeaderStyle, - - color, - foregroundColor, - backgroundColor, - underlineColor, - strikethroughColor, - strokeColor, - - actionCopy, - actionDismissKeyboard, - actionPrint, - actionRedoLatestChange, - actionUndoLatestChange, - - fileFormatRtk, - fileFormatPdf, - fileFormatRtf, - fileFormatTxt, - fileFormatJson, - - indent, - indentIncrease, - indentIncreaseDescription, - indentDecrease, - indentDecreaseDescription, - - lineSpacing, - lineSpacingIncrease, - lineSpacingIncreaseDescription, - lineSpacingDecrease, - lineSpacingDecreaseDescription, - - menuExport, - menuExportAs, - menuFormat, - menuPrint, - menuSave, - menuSaveAs, - menuShare, - menuShareAs, - menuText, - - highlightedRange, - highlightingStyle, - - pasteImage, - pasteImages, - pasteText, - selectRange, - - setAttributedString, - - styleBold, - styleItalic, - styleStrikethrough, - styleUnderlined, - - superscript, - superscriptIncrease, - superscriptIncreaseDescription, - superscriptDecrease, - superscriptDecreaseDescription, - - textAlignment, - textAlignmentLeft, - textAlignmentRight, - textAlignmentCentered, - textAlignmentJustified, - - headerDefault, - header1, - header2, - header3, - header4, - header5, - header6, - - ignoreIt + done, + more, + + font, + fontSize, + fontSizeIncrease, + fontSizeIncreaseDescription, + fontSizeDecrease, + fontSizeDecreaseDescription, + + setHeaderStyle, + + color, + foregroundColor, + backgroundColor, + underlineColor, + strikethroughColor, + strokeColor, + + actionCopy, + actionDismissKeyboard, + actionPrint, + actionRedoLatestChange, + actionUndoLatestChange, + + fileFormatRtk, + fileFormatPdf, + fileFormatRtf, + fileFormatTxt, + fileFormatJson, + + indent, + indentIncrease, + indentIncreaseDescription, + indentDecrease, + indentDecreaseDescription, + + lineSpacing, + lineSpacingIncrease, + lineSpacingIncreaseDescription, + lineSpacingDecrease, + lineSpacingDecreaseDescription, + + menuExport, + menuExportAs, + menuFormat, + menuPrint, + menuSave, + menuSaveAs, + menuShare, + menuShareAs, + menuText, + + highlightedRange, + highlightingStyle, + + pasteImage, + pasteImages, + pasteText, + selectRange, + + setAttributedString, + + styleBold, + styleItalic, + styleStrikethrough, + styleUnderlined, + + superscript, + superscriptIncrease, + superscriptIncreaseDescription, + superscriptDecrease, + superscriptDecreaseDescription, + + textAlignment, + textAlignmentLeft, + textAlignmentRight, + textAlignmentCentered, + textAlignmentJustified, + + headerDefault, + header1, + header2, + header3, + header4, + header5, + header6, + + link, + + ignoreIt } -public extension RTEL10n { +extension RTEL10n { - static func actionStepFontSize( + public static func actionStepFontSize( _ points: Int ) -> RTEL10n { - points < 0 ? - .fontSizeDecreaseDescription : - .fontSizeIncreaseDescription + points < 0 ? .fontSizeDecreaseDescription : .fontSizeIncreaseDescription } - static func actionStepIndent( + public static func actionStepIndent( _ points: Double ) -> RTEL10n { - points < 0 ? - .indentDecreaseDescription : - .indentIncreaseDescription + points < 0 ? .indentDecreaseDescription : .indentIncreaseDescription } - static func actionStepLineSpacing( + public static func actionStepLineSpacing( _ points: CGFloat ) -> RTEL10n { - points < 0 ? - .lineSpacingDecreaseDescription : - .lineSpacingIncreaseDescription + points < 0 + ? .lineSpacingDecreaseDescription : .lineSpacingIncreaseDescription } - static func actionStepSuperscript( + public static func actionStepSuperscript( _ steps: Int ) -> RTEL10n { - steps < 0 ? - .superscriptDecreaseDescription : - .superscriptIncreaseDescription + steps < 0 + ? .superscriptDecreaseDescription : .superscriptIncreaseDescription } - static func menuIndent(_ points: Double) -> RTEL10n { - points < 0 ? - .indentDecrease : - .indentIncrease + public static func menuIndent(_ points: Double) -> RTEL10n { + points < 0 ? .indentDecrease : .indentIncrease } } -public extension RTEL10n { +extension RTEL10n { /// The item's unique identifier. - var id: String { rawValue } + public var id: String { rawValue } /// The item's localization key. - var key: String { rawValue } + public var key: String { rawValue } /// The item's localized text. - var text: String { + public var text: String { rawValue } /// Get the localized text for a certain `Locale`. -// func text(for locale: Locale) -> String { -// guard let bundle = Bundle.module.bundle(for: locale) else { return "" } -// return NSLocalizedString(key, bundle: bundle, comment: "") -// } + // func text(for locale: Locale) -> String { + // guard let bundle = Bundle.module.bundle(for: locale) else { return "" } + // return NSLocalizedString(key, bundle: bundle, comment: "") + // } } diff --git a/Sources/RichEditorSwiftUI/Pdf/PdfPageConfiguration.swift b/Sources/RichEditorSwiftUI/Pdf/PdfPageConfiguration.swift index 7e8c7da..3d29c65 100644 --- a/Sources/RichEditorSwiftUI/Pdf/PdfPageConfiguration.swift +++ b/Sources/RichEditorSwiftUI/Pdf/PdfPageConfiguration.swift @@ -19,7 +19,8 @@ public struct PdfPageConfiguration: Equatable { */ public init( pageSize: CGSize = CGSize(width: 595.2, height: 841.8), - pageMargins: PdfPageMargins = .init(all: 72)) { + pageMargins: PdfPageMargins = .init(all: 72) + ) { self.pageSize = pageSize self.pageMargins = pageMargins } @@ -31,21 +32,21 @@ public struct PdfPageConfiguration: Equatable { public var pageMargins: PdfPageMargins } -public extension PdfPageConfiguration { +extension PdfPageConfiguration { /// The standard PDF page configuration. - static var standard: Self { .init() } + public static var standard: Self { .init() } } -public extension PdfPageConfiguration { +extension PdfPageConfiguration { /// Get the paper rectangle. - var paperRect: CGRect { + public var paperRect: CGRect { CGRect(x: 0, y: 0, width: pageSize.width, height: pageSize.height) } /// Get the printable rectangle. - var printableRect: CGRect { + public var printableRect: CGRect { CGRect( x: pageMargins.left, y: pageMargins.top, diff --git a/Sources/RichEditorSwiftUI/Pdf/RichTextPdfDataReader.swift b/Sources/RichEditorSwiftUI/Pdf/RichTextPdfDataReader.swift index c88a178..4d1d8e7 100644 --- a/Sources/RichEditorSwiftUI/Pdf/RichTextPdfDataReader.swift +++ b/Sources/RichEditorSwiftUI/Pdf/RichTextPdfDataReader.swift @@ -7,19 +7,17 @@ import Foundation -/** - This protocol extends ``RichTextReader`` with functionality - for generating PDF data for the current rich text. - - The protocol is implemented by `NSAttributedString` as well - as other types in the library. - */ +/// This protocol extends ``RichTextReader`` with functionality +/// for generating PDF data for the current rich text. +/// +/// The protocol is implemented by `NSAttributedString` as well +/// as other types in the library. @preconcurrency @MainActor public protocol RichTextPdfDataReader: RichTextReader {} extension NSAttributedString: RichTextPdfDataReader {} -public extension RichTextPdfDataReader { +extension RichTextPdfDataReader { /** Generate PDF data from the current rich text. @@ -28,110 +26,126 @@ public extension RichTextPdfDataReader { calling this function on other platforms, it will throw a ``PdfDataError/unsupportedPlatform`` error. */ - func richTextPdfData(configuration: PdfPageConfiguration = .standard) throws -> Data { - #if iOS || os(visionOS) - try richText.iosPdfData(for: configuration) + public func richTextPdfData(configuration: PdfPageConfiguration = .standard) + throws -> Data + { + #if os(iOS) || os(visionOS) + try richText.iosPdfData(for: configuration) #elseif macOS - try richText.macosPdfData(for: configuration) + try richText.macosPdfData(for: configuration) #else - throw PdfDataError.unsupportedPlatform + throw PdfDataError.unsupportedPlatform #endif } } #if macOS -import AppKit - -@MainActor -private extension NSAttributedString { - - func macosPdfData(for configuration: PdfPageConfiguration) throws -> Data { - do { - let fileUrl = try macosPdfFileUrl() - let printInfo = try macosPdfPrintInfo( - for: configuration, - fileUrl: fileUrl) - - let scrollView = NSTextView.scrollableTextView() - scrollView.frame = configuration.paperRect - let textView = scrollView.documentView as? NSTextView ?? NSTextView() - sleepToPrepareTextView() - textView.textStorage?.setAttributedString(self) - - let printOperation = NSPrintOperation(view: textView, printInfo: printInfo) - printOperation.showsPrintPanel = false - printOperation.showsProgressPanel = false - printOperation.run() - - return try Data(contentsOf: fileUrl) - } catch { - throw(error) + import AppKit + + @MainActor + extension NSAttributedString { + + fileprivate func macosPdfData(for configuration: PdfPageConfiguration) + throws -> Data + { + do { + let fileUrl = try macosPdfFileUrl() + let printInfo = try macosPdfPrintInfo( + for: configuration, + fileUrl: fileUrl) + + let scrollView = NSTextView.scrollableTextView() + scrollView.frame = configuration.paperRect + let textView = + scrollView.documentView as? NSTextView ?? NSTextView() + sleepToPrepareTextView() + textView.textStorage?.setAttributedString(self) + + let printOperation = NSPrintOperation( + view: textView, printInfo: printInfo) + printOperation.showsPrintPanel = false + printOperation.showsProgressPanel = false + printOperation.run() + + return try Data(contentsOf: fileUrl) + } catch { + throw (error) + } } - } - func macosPdfFileUrl() throws -> URL { - let manager = FileManager.default - let cacheUrl = try manager.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) - return cacheUrl - .appendingPathComponent(UUID().uuidString) - .appendingPathExtension("pdf") - } + fileprivate func macosPdfFileUrl() throws -> URL { + let manager = FileManager.default + let cacheUrl = try manager.url( + for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, + create: true) + return + cacheUrl + .appendingPathComponent(UUID().uuidString) + .appendingPathExtension("pdf") + } - func macosPdfPrintInfo( - for configuration: PdfPageConfiguration, - fileUrl: URL) throws -> NSPrintInfo { - let printOpts: [NSPrintInfo.AttributeKey: Any] = [ - .jobDisposition: NSPrintInfo.JobDisposition.save, - .jobSavingURL: fileUrl] - let printInfo = NSPrintInfo(dictionary: printOpts) - printInfo.horizontalPagination = .fit - printInfo.verticalPagination = .automatic - printInfo.topMargin = configuration.pageMargins.top - printInfo.leftMargin = configuration.pageMargins.left - printInfo.rightMargin = configuration.pageMargins.right - printInfo.bottomMargin = configuration.pageMargins.bottom - printInfo.isHorizontallyCentered = false - printInfo.isVerticallyCentered = false - return printInfo - } + fileprivate func macosPdfPrintInfo( + for configuration: PdfPageConfiguration, + fileUrl: URL + ) throws -> NSPrintInfo { + let printOpts: [NSPrintInfo.AttributeKey: Any] = [ + .jobDisposition: NSPrintInfo.JobDisposition.save, + .jobSavingURL: fileUrl, + ] + let printInfo = NSPrintInfo(dictionary: printOpts) + printInfo.horizontalPagination = .fit + printInfo.verticalPagination = .automatic + printInfo.topMargin = configuration.pageMargins.top + printInfo.leftMargin = configuration.pageMargins.left + printInfo.rightMargin = configuration.pageMargins.right + printInfo.bottomMargin = configuration.pageMargins.bottom + printInfo.isHorizontallyCentered = false + printInfo.isVerticallyCentered = false + return printInfo + } - func sleepToPrepareTextView() { - Thread.sleep(forTimeInterval: 0.1) + fileprivate func sleepToPrepareTextView() { + Thread.sleep(forTimeInterval: 0.1) + } } -} #endif -#if iOS || os(visionOS) -import UIKit - -@MainActor -private extension NSAttributedString { - - func iosPdfData(for configuration: PdfPageConfiguration) throws -> Data { - let pageRenderer = iosPdfPageRenderer(for: configuration) - let paperRect = configuration.paperRect - let pdfData = NSMutableData() - UIGraphicsBeginPDFContextToData(pdfData, paperRect, nil) - let range = NSRange(location: 0, length: pageRenderer.numberOfPages) - pageRenderer.prepare(forDrawingPages: range) - let bounds = UIGraphicsGetPDFContextBounds() - for i in 0 ..< pageRenderer.numberOfPages { - UIGraphicsBeginPDFPage() - pageRenderer.drawPage(at: i, in: bounds) +#if os(iOS) || os(visionOS) + import UIKit + + @MainActor + extension NSAttributedString { + + fileprivate func iosPdfData(for configuration: PdfPageConfiguration) + throws -> Data + { + let pageRenderer = iosPdfPageRenderer(for: configuration) + let paperRect = configuration.paperRect + let pdfData = NSMutableData() + UIGraphicsBeginPDFContextToData(pdfData, paperRect, nil) + let range = NSRange(location: 0, length: pageRenderer.numberOfPages) + pageRenderer.prepare(forDrawingPages: range) + let bounds = UIGraphicsGetPDFContextBounds() + for i in 0.. UIPrintPageRenderer { - let printFormatter = UISimpleTextPrintFormatter(attributedText: self) - let paperRect = NSValue(cgRect: configuration.paperRect) - let printableRect = NSValue(cgRect: configuration.printableRect) - let pageRenderer = UIPrintPageRenderer() - pageRenderer.addPrintFormatter(printFormatter, startingAtPageAt: 0) - pageRenderer.setValue(paperRect, forKey: "paperRect") - pageRenderer.setValue(printableRect, forKey: "printableRect") - return pageRenderer + fileprivate func iosPdfPageRenderer( + for configuration: PdfPageConfiguration + ) -> UIPrintPageRenderer { + let printFormatter = UISimpleTextPrintFormatter( + attributedText: self) + let paperRect = NSValue(cgRect: configuration.paperRect) + let printableRect = NSValue(cgRect: configuration.printableRect) + let pageRenderer = UIPrintPageRenderer() + pageRenderer.addPrintFormatter(printFormatter, startingAtPageAt: 0) + pageRenderer.setValue(paperRect, forKey: "paperRect") + pageRenderer.setValue(printableRect, forKey: "printableRect") + return pageRenderer + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Button.swift b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Button.swift new file mode 100644 index 0000000..e089b3b --- /dev/null +++ b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Button.swift @@ -0,0 +1,86 @@ +// +// RichTextOtherMenu+Button.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 19/12/24. +// + +import SwiftUI + +extension RichTextOtherMenu { + + /** + This button can be used to toggle a ``RichTextOtherMenu``. + + This view renders a plain `Button`, which means you can + use and configure with plain SwiftUI. + */ + public struct Button: View { + + /** + Create a rich text style button. + + - Parameters: + - style: The style to toggle. + - value: The value to bind to. + - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`. + */ + public init( + style: RichTextOtherMenu, + value: Binding, + fillVertically: Bool = false + ) { + self.style = style + self.value = value + self.fillVertically = fillVertically + } + + /** + Create a rich text style button. + + - Parameters: + - style: The style to toggle. + - context: The context to affect. + - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`. + */ + public init( + style: RichTextOtherMenu, + context: RichEditorState, + fillVertically: Bool = false + ) { + self.init( + style: style, + value: context.binding(for: style), + fillVertically: fillVertically + ) + } + + private let style: RichTextOtherMenu + private let value: Binding + private let fillVertically: Bool + + public var body: some View { + SwiftUI.Button(action: toggle) { + style.label + .labelStyle(.iconOnly) + .frame(maxHeight: fillVertically ? .infinity : nil) + .contentShape(Rectangle()) + } + .tint(.accentColor, if: isOn) + .foreground(.accentColor, if: isOn) + // .keyboardShortcut(for: style) + .accessibilityLabel(style.title) + } + } +} + +extension RichTextOtherMenu.Button { + + fileprivate var isOn: Bool { + value.wrappedValue + } + + fileprivate func toggle() { + value.wrappedValue.toggle() + } +} diff --git a/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Toggle.swift b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Toggle.swift new file mode 100644 index 0000000..0edf84c --- /dev/null +++ b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+Toggle.swift @@ -0,0 +1,87 @@ +// +// RichTextOtherMenu+Toggle.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 19/12/24. +// + +import SwiftUI + +extension RichTextOtherMenu { + + /** + This toggle can be used to toggle a ``RichTextOtherMenu``. + + This view renders a plain `Toggle`, which means you can + use and configure with plain SwiftUI. The one exception + is the tint color, which is set with a style. + */ + public struct Toggle: View { + + /** + Create a rich text style toggle toggle. + + - Parameters: + - style: The style to toggle. + - value: The value to bind to. + - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`. + */ + public init( + style: RichTextOtherMenu, + value: Binding, + fillVertically: Bool = false + ) { + self.style = style + self.value = value + self.fillVertically = fillVertically + } + + /** + Create a rich text style toggle. + + - Parameters: + - style: The style to toggle. + - context: The context to affect. + - fillVertically: Whether or not fill up vertical space in a non-greedy way, by default `false`. + */ + public init( + style: RichTextOtherMenu, + context: RichEditorState, + fillVertically: Bool = false + ) { + self.init( + style: style, + value: context.binding(for: style), + fillVertically: fillVertically + ) + } + + private let style: RichTextOtherMenu + private let value: Binding + private let fillVertically: Bool + + public var body: some View { + #if os(tvOS) || os(watchOS) + toggle + #else + toggle.toggleStyle(.button) + #endif + } + + private var toggle: some View { + SwiftUI.Toggle(isOn: value) { + style.icon + .frame(maxHeight: fillVertically ? .infinity : nil) + } + // .keyboardShortcut(for: style) + .accessibilityLabel(style.title) + } + } +} + +extension RichTextOtherMenu.Toggle { + + fileprivate var isOn: Bool { + value.wrappedValue + } +} diff --git a/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleGroup.swift b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleGroup.swift new file mode 100644 index 0000000..a0f2124 --- /dev/null +++ b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleGroup.swift @@ -0,0 +1,80 @@ +// +// RichTextOtherMenu+ToggleGroup.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 19/12/24. +// + +#if os(iOS) || os(macOS) || os(visionOS) + import SwiftUI + + extension RichTextOtherMenu { + + /** + This view can list ``RichTextOtherMenu/Toggle``s for a list + of ``RichTextOtherMenu`` values, in a bordered button group. + + Since this view uses multiple styles, it binds directly + to a ``RichTextContext`` instead of individual values. + + > Important: Since the `ControlGroup` doesn't highlight + buttons in iOS, we use a `ToggleStack` for iOS. + */ + public struct ToggleGroup: View { + + /** + Create a rich text style toggle button group. + + - Parameters: + - context: The context to affect. + - styles: The styles to list, by default ``RichTextOtherMenu/all``. + - greedy: Whether or not the group is horizontally greedy, by default `true`. + */ + public init( + context: RichEditorState, + styles: [RichTextOtherMenu] = .all, + greedy: Bool = true + ) { + self._context = ObservedObject(wrappedValue: context) + self.isGreedy = greedy + self.styles = styles + } + + private let styles: [RichTextOtherMenu] + private let isGreedy: Bool + + private var groupWidth: CGFloat? { + if isGreedy { return nil } + let count = Double(styles.count) + #if macOS + return 30 * count + #else + return 50 * count + #endif + } + + @ObservedObject + private var context: RichEditorState + + public var body: some View { + #if macOS + ControlGroup { + ForEach(styles) { + RichTextOtherMenu.Toggle( + style: $0, + context: context, + fillVertically: true + ) + } + } + .frame(width: groupWidth) + #else + RichTextOtherMenu.ToggleStack( + context: context, + styles: styles + ) + #endif + } + } + } +#endif diff --git a/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleStack.swift b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleStack.swift new file mode 100644 index 0000000..e4da70d --- /dev/null +++ b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu+ToggleStack.swift @@ -0,0 +1,58 @@ +// +// File.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 19/12/24. +// + +import SwiftUI + +extension RichTextOtherMenu { + + /** + This view can list ``RichTextOtherMenu/Toggle``s for a list + of ``RichTextOtherMenu`` values, in a horizontal stack. + + Since this view uses multiple styles, it binds directly + to a ``RichTextContext`` instead of individual values. + */ + public struct ToggleStack: View { + + /** + Create a rich text style toggle button group. + + - Parameters: + - context: The context to affect. + - styles: The styles to list, by default ``RichTextOtherMenu/all``. + - spacing: The spacing to apply to stack items, by default `5`. + */ + public init( + context: RichEditorState, + styles: [RichTextOtherMenu] = .all, + spacing: Double = 5 + ) { + self._context = ObservedObject(wrappedValue: context) + self.styles = styles + self.spacing = spacing + } + + private let styles: [RichTextOtherMenu] + private let spacing: Double + + @ObservedObject + private var context: RichEditorState + + public var body: some View { + HStack(spacing: spacing) { + ForEach(styles) { + RichTextOtherMenu.Toggle( + style: $0, + context: context, + fillVertically: true + ) + } + } + .fixedSize(horizontal: false, vertical: true) + } + } +} diff --git a/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu.swift b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu.swift new file mode 100644 index 0000000..b9dadba --- /dev/null +++ b/Sources/RichEditorSwiftUI/RichTextOtherMenu/RichTextOtherMenu.swift @@ -0,0 +1,77 @@ +// +// RichTextOtherMenu.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 19/12/24. +// + +import SwiftUI + +public enum RichTextOtherMenu: String, CaseIterable, Identifiable, + RichTextLabelValue +{ + case link + +} + +extension RichTextOtherMenu { + + /// All available rich text styles. + public static var all: [Self] { allCases } +} + +extension Collection where Element == RichTextOtherMenu { + + /// All available rich text styles. + public static var all: [RichTextOtherMenu] { RichTextOtherMenu.allCases } +} + +extension RichTextOtherMenu { + + public var id: String { rawValue } + + /// The standard icon to use for the trait. + public var icon: Image { + switch self { + case .link: .richTextLink + } + } + + /// The localized style title. + public var title: String { + titleKey.text + } + + /// The localized style title key. + public var titleKey: RTEL10n { + switch self { + case .link: .link + } + } +} + +extension Collection where Element == RichTextOtherMenu { + + /// Check if the collection contains a certain style. + public func hasStyle(_ style: RichTextOtherMenu) -> Bool { + contains(style) + } + + /// Check if a certain style change should be applied. + public func shouldAddOrRemove( + _ style: RichTextOtherMenu, + _ newValue: Bool + ) -> Bool { + let shouldAdd = newValue && !hasStyle(style) + let shouldRemove = !newValue && hasStyle(style) + return shouldAdd || shouldRemove + } +} + +extension RichTextOtherMenu { + func richTextSpanStyle() -> RichTextSpanStyle { + switch self { + case .link: .link() + } + } +} diff --git a/Sources/RichEditorSwiftUI/Styles/RichTextAction+ButtonStack.swift b/Sources/RichEditorSwiftUI/Styles/RichTextAction+ButtonStack.swift index 60529a8..6921b2f 100644 --- a/Sources/RichEditorSwiftUI/Styles/RichTextAction+ButtonStack.swift +++ b/Sources/RichEditorSwiftUI/Styles/RichTextAction+ButtonStack.swift @@ -7,7 +7,7 @@ import SwiftUI -public extension RichTextAction { +extension RichTextAction { /** This view lists ``RichTextAction`` buttons in a stack. @@ -15,7 +15,7 @@ public extension RichTextAction { Since this view uses multiple values, it binds directly to a ``RichTextContext`` instead of individual values. */ - struct ButtonStack: View { + public struct ButtonStack: View { /** Create a rich text action button stack. diff --git a/Sources/RichEditorSwiftUI/Styles/RichTextHighlightingStyle.swift b/Sources/RichEditorSwiftUI/Styles/RichTextHighlightingStyle.swift index dc28983..9aeb357 100644 --- a/Sources/RichEditorSwiftUI/Styles/RichTextHighlightingStyle.swift +++ b/Sources/RichEditorSwiftUI/Styles/RichTextHighlightingStyle.swift @@ -7,9 +7,7 @@ import SwiftUI -/** - This struct can be used to style rich text highlighting. - */ +/// This struct can be used to style rich text highlighting. public struct RichTextHighlightingStyle: Equatable, Hashable { /** @@ -34,10 +32,10 @@ public struct RichTextHighlightingStyle: Equatable, Hashable { public let foregroundColor: Color } -public extension RichTextHighlightingStyle { +extension RichTextHighlightingStyle { /// The standard rich text highlighting style. /// /// You can set a new value to change the global default. - static var standard = Self() + public static var standard = Self() } diff --git a/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Button.swift b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Button.swift index 7a2e640..6f06f1e 100644 --- a/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Button.swift +++ b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Button.swift @@ -7,7 +7,7 @@ import SwiftUI -public extension RichTextStyle { +extension RichTextStyle { /** This button can be used to toggle a ``RichTextStyle``. @@ -15,7 +15,7 @@ public extension RichTextStyle { This view renders a plain `Button`, which means you can use and configure with plain SwiftUI. */ - struct Button: View { + public struct Button: View { /** Create a rich text style button. @@ -95,13 +95,13 @@ extension View { } } -private extension RichTextStyle.Button { +extension RichTextStyle.Button { - var isOn: Bool { + fileprivate var isOn: Bool { value.wrappedValue } - func toggle() { + fileprivate func toggle() { value.wrappedValue.toggle() } } diff --git a/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Toggle.swift b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Toggle.swift index ecf0a13..9f8ef5c 100644 --- a/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Toggle.swift +++ b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+Toggle.swift @@ -7,7 +7,7 @@ import SwiftUI -public extension RichTextStyle { +extension RichTextStyle { /** This toggle can be used to toggle a ``RichTextStyle``. @@ -16,7 +16,7 @@ public extension RichTextStyle { use and configure with plain SwiftUI. The one exception is the tint color, which is set with a style. */ - struct Toggle: View { + public struct Toggle: View { /** Create a rich text style toggle toggle. @@ -62,9 +62,9 @@ public extension RichTextStyle { public var body: some View { #if os(tvOS) || os(watchOS) - toggle + toggle #else - toggle.toggleStyle(.button) + toggle.toggleStyle(.button) #endif } @@ -79,10 +79,9 @@ public extension RichTextStyle { } } -private extension RichTextStyle.Toggle { +extension RichTextStyle.Toggle { - var isOn: Bool { + fileprivate var isOn: Bool { value.wrappedValue } } - diff --git a/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleGroup.swift b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleGroup.swift index a4767b4..292c1c7 100644 --- a/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleGroup.swift +++ b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleGroup.swift @@ -5,12 +5,12 @@ // Created by Divyesh Vekariya on 22/11/24. // -#if iOS || macOS || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(visionOS) + import SwiftUI -public extension RichTextStyle { + extension RichTextStyle { - /** + /** This view can list ``RichTextStyle/Toggle``s for a list of ``RichTextStyle`` values, in a bordered button group. @@ -20,9 +20,9 @@ public extension RichTextStyle { > Important: Since the `ControlGroup` doesn't highlight buttons in iOS, we use a `ToggleStack` for iOS. */ - struct ToggleGroup: View { + public struct ToggleGroup: View { - /** + /** Create a rich text style toggle button group. - Parameters: @@ -30,51 +30,51 @@ public extension RichTextStyle { - styles: The styles to list, by default ``RichTextStyle/all``. - greedy: Whether or not the group is horizontally greedy, by default `true`. */ - public init( - context: RichEditorState, - styles: [RichTextStyle] = .all, - greedy: Bool = true - ) { - self._context = ObservedObject(wrappedValue: context) - self.isGreedy = greedy - self.styles = styles - } + public init( + context: RichEditorState, + styles: [RichTextStyle] = .all, + greedy: Bool = true + ) { + self._context = ObservedObject(wrappedValue: context) + self.isGreedy = greedy + self.styles = styles + } - private let styles: [RichTextStyle] - private let isGreedy: Bool + private let styles: [RichTextStyle] + private let isGreedy: Bool - private var groupWidth: CGFloat? { - if isGreedy { return nil } - let count = Double(styles.count) - #if macOS - return 30 * count - #else - return 50 * count - #endif - } + private var groupWidth: CGFloat? { + if isGreedy { return nil } + let count = Double(styles.count) + #if macOS + return 30 * count + #else + return 50 * count + #endif + } - @ObservedObject - private var context: RichEditorState + @ObservedObject + private var context: RichEditorState - public var body: some View { - #if macOS - ControlGroup { - ForEach(styles) { - RichTextStyle.Toggle( - style: $0, + public var body: some View { + #if macOS + ControlGroup { + ForEach(styles) { + RichTextStyle.Toggle( + style: $0, + context: context, + fillVertically: true + ) + } + } + .frame(width: groupWidth) + #else + RichTextStyle.ToggleStack( context: context, - fillVertically: true + styles: styles ) - } + #endif } - .frame(width: groupWidth) - #else - RichTextStyle.ToggleStack( - context: context, - styles: styles - ) - #endif } } -} #endif diff --git a/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleStack.swift b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleStack.swift index 191d04b..bbff362 100644 --- a/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleStack.swift +++ b/Sources/RichEditorSwiftUI/Styles/RichTextStyle+ToggleStack.swift @@ -7,7 +7,7 @@ import SwiftUI -public extension RichTextStyle { +extension RichTextStyle { /** This view can list ``RichTextStyle/Toggle``s for a list @@ -16,7 +16,7 @@ public extension RichTextStyle { Since this view uses multiple styles, it binds directly to a ``RichTextContext`` instead of individual values. */ - struct ToggleStack: View { + public struct ToggleStack: View { /** Create a rich text style toggle button group. diff --git a/Sources/RichEditorSwiftUI/Styles/RichTextStyle.swift b/Sources/RichEditorSwiftUI/Styles/RichTextStyle.swift index 79e840a..d1779d0 100644 --- a/Sources/RichEditorSwiftUI/Styles/RichTextStyle.swift +++ b/Sources/RichEditorSwiftUI/Styles/RichTextStyle.swift @@ -7,7 +7,9 @@ import SwiftUI -public enum RichTextStyle: String, CaseIterable, Identifiable, RichTextLabelValue { +public enum RichTextStyle: String, CaseIterable, Identifiable, + RichTextLabelValue +{ case bold case italic @@ -15,24 +17,24 @@ public enum RichTextStyle: String, CaseIterable, Identifiable, RichTextLabelValu case strikethrough } -public extension RichTextStyle { +extension RichTextStyle { /// All available rich text styles. - static var all: [Self] { allCases } + public static var all: [Self] { allCases } } -public extension Collection where Element == RichTextStyle { +extension Collection where Element == RichTextStyle { /// All available rich text styles. - static var all: [RichTextStyle] { RichTextStyle.allCases } + public static var all: [RichTextStyle] { RichTextStyle.allCases } } -public extension RichTextStyle { +extension RichTextStyle { - var id: String { rawValue } + public var id: String { rawValue } /// The standard icon to use for the trait. - var icon: Image { + public var icon: Image { switch self { case .bold: .richTextStyleBold case .italic: .richTextStyleItalic @@ -42,12 +44,12 @@ public extension RichTextStyle { } /// The localized style title. - var title: String { + public var title: String { titleKey.text } /// The localized style title key. - var titleKey: RTEL10n { + public var titleKey: RTEL10n { switch self { case .bold: .styleBold case .italic: .styleItalic @@ -64,7 +66,7 @@ public extension RichTextStyle { - traits: The symbolic traits to inspect. - attributes: The rich text attributes to inspect. */ - static func styles( + public static func styles( in traits: FontTraitsRepresentable?, attributes: RichTextAttributes? ) -> [RichTextStyle] { @@ -75,15 +77,15 @@ public extension RichTextStyle { } } -public extension Collection where Element == RichTextStyle { +extension Collection where Element == RichTextStyle { /// Check if the collection contains a certain style. - func hasStyle(_ style: RichTextStyle) -> Bool { + public func hasStyle(_ style: RichTextStyle) -> Bool { contains(style) } /// Check if a certain style change should be applied. - func shouldAddOrRemove( + public func shouldAddOrRemove( _ style: RichTextStyle, _ newValue: Bool ) -> Bool { @@ -94,36 +96,35 @@ public extension Collection where Element == RichTextStyle { } #if canImport(UIKit) -public extension RichTextStyle { - - /// The symbolic font traits for the style, if any. - var symbolicTraits: UIFontDescriptor.SymbolicTraits? { - switch self { - case .bold: .traitBold - case .italic: .traitItalic - case .strikethrough: nil - case .underline: nil + extension RichTextStyle { + + /// The symbolic font traits for the style, if any. + public var symbolicTraits: UIFontDescriptor.SymbolicTraits? { + switch self { + case .bold: .traitBold + case .italic: .traitItalic + case .strikethrough: nil + case .underline: nil + } } } -} #endif #if macOS -public extension RichTextStyle { - - /// The symbolic font traits for the trait, if any. - var symbolicTraits: NSFontDescriptor.SymbolicTraits? { - switch self { - case .bold: .bold - case .italic: .italic - case .strikethrough: nil - case .underline: nil + extension RichTextStyle { + + /// The symbolic font traits for the trait, if any. + public var symbolicTraits: NSFontDescriptor.SymbolicTraits? { + switch self { + case .bold: .bold + case .italic: .italic + case .strikethrough: nil + case .underline: nil + } } } -} #endif - extension RichTextStyle { var richTextSpanStyle: RichTextSpanStyle { switch self { diff --git a/Sources/RichEditorSwiftUI/Styles/View+RichTextStyle.swift b/Sources/RichEditorSwiftUI/Styles/View+RichTextStyle.swift index 1b27031..f710f5e 100644 --- a/Sources/RichEditorSwiftUI/Styles/View+RichTextStyle.swift +++ b/Sources/RichEditorSwiftUI/Styles/View+RichTextStyle.swift @@ -7,7 +7,7 @@ import SwiftUI -public extension View { +extension View { /** Add a keyboard shortcut that toggles a certain style. @@ -16,16 +16,16 @@ public extension View { keyboard shortcuts. */ @ViewBuilder - func keyboardShortcut(for style: RichTextStyle) -> some View { - #if iOS || macOS || os(visionOS) - switch style { - case .bold: keyboardShortcut("b", modifiers: .command) - case .italic: keyboardShortcut("i", modifiers: .command) - case .strikethrough: self - case .underline: keyboardShortcut("u", modifiers: .command) - } + public func keyboardShortcut(for style: RichTextStyle) -> some View { + #if os(iOS) || os(macOS) || os(visionOS) + switch style { + case .bold: keyboardShortcut("b", modifiers: .command) + case .italic: keyboardShortcut("i", modifiers: .command) + case .strikethrough: self + case .underline: keyboardShortcut("u", modifiers: .command) + } #else - self + self #endif } } diff --git a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Header.swift b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Header.swift index 664a217..e77428f 100644 --- a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Header.swift +++ b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Header.swift @@ -7,10 +7,10 @@ import SwiftUI -public extension RichEditorState { +extension RichEditorState { /// Get a binding for a certain style. - func headerBinding() -> Binding { + public func headerBinding() -> Binding { Binding( get: { self.currentHeader() }, set: { self.setHeaderStyle($0) } @@ -18,12 +18,12 @@ public extension RichEditorState { } /// Check whether or not the context has a certain header style. - func currentHeader() -> HeaderType { + public func currentHeader() -> HeaderType { return headerType } /// Set whether or not the context has a certain header style. - func setHeaderStyle( + public func setHeaderStyle( _ header: HeaderType ) { guard header != headerType else { return } @@ -31,4 +31,3 @@ public extension RichEditorState { } } - diff --git a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Link.swift b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Link.swift new file mode 100644 index 0000000..615e94f --- /dev/null +++ b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Link.swift @@ -0,0 +1,56 @@ +// +// RichEditorState+Link.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 19/12/24. +// + +import SwiftUI + +extension RichEditorState { + func insertLink(value: Bool) { + if link != nil { + alertController.showAlert( + title: "Remove link", message: "It will remove link from selected text", + onOk: { [weak self] in + guard let self else { return } + self.updateStyle(style: .link()) + }, + onCancel: { + return + }) + } else { + alertController.showAlert( + title: "Enter url", message: "", placeholder: "Enter link", + defaultText: "", + onTextChange: { text in + }, + completion: { [weak self] finalText in + self?.updateStyle(style: .link(finalText)) + }) + } + } +} + +extension RichEditorState { + + /// Get a binding for a certain style. + public func binding(for style: RichTextOtherMenu) -> Binding { + Binding( + get: { Bool(self.hasStyle(style)) }, + set: { self.setLink(to: $0) } + ) + } + + /// Check whether or not the context has a certain style. + public func hasStyle(_ style: RichTextOtherMenu) -> Bool { + link != nil + } + + /// Set whether or not the context has a certain style. + public func setLink( + to val: Bool + ) { + insertLink(value: val) + } +} diff --git a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Styles.swift b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Styles.swift index 9c02cf1..5ab990b 100644 --- a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Styles.swift +++ b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+Styles.swift @@ -7,23 +7,23 @@ import SwiftUI -public extension RichEditorState { +extension RichEditorState { /// Get a binding for a certain style. - func binding(for style: RichTextStyle) -> Binding { + public func binding(for style: RichTextStyle) -> Binding { Binding( get: { Bool(self.hasStyle(style)) }, - set: { [weak self]_ in self?.setStyle(style) } + set: { [weak self] _ in self?.setStyle(style) } ) } /// Check whether or not the context has a certain style. - func hasStyle(_ style: RichTextStyle) -> Bool { + public func hasStyle(_ style: RichTextStyle) -> Bool { styles[style] == true } /// Set whether or not the context has a certain style. - func setStyle( + public func setStyle( _ style: RichTextStyle, to val: Bool ) { @@ -33,11 +33,11 @@ public extension RichEditorState { } /// Toggle a certain style for the context. - func toggleStyle(_ style: RichTextStyle) { + public func toggleStyle(_ style: RichTextStyle) { setStyle(style, to: !hasStyle(style)) } - func setStyle(_ style: RichTextStyle) { + public func setStyle(_ style: RichTextStyle) { toggleStyle(style: style.richTextSpanStyle) } } @@ -49,8 +49,10 @@ extension RichEditorState { _ style: RichTextStyle, to val: Bool? ) { - guard let val else { return styles[style] = nil } + guard let val else { + styles[style] = nil + return + } styles[style] = val } } - diff --git a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+TextAlignment.swift b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+TextAlignment.swift index 01c1709..a4cf43f 100644 --- a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+TextAlignment.swift +++ b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState+TextAlignment.swift @@ -7,10 +7,10 @@ import SwiftUI -public extension RichEditorState { +extension RichEditorState { /// Get a binding for a certain TextAlignment style. - func textAlignmentBinding() -> Binding { + public func textAlignmentBinding() -> Binding { Binding( get: { self.currentTextAlignment() }, set: { self.setTextAlignmentStyle($0) } @@ -18,12 +18,12 @@ public extension RichEditorState { } /// Check whether or not the context has a certain TextAlignment style. - func currentTextAlignment() -> RichTextAlignment { + public func currentTextAlignment() -> RichTextAlignment { return textAlignment } /// Set whether or not the context has a certain TextAlignment style. - func setTextAlignmentStyle( + public func setTextAlignmentStyle( _ alignment: RichTextAlignment ) { guard alignment != textAlignment else { return } @@ -31,11 +31,10 @@ public extension RichEditorState { setTextAlignmentInternal(alignment: alignment) } - func setTextAlignmentInternal( + public func setTextAlignmentInternal( alignment: RichTextAlignment ) { guard alignment != textAlignment else { return } textAlignment = alignment } } - diff --git a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift index 825bcf1..85c0b46 100644 --- a/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift +++ b/Sources/RichEditorSwiftUI/UI/Context/RichEditorState.swift @@ -5,19 +5,17 @@ // Created by Divyesh Vekariya on 21/10/24. // -import SwiftUI import Combine +import SwiftUI -/** - This observable context can be used to affect and observe a - ``RichTextEditor`` and its native text view. - - Use ``handle(_:)`` to trigger actions, e.g. to change fonts, - text styles, text alignments, select a text range, etc. - - You can use ``RichEditorState/FocusedValueKey`` to handle a - context with focus in a multi-windowed app. - */ +/// This observable context can be used to affect and observe a +/// ``RichTextEditor`` and its native text view. +/// +/// Use ``handle(_:)`` to trigger actions, e.g. to change fonts, +/// text styles, text alignments, select a text range, etc. +/// +/// You can use ``RichEditorState/FocusedValueKey`` to handle a +/// context with focus in a multi-windowed app. public class RichEditorState: ObservableObject { /// Create a new rich text context instance. @@ -92,7 +90,8 @@ public class RichEditorState: ObservableObject { /// The style to apply when highlighting a range. @Published - public internal(set) var highlightingStyle = RichTextHighlightingStyle.standard + public internal(set) var highlightingStyle = RichTextHighlightingStyle + .standard /// The current paragraph style. @Published @@ -102,6 +101,9 @@ public class RichEditorState: ObservableObject { @Published public internal(set) var styles = [RichTextStyle: Bool]() + @Published + public internal(set) var link: String? = nil + // MARK: - Properties /// This publisher can emit actions to the coordinator. @@ -110,18 +112,20 @@ public class RichEditorState: ObservableObject { /// The currently highlighted range, if any. public var highlightedRange: NSRange? -//MARK: - Variables To Handle JSON + //MARK: - Variables To Handle JSON internal var adapter: EditorAdapter = DefaultAdapter() @Published internal var activeStyles: Set = [] - @Published internal var activeAttributes: [NSAttributedString.Key: Any]? = [:] - + @Published internal var activeAttributes: [NSAttributedString.Key: Any]? = + [:] internal var internalSpans: [RichTextSpanInternal] = [] internal var rawText: String = "" - internal var updateAttributesQueue: [(span: RichTextSpanInternal, shouldApply: Bool)] = [] + internal var updateAttributesQueue: + [(span: RichTextSpanInternal, shouldApply: Bool)] = [] + internal let alertController: AlertController = AlertController() /** This will provide encoded text which is of type RichText @@ -131,7 +135,11 @@ public class RichEditorState: ObservableObject { } internal var spans: RichTextSpans { - return internalSpans.map({ .init(insert: getStringWith(from: $0.from, to: $0.to), attributes: $0.attributes) }) + return internalSpans.map({ + .init( + insert: getStringWith(from: $0.from, to: $0.to), + attributes: $0.attributes) + }) } var internalRichText: RichText = .init() @@ -145,9 +153,10 @@ public class RichEditorState: ObservableObject { var tempSpans: [RichTextSpanInternal] = [] var text = "" richText.spans.forEach({ - let span = RichTextSpanInternal(from: text.utf16Length, - to: (text.utf16Length + $0.insert.utf16Length - 1), - attributes: $0.attributes) + let span = RichTextSpanInternal( + from: text.utf16Length, + to: (text.utf16Length + $0.insert.utf16Length - 1), + attributes: $0.attributes) tempSpans.append(span) text += $0.insert }) @@ -155,15 +164,18 @@ public class RichEditorState: ObservableObject { let str = NSMutableAttributedString(string: text) tempSpans.forEach { span in - str.addAttributes(span.attributes?.toAttributes(font: .standardRichTextFont) ?? [:], range: span.spanRange) + str.addAttributes( + span.attributes?.toAttributes(font: .standardRichTextFont) + ?? [:], range: span.spanRange) if span.attributes?.color == nil { var color: ColorRepresentable = .clear #if os(watchOS) - color = .black + color = .black #else - color = RichTextView.Theme.standard.fontColor + color = RichTextView.Theme.standard.fontColor #endif - str.addAttributes([.foregroundColor: color], range: span.spanRange) + str.addAttributes( + [.foregroundColor: color], range: span.spanRange) } } @@ -187,10 +199,16 @@ public class RichEditorState: ObservableObject { let str = NSMutableAttributedString(string: input) - str.addAttributes([.font: FontRepresentable.standardRichTextFont], range: str.richTextRange) + str.addAttributes( + [.font: FontRepresentable.standardRichTextFont], + range: str.richTextRange) self.attributedString = str - self.internalSpans = [.init(from: 0, to: input.utf16Length > 0 ? input.utf16Length - 1 : 0, attributes: RichAttributes())] + self.internalSpans = [ + .init( + from: 0, to: input.utf16Length > 0 ? input.utf16Length - 1 : 0, + attributes: RichAttributes()) + ] selectedRange = NSRange(location: 0, length: 0) activeStyles = [] @@ -199,68 +217,67 @@ public class RichEditorState: ObservableObject { } } -public extension RichEditorState { +extension RichEditorState { /// Whether or not the context has a selected range. - var hasHighlightedRange: Bool { + public var hasHighlightedRange: Bool { highlightedRange != nil } /// Whether or not the context has a selected range. - var hasSelectedRange: Bool { + public var hasSelectedRange: Bool { selectedRange.length > 0 } } -public extension RichEditorState { +extension RichEditorState { /// Set ``highlightedRange`` to a new, optional range. - func highlightRange(_ range: NSRange?) { + public func highlightRange(_ range: NSRange?) { actionPublisher.send(.setHighlightedRange(range)) highlightedRange = range } /// Reset the attributed string. - func resetAttributedString() { + public func resetAttributedString() { setAttributedString(to: "") } /// Reset the ``highlightedRange``. - func resetHighlightedRange() { + public func resetHighlightedRange() { guard hasHighlightedRange else { return } highlightedRange = nil } /// Reset the ``selectedRange``. - func resetSelectedRange() { + public func resetSelectedRange() { selectedRange = NSRange() } /// Set a new range and start editing. - func selectRange(_ range: NSRange) { + public func selectRange(_ range: NSRange) { isEditingText = true actionPublisher.send(.selectRange(range)) } /// Set the attributed string to a new plain text. - func setAttributedString(to text: String) { + public func setAttributedString(to text: String) { setAttributedString(to: NSAttributedString(string: text)) } /// Set the attributed string to a new rich text. - func setAttributedString(to string: NSAttributedString) { + public func setAttributedString(to string: NSAttributedString) { let mutable = NSMutableAttributedString(attributedString: string) actionPublisher.send(.setAttributedString(mutable)) } /// Set ``isEditingText`` to `false`. - func stopEditingText() { + public func stopEditingText() { isEditingText = false } /// Toggle whether or not the text is being edited. - func toggleIsEditing() { + public func toggleIsEditing() { isEditingText.toggle() } } - diff --git a/Sources/RichEditorSwiftUI/UI/Context/RichTextContext+Actions.swift b/Sources/RichEditorSwiftUI/UI/Context/RichTextContext+Actions.swift index 93ff750..14764c8 100644 --- a/Sources/RichEditorSwiftUI/UI/Context/RichTextContext+Actions.swift +++ b/Sources/RichEditorSwiftUI/UI/Context/RichTextContext+Actions.swift @@ -7,25 +7,25 @@ import SwiftUI -public extension RichEditorState { +extension RichEditorState { /// Handle a certain rich text action. - func handle(_ action: RichTextAction) { + public func handle(_ action: RichTextAction) { switch action { -// case .stepFontSize(let size): -// fontSize += CGFloat(size) -// updateStyle(style: .size(Int(fontSize))) + // case .stepFontSize(let size): + // fontSize += CGFloat(size) + // updateStyle(style: .size(Int(fontSize))) default: actionPublisher.send(action) } } /// Check if the context can handle a certain action. - func canHandle(_ action: RichTextAction) -> Bool { + public func canHandle(_ action: RichTextAction) -> Bool { switch action { case .copy: canCopy - // case .pasteImage: true - // case .pasteImages: true - // case .pasteText: true + // case .pasteImage: true + // case .pasteImages: true + // case .pasteText: true case .print: false case .redoLatestChange: canRedoLatestChange case .undoLatestChange: canUndoLatestChange @@ -34,7 +34,7 @@ public extension RichEditorState { } /// Trigger a certain rich text action. - func trigger(_ action: RichTextAction) { + public func trigger(_ action: RichTextAction) { handle(action) } } diff --git a/Sources/RichEditorSwiftUI/UI/Context/RichTextContext+Color.swift b/Sources/RichEditorSwiftUI/UI/Context/RichTextContext+Color.swift index 65ac784..ce8258e 100644 --- a/Sources/RichEditorSwiftUI/UI/Context/RichTextContext+Color.swift +++ b/Sources/RichEditorSwiftUI/UI/Context/RichTextContext+Color.swift @@ -7,10 +7,10 @@ import SwiftUI -public extension RichEditorState { +extension RichEditorState { /// Get a binding for a certain color. - func binding(for color: RichTextColor) -> Binding { + public func binding(for color: RichTextColor) -> Binding { Binding( get: { Color(self.color(for: color) ?? .clear) }, set: { self.updateStyleFor(color, to: .init($0)) } @@ -18,12 +18,12 @@ public extension RichEditorState { } /// Get the value for a certain color. - func color(for color: RichTextColor) -> ColorRepresentable? { + public func color(for color: RichTextColor) -> ColorRepresentable? { colors[color] } /// Set the value for a certain color. - func setColor( + public func setColor( _ color: RichTextColor, to val: ColorRepresentable ) { @@ -32,7 +32,9 @@ public extension RichEditorState { setColorInternal(color, to: val) } - func updateStyleFor(_ color: RichTextColor, to val: ColorRepresentable) { + public func updateStyleFor( + _ color: RichTextColor, to val: ColorRepresentable + ) { let value = Color(val) switch color { case .foreground: @@ -56,8 +58,10 @@ extension RichEditorState { _ color: RichTextColor, to val: ColorRepresentable? ) { - guard let val else { return colors[color] = nil } + guard let val else { + colors[color] = nil + return + } colors[color] = val } } - diff --git a/Sources/RichEditorSwiftUI/UI/Editor/RichEditor.swift b/Sources/RichEditorSwiftUI/UI/Editor/RichEditor.swift index 3602987..22f4c76 100644 --- a/Sources/RichEditorSwiftUI/UI/Editor/RichEditor.swift +++ b/Sources/RichEditorSwiftUI/UI/Editor/RichEditor.swift @@ -5,165 +5,164 @@ // Created by Divyesh Vekariya on 24/10/23. // +#if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) + import SwiftUI + import Combine -#if iOS || macOS || os(tvOS) || os(visionOS) -import SwiftUI -import Combine - -/** - This view can be used to view and edit rich text in SwiftUI. - - The view uses a platform-specific ``RichTextView`` together - with a ``RichEditorState`` and a ``RichTextCoordinator`` to - keep the view and context in sync. - - You can use the provided context to trigger and observe any - changes to the text editor. Note that changing the value of - the `text` binding will not yet update the editor. Until it - is fixed, use `setAttributedString(to:)`. - - Since the view wraps a native `UIKit` or `AppKit` text view, - you can't apply `.toolbar` modifiers to it, like you can do - with other SwiftUI views. This means that this doesn't work: - - ```swift - RichTextEditor(text: $text, context: context) - .toolbar { - ToolbarItemGroup(placement: .keyboard) { - .... - } - } - ``` - - This will not show anything. To work around this limitation, - use a ``RichTextKeyboardToolbar`` instead. - - You can configure and style the view by applying its config - and style view modifiers to your view hierarchy: - - ```swift - VStack { - RichTextEditor(...) - ... - } - .richTextEditorStyle(...) - .richTextEditorConfig(...) - ``` - - For more information, see ``RichTextKeyboardToolbarConfig`` - and ``RichTextKeyboardToolbarStyle``. - */ -public struct RichTextEditor: ViewRepresentable { - - @State var cancellable: Set = [] - - /// Create a rich text editor with a rich text value and - /// a certain rich text data format. + /// This view can be used to view and edit rich text in SwiftUI. /// - /// - Parameters: - /// - context: The rich editor state to use. - /// - viewConfiguration: A platform-specific view configuration, if any. - public init( - context: ObservedObject, - format: RichTextDataFormat = .archivedData, - viewConfiguration: @escaping ViewConfiguration = { _ in } - ) { - self._context = context - self.format = format - self.viewConfiguration = viewConfiguration - } - - public typealias ViewConfiguration = (RichTextViewComponent) -> Void + /// The view uses a platform-specific ``RichTextView`` together + /// with a ``RichEditorState`` and a ``RichTextCoordinator`` to + /// keep the view and context in sync. + /// + /// You can use the provided context to trigger and observe any + /// changes to the text editor. Note that changing the value of + /// the `text` binding will not yet update the editor. Until it + /// is fixed, use `setAttributedString(to:)`. + /// + /// Since the view wraps a native `UIKit` or `AppKit` text view, + /// you can't apply `.toolbar` modifiers to it, like you can do + /// with other SwiftUI views. This means that this doesn't work: + /// + /// ```swift + /// RichTextEditor(text: $text, context: context) + /// .toolbar { + /// ToolbarItemGroup(placement: .keyboard) { + /// .... + /// } + /// } + /// ``` + /// + /// This will not show anything. To work around this limitation, + /// use a ``RichTextKeyboardToolbar`` instead. + /// + /// You can configure and style the view by applying its config + /// and style view modifiers to your view hierarchy: + /// + /// ```swift + /// VStack { + /// RichTextEditor(...) + /// ... + /// } + /// .richTextEditorStyle(...) + /// .richTextEditorConfig(...) + /// ``` + /// + /// For more information, see ``RichTextKeyboardToolbarConfig`` + /// and ``RichTextKeyboardToolbarStyle``. + public struct RichTextEditor: ViewRepresentable { + + @State var cancellable: Set = [] + + /// Create a rich text editor with a rich text value and + /// a certain rich text data format. + /// + /// - Parameters: + /// - context: The rich editor state to use. + /// - viewConfiguration: A platform-specific view configuration, if any. + public init( + context: ObservedObject, + format: RichTextDataFormat = .archivedData, + viewConfiguration: @escaping ViewConfiguration = { _ in } + ) { + self._context = context + self.format = format + self.viewConfiguration = viewConfiguration + } - @ObservedObject - private var context: RichEditorState - - private var viewConfiguration: ViewConfiguration + public typealias ViewConfiguration = (RichTextViewComponent) -> Void - private var format: RichTextDataFormat + @ObservedObject + private var context: RichEditorState - @Environment(\.richTextEditorConfig) - private var config + private var viewConfiguration: ViewConfiguration - @Environment(\.richTextEditorStyle) - private var style + private var format: RichTextDataFormat -#if iOS || os(tvOS) || os(visionOS) - public let textView = RichTextView() -#endif + @Environment(\.richTextEditorConfig) + private var config -#if macOS - public let scrollView = RichTextView.scrollableTextView() + @Environment(\.richTextEditorStyle) + private var style - public var textView: RichTextView { - scrollView.documentView as? RichTextView ?? RichTextView() - } -#endif + #if os(iOS) || os(tvOS) || os(visionOS) + public let textView = RichTextView() + #endif - public func makeCoordinator() -> RichTextCoordinator { - RichTextCoordinator( - text: $context.attributedString, - textView: textView, - richTextContext: context - ) - } + #if macOS + public let scrollView = RichTextView.scrollableTextView() -#if iOS || os(tvOS) || os(visionOS) - public func makeUIView(context: Context) -> some UIView { - textView.setup(with: self.context.attributedString, format: format) - textView.configuration = config - textView.theme = style - viewConfiguration(textView) - return textView - } + public var textView: RichTextView { + scrollView.documentView as? RichTextView ?? RichTextView() + } + #endif - public func updateUIView(_ view: UIViewType, context: Context) { - if !(self.context.activeAttributes? - .contains(where: { $0.key == .font }) ?? false) { - self.textView.typingAttributes = [.font: style.font] + public func makeCoordinator() -> RichTextCoordinator { + RichTextCoordinator( + text: $context.attributedString, + textView: textView, + richTextContext: context + ) } - } -#else - - public func makeNSView(context: Context) -> some NSView { - textView.setup(with: self.context.attributedString, format: format) - textView.configuration = config - textView.theme = style - viewConfiguration(textView) - return scrollView - } - public func updateNSView(_ view: NSViewType, context: Context) { } -#endif -} + #if os(iOS) || os(tvOS) || os(visionOS) + public func makeUIView(context: Context) -> some UIView { + textView.setup( + with: self.context.attributedString, format: format) + textView.configuration = config + textView.theme = style + viewConfiguration(textView) + return textView + } + + public func updateUIView(_ view: UIViewType, context: Context) { + // if !(self.context.activeAttributes? + // .contains(where: { $0.key == .font }) ?? false) { + // self.textView.typingAttributes = [.font: style.font] + // } + } + #else + + public func makeNSView(context: Context) -> some NSView { + textView.setup( + with: self.context.attributedString, format: format) + textView.configuration = config + textView.theme = style + viewConfiguration(textView) + return scrollView + } + + public func updateNSView(_ view: NSViewType, context: Context) {} + #endif + } -// MARK: RichTextPresenter + // MARK: RichTextPresenter -public extension RichTextEditor { + extension RichTextEditor { - /// Get the currently selected range. - var selectedRange: NSRange { - textView.selectedRange + /// Get the currently selected range. + public var selectedRange: NSRange { + textView.selectedRange + } } -} -// MARK: RichTextReader + // MARK: RichTextReader -public extension RichTextEditor { + extension RichTextEditor { - /// Get the string that is managed by the editor. - var attributedString: NSAttributedString { - context.attributedString + /// Get the string that is managed by the editor. + public var attributedString: NSAttributedString { + context.attributedString + } } -} -// MARK: RichTextWriter + // MARK: RichTextWriter -public extension RichTextEditor { + extension RichTextEditor { - /// Get the mutable string that is managed by the editor. - var mutableAttributedString: NSMutableAttributedString? { - textView.mutableAttributedString + /// Get the mutable string that is managed by the editor. + public var mutableAttributedString: NSMutableAttributedString? { + textView.mutableAttributedString + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/UI/Editor/RichEditorState+Spans.swift b/Sources/RichEditorSwiftUI/UI/Editor/RichEditorState+Spans.swift index c607459..0218566 100644 --- a/Sources/RichEditorSwiftUI/UI/Editor/RichEditorState+Spans.swift +++ b/Sources/RichEditorSwiftUI/UI/Editor/RichEditorState+Spans.swift @@ -12,14 +12,16 @@ import SwiftUI extension RichEditorState { func getStringWith(from: Int, to: Int) -> String { guard (to - from) >= 0 else { return "" } - return attributedString.string.substring(from: .init(location: from, length: (to - from))) + return attributedString.string.substring( + from: .init(location: from, length: (to - from))) } /** This will provide RichText which is encoded from input and editor text */ internal func getRichText() -> RichText { - return attributedString.string.isEmpty ? RichText() : RichText(spans: spans) + return attributedString.string.isEmpty + ? RichText() : RichText(spans: spans) } /** @@ -78,14 +80,17 @@ extension RichEditorState { switch event { case .didChangeSelection(let range, let text): selectedRange = range - guard rawText.count == text.string.count && selectedRange.isCollapsed else { + guard + rawText.count == text.string.count && selectedRange.isCollapsed + else { return } onSelectionDidChanged() case .didBeginEditing(let range, _): selectedRange = range case .didChange: - onTextFieldValueChange(newText: attributedString, selection: selectedRange) + onTextFieldValueChange( + newText: attributedString, selection: selectedRange) case .didEndEditing: selectedRange = .init(location: 0, length: 0) } @@ -97,7 +102,9 @@ extension RichEditorState { - newText: is updated NSMutableAttributedString - selection: is the range of the selected text */ - private func onTextFieldValueChange(newText: NSAttributedString, selection: NSRange) { + private func onTextFieldValueChange( + newText: NSAttributedString, selection: NSRange + ) { self.selectedRange = selection if newText.string.count > rawText.count { @@ -131,8 +138,11 @@ extension RichEditorState { activeAttributes = [:] activeStyles.insert(style) - if style.isHeaderStyle || style.isDefault || style.isList || style.isAlignmentStyle { - handleAddOrRemoveStyleToLine(in: selectedRange, style: style, byAdding: !style.isDefault) + if style.isHeaderStyle || style.isDefault || style.isList + || style.isAlignmentStyle + { + handleAddOrRemoveStyleToLine( + in: selectedRange, style: style, byAdding: !style.isDefault) } else if !selectedRange.isCollapsed { let addStyle = checkIfStyleIsActiveWithSameAttributes(style) processSpansFor(new: style, in: selectedRange, addStyle: addStyle) @@ -141,7 +151,9 @@ extension RichEditorState { updateCurrentSpanStyle() } - func checkIfStyleIsActiveWithSameAttributes(_ style: RichTextSpanStyle) -> Bool { + func checkIfStyleIsActiveWithSameAttributes(_ style: RichTextSpanStyle) + -> Bool + { var addStyle: Bool = true switch style { case .size(let size): @@ -176,6 +188,8 @@ extension RichEditorState { if let alignment { addStyle = alignment != self.textAlignment || alignment != .left } + case .link(let link): + addStyle = link != nil default: return addStyle } @@ -193,17 +207,22 @@ extension RichEditorState { if selectedRange.isCollapsed { newStyles = getRichSpanStyleByTextIndex(selectedRange.location - 1) } else { - newStyles = Set(getRichSpanStyleListByTextRange(selectedRange)) + newStyles = Set(getRichSpanStyleListByTextRange(selectedRange)) } - guard activeStyles != newStyles && selectedRange.location != 0 else { return } + guard activeStyles != newStyles && selectedRange.location != 0 else { + return + } activeStyles = newStyles var attributes: [NSAttributedString.Key: Any] = [:] activeStyles.forEach({ - attributes[$0.attributedStringKey] = $0.defaultAttributeValue(font: FontRepresentable.standardRichTextFont) + attributes[$0.attributedStringKey] = $0.defaultAttributeValue( + font: FontRepresentable.standardRichTextFont) }) - headerType = activeStyles.first(where: { $0.isHeaderStyle })?.headerType ?? .default + headerType = + activeStyles.first(where: { $0.isHeaderStyle })?.headerType + ?? .default activeAttributes = attributes } @@ -221,7 +240,9 @@ extension RichEditorState { guard !activeStyles.contains(style) else { return } activeStyles.insert(style) - if (style.isHeaderStyle || style.isDefault || style.isList || style.isAlignmentStyle) { + if style.isHeaderStyle || style.isDefault || style.isList + || style.isAlignmentStyle + { handleAddOrRemoveStyleToLine(in: selectedRange, style: style) } else if !selectedRange.isCollapsed { processSpansFor(new: style, in: selectedRange) @@ -242,7 +263,8 @@ extension RichEditorState { updateTypingAttributes() if style.isHeaderStyle || style.isDefault || style.isList { - handleAddOrRemoveStyleToLine(in: selectedRange, style: style, byAdding: false) + handleAddOrRemoveStyleToLine( + in: selectedRange, style: style, byAdding: false) } else if !selectedRange.isCollapsed { processSpansFor(new: style, in: selectedRange, addStyle: false) } @@ -255,7 +277,8 @@ extension RichEditorState { var attributes: [NSAttributedString.Key: Any] = [:] activeStyles.forEach({ - attributes[$0.attributedStringKey] = $0.defaultAttributeValue(font: FontRepresentable.standardRichTextFont) + attributes[$0.attributedStringKey] = $0.defaultAttributeValue( + font: FontRepresentable.standardRichTextFont) }) activeAttributes = attributes @@ -277,8 +300,10 @@ extension RichEditorState { let startTypeIndex = selectedRange.location - typedChars let startTypeChar = newValue.string.utf16.map({ $0 })[startTypeIndex] - if startTypeChar == "\n".utf16.first && startTypeChar == "\n".utf16.last, - activeStyles.contains(where: { $0.isHeaderStyle }) { + if startTypeChar == "\n".utf16.first + && startTypeChar == "\n".utf16.last, + activeStyles.contains(where: { $0.isHeaderStyle }) + { activeStyles.removeAll() } @@ -286,8 +311,12 @@ extension RichEditorState { moveSpansForward(startTypeIndex: startTypeIndex, by: typedChars) - let startParts = internalSpans.filter { $0.closedRange.contains(startTypeIndex - 1) } - let endParts = internalSpans.filter { $0.closedRange.contains(startTypeIndex) } + let startParts = internalSpans.filter { + $0.closedRange.contains(startTypeIndex - 1) + } + let endParts = internalSpans.filter { + $0.closedRange.contains(startTypeIndex) + } let commonParts = Set(startParts).intersection(Set(endParts)) var addedInFirstPart: Bool = false @@ -304,19 +333,31 @@ extension RichEditorState { if !addedInFirstPart { endParts.filter { !commonParts.contains($0) }.forEach { part in - processSpan(part, typedChars: typedChars, startTypeIndex: startTypeIndex, selectedStyles: &selectedStyles, forward: true) + processSpan( + part, typedChars: typedChars, + startTypeIndex: startTypeIndex, + selectedStyles: &selectedStyles, forward: true) } } commonParts.forEach { part in - processSpan(part, typedChars: typedChars, startTypeIndex: startTypeIndex, selectedStyles: &selectedStyles) + processSpan( + part, typedChars: typedChars, startTypeIndex: startTypeIndex, + selectedStyles: &selectedStyles) } internalSpans = mergeSameStyledSpans(internalSpans) - guard !internalSpans.contains(where: { $0.closedRange.contains(startTypeIndex) }) else { return } - let toIndex = typedChars > 1 ? (startTypeIndex + typedChars - 1) : startTypeIndex - let span = RichTextSpanInternal(from: startTypeIndex, to: toIndex, attributes: getRichAttributesFor(styles: Array(selectedStyles))) + guard + !internalSpans.contains(where: { + $0.closedRange.contains(startTypeIndex) + }) + else { return } + let toIndex = + typedChars > 1 ? (startTypeIndex + typedChars - 1) : startTypeIndex + let span = RichTextSpanInternal( + from: startTypeIndex, to: toIndex, + attributes: getRichAttributesFor(styles: Array(selectedStyles))) internalSpans.append(span) } @@ -328,7 +369,11 @@ extension RichEditorState { This will update the span according to requirement, like break, remove, merge or extend. */ - private func processSpan(_ richTextSpan: RichTextSpanInternal, typedChars: Int, startTypeIndex: Int, selectedStyles: inout Set, forward: Bool = false) { + private func processSpan( + _ richTextSpan: RichTextSpanInternal, typedChars: Int, + startTypeIndex: Int, selectedStyles: inout Set, + forward: Bool = false + ) { let newFromIndex = richTextSpan.from + typedChars let newToIndex = richTextSpan.to + typedChars @@ -338,16 +383,22 @@ extension RichEditorState { selectedStyles.removeAll() } else { if forward { - internalSpans[index] = richTextSpan.copy(from: newFromIndex, to: newToIndex) + internalSpans[index] = richTextSpan.copy( + from: newFromIndex, to: newToIndex) } else { - divideSpanAndAddTextWithCurrentStyle(span: richTextSpan, typedChars: typedChars, startTypeIndex: startTypeIndex, with: &selectedStyles) + divideSpanAndAddTextWithCurrentStyle( + span: richTextSpan, typedChars: typedChars, + startTypeIndex: startTypeIndex, with: &selectedStyles) selectedStyles.removeAll() } } } } - func divideSpanAndAddTextWithCurrentStyle(span: RichTextSpanInternal, typedChars: Int, startTypeIndex: Int, with styles: inout Set) { + func divideSpanAndAddTextWithCurrentStyle( + span: RichTextSpanInternal, typedChars: Int, startTypeIndex: Int, + with styles: inout Set + ) { guard let index = internalSpans.firstIndex(of: span) else { return } let extendedSpan = span.copy(to: span.to + typedChars) @@ -355,7 +406,10 @@ extension RichEditorState { let endIndex = startTypeIndex + typedChars - 1 var spansToAdd: [RichTextSpanInternal] = [] - spansToAdd.append(RichTextSpanInternal(from: startIndex, to: endIndex, attributes: getRichAttributesFor(styles: Array(styles)))) + spansToAdd.append( + RichTextSpanInternal( + from: startIndex, to: endIndex, + attributes: getRichAttributesFor(styles: Array(styles)))) if startTypeIndex == extendedSpan.from { spansToAdd.append(extendedSpan.copy(from: endIndex)) @@ -365,8 +419,9 @@ extension RichEditorState { spansToAdd.append(extendedSpan.copy(to: startIndex - 1)) spansToAdd.append(extendedSpan.copy(from: endIndex + 1)) } - internalSpans.removeAll(where: { $0 == span}) - internalSpans.insert(contentsOf: spansToAdd.sorted(by: { $0.from < $1.from }), at: index) + internalSpans.removeAll(where: { $0 == span }) + internalSpans.insert( + contentsOf: spansToAdd.sorted(by: { $0.from < $1.from }), at: index) } /** @@ -382,7 +437,8 @@ extension RichEditorState { filteredSpans.forEach { part in if let index = internalSpans.firstIndex(of: part) { - internalSpans[index] = part.copy(from: part.from + step, to: part.to + step) + internalSpans[index] = part.copy( + from: part.from + step, to: part.to + step) } } } @@ -408,27 +464,43 @@ extension RichEditorState { let startRemoveIndex = selectedRange.location let endRemoveIndex = selectedRange.location + removedCharsCount - 1 let removeRange = startRemoveIndex...endRemoveIndex - let start = rawText.utf16.index(rawText.startIndex, offsetBy: startRemoveIndex) - let end = rawText.utf16.index(rawText.startIndex, offsetBy: endRemoveIndex) - - if startRemoveIndex != endRemoveIndex, let newLineIndex = String(rawText[start...end]).map({ $0 }).lastIndex(of: "\n"), newLineIndex >= 0 { - handleRemoveHeaderStyle(newText: newText.string, at: removeRange.nsRange, newLineIndex: newLineIndex) + let start = rawText.utf16.index( + rawText.startIndex, offsetBy: startRemoveIndex) + let end = rawText.utf16.index( + rawText.startIndex, offsetBy: endRemoveIndex) + + if startRemoveIndex != endRemoveIndex, + let newLineIndex = String(rawText[start...end]).map({ $0 }) + .lastIndex(of: "\n"), newLineIndex >= 0 + { + handleRemoveHeaderStyle( + newText: newText.string, at: removeRange.nsRange, + newLineIndex: newLineIndex) } let partsCopy = internalSpans - let lowerBound = removeRange.lowerBound //- (selectedRange.length < removedCharsCount ? 1 : 0) + let lowerBound = removeRange.lowerBound //- (selectedRange.length < removedCharsCount ? 1 : 0) for part in partsCopy { if let index = internalSpans.firstIndex(of: part) { if removeRange.upperBound < part.from { - internalSpans[index] = part.copy(from: part.from - (removedCharsCount), to: part.to - (removedCharsCount)) - } else if lowerBound <= part.from && removeRange.upperBound >= part.to { + internalSpans[index] = part.copy( + from: part.from - (removedCharsCount), + to: part.to - (removedCharsCount)) + } else if lowerBound <= part.from + && removeRange.upperBound >= part.to + { internalSpans.removeAll(where: { $0 == part }) } else if lowerBound <= part.from { - internalSpans[index] = part.copy(from: max(0, lowerBound), to: min(newText.string.utf16Length, part.to - removedCharsCount)) + internalSpans[index] = part.copy( + from: max(0, lowerBound), + to: min( + newText.string.utf16Length, + part.to - removedCharsCount)) } else if removeRange.upperBound <= part.to { - internalSpans[index] = part.copy(to: part.to - removedCharsCount) + internalSpans[index] = part.copy( + to: part.to - removedCharsCount) } else if lowerBound < part.to { internalSpans[index] = part.copy(to: lowerBound) } @@ -445,11 +517,12 @@ extension RichEditorState { This will move the span according to it's position if it is after the typed character then it will move forward by number or typed character which is step. */ private func moveSpansBackward(endTypeIndex: Int, by step: Int) { - let filteredSpans = internalSpans.filter { $0.to > endTypeIndex} + let filteredSpans = internalSpans.filter { $0.to > endTypeIndex } filteredSpans.forEach { part in if let index = internalSpans.firstIndex(of: part) { - internalSpans[index] = part.copy(from: part.from - step, to: part.to - step) + internalSpans[index] = part.copy( + from: part.from - step, to: part.to - step) } } } @@ -463,10 +536,15 @@ extension RichEditorState { - Parameters: - style: is of type RichTextSpanStyle */ - private func handleAddOrRemoveStyleToLine(in range: NSRange, style: RichTextSpanStyle, byAdding: Bool = true) { + private func handleAddOrRemoveStyleToLine( + in range: NSRange, style: RichTextSpanStyle, byAdding: Bool = true + ) { guard !rawText.isEmpty else { return } - let range = style.isList ? getListRangeFor(range, in: rawText) : rawText.getHeaderRangeFor(range) + let range = + style.isList + ? getListRangeFor(range, in: rawText) + : rawText.getHeaderRangeFor(range) processSpansFor(new: style, in: range, addStyle: byAdding) } @@ -477,13 +555,18 @@ extension RichEditorState { - range: is the NSRange - newLineIndex: is string index of new line where is it located */ - private func handleRemoveHeaderStyle(newText: String? = nil, at range: NSRange, newLineIndex: Int) { + private func handleRemoveHeaderStyle( + newText: String? = nil, at range: NSRange, newLineIndex: Int + ) { let text = newText ?? rawText let startIndex = max(0, text.map({ $0 }).index(before: newLineIndex)) let endIndex = text.map({ $0 }).index(after: newLineIndex) - let selectedParts = internalSpans.filter({ ($0.from < endIndex && $0.to >= startIndex && $0.attributes?.header != nil) }) + let selectedParts = internalSpans.filter({ + ($0.from < endIndex && $0.to >= startIndex + && $0.attributes?.header != nil) + }) internalSpans.removeAll(where: { selectedParts.contains($0) }) } @@ -495,7 +578,9 @@ extension RichEditorState { - styles: is of type [RichTextSpanStyle] - range: is of type NSRange */ - private func processSpansFor(new style: RichTextSpanStyle, in range: NSRange, addStyle: Bool = true) { + private func processSpansFor( + new style: RichTextSpanStyle, in range: NSRange, addStyle: Bool = true + ) { guard !range.isCollapsed else { return } @@ -509,9 +594,12 @@ extension RichEditorState { partialOverlap.removeAll(where: { completeOverlap.contains($0) }) sameSpans.removeAll(where: { completeOverlap.contains($0) }) - let partialOverlapSpan = processPartialOverlappingSpans(partialOverlap, range: range, style: style, addStyle: addStyle) - let completeOverlapSpan = processCompleteOverlappingSpans(completeOverlap, range: range, style: style, addStyle: addStyle) - let sameSpan = processSameSpans(sameSpans, range: range, style: style, addStyle: addStyle) + let partialOverlapSpan = processPartialOverlappingSpans( + partialOverlap, range: range, style: style, addStyle: addStyle) + let completeOverlapSpan = processCompleteOverlappingSpans( + completeOverlap, range: range, style: style, addStyle: addStyle) + let sameSpan = processSameSpans( + sameSpans, range: range, style: style, addStyle: addStyle) processedSpans.append(contentsOf: partialOverlapSpan) processedSpans.append(contentsOf: completeOverlapSpan) @@ -519,26 +607,39 @@ extension RichEditorState { processedSpans = mergeSameStyledSpans(processedSpans) - internalSpans.removeAll(where: { $0.closedRange.overlaps(range.closedRange) }) - internalSpans.append(contentsOf: processedSpans) + internalSpans.removeAll(where: { + $0.closedRange.overlaps(range.closedRange) + }) + internalSpans.append(contentsOf: processedSpans) internalSpans = mergeSameStyledSpans(internalSpans) internalSpans.sort(by: { $0.from < $1.from }) } - private func processCompleteOverlappingSpans(_ spans: [RichTextSpanInternal], range: NSRange, style: RichTextSpanStyle, addStyle: Bool = true) -> [RichTextSpanInternal] { + private func processCompleteOverlappingSpans( + _ spans: [RichTextSpanInternal], range: NSRange, + style: RichTextSpanStyle, addStyle: Bool = true + ) -> [RichTextSpanInternal] { var processedSpans: [RichTextSpanInternal] = [] for span in spans { if span.closedRange.isInRange(range.closedRange) { - processedSpans.append(span.copy(attributes: span.attributes?.copy(with: style, byAdding: addStyle))) + processedSpans.append( + span.copy( + attributes: span.attributes?.copy( + with: style, byAdding: addStyle))) } else { if span.from < range.lowerBound { let leftPart = span.copy(to: range.lowerBound - 1) processedSpans.append(leftPart) } - if span.from <= (range.lowerBound) && span.to >= (range.upperBound - 1) { - let centerPart = span.copy(from: range.lowerBound, to: range.upperBound - 1, attributes: span.attributes?.copy(with: style, byAdding: addStyle)) + if span.from <= (range.lowerBound) + && span.to >= (range.upperBound - 1) + { + let centerPart = span.copy( + from: range.lowerBound, to: range.upperBound - 1, + attributes: span.attributes?.copy( + with: style, byAdding: addStyle)) processedSpans.append(centerPart) } @@ -554,17 +655,26 @@ extension RichEditorState { return processedSpans } - private func processPartialOverlappingSpans(_ spans: [RichTextSpanInternal], range: NSRange, style: RichTextSpanStyle, addStyle: Bool = true) -> [RichTextSpanInternal] { + private func processPartialOverlappingSpans( + _ spans: [RichTextSpanInternal], range: NSRange, + style: RichTextSpanStyle, addStyle: Bool = true + ) -> [RichTextSpanInternal] { var processedSpans: [RichTextSpanInternal] = [] for span in spans { if span.from < range.location { let leftPart = span.copy(to: range.lowerBound - 1) - let rightPart = span.copy(from: range.lowerBound, attributes: span.attributes?.copy(with: style, byAdding: addStyle)) + let rightPart = span.copy( + from: range.lowerBound, + attributes: span.attributes?.copy( + with: style, byAdding: addStyle)) processedSpans.append(leftPart) processedSpans.append(rightPart) } else { - let leftPart = span.copy(to: min(span.to, range.upperBound), attributes: span.attributes?.copy(with: style, byAdding: addStyle)) + let leftPart = span.copy( + to: min(span.to, range.upperBound), + attributes: span.attributes?.copy( + with: style, byAdding: addStyle)) let rightPart = span.copy(from: range.location) processedSpans.append(leftPart) processedSpans.append(rightPart) @@ -575,24 +685,35 @@ extension RichEditorState { return processedSpans } - private func processSameSpans(_ spans: [RichTextSpanInternal], range: NSRange, style: RichTextSpanStyle, addStyle: Bool = true) -> [RichTextSpanInternal] { + private func processSameSpans( + _ spans: [RichTextSpanInternal], range: NSRange, + style: RichTextSpanStyle, addStyle: Bool = true + ) -> [RichTextSpanInternal] { var processedSpans: [RichTextSpanInternal] = [] - processedSpans = spans.map({ $0.copy(attributes: $0.attributes?.copy(with: style, byAdding: addStyle)) }) + processedSpans = spans.map({ + $0.copy( + attributes: $0.attributes?.copy(with: style, byAdding: addStyle) + ) + }) processedSpans = mergeSameStyledSpans(processedSpans) return processedSpans } // merge adjacent spans with same style - private func mergeSameStyledSpans(_ spans: [RichTextSpanInternal]) -> [RichTextSpanInternal] { + private func mergeSameStyledSpans(_ spans: [RichTextSpanInternal]) + -> [RichTextSpanInternal] + { guard !spans.isEmpty else { return [] } var mergedSpans: [RichTextSpanInternal] = [] var previousSpan: RichTextSpanInternal? for span in spans.sorted(by: { $0.from < $1.from }) { if let current = previousSpan { - if span.attributes?.stylesSet() == current.attributes?.stylesSet() { + if span.attributes?.stylesSet() + == current.attributes?.stylesSet() + { // Merge overlapping spans previousSpan = current.copy(to: max(current.to, span.to)) } else { @@ -625,8 +746,12 @@ extension RichEditorState { let fromIndex = range.lowerBound let toIndex = range.isCollapsed ? fromIndex : range.upperBound - let newLineStartIndex = text.utf16.prefix(fromIndex).map({ $0 }).lastIndex(of: "\n".utf16.last) ?? 0 - let newLineEndIndex = text.utf16.suffix(from: text.utf16.index(text.utf16.startIndex, offsetBy: toIndex - 1)).map({ $0 }).firstIndex(of: "\n".utf16.last) + let newLineStartIndex = + text.utf16.prefix(fromIndex).map({ $0 }).lastIndex( + of: "\n".utf16.last) ?? 0 + let newLineEndIndex = text.utf16.suffix( + from: text.utf16.index(text.utf16.startIndex, offsetBy: toIndex - 1) + ).map({ $0 }).firstIndex(of: "\n".utf16.last) ///Added +1 to start new line after \n otherwise it will create bullets for previous line as well let startIndex = max(0, (newLineStartIndex + 1)) @@ -648,8 +773,12 @@ extension RichEditorState { - Parameters: - selectedRange: is of type NSRange */ - private func getOverlappingSpans(for selectedRange: NSRange) -> [RichTextSpanInternal] { - return internalSpans.filter { $0.closedRange.overlaps(selectedRange.closedRange) } + private func getOverlappingSpans(for selectedRange: NSRange) + -> [RichTextSpanInternal] + { + return internalSpans.filter { + $0.closedRange.overlaps(selectedRange.closedRange) + } } /** @@ -657,8 +786,12 @@ extension RichEditorState { - Parameters: - selectedRange: selectedRange is of type NSRange */ - func getPartialOverlappingSpans(for selectedRange: NSRange) -> [RichTextSpanInternal] { - return getOverlappingSpans(for: selectedRange).filter({ $0.closedRange.isPartialOverlap(selectedRange.closedRange) }) + func getPartialOverlappingSpans(for selectedRange: NSRange) + -> [RichTextSpanInternal] + { + return getOverlappingSpans(for: selectedRange).filter({ + $0.closedRange.isPartialOverlap(selectedRange.closedRange) + }) } /** @@ -666,8 +799,13 @@ extension RichEditorState { - Parameters: - selectedRange: selectedRange is of type NSRange */ - func getCompleteOverlappingSpans(for selectedRange: NSRange) -> [RichTextSpanInternal] { - return getOverlappingSpans(for: selectedRange).filter({ $0.closedRange.isInRange(selectedRange.closedRange) || selectedRange.closedRange.isInRange($0.closedRange)}) + func getCompleteOverlappingSpans(for selectedRange: NSRange) + -> [RichTextSpanInternal] + { + return getOverlappingSpans(for: selectedRange).filter({ + $0.closedRange.isInRange(selectedRange.closedRange) + || selectedRange.closedRange.isInRange($0.closedRange) + }) } /** @@ -677,11 +815,12 @@ extension RichEditorState { */ func getSameSpans(for selectedRange: NSRange) -> [RichTextSpanInternal] { - return getOverlappingSpans(for: selectedRange).filter({ $0.closedRange.isSameAs(selectedRange.closedRange) }) + return getOverlappingSpans(for: selectedRange).filter({ + $0.closedRange.isSameAs(selectedRange.closedRange) + }) } } - //MARK: - Helper Methods extension RichEditorState { /** @@ -698,8 +837,13 @@ extension RichEditorState { - Parameters: - index: index or location of text */ - private func getRichSpanStyleByTextIndex(_ index: Int) -> Set { - let styles = Set(internalSpans.filter { index >= $0.from && index <= $0.to }.map { $0.attributes?.styles() ?? []}.flatMap({ $0 })) + private func getRichSpanStyleByTextIndex(_ index: Int) -> Set< + RichTextSpanStyle + > { + let styles = Set( + internalSpans.filter { index >= $0.from && index <= $0.to }.map { + $0.attributes?.styles() ?? [] + }.flatMap({ $0 })) return styles } @@ -708,8 +852,12 @@ extension RichEditorState { - Parameters: - range: range of text which is of type NSRange */ - private func getRichSpanStyleListByTextRange(_ range: NSRange) -> [RichTextSpanStyle] { - return internalSpans.filter({ range.closedRange.overlaps($0.closedRange) }).map { $0.attributes?.styles() ?? [] }.flatMap({ $0 }) + private func getRichSpanStyleListByTextRange(_ range: NSRange) + -> [RichTextSpanStyle] + { + return internalSpans.filter({ + range.closedRange.overlaps($0.closedRange) + }).map { $0.attributes?.styles() ?? [] }.flatMap({ $0 }) } } @@ -744,6 +892,8 @@ extension RichEditorState { if let alignment, alignment != self.textAlignment { actionPublisher.send(.setAlignment(alignment)) } + case .link(let link): + actionPublisher.send(.setLink(link)) } } } diff --git a/Sources/RichEditorSwiftUI/UI/Editor/RichTextSpanStyle.swift b/Sources/RichEditorSwiftUI/UI/Editor/RichTextSpanStyle.swift index 806f3af..fb44093 100644 --- a/Sources/RichEditorSwiftUI/UI/Editor/RichTextSpanStyle.swift +++ b/Sources/RichEditorSwiftUI/UI/Editor/RichTextSpanStyle.swift @@ -26,7 +26,7 @@ public enum RichTextSpanStyle: Equatable, CaseIterable, Hashable { .font(), .color(), .background(), - .align() + .align(), ] public func hash(into hasher: inout Hasher) { @@ -58,6 +58,7 @@ public enum RichTextSpanStyle: Equatable, CaseIterable, Hashable { case color(Color? = nil) case background(Color? = nil) case align(RichTextAlignment? = nil) + case link(String? = nil) var key: String { switch self { @@ -85,8 +86,8 @@ public enum RichTextSpanStyle: Equatable, CaseIterable, Hashable { return "h6" case .bullet: return "bullet" - // case .ordered: - // return "ordered" + // case .ordered: + // return "ordered" case .size: return "size" case .font: @@ -97,6 +98,8 @@ public enum RichTextSpanStyle: Equatable, CaseIterable, Hashable { return "background" case .align(let alignment): return "align" + "\(alignment?.rawValue ?? "")" + case .link(let link): + return "link" + (link ?? "") } } @@ -108,7 +111,8 @@ public enum RichTextSpanStyle: Equatable, CaseIterable, Hashable { case .default, .bold, .italic, .h1, .h2, .h3, .h4, .h5, .h6: return getFontWithUpdating(font: font) case .bullet(let indent): - return getListStyleAttributeValue(listType ?? .bullet(), indent: indent) + return getListStyleAttributeValue( + listType ?? .bullet(), indent: indent) case .strikethrough: return NSUnderlineStyle.single.rawValue case .size(let size): @@ -125,22 +129,24 @@ public enum RichTextSpanStyle: Equatable, CaseIterable, Hashable { size: .standardRichTextFontSize ) ?? font } else { -#if os(watchOS) - return CGFloat.standardRichTextFontSize + #if os(watchOS) + return CGFloat.standardRichTextFontSize #else - return RichTextView.Theme.standard.font -#endif + return RichTextView.Theme.standard.font + #endif } case .color: -#if os(watchOS) - return Color.primary -#else - return RichTextView.Theme.standard.fontColor -#endif + #if os(watchOS) + return Color.primary + #else + return RichTextView.Theme.standard.fontColor + #endif case .background: return ColorRepresentable.white case .align: return RichTextAlignment.left.nativeAlignment + case .link(let link): + return link ?? "" } } @@ -148,7 +154,8 @@ public enum RichTextSpanStyle: Equatable, CaseIterable, Hashable { switch self { case .underline: return .underlineStyle - case .default, .bold, .italic, .h1, .h2, .h3, .h4, .h5, .h6, .size, .font: + case .default, .bold, .italic, .h1, .h2, .h3, .h4, .h5, .h6, .size, + .font: return .font case .bullet, .align: return .paragraphStyle @@ -158,10 +165,14 @@ public enum RichTextSpanStyle: Equatable, CaseIterable, Hashable { return .foregroundColor case .background: return .backgroundColor + case .link: + return .link } } - public static func == (lhs: RichTextSpanStyle, rhs: RichTextSpanStyle) -> Bool { + public static func == (lhs: RichTextSpanStyle, rhs: RichTextSpanStyle) + -> Bool + { return lhs.key == rhs.key } @@ -267,9 +278,10 @@ public enum RichTextSpanStyle: Equatable, CaseIterable, Hashable { switch self { case .default: return font - case .bold,.italic: + case .bold, .italic: return font.addFontStyle(self) - case .underline, .bullet, .strikethrough, .color, .background, .align: + case .underline, .bullet, .strikethrough, .color, .background, .align, + .link: return font case .h1: return font.updateFontSize(multiple: 1.5) @@ -291,7 +303,8 @@ public enum RichTextSpanStyle: Equatable, CaseIterable, Hashable { } case .font(let name): if let name { - return FontRepresentable(name: name, size: font.pointSize) ?? font + return FontRepresentable(name: name, size: font.pointSize) + ?? font } else { return font } @@ -315,55 +328,59 @@ public enum RichTextSpanStyle: Equatable, CaseIterable, Hashable { } } - func getFontAfterRemovingStyle(font: FontRepresentable) -> FontRepresentable { + func getFontAfterRemovingStyle(font: FontRepresentable) -> FontRepresentable + { switch self { case .bold, .italic, .bullet: return font.removeFontStyle(self) - case .underline, .strikethrough, .color, .background, .align: + case .underline, .strikethrough, .color, .background, .align, .link: return font case .default, .h1, .h2, .h3, .h4, .h5, .h6, .size, .font: return font.updateFontSize(size: .standardRichTextFontSize) } } - func getListStyleAttributeValue(_ listType: ListType, indent: Int? = nil) -> NSMutableParagraphStyle { + func getListStyleAttributeValue(_ listType: ListType, indent: Int? = nil) + -> NSMutableParagraphStyle + { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = .left - let listItem = TextList(markerFormat: listType.getMarkerFormat(), options: 0) - paragraphStyle.textLists = Array(repeating: listItem, count: (indent ?? 0) + 1) + let listItem = TextList( + markerFormat: listType.getMarkerFormat(), options: 0) + paragraphStyle.textLists = Array( + repeating: listItem, count: (indent ?? 0) + 1) return paragraphStyle } } #if canImport(UIKit) -public extension RichTextSpanStyle { - - /// The symbolic font traits for the style, if any. - var symbolicTraits: UIFontDescriptor.SymbolicTraits? { - switch self { - case .bold: .traitBold - case .italic: .traitItalic - default: nil + extension RichTextSpanStyle { + + /// The symbolic font traits for the style, if any. + public var symbolicTraits: UIFontDescriptor.SymbolicTraits? { + switch self { + case .bold: .traitBold + case .italic: .traitItalic + default: nil + } } } -} #endif #if macOS -public extension RichTextSpanStyle { - - /// The symbolic font traits for the trait, if any. - var symbolicTraits: NSFontDescriptor.SymbolicTraits? { - switch self { - case .bold: .bold - case .italic: .italic - default: nil + extension RichTextSpanStyle { + + /// The symbolic font traits for the trait, if any. + public var symbolicTraits: NSFontDescriptor.SymbolicTraits? { + switch self { + case .bold: .bold + case .italic: .italic + default: nil + } } } -} #endif - extension RichTextSpanStyle { func getRichAttribute() -> RichAttributes? { switch self { @@ -401,12 +418,13 @@ extension RichTextSpanStyle { return RichAttributes(background: background?.hexString) case .align(let alignment): return RichAttributes(align: alignment) + case .link(let link): + return RichAttributes(link: link) } } } - -public extension Collection where Element == RichTextSpanStyle { +extension Collection where Element == RichTextSpanStyle { /** Check if the collection contains a certain style. @@ -414,12 +432,12 @@ public extension Collection where Element == RichTextSpanStyle { - Parameters: - style: The style to look for. */ - func hasStyle(_ style: RichTextSpanStyle) -> Bool { + public func hasStyle(_ style: RichTextSpanStyle) -> Bool { contains(style) } /// Check if a certain style change should be applied. - func shouldAddOrRemove( + public func shouldAddOrRemove( _ style: RichTextSpanStyle, _ newValue: Bool ) -> Bool { diff --git a/Sources/RichEditorSwiftUI/UI/Extensions/Character+Extension.swift b/Sources/RichEditorSwiftUI/UI/Extensions/Character+Extension.swift index 0bae43a..e95314c 100644 --- a/Sources/RichEditorSwiftUI/UI/Extensions/Character+Extension.swift +++ b/Sources/RichEditorSwiftUI/UI/Extensions/Character+Extension.swift @@ -14,4 +14,3 @@ extension Character { self == .newLine || self == .carriageReturn } } - diff --git a/Sources/RichEditorSwiftUI/UI/Extensions/Color+Extension.swift b/Sources/RichEditorSwiftUI/UI/Extensions/Color+Extension.swift index 64286e1..e6a0919 100644 --- a/Sources/RichEditorSwiftUI/UI/Extensions/Color+Extension.swift +++ b/Sources/RichEditorSwiftUI/UI/Extensions/Color+Extension.swift @@ -10,7 +10,8 @@ import SwiftUI extension Color { var hexString: String? { // Convert to ColorRepresentable and get components - guard let components = ColorRepresentable(self).cgColor.components else { return nil } + guard let components = ColorRepresentable(self).cgColor.components + else { return nil } let r = components[0] let g = components.count >= 3 ? components[1] : r @@ -19,24 +20,26 @@ extension Color { // Format the hex string with alpha if necessary if a < 1.0 { - return String(format: "#%02lX%02lX%02lX%02lX", - lround(Double(r * 255)), - lround(Double(g * 255)), - lround(Double(b * 255)), - lround(Double(a * 255))) + return String( + format: "#%02lX%02lX%02lX%02lX", + lround(Double(r * 255)), + lround(Double(g * 255)), + lround(Double(b * 255)), + lround(Double(a * 255))) } else { - return String(format: "#%02lX%02lX%02lX", - lround(Double(r * 255)), - lround(Double(g * 255)), - lround(Double(b * 255))) + return String( + format: "#%02lX%02lX%02lX", + lround(Double(r * 255)), + lround(Double(g * 255)), + lround(Double(b * 255))) } } } - -public extension Color { - init(hex string: String) { - var string: String = string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) +extension Color { + public init(hex string: String) { + var string: String = string.trimmingCharacters( + in: CharacterSet.whitespacesAndNewlines) if string.hasPrefix("#") { _ = string.removeFirst() } @@ -77,7 +80,7 @@ public extension Color { let blue = Double(b) / 255.0 self.init(.sRGB, red: red, green: green, blue: blue, opacity: 1) } else if string.count == 8 { - let mask = 0x000000FF + let mask = 0x0000_00FF let r = Int(color >> 24) & mask let g = Int(color >> 16) & mask let b = Int(color >> 8) & mask @@ -109,9 +112,13 @@ extension ColorRepresentable { } if alpha { - return String(format: "%02lX%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255), lroundf(a * 255)) + return String( + format: "%02lX%02lX%02lX%02lX", lroundf(r * 255), + lroundf(g * 255), lroundf(b * 255), lroundf(a * 255)) } else { - return String(format: "%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255)) + return String( + format: "%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), + lroundf(b * 255)) } } } diff --git a/Sources/RichEditorSwiftUI/UI/Extensions/FontRepresentable+Extension.swift b/Sources/RichEditorSwiftUI/UI/Extensions/FontRepresentable+Extension.swift index 6a9956c..a2ee81f 100644 --- a/Sources/RichEditorSwiftUI/UI/Extensions/FontRepresentable+Extension.swift +++ b/Sources/RichEditorSwiftUI/UI/Extensions/FontRepresentable+Extension.swift @@ -10,117 +10,117 @@ import SwiftUI extension FontRepresentable { /// Check if font is bold var isBold: Bool { -#if os(macOS) - return fontDescriptor.symbolicTraits.contains(.bold) -#else - return fontDescriptor.symbolicTraits.contains(.traitBold) -#endif + #if os(macOS) + return fontDescriptor.symbolicTraits.contains(.bold) + #else + return fontDescriptor.symbolicTraits.contains(.traitBold) + #endif } - + /// Check if font is italic var isItalic: Bool { -#if os(macOS) - return fontDescriptor.symbolicTraits.contains(.italic) -#else - return fontDescriptor.symbolicTraits.contains(.traitItalic) -#endif + #if os(macOS) + return fontDescriptor.symbolicTraits.contains(.italic) + #else + return fontDescriptor.symbolicTraits.contains(.traitItalic) + #endif } - + /// Make font **Bold** func makeBold() -> FontRepresentable { if isBold { return self } else { let fontDesc = fontDescriptor.byTogglingStyle(.bold) -#if os(macOS) - if let familyName { + #if os(macOS) + if let familyName { + fontDesc.withFamily(familyName) + } + return FontRepresentable( + descriptor: fontDesc, + size: pointSize + ) ?? self + #else fontDesc.withFamily(familyName) - } - return FontRepresentable( - descriptor: fontDesc, - size: pointSize - ) ?? self -#else - fontDesc.withFamily(familyName) - return FontRepresentable(descriptor: fontDesc, size: pointSize) -#endif + return FontRepresentable(descriptor: fontDesc, size: pointSize) + #endif } } - + /// Make font **Italic** func makeItalic() -> FontRepresentable { if isItalic { return self } else { let fontDesc = fontDescriptor.byTogglingStyle(.italic) -#if os(macOS) - if let familyName { + #if os(macOS) + if let familyName { + fontDesc.withFamily(familyName) + } + return FontRepresentable( + descriptor: fontDesc, + size: pointSize + ) ?? self + #else fontDesc.withFamily(familyName) - } - return FontRepresentable( - descriptor: fontDesc, - size: pointSize - ) ?? self -#else - fontDesc.withFamily(familyName) - return FontRepresentable(descriptor: fontDesc, size: pointSize) -#endif + return FontRepresentable(descriptor: fontDesc, size: pointSize) + #endif } } - + /// Make font **Bold** and **Italic** func setBoldItalicStyles() -> FontRepresentable { return makeBold().makeItalic() } - + /// Remove **Bold** style from font func removeBoldStyle() -> FontRepresentable { if !isBold { return self } else { let fontDesc = fontDescriptor.byTogglingStyle(.bold) -#if os(macOS) - if let familyName { + #if os(macOS) + if let familyName { + fontDesc.withFamily(familyName) + } + return FontRepresentable( + descriptor: fontDesc, + size: pointSize + ) ?? self + #else fontDesc.withFamily(familyName) - } - return FontRepresentable( - descriptor: fontDesc, - size: pointSize - ) ?? self -#else - fontDesc.withFamily(familyName) - return FontRepresentable(descriptor: fontDesc, size: pointSize) -#endif + return FontRepresentable(descriptor: fontDesc, size: pointSize) + #endif } } - + /// Remove **Italic** style from font func removeItalicStyle() -> FontRepresentable { if !isItalic { return self } else { let fontDesc = fontDescriptor.byTogglingStyle(.italic) -#if os(macOS) - if let familyName { + #if os(macOS) + if let familyName { + fontDesc.withFamily(familyName) + } + return FontRepresentable( + descriptor: fontDesc, + size: pointSize + ) ?? self + #else fontDesc.withFamily(familyName) - } - return FontRepresentable( - descriptor: fontDesc, - size: pointSize - ) ?? self -#else - fontDesc.withFamily(familyName) - return FontRepresentable(descriptor: fontDesc, size: pointSize) -#endif + return FontRepresentable(descriptor: fontDesc, size: pointSize) + #endif } } - + /// Remove **Bold** and **Italic** style from font func makeNormal() -> FontRepresentable { return removeBoldStyle().removeItalicStyle() } - + /// Toggle **Bold** style of font func toggleBoldTrait() -> FontRepresentable { if isBold { @@ -129,7 +129,7 @@ extension FontRepresentable { return makeBold() } } - + /// Toggle **Italic** style of font func toggleItalicStyle() -> FontRepresentable { if isItalic { @@ -138,23 +138,23 @@ extension FontRepresentable { return makeItalic() } } - + /// Get a new font with updated font size by **size** func updateFontSize(size: CGFloat) -> FontRepresentable { if pointSize != size { let fontDesc = fontDescriptor -#if os(macOS) - if let familyName { + #if os(macOS) + if let familyName { + fontDesc.withFamily(familyName) + } + return FontRepresentable( + descriptor: fontDesc, + size: size + ) ?? self + #else fontDesc.withFamily(familyName) - } - return FontRepresentable( - descriptor: fontDesc, - size: size - ) ?? self -#else - fontDesc.withFamily(familyName) - return FontRepresentable(descriptor: fontDesc, size: size) -#endif + return FontRepresentable(descriptor: fontDesc, size: size) + #endif } else { return self } @@ -164,22 +164,24 @@ extension FontRepresentable { func updateFontName(with name: String) -> FontRepresentable { if fontName != name { let fontDesc = fontDescriptor -#if os(macOS) - fontDesc.withFamily(name) - return FontRepresentable( - descriptor: fontDesc, - size: pointSize - ) ?? self -#else - fontDesc.withFamily(name) - return FontRepresentable(descriptor: fontDesc, size: pointSize) -#endif + #if os(macOS) + fontDesc.withFamily(name) + return FontRepresentable( + descriptor: fontDesc, + size: pointSize + ) ?? self + #else + fontDesc.withFamily(name) + return FontRepresentable(descriptor: fontDesc, size: pointSize) + #endif } else { return self } } - func updateNameAndSize(with name: String? = nil, size: CGFloat? = nil) -> FontRepresentable { + func updateNameAndSize(with name: String? = nil, size: CGFloat? = nil) + -> FontRepresentable + { var font = self if let size { font = font.updateFontSize(size: size) @@ -194,77 +196,84 @@ extension FontRepresentable { if pointSize != multiple * pointSize { let size = multiple * pointSize let fontDesc = fontDescriptor -#if os(macOS) - if let familyName { + #if os(macOS) + if let familyName { + fontDesc.withFamily(familyName) + } + return FontRepresentable( + descriptor: fontDesc, + size: size + ) ?? self + #else fontDesc.withFamily(familyName) - } - return FontRepresentable( - descriptor: fontDesc, - size: size - ) ?? self -#else - fontDesc.withFamily(familyName) - return FontRepresentable(descriptor: fontDesc, size: size) -#endif + return FontRepresentable(descriptor: fontDesc, size: size) + #endif } else { return self } } } -public extension FontRepresentable { +extension FontRepresentable { /// Get a new font by adding a text style. - func addFontStyle(_ style: RichTextSpanStyle) -> FontRepresentable { - guard let style = style.richTextStyle, let trait = style.symbolicTraits, !fontDescriptor.symbolicTraits.contains(trait) else { return self } + public func addFontStyle(_ style: RichTextSpanStyle) -> FontRepresentable { + guard let style = style.richTextStyle, let trait = style.symbolicTraits, + !fontDescriptor.symbolicTraits.contains(trait) + else { return self } let fontDesc = fontDescriptor.byTogglingStyle(style) -#if os(macOS) - if let familyName { + #if os(macOS) + if let familyName { + fontDesc.withFamily(familyName) + } + return FontRepresentable( + descriptor: fontDesc, + size: pointSize + ) ?? self + #else fontDesc.withFamily(familyName) - } - return FontRepresentable( - descriptor: fontDesc, - size: pointSize - ) ?? self -#else - fontDesc.withFamily(familyName) - return FontRepresentable(descriptor: fontDesc, size: pointSize) -#endif + return FontRepresentable(descriptor: fontDesc, size: pointSize) + #endif } - + ///Get a new font by removing a text style. - func removeFontStyle(_ style: RichTextSpanStyle) -> FontRepresentable { - guard let style = style.richTextStyle, let trait = style.symbolicTraits, fontDescriptor.symbolicTraits.contains(trait) else { return self } + public func removeFontStyle(_ style: RichTextSpanStyle) -> FontRepresentable + { + guard let style = style.richTextStyle, let trait = style.symbolicTraits, + fontDescriptor.symbolicTraits.contains(trait) + else { return self } let fontDesc = fontDescriptor.byTogglingStyle(style) -#if os(macOS) - if let familyName { + #if os(macOS) + if let familyName { + fontDesc.withFamily(familyName) + } + return FontRepresentable( + descriptor: fontDesc, + size: pointSize + ) ?? self + #else fontDesc.withFamily(familyName) - } - return FontRepresentable( - descriptor: fontDesc, - size: pointSize - ) ?? self -#else - fontDesc.withFamily(familyName) - return FontRepresentable(descriptor: fontDesc, size: pointSize) -#endif + return FontRepresentable(descriptor: fontDesc, size: pointSize) + #endif } - + /// Get a new font by toggling a text style. - func byTogglingFontStyle(_ style: RichTextSpanStyle) -> FontRepresentable { + public func byTogglingFontStyle(_ style: RichTextSpanStyle) + -> FontRepresentable + { guard let style = style.richTextStyle else { return self } let fontDesc = fontDescriptor.byTogglingStyle(style) -#if os(macOS) - if let familyName { - fontDesc.withFamily(familyName) - } - return FontRepresentable( - descriptor: fontDesc, - size: pointSize + #if os(macOS) + if let familyName { + fontDesc.withFamily(familyName) + } + return FontRepresentable( + descriptor: fontDesc, + size: pointSize - ) ?? self -#else - fontDesc.withFamily(familyName) - return FontRepresentable(descriptor: fontDesc, size: pointSize) -#endif + ) ?? self + #else + fontDesc.withFamily(familyName) + return FontRepresentable(descriptor: fontDesc, size: pointSize) + #endif } } diff --git a/Sources/RichEditorSwiftUI/UI/Extensions/NSAttributedString+Empty.swift b/Sources/RichEditorSwiftUI/UI/Extensions/NSAttributedString+Empty.swift index 683a1c3..2093663 100644 --- a/Sources/RichEditorSwiftUI/UI/Extensions/NSAttributedString+Empty.swift +++ b/Sources/RichEditorSwiftUI/UI/Extensions/NSAttributedString+Empty.swift @@ -7,10 +7,10 @@ import Foundation -public extension NSAttributedString { +extension NSAttributedString { /// Create an empty attributed string. - static var empty: NSAttributedString { + public static var empty: NSAttributedString { .init(string: "") } } diff --git a/Sources/RichEditorSwiftUI/UI/Extensions/NSMutableParagraphStyle+Custom.swift b/Sources/RichEditorSwiftUI/UI/Extensions/NSMutableParagraphStyle+Custom.swift index f90c511..1649251 100644 --- a/Sources/RichEditorSwiftUI/UI/Extensions/NSMutableParagraphStyle+Custom.swift +++ b/Sources/RichEditorSwiftUI/UI/Extensions/NSMutableParagraphStyle+Custom.swift @@ -8,9 +8,9 @@ import Foundation #if canImport(UIKit) -import UIKit + import UIKit #elseif canImport(AppKit) && !targetEnvironment(macCatalyst) -import AppKit + import AppKit #endif extension NSMutableParagraphStyle { diff --git a/Sources/RichEditorSwiftUI/UI/Extensions/NSRange+Extension.swift b/Sources/RichEditorSwiftUI/UI/Extensions/NSRange+Extension.swift index 2066334..d4658c0 100644 --- a/Sources/RichEditorSwiftUI/UI/Extensions/NSRange+Extension.swift +++ b/Sources/RichEditorSwiftUI/UI/Extensions/NSRange+Extension.swift @@ -11,7 +11,7 @@ extension NSRange { var isCollapsed: Bool { return self.length == 0 || self.upperBound == self.lowerBound } - + var closedRange: ClosedRange { return lowerBound...(upperBound - (length > 0 ? 1 : 0)) } @@ -23,15 +23,18 @@ extension ClosedRange { } func isInRange(_ range: ClosedRange) -> Bool { - return range.contains(self.lowerBound) && range.contains(self.upperBound) + return range.contains(self.lowerBound) + && range.contains(self.upperBound) } - + func isPartialOverlap(_ range: ClosedRange) -> Bool { - return self.contains(range.lowerBound) != self.contains(range.upperBound) + return self.contains(range.lowerBound) + != self.contains(range.upperBound) } func isSameAs(_ range: ClosedRange) -> Bool { - return (self.lowerBound == range.lowerBound) && (self.upperBound == range.upperBound) + return (self.lowerBound == range.lowerBound) + && (self.upperBound == range.upperBound) } } diff --git a/Sources/RichEditorSwiftUI/UI/Extensions/String+Characters.swift b/Sources/RichEditorSwiftUI/UI/Extensions/String+Characters.swift index f227c53..1df2458 100644 --- a/Sources/RichEditorSwiftUI/UI/Extensions/String+Characters.swift +++ b/Sources/RichEditorSwiftUI/UI/Extensions/String+Characters.swift @@ -7,38 +7,37 @@ import Foundation -internal extension String.Element { +extension String.Element { /// Get the string element for a `\r` carriage return. static var carriageReturn: String.Element { "\r" } - + /// Get the string element for a `\n` newline. static var newLine: String.Element { "\n" } - + /// Get the string element for a `\t` tab. static var tab: String.Element { "\t" } - + /// Get the string element for a ` ` space. static var space: String.Element { " " } } +extension String { -internal extension String { - /// Get the string for a `\r` carriage return. static let carriageReturn = String(.carriageReturn) - + /// Get the string for a `\n` newline. static let newLine = String(.newLine) - + /// Get the string for a `\t` tab. static let tab = String(.tab) - + /// Get the string for a ` ` space. static let space = String(.space) } -internal extension String { +extension String { func getHeaderRangeFor(_ range: NSRange) -> NSRange { let text = self guard !text.isEmpty else { return range } @@ -46,11 +45,18 @@ internal extension String { let fromIndex = range.lowerBound let toIndex = range.isCollapsed ? fromIndex : range.upperBound - let newLineStartIndex = text.utf16.prefix(fromIndex).map({ $0 }).lastIndex(of: "\n".utf16.last) ?? 0 - let newLineEndIndex = text.utf16.suffix(from: text.utf16.index(text.utf16.startIndex, offsetBy: max(0, toIndex - 1))).map({ $0 }).firstIndex(of: "\n".utf16.last) + let newLineStartIndex = + text.utf16.prefix(fromIndex).map({ $0 }).lastIndex( + of: "\n".utf16.last) ?? 0 + let newLineEndIndex = text.utf16.suffix( + from: text.utf16.index( + text.utf16.startIndex, offsetBy: max(0, toIndex - 1)) + ).map({ $0 }).firstIndex(of: "\n".utf16.last) let shouldAddOneIndex = newLineStartIndex != 0 - let startIndex = min(max(0, self.utf16Length), max(0, newLineStartIndex + (shouldAddOneIndex ? 1 : 0))) + let startIndex = min( + max(0, self.utf16Length), + max(0, newLineStartIndex + (shouldAddOneIndex ? 1 : 0))) var endIndex = (toIndex) + (newLineEndIndex ?? 0) if newLineEndIndex == nil { diff --git a/Sources/RichEditorSwiftUI/UI/Extensions/String+Extension.swift b/Sources/RichEditorSwiftUI/UI/Extensions/String+Extension.swift index f4ba05f..98f04a7 100644 --- a/Sources/RichEditorSwiftUI/UI/Extensions/String+Extension.swift +++ b/Sources/RichEditorSwiftUI/UI/Extensions/String+Extension.swift @@ -8,24 +8,27 @@ import SwiftUI extension String { - subscript (i: Int) -> String { - return self[i ..< i + 1] + subscript(i: Int) -> String { + return self[i.. String { - return self[min(fromIndex, count) ..< count] + return self[min(fromIndex, count).. String { - return self[0 ..< max(0, toIndex)] + return self[0..) -> String { - let range = Range(uncheckedBounds: (lower: max(0, min(count, r.lowerBound)), - upper: min(count, max(0, r.upperBound)))) + subscript(r: Range) -> String { + let range = Range( + uncheckedBounds: ( + lower: max(0, min(count, r.lowerBound)), + upper: min(count, max(0, r.upperBound)) + )) let start = index(startIndex, offsetBy: range.lowerBound) let end = index(start, offsetBy: range.upperBound - range.lowerBound) - return String(self[start ..< end]) + return String(self[start.. String { @@ -42,7 +45,9 @@ internal struct NSFontTraitMask: OptionSet { internal static let unboldFontMask = NSFontTraitMask(rawValue: 1 << 1) internal static let italicFontMask = NSFontTraitMask(rawValue: 1 << 2) internal static let unitalicFontMask = NSFontTraitMask(rawValue: 1 << 3) - internal static let all: NSFontTraitMask = [.boldFontMask, .unboldFontMask, .italicFontMask, .unitalicFontMask] + internal static let all: NSFontTraitMask = [ + .boldFontMask, .unboldFontMask, .italicFontMask, .unitalicFontMask, + ] internal init(rawValue: Int) { self.rawValue = rawValue } @@ -73,70 +78,67 @@ internal struct NSFontTraitMask: OptionSet { // } //} +/// This extension makes it possible to fetch characters from a +/// string, as discussed here: +/// +/// https://stackoverflow.com/questions/24092884/get-nth-character-of-a-string-in-swift-programming-language +extension StringProtocol { -/** - This extension makes it possible to fetch characters from a - string, as discussed here: - - https://stackoverflow.com/questions/24092884/get-nth-character-of-a-string-in-swift-programming-language - */ -public extension StringProtocol { - - func character(at index: Int) -> String.Element? { + public func character(at index: Int) -> String.Element? { if index < 0 { return nil } guard count > index else { return nil } return self[index] } - func character(at index: UInt) -> String.Element? { + public func character(at index: UInt) -> String.Element? { character(at: Int(index)) } - subscript(_ offset: Int) -> Element { + public subscript(_ offset: Int) -> Element { self[index(startIndex, offsetBy: offset)] } - subscript(_ range: Range) -> SubSequence { + public subscript(_ range: Range) -> SubSequence { prefix(range.lowerBound + range.count).suffix(range.count) } - subscript(_ range: ClosedRange) -> SubSequence { + public subscript(_ range: ClosedRange) -> SubSequence { prefix(range.lowerBound + range.count).suffix(range.count) } - subscript(_ range: PartialRangeThrough) -> SubSequence { + public subscript(_ range: PartialRangeThrough) -> SubSequence { prefix(range.upperBound.advanced(by: 1)) } - subscript(_ range: PartialRangeUpTo) -> SubSequence { + public subscript(_ range: PartialRangeUpTo) -> SubSequence { prefix(range.upperBound) } - subscript(_ range: PartialRangeFrom) -> SubSequence { - suffix(Swift.max(0, count-range.lowerBound)) + public subscript(_ range: PartialRangeFrom) -> SubSequence { + suffix(Swift.max(0, count - range.lowerBound)) } } -private extension LosslessStringConvertible { +extension LosslessStringConvertible { - var string: String { .init(self) } + fileprivate var string: String { .init(self) } } -private extension BidirectionalCollection { +extension BidirectionalCollection { - subscript(safe offset: Int) -> Element? { + fileprivate subscript(safe offset: Int) -> Element? { if isEmpty { return nil } - guard let index = index( - startIndex, - offsetBy: offset, - limitedBy: index(before: endIndex)) + guard + let index = index( + startIndex, + offsetBy: offset, + limitedBy: index(before: endIndex)) else { return nil } return self[index] } } - -internal extension String { +extension String { /** This will provide length of string with UTF16 character count */ diff --git a/Sources/RichEditorSwiftUI/UI/Extensions/View+BackportSupportExtension.swift b/Sources/RichEditorSwiftUI/UI/Extensions/View+BackportSupportExtension.swift index 55d9fad..1d61051 100644 --- a/Sources/RichEditorSwiftUI/UI/Extensions/View+BackportSupportExtension.swift +++ b/Sources/RichEditorSwiftUI/UI/Extensions/View+BackportSupportExtension.swift @@ -8,11 +8,13 @@ import SwiftUI extension View { - nonisolated public func onChangeBackPort(of value: V, _ action: @escaping (_ newValue: V) -> Void) -> some View where V : Equatable { + nonisolated public func onChangeBackPort( + of value: V, _ action: @escaping (_ newValue: V) -> Void + ) -> some View where V: Equatable { Group { if #available(iOS 17.0, macOS 14.0, watchOS 10.0, *) { self - //iOS17~ + //iOS17~ .onChange(of: value) { oldValue, newValue in action(newValue) } diff --git a/Sources/RichEditorSwiftUI/UI/Helper/RichEditorState+LineInfo.swift b/Sources/RichEditorSwiftUI/UI/Helper/RichEditorState+LineInfo.swift index 284ddfd..5d9b19e 100644 --- a/Sources/RichEditorSwiftUI/UI/Helper/RichEditorState+LineInfo.swift +++ b/Sources/RichEditorSwiftUI/UI/Helper/RichEditorState+LineInfo.swift @@ -7,7 +7,6 @@ import Foundation - extension RichEditorState { internal struct LineInfo { @@ -31,7 +30,9 @@ extension RichEditorState { return getCurrentLineInfo(rawText, selectedRange: selectedRange) } - internal func getCurrentLineInfo(_ string: String, selectedRange: NSRange) -> LineInfo { + internal func getCurrentLineInfo(_ string: String, selectedRange: NSRange) + -> LineInfo + { /// Determines if the user has selected (ie. highlighted) any text var hasSelectedText: Bool { @@ -43,33 +44,34 @@ extension RichEditorState { selectedRange.location } - //The line number that we're currently iterating on var lineNumber = 0 //The line number & line of text that we believe the caret to be on var selectedLineNumber = 0 - var selectedLineRange = selectedRange + var selectedLineRange = selectedRange var selectedLineOfText = "" var caretLocationInLine = 0 var foundSelectedLine = false //Iterate over every line in our TextView - string.enumerateSubstrings(in: string.startIndex..= startOfLine && caretLocation <= endOfLine { // MARK the line number selectedLineNumber = lineNumber selectedLineOfText = substring ?? "" - selectedLineRange = range + selectedLineRange = range caretLocationInLine = caretLocation - startOfLine foundSelectedLine = true @@ -82,8 +84,10 @@ extension RichEditorState { if caretLocation > 0 && !foundSelectedLine { selectedLineNumber = lineNumber selectedLineOfText = "" - selectedLineRange = NSRange(location: caretLocation, length: 0) + selectedLineRange = NSRange(location: caretLocation, length: 0) } - return LineInfo(lineNumber: selectedLineNumber, lineRange: selectedLineRange, lineString: selectedLineOfText, caretLocation: caretLocationInLine) + return LineInfo( + lineNumber: selectedLineNumber, lineRange: selectedLineRange, + lineString: selectedLineOfText, caretLocation: caretLocationInLine) } } diff --git a/Sources/RichEditorSwiftUI/UI/Helper/RichTextReader.swift b/Sources/RichEditorSwiftUI/UI/Helper/RichTextReader.swift index d3668ba..5b00d77 100644 --- a/Sources/RichEditorSwiftUI/UI/Helper/RichTextReader.swift +++ b/Sources/RichEditorSwiftUI/UI/Helper/RichTextReader.swift @@ -7,76 +7,74 @@ import Foundation -/** - This protocol can be implemented any types that can provide - a rich text string. - - The protocol is implemented by `NSAttributedString` as well - as other types in the library. - */ +/// This protocol can be implemented any types that can provide +/// a rich text string. +/// +/// The protocol is implemented by `NSAttributedString` as well +/// as other types in the library. public protocol RichTextReader { - + /// The attributed string to use as rich text. var attributedString: NSAttributedString { get } } extension NSAttributedString: RichTextReader { - + /// This type returns itself as the attributed string. public var attributedString: NSAttributedString { self } } -public extension RichTextReader { - +extension RichTextReader { + /** The rich text to use. - + This is a convenience name alias for ``attributedString`` to provide this type with a property that uses the rich text naming convention. */ - var richText: NSAttributedString { + public var richText: NSAttributedString { attributedString } - + /** Get the range of the entire ``richText``. - + This uses `safeRange(for:)` to return a range that willö always be valid for the current rich text. */ - var richTextRange: NSRange { + public var richTextRange: NSRange { let range = NSRange(location: 0, length: richText.string.utf16Length) let safeRange = safeRange(for: range) return safeRange } - + /** Get the rich text at a certain range. - + Since this function uses `safeRange(for:)` to not crash for invalid ranges, always use this function instead of the unsafe `attributedSubstring`. - + - Parameters: - range: The range for which to get the rich text. */ - func richText(at range: NSRange) -> NSAttributedString { + public func richText(at range: NSRange) -> NSAttributedString { let range = safeRange(for: range) return attributedString.attributedSubstring(from: range) } - + /** Get a safe range for the provided range. - + A safe range is limited to the bounds of the attributed string and helps protecting against range overflow. - + - Parameters: - range: The range for which to get a safe range. - isAttributeOperation: Set this to `true` to avoid last position. */ - func safeRange( + public func safeRange( for range: NSRange, isAttributeOperation: Bool = false ) -> NSRange { diff --git a/Sources/RichEditorSwiftUI/UI/Parser/EditorAdapter.swift b/Sources/RichEditorSwiftUI/UI/Parser/EditorAdapter.swift index b2ba241..61a8234 100644 --- a/Sources/RichEditorSwiftUI/UI/Parser/EditorAdapter.swift +++ b/Sources/RichEditorSwiftUI/UI/Parser/EditorAdapter.swift @@ -6,6 +6,7 @@ // import Foundation + protocol EditorAdapter { func encodeToString(type model: T) throws -> String? func decode(from jsonString: String) throws -> T? @@ -33,7 +34,7 @@ class DefaultAdapter: EditorAdapter { } } - func encode(type model: T) throws -> Data where T : Encodable { - return try JSONEncoder().encode(model) + func encode(type model: T) throws -> Data where T: Encodable { + return try JSONEncoder().encode(model) } } diff --git a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichEditorStateFocusedValueKey.swift b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichEditorStateFocusedValueKey.swift index b6f9b6a..ea41a7b 100644 --- a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichEditorStateFocusedValueKey.swift +++ b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichEditorStateFocusedValueKey.swift @@ -7,17 +7,17 @@ import SwiftUI -public extension RichEditorState { +extension RichEditorState { /// This key can be used to keep track of a context in a /// multi-windowed app. - struct FocusedValueKey: SwiftUI.FocusedValueKey { + public struct FocusedValueKey: SwiftUI.FocusedValueKey { public typealias Value = RichEditorState } } -public extension FocusedValues { +extension FocusedValues { /// This value can be used to keep track of a context in /// a multi-windowed app. @@ -35,7 +35,7 @@ public extension FocusedValues { /// @FocusedValue(\.richEditorState) /// var richEditorState: RichEditorState? /// ``` - var richEditorState: RichEditorState.FocusedValueKey.Value? { + public var richEditorState: RichEditorState.FocusedValueKey.Value? { get { self[RichEditorState.FocusedValueKey.self] } set { self[RichEditorState.FocusedValueKey.self] = newValue } } diff --git a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextEditor+Config.swift b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextEditor+Config.swift index c602c17..7505e5c 100644 --- a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextEditor+Config.swift +++ b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextEditor+Config.swift @@ -5,37 +5,36 @@ // Created by Divyesh Vekariya on 21/10/24. // -#if iOS || macOS || os(tvOS) || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) + import SwiftUI -/// This struct be used to configure a ``RichTextEditor``. -public typealias RichTextEditorConfig = RichTextView.Configuration + /// This struct be used to configure a ``RichTextEditor``. + public typealias RichTextEditorConfig = RichTextView.Configuration -public extension View { + extension View { - /// Apply a ``RichTextEditor`` configuration. - func richTextEditorConfig( - _ config: RichTextEditorConfig - ) -> some View { - self.environment(\.richTextEditorConfig, config) + /// Apply a ``RichTextEditor`` configuration. + public func richTextEditorConfig( + _ config: RichTextEditorConfig + ) -> some View { + self.environment(\.richTextEditorConfig, config) + } } -} -private extension RichTextEditorConfig { + extension RichTextEditorConfig { - struct Key: EnvironmentKey { + fileprivate struct Key: EnvironmentKey { - public static var defaultValue: RichTextEditorConfig = .standard + public static var defaultValue: RichTextEditorConfig = .standard + } } -} -public extension EnvironmentValues { + extension EnvironmentValues { - /// This value can bind to a rich text editor config. - var richTextEditorConfig: RichTextEditorConfig { - get { self [RichTextEditorConfig.Key.self] } - set { self [RichTextEditorConfig.Key.self] = newValue } + /// This value can bind to a rich text editor config. + public var richTextEditorConfig: RichTextEditorConfig { + get { self[RichTextEditorConfig.Key.self] } + set { self[RichTextEditorConfig.Key.self] = newValue } + } } -} #endif - diff --git a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextEditor+Style.swift b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextEditor+Style.swift index 25e3dfc..8d474ac 100644 --- a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextEditor+Style.swift +++ b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextEditor+Style.swift @@ -5,37 +5,36 @@ // Created by Divyesh Vekariya on 21/10/24. // -#if iOS || macOS || os(tvOS) || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) + import SwiftUI -/// This struct can be used to style a ``RichTextEditor``. -public typealias RichTextEditorStyle = RichTextView.Theme + /// This struct can be used to style a ``RichTextEditor``. + public typealias RichTextEditorStyle = RichTextView.Theme -public extension View { + extension View { - /// Apply a ``RichTextEditor`` style. - func richTextEditorStyle( - _ style: RichTextEditorStyle - ) -> some View { - self.environment(\.richTextEditorStyle, style) + /// Apply a ``RichTextEditor`` style. + public func richTextEditorStyle( + _ style: RichTextEditorStyle + ) -> some View { + self.environment(\.richTextEditorStyle, style) + } } -} -private extension RichTextEditorStyle { + extension RichTextEditorStyle { - struct Key: EnvironmentKey { + fileprivate struct Key: EnvironmentKey { - static var defaultValue: RichTextEditorStyle = .standard + static var defaultValue: RichTextEditorStyle = .standard + } } -} -public extension EnvironmentValues { + extension EnvironmentValues { - /// This value can bind to a rich text editor style. - var richTextEditorStyle: RichTextEditorStyle { - get { self [RichTextEditorStyle.Key.self] } - set { self [RichTextEditorStyle.Key.self] = newValue } + /// This value can bind to a rich text editor style. + public var richTextEditorStyle: RichTextEditorStyle { + get { self[RichTextEditorStyle.Key.self] } + set { self[RichTextEditorStyle.Key.self] = newValue } + } } -} #endif - diff --git a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config.swift b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config.swift index 320b186..d73f1f1 100644 --- a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config.swift +++ b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config.swift @@ -7,12 +7,12 @@ import Foundation -#if iOS || macOS || os(tvOS) || os(visionOS) -public extension RichTextView.Configuration { +#if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) + extension RichTextView.Configuration { - /// The standard rich text view configuration. - /// - /// You can set a new value to change the global default. - static var standard = Self() -} + /// The standard rich text view configuration. + /// + /// You can set a new value to change the global default. + public static var standard = Self() + } #endif diff --git a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config_AppKit.swift b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config_AppKit.swift index 6b47313..12c07f8 100644 --- a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config_AppKit.swift +++ b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config_AppKit.swift @@ -6,33 +6,33 @@ // #if macOS -import Foundation + import Foundation -public extension RichTextView { + extension RichTextView { - /** + /** This type can be used to configure a ``RichTextEditor``. */ - struct Configuration { + public struct Configuration { - /// Create a custom configuration - /// - Parameters: - /// - isScrollingEnabled: Whether or not the editor should scroll, by default `true`. - /// - isContinuousSpellCheckingEnabled: Whether the editor spell-checks in realtime. Defaults to `true`. - public init( - isScrollingEnabled: Bool = true, - isContinuousSpellCheckingEnabled: Bool = true - ) { - self.isScrollingEnabled = isScrollingEnabled - self.isContinuousSpellCheckingEnabled = isContinuousSpellCheckingEnabled - } + /// Create a custom configuration + /// - Parameters: + /// - isScrollingEnabled: Whether or not the editor should scroll, by default `true`. + /// - isContinuousSpellCheckingEnabled: Whether the editor spell-checks in realtime. Defaults to `true`. + public init( + isScrollingEnabled: Bool = true, + isContinuousSpellCheckingEnabled: Bool = true + ) { + self.isScrollingEnabled = isScrollingEnabled + self.isContinuousSpellCheckingEnabled = + isContinuousSpellCheckingEnabled + } - /// Whether or not the editor should scroll. - public var isScrollingEnabled: Bool + /// Whether or not the editor should scroll. + public var isScrollingEnabled: Bool - /// Whether the editor spell-checks in realtime. - public var isContinuousSpellCheckingEnabled: Bool + /// Whether the editor spell-checks in realtime. + public var isContinuousSpellCheckingEnabled: Bool + } } -} #endif - diff --git a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config_UIKit.swift b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config_UIKit.swift index 8bd10ae..f1a17d7 100644 --- a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config_UIKit.swift +++ b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Config_UIKit.swift @@ -5,17 +5,17 @@ // Created by Divyesh Vekariya on 21/10/24. // -#if iOS || os(tvOS) || os(visionOS) -import SwiftUI +#if os(iOS) || os(tvOS) || os(visionOS) + import SwiftUI -public extension RichTextView { + extension RichTextView { - /** + /** This type can be used to configure a ``RichTextEditor``. */ - struct Configuration { + public struct Configuration { - /** + /** Create a custom configuration. - Parameters: @@ -24,29 +24,30 @@ public extension RichTextView { - autocapitalizationType: Type of Auto capitalization, default is to `.sentences`. - spellCheckingType: Whether textView spell-Checks, default is `.no`. */ - public init( - isScrollingEnabled: Bool = true, - allowsEditingTextAttributes: Bool = true, - autocapitalizationType: UITextAutocapitalizationType = .sentences, - spellCheckingType: UITextSpellCheckingType = .no - ) { - self.isScrollingEnabled = isScrollingEnabled - self.allowsEditingTextAttributes = allowsEditingTextAttributes - self.autocapitalizationType = autocapitalizationType - self.spellCheckingType = spellCheckingType + public init( + isScrollingEnabled: Bool = true, + allowsEditingTextAttributes: Bool = true, + autocapitalizationType: UITextAutocapitalizationType = + .sentences, + spellCheckingType: UITextSpellCheckingType = .no + ) { + self.isScrollingEnabled = isScrollingEnabled + self.allowsEditingTextAttributes = allowsEditingTextAttributes + self.autocapitalizationType = autocapitalizationType + self.spellCheckingType = spellCheckingType + } + + /// Whether or not the editor should scroll. + public var isScrollingEnabled: Bool + + /// Whether textView allows editting text attributes + public var allowsEditingTextAttributes: Bool + + /// Kind of auto capitalization + public var autocapitalizationType: UITextAutocapitalizationType + + /// If TextView spell-checks the text. + public var spellCheckingType: UITextSpellCheckingType } - - /// Whether or not the editor should scroll. - public var isScrollingEnabled: Bool - - /// Whether textView allows editting text attributes - public var allowsEditingTextAttributes: Bool - - /// Kind of auto capitalization - public var autocapitalizationType: UITextAutocapitalizationType - - /// If TextView spell-checks the text. - public var spellCheckingType: UITextSpellCheckingType } -} #endif diff --git a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Setup.swift b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Setup.swift index d55a9cc..2fe40a7 100644 --- a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Setup.swift +++ b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Setup.swift @@ -5,26 +5,27 @@ // Created by Divyesh Vekariya on 21/10/24. // -#if iOS || macOS || os(tvOS) || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) + import SwiftUI -extension RichTextView { + extension RichTextView { - func setupSharedBehavior( - with text: NSAttributedString, - _ format: RichTextDataFormat? - ) { - attributedString = .empty - attributedString = text + func setupSharedBehavior( + with text: NSAttributedString, + _ format: RichTextDataFormat? + ) { + attributedString = .empty + attributedString = text - setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - } + setContentCompressionResistancePriority( + .defaultLow, for: .horizontal) + } - func setup(_ theme: RichTextView.Theme) { - guard richText.string.isEmpty else { return } - font = theme.font - textColor = theme.fontColor - backgroundColor = theme.backgroundColor + func setup(_ theme: RichTextView.Theme) { + guard richText.string.isEmpty else { return } + font = theme.font + textColor = theme.fontColor + backgroundColor = theme.backgroundColor + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Theme.swift b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Theme.swift index 693d5f2..854f3bf 100644 --- a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Theme.swift +++ b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView+Theme.swift @@ -5,17 +5,17 @@ // Created by Divyesh Vekariya on 21/10/24. // -#if iOS || macOS || os(tvOS) || os(visionOS) -import SwiftUI +#if os(iOS) || os(macOS) || os(tvOS) || os(visionOS) + import SwiftUI -public extension RichTextView { + extension RichTextView { - /** + /** This type can be used to configure a ``RichTextEditor``'s current color properties. */ - struct Theme { + public struct Theme { - /** + /** Create a custom configuration. - Parameters: @@ -23,28 +23,27 @@ public extension RichTextView { - fontColor: default `.textColor`. - backgroundColor: Color of whole textView default `.clear`. */ - public init( - font: FontRepresentable = .systemFont(ofSize: 16), - fontColor: ColorRepresentable = .textColor, - backgroundColor: ColorRepresentable = .clear - ) { - self.font = font - self.fontColor = fontColor - self.backgroundColor = backgroundColor + public init( + font: FontRepresentable = .systemFont(ofSize: 16), + fontColor: ColorRepresentable = .textColor, + backgroundColor: ColorRepresentable = .clear + ) { + self.font = font + self.fontColor = fontColor + self.backgroundColor = backgroundColor + } + + public let font: FontRepresentable + public let fontColor: ColorRepresentable + public let backgroundColor: ColorRepresentable } - - public let font: FontRepresentable - public let fontColor: ColorRepresentable - public let backgroundColor: ColorRepresentable } -} -public extension RichTextView.Theme { + extension RichTextView.Theme { - /// The standard rich text view theme. - /// - /// You can set a new value to change the global default. - static var standard = Self() -} + /// The standard rich text view theme. + /// + /// You can set a new value to change the global default. + public static var standard = Self() + } #endif - diff --git a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextViewRepresentable.swift b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextViewRepresentable.swift index db60261..580b8e2 100644 --- a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextViewRepresentable.swift +++ b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextViewRepresentable.swift @@ -6,15 +6,15 @@ // #if macOS -import AppKit + import AppKit -/// This typealias bridges UIKit & AppKit native text views. -public typealias RichTextViewRepresentable = NSTextView + /// This typealias bridges UIKit & AppKit native text views. + public typealias RichTextViewRepresentable = NSTextView #endif -#if iOS || os(tvOS) || os(visionOS) -import UIKit +#if os(iOS) || os(tvOS) || os(visionOS) + import UIKit -/// This typealias bridges UIKit & AppKit native text views. -public typealias RichTextViewRepresentable = UITextView + /// This typealias bridges UIKit & AppKit native text views. + public typealias RichTextViewRepresentable = UITextView #endif diff --git a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView_AppKit.swift b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView_AppKit.swift index ce206e1..b12db04 100644 --- a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView_AppKit.swift +++ b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView_AppKit.swift @@ -6,100 +6,97 @@ // #if macOS -import AppKit - -/** - This is a platform-agnostic rich text view that can be used - in both UIKit and AppKit. - - The view inherits `NSTextView` in AppKit and `UITextView` - in UIKit. It aims to make these views behave more alike and - make them implement ``RichTextViewComponent``, which is the - protocol that is used within this library. - - The view will apply a ``RichTextImageConfiguration/disabled`` - image config by default. You can change this by setting the - property manually or by using a ``RichTextDataFormat`` that - supports images. - */ -open class RichTextView: NSTextView, RichTextViewComponent { - - - // MARK: - Properties - - /// The configuration to use by the rich text view. - public var configuration: Configuration = .standard - - /// The theme for coloring and setting style to text view. - public var theme: Theme = .standard { - didSet { setup(theme) } - } + import AppKit + + /// This is a platform-agnostic rich text view that can be used + /// in both UIKit and AppKit. + /// + /// The view inherits `NSTextView` in AppKit and `UITextView` + /// in UIKit. It aims to make these views behave more alike and + /// make them implement ``RichTextViewComponent``, which is the + /// protocol that is used within this library. + /// + /// The view will apply a ``RichTextImageConfiguration/disabled`` + /// image config by default. You can change this by setting the + /// property manually or by using a ``RichTextDataFormat`` that + /// supports images. + open class RichTextView: NSTextView, RichTextViewComponent { + + // MARK: - Properties + + /// The configuration to use by the rich text view. + public var configuration: Configuration = .standard + + /// The theme for coloring and setting style to text view. + public var theme: Theme = .standard { + didSet { setup(theme) } + } - /// The style to use when highlighting text in the view. - public var highlightingStyle: RichTextHighlightingStyle = .standard + /// The style to use when highlighting text in the view. + public var highlightingStyle: RichTextHighlightingStyle = .standard - /// The image configuration to use by the rich text view. -// public var imageConfiguration: RichTextImageConfiguration = .disabled + /// The image configuration to use by the rich text view. + // public var imageConfiguration: RichTextImageConfiguration = .disabled - // MARK: - Overrides + // MARK: - Overrides - /// Paste the current pasteboard content into the view. - open override func paste(_ sender: Any?) { -// let pasteboard = NSPasteboard.general -// if let image = pasteboard.image { -// return pasteImage(image, at: selectedRange.location) -// } - super.paste(sender) - } + /// Paste the current pasteboard content into the view. + open override func paste(_ sender: Any?) { + // let pasteboard = NSPasteboard.general + // if let image = pasteboard.image { + // return pasteImage(image, at: selectedRange.location) + // } + super.paste(sender) + } - /** + /** Try to perform a certain drag operation, which will get and paste images from the drag info into the text. */ -// open override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool { -// let pasteboard = draggingInfo.draggingPasteboard -// if let images = pasteboard.images, images.count > 0 { -// pasteImages(images, at: selectedRange().location, moveCursorToPastedContent: true) -// return true -// } -// // Handle fileURLs that contain known image types -// let images = pasteboard.pasteboardItems?.compactMap { -// if let str = $0.string(forType: NSPasteboard.PasteboardType.fileURL), -// let url = URL(string: str), let image = ImageRepresentable(contentsOf: url) { -// let fileExtension = url.pathExtension.lowercased() -// let imageExtensions = ["jpg", "jpeg", "png", "gif", "tiff", "bmp", "heic"] -// if imageExtensions.contains(fileExtension) { -// return image -// } -// } -// return nil -// } ?? [ImageRepresentable]() -// if images.count > 0 { -// pasteImages(images, at: selectedRange().location, moveCursorToPastedContent: true) -// return true -// } -// -// return super.performDragOperation(draggingInfo) -// } - - open override func scrollWheel(with event: NSEvent) { - - if configuration.isScrollingEnabled { - return super.scrollWheel(with: event) + // open override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool { + // let pasteboard = draggingInfo.draggingPasteboard + // if let images = pasteboard.images, images.count > 0 { + // pasteImages(images, at: selectedRange().location, moveCursorToPastedContent: true) + // return true + // } + // // Handle fileURLs that contain known image types + // let images = pasteboard.pasteboardItems?.compactMap { + // if let str = $0.string(forType: NSPasteboard.PasteboardType.fileURL), + // let url = URL(string: str), let image = ImageRepresentable(contentsOf: url) { + // let fileExtension = url.pathExtension.lowercased() + // let imageExtensions = ["jpg", "jpeg", "png", "gif", "tiff", "bmp", "heic"] + // if imageExtensions.contains(fileExtension) { + // return image + // } + // } + // return nil + // } ?? [ImageRepresentable]() + // if images.count > 0 { + // pasteImages(images, at: selectedRange().location, moveCursorToPastedContent: true) + // return true + // } + // + // return super.performDragOperation(draggingInfo) + // } + + open override func scrollWheel(with event: NSEvent) { + + if configuration.isScrollingEnabled { + return super.scrollWheel(with: event) + } + + // 1st nextResponder is NSClipView + // 2nd nextResponder is NSScrollView + // 3rd nextResponder is NSResponder SwiftUIPlatformViewHost + self.nextResponder? + .nextResponder? + .nextResponder? + .scrollWheel(with: event) } - // 1st nextResponder is NSClipView - // 2nd nextResponder is NSScrollView - // 3rd nextResponder is NSResponder SwiftUIPlatformViewHost - self.nextResponder? - .nextResponder? - .nextResponder? - .scrollWheel(with: event) - } - - // MARK: - Setup + // MARK: - Setup - /** + /** Setup the rich text view with a rich text and a certain ``RichTextDataFormat``. @@ -107,41 +104,46 @@ open class RichTextView: NSTextView, RichTextViewComponent { - text: The text to edit with the text view. - format: The rich text format to edit. */ - open func setup( - with text: NSAttributedString, - format: RichTextDataFormat? - ) { - setupSharedBehavior(with: text, format) - allowsImageEditing = true - allowsUndo = true - layoutManager?.defaultAttachmentScaling = NSImageScaling.scaleProportionallyDown - isContinuousSpellCheckingEnabled = configuration.isContinuousSpellCheckingEnabled - setup(theme) - } - - public func setup(with richText: RichText) { - var tempSpans: [RichTextSpanInternal] = [] - var text = "" - richText.spans.forEach({ - let span = RichTextSpanInternal(from: text.utf16Length, - to: (text.utf16Length + $0.insert.utf16Length - 1), - attributes: $0.attributes) - tempSpans.append(span) - text += $0.insert - }) - - let str = NSMutableAttributedString(string: text) - - tempSpans.forEach { span in - str.addAttributes(span.attributes?.toAttributes(font: .standardRichTextFont) ?? [:], range: span.spanRange) + open func setup( + with text: NSAttributedString, + format: RichTextDataFormat? + ) { + setupSharedBehavior(with: text, format) + allowsImageEditing = true + allowsUndo = true + layoutManager?.defaultAttachmentScaling = + NSImageScaling.scaleProportionallyDown + isContinuousSpellCheckingEnabled = + configuration.isContinuousSpellCheckingEnabled + setup(theme) } - setup(with: str, format: .archivedData) - } + public func setup(with richText: RichText) { + var tempSpans: [RichTextSpanInternal] = [] + var text = "" + richText.spans.forEach({ + let span = RichTextSpanInternal( + from: text.utf16Length, + to: (text.utf16Length + $0.insert.utf16Length - 1), + attributes: $0.attributes) + tempSpans.append(span) + text += $0.insert + }) + + let str = NSMutableAttributedString(string: text) + + tempSpans.forEach { span in + str.addAttributes( + span.attributes?.toAttributes(font: .standardRichTextFont) + ?? [:], range: span.spanRange) + } + + setup(with: str, format: .archivedData) + } - // MARK: - Open Functionality + // MARK: - Open Functionality - /** + /** Alert a certain title and message. - Parameters: @@ -149,106 +151,108 @@ open class RichTextView: NSTextView, RichTextViewComponent { - message: The alert message. - buttonTitle: The alert button title. */ - open func alert(title: String, message: String, buttonTitle: String) { - let alert = NSAlert() - alert.messageText = title - alert.informativeText = message - alert.alertStyle = NSAlert.Style.warning - alert.addButton(withTitle: buttonTitle) - alert.runModal() - } + open func alert(title: String, message: String, buttonTitle: String) { + let alert = NSAlert() + alert.messageText = title + alert.informativeText = message + alert.alertStyle = NSAlert.Style.warning + alert.addButton(withTitle: buttonTitle) + alert.runModal() + } - /// Copy the current selection. - open func copySelection() { - let pasteboard = NSPasteboard.general - let range = safeRange(for: selectedRange) - let text = richText(at: range) - pasteboard.clearContents() - pasteboard.setString(text.string, forType: .string) - } + /// Copy the current selection. + open func copySelection() { + let pasteboard = NSPasteboard.general + let range = safeRange(for: selectedRange) + let text = richText(at: range) + pasteboard.clearContents() + pasteboard.setString(text.string, forType: .string) + } - /// Try to redo the latest undone change. - open func redoLatestChange() { - undoManager?.redo() - } + /// Try to redo the latest undone change. + open func redoLatestChange() { + undoManager?.redo() + } - /// Scroll to a certain range. - open func scroll(to range: NSRange) { - scrollRangeToVisible(range) - } + /// Scroll to a certain range. + open func scroll(to range: NSRange) { + scrollRangeToVisible(range) + } - /// Set the rich text in the text view. - open func setRichText(_ text: NSAttributedString) { - attributedString = text - } + /// Set the rich text in the text view. + open func setRichText(_ text: NSAttributedString) { + attributedString = text + } - /// Undo the latest change. - open func undoLatestChange() { - undoManager?.undo() + /// Undo the latest change. + open func undoLatestChange() { + undoManager?.undo() + } } -} -// MARK: - Public Extensions + // MARK: - Public Extensions -public extension RichTextView { + extension RichTextView { - /// The text view's layout manager, if any. - var layoutManagerWrapper: NSLayoutManager? { - layoutManager - } + /// The text view's layout manager, if any. + public var layoutManagerWrapper: NSLayoutManager? { + layoutManager + } - /// The spacing between the text view edges and its text. - var textContentInset: CGSize { - get { textContainerInset } - set { textContainerInset = newValue } - } + /// The spacing between the text view edges and its text. + public var textContentInset: CGSize { + get { textContainerInset } + set { textContainerInset = newValue } + } - /// The text view's text storage, if any. - var textStorageWrapper: NSTextStorage? { - textStorage + /// The text view's text storage, if any. + public var textStorageWrapper: NSTextStorage? { + textStorage + } } -} -// MARK: - RichTextProvider + // MARK: - RichTextProvider -public extension RichTextView { + extension RichTextView { - /// Get the rich text that is managed by the view. - var attributedString: NSAttributedString { - get { attributedString() } - set { textStorage?.setAttributedString(newValue) } - } + /// Get the rich text that is managed by the view. + public var attributedString: NSAttributedString { + get { attributedString() } + set { textStorage?.setAttributedString(newValue) } + } - /// Whether or not the text view is the first responder. - var isFirstResponder: Bool { - window?.firstResponder == self + /// Whether or not the text view is the first responder. + public var isFirstResponder: Bool { + window?.firstResponder == self + } } -} -// MARK: - RichTextWriter + // MARK: - RichTextWriter -public extension RichTextView { + extension RichTextView { - // Get the rich text that is managed by the view. - var mutableAttributedString: NSMutableAttributedString? { - textStorage + // Get the rich text that is managed by the view. + public var mutableAttributedString: NSMutableAttributedString? { + textStorage + } } -} -// MARK: - Additional Pasteboard Types + // MARK: - Additional Pasteboard Types -public extension RichTextView { - override var readablePasteboardTypes: [NSPasteboard.PasteboardType] { - var pasteboardTypes = super.readablePasteboardTypes - pasteboardTypes.append(.png) - return pasteboardTypes + extension RichTextView { + public override var readablePasteboardTypes: + [NSPasteboard.PasteboardType] + { + var pasteboardTypes = super.readablePasteboardTypes + pasteboardTypes.append(.png) + return pasteboardTypes + } } -} -extension RichTextView { - var textString: String { - return self.string + extension RichTextView { + var textString: String { + return self.string + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView_UIKit.swift b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView_UIKit.swift index 4c5a8c0..dd79497 100644 --- a/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView_UIKit.swift +++ b/Sources/RichEditorSwiftUI/UI/TextViewUI/RichTextView_UIKit.swift @@ -5,72 +5,71 @@ // Created by Divyesh Vekariya on 19/10/24. // -#if iOS || os(tvOS) || os(visionOS) -import UIKit - -#if iOS || os(visionOS) -import UniformTypeIdentifiers - -extension RichTextView: UIDropInteractionDelegate {} -#endif - -/** - This is a platform-agnostic rich text view that can be used - in both UIKit and AppKit. - - The view inherits `NSTextView` in AppKit and `UITextView` - in UIKit. It aims to make these views behave more alike and - make them implement ``RichTextViewComponent``, which is the - protocol that is used within this library. - */ -open class RichTextView: UITextView, RichTextViewComponent { - - // MARK: - Initializers - - public convenience init( - data: Data, - format: RichTextDataFormat = .archivedData - ) throws { - self.init() - try self.setup(with: data, format: format) - } +#if os(iOS) || os(tvOS) || os(visionOS) + import UIKit + + #if os(iOS) || os(visionOS) + import UniformTypeIdentifiers + + extension RichTextView: UIDropInteractionDelegate {} + #endif + + /// This is a platform-agnostic rich text view that can be used + /// in both UIKit and AppKit. + /// + /// The view inherits `NSTextView` in AppKit and `UITextView` + /// in UIKit. It aims to make these views behave more alike and + /// make them implement ``RichTextViewComponent``, which is the + /// protocol that is used within this library. + open class RichTextView: UITextView, RichTextViewComponent { + + // MARK: - Initializers + + public convenience init( + data: Data, + format: RichTextDataFormat = .archivedData + ) throws { + self.init() + try self.setup(with: data, format: format) + } - public convenience init( - string: NSAttributedString, - format: RichTextDataFormat = .archivedData - ) { - self.init() - self.setup(with: string, format: format) - } + public convenience init( + string: NSAttributedString, + format: RichTextDataFormat = .archivedData + ) { + self.init() + self.setup(with: string, format: format) + } - public convenience init( - spans: [RichTextSpanInternal]? = nil - ) { - self.init() - } + public convenience init( + spans: [RichTextSpanInternal]? = nil + ) { + self.init() + } - // MARK: - Properties + // MARK: - Properties - /// The configuration to use by the rich text view. - public var configuration: Configuration = .standard { - didSet { - isScrollEnabled = configuration.isScrollingEnabled - allowsEditingTextAttributes = configuration.allowsEditingTextAttributes - autocapitalizationType = configuration.autocapitalizationType - spellCheckingType = configuration.spellCheckingType + /// The configuration to use by the rich text view. + public var configuration: Configuration = .standard { + didSet { + isScrollEnabled = configuration.isScrollingEnabled + allowsEditingTextAttributes = + configuration.allowsEditingTextAttributes + autocapitalizationType = configuration.autocapitalizationType + spellCheckingType = configuration.spellCheckingType + } } - } - public var theme: Theme = .standard { - didSet { - setup(theme) + public var theme: Theme = .standard { + didSet { + setup(theme) + } } - } - /// The style to use when highlighting text in the view. - public var highlightingStyle: RichTextHighlightingStyle = .standard + /// The style to use when highlighting text in the view. + public var highlightingStyle: RichTextHighlightingStyle = .standard - /** + /** The image configuration to use by the rich text view. The view uses the ``RichTextImageConfiguration/disabled`` @@ -78,37 +77,37 @@ open class RichTextView: UITextView, RichTextViewComponent { setting the property manually or by setting up the view with a ``RichTextDataFormat`` that supports images. */ -// public var imageConfiguration: RichTextImageConfiguration = .disabled { -// didSet { -//#if iOS || os(visionOS) -// refreshDropInteraction() -//#endif -// } -// } - -#if iOS || os(visionOS) - - /// The image drop interaction to use. - lazy var imageDropInteraction: UIDropInteraction = { - UIDropInteraction(delegate: self) - }() - - /// The interaction types supported by drag & drop. - var supportedDropInteractionTypes: [UTType] { - [.image, .text, .plainText, .utf8PlainText, .utf16PlainText] - } + // public var imageConfiguration: RichTextImageConfiguration = .disabled { + // didSet { + //#if os(iOS) || os(visionOS) + // refreshDropInteraction() + //#endif + // } + // } + + #if os(iOS) || os(visionOS) + + /// The image drop interaction to use. + lazy var imageDropInteraction: UIDropInteraction = { + UIDropInteraction(delegate: self) + }() + + /// The interaction types supported by drag & drop. + var supportedDropInteractionTypes: [UTType] { + [.image, .text, .plainText, .utf8PlainText, .utf16PlainText] + } -#endif + #endif - /// Keeps track of the first time a valid frame is set. - private var isInitialFrameSetupNeeded = true + /// Keeps track of the first time a valid frame is set. + private var isInitialFrameSetupNeeded = true - /// Keeps track of the data format used by the view. - private var richTextDataFormat: RichTextDataFormat = .archivedData + /// Keeps track of the data format used by the view. + private var richTextDataFormat: RichTextDataFormat = .archivedData - // MARK: - Overrides + // MARK: - Overrides - /** + /** Layout subviews and auto-resize images in the rich text. I tried to only autosize image attachments here, but it @@ -116,46 +115,46 @@ open class RichTextView: UITextView, RichTextViewComponent { font size adjustment, but that also didn't work. So now we initialize this once, when the frame is first set. */ - open override var frame: CGRect { - didSet { - if frame.size == .zero { return } - if !isInitialFrameSetupNeeded { return } - isInitialFrameSetupNeeded = false - setup(with: attributedString, format: richTextDataFormat) + open override var frame: CGRect { + didSet { + if frame.size == .zero { return } + if !isInitialFrameSetupNeeded { return } + isInitialFrameSetupNeeded = false + setup(with: attributedString, format: richTextDataFormat) + } } - } -//#if iOS || os(visionOS) -// /** -// Check whether or not a certain action can be performed. -// */ -// open override func canPerformAction( -// _ action: Selector, -// withSender sender: Any? -// ) -> Bool { -// let pasteboard = UIPasteboard.general -// let hasImage = pasteboard.image != nil -// let isPaste = action == #selector(paste(_:)) -// let canPerformImagePaste = imagePasteConfiguration != .disabled -// if isPaste && hasImage && canPerformImagePaste { return true } -// return super.canPerformAction(action, withSender: sender) -// } -// -// /** -// Paste the current content of the general pasteboard. -// */ -// open override func paste(_ sender: Any?) { -// let pasteboard = UIPasteboard.general -// if let image = pasteboard.image { -// return pasteImage(image, at: selectedRange.location) -// } -// super.paste(sender) -// } -//#endif - - // MARK: - Setup - - /** + //#if os(iOS) || os(visionOS) + // /** + // Check whether or not a certain action can be performed. + // */ + // open override func canPerformAction( + // _ action: Selector, + // withSender sender: Any? + // ) -> Bool { + // let pasteboard = UIPasteboard.general + // let hasImage = pasteboard.image != nil + // let isPaste = action == #selector(paste(_:)) + // let canPerformImagePaste = imagePasteConfiguration != .disabled + // if isPaste && hasImage && canPerformImagePaste { return true } + // return super.canPerformAction(action, withSender: sender) + // } + // + // /** + // Paste the current content of the general pasteboard. + // */ + // open override func paste(_ sender: Any?) { + // let pasteboard = UIPasteboard.general + // if let image = pasteboard.image { + // return pasteImage(image, at: selectedRange.location) + // } + // super.paste(sender) + // } + //#endif + + // MARK: - Setup + + /** Setup the rich text view with a rich text and a certain ``RichTextDataFormat``. @@ -163,275 +162,285 @@ open class RichTextView: UITextView, RichTextViewComponent { - text: The text to edit with the text view. - format: The rich text format to edit. */ - open func setup( - with text: NSAttributedString, - format: RichTextDataFormat? = nil - ) { -// text.autosizeImageAttachments(maxSize: imageAttachmentMaxSize) - setupSharedBehavior(with: text, format) - if let format { - richTextDataFormat = format - } - setup(theme) - } - - open func setup(with richText: RichText) { - var tempSpans: [RichTextSpanInternal] = [] - var text = "" - richText.spans.forEach({ - let span = RichTextSpanInternal(from: text.utf16Length, - to: (text.utf16Length + $0.insert.utf16Length - 1), - attributes: $0.attributes) - tempSpans.append(span) - text += $0.insert - }) - - let str = NSMutableAttributedString(string: text) - - tempSpans.forEach { span in - str.addAttributes(span.attributes?.toAttributes(font: .standardRichTextFont) ?? [:], range: span.spanRange) - if span.attributes?.color == nil { - str.addAttributes([.foregroundColor: theme.fontColor], range: span.spanRange) + open func setup( + with text: NSAttributedString, + format: RichTextDataFormat? = nil + ) { + // text.autosizeImageAttachments(maxSize: imageAttachmentMaxSize) + setupSharedBehavior(with: text, format) + if let format { + richTextDataFormat = format } + setup(theme) } - setup(with: str, format: .archivedData) - } + open func setup(with richText: RichText) { + var tempSpans: [RichTextSpanInternal] = [] + var text = "" + richText.spans.forEach({ + let span = RichTextSpanInternal( + from: text.utf16Length, + to: (text.utf16Length + $0.insert.utf16Length - 1), + attributes: $0.attributes) + tempSpans.append(span) + text += $0.insert + }) + + let str = NSMutableAttributedString(string: text) + + tempSpans.forEach { span in + str.addAttributes( + span.attributes?.toAttributes(font: .standardRichTextFont) + ?? [:], range: span.spanRange) + if span.attributes?.color == nil { + str.addAttributes( + [.foregroundColor: theme.fontColor], + range: span.spanRange) + } + } - // MARK: - Open Functionality + setup(with: str, format: .archivedData) + } - /// Alert a certain title and message. - open func alert(title: String, message: String, buttonTitle: String) { - let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - let action = UIAlertAction(title: buttonTitle, style: .default, handler: nil) - alert.addAction(action) - let controller = window?.rootViewController?.presentedViewController - controller?.present(alert, animated: true, completion: nil) - } + // MARK: - Open Functionality + + /// Alert a certain title and message. + open func alert(title: String, message: String, buttonTitle: String) { + let alert = UIAlertController( + title: title, message: message, preferredStyle: .alert) + let action = UIAlertAction( + title: buttonTitle, style: .default, handler: nil) + alert.addAction(action) + let controller = window?.rootViewController?.presentedViewController + controller?.present(alert, animated: true, completion: nil) + } - /// Copy the current selection. - open func copySelection() { -#if iOS || os(visionOS) - let pasteboard = UIPasteboard.general - let range = safeRange(for: selectedRange) - let text = richText(at: range) - pasteboard.string = text.string -#else - print("Pasteboard is not available on this platform") -#endif - } + /// Copy the current selection. + open func copySelection() { + #if os(iOS) || os(visionOS) + let pasteboard = UIPasteboard.general + let range = safeRange(for: selectedRange) + let text = richText(at: range) + pasteboard.string = text.string + #else + print("Pasteboard is not available on this platform") + #endif + } - /// Get the frame of a certain range. - open func frame(of range: NSRange) -> CGRect { - let beginning = beginningOfDocument - guard - let start = position(from: beginning, offset: range.location), - let end = position(from: start, offset: range.length), - let textRange = textRange(from: start, to: end) - else { return .zero } - let rect = firstRect(for: textRange) - return convert(rect, from: textInputView) - } + /// Get the frame of a certain range. + open func frame(of range: NSRange) -> CGRect { + let beginning = beginningOfDocument + guard + let start = position(from: beginning, offset: range.location), + let end = position(from: start, offset: range.length), + let textRange = textRange(from: start, to: end) + else { return .zero } + let rect = firstRect(for: textRange) + return convert(rect, from: textInputView) + } - /// Get the text range at a certain point. - open func range(at index: CGPoint) -> NSRange? { - let range = characterRange(at: index) ?? UITextRange() - let location = offset(from: beginningOfDocument, to: range.start) - let length = offset(from: range.start, to: range.end) - return NSRange(location: location, length: length) - } + /// Get the text range at a certain point. + open func range(at index: CGPoint) -> NSRange? { + let range = characterRange(at: index) ?? UITextRange() + let location = offset(from: beginningOfDocument, to: range.start) + let length = offset(from: range.start, to: range.end) + return NSRange(location: location, length: length) + } - /// Try to redo the latest undone change. - open func redoLatestChange() { - undoManager?.redo() - } + /// Try to redo the latest undone change. + open func redoLatestChange() { + undoManager?.redo() + } - /// Scroll to a certain range. - open func scroll(to range: NSRange) { - let caret = frame(of: range) - scrollRectToVisible(caret, animated: true) - } + /// Scroll to a certain range. + open func scroll(to range: NSRange) { + let caret = frame(of: range) + scrollRectToVisible(caret, animated: true) + } - /// Set the rich text in the text view. - open func setRichText(_ text: NSAttributedString) { - attributedString = text - } + /// Set the rich text in the text view. + open func setRichText(_ text: NSAttributedString) { + attributedString = text + } - /// Set the selected range in the text view. - open func setSelectedRange(_ range: NSRange) { - selectedRange = range - } + /// Set the selected range in the text view. + open func setSelectedRange(_ range: NSRange) { + selectedRange = range + } - /// Undo the latest change. - open func undoLatestChange() { - undoManager?.undo() - } + /// Undo the latest change. + open func undoLatestChange() { + undoManager?.undo() + } -#if iOS || os(visionOS) - - // MARK: - UIDropInteractionDelegate - - /// Whether or not the view can handle a drop session. -// open func dropInteraction( -// _ interaction: UIDropInteraction, -// canHandle session: UIDropSession -// ) -> Bool { -// if session.hasImage && imageDropConfiguration == .disabled { return false } -// let identifiers = supportedDropInteractionTypes.map { $0.identifier } -// return session.hasItemsConforming(toTypeIdentifiers: identifiers) -// } - - /// Handle an updated drop session. - open func dropInteraction( - _ interaction: UIDropInteraction, - sessionDidUpdate session: UIDropSession - ) -> UIDropProposal { - let operation = dropInteractionOperation(for: session) - return UIDropProposal(operation: operation) - } + #if os(iOS) || os(visionOS) + + // MARK: - UIDropInteractionDelegate + + /// Whether or not the view can handle a drop session. + // open func dropInteraction( + // _ interaction: UIDropInteraction, + // canHandle session: UIDropSession + // ) -> Bool { + // if session.hasImage && imageDropConfiguration == .disabled { return false } + // let identifiers = supportedDropInteractionTypes.map { $0.identifier } + // return session.hasItemsConforming(toTypeIdentifiers: identifiers) + // } + + /// Handle an updated drop session. + open func dropInteraction( + _ interaction: UIDropInteraction, + sessionDidUpdate session: UIDropSession + ) -> UIDropProposal { + let operation = dropInteractionOperation(for: session) + return UIDropProposal(operation: operation) + } - /// The drop interaction operation for a certain session. - open func dropInteractionOperation( - for session: UIDropSession - ) -> UIDropOperation { - guard session.hasDroppableContent else { return .forbidden } - let location = session.location(in: self) - return frame.contains(location) ? .copy : .cancel - } + /// The drop interaction operation for a certain session. + open func dropInteractionOperation( + for session: UIDropSession + ) -> UIDropOperation { + guard session.hasDroppableContent else { return .forbidden } + let location = session.location(in: self) + return frame.contains(location) ? .copy : .cancel + } - /** + /** Handle a performed drop session. In this function, we reverse the item collection, since each item will be pasted at the drop point, which would result in a reverse result. */ - open func dropInteraction( - _ interaction: UIDropInteraction, - performDrop session: UIDropSession - ) { - guard session.hasDroppableContent else { return } - let location = session.location(in: self) - guard let range = self.range(at: location) else { return } -// performImageDrop(with: session, at: range) - performTextDrop(with: session, at: range) - } + open func dropInteraction( + _ interaction: UIDropInteraction, + performDrop session: UIDropSession + ) { + guard session.hasDroppableContent else { return } + let location = session.location(in: self) + guard let range = self.range(at: location) else { return } + // performImageDrop(with: session, at: range) + performTextDrop(with: session, at: range) + } - // MARK: - Drop Interaction Support + // MARK: - Drop Interaction Support - /** + /** Performs an image drop session. We reverse the item collection, since each item will be pasted at the original drop point. */ -// open func performImageDrop(with session: UIDropSession, at range: NSRange) { -// guard validateImageInsertion(for: imageDropConfiguration) else { return } -// session.loadObjects(ofClass: UIImage.self) { items in -// let images = items.compactMap { $0 as? UIImage }.reversed() -// images.forEach { self.pasteImage($0, at: range.location) } -// } -// } - - /** + // open func performImageDrop(with session: UIDropSession, at range: NSRange) { + // guard validateImageInsertion(for: imageDropConfiguration) else { return } + // session.loadObjects(ofClass: UIImage.self) { items in + // let images = items.compactMap { $0 as? UIImage }.reversed() + // images.forEach { self.pasteImage($0, at: range.location) } + // } + // } + + /** Perform a text drop session. We reverse the item collection, since each item will be pasted at the original drop point. */ - open func performTextDrop(with session: UIDropSession, at range: NSRange) { - if session.hasImage { return } - _ = session.loadObjects(ofClass: String.self) { items in - let strings = items.reversed() - strings.forEach { self.pasteText($0, at: range.location) } - } - } + open func performTextDrop( + with session: UIDropSession, at range: NSRange + ) { + if session.hasImage { return } + _ = session.loadObjects(ofClass: String.self) { items in + let strings = items.reversed() + strings.forEach { self.pasteText($0, at: range.location) } + } + } - /// Refresh the drop interaction based on the config. -// open func refreshDropInteraction() { -// switch imageDropConfiguration { -// case .disabled: -// removeInteraction(imageDropInteraction) -// case .disabledWithWarning, .enabled: -// addInteraction(imageDropInteraction) -// } -// } -#endif -} + /// Refresh the drop interaction based on the config. + // open func refreshDropInteraction() { + // switch imageDropConfiguration { + // case .disabled: + // removeInteraction(imageDropInteraction) + // case .disabledWithWarning, .enabled: + // addInteraction(imageDropInteraction) + // } + // } + #endif + } -#if iOS || os(visionOS) -private extension UIDropSession { + #if os(iOS) || os(visionOS) + extension UIDropSession { - var hasDroppableContent: Bool { - hasImage || hasText - } + fileprivate var hasDroppableContent: Bool { + hasImage || hasText + } - var hasImage: Bool { - canLoadObjects(ofClass: UIImage.self) - } + fileprivate var hasImage: Bool { + canLoadObjects(ofClass: UIImage.self) + } - var hasText: Bool { - canLoadObjects(ofClass: String.self) - } -} -#endif + fileprivate var hasText: Bool { + canLoadObjects(ofClass: String.self) + } + } + #endif -// MARK: - Public Extensions + // MARK: - Public Extensions -public extension RichTextView { + extension RichTextView { - /// The text view's layout manager, if any. - var layoutManagerWrapper: NSLayoutManager? { - layoutManager - } + /// The text view's layout manager, if any. + public var layoutManagerWrapper: NSLayoutManager? { + layoutManager + } - /// The spacing between the text view edges and its text. - var textContentInset: CGSize { - get { - CGSize( - width: textContainerInset.left, - height: textContainerInset.top - ) - } set { - textContainerInset = UIEdgeInsets( - top: newValue.height, - left: newValue.width, - bottom: newValue.height, - right: newValue.width - ) + /// The spacing between the text view edges and its text. + public var textContentInset: CGSize { + get { + CGSize( + width: textContainerInset.left, + height: textContainerInset.top + ) + } + set { + textContainerInset = UIEdgeInsets( + top: newValue.height, + left: newValue.width, + bottom: newValue.height, + right: newValue.width + ) + } } - } - /// The text view's text storage, if any. - var textStorageWrapper: NSTextStorage? { - textStorage + /// The text view's text storage, if any. + public var textStorageWrapper: NSTextStorage? { + textStorage + } } -} -// MARK: - RichTextProvider + // MARK: - RichTextProvider -public extension RichTextView { + extension RichTextView { - /// Get the rich text managed by the text view. - var attributedString: NSAttributedString { - get { super.attributedText ?? NSAttributedString(string: "") } - set { attributedText = newValue } + /// Get the rich text managed by the text view. + public var attributedString: NSAttributedString { + get { super.attributedText ?? NSAttributedString(string: "") } + set { attributedText = newValue } + } } -} -// MARK: - RichTextWriter + // MARK: - RichTextWriter -public extension RichTextView { + extension RichTextView { - /// Get the mutable rich text managed by the view. - var mutableAttributedString: NSMutableAttributedString? { - textStorage + /// Get the mutable rich text managed by the view. + public var mutableAttributedString: NSMutableAttributedString? { + textStorage + } } -} -extension RichTextView { - var textString: String { - return self.text ?? "" + extension RichTextView { + var textString: String { + return self.text ?? "" + } } -} #endif diff --git a/Sources/RichEditorSwiftUI/UI/Views/AlertController.swift b/Sources/RichEditorSwiftUI/UI/Views/AlertController.swift new file mode 100644 index 0000000..21146c5 --- /dev/null +++ b/Sources/RichEditorSwiftUI/UI/Views/AlertController.swift @@ -0,0 +1,194 @@ +// +// AlertController.swift +// RichEditorSwiftUI +// +// Created by Divyesh Vekariya on 19/12/24. +// + +#if canImport(UIKit) + import UIKit + + public class AlertController { + + private var onTextChange: ((String) -> Void)? + + internal var rootController: UIViewController? { + guard + let scene = UIApplication.shared.connectedScenes.first(where: { + $0.activationState == .foregroundActive + }) as? UIWindowScene, + let window = scene.windows.first(where: { $0.isKeyWindow }) + else { + return nil + } + + var root = window.rootViewController + while let presentedViewController = root?.presentedViewController { + root = presentedViewController + } + return root + } + + func showAlert( + title: String, + message: String, + placeholder: String? = nil, + defaultText: String? = nil, + onTextChange: ((String) -> Void)? = nil, + completion: @escaping (String?) -> Void + ) { + // Store the onTextChange closure + self.onTextChange = onTextChange + + // Create the alert controller + let alert = UIAlertController( + title: title, message: message, preferredStyle: .alert) + + // Add a text field + alert.addTextField { textField in + textField.placeholder = placeholder + textField.text = defaultText + + // Add a target to handle text changes + if onTextChange != nil { + textField.addTarget( + self, action: #selector(self.textFieldDidChange(_:)), + for: .editingChanged) + } + } + + alert.addAction( + UIAlertAction( + title: "OK", style: .default, + handler: { [weak alert] _ in + let textFieldText = alert?.textFields?.first?.text + completion(textFieldText) + })) + alert.addAction( + UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + + // Present the alert + rootController?.present(alert, animated: true) + } + + @objc private func textFieldDidChange(_ textField: UITextField) { + onTextChange?(textField.text ?? "") + } + + func showAlert( + title: String, + message: String, + onOk: (() -> Void)? = nil, + onCancel: (() -> Void)? = nil + ) { + // Create the alert controller + let alert = UIAlertController( + title: title, message: message, preferredStyle: .alert) + + alert.addAction( + UIAlertAction( + title: "OK", style: .default, + handler: { _ in + onOk?() + })) + alert.addAction( + UIAlertAction( + title: "Cancel", style: .cancel, + handler: { _ in + onCancel?() + })) + + // Present the alert + rootController?.present(alert, animated: true) + } + } +#endif + +#if canImport(AppKit) + import AppKit + public class AlertController { + + private var onTextChange: ((String) -> Void)? + + // Root Window or ViewController + internal var rootWindow: NSWindow? { + return NSApp.mainWindow + } + + // Show alert with a text field and real-time text change closure + func showAlert( + title: String, + message: String, + placeholder: String? = nil, + defaultText: String? = nil, + onTextChange: ((String) -> Void)? = nil, + completion: @escaping (String?) -> Void + ) { + // Store the onTextChange closure + self.onTextChange = onTextChange + + // Create the alert + let alert = NSAlert() + alert.messageText = title + alert.informativeText = message + alert.alertStyle = .informational + + // Create an input text field + let textField = NSTextField( + frame: NSRect(x: 0, y: 0, width: 200, height: 24)) + textField.placeholderString = placeholder + textField.stringValue = defaultText ?? "" + alert.accessoryView = textField + + // Show real-time text updates + if let onTextChange = onTextChange { + textField.target = self + textField.action = #selector(self.textFieldDidChange(_:)) + } + + // Add the OK and Cancel buttons + alert.addButton(withTitle: "OK") + alert.addButton(withTitle: "Cancel") + + // Show the alert + let response = alert.runModal() + + // Handle completion based on the response + if response == .alertFirstButtonReturn { + completion(textField.stringValue) + } else { + completion(nil) + } + } + + @objc private func textFieldDidChange(_ textField: NSTextField) { + // Call the closure with the updated text + onTextChange?(textField.stringValue) + } + + // Show a simple alert with OK and Cancel actions + func showAlert( + title: String, + message: String, + onOk: (() -> Void)? = nil, + onCancel: (() -> Void)? = nil + ) { + let alert = NSAlert() + alert.messageText = title + alert.informativeText = message + alert.alertStyle = .informational + alert.addButton(withTitle: "OK") + alert.addButton(withTitle: "Cancel") + + // Show the alert + let response = alert.runModal() + + // Handle actions based on the response + if response == .alertFirstButtonReturn { + onOk?() + } else { + onCancel?() + } + } + } +#endif diff --git a/Sources/RichEditorSwiftUI/UI/Views/ForEachPicker.swift b/Sources/RichEditorSwiftUI/UI/Views/ForEachPicker.swift index fb7148b..824d352 100644 --- a/Sources/RichEditorSwiftUI/UI/Views/ForEachPicker.swift +++ b/Sources/RichEditorSwiftUI/UI/Views/ForEachPicker.swift @@ -7,11 +7,9 @@ import SwiftUI -/** - This is an internal version of the original that is defined - and available in https://github.com/danielsaidi/swiftuikit. - This will not be made public or documented for this library. - */ +/// This is an internal version of the original that is defined +/// and available in https://github.com/danielsaidi/swiftuikit. +/// This will not be made public or documented for this library. struct ForEachPicker: View { init( @@ -51,20 +49,20 @@ struct ForEachPicker: View { } } -private extension ForEachPicker { +extension ForEachPicker { - var selectedId: Item.ID { + fileprivate var selectedId: Item.ID { selection.wrappedValue.id } } -private extension ForEachPicker { +extension ForEachPicker { - func isSelected(_ item: Item) -> Bool { + fileprivate func isSelected(_ item: Item) -> Bool { selectedId == item.id } - func select(_ item: Item) { + fileprivate func select(_ item: Item) { if animatedSelection { selectWithAnimation(item) } else { @@ -72,13 +70,13 @@ private extension ForEachPicker { } } - func selectWithAnimation(_ item: Item) { + fileprivate func selectWithAnimation(_ item: Item) { withAnimation { selectWithoutAnimation(item) } } - func selectWithoutAnimation(_ item: Item) { + fileprivate func selectWithoutAnimation(_ item: Item) { selection.wrappedValue = item if dismissAfterPick { dismiss() diff --git a/Sources/RichEditorSwiftUI/UI/Views/ListPicker.swift b/Sources/RichEditorSwiftUI/UI/Views/ListPicker.swift index bcc3135..ef3fddd 100644 --- a/Sources/RichEditorSwiftUI/UI/Views/ListPicker.swift +++ b/Sources/RichEditorSwiftUI/UI/Views/ListPicker.swift @@ -7,11 +7,9 @@ import SwiftUI -/** - This is an internal version of the original that is defined - and available in https://github.com/danielsaidi/swiftuikit. - This will not be made public or documented for this library. - */ +/// This is an internal version of the original that is defined +/// and available in https://github.com/danielsaidi/swiftuikit. +/// This will not be made public or documented for this library. struct ListPicker: View { init( diff --git a/Sources/RichEditorSwiftUI/UI/Views/ListPickerItem.swift b/Sources/RichEditorSwiftUI/UI/Views/ListPickerItem.swift index e38267d..edf337f 100644 --- a/Sources/RichEditorSwiftUI/UI/Views/ListPickerItem.swift +++ b/Sources/RichEditorSwiftUI/UI/Views/ListPickerItem.swift @@ -7,11 +7,9 @@ import SwiftUI -/** - This is an internal version of the original that is defined - and available in https://github.com/danielsaidi/swiftuikit. - This will not be made public or documented for this library. - */ +/// This is an internal version of the original that is defined +/// and available in https://github.com/danielsaidi/swiftuikit. +/// This will not be made public or documented for this library. protocol ListPickerItem: View { associatedtype Item: Equatable diff --git a/Sources/RichEditorSwiftUI/UI/Views/ListPickerSection.swift b/Sources/RichEditorSwiftUI/UI/Views/ListPickerSection.swift index fb79ce3..0aaa2e6 100644 --- a/Sources/RichEditorSwiftUI/UI/Views/ListPickerSection.swift +++ b/Sources/RichEditorSwiftUI/UI/Views/ListPickerSection.swift @@ -7,11 +7,9 @@ import SwiftUI -/** - This is an internal version of the original that is defined - and available in https://github.com/danielsaidi/swiftuikit. - This will not be made public or documented for this library. - */ +/// This is an internal version of the original that is defined +/// and available in https://github.com/danielsaidi/swiftuikit. +/// This will not be made public or documented for this library. struct ListPickerSection: Identifiable { init(title: String, items: [Item]) { diff --git a/Sources/RichEditorSwiftUI/UI/Views/RichTextLabelValue.swift b/Sources/RichEditorSwiftUI/UI/Views/RichTextLabelValue.swift index 7d2aed4..dd29603 100644 --- a/Sources/RichEditorSwiftUI/UI/Views/RichTextLabelValue.swift +++ b/Sources/RichEditorSwiftUI/UI/Views/RichTextLabelValue.swift @@ -18,10 +18,10 @@ public protocol RichTextLabelValue: Hashable { var title: String { get } } -public extension RichTextLabelValue { +extension RichTextLabelValue { /// The standard label to use for the value. - var label: some View { + public var label: some View { Label( title: { Text(title) }, icon: { icon } diff --git a/Sources/RichEditorSwiftUI/UI/Views/ViewRepresentable.swift b/Sources/RichEditorSwiftUI/UI/Views/ViewRepresentable.swift index c822972..63ea1a7 100644 --- a/Sources/RichEditorSwiftUI/UI/Views/ViewRepresentable.swift +++ b/Sources/RichEditorSwiftUI/UI/Views/ViewRepresentable.swift @@ -7,22 +7,18 @@ import SwiftUI -#if iOS || os(tvOS) || os(visionOS) -import UIKit +#if os(iOS) || os(tvOS) || os(visionOS) + import UIKit -/** - This typealias bridges platform-specific view representable - types to simplify multi-platform support. - */ -typealias ViewRepresentable = UIViewRepresentable + /// This typealias bridges platform-specific view representable + /// types to simplify multi-platform support. + typealias ViewRepresentable = UIViewRepresentable #endif #if macOS -import AppKit + import AppKit -/** - This typealias bridges platform-specific view representable - types to simplify multi-platform support. - */ -typealias ViewRepresentable = NSViewRepresentable + /// This typealias bridges platform-specific view representable + /// types to simplify multi-platform support. + typealias ViewRepresentable = NSViewRepresentable #endif