diff --git a/Example/Pods/Pods.xcodeproj/project.pbxproj b/Example/Pods/Pods.xcodeproj/project.pbxproj index 245100c..8e2acdc 100644 --- a/Example/Pods/Pods.xcodeproj/project.pbxproj +++ b/Example/Pods/Pods.xcodeproj/project.pbxproj @@ -20,7 +20,7 @@ 9BF3F9198BD94616E0C42F0699D95AFE /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F940C819049CFF8741C0F5E3E075E607 /* Cocoa.framework */; }; A8836C9983F0524A5E4B072BE09FC94F /* Dictionary+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = A251AEAB57C4F0623E305D7D2E8532AC /* Dictionary+Convenience.swift */; }; B047CCC24055F178CA45836F188DC624 /* URL+Images.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EB0AA13D264AE2AC3F8842D4551C3FA /* URL+Images.swift */; }; - BE2B170523E8E5B60069DA0B /* String+Lines.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE2B170423E8E5B60069DA0B /* String+Lines.swift */; }; + BE01E19623EA675A006448BE /* String+BulletPoints.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE01E19523EA675A006448BE /* String+BulletPoints.swift */; }; BECFB61D23E3EEE700544CD5 /* FontStyling.swift in Sources */ = {isa = PBXBuildFile; fileRef = BECFB61C23E3EECD00544CD5 /* FontStyling.swift */; }; C45D099150C6AD0684B37FC07F26ADFF /* Pods-RichEditor_Tests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B26BDF036DCC80E19475FB78017414B /* Pods-RichEditor_Tests-dummy.m */; }; CB7ACBA6C79C31828BBC3E446C3019D2 /* String+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE033E35858AB3ED75D20E3EAB4DC0E0 /* String+Convenience.swift */; }; @@ -79,7 +79,7 @@ 9F170FC9A00FB0FA29EE483E899CD96F /* Pods-RichEditor_Tests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-RichEditor_Tests.modulemap"; sourceTree = ""; }; A251AEAB57C4F0623E305D7D2E8532AC /* Dictionary+Convenience.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "Dictionary+Convenience.swift"; sourceTree = ""; }; AD0EA0312AC6030E860747A25DC571C6 /* RichEditor+Stack.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "RichEditor+Stack.swift"; sourceTree = ""; }; - BE2B170423E8E5B60069DA0B /* String+Lines.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Lines.swift"; sourceTree = ""; }; + BE01E19523EA675A006448BE /* String+BulletPoints.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+BulletPoints.swift"; sourceTree = ""; }; BECFB61C23E3EECD00544CD5 /* FontStyling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontStyling.swift; sourceTree = ""; }; C2BAE0C9C417B1A9DD0B78201E8BA424 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; C699BBD208ED791C1AEFF10837E2E44D /* Pods-RichEditor_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-RichEditor_Example.release.xcconfig"; sourceTree = ""; }; @@ -144,7 +144,7 @@ 6F00A72D1E8405B7D893897CC7CA91B9 /* NSMenu+Convenience.swift */, F5F00060F0CDEB413150FD7A56B6FE4B /* NSTextView+Convenience.swift */, CE033E35858AB3ED75D20E3EAB4DC0E0 /* String+Convenience.swift */, - BE2B170423E8E5B60069DA0B /* String+Lines.swift */, + BE01E19523EA675A006448BE /* String+BulletPoints.swift */, 2EB0AA13D264AE2AC3F8842D4551C3FA /* URL+Images.swift */, ); name = Extensions; @@ -437,6 +437,7 @@ 1BA7597F4A23D7C273ACB71FA65DBC79 /* RichEditor.swift in Sources */, CB7ACBA6C79C31828BBC3E446C3019D2 /* String+Convenience.swift in Sources */, B047CCC24055F178CA45836F188DC624 /* URL+Images.swift in Sources */, + BE01E19623EA675A006448BE /* String+BulletPoints.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -444,7 +445,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - BE2B170523E8E5B60069DA0B /* String+Lines.swift in Sources */, EE28395F3119EB766AD91F469011ED9E /* Pods-RichEditor_Example-dummy.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/RichEditor/Classes/RichEditor.swift b/RichEditor/Classes/RichEditor.swift index 3487053..a454a64 100644 --- a/RichEditor/Classes/RichEditor.swift +++ b/RichEditor/Classes/RichEditor.swift @@ -34,7 +34,7 @@ public class RichEditor: NSView } ///The marker that will be used for bullet points - fileprivate var bulletPointMarker = NSTextList.MarkerFormat.circle + internal static var bulletPointMarker = "•\u{00A0}" //NSTextList.MarkerFormat.circle /** Returns the FontStyling object that was derived from the selected text, or the future text if nothing is selected @@ -134,31 +134,15 @@ extension RichEditor public func startBulletPoints() { - /* - NSAttributedString - NSAttributedStringParagraphStyle - NSTextList - */ + let currentLine = self.textView.currentLine() - //Create a text list (the representation of our bullet points) - let textList = NSTextList(markerFormat: self.bulletPointMarker, options: 0) - textList.startingItemNumber = 1 + //Get the string that makes up our current string, and find out where it sits in our TextView + let currentLineStr = currentLine.lineString + let currentLineRange = currentLine.lineRange - //Create a new paragraph style, and apply our bullet points to it - let paragraph = NSMutableParagraphStyle() - paragraph.textLists = [textList] - - //Create attributes with our paragraph style (which in turn has the bullet point style within it) - var typingAttributes = self.textView.typingAttributes - typingAttributes[NSAttributedString.Key.paragraphStyle] = paragraph - - //Create an attributed string with the attributes already present in our 'future' text - //Put a \n before the bullet point, so that the bullet point is in it's own line - //Put a \t afte the bullet point so there's some space between the bullet point and the text - let attrStr = NSAttributedString(string: "\n\t\(textList.marker(forItemNumber: 1)) ", attributes: typingAttributes) - print("attrStr: \(attrStr)") - - self.textView.textStorage?.append(attrStr) + //Get the line in our TextView that our caret is on, and prepend a bulletpoint to it + let bulletPointStr = "\(RichEditor.bulletPointMarker) \(currentLineStr)" + self.textView.replaceCharacters(in: currentLineRange, with: bulletPointStr) } /** @@ -409,21 +393,22 @@ extension RichEditor: NSTextViewDelegate if newString == "\n" { let currentLine = textView.currentLine() - //If the line that we just hit enter on contains a bullet point marker - //TODO: Check for all decimal point markers - guard var currentLineRange = currentLine.1 else { return true } - guard let currentLineStr = currentLine.2 else { return true } - if currentLineStr.contains("◦") { - currentLineRange.length = currentLineRange.length + 2 + //If the line we're currently on is prefixed with a bullet point, append a bullet point to the next line + let currentLineStr = currentLine.lineString + if currentLineStr.hasPrefix(RichEditor.bulletPointMarker) { - let attributedStr = NSMutableAttributedString(attributedString: textView.attributedString()) + let currentLineRange = currentLine.lineRange - //Add another bullet point to this list of bullet points - guard let textList = textView.attributedString().textList(at: affectedCharRange) else { return true } - - //Get the current line, and replace it with the current line AND a newline with a new bullet point ready to go - let newLine = NSAttributedString(string: "\(currentLineStr)\n\(textList.marker(forItemNumber: 2))", attributes: attributedStr.attributes) - attributedStr.replaceCharacters(in: currentLineRange, with: newLine) + //If our current line is just an empty bullet point line, remove the bullet point and turn it into a regular line + if currentLineStr == RichEditor.bulletPointMarker { + self.textView.replaceCharacters(in: currentLineRange, with: "") + } + + //If our current line is a full bullet point line, append a brand spanking new bullet point line below our current line for our user + else { + let bulletPointStr = "\(currentLineStr)\n\(RichEditor.bulletPointMarker)" + self.textView.replaceCharacters(in: currentLineRange, with: bulletPointStr) + } return false } diff --git a/RichEditor/Extensions/NSAttributedString+Convenience.swift b/RichEditor/Extensions/NSAttributedString+Convenience.swift index 17e6d5f..0c065eb 100644 --- a/RichEditor/Extensions/NSAttributedString+Convenience.swift +++ b/RichEditor/Extensions/NSAttributedString+Convenience.swift @@ -115,33 +115,6 @@ extension NSAttributedString return attachments } - /** - Finds the NSTextList at a given range - - parameter range: The NSRange that we'll search for the NSTextList in - - returns: An NSTextList, if one exists at the given range - */ - public func textList(at searchRange: NSRange) -> NSTextList? - { - var textList: NSTextList? - - self.enumerateAttribute(.paragraphStyle, in: self.string.fullRange, options: .longestEffectiveRangeNotRequired, using: {(value, range, finished) in - if value != nil { - if let paragraphStyle = value as? NSParagraphStyle { - - //TODO: Improve search to see if this textlist is the one we're after - if paragraphStyle.textLists.count >= 1 { - textList = paragraphStyle.textLists[0] - - print("TextList Range: \(range)") - print("Search Range: \(searchRange)") - } - } - } - }) - - return textList - } - //MARK: - Attribute Checking /** Iterates over every font that exists within this NSAttributedString, and checks if any of the fonts contain the desired NSFontTraitMask diff --git a/RichEditor/Extensions/NSTextView+Convenience.swift b/RichEditor/Extensions/NSTextView+Convenience.swift index dce1dde..7594cd1 100644 --- a/RichEditor/Extensions/NSTextView+Convenience.swift +++ b/RichEditor/Extensions/NSTextView+Convenience.swift @@ -19,6 +19,10 @@ extension NSTextView return self.selectedRange().length > 0 } + public var caretLocation: Int { + return self.selectedRange().location + } + /** Replaces the current NSString/NSAttributedString that is currently within the NSTextView and replaces it with the provided HTML string. @@ -53,6 +57,7 @@ extension NSTextView - returns: A boolean value indicative of if the setting of the NSAttributedString was successful */ + @discardableResult public func set(attributedString: NSAttributedString) -> Bool { guard let textStorage = self.textStorage else { @@ -102,21 +107,18 @@ extension NSTextView - returns: The line number that the caret is on, the range of our line, and the string that makes up that line of text */ - func currentLine() -> (Int?, NSRange?, String?) + func currentLine() -> (lineNumber: Int, lineRange: NSRange, lineString: String) { - //Bail out if the user has selected text - if self.hasSelectedText { - return (nil, nil, nil) - } - //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: Int? - var selectedLineRange : NSRange? - var selectedLineOfText: String? - + var selectedLineNumber = 0 + var selectedLineRange = NSRange(location: 0, length: 0) + var selectedLineOfText = "" + + var foundSelectedLine = false + //Iterate over every line in our TextView self.string.enumerateSubstrings(in: self.string.startIndex..= startOfLine && caretLocation <= endOfLine { + if self.caretLocation >= startOfLine && self.caretLocation <= endOfLine { //Mark the line number selectedLineNumber = lineNumber - selectedLineOfText = substring + selectedLineOfText = substring ?? "" selectedLineRange = range + + foundSelectedLine = true } lineNumber += 1 } + //If we're not at the starting point, and we didn't find a current line, then we're at the end of our TextView + if self.caretLocation > 0 && !foundSelectedLine { + selectedLineNumber = lineNumber + selectedLineOfText = "" + selectedLineRange = NSRange(location: self.caretLocation, length: 0) + } + return (selectedLineNumber, selectedLineRange, selectedLineOfText) } + + public func append(_ string: String) + { + let textViewText = NSMutableAttributedString(attributedString: self.attributedString()) + textViewText.append(NSAttributedString(string: string, attributes: self.typingAttributes)) + + self.set(attributedString: textViewText) + } } diff --git a/RichEditor/Extensions/String+Lines.swift b/RichEditor/Extensions/String+BulletPoints.swift similarity index 86% rename from RichEditor/Extensions/String+Lines.swift rename to RichEditor/Extensions/String+BulletPoints.swift index 3234c60..c8a2b6d 100644 --- a/RichEditor/Extensions/String+Lines.swift +++ b/RichEditor/Extensions/String+BulletPoints.swift @@ -9,6 +9,10 @@ import Foundation extension String { + public var isBulletPoint: Bool { + return self.hasPrefix(RichEditor.bulletPointMarker) + } + /** Returns an array of strings that is made up of all the "lines" in this string. - returns: An array of strings that is derived from this string, using a newline as a delimiter