@byline/richtext-lexical 5.2.0 → 6.0.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/dist/config.d.ts +1 -1
- package/dist/field/apply-value-plugin.js +31 -6
- package/dist/field/capabilities/filter-transformers.d.ts +29 -0
- package/dist/field/capabilities/filter-transformers.js +8 -0
- package/dist/field/capabilities/registered-node-types.d.ts +29 -0
- package/dist/field/capabilities/registered-node-types.js +7 -0
- package/dist/field/config/built-in-extension-names.d.ts +42 -0
- package/dist/field/config/built-in-extension-names.js +12 -2
- package/dist/field/config/default-extensions.js +4 -0
- package/dist/field/config/default.js +8 -8
- package/dist/field/config/editor-config-context.d.ts +2 -2
- package/dist/field/config/editor-config-context.js +10 -13
- package/dist/field/config/resolve-editor-config.js +3 -3
- package/dist/field/config/root-dependencies.d.ts +26 -0
- package/dist/field/config/root-dependencies.js +8 -0
- package/dist/field/config/types.d.ts +61 -8
- package/dist/field/editor-context.js +44 -27
- package/dist/field/editor.js +15 -8
- package/dist/field/extensions/core-nodes/core-nodes-extension.d.ts +24 -0
- package/dist/field/extensions/core-nodes/core-nodes-extension.js +11 -0
- package/dist/field/extensions/heading/heading-extension.d.ts +20 -0
- package/dist/field/extensions/heading/heading-extension.js +9 -0
- package/dist/field/extensions/quote/quote-extension.d.ts +16 -0
- package/dist/field/extensions/quote/quote-extension.js +9 -0
- package/dist/field/hooks/use-markdown-toggle.d.ts +15 -0
- package/dist/field/hooks/use-markdown-toggle.js +12 -4
- package/dist/field/hooks/use-platform-modifier.d.ts +16 -0
- package/dist/field/hooks/use-platform-modifier.js +8 -0
- package/dist/field/markdown/transformers.d.ts +1 -0
- package/dist/field/markdown/transformers.js +5 -4
- package/dist/field/nodes/index.d.ts +11 -1
- package/dist/field/nodes/index.js +2 -2
- package/dist/field/normalize/adapted-notice.css +34 -0
- package/dist/field/normalize/adapted-notice.d.ts +29 -0
- package/dist/field/normalize/adapted-notice.js +35 -0
- package/dist/field/normalize/content-labels.d.ts +15 -0
- package/dist/field/normalize/content-labels.js +19 -0
- package/dist/field/normalize/declared-conversions.d.ts +131 -0
- package/dist/field/normalize/declared-conversions.js +93 -0
- package/dist/field/normalize/normalization-status.d.ts +33 -0
- package/dist/field/normalize/normalization-status.js +28 -0
- package/dist/field/normalize/normalize-value.d.ts +38 -0
- package/dist/field/normalize/normalize-value.js +132 -0
- package/dist/field/normalize/use-notice-text.d.ts +11 -0
- package/dist/field/normalize/use-notice-text.js +34 -0
- package/dist/field/plugins/toolbar-plugin/index.d.ts +23 -0
- package/dist/field/plugins/toolbar-plugin/index.js +104 -78
- package/dist/field/test-support/build-test-editor.js +49 -0
- package/dist/field/test-support/capture-editor.js +7 -0
- package/dist/field/test-support/frame-control.js +40 -0
- package/dist/field/test-support/jsdom-setup.js +21 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +6 -1
- package/dist/scan/capability-manifest.d.ts +30 -0
- package/dist/scan/capability-manifest.js +43 -0
- package/dist/scan/scan-documents.d.ts +29 -0
- package/dist/scan/scan-documents.js +39 -0
- package/dist/scan/types.d.ts +63 -0
- package/dist/scan/types.js +1 -0
- package/dist/scan.d.ts +25 -0
- package/dist/scan.js +1 -0
- package/package.json +14 -5
- package/src/config.ts +1 -1
- package/src/field/apply-value-plugin.tsx +71 -5
- package/src/field/capabilities/filter-transformers.test.tsx +71 -0
- package/src/field/capabilities/filter-transformers.ts +42 -0
- package/src/field/capabilities/markdown-behaviour.test.tsx +188 -0
- package/src/field/capabilities/no-blanket-registration.test.tsx +86 -0
- package/src/field/capabilities/registered-node-types.test.tsx +29 -0
- package/src/field/capabilities/registered-node-types.ts +36 -0
- package/src/field/config/built-in-extension-names.test.node.ts +38 -14
- package/src/field/config/built-in-extension-names.ts +29 -1
- package/src/field/config/default-extensions.ts +6 -0
- package/src/field/config/default.ts +8 -8
- package/src/field/config/editor-config-context.tsx +14 -9
- package/src/field/config/list-removal.test.tsx +44 -0
- package/src/field/config/resolve-editor-config.test.node.ts +27 -6
- package/src/field/config/resolve-editor-config.ts +3 -3
- package/src/field/config/root-dependencies.ts +32 -0
- package/src/field/config/types.ts +61 -17
- package/src/field/editor-component.test.tsx +18 -3
- package/src/field/editor-context.regression.test.tsx +124 -0
- package/src/field/editor-context.tsx +90 -22
- package/src/field/editor.tsx +22 -12
- package/src/field/extensions/core-nodes/core-nodes-extension.test.tsx +13 -0
- package/src/field/extensions/core-nodes/core-nodes-extension.ts +32 -0
- package/src/field/extensions/heading/heading-extension.test.tsx +32 -0
- package/src/field/extensions/heading/heading-extension.ts +27 -0
- package/src/field/extensions/quote/quote-extension.ts +23 -0
- package/src/field/hooks/markdown-source-mode.test.tsx +221 -0
- package/src/field/hooks/use-markdown-toggle.ts +39 -4
- package/src/field/hooks/use-platform-modifier.test.tsx +81 -0
- package/src/field/hooks/use-platform-modifier.ts +41 -0
- package/src/field/markdown/transformers.ts +18 -4
- package/src/field/nodes/index.ts +11 -1
- package/src/field/normalize/adapted-notice.css +52 -0
- package/src/field/normalize/adapted-notice.tsx +63 -0
- package/src/field/normalize/clipboard-equivalence.test.tsx +330 -0
- package/src/field/normalize/content-labels.ts +44 -0
- package/src/field/normalize/declared-conversions.test.node.ts +67 -0
- package/src/field/normalize/declared-conversions.ts +160 -0
- package/src/field/normalize/emission-gate.test.tsx +302 -0
- package/src/field/normalize/normalization-status.tsx +48 -0
- package/src/field/normalize/normalize-load.test.tsx +371 -0
- package/src/field/normalize/normalize-value.test.node.ts +530 -0
- package/src/field/normalize/normalize-value.ts +180 -0
- package/src/field/normalize/notice-i18n.test.tsx +128 -0
- package/src/field/normalize/use-notice-text.ts +82 -0
- package/src/field/normalize/wiring.test.tsx +348 -0
- package/src/field/plugins/toolbar-plugin/block-format-capabilities.test.tsx +57 -0
- package/src/field/plugins/toolbar-plugin/index.tsx +131 -68
- package/src/field/test-support/build-test-editor.test.tsx +17 -0
- package/src/field/test-support/build-test-editor.ts +99 -0
- package/src/field/test-support/capture-editor.tsx +20 -0
- package/src/field/test-support/frame-control.ts +91 -0
- package/src/field/test-support/jsdom-setup.ts +41 -0
- package/src/index.ts +12 -1
- package/src/scan/capability-manifest.test.tsx +82 -0
- package/src/scan/capability-manifest.ts +81 -0
- package/src/scan/scan-documents.test.node.ts +125 -0
- package/src/scan/scan-documents.ts +78 -0
- package/src/scan/types.ts +66 -0
- package/src/scan.ts +27 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { act } from 'react'
|
|
2
|
+
|
|
3
|
+
import { defineAdminConfig } from '@byline/core'
|
|
4
|
+
import { MarkNode } from '@lexical/mark'
|
|
5
|
+
import { OverflowNode } from '@lexical/overflow'
|
|
6
|
+
import { HeadingNode } from '@lexical/rich-text'
|
|
7
|
+
import type { LexicalEditor } from 'lexical'
|
|
8
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
9
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
10
|
+
|
|
11
|
+
import { builtInExtensions } from './config/built-in-extension-names'
|
|
12
|
+
import { defaultClientEditorConfig } from './config/default-extensions'
|
|
13
|
+
import { EditorContext } from './editor-context'
|
|
14
|
+
import { CoreNodesExtension } from './extensions/core-nodes/core-nodes-extension'
|
|
15
|
+
import { READABLE_NODES } from './nodes'
|
|
16
|
+
import { CaptureEditor } from './test-support/capture-editor'
|
|
17
|
+
|
|
18
|
+
// biome-ignore lint/suspicious/noExplicitAny: React act environment flag
|
|
19
|
+
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
|
|
20
|
+
|
|
21
|
+
// The editor's toolbar resolves the admin config unconditionally, so with
|
|
22
|
+
// none registered the mount throws "Byline has not been configured yet".
|
|
23
|
+
// Register a real minimal one rather than mocking `@byline/core` — the
|
|
24
|
+
// same approach `@byline/admin`'s form tests take. It lives on a global,
|
|
25
|
+
// so module scope runs it once.
|
|
26
|
+
defineAdminConfig({
|
|
27
|
+
i18n: {
|
|
28
|
+
admin: { defaultLocale: 'en', locales: ['en'] },
|
|
29
|
+
content: { defaultLocale: 'en', locales: ['en'] },
|
|
30
|
+
},
|
|
31
|
+
collections: [
|
|
32
|
+
{
|
|
33
|
+
path: 'pages',
|
|
34
|
+
labels: { singular: 'Page', plural: 'Pages' },
|
|
35
|
+
fields: [{ name: 'title', label: 'Title', type: 'text' }],
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Mounts the REAL EditorContext. The shared jsdom builders in
|
|
42
|
+
* `test-support/build-test-editor.ts` compose their own root extension
|
|
43
|
+
* and so cannot see a blanket node list living in the shipped
|
|
44
|
+
* component — this test can, which is why the defect is pinned here.
|
|
45
|
+
*/
|
|
46
|
+
/** Every root mounted by this file, unmounted after each test. */
|
|
47
|
+
const mounted: Array<{ root: Root; container: HTMLDivElement }> = []
|
|
48
|
+
|
|
49
|
+
afterEach(async () => {
|
|
50
|
+
// Without this each test leaves a live editor and its listeners
|
|
51
|
+
// attached to the document, which leaks between tests.
|
|
52
|
+
for (const { root, container } of mounted.splice(0)) {
|
|
53
|
+
await act(async () => {
|
|
54
|
+
root.unmount()
|
|
55
|
+
})
|
|
56
|
+
container.remove()
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
async function mountEditor(removals: string[]): Promise<LexicalEditor> {
|
|
61
|
+
const extensions = defaultClientEditorConfig.extensions?.clone()
|
|
62
|
+
for (const name of removals) extensions.remove(name)
|
|
63
|
+
|
|
64
|
+
let editor: LexicalEditor | undefined
|
|
65
|
+
const container = document.createElement('div')
|
|
66
|
+
document.body.appendChild(container)
|
|
67
|
+
const root = createRoot(container)
|
|
68
|
+
mounted.push({ root, container })
|
|
69
|
+
|
|
70
|
+
// Async act: Lexical flushes listeners in a microtask and ToolbarPlugin
|
|
71
|
+
// sets state from them, so a synchronous act() leaves those updates
|
|
72
|
+
// outside its scope and React warns.
|
|
73
|
+
await act(async () => {
|
|
74
|
+
root.render(
|
|
75
|
+
<EditorContext
|
|
76
|
+
composerKey="regression"
|
|
77
|
+
editorConfig={{ ...defaultClientEditorConfig, extensions }}
|
|
78
|
+
onChange={() => {}}
|
|
79
|
+
readOnly={false}
|
|
80
|
+
>
|
|
81
|
+
<CaptureEditor
|
|
82
|
+
onEditor={(instance) => {
|
|
83
|
+
editor = instance
|
|
84
|
+
}}
|
|
85
|
+
/>
|
|
86
|
+
</EditorContext>
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
if (editor == null) throw new Error('editor was never captured')
|
|
91
|
+
return editor
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe('EditorContext registers only what its extensions own', () => {
|
|
95
|
+
it('does not register headings when the heading extension is removed', async () => {
|
|
96
|
+
const editor = await mountEditor([builtInExtensions.Heading])
|
|
97
|
+
expect(editor.hasNode(HeadingNode)).toBe(false)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('still registers headings with the default configuration', async () => {
|
|
101
|
+
const editor = await mountEditor([])
|
|
102
|
+
expect(editor.hasNode(HeadingNode)).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('registers every readable node class with the default configuration', async () => {
|
|
106
|
+
const editor = await mountEditor([])
|
|
107
|
+
// The former blanket list, now the read vocabulary. Every class in it
|
|
108
|
+
// must still be reachable through an extension, or the default
|
|
109
|
+
// configuration has silently lost a feature.
|
|
110
|
+
const missing = READABLE_NODES.filter((klass) => !editor.hasNode(klass)).map((klass) =>
|
|
111
|
+
klass.getType()
|
|
112
|
+
)
|
|
113
|
+
expect(missing).toEqual([])
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('keeps the core nodes even when site code tries to remove them', async () => {
|
|
117
|
+
// `remove()` matches by name and accepts the extension object, so
|
|
118
|
+
// this is a removal a site could really attempt. The root injects
|
|
119
|
+
// CoreNodesExtension outside the configurable list, so it survives.
|
|
120
|
+
const editor = await mountEditor([CoreNodesExtension.name])
|
|
121
|
+
expect(editor.hasNode(MarkNode)).toBe(true)
|
|
122
|
+
expect(editor.hasNode(OverflowNode)).toBe(true)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -22,12 +22,17 @@ import {
|
|
|
22
22
|
|
|
23
23
|
import { defaultExtensionsList } from './config/default-extensions'
|
|
24
24
|
import { EditorConfigContext } from './config/editor-config-context'
|
|
25
|
+
import { rootDependencies } from './config/root-dependencies'
|
|
25
26
|
import { MarkdownModeProvider } from './context/markdown-mode-context'
|
|
26
27
|
import { SharedHistoryContext } from './context/shared-history-context'
|
|
27
28
|
import { SharedOnChangeContext } from './context/shared-on-change-context'
|
|
28
29
|
import { Editor } from './editor'
|
|
29
30
|
import { InlineImageExtension } from './extensions/inline-image/inline-image-extension'
|
|
30
|
-
import {
|
|
31
|
+
import { AdaptedNotice, UnsupportedContentNotice } from './normalize/adapted-notice'
|
|
32
|
+
import {
|
|
33
|
+
NormalizationStatusProvider,
|
|
34
|
+
useNormalizationStatus,
|
|
35
|
+
} from './normalize/normalization-status'
|
|
31
36
|
import type { EditorConfig } from './config/types'
|
|
32
37
|
|
|
33
38
|
// Catch any errors that occur during Lexical updates and log them
|
|
@@ -92,15 +97,30 @@ export function EditorContext(props: {
|
|
|
92
97
|
collection: editorConfig.settings.inlineImageUploadCollection,
|
|
93
98
|
})
|
|
94
99
|
}
|
|
95
|
-
|
|
100
|
+
// Optional features plus mandatory infrastructure. See
|
|
101
|
+
// `rootDependencies` for why the core nodes are injected here rather
|
|
102
|
+
// than carried in the configurable list.
|
|
103
|
+
const dependencies = rootDependencies(configured)
|
|
96
104
|
|
|
97
105
|
return defineExtension({
|
|
98
106
|
name: '[root]',
|
|
99
107
|
namespace: editorConfig.lexical.namespace,
|
|
100
|
-
nodes
|
|
108
|
+
// No `nodes` here on purpose. Every node class is owned by the
|
|
109
|
+
// extension that provides its feature, so removing an extension
|
|
110
|
+
// removes its controls, its behaviour AND its node registration.
|
|
111
|
+
// A blanket list here would let a field accept structures it does
|
|
112
|
+
// not offer — a field configured without headings would still
|
|
113
|
+
// store a heading pasted into it.
|
|
101
114
|
theme: editorConfig.lexical.theme,
|
|
102
115
|
editable,
|
|
103
|
-
|
|
116
|
+
// No `$initialEditorState`. It is evaluated while the editor is
|
|
117
|
+
// being built, before the registered node types exist, so an
|
|
118
|
+
// unnormalized value would throw inside the builder with no chance
|
|
119
|
+
// to adapt it — and its function form runs inside an update, where
|
|
120
|
+
// `setEditorState` is unsafe (see apply-value-plugin.tsx). The
|
|
121
|
+
// value arrives through ApplyValuePlugin instead, which normalizes
|
|
122
|
+
// against the built editor's real capabilities and applies in a
|
|
123
|
+
// layout effect so nothing paints empty first.
|
|
104
124
|
onError: (error: Error) => {
|
|
105
125
|
throw error
|
|
106
126
|
},
|
|
@@ -123,24 +143,72 @@ export function EditorContext(props: {
|
|
|
123
143
|
// back inside their React tree.
|
|
124
144
|
return (
|
|
125
145
|
<EditorConfigContext config={editorConfig.settings}>
|
|
126
|
-
<
|
|
127
|
-
<
|
|
128
|
-
<
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
<NormalizationStatusProvider>
|
|
147
|
+
<SharedOnChangeContext onChange={onChange}>
|
|
148
|
+
<SharedHistoryContext>
|
|
149
|
+
<MarkdownModeProvider>
|
|
150
|
+
<LexicalExtensionComposer
|
|
151
|
+
extension={rootExtension}
|
|
152
|
+
contentEditable={null}
|
|
153
|
+
key={composerKey + editable}
|
|
154
|
+
>
|
|
155
|
+
<EditorShell
|
|
156
|
+
beforeEditor={beforeEditor}
|
|
157
|
+
afterEditor={afterEditor}
|
|
158
|
+
minHeight={props.minHeight}
|
|
159
|
+
maxHeight={props.maxHeight}
|
|
160
|
+
>
|
|
161
|
+
{children}
|
|
162
|
+
</EditorShell>
|
|
163
|
+
</LexicalExtensionComposer>
|
|
164
|
+
</MarkdownModeProvider>
|
|
165
|
+
</SharedHistoryContext>
|
|
166
|
+
</SharedOnChangeContext>
|
|
167
|
+
</NormalizationStatusProvider>
|
|
144
168
|
</EditorConfigContext>
|
|
145
169
|
)
|
|
146
170
|
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The editor surface, or the read-only notice in its place.
|
|
174
|
+
*
|
|
175
|
+
* Content with no safe conversion is not adapted and not discarded: the
|
|
176
|
+
* field declines to edit it and says why. Sits inside the composer so it
|
|
177
|
+
* can read the status the apply plugin publishes.
|
|
178
|
+
*/
|
|
179
|
+
function EditorShell({
|
|
180
|
+
beforeEditor,
|
|
181
|
+
afterEditor,
|
|
182
|
+
minHeight,
|
|
183
|
+
maxHeight,
|
|
184
|
+
children,
|
|
185
|
+
}: {
|
|
186
|
+
beforeEditor?: React.ReactNode[]
|
|
187
|
+
afterEditor?: React.ReactNode[]
|
|
188
|
+
minHeight?: number | string
|
|
189
|
+
maxHeight?: number | string
|
|
190
|
+
children?: React.ReactNode
|
|
191
|
+
}): React.JSX.Element {
|
|
192
|
+
const { status } = useNormalizationStatus()
|
|
193
|
+
|
|
194
|
+
// `children` carries ApplyValuePlugin, so it stays mounted in BOTH
|
|
195
|
+
// branches. Unmounting it on refusal would strand the field: the plugin
|
|
196
|
+
// that applies values would be gone, so a later valid value could
|
|
197
|
+
// neither load nor clear the notice, and the only escape would be a
|
|
198
|
+
// full remount.
|
|
199
|
+
return (
|
|
200
|
+
<div className="editor-shell">
|
|
201
|
+
{status.kind === 'refused' ? (
|
|
202
|
+
<UnsupportedContentNotice unsupportedTypes={status.unsupportedTypes} />
|
|
203
|
+
) : (
|
|
204
|
+
<>
|
|
205
|
+
{status.kind === 'adapted' && <AdaptedNotice />}
|
|
206
|
+
{beforeEditor}
|
|
207
|
+
<Editor minHeight={minHeight} maxHeight={maxHeight} />
|
|
208
|
+
{afterEditor}
|
|
209
|
+
</>
|
|
210
|
+
)}
|
|
211
|
+
{children}
|
|
212
|
+
</div>
|
|
213
|
+
)
|
|
214
|
+
}
|
package/src/field/editor.tsx
CHANGED
|
@@ -11,18 +11,18 @@
|
|
|
11
11
|
import type * as React from 'react'
|
|
12
12
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
|
13
13
|
|
|
14
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
|
14
15
|
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'
|
|
15
16
|
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
|
|
16
17
|
import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPlugin'
|
|
17
18
|
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
|
|
18
19
|
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin'
|
|
19
20
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
|
|
20
|
-
import {
|
|
21
|
-
useExtensionDependency,
|
|
22
|
-
useOptionalExtensionDependency,
|
|
23
|
-
} from '@lexical/react/useExtensionComponent'
|
|
21
|
+
import { useExtensionDependency } from '@lexical/react/useExtensionComponent'
|
|
24
22
|
import type { EditorState, LexicalEditor } from 'lexical'
|
|
25
23
|
|
|
24
|
+
import { transformersFor } from './capabilities/filter-transformers'
|
|
25
|
+
import { supportsNodeType } from './capabilities/registered-node-types'
|
|
26
26
|
import { useEditorConfig } from './config/editor-config-context'
|
|
27
27
|
import { ContentEditable } from './content-editable'
|
|
28
28
|
import { useMarkdownMode } from './context/markdown-mode-context'
|
|
@@ -33,7 +33,6 @@ import {
|
|
|
33
33
|
BylineFloatingUIExtension,
|
|
34
34
|
selectFloatingUIItems,
|
|
35
35
|
} from './extensions/byline-floating-ui/byline-floating-ui-extension'
|
|
36
|
-
import { TableExtension as BylineTableExtension } from './extensions/table/table-extension'
|
|
37
36
|
import { BYLINE_TRANSFORMERS } from './markdown/transformers'
|
|
38
37
|
// import { AiPlugin } from './plugins/ai-plugin'
|
|
39
38
|
// import { DragDropPaste } from './plugins/drag-drop-paste-plugin'
|
|
@@ -67,11 +66,24 @@ export const Editor = memo(function Editor({
|
|
|
67
66
|
const { markdownModeRef } = useMarkdownMode()
|
|
68
67
|
const {
|
|
69
68
|
config: {
|
|
70
|
-
|
|
69
|
+
mode,
|
|
70
|
+
markdownShortcuts,
|
|
71
|
+
controls: { treeView },
|
|
72
|
+
debug,
|
|
71
73
|
placeholderText,
|
|
72
74
|
},
|
|
73
75
|
} = useEditorConfig()
|
|
74
|
-
const
|
|
76
|
+
const richText = mode === 'richText'
|
|
77
|
+
const [editor] = useLexicalComposerContext()
|
|
78
|
+
// Table availability is structural, so it follows the registered node
|
|
79
|
+
// rather than extension-list membership: TableNode arrives through
|
|
80
|
+
// `@lexical/table` as a dependency of the Byline wrapper, and a
|
|
81
|
+
// membership test would miss any other route to the same node.
|
|
82
|
+
const hasTableNode = supportsNodeType(editor, 'table')
|
|
83
|
+
// Markdown shortcuts create nodes, so the transformer list must follow
|
|
84
|
+
// what this editor registered — an unregistered dependency throws for
|
|
85
|
+
// the whole pipeline, not just its own transformer.
|
|
86
|
+
const activeTransformers = useMemo(() => transformersFor(editor, BYLINE_TRANSFORMERS), [editor])
|
|
75
87
|
// Merged floating-UI contributions from every extension in the graph.
|
|
76
88
|
// Removing a contributing extension (e.g. `c.extensions.remove(LinkExtension)`)
|
|
77
89
|
// automatically suppresses its floating UI — no separate boolean toggle.
|
|
@@ -132,10 +144,10 @@ export const Editor = memo(function Editor({
|
|
|
132
144
|
|
|
133
145
|
const content = (
|
|
134
146
|
<>
|
|
135
|
-
{
|
|
147
|
+
{hasTableNode && <TablePlugin />}
|
|
136
148
|
{richText && <ToolbarPlugin />}
|
|
137
149
|
<div
|
|
138
|
-
className={`editor-container ${
|
|
150
|
+
className={`editor-container ${treeView ? 'tree-view' : ''} ${
|
|
139
151
|
!richText ? 'plain-text' : ''
|
|
140
152
|
}`}
|
|
141
153
|
>
|
|
@@ -166,9 +178,7 @@ export const Editor = memo(function Editor({
|
|
|
166
178
|
ErrorBoundary={LexicalErrorBoundary}
|
|
167
179
|
/>
|
|
168
180
|
<HistoryPlugin externalHistoryState={historyState} />
|
|
169
|
-
{
|
|
170
|
-
<MarkdownShortcutPlugin transformers={BYLINE_TRANSFORMERS} />
|
|
171
|
-
)}
|
|
181
|
+
{markdownShortcuts && <MarkdownShortcutPlugin transformers={activeTransformers} />}
|
|
172
182
|
{floatingAnchorElem != null &&
|
|
173
183
|
!isSmallWidthViewport &&
|
|
174
184
|
floatingUIItems.map(({ id, Component }) => (
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { supportsNodeType } from '../../capabilities/registered-node-types'
|
|
4
|
+
import { buildRestrictedEditor } from '../../test-support/build-test-editor'
|
|
5
|
+
|
|
6
|
+
describe('CoreNodesExtension', () => {
|
|
7
|
+
it('keeps mark and overflow registered even in a fully stripped editor', () => {
|
|
8
|
+
const editor = buildRestrictedEditor()
|
|
9
|
+
expect(supportsNodeType(editor, 'mark')).toBe(true)
|
|
10
|
+
expect(supportsNodeType(editor, 'overflow')).toBe(true)
|
|
11
|
+
editor.dispose()
|
|
12
|
+
})
|
|
13
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { MarkNode } from '@lexical/mark'
|
|
10
|
+
import { OverflowNode } from '@lexical/overflow'
|
|
11
|
+
import { defineExtension } from 'lexical'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Owns the node classes that belong to no switchable feature.
|
|
15
|
+
*
|
|
16
|
+
* `MarkNode` and `OverflowNode` ship without an upstream extension, and
|
|
17
|
+
* neither corresponds to something a field can turn off. Without an
|
|
18
|
+
* owner both would lose their only registration once the editor root
|
|
19
|
+
* stopped registering a blanket node list.
|
|
20
|
+
*
|
|
21
|
+
* `EditorContext` injects this extension directly into the root's
|
|
22
|
+
* dependencies, outside `editorConfig.extensions`. Leaving it out of
|
|
23
|
+
* `builtInExtensions` would NOT have made it non-removable: this module
|
|
24
|
+
* is publicly exported and `ExtensionsList.remove()` accepts an
|
|
25
|
+
* extension object as readily as a name string, so site code could have
|
|
26
|
+
* removed it from the configurable list. Injection at the root is what
|
|
27
|
+
* enforces its presence.
|
|
28
|
+
*/
|
|
29
|
+
export const CoreNodesExtension = defineExtension({
|
|
30
|
+
name: '@byline/richtext-lexical/CoreNodes',
|
|
31
|
+
nodes: () => [MarkNode, OverflowNode],
|
|
32
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { builtInExtensions } from '../../config/built-in-extension-names'
|
|
4
|
+
import {
|
|
5
|
+
buildFullEditor,
|
|
6
|
+
buildRestrictedEditor,
|
|
7
|
+
parseHtmlToTypes,
|
|
8
|
+
} from '../../test-support/build-test-editor'
|
|
9
|
+
|
|
10
|
+
describe('heading and quote ownership', () => {
|
|
11
|
+
it('imports headings and quotes when both extensions are present', () => {
|
|
12
|
+
const editor = buildFullEditor()
|
|
13
|
+
expect(parseHtmlToTypes(editor, '<h1>Title</h1>').types).toEqual(['heading'])
|
|
14
|
+
expect(parseHtmlToTypes(editor, '<blockquote>Quoted</blockquote>').types).toEqual(['quote'])
|
|
15
|
+
editor.dispose()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('degrades a pasted heading to a paragraph and keeps the text', () => {
|
|
19
|
+
const editor = buildRestrictedEditor([builtInExtensions.Heading])
|
|
20
|
+
const result = parseHtmlToTypes(editor, '<h1>Pasted title</h1>')
|
|
21
|
+
expect(result.types).toEqual(['paragraph'])
|
|
22
|
+
expect(result.text).toBe('Pasted title')
|
|
23
|
+
editor.dispose()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('keeps headings while quotes are removed', () => {
|
|
27
|
+
const editor = buildRestrictedEditor([builtInExtensions.Quote])
|
|
28
|
+
expect(parseHtmlToTypes(editor, '<h1>Title</h1>').types).toEqual(['heading'])
|
|
29
|
+
expect(parseHtmlToTypes(editor, '<blockquote>Quoted</blockquote>').types).toEqual(['paragraph'])
|
|
30
|
+
editor.dispose()
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { HeadingNode } from '@lexical/rich-text'
|
|
10
|
+
import { defineExtension } from 'lexical'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Owns `HeadingNode`.
|
|
14
|
+
*
|
|
15
|
+
* Byline deliberately does not adopt `@lexical/rich-text`'s
|
|
16
|
+
* `RichTextExtension` for this. That extension binds heading and quote
|
|
17
|
+
* registration to general rich-text behaviour, so adopting it would
|
|
18
|
+
* register headings in every rich-text field — and a field configured
|
|
19
|
+
* without headings would still accept one from a paste, which is the
|
|
20
|
+
* defect this package had. Owning the node here keeps headings
|
|
21
|
+
* removable on their own, independently of quotes and of rich-text
|
|
22
|
+
* behaviour itself.
|
|
23
|
+
*/
|
|
24
|
+
export const HeadingExtension = defineExtension({
|
|
25
|
+
name: '@byline/richtext-lexical/Heading',
|
|
26
|
+
nodes: () => [HeadingNode],
|
|
27
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { QuoteNode } from '@lexical/rich-text'
|
|
10
|
+
import { defineExtension } from 'lexical'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Owns `QuoteNode`.
|
|
14
|
+
*
|
|
15
|
+
* Separate from {@link HeadingExtension} on purpose: a field may want
|
|
16
|
+
* headings without block quotes, or neither. See the heading extension
|
|
17
|
+
* for why Byline owns these nodes rather than adopting
|
|
18
|
+
* `RichTextExtension`.
|
|
19
|
+
*/
|
|
20
|
+
export const QuoteExtension = defineExtension({
|
|
21
|
+
name: '@byline/richtext-lexical/Quote',
|
|
22
|
+
nodes: () => [QuoteNode],
|
|
23
|
+
})
|