@blue-patterns/richtext 0.0.0-stage → 0.7.18
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/CHANGELOG.md +275 -0
- package/LICENSE +22 -0
- package/README.md +88 -2
- package/astryx.integration.mjs +11 -0
- package/blocks/RichTextEditorShowcase.template.mjs +13 -0
- package/blocks/RichTextEditorShowcase.tsx +19 -0
- package/dist/RichTextEditor.d.ts +282 -0
- package/dist/RichTextEditor.d.ts.map +1 -0
- package/dist/RichTextEditor.js +657 -0
- package/dist/RichTextEditorAutoLinkPlugin.d.ts +83 -0
- package/dist/RichTextEditorAutoLinkPlugin.d.ts.map +1 -0
- package/dist/RichTextEditorAutoLinkPlugin.js +99 -0
- package/dist/RichTextEditorToolbar.d.ts +186 -0
- package/dist/RichTextEditorToolbar.d.ts.map +1 -0
- package/dist/RichTextEditorToolbar.js +688 -0
- package/dist/RichTextView.d.ts +92 -0
- package/dist/RichTextView.d.ts.map +1 -0
- package/dist/RichTextView.js +267 -0
- package/dist/codeLanguages.d.ts +14 -0
- package/dist/codeLanguages.d.ts.map +1 -0
- package/dist/codeLanguages.js +34 -0
- package/dist/codeLineTokens.d.ts +53 -0
- package/dist/codeLineTokens.d.ts.map +1 -0
- package/dist/codeLineTokens.js +188 -0
- package/dist/editorNodes.d.ts +8 -0
- package/dist/editorNodes.d.ts.map +1 -0
- package/dist/editorNodes.js +30 -0
- package/dist/editorTheme.d.ts +7 -0
- package/dist/editorTheme.d.ts.map +1 -0
- package/dist/editorTheme.js +328 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/internal/CodeBackspacePlugin.d.ts +2 -0
- package/dist/internal/CodeBackspacePlugin.d.ts.map +1 -0
- package/dist/internal/CodeBackspacePlugin.js +70 -0
- package/dist/internal/CodeBlockHeaderPlugin.d.ts +11 -0
- package/dist/internal/CodeBlockHeaderPlugin.d.ts.map +1 -0
- package/dist/internal/CodeBlockHeaderPlugin.js +204 -0
- package/dist/internal/CodePastePlugin.d.ts +5 -0
- package/dist/internal/CodePastePlugin.d.ts.map +1 -0
- package/dist/internal/CodePastePlugin.js +104 -0
- package/dist/internal/CodeSyntaxPlugin.d.ts +8 -0
- package/dist/internal/CodeSyntaxPlugin.d.ts.map +1 -0
- package/dist/internal/CodeSyntaxPlugin.js +397 -0
- package/dist/internal/CodeTabPlugin.d.ts +2 -0
- package/dist/internal/CodeTabPlugin.d.ts.map +1 -0
- package/dist/internal/CodeTabPlugin.js +256 -0
- package/dist/internal/DisabledLinkGuardPlugin.d.ts +4 -0
- package/dist/internal/DisabledLinkGuardPlugin.d.ts.map +1 -0
- package/dist/internal/DisabledLinkGuardPlugin.js +93 -0
- package/dist/internal/ImageSourceCaretPlugin.d.ts +3 -0
- package/dist/internal/ImageSourceCaretPlugin.d.ts.map +1 -0
- package/dist/internal/ImageSourceCaretPlugin.js +105 -0
- package/dist/internal/ListItemShortcutsPlugin.d.ts +6 -0
- package/dist/internal/ListItemShortcutsPlugin.d.ts.map +1 -0
- package/dist/internal/ListItemShortcutsPlugin.js +314 -0
- package/dist/internal/MarkdownExtensionsPlugin.d.ts +17 -0
- package/dist/internal/MarkdownExtensionsPlugin.d.ts.map +1 -0
- package/dist/internal/MarkdownExtensionsPlugin.js +185 -0
- package/dist/internal/SelectAllScopePlugin.d.ts +2 -0
- package/dist/internal/SelectAllScopePlugin.d.ts.map +1 -0
- package/dist/internal/SelectAllScopePlugin.js +67 -0
- package/dist/internal/TableColumnFloorPlugin.d.ts +12 -0
- package/dist/internal/TableColumnFloorPlugin.d.ts.map +1 -0
- package/dist/internal/TableColumnFloorPlugin.js +84 -0
- package/dist/internal/TablePastePlugin.d.ts +9 -0
- package/dist/internal/TablePastePlugin.d.ts.map +1 -0
- package/dist/internal/TablePastePlugin.js +183 -0
- package/dist/internal/TableScrollRegionPlugin.d.ts +9 -0
- package/dist/internal/TableScrollRegionPlugin.d.ts.map +1 -0
- package/dist/internal/TableScrollRegionPlugin.js +95 -0
- package/dist/internal/TaskCheckboxPlugin.d.ts +7 -0
- package/dist/internal/TaskCheckboxPlugin.d.ts.map +1 -0
- package/dist/internal/TaskCheckboxPlugin.js +193 -0
- package/dist/internal/ToolbarLinkPopover.d.ts +74 -0
- package/dist/internal/ToolbarLinkPopover.d.ts.map +1 -0
- package/dist/internal/ToolbarLinkPopover.js +292 -0
- package/dist/linkEditing.d.ts +6 -0
- package/dist/linkEditing.d.ts.map +1 -0
- package/dist/linkEditing.js +20 -0
- package/dist/linkForm.d.ts +28 -0
- package/dist/linkForm.d.ts.map +1 -0
- package/dist/linkForm.js +137 -0
- package/dist/linkUtils.d.ts +27 -0
- package/dist/linkUtils.d.ts.map +1 -0
- package/dist/linkUtils.js +101 -0
- package/dist/markdown.d.ts +17 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +17 -0
- package/dist/markdownCharacterReferences.d.ts +135 -0
- package/dist/markdownCharacterReferences.d.ts.map +1 -0
- package/dist/markdownCharacterReferences.js +1127 -0
- package/dist/markdownCodeFence.d.ts +4 -0
- package/dist/markdownCodeFence.d.ts.map +1 -0
- package/dist/markdownCodeFence.js +167 -0
- package/dist/markdownExtensionNode.d.ts +88 -0
- package/dist/markdownExtensionNode.d.ts.map +1 -0
- package/dist/markdownExtensionNode.js +248 -0
- package/dist/markdownExtensions.d.ts +115 -0
- package/dist/markdownExtensions.d.ts.map +1 -0
- package/dist/markdownExtensions.js +392 -0
- package/dist/markdownHardLineBreak.d.ts +25 -0
- package/dist/markdownHardLineBreak.d.ts.map +1 -0
- package/dist/markdownHardLineBreak.js +59 -0
- package/dist/markdownHeading.d.ts +20 -0
- package/dist/markdownHeading.d.ts.map +1 -0
- package/dist/markdownHeading.js +30 -0
- package/dist/markdownLink.d.ts +5 -0
- package/dist/markdownLink.d.ts.map +1 -0
- package/dist/markdownLink.js +80 -0
- package/dist/markdownListExport.d.ts +3 -0
- package/dist/markdownListExport.d.ts.map +1 -0
- package/dist/markdownListExport.js +68 -0
- package/dist/markdownListIndentation.d.ts +8 -0
- package/dist/markdownListIndentation.d.ts.map +1 -0
- package/dist/markdownListIndentation.js +101 -0
- package/dist/markdownOrderedList.d.ts +3 -0
- package/dist/markdownOrderedList.d.ts.map +1 -0
- package/dist/markdownOrderedList.js +51 -0
- package/dist/markdownQuote.d.ts +22 -0
- package/dist/markdownQuote.d.ts.map +1 -0
- package/dist/markdownQuote.js +32 -0
- package/dist/markdownSerializers.d.ts +49 -0
- package/dist/markdownSerializers.d.ts.map +1 -0
- package/dist/markdownSerializers.js +83 -0
- package/dist/markdownSource.d.ts +71 -0
- package/dist/markdownSource.d.ts.map +1 -0
- package/dist/markdownSource.js +804 -0
- package/dist/markdownSpaceRuns.d.ts +24 -0
- package/dist/markdownSpaceRuns.d.ts.map +1 -0
- package/dist/markdownSpaceRuns.js +147 -0
- package/dist/markdownTable.d.ts +35 -0
- package/dist/markdownTable.d.ts.map +1 -0
- package/dist/markdownTable.js +227 -0
- package/dist/markdownTaskList.d.ts +48 -0
- package/dist/markdownTaskList.d.ts.map +1 -0
- package/dist/markdownTaskList.js +166 -0
- package/dist/markdownThematicBreak.d.ts +3 -0
- package/dist/markdownThematicBreak.d.ts.map +1 -0
- package/dist/markdownThematicBreak.js +55 -0
- package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts +12 -0
- package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts.map +1 -0
- package/dist/privateAttributes/layerPortalBoundary.stylex.js +16 -0
- package/dist/richtext.css +213 -0
- package/dist/textSemantics.d.ts +3 -0
- package/dist/textSemantics.d.ts.map +1 -0
- package/dist/textSemantics.js +87 -0
- package/package.json +158 -3
- package/src/RichTextEditor.doc.mjs +308 -0
- package/src/RichTextEditor.tsx +1109 -0
- package/src/RichTextEditorAutoLinkPlugin.doc.mjs +43 -0
- package/src/RichTextEditorAutoLinkPlugin.tsx +126 -0
- package/src/RichTextEditorToolbar.doc.mjs +92 -0
- package/src/RichTextEditorToolbar.tsx +871 -0
- package/src/RichTextView.doc.mjs +101 -0
- package/src/RichTextView.tsx +351 -0
- package/src/codeLanguages.ts +42 -0
- package/src/codeLineTokens.ts +241 -0
- package/src/doc-mjs.d.ts +23 -0
- package/src/editorNodes.ts +44 -0
- package/src/editorTheme.ts +550 -0
- package/src/index.ts +59 -0
- package/src/internal/CodeBackspacePlugin.tsx +96 -0
- package/src/internal/CodeBlockHeaderPlugin.tsx +282 -0
- package/src/internal/CodePastePlugin.tsx +144 -0
- package/src/internal/CodeSyntaxPlugin.tsx +493 -0
- package/src/internal/CodeTabPlugin.tsx +298 -0
- package/src/internal/DisabledLinkGuardPlugin.tsx +93 -0
- package/src/internal/ImageSourceCaretPlugin.tsx +159 -0
- package/src/internal/ListItemShortcutsPlugin.tsx +404 -0
- package/src/internal/MarkdownExtensionsPlugin.tsx +289 -0
- package/src/internal/SelectAllScopePlugin.tsx +86 -0
- package/src/internal/TableColumnFloorPlugin.tsx +99 -0
- package/src/internal/TablePastePlugin.tsx +223 -0
- package/src/internal/TableScrollRegionPlugin.tsx +107 -0
- package/src/internal/TaskCheckboxPlugin.tsx +246 -0
- package/src/internal/ToolbarLinkPopover.tsx +392 -0
- package/src/linkEditing.ts +20 -0
- package/src/linkForm.tsx +156 -0
- package/src/linkUtils.ts +106 -0
- package/src/markdown.ts +25 -0
- package/src/markdownCharacterReferences.ts +1137 -0
- package/src/markdownCodeFence.ts +216 -0
- package/src/markdownExtensionNode.ts +339 -0
- package/src/markdownExtensions.ts +496 -0
- package/src/markdownHardLineBreak.ts +73 -0
- package/src/markdownHeading.ts +33 -0
- package/src/markdownLink.ts +86 -0
- package/src/markdownListExport.ts +85 -0
- package/src/markdownListIndentation.ts +105 -0
- package/src/markdownOrderedList.ts +60 -0
- package/src/markdownQuote.ts +35 -0
- package/src/markdownSerializers.ts +108 -0
- package/src/markdownSource.ts +1045 -0
- package/src/markdownSpaceRuns.ts +164 -0
- package/src/markdownTable.ts +309 -0
- package/src/markdownTaskList.ts +222 -0
- package/src/markdownThematicBreak.ts +66 -0
- package/src/privateAttributes/layerPortalBoundary.stylex.ts +18 -0
- package/src/textSemantics.ts +104 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file MarkdownExtensionsPlugin.tsx
|
|
7
|
+
* @input Uses @lexical/react (composer context), @lexical/utils ($dfs),
|
|
8
|
+
* lexical, core's client-only MarkdownPluginNodeRenderer and server-safe
|
|
9
|
+
* parser, and the RichText adapter and extension node.
|
|
10
|
+
* @output Exports MarkdownExtensionsPlugin, which draws every Markdown plugin
|
|
11
|
+
* node in an editor with its plugin's renderer, and deriveExtensionNode.
|
|
12
|
+
* @position Rendered by RichTextEditor and RichTextView when they are given
|
|
13
|
+
* `markdownExtensions` (spec:AST-064 FR2). It checks the configuration
|
|
14
|
+
* first (FR6), so a conflicting one throws a RichTextExtensionError while
|
|
15
|
+
* the surface renders. Each node is derived again from its source with the
|
|
16
|
+
* adopted plugins and drawn by core's MarkdownPluginNodeRenderer, so it
|
|
17
|
+
* renders, falls back, and reports failures exactly as Markdown does (FR8,
|
|
18
|
+
* FR9); stored data is never drawn on its own, and a node its plugin no
|
|
19
|
+
* longer recognizes — or whose plugin is not adopted — shows its source
|
|
20
|
+
* (FR11). Without this plugin a node shows its source.
|
|
21
|
+
*/
|
|
22
|
+
import { useEffect, useMemo } from 'react';
|
|
23
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
24
|
+
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
|
|
25
|
+
import { $dfs, addClassNamesToElement, removeClassNamesFromElement } from '@lexical/utils';
|
|
26
|
+
import { $getSelection, $isRangeSelection, CLICK_COMMAND, COMMAND_PRIORITY_LOW, FORMAT_TEXT_COMMAND, HISTORY_MERGE_TAG, TEXT_TYPE_TO_FORMAT } from 'lexical';
|
|
27
|
+
import { MarkdownPluginNodeRenderer } from '@blue-patterns/core/Markdown/plugin-renderer';
|
|
28
|
+
import { parseInlineAst, parseMarkdownAst } from '@blue-patterns/core/Markdown/parser';
|
|
29
|
+
import { pluginsOf } from "../markdownExtensions.js";
|
|
30
|
+
import { $firstSelectedText, $isRichTextExtensionNode, setRichTextExtensionNodeDecorator } from "../markdownExtensionNode.js";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The node core derives from a stored node's source with `plugins`, or null
|
|
34
|
+
* when no adopted plugin of the same protocol version reads that exact source
|
|
35
|
+
* as that node.
|
|
36
|
+
*/
|
|
37
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
38
|
+
export function deriveExtensionNode(facts, plugins) {
|
|
39
|
+
const plugin = plugins.find(entry => entry.name === facts.plugin);
|
|
40
|
+
if (plugin == null || plugin.apiVersion !== facts.apiVersion) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const nodes = facts.display === 'inline' ? parseInlineAst(facts.source, {
|
|
44
|
+
plugins: [plugin]
|
|
45
|
+
}) : parseMarkdownAst(facts.source, {
|
|
46
|
+
plugins: [plugin]
|
|
47
|
+
}).children;
|
|
48
|
+
const [node] = nodes;
|
|
49
|
+
if (nodes.length !== 1 || node?.type !== 'extension') {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const extension = node;
|
|
53
|
+
return extension.plugin === facts.plugin && extension.name === facts.name && extension.display === facts.display && extension.source === facts.source ? extension : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The formats a plugin node can sit inside, outermost first, as RichText draws
|
|
58
|
+
* them around text (textSemantics.ts): a deletion, then emphasis, then strong.
|
|
59
|
+
*/
|
|
60
|
+
const MARKS = [['strikethrough', 'del'], ['italic', 'em'], ['bold', 'strong']];
|
|
61
|
+
|
|
62
|
+
/** The formats a toggle can put a plugin node inside. */
|
|
63
|
+
const NODE_FORMATS = new Set(MARKS.map(([format]) => format));
|
|
64
|
+
function ExtensionNodeView({
|
|
65
|
+
facts,
|
|
66
|
+
format,
|
|
67
|
+
nodeKey,
|
|
68
|
+
plugins
|
|
69
|
+
}) {
|
|
70
|
+
const [editor] = useLexicalComposerContext();
|
|
71
|
+
const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey);
|
|
72
|
+
// A click on the node selects it whole, as a click on a rule does, so a
|
|
73
|
+
// pointer user can format or delete it like a keyboard user.
|
|
74
|
+
useEffect(() => editor.registerCommand(CLICK_COMMAND, event => {
|
|
75
|
+
const element = editor.getElementByKey(nodeKey);
|
|
76
|
+
if (!editor.isEditable() || element == null || !(event.target instanceof Node) || !element.contains(event.target)) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
clearSelection();
|
|
80
|
+
setSelected(true);
|
|
81
|
+
return true;
|
|
82
|
+
}, COMMAND_PRIORITY_LOW), [clearSelection, editor, nodeKey, setSelected]);
|
|
83
|
+
// A selected node shows the focus ring a selected rule shows.
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const element = editor.getElementByKey(nodeKey);
|
|
86
|
+
const className = editor._config.theme.markdownExtensionSelected;
|
|
87
|
+
if (element == null || typeof className !== 'string' || className === '') {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (isSelected) {
|
|
91
|
+
addClassNamesToElement(element, className);
|
|
92
|
+
} else {
|
|
93
|
+
removeClassNamesFromElement(element, className);
|
|
94
|
+
}
|
|
95
|
+
}, [editor, isSelected, nodeKey]);
|
|
96
|
+
const node = useMemo(() => deriveExtensionNode(facts, plugins), [facts, plugins]);
|
|
97
|
+
let content = node == null ? facts.source : /*#__PURE__*/_jsx(MarkdownPluginNodeRenderer, {
|
|
98
|
+
plugins: plugins,
|
|
99
|
+
node: node
|
|
100
|
+
});
|
|
101
|
+
// Inside emphasis, strong, or strikethrough, the node is drawn inside the
|
|
102
|
+
// same elements, as core Markdown draws it inside the text's.
|
|
103
|
+
for (const [type, Tag] of [...MARKS].reverse()) {
|
|
104
|
+
if ((format & TEXT_TYPE_TO_FORMAT[type]) !== 0) {
|
|
105
|
+
content = /*#__PURE__*/_jsx(Tag, {
|
|
106
|
+
children: content
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return content;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Marks every plugin node so Lexical draws it again with the decorator the
|
|
115
|
+
* editor has now. Not an edit, so it adds nothing to undo history.
|
|
116
|
+
*/
|
|
117
|
+
function redrawExtensionNodes(editor) {
|
|
118
|
+
editor.update(() => {
|
|
119
|
+
for (const {
|
|
120
|
+
node
|
|
121
|
+
} of $dfs()) {
|
|
122
|
+
if ($isRichTextExtensionNode(node)) {
|
|
123
|
+
node.markDirty();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}, {
|
|
127
|
+
tag: HISTORY_MERGE_TAG
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Whether `format` will be on for the text a range selects once it is
|
|
133
|
+
* toggled, as Lexical's formatText decides it from the first selected
|
|
134
|
+
* character; null when the range selects no text, only nodes.
|
|
135
|
+
*/
|
|
136
|
+
function $nextTextFormat(selection, format) {
|
|
137
|
+
const first = $firstSelectedText(selection);
|
|
138
|
+
return first == null ? null : (first.getFormatFlags(format, null) & TEXT_TYPE_TO_FORMAT[format]) !== 0;
|
|
139
|
+
}
|
|
140
|
+
export function MarkdownExtensionsPlugin({
|
|
141
|
+
extensions,
|
|
142
|
+
transformers
|
|
143
|
+
}) {
|
|
144
|
+
const [editor] = useLexicalComposerContext();
|
|
145
|
+
const plugins = useMemo(() => pluginsOf(extensions, transformers), [extensions, transformers]);
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
const unset = setRichTextExtensionNodeDecorator(editor, (facts, format, nodeKey) => /*#__PURE__*/_jsx(ExtensionNodeView, {
|
|
148
|
+
facts: facts,
|
|
149
|
+
format: format,
|
|
150
|
+
nodeKey: nodeKey,
|
|
151
|
+
plugins: plugins
|
|
152
|
+
}));
|
|
153
|
+
redrawExtensionNodes(editor);
|
|
154
|
+
return () => {
|
|
155
|
+
unset();
|
|
156
|
+
redrawExtensionNodes(editor);
|
|
157
|
+
};
|
|
158
|
+
}, [editor, plugins]);
|
|
159
|
+
|
|
160
|
+
// Bold, italic, or strikethrough toggled on a selection takes the plugin
|
|
161
|
+
// nodes in it along, so they stay inside the marks the text around them
|
|
162
|
+
// gets — and a selection of nodes alone toggles them by themselves.
|
|
163
|
+
useEffect(() => editor.registerCommand(FORMAT_TEXT_COMMAND, format => {
|
|
164
|
+
const selection = $getSelection();
|
|
165
|
+
if (!NODE_FORMATS.has(format) || selection == null) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
const nodes = selection.getNodes().filter(node => $isRichTextExtensionNode(node) && node.isInline());
|
|
169
|
+
if (nodes.length === 0) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
// The state Lexical gives the selected text, read before it does;
|
|
173
|
+
// null when no text is selected, only nodes.
|
|
174
|
+
const textState = $isRangeSelection(selection) ? $nextTextFormat(selection, format) : null;
|
|
175
|
+
if ($isRangeSelection(selection)) {
|
|
176
|
+
selection.formatText(format);
|
|
177
|
+
}
|
|
178
|
+
const isOn = textState ?? !nodes.every(node => node.hasFormat(format));
|
|
179
|
+
for (const node of nodes) {
|
|
180
|
+
node.setFormatFlag(format, isOn);
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}, COMMAND_PRIORITY_LOW), [editor]);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SelectAllScopePlugin.d.ts","sourceRoot":"","sources":["../../src/internal/SelectAllScopePlugin.tsx"],"names":[],"mappings":"AAiFA,wBAAgB,oBAAoB,IAAI,IAAI,CAI3C"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file SelectAllScopePlugin.tsx
|
|
7
|
+
* @input Uses @lexical/react (composer context), @lexical/code (CodeNode),
|
|
8
|
+
* @lexical/table (TableCellNode), and lexical (the select-all command and
|
|
9
|
+
* selection).
|
|
10
|
+
* @output Exports SelectAllScopePlugin, which makes Cmd+A (Ctrl+A off Apple
|
|
11
|
+
* platforms) select the content of the code block or table cell holding the
|
|
12
|
+
* caret first, and the whole document on the next press.
|
|
13
|
+
* @position Rendered by RichTextEditor, always on; a read-only or disabled
|
|
14
|
+
* editor keeps the browser's and Lexical's select all. Elsewhere, and when
|
|
15
|
+
* the selection already covers its code block or cell or runs outside it,
|
|
16
|
+
* the press falls through to Lexical's own select all, which takes the
|
|
17
|
+
* whole document.
|
|
18
|
+
*/
|
|
19
|
+
import { useEffect } from 'react';
|
|
20
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
21
|
+
import { $isCodeNode } from '@lexical/code';
|
|
22
|
+
import { $isTableCellNode } from '@lexical/table';
|
|
23
|
+
import { $getSelection, $isBlockFullySelected, $isRangeSelection, COMMAND_PRIORITY_NORMAL, SELECT_ALL_COMMAND } from 'lexical';
|
|
24
|
+
|
|
25
|
+
/** Whether select all stops at `node` before taking the whole document. */
|
|
26
|
+
function $isScope(node) {
|
|
27
|
+
return $isCodeNode(node) || $isTableCellNode(node);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The scopes holding `node`, innermost first. */
|
|
31
|
+
function $scopesOf(node) {
|
|
32
|
+
const scopes = [];
|
|
33
|
+
for (let at = node; at !== null; at = at.getParent()) {
|
|
34
|
+
if ($isScope(at)) {
|
|
35
|
+
scopes.push(at);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return scopes;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Selects the content of the innermost scope that holds the whole selection
|
|
43
|
+
* and is not yet fully selected; false, to let the document be selected,
|
|
44
|
+
* when there is none.
|
|
45
|
+
*/
|
|
46
|
+
function $selectScope() {
|
|
47
|
+
const selection = $getSelection();
|
|
48
|
+
if (!$isRangeSelection(selection)) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const focusScopes = $scopesOf(selection.focus.getNode());
|
|
52
|
+
for (const scope of $scopesOf(selection.anchor.getNode())) {
|
|
53
|
+
if (focusScopes.some(each => each.is(scope)) && !$isBlockFullySelected(scope, selection)) {
|
|
54
|
+
scope.select(0, scope.getChildrenSize());
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
function registerSelectAllScope(editor) {
|
|
61
|
+
return editor.registerCommand(SELECT_ALL_COMMAND, () => editor.isEditable() && $selectScope(), COMMAND_PRIORITY_NORMAL);
|
|
62
|
+
}
|
|
63
|
+
export function SelectAllScopePlugin() {
|
|
64
|
+
const [editor] = useLexicalComposerContext();
|
|
65
|
+
useEffect(() => registerSelectAllScope(editor), [editor]);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Each column's floor in `ch`, from the text length of every cell, header row
|
|
3
|
+
* first: the longest cell wraps to about two lines, the header reads on one
|
|
4
|
+
* line up to its cap, and every floor stays between the bounds.
|
|
5
|
+
*/
|
|
6
|
+
export declare function tableColumnFloors(rows: ReadonlyArray<ReadonlyArray<number>>): Array<number>;
|
|
7
|
+
/**
|
|
8
|
+
* Sets each top-level table's header cells to their column's floor after
|
|
9
|
+
* every update, so the floors follow edits.
|
|
10
|
+
*/
|
|
11
|
+
export declare function TableColumnFloorPlugin(): null;
|
|
12
|
+
//# sourceMappingURL=TableColumnFloorPlugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableColumnFloorPlugin.d.ts","sourceRoot":"","sources":["../../src/internal/TableColumnFloorPlugin.tsx"],"names":[],"mappings":"AA8BA;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,aAAa,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,GACzC,KAAK,CAAC,MAAM,CAAC,CAiBf;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,IAAI,CAsC7C"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file TableColumnFloorPlugin.tsx
|
|
7
|
+
* @input Uses @lexical/react (composer context), @lexical/table, and lexical.
|
|
8
|
+
* @output Exports TableColumnFloorPlugin, which gives each table column core
|
|
9
|
+
* Markdown's readable width floor, and tableColumnFloors, the rule itself.
|
|
10
|
+
* @position Rendered by RichTextEditor and RichTextView. Matches core
|
|
11
|
+
* Markdown's table column floors (spec:AST-061 FR4): automatic table layout
|
|
12
|
+
* already keeps every column at least as wide as its longest word, but a
|
|
13
|
+
* prose column's longest word is short, so a narrow table still wraps one
|
|
14
|
+
* word per line. The floor lets a column's longest cell wrap to about two
|
|
15
|
+
* lines instead, keeps a header label on one line up to a cap, and stays
|
|
16
|
+
* bounded so a table of short columns still fits; a wider table scrolls
|
|
17
|
+
* inside its wrapper. The floors are `ch` on content-box cells, so they
|
|
18
|
+
* follow the reader's font size and cell padding does not eat into them.
|
|
19
|
+
*/
|
|
20
|
+
import { useEffect } from 'react';
|
|
21
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
22
|
+
import { $isTableCellNode, $isTableNode, $isTableRowNode } from '@lexical/table';
|
|
23
|
+
import { $getRoot } from 'lexical';
|
|
24
|
+
const TABLE_COLUMN_MIN_CH = 4;
|
|
25
|
+
const TABLE_COLUMN_MAX_CH = 24;
|
|
26
|
+
const TABLE_COLUMN_TARGET_LINES = 2;
|
|
27
|
+
const TABLE_HEADER_ONE_LINE_MAX_CH = 20;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Each column's floor in `ch`, from the text length of every cell, header row
|
|
31
|
+
* first: the longest cell wraps to about two lines, the header reads on one
|
|
32
|
+
* line up to its cap, and every floor stays between the bounds.
|
|
33
|
+
*/
|
|
34
|
+
export function tableColumnFloors(rows) {
|
|
35
|
+
const [header, ...body] = rows;
|
|
36
|
+
if (header == null) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
return header.map((headerLength, column) => {
|
|
40
|
+
const longest = Math.max(headerLength, ...body.map(row => row[column] ?? 0));
|
|
41
|
+
const bodyFloor = Math.ceil(longest / TABLE_COLUMN_TARGET_LINES);
|
|
42
|
+
const headerFloor = Math.min(headerLength, TABLE_HEADER_ONE_LINE_MAX_CH);
|
|
43
|
+
return Math.min(TABLE_COLUMN_MAX_CH, Math.max(TABLE_COLUMN_MIN_CH, bodyFloor, headerFloor));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Sets each top-level table's header cells to their column's floor after
|
|
49
|
+
* every update, so the floors follow edits.
|
|
50
|
+
*/
|
|
51
|
+
export function TableColumnFloorPlugin() {
|
|
52
|
+
const [editor] = useLexicalComposerContext();
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
let frame = 0;
|
|
55
|
+
const apply = () => {
|
|
56
|
+
frame = 0;
|
|
57
|
+
editor.getEditorState().read(() => {
|
|
58
|
+
for (const table of $getRoot().getChildren().filter($isTableNode)) {
|
|
59
|
+
const rows = table.getChildren().filter($isTableRowNode).map(row => row.getChildren().filter($isTableCellNode));
|
|
60
|
+
const floors = tableColumnFloors(rows.map(cells => cells.map(cell => cell.getTextContentSize())));
|
|
61
|
+
rows[0]?.forEach((cell, column) => {
|
|
62
|
+
const element = editor.getElementByKey(cell.getKey());
|
|
63
|
+
const floor = `${floors[column]}ch`;
|
|
64
|
+
if (element != null && element.style.minWidth !== floor) {
|
|
65
|
+
element.style.minWidth = floor;
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
const schedule = () => {
|
|
72
|
+
if (frame === 0) {
|
|
73
|
+
frame = requestAnimationFrame(apply);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const unregister = editor.registerUpdateListener(schedule);
|
|
77
|
+
schedule();
|
|
78
|
+
return () => {
|
|
79
|
+
cancelAnimationFrame(frame);
|
|
80
|
+
unregister();
|
|
81
|
+
};
|
|
82
|
+
}, [editor]);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pastes a table into a table's cells. A table alone goes in cell by cell
|
|
3
|
+
* (`$pasteTableIntoCells`). A table that comes with other content, such as
|
|
4
|
+
* the paragraph and table a web page copies, goes into the cell with each
|
|
5
|
+
* table as text, since a cell holds no table. Any other paste is left to
|
|
6
|
+
* the editor's own handlers.
|
|
7
|
+
*/
|
|
8
|
+
export declare function TablePastePlugin(): null;
|
|
9
|
+
//# sourceMappingURL=TablePastePlugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TablePastePlugin.d.ts","sourceRoot":"","sources":["../../src/internal/TablePastePlugin.tsx"],"names":[],"mappings":"AAoLA;;;;;;GAMG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAmCvC"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file TablePastePlugin.tsx
|
|
7
|
+
* @input Uses @lexical/react (composer context), @lexical/table,
|
|
8
|
+
* @lexical/utils, and lexical.
|
|
9
|
+
* @output Exports TablePastePlugin, which pastes into a table's cells without
|
|
10
|
+
* dropping any of the paste or emptying a cell.
|
|
11
|
+
* @position Rendered by RichTextEditor beside TablePlugin. It answers a paste
|
|
12
|
+
* that holds a table, made with the selection in one table's cells, ahead of
|
|
13
|
+
* TablePlugin's own handler. That handler refuses a table that comes with
|
|
14
|
+
* other content, and fits a pasted merged cell by merging the cells under
|
|
15
|
+
* it, which drops their text.
|
|
16
|
+
*/
|
|
17
|
+
import { useEffect } from 'react';
|
|
18
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
19
|
+
import { $computeTableCellRectBoundary, $computeTableMap, $computeTableMapSkipCellCheck, $insertTableColumnAtNode, $insertTableRowAtNode, $isTableCellNode, $isTableNode, $isTableSelection } from '@lexical/table';
|
|
20
|
+
import { $dfs, $findMatchingParent } from '@lexical/utils';
|
|
21
|
+
import { $startsInCodeBlock } from "./CodePastePlugin.js";
|
|
22
|
+
import { $createParagraphNode, $createTabNode, $createTextNode, $isRangeSelection, COMMAND_PRIORITY_LOW, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND } from 'lexical';
|
|
23
|
+
|
|
24
|
+
/** A selection that starts and ends in cells of one table. */
|
|
25
|
+
|
|
26
|
+
function $cellSelection(selection) {
|
|
27
|
+
if (!$isRangeSelection(selection) && !$isTableSelection(selection)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const points = selection.getStartEndPoints();
|
|
31
|
+
if (points == null) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const anchorCell = $findMatchingParent(points[0].getNode(), $isTableCellNode);
|
|
35
|
+
const focusCell = $findMatchingParent(points[1].getNode(), $isTableCellNode);
|
|
36
|
+
if (anchorCell == null || focusCell == null) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const table = $findMatchingParent(anchorCell, $isTableNode);
|
|
40
|
+
if (table == null || !table.isParentOf(focusCell)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
selection,
|
|
45
|
+
table,
|
|
46
|
+
anchorCell,
|
|
47
|
+
focusCell
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Writes a pasted table into the table cell by cell, starting at the
|
|
53
|
+
* top-left selected cell and adding the rows and columns it reaches past the
|
|
54
|
+
* table's edge; selected cells bound it instead. Each pasted cell replaces
|
|
55
|
+
* what its cell held. A merged pasted cell fills only the cell where it
|
|
56
|
+
* starts: the rest of its span, like the missing end of a short row, leaves
|
|
57
|
+
* the cells under it as they were, so no cell is merged or emptied.
|
|
58
|
+
*/
|
|
59
|
+
function $pasteTableIntoCells(pasted, {
|
|
60
|
+
selection,
|
|
61
|
+
table,
|
|
62
|
+
anchorCell,
|
|
63
|
+
focusCell
|
|
64
|
+
}) {
|
|
65
|
+
const [grid, anchorEntry, focusEntry] = $computeTableMap(table, anchorCell, focusCell);
|
|
66
|
+
const {
|
|
67
|
+
minRow,
|
|
68
|
+
minColumn,
|
|
69
|
+
maxRow,
|
|
70
|
+
maxColumn
|
|
71
|
+
} = $computeTableCellRectBoundary(grid, anchorEntry, focusEntry);
|
|
72
|
+
const [source] = $computeTableMapSkipCellCheck(pasted, null, null);
|
|
73
|
+
// Pasted rows can differ in length, so the widest one sets the width.
|
|
74
|
+
let rows = source.length;
|
|
75
|
+
let columns = Math.max(0, ...source.map(row => row.length));
|
|
76
|
+
if ($isTableSelection(selection)) {
|
|
77
|
+
rows = Math.min(rows, maxRow - minRow + 1);
|
|
78
|
+
columns = Math.min(columns, maxColumn - minColumn + 1);
|
|
79
|
+
}
|
|
80
|
+
for (let count = grid.length; count < minRow + rows; count++) {
|
|
81
|
+
const [current] = $computeTableMapSkipCellCheck(table, null, null);
|
|
82
|
+
$insertTableRowAtNode(current[current.length - 1][0].cell);
|
|
83
|
+
}
|
|
84
|
+
for (let count = grid[0].length; count < minColumn + columns; count++) {
|
|
85
|
+
const [current] = $computeTableMapSkipCellCheck(table, null, null);
|
|
86
|
+
$insertTableColumnAtNode(current[0][current[0].length - 1].cell, true, false);
|
|
87
|
+
}
|
|
88
|
+
const [target] = $computeTableMapSkipCellCheck(table, null, null);
|
|
89
|
+
for (let row = 0; row < rows; row++) {
|
|
90
|
+
for (let column = 0; column < columns; column++) {
|
|
91
|
+
const entry = source[row][column];
|
|
92
|
+
if (entry == null || entry.startRow !== row || entry.startColumn !== column) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const cell = target[minRow + row][minColumn + column].cell;
|
|
96
|
+
const replaced = cell.getChildren();
|
|
97
|
+
cell.append(...entry.cell.getChildren());
|
|
98
|
+
for (const node of replaced) {
|
|
99
|
+
node.remove();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
target[minRow][minColumn].cell.selectEnd();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A table as text a cell can hold: a paragraph per row, with the text of the
|
|
108
|
+
* row's cells between tabs. A merged cell's text appears once, where the
|
|
109
|
+
* cell starts.
|
|
110
|
+
*/
|
|
111
|
+
function $tableAsParagraphs(table) {
|
|
112
|
+
const [grid] = $computeTableMapSkipCellCheck(table, null, null);
|
|
113
|
+
return grid.map((row, rowIndex) => {
|
|
114
|
+
const paragraph = $createParagraphNode();
|
|
115
|
+
row.forEach((entry, column) => {
|
|
116
|
+
if (column > 0) {
|
|
117
|
+
paragraph.append($createTabNode());
|
|
118
|
+
}
|
|
119
|
+
if (entry.startRow !== rowIndex || entry.startColumn !== column) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const text = entry.cell.getTextContent().replace(/\s+/g, ' ').trim();
|
|
123
|
+
if (text !== '') {
|
|
124
|
+
paragraph.append($createTextNode(text));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
return paragraph;
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** `node` with every table in it, or the node itself if a table, as text. */
|
|
132
|
+
function $withTablesAsText(node) {
|
|
133
|
+
if ($isTableNode(node)) {
|
|
134
|
+
return $tableAsParagraphs(node);
|
|
135
|
+
}
|
|
136
|
+
for (const {
|
|
137
|
+
node: inner
|
|
138
|
+
} of $dfs(node)) {
|
|
139
|
+
if ($isTableNode(inner)) {
|
|
140
|
+
for (const paragraph of $tableAsParagraphs(inner)) {
|
|
141
|
+
inner.insertBefore(paragraph);
|
|
142
|
+
}
|
|
143
|
+
inner.remove();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return [node];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Pastes a table into a table's cells. A table alone goes in cell by cell
|
|
151
|
+
* (`$pasteTableIntoCells`). A table that comes with other content, such as
|
|
152
|
+
* the paragraph and table a web page copies, goes into the cell with each
|
|
153
|
+
* table as text, since a cell holds no table. Any other paste is left to
|
|
154
|
+
* the editor's own handlers.
|
|
155
|
+
*/
|
|
156
|
+
export function TablePastePlugin() {
|
|
157
|
+
const [editor] = useLexicalComposerContext();
|
|
158
|
+
useEffect(() => editor.registerCommand(SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, ({
|
|
159
|
+
nodes,
|
|
160
|
+
selection
|
|
161
|
+
}, dispatchEditor) => {
|
|
162
|
+
// A paste into a code block, even one in a cell, belongs to
|
|
163
|
+
// CodePastePlugin, which adds plain code lines.
|
|
164
|
+
if (dispatchEditor !== editor || $startsInCodeBlock(selection) || !nodes.some(node => $dfs(node).some(({
|
|
165
|
+
node: inner
|
|
166
|
+
}) => $isTableNode(inner)))) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
const cells = $cellSelection(selection);
|
|
170
|
+
if (cells == null) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
if (nodes.length === 1 && $isTableNode(nodes[0])) {
|
|
174
|
+
$pasteTableIntoCells(nodes[0], cells);
|
|
175
|
+
} else {
|
|
176
|
+
cells.selection.insertNodes(nodes.flatMap($withTablesAsText));
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
},
|
|
180
|
+
// Ahead of TablePlugin's handler, which runs at the editor priority.
|
|
181
|
+
COMMAND_PRIORITY_LOW), [editor]);
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A read-only view has no caret, so nothing inside a wide table can scroll it
|
|
3
|
+
* from the keyboard. Like core Table's scroll region, each table's wrapper is
|
|
4
|
+
* a group named "Table" that takes a tab stop only while the table overflows
|
|
5
|
+
* it; a table that fits adds no stop. The tab stop is explicit, so browsers
|
|
6
|
+
* that do not make scroll containers focusable on their own still reach it.
|
|
7
|
+
*/
|
|
8
|
+
export declare function TableScrollRegionPlugin(): null;
|
|
9
|
+
//# sourceMappingURL=TableScrollRegionPlugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TableScrollRegionPlugin.d.ts","sourceRoot":"","sources":["../../src/internal/TableScrollRegionPlugin.tsx"],"names":[],"mappings":"AAmCA;;;;;;GAMG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAgE9C"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file TableScrollRegionPlugin.tsx
|
|
7
|
+
* @input Lexical's TableNode mutations, the scroll wrapper that TablePlugin's
|
|
8
|
+
* horizontal-scroll mode renders around each table, and core's translator
|
|
9
|
+
* for the region's name.
|
|
10
|
+
* @output TableScrollRegionPlugin: gives each table's scroll wrapper the
|
|
11
|
+
* semantics of core Table's scroll region.
|
|
12
|
+
* @position Internal to @blue-patterns/richtext; rendered by RichTextView only.
|
|
13
|
+
* The editor needs none of this: its caret scrolls a wide table, and a tab
|
|
14
|
+
* stop inside the editable surface would break Escape-then-Tab.
|
|
15
|
+
*/
|
|
16
|
+
import { useEffect } from 'react';
|
|
17
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
18
|
+
import { TableNode } from '@lexical/table';
|
|
19
|
+
import { useTranslator } from '@blue-patterns/core/i18n';
|
|
20
|
+
/** A tab stop only while the table is wider than its wrapper. */
|
|
21
|
+
function syncTabStop(wrapper) {
|
|
22
|
+
if (wrapper.scrollWidth > wrapper.clientWidth) {
|
|
23
|
+
wrapper.tabIndex = 0;
|
|
24
|
+
} else {
|
|
25
|
+
wrapper.removeAttribute('tabindex');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A read-only view has no caret, so nothing inside a wide table can scroll it
|
|
31
|
+
* from the keyboard. Like core Table's scroll region, each table's wrapper is
|
|
32
|
+
* a group named "Table" that takes a tab stop only while the table overflows
|
|
33
|
+
* it; a table that fits adds no stop. The tab stop is explicit, so browsers
|
|
34
|
+
* that do not make scroll containers focusable on their own still reach it.
|
|
35
|
+
*/
|
|
36
|
+
export function TableScrollRegionPlugin() {
|
|
37
|
+
const [editor] = useLexicalComposerContext();
|
|
38
|
+
const t = useTranslator();
|
|
39
|
+
const label = t('@astryx.table.label');
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const regions = new Map();
|
|
42
|
+
const release = key => {
|
|
43
|
+
regions.get(key)?.observer?.disconnect();
|
|
44
|
+
regions.delete(key);
|
|
45
|
+
};
|
|
46
|
+
const track = key => {
|
|
47
|
+
// With horizontal scroll active, a table node's DOM is its wrapper.
|
|
48
|
+
const wrapper = editor.getElementByKey(key);
|
|
49
|
+
if (!(wrapper instanceof HTMLDivElement)) {
|
|
50
|
+
release(key);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (regions.get(key)?.wrapper === wrapper) {
|
|
54
|
+
syncTabStop(wrapper);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
release(key);
|
|
58
|
+
wrapper.setAttribute('role', 'group');
|
|
59
|
+
wrapper.setAttribute('aria-label', label);
|
|
60
|
+
let observer = null;
|
|
61
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
62
|
+
observer = new ResizeObserver(() => syncTabStop(wrapper));
|
|
63
|
+
// The wrapper resizes with its container, the table with its content.
|
|
64
|
+
observer.observe(wrapper);
|
|
65
|
+
const table = wrapper.querySelector(':scope > table');
|
|
66
|
+
if (table != null) {
|
|
67
|
+
observer.observe(table);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
regions.set(key, {
|
|
71
|
+
wrapper,
|
|
72
|
+
observer
|
|
73
|
+
});
|
|
74
|
+
syncTabStop(wrapper);
|
|
75
|
+
};
|
|
76
|
+
const unregister = editor.registerMutationListener(TableNode, mutations => {
|
|
77
|
+
for (const [key, mutation] of mutations) {
|
|
78
|
+
if (mutation === 'destroyed') {
|
|
79
|
+
release(key);
|
|
80
|
+
} else {
|
|
81
|
+
track(key);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}, {
|
|
85
|
+
skipInitialization: false
|
|
86
|
+
});
|
|
87
|
+
return () => {
|
|
88
|
+
unregister();
|
|
89
|
+
for (const key of [...regions.keys()]) {
|
|
90
|
+
release(key);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}, [editor, label]);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type JSX } from 'react';
|
|
2
|
+
/** Marks a task item for the theme (editorTheme.ts). */
|
|
3
|
+
export declare const TASK_ATTRIBUTE = "data-richtext-task";
|
|
4
|
+
export declare function TaskCheckboxPlugin({ isReadOnly, }: {
|
|
5
|
+
readonly isReadOnly: boolean;
|
|
6
|
+
}): JSX.Element;
|
|
7
|
+
//# sourceMappingURL=TaskCheckboxPlugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TaskCheckboxPlugin.d.ts","sourceRoot":"","sources":["../../src/internal/TaskCheckboxPlugin.tsx"],"names":[],"mappings":"AA0BA,OAAO,EAA6B,KAAK,GAAG,EAAC,MAAM,OAAO,CAAC;AAmC3D,wDAAwD;AACxD,eAAO,MAAM,cAAc,uBAAuB,CAAC;AAgDnD,wBAAgB,kBAAkB,CAAC,EACjC,UAAU,GACX,EAAE;IACD,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B,GAAG,GAAG,CAAC,OAAO,CAmId"}
|