@astryxdesign/richtext 0.7.12-canary.f543ec7 → 0.7.12
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 +32 -0
- package/README.md +9 -0
- package/dist/CodePastePlugin.d.ts +2 -0
- package/dist/CodePastePlugin.d.ts.map +1 -0
- package/dist/CodePastePlugin.js +103 -0
- package/dist/CodeTabPlugin.d.ts +2 -0
- package/dist/CodeTabPlugin.d.ts.map +1 -0
- package/dist/CodeTabPlugin.js +256 -0
- package/dist/DisabledLinkGuardPlugin.d.ts +4 -0
- package/dist/DisabledLinkGuardPlugin.d.ts.map +1 -0
- package/dist/DisabledLinkGuardPlugin.js +93 -0
- package/dist/RichTextEditor.d.ts +3 -2
- package/dist/RichTextEditor.d.ts.map +1 -1
- package/dist/RichTextEditor.js +17 -7
- package/dist/RichTextEditorAutoLinkPlugin.d.ts +5 -5
- package/dist/RichTextEditorAutoLinkPlugin.d.ts.map +1 -1
- package/dist/RichTextEditorAutoLinkPlugin.js +12 -6
- 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/package.json +5 -5
- package/src/CodePastePlugin.tsx +143 -0
- package/src/CodeTabPlugin.tsx +298 -0
- package/src/DisabledLinkGuardPlugin.tsx +93 -0
- package/src/RichTextEditor.doc.mjs +16 -1
- package/src/RichTextEditor.tsx +18 -4
- package/src/RichTextEditorAutoLinkPlugin.doc.mjs +1 -1
- package/src/RichTextEditorAutoLinkPlugin.tsx +18 -6
- package/src/privateAttributes/layerPortalBoundary.stylex.ts +18 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @astryxdesign/richtext
|
|
2
2
|
|
|
3
|
+
# 0.7.12
|
|
4
|
+
|
|
5
|
+
#### New Features
|
|
6
|
+
|
|
7
|
+
- In `RichTextEditor`, Tab and Shift+Tab in a code block indent and outdent every line the selection touches, by the indent the block already uses (a tab when one of its lines starts with a tab, otherwise the fewest leading spaces of any indented line, kept between 2 and 8, otherwise 2 spaces). The selection stays on the same characters and each press is one undo step. Tab used to put a tab character in place of the selection, and Shift+Tab did the same. Escape then Tab still moves focus out of the editor. (#8579)
|
|
8
|
+
|
|
9
|
+
#### Fixes
|
|
10
|
+
|
|
11
|
+
- Declare renderer-owned DOM boundaries so decorator overlays mount outside managed content without changing safe inline hosting. RichTextEditor also mirrors its `dir` prop onto the wrapper around the editable surface, so the editing surface and its content blocks, the placeholder, UI rendered through `plugins`, and relocated overlays follow `dir`; the editing surface previously kept the surrounding direction. (#7633)
|
|
12
|
+
- `RichTextEditorAutoLinkPlugin` no longer turns a URL inside a code block into a link. A URL typed or pasted into a code block, or one in an imported fence, now stays code. Before, the link nested inside the code block, and the exported Markdown fence lost it. (#10664)
|
|
13
|
+
- In `RichTextEditor`, pasting rich content into a code block inserts plain text: the plain text on the clipboard, or, when there is none, each pasted block as one line of its text. Headings, lists, tables, and formatting used to land inside the code block, where the Markdown export wrote them as plain lines of the fence, so the document read back differently from what the editor showed. (#8580)
|
|
14
|
+
- A link inside a disabled `RichTextEditor` no longer navigates, shows no pointer cursor, and is not a tab stop. Before, a disabled editor's links acted as live links: a click or Enter opened them, and Tab stopped on each one. (#10679)
|
|
15
|
+
- TextInput, TextArea, NumberInput, DateInput, DateRangeInput, DateTimeInput, TimeInput, Tokenizer, Typeahead, PowerSearch, ChatComposer and RichTextEditor: when the primary pointer is coarse (a phone or tablet), the focus look shows at once, with no fade, so a tapped field repaints once while the keyboard rises. With a fine primary pointer, such as a mouse, a click and the Tab key keep the fade. ChatComposer's focus depth also shows at once under reduced motion, as the other fields' focus look already does. (#10611)
|
|
16
|
+
|
|
17
|
+
#### Documentation
|
|
18
|
+
|
|
19
|
+
- `RichTextView`, `RichTextEditorToolbar` and `RichTextEditorAutoLinkPlugin` each ship a doc file, so `astryx component`, `astryx discover` and the docs list their props, defaults and usage. (#10407)
|
|
20
|
+
- A source comment in the editor theme now says that core `Markdown` gives each block the direction of its own text, while the editor keeps blocks in the surface's direction. (#10293)
|
|
21
|
+
- RichTextEditor documents how to switch between reading and editing in place without reflow: set the field's inset and border outside the reading width with `xstyle`, so every paragraph keeps its line count across the switch. A standalone editor keeps its form width. (#10429)
|
|
22
|
+
|
|
23
|
+
#### Contributors
|
|
24
|
+
|
|
25
|
+
Thanks to everyone who contributed to this release:
|
|
26
|
+
|
|
27
|
+
- @cixzhang
|
|
28
|
+
- @emilyhorsman
|
|
29
|
+
- @ernestt
|
|
30
|
+
- @freddymeta
|
|
31
|
+
- @vjeux
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
3
35
|
# 0.7.11
|
|
4
36
|
|
|
5
37
|
#### Fixes
|
package/README.md
CHANGED
|
@@ -71,9 +71,18 @@ if your app has its own reset) loads first.
|
|
|
71
71
|
|
|
72
72
|
## Editing code blocks
|
|
73
73
|
|
|
74
|
+
In a code block, Tab and Shift+Tab indent and outdent every line the selection
|
|
75
|
+
touches, by the indent the block already uses: a tab when one of its lines
|
|
76
|
+
starts with a tab, otherwise the fewest leading spaces of any indented line,
|
|
77
|
+
kept between 2 and 8, otherwise 2 spaces. Press Escape, then Tab, to move focus
|
|
78
|
+
out of the editor.
|
|
79
|
+
|
|
74
80
|
Backspace at the very start of a code block turns it into a paragraph of its
|
|
75
81
|
lines, instead of merging the code into the block above.
|
|
76
82
|
|
|
83
|
+
Pasting rich content into a code block inserts its plain text, one line per
|
|
84
|
+
block: headings, lists, tables, and formatting never land inside the code.
|
|
85
|
+
|
|
77
86
|
## Examples
|
|
78
87
|
|
|
79
88
|
Interactive examples live in Storybook under **RichText/RichTextEditor**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CodePastePlugin.d.ts","sourceRoot":"","sources":["../src/CodePastePlugin.tsx"],"names":[],"mappings":"AAuFA,wBAAgB,eAAe,IAAI,IAAI,CAuDtC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file CodePastePlugin.tsx
|
|
7
|
+
* @input Uses @lexical/react (composer context), @lexical/code (CodeNode), and
|
|
8
|
+
* lexical.
|
|
9
|
+
* @output Exports CodePastePlugin, which makes rich content pasted or dropped
|
|
10
|
+
* into a code block go in as plain lines of code.
|
|
11
|
+
* @position Rendered by RichTextEditor; always on; not exported. A paste
|
|
12
|
+
* whose selection starts in a code block goes in as plain text: the
|
|
13
|
+
* clipboard's plain text, or else one line per pasted block.
|
|
14
|
+
*/
|
|
15
|
+
import { useEffect } from 'react';
|
|
16
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
17
|
+
import { $isCodeNode } from '@lexical/code';
|
|
18
|
+
import { $addUpdateTag, $findMatchingParent, $getSelection, $isElementNode, $isRangeSelection, $isSelectionCapturedInDecoratorInput, COMMAND_PRIORITY_LOW, isDOMNode, mergeRegister, PASTE_COMMAND, PASTE_TAG, SELECTION_INSERT_CLIPBOARD_NODES_COMMAND } from 'lexical';
|
|
19
|
+
|
|
20
|
+
/** Clipboard types that carry more than plain text. */
|
|
21
|
+
const RICH_TYPES = ['text/html', 'application/x-lexical-editor'];
|
|
22
|
+
|
|
23
|
+
/** Whether content inserted at the selection lands in a code block. */
|
|
24
|
+
function $startsInCodeBlock(selection) {
|
|
25
|
+
if (!$isRangeSelection(selection)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const start = selection.isBackward() ? selection.focus : selection.anchor;
|
|
29
|
+
const node = start.getNode();
|
|
30
|
+
// A link (an autolinked URL) can sit between the text and its code block.
|
|
31
|
+
return $findMatchingParent(node, $isCodeNode) !== null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Clipboard nodes as plain lines: each block's text on a line of its own (a
|
|
36
|
+
* paragraph, a heading, a list item, a table cell), and inline content
|
|
37
|
+
* between blocks on one line.
|
|
38
|
+
*/
|
|
39
|
+
function $plainLinesOf(nodes) {
|
|
40
|
+
const lines = [];
|
|
41
|
+
let inline = null;
|
|
42
|
+
const endInline = () => {
|
|
43
|
+
if (inline != null) {
|
|
44
|
+
lines.push(inline);
|
|
45
|
+
inline = null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const visit = node => {
|
|
49
|
+
if (node.isInline()) {
|
|
50
|
+
inline = (inline ?? '') + node.getTextContent();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
endInline();
|
|
54
|
+
if ($isElementNode(node) && node.getChildren().some(child => !child.isInline())) {
|
|
55
|
+
node.getChildren().forEach(visit);
|
|
56
|
+
endInline();
|
|
57
|
+
} else {
|
|
58
|
+
lines.push(node.getTextContent());
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
nodes.forEach(visit);
|
|
62
|
+
endInline();
|
|
63
|
+
return lines.join('\n');
|
|
64
|
+
}
|
|
65
|
+
export function CodePastePlugin() {
|
|
66
|
+
const [editor] = useLexicalComposerContext();
|
|
67
|
+
useEffect(() => mergeRegister(
|
|
68
|
+
// Ahead of the rich-text paste, which inserts the clipboard's rich
|
|
69
|
+
// content when there is any.
|
|
70
|
+
editor.registerCommand(PASTE_COMMAND, event => {
|
|
71
|
+
// A paste into an input a decorator node renders is that input's.
|
|
72
|
+
if (isDOMNode(event.target) && $isSelectionCapturedInDecoratorInput(event.target)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const data = 'clipboardData' in event ? event.clipboardData : null;
|
|
76
|
+
if (!editor.isEditable() || data == null || !RICH_TYPES.some(type => data.types.includes(type))) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const text = data.getData('text/plain');
|
|
80
|
+
const selection = $getSelection();
|
|
81
|
+
if (text === '' || !$startsInCodeBlock(selection)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
// Its own undo step, as every paste is.
|
|
86
|
+
$addUpdateTag(PASTE_TAG);
|
|
87
|
+
selection.insertRawText(text);
|
|
88
|
+
return true;
|
|
89
|
+
}, COMMAND_PRIORITY_LOW),
|
|
90
|
+
// Every insertion of clipboard nodes, a paste with no plain text or a
|
|
91
|
+
// drop, comes through here; ahead of the table's own handler.
|
|
92
|
+
editor.registerCommand(SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, ({
|
|
93
|
+
nodes,
|
|
94
|
+
selection
|
|
95
|
+
}) => {
|
|
96
|
+
if (!$startsInCodeBlock(selection)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
selection.insertRawText($plainLinesOf(nodes));
|
|
100
|
+
return true;
|
|
101
|
+
}, COMMAND_PRIORITY_LOW)), [editor]);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CodeTabPlugin.d.ts","sourceRoot":"","sources":["../src/CodeTabPlugin.tsx"],"names":[],"mappings":"AAwQA,wBAAgB,aAAa,IAAI,IAAI,CAiCpC"}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file CodeTabPlugin.tsx
|
|
7
|
+
* @input Uses @lexical/react (composer context), @lexical/code (CodeNode), and
|
|
8
|
+
* lexical.
|
|
9
|
+
* @output Exports CodeTabPlugin, which makes Tab and Shift+Tab in a code block
|
|
10
|
+
* indent and outdent every line the selection touches, by the indent the
|
|
11
|
+
* block's own lines use.
|
|
12
|
+
* @position Rendered by RichTextEditor; always on, and not exported from the
|
|
13
|
+
* package. A press indents by the block's own indent, so edited code keeps
|
|
14
|
+
* the indentation its author chose; the selection stays on the same
|
|
15
|
+
* characters, and each press is one undo step. It replaces only what Tab
|
|
16
|
+
* did in a code block before: TabIndentationPlugin put a tab character in
|
|
17
|
+
* place of the selection. Its listener sits at the front of the
|
|
18
|
+
* editor-priority queue, ahead of TabIndentationPlugin and behind every
|
|
19
|
+
* low-priority listener, so Escape then Tab still moves focus out of the
|
|
20
|
+
* editor (TabFocusEscapePlugin, WCAG 2.1.2). A selection that reaches
|
|
21
|
+
* outside the code block keeps TabIndentationPlugin's meaning. Read-only
|
|
22
|
+
* and disabled editors are unchanged.
|
|
23
|
+
*/
|
|
24
|
+
import { useEffect } from 'react';
|
|
25
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
26
|
+
import { $isCodeNode } from '@lexical/code';
|
|
27
|
+
import { $addUpdateTag, $createTextNode, $getSelection, $isRangeSelection, $isTabNode, $isTextNode, COMMAND_PRIORITY_BEFORE_EDITOR, HISTORY_PUSH_TAG, KEY_TAB_COMMAND } from 'lexical';
|
|
28
|
+
|
|
29
|
+
/** The narrowest space indent a block uses, and the widest. */
|
|
30
|
+
const MIN_SPACE_INDENT = 2;
|
|
31
|
+
const MAX_SPACE_INDENT = 8;
|
|
32
|
+
function leadingSpaces(line) {
|
|
33
|
+
return /^ */.exec(line)?.[0].length ?? 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The indent a code block's lines use: a tab when any line starts with one;
|
|
38
|
+
* otherwise the fewest spaces an indented line starts with, kept between two
|
|
39
|
+
* and eight; otherwise two spaces.
|
|
40
|
+
*/
|
|
41
|
+
function codeIndentUnit(lines) {
|
|
42
|
+
if (lines.some(line => line.startsWith('\t'))) {
|
|
43
|
+
return '\t';
|
|
44
|
+
}
|
|
45
|
+
const widths = lines.map(leadingSpaces).filter(width => width > 0);
|
|
46
|
+
const width = widths.length === 0 ? MIN_SPACE_INDENT : Math.min(...widths);
|
|
47
|
+
return ' '.repeat(Math.min(MAX_SPACE_INDENT, Math.max(MIN_SPACE_INDENT, width)));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A code block's children, the offset in the block's text where each one
|
|
52
|
+
* starts, and that text. A line ends at a line break node or at a newline
|
|
53
|
+
* inside a text node: imported code holds newlines, and Enter adds line
|
|
54
|
+
* break nodes.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
function $layoutOf(code) {
|
|
58
|
+
const children = code.getChildren();
|
|
59
|
+
const starts = [];
|
|
60
|
+
let text = '';
|
|
61
|
+
for (const child of children) {
|
|
62
|
+
starts.push(text.length);
|
|
63
|
+
text += child.getTextContent();
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
children,
|
|
67
|
+
starts,
|
|
68
|
+
text
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A text node that can take a caret and grow: any but a tab node. */
|
|
73
|
+
function $isPlainText(node) {
|
|
74
|
+
return $isTextNode(node) && !$isTabNode(node);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The code block that holds both ends of the selection, or null. */
|
|
78
|
+
function $selectionCodeBlock(selection) {
|
|
79
|
+
const blockOf = point => {
|
|
80
|
+
const node = point.getNode();
|
|
81
|
+
return $isCodeNode(node) ? node : node.getParent();
|
|
82
|
+
};
|
|
83
|
+
const block = blockOf(selection.anchor);
|
|
84
|
+
return $isCodeNode(block) && block.is(blockOf(selection.focus)) ? block : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Where a point of the block's selection falls in the block's text. */
|
|
88
|
+
function $offsetOf(code, layout, point) {
|
|
89
|
+
if (point.key === code.getKey()) {
|
|
90
|
+
return point.offset < layout.children.length ? layout.starts[point.offset] : layout.text.length;
|
|
91
|
+
}
|
|
92
|
+
const index = layout.children.findIndex(child => child.getKey() === point.key);
|
|
93
|
+
return index < 0 ? 0 : layout.starts[index] + point.offset;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Moves a selection point to an offset in the block's text: into the text
|
|
98
|
+
* node there, or between children where none holds it (beside a tab or a
|
|
99
|
+
* line break, or on an empty line).
|
|
100
|
+
*/
|
|
101
|
+
function $placePoint(code, point, offset) {
|
|
102
|
+
const children = code.getChildren();
|
|
103
|
+
let start = 0;
|
|
104
|
+
for (let index = 0; index < children.length; index++) {
|
|
105
|
+
const child = children[index];
|
|
106
|
+
const size = child.getTextContentSize();
|
|
107
|
+
if ($isPlainText(child) && offset >= start && offset <= start + size) {
|
|
108
|
+
point.set(child.getKey(), offset - start, 'text');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (offset <= start) {
|
|
112
|
+
point.set(code.getKey(), index, 'element');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
start += size;
|
|
116
|
+
}
|
|
117
|
+
point.set(code.getKey(), children.length, 'element');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Inserts text at an offset in the block's text. */
|
|
121
|
+
function $insertAt(code, offset, text) {
|
|
122
|
+
const {
|
|
123
|
+
children,
|
|
124
|
+
starts
|
|
125
|
+
} = $layoutOf(code);
|
|
126
|
+
for (let index = 0; index < children.length; index++) {
|
|
127
|
+
const child = children[index];
|
|
128
|
+
const start = starts[index];
|
|
129
|
+
if ($isPlainText(child) && offset >= start && offset <= start + child.getTextContentSize()) {
|
|
130
|
+
const content = child.getTextContent();
|
|
131
|
+
const at = offset - start;
|
|
132
|
+
child.setTextContent(content.slice(0, at) + text + content.slice(at));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (offset <= start) {
|
|
136
|
+
child.insertBefore($createTextNode(text));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
code.append($createTextNode(text));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Removes `count` characters at an offset in the block's text. */
|
|
144
|
+
function $removeAt(code, offset, count) {
|
|
145
|
+
const {
|
|
146
|
+
children,
|
|
147
|
+
starts
|
|
148
|
+
} = $layoutOf(code);
|
|
149
|
+
let left = count;
|
|
150
|
+
for (let index = 0; index < children.length && left > 0; index++) {
|
|
151
|
+
const child = children[index];
|
|
152
|
+
const size = child.getTextContentSize();
|
|
153
|
+
if (!$isTextNode(child) || starts[index] + size <= offset) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const from = Math.max(0, offset - starts[index]);
|
|
157
|
+
const removed = Math.min(left, size - from);
|
|
158
|
+
const content = child.getTextContent();
|
|
159
|
+
const rest = content.slice(0, from) + content.slice(from + removed);
|
|
160
|
+
if (rest === '') {
|
|
161
|
+
child.remove();
|
|
162
|
+
} else {
|
|
163
|
+
child.setTextContent(rest);
|
|
164
|
+
}
|
|
165
|
+
left -= removed;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Indents, or outdents, every line the selection touches by the block's own
|
|
171
|
+
* indent, and puts the selection back on the characters it held.
|
|
172
|
+
*/
|
|
173
|
+
function $shiftLines(code, selection, isOutdent) {
|
|
174
|
+
const layout = $layoutOf(code);
|
|
175
|
+
const lines = layout.text.split('\n');
|
|
176
|
+
const lineStarts = [];
|
|
177
|
+
let lineStart = 0;
|
|
178
|
+
for (const line of lines) {
|
|
179
|
+
lineStarts.push(lineStart);
|
|
180
|
+
lineStart += line.length + 1;
|
|
181
|
+
}
|
|
182
|
+
const lineAt = offset => {
|
|
183
|
+
let line = 0;
|
|
184
|
+
while (line + 1 < lineStarts.length && lineStarts[line + 1] <= offset) {
|
|
185
|
+
line++;
|
|
186
|
+
}
|
|
187
|
+
return line;
|
|
188
|
+
};
|
|
189
|
+
const unit = codeIndentUnit(lines);
|
|
190
|
+
// A tab-indented block outdents a space-indented line by the narrowest
|
|
191
|
+
// space indent.
|
|
192
|
+
const spaceUnit = unit === '\t' ? MIN_SPACE_INDENT : unit.length;
|
|
193
|
+
const anchor = $offsetOf(code, layout, selection.anchor);
|
|
194
|
+
const focus = $offsetOf(code, layout, selection.focus);
|
|
195
|
+
const start = Math.min(anchor, focus);
|
|
196
|
+
const end = Math.max(anchor, focus);
|
|
197
|
+
const first = lineAt(start);
|
|
198
|
+
let last = lineAt(end);
|
|
199
|
+
// A selection that ends where a line begins does not reach into that line.
|
|
200
|
+
if (last > first && lineStarts[last] === end) {
|
|
201
|
+
last--;
|
|
202
|
+
}
|
|
203
|
+
// Each edit as [offset, characters added (negative when removed)], made
|
|
204
|
+
// from the last line up so the offsets of the lines above stay true.
|
|
205
|
+
const edits = [];
|
|
206
|
+
for (let line = last; line >= first; line--) {
|
|
207
|
+
const at = lineStarts[line];
|
|
208
|
+
if (!isOutdent) {
|
|
209
|
+
$insertAt(code, at, unit);
|
|
210
|
+
edits.push([at, unit.length]);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const removed = lines[line].startsWith('\t') ? 1 : Math.min(leadingSpaces(lines[line]), spaceUnit);
|
|
214
|
+
if (removed > 0) {
|
|
215
|
+
$removeAt(code, at, removed);
|
|
216
|
+
edits.push([at, -removed]);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// A point keeps its character: text added at or before it pushes it on,
|
|
220
|
+
// and a point inside removed indentation moves to its line's start.
|
|
221
|
+
const shifted = offset => edits.reduce((moved, [at, delta]) => {
|
|
222
|
+
if (delta > 0) {
|
|
223
|
+
return offset >= at ? moved + delta : moved;
|
|
224
|
+
}
|
|
225
|
+
return offset > at ? moved - Math.min(-delta, offset - at) : moved;
|
|
226
|
+
}, offset);
|
|
227
|
+
$placePoint(code, selection.anchor, shifted(anchor));
|
|
228
|
+
$placePoint(code, selection.focus, shifted(focus));
|
|
229
|
+
}
|
|
230
|
+
export function CodeTabPlugin() {
|
|
231
|
+
const [editor] = useLexicalComposerContext();
|
|
232
|
+
useEffect(() => editor.registerCommand(KEY_TAB_COMMAND, event => {
|
|
233
|
+
const selection = $getSelection();
|
|
234
|
+
if (!editor.isEditable() || !$isRangeSelection(selection)) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
const code = $selectionCodeBlock(selection);
|
|
238
|
+
if (code == null) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
event.preventDefault();
|
|
242
|
+
// One undo step per press, even when a press adds or removes a
|
|
243
|
+
// single character beside the caret. The push tag starts a step
|
|
244
|
+
// before the press. A dirty block makes history class the press
|
|
245
|
+
// as its own change, so typing after it starts another step.
|
|
246
|
+
$addUpdateTag(HISTORY_PUSH_TAG);
|
|
247
|
+
$shiftLines(code, selection, event.shiftKey);
|
|
248
|
+
code.markDirty();
|
|
249
|
+
return true;
|
|
250
|
+
},
|
|
251
|
+
// After TabFocusEscapePlugin's low-priority listener, which lets an
|
|
252
|
+
// armed Tab move focus, and before TabIndentationPlugin's; see the
|
|
253
|
+
// file header.
|
|
254
|
+
COMMAND_PRIORITY_BEFORE_EDITOR), [editor]);
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DisabledLinkGuardPlugin.d.ts","sourceRoot":"","sources":["../src/DisabledLinkGuardPlugin.tsx"],"names":[],"mappings":"AAoEA,wBAAgB,uBAAuB,CAAC,EAAC,UAAU,EAAC,EAAE;IAAC,UAAU,EAAE,OAAO,CAAA;CAAC,QAwB1E"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file DisabledLinkGuardPlugin.tsx
|
|
7
|
+
* @input The composer's editor, and whether RichTextEditor is disabled
|
|
8
|
+
* @output DisabledLinkGuardPlugin (internal; not re-exported from index.ts):
|
|
9
|
+
* while the editor is disabled, its links add no tab stop and never navigate
|
|
10
|
+
* @position Rendered by RichTextEditor inside its LexicalExtensionComposer
|
|
11
|
+
*
|
|
12
|
+
* A disabled editor is not editable, so the browser treats each link in it as
|
|
13
|
+
* a live link: a tab stop that navigates on a click or on Enter. A disabled
|
|
14
|
+
* control must not navigate, and it keeps the focus order it has when enabled,
|
|
15
|
+
* where its links are not tab stops.
|
|
16
|
+
*/
|
|
17
|
+
import { useEffect } from 'react';
|
|
18
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
19
|
+
|
|
20
|
+
// Enter on a focused link and a screen reader's activation arrive as a click.
|
|
21
|
+
// A middle click arrives as auxclick.
|
|
22
|
+
function preventLinkActivation(event) {
|
|
23
|
+
if (event.target instanceof Element && event.target.closest('a[href]')) {
|
|
24
|
+
event.preventDefault();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Guards the links under one root element. It records the links it changes,
|
|
30
|
+
* so it restores only those.
|
|
31
|
+
*/
|
|
32
|
+
function createLinkGuard(root) {
|
|
33
|
+
const guarded = new Set();
|
|
34
|
+
const guard = () => {
|
|
35
|
+
for (const link of root.querySelectorAll('a[href]')) {
|
|
36
|
+
if (!guarded.has(link)) {
|
|
37
|
+
guarded.add(link);
|
|
38
|
+
link.setAttribute('tabindex', '-1');
|
|
39
|
+
// Assistive technology announces each link as disabled. Only the
|
|
40
|
+
// link ignores the pointer, so a wide table still scrolls.
|
|
41
|
+
link.setAttribute('aria-disabled', 'true');
|
|
42
|
+
if (link instanceof HTMLElement) {
|
|
43
|
+
link.style.pointerEvents = 'none';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
guard();
|
|
49
|
+
root.addEventListener('click', preventLinkActivation, true);
|
|
50
|
+
root.addEventListener('auxclick', preventLinkActivation, true);
|
|
51
|
+
return {
|
|
52
|
+
guard,
|
|
53
|
+
release() {
|
|
54
|
+
for (const link of guarded) {
|
|
55
|
+
link.removeAttribute('tabindex');
|
|
56
|
+
link.removeAttribute('aria-disabled');
|
|
57
|
+
if (link instanceof HTMLElement) {
|
|
58
|
+
link.style.removeProperty('pointer-events');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
guarded.clear();
|
|
62
|
+
root.removeEventListener('click', preventLinkActivation, true);
|
|
63
|
+
root.removeEventListener('auxclick', preventLinkActivation, true);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function DisabledLinkGuardPlugin({
|
|
68
|
+
isDisabled
|
|
69
|
+
}) {
|
|
70
|
+
const [editor] = useLexicalComposerContext();
|
|
71
|
+
// Synchronizes the editor's link elements, which Lexical renders and owns,
|
|
72
|
+
// with the disabled state: guarded while disabled, restored when enabled.
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!isDisabled) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
let linkGuard = null;
|
|
78
|
+
const unregisterRoot = editor.registerRootListener(next => {
|
|
79
|
+
linkGuard?.release();
|
|
80
|
+
linkGuard = next == null ? null : createLinkGuard(next);
|
|
81
|
+
});
|
|
82
|
+
// A new or changed document renders new link elements.
|
|
83
|
+
const unregisterUpdate = editor.registerUpdateListener(() => {
|
|
84
|
+
linkGuard?.guard();
|
|
85
|
+
});
|
|
86
|
+
return () => {
|
|
87
|
+
unregisterUpdate();
|
|
88
|
+
unregisterRoot();
|
|
89
|
+
linkGuard?.release();
|
|
90
|
+
};
|
|
91
|
+
}, [editor, isDisabled]);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
package/dist/RichTextEditor.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file RichTextEditor.tsx
|
|
3
|
-
* @input Uses React, useId, Lexical (lexical + @lexical/react, composed through
|
|
3
|
+
* @input Uses React, useId, a declared DOM boundary, Lexical (lexical + @lexical/react, composed through
|
|
4
4
|
* LexicalExtensionComposer), Field, VisuallyHidden, useInputStatusIcon,
|
|
5
5
|
* mergeProps, design tokens
|
|
6
6
|
* @output Exports an accessibly labelled RichTextEditor component with a flush
|
|
@@ -122,7 +122,8 @@ export interface RichTextEditorProps extends Omit<BaseProps, 'onChange' | 'defau
|
|
|
122
122
|
isReadOnly?: boolean;
|
|
123
123
|
/**
|
|
124
124
|
* Whether the editor is disabled: non-editable, dimmed, out of the tab
|
|
125
|
-
* order, and announced as disabled.
|
|
125
|
+
* order, and announced as disabled. Its links do not navigate and are not
|
|
126
|
+
* tab stops. Wins when `isReadOnly` is also set.
|
|
126
127
|
* Takes effect when changed after mount.
|
|
127
128
|
* @default false
|
|
128
129
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../src/RichTextEditor.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAOL,KAAK,SAAS,EACd,KAAK,GAAG,EACT,MAAM,OAAO,CAAC;AASf,OAAO,EAML,KAAK,kBAAkB,EACxB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAIlD,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"RichTextEditor.d.ts","sourceRoot":"","sources":["../src/RichTextEditor.tsx"],"names":[],"mappings":"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"}
|
package/dist/RichTextEditor.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @file RichTextEditor.tsx
|
|
7
|
-
* @input Uses React, useId, Lexical (lexical + @lexical/react, composed through
|
|
7
|
+
* @input Uses React, useId, a declared DOM boundary, Lexical (lexical + @lexical/react, composed through
|
|
8
8
|
* LexicalExtensionComposer), Field, VisuallyHidden, useInputStatusIcon,
|
|
9
9
|
* mergeProps, design tokens
|
|
10
10
|
* @output Exports an accessibly labelled RichTextEditor component with a flush
|
|
@@ -51,9 +51,12 @@ import { TextSemanticsExtension } from "./textSemantics.js";
|
|
|
51
51
|
import { TableColumnFloorPlugin } from "./TableColumnFloorPlugin.js";
|
|
52
52
|
import { CodeBackspacePlugin } from "./CodeBackspacePlugin.js";
|
|
53
53
|
import { CodeBlockHeaderPlugin } from "./CodeBlockHeaderPlugin.js";
|
|
54
|
+
import { CodePastePlugin } from "./CodePastePlugin.js";
|
|
54
55
|
import { CodeSyntaxPlugin } from "./CodeSyntaxPlugin.js";
|
|
56
|
+
import { CodeTabPlugin } from "./CodeTabPlugin.js";
|
|
55
57
|
import { MarkdownExtensionsPlugin } from "./MarkdownExtensionsPlugin.js";
|
|
56
58
|
import { TaskCheckboxPlugin } from "./TaskCheckboxPlugin.js";
|
|
59
|
+
import { DisabledLinkGuardPlugin } from "./DisabledLinkGuardPlugin.js";
|
|
57
60
|
import { SelectAllScopePlugin } from "./SelectAllScopePlugin.js";
|
|
58
61
|
import { ImageSourceCaretPlugin } from "./ImageSourceCaretPlugin.js";
|
|
59
62
|
import { $generateHtmlFromNodes } from '@lexical/html';
|
|
@@ -61,6 +64,7 @@ import { DEFAULT_NODES } from "./editorNodes.js";
|
|
|
61
64
|
import { $exportMarkdownKeepingSource } from "./markdownSource.js";
|
|
62
65
|
import { DEFAULT_TRANSFORMERS } from "./markdownTable.js";
|
|
63
66
|
import { BLUR_COMMAND, COMMAND_PRIORITY_LOW, defineExtension, KEY_DOWN_COMMAND, KEY_ESCAPE_COMMAND, KEY_TAB_COMMAND, mergeRegister } from 'lexical';
|
|
67
|
+
import { rendererManagedRootProps } from "./privateAttributes/layerPortalBoundary.stylex.js";
|
|
64
68
|
|
|
65
69
|
/**
|
|
66
70
|
* Serialized state for an empty editor: a root containing a single empty
|
|
@@ -295,7 +299,7 @@ export const RichTextEditor = function RichTextEditor({
|
|
|
295
299
|
...mergeProps(themeProps('rich-text-editor', {
|
|
296
300
|
size,
|
|
297
301
|
status: status?.type ?? null
|
|
298
|
-
}), stylex.props(inputWrapperStyles.base, styles.wrapper,
|
|
302
|
+
}), stylex.props(inputWrapperStyles.base, inputWrapperStyles.textField, styles.wrapper,
|
|
299
303
|
// Only disabled is dimmed, matching TextArea: a read-only editor
|
|
300
304
|
// keeps full-opacity text and stays keyboard-reachable.
|
|
301
305
|
isDisabled && inputWrapperStyles.disabled, isDisabled && styles.disabled, status && inputStatusBorderStyles[status.type], status && !isDisabled && !isReadOnly && inputStatusHoverShadowStyles[status.type], status && inputStatusFocusWithinStyles[status.type], xstyle), className, style),
|
|
@@ -333,6 +337,7 @@ export const RichTextEditor = function RichTextEditor({
|
|
|
333
337
|
...{
|
|
334
338
|
className: "x1n2onr6 xh8yej3 x9ynric xjm74w1 xf9zqsd xw6l6zx"
|
|
335
339
|
},
|
|
340
|
+
dir: rest.dir,
|
|
336
341
|
children: [/*#__PURE__*/_jsx(RichTextPlugin, {
|
|
337
342
|
contentEditable: /*#__PURE__*/_jsx(EditorContentEditable, {
|
|
338
343
|
id: inputID,
|
|
@@ -352,7 +357,9 @@ export const RichTextEditor = function RichTextEditor({
|
|
|
352
357
|
ErrorBoundary: LexicalErrorBoundary
|
|
353
358
|
}), /*#__PURE__*/_jsx(HistoryPlugin, {}), /*#__PURE__*/_jsx(ListPlugin, {}), /*#__PURE__*/_jsx(TaskCheckboxPlugin, {
|
|
354
359
|
isReadOnly: isReadOnly || isDisabled
|
|
355
|
-
}), /*#__PURE__*/_jsx(LinkPlugin, {}), /*#__PURE__*/_jsx(
|
|
360
|
+
}), /*#__PURE__*/_jsx(LinkPlugin, {}), /*#__PURE__*/_jsx(DisabledLinkGuardPlugin, {
|
|
361
|
+
isDisabled: isDisabled
|
|
362
|
+
}), /*#__PURE__*/_jsx(TablePlugin, {
|
|
356
363
|
hasCellMerge: false,
|
|
357
364
|
hasCellBackgroundColor: false,
|
|
358
365
|
hasTabHandler: false,
|
|
@@ -361,10 +368,10 @@ export const RichTextEditor = function RichTextEditor({
|
|
|
361
368
|
codeLanguages: codeLanguages
|
|
362
369
|
}), /*#__PURE__*/_jsx(CodeSyntaxPlugin, {
|
|
363
370
|
codeLanguages: codeLanguages
|
|
364
|
-
}), /*#__PURE__*/_jsx(CodeBackspacePlugin, {}), markdownExtensions != null && markdownExtensions.length > 0 ? /*#__PURE__*/_jsx(MarkdownExtensionsPlugin, {
|
|
371
|
+
}), /*#__PURE__*/_jsx(CodeBackspacePlugin, {}), /*#__PURE__*/_jsx(CodePastePlugin, {}), markdownExtensions != null && markdownExtensions.length > 0 ? /*#__PURE__*/_jsx(MarkdownExtensionsPlugin, {
|
|
365
372
|
extensions: markdownExtensions,
|
|
366
373
|
transformers: markdownTransformers
|
|
367
|
-
}) : null, /*#__PURE__*/_jsx(TabIndentationPlugin, {}), /*#__PURE__*/_jsx(TabFocusEscapePlugin, {}), /*#__PURE__*/_jsx(SelectAllScopePlugin, {}), /*#__PURE__*/_jsx(ImageSourceCaretPlugin, {}), hasMarkdownShortcuts && /*#__PURE__*/_jsx(MarkdownShortcutPlugin, {
|
|
374
|
+
}) : null, /*#__PURE__*/_jsx(CodeTabPlugin, {}), /*#__PURE__*/_jsx(TabIndentationPlugin, {}), /*#__PURE__*/_jsx(TabFocusEscapePlugin, {}), /*#__PURE__*/_jsx(SelectAllScopePlugin, {}), /*#__PURE__*/_jsx(ImageSourceCaretPlugin, {}), hasMarkdownShortcuts && /*#__PURE__*/_jsx(MarkdownShortcutPlugin, {
|
|
368
375
|
transformers: markdownTransformers
|
|
369
376
|
}), hasAutoFocus && /*#__PURE__*/_jsx(AutoFocusOnMount, {}), onChange && /*#__PURE__*/_jsx(OnChangePlugin, {
|
|
370
377
|
onChange: onChange,
|
|
@@ -437,7 +444,8 @@ const ESCAPE_REARM_EXEMPT_KEYS = new Set(['Escape', 'Tab', 'Shift', 'Control', '
|
|
|
437
444
|
* listeners from the highest priority down, stopping at the first one that
|
|
438
445
|
* returns true. Registering at COMMAND_PRIORITY_LOW (1) therefore runs first;
|
|
439
446
|
* when the escape is armed we return true WITHOUT calling
|
|
440
|
-
* `event.preventDefault()`, so the indentation
|
|
447
|
+
* `event.preventDefault()`, so the indentation handlers (TabIndentationPlugin,
|
|
448
|
+
* and CodeTabPlugin at the front of the same priority) never see the event
|
|
441
449
|
* and the browser performs its default Tab focus navigation.
|
|
442
450
|
*/
|
|
443
451
|
function TabFocusEscapePlugin() {
|
|
@@ -621,7 +629,9 @@ function EditorContentEditable({
|
|
|
621
629
|
'aria-readonly': 'true'
|
|
622
630
|
} : null),
|
|
623
631
|
...stylex.props(styles.contentEditable, dynamicStyles.contentEditableMinHeight(minHeight)),
|
|
624
|
-
...rest
|
|
632
|
+
...rest,
|
|
633
|
+
// Lexical reconciles this DOM, so Layer keeps corrective portals out.
|
|
634
|
+
...rendererManagedRootProps
|
|
625
635
|
};
|
|
626
636
|
if (placeholderText) {
|
|
627
637
|
return /*#__PURE__*/_jsx(ContentEditable, {
|