@medway-ui/native 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/editor/NoteEditor.js +710 -0
- package/lib/commonjs/editor/NoteEditor.js.map +1 -0
- package/lib/commonjs/editor/editorHtml.generated.js +21 -0
- package/lib/commonjs/editor/editorHtml.generated.js.map +1 -0
- package/lib/commonjs/editor/index.js +33 -0
- package/lib/commonjs/editor/index.js.map +1 -0
- package/lib/commonjs/editor/protocol.js +88 -0
- package/lib/commonjs/editor/protocol.js.map +1 -0
- package/lib/module/editor/NoteEditor.js +705 -0
- package/lib/module/editor/NoteEditor.js.map +1 -0
- package/lib/module/editor/editorHtml.generated.js +17 -0
- package/lib/module/editor/editorHtml.generated.js.map +1 -0
- package/lib/module/editor/index.js +5 -0
- package/lib/module/editor/index.js.map +1 -0
- package/lib/module/editor/protocol.js +83 -0
- package/lib/module/editor/protocol.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/editor/NoteEditor.d.ts +82 -0
- package/lib/typescript/editor/NoteEditor.d.ts.map +1 -0
- package/lib/typescript/editor/editorHtml.generated.d.ts +13 -0
- package/lib/typescript/editor/editorHtml.generated.d.ts.map +1 -0
- package/lib/typescript/editor/index.d.ts +6 -0
- package/lib/typescript/editor/index.d.ts.map +1 -0
- package/lib/typescript/editor/protocol.d.ts +153 -0
- package/lib/typescript/editor/protocol.d.ts.map +1 -0
- package/package.json +24 -1
- package/setup/metro.js +27 -5
- package/src/editor/NoteEditor.tsx +802 -0
- package/src/editor/editorHtml.generated.ts +14 -0
- package/src/editor/index.ts +13 -0
- package/src/editor/protocol.ts +184 -0
- package/src/index.ts +6 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { default as NoteEditor } from "./NoteEditor";
|
|
2
|
+
export type { NoteEditorHandle } from "./NoteEditor";
|
|
3
|
+
export type { WebViewMessageEvent } from "react-native-webview";
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
EDITOR_PROTOCOL_VERSION,
|
|
7
|
+
EDITOR_RECEIVE_HANDLER,
|
|
8
|
+
isEditorMessage,
|
|
9
|
+
} from "./protocol";
|
|
10
|
+
export type {
|
|
11
|
+
EditorToNativeMessage,
|
|
12
|
+
NativeToEditorMessage,
|
|
13
|
+
} from "./protocol";
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport between React Native and the Tiptap editor inside the WebView. Both
|
|
3
|
+
* sides compile against this file — the bundle imports it through the `@/`
|
|
4
|
+
* alias in `editor-bundle/vite.config.ts`.
|
|
5
|
+
*
|
|
6
|
+
* Invariants:
|
|
7
|
+
* - The document never crosses the bridge on keystrokes: the editor emits
|
|
8
|
+
* `change` with a revision counter, and the native side asks with
|
|
9
|
+
* `getDocument`.
|
|
10
|
+
* - Every message is validated on arrival (`isEditorMessage`: envelope plus the
|
|
11
|
+
* required fields of its variant) — the WebView renders student content, so
|
|
12
|
+
* its output is untrusted input. Documents get a second pass through
|
|
13
|
+
* `normalizeDocument` before reaching the host.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
EditorCommand,
|
|
18
|
+
EditorDocument,
|
|
19
|
+
EditorSelectionState,
|
|
20
|
+
EditorTheme,
|
|
21
|
+
SlashCommandId,
|
|
22
|
+
SlashMenuState,
|
|
23
|
+
} from '@medway-ui/notes-core';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Bumped whenever the message shapes below change. The native side refuses to
|
|
27
|
+
* talk to a bundle that reports a different version, which turns a stale
|
|
28
|
+
* `editorHtml.generated.ts` into a clear error instead of silent breakage.
|
|
29
|
+
*/
|
|
30
|
+
export const EDITOR_PROTOCOL_VERSION = 3;
|
|
31
|
+
|
|
32
|
+
/** Name of the global the WebView bundle installs to receive native messages. */
|
|
33
|
+
export const EDITOR_RECEIVE_HANDLER = '__medwayNoteEditorReceive';
|
|
34
|
+
|
|
35
|
+
export type NativeToEditorMessage =
|
|
36
|
+
/**
|
|
37
|
+
* `reset` rebuilds the editor instance instead of replacing the running one's
|
|
38
|
+
* content — required when opening a *different* note, since replacing content
|
|
39
|
+
* carries the previous note's undo stack over.
|
|
40
|
+
*
|
|
41
|
+
* `doc` accepts HTML as well as JSON; parse (`generateJSON`) and whitelist
|
|
42
|
+
* (`normalizeDocument`) happen bundle-side, the only side with a DOM.
|
|
43
|
+
*
|
|
44
|
+
* `focusPosition` defaults to `"end"`. `imageDeleteGraceMs` seeds the bundle's
|
|
45
|
+
* image-removal bookkeeping; read once, not reactive.
|
|
46
|
+
*/
|
|
47
|
+
| {
|
|
48
|
+
type: 'setContent';
|
|
49
|
+
doc: EditorDocument | string;
|
|
50
|
+
focus: boolean;
|
|
51
|
+
focusPosition?: 'start' | 'end';
|
|
52
|
+
reset?: boolean;
|
|
53
|
+
imageDeleteGraceMs?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Read only on the `reset` path. Set here instead of creating editable
|
|
56
|
+
* and following up with `setEditable`, which would let a read-only note
|
|
57
|
+
* flash as editable for one frame. Defaults to `true`.
|
|
58
|
+
*/
|
|
59
|
+
editable?: boolean;
|
|
60
|
+
}
|
|
61
|
+
| { type: 'setEditable'; editable: boolean }
|
|
62
|
+
| { type: 'setTheme'; theme: EditorTheme }
|
|
63
|
+
/**
|
|
64
|
+
* `embedded` means the editor is inside a native scroll container: it stops
|
|
65
|
+
* filling the viewport and starts reporting `resize`, so React Native can
|
|
66
|
+
* size the WebView to the document instead of nesting two scrollers.
|
|
67
|
+
*/
|
|
68
|
+
| { type: 'setLayout'; embedded: boolean }
|
|
69
|
+
| { type: 'scrollCaretIntoView' }
|
|
70
|
+
| { type: 'command'; command: EditorCommand }
|
|
71
|
+
| { type: 'getDocument'; requestId: string }
|
|
72
|
+
| { type: 'runSlashCommand'; commandId: SlashCommandId }
|
|
73
|
+
| { type: 'dismissSlash' }
|
|
74
|
+
| { type: 'focus'; position: 'start' | 'end' }
|
|
75
|
+
| { type: 'blur' }
|
|
76
|
+
/**
|
|
77
|
+
* Inserts an image node at the current selection, once the host's
|
|
78
|
+
* `onImageUpload` resolved with a `src`. The bundle also starts tracking that
|
|
79
|
+
* `src` as "uploaded this session" — see `imageRemoved` below.
|
|
80
|
+
*/
|
|
81
|
+
| { type: 'insertImage'; src: string; alt?: string | null };
|
|
82
|
+
|
|
83
|
+
export type EditorToNativeMessage =
|
|
84
|
+
| { type: 'ready'; protocol: number }
|
|
85
|
+
| { type: 'state'; state: EditorSelectionState }
|
|
86
|
+
/** Content changed. `revision` increases monotonically, never resets. */
|
|
87
|
+
| { type: 'change'; revision: number }
|
|
88
|
+
| {
|
|
89
|
+
type: 'document';
|
|
90
|
+
requestId: string;
|
|
91
|
+
revision: number;
|
|
92
|
+
doc: EditorDocument;
|
|
93
|
+
}
|
|
94
|
+
| { type: 'slash'; slash: SlashMenuState }
|
|
95
|
+
/**
|
|
96
|
+
* Height of the rendered document, in CSS pixels. Only meaningful when
|
|
97
|
+
* embedded: the WebView does not scroll there, so React Native has to size it
|
|
98
|
+
* to its content.
|
|
99
|
+
*/
|
|
100
|
+
| { type: 'resize'; height: number }
|
|
101
|
+
/** A link inside the note was tapped; the native side decides what to open. */
|
|
102
|
+
| { type: 'linkPress'; href: string }
|
|
103
|
+
/**
|
|
104
|
+
* Backspace with the selection collapsed at the first cursor position of the
|
|
105
|
+
* document. `NoteEditor` uses this to hand focus back to its title field —
|
|
106
|
+
* the inverse of the title's own Enter handler.
|
|
107
|
+
*/
|
|
108
|
+
| { type: 'backspaceAtDocStart' }
|
|
109
|
+
/**
|
|
110
|
+
* A `src` that `insertImage` added this session left the document and stayed
|
|
111
|
+
* out past the grace period (`imageDeleteGraceMs`).
|
|
112
|
+
*/
|
|
113
|
+
| { type: 'imageRemoved'; src: string }
|
|
114
|
+
| { type: 'error'; message: string };
|
|
115
|
+
|
|
116
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
117
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
118
|
+
|
|
119
|
+
const isString = (value: unknown): value is string => typeof value === 'string';
|
|
120
|
+
|
|
121
|
+
/** Rejects `NaN` and the infinities, which `JSON.parse` can still produce. */
|
|
122
|
+
const isFiniteNumber = (value: unknown): value is number =>
|
|
123
|
+
typeof value === 'number' && Number.isFinite(value);
|
|
124
|
+
|
|
125
|
+
/*
|
|
126
|
+
* `Record<Union, …>` rather than an array so it is exhaustive: adding a variant
|
|
127
|
+
* to `EditorToNativeMessage` without listing it here stops compiling, instead
|
|
128
|
+
* of letting `isEditorMessage` reject the new type at runtime — which would
|
|
129
|
+
* look like the message never arrived, not like a build error.
|
|
130
|
+
*
|
|
131
|
+
* Each entry checks the fields its own variant declares, so narrowing to the
|
|
132
|
+
* union is backed by the payload and not by `type` alone: a `document` without
|
|
133
|
+
* `doc`/`requestId`, or a `ready` without `protocol`, is rejected here instead
|
|
134
|
+
* of blowing up at the use site.
|
|
135
|
+
*
|
|
136
|
+
* `state`, `slash` and `doc` are checked as objects rather than field by field:
|
|
137
|
+
* their shapes belong to `@medway-ui/notes-core` and are validated there — for
|
|
138
|
+
* `doc`, by the `normalizeDocument` pass the native side runs before handing a
|
|
139
|
+
* document to the host.
|
|
140
|
+
*/
|
|
141
|
+
const EDITOR_MESSAGE_VALIDATORS: Record<
|
|
142
|
+
EditorToNativeMessage['type'],
|
|
143
|
+
(message: Record<string, unknown>) => boolean
|
|
144
|
+
> = {
|
|
145
|
+
ready: (message) => isFiniteNumber(message.protocol),
|
|
146
|
+
state: (message) => isRecord(message.state),
|
|
147
|
+
change: (message) => isFiniteNumber(message.revision),
|
|
148
|
+
document: (message) =>
|
|
149
|
+
isString(message.requestId) &&
|
|
150
|
+
isFiniteNumber(message.revision) &&
|
|
151
|
+
isRecord(message.doc),
|
|
152
|
+
slash: (message) => isRecord(message.slash),
|
|
153
|
+
resize: (message) => isFiniteNumber(message.height),
|
|
154
|
+
linkPress: (message) => isString(message.href),
|
|
155
|
+
backspaceAtDocStart: () => true,
|
|
156
|
+
imageRemoved: (message) => isString(message.src),
|
|
157
|
+
error: (message) => isString(message.message),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Guard for anything arriving from the WebView: checks the envelope *and* the
|
|
162
|
+
* required fields of the variant its `type` names. Establishes the shape, not
|
|
163
|
+
* the contents — a `document` payload's `doc` still goes through
|
|
164
|
+
* `normalizeDocument` before the native side hands it to the host.
|
|
165
|
+
*/
|
|
166
|
+
export const isEditorMessage = (
|
|
167
|
+
value: unknown
|
|
168
|
+
): value is EditorToNativeMessage => {
|
|
169
|
+
if (!isRecord(value)) return false;
|
|
170
|
+
|
|
171
|
+
const { type } = value;
|
|
172
|
+
if (typeof type !== 'string') return false;
|
|
173
|
+
|
|
174
|
+
// `hasOwnProperty`, not `in`: `in` walks the prototype chain, so a `type` of
|
|
175
|
+
// `"toString"` or `"constructor"` matches any plain object literal.
|
|
176
|
+
// `Object.hasOwn` would read better but is ES2022 and `lib` here is ES2020.
|
|
177
|
+
if (!Object.prototype.hasOwnProperty.call(EDITOR_MESSAGE_VALIDATORS, type)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return EDITOR_MESSAGE_VALIDATORS[type as EditorToNativeMessage['type']](
|
|
182
|
+
value
|
|
183
|
+
);
|
|
184
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -392,3 +392,9 @@ export type { ThemeColorToken } from "@/hooks/useThemeColor";
|
|
|
392
392
|
// Theme-aware icon helper
|
|
393
393
|
export { ThemedIcon } from "@/components/ThemedIcon";
|
|
394
394
|
export type { ThemedIconProps } from "@/components/ThemedIcon";
|
|
395
|
+
|
|
396
|
+
// NoteEditor is deliberately absent from this barrel: it pulls in
|
|
397
|
+
// `react-native-webview` (an optional peer) and the 428 KB inlined editor HTML,
|
|
398
|
+
// which Metro does not reliably tree-shake — every consumer of this package
|
|
399
|
+
// would carry both. It ships as the `@medway-ui/native/note-editor` subpath
|
|
400
|
+
// instead. Message types alone live at `@medway-ui/native/editor/protocol`.
|