@octane-xplat/tiptap 0.0.1 → 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/LICENSE +21 -0
- package/README.md +78 -2
- package/dist/macos/browser.d.ts +1 -0
- package/dist/macos/browser.js +2 -0
- package/dist/native/index.js +3 -0
- package/dist/native/index2.js +82 -0
- package/dist/native/index3.js +211 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/web/index.js +12 -0
- package/dist/web/index2.js +183 -0
- package/dist/web/octane-client-build.json +8 -0
- package/package.json +85 -11
- package/src/Editor.macos.tsx +9 -0
- package/src/TiptapEditor.tsrx +109 -0
- package/src/TiptapEditor.web.tsrx +155 -0
- package/src/browser-entry.web.tsx +5 -0
- package/src/index.macos.ts +13 -0
- package/src/index.ts +3 -0
- package/src/index.web.ts +12 -0
- package/src/json-bridge.ts +124 -0
- package/src/types.ts +102 -0
- package/types/index.d.ts +8 -0
- package/types/index.macos.d.ts +7 -0
- package/types/types.d.ts +80 -0
- package/index.d.ts +0 -1
- package/index.js +0 -2
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
+
import { useState, useImperativeHandle, useCallback, useEffect, useRef } from 'octane'
|
|
3
|
+
import { RichTextEditor, supported as leafSupported } from '@octane-xplat/richtext'
|
|
4
|
+
import type { RichTextEditorHandle } from '@octane-xplat/richtext'
|
|
5
|
+
import type { TiptapEditorHandle, TiptapEditorProps } from './types'
|
|
6
|
+
import { ensureJSONBridge, htmlToJSON, jsonBridgeReady, jsonToHTML } from './json-bridge'
|
|
7
|
+
|
|
8
|
+
export const supported = leafSupported
|
|
9
|
+
|
|
10
|
+
/** Unified editor facade, native half. Rendering/editing run through the
|
|
11
|
+
* Aztec leaf; the tiptap document model (schema, generateJSON,
|
|
12
|
+
* renderToHTMLString) handles JSON interchange on top of it. */
|
|
13
|
+
export function TiptapEditor(props: TiptapEditorProps) @{
|
|
14
|
+
const inner = useRef<RichTextEditorHandle | null>(null)
|
|
15
|
+
const [exposedHandle, setHandle] = useState<TiptapEditorHandle | null>(null)
|
|
16
|
+
useImperativeHandle<TiptapEditorHandle | null>(props.ref, () => exposedHandle, [exposedHandle])
|
|
17
|
+
const jsonReady = useRef(false)
|
|
18
|
+
const lastJSON = useRef<string | undefined>(undefined)
|
|
19
|
+
|
|
20
|
+
const bindHost = useCallback((h: RichTextEditorHandle | null) => {
|
|
21
|
+
inner.current = h
|
|
22
|
+
|
|
23
|
+
setHandle((previous) => previous?.native === h?.native ? previous : h ? ({
|
|
24
|
+
getHTML: () => h.getHTML(),
|
|
25
|
+
setHTML: (html: string) => h.setHTML(html),
|
|
26
|
+
getJSON: () => (jsonBridgeReady() ? htmlToJSON(h.getHTML()) : null),
|
|
27
|
+
setJSON: (doc) => {
|
|
28
|
+
const html = jsonBridgeReady() ? jsonToHTML(doc) : null
|
|
29
|
+
if (html != null) {
|
|
30
|
+
lastJSON.current = JSON.stringify(doc)
|
|
31
|
+
h.setHTML(html)
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
apply: (format) => h.apply(format),
|
|
35
|
+
linkTo: (url, anchor) => h.linkTo(url, anchor),
|
|
36
|
+
removeLink: () => h.removeLink(),
|
|
37
|
+
isActive: (format) => h.isActive(format),
|
|
38
|
+
undo: () => h.undo(),
|
|
39
|
+
redo: () => h.redo(),
|
|
40
|
+
focus: () => h.focus(),
|
|
41
|
+
blur: () => h.blur(),
|
|
42
|
+
isFocused: () => h.isFocused(),
|
|
43
|
+
native: h.native,
|
|
44
|
+
} satisfies TiptapEditorHandle) : null)
|
|
45
|
+
}, [])
|
|
46
|
+
|
|
47
|
+
// The bridge loads once per process — dynamic imports of the DOM-free
|
|
48
|
+
// tiptap slices, plus the zeed-dom parse shim.
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
ensureJSONBridge().then((ok) => {
|
|
51
|
+
jsonReady.current = ok
|
|
52
|
+
props.onJSONReady?.(ok)
|
|
53
|
+
})
|
|
54
|
+
}, [])
|
|
55
|
+
|
|
56
|
+
// `json` prop → editor content, when the bridge is up and the doc wasn't
|
|
57
|
+
// emitted by the editor itself.
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (!props.json || !jsonBridgeReady() || !inner.current) return
|
|
60
|
+
const stamp = JSON.stringify(props.json)
|
|
61
|
+
if (stamp === lastJSON.current) return
|
|
62
|
+
const html = jsonToHTML(props.json)
|
|
63
|
+
if (html != null) {
|
|
64
|
+
lastJSON.current = stamp
|
|
65
|
+
inner.current.setHTML(html)
|
|
66
|
+
}
|
|
67
|
+
}, [props.json])
|
|
68
|
+
|
|
69
|
+
const emitJSON = () => {
|
|
70
|
+
if (!inner.current || !jsonBridgeReady()) return
|
|
71
|
+
const doc = htmlToJSON(inner.current.getHTML())
|
|
72
|
+
if (doc) {
|
|
73
|
+
lastJSON.current = JSON.stringify(doc)
|
|
74
|
+
props.onJSONChange?.(doc)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
<RichTextEditor
|
|
79
|
+
id={props.id}
|
|
80
|
+
className={props.className}
|
|
81
|
+
style={props.style}
|
|
82
|
+
value={props.value}
|
|
83
|
+
placeholder={props.placeholder}
|
|
84
|
+
editable={props.editable}
|
|
85
|
+
autofocus={props.autofocus}
|
|
86
|
+
onReady={props.onReady}
|
|
87
|
+
onChange={(html) => {
|
|
88
|
+
props.onChange?.(html)
|
|
89
|
+
emitJSON()
|
|
90
|
+
}}
|
|
91
|
+
onSelectionChange={props.onSelectionChange}
|
|
92
|
+
onFocus={props.onFocus}
|
|
93
|
+
onBlur={props.onBlur}
|
|
94
|
+
ref={bindHost}
|
|
95
|
+
android={props.android}
|
|
96
|
+
ios={props.ios}
|
|
97
|
+
row={props.row}
|
|
98
|
+
col={props.col}
|
|
99
|
+
rowSpan={props.rowSpan}
|
|
100
|
+
colSpan={props.colSpan}
|
|
101
|
+
dock={props.dock}
|
|
102
|
+
left={props.left}
|
|
103
|
+
top={props.top}
|
|
104
|
+
flexGrow={props.flexGrow}
|
|
105
|
+
flexShrink={props.flexShrink}
|
|
106
|
+
alignSelf={props.alignSelf}
|
|
107
|
+
order={props.order}
|
|
108
|
+
/>;
|
|
109
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { useImperativeHandle, useEffect, useRef } from 'octane'
|
|
2
|
+
import { EditorContent, useEditor } from '@octanejs/tiptap'
|
|
3
|
+
import StarterKit from '@tiptap/starter-kit'
|
|
4
|
+
import type { TiptapEditorHandle, TiptapEditorProps, TiptapFormat } from './types'
|
|
5
|
+
|
|
6
|
+
export const supported = true
|
|
7
|
+
|
|
8
|
+
type WebFormat = Exclude<TiptapFormat, 'link' | 'taskList' | 'highlight' | 'subscript' | 'superscript' | 'alignLeft' | 'alignCenter' | 'alignRight'>
|
|
9
|
+
|
|
10
|
+
const COMMAND: Record<WebFormat, (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
|
+
heading1: (c) => c.toggleHeading({ level: 1 }),
|
|
23
|
+
heading2: (c) => c.toggleHeading({ level: 2 }),
|
|
24
|
+
heading3: (c) => c.toggleHeading({ level: 3 }),
|
|
25
|
+
heading4: (c) => c.toggleHeading({ level: 4 }),
|
|
26
|
+
heading5: (c) => c.toggleHeading({ level: 5 }),
|
|
27
|
+
heading6: (c) => c.toggleHeading({ level: 6 }),
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ACTIVE_OF: [TiptapFormat, string, any?][] = [
|
|
31
|
+
['bold', 'bold'],
|
|
32
|
+
['italic', 'italic'],
|
|
33
|
+
['underline', 'underline'],
|
|
34
|
+
['strikethrough', 'strike'],
|
|
35
|
+
['code', 'code'],
|
|
36
|
+
['blockquote', 'blockquote'],
|
|
37
|
+
['bulletList', 'bulletList'],
|
|
38
|
+
['orderedList', 'orderedList'],
|
|
39
|
+
['codeBlock', 'codeBlock'],
|
|
40
|
+
['link', 'link'],
|
|
41
|
+
['heading1', 'heading', { level: 1 }],
|
|
42
|
+
['heading2', 'heading', { level: 2 }],
|
|
43
|
+
['heading3', 'heading', { level: 3 }],
|
|
44
|
+
['heading4', 'heading', { level: 4 }],
|
|
45
|
+
['heading5', 'heading', { level: 5 }],
|
|
46
|
+
['heading6', 'heading', { level: 6 }],
|
|
47
|
+
['paragraph', 'paragraph'],
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
function activeFormats(editor: any): TiptapFormat[] {
|
|
51
|
+
const out: TiptapFormat[] = []
|
|
52
|
+
for (const [name, kind, attrs] of ACTIVE_OF) {
|
|
53
|
+
if (editor.isActive(kind, attrs)) out.push(name)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return out
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Unified editor facade, web half — a thin adapter over the published
|
|
60
|
+
* `@octanejs/tiptap` binding (useEditor + EditorContent + StarterKit). */
|
|
61
|
+
export function TiptapEditor(props: TiptapEditorProps) @{
|
|
62
|
+
const lastEmitted = useRef<string | undefined>(undefined)
|
|
63
|
+
const lastJSON = useRef<string | undefined>(undefined)
|
|
64
|
+
const lastHTMLInput = useRef(JSON.stringify([props.json !== undefined, props.value]))
|
|
65
|
+
const lastJSONInput = useRef(JSON.stringify(props.json))
|
|
66
|
+
const bound = useRef<TiptapEditorHandle | undefined>(undefined)
|
|
67
|
+
|
|
68
|
+
const editor = useEditor({
|
|
69
|
+
extensions: [StarterKit],
|
|
70
|
+
content: (props.json ?? props.value ?? '') as any,
|
|
71
|
+
editable: props.editable !== false,
|
|
72
|
+
autofocus: props.autofocus ?? false,
|
|
73
|
+
onCreate: () => props.onReady?.(),
|
|
74
|
+
onUpdate: ({ editor: e }: any) => {
|
|
75
|
+
const html = e.getHTML()
|
|
76
|
+
lastEmitted.current = html
|
|
77
|
+
props.onChange?.(html)
|
|
78
|
+
const doc = e.getJSON()
|
|
79
|
+
lastJSON.current = JSON.stringify(doc)
|
|
80
|
+
props.onJSONChange?.(doc)
|
|
81
|
+
},
|
|
82
|
+
onFocus: () => props.onFocus?.(),
|
|
83
|
+
onBlur: () => props.onBlur?.(),
|
|
84
|
+
onSelectionUpdate: ({ editor: e }: any) =>
|
|
85
|
+
props.onSelectionChange?.({
|
|
86
|
+
start: e.state.selection.from,
|
|
87
|
+
end: e.state.selection.to,
|
|
88
|
+
active: activeFormats(e),
|
|
89
|
+
}),
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
// External `value` changes push setContent; editor-emitted HTML is skipped
|
|
93
|
+
// (the transaction already owns the doc).
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!editor) return
|
|
96
|
+
const stamp = JSON.stringify([props.json !== undefined, props.value])
|
|
97
|
+
if (stamp === lastHTMLInput.current) return
|
|
98
|
+
lastHTMLInput.current = stamp
|
|
99
|
+
if (props.value === undefined || props.json !== undefined) return
|
|
100
|
+
if (props.value === lastEmitted.current) return
|
|
101
|
+
editor.commands.setContent(props.value)
|
|
102
|
+
}, [editor, props.value, props.json])
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!editor) return
|
|
106
|
+
const stamp = JSON.stringify(props.json)
|
|
107
|
+
if (stamp === lastJSONInput.current) return
|
|
108
|
+
lastJSONInput.current = stamp
|
|
109
|
+
if (props.json === undefined || stamp === lastJSON.current) return
|
|
110
|
+
editor.commands.setContent(props.json as any)
|
|
111
|
+
}, [editor, props.json])
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!editor) return
|
|
115
|
+
editor.setEditable(props.editable !== false)
|
|
116
|
+
}, [editor, props.editable])
|
|
117
|
+
|
|
118
|
+
// The handle rebinds whenever the editor instance appears — stable
|
|
119
|
+
// afterward (useEditor keeps the instance identity).
|
|
120
|
+
useImperativeHandle<TiptapEditorHandle | null>(props.ref, () => {
|
|
121
|
+
if (!editor) return null
|
|
122
|
+
const e: any = editor
|
|
123
|
+
bound.current = {
|
|
124
|
+
getHTML: () => e.getHTML(),
|
|
125
|
+
setHTML: (html: string) => e.commands.setContent(html),
|
|
126
|
+
getJSON: () => e.getJSON(),
|
|
127
|
+
setJSON: (doc) => e.commands.setContent(doc),
|
|
128
|
+
apply: (format) => {
|
|
129
|
+
const run = COMMAND[format as WebFormat]
|
|
130
|
+
if (run) run(e.chain().focus()).run()
|
|
131
|
+
},
|
|
132
|
+
linkTo: (url, anchor) => {
|
|
133
|
+
if (anchor != null && anchor !== '') {
|
|
134
|
+
e.chain().focus().insertContent({ type: 'text', text: anchor, marks: [{ type: 'link', attrs: { href: url } }] }).run()
|
|
135
|
+
} else {
|
|
136
|
+
e.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
removeLink: () => e.chain().focus().unsetLink().run(),
|
|
140
|
+
isActive: (format) => activeFormats(e).includes(format),
|
|
141
|
+
undo: () => e.commands.undo(),
|
|
142
|
+
redo: () => e.commands.redo(),
|
|
143
|
+
focus: () => e.commands.focus(),
|
|
144
|
+
blur: () => e.commands.blur(),
|
|
145
|
+
isFocused: () => e.isFocused,
|
|
146
|
+
native: e,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return bound.current
|
|
150
|
+
}, [editor])
|
|
151
|
+
|
|
152
|
+
<div className={['vx-richtext', props.className]} style={props.style}>
|
|
153
|
+
<EditorContent editor={editor} />
|
|
154
|
+
</div>
|
|
155
|
+
}
|
|
@@ -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} />))
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { TiptapEditor } from './Editor.macos'
|
|
2
|
+
/** The AppKit/WKWebView backend is implemented. */
|
|
3
|
+
export const supported = true
|
|
4
|
+
export type * from './types'
|
|
5
|
+
|
|
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
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function jsonBridgeReady(): boolean {
|
|
12
|
+
return true
|
|
13
|
+
}
|
package/src/index.ts
ADDED
package/src/index.web.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { TiptapEditor, supported } from './TiptapEditor.web.tsrx'
|
|
2
|
+
export type * from './types'
|
|
3
|
+
|
|
4
|
+
/** Web needs no lazy bridge — tiptap is in the bundle already. Kept for
|
|
5
|
+
* call-site parity with the native entry. */
|
|
6
|
+
export function ensureJSONBridge(): Promise<boolean> {
|
|
7
|
+
return Promise.resolve(true)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function jsonBridgeReady(): boolean {
|
|
11
|
+
return true
|
|
12
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/** Lazy tiptap document-model bridge for native runtimes. The DOM-free
|
|
2
|
+
* slices (@tiptap/core schema + generateJSON, @tiptap/static-renderer,
|
|
3
|
+
* zeed-dom as the parser host) load on demand — on a runtime that can't
|
|
4
|
+
* host them the bridge settles to unavailable and callers get null/no-op.
|
|
5
|
+
*
|
|
6
|
+
* Import behavior was proven by the demos probe (see packages/demos
|
|
7
|
+
* TiptapProbe): all four modules load cleanly on both NS engines; the
|
|
8
|
+
* failure mode left is a future version or bundler regression, which the
|
|
9
|
+
* catch below degrades instead of crashing. */
|
|
10
|
+
|
|
11
|
+
import type { TiptapJSON } from './types'
|
|
12
|
+
|
|
13
|
+
interface Bridge {
|
|
14
|
+
generateJSON: (html: string, extensions: any[]) => any
|
|
15
|
+
renderToHTMLString: (options: { extensions: any[]; content: any }) => string
|
|
16
|
+
extensions: any[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let bridge: Bridge | null | undefined
|
|
20
|
+
|
|
21
|
+
/** zeed-dom's classList is a plain {contains,add,remove} object; tiptap's
|
|
22
|
+
* codeBlock parseHTML spreads it. Wrap the VElement getter so it hands back
|
|
23
|
+
* an iterable array that keeps the mutation methods. */
|
|
24
|
+
function patchClassList(VElement: any): void {
|
|
25
|
+
const proto = VElement?.prototype
|
|
26
|
+
if (!proto) {
|
|
27
|
+
throw new Error('VElement.prototype unavailable')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const desc = Object.getOwnPropertyDescriptor(proto, 'classList')
|
|
31
|
+
if (!desc?.get) {
|
|
32
|
+
throw new Error('classList is not a prototype getter')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if ((desc.get as any).__xplatPatched) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const patched = function (this: any) {
|
|
40
|
+
const cl = desc.get!.call(this)
|
|
41
|
+
if (cl && typeof cl[Symbol.iterator] !== 'function') {
|
|
42
|
+
const tokens = String(this.getAttribute('class') ?? '')
|
|
43
|
+
.split(/\s+/)
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
|
|
46
|
+
return Object.assign(tokens, cl)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return cl
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
;(patched as any).__xplatPatched = true
|
|
53
|
+
Object.defineProperty(proto, 'classList', {
|
|
54
|
+
configurable: true,
|
|
55
|
+
get: patched,
|
|
56
|
+
set: desc.set,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Install the minimal parse globals generateJSON's elementFromString
|
|
61
|
+
* reads. Only installs when the host lacks them, so a real DOM (web
|
|
62
|
+
* target) is never clobbered. */
|
|
63
|
+
function installDomShim(zeed: any): void {
|
|
64
|
+
const g = globalThis as any
|
|
65
|
+
if (g.window?.DOMParser) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
class ZeedDOMParser {
|
|
70
|
+
parseFromString(html: string) {
|
|
71
|
+
const doc = zeed.createHTMLDocument()
|
|
72
|
+
doc.body.innerHTML = html
|
|
73
|
+
return doc
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
g.window = { DOMParser: ZeedDOMParser }
|
|
78
|
+
g.document = zeed.createHTMLDocument()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function ensureJSONBridge(): Promise<boolean> {
|
|
82
|
+
if (bridge !== undefined) {
|
|
83
|
+
return bridge !== null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const [core, sk, sr, zeed] = await Promise.all([
|
|
88
|
+
import('@tiptap/core'),
|
|
89
|
+
import('@tiptap/starter-kit'),
|
|
90
|
+
import('@tiptap/static-renderer'),
|
|
91
|
+
import('zeed-dom'),
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
patchClassList(zeed.VElement)
|
|
95
|
+
installDomShim(zeed)
|
|
96
|
+
const StarterKit = (sk as any).default ?? (sk as any).StarterKit
|
|
97
|
+
|
|
98
|
+
bridge = {
|
|
99
|
+
generateJSON: core.generateJSON,
|
|
100
|
+
renderToHTMLString: sr.renderToHTMLString,
|
|
101
|
+
extensions: [StarterKit],
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return true
|
|
105
|
+
} catch {
|
|
106
|
+
bridge = null
|
|
107
|
+
return false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** HTML → doc JSON through tiptap's own parseHTML rules. Null when the
|
|
112
|
+
* bridge hasn't loaded (call ensureJSONBridge first / check `ready`). */
|
|
113
|
+
export function htmlToJSON(html: string): TiptapJSON | null {
|
|
114
|
+
return (bridge?.generateJSON(html, bridge.extensions) as TiptapJSON | undefined) ?? null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Doc JSON → HTML via tiptap's own renderHTML rules (no DOM). */
|
|
118
|
+
export function jsonToHTML(doc: TiptapJSON): string | null {
|
|
119
|
+
return bridge?.renderToHTMLString({ extensions: bridge.extensions, content: doc }) ?? null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function jsonBridgeReady(): boolean {
|
|
123
|
+
return bridge != null
|
|
124
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Octane } from 'octane/jsx-runtime'
|
|
2
|
+
|
|
3
|
+
/** Format vocabulary shared by the web (tiptap StarterKit) and native
|
|
4
|
+
* (Aztec) backends. On web, starter-kit covers everything except
|
|
5
|
+
* taskList/highlight/subscript/superscript/align* — those no-op there and
|
|
6
|
+
* map to Aztec formats on Android. */
|
|
7
|
+
export type TiptapFormat =
|
|
8
|
+
| 'bold'
|
|
9
|
+
| 'italic'
|
|
10
|
+
| 'underline'
|
|
11
|
+
| 'strikethrough'
|
|
12
|
+
| 'code'
|
|
13
|
+
| 'blockquote'
|
|
14
|
+
| 'bulletList'
|
|
15
|
+
| 'orderedList'
|
|
16
|
+
| 'taskList'
|
|
17
|
+
| 'codeBlock'
|
|
18
|
+
| 'horizontalRule'
|
|
19
|
+
| 'highlight'
|
|
20
|
+
| 'subscript'
|
|
21
|
+
| 'superscript'
|
|
22
|
+
| 'paragraph'
|
|
23
|
+
| 'heading1'
|
|
24
|
+
| 'heading2'
|
|
25
|
+
| 'heading3'
|
|
26
|
+
| 'heading4'
|
|
27
|
+
| 'heading5'
|
|
28
|
+
| 'heading6'
|
|
29
|
+
| 'alignLeft'
|
|
30
|
+
| 'alignCenter'
|
|
31
|
+
| 'alignRight'
|
|
32
|
+
| 'link'
|
|
33
|
+
|
|
34
|
+
/** Tiptap document JSON (`{ type: 'doc', content: [...] }`). Opaque here —
|
|
35
|
+
* callers round-trip through tiptap's own schema on both platforms. */
|
|
36
|
+
export type TiptapJSON = { type: string; content?: unknown[] } & Record<string, unknown>
|
|
37
|
+
|
|
38
|
+
export interface TiptapEditorHandle {
|
|
39
|
+
/** AppKit: returns the latest WebKit snapshot; commands are asynchronous. */
|
|
40
|
+
getHTML(): string
|
|
41
|
+
/** Web: `commands.setContent` (undo-aware). Native: Aztec `fromHtml` —
|
|
42
|
+
* resets undo history. */
|
|
43
|
+
setHTML(html: string): void
|
|
44
|
+
/** Doc JSON, or null while the native schema modules are still loading /
|
|
45
|
+
* on a runtime that can't host them (`json` flag). Web is always ready. */
|
|
46
|
+
/** AppKit: latest live engine JSON snapshot; null before onReady. */
|
|
47
|
+
getJSON(): TiptapJSON | null
|
|
48
|
+
setJSON(doc: TiptapJSON): void
|
|
49
|
+
apply(format: TiptapFormat): void
|
|
50
|
+
linkTo(url: string, anchor?: string): void
|
|
51
|
+
removeLink(): void
|
|
52
|
+
isActive(format: TiptapFormat): boolean
|
|
53
|
+
undo(): void
|
|
54
|
+
redo(): void
|
|
55
|
+
focus(): void
|
|
56
|
+
blur(): void
|
|
57
|
+
isFocused(): boolean
|
|
58
|
+
/** The platform surface — a tiptap `Editor` on web, `AztecText` on
|
|
59
|
+
* Android. */
|
|
60
|
+
/** AppKit: the XplatEditorHost transport, not the browser engine instance. */
|
|
61
|
+
native: any
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface TiptapEditorProps {
|
|
65
|
+
id?: string
|
|
66
|
+
className?: string
|
|
67
|
+
style?: any
|
|
68
|
+
/** Document HTML. Seed at mount; external changes re-push (web: via
|
|
69
|
+
* setContent, preserving the diff loop the editor emitted is skipped). */
|
|
70
|
+
value?: string
|
|
71
|
+
/** Document JSON. Takes precedence over `value` when both arrive. On
|
|
72
|
+
* native it waits for the schema modules (see `onJSONReady`). */
|
|
73
|
+
json?: TiptapJSON
|
|
74
|
+
placeholder?: string
|
|
75
|
+
editable?: boolean
|
|
76
|
+
autofocus?: boolean
|
|
77
|
+
onReady?: () => void
|
|
78
|
+
/** Fires once when the native JSON bridge settles — `true` if the
|
|
79
|
+
* DOM-free tiptap modules loaded, `false` on runtimes that can't host
|
|
80
|
+
* them (JSON calls then no-op / return null). Never fires on web. */
|
|
81
|
+
/** AppKit: fires true with mounted engine readiness; no headless conversion bridge. */
|
|
82
|
+
onJSONReady?: (ready: boolean) => void
|
|
83
|
+
onChange?: (html: string) => void
|
|
84
|
+
onJSONChange?: (doc: TiptapJSON) => void
|
|
85
|
+
onSelectionChange?: (event: { start: number; end: number; active: TiptapFormat[] }) => void
|
|
86
|
+
onFocus?: () => void
|
|
87
|
+
onBlur?: () => void
|
|
88
|
+
ref?: Octane.Ref<TiptapEditorHandle>
|
|
89
|
+
android?: Record<string, any>
|
|
90
|
+
ios?: Record<string, any>
|
|
91
|
+
row?: number | string
|
|
92
|
+
col?: number | string
|
|
93
|
+
rowSpan?: number
|
|
94
|
+
colSpan?: number
|
|
95
|
+
dock?: 'left' | 'top' | 'right' | 'bottom'
|
|
96
|
+
left?: number
|
|
97
|
+
top?: number
|
|
98
|
+
flexGrow?: number
|
|
99
|
+
flexShrink?: number
|
|
100
|
+
alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline'
|
|
101
|
+
order?: number
|
|
102
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { UniversalComponent } from 'octane/universal'
|
|
2
|
+
import type { TiptapEditorProps, TiptapJSON } from './types'
|
|
3
|
+
|
|
4
|
+
export declare const TiptapEditor: UniversalComponent<TiptapEditorProps>
|
|
5
|
+
export declare const supported: boolean
|
|
6
|
+
export declare function ensureJSONBridge(): Promise<boolean>
|
|
7
|
+
export declare function jsonBridgeReady(): boolean
|
|
8
|
+
export type * from './types'
|
|
@@ -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
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { Octane } from 'octane/jsx-runtime';
|
|
2
|
+
/** Format vocabulary shared by the web (tiptap StarterKit) and native
|
|
3
|
+
* (Aztec) backends. On web, starter-kit covers everything except
|
|
4
|
+
* taskList/highlight/subscript/superscript/align* — those no-op there and
|
|
5
|
+
* map to Aztec formats on Android. */
|
|
6
|
+
export type TiptapFormat = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'code' | 'blockquote' | 'bulletList' | 'orderedList' | 'taskList' | 'codeBlock' | 'horizontalRule' | 'highlight' | 'subscript' | 'superscript' | 'paragraph' | 'heading1' | 'heading2' | 'heading3' | 'heading4' | 'heading5' | 'heading6' | 'alignLeft' | 'alignCenter' | 'alignRight' | 'link';
|
|
7
|
+
/** Tiptap document JSON (`{ type: 'doc', content: [...] }`). Opaque here —
|
|
8
|
+
* callers round-trip through tiptap's own schema on both platforms. */
|
|
9
|
+
export type TiptapJSON = {
|
|
10
|
+
type: string;
|
|
11
|
+
content?: unknown[];
|
|
12
|
+
} & Record<string, unknown>;
|
|
13
|
+
export interface TiptapEditorHandle {
|
|
14
|
+
/** AppKit: returns the latest WebKit snapshot; commands are asynchronous. */
|
|
15
|
+
getHTML(): string;
|
|
16
|
+
/** Web: `commands.setContent` (undo-aware). Native: Aztec `fromHtml` —
|
|
17
|
+
* resets undo history. */
|
|
18
|
+
setHTML(html: string): void;
|
|
19
|
+
/** Doc JSON, or null while the native schema modules are still loading /
|
|
20
|
+
* on a runtime that can't host them (`json` flag). Web is always ready. */
|
|
21
|
+
/** AppKit: latest live engine JSON snapshot; null before onReady. */
|
|
22
|
+
getJSON(): TiptapJSON | null;
|
|
23
|
+
setJSON(doc: TiptapJSON): void;
|
|
24
|
+
apply(format: TiptapFormat): void;
|
|
25
|
+
linkTo(url: string, anchor?: string): void;
|
|
26
|
+
removeLink(): void;
|
|
27
|
+
isActive(format: TiptapFormat): boolean;
|
|
28
|
+
undo(): void;
|
|
29
|
+
redo(): void;
|
|
30
|
+
focus(): void;
|
|
31
|
+
blur(): void;
|
|
32
|
+
isFocused(): boolean;
|
|
33
|
+
/** The platform surface — a tiptap `Editor` on web, `AztecText` on
|
|
34
|
+
* Android. */
|
|
35
|
+
/** AppKit: the XplatEditorHost transport, not the browser engine instance. */
|
|
36
|
+
native: any;
|
|
37
|
+
}
|
|
38
|
+
export interface TiptapEditorProps {
|
|
39
|
+
id?: string;
|
|
40
|
+
className?: string;
|
|
41
|
+
style?: any;
|
|
42
|
+
/** Document HTML. Seed at mount; external changes re-push (web: via
|
|
43
|
+
* setContent, preserving the diff loop the editor emitted is skipped). */
|
|
44
|
+
value?: string;
|
|
45
|
+
/** Document JSON. Takes precedence over `value` when both arrive. On
|
|
46
|
+
* native it waits for the schema modules (see `onJSONReady`). */
|
|
47
|
+
json?: TiptapJSON;
|
|
48
|
+
placeholder?: string;
|
|
49
|
+
editable?: boolean;
|
|
50
|
+
autofocus?: boolean;
|
|
51
|
+
onReady?: () => void;
|
|
52
|
+
/** Fires once when the native JSON bridge settles — `true` if the
|
|
53
|
+
* DOM-free tiptap modules loaded, `false` on runtimes that can't host
|
|
54
|
+
* them (JSON calls then no-op / return null). Never fires on web. */
|
|
55
|
+
/** AppKit: fires true with mounted engine readiness; no headless conversion bridge. */
|
|
56
|
+
onJSONReady?: (ready: boolean) => void;
|
|
57
|
+
onChange?: (html: string) => void;
|
|
58
|
+
onJSONChange?: (doc: TiptapJSON) => void;
|
|
59
|
+
onSelectionChange?: (event: {
|
|
60
|
+
start: number;
|
|
61
|
+
end: number;
|
|
62
|
+
active: TiptapFormat[];
|
|
63
|
+
}) => void;
|
|
64
|
+
onFocus?: () => void;
|
|
65
|
+
onBlur?: () => void;
|
|
66
|
+
ref?: Octane.Ref<TiptapEditorHandle>;
|
|
67
|
+
android?: Record<string, any>;
|
|
68
|
+
ios?: Record<string, any>;
|
|
69
|
+
row?: number | string;
|
|
70
|
+
col?: number | string;
|
|
71
|
+
rowSpan?: number;
|
|
72
|
+
colSpan?: number;
|
|
73
|
+
dock?: 'left' | 'top' | 'right' | 'bottom';
|
|
74
|
+
left?: number;
|
|
75
|
+
top?: number;
|
|
76
|
+
flexGrow?: number;
|
|
77
|
+
flexShrink?: number;
|
|
78
|
+
alignSelf?: 'auto' | 'flex-start' | 'flex-end' | 'center' | 'stretch' | 'baseline';
|
|
79
|
+
order?: number;
|
|
80
|
+
}
|
package/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {}
|
package/index.js
DELETED