@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,193 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file TaskCheckboxPlugin.tsx
|
|
7
|
+
* @input Uses @lexical/react (composer context), @lexical/list, lexical, and
|
|
8
|
+
* core CheckboxInput.
|
|
9
|
+
* @output Exports TaskCheckboxPlugin, which draws a real checkbox beside every
|
|
10
|
+
* task list item: toggling it, or Mod+Enter in the item, checks the item in
|
|
11
|
+
* the editor, and it is read-only in RichTextView.
|
|
12
|
+
* @position Rendered by RichTextEditor and RichTextView inside the positioned
|
|
13
|
+
* element that holds the content editable, like core Markdown's task lists
|
|
14
|
+
* (spec:AST-061 FR5). A list item cannot take the checkbox role (Lexical's
|
|
15
|
+
* own checkbox lists give it one), and nothing can be added inside the
|
|
16
|
+
* editable text; so the item stays a list item, marked for the theme to
|
|
17
|
+
* make room, and the checkbox is a sibling of the editable text, positioned
|
|
18
|
+
* over the space at the start of the item. Plain items in the same list
|
|
19
|
+
* keep their markers. The item owns its checkbox (`aria-owns`), so
|
|
20
|
+
* assistive technology finds the checkbox and its state inside the item, in
|
|
21
|
+
* document order. The checkboxes take no tab stop of their own, which would
|
|
22
|
+
* come after the whole document: in the editor, Mod+Enter checks the item
|
|
23
|
+
* holding the caret, and in the view they are read-only.
|
|
24
|
+
*/
|
|
25
|
+
import * as stylex from '@stylexjs/stylex';
|
|
26
|
+
import { useEffect, useId, useState } from 'react';
|
|
27
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
28
|
+
import { $isListItemNode, $isListNode } from '@lexical/list';
|
|
29
|
+
import { $getNodeByKey, $getRoot } from 'lexical';
|
|
30
|
+
import { CheckboxInput } from '@blue-patterns/core/CheckboxInput';
|
|
31
|
+
import { $getTaskState, $setTaskState, registerTaskListEditing } from "../markdownTaskList.js";
|
|
32
|
+
|
|
33
|
+
/** Where one task item's checkbox goes, in the container's coordinates. */
|
|
34
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
35
|
+
const styles = {
|
|
36
|
+
checkbox: {
|
|
37
|
+
kVAEAm: "x10l6tqk",
|
|
38
|
+
$$css: true
|
|
39
|
+
},
|
|
40
|
+
placement: (top, left) => [{
|
|
41
|
+
k87sOh: top != null ? "xjbys53" : top,
|
|
42
|
+
kbCHJM: left != null ? "xgq9j65" : left,
|
|
43
|
+
$$css: true
|
|
44
|
+
}, {
|
|
45
|
+
"--x-top": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(top),
|
|
46
|
+
"--x-left": (val => typeof val === "number" ? val + "px" : val != null ? val : undefined)(left)
|
|
47
|
+
}]
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** The width of core CheckboxInput's small control. */
|
|
51
|
+
const CHECKBOX_SIZE = 20;
|
|
52
|
+
|
|
53
|
+
/** Marks a task item for the theme (editorTheme.ts). */
|
|
54
|
+
export const TASK_ATTRIBUTE = 'data-richtext-task';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Every bulleted list item with text of its own, in document order, and
|
|
58
|
+
* whether it is a task (markdownTaskList.ts).
|
|
59
|
+
*/
|
|
60
|
+
function $bulletItems() {
|
|
61
|
+
const items = [];
|
|
62
|
+
const visit = node => {
|
|
63
|
+
if (!$isListNode(node)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const item of node.getChildren()) {
|
|
67
|
+
if (!$isListItemNode(item)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const isWrapper = item.getChildrenSize() === 1 && $isListNode(item.getFirstChild());
|
|
71
|
+
if (!isWrapper && node.getListType() !== 'number') {
|
|
72
|
+
items.push({
|
|
73
|
+
item,
|
|
74
|
+
task: $getTaskState(item)
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
item.getChildren().forEach(visit);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
$getRoot().getChildren().forEach(visit);
|
|
81
|
+
return items;
|
|
82
|
+
}
|
|
83
|
+
function samePlacements(a, b) {
|
|
84
|
+
return a.length === b.length && a.every((placement, index) => placement.key === b[index].key && placement.checked === b[index].checked && placement.label === b[index].label && placement.top === b[index].top && placement.left === b[index].left);
|
|
85
|
+
}
|
|
86
|
+
export function TaskCheckboxPlugin({
|
|
87
|
+
isReadOnly
|
|
88
|
+
}) {
|
|
89
|
+
const [editor] = useLexicalComposerContext();
|
|
90
|
+
const idPrefix = useId();
|
|
91
|
+
const [placements, setPlacements] = useState([]);
|
|
92
|
+
|
|
93
|
+
// Enter and Mod+Enter in task items (markdownTaskList.ts).
|
|
94
|
+
useEffect(() => isReadOnly ? undefined : registerTaskListEditing(editor), [editor, isReadOnly]);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
let frame = 0;
|
|
97
|
+
const measure = () => {
|
|
98
|
+
frame = 0;
|
|
99
|
+
const root = editor.getRootElement();
|
|
100
|
+
const container = root?.parentElement;
|
|
101
|
+
if (root == null || container == null) {
|
|
102
|
+
setPlacements(current => current.length === 0 ? current : []);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const origin = container.getBoundingClientRect();
|
|
106
|
+
const next = editor.getEditorState().read(() => $bulletItems().flatMap(({
|
|
107
|
+
item,
|
|
108
|
+
task
|
|
109
|
+
}) => {
|
|
110
|
+
const element = editor.getElementByKey(item.getKey());
|
|
111
|
+
if (element == null) {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
if (task == null) {
|
|
115
|
+
// A plain item keeps its marker and owns no checkbox.
|
|
116
|
+
element.removeAttribute(TASK_ATTRIBUTE);
|
|
117
|
+
element.removeAttribute('aria-owns');
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
// The checkbox below carries the item's state, so the item keeps
|
|
121
|
+
// the list item role Lexical replaces, and owns the checkbox. The
|
|
122
|
+
// attribute lets the theme drop its marker and make room.
|
|
123
|
+
element.setAttribute(TASK_ATTRIBUTE, '');
|
|
124
|
+
element.removeAttribute('role');
|
|
125
|
+
element.removeAttribute('aria-checked');
|
|
126
|
+
element.removeAttribute('tabindex');
|
|
127
|
+
element.setAttribute('aria-owns', `${idPrefix}-task-${item.getKey()}`);
|
|
128
|
+
const box = element.getBoundingClientRect();
|
|
129
|
+
const style = getComputedStyle(element);
|
|
130
|
+
const isRightToLeft = style.direction === 'rtl';
|
|
131
|
+
return [{
|
|
132
|
+
key: item.getKey(),
|
|
133
|
+
checked: task === 'checked',
|
|
134
|
+
label: item.getTextContent(),
|
|
135
|
+
top: box.top - origin.top + parseFloat(style.paddingTop || '0'),
|
|
136
|
+
left: (isRightToLeft ? box.right - CHECKBOX_SIZE : box.left) - origin.left
|
|
137
|
+
}];
|
|
138
|
+
}));
|
|
139
|
+
setPlacements(current => samePlacements(current, next) ? current : next);
|
|
140
|
+
};
|
|
141
|
+
const schedule = () => {
|
|
142
|
+
if (frame === 0) {
|
|
143
|
+
frame = requestAnimationFrame(measure);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
|
|
147
|
+
const unregisterRoot = editor.registerRootListener(root => {
|
|
148
|
+
observer?.disconnect();
|
|
149
|
+
if (root != null) {
|
|
150
|
+
observer?.observe(root);
|
|
151
|
+
}
|
|
152
|
+
schedule();
|
|
153
|
+
});
|
|
154
|
+
const unregisterUpdate = editor.registerUpdateListener(schedule);
|
|
155
|
+
window.addEventListener('resize', schedule);
|
|
156
|
+
schedule();
|
|
157
|
+
return () => {
|
|
158
|
+
cancelAnimationFrame(frame);
|
|
159
|
+
observer?.disconnect();
|
|
160
|
+
unregisterRoot();
|
|
161
|
+
unregisterUpdate();
|
|
162
|
+
window.removeEventListener('resize', schedule);
|
|
163
|
+
};
|
|
164
|
+
}, [editor, idPrefix]);
|
|
165
|
+
return /*#__PURE__*/_jsx(_Fragment, {
|
|
166
|
+
children: placements.map(placement => /*#__PURE__*/_jsx("div", {
|
|
167
|
+
id: `${idPrefix}-task-${placement.key}`,
|
|
168
|
+
"data-richtext-task-checkbox": "",
|
|
169
|
+
...stylex.props(styles.checkbox, styles.placement(placement.top, placement.left)),
|
|
170
|
+
children: /*#__PURE__*/_jsx(CheckboxInput, {
|
|
171
|
+
ref: input => {
|
|
172
|
+
// No tab stop after the document; see the file header.
|
|
173
|
+
if (input != null) {
|
|
174
|
+
input.tabIndex = -1;
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
size: "sm",
|
|
178
|
+
label: placement.label,
|
|
179
|
+
isLabelHidden: true,
|
|
180
|
+
value: placement.checked,
|
|
181
|
+
isReadOnly: isReadOnly,
|
|
182
|
+
onChange: checked => {
|
|
183
|
+
editor.update(() => {
|
|
184
|
+
const node = $getNodeByKey(placement.key);
|
|
185
|
+
if ($isListItemNode(node)) {
|
|
186
|
+
$setTaskState(node, checked ? 'checked' : 'unchecked');
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
}, placement.key))
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file ToolbarLinkPopover.tsx
|
|
3
|
+
* @input Uses React, @lexical/link, the lexical selection API, core Popover,
|
|
4
|
+
* useTranslator, linkEditing's toLinkHref, and linkForm's LinkForm.
|
|
5
|
+
* @output Exports useToolbarLink, the link popover's state and actions for an
|
|
6
|
+
* editor, ToolbarLinkPopover, the non-modal popover that holds the URL
|
|
7
|
+
* form, and isInsertLink, the Cmd/Ctrl+K test.
|
|
8
|
+
* @position Internal to @blue-patterns/richtext; the package entry exports
|
|
9
|
+
* none of it. A toolbar holds one link popover, anchored to its own Link
|
|
10
|
+
* button, so every Link button opens the same form with the same rules.
|
|
11
|
+
*
|
|
12
|
+
* SYNC: When modified, update:
|
|
13
|
+
* - /packages/richtext/src/RichTextEditorToolbar.tsx (its Link button)
|
|
14
|
+
* - /packages/richtext/src/RichTextEditorToolbarLink.test.tsx (tests)
|
|
15
|
+
*/
|
|
16
|
+
import { type RefObject } from 'react';
|
|
17
|
+
import { type LexicalEditor } from 'lexical';
|
|
18
|
+
/**
|
|
19
|
+
* Whether `event` is the insert-link shortcut (Cmd/Ctrl+K). Uses Lexical's
|
|
20
|
+
* `isExactShortcutMatch`, which — unlike a loose `metaKey || ctrlKey` check —
|
|
21
|
+
* requires exactly the primary modifier and rejects the combo when other
|
|
22
|
+
* modifiers (Shift/Alt) are also held. Matches the playground's `isInsertLink`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isInsertLink(event: KeyboardEvent): boolean;
|
|
25
|
+
/** The link popover's state and actions for one editor. */
|
|
26
|
+
export interface ToolbarLink {
|
|
27
|
+
/** Whether the popover is open. */
|
|
28
|
+
readonly isOpen: boolean;
|
|
29
|
+
/** Whether it opened for a link, which Remove then unlinks. */
|
|
30
|
+
readonly isEditing: boolean;
|
|
31
|
+
/** The field's value: the address being entered. */
|
|
32
|
+
readonly url: string;
|
|
33
|
+
/** The message for an address the rules refused, or empty. */
|
|
34
|
+
readonly error: string;
|
|
35
|
+
/**
|
|
36
|
+
* Opens the popover for the selection. With `promptForUrl`, asks it
|
|
37
|
+
* instead, or unlinks a selection that is in a link.
|
|
38
|
+
*/
|
|
39
|
+
readonly toggle: () => void;
|
|
40
|
+
/** The popover's own open and close reports. */
|
|
41
|
+
readonly onOpenChange: (open: boolean) => void;
|
|
42
|
+
readonly onUrlChange: (url: string) => void;
|
|
43
|
+
/** Links the selection to the field's address; false when refused. */
|
|
44
|
+
readonly apply: () => boolean;
|
|
45
|
+
readonly remove: () => void;
|
|
46
|
+
readonly cancel: () => void;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The link popover's state for `editor`. It remembers the selection the
|
|
50
|
+
* popover opened for, because focus in the field takes the page's selection
|
|
51
|
+
* from the editor, and puts it back before linking, unlinking, or giving
|
|
52
|
+
* focus back to the editor. `buttonRef` is the Link button, which opening the
|
|
53
|
+
* popover scrolls into view.
|
|
54
|
+
*/
|
|
55
|
+
export declare function useToolbarLink(editor: LexicalEditor, { buttonRef, hasNewTabLinks, promptForUrl, }: {
|
|
56
|
+
buttonRef: RefObject<HTMLElement | null>;
|
|
57
|
+
hasNewTabLinks: boolean;
|
|
58
|
+
promptForUrl?: (selectedText: string) => string | null | undefined;
|
|
59
|
+
}): ToolbarLink;
|
|
60
|
+
/**
|
|
61
|
+
* The non-modal popover a Link button opens, holding the URL form. It anchors
|
|
62
|
+
* to `anchorRef`, an element around the button that a press never scales,
|
|
63
|
+
* and finds the button inside it: it sets the button's popup state and opens
|
|
64
|
+
* on a press of it. Render it beside the toolbar that holds the button, not
|
|
65
|
+
* inside it: the toolbar is one roving tab stop, which would leave the form's
|
|
66
|
+
* controls out of Tab and take its arrow keys.
|
|
67
|
+
*/
|
|
68
|
+
export declare function ToolbarLinkPopover({ link, anchorRef, size, }: {
|
|
69
|
+
link: ToolbarLink;
|
|
70
|
+
anchorRef: RefObject<HTMLElement | null>;
|
|
71
|
+
/** The toolbar's size, so the form's controls match its buttons. */
|
|
72
|
+
size: 'sm' | 'md' | 'lg';
|
|
73
|
+
}): import("react").JSX.Element;
|
|
74
|
+
//# sourceMappingURL=ToolbarLinkPopover.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ToolbarLinkPopover.d.ts","sourceRoot":"","sources":["../../src/internal/ToolbarLinkPopover.tsx"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAA2C,KAAK,SAAS,EAAC,MAAM,OAAO,CAAC;AAK/E,OAAO,EASL,KAAK,aAAa,EAEnB,MAAM,SAAS,CAAC;AAWjB;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAE1D;AAWD,2DAA2D;AAC3D,MAAM,WAAW,WAAW;IAC1B,mCAAmC;IACnC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,oDAAoD;IACpD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC;IAC5B,gDAAgD;IAChD,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/C,QAAQ,CAAC,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,sEAAsE;IACtE,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,aAAa,EACrB,EACE,SAAS,EACT,cAAc,EACd,YAAY,GACb,EAAE;IACD,SAAS,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACzC,cAAc,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;CACpE,GACA,WAAW,CAiOb;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,SAAS,EACT,IAAI,GACL,EAAE;IACD,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IACzC,oEAAoE;IACpE,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;CAC1B,+BAuCA"}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
'use client';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @file ToolbarLinkPopover.tsx
|
|
7
|
+
* @input Uses React, @lexical/link, the lexical selection API, core Popover,
|
|
8
|
+
* useTranslator, linkEditing's toLinkHref, and linkForm's LinkForm.
|
|
9
|
+
* @output Exports useToolbarLink, the link popover's state and actions for an
|
|
10
|
+
* editor, ToolbarLinkPopover, the non-modal popover that holds the URL
|
|
11
|
+
* form, and isInsertLink, the Cmd/Ctrl+K test.
|
|
12
|
+
* @position Internal to @blue-patterns/richtext; the package entry exports
|
|
13
|
+
* none of it. A toolbar holds one link popover, anchored to its own Link
|
|
14
|
+
* button, so every Link button opens the same form with the same rules.
|
|
15
|
+
*
|
|
16
|
+
* SYNC: When modified, update:
|
|
17
|
+
* - /packages/richtext/src/RichTextEditorToolbar.tsx (its Link button)
|
|
18
|
+
* - /packages/richtext/src/RichTextEditorToolbarLink.test.tsx (tests)
|
|
19
|
+
*/
|
|
20
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
21
|
+
import { mergeRegister } from '@lexical/utils';
|
|
22
|
+
import { TOGGLE_LINK_COMMAND, $isLinkNode, $createLinkNode } from '@lexical/link';
|
|
23
|
+
import { Popover } from '@blue-patterns/core/Popover';
|
|
24
|
+
import { useTranslator } from '@blue-patterns/core/i18n';
|
|
25
|
+
import { SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_CRITICAL, IS_APPLE, isExactShortcutMatch, $createTextNode, $getSelection, $isRangeSelection, $setSelection } from 'lexical';
|
|
26
|
+
import { toLinkHref } from "../linkEditing.js";
|
|
27
|
+
import { LinkForm } from "../linkForm.js";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The platform-primary modifier for shortcuts: Cmd on Apple, Ctrl elsewhere.
|
|
31
|
+
* Mirrors the Lexical playground's `CONTROL_OR_META`
|
|
32
|
+
* (packages/lexical-playground/src/plugins/ShortcutsPlugin/shortcuts.ts).
|
|
33
|
+
*/
|
|
34
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
35
|
+
const CONTROL_OR_META = {
|
|
36
|
+
ctrlKey: !IS_APPLE,
|
|
37
|
+
metaKey: IS_APPLE
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether `event` is the insert-link shortcut (Cmd/Ctrl+K). Uses Lexical's
|
|
42
|
+
* `isExactShortcutMatch`, which — unlike a loose `metaKey || ctrlKey` check —
|
|
43
|
+
* requires exactly the primary modifier and rejects the combo when other
|
|
44
|
+
* modifiers (Shift/Alt) are also held. Matches the playground's `isInsertLink`.
|
|
45
|
+
*/
|
|
46
|
+
export function isInsertLink(event) {
|
|
47
|
+
return isExactShortcutMatch(event, 'k', CONTROL_OR_META);
|
|
48
|
+
}
|
|
49
|
+
/** Room for a typical address beside the link form's icon buttons. */
|
|
50
|
+
const LINK_POPOVER_WIDTH = 320;
|
|
51
|
+
|
|
52
|
+
/** The link popover's state and actions for one editor. */
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The link popover's state for `editor`. It remembers the selection the
|
|
56
|
+
* popover opened for, because focus in the field takes the page's selection
|
|
57
|
+
* from the editor, and puts it back before linking, unlinking, or giving
|
|
58
|
+
* focus back to the editor. `buttonRef` is the Link button, which opening the
|
|
59
|
+
* popover scrolls into view.
|
|
60
|
+
*/
|
|
61
|
+
export function useToolbarLink(editor, {
|
|
62
|
+
buttonRef,
|
|
63
|
+
hasNewTabLinks,
|
|
64
|
+
promptForUrl
|
|
65
|
+
}) {
|
|
66
|
+
const t = useTranslator();
|
|
67
|
+
const savedSelectionRef = useRef(null);
|
|
68
|
+
const lastLinkContextRef = useRef({
|
|
69
|
+
selectedText: '',
|
|
70
|
+
url: '',
|
|
71
|
+
isLink: false
|
|
72
|
+
});
|
|
73
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
74
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
75
|
+
const [url, setUrl] = useState('');
|
|
76
|
+
const [error, setError] = useState('');
|
|
77
|
+
|
|
78
|
+
// The popover acts on the last range selection. So a press that moved the
|
|
79
|
+
// editor's selection first still links the words that were selected.
|
|
80
|
+
const $noteSelection = useCallback(() => {
|
|
81
|
+
const selection = $getSelection();
|
|
82
|
+
if (!$isRangeSelection(selection)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const node = selection.anchor.getNode();
|
|
86
|
+
const parent = node.getParent();
|
|
87
|
+
const linkNode = $isLinkNode(node) ? node : $isLinkNode(parent) ? parent : null;
|
|
88
|
+
savedSelectionRef.current = selection.clone();
|
|
89
|
+
lastLinkContextRef.current = {
|
|
90
|
+
selectedText: selection.getTextContent(),
|
|
91
|
+
url: linkNode?.getURL() ?? '',
|
|
92
|
+
isLink: linkNode != null
|
|
93
|
+
};
|
|
94
|
+
return lastLinkContextRef.current;
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
// Syncs with the Lexical editor's update and selection-change listeners.
|
|
98
|
+
useEffect(() => mergeRegister(editor.registerUpdateListener(({
|
|
99
|
+
editorState
|
|
100
|
+
}) => {
|
|
101
|
+
editorState.read($noteSelection);
|
|
102
|
+
}), editor.registerCommand(SELECTION_CHANGE_COMMAND, () => {
|
|
103
|
+
$noteSelection();
|
|
104
|
+
return false;
|
|
105
|
+
}, COMMAND_PRIORITY_CRITICAL)), [$noteSelection, editor]);
|
|
106
|
+
const readLinkContext = useCallback(() => editor.getEditorState().read($noteSelection) ?? lastLinkContextRef.current, [$noteSelection, editor]);
|
|
107
|
+
const restoreLinkSelection = useCallback(() => {
|
|
108
|
+
const savedSelection = savedSelectionRef.current;
|
|
109
|
+
if (!savedSelection) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
editor.update(() => {
|
|
113
|
+
$setSelection(savedSelection.clone());
|
|
114
|
+
});
|
|
115
|
+
}, [editor]);
|
|
116
|
+
const applyLinkValue = useCallback(entered => {
|
|
117
|
+
const trimmed = entered.trim();
|
|
118
|
+
const href = toLinkHref(trimmed);
|
|
119
|
+
if (href == null) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const linkAttributes = hasNewTabLinks ? {
|
|
123
|
+
target: '_blank',
|
|
124
|
+
rel: 'noopener noreferrer'
|
|
125
|
+
} : {};
|
|
126
|
+
const savedSelection = savedSelectionRef.current;
|
|
127
|
+
// One update restores the selection, decides, and applies. Inside a
|
|
128
|
+
// command listener (Cmd/Ctrl+K) Lexical queues an update, so a flag set
|
|
129
|
+
// in one update cannot steer code that runs after it.
|
|
130
|
+
editor.update(() => {
|
|
131
|
+
if (savedSelection) {
|
|
132
|
+
$setSelection(savedSelection.clone());
|
|
133
|
+
}
|
|
134
|
+
const selection = $getSelection();
|
|
135
|
+
if ($isRangeSelection(selection) && selection.isCollapsed()) {
|
|
136
|
+
const node = selection.anchor.getNode();
|
|
137
|
+
const isInLink = $isLinkNode(node) || $isLinkNode(node.getParent());
|
|
138
|
+
// At a caret outside a link there is no text to link, so the URL
|
|
139
|
+
// becomes the link's text. The toggle command would link the whole
|
|
140
|
+
// text node around the caret instead.
|
|
141
|
+
if (!isInLink) {
|
|
142
|
+
const linkNode = $createLinkNode(href, linkAttributes);
|
|
143
|
+
linkNode.append($createTextNode(trimmed));
|
|
144
|
+
selection.insertNodes([linkNode]);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, {
|
|
149
|
+
url: href,
|
|
150
|
+
...linkAttributes
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
return true;
|
|
154
|
+
}, [editor, hasNewTabLinks]);
|
|
155
|
+
const toggle = useCallback(() => {
|
|
156
|
+
if (!editor.isEditable()) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const context = readLinkContext();
|
|
160
|
+
if (promptForUrl) {
|
|
161
|
+
// Preserve the existing synchronous extension point for consumers that
|
|
162
|
+
// already provide their own URL UI.
|
|
163
|
+
if (context.isLink) {
|
|
164
|
+
restoreLinkSelection();
|
|
165
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const entered = promptForUrl(context.selectedText);
|
|
169
|
+
if (entered != null && entered.trim() !== '') {
|
|
170
|
+
applyLinkValue(entered);
|
|
171
|
+
}
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// The field starts from the link's address, or from selected text that
|
|
176
|
+
// already reads as one; otherwise it is empty and shows its placeholder.
|
|
177
|
+
setUrl(context.url || (context.selectedText.startsWith('http') ? context.selectedText : ''));
|
|
178
|
+
setIsEditing(context.isLink);
|
|
179
|
+
setError('');
|
|
180
|
+
// Cmd/Ctrl+K can open the form while the Link button is scrolled out of
|
|
181
|
+
// view, in the page or in the toolbar's own row. Bring it back so the
|
|
182
|
+
// popover opens beside it.
|
|
183
|
+
buttonRef.current?.scrollIntoView?.({
|
|
184
|
+
block: 'nearest',
|
|
185
|
+
inline: 'nearest'
|
|
186
|
+
});
|
|
187
|
+
setIsOpen(true);
|
|
188
|
+
}, [applyLinkValue, buttonRef, editor, promptForUrl, readLinkContext, restoreLinkSelection]);
|
|
189
|
+
const close = useCallback(() => {
|
|
190
|
+
setIsOpen(false);
|
|
191
|
+
setError('');
|
|
192
|
+
}, []);
|
|
193
|
+
|
|
194
|
+
// The popover reports its own opening (a press on the Link button) and its
|
|
195
|
+
// closing (a press outside it).
|
|
196
|
+
const onOpenChange = useCallback(open => {
|
|
197
|
+
if (open) {
|
|
198
|
+
toggle();
|
|
199
|
+
} else {
|
|
200
|
+
close();
|
|
201
|
+
}
|
|
202
|
+
}, [close, toggle]);
|
|
203
|
+
|
|
204
|
+
// Closes the popover and gives the editor back its focus. The editable root
|
|
205
|
+
// takes focus first and Lexical then draws its selection into it, so the
|
|
206
|
+
// selection survives the field having held the page's selection.
|
|
207
|
+
const closeToEditor = useCallback(() => {
|
|
208
|
+
close();
|
|
209
|
+
editor.getRootElement()?.focus({
|
|
210
|
+
preventScroll: true
|
|
211
|
+
});
|
|
212
|
+
editor.focus();
|
|
213
|
+
}, [close, editor]);
|
|
214
|
+
const cancel = useCallback(() => {
|
|
215
|
+
restoreLinkSelection();
|
|
216
|
+
closeToEditor();
|
|
217
|
+
}, [closeToEditor, restoreLinkSelection]);
|
|
218
|
+
const apply = useCallback(() => {
|
|
219
|
+
if (!applyLinkValue(url)) {
|
|
220
|
+
setError(t('@astryx.richTextEditor.link.invalidUrl'));
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
closeToEditor();
|
|
224
|
+
return true;
|
|
225
|
+
}, [applyLinkValue, closeToEditor, t, url]);
|
|
226
|
+
const remove = useCallback(() => {
|
|
227
|
+
restoreLinkSelection();
|
|
228
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
229
|
+
closeToEditor();
|
|
230
|
+
}, [closeToEditor, editor, restoreLinkSelection]);
|
|
231
|
+
const onUrlChange = useCallback(value => {
|
|
232
|
+
setUrl(value);
|
|
233
|
+
setError('');
|
|
234
|
+
}, []);
|
|
235
|
+
return {
|
|
236
|
+
isOpen,
|
|
237
|
+
isEditing,
|
|
238
|
+
url,
|
|
239
|
+
error,
|
|
240
|
+
toggle,
|
|
241
|
+
onOpenChange,
|
|
242
|
+
onUrlChange,
|
|
243
|
+
apply,
|
|
244
|
+
remove,
|
|
245
|
+
cancel
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The non-modal popover a Link button opens, holding the URL form. It anchors
|
|
251
|
+
* to `anchorRef`, an element around the button that a press never scales,
|
|
252
|
+
* and finds the button inside it: it sets the button's popup state and opens
|
|
253
|
+
* on a press of it. Render it beside the toolbar that holds the button, not
|
|
254
|
+
* inside it: the toolbar is one roving tab stop, which would leave the form's
|
|
255
|
+
* controls out of Tab and take its arrow keys.
|
|
256
|
+
*/
|
|
257
|
+
export function ToolbarLinkPopover({
|
|
258
|
+
link,
|
|
259
|
+
anchorRef,
|
|
260
|
+
size
|
|
261
|
+
}) {
|
|
262
|
+
const t = useTranslator();
|
|
263
|
+
return /*#__PURE__*/_jsx(Popover, {
|
|
264
|
+
anchorRef: anchorRef,
|
|
265
|
+
label: link.isEditing ? t('@astryx.richTextEditor.link.editTitle') : t('@astryx.richTextEditor.link.insertTitle'),
|
|
266
|
+
isOpen: link.isOpen,
|
|
267
|
+
onOpenChange: link.onOpenChange,
|
|
268
|
+
isModal: false
|
|
269
|
+
// Popover's focus entry focuses the URL field, the form's first
|
|
270
|
+
// control. Cancel is its close control.
|
|
271
|
+
,
|
|
272
|
+
hasCloseButton: false
|
|
273
|
+
// Above the toolbar, so the popover does not cover the editor's
|
|
274
|
+
// first lines, where the linked words often are. It flips below
|
|
275
|
+
// only when there is no room above.
|
|
276
|
+
,
|
|
277
|
+
placement: "above",
|
|
278
|
+
alignment: "start",
|
|
279
|
+
width: LINK_POPOVER_WIDTH,
|
|
280
|
+
padding: 2,
|
|
281
|
+
content: link.isOpen ? /*#__PURE__*/_jsx(LinkForm, {
|
|
282
|
+
url: link.url,
|
|
283
|
+
error: link.error,
|
|
284
|
+
isEditing: link.isEditing,
|
|
285
|
+
size: size,
|
|
286
|
+
onUrlChange: link.onUrlChange,
|
|
287
|
+
onApply: link.apply,
|
|
288
|
+
onRemove: link.remove,
|
|
289
|
+
onCancel: link.cancel
|
|
290
|
+
}) : null
|
|
291
|
+
});
|
|
292
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linkEditing.d.ts","sourceRoot":"","sources":["../src/linkEditing.ts"],"names":[],"mappings":"AAYA;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGzD"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file linkEditing.ts
|
|
5
|
+
* @input An entered address
|
|
6
|
+
* @output Exports toLinkHref
|
|
7
|
+
* @position Internal to RichText. The link rules RichTextEditorToolbar's link
|
|
8
|
+
* popover applies: which address an entry becomes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { sanitizeUrl } from "./linkUtils.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The address an entry becomes under the link rules (`sanitizeUrl`), or null
|
|
15
|
+
* when the rules refuse it.
|
|
16
|
+
*/
|
|
17
|
+
export function toLinkHref(entered) {
|
|
18
|
+
const href = sanitizeUrl(entered.trim());
|
|
19
|
+
return href === 'about:blank' ? null : href;
|
|
20
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove's glyph. No shared icon name means "unlink", so it is fixed
|
|
3
|
+
* component artwork until component icon slots exist.
|
|
4
|
+
*/
|
|
5
|
+
export declare const REMOVE_LINK_GLYPH: import("react").JSX.Element;
|
|
6
|
+
export interface LinkFormProps {
|
|
7
|
+
/** The field's value: the address being entered. */
|
|
8
|
+
url: string;
|
|
9
|
+
/** The message for a refused address, or empty. */
|
|
10
|
+
error: string;
|
|
11
|
+
/** Whether the form edits a link, which Remove then unlinks. */
|
|
12
|
+
isEditing: boolean;
|
|
13
|
+
/** The caller's control size, so the form's controls match it. */
|
|
14
|
+
size: 'sm' | 'md' | 'lg';
|
|
15
|
+
onUrlChange: (url: string) => void;
|
|
16
|
+
/** Applies the field's address; false when it was refused. */
|
|
17
|
+
onApply: () => boolean;
|
|
18
|
+
onRemove: () => void;
|
|
19
|
+
onCancel: () => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The link form: one URL field, then Apply, Remove (only when editing a
|
|
23
|
+
* link), and Cancel as icon buttons. Enter in the field
|
|
24
|
+
* applies, and Escape anywhere in the form cancels. A refused address shows
|
|
25
|
+
* the field's error status and keeps focus in the field.
|
|
26
|
+
*/
|
|
27
|
+
export declare function LinkForm({ url, error, isEditing, size, onUrlChange, onApply, onRemove, onCancel, }: LinkFormProps): import("react").JSX.Element;
|
|
28
|
+
//# sourceMappingURL=linkForm.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linkForm.d.ts","sourceRoot":"","sources":["../src/linkForm.tsx"],"names":[],"mappings":"AAqBA;;;GAGG;AACH,eAAO,MAAM,iBAAiB,6BAe7B,CAAC;AAEF,MAAM,WAAW,aAAa;IAC5B,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,SAAS,EAAE,OAAO,CAAC;IACnB,kEAAkE;IAClE,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,8DAA8D;IAC9D,OAAO,EAAE,MAAM,OAAO,CAAC;IACvB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,KAAK,EACL,SAAS,EACT,IAAI,EACJ,WAAW,EACX,OAAO,EACP,QAAQ,EACR,QAAQ,GACT,EAAE,aAAa,+BAkFf"}
|