@openeditor/react-native-prose-editor 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +35 -0
- package/android/src/main/java/com/apollohg/editor/CaretGeometry.kt +2 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +138 -597
- package/android/src/main/java/com/apollohg/editor/EditorInputConnection.kt +13 -3
- package/android/src/main/java/com/apollohg/editor/InputSnapshotSupport.kt +6 -0
- package/android/src/main/java/com/apollohg/editor/NativeBlockEditorSurface.kt +1086 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +1 -1
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +26 -50
- package/android/src/main/java/com/apollohg/editor/NativeImageLoader.kt +89 -0
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +31 -66
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +98 -33
- package/dist/EditorToolbar.d.ts +1 -1
- package/dist/EditorToolbar.js +19 -0
- package/dist/NativeEditorBridge.d.ts +62 -35
- package/dist/NativeEditorBridge.js +122 -79
- package/dist/NativeProseViewer.d.ts +13 -34
- package/dist/NativeProseViewer.js +40 -465
- package/dist/NativeRichTextEditor.d.ts +10 -1
- package/dist/NativeRichTextEditor.js +72 -34
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/schemas.d.ts +11 -0
- package/dist/schemas.js +149 -0
- package/dist/useNativeEditor.d.ts +3 -3
- package/dist/useNativeEditor.js +3 -3
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/ios/Generated_editor_core.swift +97 -2
- package/ios/NativeBlockEditorSurface.swift +1393 -0
- package/ios/NativeEditorExpoView.swift +3 -3
- package/ios/NativeEditorModule.swift +30 -45
- package/ios/NativeInputSupport.swift +396 -0
- package/ios/PositionBridge.swift +4 -4
- package/ios/RichTextEditorView.swift +333 -786
- package/ios/editor_coreFFI/editor_coreFFI.h +147 -70
- package/package.json +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/rust/bindings/kotlin/uniffi/editor_core/editor_core.kt +2087 -1399
- package/android/src/androidTest/AndroidManifest.xml +0 -8
- package/android/src/androidTest/java/com/apollohg/editor/MeasureHeightInstrumentedTest.kt +0 -53
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceCollaborationInitialSyncTest.kt +0 -241
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceImeRegressionTest.kt +0 -338
- package/android/src/androidTest/java/com/apollohg/editor/NativeDeviceOutsideTapTest.kt +0 -789
- package/android/src/androidTest/java/com/apollohg/editor/NativeDevicePerformanceTest.kt +0 -350
- package/android/src/androidTest/java/com/apollohg/editor/NativeEditorOutsideTapActivity.kt +0 -5
- package/android/src/main/java/com/apollohg/editor/NativeProseViewerExpoView.kt +0 -315
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +0 -2172
- package/android/src/sharedTest/java/com/apollohg/editor/NativePerformanceSupport.kt +0 -482
- package/android/src/test/java/com/apollohg/editor/CaretGeometryTest.kt +0 -137
- package/android/src/test/java/com/apollohg/editor/EditorEditTextHardwareKeyTest.kt +0 -321
- package/android/src/test/java/com/apollohg/editor/EditorInputConnectionTest.kt +0 -4202
- package/android/src/test/java/com/apollohg/editor/NativeEditorExpoViewTest.kt +0 -2581
- package/android/src/test/java/com/apollohg/editor/NativeEditorModuleTest.kt +0 -27
- package/android/src/test/java/com/apollohg/editor/NativePerformanceTest.kt +0 -197
- package/android/src/test/java/com/apollohg/editor/NativeProseViewerExpoViewTest.kt +0 -84
- package/android/src/test/java/com/apollohg/editor/NativeToolbarTest.kt +0 -544
- package/android/src/test/java/com/apollohg/editor/PositionBridgeTest.kt +0 -461
- package/android/src/test/java/com/apollohg/editor/RenderBridgeTest.kt +0 -2054
- package/android/src/test/java/com/apollohg/editor/RichTextEditorViewTest.kt +0 -1367
- package/ios/EditorLayoutManager.swift +0 -774
- package/ios/NativeProseViewerExpoView.swift +0 -276
- package/ios/RenderBridge.swift +0 -1708
- package/ios/Tests/NativePerformanceTests.swift +0 -687
- package/ios/Tests/PositionBridgeTests.swift +0 -706
- package/ios/Tests/RenderBridgeTests.swift +0 -2236
- package/ios/Tests/RichTextEditorViewTests.swift +0 -6548
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import ExpoModulesCore
|
|
2
|
-
import UIKit
|
|
3
|
-
|
|
4
|
-
final class NativeProseViewerExpoView: ExpoView {
|
|
5
|
-
let onContentHeightChange = EventDispatcher()
|
|
6
|
-
let onPressLink = EventDispatcher()
|
|
7
|
-
let onPressMention = EventDispatcher()
|
|
8
|
-
|
|
9
|
-
private let textView = EditorTextView(frame: .zero, textContainer: nil)
|
|
10
|
-
private var lastRenderJSON: String?
|
|
11
|
-
private var lastThemeJSON: String?
|
|
12
|
-
private var lastEmittedContentHeight: CGFloat = 0
|
|
13
|
-
private var lastMeasuredWidth: CGFloat = 0
|
|
14
|
-
private var collapsesWhenEmpty = true
|
|
15
|
-
private var isCollapsedEmptyContent = false
|
|
16
|
-
private var enableLinkTaps = true
|
|
17
|
-
private var interceptLinkTaps = false
|
|
18
|
-
|
|
19
|
-
private lazy var interactiveTapRecognizer: UITapGestureRecognizer = {
|
|
20
|
-
let recognizer = UITapGestureRecognizer(
|
|
21
|
-
target: self,
|
|
22
|
-
action: #selector(handleInteractiveTap(_:))
|
|
23
|
-
)
|
|
24
|
-
recognizer.cancelsTouchesInView = false
|
|
25
|
-
return recognizer
|
|
26
|
-
}()
|
|
27
|
-
|
|
28
|
-
required init(appContext: AppContext? = nil) {
|
|
29
|
-
super.init(appContext: appContext)
|
|
30
|
-
setupView()
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private func setupView() {
|
|
34
|
-
textView.baseBackgroundColor = .clear
|
|
35
|
-
textView.backgroundColor = .clear
|
|
36
|
-
textView.isEditable = false
|
|
37
|
-
textView.isSelectable = false
|
|
38
|
-
textView.allowImageResizing = false
|
|
39
|
-
textView.heightBehavior = .autoGrow
|
|
40
|
-
textView.onHeightMayChange = { [weak self] measuredHeight in
|
|
41
|
-
self?.emitContentHeightIfNeeded(measuredHeight: measuredHeight, force: true)
|
|
42
|
-
}
|
|
43
|
-
textView.addGestureRecognizer(interactiveTapRecognizer)
|
|
44
|
-
addSubview(textView)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
func setEnableLinkTaps(_ enabled: Bool?) {
|
|
48
|
-
enableLinkTaps = enabled ?? true
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
func setInterceptLinkTaps(_ intercept: Bool?) {
|
|
52
|
-
interceptLinkTaps = intercept ?? false
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
func setCollapsesWhenEmpty(_ collapses: Bool?) {
|
|
56
|
-
let nextValue = collapses ?? true
|
|
57
|
-
guard collapsesWhenEmpty != nextValue else { return }
|
|
58
|
-
collapsesWhenEmpty = nextValue
|
|
59
|
-
updateCollapsedEmptyState()
|
|
60
|
-
setNeedsLayout()
|
|
61
|
-
emitContentHeightIfNeeded(force: true)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
func setRenderJson(_ renderJson: String?) {
|
|
65
|
-
guard lastRenderJSON != renderJson else { return }
|
|
66
|
-
lastRenderJSON = renderJson
|
|
67
|
-
applyRenderJSON()
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
func setThemeJson(_ themeJson: String?) {
|
|
71
|
-
guard lastThemeJSON != themeJson else { return }
|
|
72
|
-
lastThemeJSON = themeJson
|
|
73
|
-
let theme = EditorTheme.from(json: themeJson)
|
|
74
|
-
textView.applyTheme(theme)
|
|
75
|
-
let cornerRadius = theme?.borderRadius ?? 0
|
|
76
|
-
layer.cornerRadius = cornerRadius
|
|
77
|
-
clipsToBounds = cornerRadius > 0
|
|
78
|
-
applyRenderJSON()
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
override var intrinsicContentSize: CGSize {
|
|
82
|
-
if isCollapsedEmptyContent {
|
|
83
|
-
return CGSize(width: UIView.noIntrinsicMetric, height: 0)
|
|
84
|
-
}
|
|
85
|
-
guard lastEmittedContentHeight > 0 else {
|
|
86
|
-
return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
|
87
|
-
}
|
|
88
|
-
return CGSize(width: UIView.noIntrinsicMetric, height: lastEmittedContentHeight)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
override func layoutSubviews() {
|
|
92
|
-
super.layoutSubviews()
|
|
93
|
-
if isCollapsedEmptyContent {
|
|
94
|
-
textView.frame = CGRect(x: 0, y: 0, width: bounds.width, height: 0)
|
|
95
|
-
textView.updateAutoGrowHostHeight(0)
|
|
96
|
-
} else {
|
|
97
|
-
textView.frame = bounds
|
|
98
|
-
textView.updateAutoGrowHostHeight(bounds.height)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
let currentWidth = ceil(bounds.width)
|
|
102
|
-
guard abs(currentWidth - lastMeasuredWidth) > 0.5 else { return }
|
|
103
|
-
lastMeasuredWidth = currentWidth
|
|
104
|
-
emitContentHeightIfNeeded(force: true)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
private func applyRenderJSON() {
|
|
108
|
-
updateCollapsedEmptyState()
|
|
109
|
-
textView.applyRenderJSON(lastRenderJSON ?? "[]")
|
|
110
|
-
textView.isHidden = isCollapsedEmptyContent
|
|
111
|
-
lastMeasuredWidth = 0
|
|
112
|
-
invalidateIntrinsicContentSize()
|
|
113
|
-
setNeedsLayout()
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
private func updateCollapsedEmptyState() {
|
|
117
|
-
isCollapsedEmptyContent = collapsesWhenEmpty
|
|
118
|
-
&& Self.renderJsonContainsOnlyEmptyParagraphs(lastRenderJSON ?? "[]")
|
|
119
|
-
textView.isHidden = isCollapsedEmptyContent
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private func emitContentHeightIfNeeded(
|
|
123
|
-
measuredHeight: CGFloat? = nil,
|
|
124
|
-
force: Bool = false
|
|
125
|
-
) {
|
|
126
|
-
let contentHeight: CGFloat
|
|
127
|
-
if isCollapsedEmptyContent {
|
|
128
|
-
contentHeight = 0
|
|
129
|
-
} else {
|
|
130
|
-
guard bounds.width > 0 else { return }
|
|
131
|
-
let fittedHeight = measuredHeight
|
|
132
|
-
?? textView.measuredAutoGrowHeightForTesting(width: bounds.width)
|
|
133
|
-
contentHeight = ceil(fittedHeight)
|
|
134
|
-
guard contentHeight > 0 else { return }
|
|
135
|
-
}
|
|
136
|
-
guard force || abs(contentHeight - lastEmittedContentHeight) > 0.5 else { return }
|
|
137
|
-
lastEmittedContentHeight = contentHeight
|
|
138
|
-
invalidateIntrinsicContentSize()
|
|
139
|
-
onContentHeightChange(["contentHeight": contentHeight])
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
@objc private func handleInteractiveTap(_ recognizer: UITapGestureRecognizer) {
|
|
143
|
-
guard recognizer.state == .ended else {
|
|
144
|
-
return
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
let location = recognizer.location(in: textView)
|
|
148
|
-
if enableLinkTaps, let link = linkHit(at: location) {
|
|
149
|
-
if interceptLinkTaps {
|
|
150
|
-
onPressLink([
|
|
151
|
-
"href": link.href,
|
|
152
|
-
"text": link.text,
|
|
153
|
-
])
|
|
154
|
-
} else {
|
|
155
|
-
openLink(link.href)
|
|
156
|
-
}
|
|
157
|
-
return
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
guard let mention = mentionHit(at: location) else { return }
|
|
161
|
-
onPressMention([
|
|
162
|
-
"docPos": mention.docPos,
|
|
163
|
-
"label": mention.label,
|
|
164
|
-
])
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
private func characterIndex(at location: CGPoint) -> Int? {
|
|
168
|
-
let textStorage = textView.textStorage
|
|
169
|
-
guard textStorage.length > 0 else { return nil }
|
|
170
|
-
|
|
171
|
-
let layoutManager = textView.layoutManager
|
|
172
|
-
let textContainer = textView.textContainer
|
|
173
|
-
var containerPoint = location
|
|
174
|
-
containerPoint.x -= textView.textContainerInset.left
|
|
175
|
-
containerPoint.y -= textView.textContainerInset.top
|
|
176
|
-
|
|
177
|
-
let usedRect = layoutManager.usedRect(for: textContainer)
|
|
178
|
-
guard usedRect.insetBy(dx: -6, dy: -6).contains(containerPoint) else {
|
|
179
|
-
return nil
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
let glyphIndex = layoutManager.glyphIndex(for: containerPoint, in: textContainer)
|
|
183
|
-
guard glyphIndex < layoutManager.numberOfGlyphs else { return nil }
|
|
184
|
-
let characterIndex = layoutManager.characterIndexForGlyph(at: glyphIndex)
|
|
185
|
-
guard characterIndex < textStorage.length else { return nil }
|
|
186
|
-
return characterIndex
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
private func linkHit(at location: CGPoint) -> (href: String, text: String)? {
|
|
190
|
-
let textStorage = textView.textStorage
|
|
191
|
-
guard let characterIndex = characterIndex(at: location) else { return nil }
|
|
192
|
-
|
|
193
|
-
var effectiveRange = NSRange(location: 0, length: 0)
|
|
194
|
-
let attrs = textStorage.attributes(at: characterIndex, effectiveRange: &effectiveRange)
|
|
195
|
-
guard let href = attrs[RenderBridgeAttributes.linkHref] as? String, !href.isEmpty else {
|
|
196
|
-
return nil
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
let text = (textStorage.string as NSString).substring(with: effectiveRange)
|
|
200
|
-
return (href: href, text: text)
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
private func mentionHit(at location: CGPoint) -> (docPos: Int, label: String)? {
|
|
204
|
-
let textStorage = textView.textStorage
|
|
205
|
-
guard let characterIndex = characterIndex(at: location) else { return nil }
|
|
206
|
-
|
|
207
|
-
var effectiveRange = NSRange(location: 0, length: 0)
|
|
208
|
-
let attrs = textStorage.attributes(at: characterIndex, effectiveRange: &effectiveRange)
|
|
209
|
-
guard (attrs[RenderBridgeAttributes.voidNodeType] as? String) == "mention" else {
|
|
210
|
-
return nil
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
let docPos =
|
|
214
|
-
(attrs[RenderBridgeAttributes.docPos] as? NSNumber)?.intValue
|
|
215
|
-
?? Int((attrs[RenderBridgeAttributes.docPos] as? UInt32) ?? 0)
|
|
216
|
-
let label = (textStorage.string as NSString).substring(with: effectiveRange)
|
|
217
|
-
return (docPos: docPos, label: label)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
private func openLink(_ href: String) {
|
|
221
|
-
guard let url = URL(string: href) else { return }
|
|
222
|
-
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
static func renderJsonContainsOnlyEmptyParagraphs(_ renderJson: String) -> Bool {
|
|
226
|
-
guard let data = renderJson.data(using: .utf8),
|
|
227
|
-
let elements = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]]
|
|
228
|
-
else {
|
|
229
|
-
return false
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if elements.isEmpty {
|
|
233
|
-
return true
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
var hasParagraph = false
|
|
237
|
-
var paragraphIsOpen = false
|
|
238
|
-
|
|
239
|
-
for element in elements {
|
|
240
|
-
guard let type = element["type"] as? String else {
|
|
241
|
-
return false
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
switch type {
|
|
245
|
-
case "blockStart":
|
|
246
|
-
guard !paragraphIsOpen,
|
|
247
|
-
element["nodeType"] as? String == "paragraph",
|
|
248
|
-
(element["depth"] as? NSNumber)?.intValue == 0
|
|
249
|
-
else {
|
|
250
|
-
return false
|
|
251
|
-
}
|
|
252
|
-
paragraphIsOpen = true
|
|
253
|
-
hasParagraph = true
|
|
254
|
-
|
|
255
|
-
case "textRun":
|
|
256
|
-
guard paragraphIsOpen,
|
|
257
|
-
let text = element["text"] as? String,
|
|
258
|
-
text.allSatisfy({ $0 == "\u{200B}" })
|
|
259
|
-
else {
|
|
260
|
-
return false
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
case "blockEnd":
|
|
264
|
-
guard paragraphIsOpen else {
|
|
265
|
-
return false
|
|
266
|
-
}
|
|
267
|
-
paragraphIsOpen = false
|
|
268
|
-
|
|
269
|
-
default:
|
|
270
|
-
return false
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return hasParagraph && !paragraphIsOpen
|
|
275
|
-
}
|
|
276
|
-
}
|