@@ -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