@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.
- package/README.md +97 -4
- package/dist/macos/browser.d.ts +1 -0
- package/dist/macos/browser.js +2 -0
- package/dist/native/RichTextEditor.android.js +9 -8
- package/dist/native/octane-client-build.json +1 -1
- package/dist/web/octane-client-build.json +1 -1
- package/package.json +25 -4
- package/platforms/macos/include/XplatEditorHost.h +1 -0
- package/platforms/macos/src/XplatEditorHost.swift +63 -0
- package/src/BrowserEditor.web.ts +107 -0
- package/src/Editor.macos.tsx +9 -0
- package/src/NativeEditor.macos.tsx +81 -0
- package/src/RichTextEditor.android.tsrx +3 -2
- package/src/RichTextEditor.windows.tsrx +15 -0
- package/src/browser-transport.web.ts +119 -0
- package/src/host-session.macos.ts +133 -0
- package/src/index.macos.ts +3 -9
- package/src/types.ts +8 -5
- package/types/browser-transport.d.ts +2 -0
- package/types/index.macos.d.ts +5 -0
- package/types/macos-host.d.ts +11 -0
- package/types/types.d.ts +7 -5
|
@@ -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(
|
|
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$
|
|
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$
|
|
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$
|
|
208
|
+
}, [props.value], _h$8);
|
|
208
209
|
useEffect(() => {
|
|
209
210
|
setEditable(props.editable !== false);
|
|
210
|
-
}, [props.editable], _h$
|
|
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$
|
|
215
|
+
}, [props.placeholder], _h$10);
|
|
215
216
|
return universalValue(__octaneUniversalPlan0, [
|
|
216
217
|
bindHost,
|
|
217
218
|
props.id,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octane-xplat/richtext",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Native rich-text editing leaf for Octane xplat —
|
|
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
|
+
}
|