@octane-xplat/tiptap 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -3
- package/dist/macos/browser.d.ts +1 -0
- package/dist/macos/browser.js +2 -0
- package/dist/native/index3.js +46 -25
- package/dist/native/octane-client-build.json +1 -1
- package/dist/web/index2.js +43 -32
- package/dist/web/octane-client-build.json +1 -1
- package/package.json +9 -6
- package/src/Editor.macos.tsx +9 -0
- package/src/TiptapEditor.mobile.test.ts +229 -0
- package/src/TiptapEditor.tsrx +59 -25
- package/src/TiptapEditor.web.test.tsrx +129 -0
- package/src/TiptapEditor.web.tsrx +43 -25
- package/src/browser-entry.web.tsx +5 -0
- package/src/index.macos.ts +7 -13
- package/src/types.ts +34 -2
- package/types/index.macos.d.ts +7 -0
- package/types/types.d.ts +32 -2
package/dist/native/index3.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { ensureJSONBridge, htmlToJSON, jsonBridgeReady, jsonToHTML } from "./index2.js";
|
|
2
|
-
import { defineUniversalComponent, hookSlots, universalComponent, universalPlan, universalProps, universalValue, useCallback, useEffect, useRef } from "@nativescript-community/octane";
|
|
2
|
+
import { defineUniversalComponent, hookSlots, universalComponent, universalPlan, universalProps, universalValue, useCallback, useEffect, useImperativeHandle, useRef, useState } from "@nativescript-community/octane";
|
|
3
3
|
import { RichTextEditor, supported } from "@octane-xplat/richtext";
|
|
4
4
|
//#region src/TiptapEditor.tsrx
|
|
5
|
-
var _hs$ = /* @__PURE__ */ hookSlots(
|
|
5
|
+
var _hs$ = /* @__PURE__ */ hookSlots(9);
|
|
6
6
|
var _h$0 = /* @__PURE__ */ Symbol(_hs$);
|
|
7
7
|
var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
|
|
8
8
|
var _h$2 = /* @__PURE__ */ Symbol(_hs$ + 2);
|
|
9
9
|
var _h$3 = /* @__PURE__ */ Symbol(_hs$ + 3);
|
|
10
10
|
var _h$4 = /* @__PURE__ */ Symbol(_hs$ + 4);
|
|
11
11
|
var _h$5 = /* @__PURE__ */ Symbol(_hs$ + 5);
|
|
12
|
+
var _h$6 = /* @__PURE__ */ Symbol(_hs$ + 6);
|
|
13
|
+
var _h$7 = /* @__PURE__ */ Symbol(_hs$ + 7);
|
|
14
|
+
var _h$8 = /* @__PURE__ */ Symbol(_hs$ + 8);
|
|
12
15
|
var __octaneUniversalPlan0 = universalPlan("nativescript", {
|
|
13
16
|
"kind": "slot",
|
|
14
17
|
"slot": 0
|
|
@@ -16,20 +19,34 @@ var __octaneUniversalPlan0 = universalPlan("nativescript", {
|
|
|
16
19
|
var supported$1 = supported;
|
|
17
20
|
var TiptapEditor = defineUniversalComponent("nativescript", function TiptapEditor(props) {
|
|
18
21
|
const inner = useRef(null, _h$0);
|
|
19
|
-
const
|
|
20
|
-
|
|
22
|
+
const [exposedHandle, setHandle] = useState(null, _h$1);
|
|
23
|
+
useImperativeHandle(props.ref, () => exposedHandle, [exposedHandle], _h$2);
|
|
24
|
+
const lastJSON = useRef(void 0, _h$3);
|
|
25
|
+
const appliedHost = useRef(null, _h$4);
|
|
26
|
+
const desiredDoc = useRef(void 0, _h$5);
|
|
27
|
+
const applyDoc = () => {
|
|
28
|
+
const host = inner.current;
|
|
29
|
+
const doc = desiredDoc.current;
|
|
30
|
+
if (!host || doc === void 0 || !jsonBridgeReady()) return;
|
|
31
|
+
const stamp = JSON.stringify(doc);
|
|
32
|
+
if (stamp === lastJSON.current && appliedHost.current === host) return;
|
|
33
|
+
const html = jsonToHTML(doc);
|
|
34
|
+
if (html != null) {
|
|
35
|
+
lastJSON.current = stamp;
|
|
36
|
+
appliedHost.current = host;
|
|
37
|
+
host.setHTML(html);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
21
40
|
const bindHost = useCallback((h) => {
|
|
22
41
|
inner.current = h;
|
|
23
|
-
|
|
42
|
+
if (h) queueMicrotask(applyDoc);
|
|
43
|
+
setHandle((previous) => previous?.native === h?.native ? previous : h ? {
|
|
24
44
|
getHTML: () => h.getHTML(),
|
|
25
45
|
setHTML: (html) => h.setHTML(html),
|
|
26
46
|
getJSON: () => jsonBridgeReady() ? htmlToJSON(h.getHTML()) : null,
|
|
27
47
|
setJSON: (doc) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
lastJSON.current = JSON.stringify(doc);
|
|
31
|
-
h.setHTML(html);
|
|
32
|
-
}
|
|
48
|
+
desiredDoc.current = doc;
|
|
49
|
+
applyDoc();
|
|
33
50
|
},
|
|
34
51
|
apply: (format) => h.apply(format),
|
|
35
52
|
linkTo: (url, anchor) => h.linkTo(url, anchor),
|
|
@@ -41,29 +58,30 @@ var TiptapEditor = defineUniversalComponent("nativescript", function TiptapEdito
|
|
|
41
58
|
blur: () => h.blur(),
|
|
42
59
|
isFocused: () => h.isFocused(),
|
|
43
60
|
native: h.native
|
|
44
|
-
});
|
|
45
|
-
}, [], _h$
|
|
61
|
+
} : null);
|
|
62
|
+
}, [], _h$6);
|
|
46
63
|
useEffect(() => {
|
|
64
|
+
let alive = true;
|
|
47
65
|
ensureJSONBridge().then((ok) => {
|
|
48
|
-
|
|
66
|
+
if (!alive) return;
|
|
49
67
|
props.onJSONReady?.(ok);
|
|
68
|
+
applyDoc();
|
|
50
69
|
});
|
|
51
|
-
|
|
70
|
+
return () => {
|
|
71
|
+
alive = false;
|
|
72
|
+
};
|
|
73
|
+
}, [], _h$7);
|
|
52
74
|
useEffect(() => {
|
|
53
|
-
if (!props.json
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (html != null) {
|
|
58
|
-
lastJSON.current = stamp;
|
|
59
|
-
inner.current.setHTML(html);
|
|
60
|
-
}
|
|
61
|
-
}, [props.json], _h$5);
|
|
75
|
+
if (!props.json) return;
|
|
76
|
+
desiredDoc.current = props.json;
|
|
77
|
+
applyDoc();
|
|
78
|
+
}, [props.json], _h$8);
|
|
62
79
|
const emitJSON = () => {
|
|
63
80
|
if (!inner.current || !jsonBridgeReady()) return;
|
|
64
81
|
const doc = htmlToJSON(inner.current.getHTML());
|
|
65
82
|
if (doc) {
|
|
66
83
|
lastJSON.current = JSON.stringify(doc);
|
|
84
|
+
appliedHost.current = inner.current;
|
|
67
85
|
props.onJSONChange?.(doc);
|
|
68
86
|
}
|
|
69
87
|
};
|
|
@@ -106,7 +124,10 @@ var TiptapEditor = defineUniversalComponent("nativescript", function TiptapEdito
|
|
|
106
124
|
[
|
|
107
125
|
"set",
|
|
108
126
|
"onReady",
|
|
109
|
-
|
|
127
|
+
() => {
|
|
128
|
+
applyDoc();
|
|
129
|
+
props.onReady?.();
|
|
130
|
+
}
|
|
110
131
|
],
|
|
111
132
|
[
|
|
112
133
|
"set",
|
|
@@ -133,7 +154,7 @@ var TiptapEditor = defineUniversalComponent("nativescript", function TiptapEdito
|
|
|
133
154
|
],
|
|
134
155
|
[
|
|
135
156
|
"set",
|
|
136
|
-
"
|
|
157
|
+
"ref",
|
|
137
158
|
bindHost
|
|
138
159
|
],
|
|
139
160
|
[
|
package/dist/web/index2.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { __s, bag3, clone, componentSlot, hookSlots, setStyle, template, useEffect, useRef, withSlot } from "octane";
|
|
2
|
-
import { updateFreshClassName } from "octane/internal/client";
|
|
1
|
+
import { __s, bag3, clone, componentSlot, hookSlots, setStyle, template, useEffect, useImperativeHandle, useRef, withSlot } from "octane";
|
|
2
|
+
import { compilerMemoRegion, hookMemoCreate, hookMemoEqual, hookMemoPublish2, updateFreshClassName } from "octane/internal/client";
|
|
3
3
|
import { EditorContent, useEditor } from "@octanejs/tiptap";
|
|
4
4
|
import StarterKit from "@tiptap/starter-kit";
|
|
5
5
|
//#region src/TiptapEditor.web.tsrx
|
|
6
6
|
var _t$0 = /* @__PURE__ */ template("<div></div>");
|
|
7
|
-
var _hs$ = /* @__PURE__ */ hookSlots(
|
|
8
|
-
var _h$
|
|
7
|
+
var _hs$ = /* @__PURE__ */ hookSlots(10);
|
|
8
|
+
var _h$5 = /* @__PURE__ */ Symbol(_hs$ + 5);
|
|
9
9
|
var supported = true;
|
|
10
10
|
var COMMAND = {
|
|
11
11
|
bold: (c) => c.toggleBold(),
|
|
@@ -26,18 +26,6 @@ var COMMAND = {
|
|
|
26
26
|
heading5: (c) => c.toggleHeading({ level: 5 }),
|
|
27
27
|
heading6: (c) => c.toggleHeading({ level: 6 })
|
|
28
28
|
};
|
|
29
|
-
var IS_ACTIVE = {
|
|
30
|
-
bold: (e) => e.isActive("bold"),
|
|
31
|
-
italic: (e) => e.isActive("italic"),
|
|
32
|
-
underline: (e) => e.isActive("underline"),
|
|
33
|
-
strikethrough: (e) => e.isActive("strike"),
|
|
34
|
-
code: (e) => e.isActive("code"),
|
|
35
|
-
blockquote: (e) => e.isActive("blockquote"),
|
|
36
|
-
bulletList: (e) => e.isActive("bulletList"),
|
|
37
|
-
orderedList: (e) => e.isActive("orderedList"),
|
|
38
|
-
codeBlock: (e) => e.isActive("codeBlock"),
|
|
39
|
-
link: (e) => e.isActive("link")
|
|
40
|
-
};
|
|
41
29
|
var ACTIVE_OF = [
|
|
42
30
|
["bold", "bold"],
|
|
43
31
|
["italic", "italic"],
|
|
@@ -86,13 +74,25 @@ function activeFormats(editor) {
|
|
|
86
74
|
for (const [name, kind, attrs] of ACTIVE_OF) if (editor.isActive(kind, attrs)) out.push(name);
|
|
87
75
|
return out;
|
|
88
76
|
}
|
|
77
|
+
function resolveExtensions(web) {
|
|
78
|
+
const extras = web?.extensions ?? [];
|
|
79
|
+
if (web?.starterKit === false || extras.some((ext) => ext?.name === "starterKit")) return extras;
|
|
80
|
+
return [web?.starterKit ? StarterKit.configure(web.starterKit) : StarterKit, ...extras];
|
|
81
|
+
}
|
|
89
82
|
var TiptapEditor = /* @__PURE__ */ __s(function TiptapEditor(props, __s, __extra) {
|
|
83
|
+
const __memoRegion = compilerMemoRegion(__s, _hs$ + 0);
|
|
84
|
+
let __hk = __memoRegion.hooks;
|
|
85
|
+
if (__hk === void 0) __memoRegion.hooks = __hk = hookMemoCreate(4);
|
|
86
|
+
let __hkd0, __hkd1;
|
|
90
87
|
const lastEmitted = useRef(void 0, _hs$ + 0);
|
|
91
88
|
const lastJSON = useRef(void 0, _hs$ + 1);
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
const lastHTMLInput = useRef(JSON.stringify([props.json !== void 0, props.value]), _hs$ + 2);
|
|
90
|
+
const lastJSONInput = useRef(JSON.stringify(props.json), _hs$ + 3);
|
|
91
|
+
const bound = useRef(void 0, _hs$ + 4);
|
|
92
|
+
const extensions = (__hkd0 = props.web?.extensions, __hkd1 = props.web?.starterKit, __hk[0] !== true || !hookMemoEqual(__hk[1], __hkd0) || !hookMemoEqual(__hk[2], __hkd1) ? hookMemoPublish2(__hk, 0, resolveExtensions(props.web), __hkd0, __hkd1) : __hk[3]);
|
|
93
|
+
const editor = withSlot(_h$5, useEditor, {
|
|
94
|
+
extensions,
|
|
95
|
+
content: props.json ?? props.value ?? "",
|
|
96
96
|
editable: props.editable !== false,
|
|
97
97
|
autofocus: props.autofocus ?? false,
|
|
98
98
|
onCreate: () => props.onReady?.(),
|
|
@@ -113,21 +113,32 @@ var TiptapEditor = /* @__PURE__ */ __s(function TiptapEditor(props, __s, __extra
|
|
|
113
113
|
})
|
|
114
114
|
});
|
|
115
115
|
useEffect(() => {
|
|
116
|
-
if (!editor
|
|
116
|
+
if (!editor) return;
|
|
117
|
+
const stamp = JSON.stringify([props.json !== void 0, props.value]);
|
|
118
|
+
if (stamp === lastHTMLInput.current) return;
|
|
119
|
+
lastHTMLInput.current = stamp;
|
|
120
|
+
if (props.value === void 0 || props.json !== void 0) return;
|
|
117
121
|
if (props.value === lastEmitted.current) return;
|
|
118
122
|
editor.commands.setContent(props.value);
|
|
119
|
-
}, [
|
|
123
|
+
}, [
|
|
124
|
+
editor,
|
|
125
|
+
props.value,
|
|
126
|
+
props.json
|
|
127
|
+
], _hs$ + 6);
|
|
120
128
|
useEffect(() => {
|
|
121
|
-
if (!editor
|
|
122
|
-
|
|
129
|
+
if (!editor) return;
|
|
130
|
+
const stamp = JSON.stringify(props.json);
|
|
131
|
+
if (stamp === lastJSONInput.current) return;
|
|
132
|
+
lastJSONInput.current = stamp;
|
|
133
|
+
if (props.json === void 0 || stamp === lastJSON.current) return;
|
|
123
134
|
editor.commands.setContent(props.json);
|
|
124
|
-
}, [editor, props.json], _hs$ +
|
|
135
|
+
}, [editor, props.json], _hs$ + 7);
|
|
125
136
|
useEffect(() => {
|
|
126
137
|
if (!editor) return;
|
|
127
138
|
editor.setEditable(props.editable !== false);
|
|
128
|
-
}, [editor, props.editable], _hs$ +
|
|
129
|
-
|
|
130
|
-
if (!editor
|
|
139
|
+
}, [editor, props.editable], _hs$ + 8);
|
|
140
|
+
useImperativeHandle(props.ref, () => {
|
|
141
|
+
if (!editor) return null;
|
|
131
142
|
const e = editor;
|
|
132
143
|
bound.current = {
|
|
133
144
|
getHTML: () => e.getHTML(),
|
|
@@ -150,7 +161,7 @@ var TiptapEditor = /* @__PURE__ */ __s(function TiptapEditor(props, __s, __extra
|
|
|
150
161
|
else e.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
|
151
162
|
},
|
|
152
163
|
removeLink: () => e.chain().focus().unsetLink().run(),
|
|
153
|
-
isActive: (format) =>
|
|
164
|
+
isActive: (format) => activeFormats(e).includes(format),
|
|
154
165
|
undo: () => e.commands.undo(),
|
|
155
166
|
redo: () => e.commands.redo(),
|
|
156
167
|
focus: () => e.commands.focus(),
|
|
@@ -158,8 +169,8 @@ var TiptapEditor = /* @__PURE__ */ __s(function TiptapEditor(props, __s, __extra
|
|
|
158
169
|
isFocused: () => e.isFocused,
|
|
159
170
|
native: e
|
|
160
171
|
};
|
|
161
|
-
|
|
162
|
-
}, [editor], _hs$ +
|
|
172
|
+
return bound.current;
|
|
173
|
+
}, [editor], _hs$ + 9);
|
|
163
174
|
if (__s.block.pending && !__s.block.crossRenderUpdate) return;
|
|
164
175
|
let _b = __s.slots[0];
|
|
165
176
|
if (_b === void 0) {
|
|
@@ -176,7 +187,7 @@ var TiptapEditor = /* @__PURE__ */ __s(function TiptapEditor(props, __s, __extra
|
|
|
176
187
|
_b.c = _v;
|
|
177
188
|
}
|
|
178
189
|
}
|
|
179
|
-
componentSlot(__s, 1, _b.b, EditorContent, { "editor": editor }, void 0, void 0, 2, void 0, void 0, "c:
|
|
190
|
+
componentSlot(__s, 1, _b.b, EditorContent, { "editor": editor }, void 0, void 0, 2, void 0, void 0, "c:95ead9de");
|
|
180
191
|
});
|
|
181
192
|
//#endregion
|
|
182
193
|
export { TiptapEditor, supported };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octane-xplat/tiptap",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Unified rich-text editor facade for Octane xplat —
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "Unified rich-text editor facade for Octane xplat — live tiptap on web/AppKit, Aztec + tiptap document model on Android",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cross-platform",
|
|
7
7
|
"editor",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"exports": {
|
|
30
30
|
".": {
|
|
31
31
|
"macos": {
|
|
32
|
-
"types": "./types/index.d.ts",
|
|
32
|
+
"types": "./types/index.macos.d.ts",
|
|
33
33
|
"default": "./src/index.macos.ts"
|
|
34
34
|
},
|
|
35
35
|
"web": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"access": "public"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@octane-xplat/richtext": "0.
|
|
51
|
+
"@octane-xplat/richtext": "0.11.0",
|
|
52
52
|
"@octanejs/tiptap": "0.0.51",
|
|
53
53
|
"@tiptap/core": "3.28.0",
|
|
54
54
|
"@tiptap/pm": "3.28.0",
|
|
@@ -59,10 +59,12 @@
|
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@nativescript-community/octane": "0.2.4",
|
|
61
61
|
"@nativescript/core": "9.1.2",
|
|
62
|
+
"@octane-xplat/macos-renderer": "0.11.0",
|
|
62
63
|
"@octanejs/vite-plugin": "0.1.61",
|
|
63
64
|
"octane": "0.6.3",
|
|
64
65
|
"typescript": "5.9.3",
|
|
65
|
-
"vite": "8.3.0"
|
|
66
|
+
"vite": "8.3.0",
|
|
67
|
+
"vitest": "5.0.1"
|
|
66
68
|
},
|
|
67
69
|
"peerDependencies": {
|
|
68
70
|
"@nativescript-community/octane": ">=0.2.1 <1",
|
|
@@ -87,6 +89,7 @@
|
|
|
87
89
|
}
|
|
88
90
|
},
|
|
89
91
|
"scripts": {
|
|
90
|
-
"build": "vite build && vite build --mode native && tsc -p tsconfig.types.json && node ../../scripts/check-native-dist.mjs"
|
|
92
|
+
"build": "vite build && vite build --mode native && tsc -p tsconfig.types.json && node ../../scripts/check-native-dist.mjs && node ../richtext/build-browser.mjs",
|
|
93
|
+
"test:native": "vitest run --config vitest.native.config.mts"
|
|
91
94
|
}
|
|
92
95
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** @jsxImportSource @octane-xplat/macos-renderer */
|
|
2
|
+
import type { TiptapEditorProps } from './types'
|
|
3
|
+
import browser from '../dist/macos/browser.js'
|
|
4
|
+
import { NativeEditor } from '@octane-xplat/richtext/macos-host'
|
|
5
|
+
|
|
6
|
+
/** AppKit mounts the local editor engine in WKWebView. */
|
|
7
|
+
export function TiptapEditor(props: TiptapEditorProps) {
|
|
8
|
+
return <NativeEditor {...props} browser={browser} />
|
|
9
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
createObjectContainer,
|
|
4
|
+
createObjectDriver,
|
|
5
|
+
createUniversalRoot,
|
|
6
|
+
flushUniversalSync,
|
|
7
|
+
} from 'octane/universal/native'
|
|
8
|
+
|
|
9
|
+
import type { TiptapJSON } from './types'
|
|
10
|
+
|
|
11
|
+
// Controlled fake for the lazy JSON bridge — the real module's dynamic
|
|
12
|
+
// imports aren't needed to test facade timing. `resolved`/`ready` model the
|
|
13
|
+
// bridge settling; docs carry `html` so conversions stay deterministic.
|
|
14
|
+
const bridge = vi.hoisted(() => ({
|
|
15
|
+
ready: false,
|
|
16
|
+
resolved: undefined as boolean | undefined,
|
|
17
|
+
waiters: [] as Array<(ok: boolean) => void>,
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
vi.mock('./json-bridge', () => ({
|
|
21
|
+
ensureJSONBridge: () =>
|
|
22
|
+
bridge.resolved !== undefined
|
|
23
|
+
? Promise.resolve(bridge.resolved)
|
|
24
|
+
: new Promise<boolean>((resolve) => bridge.waiters.push(resolve)),
|
|
25
|
+
jsonBridgeReady: () => bridge.ready,
|
|
26
|
+
jsonToHTML: (doc: any) =>
|
|
27
|
+
doc && typeof doc === 'object' && typeof doc.html === 'string' ? doc.html : null,
|
|
28
|
+
htmlToJSON: (html: string) => ({ type: 'doc', html }),
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
vi.mock('@octane-xplat/richtext', async () => {
|
|
32
|
+
return await import('../tests/fake-richtext.fixture.mobile.tsrx')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
import { TiptapEditor } from './TiptapEditor.tsrx'
|
|
36
|
+
import { hosts, resetFakeRichtext, setDefer } from '../tests/fake-richtext.fixture.mobile.tsrx'
|
|
37
|
+
|
|
38
|
+
const docA = { type: 'doc', html: '<p>alpha</p>' } as unknown as TiptapJSON
|
|
39
|
+
const docB = { type: 'doc', html: '<p>bravo</p>' } as unknown as TiptapJSON
|
|
40
|
+
|
|
41
|
+
const roots: ReturnType<typeof createUniversalRoot>[] = []
|
|
42
|
+
|
|
43
|
+
function mount(props: any) {
|
|
44
|
+
const container = createObjectContainer('nativescript')
|
|
45
|
+
const root = createUniversalRoot(container, createObjectDriver('nativescript'))
|
|
46
|
+
roots.push(root)
|
|
47
|
+
let handle: any = null
|
|
48
|
+
root.render(TiptapEditor, {
|
|
49
|
+
...props,
|
|
50
|
+
ref: (next: any) => {
|
|
51
|
+
handle = next
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
root,
|
|
57
|
+
container,
|
|
58
|
+
get handle() {
|
|
59
|
+
return handle
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function settleBridge(ok: boolean) {
|
|
65
|
+
bridge.ready = ok
|
|
66
|
+
bridge.resolved = ok
|
|
67
|
+
for (const resolve of bridge.waiters.splice(0)) {
|
|
68
|
+
resolve(ok)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function settle() {
|
|
73
|
+
for (let i = 0; i < 12; i++) {
|
|
74
|
+
await Promise.resolve()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
flushUniversalSync(() => {})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
bridge.ready = false
|
|
82
|
+
bridge.resolved = undefined
|
|
83
|
+
bridge.waiters.length = 0
|
|
84
|
+
resetFakeRichtext()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
for (const root of roots.splice(0)) {
|
|
89
|
+
root.unmount()
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('TiptapEditor native JSON readiness', () => {
|
|
94
|
+
it('applies an initial props.json once when the bridge settles after mount', async () => {
|
|
95
|
+
const onJSONReady = vi.fn()
|
|
96
|
+
mount({ json: docA, onJSONReady })
|
|
97
|
+
await settle()
|
|
98
|
+
expect(hosts[0].setCalls).toEqual([])
|
|
99
|
+
|
|
100
|
+
settleBridge(true)
|
|
101
|
+
await settle()
|
|
102
|
+
expect(onJSONReady).toHaveBeenCalledWith(true)
|
|
103
|
+
expect(hosts[0].html).toBe('<p>alpha</p>')
|
|
104
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>'])
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('applies an initial props.json when the host readies after the bridge', async () => {
|
|
108
|
+
bridge.ready = true
|
|
109
|
+
bridge.resolved = true
|
|
110
|
+
setDefer(true)
|
|
111
|
+
mount({ json: docA })
|
|
112
|
+
await settle()
|
|
113
|
+
expect(hosts[0].setCalls).toEqual([])
|
|
114
|
+
|
|
115
|
+
hosts[0].ready()
|
|
116
|
+
await settle()
|
|
117
|
+
expect(hosts[0].html).toBe('<p>alpha</p>')
|
|
118
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>'])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('applies an initial props.json once when host and bridge are both late', async () => {
|
|
122
|
+
setDefer(true)
|
|
123
|
+
mount({ json: docA })
|
|
124
|
+
await settle()
|
|
125
|
+
hosts[0].ready()
|
|
126
|
+
await settle()
|
|
127
|
+
expect(hosts[0].setCalls).toEqual([])
|
|
128
|
+
|
|
129
|
+
settleBridge(true)
|
|
130
|
+
await settle()
|
|
131
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>'])
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('keeps json precedence over value once readiness lands', async () => {
|
|
135
|
+
mount({ value: '<p>value</p>', json: docA })
|
|
136
|
+
await settle()
|
|
137
|
+
settleBridge(true)
|
|
138
|
+
await settle()
|
|
139
|
+
expect(hosts[0].html).toBe('<p>alpha</p>')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('applies later props.json updates and skips identical docs', async () => {
|
|
143
|
+
const view = mount({ json: docA })
|
|
144
|
+
settleBridge(true)
|
|
145
|
+
await settle()
|
|
146
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>'])
|
|
147
|
+
|
|
148
|
+
view.root.render(TiptapEditor, { json: docB } as any)
|
|
149
|
+
await settle()
|
|
150
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>', '<p>bravo</p>'])
|
|
151
|
+
|
|
152
|
+
view.root.render(TiptapEditor, { json: docB } as any)
|
|
153
|
+
await settle()
|
|
154
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>', '<p>bravo</p>'])
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('does not push a doc the editor itself emitted back at it', async () => {
|
|
158
|
+
const onJSONChange = vi.fn()
|
|
159
|
+
const view = mount({ onJSONChange })
|
|
160
|
+
settleBridge(true)
|
|
161
|
+
await settle()
|
|
162
|
+
|
|
163
|
+
const rig = hosts[0]
|
|
164
|
+
rig.html = '<p>typed</p>'
|
|
165
|
+
// The leaf calls props.onChange(html); the facade wraps it to emit JSON.
|
|
166
|
+
rig.props.onChange('<p>typed</p>')
|
|
167
|
+
await settle()
|
|
168
|
+
const emitted = onJSONChange.mock.calls[0][0]
|
|
169
|
+
expect(emitted).toEqual({ type: 'doc', html: '<p>typed</p>' })
|
|
170
|
+
|
|
171
|
+
view.root.render(TiptapEditor, { json: emitted, onJSONChange } as any)
|
|
172
|
+
await settle()
|
|
173
|
+
expect(rig.setCalls).toEqual([])
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('defers an imperative setJSON issued before the bridge settles', async () => {
|
|
177
|
+
const view = mount({})
|
|
178
|
+
await settle()
|
|
179
|
+
expect(view.handle).not.toBeNull()
|
|
180
|
+
|
|
181
|
+
view.handle.setJSON(docB)
|
|
182
|
+
expect(hosts[0].setCalls).toEqual([])
|
|
183
|
+
settleBridge(true)
|
|
184
|
+
await settle()
|
|
185
|
+
expect(hosts[0].setCalls).toEqual(['<p>bravo</p>'])
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('re-applies the current doc when the host rebinds', async () => {
|
|
189
|
+
mount({ json: docA })
|
|
190
|
+
settleBridge(true)
|
|
191
|
+
await settle()
|
|
192
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>'])
|
|
193
|
+
|
|
194
|
+
hosts[0].rebind()
|
|
195
|
+
await settle()
|
|
196
|
+
expect(hosts[0].html).toBe('<p>alpha</p>')
|
|
197
|
+
expect(hosts[0].setCalls).toEqual(['<p>alpha</p>'])
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('ignores a bridge that settles after unmount', async () => {
|
|
201
|
+
const onJSONReady = vi.fn()
|
|
202
|
+
const view = mount({ json: docA, onJSONReady })
|
|
203
|
+
await settle()
|
|
204
|
+
view.root.unmount()
|
|
205
|
+
|
|
206
|
+
settleBridge(true)
|
|
207
|
+
await settle()
|
|
208
|
+
expect(onJSONReady).not.toHaveBeenCalled()
|
|
209
|
+
expect(hosts[0].setCalls).toEqual([])
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('reports bridge failure through onJSONReady without touching the host', async () => {
|
|
213
|
+
const onJSONReady = vi.fn()
|
|
214
|
+
const view = mount({ json: docA, onJSONReady })
|
|
215
|
+
await settle()
|
|
216
|
+
settleBridge(false)
|
|
217
|
+
await settle()
|
|
218
|
+
expect(onJSONReady).toHaveBeenCalledWith(false)
|
|
219
|
+
expect(hosts[0].setCalls).toEqual([])
|
|
220
|
+
expect(view.handle.getJSON()).toBeNull()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('drops a doc the converter rejects instead of writing null HTML', async () => {
|
|
224
|
+
mount({ json: { type: 'doc', content: [{ type: 'mystery' }] } as any })
|
|
225
|
+
settleBridge(true)
|
|
226
|
+
await settle()
|
|
227
|
+
expect(hosts[0].setCalls).toEqual([])
|
|
228
|
+
})
|
|
229
|
+
})
|