@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.
Files changed (201) hide show
  1. package/CHANGELOG.md +275 -0
  2. package/LICENSE +22 -0
  3. package/README.md +88 -2
  4. package/astryx.integration.mjs +11 -0
  5. package/blocks/RichTextEditorShowcase.template.mjs +13 -0
  6. package/blocks/RichTextEditorShowcase.tsx +19 -0
  7. package/dist/RichTextEditor.d.ts +282 -0
  8. package/dist/RichTextEditor.d.ts.map +1 -0
  9. package/dist/RichTextEditor.js +657 -0
  10. package/dist/RichTextEditorAutoLinkPlugin.d.ts +83 -0
  11. package/dist/RichTextEditorAutoLinkPlugin.d.ts.map +1 -0
  12. package/dist/RichTextEditorAutoLinkPlugin.js +99 -0
  13. package/dist/RichTextEditorToolbar.d.ts +186 -0
  14. package/dist/RichTextEditorToolbar.d.ts.map +1 -0
  15. package/dist/RichTextEditorToolbar.js +688 -0
  16. package/dist/RichTextView.d.ts +92 -0
  17. package/dist/RichTextView.d.ts.map +1 -0
  18. package/dist/RichTextView.js +267 -0
  19. package/dist/codeLanguages.d.ts +14 -0
  20. package/dist/codeLanguages.d.ts.map +1 -0
  21. package/dist/codeLanguages.js +34 -0
  22. package/dist/codeLineTokens.d.ts +53 -0
  23. package/dist/codeLineTokens.d.ts.map +1 -0
  24. package/dist/codeLineTokens.js +188 -0
  25. package/dist/editorNodes.d.ts +8 -0
  26. package/dist/editorNodes.d.ts.map +1 -0
  27. package/dist/editorNodes.js +30 -0
  28. package/dist/editorTheme.d.ts +7 -0
  29. package/dist/editorTheme.d.ts.map +1 -0
  30. package/dist/editorTheme.js +328 -0
  31. package/dist/index.d.ts +31 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +28 -0
  34. package/dist/internal/CodeBackspacePlugin.d.ts +2 -0
  35. package/dist/internal/CodeBackspacePlugin.d.ts.map +1 -0
  36. package/dist/internal/CodeBackspacePlugin.js +70 -0
  37. package/dist/internal/CodeBlockHeaderPlugin.d.ts +11 -0
  38. package/dist/internal/CodeBlockHeaderPlugin.d.ts.map +1 -0
  39. package/dist/internal/CodeBlockHeaderPlugin.js +204 -0
  40. package/dist/internal/CodePastePlugin.d.ts +5 -0
  41. package/dist/internal/CodePastePlugin.d.ts.map +1 -0
  42. package/dist/internal/CodePastePlugin.js +104 -0
  43. package/dist/internal/CodeSyntaxPlugin.d.ts +8 -0
  44. package/dist/internal/CodeSyntaxPlugin.d.ts.map +1 -0
  45. package/dist/internal/CodeSyntaxPlugin.js +397 -0
  46. package/dist/internal/CodeTabPlugin.d.ts +2 -0
  47. package/dist/internal/CodeTabPlugin.d.ts.map +1 -0
  48. package/dist/internal/CodeTabPlugin.js +256 -0
  49. package/dist/internal/DisabledLinkGuardPlugin.d.ts +4 -0
  50. package/dist/internal/DisabledLinkGuardPlugin.d.ts.map +1 -0
  51. package/dist/internal/DisabledLinkGuardPlugin.js +93 -0
  52. package/dist/internal/ImageSourceCaretPlugin.d.ts +3 -0
  53. package/dist/internal/ImageSourceCaretPlugin.d.ts.map +1 -0
  54. package/dist/internal/ImageSourceCaretPlugin.js +105 -0
  55. package/dist/internal/ListItemShortcutsPlugin.d.ts +6 -0
  56. package/dist/internal/ListItemShortcutsPlugin.d.ts.map +1 -0
  57. package/dist/internal/ListItemShortcutsPlugin.js +314 -0
  58. package/dist/internal/MarkdownExtensionsPlugin.d.ts +17 -0
  59. package/dist/internal/MarkdownExtensionsPlugin.d.ts.map +1 -0
  60. package/dist/internal/MarkdownExtensionsPlugin.js +185 -0
  61. package/dist/internal/SelectAllScopePlugin.d.ts +2 -0
  62. package/dist/internal/SelectAllScopePlugin.d.ts.map +1 -0
  63. package/dist/internal/SelectAllScopePlugin.js +67 -0
  64. package/dist/internal/TableColumnFloorPlugin.d.ts +12 -0
  65. package/dist/internal/TableColumnFloorPlugin.d.ts.map +1 -0
  66. package/dist/internal/TableColumnFloorPlugin.js +84 -0
  67. package/dist/internal/TablePastePlugin.d.ts +9 -0
  68. package/dist/internal/TablePastePlugin.d.ts.map +1 -0
  69. package/dist/internal/TablePastePlugin.js +183 -0
  70. package/dist/internal/TableScrollRegionPlugin.d.ts +9 -0
  71. package/dist/internal/TableScrollRegionPlugin.d.ts.map +1 -0
  72. package/dist/internal/TableScrollRegionPlugin.js +95 -0
  73. package/dist/internal/TaskCheckboxPlugin.d.ts +7 -0
  74. package/dist/internal/TaskCheckboxPlugin.d.ts.map +1 -0
  75. package/dist/internal/TaskCheckboxPlugin.js +193 -0
  76. package/dist/internal/ToolbarLinkPopover.d.ts +74 -0
  77. package/dist/internal/ToolbarLinkPopover.d.ts.map +1 -0
  78. package/dist/internal/ToolbarLinkPopover.js +292 -0
  79. package/dist/linkEditing.d.ts +6 -0
  80. package/dist/linkEditing.d.ts.map +1 -0
  81. package/dist/linkEditing.js +20 -0
  82. package/dist/linkForm.d.ts +28 -0
  83. package/dist/linkForm.d.ts.map +1 -0
  84. package/dist/linkForm.js +137 -0
  85. package/dist/linkUtils.d.ts +27 -0
  86. package/dist/linkUtils.d.ts.map +1 -0
  87. package/dist/linkUtils.js +101 -0
  88. package/dist/markdown.d.ts +17 -0
  89. package/dist/markdown.d.ts.map +1 -0
  90. package/dist/markdown.js +17 -0
  91. package/dist/markdownCharacterReferences.d.ts +135 -0
  92. package/dist/markdownCharacterReferences.d.ts.map +1 -0
  93. package/dist/markdownCharacterReferences.js +1127 -0
  94. package/dist/markdownCodeFence.d.ts +4 -0
  95. package/dist/markdownCodeFence.d.ts.map +1 -0
  96. package/dist/markdownCodeFence.js +167 -0
  97. package/dist/markdownExtensionNode.d.ts +88 -0
  98. package/dist/markdownExtensionNode.d.ts.map +1 -0
  99. package/dist/markdownExtensionNode.js +248 -0
  100. package/dist/markdownExtensions.d.ts +115 -0
  101. package/dist/markdownExtensions.d.ts.map +1 -0
  102. package/dist/markdownExtensions.js +392 -0
  103. package/dist/markdownHardLineBreak.d.ts +25 -0
  104. package/dist/markdownHardLineBreak.d.ts.map +1 -0
  105. package/dist/markdownHardLineBreak.js +59 -0
  106. package/dist/markdownHeading.d.ts +20 -0
  107. package/dist/markdownHeading.d.ts.map +1 -0
  108. package/dist/markdownHeading.js +30 -0
  109. package/dist/markdownLink.d.ts +5 -0
  110. package/dist/markdownLink.d.ts.map +1 -0
  111. package/dist/markdownLink.js +80 -0
  112. package/dist/markdownListExport.d.ts +3 -0
  113. package/dist/markdownListExport.d.ts.map +1 -0
  114. package/dist/markdownListExport.js +68 -0
  115. package/dist/markdownListIndentation.d.ts +8 -0
  116. package/dist/markdownListIndentation.d.ts.map +1 -0
  117. package/dist/markdownListIndentation.js +101 -0
  118. package/dist/markdownOrderedList.d.ts +3 -0
  119. package/dist/markdownOrderedList.d.ts.map +1 -0
  120. package/dist/markdownOrderedList.js +51 -0
  121. package/dist/markdownQuote.d.ts +22 -0
  122. package/dist/markdownQuote.d.ts.map +1 -0
  123. package/dist/markdownQuote.js +32 -0
  124. package/dist/markdownSerializers.d.ts +49 -0
  125. package/dist/markdownSerializers.d.ts.map +1 -0
  126. package/dist/markdownSerializers.js +83 -0
  127. package/dist/markdownSource.d.ts +71 -0
  128. package/dist/markdownSource.d.ts.map +1 -0
  129. package/dist/markdownSource.js +804 -0
  130. package/dist/markdownSpaceRuns.d.ts +24 -0
  131. package/dist/markdownSpaceRuns.d.ts.map +1 -0
  132. package/dist/markdownSpaceRuns.js +147 -0
  133. package/dist/markdownTable.d.ts +35 -0
  134. package/dist/markdownTable.d.ts.map +1 -0
  135. package/dist/markdownTable.js +227 -0
  136. package/dist/markdownTaskList.d.ts +48 -0
  137. package/dist/markdownTaskList.d.ts.map +1 -0
  138. package/dist/markdownTaskList.js +166 -0
  139. package/dist/markdownThematicBreak.d.ts +3 -0
  140. package/dist/markdownThematicBreak.d.ts.map +1 -0
  141. package/dist/markdownThematicBreak.js +55 -0
  142. package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts +12 -0
  143. package/dist/privateAttributes/layerPortalBoundary.stylex.d.ts.map +1 -0
  144. package/dist/privateAttributes/layerPortalBoundary.stylex.js +16 -0
  145. package/dist/richtext.css +213 -0
  146. package/dist/textSemantics.d.ts +3 -0
  147. package/dist/textSemantics.d.ts.map +1 -0
  148. package/dist/textSemantics.js +87 -0
  149. package/package.json +158 -3
  150. package/src/RichTextEditor.doc.mjs +308 -0
  151. package/src/RichTextEditor.tsx +1109 -0
  152. package/src/RichTextEditorAutoLinkPlugin.doc.mjs +43 -0
  153. package/src/RichTextEditorAutoLinkPlugin.tsx +126 -0
  154. package/src/RichTextEditorToolbar.doc.mjs +92 -0
  155. package/src/RichTextEditorToolbar.tsx +871 -0
  156. package/src/RichTextView.doc.mjs +101 -0
  157. package/src/RichTextView.tsx +351 -0
  158. package/src/codeLanguages.ts +42 -0
  159. package/src/codeLineTokens.ts +241 -0
  160. package/src/doc-mjs.d.ts +23 -0
  161. package/src/editorNodes.ts +44 -0
  162. package/src/editorTheme.ts +550 -0
  163. package/src/index.ts +59 -0
  164. package/src/internal/CodeBackspacePlugin.tsx +96 -0
  165. package/src/internal/CodeBlockHeaderPlugin.tsx +282 -0
  166. package/src/internal/CodePastePlugin.tsx +144 -0
  167. package/src/internal/CodeSyntaxPlugin.tsx +493 -0
  168. package/src/internal/CodeTabPlugin.tsx +298 -0
  169. package/src/internal/DisabledLinkGuardPlugin.tsx +93 -0
  170. package/src/internal/ImageSourceCaretPlugin.tsx +159 -0
  171. package/src/internal/ListItemShortcutsPlugin.tsx +404 -0
  172. package/src/internal/MarkdownExtensionsPlugin.tsx +289 -0
  173. package/src/internal/SelectAllScopePlugin.tsx +86 -0
  174. package/src/internal/TableColumnFloorPlugin.tsx +99 -0
  175. package/src/internal/TablePastePlugin.tsx +223 -0
  176. package/src/internal/TableScrollRegionPlugin.tsx +107 -0
  177. package/src/internal/TaskCheckboxPlugin.tsx +246 -0
  178. package/src/internal/ToolbarLinkPopover.tsx +392 -0
  179. package/src/linkEditing.ts +20 -0
  180. package/src/linkForm.tsx +156 -0
  181. package/src/linkUtils.ts +106 -0
  182. package/src/markdown.ts +25 -0
  183. package/src/markdownCharacterReferences.ts +1137 -0
  184. package/src/markdownCodeFence.ts +216 -0
  185. package/src/markdownExtensionNode.ts +339 -0
  186. package/src/markdownExtensions.ts +496 -0
  187. package/src/markdownHardLineBreak.ts +73 -0
  188. package/src/markdownHeading.ts +33 -0
  189. package/src/markdownLink.ts +86 -0
  190. package/src/markdownListExport.ts +85 -0
  191. package/src/markdownListIndentation.ts +105 -0
  192. package/src/markdownOrderedList.ts +60 -0
  193. package/src/markdownQuote.ts +35 -0
  194. package/src/markdownSerializers.ts +108 -0
  195. package/src/markdownSource.ts +1045 -0
  196. package/src/markdownSpaceRuns.ts +164 -0
  197. package/src/markdownTable.ts +309 -0
  198. package/src/markdownTaskList.ts +222 -0
  199. package/src/markdownThematicBreak.ts +66 -0
  200. package/src/privateAttributes/layerPortalBoundary.stylex.ts +18 -0
  201. 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,2 @@
1
+ export declare function SelectAllScopePlugin(): null;
2
+ //# sourceMappingURL=SelectAllScopePlugin.d.ts.map
@@ -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"}