@octane-xplat/richtext 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -6
- package/dist/macos/browser.d.ts +1 -0
- package/dist/macos/browser.js +2 -0
- package/dist/native/RichTextEditor.android.js +48 -8
- package/dist/native/RichTextEditor.ios.js +194 -8
- package/dist/native/octane-client-build.json +1 -1
- package/dist/web/octane-client-build.json +1 -1
- package/nativescript.config.ts +22 -0
- package/package.json +27 -5
- package/platforms/ios/LICENSE-Aztec-MPL-2.0.txt +355 -0
- package/platforms/ios/UPSTREAM.md +14 -0
- package/platforms/ios/src/XplatAztecEditor.swift +357 -0
- 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 +42 -2
- package/src/RichTextEditor.ios.tsrx +221 -6
- package/src/RichTextEditor.windows.tsrx +15 -0
- package/src/browser-transport.web.ts +119 -0
- package/src/host-session.macos.ts +142 -0
- package/src/index.macos.ts +3 -9
- package/src/index.ts +2 -2
- package/src/types.ts +54 -9
- 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 +53 -9
|
@@ -1,15 +1,230 @@
|
|
|
1
1
|
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
-
import
|
|
2
|
+
import { useImperativeHandle, useCallback, useEffect, useRef } from 'octane'
|
|
3
|
+
import { View } from '@nativescript/core'
|
|
4
|
+
import { registerElement } from '@nativescript-community/octane'
|
|
5
|
+
import type { Attributes } from '@nativescript-community/octane/intrinsics'
|
|
6
|
+
import type {
|
|
7
|
+
RichTextEditorHandle,
|
|
8
|
+
RichTextEditorProps,
|
|
9
|
+
RichTextFormat,
|
|
10
|
+
} from './types'
|
|
3
11
|
|
|
4
|
-
|
|
12
|
+
/** `XplatAztecEditorView` arrives through the leaf's
|
|
13
|
+
* `platforms/ios/src` Swift facade over `Aztec.TextView` — there are no
|
|
14
|
+
* generated typings for it, so every touch goes through `as any` at one
|
|
15
|
+
* boundary instead of `declare const` per call site. */
|
|
16
|
+
declare const XplatAztecEditorView: any
|
|
5
17
|
|
|
6
|
-
/** iOS
|
|
7
|
-
*
|
|
18
|
+
/** AztecEditor-iOS has no task-list or text-alignment API — the facade
|
|
19
|
+
* reports these as unsupported rather than faking a result. `link` is
|
|
20
|
+
* valid but goes through `linkTo`, matching the Android leaf. */
|
|
21
|
+
const UNSUPPORTED: ReadonlySet<RichTextFormat> = new Set([
|
|
22
|
+
'taskList',
|
|
23
|
+
'alignLeft',
|
|
24
|
+
'alignCenter',
|
|
25
|
+
'alignRight',
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
class AztecEditor extends View {
|
|
29
|
+
createNativeView() {
|
|
30
|
+
return new XplatAztecEditorView()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare module '@nativescript-community/octane/intrinsics' {
|
|
35
|
+
interface NativeScriptElements {
|
|
36
|
+
xplatrichtext: Attributes<typeof AztecEditor>
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
registerElement('xplatrichtext', AztecEditor)
|
|
41
|
+
|
|
42
|
+
export const supported = true
|
|
43
|
+
|
|
44
|
+
/** WordPress AztecEditor-iOS surface — a UITextView-backed native editor,
|
|
45
|
+
* not a WebView. Content in/out is HTML (`setHTML`/`getHTML`); JSON
|
|
46
|
+
* interchange lives one layer up in @octane-xplat/tiptap. */
|
|
8
47
|
export function RichTextEditor(props: RichTextEditorProps) @{
|
|
9
|
-
<
|
|
48
|
+
const host = useRef<any>(null)
|
|
49
|
+
const lastEmitted = useRef<string | undefined>(undefined)
|
|
50
|
+
const applyingValue = useRef(false)
|
|
51
|
+
const emitTimer = useRef<any>(null)
|
|
52
|
+
const warned = useRef<Set<string>>(new Set())
|
|
53
|
+
|
|
54
|
+
// Stable identity — an inline closure re-fires ref(null)/ref(v) per
|
|
55
|
+
// render, which would bounce host.current around the effects below.
|
|
56
|
+
useImperativeHandle(props.ref, () => handle(editor()), []);
|
|
57
|
+
|
|
58
|
+
const bindHost = useCallback((v: any) => {
|
|
59
|
+
host.current = v
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
const editor = () => host.current?.ios
|
|
63
|
+
|
|
64
|
+
const emitChange = () => {
|
|
65
|
+
const e = editor()
|
|
66
|
+
if (!e || applyingValue.current) return
|
|
67
|
+
clearTimeout(emitTimer.current)
|
|
68
|
+
emitTimer.current = setTimeout(() => {
|
|
69
|
+
const current = editor()
|
|
70
|
+
if (!current || applyingValue.current) return
|
|
71
|
+
const html = current.getHTML()
|
|
72
|
+
lastEmitted.current = html
|
|
73
|
+
props.onChange?.(html)
|
|
74
|
+
}, 150)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const reportUnsupported = (format: RichTextFormat) => {
|
|
78
|
+
if (warned.current.has(format)) return
|
|
79
|
+
warned.current.add(format)
|
|
80
|
+
console.warn(
|
|
81
|
+
`[richtext] apply('${format}') is not supported by the iOS Aztec surface`,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const handle = (e: any): RichTextEditorHandle => ({
|
|
86
|
+
getHTML: () => e.getHTML(),
|
|
87
|
+
setHTML: (html: string) => {
|
|
88
|
+
applyingValue.current = true
|
|
89
|
+
try {
|
|
90
|
+
e.setHTML(html)
|
|
91
|
+
lastEmitted.current = html
|
|
92
|
+
} finally {
|
|
93
|
+
applyingValue.current = false
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
apply: (format: RichTextFormat) => {
|
|
97
|
+
if (format === 'link') return
|
|
98
|
+
if (UNSUPPORTED.has(format) || !e.applyFormat(format)) {
|
|
99
|
+
reportUnsupported(format)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
emitChange()
|
|
104
|
+
},
|
|
105
|
+
linkTo: (url: string, anchor?: string) => {
|
|
106
|
+
e.linkTo(url, anchor ?? null)
|
|
107
|
+
emitChange()
|
|
108
|
+
},
|
|
109
|
+
removeLink: () => {
|
|
110
|
+
e.removeLink()
|
|
111
|
+
emitChange()
|
|
112
|
+
},
|
|
113
|
+
isActive: (format: RichTextFormat) =>
|
|
114
|
+
!UNSUPPORTED.has(format) && Boolean(e.isFormatActive(format)),
|
|
115
|
+
undo: () => e.undo(),
|
|
116
|
+
redo: () => e.redo(),
|
|
117
|
+
split: () => {
|
|
118
|
+
const ok = Boolean(e.split())
|
|
119
|
+
if (ok) emitChange()
|
|
120
|
+
return ok
|
|
121
|
+
},
|
|
122
|
+
join: () => {
|
|
123
|
+
const ok = Boolean(e.join())
|
|
124
|
+
if (ok) emitChange()
|
|
125
|
+
return ok
|
|
126
|
+
},
|
|
127
|
+
indent: () => {
|
|
128
|
+
const ok = Boolean(e.indent())
|
|
129
|
+
if (ok) emitChange()
|
|
130
|
+
return ok
|
|
131
|
+
},
|
|
132
|
+
outdent: () => {
|
|
133
|
+
const ok = Boolean(e.outdent())
|
|
134
|
+
if (ok) emitChange()
|
|
135
|
+
return ok
|
|
136
|
+
},
|
|
137
|
+
canIndent: () => Boolean(e.canIndent()),
|
|
138
|
+
canOutdent: () => Boolean(e.canOutdent()),
|
|
139
|
+
focus: () => e.focus(),
|
|
140
|
+
blur: () => e.blur(),
|
|
141
|
+
isFocused: () => e.focused(),
|
|
142
|
+
native: e,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const e = editor()
|
|
147
|
+
if (!e) return
|
|
148
|
+
|
|
149
|
+
e.onTextChange = () => emitChange()
|
|
150
|
+
e.onSelectionChange = (start: number, end: number, active: any) => {
|
|
151
|
+
const formats: RichTextFormat[] = []
|
|
152
|
+
const count = active?.count ?? 0
|
|
153
|
+
for (let i = 0; i < count; i++) {
|
|
154
|
+
formats.push(active.objectAtIndex(i))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
props.onSelectionChange?.({ start, end, active: formats })
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
e.onFocus = () => props.onFocus?.()
|
|
161
|
+
e.onBlur = () => props.onBlur?.()
|
|
162
|
+
|
|
163
|
+
if (props.placeholder !== undefined) e.placeholder = props.placeholder
|
|
164
|
+
e.editable = props.editable !== false
|
|
165
|
+
|
|
166
|
+
if (props.value !== undefined) {
|
|
167
|
+
applyingValue.current = true
|
|
168
|
+
try {
|
|
169
|
+
e.setHTML(props.value)
|
|
170
|
+
} finally {
|
|
171
|
+
applyingValue.current = false
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (props.autofocus) e.focus()
|
|
176
|
+
|
|
177
|
+
if (props.ios) Object.assign(e, props.ios)
|
|
178
|
+
props.onReady?.()
|
|
179
|
+
|
|
180
|
+
return () => {
|
|
181
|
+
e.onTextChange = null
|
|
182
|
+
e.onSelectionChange = null
|
|
183
|
+
e.onFocus = null
|
|
184
|
+
e.onBlur = null
|
|
185
|
+
}
|
|
186
|
+
}, [])
|
|
187
|
+
|
|
188
|
+
// `value` lands like a controlled prop, minus the loop: text the editor
|
|
189
|
+
// emitted through onChange is never pushed back; any other change is an
|
|
190
|
+
// external reset (Aztec-iOS setHTML is non-undoable, matching Android).
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
const e = editor()
|
|
193
|
+
if (!e || props.value === undefined) return
|
|
194
|
+
if (props.value === lastEmitted.current) return
|
|
195
|
+
applyingValue.current = true
|
|
196
|
+
try {
|
|
197
|
+
e.setHTML(props.value)
|
|
198
|
+
} finally {
|
|
199
|
+
applyingValue.current = false
|
|
200
|
+
}
|
|
201
|
+
}, [props.value])
|
|
202
|
+
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
const e = editor()
|
|
205
|
+
if (e) e.editable = props.editable !== false
|
|
206
|
+
}, [props.editable])
|
|
207
|
+
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
const e = editor()
|
|
210
|
+
if (e && props.placeholder !== undefined) e.placeholder = props.placeholder
|
|
211
|
+
}, [props.placeholder])
|
|
212
|
+
|
|
213
|
+
<xplatrichtext
|
|
214
|
+
ref={bindHost}
|
|
10
215
|
id={props.id}
|
|
11
216
|
className={props.className}
|
|
12
217
|
style={props.style}
|
|
13
|
-
|
|
218
|
+
row={props.row}
|
|
219
|
+
col={props.col}
|
|
220
|
+
rowSpan={props.rowSpan}
|
|
221
|
+
colSpan={props.colSpan}
|
|
222
|
+
dock={props.dock}
|
|
223
|
+
left={props.left}
|
|
224
|
+
top={props.top}
|
|
225
|
+
flexGrow={props.flexGrow}
|
|
226
|
+
flexShrink={props.flexShrink}
|
|
227
|
+
alignSelf={props.alignSelf}
|
|
228
|
+
order={props.order}
|
|
14
229
|
/>;
|
|
15
230
|
}
|
|
@@ -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,142 @@
|
|
|
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
|
+
// Structural commands are Aztec-only for now: the AppKit transport
|
|
53
|
+
// is one-way async dispatch, so a synchronous "applied" result cannot
|
|
54
|
+
// be reported and a fire-and-forget forward would lie to the caller.
|
|
55
|
+
split: () => false,
|
|
56
|
+
join: () => false,
|
|
57
|
+
indent: () => false,
|
|
58
|
+
outdent: () => false,
|
|
59
|
+
canIndent: () => false,
|
|
60
|
+
canOutdent: () => false,
|
|
61
|
+
focus: () => command('focus'),
|
|
62
|
+
blur: () => command('blur'),
|
|
63
|
+
isFocused: () => focused,
|
|
64
|
+
native,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
handle,
|
|
69
|
+
update(props: object) {
|
|
70
|
+
command('props', props)
|
|
71
|
+
},
|
|
72
|
+
receive(serialized: string) {
|
|
73
|
+
if (disposed) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const packet: EditorPacket = JSON.parse(serialized)
|
|
78
|
+
const props = readProps()
|
|
79
|
+
if (packet.html !== undefined) {
|
|
80
|
+
html = packet.html
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if ('json' in packet) {
|
|
84
|
+
json = packet.json
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (packet.selection) {
|
|
88
|
+
active = packet.selection.active
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (packet.focused !== undefined) {
|
|
92
|
+
focused = packet.focused
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
switch (packet.event) {
|
|
96
|
+
case 'boot':
|
|
97
|
+
ready = true
|
|
98
|
+
for (const packet of pending) {
|
|
99
|
+
send(packet)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
pending = []
|
|
103
|
+
break
|
|
104
|
+
case 'ready':
|
|
105
|
+
props.onJSONReady?.(true)
|
|
106
|
+
props.onReady?.()
|
|
107
|
+
break
|
|
108
|
+
case 'change':
|
|
109
|
+
props.onChange?.(html)
|
|
110
|
+
if (json) {
|
|
111
|
+
props.onJSONChange?.(json)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
break
|
|
115
|
+
case 'selection':
|
|
116
|
+
if (packet.selection) {
|
|
117
|
+
props.onSelectionChange?.(packet.selection)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
break
|
|
121
|
+
case 'focus':
|
|
122
|
+
props.onFocus?.()
|
|
123
|
+
break
|
|
124
|
+
case 'blur':
|
|
125
|
+
props.onBlur?.()
|
|
126
|
+
break
|
|
127
|
+
case 'error':
|
|
128
|
+
console.error('[macos-editor] ' + packet.message)
|
|
129
|
+
break
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
dispose() {
|
|
133
|
+
if (disposed) {
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
disposed = true
|
|
138
|
+
pending = []
|
|
139
|
+
native.dispose()
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
}
|
package/src/index.macos.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export const supported =
|
|
4
|
-
|
|
5
|
-
/** The macOS editor is deferred; the tiptap facade covers desktop needs. */
|
|
6
|
-
export function RichTextEditor(_props: RichTextEditorProps): null {
|
|
7
|
-
return null
|
|
8
|
-
}
|
|
9
|
-
|
|
1
|
+
export { RichTextEditor } from './Editor.macos'
|
|
2
|
+
/** The AppKit/WKWebView backend is implemented. */
|
|
3
|
+
export const supported = true
|
|
10
4
|
export type * from './types'
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Keep this relative import extensionless so the NativeScript resolver picks
|
|
2
|
-
// the Android (Aztec) or iOS (
|
|
3
|
-
//
|
|
2
|
+
// the Android (Aztec) or iOS (AztecEditor-iOS facade) component for the
|
|
3
|
+
// current build target.
|
|
4
4
|
export { RichTextEditor, supported } from './RichTextEditor'
|
|
5
5
|
export type * from './types'
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type { Octane } from 'octane/jsx-runtime'
|
|
2
|
+
|
|
3
|
+
/** Android uses WordPress Aztec; iOS uses AztecEditor-iOS; AppKit uses
|
|
4
|
+
* StarterKit in WKWebView. Web/Windows use the tiptap facade instead of
|
|
5
|
+
* this leaf. */
|
|
4
6
|
export type RichTextFormat =
|
|
5
7
|
| 'bold'
|
|
6
8
|
| 'italic'
|
|
@@ -31,25 +33,68 @@ export type RichTextFormat =
|
|
|
31
33
|
export interface RichTextEditorHandle {
|
|
32
34
|
/** Serialized document HTML (Aztec's "plain" HTML — no contenteditable
|
|
33
35
|
* markup). */
|
|
36
|
+
/** AppKit: returns the latest WebKit snapshot; commands are asynchronous. */
|
|
34
37
|
getHTML(): string
|
|
35
|
-
/** Replace the document
|
|
38
|
+
/** Replace the document. Android resets Aztec history; the iOS
|
|
39
|
+
* `setHTML` variant is likewise non-undoable; AppKit uses engine
|
|
40
|
+
* history. */
|
|
36
41
|
setHTML(html: string): void
|
|
37
42
|
/** Toggle a format at the current selection. `link` needs
|
|
38
|
-
* `linkTo(url, anchor)` instead.
|
|
43
|
+
* `linkTo(url, anchor)` instead. iOS has no taskList or text-alignment
|
|
44
|
+
* engine support — those calls report a console warning and do not
|
|
45
|
+
* change the document. */
|
|
39
46
|
apply(format: RichTextFormat): void
|
|
40
47
|
/** Insert or re-target a link over the current selection. */
|
|
41
48
|
linkTo(url: string, anchor?: string): void
|
|
42
49
|
removeLink(): void
|
|
43
|
-
/** Formats active at the selection — powers toolbar highlight state.
|
|
50
|
+
/** Formats active at the selection — powers toolbar highlight state.
|
|
51
|
+
* iOS always reports false for taskList and the align* formats (no
|
|
52
|
+
* engine support). */
|
|
44
53
|
isActive(format: RichTextFormat): boolean
|
|
45
54
|
/** Android: Aztec's history batches keyboard input only — format
|
|
46
|
-
* toggles and programmatic edits do not register.
|
|
55
|
+
* toggles and programmatic edits do not register. iOS: the engine
|
|
56
|
+
* undoManager covers format toggles and keyboard input; `setHTML` and
|
|
57
|
+
* split/join edits sit outside it. */
|
|
47
58
|
undo(): void
|
|
48
59
|
redo(): void
|
|
60
|
+
/** Split the block at the caret into two siblings — the Enter-key
|
|
61
|
+
* semantic (a non-collapsed selection is deleted first). Android: a
|
|
62
|
+
* real block break through Aztec's watcher pipeline — a caret inside
|
|
63
|
+
* a list item produces a sibling `<li>`. iOS routes through Aztec's
|
|
64
|
+
* `insertText` newline handling for the same semantics. Inside hidden
|
|
65
|
+
* block markup Aztec does not understand (`<div data-*>` wrappers),
|
|
66
|
+
* this is a line break, not a new node — it serializes as `<br>`.
|
|
67
|
+
* Returns false when
|
|
68
|
+
* there is no valid selection. AppKit: returns false — the engine
|
|
69
|
+
* transport is asynchronous and cannot report application. */
|
|
70
|
+
split(): boolean
|
|
71
|
+
/** Merge the block at the caret into the previous block — the
|
|
72
|
+
* Backspace-at-block-start semantic. Returns false unless the
|
|
73
|
+
* selection is a collapsed caret directly after a block boundary. */
|
|
74
|
+
join(): boolean
|
|
75
|
+
/** Demote the block(s) in the selection. Aztec semantics: a list item
|
|
76
|
+
* nests under its previous sibling; a plain paragraph, heading,
|
|
77
|
+
* quote, or preformat line gains a literal `\t` indent. Hidden
|
|
78
|
+
* `<div data-*>` nodes are opaque to Aztec and cannot be demoted.
|
|
79
|
+
* iOS: `increaseIndent` acts on list/quote depth and falls back to a
|
|
80
|
+
* `\t` insert on plain blocks. Returns false when Aztec reports no
|
|
81
|
+
* indentable selection (iOS: when the view is not editable). */
|
|
82
|
+
indent(): boolean
|
|
83
|
+
/** Promote the block(s) in the selection one level — the inverse of
|
|
84
|
+
* `indent()`. iOS has no plain-block outdent, so this applies only to
|
|
85
|
+
* list items and blockquotes. Returns false when Aztec reports no
|
|
86
|
+
* outdentable selection. */
|
|
87
|
+
outdent(): boolean
|
|
88
|
+
/** Whether `indent()` can apply to the current selection. */
|
|
89
|
+
canIndent(): boolean
|
|
90
|
+
/** Whether `outdent()` can apply to the current selection. */
|
|
91
|
+
canOutdent(): boolean
|
|
49
92
|
focus(): void
|
|
50
93
|
blur(): void
|
|
51
94
|
isFocused(): boolean
|
|
52
|
-
/** The platform surface — `AztecText` on Android
|
|
95
|
+
/** The platform surface — `AztecText` on Android, the
|
|
96
|
+
* `XplatAztecEditorView` facade on iOS. */
|
|
97
|
+
/** AppKit: the XplatEditorHost transport, not the browser engine instance. */
|
|
53
98
|
native: any
|
|
54
99
|
}
|
|
55
100
|
|
|
@@ -77,7 +122,7 @@ export interface RichTextEditorProps {
|
|
|
77
122
|
onFocus?: () => void
|
|
78
123
|
onBlur?: () => void
|
|
79
124
|
/** Imperative handle — fires once when the native editor exists. */
|
|
80
|
-
|
|
125
|
+
ref?: Octane.Ref<RichTextEditorHandle>
|
|
81
126
|
/** Per-platform escape bag — properties are assigned onto the native
|
|
82
127
|
* `AztecText` after the leaf's own props. */
|
|
83
128
|
android?: Record<string, any>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { UniversalComponent } from 'octane/universal'
|
|
2
|
+
import type { RichTextEditorProps } from '../src/types'
|
|
3
|
+
export declare const RichTextEditor: UniversalComponent<RichTextEditorProps>
|
|
4
|
+
export declare const supported: boolean
|
|
5
|
+
export type * from '../src/types'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { UniversalComponent } from 'octane/universal'
|
|
2
|
+
import type { RichTextEditorProps } from '../src/types'
|
|
3
|
+
/** Internal shared editor host. */
|
|
4
|
+
export declare const NativeEditor: UniversalComponent<
|
|
5
|
+
RichTextEditorProps & {
|
|
6
|
+
browser: string
|
|
7
|
+
json?: any
|
|
8
|
+
onJSONReady?: (ok: boolean) => void
|
|
9
|
+
onJSONChange?: (doc: any) => void
|
|
10
|
+
}
|
|
11
|
+
>
|