@byline/richtext-lexical 5.2.0 → 6.0.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/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
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
INSERT_CHECK_LIST_COMMAND,
|
|
33
33
|
INSERT_ORDERED_LIST_COMMAND,
|
|
34
34
|
INSERT_UNORDERED_LIST_COMMAND,
|
|
35
|
-
|
|
35
|
+
ListItemNode,
|
|
36
36
|
ListNode,
|
|
37
37
|
REMOVE_LIST_COMMAND,
|
|
38
38
|
} from '@lexical/list'
|
|
@@ -47,7 +47,9 @@ import {
|
|
|
47
47
|
$createQuoteNode,
|
|
48
48
|
$isHeadingNode,
|
|
49
49
|
$isQuoteNode,
|
|
50
|
+
HeadingNode,
|
|
50
51
|
type HeadingTagType,
|
|
52
|
+
QuoteNode,
|
|
51
53
|
} from '@lexical/rich-text'
|
|
52
54
|
import { $setBlocksType } from '@lexical/selection'
|
|
53
55
|
import { $isTableNode } from '@lexical/table'
|
|
@@ -92,8 +94,8 @@ import {
|
|
|
92
94
|
OPEN_LINK_MODAL_COMMAND,
|
|
93
95
|
TOGGLE_LINK_COMMAND,
|
|
94
96
|
} from '../../extensions/link'
|
|
95
|
-
import { useMarkdownToggle } from '../../hooks/use-markdown-toggle'
|
|
96
|
-
import {
|
|
97
|
+
import { canUseMarkdownSourceMode, useMarkdownToggle } from '../../hooks/use-markdown-toggle'
|
|
98
|
+
import { useIsApplePlatform } from '../../hooks/use-platform-modifier'
|
|
97
99
|
import { DropDown, DropDownItem } from '../../ui/dropdown'
|
|
98
100
|
import { getSelectedNode } from '../../utils/getSelectedNode'
|
|
99
101
|
import { sanitizeUrl } from '../../utils/url'
|
|
@@ -136,6 +138,53 @@ function dropDownActiveClass(active: boolean): string {
|
|
|
136
138
|
return ''
|
|
137
139
|
}
|
|
138
140
|
|
|
141
|
+
export type BlockFormat =
|
|
142
|
+
| 'paragraph'
|
|
143
|
+
| 'h1'
|
|
144
|
+
| 'h2'
|
|
145
|
+
| 'h3'
|
|
146
|
+
| 'h4'
|
|
147
|
+
| 'bullet'
|
|
148
|
+
| 'number'
|
|
149
|
+
| 'check'
|
|
150
|
+
| 'quote'
|
|
151
|
+
| 'code'
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Block formats this editor can actually produce. Paragraph is always
|
|
155
|
+
* available.
|
|
156
|
+
*
|
|
157
|
+
* Structural availability comes from REGISTERED NODES, not from whether
|
|
158
|
+
* an extension appears in the configuration list: table and list nodes
|
|
159
|
+
* arrive through `@lexical/table` and `@lexical/list` dependencies, so a
|
|
160
|
+
* membership test would be wrong for exactly those cases.
|
|
161
|
+
*
|
|
162
|
+
* Registered nodes are necessary but not always sufficient.
|
|
163
|
+
* `hasCheckListBehaviour` is passed in because check lists need
|
|
164
|
+
* `CheckListExtension` registered as well as the list nodes — removing
|
|
165
|
+
* that extension alone leaves `ListNode` and `ListItemNode` in place, so
|
|
166
|
+
* a node-only rule would keep offering a control that no longer works.
|
|
167
|
+
*
|
|
168
|
+
* This is a correctness concern rather than a cosmetic one: creating an
|
|
169
|
+
* unregistered node throws "Attempted to create node X that was not
|
|
170
|
+
* configured to be used on the editor", so an offered-but-unsupported
|
|
171
|
+
* format is a crash.
|
|
172
|
+
*/
|
|
173
|
+
export function availableBlockFormats(
|
|
174
|
+
editor: LexicalEditor,
|
|
175
|
+
hasCheckListBehaviour: boolean
|
|
176
|
+
): BlockFormat[] {
|
|
177
|
+
const formats: BlockFormat[] = ['paragraph']
|
|
178
|
+
if (editor.hasNodes([HeadingNode])) formats.push('h1', 'h2', 'h3', 'h4')
|
|
179
|
+
if (editor.hasNodes([ListNode, ListItemNode])) {
|
|
180
|
+
formats.push('bullet', 'number')
|
|
181
|
+
if (hasCheckListBehaviour) formats.push('check')
|
|
182
|
+
}
|
|
183
|
+
if (editor.hasNodes([QuoteNode])) formats.push('quote')
|
|
184
|
+
if (editor.hasNodes([CodeNode])) formats.push('code')
|
|
185
|
+
return formats
|
|
186
|
+
}
|
|
187
|
+
|
|
139
188
|
function BlockFormatDropDown({
|
|
140
189
|
editor,
|
|
141
190
|
blockType,
|
|
@@ -146,15 +195,13 @@ function BlockFormatDropDown({
|
|
|
146
195
|
rootType: keyof typeof rootTypeToRootName
|
|
147
196
|
editor: LexicalEditor
|
|
148
197
|
disabled?: boolean
|
|
149
|
-
}): React.JSX.Element {
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// hides the list options here without any flag in EditorSettings.
|
|
154
|
-
const hasList = useOptionalExtensionDependency(ListExtension) !== undefined
|
|
155
|
-
const hasCheckList = useOptionalExtensionDependency(CheckListExtension) !== undefined
|
|
198
|
+
}): React.JSX.Element | null {
|
|
199
|
+
// Entries follow what the resolved editor can actually produce, so
|
|
200
|
+
// removing an extension removes its control along with its node
|
|
201
|
+
// registration — no flag in EditorSettings is involved.
|
|
156
202
|
const [composerEditor] = useLexicalComposerContext()
|
|
157
|
-
const
|
|
203
|
+
const hasCheckListBehaviour = useOptionalExtensionDependency(CheckListExtension) !== undefined
|
|
204
|
+
const formats = availableBlockFormats(composerEditor, hasCheckListBehaviour)
|
|
158
205
|
|
|
159
206
|
const formatParagraph = (): void => {
|
|
160
207
|
editor.update(() => {
|
|
@@ -233,6 +280,10 @@ function BlockFormatDropDown({
|
|
|
233
280
|
}
|
|
234
281
|
}
|
|
235
282
|
|
|
283
|
+
// Paragraph alone is not a choice — with every structural format
|
|
284
|
+
// removed the control would offer a single no-op entry.
|
|
285
|
+
if (formats.length === 1) return null
|
|
286
|
+
|
|
236
287
|
return (
|
|
237
288
|
<DropDown
|
|
238
289
|
disabled={disabled}
|
|
@@ -248,43 +299,47 @@ function BlockFormatDropDown({
|
|
|
248
299
|
<i className="icon paragraph" />
|
|
249
300
|
<span className="text">Normal</span>
|
|
250
301
|
</DropDownItem>
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
302
|
+
{formats.includes('h1') && (
|
|
303
|
+
<>
|
|
304
|
+
<DropDownItem
|
|
305
|
+
className={`item ${dropDownActiveClass(blockType === 'h1')}`}
|
|
306
|
+
onClick={() => {
|
|
307
|
+
formatHeading('h1')
|
|
308
|
+
}}
|
|
309
|
+
>
|
|
310
|
+
<i className="icon h1" />
|
|
311
|
+
<span className="text">Heading 1</span>
|
|
312
|
+
</DropDownItem>
|
|
313
|
+
<DropDownItem
|
|
314
|
+
className={`item ${dropDownActiveClass(blockType === 'h2')}`}
|
|
315
|
+
onClick={() => {
|
|
316
|
+
formatHeading('h2')
|
|
317
|
+
}}
|
|
318
|
+
>
|
|
319
|
+
<i className="icon h2" />
|
|
320
|
+
<span className="text">Heading 2</span>
|
|
321
|
+
</DropDownItem>
|
|
322
|
+
<DropDownItem
|
|
323
|
+
className={`item ${dropDownActiveClass(blockType === 'h3')}`}
|
|
324
|
+
onClick={() => {
|
|
325
|
+
formatHeading('h3')
|
|
326
|
+
}}
|
|
327
|
+
>
|
|
328
|
+
<i className="icon h3" />
|
|
329
|
+
<span className="text">Heading 3</span>
|
|
330
|
+
</DropDownItem>
|
|
331
|
+
<DropDownItem
|
|
332
|
+
className={`item ${dropDownActiveClass(blockType === 'h4')}`}
|
|
333
|
+
onClick={() => {
|
|
334
|
+
formatHeading('h4')
|
|
335
|
+
}}
|
|
336
|
+
>
|
|
337
|
+
<i className="icon h4" />
|
|
338
|
+
<span className="text">Heading 4</span>
|
|
339
|
+
</DropDownItem>
|
|
340
|
+
</>
|
|
341
|
+
)}
|
|
342
|
+
{formats.includes('bullet') && (
|
|
288
343
|
<>
|
|
289
344
|
<DropDownItem
|
|
290
345
|
className={`item ${dropDownActiveClass(blockType === 'bullet')}`}
|
|
@@ -303,7 +358,7 @@ function BlockFormatDropDown({
|
|
|
303
358
|
</>
|
|
304
359
|
)}
|
|
305
360
|
|
|
306
|
-
{
|
|
361
|
+
{formats.includes('check') && (
|
|
307
362
|
<DropDownItem
|
|
308
363
|
className={`item ${dropDownActiveClass(blockType === 'check')}`}
|
|
309
364
|
onClick={formatCheckList}
|
|
@@ -313,14 +368,16 @@ function BlockFormatDropDown({
|
|
|
313
368
|
</DropDownItem>
|
|
314
369
|
)}
|
|
315
370
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
371
|
+
{formats.includes('quote') && (
|
|
372
|
+
<DropDownItem
|
|
373
|
+
className={`item ${dropDownActiveClass(blockType === 'quote')}`}
|
|
374
|
+
onClick={formatQuote}
|
|
375
|
+
>
|
|
376
|
+
<i className="icon quote" />
|
|
377
|
+
<span className="text">Quote</span>
|
|
378
|
+
</DropDownItem>
|
|
379
|
+
)}
|
|
380
|
+
{formats.includes('code') && (
|
|
324
381
|
<DropDownItem
|
|
325
382
|
className={`item ${dropDownActiveClass(blockType === 'code')}`}
|
|
326
383
|
onClick={formatCode}
|
|
@@ -373,10 +430,16 @@ export function ToolbarPlugin(): React.JSX.Element {
|
|
|
373
430
|
const {
|
|
374
431
|
uuid,
|
|
375
432
|
config: {
|
|
376
|
-
|
|
433
|
+
controls: { textAlignment, undoRedo, blockFormat, inlineCode, markdownToggle },
|
|
377
434
|
},
|
|
378
435
|
} = useEditorConfig()
|
|
379
436
|
const { isMarkdown, toggleMarkdown } = useMarkdownToggle()
|
|
437
|
+
// Resolved after mount so the first client render matches the server;
|
|
438
|
+
// reading the platform during render mismatches on hydration.
|
|
439
|
+
const isApple = useIsApplePlatform()
|
|
440
|
+
// Source mode needs a CodeNode to hold the Markdown text, so the
|
|
441
|
+
// control follows that capability as well as the preference.
|
|
442
|
+
const canToggleMarkdown = canUseMarkdownSourceMode(editor)
|
|
380
443
|
// const { openModal } = usePayloadModal()
|
|
381
444
|
// const editDepth = useEditDepth()
|
|
382
445
|
|
|
@@ -600,7 +663,7 @@ export function ToolbarPlugin(): React.JSX.Element {
|
|
|
600
663
|
onClick={() => {
|
|
601
664
|
activeEditor.dispatchCommand(UNDO_COMMAND, undefined)
|
|
602
665
|
}}
|
|
603
|
-
title={
|
|
666
|
+
title={isApple ? 'Undo (⌘Z)' : 'Undo (Ctrl+Z)'}
|
|
604
667
|
type="button"
|
|
605
668
|
className="toolbar-item spaced"
|
|
606
669
|
aria-label="Undo"
|
|
@@ -612,7 +675,7 @@ export function ToolbarPlugin(): React.JSX.Element {
|
|
|
612
675
|
onClick={() => {
|
|
613
676
|
activeEditor.dispatchCommand(REDO_COMMAND, undefined)
|
|
614
677
|
}}
|
|
615
|
-
title={
|
|
678
|
+
title={isApple ? 'Redo (⌘Y)' : 'Redo (Ctrl+Y)'}
|
|
616
679
|
type="button"
|
|
617
680
|
className="toolbar-item"
|
|
618
681
|
aria-label="Redo"
|
|
@@ -625,7 +688,7 @@ export function ToolbarPlugin(): React.JSX.Element {
|
|
|
625
688
|
|
|
626
689
|
{blockType in blockTypeToBlockName && activeEditor === editor && (
|
|
627
690
|
<>
|
|
628
|
-
{
|
|
691
|
+
{blockFormat && (
|
|
629
692
|
<>
|
|
630
693
|
<BlockFormatDropDown
|
|
631
694
|
disabled={!isEditable}
|
|
@@ -736,9 +799,9 @@ export function ToolbarPlugin(): React.JSX.Element {
|
|
|
736
799
|
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')
|
|
737
800
|
}}
|
|
738
801
|
className={`toolbar-item spaced ${isBold ? 'active' : ''}`}
|
|
739
|
-
title={
|
|
802
|
+
title={isApple ? 'Bold (⌘B)' : 'Bold (Ctrl+B)'}
|
|
740
803
|
type="button"
|
|
741
|
-
aria-label={`Format text as bold. Shortcut: ${
|
|
804
|
+
aria-label={`Format text as bold. Shortcut: ${isApple ? '⌘B' : 'Ctrl+B'}`}
|
|
742
805
|
>
|
|
743
806
|
<i className="format bold" />
|
|
744
807
|
</button>
|
|
@@ -748,9 +811,9 @@ export function ToolbarPlugin(): React.JSX.Element {
|
|
|
748
811
|
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')
|
|
749
812
|
}}
|
|
750
813
|
className={`toolbar-item spaced ${isItalic ? 'active' : ''}`}
|
|
751
|
-
title={
|
|
814
|
+
title={isApple ? 'Italic (⌘I)' : 'Italic (Ctrl+I)'}
|
|
752
815
|
type="button"
|
|
753
|
-
aria-label={`Format text as italics. Shortcut: ${
|
|
816
|
+
aria-label={`Format text as italics. Shortcut: ${isApple ? '⌘I' : 'Ctrl+I'}`}
|
|
754
817
|
>
|
|
755
818
|
<i className="format italic" />
|
|
756
819
|
</button>
|
|
@@ -760,9 +823,9 @@ export function ToolbarPlugin(): React.JSX.Element {
|
|
|
760
823
|
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')
|
|
761
824
|
}}
|
|
762
825
|
className={`toolbar-item spaced ${isUnderline ? 'active' : ''}`}
|
|
763
|
-
title={
|
|
826
|
+
title={isApple ? 'Underline (⌘U)' : 'Underline (Ctrl+U)'}
|
|
764
827
|
type="button"
|
|
765
|
-
aria-label={`Format text to underlined. Shortcut: ${
|
|
828
|
+
aria-label={`Format text to underlined. Shortcut: ${isApple ? '⌘U' : 'Ctrl+U'}`}
|
|
766
829
|
>
|
|
767
830
|
<i className="format underline" />
|
|
768
831
|
</button>
|
|
@@ -872,7 +935,7 @@ export function ToolbarPlugin(): React.JSX.Element {
|
|
|
872
935
|
))}
|
|
873
936
|
</ToolbarActiveEditorProvider>
|
|
874
937
|
|
|
875
|
-
{markdownToggle && (
|
|
938
|
+
{markdownToggle && canToggleMarkdown && (
|
|
876
939
|
<>
|
|
877
940
|
<Divider />
|
|
878
941
|
<button
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { buildFullEditor, buildRestrictedEditor, parseHtmlToTypes } from './build-test-editor'
|
|
4
|
+
|
|
5
|
+
describe('test harness', () => {
|
|
6
|
+
it('builds the full default extension set', () => {
|
|
7
|
+
const editor = buildFullEditor()
|
|
8
|
+
expect(parseHtmlToTypes(editor, '<p>hello</p>').types).toEqual(['paragraph'])
|
|
9
|
+
editor.dispose()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('builds a restricted editor', () => {
|
|
13
|
+
const editor = buildRestrictedEditor()
|
|
14
|
+
expect(parseHtmlToTypes(editor, '<p>hello</p>').types).toEqual(['paragraph'])
|
|
15
|
+
editor.dispose()
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* Shared editor builders for jsdom tests.
|
|
11
|
+
*
|
|
12
|
+
* These compose a root extension directly rather than rendering
|
|
13
|
+
* `EditorContext`, which keeps capability tests fast and free of React.
|
|
14
|
+
* That also means they do NOT exercise the shipped component: the
|
|
15
|
+
* production root extension lives in `editor-context.tsx`, and a
|
|
16
|
+
* regression there is caught by `editor-context.regression.test.tsx`,
|
|
17
|
+
* not here.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { buildEditorFromExtensions } from '@lexical/extension'
|
|
21
|
+
import { $generateNodesFromDOM } from '@lexical/html'
|
|
22
|
+
import { ReactPluginHostExtension } from '@lexical/react/ReactPluginHostExtension'
|
|
23
|
+
import { defineExtension, type LexicalEditor } from 'lexical'
|
|
24
|
+
|
|
25
|
+
import { builtInExtensions } from '../config/built-in-extension-names'
|
|
26
|
+
import { defaultExtensionsList } from '../config/default-extensions'
|
|
27
|
+
import { rootDependencies } from '../config/root-dependencies'
|
|
28
|
+
import type { ExtensionsList } from '../config/extensions-list'
|
|
29
|
+
|
|
30
|
+
const NAMESPACE = 'LexicalRichText'
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `ReactPluginHostExtension` is required, not optional: several default
|
|
34
|
+
* extensions host React decorators and `buildEditorFromExtensions`
|
|
35
|
+
* throws "No ReactProviderExtension detected" without it.
|
|
36
|
+
*/
|
|
37
|
+
function build(extensions: ExtensionsList) {
|
|
38
|
+
return buildEditorFromExtensions(
|
|
39
|
+
defineExtension({
|
|
40
|
+
name: '[test-root]',
|
|
41
|
+
namespace: NAMESPACE,
|
|
42
|
+
// Through the same seam the live editor uses, so these builders
|
|
43
|
+
// mirror production rather than flattering it.
|
|
44
|
+
// biome-ignore lint/suspicious/noExplicitAny: extension arguments are heterogeneous by design
|
|
45
|
+
dependencies: [ReactPluginHostExtension, ...rootDependencies(extensions)] as any,
|
|
46
|
+
})
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Every default extension, as a production editor would have them. */
|
|
51
|
+
export function buildFullEditor() {
|
|
52
|
+
return build(defaultExtensionsList())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The defaults minus the named extensions. Defaults to removing every
|
|
57
|
+
* Byline built-in plus the upstream list extensions — the "minimal
|
|
58
|
+
* editor" a downstream site builds when it strips a field back.
|
|
59
|
+
*/
|
|
60
|
+
export function buildRestrictedEditor(removals?: string[]) {
|
|
61
|
+
const names = removals ?? [
|
|
62
|
+
...Object.values(builtInExtensions),
|
|
63
|
+
'@lexical/list/List',
|
|
64
|
+
'@lexical/list/CheckList',
|
|
65
|
+
]
|
|
66
|
+
const list = defaultExtensionsList()
|
|
67
|
+
for (const name of names) list.remove(name)
|
|
68
|
+
return build(list)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse `html` through the legacy `$generateNodesFromDOM` path — the one
|
|
73
|
+
* a clipboard paste actually uses — and report what the editor made of
|
|
74
|
+
* it. Assertions are on node types and text, never on markup.
|
|
75
|
+
*
|
|
76
|
+
* Scope: this inspects the imported nodes BEFORE insertion, so it
|
|
77
|
+
* evidences HTML conversion only. Real clipboard insertion — the
|
|
78
|
+
* `application/x-lexical-editor` branch, its fallthrough, and what
|
|
79
|
+
* survives `$insertGeneratedNodes` — is covered separately by the
|
|
80
|
+
* clipboard-equivalence tests.
|
|
81
|
+
*/
|
|
82
|
+
export function parseHtmlToTypes(
|
|
83
|
+
editor: LexicalEditor,
|
|
84
|
+
html: string
|
|
85
|
+
): { types: string[]; text: string } {
|
|
86
|
+
const dom = new DOMParser().parseFromString(html, 'text/html')
|
|
87
|
+
let result = { types: [] as string[], text: '' }
|
|
88
|
+
editor.update(
|
|
89
|
+
() => {
|
|
90
|
+
const nodes = $generateNodesFromDOM(editor, dom)
|
|
91
|
+
result = {
|
|
92
|
+
types: nodes.map((node) => node.getType()),
|
|
93
|
+
text: nodes.map((node) => node.getTextContent()).join('|'),
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{ discrete: true }
|
|
97
|
+
)
|
|
98
|
+
return result
|
|
99
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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 { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
|
10
|
+
import type { LexicalEditor } from 'lexical'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Hands the composer's editor to a test. Must be rendered as a child of
|
|
14
|
+
* `EditorContext` so it sits inside the composer's React tree.
|
|
15
|
+
*/
|
|
16
|
+
export function CaptureEditor({ onEditor }: { onEditor: (editor: LexicalEditor) => void }): null {
|
|
17
|
+
const [editor] = useLexicalComposerContext()
|
|
18
|
+
onEditor(editor)
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* Deterministic control over the frames and idle callbacks the editor
|
|
11
|
+
* defers work through.
|
|
12
|
+
*
|
|
13
|
+
* Two pieces of editor behaviour are scheduled rather than immediate,
|
|
14
|
+
* and a test that waits a fixed number of milliseconds for either is
|
|
15
|
+
* guessing:
|
|
16
|
+
*
|
|
17
|
+
* - `ApplyValuePlugin` captures its normalization baseline after a
|
|
18
|
+
* microtask and **two** animation frames.
|
|
19
|
+
* - `editor-component` defers change emission through
|
|
20
|
+
* `requestIdleCallback`, which jsdom does not implement at all — so
|
|
21
|
+
* without a shim it takes a synchronous branch and an entire class of
|
|
22
|
+
* ordering bug becomes invisible.
|
|
23
|
+
*
|
|
24
|
+
* Installing controllable versions of both makes the ordering explicit:
|
|
25
|
+
* a test says when a frame happens, rather than sleeping and hoping.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
interface FrameControl {
|
|
29
|
+
/** Run pending animation-frame callbacks, `generations` deep. */
|
|
30
|
+
flushFrames: (generations?: number) => Promise<void>
|
|
31
|
+
/** Run every pending idle callback. */
|
|
32
|
+
flushIdle: () => void
|
|
33
|
+
/** How many idle callbacks are scheduled but not yet run. */
|
|
34
|
+
pendingIdle: () => number
|
|
35
|
+
/** Settle the normalization baseline: a microtask, then two frames. */
|
|
36
|
+
settleBaseline: () => Promise<void>
|
|
37
|
+
/** Restore the original globals. */
|
|
38
|
+
restore: () => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function installFrameControl(): FrameControl {
|
|
42
|
+
const frameCallbacks: FrameRequestCallback[] = []
|
|
43
|
+
const idleCallbacks: Array<() => void> = []
|
|
44
|
+
|
|
45
|
+
const originalRaf = globalThis.requestAnimationFrame
|
|
46
|
+
const originalCaf = globalThis.cancelAnimationFrame
|
|
47
|
+
// biome-ignore lint/suspicious/noExplicitAny: jsdom does not implement these
|
|
48
|
+
const originalRic = (globalThis as any).requestIdleCallback
|
|
49
|
+
// biome-ignore lint/suspicious/noExplicitAny: jsdom does not implement these
|
|
50
|
+
const originalCic = (globalThis as any).cancelIdleCallback
|
|
51
|
+
|
|
52
|
+
globalThis.requestAnimationFrame = ((callback: FrameRequestCallback) => {
|
|
53
|
+
frameCallbacks.push(callback)
|
|
54
|
+
return frameCallbacks.length
|
|
55
|
+
}) as typeof requestAnimationFrame
|
|
56
|
+
globalThis.cancelAnimationFrame = (() => {}) as typeof cancelAnimationFrame
|
|
57
|
+
// biome-ignore lint/suspicious/noExplicitAny: shimming a missing jsdom API
|
|
58
|
+
;(globalThis as any).requestIdleCallback = (callback: () => void) => {
|
|
59
|
+
idleCallbacks.push(callback)
|
|
60
|
+
return idleCallbacks.length
|
|
61
|
+
}
|
|
62
|
+
// biome-ignore lint/suspicious/noExplicitAny: shimming a missing jsdom API
|
|
63
|
+
;(globalThis as any).cancelIdleCallback = () => {}
|
|
64
|
+
|
|
65
|
+
const flushFrames = async (generations = 1): Promise<void> => {
|
|
66
|
+
for (let generation = 0; generation < generations; generation++) {
|
|
67
|
+
// Drain microtasks first: the plugin schedules its first frame
|
|
68
|
+
// from inside `queueMicrotask`.
|
|
69
|
+
await Promise.resolve()
|
|
70
|
+
for (const callback of frameCallbacks.splice(0)) callback(performance.now())
|
|
71
|
+
}
|
|
72
|
+
await Promise.resolve()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
flushFrames,
|
|
77
|
+
flushIdle: () => {
|
|
78
|
+
for (const callback of idleCallbacks.splice(0)) callback()
|
|
79
|
+
},
|
|
80
|
+
pendingIdle: () => idleCallbacks.length,
|
|
81
|
+
settleBaseline: () => flushFrames(2),
|
|
82
|
+
restore: () => {
|
|
83
|
+
globalThis.requestAnimationFrame = originalRaf
|
|
84
|
+
globalThis.cancelAnimationFrame = originalCaf
|
|
85
|
+
// biome-ignore lint/suspicious/noExplicitAny: restoring a shimmed API
|
|
86
|
+
;(globalThis as any).requestIdleCallback = originalRic
|
|
87
|
+
// biome-ignore lint/suspicious/noExplicitAny: restoring a shimmed API
|
|
88
|
+
;(globalThis as any).cancelIdleCallback = originalCic
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* jsdom gaps that the editor's React tree depends on.
|
|
11
|
+
*
|
|
12
|
+
* Mounting the real `EditorContext` pulls in components that query
|
|
13
|
+
* media features and observe element size; jsdom implements neither, so
|
|
14
|
+
* they are stubbed here rather than mocked per-test.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
if (typeof window !== 'undefined') {
|
|
18
|
+
if (typeof window.matchMedia !== 'function') {
|
|
19
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
20
|
+
writable: true,
|
|
21
|
+
value: (query: string) => ({
|
|
22
|
+
matches: false,
|
|
23
|
+
media: query,
|
|
24
|
+
onchange: null,
|
|
25
|
+
addListener: () => {},
|
|
26
|
+
removeListener: () => {},
|
|
27
|
+
addEventListener: () => {},
|
|
28
|
+
removeEventListener: () => {},
|
|
29
|
+
dispatchEvent: () => false,
|
|
30
|
+
}),
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof globalThis.ResizeObserver === 'undefined') {
|
|
35
|
+
globalThis.ResizeObserver = class {
|
|
36
|
+
observe(): void {}
|
|
37
|
+
unobserve(): void {}
|
|
38
|
+
disconnect(): void {}
|
|
39
|
+
} as unknown as typeof ResizeObserver
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -42,7 +42,9 @@ export {
|
|
|
42
42
|
selectToolbarItems,
|
|
43
43
|
} from './field/extensions/byline-toolbar'
|
|
44
44
|
export { CodeHighlightExtension } from './field/extensions/code-highlight/code-highlight-extension'
|
|
45
|
+
export { CoreNodesExtension } from './field/extensions/core-nodes/core-nodes-extension'
|
|
45
46
|
export { FloatingTextFormatExtension } from './field/extensions/floating-text-format/floating-text-format-extension'
|
|
47
|
+
export { HeadingExtension } from './field/extensions/heading/heading-extension'
|
|
46
48
|
export { HorizontalRuleExtension } from './field/extensions/horizontal-rule/horizontal-rule-extension'
|
|
47
49
|
export {
|
|
48
50
|
type InlineImageConfig,
|
|
@@ -50,10 +52,13 @@ export {
|
|
|
50
52
|
} from './field/extensions/inline-image/inline-image-extension'
|
|
51
53
|
export { LayoutExtension } from './field/extensions/layout/layout-extension'
|
|
52
54
|
export { AutoLinkExtension, LinkExtension } from './field/extensions/link'
|
|
55
|
+
export { QuoteExtension } from './field/extensions/quote/quote-extension'
|
|
53
56
|
export { TableExtension } from './field/extensions/table/table-extension'
|
|
54
57
|
export { VimeoExtension } from './field/extensions/vimeo/vimeo-extension'
|
|
55
58
|
export { YouTubeExtension } from './field/extensions/youtube/youtube-extension'
|
|
56
|
-
|
|
59
|
+
// Not a registration list: the vocabulary of node classes Byline can
|
|
60
|
+
// read, used by normalization and the capability manifest.
|
|
61
|
+
export { READABLE_NODES } from './field/nodes'
|
|
57
62
|
// Hook for extensions that contribute toolbar items via
|
|
58
63
|
// BylineToolbarExtension and need the active editor for command dispatch.
|
|
59
64
|
export {
|
|
@@ -62,6 +67,11 @@ export {
|
|
|
62
67
|
} from './field/plugins/toolbar-plugin/toolbar-active-editor'
|
|
63
68
|
export { lexicalEditor } from './lexical-editor'
|
|
64
69
|
export { RichTextField } from './richtext-field'
|
|
70
|
+
// Pre-upgrade capability scan. `capabilitiesFor` / `capabilitiesFromEditor`
|
|
71
|
+
// need a DOM and run in a browser or jsdom; `scanDocument` is pure and runs
|
|
72
|
+
// anywhere, which is why the manifest travels between them as a file.
|
|
73
|
+
export { buildManifest, capabilitiesFor, capabilitiesFromEditor } from './scan/capability-manifest'
|
|
74
|
+
export { scanDocument, summarise } from './scan/scan-documents'
|
|
65
75
|
export { createEmptyEditorState } from './validate/createEmptyEditorState'
|
|
66
76
|
export { hasText } from './validate/hasText'
|
|
67
77
|
export type { EditorConfig, EditorSettings, EditorSettingsOverride } from './field/config/types'
|
|
@@ -71,3 +81,4 @@ export type {
|
|
|
71
81
|
SerializedInlineImageNode,
|
|
72
82
|
} from './field/extensions/inline-image/node-types'
|
|
73
83
|
export type { DocumentRelation } from './field/nodes/document-relation'
|
|
84
|
+
export type { CapabilityManifest, FieldCapabilities, ScanFinding } from './scan/types'
|