@apollohg/react-native-prose-editor 0.1.1 → 0.3.0
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/README.md +12 -7
- package/android/build.gradle +7 -2
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +289 -2
- package/android/src/main/java/com/apollohg/editor/EditorTheme.kt +51 -1
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +199 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +16 -3
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +82 -1
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +403 -45
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +246 -0
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +841 -155
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +125 -8
- package/{src/EditorTheme.ts → dist/EditorTheme.d.ts} +12 -52
- package/dist/EditorTheme.js +29 -0
- package/dist/EditorToolbar.d.ts +129 -0
- package/dist/EditorToolbar.js +394 -0
- package/dist/NativeEditorBridge.d.ts +242 -0
- package/dist/NativeEditorBridge.js +647 -0
- package/dist/NativeRichTextEditor.d.ts +142 -0
- package/dist/NativeRichTextEditor.js +649 -0
- package/dist/YjsCollaboration.d.ts +83 -0
- package/dist/YjsCollaboration.js +585 -0
- package/dist/addons.d.ts +70 -0
- package/dist/addons.js +77 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +26 -0
- package/dist/schemas.d.ts +35 -0
- package/{src/schemas.ts → dist/schemas.js} +62 -27
- package/dist/useNativeEditor.d.ts +40 -0
- package/dist/useNativeEditor.js +117 -0
- package/ios/EditorAddons.swift +26 -3
- package/ios/EditorCore.xcframework/Info.plist +5 -5
- 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/EditorLayoutManager.swift +236 -0
- package/ios/EditorTheme.swift +51 -1
- package/ios/Generated_editor_core.swift +270 -2
- package/ios/NativeEditorExpoView.swift +612 -45
- package/ios/NativeEditorModule.swift +81 -0
- package/ios/PositionBridge.swift +22 -0
- package/ios/RenderBridge.swift +427 -39
- package/ios/RichTextEditorView.swift +1342 -18
- package/ios/editor_coreFFI/editor_coreFFI.h +209 -0
- package/package.json +80 -64
- 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 +404 -4
- package/src/EditorToolbar.tsx +0 -620
- package/src/NativeEditorBridge.ts +0 -607
- package/src/NativeRichTextEditor.tsx +0 -951
- package/src/addons.ts +0 -158
- package/src/index.ts +0 -63
- package/src/useNativeEditor.ts +0 -173
|
@@ -5,6 +5,54 @@ import CoreText
|
|
|
5
5
|
/// editable text storage. This keeps UIKit paragraph-start behaviors, such as
|
|
6
6
|
/// sentence auto-capitalization, working naturally inside list items.
|
|
7
7
|
final class EditorLayoutManager: NSLayoutManager {
|
|
8
|
+
private(set) var blockquoteStripeDrawPassesForTesting: [[CGRect]] = []
|
|
9
|
+
|
|
10
|
+
func blockquoteStripeRectsForTesting(
|
|
11
|
+
in textStorage: NSTextStorage,
|
|
12
|
+
visibleGlyphRange: NSRange? = nil,
|
|
13
|
+
origin: CGPoint = .zero
|
|
14
|
+
) -> [CGRect] {
|
|
15
|
+
let glyphsToShow = visibleGlyphRange ?? NSRange(location: 0, length: numberOfGlyphs)
|
|
16
|
+
guard glyphsToShow.length > 0 else { return [] }
|
|
17
|
+
|
|
18
|
+
let characterRange = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
|
|
19
|
+
let nsString = textStorage.string as NSString
|
|
20
|
+
var drawnBlockquoteStarts = Set<Int>()
|
|
21
|
+
var rects: [CGRect] = []
|
|
22
|
+
|
|
23
|
+
textStorage.enumerateAttribute(
|
|
24
|
+
RenderBridgeAttributes.blockquoteBorderColor,
|
|
25
|
+
in: characterRange,
|
|
26
|
+
options: []
|
|
27
|
+
) { value, range, _ in
|
|
28
|
+
guard range.length > 0, let color = value as? UIColor else { return }
|
|
29
|
+
|
|
30
|
+
let paragraphRange = nsString.paragraphRange(for: NSRange(location: range.location, length: 0))
|
|
31
|
+
let paragraphStart = paragraphRange.location
|
|
32
|
+
let groupRange = Self.blockquoteGroupCharacterRange(
|
|
33
|
+
containing: paragraphStart,
|
|
34
|
+
in: textStorage,
|
|
35
|
+
nsString: nsString
|
|
36
|
+
)
|
|
37
|
+
let groupStart = groupRange.location
|
|
38
|
+
guard drawnBlockquoteStarts.insert(groupStart).inserted else { return }
|
|
39
|
+
guard let rect = blockquoteStripeRect(
|
|
40
|
+
characterRange: groupRange,
|
|
41
|
+
color: color,
|
|
42
|
+
textStorage: textStorage,
|
|
43
|
+
origin: origin
|
|
44
|
+
) else {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
rects.append(rect)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return rects
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func resetBlockquoteStripeDrawPassesForTesting() {
|
|
54
|
+
blockquoteStripeDrawPassesForTesting.removeAll()
|
|
55
|
+
}
|
|
8
56
|
|
|
9
57
|
override func drawGlyphs(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
|
|
10
58
|
super.drawGlyphs(forGlyphRange: glyphsToShow, at: origin)
|
|
@@ -14,6 +62,8 @@ final class EditorLayoutManager: NSLayoutManager {
|
|
|
14
62
|
let characterRange = characterRange(forGlyphRange: glyphsToShow, actualGlyphRange: nil)
|
|
15
63
|
let nsString = textStorage.string as NSString
|
|
16
64
|
var drawnParagraphStarts = Set<Int>()
|
|
65
|
+
var drawnBlockquoteStarts = Set<Int>()
|
|
66
|
+
var drawnStripeRects: [CGRect] = []
|
|
17
67
|
|
|
18
68
|
textStorage.enumerateAttribute(
|
|
19
69
|
RenderBridgeAttributes.listMarkerContext,
|
|
@@ -36,6 +86,42 @@ final class EditorLayoutManager: NSLayoutManager {
|
|
|
36
86
|
textStorage: textStorage
|
|
37
87
|
)
|
|
38
88
|
}
|
|
89
|
+
|
|
90
|
+
textStorage.enumerateAttribute(
|
|
91
|
+
RenderBridgeAttributes.blockquoteBorderColor,
|
|
92
|
+
in: characterRange,
|
|
93
|
+
options: []
|
|
94
|
+
) { value, range, _ in
|
|
95
|
+
guard range.length > 0, let color = value as? UIColor else { return }
|
|
96
|
+
|
|
97
|
+
let paragraphRange = nsString.paragraphRange(for: NSRange(location: range.location, length: 0))
|
|
98
|
+
let paragraphStart = paragraphRange.location
|
|
99
|
+
let groupRange = Self.blockquoteGroupCharacterRange(
|
|
100
|
+
containing: paragraphStart,
|
|
101
|
+
in: textStorage,
|
|
102
|
+
nsString: nsString
|
|
103
|
+
)
|
|
104
|
+
let groupStart = groupRange.location
|
|
105
|
+
guard drawnBlockquoteStarts.insert(groupStart).inserted else { return }
|
|
106
|
+
|
|
107
|
+
guard let stripeRect = self.blockquoteStripeRect(
|
|
108
|
+
characterRange: groupRange,
|
|
109
|
+
color: color,
|
|
110
|
+
textStorage: textStorage,
|
|
111
|
+
origin: origin
|
|
112
|
+
) else {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
self.drawBlockquoteBorder(
|
|
116
|
+
stripeRect: stripeRect,
|
|
117
|
+
color: color
|
|
118
|
+
)
|
|
119
|
+
drawnStripeRects.append(stripeRect)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if !drawnStripeRects.isEmpty {
|
|
123
|
+
blockquoteStripeDrawPassesForTesting.append(drawnStripeRects)
|
|
124
|
+
}
|
|
39
125
|
}
|
|
40
126
|
|
|
41
127
|
private func drawListMarker(
|
|
@@ -107,6 +193,156 @@ final class EditorLayoutManager: NSLayoutManager {
|
|
|
107
193
|
path.fill()
|
|
108
194
|
}
|
|
109
195
|
|
|
196
|
+
private func blockquoteStripeRect(
|
|
197
|
+
characterRange: NSRange,
|
|
198
|
+
color: UIColor,
|
|
199
|
+
textStorage: NSTextStorage,
|
|
200
|
+
origin: CGPoint
|
|
201
|
+
) -> CGRect? {
|
|
202
|
+
guard characterRange.location < textStorage.length, !textContainers.isEmpty else {
|
|
203
|
+
return nil
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
ensureLayout(forCharacterRange: characterRange)
|
|
207
|
+
let glyphRange = self.glyphRange(forCharacterRange: characterRange, actualCharacterRange: nil)
|
|
208
|
+
guard glyphRange.length > 0 else { return nil }
|
|
209
|
+
|
|
210
|
+
var topEdge: CGFloat?
|
|
211
|
+
var bottomEdge: CGFloat?
|
|
212
|
+
var textLeadingEdge: CGFloat?
|
|
213
|
+
enumerateLineFragments(forGlyphRange: glyphRange) { lineFragmentRect, usedRect, _, _, _ in
|
|
214
|
+
let verticalReferenceRect = usedRect.height > 0 ? usedRect : lineFragmentRect
|
|
215
|
+
if let currentTop = topEdge {
|
|
216
|
+
topEdge = min(currentTop, lineFragmentRect.minY)
|
|
217
|
+
} else {
|
|
218
|
+
topEdge = lineFragmentRect.minY
|
|
219
|
+
}
|
|
220
|
+
if let currentBottom = bottomEdge {
|
|
221
|
+
bottomEdge = max(currentBottom, verticalReferenceRect.maxY)
|
|
222
|
+
} else {
|
|
223
|
+
bottomEdge = verticalReferenceRect.maxY
|
|
224
|
+
}
|
|
225
|
+
let referenceMinX = usedRect.width > 0 ? usedRect.minX : lineFragmentRect.minX
|
|
226
|
+
if let current = textLeadingEdge {
|
|
227
|
+
textLeadingEdge = min(current, referenceMinX)
|
|
228
|
+
} else {
|
|
229
|
+
textLeadingEdge = referenceMinX
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
guard let topEdge, let bottomEdge, bottomEdge > topEdge, let textLeadingEdge else { return nil }
|
|
233
|
+
|
|
234
|
+
let attrs = textStorage.attributes(at: characterRange.location, effectiveRange: nil)
|
|
235
|
+
let borderWidth = (attrs[RenderBridgeAttributes.blockquoteBorderWidth] as? NSNumber)
|
|
236
|
+
.map { CGFloat(truncating: $0) }
|
|
237
|
+
?? LayoutConstants.blockquoteBorderWidth
|
|
238
|
+
let gap = (attrs[RenderBridgeAttributes.blockquoteMarkerGap] as? NSNumber)
|
|
239
|
+
.map { CGFloat(truncating: $0) }
|
|
240
|
+
?? LayoutConstants.blockquoteMarkerGap
|
|
241
|
+
|
|
242
|
+
let stripeX = origin.x + textLeadingEdge - gap - borderWidth
|
|
243
|
+
let stripeRect = CGRect(
|
|
244
|
+
x: stripeX,
|
|
245
|
+
y: origin.y + topEdge,
|
|
246
|
+
width: borderWidth,
|
|
247
|
+
height: bottomEdge - topEdge
|
|
248
|
+
)
|
|
249
|
+
return stripeRect
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private func drawBlockquoteBorder(
|
|
253
|
+
stripeRect: CGRect,
|
|
254
|
+
color: UIColor
|
|
255
|
+
) {
|
|
256
|
+
color.setFill()
|
|
257
|
+
UIBezierPath(rect: stripeRect).fill()
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
private static func blockquoteGroupCharacterRange(
|
|
261
|
+
containing paragraphStart: Int,
|
|
262
|
+
in textStorage: NSTextStorage,
|
|
263
|
+
nsString: NSString
|
|
264
|
+
) -> NSRange {
|
|
265
|
+
let initialParagraphRange = nsString.paragraphRange(
|
|
266
|
+
for: NSRange(location: paragraphStart, length: 0)
|
|
267
|
+
)
|
|
268
|
+
var groupStart = initialParagraphRange.location
|
|
269
|
+
var groupEnd = NSMaxRange(initialParagraphRange)
|
|
270
|
+
|
|
271
|
+
var probeStart = groupStart
|
|
272
|
+
while probeStart > 0 {
|
|
273
|
+
let previousParagraphRange = nsString.paragraphRange(
|
|
274
|
+
for: NSRange(location: probeStart - 1, length: 0)
|
|
275
|
+
)
|
|
276
|
+
let previousStart = previousParagraphRange.location
|
|
277
|
+
guard paragraphHasBlockquoteBorder(
|
|
278
|
+
previousParagraphRange,
|
|
279
|
+
in: textStorage
|
|
280
|
+
)
|
|
281
|
+
else {
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
groupStart = previousStart
|
|
286
|
+
probeStart = previousStart
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
var nextParagraphLocation = groupEnd
|
|
290
|
+
while nextParagraphLocation < textStorage.length {
|
|
291
|
+
let nextParagraphRange = nsString.paragraphRange(
|
|
292
|
+
for: NSRange(location: nextParagraphLocation, length: 0)
|
|
293
|
+
)
|
|
294
|
+
guard paragraphHasBlockquoteBorder(
|
|
295
|
+
nextParagraphRange,
|
|
296
|
+
in: textStorage
|
|
297
|
+
)
|
|
298
|
+
else {
|
|
299
|
+
break
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
groupEnd = NSMaxRange(nextParagraphRange)
|
|
303
|
+
nextParagraphLocation = groupEnd
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return NSRange(location: groupStart, length: groupEnd - groupStart)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private static func paragraphHasBlockquoteBorder(
|
|
310
|
+
_ paragraphRange: NSRange,
|
|
311
|
+
in textStorage: NSTextStorage
|
|
312
|
+
) -> Bool {
|
|
313
|
+
guard paragraphRange.length > 0 else { return false }
|
|
314
|
+
let nsString = textStorage.string as NSString
|
|
315
|
+
var sawQuotedContent = false
|
|
316
|
+
var sawAnyQuotedCharacter = false
|
|
317
|
+
|
|
318
|
+
for offset in 0..<paragraphRange.length {
|
|
319
|
+
let index = paragraphRange.location + offset
|
|
320
|
+
guard index < textStorage.length else { break }
|
|
321
|
+
|
|
322
|
+
let hasBorder = textStorage.attribute(
|
|
323
|
+
RenderBridgeAttributes.blockquoteBorderColor,
|
|
324
|
+
at: index,
|
|
325
|
+
effectiveRange: nil
|
|
326
|
+
) != nil
|
|
327
|
+
guard hasBorder else { continue }
|
|
328
|
+
sawAnyQuotedCharacter = true
|
|
329
|
+
|
|
330
|
+
let scalar = nsString.character(at: index)
|
|
331
|
+
if scalar != 0x000A, scalar != 0x000D {
|
|
332
|
+
sawQuotedContent = true
|
|
333
|
+
break
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if sawQuotedContent {
|
|
338
|
+
return true
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let trimmed = nsString.substring(with: paragraphRange)
|
|
342
|
+
.trimmingCharacters(in: .newlines)
|
|
343
|
+
return trimmed.isEmpty && sawAnyQuotedCharacter
|
|
344
|
+
}
|
|
345
|
+
|
|
110
346
|
static func markerParagraphStyle(from attrs: [NSAttributedString.Key: Any]) -> NSMutableParagraphStyle {
|
|
111
347
|
let markerStyle = NSMutableParagraphStyle()
|
|
112
348
|
let sourceStyle = attrs[.paragraphStyle] as? NSParagraphStyle
|
package/ios/EditorTheme.swift
CHANGED
|
@@ -104,6 +104,24 @@ struct EditorHorizontalRuleTheme {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
struct EditorBlockquoteTheme {
|
|
108
|
+
var text: EditorTextStyle?
|
|
109
|
+
var indent: CGFloat?
|
|
110
|
+
var borderColor: UIColor?
|
|
111
|
+
var borderWidth: CGFloat?
|
|
112
|
+
var markerGap: CGFloat?
|
|
113
|
+
|
|
114
|
+
init(dictionary: [String: Any]) {
|
|
115
|
+
if let text = dictionary["text"] as? [String: Any] {
|
|
116
|
+
self.text = EditorTextStyle(dictionary: text)
|
|
117
|
+
}
|
|
118
|
+
indent = EditorTheme.cgFloat(dictionary["indent"])
|
|
119
|
+
borderColor = EditorTheme.color(from: dictionary["borderColor"])
|
|
120
|
+
borderWidth = EditorTheme.cgFloat(dictionary["borderWidth"])
|
|
121
|
+
markerGap = EditorTheme.cgFloat(dictionary["markerGap"])
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
107
125
|
struct EditorMentionTheme {
|
|
108
126
|
var textColor: UIColor?
|
|
109
127
|
var backgroundColor: UIColor?
|
|
@@ -140,7 +158,13 @@ struct EditorMentionTheme {
|
|
|
140
158
|
}
|
|
141
159
|
}
|
|
142
160
|
|
|
161
|
+
enum EditorToolbarAppearance: String {
|
|
162
|
+
case custom
|
|
163
|
+
case native
|
|
164
|
+
}
|
|
165
|
+
|
|
143
166
|
struct EditorToolbarTheme {
|
|
167
|
+
var appearance: EditorToolbarAppearance?
|
|
144
168
|
var backgroundColor: UIColor?
|
|
145
169
|
var borderColor: UIColor?
|
|
146
170
|
var borderWidth: CGFloat?
|
|
@@ -155,6 +179,7 @@ struct EditorToolbarTheme {
|
|
|
155
179
|
var buttonBorderRadius: CGFloat?
|
|
156
180
|
|
|
157
181
|
init(dictionary: [String: Any]) {
|
|
182
|
+
appearance = (dictionary["appearance"] as? String).flatMap(EditorToolbarAppearance.init(rawValue:))
|
|
158
183
|
backgroundColor = EditorTheme.color(from: dictionary["backgroundColor"])
|
|
159
184
|
borderColor = EditorTheme.color(from: dictionary["borderColor"])
|
|
160
185
|
borderWidth = EditorTheme.cgFloat(dictionary["borderWidth"])
|
|
@@ -168,6 +193,26 @@ struct EditorToolbarTheme {
|
|
|
168
193
|
buttonActiveBackgroundColor = EditorTheme.color(from: dictionary["buttonActiveBackgroundColor"])
|
|
169
194
|
buttonBorderRadius = EditorTheme.cgFloat(dictionary["buttonBorderRadius"])
|
|
170
195
|
}
|
|
196
|
+
|
|
197
|
+
var resolvedKeyboardOffset: CGFloat {
|
|
198
|
+
keyboardOffset ?? (appearance == .native ? 6 : 0)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
var resolvedHorizontalInset: CGFloat {
|
|
202
|
+
horizontalInset ?? (appearance == .native ? 10 : 0)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
var resolvedBorderRadius: CGFloat {
|
|
206
|
+
borderRadius ?? (appearance == .native ? 20 : 0)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
var resolvedBorderWidth: CGFloat {
|
|
210
|
+
borderWidth ?? (appearance == .native ? 0 : 0.5)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
var resolvedButtonBorderRadius: CGFloat {
|
|
214
|
+
buttonBorderRadius ?? (appearance == .native ? 10 : 8)
|
|
215
|
+
}
|
|
171
216
|
}
|
|
172
217
|
|
|
173
218
|
struct EditorContentInsets {
|
|
@@ -187,6 +232,7 @@ struct EditorContentInsets {
|
|
|
187
232
|
struct EditorTheme {
|
|
188
233
|
var text: EditorTextStyle?
|
|
189
234
|
var paragraph: EditorTextStyle?
|
|
235
|
+
var blockquote: EditorBlockquoteTheme?
|
|
190
236
|
var headings: [String: EditorTextStyle] = [:]
|
|
191
237
|
var list: EditorListTheme?
|
|
192
238
|
var horizontalRule: EditorHorizontalRuleTheme?
|
|
@@ -213,6 +259,9 @@ struct EditorTheme {
|
|
|
213
259
|
if let paragraph = dictionary["paragraph"] as? [String: Any] {
|
|
214
260
|
self.paragraph = EditorTextStyle(dictionary: paragraph)
|
|
215
261
|
}
|
|
262
|
+
if let blockquote = dictionary["blockquote"] as? [String: Any] {
|
|
263
|
+
self.blockquote = EditorBlockquoteTheme(dictionary: blockquote)
|
|
264
|
+
}
|
|
216
265
|
if let headings = dictionary["headings"] as? [String: Any] {
|
|
217
266
|
for level in ["h1", "h2", "h3", "h4", "h5", "h6"] {
|
|
218
267
|
if let style = headings[level] as? [String: Any] {
|
|
@@ -239,8 +288,9 @@ struct EditorTheme {
|
|
|
239
288
|
}
|
|
240
289
|
}
|
|
241
290
|
|
|
242
|
-
func effectiveTextStyle(for nodeType: String) -> EditorTextStyle {
|
|
291
|
+
func effectiveTextStyle(for nodeType: String, inBlockquote: Bool = false) -> EditorTextStyle {
|
|
243
292
|
var style = text ?? EditorTextStyle()
|
|
293
|
+
style = style.merged(with: inBlockquote ? blockquote?.text : nil)
|
|
244
294
|
if nodeType == "paragraph" {
|
|
245
295
|
style = style.merged(with: paragraph)
|
|
246
296
|
if paragraph?.lineHeight == nil {
|