@mx-space/cli 0.7.7 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Lexical.dev-rcxzwpir.mjs → Lexical.dev-By5dOKBN.mjs} +1486 -356
- package/dist/{Lexical.node-BUsI9gWr.mjs → Lexical.node-fXM8Jp7p.mjs} +39 -17
- package/dist/{Lexical.prod-C8HtkSD6.mjs → Lexical.prod-BlCvXPGa.mjs} +2279 -1768
- package/dist/LexicalClipboard.dev--HxGoSkO.mjs +915 -0
- package/dist/{LexicalClipboard.node-CO-MCKx5.mjs → LexicalClipboard.node-C3mRbrmW.mjs} +10 -3
- package/dist/LexicalClipboard.prod-DNJzFMcT.mjs +371 -0
- package/dist/LexicalCodeCore.dev-DjSrd4A4.mjs +1109 -0
- package/dist/LexicalCodeCore.prod-DTDShkR3.mjs +642 -0
- package/dist/{LexicalDragon.dev-gBP8WSRS.mjs → LexicalDragon.dev-DwprRH0z.mjs} +3 -3
- package/dist/{LexicalDragon.node-OG6G55nl.mjs → LexicalDragon.node-BVhw4C7s.mjs} +2 -2
- package/dist/{LexicalDragon.prod-DP-_Q8cv.mjs → LexicalDragon.prod-nh4J-8vn.mjs} +3 -3
- package/dist/{LexicalExtension.dev-DVbjljrm.mjs → LexicalExtension.dev-BPNyvGqe.mjs} +335 -6
- package/dist/{LexicalExtension.node-CF3EAPN7.mjs → LexicalExtension.node-BVlcrgOx.mjs} +15 -8
- package/dist/{LexicalExtension.prod-DklHHErZ.mjs → LexicalExtension.prod-DFKbiNt-.mjs} +375 -265
- package/dist/{LexicalHeadless.dev-DdWLM7Nl.mjs → LexicalHeadless.dev-B-cj7Pr5.mjs} +2 -2
- package/dist/{LexicalHeadless.prod-CoiiTDzR.mjs → LexicalHeadless.prod-Byp1vNzT.mjs} +2 -2
- package/dist/LexicalHtml.dev-prBAwszN.mjs +2424 -0
- package/dist/LexicalHtml.node-BvNF96jZ.mjs +56 -0
- package/dist/LexicalHtml.prod-Sz3CW1ht.mjs +1207 -0
- package/dist/{LexicalLink.dev-BS1Bf-yD.mjs → LexicalLink.dev-NySTYhVR.mjs} +42 -8
- package/dist/{LexicalLink.prod-B_crkoHP.mjs → LexicalLink.prod-BuCM8gts.mjs} +209 -191
- package/dist/{LexicalList.dev-OMqmcnQ-.mjs → LexicalList.dev-DIMOvSFD.mjs} +252 -80
- package/dist/{LexicalList.prod-CpL1s8vS.mjs → LexicalList.prod-MVqfA6Vo.mjs} +364 -278
- package/dist/{LexicalMarkdown.dev-BHSdx_Gr.mjs → LexicalMarkdown.dev-C7aylvCG.mjs} +729 -552
- package/dist/LexicalMarkdown.prod-CfQSM406.mjs +868 -0
- package/dist/{LexicalRichText.dev-Bz9A_WbM.mjs → LexicalRichText.dev-DfICq6lU.mjs} +272 -77
- package/dist/{LexicalRichText.node-DDpwfE-P.mjs → LexicalRichText.node-Bg9jND3Q.mjs} +21 -8
- package/dist/LexicalRichText.prod-GsFzCK-o.mjs +550 -0
- package/dist/{LexicalSelection.dev-CQ380fnD.mjs → LexicalSelection.dev-DGjj12wW.mjs} +50 -38
- package/dist/{LexicalSelection.node-DesBMjIE.mjs → LexicalSelection.node-BU5L4193.mjs} +4 -4
- package/dist/{LexicalSelection.prod-D2jivCDB.mjs → LexicalSelection.prod-D-PwRbyK.mjs} +158 -140
- package/dist/{LexicalTable.dev-Ca7mQ7Jk.mjs → LexicalTable.dev-ChP7cugd.mjs} +215 -53
- package/dist/{LexicalTable.prod-DyPAcC7y.mjs → LexicalTable.prod-Bpr9YRXp.mjs} +668 -573
- package/dist/{LexicalUtils.dev-HxM8F0g6.mjs → LexicalUtils.dev-BiFU3789.mjs} +10 -47
- package/dist/{LexicalUtils.node-lCWjRjoG.mjs → LexicalUtils.node-j3Llm7Y3.mjs} +2 -2
- package/dist/{LexicalUtils.prod-Bti5w4ki.mjs → LexicalUtils.prod-DCvrA78p.mjs} +103 -105
- package/dist/bin/mxs.d.mts +1 -1
- package/dist/bin/mxs.mjs +1 -1
- package/dist/esm-By7trG9K.mjs +2 -0
- package/dist/{esm-CK9m5JKX.mjs → esm-Dg-WhPCe.mjs} +989 -989
- package/dist/index.d.mts +22 -2
- package/dist/index.mjs +1 -1
- package/dist/{mxs-vbxEu09D.mjs → mxs-DT0oYk6U.mjs} +10387 -8278
- package/package.json +18 -17
- package/dist/LexicalClipboard.dev-BpqRwTLP.mjs +0 -494
- package/dist/LexicalClipboard.prod-B_xdeiAC.mjs +0 -291
- package/dist/LexicalCodeCore.dev-7_cexm39.mjs +0 -537
- package/dist/LexicalCodeCore.prod-Bd6WFHcO.mjs +0 -381
- package/dist/LexicalHtml.dev-C3ZSWBFZ.mjs +0 -684
- package/dist/LexicalHtml.node-D0TD152E.mjs +0 -24
- package/dist/LexicalHtml.prod-CX47X0zl.mjs +0 -430
- package/dist/LexicalMarkdown.prod-C-S7Ii6-.mjs +0 -754
- package/dist/LexicalRichText.prod-C8qXACDL.mjs +0 -427
- package/dist/esm-DRbkkY3r.mjs +0 -2
- /package/dist/{mxs-CM8-6uAr.d.mts → mxs-iqJBopvE.d.mts} +0 -0
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
import { c as configExtension, d as getPeerDependencyFromEditor, r as $getPeerDependency } from "./LexicalExtension.node-BVlcrgOx.mjs";
|
|
2
|
+
import { $ as $isElementNode, A as $getChildCaretAtIndex, D as $getCaretRangeInDirection, Dt as $splitAtPointCaretNext, E as $getCaretRange, Er as isSelectionWithinEditor, F as $getEditor, Fr as shallowMergeConfig, H as $getRoot, K as $getTextPointCaret, L as $getNearestNodeFromDOMNode, Lt as COMMAND_PRIORITY_CRITICAL, M as $getCollapsedCaretRange, Rr as tokenizeRawText, Tt as $setSelectionFromCaretRange, U as $getSelection, Ut as COPY_COMMAND, _t as $parseSerializedNode, cr as defineExtension, ft as $isTextNode, g as $createTabNode, i as $caretRangeFromSelection, jr as safeCast, k as $getChildCaret, lr as getDOMSelection, pt as $isTextPointCaret, qn as SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, r as $caretFromPoint, s as $comparePointCaretNext, st as $isRangeSelection } from "./Lexical.node-fXM8Jp7p.mjs";
|
|
3
|
+
import { c as ImportSource, f as contextValue, i as $generateNodesFromDOMViaExtension, l as ImportSourceDataTransfer, n as $generateHtmlFromNodes, r as $generateNodesFromDOM } from "./LexicalHtml.node-BvNF96jZ.mjs";
|
|
4
|
+
import { i as $sliceSelectedTextNodeContent } from "./LexicalSelection.node-BU5L4193.mjs";
|
|
5
|
+
import { g as objectKlassEquals } from "./LexicalUtils.node-j3Llm7Y3.mjs";
|
|
6
|
+
//#region ../../node_modules/.pnpm/@lexical+clipboard@0.45.0/node_modules/@lexical/clipboard/dist/LexicalClipboard.dev.mjs
|
|
7
|
+
/**
|
|
8
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
9
|
+
*
|
|
10
|
+
* This source code is licensed under the MIT license found in the
|
|
11
|
+
* LICENSE file in the root directory of this source tree.
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
16
|
+
*
|
|
17
|
+
* This source code is licensed under the MIT license found in the
|
|
18
|
+
* LICENSE file in the root directory of this source tree.
|
|
19
|
+
*
|
|
20
|
+
*/
|
|
21
|
+
/** @internal */
|
|
22
|
+
function caretFromPoint(x, y) {
|
|
23
|
+
if (typeof document.caretRangeFromPoint !== "undefined") {
|
|
24
|
+
const range = document.caretRangeFromPoint(x, y);
|
|
25
|
+
if (range === null) return null;
|
|
26
|
+
return {
|
|
27
|
+
node: range.startContainer,
|
|
28
|
+
offset: range.startOffset
|
|
29
|
+
};
|
|
30
|
+
} else if (document.caretPositionFromPoint !== "undefined") {
|
|
31
|
+
const range = document.caretPositionFromPoint(x, y);
|
|
32
|
+
if (range === null) return null;
|
|
33
|
+
return {
|
|
34
|
+
node: range.offsetNode,
|
|
35
|
+
offset: range.offset
|
|
36
|
+
};
|
|
37
|
+
} else return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
41
|
+
*
|
|
42
|
+
* This source code is licensed under the MIT license found in the
|
|
43
|
+
* LICENSE file in the root directory of this source tree.
|
|
44
|
+
*
|
|
45
|
+
*/
|
|
46
|
+
function formatDevErrorMessage(message) {
|
|
47
|
+
throw new Error(message);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
51
|
+
*
|
|
52
|
+
* This source code is licensed under the MIT license found in the
|
|
53
|
+
* LICENSE file in the root directory of this source tree.
|
|
54
|
+
*
|
|
55
|
+
*/
|
|
56
|
+
/**
|
|
57
|
+
* A middleware function in a per-MIME-type clipboard-import stack. Mirrors
|
|
58
|
+
* the shape of {@link ExportMimeTypeFunction} on the export side.
|
|
59
|
+
*
|
|
60
|
+
* - `data` is the non-empty string returned by `DataTransfer.getData(mime)`
|
|
61
|
+
* for this MIME type.
|
|
62
|
+
* - `selection` is the current editor selection at the insertion point.
|
|
63
|
+
* - `$next` defers to the next-lower handler in the stack (i.e. the handler
|
|
64
|
+
* that was registered earlier). Returns `true` if that handler claimed
|
|
65
|
+
* the data; `false` if no handler accepted it.
|
|
66
|
+
* - `dataTransfer` is the full {@link DataTransfer} the paste/drop came
|
|
67
|
+
* from, so a handler can inspect companion MIME types or attached
|
|
68
|
+
* files in addition to the slot it was invoked for (e.g. peek at
|
|
69
|
+
* `'application/x-vscode-source'` while handling `'text/html'`). When
|
|
70
|
+
* threading through the new pipeline, pass this into
|
|
71
|
+
* `$generateNodesFromDOMViaExtension(dom, {
|
|
72
|
+
* context: [contextValue(ImportSourceDataTransfer, dataTransfer)],
|
|
73
|
+
* })` so rules and preprocessors can read it via
|
|
74
|
+
* `ctx.get(ImportSourceDataTransfer)`.
|
|
75
|
+
*
|
|
76
|
+
* The function should return `true` if it consumed the data (the caller
|
|
77
|
+
* stops trying further handlers for this MIME type and does not move on to
|
|
78
|
+
* the next MIME type). Return `$next()` to delegate. Return `false` if the
|
|
79
|
+
* function decided not to handle the data after inspecting it (e.g. the
|
|
80
|
+
* JSON namespace didn't match) so a lower-priority handler — or the next
|
|
81
|
+
* MIME type — gets a chance.
|
|
82
|
+
*
|
|
83
|
+
* @experimental
|
|
84
|
+
*/
|
|
85
|
+
/**
|
|
86
|
+
* A mapping from MIME type to a stack of {@link ImportMimeTypeFunction}.
|
|
87
|
+
*
|
|
88
|
+
* Each entry is an ordered array; the function at the highest index runs
|
|
89
|
+
* first and may call `next()` to fall through to the function below it.
|
|
90
|
+
* The default config provides one handler each for
|
|
91
|
+
* `'application/x-lexical-editor'`, `'text/html'`, and `'text/plain'` that
|
|
92
|
+
* matches the legacy {@link $insertDataTransferForRichText} behavior.
|
|
93
|
+
*
|
|
94
|
+
* When {@link ClipboardImportExtension} merges a partial config, new
|
|
95
|
+
* functions are appended to the existing array for each MIME type, so
|
|
96
|
+
* later-registered handlers run before earlier ones (including the
|
|
97
|
+
* defaults) and may delegate to them via `next()`.
|
|
98
|
+
*
|
|
99
|
+
* @experimental
|
|
100
|
+
*/
|
|
101
|
+
/**
|
|
102
|
+
* Per-MIME-type ordering weights. Lower numbers run first.
|
|
103
|
+
*
|
|
104
|
+
* Composable across extensions: each extension contributes weights for
|
|
105
|
+
* its MIME types without needing to coordinate. A partial config that
|
|
106
|
+
* sets `{'application/vnd.myapp+json': 5}` slots its type between the
|
|
107
|
+
* built-in `application/x-lexical-editor` (0) and `text/html` (10) — no
|
|
108
|
+
* need to enumerate the full ordering. mergeConfig spreads pairs (later
|
|
109
|
+
* keys override earlier ones for the same MIME type, so an extension
|
|
110
|
+
* can also re-rank a built-in by repeating its key with a new weight).
|
|
111
|
+
*
|
|
112
|
+
* Iteration: every MIME type that has a handler stack and is present in
|
|
113
|
+
* the dataTransfer (regardless of whether it has an explicit weight) is
|
|
114
|
+
* tried; MIME types with no explicit weight sort to the end, behind all
|
|
115
|
+
* weighted ones, in lexical order.
|
|
116
|
+
*
|
|
117
|
+
* @experimental
|
|
118
|
+
*/
|
|
119
|
+
/**
|
|
120
|
+
* Configuration for {@link ClipboardImportExtension}.
|
|
121
|
+
*
|
|
122
|
+
* @experimental
|
|
123
|
+
*/
|
|
124
|
+
/**
|
|
125
|
+
* Default per-MIME-type weights reproducing the legacy
|
|
126
|
+
* `$insertDataTransferForRichText` ordering:
|
|
127
|
+
*
|
|
128
|
+
* `application/x-lexical-editor` (0) → `text/html` (10) →
|
|
129
|
+
* `text/plain` (20) → `text/uri-list` (30).
|
|
130
|
+
*
|
|
131
|
+
* Gaps between weights let third-party MIME types slot in (e.g. weight
|
|
132
|
+
* 5 to run between lexical and html). Apps can also override built-in
|
|
133
|
+
* weights to demote them.
|
|
134
|
+
*
|
|
135
|
+
* @experimental
|
|
136
|
+
*/
|
|
137
|
+
const DEFAULT_IMPORT_MIME_TYPE_PRIORITY = {
|
|
138
|
+
"application/x-lexical-editor": 0,
|
|
139
|
+
"text/html": 10,
|
|
140
|
+
"text/plain": 20,
|
|
141
|
+
"text/uri-list": 30
|
|
142
|
+
};
|
|
143
|
+
function trustHTML(html) {
|
|
144
|
+
if (window.trustedTypes && window.trustedTypes.createPolicy) return window.trustedTypes.createPolicy("lexical", { createHTML: (input) => input }).createHTML(html);
|
|
145
|
+
return html;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Default handler for `'application/x-lexical-editor'`: parse the JSON,
|
|
149
|
+
* verify the namespace, and insert the serialized nodes.
|
|
150
|
+
*/
|
|
151
|
+
const $defaultLexicalEditorImporter = (data, selection, $next) => {
|
|
152
|
+
try {
|
|
153
|
+
const editor = $getEditor();
|
|
154
|
+
const payload = JSON.parse(data);
|
|
155
|
+
if (payload && payload.namespace === editor._config.namespace && Array.isArray(payload.nodes)) {
|
|
156
|
+
$insertGeneratedNodes(editor, $generateNodesFromSerializedNodes(payload.nodes), selection);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error(error);
|
|
161
|
+
}
|
|
162
|
+
return $next();
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Default handler for `'text/html'`: parse the HTML and run the legacy
|
|
166
|
+
* `$generateNodesFromDOM`. Override (or stack a higher-priority handler
|
|
167
|
+
* on top) to route HTML pastes through {@link DOMImportExtension} or any
|
|
168
|
+
* custom pipeline. See {@link $generateNodesFromDOMViaExtension} for the
|
|
169
|
+
* built-in `DOMImportExtension` adapter.
|
|
170
|
+
*/
|
|
171
|
+
const $defaultHtmlImporter = (data, selection, $next) => {
|
|
172
|
+
try {
|
|
173
|
+
const editor = $getEditor();
|
|
174
|
+
$insertGeneratedNodes(editor, $generateNodesFromDOM(editor, new DOMParser().parseFromString(trustHTML(data), "text/html")), selection);
|
|
175
|
+
return true;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.error(error);
|
|
178
|
+
return $next();
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* Default handler for `'text/plain'`. On a RangeSelection, drive the
|
|
183
|
+
* insertion off {@link tokenizeRawText} so each `\n` becomes a real
|
|
184
|
+
* paragraph break via `insertParagraph` (preserving current text
|
|
185
|
+
* format / style on the surrounding `insertText` calls). For other
|
|
186
|
+
* selection types, defer to the selection's own `insertRawText`.
|
|
187
|
+
*/
|
|
188
|
+
const $defaultPlainTextImporter = (data, selection) => {
|
|
189
|
+
if (!$isRangeSelection(selection)) {
|
|
190
|
+
selection.insertRawText(data);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
const withCurrentRange = (fn) => {
|
|
194
|
+
const cur = $getSelection();
|
|
195
|
+
if ($isRangeSelection(cur)) fn(cur);
|
|
196
|
+
};
|
|
197
|
+
tokenizeRawText(data, {
|
|
198
|
+
linebreak: () => withCurrentRange((cur) => cur.insertParagraph()),
|
|
199
|
+
tab: () => withCurrentRange((cur) => cur.insertNodes([$createTabNode()])),
|
|
200
|
+
text: (part) => withCurrentRange((cur) => cur.insertText(part))
|
|
201
|
+
});
|
|
202
|
+
return true;
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* The default per-MIME-type handler stacks reproducing the legacy
|
|
206
|
+
* {@link $insertDataTransferForRichText} behavior exactly. Stacked
|
|
207
|
+
* extensions append on top of these.
|
|
208
|
+
*
|
|
209
|
+
* @experimental
|
|
210
|
+
*/
|
|
211
|
+
const DEFAULT_IMPORT_MIME_TYPE = {
|
|
212
|
+
"application/x-lexical-editor": [$defaultLexicalEditorImporter],
|
|
213
|
+
"text/html": [$defaultHtmlImporter],
|
|
214
|
+
"text/plain": [$defaultPlainTextImporter],
|
|
215
|
+
"text/uri-list": [$defaultPlainTextImporter]
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Output of {@link ClipboardImportExtension}: the merged configuration
|
|
219
|
+
* plus a self-contained {@link $insertDataTransfer} function that owns
|
|
220
|
+
* the entire paste-side iteration over the priority list. Apps look this
|
|
221
|
+
* up via peer-dependency and call it directly; {@link
|
|
222
|
+
* $insertDataTransferForRichText} delegates to it.
|
|
223
|
+
*
|
|
224
|
+
* @experimental
|
|
225
|
+
*/
|
|
226
|
+
function $callImportMimeTypeFunctionStack(fns, data, selection, dataTransfer) {
|
|
227
|
+
if (!fns) return false;
|
|
228
|
+
const callAt = (i) => fns[i] ? fns[i](data, selection, callAt.bind(null, i - 1), dataTransfer) : false;
|
|
229
|
+
return callAt(fns.length - 1);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Sort the MIME types that have a registered handler stack by their
|
|
233
|
+
* configured priority weight (ascending). Types with no explicit weight
|
|
234
|
+
* sort after all weighted types, in lexical order, so unknown types
|
|
235
|
+
* remain reachable but never preempt a known one.
|
|
236
|
+
*/
|
|
237
|
+
function orderedMimeTypes(config) {
|
|
238
|
+
return Object.keys(config.$importMimeType).filter((k) => config.$importMimeType[k] !== void 0).sort((a, b) => {
|
|
239
|
+
const wa = config.priority[a];
|
|
240
|
+
const wb = config.priority[b];
|
|
241
|
+
if (wa === void 0 && wb === void 0) return a < b ? -1 : a > b ? 1 : 0;
|
|
242
|
+
if (wa === void 0) return 1;
|
|
243
|
+
if (wb === void 0) return -1;
|
|
244
|
+
return wa - wb;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function $runImport(config, dataTransfer, selection) {
|
|
248
|
+
const plainString = dataTransfer.getData("text/plain");
|
|
249
|
+
for (const mime of orderedMimeTypes(config)) {
|
|
250
|
+
const data = dataTransfer.getData(mime);
|
|
251
|
+
if (!data) continue;
|
|
252
|
+
if (mime === "text/html" && data === plainString) continue;
|
|
253
|
+
if ($callImportMimeTypeFunctionStack(config.$importMimeType[mime], data, selection, dataTransfer)) return true;
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
const DEFAULT_OUTPUT = {
|
|
258
|
+
$importMimeType: DEFAULT_IMPORT_MIME_TYPE,
|
|
259
|
+
$insertDataTransfer: (dataTransfer, selection) => $runImport({
|
|
260
|
+
$importMimeType: DEFAULT_IMPORT_MIME_TYPE,
|
|
261
|
+
priority: DEFAULT_IMPORT_MIME_TYPE_PRIORITY
|
|
262
|
+
}, dataTransfer, selection),
|
|
263
|
+
priority: DEFAULT_IMPORT_MIME_TYPE_PRIORITY
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* @internal
|
|
267
|
+
*
|
|
268
|
+
* Look up the {@link ClipboardImportOutput} on the active editor. Returns
|
|
269
|
+
* a static default-backed output when no {@link ClipboardImportExtension}
|
|
270
|
+
* is configured, so callers can always invoke `output.$insertDataTransfer`
|
|
271
|
+
* regardless of whether the editor opted in.
|
|
272
|
+
*/
|
|
273
|
+
function $getImportOutput() {
|
|
274
|
+
const dep = $getPeerDependency(ClipboardImportExtension.name);
|
|
275
|
+
return dep ? dep.output : DEFAULT_OUTPUT;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* @experimental
|
|
279
|
+
*
|
|
280
|
+
* Mirror of {@link GetClipboardDataExtension} for the import direction.
|
|
281
|
+
* Holds a per-MIME-type stack of {@link ImportMimeTypeFunction}s.
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* Route `text/html` pastes through {@link DOMImportExtension}, leaving the
|
|
285
|
+
* defaults for other MIME types untouched:
|
|
286
|
+
* ```ts
|
|
287
|
+
* import {configExtension, defineExtension, $getEditor} from 'lexical';
|
|
288
|
+
* import {
|
|
289
|
+
* ClipboardImportExtension,
|
|
290
|
+
* $insertGeneratedNodes,
|
|
291
|
+
* } from '@lexical/clipboard';
|
|
292
|
+
* import {
|
|
293
|
+
* contextValue,
|
|
294
|
+
* DOMImportExtension,
|
|
295
|
+
* ImportSource,
|
|
296
|
+
* ImportSourceDataTransfer,
|
|
297
|
+
* $generateNodesFromDOMViaExtension,
|
|
298
|
+
* } from '@lexical/html';
|
|
299
|
+
*
|
|
300
|
+
* defineExtension({
|
|
301
|
+
* name: 'app',
|
|
302
|
+
* dependencies: [
|
|
303
|
+
* DOMImportExtension,
|
|
304
|
+
* configExtension(ClipboardImportExtension, {
|
|
305
|
+
* $importMimeType: {
|
|
306
|
+
* 'text/html': [
|
|
307
|
+
* (html, selection, _$next, dataTransfer) => {
|
|
308
|
+
* const parser = new DOMParser();
|
|
309
|
+
* const dom = parser.parseFromString(html, 'text/html');
|
|
310
|
+
* const nodes = $generateNodesFromDOMViaExtension(dom, {
|
|
311
|
+
* context: [
|
|
312
|
+
* contextValue(ImportSource, 'paste'),
|
|
313
|
+
* contextValue(ImportSourceDataTransfer, dataTransfer),
|
|
314
|
+
* ],
|
|
315
|
+
* });
|
|
316
|
+
* $insertGeneratedNodes($getEditor(), nodes, selection);
|
|
317
|
+
* return true;
|
|
318
|
+
* },
|
|
319
|
+
* ],
|
|
320
|
+
* },
|
|
321
|
+
* }),
|
|
322
|
+
* ],
|
|
323
|
+
* });
|
|
324
|
+
* ```
|
|
325
|
+
*/
|
|
326
|
+
const ClipboardImportExtension = defineExtension({
|
|
327
|
+
build: (_editor, config) => ({
|
|
328
|
+
$importMimeType: config.$importMimeType,
|
|
329
|
+
$insertDataTransfer: (dataTransfer, selection) => $runImport(config, dataTransfer, selection),
|
|
330
|
+
priority: config.priority
|
|
331
|
+
}),
|
|
332
|
+
config: safeCast({
|
|
333
|
+
$importMimeType: DEFAULT_IMPORT_MIME_TYPE,
|
|
334
|
+
priority: DEFAULT_IMPORT_MIME_TYPE_PRIORITY
|
|
335
|
+
}),
|
|
336
|
+
mergeConfig(config, partial) {
|
|
337
|
+
const merged = shallowMergeConfig(config, partial);
|
|
338
|
+
if (partial.$importMimeType) {
|
|
339
|
+
const $importMimeType = { ...config.$importMimeType };
|
|
340
|
+
for (const [k, v] of Object.entries(partial.$importMimeType)) if (v) {
|
|
341
|
+
const prev = $importMimeType[k];
|
|
342
|
+
$importMimeType[k] = prev ? [...prev, ...v] : v;
|
|
343
|
+
}
|
|
344
|
+
merged.$importMimeType = $importMimeType;
|
|
345
|
+
}
|
|
346
|
+
if (partial.priority) merged.priority = {
|
|
347
|
+
...config.priority,
|
|
348
|
+
...partial.priority
|
|
349
|
+
};
|
|
350
|
+
return merged;
|
|
351
|
+
},
|
|
352
|
+
name: "@lexical/clipboard/Import"
|
|
353
|
+
});
|
|
354
|
+
/**
|
|
355
|
+
* @experimental
|
|
356
|
+
*
|
|
357
|
+
* Drop-in extension that routes `text/html` clipboard pastes and drops
|
|
358
|
+
* through the {@link DOMImportExtension} pipeline (rules, schemas,
|
|
359
|
+
* preprocessors, overlays) instead of the legacy
|
|
360
|
+
* {@link $generateNodesFromDOM}. Add to your extension dependencies along
|
|
361
|
+
* with the per-package import extensions you want active
|
|
362
|
+
* ({@link CoreImportExtension}, {@link RichTextImportExtension}, etc.).
|
|
363
|
+
*
|
|
364
|
+
* The original {@link DataTransfer} and `'paste'` source kind are forwarded
|
|
365
|
+
* into the import context so rules and preprocessors can read them via
|
|
366
|
+
* `ctx.get(ImportSourceDataTransfer)` / `ctx.get(ImportSource)`.
|
|
367
|
+
*
|
|
368
|
+
* Equivalent to stacking this `text/html` handler manually via
|
|
369
|
+
* `configExtension(ClipboardImportExtension, {...})`.
|
|
370
|
+
*
|
|
371
|
+
* @example
|
|
372
|
+
* ```ts
|
|
373
|
+
* import {defineExtension} from 'lexical';
|
|
374
|
+
* import {ClipboardDOMImportExtension} from '@lexical/clipboard';
|
|
375
|
+
* import {CoreImportExtension, RichTextImportExtension} from '@lexical/html';
|
|
376
|
+
*
|
|
377
|
+
* defineExtension({
|
|
378
|
+
* name: 'app',
|
|
379
|
+
* dependencies: [
|
|
380
|
+
* CoreImportExtension,
|
|
381
|
+
* RichTextImportExtension,
|
|
382
|
+
* ClipboardDOMImportExtension,
|
|
383
|
+
* ],
|
|
384
|
+
* });
|
|
385
|
+
* ```
|
|
386
|
+
*/
|
|
387
|
+
const ClipboardDOMImportExtension = defineExtension({
|
|
388
|
+
dependencies: [configExtension(ClipboardImportExtension, { $importMimeType: { "text/html": [(html, selection, _$next, dataTransfer) => {
|
|
389
|
+
const nodes = $generateNodesFromDOMViaExtension(new DOMParser().parseFromString(trustHTML(html), "text/html"), { context: [contextValue(ImportSource, "paste"), contextValue(ImportSourceDataTransfer, dataTransfer)] });
|
|
390
|
+
$insertGeneratedNodes($getEditor(), nodes, selection);
|
|
391
|
+
return true;
|
|
392
|
+
}] } })],
|
|
393
|
+
name: "@lexical/clipboard/DOMImport"
|
|
394
|
+
});
|
|
395
|
+
/**
|
|
396
|
+
* Returns the *currently selected* Lexical content as an HTML string, relying on the
|
|
397
|
+
* logic defined in the exportDOM methods on the LexicalNode classes. Note that
|
|
398
|
+
* this will not return the HTML content of the entire editor (unless all the content is included
|
|
399
|
+
* in the current selection).
|
|
400
|
+
*
|
|
401
|
+
* @param editor - LexicalEditor instance to get HTML content from
|
|
402
|
+
* @param selection - The selection to use (default is $getSelection())
|
|
403
|
+
* @returns a string of HTML content
|
|
404
|
+
*/
|
|
405
|
+
function $getHtmlContent(editor, selection = $getSelection()) {
|
|
406
|
+
if (selection == null) formatDevErrorMessage(`Expected valid LexicalSelection`);
|
|
407
|
+
if ($isRangeSelection(selection) && selection.isCollapsed() || selection.getNodes().length === 0) return "";
|
|
408
|
+
return $generateHtmlFromNodes(editor, selection);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Returns the *currently selected* Lexical content as a JSON string, relying on the
|
|
412
|
+
* logic defined in the exportJSON methods on the LexicalNode classes. Note that
|
|
413
|
+
* this will not return the JSON content of the entire editor (unless all the content is included
|
|
414
|
+
* in the current selection).
|
|
415
|
+
*
|
|
416
|
+
* @param editor - LexicalEditor instance to get the JSON content from
|
|
417
|
+
* @param selection - The selection to use (default is $getSelection())
|
|
418
|
+
* @returns
|
|
419
|
+
*/
|
|
420
|
+
function $getLexicalContent(editor, selection = $getSelection()) {
|
|
421
|
+
if (selection == null) formatDevErrorMessage(`Expected valid LexicalSelection`);
|
|
422
|
+
if ($isRangeSelection(selection) && selection.isCollapsed() || selection.getNodes().length === 0) return null;
|
|
423
|
+
return JSON.stringify($generateJSONFromSelectedNodes(editor, selection));
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Attempts to insert content of the mime-types text/plain or text/uri-list from
|
|
427
|
+
* the provided DataTransfer object into the editor at the provided selection.
|
|
428
|
+
* text/uri-list is only used if text/plain is not also provided.
|
|
429
|
+
*
|
|
430
|
+
* @param dataTransfer an object conforming to the [DataTransfer interface] (https://html.spec.whatwg.org/multipage/dnd.html#the-datatransfer-interface)
|
|
431
|
+
* @param selection the selection to use as the insertion point for the content in the DataTransfer object
|
|
432
|
+
*/
|
|
433
|
+
function $insertDataTransferForPlainText(dataTransfer, selection) {
|
|
434
|
+
const text = dataTransfer.getData("text/plain") || dataTransfer.getData("text/uri-list");
|
|
435
|
+
if (text != null) selection.insertRawText(text);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Insert the contents of `dataTransfer` at `selection` using the rich-text
|
|
439
|
+
* import pipeline (`application/x-lexical-editor` → `text/html` → `text/plain`
|
|
440
|
+
* → `text/uri-list`, in descending order of priority).
|
|
441
|
+
*
|
|
442
|
+
* @param dataTransfer an object conforming to the [DataTransfer interface] (https://html.spec.whatwg.org/multipage/dnd.html#the-datatransfer-interface)
|
|
443
|
+
* @param selection the selection to use as the insertion point for the content in the DataTransfer object
|
|
444
|
+
* @param _editor unused; retained for backwards compatibility. Safe to
|
|
445
|
+
* omit on new call sites.
|
|
446
|
+
*/
|
|
447
|
+
function $insertDataTransferForRichText(dataTransfer, selection, _editor) {
|
|
448
|
+
$getImportOutput().$insertDataTransfer(dataTransfer, selection);
|
|
449
|
+
}
|
|
450
|
+
const LEXICAL_DRAG_MIME_TYPE = "application/x-lexical-drag";
|
|
451
|
+
/**
|
|
452
|
+
* Populate `dataTransfer` with a marker identifying the current editor as a
|
|
453
|
+
* drag source. Pair this with {@link $handleRichTextDrop} or
|
|
454
|
+
* {@link $handlePlainTextDrop} on the drop side to get cut-and-paste semantics
|
|
455
|
+
* for drags that end in a different editor.
|
|
456
|
+
*
|
|
457
|
+
* Only the source editor's key needs to round-trip — the source's
|
|
458
|
+
* RangeSelection itself is preserved on the source editor between drag start
|
|
459
|
+
* and drop (Lexical suppresses selectionchange during drag), so the drop
|
|
460
|
+
* handler reads it directly via `$getSelection()` on the resolved source
|
|
461
|
+
* editor.
|
|
462
|
+
*
|
|
463
|
+
* Callers typically invoke this from a DRAGSTART_COMMAND handler alongside
|
|
464
|
+
* {@link setLexicalClipboardDataTransfer} (so that the dragged content itself
|
|
465
|
+
* round-trips with full node fidelity).
|
|
466
|
+
*/
|
|
467
|
+
function $writeDragSourceToDataTransfer(dataTransfer, editor) {
|
|
468
|
+
const marker = { editorKey: editor.getKey() };
|
|
469
|
+
dataTransfer.setData(LEXICAL_DRAG_MIME_TYPE, JSON.stringify(marker));
|
|
470
|
+
}
|
|
471
|
+
function isLexicalDragMarker(value) {
|
|
472
|
+
return value !== null && typeof value === "object" && "editorKey" in value && typeof value.editorKey === "string";
|
|
473
|
+
}
|
|
474
|
+
function readDragMarker(dataTransfer) {
|
|
475
|
+
const raw = dataTransfer.getData(LEXICAL_DRAG_MIME_TYPE);
|
|
476
|
+
if (!raw) return null;
|
|
477
|
+
let parsed;
|
|
478
|
+
try {
|
|
479
|
+
parsed = JSON.parse(raw);
|
|
480
|
+
} catch (_unused) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
return isLexicalDragMarker(parsed) ? parsed : null;
|
|
484
|
+
}
|
|
485
|
+
function findEditorRootByKey(key, doc) {
|
|
486
|
+
const elements = doc.querySelectorAll("[data-lexical-editor=\"true\"]");
|
|
487
|
+
for (const el of Array.from(elements)) {
|
|
488
|
+
const editor = el.__lexicalEditor;
|
|
489
|
+
if (editor && editor.getKey() === key) return el;
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
function $resolveDropPointCaret(event) {
|
|
494
|
+
const hit = caretFromPoint(event.clientX, event.clientY);
|
|
495
|
+
if (hit === null) return null;
|
|
496
|
+
const node = $getNearestNodeFromDOMNode(hit.node);
|
|
497
|
+
if (node === null) return null;
|
|
498
|
+
if ($isTextNode(node)) return $getTextPointCaret(node, "next", hit.offset);
|
|
499
|
+
if ($isElementNode(node)) return $getChildCaretAtIndex(node, hit.offset, "next");
|
|
500
|
+
const parent = node.getParent();
|
|
501
|
+
if (parent === null) return null;
|
|
502
|
+
return $getChildCaretAtIndex(parent, node.getIndexWithinParent() + 1, "next");
|
|
503
|
+
}
|
|
504
|
+
function $isDropCaretInsideSelection(dropCaret, selection) {
|
|
505
|
+
const { anchor: start, focus: end } = $getCaretRangeInDirection($caretRangeFromSelection(selection), "next");
|
|
506
|
+
return $comparePointCaretNext(start, dropCaret) < 0 && $comparePointCaretNext(dropCaret, end) < 0;
|
|
507
|
+
}
|
|
508
|
+
function $doDrop(event, editor, $insertDataTransfer) {
|
|
509
|
+
const dataTransfer = event.dataTransfer;
|
|
510
|
+
if (dataTransfer === null) return false;
|
|
511
|
+
const marker = readDragMarker(dataTransfer);
|
|
512
|
+
if (marker === null) return false;
|
|
513
|
+
const dropCaret = $resolveDropPointCaret(event);
|
|
514
|
+
if (dropCaret === null) return false;
|
|
515
|
+
const stableDropCaret = $splitAtPointCaretNext(dropCaret);
|
|
516
|
+
if (stableDropCaret === null) return false;
|
|
517
|
+
const isSameEditorDrag = marker.editorKey === editor.getKey();
|
|
518
|
+
const currentSelection = $getSelection();
|
|
519
|
+
if (isSameEditorDrag) {
|
|
520
|
+
if (!$isRangeSelection(currentSelection) || currentSelection.isCollapsed()) return false;
|
|
521
|
+
if ($isDropCaretInsideSelection(dropCaret, currentSelection)) {
|
|
522
|
+
event.preventDefault();
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
currentSelection.removeText();
|
|
526
|
+
}
|
|
527
|
+
if (!stableDropCaret.origin.isAttached()) {
|
|
528
|
+
event.preventDefault();
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
$insertDataTransfer(dataTransfer, $setSelectionFromCaretRange($getCollapsedCaretRange(stableDropCaret)), editor);
|
|
532
|
+
if (!isSameEditorDrag) {
|
|
533
|
+
const rootElement = editor.getRootElement();
|
|
534
|
+
const doc = rootElement ? rootElement.ownerDocument : null;
|
|
535
|
+
const sourceRoot = doc ? findEditorRootByKey(marker.editorKey, doc) : null;
|
|
536
|
+
if (sourceRoot !== null) sourceRoot.dispatchEvent(new InputEvent("beforeinput", {
|
|
537
|
+
bubbles: true,
|
|
538
|
+
cancelable: true,
|
|
539
|
+
inputType: "deleteByDrag"
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
542
|
+
event.preventDefault();
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Drop handler for rich-text editors. Inserts the DataTransfer payload via
|
|
547
|
+
* {@link $insertDataTransferForRichText} at the drop caret and, when the drag
|
|
548
|
+
* originated from a Lexical editor (marked via
|
|
549
|
+
* {@link $writeDragSourceToDataTransfer} on DRAGSTART), removes the source
|
|
550
|
+
* range — producing cut-and-paste semantics whether the drop is in the same
|
|
551
|
+
* editor or a different one on the same page.
|
|
552
|
+
*/
|
|
553
|
+
function $handleRichTextDrop(event, editor) {
|
|
554
|
+
return $doDrop(event, editor, $insertDataTransferForRichText);
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Drop handler for plain-text editors. Same semantics as
|
|
558
|
+
* {@link $handleRichTextDrop} but inserts via
|
|
559
|
+
* {@link $insertDataTransferForPlainText}.
|
|
560
|
+
*/
|
|
561
|
+
function $handlePlainTextDrop(event, editor) {
|
|
562
|
+
return $doDrop(event, editor, (dataTransfer, selection) => $insertDataTransferForPlainText(dataTransfer, selection));
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Inserts Lexical nodes into the editor using different strategies depending on
|
|
566
|
+
* some simple selection-based heuristics. If you're looking for a generic way to
|
|
567
|
+
* to insert nodes into the editor at a specific selection point, you probably want
|
|
568
|
+
* {@link lexical.$insertNodes}
|
|
569
|
+
*
|
|
570
|
+
* @param editor LexicalEditor instance to insert the nodes into.
|
|
571
|
+
* @param nodes The nodes to insert.
|
|
572
|
+
* @param selection The selection to insert the nodes into.
|
|
573
|
+
*/
|
|
574
|
+
function $insertGeneratedNodes(editor, nodes, selection) {
|
|
575
|
+
if (!editor.dispatchCommand(SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, {
|
|
576
|
+
nodes,
|
|
577
|
+
selection
|
|
578
|
+
})) {
|
|
579
|
+
selection.insertNodes(nodes);
|
|
580
|
+
$updateSelectionOnInsert(selection);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function $updateSelectionOnInsert(selection) {
|
|
584
|
+
if ($isRangeSelection(selection) && selection.isCollapsed()) {
|
|
585
|
+
const anchor = selection.anchor;
|
|
586
|
+
let nodeToInspect = null;
|
|
587
|
+
const anchorCaret = $caretFromPoint(anchor, "previous");
|
|
588
|
+
if (anchorCaret) if ($isTextPointCaret(anchorCaret)) nodeToInspect = anchorCaret.origin;
|
|
589
|
+
else {
|
|
590
|
+
const range = $getCaretRange(anchorCaret, $getChildCaret($getRoot(), "next").getFlipped());
|
|
591
|
+
for (const caret of range) if ($isTextNode(caret.origin)) {
|
|
592
|
+
nodeToInspect = caret.origin;
|
|
593
|
+
break;
|
|
594
|
+
} else if ($isElementNode(caret.origin) && !caret.origin.isInline()) break;
|
|
595
|
+
}
|
|
596
|
+
if (nodeToInspect && $isTextNode(nodeToInspect)) {
|
|
597
|
+
const newFormat = nodeToInspect.getFormat();
|
|
598
|
+
const newStyle = nodeToInspect.getStyle();
|
|
599
|
+
if (selection.format !== newFormat || selection.style !== newStyle) {
|
|
600
|
+
selection.format = newFormat;
|
|
601
|
+
selection.style = newStyle;
|
|
602
|
+
selection.dirty = true;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function exportNodeToJSON(node) {
|
|
608
|
+
const serializedNode = node.exportJSON();
|
|
609
|
+
const nodeClass = node.constructor;
|
|
610
|
+
if (serializedNode.type !== nodeClass.getType()) formatDevErrorMessage(`LexicalNode: Node ${nodeClass.name} does not implement .exportJSON().`);
|
|
611
|
+
if ($isElementNode(node)) {
|
|
612
|
+
const serializedChildren = serializedNode.children;
|
|
613
|
+
if (!Array.isArray(serializedChildren)) formatDevErrorMessage(`LexicalNode: Node ${nodeClass.name} is an element but .exportJSON() does not have a children array.`);
|
|
614
|
+
}
|
|
615
|
+
return serializedNode;
|
|
616
|
+
}
|
|
617
|
+
function $appendNodesToJSON(editor, selection, currentNode, targetArray = []) {
|
|
618
|
+
let shouldInclude = selection !== null ? currentNode.isSelected(selection) : true;
|
|
619
|
+
const shouldExclude = $isElementNode(currentNode) && currentNode.excludeFromCopy("html");
|
|
620
|
+
let target = currentNode;
|
|
621
|
+
if (selection !== null && $isTextNode(target)) target = $sliceSelectedTextNodeContent(selection, target, "clone");
|
|
622
|
+
const children = $isElementNode(target) ? target.getChildren() : [];
|
|
623
|
+
const serializedNode = exportNodeToJSON(target);
|
|
624
|
+
if ($isTextNode(target) && target.getTextContentSize() === 0) shouldInclude = false;
|
|
625
|
+
for (let i = 0; i < children.length; i++) {
|
|
626
|
+
const childNode = children[i];
|
|
627
|
+
const shouldIncludeChild = $appendNodesToJSON(editor, selection, childNode, serializedNode.children);
|
|
628
|
+
if (!shouldInclude && $isElementNode(currentNode) && shouldIncludeChild && currentNode.extractWithChild(childNode, selection, "clone")) shouldInclude = true;
|
|
629
|
+
}
|
|
630
|
+
if (shouldInclude && !shouldExclude) targetArray.push(serializedNode);
|
|
631
|
+
else if (Array.isArray(serializedNode.children)) for (let i = 0; i < serializedNode.children.length; i++) {
|
|
632
|
+
const serializedChildNode = serializedNode.children[i];
|
|
633
|
+
targetArray.push(serializedChildNode);
|
|
634
|
+
}
|
|
635
|
+
return shouldInclude;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Gets the Lexical JSON of the nodes inside the provided Selection.
|
|
639
|
+
*
|
|
640
|
+
* @param editor LexicalEditor to get the JSON content from.
|
|
641
|
+
* @param selection Selection to get the JSON content from.
|
|
642
|
+
* @returns an object with the editor namespace and a list of serializable nodes as JavaScript objects.
|
|
643
|
+
*/
|
|
644
|
+
function $generateJSONFromSelectedNodes(editor, selection) {
|
|
645
|
+
const nodes = [];
|
|
646
|
+
const topLevelChildren = $getRoot().getChildren();
|
|
647
|
+
for (let i = 0; i < topLevelChildren.length; i++) {
|
|
648
|
+
const topLevelNode = topLevelChildren[i];
|
|
649
|
+
$appendNodesToJSON(editor, selection, topLevelNode, nodes);
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
namespace: editor._config.namespace,
|
|
653
|
+
nodes
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
/**
|
|
657
|
+
* This method takes an array of objects conforming to the BaseSerializedNode interface and returns
|
|
658
|
+
* an Array containing instances of the corresponding LexicalNode classes registered on the editor.
|
|
659
|
+
* Normally, you'd get an Array of BaseSerialized nodes from {@link $generateJSONFromSelectedNodes}
|
|
660
|
+
*
|
|
661
|
+
* @param serializedNodes an Array of objects conforming to the BaseSerializedNode interface.
|
|
662
|
+
* @returns an Array of Lexical Node objects.
|
|
663
|
+
*/
|
|
664
|
+
function $generateNodesFromSerializedNodes(serializedNodes) {
|
|
665
|
+
const nodes = [];
|
|
666
|
+
for (const serializedNode of serializedNodes) nodes.push($parseSerializedNode(serializedNode));
|
|
667
|
+
return nodes;
|
|
668
|
+
}
|
|
669
|
+
const EVENT_LATENCY = 50;
|
|
670
|
+
let clipboardEventTimeout = null;
|
|
671
|
+
/**
|
|
672
|
+
* Copies the content of the current selection to the clipboard in
|
|
673
|
+
* text/plain, text/html, and application/x-lexical-editor (Lexical JSON)
|
|
674
|
+
* formats.
|
|
675
|
+
*
|
|
676
|
+
* @param editor the LexicalEditor instance to copy content from
|
|
677
|
+
* @param event the native browser ClipboardEvent to add the content to.
|
|
678
|
+
* @returns
|
|
679
|
+
*/
|
|
680
|
+
async function copyToClipboard(editor, event, data) {
|
|
681
|
+
if (clipboardEventTimeout !== null) return false;
|
|
682
|
+
if (event !== null) return new Promise((resolve, reject) => {
|
|
683
|
+
editor.update(() => {
|
|
684
|
+
resolve($copyToClipboardEvent(editor, event, data));
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
const rootElement = editor.getRootElement();
|
|
688
|
+
const editorWindow = editor._window || window;
|
|
689
|
+
const windowDocument = editorWindow.document;
|
|
690
|
+
const domSelection = getDOMSelection(editorWindow);
|
|
691
|
+
if (rootElement === null || domSelection === null) return false;
|
|
692
|
+
const element = windowDocument.createElement("span");
|
|
693
|
+
element.style.position = "fixed";
|
|
694
|
+
element.style.top = "-1000px";
|
|
695
|
+
element.append(windowDocument.createTextNode("#"));
|
|
696
|
+
rootElement.append(element);
|
|
697
|
+
const range = new Range();
|
|
698
|
+
range.setStart(element, 0);
|
|
699
|
+
range.setEnd(element, 1);
|
|
700
|
+
domSelection.removeAllRanges();
|
|
701
|
+
domSelection.addRange(range);
|
|
702
|
+
return new Promise((resolve, reject) => {
|
|
703
|
+
const removeListener = editor.registerCommand(COPY_COMMAND, (secondEvent) => {
|
|
704
|
+
if (objectKlassEquals(secondEvent, ClipboardEvent)) {
|
|
705
|
+
removeListener();
|
|
706
|
+
if (clipboardEventTimeout !== null) {
|
|
707
|
+
editorWindow.clearTimeout(clipboardEventTimeout);
|
|
708
|
+
clipboardEventTimeout = null;
|
|
709
|
+
}
|
|
710
|
+
resolve($copyToClipboardEvent(editor, secondEvent, data));
|
|
711
|
+
}
|
|
712
|
+
return true;
|
|
713
|
+
}, COMMAND_PRIORITY_CRITICAL);
|
|
714
|
+
clipboardEventTimeout = editorWindow.setTimeout(() => {
|
|
715
|
+
removeListener();
|
|
716
|
+
clipboardEventTimeout = null;
|
|
717
|
+
resolve(false);
|
|
718
|
+
}, EVENT_LATENCY);
|
|
719
|
+
windowDocument.execCommand("copy");
|
|
720
|
+
element.remove();
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
function $copyToClipboardEvent(editor, event, data) {
|
|
724
|
+
if (data === void 0) {
|
|
725
|
+
const domSelection = getDOMSelection(editor._window);
|
|
726
|
+
const selection = $getSelection();
|
|
727
|
+
if (!selection || selection.isCollapsed()) return false;
|
|
728
|
+
if (!domSelection) return false;
|
|
729
|
+
const anchorDOM = domSelection.anchorNode;
|
|
730
|
+
const focusDOM = domSelection.focusNode;
|
|
731
|
+
if (anchorDOM !== null && focusDOM !== null && !isSelectionWithinEditor(editor, anchorDOM, focusDOM)) return false;
|
|
732
|
+
data = $getClipboardDataFromSelection(selection);
|
|
733
|
+
}
|
|
734
|
+
event.preventDefault();
|
|
735
|
+
const clipboardData = event.clipboardData;
|
|
736
|
+
if (clipboardData === null) return false;
|
|
737
|
+
setLexicalClipboardDataTransfer(clipboardData, data);
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
const clipboardDataFunctions = [["text/html", $getHtmlContent], ["application/x-lexical-editor", $getLexicalContent]];
|
|
741
|
+
/**
|
|
742
|
+
* Serialize the content of the current selection to strings in
|
|
743
|
+
* text/plain, text/html, and application/x-lexical-editor (Lexical JSON)
|
|
744
|
+
* formats (as available).
|
|
745
|
+
*
|
|
746
|
+
* @param selection the selection to serialize (defaults to $getSelection())
|
|
747
|
+
* @returns LexicalClipboardData
|
|
748
|
+
*/
|
|
749
|
+
function $getClipboardDataFromSelection(selection = $getSelection()) {
|
|
750
|
+
return $getClipboardDataWithConfigFromSelection($getExportConfig(), selection);
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Call setData on the given clipboardData for each MIME type present
|
|
754
|
+
* in the given data (from {@link $getClipboardDataFromSelection})
|
|
755
|
+
*
|
|
756
|
+
* @param clipboardData the event.clipboardData to populate from data
|
|
757
|
+
* @param data The lexical data
|
|
758
|
+
*/
|
|
759
|
+
function setLexicalClipboardDataTransfer(clipboardData, data) {
|
|
760
|
+
for (const [k] of clipboardDataFunctions) if (data[k] === void 0) clipboardData.setData(k, "");
|
|
761
|
+
for (const k in data) {
|
|
762
|
+
const v = data[k];
|
|
763
|
+
if (v !== void 0) clipboardData.setData(k, v);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* A function that produces the serialized representation of a selection for
|
|
768
|
+
* a single MIME type. Functions are arranged in a stack per MIME type (see
|
|
769
|
+
* {@link ExportMimeTypeConfig}); the function at the top of the stack is
|
|
770
|
+
* invoked first and may call `next()` to delegate to the previous function
|
|
771
|
+
* in the stack (typically the default Lexical serializer).
|
|
772
|
+
*
|
|
773
|
+
* Returning `null` from the top-most function omits that MIME type from the
|
|
774
|
+
* resulting {@link LexicalClipboardData}.
|
|
775
|
+
*
|
|
776
|
+
* @param selection - The selection to serialize, or `null` if there is none.
|
|
777
|
+
* @param next - Calls the previous handler in the stack and returns its
|
|
778
|
+
* result, or `null` if there is no previous handler.
|
|
779
|
+
* @returns The serialized string for this MIME type, or `null` to omit it.
|
|
780
|
+
*/
|
|
781
|
+
/**
|
|
782
|
+
* Configuration for {@link GetClipboardDataExtension}.
|
|
783
|
+
*/
|
|
784
|
+
/**
|
|
785
|
+
* A mapping from MIME type to a stack of {@link ExportMimeTypeFunction}.
|
|
786
|
+
*
|
|
787
|
+
* Each entry is an ordered array; the function at the highest index runs
|
|
788
|
+
* first and may call `next()` to fall through to the function below it.
|
|
789
|
+
* The default config provides a single fallback handler for
|
|
790
|
+
* `'application/x-lexical-editor'`, `'text/html'`, and `'text/plain'`.
|
|
791
|
+
*
|
|
792
|
+
* When {@link GetClipboardDataExtension} merges a partial config, new
|
|
793
|
+
* functions are appended to the existing array for each MIME type, so
|
|
794
|
+
* later-registered handlers run before earlier ones (including the
|
|
795
|
+
* defaults) and may delegate to them via `next()`. To register a brand new
|
|
796
|
+
* MIME type, supply a key not present in the default config; arbitrary
|
|
797
|
+
* string keys are accepted in addition to the keys of
|
|
798
|
+
* {@link LexicalClipboardData}.
|
|
799
|
+
*/
|
|
800
|
+
function $getExportConfig(editor = $getEditor()) {
|
|
801
|
+
const dep = getPeerDependencyFromEditor(editor, GetClipboardDataExtension.name);
|
|
802
|
+
return dep ? dep.output : DEFAULT_EXPORT_MIME_TYPE;
|
|
803
|
+
}
|
|
804
|
+
const DEFAULT_EXPORT_MIME_TYPE = {
|
|
805
|
+
"application/x-lexical-editor": [(sel, next) => sel ? $getLexicalContent($getEditor(), sel) : next()],
|
|
806
|
+
"text/html": [(sel, next) => sel ? $getHtmlContent($getEditor(), sel) : next()],
|
|
807
|
+
"text/plain": [(sel, next) => sel ? sel.getTextContent() : next()]
|
|
808
|
+
};
|
|
809
|
+
function $getClipboardDataWithConfigFromSelection($exportMimeType, selection) {
|
|
810
|
+
const clipboardData = { "text/plain": "" };
|
|
811
|
+
for (const [k, fns] of Object.entries($exportMimeType)) if (fns) {
|
|
812
|
+
const v = callExportMimeTypeFunctionStack(fns, selection);
|
|
813
|
+
if (v !== null) clipboardData[k] = v;
|
|
814
|
+
}
|
|
815
|
+
return clipboardData;
|
|
816
|
+
}
|
|
817
|
+
function callExportMimeTypeFunctionStack(fns, selection) {
|
|
818
|
+
const callAt = (i) => fns[i] ? fns[i](selection, callAt.bind(null, i - 1)) : null;
|
|
819
|
+
return callAt(fns.length - 1);
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Serialize the given selection for a single MIME type using the active
|
|
823
|
+
* editor's configured {@link ExportMimeTypeConfig}. The configured stack is
|
|
824
|
+
* read from {@link GetClipboardDataExtension} via the editor's peer
|
|
825
|
+
* dependency lookup; if the extension was not built into the editor, the
|
|
826
|
+
* default stack is used.
|
|
827
|
+
*
|
|
828
|
+
* Useful when only one MIME representation is needed rather than the full
|
|
829
|
+
* {@link LexicalClipboardData} produced by
|
|
830
|
+
* {@link $getClipboardDataFromSelection}.
|
|
831
|
+
*
|
|
832
|
+
* Must be called from within an editor update or read.
|
|
833
|
+
*
|
|
834
|
+
* @param mimeType - The MIME type to serialize, e.g. `'text/html'`,
|
|
835
|
+
* `'application/x-lexical-editor'`, `'text/plain'`, or any custom key
|
|
836
|
+
* registered in the {@link ExportMimeTypeConfig}.
|
|
837
|
+
* @param selection - The selection to serialize (defaults to
|
|
838
|
+
* `$getSelection()`).
|
|
839
|
+
* @returns The serialized string for the requested MIME type, or `null` if
|
|
840
|
+
* no handler is registered for it or every handler returned `null`.
|
|
841
|
+
*/
|
|
842
|
+
function $exportMimeTypeFromSelection(mimeType, selection = $getSelection()) {
|
|
843
|
+
return callExportMimeTypeFunctionStack($getExportConfig()[mimeType] || [], selection);
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Lexical extension that controls how the current selection is serialized
|
|
847
|
+
* into clipboard MIME types when copying or dragging out of the editor.
|
|
848
|
+
*
|
|
849
|
+
* The extension's config holds an {@link ExportMimeTypeConfig} — a stack of
|
|
850
|
+
* {@link ExportMimeTypeFunction} per MIME type. Out of the box it provides
|
|
851
|
+
* fallback serializers for `'application/x-lexical-editor'`, `'text/html'`,
|
|
852
|
+
* and `'text/plain'` that defer to {@link $getLexicalContent},
|
|
853
|
+
* {@link $getHtmlContent}, and `selection.getTextContent()` respectively.
|
|
854
|
+
*
|
|
855
|
+
* Apps can layer additional handlers on top to customize an existing
|
|
856
|
+
* payload (delegating to the default via `next()`) or to register an
|
|
857
|
+
* entirely new MIME type. Functions provided through `mergeConfig` are
|
|
858
|
+
* appended to the existing stack for each MIME type, so a newly registered
|
|
859
|
+
* handler runs first and may fall through to the previously registered
|
|
860
|
+
* handlers via its `next` argument.
|
|
861
|
+
*
|
|
862
|
+
* The extension's `output` is the resolved {@link ExportMimeTypeConfig},
|
|
863
|
+
* which {@link $getClipboardDataFromSelection} and
|
|
864
|
+
* {@link $exportMimeTypeFromSelection} read via the editor's peer
|
|
865
|
+
* dependency lookup.
|
|
866
|
+
*
|
|
867
|
+
* @example
|
|
868
|
+
* ```ts
|
|
869
|
+
* import {configExtension, defineExtension} from '@lexical/extension';
|
|
870
|
+
* import {GetClipboardDataExtension} from '@lexical/clipboard';
|
|
871
|
+
*
|
|
872
|
+
* const MyClipboardExtension = defineExtension({
|
|
873
|
+
* name: 'my-app/clipboard',
|
|
874
|
+
* dependencies: [
|
|
875
|
+
* configExtension(GetClipboardDataExtension, {
|
|
876
|
+
* $exportMimeType: {
|
|
877
|
+
* // Wrap the default HTML output with an app-specific marker.
|
|
878
|
+
* 'text/html': [
|
|
879
|
+
* (selection, next) => {
|
|
880
|
+
* const html = next();
|
|
881
|
+
* return html ? wrapWithMyAppMarker(html) : html;
|
|
882
|
+
* },
|
|
883
|
+
* ],
|
|
884
|
+
* // Add a brand-new MIME type.
|
|
885
|
+
* 'application/vnd.myapp+json': [
|
|
886
|
+
* (selection) =>
|
|
887
|
+
* selection ? exportMyAppFormat(selection) : null,
|
|
888
|
+
* ],
|
|
889
|
+
* },
|
|
890
|
+
* }),
|
|
891
|
+
* ],
|
|
892
|
+
* });
|
|
893
|
+
* ```
|
|
894
|
+
*/
|
|
895
|
+
const GetClipboardDataExtension = defineExtension({
|
|
896
|
+
build(editor, config, state) {
|
|
897
|
+
return config.$exportMimeType;
|
|
898
|
+
},
|
|
899
|
+
config: safeCast({ $exportMimeType: DEFAULT_EXPORT_MIME_TYPE }),
|
|
900
|
+
mergeConfig(config, partial) {
|
|
901
|
+
const merged = shallowMergeConfig(config, partial);
|
|
902
|
+
if (partial.$exportMimeType) {
|
|
903
|
+
const $exportMimeType = { ...config.$exportMimeType };
|
|
904
|
+
for (const [k, v] of Object.entries(partial.$exportMimeType)) if (v) {
|
|
905
|
+
const prev = $exportMimeType[k];
|
|
906
|
+
$exportMimeType[k] = prev ? [...prev, ...v] : v;
|
|
907
|
+
}
|
|
908
|
+
merged.$exportMimeType = $exportMimeType;
|
|
909
|
+
}
|
|
910
|
+
return merged;
|
|
911
|
+
},
|
|
912
|
+
name: "@lexical/clipboard/GetClipboardData"
|
|
913
|
+
});
|
|
914
|
+
//#endregion
|
|
915
|
+
export { $exportMimeTypeFromSelection, $generateJSONFromSelectedNodes, $generateNodesFromSerializedNodes, $getClipboardDataFromSelection, $getHtmlContent, $getLexicalContent, $handlePlainTextDrop, $handleRichTextDrop, $insertDataTransferForPlainText, $insertDataTransferForRichText, $insertGeneratedNodes, $writeDragSourceToDataTransfer, ClipboardDOMImportExtension, ClipboardImportExtension, DEFAULT_IMPORT_MIME_TYPE, DEFAULT_IMPORT_MIME_TYPE_PRIORITY, GetClipboardDataExtension, caretFromPoint, copyToClipboard, setLexicalClipboardDataTransfer };
|