@octane-xplat/richtext 0.9.0 → 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.
@@ -1,7 +1,7 @@
1
- import { defineUniversalComponent, hookSlots, registerElement, universalPlan, universalValue, useCallback, useEffect, useRef } from "@nativescript-community/octane";
1
+ import { defineUniversalComponent, hookSlots, registerElement, universalPlan, universalValue, useCallback, useEffect, useImperativeHandle, useRef } from "@nativescript-community/octane";
2
2
  import { View } from "@nativescript/core";
3
3
  //#region src/RichTextEditor.android.tsrx
4
- var _hs$ = /* @__PURE__ */ hookSlots(10);
4
+ var _hs$ = /* @__PURE__ */ hookSlots(11);
5
5
  var _h$0 = /* @__PURE__ */ Symbol(_hs$);
6
6
  var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
7
7
  var _h$2 = /* @__PURE__ */ Symbol(_hs$ + 2);
@@ -12,6 +12,7 @@ var _h$6 = /* @__PURE__ */ Symbol(_hs$ + 6);
12
12
  var _h$7 = /* @__PURE__ */ Symbol(_hs$ + 7);
13
13
  var _h$8 = /* @__PURE__ */ Symbol(_hs$ + 8);
14
14
  var _h$9 = /* @__PURE__ */ Symbol(_hs$ + 9);
15
+ var _h$10 = /* @__PURE__ */ Symbol(_hs$ + 10);
15
16
  var __octaneUniversalPlan0 = universalPlan("nativescript", {
16
17
  "kind": "host",
17
18
  "type": "xplatrichtext",
@@ -87,9 +88,10 @@ var RichTextEditor = defineUniversalComponent("nativescript", function RichTextE
87
88
  const applyingValue = useRef(false, _h$2);
88
89
  const keyListener = useRef(null, _h$3);
89
90
  const emitTimer = useRef(null, _h$4);
91
+ useImperativeHandle(props.ref, () => handle(editor()), [], _h$5);
90
92
  const bindHost = useCallback((v) => {
91
93
  host.current = v;
92
- }, [], _h$5);
94
+ }, [], _h$6);
93
95
  const editor = () => host.current?.android;
94
96
  const setEditable = (next) => {
95
97
  const e = editor();
@@ -191,9 +193,8 @@ var RichTextEditor = defineUniversalComponent("nativescript", function RichTextE
191
193
  showKeyboard(e);
192
194
  }
193
195
  if (props.android) Object.assign(e, props.android);
194
- props.bind?.(handle(e));
195
196
  props.onReady?.();
196
- }, [], _h$6);
197
+ }, [], _h$7);
197
198
  useEffect(() => {
198
199
  const e = editor();
199
200
  if (!e || props.value === void 0) return;
@@ -204,14 +205,14 @@ var RichTextEditor = defineUniversalComponent("nativescript", function RichTextE
204
205
  } finally {
205
206
  applyingValue.current = false;
206
207
  }
207
- }, [props.value], _h$7);
208
+ }, [props.value], _h$8);
208
209
  useEffect(() => {
209
210
  setEditable(props.editable !== false);
210
- }, [props.editable], _h$8);
211
+ }, [props.editable], _h$9);
211
212
  useEffect(() => {
212
213
  const e = editor();
213
214
  if (e && props.placeholder !== void 0) e.setHint(props.placeholder);
214
- }, [props.placeholder], _h$9);
215
+ }, [props.placeholder], _h$10);
215
216
  return universalValue(__octaneUniversalPlan0, [
216
217
  bindHost,
217
218
  props.id,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "buildId": "f6769804-c57f-408a-9066-2cfebe36da4e",
3
+ "buildId": "ca3e4cfe-2095-45b7-8420-77a24704b7b4",
4
4
  "mode": "production",
5
5
  "capabilities": {
6
6
  "independentHydration": false
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "buildId": "bd95f265-682a-446a-a284-1f3419b2fb06",
3
+ "buildId": "87e27032-9a72-48e8-857d-c8a8216a9331",
4
4
  "mode": "production",
5
5
  "capabilities": {
6
6
  "independentHydration": false
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@octane-xplat/richtext",
3
- "version": "0.9.0",
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
5
  "keywords": [
6
6
  "android",
7
7
  "aztec",
@@ -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": {
@@ -42,6 +42,14 @@
42
42
  },
43
43
  "types": "./types/index.d.ts",
44
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"
45
53
  }
46
54
  },
47
55
  "publishConfig": {
@@ -50,7 +58,10 @@
50
58
  "devDependencies": {
51
59
  "@nativescript-community/octane": "0.2.4",
52
60
  "@nativescript/core": "9.1.2",
61
+ "@octane-xplat/macos-renderer": "0.10.0",
53
62
  "@octanejs/vite-plugin": "0.1.61",
63
+ "@tiptap/core": "3.28.0",
64
+ "@tiptap/starter-kit": "3.28.0",
54
65
  "octane": "0.6.3",
55
66
  "typescript": "5.9.3",
56
67
  "vite": "8.3.0"
@@ -77,7 +88,17 @@
77
88
  "ios": "9.1.0"
78
89
  }
79
90
  },
91
+ "xplat": {
92
+ "macos": {
93
+ "frameworks": [
94
+ "WebKit"
95
+ ]
96
+ }
97
+ },
80
98
  "scripts": {
81
- "build": "vite build && vite build --mode native && tsc -p tsconfig.types.json && node ../../scripts/check-native-dist.mjs"
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"
82
103
  }
83
104
  }
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  /** @jsxImportSource @nativescript-community/octane */
2
- import { useCallback, useEffect, useRef } from 'octane'
2
+ import { useImperativeHandle, useCallback, useEffect, useRef } from 'octane'
3
3
  import { View } from '@nativescript/core'
4
4
  import { registerElement } from '@nativescript-community/octane'
5
5
  import type { Attributes } from '@nativescript-community/octane/intrinsics'
@@ -96,6 +96,8 @@ export function RichTextEditor(props: RichTextEditorProps) @{
96
96
 
97
97
  // Stable identity — an inline closure re-fires ref(null)/ref(v) per
98
98
  // render, which would bounce host.current around the effects below.
99
+ useImperativeHandle(props.ref, () => handle(editor()), []);
100
+
99
101
  const bindHost = useCallback((v: any) => {
100
102
  host.current = v
101
103
  }, [])
@@ -233,7 +235,6 @@ export function RichTextEditor(props: RichTextEditorProps) @{
233
235
  }
234
236
 
235
237
  if (props.android) Object.assign(e, props.android)
236
- props.bind?.(handle(e))
237
238
  props.onReady?.()
238
239
  }, [])
