@octane-xplat/tiptap 0.0.1 → 0.9.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/LICENSE +21 -0
- package/README.md +25 -2
- package/dist/native/index.js +3 -0
- package/dist/native/index2.js +82 -0
- package/dist/native/index3.js +207 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/web/index.js +12 -0
- package/dist/web/index2.js +182 -0
- package/dist/web/octane-client-build.json +8 -0
- package/package.json +83 -10
- package/src/TiptapEditor.tsrx +107 -0
- package/src/TiptapEditor.web.tsrx +159 -0
- package/src/index.macos.ts +19 -0
- package/src/index.ts +3 -0
- package/src/index.web.ts +12 -0
- package/src/json-bridge.ts +124 -0
- package/src/types.ts +96 -0
- package/types/index.d.ts +8 -0
- package/types/types.d.ts +75 -0
- package/index.d.ts +0 -1
- package/index.js +0 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alec Larson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@octane-xplat/tiptap`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The unified rich-text facade: one `TiptapEditor` component across web and
|
|
4
|
+
native. Web renders a real tiptap `Editor` through `@octanejs/tiptap`'s
|
|
5
|
+
`EditorContent`; native renders the `@octane-xplat/richtext` leaf (WordPress
|
|
6
|
+
Aztec on Android, iOS stub) and layers tiptap document JSON on top through
|
|
7
|
+
the DOM-free ProseMirror slices (`@tiptap/pm`, `@tiptap/static-renderer`)
|
|
8
|
+
with a `zeed-dom` parse shim. No DOM-bound tiptap code (`EditorView`) ever
|
|
9
|
+
enters a native bundle — divergence lives at the file-suffix boundary.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { TiptapEditor, supported } from '@octane-xplat/tiptap'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`value`/`onChange` exchange document HTML. `getJSON`/`setJSON` exchange
|
|
16
|
+
tiptap document JSON: synchronous on web, bridged on native after
|
|
17
|
+
`onJSONReady(true)` — the lazy bridge reports `false` (and `getJSON()`
|
|
18
|
+
returns `null`) on runtimes that cannot host the schema modules. On native,
|
|
19
|
+
HTML is the canonical interchange format; JSON output is a best-effort
|
|
20
|
+
mapping of Aztec's flat span list onto ProseMirror's tree.
|
|
21
|
+
|
|
22
|
+
The tiptap family pins to `3.28.0` to match `@octanejs/tiptap@0.0.51` —
|
|
23
|
+
extension `^` ranges float ahead of core and break (`getPreviousBlockSibling`),
|
|
24
|
+
so consumers should keep the workspace override. See
|
|
25
|
+
[`docs/rich-text.md`](../../docs/rich-text.md) for the shared contract,
|
|
26
|
+
format vocabulary, and known limits.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
//#region src/json-bridge.ts
|
|
2
|
+
var bridge;
|
|
3
|
+
/** zeed-dom's classList is a plain {contains,add,remove} object; tiptap's
|
|
4
|
+
* codeBlock parseHTML spreads it. Wrap the VElement getter so it hands back
|
|
5
|
+
* an iterable array that keeps the mutation methods. */
|
|
6
|
+
function patchClassList(VElement) {
|
|
7
|
+
const proto = VElement?.prototype;
|
|
8
|
+
if (!proto) throw new Error("VElement.prototype unavailable");
|
|
9
|
+
const desc = Object.getOwnPropertyDescriptor(proto, "classList");
|
|
10
|
+
if (!desc?.get) throw new Error("classList is not a prototype getter");
|
|
11
|
+
if (desc.get.__xplatPatched) return;
|
|
12
|
+
const patched = function() {
|
|
13
|
+
const cl = desc.get.call(this);
|
|
14
|
+
if (cl && typeof cl[Symbol.iterator] !== "function") {
|
|
15
|
+
const tokens = String(this.getAttribute("class") ?? "").split(/\s+/).filter(Boolean);
|
|
16
|
+
return Object.assign(tokens, cl);
|
|
17
|
+
}
|
|
18
|
+
return cl;
|
|
19
|
+
};
|
|
20
|
+
patched.__xplatPatched = true;
|
|
21
|
+
Object.defineProperty(proto, "classList", {
|
|
22
|
+
configurable: true,
|
|
23
|
+
get: patched,
|
|
24
|
+
set: desc.set
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/** Install the minimal parse globals generateJSON's elementFromString
|
|
28
|
+
* reads. Only installs when the host lacks them, so a real DOM (web
|
|
29
|
+
* target) is never clobbered. */
|
|
30
|
+
function installDomShim(zeed) {
|
|
31
|
+
const g = globalThis;
|
|
32
|
+
if (g.window?.DOMParser) return;
|
|
33
|
+
class ZeedDOMParser {
|
|
34
|
+
parseFromString(html) {
|
|
35
|
+
const doc = zeed.createHTMLDocument();
|
|
36
|
+
doc.body.innerHTML = html;
|
|
37
|
+
return doc;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
g.window = { DOMParser: ZeedDOMParser };
|
|
41
|
+
g.document = zeed.createHTMLDocument();
|
|
42
|
+
}
|
|
43
|
+
async function ensureJSONBridge() {
|
|
44
|
+
if (bridge !== void 0) return bridge !== null;
|
|
45
|
+
try {
|
|
46
|
+
const [core, sk, sr, zeed] = await Promise.all([
|
|
47
|
+
import("@tiptap/core"),
|
|
48
|
+
import("@tiptap/starter-kit"),
|
|
49
|
+
import("@tiptap/static-renderer"),
|
|
50
|
+
import("zeed-dom")
|
|
51
|
+
]);
|
|
52
|
+
patchClassList(zeed.VElement);
|
|
53
|
+
installDomShim(zeed);
|
|
54
|
+
const StarterKit = sk.default ?? sk.StarterKit;
|
|
55
|
+
bridge = {
|
|
56
|
+
generateJSON: core.generateJSON,
|
|
57
|
+
renderToHTMLString: sr.renderToHTMLString,
|
|
58
|
+
extensions: [StarterKit]
|
|
59
|
+
};
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
bridge = null;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** HTML → doc JSON through tiptap's own parseHTML rules. Null when the
|
|
67
|
+
* bridge hasn't loaded (call ensureJSONBridge first / check `ready`). */
|
|
68
|
+
function htmlToJSON(html) {
|
|
69
|
+
return bridge?.generateJSON(html, bridge.extensions) ?? null;
|
|
70
|
+
}
|
|
71
|
+
/** Doc JSON → HTML via tiptap's own renderHTML rules (no DOM). */
|
|
72
|
+
function jsonToHTML(doc) {
|
|
73
|
+
return bridge?.renderToHTMLString({
|
|
74
|
+
extensions: bridge.extensions,
|
|
75
|
+
content: doc
|
|
76
|
+
}) ?? null;
|
|
77
|
+
}
|
|
78
|
+
function jsonBridgeReady() {
|
|
79
|
+
return bridge != null;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
export { ensureJSONBridge, htmlToJSON, jsonBridgeReady, jsonToHTML };
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { ensureJSONBridge, htmlToJSON, jsonBridgeReady, jsonToHTML } from "./index2.js";
|
|
2
|
+
import { defineUniversalComponent, hookSlots, universalComponent, universalPlan, universalProps, universalValue, useCallback, useEffect, useRef } from "@nativescript-community/octane";
|
|
3
|
+
import { RichTextEditor, supported } from "@octane-xplat/richtext";
|
|
4
|
+
//#region src/TiptapEditor.tsrx
|
|
5
|
+
var _hs$ = /* @__PURE__ */ hookSlots(6);
|
|
6
|
+
var _h$0 = /* @__PURE__ */ Symbol(_hs$);
|
|
7
|
+
var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
|
|
8
|
+
var _h$2 = /* @__PURE__ */ Symbol(_hs$ + 2);
|
|
9
|
+
var _h$3 = /* @__PURE__ */ Symbol(_hs$ + 3);
|
|
10
|
+
var _h$4 = /* @__PURE__ */ Symbol(_hs$ + 4);
|
|
11
|
+
var _h$5 = /* @__PURE__ */ Symbol(_hs$ + 5);
|
|
12
|
+
var __octaneUniversalPlan0 = universalPlan("nativescript", {
|
|
13
|
+
"kind": "slot",
|
|
14
|
+
"slot": 0
|
|
15
|
+
});
|
|
16
|
+
var supported$1 = supported;
|
|
17
|
+
var TiptapEditor = defineUniversalComponent("nativescript", function TiptapEditor(props) {
|
|
18
|
+
const inner = useRef(null, _h$0);
|
|
19
|
+
const jsonReady = useRef(false, _h$1);
|
|
20
|
+
const lastJSON = useRef(void 0, _h$2);
|
|
21
|
+
const bindHost = useCallback((h) => {
|
|
22
|
+
inner.current = h;
|
|
23
|
+
props.bind?.({
|
|
24
|
+
getHTML: () => h.getHTML(),
|
|
25
|
+
setHTML: (html) => h.setHTML(html),
|
|
26
|
+
getJSON: () => jsonBridgeReady() ? htmlToJSON(h.getHTML()) : null,
|
|
27
|
+
setJSON: (doc) => {
|
|
28
|
+
const html = jsonBridgeReady() ? jsonToHTML(doc) : null;
|
|
29
|
+
if (html != null) {
|
|
30
|
+
lastJSON.current = JSON.stringify(doc);
|
|
31
|
+
h.setHTML(html);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
apply: (format) => h.apply(format),
|
|
35
|
+
linkTo: (url, anchor) => h.linkTo(url, anchor),
|
|
36
|
+
removeLink: () => h.removeLink(),
|
|
37
|
+
isActive: (format) => h.isActive(format),
|
|
38
|
+
undo: () => h.undo(),
|
|
39
|
+
redo: () => h.redo(),
|
|
40
|
+
focus: () => h.focus(),
|
|
41
|
+
blur: () => h.blur(),
|
|
42
|
+
isFocused: () => h.isFocused(),
|
|
43
|
+
native: h.native
|
|
44
|
+
});
|
|
45
|
+
}, [], _h$3);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
ensureJSONBridge().then((ok) => {
|
|
48
|
+
jsonReady.current = ok;
|
|
49
|
+
props.onJSONReady?.(ok);
|
|
50
|
+
});
|
|
51
|
+
}, [], _h$4);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!props.json || !jsonBridgeReady() || !inner.current) return;
|
|
54
|
+
const stamp = JSON.stringify(props.json);
|
|
55
|
+
if (stamp === lastJSON.current) return;
|
|
56
|
+
const html = jsonToHTML(props.json);
|
|
57
|
+
if (html != null) {
|
|
58
|
+
lastJSON.current = stamp;
|
|
59
|
+
inner.current.setHTML(html);
|
|
60
|
+
}
|
|
61
|
+
}, [props.json], _h$5);
|
|
62
|
+
const emitJSON = () => {
|
|
63
|
+
if (!inner.current || !jsonBridgeReady()) return;
|
|
64
|
+
const doc = htmlToJSON(inner.current.getHTML());
|
|
65
|
+
if (doc) {
|
|
66
|
+
lastJSON.current = JSON.stringify(doc);
|
|
67
|
+
props.onJSONChange?.(doc);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return universalValue(__octaneUniversalPlan0, [universalComponent("nativescript", RichTextEditor, universalProps([
|
|
71
|
+
[
|
|
72
|
+
"set",
|
|
73
|
+
"id",
|
|
74
|
+
props.id
|
|
75
|
+
],
|
|
76
|
+
[
|
|
77
|
+
"set",
|
|
78
|
+
"className",
|
|
79
|
+
props.className
|
|
80
|
+
],
|
|
81
|
+
[
|
|
82
|
+
"set",
|
|
83
|
+
"style",
|
|
84
|
+
props.style
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
"set",
|
|
88
|
+
"value",
|
|
89
|
+
props.value
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
"set",
|
|
93
|
+
"placeholder",
|
|
94
|
+
props.placeholder
|
|
95
|
+
],
|
|
96
|
+
[
|
|
97
|
+
"set",
|
|
98
|
+
"editable",
|
|
99
|
+
props.editable
|
|
100
|
+
],
|
|
101
|
+
[
|
|
102
|
+
"set",
|
|
103
|
+
"autofocus",
|
|
104
|
+
props.autofocus
|
|
105
|
+
],
|
|
106
|
+
[
|
|
107
|
+
"set",
|
|
108
|
+
"onReady",
|
|
109
|
+
props.onReady
|
|
110
|
+
],
|
|
111
|
+
[
|
|
112
|
+
"set",
|
|
113
|
+
"onChange",
|
|
114
|
+
(html) => {
|
|
115
|
+
props.onChange?.(html);
|
|
116
|
+
emitJSON();
|
|
117
|
+
}
|
|
118
|
+
],
|
|
119
|
+
[
|
|
120
|
+
"set",
|
|
121
|
+
"onSelectionChange",
|
|
122
|
+
props.onSelectionChange
|
|
123
|
+
],
|
|
124
|
+
[
|
|
125
|
+
"set",
|
|
126
|
+
"onFocus",
|
|
127
|
+
props.onFocus
|
|
128
|
+
],
|
|
129
|
+
[
|
|
130
|
+
"set",
|
|
131
|
+
"onBlur",
|
|
132
|
+
props.onBlur
|
|
133
|
+
],
|
|
134
|
+
[
|
|
135
|
+
"set",
|
|
136
|
+
"bind",
|
|
137
|
+
bindHost
|
|
138
|
+
],
|
|
139
|
+
[
|
|
140
|
+
"set",
|
|
141
|
+
"android",
|
|
142
|
+
props.android
|
|
143
|
+
],
|
|
144
|
+
[
|
|
145
|
+
"set",
|
|
146
|
+
"ios",
|
|
147
|
+
props.ios
|
|
148
|
+
],
|
|
149
|
+
[
|
|
150
|
+
"set",
|
|
151
|
+
"row",
|
|
152
|
+
props.row
|
|
153
|
+
],
|
|
154
|
+
[
|
|
155
|
+
"set",
|
|
156
|
+
"col",
|
|
157
|
+
props.col
|
|
158
|
+
],
|
|
159
|
+
[
|
|
160
|
+
"set",
|
|
161
|
+
"rowSpan",
|
|
162
|
+
props.rowSpan
|
|
163
|
+
],
|
|
164
|
+
[
|
|
165
|
+
"set",
|
|
166
|
+
"colSpan",
|
|
167
|
+
props.colSpan
|
|
168
|
+
],
|
|
169
|
+
[
|
|
170
|
+
"set",
|
|
171
|
+
"dock",
|
|
172
|
+
props.dock
|
|
173
|
+
],
|
|
174
|
+
[
|
|
175
|
+
"set",
|
|
176
|
+
"left",
|
|
177
|
+
props.left
|
|
178
|
+
],
|
|
179
|
+
[
|
|
180
|
+
"set",
|
|
181
|
+
"top",
|
|
182
|
+
props.top
|
|
183
|
+
],
|
|
184
|
+
[
|
|
185
|
+
"set",
|
|
186
|
+
"flexGrow",
|
|
187
|
+
props.flexGrow
|
|
188
|
+
],
|
|
189
|
+
[
|
|
190
|
+
"set",
|
|
191
|
+
"flexShrink",
|
|
192
|
+
props.flexShrink
|
|
193
|
+
],
|
|
194
|
+
[
|
|
195
|
+
"set",
|
|
196
|
+
"alignSelf",
|
|
197
|
+
props.alignSelf
|
|
198
|
+
],
|
|
199
|
+
[
|
|
200
|
+
"set",
|
|
201
|
+
"order",
|
|
202
|
+
props.order
|
|
203
|
+
]
|
|
204
|
+
]))]);
|
|
205
|
+
}, { "module": "@nativescript-community/octane" });
|
|
206
|
+
//#endregion
|
|
207
|
+
export { TiptapEditor, supported$1 as supported };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TiptapEditor, supported } from "./index2.js";
|
|
2
|
+
//#region src/index.web.ts
|
|
3
|
+
/** Web needs no lazy bridge — tiptap is in the bundle already. Kept for
|
|
4
|
+
* call-site parity with the native entry. */
|
|
5
|
+
function ensureJSONBridge() {
|
|
6
|
+
return Promise.resolve(true);
|
|
7
|
+
}
|
|
8
|
+
function jsonBridgeReady() {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { TiptapEditor, ensureJSONBridge, jsonBridgeReady, supported };
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { __s, bag3, clone, componentSlot, hookSlots, setStyle, template, useEffect, useRef, withSlot } from "octane";
|
|
2
|
+
import { updateFreshClassName } from "octane/internal/client";
|
|
3
|
+
import { EditorContent, useEditor } from "@octanejs/tiptap";
|
|
4
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
5
|
+
//#region src/TiptapEditor.web.tsrx
|
|
6
|
+
var _t$0 = /* @__PURE__ */ template("<div></div>");
|
|
7
|
+
var _hs$ = /* @__PURE__ */ hookSlots(8);
|
|
8
|
+
var _h$3 = /* @__PURE__ */ Symbol(_hs$ + 3);
|
|
9
|
+
var supported = true;
|
|
10
|
+
var COMMAND = {
|
|
11
|
+
bold: (c) => c.toggleBold(),
|
|
12
|
+
italic: (c) => c.toggleItalic(),
|
|
13
|
+
underline: (c) => c.toggleUnderline(),
|
|
14
|
+
strikethrough: (c) => c.toggleStrike(),
|
|
15
|
+
code: (c) => c.toggleCode(),
|
|
16
|
+
blockquote: (c) => c.toggleBlockquote(),
|
|
17
|
+
bulletList: (c) => c.toggleBulletList(),
|
|
18
|
+
orderedList: (c) => c.toggleOrderedList(),
|
|
19
|
+
codeBlock: (c) => c.toggleCodeBlock(),
|
|
20
|
+
horizontalRule: (c) => c.setHorizontalRule(),
|
|
21
|
+
paragraph: (c) => c.setParagraph(),
|
|
22
|
+
heading1: (c) => c.toggleHeading({ level: 1 }),
|
|
23
|
+
heading2: (c) => c.toggleHeading({ level: 2 }),
|
|
24
|
+
heading3: (c) => c.toggleHeading({ level: 3 }),
|
|
25
|
+
heading4: (c) => c.toggleHeading({ level: 4 }),
|
|
26
|
+
heading5: (c) => c.toggleHeading({ level: 5 }),
|
|
27
|
+
heading6: (c) => c.toggleHeading({ level: 6 })
|
|
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
|
+
var ACTIVE_OF = [
|
|
42
|
+
["bold", "bold"],
|
|
43
|
+
["italic", "italic"],
|
|
44
|
+
["underline", "underline"],
|
|
45
|
+
["strikethrough", "strike"],
|
|
46
|
+
["code", "code"],
|
|
47
|
+
["blockquote", "blockquote"],
|
|
48
|
+
["bulletList", "bulletList"],
|
|
49
|
+
["orderedList", "orderedList"],
|
|
50
|
+
["codeBlock", "codeBlock"],
|
|
51
|
+
["link", "link"],
|
|
52
|
+
[
|
|
53
|
+
"heading1",
|
|
54
|
+
"heading",
|
|
55
|
+
{ level: 1 }
|
|
56
|
+
],
|
|
57
|
+
[
|
|
58
|
+
"heading2",
|
|
59
|
+
"heading",
|
|
60
|
+
{ level: 2 }
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
"heading3",
|
|
64
|
+
"heading",
|
|
65
|
+
{ level: 3 }
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
"heading4",
|
|
69
|
+
"heading",
|
|
70
|
+
{ level: 4 }
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
"heading5",
|
|
74
|
+
"heading",
|
|
75
|
+
{ level: 5 }
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
"heading6",
|
|
79
|
+
"heading",
|
|
80
|
+
{ level: 6 }
|
|
81
|
+
],
|
|
82
|
+
["paragraph", "paragraph"]
|
|
83
|
+
];
|
|
84
|
+
function activeFormats(editor) {
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const [name, kind, attrs] of ACTIVE_OF) if (editor.isActive(kind, attrs)) out.push(name);
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
var TiptapEditor = /* @__PURE__ */ __s(function TiptapEditor(props, __s, __extra) {
|
|
90
|
+
const lastEmitted = useRef(void 0, _hs$ + 0);
|
|
91
|
+
const lastJSON = useRef(void 0, _hs$ + 1);
|
|
92
|
+
const bound = useRef(void 0, _hs$ + 2);
|
|
93
|
+
const editor = withSlot(_h$3, useEditor, {
|
|
94
|
+
extensions: [StarterKit],
|
|
95
|
+
content: props.value ?? props.json ?? "",
|
|
96
|
+
editable: props.editable !== false,
|
|
97
|
+
autofocus: props.autofocus ?? false,
|
|
98
|
+
onCreate: () => props.onReady?.(),
|
|
99
|
+
onUpdate: ({ editor: e }) => {
|
|
100
|
+
const html = e.getHTML();
|
|
101
|
+
lastEmitted.current = html;
|
|
102
|
+
props.onChange?.(html);
|
|
103
|
+
const doc = e.getJSON();
|
|
104
|
+
lastJSON.current = JSON.stringify(doc);
|
|
105
|
+
props.onJSONChange?.(doc);
|
|
106
|
+
},
|
|
107
|
+
onFocus: () => props.onFocus?.(),
|
|
108
|
+
onBlur: () => props.onBlur?.(),
|
|
109
|
+
onSelectionUpdate: ({ editor: e }) => props.onSelectionChange?.({
|
|
110
|
+
start: e.state.selection.from,
|
|
111
|
+
end: e.state.selection.to,
|
|
112
|
+
active: activeFormats(e)
|
|
113
|
+
})
|
|
114
|
+
});
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!editor || props.value === void 0) return;
|
|
117
|
+
if (props.value === lastEmitted.current) return;
|
|
118
|
+
editor.commands.setContent(props.value);
|
|
119
|
+
}, [editor, props.value], _hs$ + 4);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!editor || props.json === void 0) return;
|
|
122
|
+
if (JSON.stringify(props.json) === lastJSON.current) return;
|
|
123
|
+
editor.commands.setContent(props.json);
|
|
124
|
+
}, [editor, props.json], _hs$ + 5);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!editor) return;
|
|
127
|
+
editor.setEditable(props.editable !== false);
|
|
128
|
+
}, [editor, props.editable], _hs$ + 6);
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (!editor || bound.current?.native === editor) return;
|
|
131
|
+
const e = editor;
|
|
132
|
+
bound.current = {
|
|
133
|
+
getHTML: () => e.getHTML(),
|
|
134
|
+
setHTML: (html) => e.commands.setContent(html),
|
|
135
|
+
getJSON: () => e.getJSON(),
|
|
136
|
+
setJSON: (doc) => e.commands.setContent(doc),
|
|
137
|
+
apply: (format) => {
|
|
138
|
+
const run = COMMAND[format];
|
|
139
|
+
if (run) run(e.chain().focus()).run();
|
|
140
|
+
},
|
|
141
|
+
linkTo: (url, anchor) => {
|
|
142
|
+
if (anchor != null && anchor !== "") e.chain().focus().insertContent({
|
|
143
|
+
type: "text",
|
|
144
|
+
text: anchor,
|
|
145
|
+
marks: [{
|
|
146
|
+
type: "link",
|
|
147
|
+
attrs: { href: url }
|
|
148
|
+
}]
|
|
149
|
+
}).run();
|
|
150
|
+
else e.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
|
151
|
+
},
|
|
152
|
+
removeLink: () => e.chain().focus().unsetLink().run(),
|
|
153
|
+
isActive: (format) => IS_ACTIVE[format]?.(e) ?? false,
|
|
154
|
+
undo: () => e.commands.undo(),
|
|
155
|
+
redo: () => e.commands.redo(),
|
|
156
|
+
focus: () => e.commands.focus(),
|
|
157
|
+
blur: () => e.commands.blur(),
|
|
158
|
+
isFocused: () => e.isFocused,
|
|
159
|
+
native: e
|
|
160
|
+
};
|
|
161
|
+
props.bind?.(bound.current);
|
|
162
|
+
}, [editor], _hs$ + 7);
|
|
163
|
+
if (__s.block.pending && !__s.block.crossRenderUpdate) return;
|
|
164
|
+
let _b = __s.slots[0];
|
|
165
|
+
if (_b === void 0) {
|
|
166
|
+
let _m1;
|
|
167
|
+
const _root = clone(_t$0);
|
|
168
|
+
_m1 = _root;
|
|
169
|
+
_b = bag3(__s, _root, void 0, _m1, void 0);
|
|
170
|
+
}
|
|
171
|
+
_b.a = updateFreshClassName(["vx-richtext", props.className], _b.a, _b.b);
|
|
172
|
+
{
|
|
173
|
+
const _v = props.style;
|
|
174
|
+
if (_b.c !== _v) {
|
|
175
|
+
setStyle(_b.b, _v, _b.c);
|
|
176
|
+
_b.c = _v;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
componentSlot(__s, 1, _b.b, EditorContent, { "editor": editor }, void 0, void 0, 2, void 0, void 0, "c:077f1c5f");
|
|
180
|
+
});
|
|
181
|
+
//#endregion
|
|
182
|
+
export { TiptapEditor, supported };
|
package/package.json
CHANGED
|
@@ -1,19 +1,92 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octane-xplat/tiptap",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Unified rich-text editor facade for Octane xplat — @octanejs/tiptap on web, @octane-xplat/richtext (Aztec) + tiptap doc model on native",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cross-platform",
|
|
7
|
+
"editor",
|
|
8
|
+
"octane",
|
|
9
|
+
"prosemirror",
|
|
10
|
+
"rich-text",
|
|
11
|
+
"richtext",
|
|
12
|
+
"tiptap",
|
|
13
|
+
"xplat"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/tiptap",
|
|
5
16
|
"license": "MIT",
|
|
6
17
|
"repository": {
|
|
7
18
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/octane-xplat/octane-xplat
|
|
19
|
+
"url": "https://github.com/octane-xplat/octane-xplat",
|
|
9
20
|
"directory": "packages/tiptap"
|
|
10
21
|
},
|
|
11
|
-
"homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/tiptap",
|
|
12
|
-
"main": "./index.js",
|
|
13
|
-
"types": "./index.d.ts",
|
|
14
22
|
"files": [
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
|
|
19
|
-
|
|
23
|
+
"dist",
|
|
24
|
+
"types",
|
|
25
|
+
"src",
|
|
26
|
+
"platforms"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"macos": {
|
|
32
|
+
"types": "./types/index.d.ts",
|
|
33
|
+
"default": "./src/index.macos.ts"
|
|
34
|
+
},
|
|
35
|
+
"web": {
|
|
36
|
+
"types": "./types/index.d.ts",
|
|
37
|
+
"default": "./dist/web/index.js"
|
|
38
|
+
},
|
|
39
|
+
"native": {
|
|
40
|
+
"types": "./types/index.d.ts",
|
|
41
|
+
"default": "./dist/native/index.js"
|
|
42
|
+
},
|
|
43
|
+
"types": "./types/index.d.ts",
|
|
44
|
+
"default": "./dist/native/index.js"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@octane-xplat/richtext": "0.9.0",
|
|
52
|
+
"@octanejs/tiptap": "0.0.51",
|
|
53
|
+
"@tiptap/core": "3.28.0",
|
|
54
|
+
"@tiptap/pm": "3.28.0",
|
|
55
|
+
"@tiptap/starter-kit": "3.28.0",
|
|
56
|
+
"@tiptap/static-renderer": "3.28.0",
|
|
57
|
+
"zeed-dom": "^0.15.1"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@nativescript-community/octane": "0.2.4",
|
|
61
|
+
"@nativescript/core": "9.1.2",
|
|
62
|
+
"@octanejs/vite-plugin": "0.1.61",
|
|
63
|
+
"octane": "0.6.3",
|
|
64
|
+
"typescript": "5.9.3",
|
|
65
|
+
"vite": "8.3.0"
|
|
66
|
+
},
|
|
67
|
+
"peerDependencies": {
|
|
68
|
+
"@nativescript-community/octane": ">=0.2.1 <1",
|
|
69
|
+
"@nativescript/core": ">=9.1.0 <10",
|
|
70
|
+
"octane": ">=0.6.3 <1"
|
|
71
|
+
},
|
|
72
|
+
"peerDependenciesMeta": {
|
|
73
|
+
"@nativescript-community/octane": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@nativescript/core": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"octane": {
|
|
80
|
+
"optional": true
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"nativescript": {
|
|
84
|
+
"platforms": {
|
|
85
|
+
"android": "9.1.0",
|
|
86
|
+
"ios": "9.1.0"
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"scripts": {
|
|
90
|
+
"build": "vite build && vite build --mode native && tsc -p tsconfig.types.json && node ../../scripts/check-native-dist.mjs"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
+
import { useCallback, useEffect, useRef } from 'octane'
|
|
3
|
+
import { RichTextEditor, supported as leafSupported } from '@octane-xplat/richtext'
|
|
4
|
+
import type { RichTextEditorHandle } from '@octane-xplat/richtext'
|
|
5
|
+
import type { TiptapEditorHandle, TiptapEditorProps } from './types'
|
|
6
|
+
import { ensureJSONBridge, htmlToJSON, jsonBridgeReady, jsonToHTML } from './json-bridge'
|
|
7
|
+
|
|
8
|
+
export const supported = leafSupported
|
|
9
|
+
|
|
10
|
+
/** Unified editor facade, native half. Rendering/editing run through the
|
|
11
|
+
* Aztec leaf; the tiptap document model (schema, generateJSON,
|
|
12
|
+
* renderToHTMLString) handles JSON interchange on top of it. */
|
|
13
|
+
export function TiptapEditor(props: TiptapEditorProps) @{
|
|
14
|
+
const inner = useRef<RichTextEditorHandle | null>(null)
|
|
15
|
+
const jsonReady = useRef(false)
|
|
16
|
+
const lastJSON = useRef<string | undefined>(undefined)
|
|
17
|
+
|
|
18
|
+
const bindHost = useCallback((h: RichTextEditorHandle) => {
|
|
19
|
+
inner.current = h
|
|
20
|
+
|
|
21
|
+
props.bind?.({
|
|
22
|
+
getHTML: () => h.getHTML(),
|
|
23
|
+
setHTML: (html: string) => h.setHTML(html),
|
|
24
|
+
getJSON: () => (jsonBridgeReady() ? htmlToJSON(h.getHTML()) : null),
|
|
25
|
+
setJSON: (doc) => {
|
|
26
|
+
const html = jsonBridgeReady() ? jsonToHTML(doc) : null
|
|
27
|
+
if (html != null) {
|
|
28
|
+
lastJSON.current = JSON.stringify(doc)
|
|
29
|
+
h.setHTML(html)
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
apply: (format) => h.apply(format),
|
|
33
|
+
linkTo: (url, anchor) => h.linkTo(url, anchor),
|
|
34
|
+
removeLink: () => h.removeLink(),
|
|
35
|
+
isActive: (format) => h.isActive(format),
|
|
36
|
+
undo: () => h.undo(),
|
|
37
|
+
redo: () => h.redo(),
|
|
38
|
+
focus: () => h.focus(),
|
|
39
|
+
blur: () => h.blur(),
|
|
40
|
+
isFocused: () => h.isFocused(),
|
|
41
|
+
native: h.native,
|
|
42
|
+
} satisfies TiptapEditorHandle)
|
|
43
|
+
}, [])
|
|
44
|
+
|
|
45
|
+
// The bridge loads once per process — dynamic imports of the DOM-free
|
|
46
|
+
// tiptap slices, plus the zeed-dom parse shim.
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
ensureJSONBridge().then((ok) => {
|
|
49
|
+
jsonReady.current = ok
|
|
50
|
+
props.onJSONReady?.(ok)
|
|
51
|
+
})
|
|
52
|
+
}, [])
|
|
53
|
+
|
|
54
|
+
// `json` prop → editor content, when the bridge is up and the doc wasn't
|
|
55
|
+
// emitted by the editor itself.
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!props.json || !jsonBridgeReady() || !inner.current) return
|
|
58
|
+
const stamp = JSON.stringify(props.json)
|
|
59
|
+
if (stamp === lastJSON.current) return
|
|
60
|
+
const html = jsonToHTML(props.json)
|
|
61
|
+
if (html != null) {
|
|
62
|
+
lastJSON.current = stamp
|
|
63
|
+
inner.current.setHTML(html)
|
|
64
|
+
}
|
|
65
|
+
}, [props.json])
|
|
66
|
+
|
|
67
|
+
const emitJSON = () => {
|
|
68
|
+
if (!inner.current || !jsonBridgeReady()) return
|
|
69
|
+
const doc = htmlToJSON(inner.current.getHTML())
|
|
70
|
+
if (doc) {
|
|
71
|
+
lastJSON.current = JSON.stringify(doc)
|
|
72
|
+
props.onJSONChange?.(doc)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
<RichTextEditor
|
|
77
|
+
id={props.id}
|
|
78
|
+
className={props.className}
|
|
79
|
+
style={props.style}
|
|
80
|
+
value={props.value}
|
|
81
|
+
placeholder={props.placeholder}
|
|
82
|
+
editable={props.editable}
|
|
83
|
+
autofocus={props.autofocus}
|
|
84
|
+
onReady={props.onReady}
|
|
85
|
+
onChange={(html) => {
|
|
86
|
+
props.onChange?.(html)
|
|
87
|
+
emitJSON()
|
|
88
|
+
}}
|
|
89
|
+
onSelectionChange={props.onSelectionChange}
|
|
90
|
+
onFocus={props.onFocus}
|
|
91
|
+
onBlur={props.onBlur}
|
|
92
|
+
bind={bindHost}
|
|
93
|
+
android={props.android}
|
|
94
|
+
ios={props.ios}
|
|
95
|
+
row={props.row}
|
|
96
|
+
col={props.col}
|
|
97
|
+
rowSpan={props.rowSpan}
|
|
98
|
+
colSpan={props.colSpan}
|
|
99
|
+
dock={props.dock}
|
|
100
|
+
left={props.left}
|
|
101
|
+
top={props.top}
|
|
102
|
+
flexGrow={props.flexGrow}
|
|
103
|
+
flexShrink={props.flexShrink}
|
|
104
|
+
alignSelf={props.alignSelf}
|
|
105
|
+
order={props.order}
|
|
106
|
+
/>;
|
|
107
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'octane'
|
|
2
|
+
import { EditorContent, useEditor } from '@octanejs/tiptap'
|
|
3
|
+
import StarterKit from '@tiptap/starter-kit'
|
|
4
|
+
import type { TiptapEditorHandle, TiptapEditorProps, TiptapFormat } from './types'
|
|
5
|
+
|
|
6
|
+
export const supported = true
|
|
7
|
+
|
|
8
|
+
type WebFormat = Exclude<TiptapFormat, 'link' | 'taskList' | 'highlight' | 'subscript' | 'superscript' | 'alignLeft' | 'alignCenter' | 'alignRight'>
|
|
9
|
+
|
|
10
|
+
const COMMAND: Record<WebFormat, (chain: any) => any> = {
|
|
11
|
+
bold: (c) => c.toggleBold(),
|
|
12
|
+
italic: (c) => c.toggleItalic(),
|
|
13
|
+
underline: (c) => c.toggleUnderline(),
|
|
14
|
+
strikethrough: (c) => c.toggleStrike(),
|
|
15
|
+
code: (c) => c.toggleCode(),
|
|
16
|
+
blockquote: (c) => c.toggleBlockquote(),
|
|
17
|
+
bulletList: (c) => c.toggleBulletList(),
|
|
18
|
+
orderedList: (c) => c.toggleOrderedList(),
|
|
19
|
+
codeBlock: (c) => c.toggleCodeBlock(),
|
|
20
|
+
horizontalRule: (c) => c.setHorizontalRule(),
|
|
21
|
+
paragraph: (c) => c.setParagraph(),
|
|
22
|
+
heading1: (c) => c.toggleHeading({ level: 1 }),
|
|
23
|
+
heading2: (c) => c.toggleHeading({ level: 2 }),
|
|
24
|
+
heading3: (c) => c.toggleHeading({ level: 3 }),
|
|
25
|
+
heading4: (c) => c.toggleHeading({ level: 4 }),
|
|
26
|
+
heading5: (c) => c.toggleHeading({ level: 5 }),
|
|
27
|
+
heading6: (c) => c.toggleHeading({ level: 6 }),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const IS_ACTIVE: Partial<Record<TiptapFormat, (editor: any) => boolean>> = {
|
|
31
|
+
bold: (e) => e.isActive('bold'),
|
|
32
|
+
italic: (e) => e.isActive('italic'),
|
|
33
|
+
underline: (e) => e.isActive('underline'),
|
|
34
|
+
strikethrough: (e) => e.isActive('strike'),
|
|
35
|
+
code: (e) => e.isActive('code'),
|
|
36
|
+
blockquote: (e) => e.isActive('blockquote'),
|
|
37
|
+
bulletList: (e) => e.isActive('bulletList'),
|
|
38
|
+
orderedList: (e) => e.isActive('orderedList'),
|
|
39
|
+
codeBlock: (e) => e.isActive('codeBlock'),
|
|
40
|
+
link: (e) => e.isActive('link'),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ACTIVE_OF: [TiptapFormat, string, any?][] = [
|
|
44
|
+
['bold', 'bold'],
|
|
45
|
+
['italic', 'italic'],
|
|
46
|
+
['underline', 'underline'],
|
|
47
|
+
['strikethrough', 'strike'],
|
|
48
|
+
['code', 'code'],
|
|
49
|
+
['blockquote', 'blockquote'],
|
|
50
|
+
['bulletList', 'bulletList'],
|
|
51
|
+
['orderedList', 'orderedList'],
|
|
52
|
+
['codeBlock', 'codeBlock'],
|
|
53
|
+
['link', 'link'],
|
|
54
|
+
['heading1', 'heading', { level: 1 }],
|
|
55
|
+
['heading2', 'heading', { level: 2 }],
|
|
56
|
+
['heading3', 'heading', { level: 3 }],
|
|
57
|
+
['heading4', 'heading', { level: 4 }],
|
|
58
|
+
['heading5', 'heading', { level: 5 }],
|
|
59
|
+
['heading6', 'heading', { level: 6 }],
|
|
60
|
+
['paragraph', 'paragraph'],
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
function activeFormats(editor: any): TiptapFormat[] {
|
|
64
|
+
const out: TiptapFormat[] = []
|
|
65
|
+
for (const [name, kind, attrs] of ACTIVE_OF) {
|
|
66
|
+
if (editor.isActive(kind, attrs)) out.push(name)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return out
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Unified editor facade, web half — a thin adapter over the published
|
|
73
|
+
* `@octanejs/tiptap` binding (useEditor + EditorContent + StarterKit). */
|
|
74
|
+
export function TiptapEditor(props: TiptapEditorProps) @{
|
|
75
|
+
const lastEmitted = useRef<string | undefined>(undefined)
|
|
76
|
+
const lastJSON = useRef<string | undefined>(undefined)
|
|
77
|
+
const bound = useRef<TiptapEditorHandle | undefined>(undefined)
|
|
78
|
+
|
|
79
|
+
const editor = useEditor({
|
|
80
|
+
extensions: [StarterKit],
|
|
81
|
+
content: (props.value ?? props.json ?? '') as any,
|
|
82
|
+
editable: props.editable !== false,
|
|
83
|
+
autofocus: props.autofocus ?? false,
|
|
84
|
+
onCreate: () => props.onReady?.(),
|
|
85
|
+
onUpdate: ({ editor: e }: any) => {
|
|
86
|
+
const html = e.getHTML()
|
|
87
|
+
lastEmitted.current = html
|
|
88
|
+
props.onChange?.(html)
|
|
89
|
+
const doc = e.getJSON()
|
|
90
|
+
lastJSON.current = JSON.stringify(doc)
|
|
91
|
+
props.onJSONChange?.(doc)
|
|
92
|
+
},
|
|
93
|
+
onFocus: () => props.onFocus?.(),
|
|
94
|
+
onBlur: () => props.onBlur?.(),
|
|
95
|
+
onSelectionUpdate: ({ editor: e }: any) =>
|
|
96
|
+
props.onSelectionChange?.({
|
|
97
|
+
start: e.state.selection.from,
|
|
98
|
+
end: e.state.selection.to,
|
|
99
|
+
active: activeFormats(e),
|
|
100
|
+
}),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// External `value` changes push setContent; editor-emitted HTML is skipped
|
|
104
|
+
// (the transaction already owns the doc).
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!editor || props.value === undefined) return
|
|
107
|
+
if (props.value === lastEmitted.current) return
|
|
108
|
+
editor.commands.setContent(props.value)
|
|
109
|
+
}, [editor, props.value])
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
if (!editor || props.json === undefined) return
|
|
113
|
+
if (JSON.stringify(props.json) === lastJSON.current) return
|
|
114
|
+
editor.commands.setContent(props.json as any)
|
|
115
|
+
}, [editor, props.json])
|
|
116
|
+
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!editor) return
|
|
119
|
+
editor.setEditable(props.editable !== false)
|
|
120
|
+
}, [editor, props.editable])
|
|
121
|
+
|
|
122
|
+
// The handle rebinds whenever the editor instance appears — stable
|
|
123
|
+
// afterward (useEditor keeps the instance identity).
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!editor || bound.current?.native === editor) return
|
|
126
|
+
const e: any = editor
|
|
127
|
+
bound.current = {
|
|
128
|
+
getHTML: () => e.getHTML(),
|
|
129
|
+
setHTML: (html: string) => e.commands.setContent(html),
|
|
130
|
+
getJSON: () => e.getJSON(),
|
|
131
|
+
setJSON: (doc) => e.commands.setContent(doc),
|
|
132
|
+
apply: (format) => {
|
|
133
|
+
const run = COMMAND[format as WebFormat]
|
|
134
|
+
if (run) run(e.chain().focus()).run()
|
|
135
|
+
},
|
|
136
|
+
linkTo: (url, anchor) => {
|
|
137
|
+
if (anchor != null && anchor !== '') {
|
|
138
|
+
e.chain().focus().insertContent({ type: 'text', text: anchor, marks: [{ type: 'link', attrs: { href: url } }] }).run()
|
|
139
|
+
} else {
|
|
140
|
+
e.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
removeLink: () => e.chain().focus().unsetLink().run(),
|
|
144
|
+
isActive: (format) => IS_ACTIVE[format]?.(e) ?? false,
|
|
145
|
+
undo: () => e.commands.undo(),
|
|
146
|
+
redo: () => e.commands.redo(),
|
|
147
|
+
focus: () => e.commands.focus(),
|
|
148
|
+
blur: () => e.commands.blur(),
|
|
149
|
+
isFocused: () => e.isFocused,
|
|
150
|
+
native: e,
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
props.bind?.(bound.current)
|
|
154
|
+
}, [editor])
|
|
155
|
+
|
|
156
|
+
<div className={['vx-richtext', props.className]} style={props.style}>
|
|
157
|
+
<EditorContent editor={editor} />
|
|
158
|
+
</div>
|
|
159
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { TiptapEditorProps } from './types'
|
|
2
|
+
|
|
3
|
+
export const supported = false
|
|
4
|
+
|
|
5
|
+
/** AppKit host has no rich-text surface yet. */
|
|
6
|
+
export function TiptapEditor(_props: TiptapEditorProps): null {
|
|
7
|
+
return null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type * from './types'
|
|
11
|
+
|
|
12
|
+
/** No editor, no bridge. */
|
|
13
|
+
export function ensureJSONBridge(): Promise<boolean> {
|
|
14
|
+
return Promise.resolve(false)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function jsonBridgeReady(): boolean {
|
|
18
|
+
return false
|
|
19
|
+
}
|
package/src/index.ts
ADDED
package/src/index.web.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { TiptapEditor, supported } from './TiptapEditor.web.tsrx'
|
|
2
|
+
export type * from './types'
|
|
3
|
+
|
|
4
|
+
/** Web needs no lazy bridge — tiptap is in the bundle already. Kept for
|
|
5
|
+
* call-site parity with the native entry. */
|
|
6
|
+
export function ensureJSONBridge(): Promise<boolean> {
|
|
7
|
+
return Promise.resolve(true)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function jsonBridgeReady(): boolean {
|
|
11
|
+
return true
|
|
12
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/** Lazy tiptap document-model bridge for native runtimes. The DOM-free
|
|
2
|
+
* slices (@tiptap/core schema + generateJSON, @tiptap/static-renderer,
|
|
3
|
+
* zeed-dom as the parser host) load on demand — on a runtime that can't
|
|
4
|
+
* host them the bridge settles to unavailable and callers get null/no-op.
|
|
5
|
+
*
|
|
6
|
+
* Import behavior was proven by the demos probe (see packages/demos
|
|
7
|
+
* TiptapProbe): all four modules load cleanly on both NS engines; the
|
|
8
|
+
* failure mode left is a future version or bundler regression, which the
|
|
9
|
+
* catch below degrades instead of crashing. */
|
|
10
|
+
|
|
11
|
+
import type { TiptapJSON } from './types'
|
|
12
|
+
|
|
13
|
+
interface Bridge {
|
|
14
|
+
generateJSON: (html: string, extensions: any[]) => any
|
|
15
|
+
renderToHTMLString: (options: { extensions: any[]; content: any }) => string
|
|
16
|
+
extensions: any[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let bridge: Bridge | null | undefined
|
|
20
|
+
|
|
21
|
+
/** zeed-dom's classList is a plain {contains,add,remove} object; tiptap's
|
|
22
|
+
* codeBlock parseHTML spreads it. Wrap the VElement getter so it hands back
|
|
23
|
+
* an iterable array that keeps the mutation methods. */
|
|
24
|
+
function patchClassList(VElement: any): void {
|
|
25
|
+
const proto = VElement?.prototype
|
|
26
|
+
if (!proto) {
|
|
27
|
+
throw new Error('VElement.prototype unavailable')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const desc = Object.getOwnPropertyDescriptor(proto, 'classList')
|
|
31
|
+
if (!desc?.get) {
|
|
32
|
+
throw new Error('classList is not a prototype getter')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if ((desc.get as any).__xplatPatched) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const patched = function (this: any) {
|
|
40
|
+
const cl = desc.get!.call(this)
|
|
41
|
+
if (cl && typeof cl[Symbol.iterator] !== 'function') {
|
|
42
|
+
const tokens = String(this.getAttribute('class') ?? '')
|
|
43
|
+
.split(/\s+/)
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
|
|
46
|
+
return Object.assign(tokens, cl)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return cl
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
;(patched as any).__xplatPatched = true
|
|
53
|
+
Object.defineProperty(proto, 'classList', {
|
|
54
|
+
configurable: true,
|
|
55
|
+
get: patched,
|
|
56
|
+
set: desc.set,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Install the minimal parse globals generateJSON's elementFromString
|
|
61
|
+
* reads. Only installs when the host lacks them, so a real DOM (web
|
|
62
|
+
* target) is never clobbered. */
|
|
63
|
+
function installDomShim(zeed: any): void {
|
|
64
|
+
const g = globalThis as any
|
|
65
|
+
if (g.window?.DOMParser) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class ZeedDOMParser {
|
|
70
|
+
parseFromString(html: string) {
|
|
71
|
+
const doc = zeed.createHTMLDocument()
|
|
72
|
+
doc.body.innerHTML = html
|
|
73
|
+
return doc
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
g.window = { DOMParser: ZeedDOMParser }
|
|
78
|
+
g.document = zeed.createHTMLDocument()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function ensureJSONBridge(): Promise<boolean> {
|
|
82
|
+
if (bridge !== undefined) {
|
|
83
|
+
return bridge !== null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const [core, sk, sr, zeed] = await Promise.all([
|
|
88
|
+
import('@tiptap/core'),
|
|
89
|
+
import('@tiptap/starter-kit'),
|
|
90
|
+
import('@tiptap/static-renderer'),
|
|
91
|
+
import('zeed-dom'),
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
patchClassList(zeed.VElement)
|
|
95
|
+
installDomShim(zeed)
|
|
96
|
+
const StarterKit = (sk as any).default ?? (sk as any).StarterKit
|
|
97
|
+
|
|
98
|
+
bridge = {
|
|
99
|
+
generateJSON: core.generateJSON,
|
|
100
|
+
renderToHTMLString: sr.renderToHTMLString,
|
|
101
|
+
extensions: [StarterKit],
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return true
|
|
105
|
+
} catch {
|
|
106
|
+
bridge = null
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** HTML → doc JSON through tiptap's own parseHTML rules. Null when the
|
|
112
|
+
* bridge hasn't loaded (call ensureJSONBridge first / check `ready`). */
|
|
113
|
+
export function htmlToJSON(html: string): TiptapJSON | null {
|
|
114
|
+
return (bridge?.generateJSON(html, bridge.extensions) as TiptapJSON | undefined) ?? null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Doc JSON → HTML via tiptap's own renderHTML rules (no DOM). */
|
|
118
|
+
export function jsonToHTML(doc: TiptapJSON): string | null {
|
|
119
|
+
return bridge?.renderToHTMLString({ extensions: bridge.extensions, content: doc }) ?? null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function jsonBridgeReady(): boolean {
|
|
123
|
+
return bridge != null
|
|
124
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Format vocabulary shared by the web (tiptap StarterKit) and native
|
|
2
|
+
* (Aztec) backends. On web, starter-kit covers everything except
|
|
3
|
+
* taskList/highlight/subscript/superscript/align* — those no-op there and
|
|
4
|
+
* map to Aztec formats on Android. */
|
|
5
|
+
export type TiptapFormat =
|
|
6
|
+
| 'bold'
|
|
7
|
+
| 'italic'
|
|
8
|
+
| 'underline'
|
|
9
|
+
| 'strikethrough'
|
|
10
|
+
| 'code'
|
|
11
|
+
| 'blockquote'
|
|
12
|
+
| 'bulletList'
|
|
13
|
+
| 'orderedList'
|
|
14
|
+
| 'taskList'
|
|
15
|
+
| 'codeBlock'
|
|
16
|
+
| 'horizontalRule'
|
|
17
|
+
| 'highlight'
|
|
18
|
+
| 'subscript'
|
|
19
|
+
| 'superscript'
|
|
20
|
+
| 'paragraph'
|
|
21
|
+
| 'heading1'
|
|
22
|
+
| 'heading2'
|
|
23
|
+
| 'heading3'
|
|
24
|
+
| 'heading4'
|
|
25
|
+
| 'heading5'
|
|
26
|
+
| 'heading6'
|
|
27
|
+
| 'alignLeft'
|
|
28
|
+
| 'alignCenter'
|
|
29
|
+
| 'alignRight'
|
|
30
|
+
| 'link'
|
|
31
|
+
|
|
32
|
+
/** Tiptap document JSON (`{ type: 'doc', content: [...] }`). Opaque here —
|
|
33
|
+
* callers round-trip through tiptap's own schema on both platforms. */
|
|
34
|
+
export type TiptapJSON = { type: string; content?: unknown[] } & Record<string, unknown>
|
|
35
|
+
|
|
36
|
+
export interface TiptapEditorHandle {
|
|
37
|
+
getHTML(): string
|
|
38
|
+
/** Web: `commands.setContent` (undo-aware). Native: Aztec `fromHtml` —
|
|
39
|
+
* resets undo history. */
|
|
40
|
+
setHTML(html: string): void
|
|
41
|
+
/** Doc JSON, or null while the native schema modules are still loading /
|
|
42
|
+
* on a runtime that can't host them (`json` flag). Web is always ready. */
|
|
43
|
+
getJSON(): TiptapJSON | null
|
|
44
|
+
setJSON(doc: TiptapJSON): void
|
|
45
|
+
apply(format: TiptapFormat): void
|
|
46
|
+
linkTo(url: string, anchor?: string): void
|
|
47
|
+
removeLink(): void
|
|
48
|
+
isActive(format: TiptapFormat): boolean
|
|
49
|
+
undo(): void
|
|
50
|
+
redo(): void
|
|
51
|
+
focus(): void
|
|
52
|
+
blur(): void
|
|
53
|
+
isFocused(): boolean
|
|
54
|
+
/** The platform surface — a tiptap `Editor` on web, `AztecText` on
|
|
55
|
+
* Android. */
|
|
56
|
+
native: any
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TiptapEditorProps {
|
|
60
|
+
id?: string
|
|
61
|
+
className?: string
|
|
62
|
+
style?: any
|
|
63
|
+
/** Document HTML. Seed at mount; external changes re-push (web: via
|
|
64
|
+
* setContent, preserving the diff loop the editor emitted is skipped). */
|
|
65
|
+
value?: string
|
|
66
|
+
/** Document JSON. Takes precedence over `value` when both arrive. On
|
|
67
|
+
* native it waits for the schema modules (see `onJSONReady`). */
|
|
68
|
+
json?: TiptapJSON
|
|
69
|
+
placeholder?: string
|
|
70
|
+
editable?: boolean
|
|
71
|
+
autofocus?: boolean
|
|
72
|
+
onReady?: () => void
|
|
73
|
+
/** Fires once when the native JSON bridge settles — `true` if the
|
|
74
|
+
* DOM-free tiptap modules loaded, `false` on runtimes that can't host
|
|
75
|
+
* them (JSON calls then no-op / return null). Never fires on web. */
|
|
76
|
+
onJSONReady?: (ready: boolean) => void
|
|
77
|
+
onChange?: (html: string) => void
|
|
78
|
+
onJSONChange?: (doc: TiptapJSON) => void
|
|
79
|
+
onSelectionChange?: (event: { start: number; end: number; active: TiptapFormat[] }) => void
|
|
80
|
+
onFocus?: () => void
|
|
81
|
+
onBlur?: () => void
|
|
82
|
+
bind?: (handle: TiptapEditorHandle) => void
|
|
83
|
+
android?: Record<string, any>
|
|
84
|
+
ios?: Record<string, any>
|
|
85
|
+
row?: number | string
|
|
86
|
+
col?: number | string
|
|
87
|
+
rowSpan?: number
|
|
88
|
+
colSpan?: number
|
|
89
|
+
dock?: 'left' | 'top' | 'right' | 'bottom'
|
|
90
|
+
left?: number
|
|
91
|
+
top?: number
|
|
92
|
+
flexGrow?: number
|
|
93
|
+
flexShrink?: number
|
|
94
|
+
alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline'
|
|
95
|
+
order?: number
|
|
96
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { UniversalComponent } from 'octane/universal'
|
|
2
|
+
import type { TiptapEditorProps, TiptapJSON } from './types'
|
|
3
|
+
|
|
4
|
+
export declare const TiptapEditor: UniversalComponent<TiptapEditorProps>
|
|
5
|
+
export declare const supported: boolean
|
|
6
|
+
export declare function ensureJSONBridge(): Promise<boolean>
|
|
7
|
+
export declare function jsonBridgeReady(): boolean
|
|
8
|
+
export type * from './types'
|
package/types/types.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Format vocabulary shared by the web (tiptap StarterKit) and native
|
|
2
|
+
* (Aztec) backends. On web, starter-kit covers everything except
|
|
3
|
+
* taskList/highlight/subscript/superscript/align* — those no-op there and
|
|
4
|
+
* map to Aztec formats on Android. */
|
|
5
|
+
export type TiptapFormat = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'code' | 'blockquote' | 'bulletList' | 'orderedList' | 'taskList' | 'codeBlock' | 'horizontalRule' | 'highlight' | 'subscript' | 'superscript' | 'paragraph' | 'heading1' | 'heading2' | 'heading3' | 'heading4' | 'heading5' | 'heading6' | 'alignLeft' | 'alignCenter' | 'alignRight' | 'link';
|
|
6
|
+
/** Tiptap document JSON (`{ type: 'doc', content: [...] }`). Opaque here —
|
|
7
|
+
* callers round-trip through tiptap's own schema on both platforms. */
|
|
8
|
+
export type TiptapJSON = {
|
|
9
|
+
type: string;
|
|
10
|
+
content?: unknown[];
|
|
11
|
+
} & Record<string, unknown>;
|
|
12
|
+
export interface TiptapEditorHandle {
|
|
13
|
+
getHTML(): string;
|
|
14
|
+
/** Web: `commands.setContent` (undo-aware). Native: Aztec `fromHtml` —
|
|
15
|
+
* resets undo history. */
|
|
16
|
+
setHTML(html: string): void;
|
|
17
|
+
/** Doc JSON, or null while the native schema modules are still loading /
|
|
18
|
+
* on a runtime that can't host them (`json` flag). Web is always ready. */
|
|
19
|
+
getJSON(): TiptapJSON | null;
|
|
20
|
+
setJSON(doc: TiptapJSON): void;
|
|
21
|
+
apply(format: TiptapFormat): void;
|
|
22
|
+
linkTo(url: string, anchor?: string): void;
|
|
23
|
+
removeLink(): void;
|
|
24
|
+
isActive(format: TiptapFormat): boolean;
|
|
25
|
+
undo(): void;
|
|
26
|
+
redo(): void;
|
|
27
|
+
focus(): void;
|
|
28
|
+
blur(): void;
|
|
29
|
+
isFocused(): boolean;
|
|
30
|
+
/** The platform surface — a tiptap `Editor` on web, `AztecText` on
|
|
31
|
+
* Android. */
|
|
32
|
+
native: any;
|
|
33
|
+
}
|
|
34
|
+
export interface TiptapEditorProps {
|
|
35
|
+
id?: string;
|
|
36
|
+
className?: string;
|
|
37
|
+
style?: any;
|
|
38
|
+
/** Document HTML. Seed at mount; external changes re-push (web: via
|
|
39
|
+
* setContent, preserving the diff loop the editor emitted is skipped). */
|
|
40
|
+
value?: string;
|
|
41
|
+
/** Document JSON. Takes precedence over `value` when both arrive. On
|
|
42
|
+
* native it waits for the schema modules (see `onJSONReady`). */
|
|
43
|
+
json?: TiptapJSON;
|
|
44
|
+
placeholder?: string;
|
|
45
|
+
editable?: boolean;
|
|
46
|
+
autofocus?: boolean;
|
|
47
|
+
onReady?: () => void;
|
|
48
|
+
/** Fires once when the native JSON bridge settles — `true` if the
|
|
49
|
+
* DOM-free tiptap modules loaded, `false` on runtimes that can't host
|
|
50
|
+
* them (JSON calls then no-op / return null). Never fires on web. */
|
|
51
|
+
onJSONReady?: (ready: boolean) => void;
|
|
52
|
+
onChange?: (html: string) => void;
|
|
53
|
+
onJSONChange?: (doc: TiptapJSON) => void;
|
|
54
|
+
onSelectionChange?: (event: {
|
|
55
|
+
start: number;
|
|
56
|
+
end: number;
|
|
57
|
+
active: TiptapFormat[];
|
|
58
|
+
}) => void;
|
|
59
|
+
onFocus?: () => void;
|
|
60
|
+
onBlur?: () => void;
|
|
61
|
+
bind?: (handle: TiptapEditorHandle) => void;
|
|
62
|
+
android?: Record<string, any>;
|
|
63
|
+
ios?: Record<string, any>;
|
|
64
|
+
row?: number | string;
|
|
65
|
+
col?: number | string;
|
|
66
|
+
rowSpan?: number;
|
|
67
|
+
colSpan?: number;
|
|
68
|
+
dock?: 'left' | 'top' | 'right' | 'bottom';
|
|
69
|
+
left?: number;
|
|
70
|
+
top?: number;
|
|
71
|
+
flexGrow?: number;
|
|
72
|
+
flexShrink?: number;
|
|
73
|
+
alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
|
|
74
|
+
order?: number;
|
|
75
|
+
}
|
package/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {}
|
package/index.js
DELETED