-
Notifications
You must be signed in to change notification settings - Fork 85
/
Copy pathEditorView.swift
1649 lines (1414 loc) · 72.9 KB
/
EditorView.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//
// EditorView.swift
// Proton
//
// Created by Rajdeep Kwatra on 5/1/20.
// Copyright © 2020 Rajdeep Kwatra. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import UIKit
import ProtonCore
/// Describes an object interested in observing the bounds of a view. `Attachment` is `BoundsObserving` and reacts to
/// changes in the bounds of views hosted within the `Attachment`. Any view contained in the `Attachment` that is capable of
/// changing its bounds must define and set `BoundsObserving` to `Attachment`.
/// ### Usage Example ###
/// ```
/// class MyAttachmentView: UIView {
/// weak var boundsObserver: BoundsObserving?
///
/// override var bounds: CGRect {
/// didSet {
/// guard oldValue != bounds else { return }
/// boundsObserver?.didChangeBounds(bounds)
/// }
/// }
/// }
///
/// let myView = MyAttachmentView()
/// let attachment = Attachment(myView, size: .matchContent)
/// myView.boundsObserver = attachment
/// ```
public protocol BoundsObserving: AnyObject {
/// Lets the observer know that bounds of current object have changed
/// - Parameter bounds: New bounds
func didChangeBounds(_ bounds: CGRect, oldBounds: CGRect)
}
/// Describes opening and closing separators for `EditorView`'`getFullAttributedText(:)` function.
public struct AttachmentContentIdentifier {
public let openingID: NSAttributedString
public let closingID: NSAttributedString
/// Constructs separators for using in `getFullAttributedText(:)`
/// - Parameters:
/// - openingID: Used to identify start of attachment content
/// - closingID: Used to identify end of attachment content
init(openingID: NSAttributedString, closingID: NSAttributedString) {
self.openingID = openingID
self.closingID = closingID
}
}
public struct PreserveBlockAttachmentNewline: OptionSet {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let before = PreserveBlockAttachmentNewline(rawValue: 1 << 0)
public static let after = PreserveBlockAttachmentNewline(rawValue: 1 << 1)
public static let none: PreserveBlockAttachmentNewline = []
public static let both: PreserveBlockAttachmentNewline = [.before, .after]
}
/// Defines the height for the Editor
public enum EditorHeight {
/// Default controlled via autolayout.
case `default`
/// Maximum height editor is allowed to grow to before it starts scrolling
case max(_ height: CGFloat)
/// Boundless height.
/// - Important: Editor must not have auto-layout constraints on height failing which the editor will stop growing per height
/// constraints and will not scroll beyond that point i.e. scrollbars would not be visible.
case infinite
}
/// Representation of a line of text in `EditorView`. A line is defined as a single fragment starting from the beginning of
/// bounds of `EditorView` to the end. A line may have any number of characters based on the contents in the `EditorView`.
/// - Note:
/// A line does not represent a full sentence in the `EditorView` but instead may start and/or end in the middle of
/// another based on how the content is laid out in the `EditorView`.
public struct EditorLine {
/// Text contained in the current line.
public let text: NSAttributedString
/// Range of text in the `EditorView` for the current line.
public let range: NSRange
/// Determines if the current line starts with given text.
/// Text comparison is case-sensitive.
/// - Parameter text: Text to compare
/// - Returns:
/// `true` if the current line text starts with the given string.
public func startsWith(_ text: String) -> Bool {
return self.text.string.hasPrefix(text)
}
/// Determines if the current line ends with given text.
/// Text comparison is case-sensitive.
/// - Parameter text: Text to compare
/// - Returns:
/// `true` if the current line text ends with the given string.
public func endsWith(_ text: String) -> Bool {
self.text.string.hasSuffix(text)
}
// EditorLine may only be initialized internally
init(text: NSAttributedString, range: NSRange) {
self.text = text
self.range = range
}
}
/// A scrollable, multiline text region capable of resizing itself based of the height of the content. Maximum height of `EditorView`
/// may be restricted using an absolute value or by using auto-layout constraints. Instantiation of `EditorView` is simple and straightforward
/// and can be used to host simple formatted text or complex layout containing multiple nested `EditorView` via use of `Attachment`.
open class EditorView: UIView {
private var defaultTextColor: UIColor?
var textProcessor: TextProcessor?
let richTextView: RichTextView
let context: RichTextViewContext
var needsAsyncTextResolution = false
private let attachmentRenderingScheduler = AsyncTaskScheduler()
// Used for tracking rendered viewport in async behaviour specifically to ensure calling
// `didCompleteRenderingViewport` only once for each viewport value.
private var renderedViewport: CGRect? {
didSet {
guard let renderedViewport,
renderedViewport != oldValue else { return }
asyncAttachmentRenderingDelegate?.didCompleteRenderingViewport(renderedViewport, in: self)
}
}
var editorContextDelegate: EditorViewDelegate? {
get { editorViewContext.delegate }
}
public var preserveBlockAttachmentNewline: PreserveBlockAttachmentNewline {
get { richTextView.preserveBlockAttachmentNewline }
set { richTextView.preserveBlockAttachmentNewline = newValue }
}
public var scrollView: UIScrollView {
richTextView as UIScrollView
}
/// Context for the current Editor
public let editorViewContext: EditorViewContext
/// Enables asynchronous rendering of attachments.
/// - Note:
/// Since attachments must me rendered on main thread, the rendering only continues when there is no user interaction. By default, rendering starts
/// immediately after the content is set in the `EditorView`. However, since attachments must render on main thread only, as soon as there is a user
/// interaction event, like scrolling, is received, the rendering is paused until scrolling stops and then, resumes again.
/// - Important:
/// This feature allows for almost instantaneous load of the editor content. However, this is only recommended when there are lots of attachments that
/// may be causing overall load time to be in unacceptable region. Since attachments are rendered one at a time, for simple content, the overall load time
/// mat be more than when synchronous mode, ie default, is used. The perceived performance/TTI will almost always be better with asynchronous rendering.
public weak var asyncAttachmentRenderingDelegate: AsyncAttachmentRenderingDelegate?
/// Returns `UITextInput` of current instance
public var textInput: UITextInput {
richTextView
}
public var textInteractions: [UITextInteraction] {
richTextView.interactions.compactMap({ $0 as? UITextInteraction })
}
public var textViewGestures: [UIGestureRecognizer] {
richTextView.gestureRecognizers ?? []
}
public var textDragInteractionEnabled: Bool {
get { richTextView.textDragInteraction?.isEnabled ?? false }
set { richTextView.textDragInteraction?.isEnabled = newValue }
}
/// Line number provider to be used to show custom line numbers in gutter.
/// - Note: Only applicable when `isLineNumbersEnabled` is set to `true`
public var lineNumberProvider: LineNumberProvider? {
get { richTextView.lineNumberProvider }
set { richTextView.lineNumberProvider = newValue }
}
public var isLineNumbersEnabled: Bool {
get { richTextView.isLineNumbersEnabled }
set { richTextView.isLineNumbersEnabled = newValue }
}
public var lineNumberFormatting: LineNumberFormatting {
get { richTextView.lineNumberFormatting }
set { richTextView.lineNumberFormatting = newValue }
}
public override var bounds: CGRect {
didSet {
guard oldValue != bounds else { return }
for (attachment, _) in attributedText.attachmentRanges where attachment.isContainerDependentSizing {
if attachment.cachedContainerSize != bounds.size {
attachment.cachedBounds = nil
}
}
AggregateEditorViewDelegate.editor(self, didChangeSize: bounds.size, previousSize: oldValue.size)
}
}
/// An object interested in responding to editing and focus related events in the `EditorView`.
open weak var delegate: EditorViewDelegate?
/// List formatting provider to be used for rendering lists in the Editor.
public weak var listFormattingProvider: EditorListFormattingProvider?
/// List of commands supported by the editor.
/// - Note:
/// * To support any command, set value to nil. Default behaviour.
/// * To prevent any command to be executed, set value to be an empty array.
public var registeredCommands: [EditorCommand]?
/// Async Text Resolvers supported by the Editor.
public var asyncTextResolvers: [AsyncTextResolving] = []
/// Low-tech lock mechanism to know when `attributedText` is being set
private(set) var isSettingAttributedText = false
// Making this a convenience init fails the test `testRendersWidthRangeAttachment` as the init of a class subclassed from
// `EditorView` is returned as type `EditorView` and not the class itself, causing the test to fail.
/// Initializes the EditorView
/// - Parameters:
/// - frame: Frame to be used for `EditorView`.
/// - context: Optional context to be used. `EditorViewContext` is link between `EditorCommandExecutor` and the `EditorView`.
/// `EditorCommandExecutor` needs to have same context as the `EditorView` to execute a command on it. Unless you need to have
/// restriction around some commands to be restricted in execution on certain specific editors, the default value may be used.
/// - allowAutogrowing: When set to `true`, editor automatically grows based on content size and constraints applied. e.g. when used in non-fullscreen mode
/// - undoManager: Override `UndoManager`. When nil, default `UndoManager` from underlying `UITextView` is used.
public init(
frame: CGRect = .zero,
context: EditorViewContext = .shared,
allowAutogrowing: Bool = true,
undoManager: UndoManager? = nil) {
self.context = context.richTextViewContext
self.editorViewContext = context
self.richTextView = RichTextView(
frame: frame,
context: self.context,
allowAutogrowing: allowAutogrowing,
undoManager: undoManager
)
super.init(frame: frame)
self.textProcessor = TextProcessor(editor: self)
self.richTextView.textProcessor = textProcessor
setup()
}
init(
frame: CGRect,
richTextViewContext: RichTextViewContext,
allowAutogrowing: Bool = true,
undoManager: UndoManager?) {
self.context = richTextViewContext
self.richTextView = RichTextView(
frame: frame,
context: context,
allowAutogrowing: allowAutogrowing,
undoManager: undoManager
)
self.editorViewContext = .null
super.init(frame: frame)
self.textProcessor = TextProcessor(editor: self)
self.richTextView.textProcessor = textProcessor
setup()
}
/// Input accessory view to be used
open var editorInputAccessoryView: UIView? {
get {
#if !os(visionOS)
return richTextView.inputAccessoryView
#else
return nil
#endif
} set {
#if os(visionOS)
return
#else
richTextView.inputAccessoryView = newValue
#endif
}
}
/// Input view to be used
open var editorInputView: UIView? {
get { richTextView.inputView }
set { richTextView.inputView = newValue }
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// List of all the registered `TextProcessors` in the `EditorView`. This may be used by nested `EditorView` to inherit all the
/// text processors from the container `EditorView`.
/// ### Usage example ###
/// ```
/// func execute(on editor: EditorView) {
/// let attachment = PanelAttachment(frame: .zero)
/// let panel = attachment.view
/// panel.editor.registerProcessors(editor.registeredProcessors)
/// editor.insertAttachment(in: editor.selectedRange, attachment: attachment)
/// }
/// ```
public var registeredProcessors: [TextProcessing] {
return textProcessor?.activeProcessors ?? []
}
public var selectedTextRange: UITextRange? {
get { richTextView.selectedTextRange }
set { richTextView.selectedTextRange = newValue }
}
public var scrollViewDelegate: UIScrollViewDelegate? {
get { richTextView.richTextScrollViewDelegate }
set { richTextView.richTextScrollViewDelegate = newValue }
}
public var panGestureRecognizer: UIGestureRecognizer {
get { scrollView.panGestureRecognizer }
}
public var pinchGestureRecognizer: UIPinchGestureRecognizer? {
get { scrollView.pinchGestureRecognizer }
}
public var directionalPressGestureRecognizer: UIGestureRecognizer? {
get { scrollView.directionalPressGestureRecognizer }
}
/// Placeholder text for the `EditorView`. The value can contain any attributes which is natively
/// supported in the `NSAttributedString`.
public var placeholderText: NSAttributedString? {
get { richTextView.placeholderText }
set { richTextView.placeholderText = newValue }
}
/// Gets or sets insets for additional scroll area around the content. Default value is UIEdgeInsetsZero.
public var contentInset: UIEdgeInsets {
get { richTextView.contentInset }
set { richTextView.contentInset = newValue }
}
public var verticalScrollIndicatorInsets: UIEdgeInsets {
get { richTextView.verticalScrollIndicatorInsets }
set { richTextView.verticalScrollIndicatorInsets = newValue }
}
#if !os(visionOS)
public var keyboardDismissMode: UIScrollView.KeyboardDismissMode {
get { richTextView.keyboardDismissMode }
set { richTextView.keyboardDismissMode = newValue }
}
#endif
public var isScrollEnabled: Bool {
get { richTextView.isScrollEnabled }
set { richTextView.isScrollEnabled = newValue }
}
/// Gets or sets the insets for the text container's layout area within the editor's content area
public var textContainerInset: UIEdgeInsets {
get { richTextView.textContainerInset }
set { richTextView.textContainerInset = newValue }
}
/// The types of data converted to tappable URLs in the editor view.
public var dataDetectorTypes: UIDataDetectorTypes {
get { richTextView.dataDetectorTypes }
set { richTextView.dataDetectorTypes = newValue }
}
/// Length of content within the Editor.
/// - Note:
/// An attachment is only counted as a single character. Content length does not include
/// length of content within the Attachment that is hosting another `EditorView`.
public var contentLength: Int {
return richTextView.contentLength
}
/// Determines if the `EditorView` is editable or not.
/// - Note:
/// Setting `isEditable` to `false` before setting the `attributedText` will make `EditorView` skip certain layout paths
/// and calculations for attachments containing `UIView`s, This is done primarily to improve the rendering performance of the `EditorView`
/// in case of text with large number of attachments.
public var isEditable: Bool {
get { richTextView.isEditable }
set {
guard richTextView.isEditable != newValue else { return }
richTextView.isEditable = newValue
AggregateEditorViewDelegate.editor(self, didChangeEditable: newValue)
}
}
/// Determines if the editor is empty.
public var isEmpty: Bool {
return richTextView.attributedText.length == 0
}
/// Current line information based the caret position or selected range. If the selected range spans across multiple
/// lines, only the line information of the line containing the start of the range is returned.
/// - Note:
/// This is based on the layout of text in the `EditorView` and not on the actual lines based on `\n`. The range may
/// contain multiple lines or part of different lines separated by `\n`.
/// To get lines based on new line characters, please use `contentLinesInRange(range)`, `previousContentLine(location)`
/// and `nextContentLine(location)`.
public var currentLayoutLine: EditorLine? {
return editorLayoutLineFrom(range: richTextView.currentLineRange )
}
/// First line of content based on layout in the Editor. Nil if editor is empty.
/// - Note:
/// This is based on the layout of text in the `EditorView` and not on the actual lines based on `\n`. The range may
/// contain multiple lines or part of different lines separated by `\n`.
/// To get lines based on new line characters, please use `contentLinesInRange(range)`, `previousContentLine(location)`
/// and `nextContentLine(location)`.
public var firstLayoutLine: EditorLine? {
return editorLayoutLineFrom(range: NSRange(location: 1, length: 0) )
}
/// Last line of content based on layout in the Editor. Nil if editor is empty.
/// - Note:
/// This is based on the layout of text in the `EditorView` and not on the actual lines based on `\n`. The range may
/// contain multiple lines or part of different lines separated by `\n`.
/// To get lines based on new line characters, please use `contentLinesInRange(range)`, `previousContentLine(location)`
/// and `nextContentLine(location)`.
public var lastLayoutLine: EditorLine? {
return editorLayoutLineFrom(range: NSRange(location: contentLength - 1, length: 0) )
}
/// Selected text in the editor.
public var selectedText: NSAttributedString {
return attributedText.attributedSubstring(from: selectedRange)
}
/// Background color for the editor.
public override var backgroundColor: UIColor? {
didSet {
richTextView.backgroundColor = backgroundColor
if backgroundColor != oldValue {
updateBackgroundInheritingViews(color: backgroundColor, oldColor: backgroundColor)
}
delegate?.editor(self, didChangeBackgroundColor: backgroundColor, oldColor: oldValue)
}
}
/// Default font to be used by the Editor. A font may be overridden on whole or part of content in `EditorView` by an `EditorCommand` or
/// `TextProcessing` conformances.
public var font: UIFont = UIFont.preferredFont(forTextStyle: .body) {
didSet { richTextView.typingAttributes[.font] = font }
}
/// Default paragraph style to be used by the Editor. The style may be overridden on whole or part of content in
/// `EditorView` by an `EditorCommand` or `TextProcessing` conformances.
public var paragraphStyle: NSMutableParagraphStyle = NSMutableParagraphStyle() {
didSet { richTextView.typingAttributes[.paragraphStyle] = paragraphStyle }
}
/// Default text color to be used by the Editor. The color may be overridden on whole or part of content in
/// `EditorView` by an `EditorCommand` or `TextProcessing` conformances.
public var textColor: UIColor {
get { defaultTextColor ?? richTextView.defaultTextColor }
set {
defaultTextColor = newValue
richTextView.textColor = newValue
}
}
/// Maximum height that the `EditorView` can expand to. After reaching the maximum specified height, the editor becomes scrollable.
/// - Note:
/// If both auto-layout constraints and `maxHeight` are used, the lower of the two height would be used as maximum allowed height.
public var maxHeight: EditorHeight {
get {
let height = richTextView.maxHeight
switch height {
case 0:
return .default
case .greatestFiniteMagnitude:
return .infinite
default:
return .max(height)
}
}
set {
switch newValue {
case let .max(height):
richTextView.maxHeight = height
case .default:
richTextView.maxHeight = 0
case .infinite:
richTextView.maxHeight = .greatestFiniteMagnitude
}
}
}
/// Text to be set in the `EditorView`
public var attributedText: NSAttributedString {
get {
richTextView.attributedText
}
set {
isSettingAttributedText = true
attachmentRenderingScheduler.cancel()
renderedViewport = nil
// Clear text before setting new value to avoid issues with formatting/layout when
// editor is hosted in a scrollable container and content is set multiple times.
richTextView.attributedText = NSAttributedString()
let isDeferred = false
AggregateEditorViewDelegate.editor(self, willSetAttributedText: newValue, isDeferred: isDeferred)
richTextView.attributedText = newValue
isSettingAttributedText = false
AggregateEditorViewDelegate.editor(self, didSetAttributedText: newValue, isDeferred: isDeferred)
}
}
public var nestedEditors: [EditorView] {
richTextView.nestedTextViews.compactMap { $0.editorView }
}
public var text: String {
richTextView.text
}
// Override canBecomeFirstResponder property
open override var canBecomeFirstResponder: Bool {
return richTextView.canBecomeFirstResponder
}
// Method to disable becoming first responder
func disableFirstResponder() {
richTextView.disableFirstResponder()
}
// Method to enable becoming first responder
func enableFirstResponder() {
richTextView.enableFirstResponder()
}
public var selectedRange: NSRange {
get { richTextView.ensuringValidSelectedRange() }
set { richTextView.selectedRange = newValue }
}
public var lineFragmentPadding: CGFloat {
richTextView.textContainer.lineFragmentPadding
}
/// Typing attributes to be used. Automatically resets when the selection changes.
/// To apply an attribute in the current position such that it is applied as text is typed,
/// the attribute must be added to `typingAttributes` collection.
public var typingAttributes: [NSAttributedString.Key: Any] {
get { richTextView.typingAttributes }
set { richTextView.typingAttributes = newValue }
}
/// An object interested in observing the changes in bounds of the `Editor`, typically an `Attachment`.
public var boundsObserver: BoundsObserving? {
get { richTextView.boundsObserver }
set { richTextView.boundsObserver = newValue }
}
/// Gets and sets the content offset.
public var contentOffset: CGPoint {
get { richTextView.contentOffset }
set { richTextView.contentOffset = newValue }
}
/// The size of the content view.
public var contentSize: CGSize {
get { richTextView.contentSize }
}
/// The attributes to apply to links.
public var linkTextAttributes: [NSAttributedString.Key: Any]! {
get { richTextView.linkTextAttributes }
set { richTextView.linkTextAttributes = newValue }
}
/// Range of end of text in the `EditorView`. The range has always has length of 0.
public var textEndRange: NSRange {
return richTextView.textEndRange
}
/// Determines if the current Editor is contained in an attachment
public var isContainedInAnAttachment: Bool {
return getAttachmentContentView(view: superview) != nil
}
/// Name of the content if the Editor is contained within an `Attachment`.
/// This is done by recursive look-up of views in the `Attachment` content view
/// i.e. the Editor may be nested in subviews within the contentView of Attachment.
/// The value is nil if the Editor is not contained within an `Attachment`.
public var contentName: EditorContent.Name? {
return getAttachmentContentView(view: superview)?.name
}
/// Returns the visible bounds of the `EditorView` within a scrollable container.
/// - Note:
/// If `EditorView` has defined a `ViewportProvider`, the `viewport` is calculated per the provider.
/// A `ViewportProvider` may be needed in cases where `EditorView` is hosted inside another `UIScrollView` and the
/// viewport needs to be calculated based on the viewport of container `UIScrollView`.
open var viewport: CGRect {
return asyncAttachmentRenderingDelegate?.prioritizedViewport ?? richTextView.viewport
}
/// Returns the visible text range. In case of non-scrollable `EditorView`, entire range is `visibleRange`.
/// The range may be `nil` if it is queried before layout has begun
/// - Note:
/// If `EditorView` has defined a `ViewportProvider`, the `visibleRange` is calculated per the `viewport` from provider.
/// A `ViewportProvider` may be needed in cases where `EditorView` is hosted inside another `UIScrollView` and the
/// viewport needs to be calculated based on the viewport of container `UIScrollView`.
public var visibleRange: NSRange? {
rangeForRect(viewport)
}
/// Attachment containing the current Editor.
public var containerAttachment: Attachment? {
return getAttachmentContentView(view: superview)?.attachment
}
/// Nesting level of current Editor within other attachments containing Editors.
/// 0 indicates that the Editor is not contained in an attachment.
public var nestingLevel: Int {
var nestingLevel = 0
var containerEditor = containerAttachment?.containerEditorView
while containerEditor != nil {
nestingLevel += 1
containerEditor = containerEditor?.containerAttachment?.containerEditorView
}
return nestingLevel
}
/// Returns if the `EditorView` is a root editor i.e. not contained in any `Attachment`
public var isRootEditor: Bool {
parentEditor == nil
}
/// Returns the root editor of the current Editor. Returns `self` where the current editor is not contained within an `Attachment`.
/// - Note:This is different from `parentEditor` which is immediate parent of the current editor
public var rootEditor: EditorView {
if let parentEditor {
return parentEditor.rootEditor
}
return self
}
/// `EditorView` containing the current `EditorView` in an `Attachment`
public var parentEditor: EditorView? {
containerAttachment?.containerEditorView
}
/// Clears the contents in the Editor.
public func clear() {
self.attributedText = NSAttributedString()
}
/// The auto-capitalization style for the text object.
/// default is `UITextAutocapitalizationTypeSentences`
public var autocapitalizationType: UITextAutocapitalizationType {
get { richTextView.autocapitalizationType }
set { richTextView.autocapitalizationType = newValue }
}
/// The autocorrection style for the text object.
/// default is `UITextAutocorrectionTypeDefault`
public var autocorrectionType: UITextAutocorrectionType {
get { richTextView.autocorrectionType }
set { richTextView.autocorrectionType = newValue }
}
/// The spell-checking style for the text object.
public var spellCheckingType: UITextSpellCheckingType {
get { richTextView.spellCheckingType }
set { richTextView.spellCheckingType = newValue }
}
/// The configuration state for smart quotes.
public var smartQuotesType: UITextSmartQuotesType {
get { richTextView.smartQuotesType }
set { richTextView.smartQuotesType = newValue }
}
/// The configuration state for smart dashes.
public var smartDashesType: UITextSmartDashesType {
get { richTextView.smartDashesType }
set { richTextView.smartDashesType = newValue }
}
/// The configuration state for the smart insertion and deletion of space characters.
public var smartInsertDeleteType: UITextSmartInsertDeleteType {
get { richTextView.smartInsertDeleteType }
set { richTextView.smartInsertDeleteType = newValue }
}
/// The keyboard style associated with the text object.
public var keyboardType: UIKeyboardType {
get { richTextView.keyboardType }
set { richTextView.keyboardType = newValue }
}
/// The appearance style of the keyboard that is associated with the text object
public var keyboardAppearance: UIKeyboardAppearance {
get { richTextView.keyboardAppearance }
set { richTextView.keyboardAppearance = newValue }
}
/// The visible title of the Return key.
public var returnKeyType: UIReturnKeyType {
get { richTextView.returnKeyType }
set { richTextView.returnKeyType = newValue }
}
/// A Boolean value indicating whether the Return key is automatically enabled when the user is entering text.
/// default is `NO` (when `YES`, will automatically disable return key when text widget has zero-length contents, and will automatically enable when text widget has non-zero-length contents)
public var enablesReturnKeyAutomatically: Bool {
get { richTextView.enablesReturnKeyAutomatically }
set { richTextView.enablesReturnKeyAutomatically = newValue }
}
/// Identifies whether the text object should disable text copying and in some cases hide the text being entered.
/// default is `NO`
public var isSecureTextEntry: Bool {
get { richTextView.isSecureTextEntry }
set { richTextView.isSecureTextEntry = newValue }
}
/// The semantic meaning expected by a text input area.
/// The textContentType property is to provide the keyboard with extra information about the semantic intent of the text document.
/// default is `nil`
public var textContentType: UITextContentType! {
get { richTextView.textContentType }
set { richTextView.textContentType = newValue }
}
/// A Boolean value indicating whether the text view allows the user to edit style information.
public var allowsEditingTextAttributes: Bool {
get { richTextView.allowsEditingTextAttributes }
set { richTextView.allowsEditingTextAttributes = newValue }
}
/// A Boolean value indicating whether the receiver is selectable.
/// This property controls the ability of the user to select content and interact with URLs and text attachments. The default value is true.
public var isSelectable: Bool {
get { richTextView.isSelectable }
set { richTextView.isSelectable = newValue }
}
/// A text drag delegate object for customizing the drag source behavior of a text view.
public var textDragDelegate: UITextDragDelegate? {
get { richTextView.textDragDelegate }
set { richTextView.textDragDelegate = newValue }
}
/// The text drop delegate for interacting with a drop activity in the text view.
public var textDropDelegate: UITextDropDelegate? {
get { richTextView.textDropDelegate }
set { richTextView.textDropDelegate = newValue }
}
/// Shows or hides invisible characters
/// Invisible character ranges are governed by presence of `NSAttributedString.Key.invisible` attribute
public var showsInvisibleCharacters: Bool {
get { richTextView.showsInvisibleCharacters }
set { richTextView.showsInvisibleCharacters = newValue }
}
/// Returns the nearest shared undo manager in the responder chain.
open override var undoManager: UndoManager? {
get { richTextView.undoManager }
}
private func getAttachmentContentView(view: UIView?) -> AttachmentContentView? {
guard let view = view else { return nil }
if let attachmentContentView = view.superview as? AttachmentContentView {
return attachmentContentView
}
return getAttachmentContentView(view: view.superview)
}
private func setup() {
maxHeight = .default
richTextView.autocorrectionType = .default
richTextView.translatesAutoresizingMaskIntoConstraints = false
richTextView.defaultTextFormattingProvider = self
richTextView.richTextViewDelegate = self
richTextView.richTextViewListDelegate = self
addSubview(richTextView)
NSLayoutConstraint.activate([
richTextView.topAnchor.constraint(equalTo: topAnchor),
richTextView.bottomAnchor.constraint(equalTo: bottomAnchor),
richTextView.leadingAnchor.constraint(equalTo: leadingAnchor),
richTextView.trailingAnchor.constraint(equalTo: trailingAnchor),
])
typingAttributes = [
.font: font,
.paragraphStyle: paragraphStyle
]
richTextView.adjustsFontForContentSizeCategory = true
AggregateEditorViewDelegate.editor(self, isReady: false)
attachmentRenderingScheduler.delegate = self
}
/// Subclasses can override it to perform additional actions whenever the window changes.
/// - IMPORTANT: Overriding implementations must call `super.didMoveToWindow()`
open override func didMoveToWindow() {
super.didMoveToWindow()
let isReady = window != nil
AggregateEditorViewDelegate.editor(self, isReady: isReady)
}
/// Asks the view to calculate and return the size that best fits the specified size.
/// - Parameter size: The size for which the view should calculate its best-fitting size.
/// - Returns:
/// A new size that fits the receiver’s subviews.
override open func sizeThatFits(_ size: CGSize) -> CGSize {
return richTextView.sizeThatFits(size)
}
/// Adds an interaction to the view.
/// - Parameter interaction: The interaction object to add to the view.
open override func addInteraction(_ interaction: UIInteraction) {
richTextView.addInteraction(interaction)
}
/// Asks UIKit to make this object the first responder in its window.
/// - Returns:
/// `true` if this object is now the first-responder or `false` if it is not.
@discardableResult
public override func becomeFirstResponder() -> Bool {
return richTextView.becomeFirstResponder()
}
/// Denotes of the Editor is first responder
/// - Returns: true, if is first responder
public func isFirstResponder() -> Bool {
richTextView.isFirstResponder
}
/// Describes if one of the nested editor is first responder
/// - Returns: `true` if a nested editor is first responder.
/// - Note:
/// To check if current Editor itself is first responder, use `isFirstResponder()`.
public func containsFirstResponder() -> Bool {
nestedEditors.contains(where: { $0.isFirstResponder() })
}
/// Resets typing attributes back to default text color, font and paragraph style.
///All other attributes are dropped.
open func resetTypingAttributes() {
richTextView.resetTypingAttributes()
}
public func attachmentsInRange(_ range: NSRange) -> [AttachmentRange] {
guard range.endLocation <= attributedText.length else { return [] }
let substring = attributedText.attributedSubstring(from: range)
return substring.attachmentRanges
}
/// Converts given range to `UITextRange`, if valid
/// - Parameter range: Range to convert
/// - Returns: `UITextRange` representation of provided NSRange, if valid.
public func textRange(from range: NSRange) -> UITextRange? {
range.toTextRange(textInput: richTextView)
}
/// Cancels any pending rendering when async rendering of attachment is schedules.
/// - Note:
/// Asynchronous rendering is opt-in feature scheduled by providing `asyncAttachmentRenderingDelegate` to `EditorView`
public func cancelPendingAsyncRendering() {
attachmentRenderingScheduler.cancel()
}
/// The range of currently marked text in a document.
/// If there is no marked text, the value of the property is `nil`. Marked text is provisionally inserted text that requires user confirmation; it occurs in multistage text input. The current selection, which can be a caret or an extended range, always occurs within the marked text.
public var markedRange: NSRange? {
guard let range = richTextView.markedTextRange else { return nil }
let location = richTextView.offset(from: richTextView.beginningOfDocument, to: range.start)
let length = richTextView.offset(from: range.start, to: range.end)
// It returns `NSRange`, because `UITextPosition` is not very helpful without having access to additional methods and properties.
return NSRange(location: location, length: length)
}
public func setAttributes(_ attributes: [NSAttributedString.Key: Any], at range: NSRange) {
// self.richTextView.setAttributes(attributes, range: range)
// self.richTextView.enumerateAttribute(.attachment, in: range, options: .longestEffectiveRangeNotRequired) { value, rangeInContainer, _ in
// if let attachment = value as? Attachment {
// attachment.addedAttributesOnContainingRange(rangeInContainer: rangeInContainer, attributes: attributes)
// }
// }
}
/// Returns the full attributed text contained in the `EditorView` along with the ones in editors nested in contained Attachments.
/// - Parameter attachmentContentIdentifier: Identifier for opening and closing ranges for Attachment Content
/// - Returns: Full attributed text
/// - Note: An additional attribute with value of `Attachment.name` is automatically added with key `NSAttributedString.Key.viewOnly`.
/// This can be changed by overriding default implementation of `getFullTextRangeIdentificationAttributes()` in `Attachment`.
public func getFullAttributedText(using attachmentContentIdentifier: AttachmentContentIdentifier, in range: NSRange? = nil) -> NSAttributedString {
let text = NSMutableAttributedString()
let rangeToUse = range ?? attributedText.fullRange
let substring = attributedText.attributedSubstring(from: rangeToUse)
substring.enumerateAttribute(.attachment, in: substring.fullRange) { value, range, _ in
if let attachment = value as? Attachment {
let attachmentID = attachment.getFullTextRangeIdentificationAttributes()
attachment.contentEditors.forEach { editor in
let editorText = NSMutableAttributedString(attributedString: editor.getFullAttributedText(using: attachmentContentIdentifier))
let openingID = attachmentContentIdentifier.openingID.addingAttributes(attachmentID)
let closingID = attachmentContentIdentifier.closingID.addingAttributes(attachmentID)
editorText.insert(openingID, at: 0)
editorText.insert(closingID, at: editorText.length)
text.append(editorText)
}
} else {
let string = NSMutableAttributedString(attributedString: substring.attributedSubstring(from: range))
text.append(string)
}
}
return text
}
/// Sets async text resolution to resolve on next text layout pass.
/// - Note: Changing attributes also causes layout pass to be performed, and this any applicable `AsyncTextResolvers` will be executed.
public func setNeedsAsyncTextResolution() {
needsAsyncTextResolution = true
}
/// Invokes async text resolution to resolve on demand.
public func resolveAsyncTextIfNeeded() {
needsAsyncTextResolution = true
resolveAsyncText()
}
/// Returns the range of character at the given point
/// - Parameter point: Point to get range from
/// - Returns: Character range if available, else nil
public func rangeOfCharacter(at point: CGPoint) -> NSRange? {
let location = richTextView.convert(point, from: self)
return richTextView.rangeOfCharacter(at: location)
}
/// Gets the lines separated by newline characters from the given range.
/// - Parameter range: Range to get lines from.
/// - Returns: Array of `EditorLine` from the given content range.
/// - Note:
/// Lines returned from this function do not contain terminating newline character in the text content.
public func contentLinesInRange(_ range: NSRange) -> [EditorLine] {
return richTextView.contentLinesInRange(range)
}
/// Gets the previous line of content from the given location. A content line is defined by the presence of a
/// newline character.
/// - Parameter location: Location to find line from, in reverse direction
/// - Returns: Content line if a newline character exists before the current location, else nil
public func previousContentLine(from location: Int) -> EditorLine? {
return richTextView.previousContentLine(from: location)
}
/// Gets the next line of content from the given location. A content line is defined by the presence of a
/// newline character.