@octane-xplat/tiptap 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 +112 -3
- package/dist/macos/browser.d.ts +1 -0
- package/dist/macos/browser.js +2 -0
- package/dist/native/index3.js +46 -25
- package/dist/native/octane-client-build.json +1 -1
- package/dist/web/index2.js +43 -32
- package/dist/web/octane-client-build.json +1 -1
- package/package.json +9 -6
- package/src/Editor.macos.tsx +9 -0
- package/src/TiptapEditor.mobile.test.ts +229 -0
- package/src/TiptapEditor.tsrx +59 -25
- package/src/TiptapEditor.web.test.tsrx +129 -0
- package/src/TiptapEditor.web.tsrx +43 -25
- package/src/browser-entry.web.tsx +5 -0
- package/src/index.macos.ts +7 -13
- package/src/types.ts +34 -2
- package/types/index.macos.d.ts +7 -0
- package/types/types.d.ts +32 -2
package/src/TiptapEditor.tsrx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
-
import { useCallback, useEffect, useRef } from 'octane'
|
|
2
|
+
import { useState, useImperativeHandle, useCallback, useEffect, useRef } from 'octane'
|
|
3
3
|
import { RichTextEditor, supported as leafSupported } from '@octane-xplat/richtext'
|
|
4
4
|
import type { RichTextEditorHandle } from '@octane-xplat/richtext'
|
|
5
|
-
import type { TiptapEditorHandle, TiptapEditorProps } from './types'
|
|
5
|
+
import type { TiptapEditorHandle, TiptapEditorProps, TiptapJSON } from './types'
|
|
6
6
|
import { ensureJSONBridge, htmlToJSON, jsonBridgeReady, jsonToHTML } from './json-bridge'
|
|
7
7
|
|
|
8
8
|
export const supported = leafSupported
|
|
@@ -12,22 +12,47 @@ export const supported = leafSupported
|
|
|
12
12
|
* renderToHTMLString) handles JSON interchange on top of it. */
|
|
13
13
|
export function TiptapEditor(props: TiptapEditorProps) @{
|
|
14
14
|
const inner = useRef<RichTextEditorHandle | null>(null)
|
|
15
|
-
const
|
|
15
|
+
const [exposedHandle, setHandle] = useState<TiptapEditorHandle | null>(null)
|
|
16
|
+
useImperativeHandle<TiptapEditorHandle | null>(props.ref, () => exposedHandle, [exposedHandle])
|
|
16
17
|
const lastJSON = useRef<string | undefined>(undefined)
|
|
18
|
+
const appliedHost = useRef<RichTextEditorHandle | null>(null)
|
|
19
|
+
// Latest doc requested for the host — by `json` prop or `setJSON`. Kept
|
|
20
|
+
// after a successful push so a rebound (recreated) host still gets it.
|
|
21
|
+
const desiredDoc = useRef<TiptapJSON | undefined>(undefined)
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
// Push the requested doc once both halves are up — the JSON bridge
|
|
24
|
+
// (async module load) and the bound host (ref/onReady timing). Skips the
|
|
25
|
+
// stamp the host already shows, so editor-emitted echoes and repeat props
|
|
26
|
+
// never reach setHTML; a different host always gets its own copy.
|
|
27
|
+
const applyDoc = () => {
|
|
28
|
+
const host = inner.current
|
|
29
|
+
const doc = desiredDoc.current
|
|
30
|
+
if (!host || doc === undefined || !jsonBridgeReady()) return
|
|
31
|
+
const stamp = JSON.stringify(doc)
|
|
32
|
+
if (stamp === lastJSON.current && appliedHost.current === host) return
|
|
33
|
+
const html = jsonToHTML(doc)
|
|
34
|
+
if (html != null) {
|
|
35
|
+
lastJSON.current = stamp
|
|
36
|
+
appliedHost.current = host
|
|
37
|
+
host.setHTML(html)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const bindHost = useCallback((h: RichTextEditorHandle | null) => {
|
|
19
42
|
inner.current = h
|
|
43
|
+
if (h) {
|
|
44
|
+
// The ref binds inside the leaf's commit, before its own `value`
|
|
45
|
+
// seed — defer so `json` keeps precedence over `value`.
|
|
46
|
+
queueMicrotask(applyDoc)
|
|
47
|
+
}
|
|
20
48
|
|
|
21
|
-
|
|
49
|
+
setHandle((previous) => previous?.native === h?.native ? previous : h ? ({
|
|
22
50
|
getHTML: () => h.getHTML(),
|
|
23
51
|
setHTML: (html: string) => h.setHTML(html),
|
|
24
52
|
getJSON: () => (jsonBridgeReady() ? htmlToJSON(h.getHTML()) : null),
|
|
25
53
|
setJSON: (doc) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
lastJSON.current = JSON.stringify(doc)
|
|
29
|
-
h.setHTML(html)
|
|
30
|
-
}
|
|
54
|
+
desiredDoc.current = doc
|
|
55
|
+
applyDoc()
|
|
31
56
|
},
|
|
32
57
|
apply: (format) => h.apply(format),
|
|
33
58
|
linkTo: (url, anchor) => h.linkTo(url, anchor),
|
|
@@ -39,29 +64,32 @@ export function TiptapEditor(props: TiptapEditorProps) @{
|
|
|
39
64
|
blur: () => h.blur(),
|
|
40
65
|
isFocused: () => h.isFocused(),
|
|
41
66
|
native: h.native,
|
|
42
|
-
} satisfies TiptapEditorHandle)
|
|
67
|
+
} satisfies TiptapEditorHandle) : null)
|
|
43
68
|
}, [])
|
|
44
69
|
|
|
45
70
|
// The bridge loads once per process — dynamic imports of the DOM-free
|
|
46
|
-
// tiptap slices, plus the zeed-dom parse shim.
|
|
71
|
+
// tiptap slices, plus the zeed-dom parse shim. A doc that arrived before
|
|
72
|
+
// it settles is applied here, not dropped.
|
|
47
73
|
useEffect(() => {
|
|
74
|
+
let alive = true
|
|
48
75
|
ensureJSONBridge().then((ok) => {
|
|
49
|
-
|
|
76
|
+
if (!alive) return
|
|
50
77
|
props.onJSONReady?.(ok)
|
|
78
|
+
applyDoc()
|
|
51
79
|
})
|
|
80
|
+
|
|
81
|
+
return () => {
|
|
82
|
+
alive = false
|
|
83
|
+
}
|
|
52
84
|
}, [])
|
|
53
85
|
|
|
54
|
-
// `json` prop → editor content
|
|
55
|
-
//
|
|
86
|
+
// `json` prop → editor content. It may land before the bridge settles or
|
|
87
|
+
// before the host binds — park it in desiredDoc and let the readiness
|
|
88
|
+
// flushes apply it.
|
|
56
89
|
useEffect(() => {
|
|
57
|
-
if (!props.json
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const html = jsonToHTML(props.json)
|
|
61
|
-
if (html != null) {
|
|
62
|
-
lastJSON.current = stamp
|
|
63
|
-
inner.current.setHTML(html)
|
|
64
|
-
}
|
|
90
|
+
if (!props.json) return
|
|
91
|
+
desiredDoc.current = props.json
|
|
92
|
+
applyDoc()
|
|
65
93
|
}, [props.json])
|
|
66
94
|
|
|
67
95
|
const emitJSON = () => {
|
|
@@ -69,6 +97,7 @@ export function TiptapEditor(props: TiptapEditorProps) @{
|
|
|
69
97
|
const doc = htmlToJSON(inner.current.getHTML())
|
|
70
98
|
if (doc) {
|
|
71
99
|
lastJSON.current = JSON.stringify(doc)
|
|
100
|
+
appliedHost.current = inner.current
|
|
72
101
|
props.onJSONChange?.(doc)
|
|
73
102
|
}
|
|
74
103
|
}
|
|
@@ -81,7 +110,12 @@ export function TiptapEditor(props: TiptapEditorProps) @{
|
|
|
81
110
|
placeholder={props.placeholder}
|
|
82
111
|
editable={props.editable}
|
|
83
112
|
autofocus={props.autofocus}
|
|
84
|
-
onReady={
|
|
113
|
+
onReady={() => {
|
|
114
|
+
// Host signalled ready after the leaf's own initial content seed —
|
|
115
|
+
// the last apply point that still outranks a `value` prop.
|
|
116
|
+
applyDoc()
|
|
117
|
+
props.onReady?.()
|
|
118
|
+
}}
|
|
85
119
|
onChange={(html) => {
|
|
86
120
|
props.onChange?.(html)
|
|
87
121
|
emitJSON()
|
|
@@ -89,7 +123,7 @@ export function TiptapEditor(props: TiptapEditorProps) @{
|
|
|
89
123
|
onSelectionChange={props.onSelectionChange}
|
|
90
124
|
onFocus={props.onFocus}
|
|
91
125
|
onBlur={props.onBlur}
|
|
92
|
-
|
|
126
|
+
ref={bindHost}
|
|
93
127
|
android={props.android}
|
|
94
128
|
ios={props.ios}
|
|
95
129
|
row={props.row}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, expect, it, vi } from 'vitest'
|
|
3
|
+
import { act, createRoot } from 'octane'
|
|
4
|
+
import { Extension, Mark, Node } from '@tiptap/core'
|
|
5
|
+
import StarterKit from '@tiptap/starter-kit'
|
|
6
|
+
import { TiptapEditor } from './TiptapEditor.web.tsrx'
|
|
7
|
+
import type { TiptapEditorHandle } from './types'
|
|
8
|
+
|
|
9
|
+
const Spoiler = Mark.create({
|
|
10
|
+
name: 'spoiler',
|
|
11
|
+
parseHTML: () => [{ tag: 'span[data-spoiler]' }],
|
|
12
|
+
renderHTML: () => ['span', { 'data-spoiler': '' }, 0],
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const Shout = Extension.create({
|
|
16
|
+
name: 'shout',
|
|
17
|
+
addCommands() {
|
|
18
|
+
return {
|
|
19
|
+
shout: () => ({ commands }: any) => commands.setContent('<p>SHOUT</p>'),
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// Minimal schema for the starterKit:false path — callers dropping the
|
|
25
|
+
// built-in kit supply the base nodes themselves.
|
|
26
|
+
const Doc = Node.create({ name: 'doc', topNode: true, content: 'block+' })
|
|
27
|
+
const Paragraph = Node.create({
|
|
28
|
+
name: 'paragraph',
|
|
29
|
+
group: 'block',
|
|
30
|
+
content: 'inline*',
|
|
31
|
+
parseHTML: () => [{ tag: 'p' }],
|
|
32
|
+
renderHTML: () => ['p', 0],
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const Text = Node.create({ name: 'text', group: 'inline' })
|
|
36
|
+
|
|
37
|
+
const roots: any[] = []
|
|
38
|
+
|
|
39
|
+
function mount(props: any = {}) {
|
|
40
|
+
const el = document.createElement('div')
|
|
41
|
+
document.body.append(el)
|
|
42
|
+
const root = createRoot(el)
|
|
43
|
+
roots.push(root)
|
|
44
|
+
let handle: TiptapEditorHandle | null = null
|
|
45
|
+
act(() =>
|
|
46
|
+
root.render(
|
|
47
|
+
<TiptapEditor
|
|
48
|
+
{...props}
|
|
49
|
+
ref={(next: TiptapEditorHandle | null) => {
|
|
50
|
+
handle = next
|
|
51
|
+
}}
|
|
52
|
+
/>,
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return { el, root, get handle() { return handle } }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
for (const root of roots.splice(0)) act(() => root.unmount())
|
|
61
|
+
document.body.innerHTML = ''
|
|
62
|
+
vi.restoreAllMocks()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('mounts StarterKit by default', () => {
|
|
66
|
+
const { handle } = mount({ value: '<p>Travel notes</p>' })
|
|
67
|
+
const editor = handle!.native
|
|
68
|
+
expect(editor.schema.marks.bold).toBeTruthy()
|
|
69
|
+
expect(editor.schema.nodes.heading).toBeTruthy()
|
|
70
|
+
expect(handle!.getHTML()).toContain('<p>Travel notes</p>')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('registers caller mark extensions alongside StarterKit', () => {
|
|
74
|
+
const { handle } = mount({
|
|
75
|
+
value: '<p>shh <span data-spoiler>secret</span></p>',
|
|
76
|
+
web: { extensions: [Spoiler] },
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const editor = handle!.native
|
|
80
|
+
expect(editor.schema.marks.spoiler).toBeTruthy()
|
|
81
|
+
expect(editor.schema.marks.bold).toBeTruthy()
|
|
82
|
+
|
|
83
|
+
const doc = handle!.getJSON()!
|
|
84
|
+
const marks = JSON.stringify(doc)
|
|
85
|
+
expect(marks).toContain('"spoiler"')
|
|
86
|
+
expect(handle!.getHTML()).toContain('data-spoiler')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('exposes commands added by caller extensions', () => {
|
|
90
|
+
const { handle } = mount({ web: { extensions: [Shout] } })
|
|
91
|
+
handle!.native.commands.shout()
|
|
92
|
+
expect(handle!.getHTML()).toContain('SHOUT')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('passes starterKit options to the built-in kit', () => {
|
|
96
|
+
const { handle } = mount({ web: { starterKit: { link: false } } })
|
|
97
|
+
const editor = handle!.native
|
|
98
|
+
expect(editor.schema.marks.link).toBeUndefined()
|
|
99
|
+
expect(editor.schema.marks.bold).toBeTruthy()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('hands the schema to caller extensions when starterKit is false', () => {
|
|
103
|
+
const { handle } = mount({
|
|
104
|
+
web: { starterKit: false, extensions: [Doc, Paragraph, Text, Spoiler] },
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const editor = handle!.native
|
|
108
|
+
expect(editor.schema.marks.spoiler).toBeTruthy()
|
|
109
|
+
expect(editor.schema.marks.bold).toBeUndefined()
|
|
110
|
+
expect(editor.schema.nodes.heading).toBeUndefined()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('never registers StarterKit twice', () => {
|
|
114
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
115
|
+
|
|
116
|
+
const { handle } = mount({
|
|
117
|
+
web: { extensions: [StarterKit.configure({ link: false })] },
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const kits = handle!.native.extensionManager.extensions.filter(
|
|
121
|
+
(extension: any) => extension.name === 'starterKit',
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
expect(kits).toHaveLength(1)
|
|
125
|
+
expect(handle!.native.schema.marks.link).toBeUndefined()
|
|
126
|
+
expect(warn).not.toHaveBeenCalledWith(
|
|
127
|
+
expect.stringContaining('Duplicate extension names'),
|
|
128
|
+
)
|
|
129
|
+
})
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'octane'
|
|
1
|
+
import { useImperativeHandle, useEffect, useMemo, useRef } from 'octane'
|
|
2
2
|
import { EditorContent, useEditor } from '@octanejs/tiptap'
|
|
3
3
|
import StarterKit from '@tiptap/starter-kit'
|
|
4
|
-
import type {
|
|
4
|
+
import type { Extensions } from '@tiptap/core'
|
|
5
|
+
import type { TiptapEditorHandle, TiptapEditorProps, TiptapFormat, TiptapWebOptions } from './types'
|
|
5
6
|
|
|
6
7
|
export const supported = true
|
|
7
8
|
|
|
@@ -27,19 +28,6 @@ const COMMAND: Record<WebFormat, (chain: any) => any> = {
|
|
|
27
28
|
heading6: (c) => c.toggleHeading({ level: 6 }),
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const IS_ACTIVE: Partial<Record<TiptapFormat, (editor: any) => boolean>> = {
|
|
31
|
-
bold: (e) => e.isActive('bold'),
|
|
32
|
-
italic: (e) => e.isActive('italic'),
|
|
33
|
-
underline: (e) => e.isActive('underline'),
|
|
34
|
-
strikethrough: (e) => e.isActive('strike'),
|
|
35
|
-
code: (e) => e.isActive('code'),
|
|
36
|
-
blockquote: (e) => e.isActive('blockquote'),
|
|
37
|
-
bulletList: (e) => e.isActive('bulletList'),
|
|
38
|
-
orderedList: (e) => e.isActive('orderedList'),
|
|
39
|
-
codeBlock: (e) => e.isActive('codeBlock'),
|
|
40
|
-
link: (e) => e.isActive('link'),
|
|
41
|
-
}
|
|
42
|
-
|
|
43
31
|
const ACTIVE_OF: [TiptapFormat, string, any?][] = [
|
|
44
32
|
['bold', 'bold'],
|
|
45
33
|
['italic', 'italic'],
|
|
@@ -69,16 +57,39 @@ function activeFormats(editor: any): TiptapFormat[] {
|
|
|
69
57
|
return out
|
|
70
58
|
}
|
|
71
59
|
|
|
60
|
+
/** Extension list for the web engine: the built-in StarterKit (optionally
|
|
61
|
+
* configured) followed by caller extras. `starterKit: false` or a
|
|
62
|
+
* starterKit-named caller entry hands the schema over to the caller —
|
|
63
|
+
* registering two StarterKits trips tiptap's duplicate-name warning. */
|
|
64
|
+
function resolveExtensions(web: TiptapWebOptions | undefined): Extensions {
|
|
65
|
+
const extras = web?.extensions ?? []
|
|
66
|
+
if (web?.starterKit === false || extras.some((ext) => ext?.name === 'starterKit')) {
|
|
67
|
+
return extras
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const kit = web?.starterKit ? StarterKit.configure(web.starterKit) : StarterKit
|
|
71
|
+
return [kit, ...extras]
|
|
72
|
+
}
|
|
73
|
+
|
|
72
74
|
/** Unified editor facade, web half — a thin adapter over the published
|
|
73
75
|
* `@octanejs/tiptap` binding (useEditor + EditorContent + StarterKit). */
|
|
74
76
|
export function TiptapEditor(props: TiptapEditorProps) @{
|
|
75
77
|
const lastEmitted = useRef<string | undefined>(undefined)
|
|
76
78
|
const lastJSON = useRef<string | undefined>(undefined)
|
|
79
|
+
const lastHTMLInput = useRef(JSON.stringify([props.json !== undefined, props.value]))
|
|
80
|
+
const lastJSONInput = useRef(JSON.stringify(props.json))
|
|
77
81
|
const bound = useRef<TiptapEditorHandle | undefined>(undefined)
|
|
78
82
|
|
|
83
|
+
// tiptap fixes the schema at construction; memoized on the option values
|
|
84
|
+
// so a stable caller config never re-triggers setOptions.
|
|
85
|
+
const extensions = useMemo(
|
|
86
|
+
() => resolveExtensions(props.web),
|
|
87
|
+
[props.web?.extensions, props.web?.starterKit],
|
|
88
|
+
)
|
|
89
|
+
|
|
79
90
|
const editor = useEditor({
|
|
80
|
-
extensions
|
|
81
|
-
content: (props.
|
|
91
|
+
extensions,
|
|
92
|
+
content: (props.json ?? props.value ?? '') as any,
|
|
82
93
|
editable: props.editable !== false,
|
|
83
94
|
autofocus: props.autofocus ?? false,
|
|
84
95
|
onCreate: () => props.onReady?.(),
|
|
@@ -103,14 +114,21 @@ export function TiptapEditor(props: TiptapEditorProps) @{
|
|
|
103
114
|
// External `value` changes push setContent; editor-emitted HTML is skipped
|
|
104
115
|
// (the transaction already owns the doc).
|
|
105
116
|
useEffect(() => {
|
|
106
|
-
if (!editor
|
|
117
|
+
if (!editor) return
|
|
118
|
+
const stamp = JSON.stringify([props.json !== undefined, props.value])
|
|
119
|
+
if (stamp === lastHTMLInput.current) return
|
|
120
|
+
lastHTMLInput.current = stamp
|
|
121
|
+
if (props.value === undefined || props.json !== undefined) return
|
|
107
122
|
if (props.value === lastEmitted.current) return
|
|
108
123
|
editor.commands.setContent(props.value)
|
|
109
|
-
}, [editor, props.value])
|
|
124
|
+
}, [editor, props.value, props.json])
|
|
110
125
|
|
|
111
126
|
useEffect(() => {
|
|
112
|
-
if (!editor
|
|
113
|
-
|
|
127
|
+
if (!editor) return
|
|
128
|
+
const stamp = JSON.stringify(props.json)
|
|
129
|
+
if (stamp === lastJSONInput.current) return
|
|
130
|
+
lastJSONInput.current = stamp
|
|
131
|
+
if (props.json === undefined || stamp === lastJSON.current) return
|
|
114
132
|
editor.commands.setContent(props.json as any)
|
|
115
133
|
}, [editor, props.json])
|
|
116
134
|
|
|
@@ -121,8 +139,8 @@ export function TiptapEditor(props: TiptapEditorProps) @{
|
|
|
121
139
|
|
|
122
140
|
// The handle rebinds whenever the editor instance appears — stable
|
|
123
141
|
// afterward (useEditor keeps the instance identity).
|
|
124
|
-
|
|
125
|
-
if (!editor
|
|
142
|
+
useImperativeHandle<TiptapEditorHandle | null>(props.ref, () => {
|
|
143
|
+
if (!editor) return null
|
|
126
144
|
const e: any = editor
|
|
127
145
|
bound.current = {
|
|
128
146
|
getHTML: () => e.getHTML(),
|
|
@@ -141,7 +159,7 @@ export function TiptapEditor(props: TiptapEditorProps) @{
|
|
|
141
159
|
}
|
|
142
160
|
},
|
|
143
161
|
removeLink: () => e.chain().focus().unsetLink().run(),
|
|
144
|
-
isActive: (format) =>
|
|
162
|
+
isActive: (format) => activeFormats(e).includes(format),
|
|
145
163
|
undo: () => e.commands.undo(),
|
|
146
164
|
redo: () => e.commands.redo(),
|
|
147
165
|
focus: () => e.commands.focus(),
|
|
@@ -150,7 +168,7 @@ export function TiptapEditor(props: TiptapEditorProps) @{
|
|
|
150
168
|
native: e,
|
|
151
169
|
}
|
|
152
170
|
|
|
153
|
-
|
|
171
|
+
return bound.current
|
|
154
172
|
}, [editor])
|
|
155
173
|
|
|
156
174
|
<div className={['vx-richtext', props.className]} style={props.style}>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { createRoot } from 'octane'
|
|
2
|
+
import { TiptapEditor } from './TiptapEditor.web.tsrx'
|
|
3
|
+
import { installEditorDocument } from '@octane-xplat/richtext/browser-transport'
|
|
4
|
+
const root = createRoot(document.getElementById('root')!)
|
|
5
|
+
installEditorDocument((props) => root.render(<TiptapEditor {...props} />))
|
package/src/index.macos.ts
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export const supported =
|
|
4
|
-
|
|
5
|
-
/** AppKit host has no rich-text surface yet. */
|
|
6
|
-
export function TiptapEditor(_props: TiptapEditorProps): null {
|
|
7
|
-
return null
|
|
8
|
-
}
|
|
9
|
-
|
|
1
|
+
export { TiptapEditor } from './Editor.macos'
|
|
2
|
+
/** The AppKit/WKWebView backend is implemented. */
|
|
3
|
+
export const supported = true
|
|
10
4
|
export type * from './types'
|
|
11
5
|
|
|
12
|
-
/**
|
|
13
|
-
export function ensureJSONBridge(): Promise<boolean> {
|
|
14
|
-
return
|
|
6
|
+
/** The local bundle includes its JSON model; wait for onReady before reading a document. */
|
|
7
|
+
export async function ensureJSONBridge(): Promise<boolean> {
|
|
8
|
+
return true
|
|
15
9
|
}
|
|
16
10
|
|
|
17
11
|
export function jsonBridgeReady(): boolean {
|
|
18
|
-
return
|
|
12
|
+
return true
|
|
19
13
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import type { Octane } from 'octane/jsx-runtime'
|
|
2
|
+
import type { Extensions } from '@tiptap/core'
|
|
3
|
+
import type { StarterKitOptions } from '@tiptap/starter-kit'
|
|
4
|
+
|
|
1
5
|
/** Format vocabulary shared by the web (tiptap StarterKit) and native
|
|
2
6
|
* (Aztec) backends. On web, starter-kit covers everything except
|
|
3
7
|
* taskList/highlight/subscript/superscript/align* — those no-op there and
|
|
@@ -33,13 +37,34 @@ export type TiptapFormat =
|
|
|
33
37
|
* callers round-trip through tiptap's own schema on both platforms. */
|
|
34
38
|
export type TiptapJSON = { type: string; content?: unknown[] } & Record<string, unknown>
|
|
35
39
|
|
|
40
|
+
/** Engine options for the web target only. The `.web` entry mounts a real
|
|
41
|
+
* tiptap `Editor`, so callers can extend its schema here. The Android
|
|
42
|
+
* facade (Aztec leaf) and the bundled AppKit engine cannot host DOM-bound
|
|
43
|
+
* ProseMirror/tiptap extension objects and ignore these options. Caller
|
|
44
|
+
* plugins, node views, and browser menus are web behavior, not shared
|
|
45
|
+
* contract. */
|
|
46
|
+
export interface TiptapWebOptions {
|
|
47
|
+
/** Extra tiptap extensions appended after the built-in StarterKit — any
|
|
48
|
+
* `Extension`/`Node`/`Mark` from `@tiptap/*` or app code. An entry named
|
|
49
|
+
* `starterKit` replaces the built-in instead of registering it twice.
|
|
50
|
+
* The editor fixes its schema at construction; keep the list
|
|
51
|
+
* referentially stable across renders. */
|
|
52
|
+
extensions?: Extensions
|
|
53
|
+
/** `StarterKit.configure()` options for the built-in StarterKit, or
|
|
54
|
+
* `false` to omit it — `extensions` must then supply a complete schema
|
|
55
|
+
* (document, paragraph, and text nodes at minimum). */
|
|
56
|
+
starterKit?: StarterKitOptions | false
|
|
57
|
+
}
|
|
58
|
+
|
|
36
59
|
export interface TiptapEditorHandle {
|
|
60
|
+
/** AppKit: returns the latest WebKit snapshot; commands are asynchronous. */
|
|
37
61
|
getHTML(): string
|
|
38
62
|
/** Web: `commands.setContent` (undo-aware). Native: Aztec `fromHtml` —
|
|
39
63
|
* resets undo history. */
|
|
40
64
|
setHTML(html: string): void
|
|
41
65
|
/** Doc JSON, or null while the native schema modules are still loading /
|
|
42
66
|
* on a runtime that can't host them (`json` flag). Web is always ready. */
|
|
67
|
+
/** AppKit: latest live engine JSON snapshot; null before onReady. */
|
|
43
68
|
getJSON(): TiptapJSON | null
|
|
44
69
|
setJSON(doc: TiptapJSON): void
|
|
45
70
|
apply(format: TiptapFormat): void
|
|
@@ -53,6 +78,7 @@ export interface TiptapEditorHandle {
|
|
|
53
78
|
isFocused(): boolean
|
|
54
79
|
/** The platform surface — a tiptap `Editor` on web, `AztecText` on
|
|
55
80
|
* Android. */
|
|
81
|
+
/** AppKit: the XplatEditorHost transport, not the browser engine instance. */
|
|
56
82
|
native: any
|
|
57
83
|
}
|
|
58
84
|
|
|
@@ -64,7 +90,9 @@ export interface TiptapEditorProps {
|
|
|
64
90
|
* setContent, preserving the diff loop the editor emitted is skipped). */
|
|
65
91
|
value?: string
|
|
66
92
|
/** Document JSON. Takes precedence over `value` when both arrive. On
|
|
67
|
-
* native it waits for the schema modules (see
|
|
93
|
+
* native it waits for the schema modules and host readiness (see
|
|
94
|
+
* `onJSONReady`); a doc that arrives early is applied once both are up
|
|
95
|
+
* rather than dropped. */
|
|
68
96
|
json?: TiptapJSON
|
|
69
97
|
placeholder?: string
|
|
70
98
|
editable?: boolean
|
|
@@ -73,15 +101,19 @@ export interface TiptapEditorProps {
|
|
|
73
101
|
/** Fires once when the native JSON bridge settles — `true` if the
|
|
74
102
|
* DOM-free tiptap modules loaded, `false` on runtimes that can't host
|
|
75
103
|
* them (JSON calls then no-op / return null). Never fires on web. */
|
|
104
|
+
/** AppKit: fires true with mounted engine readiness; no headless conversion bridge. */
|
|
76
105
|
onJSONReady?: (ready: boolean) => void
|
|
77
106
|
onChange?: (html: string) => void
|
|
78
107
|
onJSONChange?: (doc: TiptapJSON) => void
|
|
79
108
|
onSelectionChange?: (event: { start: number; end: number; active: TiptapFormat[] }) => void
|
|
80
109
|
onFocus?: () => void
|
|
81
110
|
onBlur?: () => void
|
|
82
|
-
|
|
111
|
+
ref?: Octane.Ref<TiptapEditorHandle>
|
|
83
112
|
android?: Record<string, any>
|
|
84
113
|
ios?: Record<string, any>
|
|
114
|
+
/** Web-target engine configuration — tiptap extensions and StarterKit
|
|
115
|
+
* options. See `TiptapWebOptions`; ignored on native and AppKit. */
|
|
116
|
+
web?: TiptapWebOptions
|
|
85
117
|
row?: number | string
|
|
86
118
|
col?: number | string
|
|
87
119
|
rowSpan?: number
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { UniversalComponent } from 'octane/universal'
|
|
2
|
+
import type { TiptapEditorProps } from '../src/types'
|
|
3
|
+
export declare const TiptapEditor: UniversalComponent<TiptapEditorProps>
|
|
4
|
+
export declare const supported: boolean
|
|
5
|
+
export type * from '../src/types'
|
|
6
|
+
export declare function ensureJSONBridge(): Promise<boolean>
|
|
7
|
+
export declare function jsonBridgeReady(): boolean
|
package/types/types.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import type { Octane } from 'octane/jsx-runtime';
|
|
2
|
+
import type { Extensions } from '@tiptap/core';
|
|
3
|
+
import type { StarterKitOptions } from '@tiptap/starter-kit';
|
|
1
4
|
/** Format vocabulary shared by the web (tiptap StarterKit) and native
|
|
2
5
|
* (Aztec) backends. On web, starter-kit covers everything except
|
|
3
6
|
* taskList/highlight/subscript/superscript/align* — those no-op there and
|
|
@@ -9,13 +12,33 @@ export type TiptapJSON = {
|
|
|
9
12
|
type: string;
|
|
10
13
|
content?: unknown[];
|
|
11
14
|
} & Record<string, unknown>;
|
|
15
|
+
/** Engine options for the web target only. The `.web` entry mounts a real
|
|
16
|
+
* tiptap `Editor`, so callers can extend its schema here. The Android
|
|
17
|
+
* facade (Aztec leaf) and the bundled AppKit engine cannot host DOM-bound
|
|
18
|
+
* ProseMirror/tiptap extension objects and ignore these options. Caller
|
|
19
|
+
* plugins, node views, and browser menus are web behavior, not shared
|
|
20
|
+
* contract. */
|
|
21
|
+
export interface TiptapWebOptions {
|
|
22
|
+
/** Extra tiptap extensions appended after the built-in StarterKit — any
|
|
23
|
+
* `Extension`/`Node`/`Mark` from `@tiptap/*` or app code. An entry named
|
|
24
|
+
* `starterKit` replaces the built-in instead of registering it twice.
|
|
25
|
+
* The editor fixes its schema at construction; keep the list
|
|
26
|
+
* referentially stable across renders. */
|
|
27
|
+
extensions?: Extensions;
|
|
28
|
+
/** `StarterKit.configure()` options for the built-in StarterKit, or
|
|
29
|
+
* `false` to omit it — `extensions` must then supply a complete schema
|
|
30
|
+
* (document, paragraph, and text nodes at minimum). */
|
|
31
|
+
starterKit?: StarterKitOptions | false;
|
|
32
|
+
}
|
|
12
33
|
export interface TiptapEditorHandle {
|
|
34
|
+
/** AppKit: returns the latest WebKit snapshot; commands are asynchronous. */
|
|
13
35
|
getHTML(): string;
|
|
14
36
|
/** Web: `commands.setContent` (undo-aware). Native: Aztec `fromHtml` —
|
|
15
37
|
* resets undo history. */
|
|
16
38
|
setHTML(html: string): void;
|
|
17
39
|
/** Doc JSON, or null while the native schema modules are still loading /
|
|
18
40
|
* on a runtime that can't host them (`json` flag). Web is always ready. */
|
|
41
|
+
/** AppKit: latest live engine JSON snapshot; null before onReady. */
|
|
19
42
|
getJSON(): TiptapJSON | null;
|
|
20
43
|
setJSON(doc: TiptapJSON): void;
|
|
21
44
|
apply(format: TiptapFormat): void;
|
|
@@ -29,6 +52,7 @@ export interface TiptapEditorHandle {
|
|
|
29
52
|
isFocused(): boolean;
|
|
30
53
|
/** The platform surface — a tiptap `Editor` on web, `AztecText` on
|
|
31
54
|
* Android. */
|
|
55
|
+
/** AppKit: the XplatEditorHost transport, not the browser engine instance. */
|
|
32
56
|
native: any;
|
|
33
57
|
}
|
|
34
58
|
export interface TiptapEditorProps {
|
|
@@ -39,7 +63,9 @@ export interface TiptapEditorProps {
|
|
|
39
63
|
* setContent, preserving the diff loop the editor emitted is skipped). */
|
|
40
64
|
value?: string;
|
|
41
65
|
/** Document JSON. Takes precedence over `value` when both arrive. On
|
|
42
|
-
* native it waits for the schema modules (see
|
|
66
|
+
* native it waits for the schema modules and host readiness (see
|
|
67
|
+
* `onJSONReady`); a doc that arrives early is applied once both are up
|
|
68
|
+
* rather than dropped. */
|
|
43
69
|
json?: TiptapJSON;
|
|
44
70
|
placeholder?: string;
|
|
45
71
|
editable?: boolean;
|
|
@@ -48,6 +74,7 @@ export interface TiptapEditorProps {
|
|
|
48
74
|
/** Fires once when the native JSON bridge settles — `true` if the
|
|
49
75
|
* DOM-free tiptap modules loaded, `false` on runtimes that can't host
|
|
50
76
|
* them (JSON calls then no-op / return null). Never fires on web. */
|
|
77
|
+
/** AppKit: fires true with mounted engine readiness; no headless conversion bridge. */
|
|
51
78
|
onJSONReady?: (ready: boolean) => void;
|
|
52
79
|
onChange?: (html: string) => void;
|
|
53
80
|
onJSONChange?: (doc: TiptapJSON) => void;
|
|
@@ -58,9 +85,12 @@ export interface TiptapEditorProps {
|
|
|
58
85
|
}) => void;
|
|
59
86
|
onFocus?: () => void;
|
|
60
87
|
onBlur?: () => void;
|
|
61
|
-
|
|
88
|
+
ref?: Octane.Ref<TiptapEditorHandle>;
|
|
62
89
|
android?: Record<string, any>;
|
|
63
90
|
ios?: Record<string, any>;
|
|
91
|
+
/** Web-target engine configuration — tiptap extensions and StarterKit
|
|
92
|
+
* options. See `TiptapWebOptions`; ignored on native and AppKit. */
|
|
93
|
+
web?: TiptapWebOptions;
|
|
64
94
|
row?: number | string;
|
|
65
95
|
col?: number | string;
|
|
66
96
|
rowSpan?: number;
|