Skip to content

Commit cc40df2

Browse files
Merge pull request #586 from wordpress-mobile/issue/backporting-nested-lists-shortcuts
Backport: Support for Tab + Shift Tab
2 parents b091555 + 0beb90e commit cc40df2

File tree

3 files changed

+84
-14
lines changed

3 files changed

+84
-14
lines changed

Aztec/Classes/Extensions/Character+Name.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ extension Character {
1111
case objectReplacement = "\u{FFFC}"
1212
case paragraphSeparator = "\u{2029}"
1313
case space = " "
14+
case tab = "\t"
1415
case zeroWidthSpace = "\u{200B}"
1516
}
1617

Aztec/Classes/Formatters/Implementations/TextListFormatter.swift

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,20 @@ class TextListFormatter: ParagraphAttributeFormatter {
6464
return resultingAttributes
6565
}
6666

67-
func present(in attributes: [String : Any]) -> Bool {
68-
guard let style = attributes[NSParagraphStyleAttributeName] as? ParagraphStyle, let list = style.lists.last else {
69-
return false
70-
}
71-
72-
return list.style == listStyle
67+
func present(in attributes: [String: Any]) -> Bool {
68+
return TextListFormatter.lists(in: attributes).last?.style == listStyle
7369
}
7470

71+
72+
// MARK: - Static Helpers
73+
7574
static func listsOfAnyKindPresent(in attributes: [String: Any]) -> Bool {
76-
guard let style = attributes[NSParagraphStyleAttributeName] as? ParagraphStyle else {
77-
return false
78-
}
79-
return !(style.lists.isEmpty)
75+
return lists(in: attributes).isEmpty == false
76+
}
77+
78+
static func lists(in attributes: [String: Any]) -> [TextList] {
79+
let style = attributes[NSParagraphStyleAttributeName] as? ParagraphStyle
80+
return style?.lists ?? []
8081
}
8182
}
8283

Aztec/Classes/TextKit/TextView.swift

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ open class TextView: UITextView {
145145

146146
open weak var formattingDelegate: TextViewFormattingDelegate?
147147

148+
// MARK: - Properties: Text Lists
149+
150+
var maximumListIndentationLevels = 7
151+
148152
// MARK: - Properties: GUI Defaults
149153

150154
let defaultFont: UIFont
@@ -292,14 +296,51 @@ open class TextView: UITextView {
292296
// When the keyboard "enter" key is pressed, the keycode corresponds to .carriageReturn,
293297
// even if it's later converted to .lineFeed by default.
294298
//
295-
return [UIKeyCommand(input: String(.carriageReturn), modifierFlags: .shift , action: #selector(handleShiftEnter(command:)))]
299+
return [
300+
UIKeyCommand(input: String(.carriageReturn), modifierFlags: .shift, action: #selector(handleShiftEnter(command:))),
301+
UIKeyCommand(input: String(.tab), modifierFlags: .shift, action: #selector(handleShiftTab(command:))),
302+
UIKeyCommand(input: String(.tab), modifierFlags: [], action: #selector(handleTab(command:)))
303+
]
296304
}
297305
}
298306

299307
func handleShiftEnter(command: UIKeyCommand) {
300308
insertText(String(.lineSeparator))
301309
}
302310

311+
func handleShiftTab(command: UIKeyCommand) {
312+
guard let list = TextListFormatter.lists(in: typingAttributes).last else {
313+
return
314+
}
315+
316+
let formatter = TextListFormatter(style: list.style, placeholderAttributes: nil, increaseDepth: true)
317+
let targetRange = formatter.applicationRange(for: selectedRange, in: storage)
318+
319+
performUndoable(at: targetRange) {
320+
let finalRange = formatter.removeAttributes(from: storage, at: targetRange)
321+
typingAttributes = textStorage.attributes(at: targetRange.location, effectiveRange: nil)
322+
return finalRange
323+
}
324+
}
325+
326+
func handleTab(command: UIKeyCommand) {
327+
let lists = TextListFormatter.lists(in: typingAttributes)
328+
guard let list = lists.last, lists.count < maximumListIndentationLevels else {
329+
insertText(String(.tab))
330+
return
331+
}
332+
333+
let formatter = TextListFormatter(style: list.style, placeholderAttributes: nil, increaseDepth: true)
334+
let targetRange = formatter.applicationRange(for: selectedRange, in: storage)
335+
336+
performUndoable(at: targetRange) {
337+
let finalRange = formatter.applyAttributes(to: storage, at: targetRange)
338+
typingAttributes = textStorage.attributes(at: targetRange.location, effectiveRange: nil)
339+
return finalRange
340+
}
341+
}
342+
343+
303344
// MARK: - Pasteboard Helpers
304345

305346
private func storeInPasteboard(encoded data: Data) {
@@ -1532,10 +1573,37 @@ extension TextView: TextStorageAttachmentsDelegate {
15321573
}
15331574
}
15341575

1535-
//MARK: - Undo implementation
15361576

1537-
extension TextView {
1538-
1577+
// MARK: - Undo implementation
1578+
1579+
private extension TextView {
1580+
1581+
/// Undoable Operation. Returns the Final Text Range, resulting from applying the undoable Operation
1582+
/// Note that for Styling Operations, the Final Range will most likely match the Initial Range.
1583+
/// For text editing it will only match the initial range if the original string was replaced with a
1584+
/// string of the same length.
1585+
///
1586+
typealias Undoable = () -> NSRange
1587+
1588+
1589+
/// Registers an Undoable Operation, which will be applied at the specified Initial Range.
1590+
///
1591+
/// - Parameters:
1592+
/// - initialRange: Initial Storage Range upon which we'll apply a transformation.
1593+
/// - block: Undoable Operation. Should return the resulting Substring's Range.
1594+
///
1595+
func performUndoable(at initialRange: NSRange, block: Undoable) {
1596+
let originalString = storage.attributedSubstring(from: initialRange)
1597+
1598+
let finalRange = block()
1599+
1600+
undoManager?.registerUndo(withTarget: self, handler: { [weak self] target in
1601+
self?.undoTextReplacement(of: originalString, finalRange: finalRange)
1602+
})
1603+
1604+
delegate?.textViewDidChange?(self)
1605+
}
1606+
15391607
func undoTextReplacement(of originalText: NSAttributedString, finalRange: NSRange) {
15401608

15411609
let redoFinalRange = NSRange(location: finalRange.location, length: originalText.length)

0 commit comments

Comments
 (0)