@canvist/react-native 0.0.0 → 0.2.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/CanvistReactNative.podspec +31 -0
- package/android/build.gradle +50 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/kotlin/io/github/ifiokjr/canvist/reactnative/CanvistEditorEvent.kt +32 -0
- package/android/src/main/kotlin/io/github/ifiokjr/canvist/reactnative/CanvistEditorViewManager.kt +145 -0
- package/android/src/main/kotlin/io/github/ifiokjr/canvist/reactnative/CanvistEditorsModule.kt +81 -0
- package/android/src/main/kotlin/io/github/ifiokjr/canvist/reactnative/CanvistReactNativePackage.kt +33 -0
- package/dist/CanvistEditorViewNativeComponent.d.ts +52 -0
- package/dist/NativeCanvistEditors.d.ts +31 -0
- package/dist/bridge.d.ts +18 -0
- package/dist/editor.d.ts +91 -0
- package/dist/mod.d.ts +27 -0
- package/ios/CanvistEditorComponentView.h +10 -0
- package/ios/CanvistEditorComponentView.mm +132 -0
- package/ios/CanvistEditorsModule.h +10 -0
- package/ios/CanvistEditorsModule.mm +101 -0
- package/ios/CanvistSwift.h +3 -0
- package/package.json +94 -1
- package/readme.md +105 -0
- package/src/CanvistEditorViewNativeComponent.ts +63 -0
- package/src/NativeCanvistEditors.ts +33 -0
- package/src/bridge.ts +64 -0
- package/src/editor.tsx +268 -0
- package/src/mod.ts +61 -0
- package/CHANGELOG.md +0 -3
- package/LICENSE +0 -24
- package/README.md +0 -3
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#import "CanvistEditorComponentView.h"
|
|
2
|
+
|
|
3
|
+
#import <react/renderer/components/CanvistReactNativeSpec/ComponentDescriptors.h>
|
|
4
|
+
#import <react/renderer/components/CanvistReactNativeSpec/EventEmitters.h>
|
|
5
|
+
#import <react/renderer/components/CanvistReactNativeSpec/Props.h>
|
|
6
|
+
#import <react/renderer/components/CanvistReactNativeSpec/RCTComponentViewHelpers.h>
|
|
7
|
+
|
|
8
|
+
#import "CanvistSwift.h"
|
|
9
|
+
|
|
10
|
+
using namespace facebook::react;
|
|
11
|
+
|
|
12
|
+
@interface CanvistEditorComponentView () <RCTCanvistEditorViewViewProtocol>
|
|
13
|
+
@end
|
|
14
|
+
|
|
15
|
+
@implementation CanvistEditorComponentView {
|
|
16
|
+
CanvistHostView *_host;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
20
|
+
{
|
|
21
|
+
return concreteComponentDescriptorProvider<CanvistEditorViewComponentDescriptor>();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Each view owns one editor for its whole life, created from its first props.
|
|
25
|
+
+ (BOOL)shouldBeRecycled
|
|
26
|
+
{
|
|
27
|
+
return NO;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
31
|
+
{
|
|
32
|
+
if (self = [super initWithFrame:frame]) {
|
|
33
|
+
static const auto defaultProps = std::make_shared<const CanvistEditorViewProps>();
|
|
34
|
+
_props = defaultProps;
|
|
35
|
+
_host = [CanvistHostView new];
|
|
36
|
+
self.contentView = _host;
|
|
37
|
+
|
|
38
|
+
__weak CanvistEditorComponentView *weakSelf = self;
|
|
39
|
+
_host.onReady = ^(NSString *editorId) {
|
|
40
|
+
if (auto emitter = [weakSelf editorEventEmitter]) {
|
|
41
|
+
emitter->onEditorReady({.editorId = std::string(editorId.UTF8String)});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
_host.onChange = ^(BOOL document, BOOL selection, BOOL composition) {
|
|
45
|
+
if (auto emitter = [weakSelf editorEventEmitter]) {
|
|
46
|
+
emitter->onEditorChange({.document = (bool)document, .selection = (bool)selection, .composition = (bool)composition});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
_host.onFocusChange = ^(BOOL focused) {
|
|
50
|
+
if (auto emitter = [weakSelf editorEventEmitter]) {
|
|
51
|
+
emitter->onEditorFocus({.focused = (bool)focused});
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
_host.onError = ^(NSString *code, NSString *message) {
|
|
55
|
+
if (auto emitter = [weakSelf editorEventEmitter]) {
|
|
56
|
+
emitter->onEditorError({.code = std::string(code.UTF8String), .message = std::string(message.UTF8String)});
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return self;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
- (void)dealloc
|
|
65
|
+
{
|
|
66
|
+
[_host stop];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
- (std::shared_ptr<const CanvistEditorViewEventEmitter>)editorEventEmitter
|
|
70
|
+
{
|
|
71
|
+
return std::static_pointer_cast<const CanvistEditorViewEventEmitter>(_eventEmitter);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
75
|
+
{
|
|
76
|
+
const auto &oldViewProps = *std::static_pointer_cast<const CanvistEditorViewProps>(_props);
|
|
77
|
+
const auto &newViewProps = *std::static_pointer_cast<const CanvistEditorViewProps>(props);
|
|
78
|
+
|
|
79
|
+
if (oldViewProps.fontSize != newViewProps.fontSize) {
|
|
80
|
+
_host.fontSize = newViewProps.fontSize;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (oldViewProps.accessibilityLabel != newViewProps.accessibilityLabel) {
|
|
84
|
+
_host.accessibilityLabel = newViewProps.accessibilityLabel.empty()
|
|
85
|
+
? nil
|
|
86
|
+
: [NSString stringWithUTF8String:newViewProps.accessibilityLabel.c_str()];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
[super updateProps:props oldProps:oldProps];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The editor starts once the first transaction has mounted, when the event
|
|
93
|
+
// emitter that announces it exists.
|
|
94
|
+
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
|
|
95
|
+
{
|
|
96
|
+
[super finalizeUpdates:updateMask];
|
|
97
|
+
|
|
98
|
+
const auto &props = *std::static_pointer_cast<const CanvistEditorViewProps>(_props);
|
|
99
|
+
|
|
100
|
+
if (props.editorId.empty()) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
NSMutableArray<NSString *> *extensions = [NSMutableArray arrayWithCapacity:props.extensions.size()];
|
|
105
|
+
for (const auto &name : props.extensions) {
|
|
106
|
+
[extensions addObject:[NSString stringWithUTF8String:name.c_str()]];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
NSString *format = props.contentFormat.empty() ? nil : [NSString stringWithUTF8String:props.contentFormat.c_str()];
|
|
110
|
+
NSString *content = props.content.empty() ? nil : [NSString stringWithUTF8String:props.content.c_str()];
|
|
111
|
+
[_host startWithEditorId:[NSString stringWithUTF8String:props.editorId.c_str()]
|
|
112
|
+
extensions:extensions
|
|
113
|
+
format:format
|
|
114
|
+
content:content];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
|
|
118
|
+
{
|
|
119
|
+
RCTCanvistEditorViewHandleCommand(self, commandName, args);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
- (void)focus
|
|
123
|
+
{
|
|
124
|
+
[_host focus];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
- (void)blur
|
|
128
|
+
{
|
|
129
|
+
[_host blur];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#import <CanvistReactNativeSpec/CanvistReactNativeSpec.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
/// The `CanvistEditors` TurboModule: synchronous calls into the editors of
|
|
6
|
+
/// mounted `CanvistEditorView`s.
|
|
7
|
+
@interface CanvistEditorsModule : NSObject <NativeCanvistEditorsSpec>
|
|
8
|
+
@end
|
|
9
|
+
|
|
10
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#import "CanvistEditorsModule.h"
|
|
2
|
+
|
|
3
|
+
#import "CanvistSwift.h"
|
|
4
|
+
|
|
5
|
+
// Every method runs synchronously on the JavaScript thread. A failure raises
|
|
6
|
+
// an exception, which React Native rethrows in JavaScript with its reason:
|
|
7
|
+
// `[canvist:<code>] <message>`.
|
|
8
|
+
static id Checked(id result, NSError *error)
|
|
9
|
+
{
|
|
10
|
+
if (result == nil) {
|
|
11
|
+
@throw [NSException exceptionWithName:@"CanvistError" reason:error.localizedDescription userInfo:nil];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#define CANVIST_CALL(expression) \
|
|
18
|
+
({ \
|
|
19
|
+
NSError *error = nil; \
|
|
20
|
+
id result = [CanvistHostCalls expression]; \
|
|
21
|
+
Checked(result, error); \
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
@implementation CanvistEditorsModule
|
|
25
|
+
|
|
26
|
+
+ (NSString *)moduleName
|
|
27
|
+
{
|
|
28
|
+
return @"CanvistEditors";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
32
|
+
{
|
|
33
|
+
return std::make_shared<facebook::react::NativeCanvistEditorsSpecJSI>(params);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
- (NSNumber *)run:(NSString *)editorId name:(NSString *)name args:(NSString *)args
|
|
37
|
+
{
|
|
38
|
+
return CANVIST_CALL(runWithId:editorId name:name args:args error:&error);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
- (NSNumber *)can:(NSString *)editorId name:(NSString *)name args:(NSString *)args
|
|
42
|
+
{
|
|
43
|
+
return CANVIST_CALL(canWithId:editorId name:name args:args error:&error);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
- (NSString *)query:(NSString *)editorId name:(NSString *)name args:(NSString *)args
|
|
47
|
+
{
|
|
48
|
+
return CANVIST_CALL(queryWithId:editorId name:name args:args error:&error);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
- (NSNumber *)load:(NSString *)editorId format:(NSString *)format content:(NSString *)content
|
|
52
|
+
{
|
|
53
|
+
return CANVIST_CALL(loadWithId:editorId format:format content:content error:&error);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
- (NSString *)exportDocument:(NSString *)editorId format:(NSString *)format
|
|
57
|
+
{
|
|
58
|
+
return CANVIST_CALL(exportWithId:editorId format:format error:&error);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
- (NSString *)selection:(NSString *)editorId
|
|
62
|
+
{
|
|
63
|
+
return CANVIST_CALL(selectionWithId:editorId error:&error);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
- (NSNumber *)select:(NSString *)editorId range:(NSString *)range
|
|
67
|
+
{
|
|
68
|
+
return CANVIST_CALL(selectWithId:editorId range:range error:&error);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
- (NSNumber *)selectAll:(NSString *)editorId
|
|
72
|
+
{
|
|
73
|
+
return CANVIST_CALL(selectAllWithId:editorId error:&error);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
- (NSNumber *)insertText:(NSString *)editorId text:(NSString *)text
|
|
77
|
+
{
|
|
78
|
+
return CANVIST_CALL(insertTextWithId:editorId text:text error:&error);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
- (NSNumber *)undo:(NSString *)editorId
|
|
82
|
+
{
|
|
83
|
+
return CANVIST_CALL(undoWithId:editorId error:&error);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
- (NSNumber *)redo:(NSString *)editorId
|
|
87
|
+
{
|
|
88
|
+
return CANVIST_CALL(redoWithId:editorId error:&error);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
- (NSNumber *)canUndo:(NSString *)editorId
|
|
92
|
+
{
|
|
93
|
+
return CANVIST_CALL(canUndoWithId:editorId error:&error);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
- (NSNumber *)canRedo:(NSString *)editorId
|
|
97
|
+
{
|
|
98
|
+
return CANVIST_CALL(canRedoWithId:editorId error:&error);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@end
|
package/package.json
CHANGED
|
@@ -1 +1,94 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@canvist/react-native",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "The canvist rich-text editor for React Native: the native iOS and Android editors as a Fabric component, with commands and queries typed from the extensions you register.",
|
|
5
|
+
"license": "Unlicense",
|
|
6
|
+
"homepage": "https://github.com/canvist/canvist/tree/main/packages/canvist-react-native#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/canvist/canvist.git",
|
|
10
|
+
"directory": "packages/canvist-react-native"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/canvist/canvist/issues",
|
|
14
|
+
"email": "support@canvist.dev"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"react-native",
|
|
18
|
+
"rich-text",
|
|
19
|
+
"editor",
|
|
20
|
+
"ios",
|
|
21
|
+
"android",
|
|
22
|
+
"fabric"
|
|
23
|
+
],
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/mod.d.ts",
|
|
28
|
+
"react-native": "./src/mod.ts"
|
|
29
|
+
},
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"src",
|
|
35
|
+
"ios",
|
|
36
|
+
"android/build.gradle",
|
|
37
|
+
"android/src/main",
|
|
38
|
+
"CanvistReactNative.podspec"
|
|
39
|
+
],
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"scripts": {
|
|
42
|
+
"check:types": "tsc --noEmit",
|
|
43
|
+
"test": "jest",
|
|
44
|
+
"build": "tsc -p tsconfig.build.json",
|
|
45
|
+
"prepack": "npm run build"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@canvist/headless": "^0.2.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"react": "^19.0.0",
|
|
52
|
+
"react-native": ">=0.80.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@react-native/babel-preset": "0.88.0",
|
|
56
|
+
"@react-native/jest-preset": "0.88.0",
|
|
57
|
+
"@testing-library/react-native": "^14.1.0",
|
|
58
|
+
"@types/jest": "^29.5.14",
|
|
59
|
+
"@types/react": "^19.2.0",
|
|
60
|
+
"jest": "^29.7.0",
|
|
61
|
+
"react": "19.3.0",
|
|
62
|
+
"react-native": "0.88.0",
|
|
63
|
+
"test-renderer": "^1.3.1",
|
|
64
|
+
"typescript": "^7.0.2"
|
|
65
|
+
},
|
|
66
|
+
"codegenConfig": {
|
|
67
|
+
"name": "CanvistReactNativeSpec",
|
|
68
|
+
"type": "all",
|
|
69
|
+
"jsSrcsDir": "src",
|
|
70
|
+
"android": {
|
|
71
|
+
"javaPackageName": "io.github.ifiokjr.canvist.reactnative"
|
|
72
|
+
},
|
|
73
|
+
"ios": {
|
|
74
|
+
"componentProvider": {
|
|
75
|
+
"CanvistEditorView": "CanvistEditorComponentView"
|
|
76
|
+
},
|
|
77
|
+
"modulesProvider": {
|
|
78
|
+
"CanvistEditors": "CanvistEditorsModule"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"jest": {
|
|
83
|
+
"preset": "@react-native/jest-preset",
|
|
84
|
+
"testMatch": [
|
|
85
|
+
"<rootDir>/tests/**/*.test.ts?(x)"
|
|
86
|
+
],
|
|
87
|
+
"transformIgnorePatterns": [
|
|
88
|
+
"node_modules/(?!((jest-)?react-native|@react-native(-community)?|@canvist)/)"
|
|
89
|
+
],
|
|
90
|
+
"modulePaths": [
|
|
91
|
+
"<rootDir>/node_modules"
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/canvist/canvist">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/canvist/canvist/main/assets/brand/vector/lockup-dark.svg">
|
|
5
|
+
<img width="220" src="https://raw.githubusercontent.com/canvist/canvist/main/assets/brand/vector/lockup.svg" alt="canvist" />
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<a href="https://github.com/canvist/canvist/blob/main/docs/src/quick-start/react-native.md"><strong>Quick start</strong></a> ·
|
|
12
|
+
<a href="https://github.com/canvist/canvist/blob/main/docs/src/readme.md"><strong>Docs</strong></a> ·
|
|
13
|
+
<a href="https://github.com/canvist/canvist/blob/main/docs/src/reference/contributing.md"><strong>Contributing</strong></a> ·
|
|
14
|
+
<a href="https://github.com/canvist/canvist"><strong>canvist</strong></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="https://github.com/canvist/canvist/actions/workflows/react-native.yml"><img src="https://github.com/canvist/canvist/actions/workflows/react-native.yml/badge.svg?branch=main" alt="react-native status" /></a>
|
|
19
|
+
<a href="https://unlicense.org"><img src="https://img.shields.io/badge/license-Unlicense-3B2CF5" alt="Unlicense" /></a>
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
# @canvist/react-native
|
|
23
|
+
|
|
24
|
+
canvist's rich-text editor for React Native: the native iOS and Android `CanvistView`s as a Fabric component, not a WebView. Typing goes through each platform's own text input (the keyboard, input methods, autocorrect, dictation, selection handles), and the Rust engine lays out and paints every line. Commands, queries, and documents are typed from the extensions you register, with the same API as [`@canvist/web`](../canvist-web).
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { CanvistEditor } from "@canvist/react-native";
|
|
28
|
+
|
|
29
|
+
<CanvistEditor
|
|
30
|
+
content={{ format: "markdown", input: "# Notes" }}
|
|
31
|
+
onUpdate={(editor) => save(editor.export("markdown"))}
|
|
32
|
+
accessibilityLabel="Note"
|
|
33
|
+
style={{ flex: 1 }}
|
|
34
|
+
/>;
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Needs React Native 0.80 or newer with the New Architecture, iOS 17, and Android 7 (API 24).
|
|
38
|
+
|
|
39
|
+
## A toolbar that follows the selection
|
|
40
|
+
|
|
41
|
+
`useCanvistEditor` gives you the editor and the props for its view; `useEditorState` re-renders when something you read changes. Calls are synchronous, so a query answers at once:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
import {
|
|
45
|
+
bold,
|
|
46
|
+
CanvistView,
|
|
47
|
+
heading,
|
|
48
|
+
useCanvistEditor,
|
|
49
|
+
useEditorState,
|
|
50
|
+
} from "@canvist/react-native";
|
|
51
|
+
|
|
52
|
+
function Notes() {
|
|
53
|
+
const { editor, props } = useCanvistEditor({ extensions: [bold, heading] });
|
|
54
|
+
const isBold = useEditorState(editor, (e) => e.query.isBold(), false);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
<Button
|
|
59
|
+
title={isBold ? "Unbold" : "Bold"}
|
|
60
|
+
onPress={() => editor?.commands.toggleBold()}
|
|
61
|
+
/>
|
|
62
|
+
<CanvistView {...props} style={{ flex: 1 }} />
|
|
63
|
+
</>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Only registered extensions have commands: with `[bold, heading]`, `editor.commands.toggleItalic()` is a type error, and so is `setHeading({ level: 7 })`.
|
|
69
|
+
|
|
70
|
+
## The editor
|
|
71
|
+
|
|
72
|
+
`editor` is `null` until the native view has created its editor. Then it offers:
|
|
73
|
+
|
|
74
|
+
- `commands`, `can`, and `query`, one method per registered command and query, plus `run`, `canRun`, and `ask` by name.
|
|
75
|
+
- `setContent(content)`, `export(format)` (`"json"`, `"html"`, `"markdown"`, or `"text"`), and `toJson()`.
|
|
76
|
+
- `selection`, `select(anchor, head)`, `selectAll()`, and `insertText(text)`, which types as the keyboard does.
|
|
77
|
+
- `undo()`, `redo()`, `canUndo()`, and `canRedo()`.
|
|
78
|
+
- `focus()`, `blur()`, and `focused`.
|
|
79
|
+
- `subscribe(listener)`, called after every change to the document, the selection, or focus.
|
|
80
|
+
|
|
81
|
+
Failures throw a `CanvistError` with a stable `code`, as on every other platform.
|
|
82
|
+
|
|
83
|
+
## Install
|
|
84
|
+
|
|
85
|
+
The native editors aren't on a package registry yet, so build them from the repository. For iOS, add the `Canvist` pod to your Podfile after building it with `platforms/apple/build.sh`:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
pod "Canvist", path: "path/to/canvist/platforms/apple"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For Android, the package depends on `io.github.ifiokjr:canvist-android`; until it's on Maven Central, publish it to Maven Local with `platforms/android/build.sh` and `./gradlew :canvist:publishToMavenLocal`, and add `mavenLocal()` to your repositories. [`examples/react-native`](../../examples/react-native) is set up this way.
|
|
92
|
+
|
|
93
|
+
<details>
|
|
94
|
+
<summary><strong>Development</strong></summary>
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
npm test # Jest: the typed editor, the view's props and events
|
|
98
|
+
npx tsc --noEmit # types, including the type-level tests
|
|
99
|
+
../../examples/react-native/e2e/test.sh ios # the example in Release on a simulator, driven by Maestro
|
|
100
|
+
../../examples/react-native/e2e/test.sh android
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The package's `react-native` export is its TypeScript source: React Native's codegen reads the Fabric and TurboModule specs in `src/` when an app builds.
|
|
104
|
+
|
|
105
|
+
</details>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Fabric component spec for the native editor view: React Native's
|
|
3
|
+
* codegen reads this file to generate the iOS and Android glue, so it keeps
|
|
4
|
+
* to the shapes codegen understands.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import type * as React from "react";
|
|
9
|
+
import type { CodegenTypes, HostComponent, ViewProps } from "react-native";
|
|
10
|
+
import { codegenNativeCommands, codegenNativeComponent } from "react-native";
|
|
11
|
+
|
|
12
|
+
/** What changed, after every change to the document or selection. */
|
|
13
|
+
export type EditorChangeEvent = Readonly<{
|
|
14
|
+
document: boolean;
|
|
15
|
+
selection: boolean;
|
|
16
|
+
composition: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
/** The view gained or lost focus. */
|
|
20
|
+
export type EditorFocusEvent = Readonly<{
|
|
21
|
+
focused: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
/** The view could not create or load its editor. */
|
|
25
|
+
export type EditorErrorEvent = Readonly<{
|
|
26
|
+
code: string;
|
|
27
|
+
message: string;
|
|
28
|
+
}>;
|
|
29
|
+
|
|
30
|
+
/** The view is ready for calls through the `CanvistEditors` module. */
|
|
31
|
+
export type EditorReadyEvent = Readonly<{
|
|
32
|
+
editorId: string;
|
|
33
|
+
}>;
|
|
34
|
+
|
|
35
|
+
export interface NativeProps extends ViewProps {
|
|
36
|
+
/** The key the `CanvistEditors` module finds this view's editor by. */
|
|
37
|
+
editorId: string;
|
|
38
|
+
/** The extensions to register, by name. Read when the view is created. */
|
|
39
|
+
extensions: ReadonlyArray<string>;
|
|
40
|
+
/** The format of `content`. Read when the view is created. */
|
|
41
|
+
contentFormat?: string;
|
|
42
|
+
/** The document to start with. Read when the view is created. */
|
|
43
|
+
content?: string;
|
|
44
|
+
/** The body text size before the user's text size setting; 0 is the platform's default. */
|
|
45
|
+
fontSize?: CodegenTypes.WithDefault<CodegenTypes.Float, 0>;
|
|
46
|
+
onEditorReady?: CodegenTypes.DirectEventHandler<EditorReadyEvent>;
|
|
47
|
+
onEditorChange?: CodegenTypes.DirectEventHandler<EditorChangeEvent>;
|
|
48
|
+
onEditorFocus?: CodegenTypes.DirectEventHandler<EditorFocusEvent>;
|
|
49
|
+
onEditorError?: CodegenTypes.DirectEventHandler<EditorErrorEvent>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type CanvistEditorViewType = HostComponent<NativeProps>;
|
|
53
|
+
|
|
54
|
+
interface NativeCommands {
|
|
55
|
+
focus: (viewRef: React.ElementRef<CanvistEditorViewType>) => void;
|
|
56
|
+
blur: (viewRef: React.ElementRef<CanvistEditorViewType>) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
|
|
60
|
+
supportedCommands: ["focus", "blur"],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export default codegenNativeComponent<NativeProps>("CanvistEditorView");
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The TurboModule spec for calls into a view's editor. Every method is
|
|
3
|
+
* synchronous on the JavaScript thread, so commands and queries answer at
|
|
4
|
+
* once, as they do on the web. Methods return a value even when there is
|
|
5
|
+
* nothing to say, because React Native runs `void` methods asynchronously.
|
|
6
|
+
*
|
|
7
|
+
* Failures throw with `[canvist:<code>] <reason>` in the message.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import type { TurboModule } from "react-native";
|
|
12
|
+
import { TurboModuleRegistry } from "react-native";
|
|
13
|
+
|
|
14
|
+
export interface Spec extends TurboModule {
|
|
15
|
+
run(editorId: string, name: string, args: string): boolean;
|
|
16
|
+
can(editorId: string, name: string, args: string): boolean;
|
|
17
|
+
query(editorId: string, name: string, args: string): string;
|
|
18
|
+
/** Returns `true`. */
|
|
19
|
+
load(editorId: string, format: string, content: string): boolean;
|
|
20
|
+
exportDocument(editorId: string, format: string): string;
|
|
21
|
+
selection(editorId: string): string;
|
|
22
|
+
/** Returns whether the selection moved. */
|
|
23
|
+
select(editorId: string, range: string): boolean;
|
|
24
|
+
/** Returns whether the selection moved. */
|
|
25
|
+
selectAll(editorId: string): boolean;
|
|
26
|
+
insertText(editorId: string, text: string): boolean;
|
|
27
|
+
undo(editorId: string): boolean;
|
|
28
|
+
redo(editorId: string): boolean;
|
|
29
|
+
canUndo(editorId: string): boolean;
|
|
30
|
+
canRedo(editorId: string): boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default TurboModuleRegistry.getEnforcing<Spec>("CanvistEditors");
|
package/src/bridge.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bridge from the typed editor to one native view: synchronous calls
|
|
3
|
+
* through the `CanvistEditors` TurboModule, and focus through the view's
|
|
4
|
+
* commands.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
import type { NativeBridge } from "@canvist/headless/native";
|
|
9
|
+
import type * as React from "react";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type CanvistEditorViewType,
|
|
13
|
+
Commands,
|
|
14
|
+
} from "./CanvistEditorViewNativeComponent.ts";
|
|
15
|
+
import type { Spec } from "./NativeCanvistEditors.ts";
|
|
16
|
+
|
|
17
|
+
/** The mounted native view, as React refers to it. */
|
|
18
|
+
export type EditorViewInstance = React.ElementRef<CanvistEditorViewType>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A bridge to the editor `editorId` names through `module`, focusing the
|
|
22
|
+
* view `view` returns. Focus calls do nothing once the view is gone.
|
|
23
|
+
*/
|
|
24
|
+
export function nativeBridge(
|
|
25
|
+
module: Spec,
|
|
26
|
+
editorId: string,
|
|
27
|
+
view: () => EditorViewInstance | null,
|
|
28
|
+
): NativeBridge {
|
|
29
|
+
return {
|
|
30
|
+
run: (name, args) => module.run(editorId, name, args),
|
|
31
|
+
can: (name, args) => module.can(editorId, name, args),
|
|
32
|
+
query: (name, args) => module.query(editorId, name, args),
|
|
33
|
+
load: (format, content) => {
|
|
34
|
+
module.load(editorId, format, content);
|
|
35
|
+
},
|
|
36
|
+
export: (format) => module.exportDocument(editorId, format),
|
|
37
|
+
selection: () => module.selection(editorId),
|
|
38
|
+
select: (range) => {
|
|
39
|
+
module.select(editorId, range);
|
|
40
|
+
},
|
|
41
|
+
selectAll: () => {
|
|
42
|
+
module.selectAll(editorId);
|
|
43
|
+
},
|
|
44
|
+
insertText: (text) => module.insertText(editorId, text),
|
|
45
|
+
undo: () => module.undo(editorId),
|
|
46
|
+
redo: () => module.redo(editorId),
|
|
47
|
+
canUndo: () => module.canUndo(editorId),
|
|
48
|
+
canRedo: () => module.canRedo(editorId),
|
|
49
|
+
focus: () => {
|
|
50
|
+
const instance = view();
|
|
51
|
+
|
|
52
|
+
if (instance !== null) {
|
|
53
|
+
Commands.focus(instance);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
blur: () => {
|
|
57
|
+
const instance = view();
|
|
58
|
+
|
|
59
|
+
if (instance !== null) {
|
|
60
|
+
Commands.blur(instance);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|