Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
1e04997
LayoutManager: Minor cleanup
jleandroperez Apr 17, 2017
c2cb0d3
TextListFormatter: Adds comments
jleandroperez Apr 17, 2017
a7a620a
TextListFormatter: Disabling empty line placeholders
jleandroperez Apr 17, 2017
9a99ac0
LayoutManager: Removes extraLineFragment Workaround for lists
jleandroperez Apr 17, 2017
a47ebb3
TextView: Hack hack
jleandroperez Apr 17, 2017
63eee3e
AttributeFormatter: Style Updates
jleandroperez Apr 17, 2017
2b6fae0
TextView: Re-Enables Redraw Workaround
jleandroperez Apr 17, 2017
6e01e48
TextView: Overwritting Typing Attributes
jleandroperez Apr 17, 2017
a5d544c
TextView: Cleanup
jleandroperez Apr 17, 2017
03066ff
TextListFormatter: Revers faulty update
jleandroperez Apr 17, 2017
463dc88
TextView: Updates refreshStylesAfterDeletion
jleandroperez Apr 18, 2017
57f1aec
TextStorage: Updates Style
jleandroperez Apr 18, 2017
39176ef
TextView: New Unit Tests
jleandroperez Apr 18, 2017
08b9951
TextView: Fixing newline behavior
jleandroperez Apr 18, 2017
af9a78f
TextView: Updates ensureRemovalOfListAttribute Logic
jleandroperez Apr 18, 2017
f33abea
TextView: Updates Newline Insertion Logic
jleandroperez Apr 18, 2017
5b148d4
TextView: Wiring List Formatter Check
jleandroperez Apr 18, 2017
9ba0490
TextViewTests: New Unit Tests
jleandroperez Apr 18, 2017
f2e0f47
Implements new NSAttributedString Helper
jleandroperez Apr 18, 2017
6e51226
TextView: New Unit Test
jleandroperez Apr 18, 2017
d4755a9
TextView: Fixing unit test
jleandroperez Apr 18, 2017
c98217d
TextView: New Unit Test
jleandroperez Apr 18, 2017
ec35ce0
TextView: Addressing Unit Tests Comments
jleandroperez Apr 18, 2017
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
11 changes: 11 additions & 0 deletions Aztec/Classes/Extensions/NSAttributedString+Analyzers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,15 @@ extension NSAttributedString {

return attribute(NSLinkAttributeName, at: afterRange.location, effectiveRange: nil) != nil
}

/// Returns the Substring at the specified range, whenever the received range is valid, or nil
/// otherwise.
///
func safeSubstring(at range: NSRange) -> String? {
guard range.location >= 0 && range.endLocation <= length else {
return nil
}

return attributedSubstring(from: range).string
}
}
9 changes: 6 additions & 3 deletions Aztec/Classes/Formatters/AttributeFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ extension AttributeFormatter {
return result && enumerateAtLeastOnce
}

