diff --git a/Proton/Proton.xcodeproj/project.pbxproj b/Proton/Proton.xcodeproj/project.pbxproj index db765914..3b92f085 100644 --- a/Proton/Proton.xcodeproj/project.pbxproj +++ b/Proton/Proton.xcodeproj/project.pbxproj @@ -34,7 +34,6 @@ 1B45CDD023C007AF001EB196 /* RichTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B45CDCF23C007AF001EB196 /* RichTextView.swift */; }; 1B45CDD223C00856001EB196 /* TextContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B45CDD123C00856001EB196 /* TextContainer.swift */; }; 1B45CDD523C00927001EB196 /* EditorContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B45CDD423C00927001EB196 /* EditorContent.swift */; }; - 1B45CDD723C00A0A001EB196 /* EditorAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B45CDD623C00A0A001EB196 /* EditorAttribute.swift */; }; 1B45CDD923C0484A001EB196 /* RichTextViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B45CDD823C0484A001EB196 /* RichTextViewTests.swift */; }; 1B45CDDB23C04BE3001EB196 /* RichTextViewSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B45CDDA23C04BE3001EB196 /* RichTextViewSnapshotTests.swift */; }; 1B45CDDD23C05340001EB196 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B45CDDC23C05340001EB196 /* Attachment.swift */; }; @@ -158,7 +157,6 @@ 1B45CDCF23C007AF001EB196 /* RichTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichTextView.swift; sourceTree = ""; }; 1B45CDD123C00856001EB196 /* TextContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextContainer.swift; sourceTree = ""; }; 1B45CDD423C00927001EB196 /* EditorContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorContent.swift; sourceTree = ""; }; - 1B45CDD623C00A0A001EB196 /* EditorAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditorAttribute.swift; sourceTree = ""; }; 1B45CDD823C0484A001EB196 /* RichTextViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichTextViewTests.swift; sourceTree = ""; }; 1B45CDDA23C04BE3001EB196 /* RichTextViewSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichTextViewSnapshotTests.swift; sourceTree = ""; }; 1B45CDDC23C05340001EB196 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; @@ -453,7 +451,6 @@ isa = PBXGroup; children = ( 1B45CDD423C00927001EB196 /* EditorContent.swift */, - 1B45CDD623C00A0A001EB196 /* EditorAttribute.swift */, 1B45CDDE23C053FB001EB196 /* EditorContentIdentifying.swift */, 1B004EC623C1C287007893AA /* EditorView.swift */, 1BFFEF0B23C30D5200D2BA35 /* EditorContentView.swift */, @@ -864,7 +861,6 @@ 1B45CDE623C05942001EB196 /* AttachmentSize.swift in Sources */, 1B975AFB23CD3E9B00EC410C /* RendererView.swift in Sources */, 1B164F1023D1BE9900A0869A /* AttributesDecoding.swift in Sources */, - 1B45CDD723C00A0A001EB196 /* EditorAttribute.swift in Sources */, 1B45CDC723BF14DF001EB196 /* NSAttributedString+Range.swift in Sources */, 1B45CDD223C00856001EB196 /* TextContainer.swift in Sources */, 1BBAC3CF23CD5A1B0088A1C8 /* UITextRangeExtensions.swift in Sources */, diff --git a/Proton/Sources/Attachment/Attachment.swift b/Proton/Sources/Attachment/Attachment.swift index 867f1036..defe62b7 100644 --- a/Proton/Sources/Attachment/Attachment.swift +++ b/Proton/Sources/Attachment/Attachment.swift @@ -108,6 +108,14 @@ open class Attachment: NSTextAttachment, BoundsObserving { return updatedString } + /// Attributed string representation of the `Attachment`. This can be used directly to replace a range of text in `EditorView` + /// ### Usage Example ### + /// ``` + /// let attachment = Attachment(PanelView(), size: .fullWidth) + /// let attrString = NSMutableAttributedString(string: "This is a test string") + /// attrString.append(attachment.string) + /// editor.attributedText = attrString + /// ``` public var string: NSAttributedString { guard let isBlockAttachment = isBlockAttachment else { return NSAttributedString(string: "") } // let key = isBlockAttachment == true ? NSAttributedString.Key.contentType: NSAttributedString.Key.inlineContentType @@ -131,8 +139,12 @@ open class Attachment: NSTextAttachment, BoundsObserving { } } + /// `EditorView` containing this attachment public private(set) var containerEditorView: EditorView? + /// Name of the content for the `EditorView` + /// - SeeAlso: + /// `EditorView` public var containerContentName: EditorContent.Name? { return containerEditorView?.contentName } @@ -141,6 +153,10 @@ open class Attachment: NSTextAttachment, BoundsObserving { return containerEditorView?.richTextView } + /// Causes invalidation of layout of the attachment when the containing view bounds are changed + /// - Parameter bounds: Updated bounds + /// - SeeAlso: + /// `BoundsObserving` public func didChangeBounds(_ bounds: CGRect) { invalidateLayout() } @@ -155,15 +171,21 @@ open class Attachment: NSTextAttachment, BoundsObserving { } } + /// Bounds of the container public var containerBounds: CGRect? { return containerTextView?.bounds } + /// The bounds rectangle, which describes the attachment's location and size in its own coordinate system. public override var bounds: CGRect { didSet { view.bounds = bounds } } // This cannot be made convenience init as it prevents this being called from a class that inherits from `Attachment` + /// Initializes the attachment with the given content view + /// - Parameters: + /// - contentView: Content view to be hosted within the attachment + /// - size: Size rule for attachment public init(_ contentView: AttachmentView, size: AttachmentSize) { self.view = AttachmentContentView(name: contentView.name, frame: contentView.frame) self.size = size @@ -174,6 +196,10 @@ open class Attachment: NSTextAttachment, BoundsObserving { } // This cannot be made convenience init as it prevents this being called from a class that inherits from `Attachment` + /// Initializes the attachment with the given content view + /// - Parameters: + /// - contentView: Content view to be hosted within the attachment + /// - size: Size rule for attachment public init(_ contentView: AttachmentView, size: AttachmentSize) { self.view = AttachmentContentView(name: contentView.name, frame: contentView.frame) self.size = size @@ -231,6 +257,7 @@ open class Attachment: NSTextAttachment, BoundsObserving { view.removeFromSuperview() } + /// Removes this attachment from the `EditorView` it is contained in. public func removeFromContainer() { guard let containerTextView = containerTextView, let range = containerTextView.attributedText.rangeFor(attachment: self) else { @@ -239,14 +266,23 @@ open class Attachment: NSTextAttachment, BoundsObserving { containerTextView.textStorage.replaceCharacters(in: range, with: "") } + /// Range of this attachment in it's container public func rangeInContainer() -> NSRange? { return containerTextView?.attributedText.rangeFor(attachment: self) } + /// Invoked when attributes are added in the containing `EditorView` in the range of string in which this attachment is contained. + /// - Parameters: + /// - range: Affected range + /// - attributes: Attributes applied open func addedAttributesOnContainingRange(rangeInContainer range: NSRange, attributes: [NSAttributedString.Key: Any]) { } + // Invoked when attributes are removed in the containing `EditorView` in the range of string in which this attachment is contained. + /// - Parameters: + /// - range: Affected range + /// - attributes: Attributes removed open func removedAttributesFromContainingRange(rangeInContainer range: NSRange, attributes: [NSAttributedString.Key]) { } @@ -256,6 +292,12 @@ open class Attachment: NSTextAttachment, BoundsObserving { fatalError("init(coder:) has not been implemented") } + /// Returns the calculated bounds for the attachment based on size rule and content view provided during initialization. + /// - Parameters: + /// - textContainer: Text container for attachment + /// - lineFrag: Line fragment containing the attachment + /// - position: Position in the text container. + /// - charIndex: Character index public override func attachmentBounds(for textContainer: NSTextContainer?, proposedLineFragment lineFrag: CGRect, glyphPosition position: CGPoint, characterIndex charIndex: Int) -> CGRect { guard let textContainer = textContainer else { return .zero } diff --git a/Proton/Sources/Attachment/Focusable.swift b/Proton/Sources/Attachment/Focusable.swift index 13baafa4..413b73ef 100644 --- a/Proton/Sources/Attachment/Focusable.swift +++ b/Proton/Sources/Attachment/Focusable.swift @@ -20,7 +20,10 @@ import Foundation -/// Describes an object capable of gaining focus +/// Describes an object capable of gaining focus. +/// - Note: +/// If a content view in an `Attachment` is made `Focusable`, `setFocus` will automatically be called when the +/// attachment with a focusable view is added to the editor. public protocol Focusable { func setFocus() } diff --git a/Proton/Sources/Base/NSAttributedString+ContentTypes.swift b/Proton/Sources/Base/NSAttributedString+ContentTypes.swift index 241ede60..1f7b1e8a 100644 --- a/Proton/Sources/Base/NSAttributedString+ContentTypes.swift +++ b/Proton/Sources/Base/NSAttributedString+ContentTypes.swift @@ -29,5 +29,8 @@ extension NSAttributedString.Key { } public extension NSAttributedString.Key { + /// Applying this attribute with value of `true` to a range of text makes that text non-focusable. + /// The content can still be deleted and selected but cursor cannot be moved to non-focusable range + /// using taps or mouse/keys (macOS Catalyst) static let noFocus = NSAttributedString.Key("_noFocus") } diff --git a/Proton/Sources/Editor/EditorAttribute.swift b/Proton/Sources/Editor/EditorAttribute.swift deleted file mode 100644 index 110d4ec7..00000000 --- a/Proton/Sources/Editor/EditorAttribute.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// EditorAttribute.swift -// Proton -// -// Created by Rajdeep Kwatra on 4/1/20. -// Copyright © 2020 Rajdeep Kwatra. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import Foundation -import UIKit - -public enum EditorAttribute { - case bold - case italics - case underline - case color(UIColor) - case font(UIFont) - case contentName(EditorContent.Name) - case paragraph(NSParagraphStyle) - case custom(Decodable) -} diff --git a/Proton/Sources/Editor/EditorContent.swift b/Proton/Sources/Editor/EditorContent.swift index 5e711435..b50dfb87 100644 --- a/Proton/Sources/Editor/EditorContent.swift +++ b/Proton/Sources/Editor/EditorContent.swift @@ -55,14 +55,17 @@ public struct EditorContent { } public extension EditorContent { + + /// Name for the content within the Editor. All the content (text and attachments) must have + /// a name. By default, text contained in Editor is considered a paragraph. struct Name: Hashable, Equatable, RawRepresentable { public var rawValue: String - public static let paragraph = Name("paragraph") - public static let viewOnly = Name("viewOnly") - public static let newline = Name("newline") - public static let text = Name("text") - public static let unknown = Name("unknown") + public static let paragraph = Name("_paragraph") + public static let viewOnly = Name("_viewOnly") + public static let newline = Name("_newline") + public static let text = Name("_text") + public static let unknown = Name("_unknown") public init(rawValue: String) { self.rawValue = rawValue diff --git a/Proton/Sources/Editor/EditorContentIdentifying.swift b/Proton/Sources/Editor/EditorContentIdentifying.swift index eebc603f..5b9d6a32 100644 --- a/Proton/Sources/Editor/EditorContentIdentifying.swift +++ b/Proton/Sources/Editor/EditorContentIdentifying.swift @@ -29,6 +29,10 @@ public protocol EditorContentIdentifying { // Convenience type for a UIView that can be placed within the Editor as the content of an `Attachment` typealias AttachmentView = UIView & EditorContentIdentifying +/// Block type content hosted within the editor. Any attachment containing `BlockContent` will have a new line character appended +/// after the attachment on insertion. public protocol BlockContent: EditorContentIdentifying { } +/// Inline content hosted within the editor. Any attachment containing `InlineContent` will have a whitespace character appended +/// after the attachment on insertion. public protocol InlineContent: EditorContentIdentifying { } diff --git a/Proton/Sources/Editor/EditorContentView.swift b/Proton/Sources/Editor/EditorContentView.swift index 79c27eb7..894856cf 100644 --- a/Proton/Sources/Editor/EditorContentView.swift +++ b/Proton/Sources/Editor/EditorContentView.swift @@ -21,6 +21,9 @@ import Foundation import UIKit +/// Describes a view contained in `Attachment` that contains a single `EditorView`. +/// This is a helper protocol that can be applied to the view so that +/// basic properties and functions are made available on the view as passthrough. public protocol EditorContentView: Focusable { var editor: EditorView { get } diff --git a/Proton/Sources/Editor/EditorViewDelegate.swift b/Proton/Sources/Editor/EditorViewDelegate.swift index 463658a0..cf742379 100644 --- a/Proton/Sources/Editor/EditorViewDelegate.swift +++ b/Proton/Sources/Editor/EditorViewDelegate.swift @@ -20,12 +20,48 @@ import Foundation +/// Describes an object interested in listening to events raised from EditorView public protocol EditorViewDelegate: AnyObject { + + /// Invoked when a special key like `enter`, `tab` etc. is intercepted in the `Editor` + /// - Parameters: + /// - editor: Editor view receiving the event. + /// - key: Key that is intercepted. + /// - range: Range of the key in editor + /// - handled: Set to `true` to hijack the key press i.e. when `true`, the key press is not passed to the `Editor` func editor(_ editor: EditorView, didReceiveKey key: EditorKey, at range: NSRange, handled: inout Bool) + + /// Invoked when editor receives focus. + /// - Parameters: + /// - editor: Editor view receiving the event. + /// - range: Range where focus is received. func editor(_ editor: EditorView, didReceiveFocusAt range: NSRange) + + /// Invoked when editor loses the focus. + /// - Parameters: + /// - editor: Editor view receiving the event. + /// - range: Range from where focus is lost. func editor(_ editor: EditorView, didLoseFocusFrom range: NSRange) + + /// Invoked when text is changed in editor. + /// - Parameters: + /// - editor: Editor view receiving the event. + /// - range: Range where text is modified. func editor(_ editor: EditorView, didChangeTextAt range: NSRange) + + /// Invoked when the selection range changes in the editor as a result of moving the cursor using keys/mouse or taps. + /// - Parameters: + /// - editor: Editor view receiving the event. + /// - range: Range where selection is changed. + /// - attributes: Attributes at the updated range. + /// - contentType: Name of the content at the updated range. func editor(_ editor: EditorView, didChangeSelectionAt range: NSRange, attributes: [NSAttributedString.Key: Any], contentType: EditorContent.Name) + + /// Invoked when text processors are executed in the editor. + /// - Parameters: + /// - editor: Editor view receiving the event. + /// - processors: Processors that are executed. + /// - range: Range where processors are executed. func editor(_ editor: EditorView, didExecuteProcessors processors: [TextProcessing], at range: NSRange) } diff --git a/Proton/Sources/EditorCommand/BoldCommand.swift b/Proton/Sources/EditorCommand/BoldCommand.swift index 9eae862f..00e2954b 100644 --- a/Proton/Sources/EditorCommand/BoldCommand.swift +++ b/Proton/Sources/EditorCommand/BoldCommand.swift @@ -21,6 +21,7 @@ import Foundation import UIKit +/// Editor command that toggles Bold attribute to the selected range in the Editor. public class BoldCommand: FontTraitToggleCommand { public init() { super.init(name: CommandName("_BoldCommand"), trait: .traitBold) diff --git a/Proton/Sources/EditorCommand/EditorCommand.swift b/Proton/Sources/EditorCommand/EditorCommand.swift index 850b595a..976ecabc 100644 --- a/Proton/Sources/EditorCommand/EditorCommand.swift +++ b/Proton/Sources/EditorCommand/EditorCommand.swift @@ -41,6 +41,6 @@ public protocol EditorCommand: AnyObject { public extension EditorCommand { func canExecute(on editor: EditorView) -> Bool { - return editor.registeredCommands?.contains { $0 === self } ?? true + return editor.registeredCommands?.contains { $0.name == self.name } ?? true } } diff --git a/Proton/Sources/EditorCommand/FontTraitToggleCommand.swift b/Proton/Sources/EditorCommand/FontTraitToggleCommand.swift index 1af19dbb..2ceec0c6 100644 --- a/Proton/Sources/EditorCommand/FontTraitToggleCommand.swift +++ b/Proton/Sources/EditorCommand/FontTraitToggleCommand.swift @@ -21,6 +21,7 @@ import Foundation import UIKit +/// Editor command that toggles given font trait to the selected range in the Editor. public class FontTraitToggleCommand: EditorCommand { public let trait: UIFontDescriptor.SymbolicTraits diff --git a/Proton/Sources/EditorCommand/ItalicsCommand.swift b/Proton/Sources/EditorCommand/ItalicsCommand.swift index dc5cf90b..ebbce4c7 100644 --- a/Proton/Sources/EditorCommand/ItalicsCommand.swift +++ b/Proton/Sources/EditorCommand/ItalicsCommand.swift @@ -21,6 +21,7 @@ import Foundation import UIKit +/// Editor command that toggles Italics attribute to the selected range in the Editor. public class ItalicsCommand: FontTraitToggleCommand { public init() { super.init(name: CommandName("_ItalicsCommand"), trait: .traitItalic) diff --git a/Proton/Sources/Encoding/AttributesEncoding.swift b/Proton/Sources/Encoding/AttributesEncoding.swift index bf07445f..b64f4fd9 100644 --- a/Proton/Sources/Encoding/AttributesEncoding.swift +++ b/Proton/Sources/Encoding/AttributesEncoding.swift @@ -21,37 +21,82 @@ import Foundation import UIKit +/// Describes an encoder for a content type in Editor. This can be used in conjunction with `AnyEditorTextEncoding` +/// to register various encoders for each of the supported content types. +/// ### Usage Example ### +///``` +/// struct ParagraphEncoder: EditorTextEncoding { +/// func encode(name: EditorContent.Name, string: NSAttributedString) -> JSON { +/// var paragraph = JSON() +/// paragraph.type = name.rawValue +/// if let style = string.attribute(.paragraphStyle, at: 0, effectiveRange: nil) as? NSParagraphStyle { +/// paragraph[style.key] = style.value +/// } +/// paragraph.contents = contentsFrom(string) +/// return paragraph +/// } +/// } +///``` +/// - SeeAlso: +/// `EditorContentEncoder` public protocol EditorTextEncoding { associatedtype EncodedType + + /// Encodes the given attributed string to `EncodedType` + /// - Parameters: + /// - name: Name of the content to encode + /// - string: Attributed string to encode func encode(name: EditorContent.Name, string: NSAttributedString) -> EncodedType } +/// /// A type-erased implementation of `EditorTextEncoding` +/// - SeeAlso: +/// `EditorTextEncoding` public struct AnyEditorTextEncoding: EditorTextEncoding { public typealias EncodedType = T let encoding: (_ name: EditorContent.Name, _ string: NSAttributedString) -> T + /// Initializes the Encoder + /// - Parameter encoder: Encoder implementation to use public init(_ encoder: E) where E.EncodedType == T { encoding = encoder.encode } + /// Encodes contents based on concrete encoder provided during initialization + /// - Parameters: + /// - name: Content name + /// - string: Attributed String to be encoded public func encode(name: EditorContent.Name, string: NSAttributedString) -> T { return encoding(name, string) } } +/// Describes an object capable of encoding contents of at `Attachment` public protocol AttachmentEncoding { associatedtype EncodedType + + /// Encodes given `Attachment` content view to given type + /// - Parameters: + /// - name: Name of the content + /// - view: Attachment content view func encode(name: EditorContent.Name, view: UIView) -> EncodedType } +/// A type-erased implementation of `AttachmentEncoding`. public struct AnyEditorContentAttachmentEncoding: AttachmentEncoding { public typealias EncodedType = T let encoding: (_ name: EditorContent.Name, _ view: UIView) -> T + /// Initializes the Encoder + /// - Parameter encoder: Encoder implementation to use public init(_ encoder: E) where E.EncodedType == T { encoding = encoder.encode } + /// Encodes contents based on concrete encoder provided during initialization + /// - Parameters: + /// - name: Content name + /// - string: Attachment view to be encoded public func encode(name: EditorContent.Name, view: UIView) -> T { return encoding(name, view) } diff --git a/Proton/Sources/Helpers/NSAttributedString+Content.swift b/Proton/Sources/Helpers/NSAttributedString+Content.swift index 9ae6a16e..da4668e4 100644 --- a/Proton/Sources/Helpers/NSAttributedString+Content.swift +++ b/Proton/Sources/Helpers/NSAttributedString+Content.swift @@ -22,18 +22,24 @@ import Foundation import UIKit public extension NSAttributedString { + + /// Enumerates block contents in given range. + /// - Parameter range: Range to enumerate contents in. Nil to enumerate in entire string. func enumerateContents(in range: NSRange? = nil) -> AnySequence { return self.enumerateContentType(.contentType, defaultIfMissing: .paragraph, in: range) { attributes in attributes[.isBlockAttachment] as? Bool != false } } - + /// Enumerates only inline content in given range. + /// - Parameter range: Range to enumerate contents in. Nil to enumerate in entire string. func enumerateInlineContents(in range: NSRange? = nil) -> AnySequence { return self.enumerateContentType(.contentType, defaultIfMissing: .text, in: range) { attributes in attributes[.isInlineAttachment] as? Bool != false } } + /// Returns in range of CharacterSet from this string. + /// - Parameter characterSet: CharacterSet to search. func rangeOfCharacter(from characterSet: CharacterSet) -> NSRange? { guard let range = string.rangeOfCharacter(from: characterSet) else { return nil diff --git a/Proton/Sources/Helpers/NSAttributedString+Range.swift b/Proton/Sources/Helpers/NSAttributedString+Range.swift index d1c35241..cfe6abdd 100644 --- a/Proton/Sources/Helpers/NSAttributedString+Range.swift +++ b/Proton/Sources/Helpers/NSAttributedString+Range.swift @@ -22,10 +22,13 @@ import Foundation import UIKit public extension NSAttributedString { + + /// Full range of this attributed string. var fullRange: NSRange { return NSRange(location: 0, length: length) } + /// Collection of all the attachments with containing ranges in this attributed string. var attachmentRanges: [(attachment: Attachment, range: NSRange)] { var ranges = [(Attachment, NSRange)]() @@ -38,14 +41,21 @@ public extension NSAttributedString { return ranges } + /// Range of given attachment in this attributed string. + /// - Parameter attachment: Attachment to find. Nil if given attachment does not exists in this attributed string. func rangeFor(attachment: Attachment) -> NSRange? { return attachmentRanges.reversed().first(where: { $0.attachment == attachment })?.range } + /// Ranges of `CharacterSet` in this attributed string. + /// - Parameter characterSet: CharacterSet to search. func rangesOf(characterSet: CharacterSet) -> [NSRange] { return string.rangesOf(characterSet: characterSet).map { string.makeNSRange(from: $0) } } + /// Attributed substring in reverse direction. + /// - Parameter range: Range for substring. Substring starts from location in range to number of characters towards beginning per length + /// specified in range. func reverseAttributedSubstring(from range: NSRange) -> NSAttributedString? { guard length > 0 && range.location + range.length < length else { return nil diff --git a/Proton/Sources/Helpers/NSRangeExtensions.swift b/Proton/Sources/Helpers/NSRangeExtensions.swift index 54943a80..9c7b7e60 100644 --- a/Proton/Sources/Helpers/NSRangeExtensions.swift +++ b/Proton/Sources/Helpers/NSRangeExtensions.swift @@ -22,6 +22,8 @@ import Foundation import UIKit public extension NSRange { + + /// Range with 0 location and length static var zero: NSRange { return NSRange(location: 0, length: 0) } @@ -38,6 +40,9 @@ public extension NSRange { return NSRange(location: location + 1, length: 0) } + + /// Converts the range to `UITextRange` in given `UITextInput`. Returns nil if the range is invalid in the `UITextInput`. + /// - Parameter textInput: UITextInput to convert the range in. func toTextRange(textInput: UITextInput) -> UITextRange? { guard let rangeStart = textInput.position(from: textInput.beginningOfDocument, offset: location), let rangeEnd = textInput.position(from: rangeStart, offset: length) else { @@ -46,6 +51,8 @@ public extension NSRange { return textInput.textRange(from: rangeStart, to: rangeEnd) } + /// Checks if the range is valid in given `UITextInput` + /// - Parameter textInput: UITextInput to validate the range in. func isValidIn(_ textInput: UITextInput) -> Bool { guard location > 0 else { return false } let end = location + length diff --git a/Proton/Sources/Helpers/String+NSRange.swift b/Proton/Sources/Helpers/String+NSRange.swift index 2677f147..4652729c 100644 --- a/Proton/Sources/Helpers/String+NSRange.swift +++ b/Proton/Sources/Helpers/String+NSRange.swift @@ -21,6 +21,9 @@ import Foundation public extension String { + + /// Converts given Range to NSRange in this string. + /// - Parameter range: Range to convert. func makeNSRange(from range: Range) -> NSRange { let range = range.lowerBound ..< min(range.upperBound, endIndex) @@ -33,6 +36,8 @@ public extension String { length: utf16.distance(from: from, to: to)) } + /// Created String Range from given NSRange. Returns nil if range cannot be converted. + /// - Parameter range: Range to convert. func rangeFromNSRange(range: NSRange) -> Range? { guard let from16 = utf16.index(utf16.startIndex, offsetBy: range.location, limitedBy: utf16.endIndex), @@ -43,6 +48,8 @@ public extension String { return from ..< to } + /// Returns ranges of given CharacterSet in this string. + /// - Parameter characterSet: CharacterSet to find. func rangesOf(characterSet: CharacterSet) -> [Range] { var ranges = [Range]() var newlineRange = rangeOfCharacter(from: characterSet, options: [], range: nil) diff --git a/Proton/Sources/Helpers/UITextRangeExtensions.swift b/Proton/Sources/Helpers/UITextRangeExtensions.swift index af5f8e18..20793c46 100644 --- a/Proton/Sources/Helpers/UITextRangeExtensions.swift +++ b/Proton/Sources/Helpers/UITextRangeExtensions.swift @@ -22,6 +22,9 @@ import Foundation import UIKit public extension UITextRange { + + /// Converts this range to `NSRange`. Returns nil if range cannot be converted. + /// - Parameter input: Input to use to get range. func toNSRange(in input: UITextInput) -> NSRange? { let location = input.offset(from: input.beginningOfDocument, to: start) let length = input.offset(from: start, to: end) diff --git a/Proton/Sources/Renderer/RendererViewDelegate.swift b/Proton/Sources/Renderer/RendererViewDelegate.swift index e9da1c9c..dac19a8a 100644 --- a/Proton/Sources/Renderer/RendererViewDelegate.swift +++ b/Proton/Sources/Renderer/RendererViewDelegate.swift @@ -21,7 +21,14 @@ import Foundation import CoreGraphics +/// An object that is interested in listening to events raised within the Renderer. public protocol RendererViewDelegate: AnyObject { + + /// Invoked on tap/mouse click on the Renderer. + /// - Parameters: + /// - renderer: Renderer view receiving the event. + /// - location: Location of tap. + /// - characterRange: Range at the tapped location. func didTap(_ renderer: RendererView, didTapAtLocation location: CGPoint, characterRange: NSRange?) func didChangeSelection(_ renderer: RendererView, range: NSRange) } diff --git a/Proton/Sources/RendererCommand/HighlightTextCommand.swift b/Proton/Sources/RendererCommand/HighlightTextCommand.swift index d39479e5..c7ec7179 100644 --- a/Proton/Sources/RendererCommand/HighlightTextCommand.swift +++ b/Proton/Sources/RendererCommand/HighlightTextCommand.swift @@ -22,10 +22,11 @@ import Foundation import UIKit public extension NSAttributedString.Key { - static let isHighlighted = NSAttributedString.Key("IsHighlighted") + static let isHighlighted = NSAttributedString.Key("_IsHighlighted") } @available(iOS 13.0, *) +/// Renderer command that toggles highlights in the selected range in Renderer. public class HighlightTextCommand: RendererCommand { public let name = CommandName("_highlightCommand") @@ -38,7 +39,11 @@ public class HighlightTextCommand: RendererCommand { return UIColor(red: 1.0, green: 0.98, blue: 0.80, alpha: 1.0) } }) + public init() { } + + /// Executes the command on Renderer in the selected range + /// - Parameter renderer: Renderer to execute the command on. public func execute(on renderer: RendererView) { guard renderer.selectedText.length > 0 else { return } let highlightedColor = renderer.selectedText.attribute(.backgroundColor, at: 0, effectiveRange: nil) as? UIColor