Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Aztec/Classes/Extensions/String+RangeConversion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
47 changes: 32 additions & 15 deletions Aztec/Classes/Formatters/BlockquoteFormatter.swift
Original file line number Diff line number Diff line change
@@ -1,56 +1,73 @@
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] {
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
newParagraphStyle.tailIndent -= Metrics.defaultIndentation
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
}

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
}
}
Expand Down
5 changes: 0 additions & 5 deletions Aztec/Classes/TextKit/LayoutManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

}
Expand Down
106 changes: 73 additions & 33 deletions Aztec/Classes/TextKit/TextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
}

Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -767,59 +769,97 @@ 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)
for formatter in formatters where formatter.present(in: typingAttributes) {
return formatter.remove(from: typingAttributes)
}

let unorderedListFormatter = TextListFormatter(style: .unordered)
if unorderedListFormatter.present(in: attributes) {
return unorderedListFormatter.remove(from: attributes)
return typingAttributes
}


/// Inserts an empty line whenever we're at the end of the document
///
private func ensureInsertionOfNewlineWhenEditingEdgeOfTheDocument() {
guard selectedRange.location == storage.length else {
return
}

return attributes
insertNewline()
}


/// Inserts an empty line, whenever we're at the end of the document, and there's no selected text.
/// 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 ensureInsertionOfNewlineOnEmptyDocuments() {
guard selectedRange.location == storage.length && selectedRange.length == 0 else {
private func ensureInsertionOfNewline(beforeInserting text: String) {
guard text == String(.newline) else {
return
}

guard selectedRange.location == storage.length else {
return
}

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
}

insertNewline()
}


/// Inserts a New Line at the current position, while retaining the selectedRange and typingAttributes.
///
private func insertNewline() {
let previousRange = selectedRange
let previousStyle = typingAttributes

Expand Down
18 changes: 17 additions & 1 deletion AztecTests/StringRangeConversionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Loading