@discardableResult func toggle(in attributes: [String: Any]) -> [String: Any] {
@discardableResult
func toggle(in attributes: [String: Any]) -> [String: Any] {
if present(in: attributes) {
return remove(from: attributes)
} else {
Expand All @@ -117,7 +118,8 @@ extension AttributeFormatter {
///
/// - Returns: the full range where the attributes where applied
///
@discardableResult func applyAttributes(to text: NSMutableAttributedString, at range: NSRange) -> NSRange {
@discardableResult
func applyAttributes(to text: NSMutableAttributedString, at range: NSRange) -> NSRange {
var rangeToApply = applicationRange(for: range, in: text)

if needsEmptyLinePlaceholder() && worksInEmptyRange() && ( rangeToApply.length == 0 || text.length == 0) {
Expand All @@ -139,7 +141,8 @@ extension AttributeFormatter {
///
/// - Returns: the full range where the attributes where removed
///
@discardableResult func removeAttributes(from text: NSMutableAttributedString, at range: NSRange) -> NSRange {
@discardableResult
func removeAttributes(from text: NSMutableAttributedString, at range: NSRange) -> NSRange {
let rangeToApply = applicationRange(for: range, in: text)
text.enumerateAttributes(in: rangeToApply, options: []) { (attributes, range, stop) in
let currentAttributes = text.attributes(at: range.location, effectiveRange: nil)
Expand Down
48 changes: 39 additions & 9 deletions Aztec/Classes/Formatters/TextListFormatter.swift
Original file line number Diff line number Diff line change
@@ -1,53 +1,83 @@
import Foundation
import UIKit


// MARK: - Lists Formatter
//
class TextListFormatter: ParagraphAttributeFormatter {


/// Style of the list
///
let listStyle: TextList.Style

/// Attributes to be added by default
///
let placeholderAttributes: [String : Any]?


/// Designated Initializer
///
init(style: TextList.Style, placeholderAttributes: [String : Any]? = nil) {
self.listStyle = style
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.textList == nil {
newParagraphStyle.headIndent += Metrics.listTextIndentation
newParagraphStyle.firstLineHeadIndent += Metrics.listTextIndentation
}

newParagraphStyle.textList = TextList(style: self.listStyle)

var resultingAttributes = attributes
resultingAttributes[NSParagraphStyleAttributeName] = newParagraphStyle

return resultingAttributes
}

func remove(from attributes:[String: Any]) -> [String: Any] {
var resultingAttributes = attributes
let newParagraphStyle = ParagraphStyle()
func remove(from attributes: [String: Any]) -> [String: Any] {
guard let paragraphStyle = attributes[NSParagraphStyleAttributeName] as? ParagraphStyle,
paragraphStyle.textList?.style == self.listStyle
else {
return resultingAttributes
return attributes
}

let newParagraphStyle = ParagraphStyle()
newParagraphStyle.setParagraphStyle(paragraphStyle)
newParagraphStyle.headIndent -= Metrics.listTextIndentation
newParagraphStyle.firstLineHeadIndent -= Metrics.listTextIndentation
newParagraphStyle.textList = nil

var resultingAttributes = attributes
resultingAttributes[NSParagraphStyleAttributeName] = newParagraphStyle

return resultingAttributes
}

func present(in attributes: [String : Any]) -> Bool {
guard let paragraphStyle = attributes[NSParagraphStyleAttributeName] as? ParagraphStyle,
let textList = paragraphStyle.textList else {
guard let style = attributes[NSParagraphStyleAttributeName] as? ParagraphStyle, let list = style.textList else {
return false
}
return textList.style == listStyle

return list.style == listStyle
}

func needsEmptyLinePlaceholder() -> Bool {
return false
}

static func listsOfAnyKindPresent(in attributes: [String: Any]) -> Bool {
let style = attributes[NSParagraphStyleAttributeName] as? ParagraphStyle
return style?.textList != nil
}
}

13 changes: 1 addition & 12 deletions Aztec/Classes/TextKit/LayoutManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class LayoutManager: NSLayoutManager {

/// Blockquote's Left Border Color
///
var blockquoteBorderColor: UIColor = UIColor(red: 0.52, green: 0.65, blue: 0.73, alpha: 1.0)
var blockquoteBorderColor = UIColor(red: 0.52, green: 0.65, blue: 0.73, alpha: 1.0)

/// Blockquote's Background Color
///
Expand Down Expand Up @@ -108,17 +108,6 @@ private extension LayoutManager {

self.drawItem(number: markerNumber, in: lineRect, from: list, using: paragraphStyle, at: location)
}

// Draw the Last Line's Item
guard range.endLocation == textStorage.rangeOfEntireString.endLocation, !extraLineFragmentRect.isEmpty else {
return
}

let location = range.endLocation - 1
let lineRect = extraLineFragmentRect.offsetBy(dx: origin.x, dy: origin.y)
let markerNumber = textStorage.itemNumber(in: list, at: location) + 1

drawItem(number: markerNumber, in: lineRect, from: list, using: paragraphStyle, at: location)
}
}

Expand Down
3 changes: 2 additions & 1 deletion Aztec/Classes/TextKit/TextStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,8 @@ open class TextStorage: NSTextStorage {
}

// MARK: - Styles: Toggling
@discardableResult func toggle(formatter: AttributeFormatter, at range: NSRange) -> NSRange {
@discardableResult
func toggle(formatter: AttributeFormatter, at range: NSRange) -> NSRange {
let applicationRange = formatter.applicationRange(for: range, in: self)
if applicationRange.length == 0, !formatter.worksInEmptyRange() {
return applicationRange
Expand Down
124 changes: 115 additions & 9 deletions Aztec/Classes/TextKit/TextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ open class TextView: UITextView {
storage.undoManager = undoManager
commonInit()
}

required public init?(coder aDecoder: NSCoder) {

defaultFont = UIFont.systemFont(ofSize: 14)
Expand Down Expand Up @@ -211,6 +211,26 @@ open class TextView: UITextView {
addGestureRecognizer(attachmentGestureRecognizer)
}


// MARK: - Overwritten Properties

/// Overwrites Typing Attributes:
/// This is the (only) valid hook we've found, in order to (selectively) remove the 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)
super.typingAttributes = updatedAttributes

return updatedAttributes
}
set {
super.typingAttributes = newValue
}
}


// MARK: - Intercept copy paste operations

open override func cut(_ sender: Any?) {
Expand Down Expand Up @@ -267,6 +287,14 @@ open class TextView: UITextView {
// MARK: - Intercept keyboard operations

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.
///
if TextListFormatter.listsOfAnyKindPresent(in: typingAttributes) {
ensureInsertionOfNewlineOnEmptyDocuments()
}

// Note:
// Whenever the entered text causes the Paragraph Attributes to be removed, we should prevent the actual
// text insertion to happen. Thus, we won't call super.insertText.
Expand Down Expand Up @@ -497,7 +525,7 @@ open class TextView: UITextView {
// MARK: - Formatting

func toggle(formatter: AttributeFormatter, atRange range: NSRange) {
let applicationRange = storage.toggle(formatter: formatter, at: range)
let applicationRange = storage.toggle(formatter: formatter, at: range)
if applicationRange.length == 0 {
typingAttributes = formatter.toggle(in: typingAttributes)
} else {
Expand Down Expand Up @@ -568,8 +596,11 @@ open class TextView: UITextView {
/// - Parameter range: The NSRange to edit.
///
open func toggleOrderedList(range: NSRange) {
ensureInsertionOfNewlineOnEmptyDocuments()

let formatter = TextListFormatter(style: .ordered, placeholderAttributes: typingAttributes)
toggle(formatter: formatter, atRange: range)

forceRedrawCursorAfterDelay()
}

Expand All @@ -579,8 +610,11 @@ open class TextView: UITextView {
/// - Parameter range: The NSRange to edit.
///
open func toggleUnorderedList(range: NSRange) {
ensureInsertionOfNewlineOnEmptyDocuments()

let formatter = TextListFormatter(style: .unordered, placeholderAttributes: typingAttributes)
toggle(formatter: formatter, atRange: range)

forceRedrawCursorAfterDelay()
}

Expand Down Expand Up @@ -612,9 +646,7 @@ open class TextView: UITextView {
}()

private lazy var paragraphFormatters: [AttributeFormatter] = [
TextListFormatter(style: .ordered),
TextListFormatter(style: .unordered),
BlockquoteFormatter(),
BlockquoteFormatter(),
HeaderFormatter(headerLevel:.h1),
HeaderFormatter(headerLevel:.h2),
HeaderFormatter(headerLevel:.h3),
Expand All @@ -635,14 +667,13 @@ open class TextView: UITextView {
guard deletedText.string == String(.newline) || range.location == 0 else {
return
}

for formatter in paragraphFormatters {
if let locationBefore = storage.string.location(before: range.location),
formatter.present(in: textStorage, at: locationBefore) {
if range.endLocation < storage.length {
formatter.applyAttributes(to: storage, at: range)
}
} else if formatter.present(in: textStorage, at: range.location) || range.location == 0 {
formatter.removeAttributes(from: textStorage, at: range)
}
}
}
Expand Down Expand Up @@ -712,9 +743,21 @@ open class TextView: UITextView {
return false
}

let formatters:[AttributeFormatter] = [TextListFormatter(style: .ordered), TextListFormatter(style: .unordered), BlockquoteFormatter()]
let formatters:[AttributeFormatter] = [
TextListFormatter(style: .ordered),
TextListFormatter(style: .unordered),
BlockquoteFormatter()
]

let atEdgeOfDocument = range.location >= storage.length

for formatter in formatters {
if formatter.present(in: textStorage, at: range.location) {
if atEdgeOfDocument && formatter.present(in: typingAttributes) {
typingAttributes = formatter.remove(from: typingAttributes)
return true
}

if !atEdgeOfDocument && formatter.present(in: textStorage, at: range.location) {
formatter.removeAttributes(from: textStorage, at: range)
return true
}
Expand All @@ -724,6 +767,69 @@ open class TextView: UITextView {
}


/// Removes the List Attributes from a collection of attributes, whenever:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd consider adding extra documentation to explain why this is necessary. Specifically something like:

// 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`).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, thank you!

///
/// A. The selected location is at the very end of the document
/// B. There's a list!
/// C. The previous character is a '\n'
///
/// 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`).
///
/// - Parameter attributes: Typing Attributes.
///
/// - Returns: Updated Typing Attributes.
///
private func ensureRemovalOfListAttribute(from attributes: [String: Any]) -> [String: Any] {
guard selectedRange.location == storage.length else {
return attributes
}

guard TextListFormatter.listsOfAnyKindPresent(in: attributes) else {
return attributes
}

let previousRange = NSRange(location: selectedRange.location - 1, length: 1)
let previousString = storage.safeSubstring(at: previousRange) ?? String(.newline)
guard previousString == String(.newline) else {
return attributes
}


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)
}

return attributes
}


/// Inserts an empty line, whenever we're at the end of the document, and there's no selected text.
///
private func ensureInsertionOfNewlineOnEmptyDocuments() {
guard selectedRange.location == storage.length && selectedRange.length == 0 else {
return
}

let previousRange = selectedRange
let previousStyle = typingAttributes

super.insertText(String(.newline))

selectedRange = previousRange
typingAttributes = previousStyle
}


/// Indicates whether a new empty paragraph was created after the insertion of text at the specified location
///
/// - Parameters:
Expand Down
Loading