@apollohg/react-native-prose-editor 0.3.0 → 0.4.1
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 +18 -0
- package/android/build.gradle +23 -0
- package/android/src/main/java/com/apollohg/editor/EditorEditText.kt +515 -39
- package/android/src/main/java/com/apollohg/editor/NativeEditorExpoView.kt +58 -28
- package/android/src/main/java/com/apollohg/editor/NativeEditorModule.kt +25 -0
- package/android/src/main/java/com/apollohg/editor/NativeToolbar.kt +232 -62
- package/android/src/main/java/com/apollohg/editor/PositionBridge.kt +57 -27
- package/android/src/main/java/com/apollohg/editor/RemoteSelectionOverlayView.kt +147 -78
- package/android/src/main/java/com/apollohg/editor/RenderBridge.kt +249 -71
- package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +7 -6
- package/dist/EditorToolbar.d.ts +26 -6
- package/dist/EditorToolbar.js +299 -65
- package/dist/NativeEditorBridge.d.ts +40 -1
- package/dist/NativeEditorBridge.js +184 -90
- package/dist/NativeRichTextEditor.d.ts +5 -1
- package/dist/NativeRichTextEditor.js +201 -78
- package/dist/YjsCollaboration.d.ts +2 -0
- package/dist/YjsCollaboration.js +142 -20
- package/dist/index.d.ts +1 -1
- package/dist/schemas.js +12 -0
- package/dist/useNativeEditor.d.ts +2 -0
- package/dist/useNativeEditor.js +7 -0
- 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 +3 -3
- package/ios/Generated_editor_core.swift +87 -0
- package/ios/NativeEditorExpoView.swift +488 -178
- package/ios/NativeEditorModule.swift +25 -0
- package/ios/PositionBridge.swift +310 -75
- package/ios/RenderBridge.swift +362 -27
- package/ios/RichTextEditorView.swift +2001 -189
- package/ios/editor_coreFFI/editor_coreFFI.h +55 -0
- package/package.json +11 -2
- 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 +128 -0
|
@@ -92,6 +92,12 @@ private enum ToolbarDefaultIconId: String {
|
|
|
92
92
|
case strike
|
|
93
93
|
case link
|
|
94
94
|
case image
|
|
95
|
+
case h1
|
|
96
|
+
case h2
|
|
97
|
+
case h3
|
|
98
|
+
case h4
|
|
99
|
+
case h5
|
|
100
|
+
case h6
|
|
95
101
|
case blockquote
|
|
96
102
|
case bulletList
|
|
97
103
|
case orderedList
|
|
@@ -105,14 +111,21 @@ private enum ToolbarDefaultIconId: String {
|
|
|
105
111
|
|
|
106
112
|
private enum ToolbarItemKind: String {
|
|
107
113
|
case mark
|
|
114
|
+
case heading
|
|
108
115
|
case blockquote
|
|
109
116
|
case list
|
|
110
117
|
case command
|
|
111
118
|
case node
|
|
112
119
|
case action
|
|
120
|
+
case group
|
|
113
121
|
case separator
|
|
114
122
|
}
|
|
115
123
|
|
|
124
|
+
private enum ToolbarGroupPresentation: String {
|
|
125
|
+
case expand
|
|
126
|
+
case menu
|
|
127
|
+
}
|
|
128
|
+
|
|
116
129
|
private struct NativeToolbarIcon {
|
|
117
130
|
let defaultId: ToolbarDefaultIconId?
|
|
118
131
|
let glyphText: String?
|
|
@@ -133,6 +146,12 @@ private struct NativeToolbarIcon {
|
|
|
133
146
|
.outdentList: "decrease.indent",
|
|
134
147
|
.lineBreak: "return.left",
|
|
135
148
|
.horizontalRule: "minus",
|
|
149
|
+
.h1: "paragraphsign",
|
|
150
|
+
.h2: "paragraphsign",
|
|
151
|
+
.h3: "paragraphsign",
|
|
152
|
+
.h4: "paragraphsign",
|
|
153
|
+
.h5: "paragraphsign",
|
|
154
|
+
.h6: "paragraphsign",
|
|
136
155
|
.undo: "arrow.uturn.backward",
|
|
137
156
|
.redo: "arrow.uturn.forward",
|
|
138
157
|
]
|
|
@@ -144,6 +163,12 @@ private struct NativeToolbarIcon {
|
|
|
144
163
|
.strike: "S",
|
|
145
164
|
.link: "🔗",
|
|
146
165
|
.image: "🖼",
|
|
166
|
+
.h1: "H1",
|
|
167
|
+
.h2: "H2",
|
|
168
|
+
.h3: "H3",
|
|
169
|
+
.h4: "H4",
|
|
170
|
+
.h5: "H5",
|
|
171
|
+
.h6: "H6",
|
|
147
172
|
.blockquote: "❝",
|
|
148
173
|
.bulletList: "•≡",
|
|
149
174
|
.orderedList: "1.",
|
|
@@ -238,178 +263,272 @@ private struct NativeToolbarItem {
|
|
|
238
263
|
let label: String?
|
|
239
264
|
let icon: NativeToolbarIcon?
|
|
240
265
|
let mark: String?
|
|
266
|
+
let headingLevel: Int?
|
|
241
267
|
let listType: ToolbarListType?
|
|
242
268
|
let command: ToolbarCommand?
|
|
243
269
|
let nodeType: String?
|
|
244
270
|
let isActive: Bool
|
|
245
271
|
let isDisabled: Bool
|
|
272
|
+
let presentation: ToolbarGroupPresentation?
|
|
273
|
+
let items: [NativeToolbarItem]
|
|
274
|
+
let parentGroupKey: String?
|
|
246
275
|
|
|
247
276
|
static let defaults: [NativeToolbarItem] = [
|
|
248
|
-
NativeToolbarItem(type: .mark, key: nil, label: "Bold", icon: .defaultIcon(.bold), mark: "bold", listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
249
|
-
NativeToolbarItem(type: .mark, key: nil, label: "Italic", icon: .defaultIcon(.italic), mark: "italic", listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
250
|
-
NativeToolbarItem(type: .mark, key: nil, label: "Underline", icon: .defaultIcon(.underline), mark: "underline", listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
251
|
-
NativeToolbarItem(type: .mark, key: nil, label: "Strikethrough", icon: .defaultIcon(.strike), mark: "strike", listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
252
|
-
NativeToolbarItem(type: .blockquote, key: nil, label: "Blockquote", icon: .defaultIcon(.blockquote), mark: nil, listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
253
|
-
NativeToolbarItem(type: .separator, key: nil, label: nil, icon: nil, mark: nil, listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
254
|
-
NativeToolbarItem(type: .list, key: nil, label: "Bullet List", icon: .defaultIcon(.bulletList), mark: nil, listType: .bulletList, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
255
|
-
NativeToolbarItem(type: .list, key: nil, label: "Ordered List", icon: .defaultIcon(.orderedList), mark: nil, listType: .orderedList, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
256
|
-
NativeToolbarItem(type: .command, key: nil, label: "Indent List", icon: .defaultIcon(.indentList), mark: nil, listType: nil, command: .indentList, nodeType: nil, isActive: false, isDisabled: false),
|
|
257
|
-
NativeToolbarItem(type: .command, key: nil, label: "Outdent List", icon: .defaultIcon(.outdentList), mark: nil, listType: nil, command: .outdentList, nodeType: nil, isActive: false, isDisabled: false),
|
|
258
|
-
NativeToolbarItem(type: .node, key: nil, label: "Line Break", icon: .defaultIcon(.lineBreak), mark: nil, listType: nil, command: nil, nodeType: "hardBreak", isActive: false, isDisabled: false),
|
|
259
|
-
NativeToolbarItem(type: .node, key: nil, label: "Horizontal Rule", icon: .defaultIcon(.horizontalRule), mark: nil, listType: nil, command: nil, nodeType: "horizontalRule", isActive: false, isDisabled: false),
|
|
260
|
-
NativeToolbarItem(type: .separator, key: nil, label: nil, icon: nil, mark: nil, listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false),
|
|
261
|
-
NativeToolbarItem(type: .command, key: nil, label: "Undo", icon: .defaultIcon(.undo), mark: nil, listType: nil, command: .undo, nodeType: nil, isActive: false, isDisabled: false),
|
|
262
|
-
NativeToolbarItem(type: .command, key: nil, label: "Redo", icon: .defaultIcon(.redo), mark: nil, listType: nil, command: .redo, nodeType: nil, isActive: false, isDisabled: false),
|
|
277
|
+
NativeToolbarItem(type: .mark, key: nil, label: "Bold", icon: .defaultIcon(.bold), mark: "bold", headingLevel: nil, listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false, presentation: nil, items: [], parentGroupKey: nil),
|
|
278
|
+
NativeToolbarItem(type: .mark, key: nil, label: "Italic", icon: .defaultIcon(.italic), mark: "italic", headingLevel: nil, listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false, presentation: nil, items: [], parentGroupKey: nil),
|
|
279
|
+
NativeToolbarItem(type: .mark, key: nil, label: "Underline", icon: .defaultIcon(.underline), mark: "underline", headingLevel: nil, listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false, presentation: nil, items: [], parentGroupKey: nil),
|
|
280
|
+
NativeToolbarItem(type: .mark, key: nil, label: "Strikethrough", icon: .defaultIcon(.strike), mark: "strike", headingLevel: nil, listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false, presentation: nil, items: [], parentGroupKey: nil),
|
|
281
|
+
NativeToolbarItem(type: .blockquote, key: nil, label: "Blockquote", icon: .defaultIcon(.blockquote), mark: nil, headingLevel: nil, listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false, presentation: nil, items: [], parentGroupKey: nil),
|
|
282
|
+
NativeToolbarItem(type: .separator, key: nil, label: nil, icon: nil, mark: nil, headingLevel: nil, listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false, presentation: nil, items: [], parentGroupKey: nil),
|
|
283
|
+
NativeToolbarItem(type: .list, key: nil, label: "Bullet List", icon: .defaultIcon(.bulletList), mark: nil, headingLevel: nil, listType: .bulletList, command: nil, nodeType: nil, isActive: false, isDisabled: false, presentation: nil, items: [], parentGroupKey: nil),
|
|
284
|
+
NativeToolbarItem(type: .list, key: nil, label: "Ordered List", icon: .defaultIcon(.orderedList), mark: nil, headingLevel: nil, listType: .orderedList, command: nil, nodeType: nil, isActive: false, isDisabled: false, presentation: nil, items: [], parentGroupKey: nil),
|
|
285
|
+
NativeToolbarItem(type: .command, key: nil, label: "Indent List", icon: .defaultIcon(.indentList), mark: nil, headingLevel: nil, listType: nil, command: .indentList, nodeType: nil, isActive: false, isDisabled: false, presentation: nil, items: [], parentGroupKey: nil),
|
|
286
|
+
NativeToolbarItem(type: .command, key: nil, label: "Outdent List", icon: .defaultIcon(.outdentList), mark: nil, headingLevel: nil, listType: nil, command: .outdentList, nodeType: nil, isActive: false, isDisabled: false, presentation: nil, items: [], parentGroupKey: nil),
|
|
287
|
+
NativeToolbarItem(type: .node, key: nil, label: "Line Break", icon: .defaultIcon(.lineBreak), mark: nil, headingLevel: nil, listType: nil, command: nil, nodeType: "hardBreak", isActive: false, isDisabled: false, presentation: nil, items: [], parentGroupKey: nil),
|
|
288
|
+
NativeToolbarItem(type: .node, key: nil, label: "Horizontal Rule", icon: .defaultIcon(.horizontalRule), mark: nil, headingLevel: nil, listType: nil, command: nil, nodeType: "horizontalRule", isActive: false, isDisabled: false, presentation: nil, items: [], parentGroupKey: nil),
|
|
289
|
+
NativeToolbarItem(type: .separator, key: nil, label: nil, icon: nil, mark: nil, headingLevel: nil, listType: nil, command: nil, nodeType: nil, isActive: false, isDisabled: false, presentation: nil, items: [], parentGroupKey: nil),
|
|
290
|
+
NativeToolbarItem(type: .command, key: nil, label: "Undo", icon: .defaultIcon(.undo), mark: nil, headingLevel: nil, listType: nil, command: .undo, nodeType: nil, isActive: false, isDisabled: false, presentation: nil, items: [], parentGroupKey: nil),
|
|
291
|
+
NativeToolbarItem(type: .command, key: nil, label: "Redo", icon: .defaultIcon(.redo), mark: nil, headingLevel: nil, listType: nil, command: .redo, nodeType: nil, isActive: false, isDisabled: false, presentation: nil, items: [], parentGroupKey: nil),
|
|
263
292
|
]
|
|
264
293
|
|
|
265
|
-
static func
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
294
|
+
private static func parse(
|
|
295
|
+
rawItem: [String: Any],
|
|
296
|
+
allowGroup: Bool = true,
|
|
297
|
+
allowSeparator: Bool = true
|
|
298
|
+
) -> NativeToolbarItem? {
|
|
299
|
+
guard let rawType = rawItem["type"] as? String,
|
|
300
|
+
let type = ToolbarItemKind(rawValue: rawType)
|
|
269
301
|
else {
|
|
270
|
-
return
|
|
302
|
+
return nil
|
|
271
303
|
}
|
|
272
304
|
|
|
273
|
-
let
|
|
274
|
-
|
|
275
|
-
|
|
305
|
+
let key = rawItem["key"] as? String
|
|
306
|
+
switch type {
|
|
307
|
+
case .separator:
|
|
308
|
+
guard allowSeparator else { return nil }
|
|
309
|
+
return NativeToolbarItem(
|
|
310
|
+
type: .separator,
|
|
311
|
+
key: key,
|
|
312
|
+
label: nil,
|
|
313
|
+
icon: nil,
|
|
314
|
+
mark: nil,
|
|
315
|
+
headingLevel: nil,
|
|
316
|
+
listType: nil,
|
|
317
|
+
command: nil,
|
|
318
|
+
nodeType: nil,
|
|
319
|
+
isActive: false,
|
|
320
|
+
isDisabled: false,
|
|
321
|
+
presentation: nil,
|
|
322
|
+
items: [],
|
|
323
|
+
parentGroupKey: nil
|
|
324
|
+
)
|
|
325
|
+
case .mark:
|
|
326
|
+
guard let mark = rawItem["mark"] as? String,
|
|
327
|
+
let label = rawItem["label"] as? String,
|
|
328
|
+
let icon = NativeToolbarIcon.from(jsonValue: rawItem["icon"])
|
|
276
329
|
else {
|
|
277
330
|
return nil
|
|
278
331
|
}
|
|
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
|
-
|
|
332
|
+
return NativeToolbarItem(
|
|
333
|
+
type: .mark,
|
|
334
|
+
key: key,
|
|
335
|
+
label: label,
|
|
336
|
+
icon: icon,
|
|
337
|
+
mark: mark,
|
|
338
|
+
headingLevel: nil,
|
|
339
|
+
listType: nil,
|
|
340
|
+
command: nil,
|
|
341
|
+
nodeType: nil,
|
|
342
|
+
isActive: false,
|
|
343
|
+
isDisabled: false,
|
|
344
|
+
presentation: nil,
|
|
345
|
+
items: [],
|
|
346
|
+
parentGroupKey: nil
|
|
347
|
+
)
|
|
348
|
+
case .heading:
|
|
349
|
+
guard let level = (rawItem["level"] as? NSNumber)?.intValue,
|
|
350
|
+
(1...6).contains(level),
|
|
351
|
+
let label = rawItem["label"] as? String,
|
|
352
|
+
let icon = NativeToolbarIcon.from(jsonValue: rawItem["icon"])
|
|
353
|
+
else {
|
|
354
|
+
return nil
|
|
355
|
+
}
|
|
356
|
+
return NativeToolbarItem(
|
|
357
|
+
type: .heading,
|
|
358
|
+
key: key,
|
|
359
|
+
label: label,
|
|
360
|
+
icon: icon,
|
|
361
|
+
mark: nil,
|
|
362
|
+
headingLevel: level,
|
|
363
|
+
listType: nil,
|
|
364
|
+
command: nil,
|
|
365
|
+
nodeType: nil,
|
|
366
|
+
isActive: false,
|
|
367
|
+
isDisabled: false,
|
|
368
|
+
presentation: nil,
|
|
369
|
+
items: [],
|
|
370
|
+
parentGroupKey: nil
|
|
371
|
+
)
|
|
372
|
+
case .blockquote:
|
|
373
|
+
guard let label = rawItem["label"] as? String,
|
|
374
|
+
let icon = NativeToolbarIcon.from(jsonValue: rawItem["icon"])
|
|
375
|
+
else {
|
|
376
|
+
return nil
|
|
377
|
+
}
|
|
378
|
+
return NativeToolbarItem(
|
|
379
|
+
type: .blockquote,
|
|
380
|
+
key: key,
|
|
381
|
+
label: label,
|
|
382
|
+
icon: icon,
|
|
383
|
+
mark: nil,
|
|
384
|
+
headingLevel: nil,
|
|
385
|
+
listType: nil,
|
|
386
|
+
command: nil,
|
|
387
|
+
nodeType: nil,
|
|
388
|
+
isActive: false,
|
|
389
|
+
isDisabled: false,
|
|
390
|
+
presentation: nil,
|
|
391
|
+
items: [],
|
|
392
|
+
parentGroupKey: nil
|
|
393
|
+
)
|
|
394
|
+
case .list:
|
|
395
|
+
guard let listTypeRaw = rawItem["listType"] as? String,
|
|
396
|
+
let listType = ToolbarListType(rawValue: listTypeRaw),
|
|
397
|
+
let label = rawItem["label"] as? String,
|
|
398
|
+
let icon = NativeToolbarIcon.from(jsonValue: rawItem["icon"])
|
|
399
|
+
else {
|
|
400
|
+
return nil
|
|
401
|
+
}
|
|
402
|
+
return NativeToolbarItem(
|
|
403
|
+
type: .list,
|
|
404
|
+
key: key,
|
|
405
|
+
label: label,
|
|
406
|
+
icon: icon,
|
|
407
|
+
mark: nil,
|
|
408
|
+
headingLevel: nil,
|
|
409
|
+
listType: listType,
|
|
410
|
+
command: nil,
|
|
411
|
+
nodeType: nil,
|
|
412
|
+
isActive: false,
|
|
413
|
+
isDisabled: false,
|
|
414
|
+
presentation: nil,
|
|
415
|
+
items: [],
|
|
416
|
+
parentGroupKey: nil
|
|
417
|
+
)
|
|
418
|
+
case .command:
|
|
419
|
+
guard let commandRaw = rawItem["command"] as? String,
|
|
420
|
+
let command = ToolbarCommand(rawValue: commandRaw),
|
|
421
|
+
let label = rawItem["label"] as? String,
|
|
422
|
+
let icon = NativeToolbarIcon.from(jsonValue: rawItem["icon"])
|
|
423
|
+
else {
|
|
424
|
+
return nil
|
|
425
|
+
}
|
|
426
|
+
return NativeToolbarItem(
|
|
427
|
+
type: .command,
|
|
428
|
+
key: key,
|
|
429
|
+
label: label,
|
|
430
|
+
icon: icon,
|
|
431
|
+
mark: nil,
|
|
432
|
+
headingLevel: nil,
|
|
433
|
+
listType: nil,
|
|
434
|
+
command: command,
|
|
435
|
+
nodeType: nil,
|
|
436
|
+
isActive: false,
|
|
437
|
+
isDisabled: false,
|
|
438
|
+
presentation: nil,
|
|
439
|
+
items: [],
|
|
440
|
+
parentGroupKey: nil
|
|
441
|
+
)
|
|
442
|
+
case .node:
|
|
443
|
+
guard let nodeType = rawItem["nodeType"] as? String,
|
|
444
|
+
let label = rawItem["label"] as? String,
|
|
445
|
+
let icon = NativeToolbarIcon.from(jsonValue: rawItem["icon"])
|
|
446
|
+
else {
|
|
447
|
+
return nil
|
|
448
|
+
}
|
|
449
|
+
return NativeToolbarItem(
|
|
450
|
+
type: .node,
|
|
451
|
+
key: key,
|
|
452
|
+
label: label,
|
|
453
|
+
icon: icon,
|
|
454
|
+
mark: nil,
|
|
455
|
+
headingLevel: nil,
|
|
456
|
+
listType: nil,
|
|
457
|
+
command: nil,
|
|
458
|
+
nodeType: nodeType,
|
|
459
|
+
isActive: false,
|
|
460
|
+
isDisabled: false,
|
|
461
|
+
presentation: nil,
|
|
462
|
+
items: [],
|
|
463
|
+
parentGroupKey: nil
|
|
464
|
+
)
|
|
465
|
+
case .action:
|
|
466
|
+
guard let key,
|
|
467
|
+
let label = rawItem["label"] as? String,
|
|
468
|
+
let icon = NativeToolbarIcon.from(jsonValue: rawItem["icon"])
|
|
469
|
+
else {
|
|
470
|
+
return nil
|
|
471
|
+
}
|
|
472
|
+
return NativeToolbarItem(
|
|
473
|
+
type: .action,
|
|
474
|
+
key: key,
|
|
475
|
+
label: label,
|
|
476
|
+
icon: icon,
|
|
477
|
+
mark: nil,
|
|
478
|
+
headingLevel: nil,
|
|
479
|
+
listType: nil,
|
|
480
|
+
command: nil,
|
|
481
|
+
nodeType: nil,
|
|
482
|
+
isActive: (rawItem["isActive"] as? Bool) ?? false,
|
|
483
|
+
isDisabled: (rawItem["isDisabled"] as? Bool) ?? false,
|
|
484
|
+
presentation: nil,
|
|
485
|
+
items: [],
|
|
486
|
+
parentGroupKey: nil
|
|
487
|
+
)
|
|
488
|
+
case .group:
|
|
489
|
+
guard allowGroup,
|
|
490
|
+
let key,
|
|
491
|
+
let label = rawItem["label"] as? String,
|
|
492
|
+
let icon = NativeToolbarIcon.from(jsonValue: rawItem["icon"]),
|
|
493
|
+
let rawChildren = rawItem["items"] as? [[String: Any]]
|
|
494
|
+
else {
|
|
495
|
+
return nil
|
|
410
496
|
}
|
|
497
|
+
let presentation = (rawItem["presentation"] as? String)
|
|
498
|
+
.flatMap(ToolbarGroupPresentation.init(rawValue:))
|
|
499
|
+
?? .expand
|
|
500
|
+
let children = rawChildren.compactMap {
|
|
501
|
+
parse(rawItem: $0, allowGroup: false, allowSeparator: false)
|
|
502
|
+
}
|
|
503
|
+
guard !children.isEmpty else { return nil }
|
|
504
|
+
return NativeToolbarItem(
|
|
505
|
+
type: .group,
|
|
506
|
+
key: key,
|
|
507
|
+
label: label,
|
|
508
|
+
icon: icon,
|
|
509
|
+
mark: nil,
|
|
510
|
+
headingLevel: nil,
|
|
511
|
+
listType: nil,
|
|
512
|
+
command: nil,
|
|
513
|
+
nodeType: nil,
|
|
514
|
+
isActive: false,
|
|
515
|
+
isDisabled: false,
|
|
516
|
+
presentation: presentation,
|
|
517
|
+
items: children,
|
|
518
|
+
parentGroupKey: nil
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
static func from(json: String?) -> [NativeToolbarItem] {
|
|
524
|
+
guard let json,
|
|
525
|
+
let data = json.data(using: .utf8),
|
|
526
|
+
let rawItems = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]]
|
|
527
|
+
else {
|
|
528
|
+
return defaults
|
|
411
529
|
}
|
|
412
530
|
|
|
531
|
+
let parsed = rawItems.compactMap { parse(rawItem: $0) }
|
|
413
532
|
return parsed.isEmpty ? defaults : parsed
|
|
414
533
|
}
|
|
415
534
|
|
|
@@ -420,6 +539,8 @@ private struct NativeToolbarItem {
|
|
|
420
539
|
switch type {
|
|
421
540
|
case .mark:
|
|
422
541
|
return "mark:\(mark ?? ""):\(index)"
|
|
542
|
+
case .heading:
|
|
543
|
+
return "heading:\(headingLevel ?? 0):\(index)"
|
|
423
544
|
case .blockquote:
|
|
424
545
|
return "blockquote:\(index)"
|
|
425
546
|
case .list:
|
|
@@ -430,10 +551,31 @@ private struct NativeToolbarItem {
|
|
|
430
551
|
return "node:\(nodeType ?? ""):\(index)"
|
|
431
552
|
case .action:
|
|
432
553
|
return "action:\(key ?? ""):\(index)"
|
|
554
|
+
case .group:
|
|
555
|
+
return "group:\(key ?? ""):\(index)"
|
|
433
556
|
case .separator:
|
|
434
557
|
return "separator:\(index)"
|
|
435
558
|
}
|
|
436
559
|
}
|
|
560
|
+
|
|
561
|
+
func with(parentGroupKey: String?) -> NativeToolbarItem {
|
|
562
|
+
NativeToolbarItem(
|
|
563
|
+
type: type,
|
|
564
|
+
key: key,
|
|
565
|
+
label: label,
|
|
566
|
+
icon: icon,
|
|
567
|
+
mark: mark,
|
|
568
|
+
headingLevel: headingLevel,
|
|
569
|
+
listType: listType,
|
|
570
|
+
command: command,
|
|
571
|
+
nodeType: nodeType,
|
|
572
|
+
isActive: isActive,
|
|
573
|
+
isDisabled: isDisabled,
|
|
574
|
+
presentation: presentation,
|
|
575
|
+
items: items,
|
|
576
|
+
parentGroupKey: parentGroupKey
|
|
577
|
+
)
|
|
578
|
+
}
|
|
437
579
|
}
|
|
438
580
|
|
|
439
581
|
final class EditorAccessoryToolbarView: UIInputView {
|
|
@@ -474,6 +616,7 @@ final class EditorAccessoryToolbarView: UIInputView {
|
|
|
474
616
|
private var separators: [UIView] = []
|
|
475
617
|
private var mentionButtons: [MentionSuggestionChipButton] = []
|
|
476
618
|
private var items: [NativeToolbarItem] = NativeToolbarItem.defaults
|
|
619
|
+
private var expandedGroupKey: String?
|
|
477
620
|
private var currentState = NativeToolbarState.empty
|
|
478
621
|
private var theme: EditorToolbarTheme?
|
|
479
622
|
private var mentionTheme: EditorMentionTheme?
|
|
@@ -524,6 +667,16 @@ final class EditorAccessoryToolbarView: UIInputView {
|
|
|
524
667
|
func mentionButtonAtForTesting(_ index: Int) -> MentionSuggestionChipButton? {
|
|
525
668
|
mentionButtons.indices.contains(index) ? mentionButtons[index] : nil
|
|
526
669
|
}
|
|
670
|
+
func buttonCountForTesting() -> Int {
|
|
671
|
+
buttonBindings.count
|
|
672
|
+
}
|
|
673
|
+
func buttonLabelForTesting(_ index: Int) -> String? {
|
|
674
|
+
buttonBindings.indices.contains(index) ? buttonBindings[index].button.accessibilityLabel : nil
|
|
675
|
+
}
|
|
676
|
+
func triggerButtonTapForTesting(_ index: Int) {
|
|
677
|
+
guard buttonBindings.indices.contains(index) else { return }
|
|
678
|
+
buttonBindings[index].button.sendActions(for: .touchUpInside)
|
|
679
|
+
}
|
|
527
680
|
|
|
528
681
|
override var intrinsicContentSize: CGSize {
|
|
529
682
|
let contentHeight = mentionButtons.isEmpty ? Self.baseHeight : Self.mentionRowHeight
|
|
@@ -569,8 +722,22 @@ final class EditorAccessoryToolbarView: UIInputView {
|
|
|
569
722
|
|
|
570
723
|
fileprivate func setItems(_ items: [NativeToolbarItem]) {
|
|
571
724
|
self.items = items
|
|
725
|
+
if let expandedGroupKey,
|
|
726
|
+
!items.contains(where: {
|
|
727
|
+
$0.type == .group && $0.key == expandedGroupKey && ($0.presentation ?? .expand) == .expand
|
|
728
|
+
})
|
|
729
|
+
{
|
|
730
|
+
self.expandedGroupKey = nil
|
|
731
|
+
}
|
|
572
732
|
rebuildButtons()
|
|
573
733
|
}
|
|
734
|
+
func setItemsJSONForTesting(_ json: String) {
|
|
735
|
+
setItems(NativeToolbarItem.from(json: json))
|
|
736
|
+
}
|
|
737
|
+
func applyStateJSONForTesting(_ json: String) {
|
|
738
|
+
guard let state = NativeToolbarState(updateJSON: json) else { return }
|
|
739
|
+
apply(state: state)
|
|
740
|
+
}
|
|
574
741
|
|
|
575
742
|
func apply(mentionTheme: EditorMentionTheme?) {
|
|
576
743
|
self.mentionTheme = mentionTheme
|
|
@@ -687,6 +854,9 @@ final class EditorAccessoryToolbarView: UIInputView {
|
|
|
687
854
|
binding.button.isEnabled = buttonState.enabled
|
|
688
855
|
binding.button.isSelected = buttonState.active
|
|
689
856
|
binding.button.accessibilityTraits = buttonState.active ? [.button, .selected] : .button
|
|
857
|
+
if binding.item.type == .group, (binding.item.presentation ?? .expand) == .menu {
|
|
858
|
+
binding.button.menu = makeGroupMenu(item: binding.item)
|
|
859
|
+
}
|
|
690
860
|
updateButtonAppearance(binding.button, item: binding.item, enabled: buttonState.enabled, active: buttonState.active)
|
|
691
861
|
}
|
|
692
862
|
|
|
@@ -697,6 +867,9 @@ final class EditorAccessoryToolbarView: UIInputView {
|
|
|
697
867
|
binding.button.isEnabled = state.enabled
|
|
698
868
|
binding.button.isSelected = state.active
|
|
699
869
|
binding.button.style = state.active ? .prominent : .plain
|
|
870
|
+
if binding.item.type == .group, (binding.item.presentation ?? .expand) == .menu {
|
|
871
|
+
binding.button.menu = makeGroupMenu(item: binding.item)
|
|
872
|
+
}
|
|
700
873
|
}
|
|
701
874
|
}
|
|
702
875
|
#endif
|
|
@@ -872,13 +1045,9 @@ final class EditorAccessoryToolbarView: UIInputView {
|
|
|
872
1045
|
arrangedSubview.removeFromSuperview()
|
|
873
1046
|
}
|
|
874
1047
|
|
|
875
|
-
let
|
|
876
|
-
guard item.type == .separator else { return true }
|
|
877
|
-
guard index > 0, index < items.count - 1 else { return false }
|
|
878
|
-
return items[index - 1].type != .separator && items[index + 1].type != .separator
|
|
879
|
-
}.map(\.element)
|
|
1048
|
+
let visibleItems = visibleToolbarItems()
|
|
880
1049
|
|
|
881
|
-
for item in
|
|
1050
|
+
for item in visibleItems {
|
|
882
1051
|
if item.type == .separator {
|
|
883
1052
|
stackView.addArrangedSubview(makeSeparator())
|
|
884
1053
|
continue
|
|
@@ -891,7 +1060,7 @@ final class EditorAccessoryToolbarView: UIInputView {
|
|
|
891
1060
|
|
|
892
1061
|
#if compiler(>=6.2)
|
|
893
1062
|
if #available(iOS 26.0, *) {
|
|
894
|
-
nativeToolbarView.setItems(makeNativeToolbarItems(from:
|
|
1063
|
+
nativeToolbarView.setItems(makeNativeToolbarItems(from: visibleItems), animated: false)
|
|
895
1064
|
} else {
|
|
896
1065
|
nativeToolbarView.setItems([], animated: false)
|
|
897
1066
|
}
|
|
@@ -904,6 +1073,75 @@ final class EditorAccessoryToolbarView: UIInputView {
|
|
|
904
1073
|
apply(state: currentState)
|
|
905
1074
|
}
|
|
906
1075
|
|
|
1076
|
+
private func compactToolbarItems(_ items: [NativeToolbarItem]) -> [NativeToolbarItem] {
|
|
1077
|
+
items.enumerated().filter { index, item in
|
|
1078
|
+
guard item.type == .separator else { return true }
|
|
1079
|
+
guard index > 0, index < items.count - 1 else { return false }
|
|
1080
|
+
return items[index - 1].type != .separator && items[index + 1].type != .separator
|
|
1081
|
+
}.map(\.element)
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
private func visibleToolbarItems() -> [NativeToolbarItem] {
|
|
1085
|
+
var visible: [NativeToolbarItem] = []
|
|
1086
|
+
for item in compactToolbarItems(items) {
|
|
1087
|
+
visible.append(item)
|
|
1088
|
+
if item.type == .group,
|
|
1089
|
+
(item.presentation ?? .expand) == .expand,
|
|
1090
|
+
expandedGroupKey == item.key
|
|
1091
|
+
{
|
|
1092
|
+
visible.append(contentsOf: item.items.map { $0.with(parentGroupKey: item.key) })
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return compactToolbarItems(visible)
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
private func handleToolbarButtonPress(_ item: NativeToolbarItem) {
|
|
1099
|
+
switch item.type {
|
|
1100
|
+
case .group:
|
|
1101
|
+
handleGroupPress(item)
|
|
1102
|
+
default:
|
|
1103
|
+
onPressItem?(item.with(parentGroupKey: nil))
|
|
1104
|
+
if let parentGroupKey = item.parentGroupKey,
|
|
1105
|
+
expandedGroupKey == parentGroupKey
|
|
1106
|
+
{
|
|
1107
|
+
expandedGroupKey = nil
|
|
1108
|
+
rebuildButtons()
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
private func handleGroupPress(_ item: NativeToolbarItem) {
|
|
1114
|
+
guard item.type == .group, !item.items.isEmpty else { return }
|
|
1115
|
+
switch item.presentation ?? .expand {
|
|
1116
|
+
case .expand:
|
|
1117
|
+
expandedGroupKey = expandedGroupKey == item.key ? nil : item.key
|
|
1118
|
+
rebuildButtons()
|
|
1119
|
+
case .menu:
|
|
1120
|
+
break
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
private func makeGroupMenu(item: NativeToolbarItem) -> UIMenu? {
|
|
1125
|
+
guard item.type == .group else { return nil }
|
|
1126
|
+
let actions = item.items.compactMap { child -> UIAction? in
|
|
1127
|
+
let state = buttonState(for: child, state: currentState)
|
|
1128
|
+
let image = child.icon?.resolvedSFSymbolName().flatMap { UIImage(systemName: $0) }
|
|
1129
|
+
let title = child.label ?? child.icon?.resolvedGlyphText() ?? "Item"
|
|
1130
|
+
return UIAction(
|
|
1131
|
+
title: title,
|
|
1132
|
+
image: image,
|
|
1133
|
+
identifier: nil,
|
|
1134
|
+
discoverabilityTitle: child.label,
|
|
1135
|
+
attributes: state.enabled ? [] : [.disabled],
|
|
1136
|
+
state: state.active ? .on : .off
|
|
1137
|
+
) { [weak self] _ in
|
|
1138
|
+
self?.handleToolbarButtonPress(child)
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
guard !actions.isEmpty else { return nil }
|
|
1142
|
+
return UIMenu(title: item.label ?? "", children: actions)
|
|
1143
|
+
}
|
|
1144
|
+
|
|
907
1145
|
private func updateNativeToolbarMetricsIfNeeded() {
|
|
908
1146
|
#if compiler(>=6.2)
|
|
909
1147
|
guard #available(iOS 26.0, *), usesNativeBarToolbar else {
|
|
@@ -998,10 +1236,20 @@ final class EditorAccessoryToolbarView: UIInputView {
|
|
|
998
1236
|
) -> UIBarButtonItem {
|
|
999
1237
|
let image = item.icon?.resolvedSFSymbolName().flatMap { UIImage(systemName: $0) }
|
|
1000
1238
|
let title = image == nil ? item.icon?.resolvedGlyphText() : nil
|
|
1001
|
-
let
|
|
1002
|
-
|
|
1239
|
+
let barButtonItem: UIBarButtonItem
|
|
1240
|
+
if item.type == .group, (item.presentation ?? .expand) == .menu {
|
|
1241
|
+
barButtonItem = UIBarButtonItem(
|
|
1242
|
+
title: title,
|
|
1243
|
+
image: image,
|
|
1244
|
+
primaryAction: nil,
|
|
1245
|
+
menu: makeGroupMenu(item: item)
|
|
1246
|
+
)
|
|
1247
|
+
} else {
|
|
1248
|
+
let action = UIAction { [weak self] _ in
|
|
1249
|
+
self?.handleToolbarButtonPress(item)
|
|
1250
|
+
}
|
|
1251
|
+
barButtonItem = UIBarButtonItem(title: title, image: image, primaryAction: action, menu: nil)
|
|
1003
1252
|
}
|
|
1004
|
-
let barButtonItem = UIBarButtonItem(title: title, image: image, primaryAction: action, menu: nil)
|
|
1005
1253
|
|
|
1006
1254
|
barButtonItem.accessibilityLabel = item.label
|
|
1007
1255
|
barButtonItem.isEnabled = enabled
|
|
@@ -1048,9 +1296,17 @@ final class EditorAccessoryToolbarView: UIInputView {
|
|
|
1048
1296
|
}
|
|
1049
1297
|
button.widthAnchor.constraint(greaterThanOrEqualToConstant: 36).isActive = true
|
|
1050
1298
|
button.heightAnchor.constraint(equalToConstant: 36).isActive = true
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1299
|
+
if item.type == .group,
|
|
1300
|
+
(item.presentation ?? .expand) == .menu,
|
|
1301
|
+
#available(iOS 14.0, *)
|
|
1302
|
+
{
|
|
1303
|
+
button.menu = makeGroupMenu(item: item)
|
|
1304
|
+
button.showsMenuAsPrimaryAction = true
|
|
1305
|
+
} else {
|
|
1306
|
+
button.addAction(UIAction { [weak self] _ in
|
|
1307
|
+
self?.handleToolbarButtonPress(item)
|
|
1308
|
+
}, for: .touchUpInside)
|
|
1309
|
+
}
|
|
1054
1310
|
updateButtonAppearance(button, item: item, enabled: true, active: false)
|
|
1055
1311
|
return button
|
|
1056
1312
|
}
|
|
@@ -1078,6 +1334,13 @@ final class EditorAccessoryToolbarView: UIInputView {
|
|
|
1078
1334
|
enabled: state.allowedMarks.contains(mark),
|
|
1079
1335
|
active: state.marks[mark] == true
|
|
1080
1336
|
)
|
|
1337
|
+
case .heading:
|
|
1338
|
+
let level = item.headingLevel ?? 0
|
|
1339
|
+
let headingType = "h\(level)"
|
|
1340
|
+
return (
|
|
1341
|
+
enabled: state.commands["toggleHeading\(level)"] == true,
|
|
1342
|
+
active: state.nodes[headingType] == true
|
|
1343
|
+
)
|
|
1081
1344
|
case .blockquote:
|
|
1082
1345
|
return (
|
|
1083
1346
|
enabled: state.commands["toggleBlockquote"] == true,
|
|
@@ -1128,6 +1391,16 @@ final class EditorAccessoryToolbarView: UIInputView {
|
|
|
1128
1391
|
enabled: !item.isDisabled,
|
|
1129
1392
|
active: item.isActive
|
|
1130
1393
|
)
|
|
1394
|
+
case .group:
|
|
1395
|
+
let childStates = item.items.map { buttonState(for: $0, state: state) }
|
|
1396
|
+
return (
|
|
1397
|
+
enabled: childStates.contains { $0.enabled },
|
|
1398
|
+
active: childStates.contains { $0.active } ||
|
|
1399
|
+
(
|
|
1400
|
+
(item.presentation ?? .expand) == .expand &&
|
|
1401
|
+
expandedGroupKey == item.key
|
|
1402
|
+
)
|
|
1403
|
+
)
|
|
1131
1404
|
case .separator:
|
|
1132
1405
|
return (enabled: false, active: false)
|
|
1133
1406
|
}
|
|
@@ -1323,6 +1596,11 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1323
1596
|
private var addons = NativeEditorAddons(mentions: nil)
|
|
1324
1597
|
private var mentionQueryState: MentionQueryState?
|
|
1325
1598
|
private var lastMentionEventJSON: String?
|
|
1599
|
+
private var lastThemeJSON: String?
|
|
1600
|
+
private var lastAddonsJSON: String?
|
|
1601
|
+
private var lastRemoteSelectionsJSON: String?
|
|
1602
|
+
private var lastToolbarItemsJSON: String?
|
|
1603
|
+
private var lastToolbarFrameJSON: String?
|
|
1326
1604
|
private var pendingEditorUpdateJSON: String?
|
|
1327
1605
|
private var pendingEditorUpdateRevision = 0
|
|
1328
1606
|
private var appliedEditorUpdateRevision = 0
|
|
@@ -1351,17 +1629,18 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1351
1629
|
let onToolbarAction = EventDispatcher()
|
|
1352
1630
|
let onAddonEvent = EventDispatcher()
|
|
1353
1631
|
private var lastEmittedContentHeight: CGFloat = 0
|
|
1632
|
+
private var cachedAutoGrowContentHeight: CGFloat = 0
|
|
1354
1633
|
|
|
1355
1634
|
// MARK: - Initialization
|
|
1356
1635
|
|
|
1357
1636
|
required init(appContext: AppContext? = nil) {
|
|
1358
1637
|
richTextView = RichTextEditorView(frame: .zero)
|
|
1359
1638
|
super.init(appContext: appContext)
|
|
1360
|
-
richTextView.onHeightMayChange = { [weak self] in
|
|
1639
|
+
richTextView.onHeightMayChange = { [weak self] measuredHeight in
|
|
1361
1640
|
guard let self, self.heightBehavior == .autoGrow else { return }
|
|
1641
|
+
self.cachedAutoGrowContentHeight = measuredHeight
|
|
1362
1642
|
self.invalidateIntrinsicContentSize()
|
|
1363
|
-
self.
|
|
1364
|
-
self.emitContentHeightIfNeeded(force: true)
|
|
1643
|
+
self.emitContentHeightIfNeeded(force: true, measuredHeight: measuredHeight)
|
|
1365
1644
|
}
|
|
1366
1645
|
richTextView.textView.editorDelegate = self
|
|
1367
1646
|
configureAccessoryToolbar()
|
|
@@ -1393,6 +1672,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1393
1672
|
guard heightBehavior == .autoGrow else {
|
|
1394
1673
|
return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
|
1395
1674
|
}
|
|
1675
|
+
if cachedAutoGrowContentHeight > 0 {
|
|
1676
|
+
return CGSize(width: UIView.noIntrinsicMetric, height: cachedAutoGrowContentHeight)
|
|
1677
|
+
}
|
|
1396
1678
|
return richTextView.intrinsicContentSize
|
|
1397
1679
|
}
|
|
1398
1680
|
|
|
@@ -1403,6 +1685,7 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1403
1685
|
let currentWidth = bounds.width.rounded(.towardZero)
|
|
1404
1686
|
guard currentWidth != lastAutoGrowWidth else { return }
|
|
1405
1687
|
lastAutoGrowWidth = currentWidth
|
|
1688
|
+
cachedAutoGrowContentHeight = 0
|
|
1406
1689
|
invalidateIntrinsicContentSize()
|
|
1407
1690
|
emitContentHeightIfNeeded(force: true)
|
|
1408
1691
|
}
|
|
@@ -1438,6 +1721,8 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1438
1721
|
}
|
|
1439
1722
|
|
|
1440
1723
|
func setThemeJson(_ themeJson: String?) {
|
|
1724
|
+
guard lastThemeJSON != themeJson else { return }
|
|
1725
|
+
lastThemeJSON = themeJson
|
|
1441
1726
|
let theme = EditorTheme.from(json: themeJson)
|
|
1442
1727
|
richTextView.applyTheme(theme)
|
|
1443
1728
|
accessoryToolbar.apply(theme: theme?.toolbar)
|
|
@@ -1451,12 +1736,16 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1451
1736
|
}
|
|
1452
1737
|
|
|
1453
1738
|
func setAddonsJson(_ addonsJson: String?) {
|
|
1739
|
+
guard lastAddonsJSON != addonsJson else { return }
|
|
1740
|
+
lastAddonsJSON = addonsJson
|
|
1454
1741
|
addons = NativeEditorAddons.from(json: addonsJson)
|
|
1455
1742
|
accessoryToolbar.apply(mentionTheme: richTextView.textView.theme?.mentions ?? addons.mentions?.theme)
|
|
1456
1743
|
refreshMentionQuery()
|
|
1457
1744
|
}
|
|
1458
1745
|
|
|
1459
1746
|
func setRemoteSelectionsJson(_ remoteSelectionsJson: String?) {
|
|
1747
|
+
guard lastRemoteSelectionsJSON != remoteSelectionsJson else { return }
|
|
1748
|
+
lastRemoteSelectionsJSON = remoteSelectionsJson
|
|
1460
1749
|
richTextView.setRemoteSelections(RemoteSelectionDecoration.from(json: remoteSelectionsJson))
|
|
1461
1750
|
}
|
|
1462
1751
|
|
|
@@ -1485,6 +1774,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1485
1774
|
let nextBehavior = EditorHeightBehavior(rawValue: rawHeightBehavior) ?? .fixed
|
|
1486
1775
|
guard nextBehavior != heightBehavior else { return }
|
|
1487
1776
|
heightBehavior = nextBehavior
|
|
1777
|
+
if nextBehavior != .autoGrow {
|
|
1778
|
+
cachedAutoGrowContentHeight = 0
|
|
1779
|
+
}
|
|
1488
1780
|
richTextView.heightBehavior = nextBehavior
|
|
1489
1781
|
invalidateIntrinsicContentSize()
|
|
1490
1782
|
setNeedsLayout()
|
|
@@ -1497,22 +1789,29 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1497
1789
|
richTextView.allowImageResizing = allowImageResizing
|
|
1498
1790
|
}
|
|
1499
1791
|
|
|
1500
|
-
private func emitContentHeightIfNeeded(force: Bool = false) {
|
|
1792
|
+
private func emitContentHeightIfNeeded(force: Bool = false, measuredHeight: CGFloat? = nil) {
|
|
1501
1793
|
guard heightBehavior == .autoGrow else { return }
|
|
1502
|
-
let
|
|
1794
|
+
let resolvedHeight = measuredHeight
|
|
1795
|
+
?? (cachedAutoGrowContentHeight > 0 ? cachedAutoGrowContentHeight : richTextView.intrinsicContentSize.height)
|
|
1796
|
+
let contentHeight = ceil(resolvedHeight)
|
|
1503
1797
|
guard contentHeight > 0 else { return }
|
|
1504
1798
|
guard force || abs(contentHeight - lastEmittedContentHeight) > 0.5 else { return }
|
|
1799
|
+
cachedAutoGrowContentHeight = contentHeight
|
|
1505
1800
|
lastEmittedContentHeight = contentHeight
|
|
1506
1801
|
onContentHeightChange(["contentHeight": contentHeight])
|
|
1507
1802
|
}
|
|
1508
1803
|
|
|
1509
1804
|
func setToolbarButtonsJson(_ toolbarButtonsJson: String?) {
|
|
1805
|
+
guard lastToolbarItemsJSON != toolbarButtonsJson else { return }
|
|
1806
|
+
lastToolbarItemsJSON = toolbarButtonsJson
|
|
1510
1807
|
toolbarItems = NativeToolbarItem.from(json: toolbarButtonsJson)
|
|
1511
1808
|
accessoryToolbar.setItems(toolbarItems)
|
|
1512
1809
|
refreshSystemAssistantToolbarIfNeeded()
|
|
1513
1810
|
}
|
|
1514
1811
|
|
|
1515
1812
|
func setToolbarFrameJson(_ toolbarFrameJson: String?) {
|
|
1813
|
+
guard lastToolbarFrameJSON != toolbarFrameJson else { return }
|
|
1814
|
+
lastToolbarFrameJSON = toolbarFrameJson
|
|
1516
1815
|
guard let toolbarFrameJson,
|
|
1517
1816
|
let data = toolbarFrameJson.data(using: .utf8),
|
|
1518
1817
|
let raw = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
@@ -1644,11 +1943,15 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1644
1943
|
// MARK: - EditorTextViewDelegate
|
|
1645
1944
|
|
|
1646
1945
|
func editorTextView(_ textView: EditorTextView, selectionDidChange anchor: UInt32, head: UInt32) {
|
|
1647
|
-
refreshToolbarStateFromEditorSelection()
|
|
1946
|
+
let stateJSON = refreshToolbarStateFromEditorSelection()
|
|
1648
1947
|
refreshSystemAssistantToolbarIfNeeded()
|
|
1649
1948
|
refreshMentionQuery()
|
|
1650
1949
|
richTextView.refreshRemoteSelections()
|
|
1651
|
-
|
|
1950
|
+
var event: [String: Any] = ["anchor": Int(anchor), "head": Int(head)]
|
|
1951
|
+
if let stateJSON {
|
|
1952
|
+
event["stateJson"] = stateJSON
|
|
1953
|
+
}
|
|
1954
|
+
onSelectionChange(event)
|
|
1652
1955
|
}
|
|
1653
1956
|
|
|
1654
1957
|
func editorTextView(_ textView: EditorTextView, didReceiveUpdate updateJSON: String) {
|
|
@@ -1663,12 +1966,14 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1663
1966
|
onEditorUpdate(["updateJson": updateJSON])
|
|
1664
1967
|
}
|
|
1665
1968
|
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1969
|
+
@discardableResult
|
|
1970
|
+
private func refreshToolbarStateFromEditorSelection() -> String? {
|
|
1971
|
+
guard richTextView.editorId != 0 else { return nil }
|
|
1972
|
+
let stateJSON = editorGetSelectionState(id: richTextView.editorId)
|
|
1973
|
+
guard let state = NativeToolbarState(updateJSON: stateJSON) else { return nil }
|
|
1670
1974
|
toolbarState = state
|
|
1671
1975
|
accessoryToolbar.apply(state: state)
|
|
1976
|
+
return stateJSON
|
|
1672
1977
|
}
|
|
1673
1978
|
|
|
1674
1979
|
private func configureAccessoryToolbar() {
|
|
@@ -1953,6 +2258,9 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1953
2258
|
case .mark:
|
|
1954
2259
|
guard let mark = item.mark else { return }
|
|
1955
2260
|
richTextView.textView.performToolbarToggleMark(mark)
|
|
2261
|
+
case .heading:
|
|
2262
|
+
guard let level = item.headingLevel else { return }
|
|
2263
|
+
richTextView.textView.performToolbarToggleHeading(level)
|
|
1956
2264
|
case .blockquote:
|
|
1957
2265
|
richTextView.textView.performToolbarToggleBlockquote()
|
|
1958
2266
|
case .list:
|
|
@@ -1977,6 +2285,8 @@ class NativeEditorExpoView: ExpoView, EditorTextViewDelegate, UIGestureRecognize
|
|
|
1977
2285
|
case .action:
|
|
1978
2286
|
guard let key = item.key else { return }
|
|
1979
2287
|
onToolbarAction(["key": key])
|
|
2288
|
+
case .group:
|
|
2289
|
+
break
|
|
1980
2290
|
case .separator:
|
|
1981
2291
|
break
|
|
1982
2292
|
}
|