From 43e4154e39569d3fa67b0c7eed0866e4f12fe792 Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 19 Apr 2017 10:47:29 -0300 Subject: [PATCH 01/14] BlockquoteFormatter: Minor style updates --- .../Formatters/BlockquoteFormatter.swift | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Aztec/Classes/Formatters/BlockquoteFormatter.swift b/Aztec/Classes/Formatters/BlockquoteFormatter.swift index 7a0022265..cfc8e268b 100644 --- a/Aztec/Classes/Formatters/BlockquoteFormatter.swift +++ b/Aztec/Classes/Formatters/BlockquoteFormatter.swift @@ -10,11 +10,11 @@ class BlockquoteFormatter: ParagraphAttributeFormatter { } func apply(to attributes: [String : Any]) -> [String: Any] { - var resultingAttributes = attributes let newParagraphStyle = ParagraphStyle() if let paragraphStyle = attributes[NSParagraphStyleAttributeName] as? NSParagraphStyle { newParagraphStyle.setParagraphStyle(paragraphStyle) } + if newParagraphStyle.blockquote == nil { newParagraphStyle.headIndent += Metrics.defaultIndentation newParagraphStyle.firstLineHeadIndent = newParagraphStyle.headIndent @@ -22,27 +22,31 @@ class BlockquoteFormatter: ParagraphAttributeFormatter { newParagraphStyle.paragraphSpacing += Metrics.defaultIndentation newParagraphStyle.paragraphSpacingBefore += Metrics.defaultIndentation } + newParagraphStyle.blockquote = Blockquote() + + var resultingAttributes = attributes resultingAttributes[NSParagraphStyleAttributeName] = newParagraphStyle return resultingAttributes } func remove(from attributes:[String: Any]) -> [String: Any] { - var resultingAttributes = attributes - let newParagraphStyle = ParagraphStyle() guard let paragraphStyle = attributes[NSParagraphStyleAttributeName] as? ParagraphStyle, - paragraphStyle.blockquote != nil else { - return resultingAttributes + paragraphStyle.blockquote != nil + else { + return attributes } + + let newParagraphStyle = ParagraphStyle() newParagraphStyle.setParagraphStyle(paragraphStyle) - if newParagraphStyle.blockquote != nil { - newParagraphStyle.headIndent -= Metrics.defaultIndentation - newParagraphStyle.firstLineHeadIndent = newParagraphStyle.headIndent - newParagraphStyle.tailIndent += Metrics.defaultIndentation - newParagraphStyle.paragraphSpacing -= Metrics.defaultIndentation - newParagraphStyle.paragraphSpacingBefore -= Metrics.defaultIndentation - } + newParagraphStyle.headIndent -= Metrics.defaultIndentation + newParagraphStyle.firstLineHeadIndent = newParagraphStyle.headIndent + newParagraphStyle.tailIndent += Metrics.defaultIndentation + newParagraphStyle.paragraphSpacing -= Metrics.defaultIndentation + newParagraphStyle.paragraphSpacingBefore -= Metrics.defaultIndentation newParagraphStyle.blockquote = nil + + var resultingAttributes = attributes resultingAttributes[NSParagraphStyleAttributeName] = newParagraphStyle return resultingAttributes } From ba20a9025b5346b9cff558e2f71197e6754f7f0f Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 19 Apr 2017 10:47:37 -0300 Subject: [PATCH 02/14] BlockquoteFormatter: Adds comments --- Aztec/Classes/Formatters/BlockquoteFormatter.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Aztec/Classes/Formatters/BlockquoteFormatter.swift b/Aztec/Classes/Formatters/BlockquoteFormatter.swift index cfc8e268b..5264ec98b 100644 --- a/Aztec/Classes/Formatters/BlockquoteFormatter.swift +++ b/Aztec/Classes/Formatters/BlockquoteFormatter.swift @@ -1,14 +1,25 @@ import Foundation import UIKit + +// MARK: - Blockquote Formatter +// class BlockquoteFormatter: ParagraphAttributeFormatter { + /// Attributes to be added by default + /// let placeholderAttributes: [String : Any]? + + /// Designated Initializer + /// init(placeholderAttributes: [String : Any]? = nil) { self.placeholderAttributes = placeholderAttributes } + + // MARK: - Overwriten Methods + func apply(to attributes: [String : Any]) -> [String: Any] { let newParagraphStyle = ParagraphStyle() if let paragraphStyle = attributes[NSParagraphStyleAttributeName] as? NSParagraphStyle { From fc7b7f700f55d447ccb85aa9727af10b170c925a Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 19 Apr 2017 10:47:48 -0300 Subject: [PATCH 03/14] BlockquoteFormatter: Disabling Empty Line Placeholder --- Aztec/Classes/Formatters/BlockquoteFormatter.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Aztec/Classes/Formatters/BlockquoteFormatter.swift b/Aztec/Classes/Formatters/BlockquoteFormatter.swift index 5264ec98b..8ab079b00 100644 --- a/Aztec/Classes/Formatters/BlockquoteFormatter.swift +++ b/Aztec/Classes/Formatters/BlockquoteFormatter.swift @@ -63,9 +63,11 @@ class BlockquoteFormatter: ParagraphAttributeFormatter { } func present(in attributes: [String : Any]) -> Bool { - if let paragraphStyle = attributes[NSParagraphStyleAttributeName] as? ParagraphStyle { - return paragraphStyle.blockquote != nil - } + let style = attributes[NSParagraphStyleAttributeName] as? ParagraphStyle + return style?.blockquote != nil + } + + func needsEmptyLinePlaceholder() -> Bool { return false } } From 9c1801587317c2db497130fd4794c545d9188281 Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 19 Apr 2017 10:48:09 -0300 Subject: [PATCH 04/14] LayoutManager: Nukes Extra Line Fragment workaround for Blockquotes --- Aztec/Classes/TextKit/LayoutManager.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Aztec/Classes/TextKit/LayoutManager.swift b/Aztec/Classes/TextKit/LayoutManager.swift index 7f770620c..4f5ce07cb 100644 --- a/Aztec/Classes/TextKit/LayoutManager.swift +++ b/Aztec/Classes/TextKit/LayoutManager.swift @@ -55,11 +55,6 @@ private extension LayoutManager { let lineRect = rect.offsetBy(dx: origin.x, dy: origin.y) self.drawBlockquote(in: lineRect, with: context) } - - if range.endLocation == textStorage.rangeOfEntireString.endLocation { - let extraLineRect = extraLineFragmentRect.offsetBy(dx: origin.x, dy: origin.y) - drawBlockquote(in: extraLineRect, with: context) - } } } From f8fd4d7fdc62f5435ba421b1e42906590aab8c50 Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 19 Apr 2017 11:43:02 -0300 Subject: [PATCH 05/14] String: endOfStringNSRange Helper --- .../Extensions/String+RangeConversion.swift | 6 ++++++ AztecTests/StringRangeConversionTests.swift | 18 +++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Aztec/Classes/Extensions/String+RangeConversion.swift b/Aztec/Classes/Extensions/String+RangeConversion.swift index b40d53713..b74c76d1a 100644 --- a/Aztec/Classes/Extensions/String+RangeConversion.swift +++ b/Aztec/Classes/Extensions/String+RangeConversion.swift @@ -43,6 +43,12 @@ extension String return NSRange(location: location, length: length) } + /// Returns a NSRange with a starting location at the very end of the string + /// + func endOfStringNSRange() -> NSRange { + return NSRange(location: characters.count, length: 0) + } + func indexFromLocation(_ location: Int) -> String.Index? { guard let unicodeLocation = utf16.index(utf16.startIndex, offsetBy: location, limitedBy: utf16.endIndex), diff --git a/AztecTests/StringRangeConversionTests.swift b/AztecTests/StringRangeConversionTests.swift index 118cdea15..cf90d242e 100644 --- a/AztecTests/StringRangeConversionTests.swift +++ b/AztecTests/StringRangeConversionTests.swift @@ -229,5 +229,21 @@ class StringRangeConversionTests: XCTestCase { XCTAssertEqual(nsRange, finalNSRange) } - + + /// Tests that endOfStringNSRange returns a NSRange mapping to the edge of the string. (Also known as + /// "after the last character range). + /// + /// Input: + /// - "Some random content here" + /// + /// Expected Output: + /// - NSRange(location: length of input, length: 0) + /// + func testEndOfStringNSRangeReturnsANSRangeThatStartsAtTheEndOfTheReceiver() { + let string = "Some random content here" + let endingRange = string.endOfStringNSRange() + + XCTAssert(endingRange.length == 0) + XCTAssert(endingRange.location == string.characters.count) + } } From 74e0900417bced386506387830694850ab83ff9d Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 19 Apr 2017 11:43:30 -0300 Subject: [PATCH 06/14] TextView: New Lists Unit Tests --- AztecTests/TextViewTests.swift | 76 ++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/AztecTests/TextViewTests.swift b/AztecTests/TextViewTests.swift index a59c77a31..304035598 100644 --- a/AztecTests/TextViewTests.swift +++ b/AztecTests/TextViewTests.swift @@ -839,4 +839,80 @@ class AztecVisualTextViewTests: XCTestCase { XCTAssertFalse(TextListFormatter.listsOfAnyKindPresent(in: textView.typingAttributes)) } + + /// Verifies that toggling an Unordered List, when editing an empty document, inserts a Newline. + /// + /// Input: + /// - Unordered List + /// + /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/414 + /// + func testTogglingUnorderedListsOnEmptyDocumentsInsertsNewline() { + let textView = createTextView(withHTML: "") + + textView.toggleUnorderedList(range: .zero) + XCTAssertEqual(textView.text, "\n") + } + + /// Verifies that toggling an Unordered List, when editing the end of a non empty document, inserts a Newline. + /// + /// Input: + /// - "Something Here" + /// - Selection of the end of document + /// - Unordered List + /// + /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/414 + /// + func testTogglingUnorderedListsOnNonEmptyDocumentsWhenSelectedRangeIsAtTheEndOfDocumentWillInsertNewline() { + let textView = createTextView(withHTML: Constants.sampleText0) + + textView.selectedRange = textView.text.endOfStringNSRange() + textView.toggleUnorderedList(range: .zero) + XCTAssertEqual(textView.text, Constants.sampleText0 + "\n") + + textView.selectedRange = textView.text.endOfStringNSRange() + textView.deleteBackward() + textView.insertText(Constants.sampleText1) + + XCTAssertEqual(textView.text, Constants.sampleText0 + Constants.sampleText1 + "\n") + } + + /// Verifies that toggling an Ordered List, when editing an empty document, inserts a Newline. + /// + /// Input: + /// - Ordered List + /// + /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/414 + /// + func testTogglingOrderedListsOnEmptyDocumentsInsertsNewline() { + let textView = createTextView(withHTML: "") + + textView.toggleOrderedList(range: .zero) + XCTAssertEqual(textView.text, "\n") + } + + /// Verifies that toggling an Ordered List, when editing the end of a non empty document, inserts a Newline. + /// + /// Input: + /// - "Something Here" + /// - Selection of the end of document + /// - Ordered List + /// + /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/414 + /// + func testTogglingOrderedListsOnNonEmptyDocumentsWhenSelectedRangeIsAtTheEndOfDocumentWillInsertNewline() { + let textView = createTextView(withHTML: Constants.sampleText0) + + textView.selectedRange = textView.text.endOfStringNSRange() + textView.toggleOrderedList(range: .zero) + XCTAssertEqual(textView.text, Constants.sampleText0 + "\n") + + textView.selectedRange = textView.text.endOfStringNSRange() + textView.deleteBackward() + textView.insertText(Constants.sampleText1) + + XCTAssertEqual(textView.text, Constants.sampleText0 + Constants.sampleText1 + "\n") + } + + } From 04b5a02fe71e20b1ee4b70eadfa0a0ca65ac4bde Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 19 Apr 2017 11:45:08 -0300 Subject: [PATCH 07/14] TextViewTests: Wiring endOfStringNSRange Helper --- AztecTests/TextViewTests.swift | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/AztecTests/TextViewTests.swift b/AztecTests/TextViewTests.swift index 304035598..29a84f745 100644 --- a/AztecTests/TextViewTests.swift +++ b/AztecTests/TextViewTests.swift @@ -721,17 +721,13 @@ class AztecVisualTextViewTests: XCTestCase { let textView = createTextView(withHTML: "") textView.toggleOrderedList(range: .zero) - - let length = textView.storage.length - textView.selectedRange = NSRange(location: length, length: 0) - + textView.selectedRange = textView.text.endOfStringNSRange() textView.deleteBackward() XCTAssertFalse(TextListFormatter.listsOfAnyKindPresent(in: textView.typingAttributes)) XCTAssert(textView.storage.length == 0) } - /// Verifies that New Line Characters get effectively inserted after a Text List. /// /// Input: @@ -779,8 +775,7 @@ class AztecVisualTextViewTests: XCTestCase { textView.insertText(firstItemText) // Select the end of the document - let length = textView.text.characters.count - textView.selectedRange = NSRange(location: length, length: 0) + textView.selectedRange = textView.text.endOfStringNSRange() // Delete + Insert Newline textView.deleteBackward() @@ -809,9 +804,7 @@ class AztecVisualTextViewTests: XCTestCase { let textView = createTextView(withHTML: "") textView.toggleOrderedList(range: .zero) - - let length = textView.text.characters.count - textView.selectedRange = NSRange(location: length, length: 0) + textView.selectedRange = textView.text.endOfStringNSRange() XCTAssertFalse(TextListFormatter.listsOfAnyKindPresent(in: textView.typingAttributes)) } From a702ce150d5c011bdd03fdba7da80950eef67a5e Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 19 Apr 2017 11:45:25 -0300 Subject: [PATCH 08/14] TextViewTests: New Blockquote Unit Tests --- AztecTests/TextViewTests.swift | 53 ++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/AztecTests/TextViewTests.swift b/AztecTests/TextViewTests.swift index 29a84f745..71328baec 100644 --- a/AztecTests/TextViewTests.swift +++ b/AztecTests/TextViewTests.swift @@ -3,7 +3,12 @@ import XCTest import Gridicons class AztecVisualTextViewTests: XCTestCase { - + + struct Constants { + static let sampleText0 = "Lorem ipsum sarasum naradum taradum insumun" + static let sampleText1 = " patronum sitanum elanum zoipancoiamum." + } + override func setUp() { super.setUp() // Put setup code here. This method is called before the invocation of each test method in the class. @@ -14,6 +19,7 @@ class AztecVisualTextViewTests: XCTestCase { super.tearDown() } + // MARK: - TextView construction func createEmptyTextView() -> Aztec.TextView { @@ -138,6 +144,7 @@ class AztecVisualTextViewTests: XCTestCase { XCTAssert(identifiers.count == 0) } + // MARK: - Toggle Attributes func testToggleBold() { @@ -260,6 +267,7 @@ class AztecVisualTextViewTests: XCTestCase { // The test not crashing would be successful. } + // MARK: - Test Attributes Exist func check(textView: TextView, range:NSRange, forIndentifier identifier: FormattingIdentifier) -> Bool { @@ -381,6 +389,7 @@ class AztecVisualTextViewTests: XCTestCase { XCTAssert(!textView.formatIdentifiersAtIndex(1).contains(.blockquote)) } + // MARK: - Adding newlines /// Tests that entering a newline in an empty editor does not crash it. @@ -405,6 +414,7 @@ class AztecVisualTextViewTests: XCTestCase { XCTAssertEqual(textView.text, "Testing bold newlines\n") } + // MARK: - Deleting newlines /// Tests that deleting a newline works by merging the component around it. @@ -653,7 +663,6 @@ class AztecVisualTextViewTests: XCTestCase { XCTAssertEqual(textView.getHTML(), "

Header

") } - /// Tests that there is no HTML Corruption when editing text, after toggling H1 and entering two lines of text. /// /// Input: @@ -707,7 +716,6 @@ class AztecVisualTextViewTests: XCTestCase { XCTAssertTrue(present) } - /// Verifies that the Text List does get nuked whenever the only `\n` present in the document is deleted. /// /// Input: @@ -908,4 +916,43 @@ class AztecVisualTextViewTests: XCTestCase { } + // MARK: - Blockquotes + + + /// Verifies that toggling a Blockquote, when editing an empty document, inserts a Newline. + /// + /// Input: + /// - Blockquote + /// + /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/414 + /// + func testTogglingBlockquoteOnEmptyDocumentsInsertsNewline() { + let textView = createTextView(withHTML: "") + textView.toggleBlockquote(range: .zero) + + XCTAssertEqual(textView.text, "\n") + } + + /// Verifies that toggling a Blockquote, when editing the end of a non empty document, inserts a Newline. + /// + /// Input: + /// - "Something Here" + /// - Selection of the end of document + /// - Ordered List + /// + /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/414 + /// + func testTogglingBlockquoteOnNonEmptyDocumentsWhenSelectedRangeIsAtTheEndOfDocumentWillInsertNewline() { + let textView = createTextView(withHTML: Constants.sampleText0) + + textView.selectedRange = textView.text.endOfStringNSRange() + textView.toggleBlockquote(range: .zero) + XCTAssertEqual(textView.text, Constants.sampleText0 + "\n") + + textView.selectedRange = textView.text.endOfStringNSRange() + textView.deleteBackward() + textView.insertText(Constants.sampleText1) + + XCTAssertEqual(textView.text, Constants.sampleText0 + Constants.sampleText1 + "\n") + } } From 1afaeabdea482b65cf88518878d5e36fb63c3161 Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 19 Apr 2017 12:53:08 -0300 Subject: [PATCH 09/14] TextViewTests: Updates Tests --- AztecTests/TextViewTests.swift | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/AztecTests/TextViewTests.swift b/AztecTests/TextViewTests.swift index 71328baec..cc4a02805 100644 --- a/AztecTests/TextViewTests.swift +++ b/AztecTests/TextViewTests.swift @@ -829,7 +829,7 @@ class AztecVisualTextViewTests: XCTestCase { let textView = createTextView(withHTML: "") textView.toggleOrderedList(range: .zero) - textView.insertText("\n") + textView.insertText(String(.newline)) let formatter = TextListFormatter(style: .ordered) let attributedText = textView.attributedText! @@ -852,7 +852,7 @@ class AztecVisualTextViewTests: XCTestCase { let textView = createTextView(withHTML: "") textView.toggleUnorderedList(range: .zero) - XCTAssertEqual(textView.text, "\n") + XCTAssertEqual(textView.text, String(.newline)) } /// Verifies that toggling an Unordered List, when editing the end of a non empty document, inserts a Newline. @@ -869,13 +869,14 @@ class AztecVisualTextViewTests: XCTestCase { textView.selectedRange = textView.text.endOfStringNSRange() textView.toggleUnorderedList(range: .zero) - XCTAssertEqual(textView.text, Constants.sampleText0 + "\n") + XCTAssertEqual(textView.text, Constants.sampleText0 + String(.newline)) textView.selectedRange = textView.text.endOfStringNSRange() textView.deleteBackward() textView.insertText(Constants.sampleText1) + textView.insertText(String(.newline)) - XCTAssertEqual(textView.text, Constants.sampleText0 + Constants.sampleText1 + "\n") + XCTAssertEqual(textView.text, Constants.sampleText0 + Constants.sampleText1 + String(.newline) + String(.newline)) } /// Verifies that toggling an Ordered List, when editing an empty document, inserts a Newline. @@ -889,7 +890,7 @@ class AztecVisualTextViewTests: XCTestCase { let textView = createTextView(withHTML: "") textView.toggleOrderedList(range: .zero) - XCTAssertEqual(textView.text, "\n") + XCTAssertEqual(textView.text, String(.newline)) } /// Verifies that toggling an Ordered List, when editing the end of a non empty document, inserts a Newline. @@ -906,19 +907,19 @@ class AztecVisualTextViewTests: XCTestCase { textView.selectedRange = textView.text.endOfStringNSRange() textView.toggleOrderedList(range: .zero) - XCTAssertEqual(textView.text, Constants.sampleText0 + "\n") + XCTAssertEqual(textView.text, Constants.sampleText0 + String(.newline)) textView.selectedRange = textView.text.endOfStringNSRange() textView.deleteBackward() textView.insertText(Constants.sampleText1) + textView.insertText(String(.newline)) - XCTAssertEqual(textView.text, Constants.sampleText0 + Constants.sampleText1 + "\n") + XCTAssertEqual(textView.text, Constants.sampleText0 + Constants.sampleText1 + String(.newline) + String(.newline)) } // MARK: - Blockquotes - /// Verifies that toggling a Blockquote, when editing an empty document, inserts a Newline. /// /// Input: @@ -930,7 +931,7 @@ class AztecVisualTextViewTests: XCTestCase { let textView = createTextView(withHTML: "") textView.toggleBlockquote(range: .zero) - XCTAssertEqual(textView.text, "\n") + XCTAssertEqual(textView.text, String(.newline)) } /// Verifies that toggling a Blockquote, when editing the end of a non empty document, inserts a Newline. @@ -947,12 +948,13 @@ class AztecVisualTextViewTests: XCTestCase { textView.selectedRange = textView.text.endOfStringNSRange() textView.toggleBlockquote(range: .zero) - XCTAssertEqual(textView.text, Constants.sampleText0 + "\n") + XCTAssertEqual(textView.text, Constants.sampleText0 + String(.newline)) textView.selectedRange = textView.text.endOfStringNSRange() textView.deleteBackward() textView.insertText(Constants.sampleText1) + textView.insertText(String(.newline)) - XCTAssertEqual(textView.text, Constants.sampleText0 + Constants.sampleText1 + "\n") + XCTAssertEqual(textView.text, Constants.sampleText0 + Constants.sampleText1 + String(.newline) + String(.newline)) } } From 7e0b23f1bec2b3e4c1235a4a7c757666e84ec358 Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 19 Apr 2017 14:03:32 -0300 Subject: [PATCH 10/14] TextView: Newline Insertion support for Blockquotes --- Aztec/Classes/TextKit/TextView.swift | 58 +++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/Aztec/Classes/TextKit/TextView.swift b/Aztec/Classes/TextKit/TextView.swift index 390304fb5..b8f1a28fe 100644 --- a/Aztec/Classes/TextKit/TextView.swift +++ b/Aztec/Classes/TextKit/TextView.swift @@ -288,12 +288,11 @@ open class TextView: UITextView { open override func insertText(_ text: String) { - /// Insert `\n` characters whenever we're in a Text List, the user presses \n, and we're literally - /// at the End of the Document. + /// Whenever the user is at the end of the document, while editing a [List, Blockquote], we'll need + /// to insert a `\n` character, so that the Layout Manager immediately renders the List's new bullet + /// (or Blockquote's BG). /// - if TextListFormatter.listsOfAnyKindPresent(in: typingAttributes) { - ensureInsertionOfNewlineOnEmptyDocuments() - } + ensureInsertionOfNewline(beforeInserting: text) // Note: // Whenever the entered text causes the Paragraph Attributes to be removed, we should prevent the actual @@ -586,8 +585,11 @@ open class TextView: UITextView { /// - range: The NSRange to edit. /// open func toggleBlockquote(range: NSRange) { + ensureInsertionOfNewlineWhenEditingEdgeOfTheDocument() + let formatter = BlockquoteFormatter(placeholderAttributes: typingAttributes) toggle(formatter: formatter, atRange: range) + forceRedrawCursorAfterDelay() } @@ -596,7 +598,7 @@ open class TextView: UITextView { /// - Parameter range: The NSRange to edit. /// open func toggleOrderedList(range: NSRange) { - ensureInsertionOfNewlineOnEmptyDocuments() + ensureInsertionOfNewlineWhenEditingEdgeOfTheDocument() let formatter = TextListFormatter(style: .ordered, placeholderAttributes: typingAttributes) toggle(formatter: formatter, atRange: range) @@ -610,7 +612,7 @@ open class TextView: UITextView { /// - Parameter range: The NSRange to edit. /// open func toggleUnorderedList(range: NSRange) { - ensureInsertionOfNewlineOnEmptyDocuments() + ensureInsertionOfNewlineWhenEditingEdgeOfTheDocument() let formatter = TextListFormatter(style: .unordered, placeholderAttributes: typingAttributes) toggle(formatter: formatter, atRange: range) @@ -813,13 +815,49 @@ open class TextView: UITextView { } - /// Inserts an empty line, whenever we're at the end of the document, and there's no selected text. + /// Inserts an empty line whenever we're at the end of the document /// - private func ensureInsertionOfNewlineOnEmptyDocuments() { - guard selectedRange.location == storage.length && selectedRange.length == 0 else { + private func ensureInsertionOfNewlineWhenEditingEdgeOfTheDocument() { + guard selectedRange.location == storage.length else { return } + insertNewline() + } + + + /// Inserts an empty line whenever: + /// + /// A. We're about to insert a new line + /// B. We're at the end of the document + /// C. There's a List (OR) Blockquote active + /// + /// We're doing this as a workaround, in order to force the LayoutManager render the Bullet (OR) + /// Blockquote's background. + /// + private func ensureInsertionOfNewline(beforeInserting text: String) { + guard text == String(.newline) else { + return + } + + guard selectedRange.location == storage.length else { + return + } + + guard BlockquoteFormatter().present(in: typingAttributes) || + TextListFormatter(style: .ordered).present(in: typingAttributes) || + TextListFormatter(style: .unordered).present(in: typingAttributes) + else { + return + } + + insertNewline() + } + + + /// Inserts a New Line at the current position, while retaining the selectedRange and typingAttributes. + /// + private func insertNewline() { let previousRange = selectedRange let previousStyle = typingAttributes From 4b77e73e31e8dc5024762a733ec9365490783efc Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 19 Apr 2017 15:33:23 -0300 Subject: [PATCH 11/14] TextView: Removing Blockquote from Typing Attributes --- Aztec/Classes/TextKit/TextView.swift | 60 +++++++++++++------------- AztecTests/TextViewTests.swift | 63 +++++++++++++++++++++++++--- 2 files changed, 89 insertions(+), 34 deletions(-) diff --git a/Aztec/Classes/TextKit/TextView.swift b/Aztec/Classes/TextKit/TextView.swift index b8f1a28fe..9bffcec93 100644 --- a/Aztec/Classes/TextKit/TextView.swift +++ b/Aztec/Classes/TextKit/TextView.swift @@ -215,12 +215,12 @@ open class TextView: UITextView { // MARK: - Overwritten Properties /// Overwrites Typing Attributes: - /// This is the (only) valid hook we've found, in order to (selectively) remove the List attributes. + /// This is the (only) valid hook we've found, in order to (selectively) remove the Blockquote/List attributes. /// For details, see: https://github.com/wordpress-mobile/AztecEditor-iOS/issues/414 /// override open var typingAttributes: [String: Any] { get { - let updatedAttributes = ensureRemovalOfListAttribute(from: super.typingAttributes) + let updatedAttributes = ensureRemovalOfListAndBlockquoteAttribute(from: super.typingAttributes) super.typingAttributes = updatedAttributes return updatedAttributes @@ -769,49 +769,44 @@ open class TextView: UITextView { } - /// Removes the List Attributes from a collection of attributes, whenever: + /// Removes the List Attributes from the Typing Attributes whenever: /// /// A. The selected location is at the very end of the document - /// B. There's a list! - /// C. The previous character is a '\n' + /// B. The previous character is a '\n' + /// C. There's a list (OR) blockquote. /// /// This is necessary because when the caret is at EOF, and the previous `\n` character has - /// a textList style, that style will remain in the `typingAttributes`. We'll only allow the style - /// to remain if there are contents in the current line with the textList style (in which case - /// this condition won't ever trigger because we'll either no longer be at EOF, or the previous - /// character won't be `\n`). + /// a [List, Blockquote] styles, that style will remain in the `typingAttributes`. We'll only + /// allow the style to remain if there are contents in the current line with the textList style + /// (in which case this condition won't ever trigger because we'll either no longer be at EOF, + /// or the previous character won't be `\n`). /// /// - Parameter attributes: Typing Attributes. /// /// - Returns: Updated Typing Attributes. /// - private func ensureRemovalOfListAttribute(from attributes: [String: Any]) -> [String: Any] { + private func ensureRemovalOfListAndBlockquoteAttribute(from typingAttributes: [String: Any]) -> [String: Any] { guard selectedRange.location == storage.length else { - return attributes - } - - guard TextListFormatter.listsOfAnyKindPresent(in: attributes) else { - return attributes + return typingAttributes } let previousRange = NSRange(location: selectedRange.location - 1, length: 1) let previousString = storage.safeSubstring(at: previousRange) ?? String(.newline) guard previousString == String(.newline) else { - return attributes + return typingAttributes } + let formatters: [AttributeFormatter] = [ + TextListFormatter(style: .ordered), + TextListFormatter(style: .unordered), + BlockquoteFormatter() + ] - let orderedListFormatter = TextListFormatter(style: .ordered) - if orderedListFormatter.present(in: attributes) { - return orderedListFormatter.remove(from: attributes) - } - - let unorderedListFormatter = TextListFormatter(style: .unordered) - if unorderedListFormatter.present(in: attributes) { - return unorderedListFormatter.remove(from: attributes) + for formatter in formatters where formatter.present(in: typingAttributes) { + return formatter.remove(from: typingAttributes) } - return attributes + return typingAttributes } @@ -844,10 +839,17 @@ open class TextView: UITextView { return } - guard BlockquoteFormatter().present(in: typingAttributes) || - TextListFormatter(style: .ordered).present(in: typingAttributes) || - TextListFormatter(style: .unordered).present(in: typingAttributes) - else { + let formatters: [AttributeFormatter] = [ + BlockquoteFormatter(), + TextListFormatter(style: .ordered), + TextListFormatter(style: .unordered) + ] + + let found = formatters.first { formatter in + return formatter.present(in: typingAttributes) + } + + guard found != nil else { return } diff --git a/AztecTests/TextViewTests.swift b/AztecTests/TextViewTests.swift index cc4a02805..7aae7764a 100644 --- a/AztecTests/TextViewTests.swift +++ b/AztecTests/TextViewTests.swift @@ -750,18 +750,17 @@ class AztecVisualTextViewTests: XCTestCase { // Toggle List + Move the selection to the EOD textView.toggleOrderedList(range: .zero) - - var expectedLength = textView.text.characters.count - textView.selectedRange = NSRange(location: expectedLength, length: 0) + textView.selectedRange = textView.text.endOfStringNSRange() // Insert Newline + var expectedLength = textView.text.characters.count textView.insertText(newline) expectedLength += newline.characters.count XCTAssertEqual(textView.text.characters.count, expectedLength) } - /// Verifies that New List Items do get their bulet, even when the ending `\n` character was deleted. + /// Verifies that New List Items do get their bullet, even when the ending `\n` character was deleted. /// /// Input: /// - Ordered List @@ -799,7 +798,7 @@ class AztecVisualTextViewTests: XCTestCase { XCTAssert(present) } - /// Verifies that after selecting a newline below a TextList does, TextView wil not render (nor carry over) + /// Verifies that after selecting a newline below a TextList, TextView wil not render (nor carry over) /// the Text List formatting attributes. /// /// Input: @@ -957,4 +956,58 @@ class AztecVisualTextViewTests: XCTestCase { XCTAssertEqual(textView.text, Constants.sampleText0 + Constants.sampleText1 + String(.newline) + String(.newline)) } + + /// Verifies that after selecting a newline below a Blockquote, TextView wil not render (nor carry over) + /// the Blockquote formatting attributes. + /// + /// Input: + /// - Blockquote + /// - Selection of the `\n` at the EOD + /// + func testTypingAttributesLooseBlockquoteWhenSelectingAnEmptyNewlineBelowTextList() { + let textView = createTextView(withHTML: "") + + textView.toggleBlockquote(range: .zero) + textView.selectedRange = textView.text.endOfStringNSRange() + + XCTAssertFalse(BlockquoteFormatter().present(in: textView.typingAttributes)) + } + + /// Verifies that New Line Characters get effectively inserted after a Blockquote. + /// + /// Input: + /// - Blockquote + /// - \n at the end of the document + /// + func testNewLinesAreInsertedAfterEmptyBlockquote() { + let newline = String(.newline) + let textView = createTextView(withHTML: "") + + textView.toggleBlockquote(range: .zero) + textView.selectedRange = textView.text.endOfStringNSRange() + + var expectedLength = textView.text.characters.count + textView.insertText(newline) + expectedLength += newline.characters.count + + XCTAssertEqual(textView.text.characters.count, expectedLength) + } + + /// + func testBlockquoteGetsRemovedWhenTypingNewLineOnAnEmptyBullet() { + let textView = createTextView(withHTML: "") + + textView.toggleBlockquote(range: .zero) + textView.insertText(String(.newline)) + + let formatter = BlockquoteFormatter() + let attributedText = textView.attributedText! + + for location in 0 ..< attributedText.length { + XCTAssertFalse(formatter.present(in: attributedText, at: location)) + } + + XCTAssertFalse(formatter.present(in: textView.typingAttributes)) + } } + From f8d5299d70b4b5a16b9dc5e6c970ac6758b6270b Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 19 Apr 2017 16:04:01 -0300 Subject: [PATCH 12/14] TextViewTests: New Blockquote Unit Tests --- AztecTests/TextViewTests.swift | 152 ++++++++++++++++++++++++++------- 1 file changed, 121 insertions(+), 31 deletions(-) diff --git a/AztecTests/TextViewTests.swift b/AztecTests/TextViewTests.swift index 7aae7764a..d10762d1a 100644 --- a/AztecTests/TextViewTests.swift +++ b/AztecTests/TextViewTests.swift @@ -716,7 +716,7 @@ class AztecVisualTextViewTests: XCTestCase { XCTAssertTrue(present) } - /// Verifies that the Text List does get nuked whenever the only `\n` present in the document is deleted. + /// Verifies that the List gets nuked whenever the only `\n` present in the document is deleted. /// /// Input: /// - Ordered List @@ -919,82 +919,134 @@ class AztecVisualTextViewTests: XCTestCase { // MARK: - Blockquotes - /// Verifies that toggling a Blockquote, when editing an empty document, inserts a Newline. + /// Verifies that a Blockquote does not get removed whenever the user presses backspace /// /// Input: /// - Blockquote + /// - Text: Constants.sampleText0 + /// - Backspace /// - /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/414 + /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/422 /// - func testTogglingBlockquoteOnEmptyDocumentsInsertsNewline() { + func testBlockquoteDoesNotGetLostAfterPressingBackspace() { let textView = createTextView(withHTML: "") + textView.toggleBlockquote(range: .zero) + textView.insertText(Constants.sampleText0) + textView.deleteBackward() - XCTAssertEqual(textView.text, String(.newline)) + let formatter = BlockquoteFormatter() + let range = textView.storage.rangeOfEntireString + + XCTAssertTrue(formatter.present(in: textView.storage, at: range)) } - /// Verifies that toggling a Blockquote, when editing the end of a non empty document, inserts a Newline. + /// Verifies that the Blockquote gets nuked whenever the only `\n` present in the document is deleted. /// /// Input: - /// - "Something Here" - /// - Selection of the end of document - /// - Ordered List + /// - Blockquote + /// - Selection of the EOD + /// - Backspace /// - /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/414 + /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/422 /// - func testTogglingBlockquoteOnNonEmptyDocumentsWhenSelectedRangeIsAtTheEndOfDocumentWillInsertNewline() { - let textView = createTextView(withHTML: Constants.sampleText0) + func testEmptyBlockquoteGetsNukedWheneverTheOnlyNewlineCharacterInTheDocumentIsNuked() { + let textView = createTextView(withHTML: "") - textView.selectedRange = textView.text.endOfStringNSRange() textView.toggleBlockquote(range: .zero) - XCTAssertEqual(textView.text, Constants.sampleText0 + String(.newline)) - textView.selectedRange = textView.text.endOfStringNSRange() textView.deleteBackward() - textView.insertText(Constants.sampleText1) - textView.insertText(String(.newline)) - - XCTAssertEqual(textView.text, Constants.sampleText0 + Constants.sampleText1 + String(.newline) + String(.newline)) + + let formatter = BlockquoteFormatter() + + XCTAssertFalse(formatter.present(in: textView.typingAttributes)) + XCTAssert(textView.storage.length == 0) } - /// Verifies that after selecting a newline below a Blockquote, TextView wil not render (nor carry over) - /// the Blockquote formatting attributes. + /// Verifies that New Line Characters get effectively inserted after a Blockquote. /// /// Input: /// - Blockquote - /// - Selection of the `\n` at the EOD + /// - \n at the end of the document + /// + /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/422 /// - func testTypingAttributesLooseBlockquoteWhenSelectingAnEmptyNewlineBelowTextList() { + func testNewLinesAreInsertedAfterEmptyBlockquote() { + let newline = String(.newline) let textView = createTextView(withHTML: "") textView.toggleBlockquote(range: .zero) textView.selectedRange = textView.text.endOfStringNSRange() - XCTAssertFalse(BlockquoteFormatter().present(in: textView.typingAttributes)) + var expectedLength = textView.text.characters.count + textView.insertText(newline) + expectedLength += newline.characters.count + + XCTAssertEqual(textView.text.characters.count, expectedLength) } - /// Verifies that New Line Characters get effectively inserted after a Blockquote. + /// Verifies that New List Items do get their bullet, even when the ending `\n` character was deleted. /// /// Input: /// - Blockquote - /// - \n at the end of the document + /// - Text: Constants.sampleText0 + /// - Selection of the `\n` at the EOD, and backspace + /// - Text: "\n" + /// - Text: Constants.sampleText1 /// - func testNewLinesAreInsertedAfterEmptyBlockquote() { + /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/422 + /// + func testNewLinesGetBlockquoteStyleEvenAfterDeletingEndOfDocumentNewline() { let newline = String(.newline) + let textView = createTextView(withHTML: "") textView.toggleBlockquote(range: .zero) + textView.insertText(Constants.sampleText0) textView.selectedRange = textView.text.endOfStringNSRange() - var expectedLength = textView.text.characters.count + // Delete + Insert Newline + textView.deleteBackward() textView.insertText(newline) - expectedLength += newline.characters.count + textView.insertText(Constants.sampleText1) - XCTAssertEqual(textView.text.characters.count, expectedLength) + // Verify it's still present + let secondLineIndex = Constants.sampleText0.characters.count + newline.characters.count + let secondLineRange = NSRange(location: secondLineIndex, length: Constants.sampleText1.characters.count) + + let formatter = BlockquoteFormatter() + let present = formatter.present(in: textView.storage, at: secondLineRange) + + XCTAssert(present) } + /// Verifies that after selecting a newline below a Blockquote, TextView wil not render (nor carry over) + /// the Blockquote formatting attributes. + /// + /// Input: + /// - Blockquote + /// - Selection of the `\n` at the EOD /// - func testBlockquoteGetsRemovedWhenTypingNewLineOnAnEmptyBullet() { + /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/422 + /// + func testTypingAttributesLooseBlockquoteWhenSelectingAnEmptyNewlineBelowBlockquote() { + let textView = createTextView(withHTML: "") + + textView.toggleBlockquote(range: .zero) + textView.selectedRange = textView.text.endOfStringNSRange() + + XCTAssertFalse(BlockquoteFormatter().present(in: textView.typingAttributes)) + } + + /// Verifies that Blockquotes get removed whenever the user types `\n` in an empty line. + /// + /// Input: + /// - Ordered List + /// - `\n` on the first line + /// + /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/422 + /// + func testBlockquoteGetsRemovedWhenTypingNewLineOnAnEmptyBlockquoteLine() { let textView = createTextView(withHTML: "") textView.toggleBlockquote(range: .zero) @@ -1009,5 +1061,43 @@ class AztecVisualTextViewTests: XCTestCase { XCTAssertFalse(formatter.present(in: textView.typingAttributes)) } + + /// Verifies that toggling a Blockquote, when editing an empty document, inserts a Newline. + /// + /// Input: + /// - Blockquote + /// + /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/422 + /// + func testTogglingBlockquoteOnEmptyDocumentsInsertsNewline() { + let textView = createTextView(withHTML: "") + + textView.toggleBlockquote(range: .zero) + XCTAssertEqual(textView.text, String(.newline)) + } + + /// Verifies that toggling a Blockquote, when editing the end of a non empty document, inserts a Newline. + /// + /// Input: + /// - "Something Here" + /// - Selection of the end of document + /// - Ordered List + /// + /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/422 + /// + func testTogglingBlockquoteOnNonEmptyDocumentsWhenSelectedRangeIsAtTheEndOfDocumentWillInsertNewline() { + let textView = createTextView(withHTML: Constants.sampleText0) + + textView.selectedRange = textView.text.endOfStringNSRange() + textView.toggleBlockquote(range: .zero) + XCTAssertEqual(textView.text, Constants.sampleText0 + String(.newline)) + + textView.selectedRange = textView.text.endOfStringNSRange() + textView.deleteBackward() + textView.insertText(Constants.sampleText1) + textView.insertText(String(.newline)) + + XCTAssertEqual(textView.text, Constants.sampleText0 + Constants.sampleText1 + String(.newline) + String(.newline)) + } } From 4bc0dc05568735b5f43c1d2853dba0d980860251 Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 19 Apr 2017 16:20:46 -0300 Subject: [PATCH 13/14] Updates TextView Unit Tests --- AztecTests/TextViewTests.swift | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/AztecTests/TextViewTests.swift b/AztecTests/TextViewTests.swift index d10762d1a..609478dfb 100644 --- a/AztecTests/TextViewTests.swift +++ b/AztecTests/TextViewTests.swift @@ -764,33 +764,32 @@ class AztecVisualTextViewTests: XCTestCase { /// /// Input: /// - Ordered List - /// - Text: "First Item" + /// - Text: Constants.sampleText0 /// - Selection of the `\n` at the EOD, and backspace - /// - Text: "\nSecond Item" + /// - Text: "\n" + /// - Text: Constants.sampleText1 /// /// Ref. Scenario Mark IV on Issue https://github.com/wordpress-mobile/AztecEditor-iOS/pull/425 /// func testNewLinesGetBulletStyleEvenAfterDeletingEndOfDocumentNewline() { - let firstItemText = "First Item" - let secondItemText = "Second Item" let newline = String(.newline) let textView = createTextView(withHTML: "") textView.toggleOrderedList(range: .zero) - textView.insertText(firstItemText) + textView.insertText(Constants.sampleText0) // Select the end of the document textView.selectedRange = textView.text.endOfStringNSRange() // Delete + Insert Newline textView.deleteBackward() - textView.insertText(newline + secondItemText) + textView.insertText(newline + Constants.sampleText1) // Verify it's still present - let secondLineIndex = firstItemText.characters.count + newline.characters.count - let secondLineRange = NSRange(location: secondLineIndex, length: secondItemText.characters.count) + let secondLineIndex = Constants.sampleText0.characters.count + newline.characters.count + let secondLineRange = NSRange(location: secondLineIndex, length: sampleText1.characters.count) let formatter = TextListFormatter(style: .ordered) let present = formatter.present(in: textView.storage, at: secondLineRange) @@ -985,7 +984,7 @@ class AztecVisualTextViewTests: XCTestCase { XCTAssertEqual(textView.text.characters.count, expectedLength) } - /// Verifies that New List Items do get their bullet, even when the ending `\n` character was deleted. + /// Verifies that New Blockquote Lines do get their style, even when the ending `\n` character was deleted. /// /// Input: /// - Blockquote @@ -1079,9 +1078,12 @@ class AztecVisualTextViewTests: XCTestCase { /// Verifies that toggling a Blockquote, when editing the end of a non empty document, inserts a Newline. /// /// Input: - /// - "Something Here" + /// - Text: Constants.sampleText0 /// - Selection of the end of document - /// - Ordered List + /// - Blockquote + /// - Backspace + /// - Text: Constants.sampleText1 + /// - Text: newline /// /// Ref. Issue https://github.com/wordpress-mobile/AztecEditor-iOS/issues/422 /// From 8e5727bf3086240bc48dc88e70433f916257e1c9 Mon Sep 17 00:00:00 2001 From: Jorge Leandro Perez Date: Wed, 19 Apr 2017 16:22:30 -0300 Subject: [PATCH 14/14] TextViewTests: Fixing invalid indirection --- AztecTests/TextViewTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AztecTests/TextViewTests.swift b/AztecTests/TextViewTests.swift index 609478dfb..c66c1c9b9 100644 --- a/AztecTests/TextViewTests.swift +++ b/AztecTests/TextViewTests.swift @@ -789,7 +789,7 @@ class AztecVisualTextViewTests: XCTestCase { // Verify it's still present let secondLineIndex = Constants.sampleText0.characters.count + newline.characters.count - let secondLineRange = NSRange(location: secondLineIndex, length: sampleText1.characters.count) + let secondLineRange = NSRange(location: secondLineIndex, length: Constants.sampleText1.characters.count) let formatter = TextListFormatter(style: .ordered) let present = formatter.present(in: textView.storage, at: secondLineRange)