@apollohg/react-native-rich-text-editor 2.0.1 → 2.0.3
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 +29 -123
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +20 -6
- package/android/src/main/java/com/apollohg/editor/EditorEditTextClipboard.kt +181 -36
- package/android/src/main/java/com/apollohg/editor/EditorEditTextImages.kt +25 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditTextSelection.kt +7 -0
- package/android/src/main/java/com/apollohg/editor/EditorTextSurfaceInteraction.kt +31 -11
- package/android/src/main/java/com/apollohg/editor/EditorV2Adapter.kt +36 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2Backend.kt +1 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2Driver.kt +10 -0
- package/android/src/main/java/com/apollohg/editor/EditorV2RenderValidation.kt +1 -0
- package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +101 -33
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +2 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoViewProperties.kt +4 -0
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +3 -0
- package/android/src/main/java/com/apollohg/editor/RenderMediaSpans.kt +15 -3
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +14 -0
- package/android/src/main/java/com/apollohg/editor/UniffiEditorV2Backend.kt +4 -0
- package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +1 -1
- package/dist/EditorStyleSheetNormalization.js +1 -1
- package/dist/EditorStyleSheetTypes.d.ts +1 -1
- package/dist/NativeRichTextEditor.d.ts +1 -1
- package/dist/RichTextEditorNativeTypes.d.ts +2 -1
- package/dist/RichTextEditorTypes.d.ts +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/useRichTextEditorPresentation.d.ts +1 -1
- package/dist/useRichTextEditorPresentation.js +2 -2
- package/dist/useRichTextEditorState.d.ts +5 -3
- package/dist/useRichTextEditorState.js +4 -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/EditorInteractionOverlays.swift +114 -122
- package/ios/EditorLayoutManager.swift +13 -3
- package/ios/EditorStyleSheet.swift +12 -1
- package/ios/EditorTextView+Images.swift +32 -10
- package/ios/EditorTextView+KeyboardInput.swift +299 -0
- package/ios/EditorTextView.swift +64 -44
- package/ios/EditorV2Adapter+Commands.swift +22 -0
- package/ios/EditorV2Adapter+RenderValidation.swift +2 -2
- package/ios/EditorV2Shadow.swift +12 -0
- package/ios/Generated_editor_core.swift +10 -0
- package/ios/NativeEditorExpoView+Properties.swift +4 -0
- package/ios/NativeEditorModule.swift +3 -0
- package/ios/NativeToolbar+ButtonStyle.swift +17 -5
- package/ios/NativeToolbar+Diagnostics.swift +15 -0
- package/ios/RenderBridge+Atoms.swift +0 -1
- package/ios/RenderBridge+Attachments.swift +0 -29
- package/ios/RichTextEditorView.swift +4 -12
- package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +3 -2
- package/ios/editor_coreFFI/editor_coreFFI.h +11 -0
- 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/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 +18 -0
- package/src/EditorStyleSheetNormalization.ts +1 -1
- package/src/EditorStyleSheetTypes.ts +1 -1
- package/src/NativeRichTextEditor.tsx +1 -0
- package/src/RichTextEditorNativeTypes.tsx +2 -0
- package/src/RichTextEditorTypes.tsx +5 -0
- package/src/index.ts +1 -0
- package/src/useRichTextEditorPresentation.tsx +3 -0
- package/src/useRichTextEditorState.tsx +5 -2
|
@@ -1,7 +1,306 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import UIKit
|
|
3
3
|
|
|
4
|
+
enum EditorPasteMode: String {
|
|
5
|
+
case rich
|
|
6
|
+
case plainText
|
|
7
|
+
case disabled
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
struct EditorClipboardPayload {
|
|
11
|
+
static let fragmentType = "com.apollohg.native-editor.fragment"
|
|
12
|
+
|
|
13
|
+
let fragment: String
|
|
14
|
+
let html: String
|
|
15
|
+
let text: String
|
|
16
|
+
|
|
17
|
+
init(fragment: String, html: String, text: String) {
|
|
18
|
+
self.fragment = fragment
|
|
19
|
+
self.html = html
|
|
20
|
+
self.text = text
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
init?(json: String) {
|
|
24
|
+
guard let data = json.data(using: .utf8),
|
|
25
|
+
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
26
|
+
object["empty"] as? Bool != true,
|
|
27
|
+
let fragment = object["fragment"] as? String,
|
|
28
|
+
let html = object["html"] as? String,
|
|
29
|
+
let text = object["text"] as? String
|
|
30
|
+
else { return nil }
|
|
31
|
+
self.init(fragment: fragment, html: html, text: text)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
@discardableResult
|
|
35
|
+
func write(to pasteboard: UIPasteboard) -> Bool {
|
|
36
|
+
pasteboard.items = [[
|
|
37
|
+
Self.fragmentType: Data(fragment.utf8),
|
|
38
|
+
"public.html": Data(html.utf8),
|
|
39
|
+
"public.utf8-plain-text": text
|
|
40
|
+
]]
|
|
41
|
+
return pasteboard.data(forPasteboardType: Self.fragmentType) != nil
|
|
42
|
+
&& pasteboard.data(forPasteboardType: "public.html") != nil
|
|
43
|
+
&& pasteboard.string != nil
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
enum EditorClipboardPaste {
|
|
48
|
+
static let maximumRTFBytes = 64 * 1_024 * 1_024
|
|
49
|
+
|
|
50
|
+
static let supportedTypes = [
|
|
51
|
+
EditorClipboardPayload.fragmentType,
|
|
52
|
+
"public.html",
|
|
53
|
+
"public.rtf",
|
|
54
|
+
"public.utf8-plain-text",
|
|
55
|
+
"public.plain-text",
|
|
56
|
+
"public.text"
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
static func hasSupportedContent(in pasteboard: UIPasteboard) -> Bool {
|
|
60
|
+
pasteboard.contains(pasteboardTypes: supportedTypes)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
static func command(
|
|
64
|
+
from pasteboard: UIPasteboard,
|
|
65
|
+
mode: EditorPasteMode,
|
|
66
|
+
maximumRTFBytes: Int = EditorClipboardPaste.maximumRTFBytes
|
|
67
|
+
) -> [String: Any]? {
|
|
68
|
+
guard mode != .disabled else { return nil }
|
|
69
|
+
var command: [String: Any] = ["type": "paste"]
|
|
70
|
+
if let fragment = utf8String(pasteboard.data(forPasteboardType: EditorClipboardPayload.fragmentType)) {
|
|
71
|
+
command["fragment"] = fragment
|
|
72
|
+
}
|
|
73
|
+
var html = utf8String(pasteboard.data(forPasteboardType: "public.html"))
|
|
74
|
+
var text = pasteboard.string
|
|
75
|
+
if html == nil,
|
|
76
|
+
let rtf = pasteboard.data(forPasteboardType: "public.rtf"),
|
|
77
|
+
rtf.count <= maximumRTFBytes,
|
|
78
|
+
let attributed = try? NSAttributedString(
|
|
79
|
+
data: rtf,
|
|
80
|
+
options: [.documentType: NSAttributedString.DocumentType.rtf],
|
|
81
|
+
documentAttributes: nil
|
|
82
|
+
) {
|
|
83
|
+
let convertedHTML = semanticHTML(from: attributed)
|
|
84
|
+
if !convertedHTML.isEmpty {
|
|
85
|
+
html = convertedHTML
|
|
86
|
+
}
|
|
87
|
+
if text == nil {
|
|
88
|
+
text = attributed.string
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
let hasStructuredContent = command["fragment"] != nil || html != nil
|
|
92
|
+
guard hasStructuredContent || !(text?.isEmpty ?? true) else { return nil }
|
|
93
|
+
if let html {
|
|
94
|
+
command["html"] = html
|
|
95
|
+
}
|
|
96
|
+
if let text {
|
|
97
|
+
command["text"] = text
|
|
98
|
+
}
|
|
99
|
+
if mode == .plainText {
|
|
100
|
+
command["plainText"] = true
|
|
101
|
+
}
|
|
102
|
+
return command.count > 1 ? command : nil
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private static func utf8String(_ data: Data?) -> String? {
|
|
106
|
+
guard let data else { return nil }
|
|
107
|
+
return String(data: data, encoding: .utf8)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private static func semanticHTML(from attributed: NSAttributedString) -> String {
|
|
111
|
+
let range = NSRange(location: 0, length: attributed.length)
|
|
112
|
+
var hasList = false
|
|
113
|
+
attributed.enumerateAttribute(.paragraphStyle, in: range) { value, _, stop in
|
|
114
|
+
if let style = value as? NSParagraphStyle, !style.textLists.isEmpty {
|
|
115
|
+
hasList = true
|
|
116
|
+
stop.pointee = true
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
guard attributed.string.contains("\n") || attributed.string.contains("\r") || hasList else {
|
|
120
|
+
return semanticInlineHTML(from: attributed, range: range)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let string = attributed.string as NSString
|
|
124
|
+
var paragraphs: [(html: String, listTags: [String])] = []
|
|
125
|
+
var location = 0
|
|
126
|
+
while location < string.length {
|
|
127
|
+
let paragraphRange = string.paragraphRange(
|
|
128
|
+
for: NSRange(location: location, length: 0)
|
|
129
|
+
)
|
|
130
|
+
var contentLength = paragraphRange.length
|
|
131
|
+
while contentLength > 0 {
|
|
132
|
+
let scalar = string.character(at: paragraphRange.location + contentLength - 1)
|
|
133
|
+
guard scalar == 0x000A || scalar == 0x000D else { break }
|
|
134
|
+
contentLength -= 1
|
|
135
|
+
}
|
|
136
|
+
let contentRange = NSRange(location: paragraphRange.location, length: contentLength)
|
|
137
|
+
let styleLocation = min(paragraphRange.location, max(attributed.length - 1, 0))
|
|
138
|
+
let style = attributed.length > 0
|
|
139
|
+
? attributed.attribute(.paragraphStyle, at: styleLocation, effectiveRange: nil)
|
|
140
|
+
as? NSParagraphStyle
|
|
141
|
+
: nil
|
|
142
|
+
paragraphs.append((
|
|
143
|
+
html: semanticInlineHTML(from: attributed, range: contentRange),
|
|
144
|
+
listTags: style?.textLists.map(listTag) ?? []
|
|
145
|
+
))
|
|
146
|
+
location = NSMaxRange(paragraphRange)
|
|
147
|
+
}
|
|
148
|
+
if let finalScalar = attributed.string.utf16.last,
|
|
149
|
+
finalScalar == 0x000A || finalScalar == 0x000D {
|
|
150
|
+
let style = attributed.attribute(
|
|
151
|
+
.paragraphStyle,
|
|
152
|
+
at: attributed.length - 1,
|
|
153
|
+
effectiveRange: nil
|
|
154
|
+
) as? NSParagraphStyle
|
|
155
|
+
paragraphs.append((html: "", listTags: style?.textLists.map(listTag) ?? []))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
var html = ""
|
|
159
|
+
var openListTags: [String] = []
|
|
160
|
+
for paragraph in paragraphs {
|
|
161
|
+
guard !paragraph.listTags.isEmpty else {
|
|
162
|
+
closeLists(&openListTags, into: &html)
|
|
163
|
+
html += "<p>\(paragraph.html)</p>"
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let sharedDepth = zip(openListTags, paragraph.listTags)
|
|
168
|
+
.prefix { $0.0 == $0.1 }
|
|
169
|
+
.count
|
|
170
|
+
while openListTags.count > sharedDepth {
|
|
171
|
+
let tag = openListTags.removeLast()
|
|
172
|
+
html += "</li></\(tag)>"
|
|
173
|
+
}
|
|
174
|
+
if !openListTags.isEmpty, openListTags.count == paragraph.listTags.count {
|
|
175
|
+
html += "</li><li>"
|
|
176
|
+
}
|
|
177
|
+
for tag in paragraph.listTags.dropFirst(sharedDepth) {
|
|
178
|
+
html += "<\(tag)><li>"
|
|
179
|
+
openListTags.append(tag)
|
|
180
|
+
}
|
|
181
|
+
html += paragraph.html
|
|
182
|
+
}
|
|
183
|
+
closeLists(&openListTags, into: &html)
|
|
184
|
+
return html
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private static func semanticInlineHTML(
|
|
188
|
+
from attributed: NSAttributedString,
|
|
189
|
+
range: NSRange
|
|
190
|
+
) -> String {
|
|
191
|
+
var html = ""
|
|
192
|
+
attributed.enumerateAttributes(in: range) { attributes, range, _ in
|
|
193
|
+
var run = escapeHTMLText(attributed.attributedSubstring(from: range).string)
|
|
194
|
+
if let font = attributes[.font] as? UIFont {
|
|
195
|
+
let traits = font.fontDescriptor.symbolicTraits
|
|
196
|
+
if traits.contains(.traitBold) {
|
|
197
|
+
run = "<strong>\(run)</strong>"
|
|
198
|
+
}
|
|
199
|
+
if traits.contains(.traitItalic) {
|
|
200
|
+
run = "<em>\(run)</em>"
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if styleIsSet(attributes[.underlineStyle]) {
|
|
204
|
+
run = "<u>\(run)</u>"
|
|
205
|
+
}
|
|
206
|
+
if styleIsSet(attributes[.strikethroughStyle]) {
|
|
207
|
+
run = "<s>\(run)</s>"
|
|
208
|
+
}
|
|
209
|
+
if let href = linkString(attributes[.link]) {
|
|
210
|
+
run = "<a href=\"\(escapeHTMLAttribute(href))\">\(run)</a>"
|
|
211
|
+
}
|
|
212
|
+
html += run
|
|
213
|
+
}
|
|
214
|
+
guard !html.isEmpty else { return "" }
|
|
215
|
+
return "<span style=\"white-space:pre-wrap\">\(html)</span>"
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private static func listTag(_ list: NSTextList) -> String {
|
|
219
|
+
let marker = String(describing: list.markerFormat).lowercased()
|
|
220
|
+
let orderedMarkers = ["decimal", "roman", "alpha", "latin"]
|
|
221
|
+
return orderedMarkers.contains { marker.contains($0) } ? "ol" : "ul"
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private static func closeLists(_ tags: inout [String], into html: inout String) {
|
|
225
|
+
while let tag = tags.popLast() {
|
|
226
|
+
html += "</li></\(tag)>"
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private static func escapeHTMLText(_ value: String) -> String {
|
|
231
|
+
value
|
|
232
|
+
.replacingOccurrences(of: "&", with: "&")
|
|
233
|
+
.replacingOccurrences(of: "<", with: "<")
|
|
234
|
+
.replacingOccurrences(of: ">", with: ">")
|
|
235
|
+
.replacingOccurrences(of: "\r\n", with: "\n")
|
|
236
|
+
.replacingOccurrences(of: "\r", with: "\n")
|
|
237
|
+
.replacingOccurrences(of: "\n", with: "<br>")
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private static func escapeHTMLAttribute(_ value: String) -> String {
|
|
241
|
+
value
|
|
242
|
+
.replacingOccurrences(of: "&", with: "&")
|
|
243
|
+
.replacingOccurrences(of: "<", with: "<")
|
|
244
|
+
.replacingOccurrences(of: ">", with: ">")
|
|
245
|
+
.replacingOccurrences(of: "\"", with: """)
|
|
246
|
+
.replacingOccurrences(of: "'", with: "'")
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private static func styleIsSet(_ value: Any?) -> Bool {
|
|
250
|
+
guard let number = value as? NSNumber else { return false }
|
|
251
|
+
return number.intValue != 0
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private static func linkString(_ value: Any?) -> String? {
|
|
255
|
+
if let url = value as? URL {
|
|
256
|
+
return url.absoluteString
|
|
257
|
+
}
|
|
258
|
+
return value as? String
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
4
262
|
extension EditorTextView {
|
|
263
|
+
@discardableResult
|
|
264
|
+
func exportSelectionToPasteboard(_ pasteboard: UIPasteboard = .general) -> Bool {
|
|
265
|
+
ensureInternalTextViewDelegate()
|
|
266
|
+
guard editorId != 0 else { return false }
|
|
267
|
+
guard finishExternalTextCompositionBeforeInteractionIfNeeded() else { return false }
|
|
268
|
+
guard prepareForExternalEditorUpdate() else { return false }
|
|
269
|
+
guard syncClipboardSelectionToRust(requiresRange: true) != nil else { return false }
|
|
270
|
+
guard let payload = EditorV2Shadow.clipboardPayload(id: editorId) else { return false }
|
|
271
|
+
return payload.write(to: pasteboard)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
func syncClipboardSelectionToRust(
|
|
275
|
+
requiresRange: Bool = false
|
|
276
|
+
) -> (anchor: UInt32, head: UInt32)? {
|
|
277
|
+
guard let selection = currentScalarSelection() else { return nil }
|
|
278
|
+
guard !requiresRange
|
|
279
|
+
|| selection.anchor != selection.head
|
|
280
|
+
|| selectedImageSelectionState() != nil
|
|
281
|
+
else { return nil }
|
|
282
|
+
let sync: EditorV2SelectionSync?
|
|
283
|
+
if let image = selectedImageSelectionState() {
|
|
284
|
+
sync = EditorV2Shadow.setNodeSelection(id: editorId, docPos: image.docPos)
|
|
285
|
+
} else {
|
|
286
|
+
sync = EditorV2Shadow.setSelectionScalar(
|
|
287
|
+
id: editorId,
|
|
288
|
+
scalarAnchor: selection.anchor,
|
|
289
|
+
scalarHead: selection.head
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
guard let sync else { return nil }
|
|
293
|
+
if let refreshed = sync.refreshedUpdateJSON {
|
|
294
|
+
applyUpdateJSON(refreshed, notifyDelegate: false)
|
|
295
|
+
}
|
|
296
|
+
return selection
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
func applyClipboardCommand(_ command: [String: Any]) {
|
|
300
|
+
let updateJSON = EditorV2Shadow.applyClipboardCommand(id: editorId, command: command)
|
|
301
|
+
applyUpdateJSON(updateJSON)
|
|
302
|
+
}
|
|
303
|
+
|
|
5
304
|
@objc func handleIndentKeyCommand() {
|
|
6
305
|
handleListDepthKeyCommand(outdent: false)
|
|
7
306
|
}
|
package/ios/EditorTextView.swift
CHANGED
|
@@ -35,6 +35,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate, UITextDragD
|
|
|
35
35
|
var hidesNativeSelectionChrome = false
|
|
36
36
|
var isPreviewingImageResize = false
|
|
37
37
|
var allowImageResizing = true
|
|
38
|
+
var pasteMode: EditorPasteMode = .rich
|
|
38
39
|
|
|
39
40
|
override var isEditable: Bool {
|
|
40
41
|
didSet {
|
|
@@ -415,7 +416,7 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate, UITextDragD
|
|
|
415
416
|
height: min(maxPlaceholderHeight, ceil(fittedHeight))
|
|
416
417
|
)
|
|
417
418
|
}
|
|
418
|
-
if heightBehavior == .autoGrow
|
|
419
|
+
if heightBehavior == .autoGrow {
|
|
419
420
|
let currentWidth = ceil(bounds.width)
|
|
420
421
|
if abs(currentWidth - lastAutoGrowMeasuredWidth) > 0.5 {
|
|
421
422
|
autoGrowHeightCheckIsDirty = true
|
|
@@ -622,6 +623,11 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate, UITextDragD
|
|
|
622
623
|
input: "\t",
|
|
623
624
|
modifierFlags: [.shift],
|
|
624
625
|
action: #selector(handleOutdentKeyCommand)
|
|
626
|
+
),
|
|
627
|
+
UIKeyCommand(
|
|
628
|
+
input: "v",
|
|
629
|
+
modifierFlags: [.command, .shift],
|
|
630
|
+
action: #selector(pasteAndMatchStyle(_:))
|
|
625
631
|
)
|
|
626
632
|
]
|
|
627
633
|
}
|
|
@@ -875,61 +881,75 @@ final class EditorTextView: UITextView, UIGestureRecognizerDelegate, UITextDragD
|
|
|
875
881
|
}
|
|
876
882
|
}
|
|
877
883
|
|
|
878
|
-
// MARK: -
|
|
884
|
+
// MARK: - Clipboard
|
|
885
|
+
|
|
886
|
+
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
|
887
|
+
guard editorId != 0 else {
|
|
888
|
+
return super.canPerformAction(action, withSender: sender)
|
|
889
|
+
}
|
|
890
|
+
switch action {
|
|
891
|
+
case #selector(copy(_:)):
|
|
892
|
+
return selectedTextRange?.isEmpty == false
|
|
893
|
+
case #selector(cut(_:)):
|
|
894
|
+
return isEditable && selectedTextRange?.isEmpty == false
|
|
895
|
+
case #selector(paste(_:)), #selector(pasteAndMatchStyle(_:)):
|
|
896
|
+
return isEditable
|
|
897
|
+
&& pasteMode != .disabled
|
|
898
|
+
&& EditorClipboardPaste.hasSupportedContent(in: .general)
|
|
899
|
+
default:
|
|
900
|
+
return super.canPerformAction(action, withSender: sender)
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
override func copy(_ sender: Any?) {
|
|
905
|
+
guard editorId != 0 else {
|
|
906
|
+
super.copy(sender)
|
|
907
|
+
return
|
|
908
|
+
}
|
|
909
|
+
_ = exportSelectionToPasteboard()
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
override func cut(_ sender: Any?) {
|
|
913
|
+
guard editorId != 0 else {
|
|
914
|
+
super.cut(sender)
|
|
915
|
+
return
|
|
916
|
+
}
|
|
917
|
+
guard isEditable, exportSelectionToPasteboard() else { return }
|
|
918
|
+
performInterceptedInput {
|
|
919
|
+
applyClipboardCommand(["type": "paste", "text": "", "plainText": true])
|
|
920
|
+
}
|
|
921
|
+
}
|
|
879
922
|
|
|
880
|
-
/// Intercept paste operations to route content through Rust.
|
|
881
|
-
///
|
|
882
|
-
/// Attempts to extract HTML from the pasteboard first (for rich text paste),
|
|
883
|
-
/// falling back to plain text.
|
|
884
923
|
override func paste(_ sender: Any?) {
|
|
924
|
+
pasteFromPasteboard(forcePlainText: false, sender: sender)
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
override func pasteAndMatchStyle(_ sender: Any?) {
|
|
928
|
+
pasteFromPasteboard(forcePlainText: true, sender: sender)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
private func pasteFromPasteboard(forcePlainText: Bool, sender: Any?) {
|
|
885
932
|
ensureInternalTextViewDelegate()
|
|
886
933
|
guard editorId != 0 else {
|
|
887
|
-
|
|
934
|
+
if forcePlainText {
|
|
935
|
+
super.pasteAndMatchStyle(sender)
|
|
936
|
+
} else {
|
|
937
|
+
super.paste(sender)
|
|
938
|
+
}
|
|
888
939
|
return
|
|
889
940
|
}
|
|
941
|
+
guard isEditable, pasteMode != .disabled else { return }
|
|
890
942
|
guard finishExternalTextCompositionBeforeInteractionIfNeeded() else { return }
|
|
891
943
|
guard prepareForExternalEditorUpdate() else { return }
|
|
944
|
+
guard syncClipboardSelectionToRust() != nil else { return }
|
|
945
|
+
let mode: EditorPasteMode = forcePlainText ? .plainText : pasteMode
|
|
946
|
+
guard let command = EditorClipboardPaste.command(from: .general, mode: mode) else { return }
|
|
892
947
|
|
|
893
948
|
Self.inputLog.debug(
|
|
894
949
|
"[paste] selection=\(self.selectionSummary(), privacy: .public) textState=\(self.textSnapshotSummary(), privacy: .public)"
|
|
895
950
|
)
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
if let htmlData = pasteboard.data(forPasteboardType: "public.html"),
|
|
900
|
-
let html = String(data: htmlData, encoding: .utf8) {
|
|
901
|
-
performInterceptedInput {
|
|
902
|
-
pasteHTML(html)
|
|
903
|
-
}
|
|
904
|
-
return
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
if let rtfData = pasteboard.data(forPasteboardType: "public.rtf") {
|
|
908
|
-
if let attrStr = try? NSAttributedString(
|
|
909
|
-
data: rtfData,
|
|
910
|
-
options: [.documentType: NSAttributedString.DocumentType.rtf],
|
|
911
|
-
documentAttributes: nil
|
|
912
|
-
) {
|
|
913
|
-
if let htmlData = try? attrStr.data(
|
|
914
|
-
from: NSRange(location: 0, length: attrStr.length),
|
|
915
|
-
documentAttributes: [.documentType: NSAttributedString.DocumentType.html]
|
|
916
|
-
), let html = String(data: htmlData, encoding: .utf8) {
|
|
917
|
-
performInterceptedInput {
|
|
918
|
-
if !pasteHTML(html, detectContentChange: true),
|
|
919
|
-
!attrStr.string.isEmpty {
|
|
920
|
-
pastePlainText(attrStr.string)
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
return
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
if let text = pasteboard.string {
|
|
929
|
-
performInterceptedInput {
|
|
930
|
-
pastePlainText(text)
|
|
931
|
-
}
|
|
932
|
-
return
|
|
951
|
+
performInterceptedInput {
|
|
952
|
+
applyClipboardCommand(command)
|
|
933
953
|
}
|
|
934
954
|
}
|
|
935
955
|
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
|
|
3
3
|
extension EditorV2Adapter {
|
|
4
|
+
func clipboardPayloadJSON() -> String? {
|
|
5
|
+
guard beginRuntimeOperation() else { return nil }
|
|
6
|
+
defer { endRuntimeOperation() }
|
|
7
|
+
switch Self.normalizeJsonResult(editorV2GetClipboard(editorId: editorId)) {
|
|
8
|
+
case .failure(let error):
|
|
9
|
+
emit(error)
|
|
10
|
+
return nil
|
|
11
|
+
case .success(let json):
|
|
12
|
+
return json
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func applyClipboardCommand(_ command: [String: Any]) -> String? {
|
|
17
|
+
guard beginRuntimeOperation() else { return nil }
|
|
18
|
+
defer { endRuntimeOperation() }
|
|
19
|
+
return performMutation(adoptEngineSelection: true) {
|
|
20
|
+
self.callWithEnvelope(["command": command]) { requestJson in
|
|
21
|
+
editorV2ApplyCommand(editorId: self.editorId, requestJson: requestJson)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
4
26
|
func toggleMark(_ markType: String, anchor: UInt32, head: UInt32) -> String? {
|
|
5
27
|
guard beginRuntimeOperation() else { return nil }
|
|
6
28
|
defer { endRuntimeOperation() }
|
|
@@ -150,7 +150,7 @@ extension EditorV2Adapter {
|
|
|
150
150
|
private static func isValidMentionStyle(_ value: Any) -> Bool {
|
|
151
151
|
guard let style = value as? [String: Any] else { return false }
|
|
152
152
|
let colors: Set<String> = ["color", "backgroundColor", "textDecorationColor", "borderColor", "borderTopColor", "borderRightColor", "borderBottomColor", "borderLeftColor"]
|
|
153
|
-
let dimensions: Set<String> = ["fontSize", "lineHeight", "letterSpacing", "borderWidth", "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth", "borderRadius", "borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius"]
|
|
153
|
+
let dimensions: Set<String> = ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft", "fontSize", "lineHeight", "letterSpacing", "borderWidth", "borderTopWidth", "borderRightWidth", "borderBottomWidth", "borderLeftWidth", "borderRadius", "borderTopLeftRadius", "borderTopRightRadius", "borderBottomLeftRadius", "borderBottomRightRadius"]
|
|
154
154
|
let strings: Set<String> = ["fontFamily", "fontWeight", "fontStyle", "textDecorationLine", "textDecorationStyle", "borderStyle"]
|
|
155
155
|
guard hasOnlyKeys(style, colors.union(dimensions).union(strings)) else { return false }
|
|
156
156
|
for key in colors where style[key] != nil {
|
|
@@ -159,7 +159,7 @@ extension EditorV2Adapter {
|
|
|
159
159
|
for key in dimensions where style[key] != nil {
|
|
160
160
|
guard let number = finiteNumber(style[key])?.doubleValue else { return false }
|
|
161
161
|
if ["fontSize", "lineHeight"].contains(key), number <= 0 { return false }
|
|
162
|
-
if key.hasPrefix("border"), number < 0 { return false }
|
|
162
|
+
if key.hasPrefix("border") || key.hasPrefix("padding"), number < 0 { return false }
|
|
163
163
|
}
|
|
164
164
|
for key in strings where style[key] != nil {
|
|
165
165
|
guard let value = style[key] as? String else { return false }
|
package/ios/EditorV2Shadow.swift
CHANGED
|
@@ -71,6 +71,11 @@ enum EditorV2Shadow {
|
|
|
71
71
|
adapter(for: id)?.contentSnapshotJSON() ?? "{\"html\":\"\",\"json\":{}}"
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
static func clipboardPayload(id: UInt64) -> EditorClipboardPayload? {
|
|
75
|
+
guard let json = adapter(for: id)?.clipboardPayloadJSON() else { return nil }
|
|
76
|
+
return EditorClipboardPayload(json: json)
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
// MARK: - State / selection reads
|
|
75
80
|
|
|
76
81
|
static func getCurrentState(id: UInt64) -> String {
|
|
@@ -295,6 +300,13 @@ enum EditorV2Shadow {
|
|
|
295
300
|
adapter(for: id)?.insertContentHtmlAtEngineSelection(html) ?? "{}"
|
|
296
301
|
}
|
|
297
302
|
|
|
303
|
+
static func applyClipboardCommand(
|
|
304
|
+
id: UInt64,
|
|
305
|
+
command: [String: Any]
|
|
306
|
+
) -> String {
|
|
307
|
+
adapter(for: id)?.applyClipboardCommand(command) ?? "{}"
|
|
308
|
+
}
|
|
309
|
+
|
|
298
310
|
static func insertContentJson(id: UInt64, json: String) -> String {
|
|
299
311
|
adapter(for: id)?.insertContentJsonAtEngineSelection(json) ?? "{}"
|
|
300
312
|
}
|
|
@@ -2077,6 +2077,13 @@ public func editorV2DocToScalar(editorId: String, docPos: UInt32) -> FfiJsonResu
|
|
|
2077
2077
|
)
|
|
2078
2078
|
})
|
|
2079
2079
|
}
|
|
2080
|
+
public func editorV2GetClipboard(editorId: String) -> FfiJsonResult {
|
|
2081
|
+
return try! FfiConverterTypeFfiJsonResult_lift(try! rustCall() {
|
|
2082
|
+
uniffi_editor_core_fn_func_editor_v2_get_clipboard(
|
|
2083
|
+
FfiConverterString.lower(editorId),$0
|
|
2084
|
+
)
|
|
2085
|
+
})
|
|
2086
|
+
}
|
|
2080
2087
|
public func editorV2GetContentSnapshot(editorId: String) -> FfiJsonResult {
|
|
2081
2088
|
return try! FfiConverterTypeFfiJsonResult_lift(try! rustCall() {
|
|
2082
2089
|
uniffi_editor_core_fn_func_editor_v2_get_content_snapshot(
|
|
@@ -2289,6 +2296,9 @@ private let initializationResult: InitializationResult = {
|
|
|
2289
2296
|
if (uniffi_editor_core_checksum_func_editor_v2_doc_to_scalar() != 62503) {
|
|
2290
2297
|
return InitializationResult.apiChecksumMismatch
|
|
2291
2298
|
}
|
|
2299
|
+
if (uniffi_editor_core_checksum_func_editor_v2_get_clipboard() != 3214) {
|
|
2300
|
+
return InitializationResult.apiChecksumMismatch
|
|
2301
|
+
}
|
|
2292
2302
|
if (uniffi_editor_core_checksum_func_editor_v2_get_content_snapshot() != 21323) {
|
|
2293
2303
|
return InitializationResult.apiChecksumMismatch
|
|
2294
2304
|
}
|
|
@@ -104,6 +104,10 @@ extension NativeEditorExpoView {
|
|
|
104
104
|
updateAccessoryToolbarVisibility()
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
func setPasteMode(_ pasteMode: String) {
|
|
108
|
+
richTextView.textView.pasteMode = EditorPasteMode(rawValue: pasteMode) ?? .rich
|
|
109
|
+
}
|
|
110
|
+
|
|
107
111
|
func setAccessibilityLabel(_ label: String?) {
|
|
108
112
|
richTextView.textView.accessibilityLabel = label
|
|
109
113
|
}
|
|
@@ -753,6 +753,9 @@ public class NativeEditorModule: BaseModule, @preconcurrency AnyModule {
|
|
|
753
753
|
Prop("editable") { (view: NativeEditorExpoView, editable: Bool) in
|
|
754
754
|
view.setEditable(editable)
|
|
755
755
|
}
|
|
756
|
+
Prop("pasteMode") { (view: NativeEditorExpoView, pasteMode: String) in
|
|
757
|
+
view.setPasteMode(pasteMode)
|
|
758
|
+
}
|
|
756
759
|
Prop("accessibilityLabel") { (view: NativeEditorExpoView, label: String?) in
|
|
757
760
|
view.setAccessibilityLabel(label)
|
|
758
761
|
}
|
|
@@ -108,9 +108,7 @@ extension EditorAccessoryToolbarView {
|
|
|
108
108
|
?? (resolvedAppearance == .native ? self.tintColor : .secondaryLabel)
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
button
|
|
112
|
-
button.setTitleColor(tintColor, for: .normal)
|
|
113
|
-
button.setTitleColor(tintColor, for: .disabled)
|
|
111
|
+
applyButtonForeground(to: button, color: tintColor)
|
|
114
112
|
button.tintAdjustmentMode = enabled ? .automatic : .normal
|
|
115
113
|
button.alpha = enabled || resolvedAppearance == .native ? 1 : 0.7
|
|
116
114
|
let inactiveBackgroundColor = buttonStyle?.backgroundColor
|
|
@@ -139,7 +137,20 @@ extension EditorAccessoryToolbarView {
|
|
|
139
137
|
color: backgroundColor,
|
|
140
138
|
cornerRadius: cornerRadius
|
|
141
139
|
)
|
|
142
|
-
applyButtonIconStyle(to: button, item: item)
|
|
140
|
+
applyButtonIconStyle(to: button, item: item, color: tintColor)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// A configured `UIButton` paints disabled content in its own system gray,
|
|
144
|
+
/// ignoring `tintColor`, so the colour must be forced through the
|
|
145
|
+
/// configuration's transformers as well.
|
|
146
|
+
private func applyButtonForeground(to button: UIButton, color: UIColor) {
|
|
147
|
+
button.tintColor = color
|
|
148
|
+
button.setTitleColor(color, for: .normal)
|
|
149
|
+
button.setTitleColor(color, for: .disabled)
|
|
150
|
+
guard #available(iOS 15.0, *), var configuration = button.configuration else { return }
|
|
151
|
+
configuration.baseForegroundColor = color
|
|
152
|
+
configuration.imageColorTransformer = UIConfigurationColorTransformer { _ in color }
|
|
153
|
+
button.configuration = configuration
|
|
143
154
|
}
|
|
144
155
|
|
|
145
156
|
/// Own the configured background to avoid stacking it with UIButton state fills.
|
|
@@ -175,13 +186,14 @@ extension EditorAccessoryToolbarView {
|
|
|
175
186
|
return min(requestedSize, resolvedButtonSize)
|
|
176
187
|
}
|
|
177
188
|
|
|
178
|
-
private func applyButtonIconStyle(to button: UIButton, item: NativeToolbarItem) {
|
|
189
|
+
private func applyButtonIconStyle(to button: UIButton, item: NativeToolbarItem, color: UIColor) {
|
|
179
190
|
let iconSize = resolvedButtonIconSize(for: item)
|
|
180
191
|
let font = UIFont.systemFont(ofSize: iconSize, weight: .semibold)
|
|
181
192
|
if #available(iOS 15.0, *), var configuration = button.configuration {
|
|
182
193
|
configuration.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { incoming in
|
|
183
194
|
var outgoing = incoming
|
|
184
195
|
outgoing.font = font
|
|
196
|
+
outgoing.foregroundColor = color
|
|
185
197
|
return outgoing
|
|
186
198
|
}
|
|
187
199
|
button.configuration = configuration
|
|
@@ -25,6 +25,21 @@ extension EditorAccessoryToolbarView {
|
|
|
25
25
|
buttonBindings.indices.contains(index) ? buttonBindings[index].button.tintColor : nil
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/// The foreground colour a configured button will actually paint for its
|
|
29
|
+
/// current state, resolved through the configuration transformers rather
|
|
30
|
+
/// than `tintColor`, which UIKit ignores for disabled configured buttons.
|
|
31
|
+
func buttonPaintedForegroundColorForTesting(_ index: Int) -> UIColor? {
|
|
32
|
+
guard buttonBindings.indices.contains(index) else { return nil }
|
|
33
|
+
let button = buttonBindings[index].button
|
|
34
|
+
guard #available(iOS 15.0, *), let configuration = button.configuration else {
|
|
35
|
+
return button.titleColor(for: button.state)
|
|
36
|
+
}
|
|
37
|
+
if button.image(for: .normal) != nil {
|
|
38
|
+
return configuration.imageColorTransformer?(.black)
|
|
39
|
+
}
|
|
40
|
+
return configuration.titleTextAttributesTransformer?(AttributeContainer()).foregroundColor
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
func buttonFontSizeForTesting(_ index: Int) -> CGFloat? {
|
|
29
44
|
buttonBindings.indices.contains(index) ? buttonBindings[index].button.titleLabel?.font.pointSize : nil
|
|
30
45
|
}
|