@octane-xplat/lexical 0.10.0 → 0.11.1
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 +93 -7
- package/dist/macos/browser.js +1 -1
- package/dist/native/index3.js +38 -21
- package/dist/native/octane-client-build.json +1 -1
- package/dist/web/index2.js +24 -21
- package/dist/web/octane-client-build.json +1 -1
- package/package.json +7 -5
- package/src/LexicalEditor.mobile.test.ts +229 -0
- package/src/LexicalEditor.tsrx +52 -20
- package/src/LexicalEditor.web.test.tsrx +111 -0
- package/src/LexicalEditor.web.tsrx +19 -12
- package/src/types.ts +32 -6
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, expect, it, vi } from 'vitest'
|
|
3
|
+
import { act, createRoot } from 'octane'
|
|
4
|
+
import { useLexicalComposerContext } from '@octanejs/lexical/LexicalComposerContext'
|
|
5
|
+
import { LexicalEditor } from './LexicalEditor.web.tsrx'
|
|
6
|
+
import type { LexicalEditorHandle } from './types'
|
|
7
|
+
import { BadgeNode } from '../tests/browser/BadgeNode'
|
|
8
|
+
|
|
9
|
+
const roots: any[] = []
|
|
10
|
+
function UpdatePlugin() {
|
|
11
|
+
useLexicalComposerContext()
|
|
12
|
+
return <span data-plugin-mounted="true" />
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function mount(props: any = {}) {
|
|
16
|
+
const el = document.createElement('div')
|
|
17
|
+
document.body.append(el)
|
|
18
|
+
const root = createRoot(el)
|
|
19
|
+
roots.push(root)
|
|
20
|
+
let handle: LexicalEditorHandle | null = null
|
|
21
|
+
const render = (next: any) =>
|
|
22
|
+
act(() =>
|
|
23
|
+
root.render(
|
|
24
|
+
<LexicalEditor
|
|
25
|
+
{...next}
|
|
26
|
+
ref={(value: LexicalEditorHandle | null) => {
|
|
27
|
+
handle = value
|
|
28
|
+
}}
|
|
29
|
+
/>,
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
render(props)
|
|
33
|
+
return { el, root, render, get handle() { return handle } }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
for (const root of roots.splice(0)) act(() => root.unmount())
|
|
38
|
+
document.body.innerHTML = ''
|
|
39
|
+
vi.restoreAllMocks()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('reports the live JSON and editor handles once when ready', () => {
|
|
43
|
+
const onJSONReady = vi.fn()
|
|
44
|
+
const onReady = vi.fn()
|
|
45
|
+
const { handle } = mount({ value: '<p>Travel notes</p>', onJSONReady, onReady })
|
|
46
|
+
|
|
47
|
+
expect(handle).not.toBeNull()
|
|
48
|
+
expect(handle!.getJSON()).not.toBeNull()
|
|
49
|
+
expect(onJSONReady).toHaveBeenCalledExactlyOnceWith(true)
|
|
50
|
+
expect(onReady).toHaveBeenCalledTimes(1)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('wires focus and blur callbacks from the real editable element', () => {
|
|
54
|
+
const onFocus = vi.fn()
|
|
55
|
+
const onBlur = vi.fn()
|
|
56
|
+
const { el } = mount({ onFocus, onBlur })
|
|
57
|
+
const editable = el.querySelector('[contenteditable]') as HTMLElement
|
|
58
|
+
|
|
59
|
+
editable.focus()
|
|
60
|
+
expect(onFocus).toHaveBeenCalledTimes(1)
|
|
61
|
+
editable.blur()
|
|
62
|
+
expect(onBlur).toHaveBeenCalledTimes(1)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('accepts controlled HTML updates and read-only changes', () => {
|
|
66
|
+
const mounted = mount({ value: '<p>First</p>', editable: false })
|
|
67
|
+
const editable = mounted.el.querySelector('[contenteditable]') as HTMLElement
|
|
68
|
+
expect(editable.getAttribute('contenteditable')).toBe('false')
|
|
69
|
+
|
|
70
|
+
mounted.render({ value: '<p>Second</p>', editable: true })
|
|
71
|
+
expect(mounted.handle!.getHTML()).toContain('Second')
|
|
72
|
+
expect(
|
|
73
|
+
(mounted.el.querySelector('[contenteditable]') as HTMLElement).getAttribute('contenteditable'),
|
|
74
|
+
).toBe('true')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('adds custom nodes to defaults and round-trips their JSON and HTML', () => {
|
|
78
|
+
const { handle } = mount({
|
|
79
|
+
value: '<p><span data-badge>Voyager</span></p><h1>Default heading</h1>',
|
|
80
|
+
web: { nodes: [BadgeNode] },
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const json = handle!.getJSON()!
|
|
84
|
+
expect(JSON.stringify(json)).toContain('"type":"badge"')
|
|
85
|
+
expect(handle!.getHTML()).toContain('data-badge')
|
|
86
|
+
expect(handle!.getHTML()).toContain('<h1><span')
|
|
87
|
+
|
|
88
|
+
handle!.setJSON(json)
|
|
89
|
+
expect(handle!.getHTML()).toContain('Voyager')
|
|
90
|
+
expect(handle!.getHTML()).toContain('data-badge')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('renders custom plugins inside the composer context', () => {
|
|
94
|
+
const { el } = mount({ web: { plugins: <UpdatePlugin /> } })
|
|
95
|
+
expect(el.querySelector('[data-plugin-mounted]')).not.toBeNull()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('can replace the default custom-node registry', () => {
|
|
99
|
+
const { handle } = mount({
|
|
100
|
+
value: '<p><span data-badge>Caller schema</span></p>',
|
|
101
|
+
web: {
|
|
102
|
+
replaceNodes: true,
|
|
103
|
+
nodes: [BadgeNode],
|
|
104
|
+
replacePlugins: true,
|
|
105
|
+
plugins: <UpdatePlugin />,
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
expect(handle).not.toBeNull()
|
|
109
|
+
expect((handle!.native as any).getRegisteredNode(BadgeNode)).toBeTruthy()
|
|
110
|
+
expect(handle!.getJSON()!.root).toBeTruthy()
|
|
111
|
+
})
|
|
@@ -46,9 +46,8 @@ import type { LexicalEditorHandle, LexicalEditorProps, LexicalFormat } from './t
|
|
|
46
46
|
|
|
47
47
|
export const supported = true
|
|
48
48
|
|
|
49
|
-
/** The facade's
|
|
50
|
-
* AutoLinkNode (parsing safety for docs produced by fuller editors).
|
|
51
|
-
* needing custom nodes or plugins import @octanejs/lexical directly. */
|
|
49
|
+
/** The facade's default node set — Aztec's capability surface plus
|
|
50
|
+
* AutoLinkNode (parsing safety for docs produced by fuller editors). */
|
|
52
51
|
const NODES = [
|
|
53
52
|
HeadingNode,
|
|
54
53
|
QuoteNode,
|
|
@@ -241,10 +240,8 @@ function activeFormats(editor: any): LexicalFormat[] {
|
|
|
241
240
|
return out
|
|
242
241
|
}
|
|
243
242
|
|
|
244
|
-
/** Unified editor facade, web half — a
|
|
245
|
-
*
|
|
246
|
-
* purpose: the facade's contract is the native-compatible vocabulary, and
|
|
247
|
-
* apps that need arbitrary plugins use the upstream package directly. */
|
|
243
|
+
/** Unified editor facade, web half — a LexicalComposer over the published
|
|
244
|
+
* `@octanejs/lexical` binding, with optional web-only node and plugin composition. */
|
|
248
245
|
export function LexicalEditor(props: LexicalEditorProps) @{
|
|
249
246
|
const editorRef = useRef<any>(null)
|
|
250
247
|
const lastEmitted = useRef<string | undefined>(undefined)
|
|
@@ -257,7 +254,9 @@ export function LexicalEditor(props: LexicalEditorProps) @{
|
|
|
257
254
|
|
|
258
255
|
const initialConfig = {
|
|
259
256
|
namespace: 'octane-xplat/lexical',
|
|
260
|
-
nodes:
|
|
257
|
+
nodes: props.web?.replaceNodes
|
|
258
|
+
? props.web.nodes ?? []
|
|
259
|
+
: [...new Set([...NODES, ...(props.web?.nodes ?? [])])],
|
|
261
260
|
editable: props.editable !== false,
|
|
262
261
|
editorState: props.json != null
|
|
263
262
|
? JSON.stringify(props.json)
|
|
@@ -317,6 +316,7 @@ export function LexicalEditor(props: LexicalEditorProps) @{
|
|
|
317
316
|
}
|
|
318
317
|
|
|
319
318
|
setHandle(bound.current!)
|
|
319
|
+
props.onJSONReady?.(true)
|
|
320
320
|
props.onReady?.()
|
|
321
321
|
}
|
|
322
322
|
|
|
@@ -376,6 +376,8 @@ export function LexicalEditor(props: LexicalEditorProps) @{
|
|
|
376
376
|
placeholder={null}
|
|
377
377
|
className="vx-lexical-input"
|
|
378
378
|
style={{ outline: 'none' }}
|
|
379
|
+
onFocus={props.onFocus}
|
|
380
|
+
onBlur={props.onBlur}
|
|
379
381
|
/>
|
|
380
382
|
}
|
|
381
383
|
placeholder={
|
|
@@ -395,12 +397,17 @@ export function LexicalEditor(props: LexicalEditorProps) @{
|
|
|
395
397
|
}
|
|
396
398
|
ErrorBoundary={LexicalErrorBoundary}
|
|
397
399
|
/>
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
400
|
+
{!props.web?.replacePlugins ? (
|
|
401
|
+
<>
|
|
402
|
+
<HistoryPlugin />
|
|
403
|
+
<ListPlugin />
|
|
404
|
+
<LinkPlugin />
|
|
405
|
+
{props.autofocus ? <AutoFocusPlugin /> : null}
|
|
406
|
+
</>
|
|
407
|
+
) : null}
|
|
402
408
|
<OnChangePlugin onChange={emitChange} />
|
|
403
409
|
<EditorRefPlugin editorRef={attach} />
|
|
410
|
+
{props.web?.plugins}
|
|
404
411
|
</LexicalComposer>
|
|
405
412
|
</div>
|
|
406
413
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Octane } from 'octane/jsx-runtime'
|
|
2
|
+
import type { OctaneNode } from 'octane'
|
|
3
|
+
import type { Klass, LexicalNode, LexicalNodeReplacement } from 'lexical'
|
|
2
4
|
|
|
3
5
|
/** Format vocabulary shared by the web (lexical core + registered nodes)
|
|
4
6
|
* and native (Aztec) backends. On web, `taskList`/`highlight`/`subscript`/
|
|
@@ -37,6 +39,27 @@ export type LexicalFormat =
|
|
|
37
39
|
* lexical's own schema on both platforms. */
|
|
38
40
|
export type LexicalJSON = { root: Record<string, unknown> } & Record<string, unknown>
|
|
39
41
|
|
|
42
|
+
/** Web-only extensions for the live Lexical composer. These options are ignored
|
|
43
|
+
* by native backends; caller plugins, custom nodes, transforms, and browser
|
|
44
|
+
* views require a separate native implementation. */
|
|
45
|
+
export interface LexicalWebOptions {
|
|
46
|
+
/** Additional Lexical node classes or replacements. By default these are
|
|
47
|
+
* added to the facade's built-in node set. Set `replaceNodes` to use this
|
|
48
|
+
* list as the complete custom node registry instead. */
|
|
49
|
+
nodes?: readonly (Klass<LexicalNode> | LexicalNodeReplacement)[]
|
|
50
|
+
/** Replace the facade's built-in custom node registry with `nodes`. Core
|
|
51
|
+
* Lexical nodes remain managed by Lexical. Defaults to `false`. */
|
|
52
|
+
replaceNodes?: boolean
|
|
53
|
+
/** Replace the default history, list, link, and autofocus plugins with the
|
|
54
|
+
* plugin components supplied in `plugins`. Defaults to `false`. The rich
|
|
55
|
+
* text surface and facade change/ref bindings remain installed. */
|
|
56
|
+
replacePlugins?: boolean
|
|
57
|
+
/** Additional Octane plugin components rendered inside the Lexical composer,
|
|
58
|
+
* or the replacement plugin composition when `replacePlugins` is `true`.
|
|
59
|
+
* Use a fragment when composing more than one plugin. */
|
|
60
|
+
plugins?: OctaneNode
|
|
61
|
+
}
|
|
62
|
+
|
|
40
63
|
export interface LexicalEditorHandle {
|
|
41
64
|
/** AppKit: returns the latest WebKit snapshot; commands are asynchronous. */
|
|
42
65
|
getHTML(): string
|
|
@@ -71,23 +94,26 @@ export interface LexicalEditorProps {
|
|
|
71
94
|
/** Document HTML. Seed at mount; external changes re-push. */
|
|
72
95
|
value?: string
|
|
73
96
|
/** Serialized editor state. Takes precedence over `value` when both
|
|
74
|
-
* arrive. On native it waits for the doc-model modules
|
|
75
|
-
* `onJSONReady`)
|
|
97
|
+
* arrive. On native it waits for the doc-model modules and host
|
|
98
|
+
* readiness (see `onJSONReady`); a doc that arrives early is applied
|
|
99
|
+
* once both are up rather than dropped. */
|
|
76
100
|
json?: LexicalJSON
|
|
77
101
|
placeholder?: string
|
|
78
102
|
editable?: boolean
|
|
79
103
|
autofocus?: boolean
|
|
80
104
|
onReady?: () => void
|
|
81
|
-
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
|
|
105
|
+
/** Reports when serialized JSON access is available. Web and AppKit report
|
|
106
|
+
* `true` when their live editor handle exists. Native reports whether its
|
|
107
|
+
* lazy DOM-free conversion bridge loaded; unsupported runtimes report
|
|
108
|
+
* `false` and JSON getters return `null`. */
|
|
85
109
|
onJSONReady?: (ready: boolean) => void
|
|
86
110
|
onChange?: (html: string) => void
|
|
87
111
|
onJSONChange?: (doc: LexicalJSON) => void
|
|
88
112
|
onSelectionChange?: (event: { start: number; end: number; active: LexicalFormat[] }) => void
|
|
89
113
|
onFocus?: () => void
|
|
90
114
|
onBlur?: () => void
|
|
115
|
+
/** Web-only Lexical node and plugin composition. Ignored by native backends. */
|
|
116
|
+
web?: LexicalWebOptions
|
|
91
117
|
ref?: Octane.Ref<LexicalEditorHandle>
|
|
92
118
|
android?: Record<string, any>
|
|
93
119
|
ios?: Record<string, any>
|