@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.
@@ -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 jsonReady = useRef(false)
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
- const bindHost = useCallback((h: RichTextEditorHandle) => {
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
- props.bind?.({
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
- const html = jsonBridgeReady() ? jsonToHTML(doc) : null
27
- if (html != null) {
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
- jsonReady.current = ok
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, when the bridge is up and the doc wasn't
55
- // emitted by the editor itself.
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 || !jsonBridgeReady() || !inner.current) return
58
- const stamp = JSON.stringify(props.json)
59
- if (stamp === lastJSON.current) return
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={props.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
- bind={bindHost}
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 { TiptapEditorHandle, TiptapEditorProps, TiptapFormat } from './types'
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: [StarterKit],
81
- content: (props.value ?? props.json ?? '') as any,
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 || props.value === undefined) return
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 || props.json === undefined) return
113
- if (JSON.stringify(props.json) === lastJSON.current) return
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
- useEffect(() => {
125
- if (!editor || bound.current?.native === editor) return
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) => IS_ACTIVE[format]?.(e) ?? false,
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
- props.bind?.(bound.current)
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} />))
@@ -1,19 +1,13 @@
1
- import type { TiptapEditorProps } from './types'
2
-
3
- export const supported = false
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
- /** No editor, no bridge. */
13
- export function ensureJSONBridge(): Promise<boolean> {
14
- return Promise.resolve(false)
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 false
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 `onJSONReady`). */
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
- bind?: (handle: TiptapEditorHandle) => void
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 `onJSONReady`). */
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
- bind?: (handle: TiptapEditorHandle) => void;
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;