239
240
 
@@ -0,0 +1,15 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import type { RichTextEditorProps } from './types'
3
+
4
+ export const supported = false
5
+
6
+ /** Windows bring-up is deferred — renders the placeholder so a shared
7
+ * screen still mounts. */
8
+ export function RichTextEditor(props: RichTextEditorProps) @{
9
+ <label
10
+ id={props.id}
11
+ className={props.className}
12
+ style={props.style}
13
+ text="Rich text editing is not supported on Windows yet."
14
+ />;
15
+ }
@@ -0,0 +1,119 @@
1
+ const formats = [
2
+ 'bold',
3
+ 'italic',
4
+ 'underline',
5
+ 'strikethrough',
6
+ 'code',
7
+ 'blockquote',
8
+ 'bulletList',
9
+ 'orderedList',
10
+ 'taskList',
11
+ 'codeBlock',
12
+ 'horizontalRule',
13
+ 'highlight',
14
+ 'subscript',
15
+ 'superscript',
16
+ 'paragraph',
17
+ 'heading1',
18
+ 'heading2',
19
+ 'heading3',
20
+ 'heading4',
21
+ 'heading5',
22
+ 'heading6',
23
+ 'alignLeft',
24
+ 'alignCenter',
25
+ 'alignRight',
26
+ 'link',
27
+ ]
28
+
29
+ /** Internal document transport. Engine handles never cross the WebKit boundary. */
30
+ export function installEditorDocument(render: (props: any) => void) {
31
+ let handle: any = null
32
+ const pending: { method: string; args: any[] }[] = []
33
+ let selection = { start: 0, end: 0, active: [] as string[] }
34
+ const post = (event: string, extra: object = {}) => {
35
+ const packet = {
36
+ event,
37
+ html: handle?.getHTML() ?? '',
38
+ json: handle?.getJSON?.() ?? null,
39
+ focused: handle?.isFocused() ?? false,
40
+ selection,
41
+ ...extra,
42
+ }
43
+
44
+ ;(window as any).webkit.messageHandlers.editor.postMessage(JSON.stringify(packet))
45
+ }
46
+
47
+ const snapshot = (event: string) => {
48
+ setTimeout(() => {
49
+ if (!handle) {
50
+ return
51
+ }
52
+
53
+ selection = { ...selection, active: formats.filter((f) => handle.isActive(f)) }
54
+ post(event)
55
+ }, 0)
56
+ }
57
+
58
+ const command = (packet: { method: string; args: any[] }) => {
59
+ if (packet.method === 'props') {
60
+ render({ ...packet.args[0], ...callbacks })
61
+ if (handle) {
62
+ snapshot('snapshot')
63
+ }
64
+ } else if (!handle) {
65
+ pending.push(packet)
66
+ } else if (
67
+ [
68
+ 'setHTML',
69
+ 'setJSON',
70
+ 'apply',
71
+ 'linkTo',
72
+ 'removeLink',
73
+ 'undo',
74
+ 'redo',
75
+ 'focus',
76
+ 'blur',
77
+ ].includes(packet.method)
78
+ ) {
79
+ handle[packet.method]?.(...packet.args)
80
+ snapshot('snapshot')
81
+ }
82
+ }
83
+
84
+ const callbacks = {
85
+ ref(next: any) {
86
+ if (!next || next === handle) {
87
+ return
88
+ }
89
+
90
+ handle = next
91
+ snapshot('ready')
92
+ for (const packet of pending.splice(0)) {
93
+ command(packet)
94
+ }
95
+ },
96
+ onChange: () => snapshot('change'),
97
+ onSelectionChange(next: typeof selection) {
98
+ selection = next
99
+ snapshot('selection')
100
+ },
101
+ onFocus: () => snapshot('focus'),
102
+ onBlur: () => snapshot('blur'),
103
+ }
104
+
105
+ ;(window as any).__xplatEditorReceive = (serialized: string) => {
106
+ try {
107
+ command(JSON.parse(serialized))
108
+ } catch (error) {
109
+ post('error', { message: String(error) })
110
+ }
111
+ }
112
+
113
+ window.addEventListener('error', (event) => post('error', { message: event.message }))
114
+ window.addEventListener('unhandledrejection', (event) =>
115
+ post('error', { message: String(event.reason) }),
116
+ )
117
+
118
+ post('boot')
119
+ }
@@ -0,0 +1,133 @@
1
+ import type { RichTextEditorHandle, RichTextEditorProps, RichTextFormat } from './types'
2
+
3
+ export interface EditorPacket {
4
+ event: string
5
+ html?: string
6
+ json?: any
7
+ selection?: { start: number; end: number; active: RichTextFormat[] }
8
+ focused?: boolean
9
+ message?: string
10
+ }
11
+
12
+ /** Internal transport adapter: synchronous getters read the last WebKit snapshot. */
13
+ export function createEditorSession(
14
+ send: (packet: string) => void,
15
+ native: any,
16
+ readProps: () => RichTextEditorProps & {
17
+ onJSONChange?: (doc: any) => void
18
+ onJSONReady?: (ok: boolean) => void
19
+ },
20
+ ) {
21
+ let disposed = false
22
+ let ready = false
23
+ let html = ''
24
+ let json: any = null
25
+ let active: RichTextFormat[] = []
26
+ let focused = false
27
+ let pending: string[] = []
28
+ const command = (method: string, ...args: any[]) => {
29
+ if (disposed) {
30
+ return
31
+ }
32
+
33
+ const packet = JSON.stringify({ method, args })
34
+ if (ready) {
35
+ send(packet)
36
+ } else {
37
+ pending.push(packet)
38
+ }
39
+ }
40
+
41
+ const handle: RichTextEditorHandle & { getJSON(): any; setJSON(doc: any): void } = {
42
+ getHTML: () => html,
43
+ setHTML: (value) => command('setHTML', value),
44
+ getJSON: () => json,
45
+ setJSON: (doc) => command('setJSON', doc),
46
+ apply: (format) => command('apply', format),
47
+ linkTo: (url, anchor) => command('linkTo', url, anchor),
48
+ removeLink: () => command('removeLink'),
49
+ isActive: (format) => active.includes(format),
50
+ undo: () => command('undo'),
51
+ redo: () => command('redo'),
52
+ focus: () => command('focus'),
53
+ blur: () => command('blur'),
54
+ isFocused: () => focused,
55
+ native,
56
+ }
57
+
58
+ return {
59
+ handle,
60
+ update(props: object) {
61
+ command('props', props)
62
+ },
63
+ receive(serialized: string) {
64
+ if (disposed) {
65
+ return
66
+ }
67
+
68
+ const packet: EditorPacket = JSON.parse(serialized)
69
+ const props = readProps()
70
+ if (packet.html !== undefined) {
71
+ html = packet.html
72
+ }
73
+
74
+ if ('json' in packet) {
75
+ json = packet.json
76
+ }
77
+
78
+ if (packet.selection) {
79
+ active = packet.selection.active
80
+ }
81
+
82
+ if (packet.focused !== undefined) {
83
+ focused = packet.focused
84
+ }
85
+
86
+ switch (packet.event) {
87
+ case 'boot':
88
+ ready = true
89
+ for (const packet of pending) {
90
+ send(packet)
91
+ }
92
+
93
+ pending = []
94
+ break
95
+ case 'ready':
96
+ props.onJSONReady?.(true)
97
+ props.onReady?.()
98
+ break
99
+ case 'change':
100
+ props.onChange?.(html)
101
+ if (json) {
102
+ props.onJSONChange?.(json)
103
+ }
104
+
105
+ break
106
+ case 'selection':
107
+ if (packet.selection) {
108
+ props.onSelectionChange?.(packet.selection)
109
+ }
110
+
111
+ break
112
+ case 'focus':
113
+ props.onFocus?.()
114
+ break
115
+ case 'blur':
116
+ props.onBlur?.()
117
+ break
118
+ case 'error':
119
+ console.error('[macos-editor] ' + packet.message)
120
+ break
121
+ }
122
+ },
123
+ dispose() {
124
+ if (disposed) {
125
+ return
126
+ }
127
+
128
+ disposed = true
129
+ pending = []
130
+ native.dispose()
131
+ },
132
+ }
133
+ }