@octane-xplat/richtext 0.0.1 → 0.10.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.
@@ -0,0 +1,235 @@
1
+ import { defineUniversalComponent, hookSlots, registerElement, universalPlan, universalValue, useCallback, useEffect, useImperativeHandle, useRef } from "@nativescript-community/octane";
2
+ import { View } from "@nativescript/core";
3
+ //#region src/RichTextEditor.android.tsrx
4
+ var _hs$ = /* @__PURE__ */ hookSlots(11);
5
+ var _h$0 = /* @__PURE__ */ Symbol(_hs$);
6
+ var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
7
+ var _h$2 = /* @__PURE__ */ Symbol(_hs$ + 2);
8
+ var _h$3 = /* @__PURE__ */ Symbol(_hs$ + 3);
9
+ var _h$4 = /* @__PURE__ */ Symbol(_hs$ + 4);
10
+ var _h$5 = /* @__PURE__ */ Symbol(_hs$ + 5);
11
+ var _h$6 = /* @__PURE__ */ Symbol(_hs$ + 6);
12
+ var _h$7 = /* @__PURE__ */ Symbol(_hs$ + 7);
13
+ var _h$8 = /* @__PURE__ */ Symbol(_hs$ + 8);
14
+ var _h$9 = /* @__PURE__ */ Symbol(_hs$ + 9);
15
+ var _h$10 = /* @__PURE__ */ Symbol(_hs$ + 10);
16
+ var __octaneUniversalPlan0 = universalPlan("nativescript", {
17
+ "kind": "host",
18
+ "type": "xplatrichtext",
19
+ "bindings": [
20
+ ["ref", 0],
21
+ ["id", 1],
22
+ ["className", 2],
23
+ ["style", 3],
24
+ ["row", 4],
25
+ ["col", 5],
26
+ ["rowSpan", 6],
27
+ ["colSpan", 7],
28
+ ["dock", 8],
29
+ ["left", 9],
30
+ ["top", 10],
31
+ ["flexGrow", 11],
32
+ ["flexShrink", 12],
33
+ ["alignSelf", 13],
34
+ ["order", 14]
35
+ ]
36
+ });
37
+ var aztec = globalThis.org?.wordpress?.aztec;
38
+ var androidText = globalThis.android?.text;
39
+ var androidView = globalThis.android?.view;
40
+ var FORMATS = {
41
+ bold: () => aztec.AztecTextFormat.FORMAT_BOLD,
42
+ italic: () => aztec.AztecTextFormat.FORMAT_ITALIC,
43
+ underline: () => aztec.AztecTextFormat.FORMAT_UNDERLINE,
44
+ strikethrough: () => aztec.AztecTextFormat.FORMAT_STRIKETHROUGH,
45
+ code: () => aztec.AztecTextFormat.FORMAT_CODE,
46
+ blockquote: () => aztec.AztecTextFormat.FORMAT_QUOTE,
47
+ bulletList: () => aztec.AztecTextFormat.FORMAT_UNORDERED_LIST,
48
+ orderedList: () => aztec.AztecTextFormat.FORMAT_ORDERED_LIST,
49
+ taskList: () => aztec.AztecTextFormat.FORMAT_TASK_LIST,
50
+ codeBlock: () => aztec.AztecTextFormat.FORMAT_PREFORMAT,
51
+ horizontalRule: () => aztec.AztecTextFormat.FORMAT_HORIZONTAL_RULE,
52
+ highlight: () => aztec.AztecTextFormat.FORMAT_MARK,
53
+ subscript: () => aztec.AztecTextFormat.FORMAT_SUBSCRIPT,
54
+ superscript: () => aztec.AztecTextFormat.FORMAT_SUPERSCRIPT,
55
+ paragraph: () => aztec.AztecTextFormat.FORMAT_PARAGRAPH,
56
+ heading1: () => aztec.AztecTextFormat.FORMAT_HEADING_1,
57
+ heading2: () => aztec.AztecTextFormat.FORMAT_HEADING_2,
58
+ heading3: () => aztec.AztecTextFormat.FORMAT_HEADING_3,
59
+ heading4: () => aztec.AztecTextFormat.FORMAT_HEADING_4,
60
+ heading5: () => aztec.AztecTextFormat.FORMAT_HEADING_5,
61
+ heading6: () => aztec.AztecTextFormat.FORMAT_HEADING_6,
62
+ alignLeft: () => aztec.AztecTextFormat.FORMAT_ALIGN_LEFT,
63
+ alignCenter: () => aztec.AztecTextFormat.FORMAT_ALIGN_CENTER,
64
+ alignRight: () => aztec.AztecTextFormat.FORMAT_ALIGN_RIGHT
65
+ };
66
+ var AZTEC_OF = Object.entries(FORMATS);
67
+ var formatOf = (format) => format === "link" ? aztec.AztecTextFormat.FORMAT_LINK : FORMATS[format]();
68
+ var AztecEditor = class extends View {
69
+ createNativeView() {
70
+ return new aztec.AztecText(this._context);
71
+ }
72
+ };
73
+ registerElement("xplatrichtext", AztecEditor);
74
+ var supported = true;
75
+ var activeFormats = (editor) => {
76
+ const styles = editor.getAppliedStyles(editor.getSelectionStart(), editor.getSelectionEnd());
77
+ const count = styles?.size?.() ?? 0;
78
+ const active = /* @__PURE__ */ new Set();
79
+ for (let i = 0; i < count; i++) active.add(styles.get(i));
80
+ const out = [];
81
+ for (const [name, get] of AZTEC_OF) if (active.has(get())) out.push(name);
82
+ if (active.has(aztec.AztecTextFormat.FORMAT_LINK)) out.push("link");
83
+ return out;
84
+ };
85
+ var RichTextEditor = defineUniversalComponent("nativescript", function RichTextEditor(props) {
86
+ const host = useRef(null, _h$0);
87
+ const lastEmitted = useRef(void 0, _h$1);
88
+ const applyingValue = useRef(false, _h$2);
89
+ const keyListener = useRef(null, _h$3);
90
+ const emitTimer = useRef(null, _h$4);
91
+ useImperativeHandle(props.ref, () => handle(editor()), [], _h$5);
92
+ const bindHost = useCallback((v) => {
93
+ host.current = v;
94
+ }, [], _h$6);
95
+ const editor = () => host.current?.android;
96
+ const setEditable = (next) => {
97
+ const e = editor();
98
+ if (!e) return;
99
+ if (keyListener.current == null) keyListener.current = e.getKeyListener();
100
+ if (next) {
101
+ e.setKeyListener(keyListener.current);
102
+ e.setFocusable(true);
103
+ e.setCursorVisible(true);
104
+ } else {
105
+ e.setKeyListener(null);
106
+ e.setFocusable(false);
107
+ e.setCursorVisible(false);
108
+ }
109
+ };
110
+ const showKeyboard = (e) => {
111
+ try {
112
+ e.getContext().getSystemService(globalThis.android.content.Context.INPUT_METHOD_SERVICE)?.showSoftInput(e, androidView.inputmethod.InputMethodManager.SHOW_IMPLICIT);
113
+ } catch {}
114
+ };
115
+ const emitChange = () => {
116
+ if (!editor() || applyingValue.current) return;
117
+ clearTimeout(emitTimer.current);
118
+ emitTimer.current = setTimeout(() => {
119
+ const current = editor();
120
+ if (!current || applyingValue.current) return;
121
+ const html = current.toPlainHtml(false);
122
+ lastEmitted.current = html;
123
+ props.onChange?.(html);
124
+ }, 150);
125
+ };
126
+ const handle = (e) => ({
127
+ getHTML: () => e.toPlainHtml(false),
128
+ setHTML: (html) => {
129
+ applyingValue.current = true;
130
+ try {
131
+ e.fromHtml(html, true);
132
+ lastEmitted.current = html;
133
+ } finally {
134
+ applyingValue.current = false;
135
+ }
136
+ },
137
+ apply: (format) => {
138
+ if (format === "link") return;
139
+ e.toggleFormatting(formatOf(format));
140
+ emitChange();
141
+ },
142
+ linkTo: (url, anchor) => {
143
+ e.link(url, anchor ?? url, false);
144
+ emitChange();
145
+ },
146
+ removeLink: () => {
147
+ e.removeLink();
148
+ emitChange();
149
+ },
150
+ isActive: (format) => {
151
+ const applied = e.getAppliedStyles(e.getSelectionStart(), e.getSelectionEnd());
152
+ const count = applied?.size?.() ?? 0;
153
+ const target = formatOf(format);
154
+ for (let i = 0; i < count; i++) if (applied.get(i) === target) return true;
155
+ return false;
156
+ },
157
+ undo: () => e.undo(),
158
+ redo: () => e.redo(),
159
+ focus: () => {
160
+ e.requestFocus();
161
+ showKeyboard(e);
162
+ },
163
+ blur: () => e.clearFocus(),
164
+ isFocused: () => e.isFocused(),
165
+ native: e
166
+ });
167
+ useEffect(() => {
168
+ const e = editor();
169
+ if (!e || !aztec) return;
170
+ e.setOnSelectionChangedListener(new aztec.AztecText.OnSelectionChangedListener({ onSelectionChanged: (start, end) => props.onSelectionChange?.({
171
+ start,
172
+ end,
173
+ active: activeFormats(e)
174
+ }) }));
175
+ e.addTextChangedListener(new androidText.TextWatcher({
176
+ beforeTextChanged: () => {},
177
+ onTextChanged: () => {},
178
+ afterTextChanged: () => emitChange()
179
+ }));
180
+ e.setOnFocusChangeListener(new androidView.View.OnFocusChangeListener({ onFocusChange: (_v, focused) => focused ? props.onFocus?.() : props.onBlur?.() }));
181
+ if (props.placeholder !== void 0) e.setHint(props.placeholder);
182
+ setEditable(props.editable !== false);
183
+ if (props.value !== void 0) {
184
+ applyingValue.current = true;
185
+ try {
186
+ e.fromHtml(props.value, true);
187
+ } finally {
188
+ applyingValue.current = false;
189
+ }
190
+ }
191
+ if (props.autofocus) {
192
+ e.requestFocus();
193
+ showKeyboard(e);
194
+ }
195
+ if (props.android) Object.assign(e, props.android);
196
+ props.onReady?.();
197
+ }, [], _h$7);
198
+ useEffect(() => {
199
+ const e = editor();
200
+ if (!e || props.value === void 0) return;
201
+ if (props.value === lastEmitted.current) return;
202
+ applyingValue.current = true;
203
+ try {
204
+ e.fromHtml(props.value, true);
205
+ } finally {
206
+ applyingValue.current = false;
207
+ }
208
+ }, [props.value], _h$8);
209
+ useEffect(() => {
210
+ setEditable(props.editable !== false);
211
+ }, [props.editable], _h$9);
212
+ useEffect(() => {
213
+ const e = editor();
214
+ if (e && props.placeholder !== void 0) e.setHint(props.placeholder);
215
+ }, [props.placeholder], _h$10);
216
+ return universalValue(__octaneUniversalPlan0, [
217
+ bindHost,
218
+ props.id,
219
+ props.className,
220
+ props.style,
221
+ props.row,
222
+ props.col,
223
+ props.rowSpan,
224
+ props.colSpan,
225
+ props.dock,
226
+ props.left,
227
+ props.top,
228
+ props.flexGrow,
229
+ props.flexShrink,
230
+ props.alignSelf,
231
+ props.order
232
+ ]);
233
+ }, { "module": "@nativescript-community/octane" });
234
+ //#endregion
235
+ export { RichTextEditor, supported };
@@ -0,0 +1,22 @@
1
+ import { defineUniversalComponent, universalPlan, universalValue } from "@nativescript-community/octane";
2
+ //#region src/RichTextEditor.ios.tsrx
3
+ var __octaneUniversalPlan0 = universalPlan("nativescript", {
4
+ "kind": "host",
5
+ "type": "label",
6
+ "props": { "text": "Rich text editing is not supported on iOS yet." },
7
+ "bindings": [
8
+ ["id", 0],
9
+ ["className", 1],
10
+ ["style", 2]
11
+ ]
12
+ });
13
+ var supported = false;
14
+ var RichTextEditor = defineUniversalComponent("nativescript", function RichTextEditor(props) {
15
+ return universalValue(__octaneUniversalPlan0, [
16
+ props.id,
17
+ props.className,
18
+ props.style
19
+ ]);
20
+ }, { "module": "@nativescript-community/octane" });
21
+ //#endregion
22
+ export { RichTextEditor, supported };
@@ -0,0 +1,2 @@
1
+ import { RichTextEditor, supported } from "./RichTextEditor";
2
+ export { RichTextEditor, supported };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "ca3e4cfe-2095-45b7-8420-77a24704b7b4",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
@@ -0,0 +1,9 @@
1
+ //#region src/index.web.ts
2
+ var supported = false;
3
+ /** Web routes through @octane-xplat/tiptap — this leaf only exists for the
4
+ * native (Aztec) surface. */
5
+ function RichTextEditor(_props) {
6
+ return null;
7
+ }
8
+ //#endregion
9
+ export { RichTextEditor, supported };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "87e27032-9a72-48e8-857d-c8a8216a9331",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
package/package.json CHANGED
@@ -1,19 +1,104 @@
1
1
  {
2
2
  "name": "@octane-xplat/richtext",
3
- "version": "0.0.1",
4
- "description": "Native rich-text editing leaf for Octane xplat — WordPress Aztec on Android (iOS stubbed, deferred)",
3
+ "version": "0.10.0",
4
+ "description": "Native rich-text editing leaf for Octane xplat — Android Aztec and AppKit WKWebView StarterKit",
5
+ "keywords": [
6
+ "android",
7
+ "aztec",
8
+ "cross-platform",
9
+ "editor",
10
+ "octane",
11
+ "rich-text",
12
+ "richtext",
13
+ "xplat"
14
+ ],
15
+ "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/richtext",
5
16
  "license": "MIT",
6
17
  "repository": {
7
18
  "type": "git",
8
- "url": "https://github.com/octane-xplat/octane-xplat.git",
19
+ "url": "https://github.com/octane-xplat/octane-xplat",
9
20
  "directory": "packages/richtext"
10
21
  },
11
- "homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/richtext",
12
- "main": "./index.js",
13
- "types": "./index.d.ts",
14
22
  "files": [
15
- "README.md",
16
- "index.js",
17
- "index.d.ts"
18
- ]
19
- }
23
+ "dist",
24
+ "types",
25
+ "src",
26
+ "platforms"
27
+ ],
28
+ "type": "module",
29
+ "exports": {
30
+ ".": {
31
+ "macos": {
32
+ "types": "./types/index.macos.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
+ "./macos-host": {
47
+ "types": "./types/macos-host.d.ts",
48
+ "default": "./src/NativeEditor.macos.tsx"
49
+ },
50
+ "./browser-transport": {
51
+ "types": "./types/browser-transport.d.ts",
52
+ "default": "./src/browser-transport.web.ts"
53
+ }
54
+ },
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "devDependencies": {
59
+ "@nativescript-community/octane": "0.2.4",
60
+ "@nativescript/core": "9.1.2",
61
+ "@octane-xplat/macos-renderer": "0.10.0",
62
+ "@octanejs/vite-plugin": "0.1.61",
63
+ "@tiptap/core": "3.28.0",
64
+ "@tiptap/starter-kit": "3.28.0",
65
+ "octane": "0.6.3",
66
+ "typescript": "5.9.3",
67
+ "vite": "8.3.0"
68
+ },
69
+ "peerDependencies": {
70
+ "@nativescript-community/octane": ">=0.2.1 <1",
71
+ "@nativescript/core": ">=9.1.0 <10",
72
+ "octane": ">=0.6.3 <1"
73
+ },
74
+ "peerDependenciesMeta": {
75
+ "@nativescript-community/octane": {
76
+ "optional": true
77
+ },
78
+ "@nativescript/core": {
79
+ "optional": true
80
+ },
81
+ "octane": {
82
+ "optional": true
83
+ }
84
+ },
85
+ "nativescript": {
86
+ "platforms": {
87
+ "android": "9.1.0",
88
+ "ios": "9.1.0"
89
+ }
90
+ },
91
+ "xplat": {
92
+ "macos": {
93
+ "frameworks": [
94
+ "WebKit"
95
+ ]
96
+ }
97
+ },
98
+ "scripts": {
99
+ "build": "vite build && vite build --mode native && tsc -p tsconfig.types.json && node ../../scripts/check-native-dist.mjs && node build-browser.mjs",
100
+ "test": "node --test test/*.test.mjs",
101
+ "test:macos": "node test/verify-wk.mjs",
102
+ "test:packed": "node test/packed-consumer.mjs"
103
+ }
104
+ }
@@ -0,0 +1,7 @@
1
+ repositories {
2
+ maven { url "https://a8c-libs.s3.amazonaws.com/android" }
3
+ }
4
+
5
+ dependencies {
6
+ implementation "org.wordpress:aztec:v2.1.7"
7
+ }
@@ -0,0 +1 @@
1
+ #import <AppKit/AppKit.h>
@@ -0,0 +1,63 @@
1
+ import AppKit
2
+ import WebKit
3
+
4
+ // One isolated, local document per editor. No general app/service bridge.
5
+ private final class EditorChannel: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
6
+ var dispatch: ((String) -> Void)?
7
+ func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) {
8
+ guard message.frameInfo.isMainFrame, let body = message.body as? String else { return }
9
+ dispatch?(body)
10
+ }
11
+ func webView(_: WKWebView, decidePolicyFor action: WKNavigationAction,
12
+ decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
13
+ decisionHandler(action.request.url?.absoluteString == "about:blank" ? .allow : .cancel)
14
+ }
15
+ }
16
+
17
+ @objc(XplatEditorHost)
18
+ public final class XplatEditorHost: NSObject {
19
+ private let webView: WKWebView
20
+ private let channel = EditorChannel()
21
+ private var disposed = false
22
+ @objc public override init() {
23
+ let config = WKWebViewConfiguration()
24
+ config.websiteDataStore = .nonPersistent()
25
+ webView = WKWebView(frame: .zero, configuration: config)
26
+ super.init()
27
+ config.userContentController.add(channel, name: "editor")
28
+ webView.navigationDelegate = channel
29
+ webView.autoresizingMask = [.width, .height]
30
+ }
31
+ @objc public func attach(to parent: NSView) {
32
+ webView.frame = parent.bounds
33
+ parent.addSubview(webView)
34
+ }
35
+ @objc public func installDispatcher(_ dispatch: @escaping (String) -> Void) {
36
+ channel.dispatch = dispatch
37
+ }
38
+ @objc public func load(_ html: String) {
39
+ guard !disposed else { return }
40
+ webView.loadHTMLString(html, baseURL: nil)
41
+ }
42
+ @objc public func deliver(_ packet: String) {
43
+ guard !disposed,
44
+ let data = try? JSONSerialization.data(withJSONObject: [packet]),
45
+ let literal = String(data: data, encoding: .utf8) else { return }
46
+ webView.evaluateJavaScript("window.__xplatEditorReceive?.(\(literal)[0])") { [weak self] _, error in
47
+ if let error {
48
+ let body = ["event": "error", "message": error.localizedDescription]
49
+ if let data = try? JSONSerialization.data(withJSONObject: body),
50
+ let text = String(data: data, encoding: .utf8) { self?.channel.dispatch?(text) }
51
+ }
52
+ }
53
+ }
54
+ @objc public func dispose() {
55
+ guard !disposed else { return }
56
+ disposed = true
57
+ channel.dispatch = nil
58
+ webView.stopLoading()
59
+ webView.configuration.userContentController.removeScriptMessageHandler(forName: "editor")
60
+ webView.navigationDelegate = nil
61
+ webView.removeFromSuperview()
62
+ }
63
+ }
@@ -0,0 +1,107 @@
1
+ import { Editor } from '@tiptap/core'
2
+ import StarterKit from '@tiptap/starter-kit'
3
+ import { installEditorDocument } from './browser-transport.web'
4
+
5
+ let editor: Editor | undefined
6
+ let current: any
7
+ let lastHTML: string | undefined
8
+ let lastJSON: string | undefined
9
+ let lastHTMLInput: string | undefined
10
+ const command: Record<string, (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
+ }
23
+
24
+ for (let level = 1; level <= 6; level++) {
25
+ command['heading' + level] = (c) => c.toggleHeading({ level })
26
+ }
27
+
28
+ installEditorDocument((props) => {
29
+ current = props
30
+ if (!editor) {
31
+ const element = document.createElement('div')
32
+ document.body.append(element)
33
+ editor = new Editor({
34
+ element,
35
+ extensions: [StarterKit],
36
+ content: props.value ?? '',
37
+ editable: props.editable !== false,
38
+ autofocus: props.autofocus ?? false,
39
+ onUpdate: ({ editor: e }) => {
40
+ lastHTML = e.getHTML()
41
+ current.onChange?.(lastHTML)
42
+ },
43
+ onSelectionUpdate: ({ editor: e }) =>
44
+ current.onSelectionChange?.({
45
+ start: e.state.selection.from,
46
+ end: e.state.selection.to,
47
+ active: [],
48
+ }),
49
+ onFocus: () => current.onFocus?.(),
50
+ onBlur: () => current.onBlur?.(),
51
+ })
52
+
53
+ lastHTMLInput = JSON.stringify([props.json !== undefined, props.value])
54
+ const e = editor
55
+ props.ref({
56
+ getHTML: () => e.getHTML(),
57
+ setHTML: (html: string) => e.commands.setContent(html),
58
+ getJSON: () => e.getJSON(),
59
+ setJSON: (doc: any) => e.commands.setContent(doc),
60
+ apply: (format: string) => {
61
+ command[format]?.(e.chain().focus()).run()
62
+ },
63
+ linkTo: (url: string, anchor?: string) =>
64
+ anchor
65
+ ? e
66
+ .chain()
67
+ .focus()
68
+ .insertContent({
69
+ type: 'text',
70
+ text: anchor,
71
+ marks: [{ type: 'link', attrs: { href: url } }],
72
+ })
73
+ .run()
74
+ : e.chain().focus().extendMarkRange('link').setLink({ href: url }).run(),
75
+ removeLink: () => e.chain().focus().unsetLink().run(),
76
+ isActive: (format: string) =>
77
+ format.startsWith('heading')
78
+ ? e.isActive('heading', { level: Number(format.slice(7)) })
79
+ : e.isActive(format === 'strikethrough' ? 'strike' : format),
80
+ undo: () => e.commands.undo(),
81
+ redo: () => e.commands.redo(),
82
+ focus: () => e.commands.focus(),
83
+ blur: () => e.commands.blur(),
84
+ isFocused: () => e.isFocused,
85
+ })
86
+ }
87
+
88
+ if (props.json !== undefined) {
89
+ const stamp = JSON.stringify(props.json)
90
+ if (stamp !== lastJSON && stamp !== JSON.stringify(editor.getJSON())) {
91
+ editor.commands.setContent(props.json)
92
+ }
93
+
94
+ lastJSON = stamp
95
+ } else {
96
+ const stamp = JSON.stringify([false, props.value])
97
+ if (stamp !== lastHTMLInput && props.value !== undefined && props.value !== lastHTML) {
98
+ editor.commands.setContent(props.value)
99
+ }
100
+
101
+ lastHTMLInput = stamp
102
+ }
103
+
104
+ editor.setEditable(props.editable !== false)
105
+ editor.view.dom.dataset.placeholder = props.placeholder ?? ''
106
+ editor.view.dom.setAttribute('aria-label', props.placeholder ?? 'Rich text editor')
107
+ })
@@ -0,0 +1,9 @@
1
+ /** @jsxImportSource @octane-xplat/macos-renderer */
2
+ import type { RichTextEditorProps } from './types'
3
+ import browser from '../dist/macos/browser.js'
4
+ import { NativeEditor } from './NativeEditor.macos'
5
+
6
+ /** AppKit mounts the local editor engine in WKWebView. */
7
+ export function RichTextEditor(props: RichTextEditorProps) {
8
+ return <NativeEditor {...props} browser={browser} />
9
+ }
@@ -0,0 +1,81 @@
1
+ /** @jsxImportSource @octane-xplat/macos-renderer */
2
+ import { useCallback, useEffect, useImperativeHandle, useRef, useState } from 'octane'
3
+ import type { RichTextEditorProps } from './types'
4
+ import { createEditorSession } from './host-session.macos'
5
+
6
+ declare const XplatEditorHost: { alloc(): { init(): any } }
7
+
8
+ type HostProps = RichTextEditorProps & {
9
+ browser: string
10
+ json?: any
11
+ onJSONReady?: (ready: boolean) => void
12
+ onJSONChange?: (doc: any) => void
13
+ }
14
+
15
+ /** Internal shared AppKit/WKWebView surface, owned by the editor leaf. */
16
+ export function NativeEditor(props: HostProps) {
17
+ const latest = useRef(props)
18
+ latest.current = props
19
+ const session = useRef<ReturnType<typeof createEditorSession> | null>(null)
20
+ const [handle, setHandle] = useState<any>(null)
21
+ useImperativeHandle(props.ref, () => handle, [handle])
22
+ const attach = useCallback((parent: any) => {
23
+ session.current?.dispose()
24
+ session.current = null
25
+ if (!parent) {
26
+ setHandle(null)
27
+ return
28
+ }
29
+
30
+ const native = XplatEditorHost.alloc().init()
31
+ const next = createEditorSession(
32
+ (packet) => native.deliver(packet),
33
+ native,
34
+ () => latest.current,
35
+ )
36
+
37
+ session.current = next
38
+ native.installDispatcher((packet: string) => next.receive(packet))
39
+ native.attachTo(parent)
40
+ native.load(props.browser)
41
+ setHandle(next.handle)
42
+ }, [])
43
+
44
+ useEffect(() => {
45
+ session.current?.update({
46
+ value: props.value,
47
+ json: props.json,
48
+ placeholder: props.placeholder,
49
+ editable: props.editable,
50
+ autofocus: props.autofocus,
51
+ })
52
+ }, [handle, props.value, props.json, props.placeholder, props.editable, props.autofocus])
53
+
54
+ useEffect(
55
+ () => () => {
56
+ session.current?.dispose()
57
+ session.current = null
58
+ },
59
+ [],
60
+ )
61
+
62
+ return (
63
+ <flexboxlayout
64
+ id={props.id}
65
+ className={props.className}
66
+ style={props.style}
67
+ row={props.row}
68
+ col={props.col}
69
+ rowSpan={props.rowSpan}
70
+ colSpan={props.colSpan}
71
+ dock={props.dock}
72
+ left={props.left}
73
+ top={props.top}
74
+ flexGrow={props.flexGrow}
75
+ flexShrink={props.flexShrink}
76
+ alignSelf={props.alignSelf}
77
+ order={props.order}
78
+ ref={attach}
79
+ />
80
+ )
81
+ }