@keboola/design 13.0.2 → 13.1.1
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/AGENTS.md +2 -0
- package/CHANGELOG.md +36 -0
- package/README.md +2 -2
- package/dist/{AIKit-DDIi0r4w.js → AIKit-BVCWP9Ks.js} +260 -68
- package/dist/{AIKit-DDIi0r4w.js.map → AIKit-BVCWP9Ks.js.map} +1 -1
- package/dist/{CodeEditor-jZMw1ioj.js → CodeEditor-Df7VVx08.js} +2 -2
- package/dist/{CodeEditor-jZMw1ioj.js.map → CodeEditor-Df7VVx08.js.map} +1 -1
- package/dist/Toast-NrOdDDru.js +76 -0
- package/dist/Toast-NrOdDDru.js.map +1 -0
- package/dist/editors.js +1 -1
- package/dist/index.d.ts +46 -12
- package/dist/index.js +5 -4
- package/dist/internal/blocks/AIKit.js +1 -1
- package/dist/internal/components/Toast.js +1 -1
- package/dist/internal/hooks/useAltDigitHotkey.js +51 -0
- package/dist/internal/hooks/useAltDigitHotkey.js.map +1 -0
- package/dist/markdown-editor.d.ts +22 -1
- package/dist/markdown-editor.js +512 -214
- package/dist/markdown-editor.js.map +1 -1
- package/dist/rjsf.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/dist/Toast-PfQf3vj-.js +0 -49
- package/dist/Toast-PfQf3vj-.js.map +0 -1
package/dist/markdown-editor.js
CHANGED
|
@@ -9,32 +9,35 @@ import { useStableCallback } from "./internal/hooks/useStableCallback.js";
|
|
|
9
9
|
import { TextInput } from "./TextInput-BWYXbfs4.js";
|
|
10
10
|
import { FormGroup } from "./FormGroup-CmCppxOn.js";
|
|
11
11
|
import { Skeleton } from "./internal/components/Skeleton/Skeleton.js";
|
|
12
|
+
import { Kbd } from "./Kbd-J4n1J6i4.js";
|
|
12
13
|
import { toast } from "./toast-CyXEdaCZ.js";
|
|
13
|
-
import { Suspense, createContext, lazy, use, useCallback, useEffect, useRef, useState } from "react";
|
|
14
|
+
import { Suspense, createContext, lazy, use, useCallback, useEffect, useId, useRef, useState } from "react";
|
|
14
15
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
15
16
|
import { createPortal } from "react-dom";
|
|
17
|
+
import { DismissableLayer } from "@radix-ui/react-dismissable-layer";
|
|
16
18
|
import { $createCodeNode, $isCodeNode, CodeNode } from "@lexical/code";
|
|
17
|
-
import { $createLinkNode, $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
|
|
18
|
-
import { $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode, REMOVE_LIST_COMMAND } from "@lexical/list";
|
|
19
|
-
import { $convertFromMarkdownString, $convertToMarkdownString, CHECK_LIST, TRANSFORMERS } from "@lexical/markdown";
|
|
20
19
|
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
|
|
20
|
+
import { AutoLinkPlugin } from "@lexical/react/LexicalAutoLinkPlugin";
|
|
21
21
|
import { CheckListPlugin } from "@lexical/react/LexicalCheckListPlugin";
|
|
22
22
|
import { ClickableLinkPlugin } from "@lexical/react/LexicalClickableLinkPlugin";
|
|
23
23
|
import { LexicalComposer } from "@lexical/react/LexicalComposer";
|
|
24
24
|
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
|
|
25
25
|
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
|
|
26
26
|
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
|
|
27
|
-
import { $createHorizontalRuleNode, $isHorizontalRuleNode, HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode";
|
|
28
27
|
import { HorizontalRulePlugin } from "@lexical/react/LexicalHorizontalRulePlugin";
|
|
29
28
|
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
|
|
30
29
|
import { ListPlugin } from "@lexical/react/LexicalListPlugin";
|
|
31
30
|
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
|
|
32
31
|
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
|
33
32
|
import { TabIndentationPlugin } from "@lexical/react/LexicalTabIndentationPlugin";
|
|
34
|
-
import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode, HeadingNode, QuoteNode } from "@lexical/rich-text";
|
|
35
|
-
import { $createParagraphNode, $createTextNode, $getNodeByKey, $getSelection, $insertNodes, $isRangeSelection, $setSelection, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_HIGH, DROP_COMMAND, DecoratorNode, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, KEY_DOWN_COMMAND, PASTE_COMMAND } from "lexical";
|
|
36
33
|
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
37
|
-
import { $
|
|
34
|
+
import { $addUpdateTag, $createParagraphNode, $createTextNode, $getNodeByKey, $getSelection, $insertNodes, $isParagraphNode, $isRangeSelection, $isTextNode, $parseSerializedNode, $setSelection, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, DROP_COMMAND, DecoratorNode, FORMAT_ELEMENT_COMMAND, FORMAT_TEXT_COMMAND, HISTORY_PUSH_TAG, KEY_DOWN_COMMAND, PASTE_COMMAND, PASTE_TAG, createEditor } from "lexical";
|
|
35
|
+
import { $findMatchingParent, $getNearestBlockElementAncestorOrThrow, IS_APPLE } from "@lexical/utils";
|
|
36
|
+
import { $createLinkNode, $isAutoLinkNode, $isLinkNode, AutoLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
|
|
37
|
+
import { $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode, REMOVE_LIST_COMMAND } from "@lexical/list";
|
|
38
|
+
import { $createHorizontalRuleNode, $isHorizontalRuleNode, HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode";
|
|
39
|
+
import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode, HeadingNode, QuoteNode } from "@lexical/rich-text";
|
|
40
|
+
import { $convertFromMarkdownString, $convertToMarkdownString, CHECK_LIST, TRANSFORMERS } from "@lexical/markdown";
|
|
38
41
|
//#region src/blocks/MarkdownEditor/context.ts
|
|
39
42
|
const ImageRendererContext = createContext(null);
|
|
40
43
|
const ImageRendererProvider = ImageRendererContext;
|
|
@@ -140,73 +143,6 @@ const $isImageNode = (node) => {
|
|
|
140
143
|
return node instanceof ImageNode;
|
|
141
144
|
};
|
|
142
145
|
//#endregion
|
|
143
|
-
//#region src/blocks/MarkdownEditor/nodes/MermaidNodeComponent.tsx
|
|
144
|
-
const MermaidDiagram = lazy(() => import("./MermaidDiagram-B30x2yV7.js").then((n) => n.MermaidDiagram_exports).then((m) => ({ default: m.MermaidDiagram }), (error) => {
|
|
145
|
-
console.error("[MarkdownEditor] failed to load MermaidDiagram chunk", error);
|
|
146
|
-
return { default: ({ code }) => /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: code }) }) };
|
|
147
|
-
}));
|
|
148
|
-
const MermaidNodeComponent = ({ code }) => {
|
|
149
|
-
return /* @__PURE__ */ jsx(Suspense, {
|
|
150
|
-
fallback: /* @__PURE__ */ jsx("div", {
|
|
151
|
-
className: "tw:my-3 tw:overflow-hidden tw:rounded-card tw:border tw:border-neutral-150 tw:bg-neutral-50 tw:p-6",
|
|
152
|
-
children: /* @__PURE__ */ jsx(Skeleton, { className: "tw:h-40" })
|
|
153
|
-
}),
|
|
154
|
-
children: /* @__PURE__ */ jsx(MermaidDiagram, { code })
|
|
155
|
-
});
|
|
156
|
-
};
|
|
157
|
-
//#endregion
|
|
158
|
-
//#region src/blocks/MarkdownEditor/nodes/MermaidNode.tsx
|
|
159
|
-
var MermaidNode = class MermaidNode extends DecoratorNode {
|
|
160
|
-
__code;
|
|
161
|
-
static getType() {
|
|
162
|
-
return "mermaid";
|
|
163
|
-
}
|
|
164
|
-
static clone(node) {
|
|
165
|
-
return new MermaidNode(node.__code, node.__key);
|
|
166
|
-
}
|
|
167
|
-
constructor(code, key) {
|
|
168
|
-
super(key);
|
|
169
|
-
this.__code = code;
|
|
170
|
-
}
|
|
171
|
-
getCode() {
|
|
172
|
-
return this.__code;
|
|
173
|
-
}
|
|
174
|
-
setCode(code) {
|
|
175
|
-
const writable = this.getWritable();
|
|
176
|
-
writable.__code = code;
|
|
177
|
-
}
|
|
178
|
-
createDOM() {
|
|
179
|
-
const div = document.createElement("div");
|
|
180
|
-
div.setAttribute("data-lexical-mermaid", "true");
|
|
181
|
-
return div;
|
|
182
|
-
}
|
|
183
|
-
updateDOM() {
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
decorate() {
|
|
187
|
-
return /* @__PURE__ */ jsx(MermaidNodeComponent, { code: this.__code });
|
|
188
|
-
}
|
|
189
|
-
isInline() {
|
|
190
|
-
return false;
|
|
191
|
-
}
|
|
192
|
-
static importJSON(serializedNode) {
|
|
193
|
-
return $createMermaidNode(serializedNode.code);
|
|
194
|
-
}
|
|
195
|
-
exportJSON() {
|
|
196
|
-
return {
|
|
197
|
-
type: "mermaid",
|
|
198
|
-
version: 1,
|
|
199
|
-
code: this.__code
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
const $createMermaidNode = (code) => {
|
|
204
|
-
return new MermaidNode(code);
|
|
205
|
-
};
|
|
206
|
-
const $isMermaidNode = (node) => {
|
|
207
|
-
return node instanceof MermaidNode;
|
|
208
|
-
};
|
|
209
|
-
//#endregion
|
|
210
146
|
//#region src/blocks/MarkdownEditor/plugins/ImagePastePlugin.tsx
|
|
211
147
|
const ACCEPTED_IMAGE_TYPES = [
|
|
212
148
|
"image/png",
|
|
@@ -339,6 +275,186 @@ const KeyboardPlugin = ({ onSave, onCancel, singleLine, blurOnEscape, skipBlurSa
|
|
|
339
275
|
]);
|
|
340
276
|
return null;
|
|
341
277
|
};
|
|
278
|
+
//#endregion
|
|
279
|
+
//#region src/blocks/MarkdownEditor/nodes/MermaidNodeComponent.tsx
|
|
280
|
+
const MermaidDiagram = lazy(() => import("./MermaidDiagram-B30x2yV7.js").then((n) => n.MermaidDiagram_exports).then((m) => ({ default: m.MermaidDiagram }), (error) => {
|
|
281
|
+
console.error("[MarkdownEditor] failed to load MermaidDiagram chunk", error);
|
|
282
|
+
return { default: ({ code }) => /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: code }) }) };
|
|
283
|
+
}));
|
|
284
|
+
const MermaidNodeComponent = ({ code }) => {
|
|
285
|
+
return /* @__PURE__ */ jsx(Suspense, {
|
|
286
|
+
fallback: /* @__PURE__ */ jsx("div", {
|
|
287
|
+
className: "tw:my-3 tw:overflow-hidden tw:rounded-card tw:border tw:border-neutral-150 tw:bg-neutral-50 tw:p-6",
|
|
288
|
+
children: /* @__PURE__ */ jsx(Skeleton, { className: "tw:h-40" })
|
|
289
|
+
}),
|
|
290
|
+
children: /* @__PURE__ */ jsx(MermaidDiagram, { code })
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/blocks/MarkdownEditor/nodes/MermaidNode.tsx
|
|
295
|
+
var MermaidNode = class MermaidNode extends DecoratorNode {
|
|
296
|
+
__code;
|
|
297
|
+
static getType() {
|
|
298
|
+
return "mermaid";
|
|
299
|
+
}
|
|
300
|
+
static clone(node) {
|
|
301
|
+
return new MermaidNode(node.__code, node.__key);
|
|
302
|
+
}
|
|
303
|
+
constructor(code, key) {
|
|
304
|
+
super(key);
|
|
305
|
+
this.__code = code;
|
|
306
|
+
}
|
|
307
|
+
getCode() {
|
|
308
|
+
return this.__code;
|
|
309
|
+
}
|
|
310
|
+
setCode(code) {
|
|
311
|
+
const writable = this.getWritable();
|
|
312
|
+
writable.__code = code;
|
|
313
|
+
}
|
|
314
|
+
createDOM() {
|
|
315
|
+
const div = document.createElement("div");
|
|
316
|
+
div.setAttribute("data-lexical-mermaid", "true");
|
|
317
|
+
return div;
|
|
318
|
+
}
|
|
319
|
+
updateDOM() {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
decorate() {
|
|
323
|
+
return /* @__PURE__ */ jsx(MermaidNodeComponent, { code: this.__code });
|
|
324
|
+
}
|
|
325
|
+
isInline() {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
static importJSON(serializedNode) {
|
|
329
|
+
return $createMermaidNode(serializedNode.code);
|
|
330
|
+
}
|
|
331
|
+
exportJSON() {
|
|
332
|
+
return {
|
|
333
|
+
type: "mermaid",
|
|
334
|
+
version: 1,
|
|
335
|
+
code: this.__code
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
const $createMermaidNode = (code) => {
|
|
340
|
+
return new MermaidNode(code);
|
|
341
|
+
};
|
|
342
|
+
const $isMermaidNode = (node) => {
|
|
343
|
+
return node instanceof MermaidNode;
|
|
344
|
+
};
|
|
345
|
+
//#endregion
|
|
346
|
+
//#region src/blocks/MarkdownEditor/editorNodes.ts
|
|
347
|
+
/**
|
|
348
|
+
* Every node the editor registers, shared by the composer and the paste parser.
|
|
349
|
+
*
|
|
350
|
+
* - `AutoLinkNode` is registered always; only `autoLink` mounts the plugin that creates one.
|
|
351
|
+
*/
|
|
352
|
+
const EDITOR_NODES = [
|
|
353
|
+
ImageNode,
|
|
354
|
+
MermaidNode,
|
|
355
|
+
HeadingNode,
|
|
356
|
+
QuoteNode,
|
|
357
|
+
ListNode,
|
|
358
|
+
ListItemNode,
|
|
359
|
+
CodeNode,
|
|
360
|
+
LinkNode,
|
|
361
|
+
AutoLinkNode,
|
|
362
|
+
HorizontalRuleNode
|
|
363
|
+
];
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region src/blocks/MarkdownEditor/bareUrls.ts
|
|
366
|
+
const DOMAIN_CHAR = String.raw`(?:[^\s\p{P}\p{S}]|-)`;
|
|
367
|
+
const DOMAIN = String.raw`${DOMAIN_CHAR}+(?:[._]${DOMAIN_CHAR}+)*`;
|
|
368
|
+
const URL_START = String.raw`(?:[hH][tT][tT][pP][sS]?:\/\/|[wW][wW][wW]\.)`;
|
|
369
|
+
const URL_BODY = String.raw`${URL_START}${DOMAIN}[^\s<]*`;
|
|
370
|
+
const EMAIL_BODY = String.raw`[\w.+-]+@[\w-]+(?:\.[\w-]+)+`;
|
|
371
|
+
const CANDIDATE = new RegExp(String.raw`(?<![a-zA-Z])(?=[hH])${URL_BODY}|(?<=^|[\s(*_[\]~])(?=[wW])${URL_BODY}|(?<![\w.+/-])${EMAIL_BODY}`, "gu");
|
|
372
|
+
const URL_DOMAIN = new RegExp(String.raw`^${URL_START}(${DOMAIN})`, "u");
|
|
373
|
+
const TRAILING_PUNCTUATION = /[!"'*,.:;?_~\]]$/;
|
|
374
|
+
const TRAILING_ENTITY = /&[a-zA-Z][a-zA-Z0-9]*;$/;
|
|
375
|
+
const count = (text, char) => text.split(char).length - 1;
|
|
376
|
+
const trimTrailing = (candidate) => {
|
|
377
|
+
let text = candidate;
|
|
378
|
+
for (;;) {
|
|
379
|
+
const entity = TRAILING_ENTITY.exec(text);
|
|
380
|
+
if (entity) text = text.slice(0, -entity[0].length);
|
|
381
|
+
else if (TRAILING_PUNCTUATION.test(text)) text = text.slice(0, -1);
|
|
382
|
+
else if (text.endsWith(")") && count(text, ")") > count(text, "(")) text = text.slice(0, -1);
|
|
383
|
+
else return text;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
const hasLinkableDomain = (url) => {
|
|
387
|
+
const domain = URL_DOMAIN.exec(url)?.[1];
|
|
388
|
+
return domain !== void 0 && !domain.split(".").slice(-2).some((part) => part.includes("_"));
|
|
389
|
+
};
|
|
390
|
+
/**
|
|
391
|
+
* The bare URLs and email addresses in `text`, as GFM's autolink extension finds them.
|
|
392
|
+
*
|
|
393
|
+
* - `http://`, `https://` or `www.` and a domain, then anything up to whitespace or `<`.
|
|
394
|
+
* - Trailing `!"'*,.:;?_~]`, an unbalanced `)` and an entity like `&` stay outside.
|
|
395
|
+
* - `http(s)://` starts after anything but a letter, `www.` only after whitespace or `(*_[]~`.
|
|
396
|
+
* - An email ending in `-` or `_` is not one.
|
|
397
|
+
*/
|
|
398
|
+
const findBareUrls = (text, limit = Infinity) => {
|
|
399
|
+
const found = [];
|
|
400
|
+
for (const match of text.matchAll(CANDIDATE)) {
|
|
401
|
+
const source = match[0];
|
|
402
|
+
if (/^(?:https?:\/\/|www\.)/i.test(source)) {
|
|
403
|
+
const trimmed = trimTrailing(source);
|
|
404
|
+
if (!hasLinkableDomain(trimmed)) continue;
|
|
405
|
+
const url = /^www\./i.test(trimmed) ? `https://${trimmed}` : trimmed;
|
|
406
|
+
found.push({
|
|
407
|
+
index: match.index,
|
|
408
|
+
text: trimmed,
|
|
409
|
+
url
|
|
410
|
+
});
|
|
411
|
+
} else if (!/[-_]$/.test(source)) found.push({
|
|
412
|
+
index: match.index,
|
|
413
|
+
text: source,
|
|
414
|
+
url: `mailto:${source}`
|
|
415
|
+
});
|
|
416
|
+
if (found.length >= limit) break;
|
|
417
|
+
}
|
|
418
|
+
return found;
|
|
419
|
+
};
|
|
420
|
+
/**
|
|
421
|
+
* Every stretch of `text` GFM starts scanning as a URL or email, linked in the end or not.
|
|
422
|
+
*
|
|
423
|
+
* - Export writes these raw: GFM scans the run before trimming its end, and an escape moves where
|
|
424
|
+
* it ends a domain, so `https://a\_b.c\_d` is linked with its backslashes, `https://a_b.c_d` is not.
|
|
425
|
+
*/
|
|
426
|
+
const findUrlRuns = (text) => Array.from(text.matchAll(CANDIDATE), (match) => ({
|
|
427
|
+
index: match.index,
|
|
428
|
+
source: match[0]
|
|
429
|
+
}));
|
|
430
|
+
/**
|
|
431
|
+
* A `RegExp` whose `String#match` is the first URL `findBareUrls` finds, for Lexical's `importRegExp`.
|
|
432
|
+
*
|
|
433
|
+
* - Lexical's markdown import reads `importRegExp` only through `text.match(importRegExp)`.
|
|
434
|
+
* - So a candidate GFM rejects (`https://a_b.c_d`) never hides a valid URL after it.
|
|
435
|
+
*/
|
|
436
|
+
var FirstBareUrl = class extends RegExp {
|
|
437
|
+
[Symbol.match](text) {
|
|
438
|
+
const [url] = findBareUrls(text, 1);
|
|
439
|
+
if (!url) return null;
|
|
440
|
+
const match = [url.text];
|
|
441
|
+
return Object.assign(match, {
|
|
442
|
+
index: url.index,
|
|
443
|
+
input: text
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
const FIRST_BARE_URL = new FirstBareUrl(CANDIDATE.source, "u");
|
|
448
|
+
/** Lexical `AutoLinkPlugin` matcher: the first bare URL in the text, as `findBareUrls` reads it. */
|
|
449
|
+
const matchBareUrl = (text) => {
|
|
450
|
+
const [first] = findBareUrls(text, 1);
|
|
451
|
+
return first ? {
|
|
452
|
+
index: first.index,
|
|
453
|
+
length: first.text.length,
|
|
454
|
+
text: first.text,
|
|
455
|
+
url: first.url
|
|
456
|
+
} : null;
|
|
457
|
+
};
|
|
342
458
|
/**
|
|
343
459
|
* Our transformers plus Lexical's built-ins; order matters.
|
|
344
460
|
*
|
|
@@ -409,6 +525,44 @@ const CUSTOM_TRANSFORMERS = [
|
|
|
409
525
|
},
|
|
410
526
|
...TRANSFORMERS
|
|
411
527
|
];
|
|
528
|
+
const urlPlaceholder = (index) => `\uE000${index}\uE001`;
|
|
529
|
+
const URL_PLACEHOLDER = /\uE000(\d+)\uE001/g;
|
|
530
|
+
/** `CUSTOM_TRANSFORMERS` plus bare URLs, for `autoLink`. */
|
|
531
|
+
const AUTO_LINK_TRANSFORMERS = [{
|
|
532
|
+
dependencies: [],
|
|
533
|
+
importRegExp: FIRST_BARE_URL,
|
|
534
|
+
replace: (node) => {
|
|
535
|
+
const literal = $createTextNode(unescapeMarkdownText(node.getTextContent()));
|
|
536
|
+
literal.setFormat(node.getFormat());
|
|
537
|
+
node.replace(literal);
|
|
538
|
+
},
|
|
539
|
+
export: (node, _exportChildren, exportFormat) => {
|
|
540
|
+
if (!$isTextNode(node) || node.hasFormat("code")) return null;
|
|
541
|
+
const parent = node.getParent();
|
|
542
|
+
if ($isLinkNode(parent) && !$isAutoLinkNode(parent)) return null;
|
|
543
|
+
const text = node.getTextContent();
|
|
544
|
+
if (text.includes("")) return null;
|
|
545
|
+
const lead = $isAutoLinkNode(node.getPreviousSibling()) ? /^[^\s<]+/.exec(text)?.[0] ?? "" : "";
|
|
546
|
+
const urls = $isAutoLinkNode(parent) ? [{
|
|
547
|
+
index: 0,
|
|
548
|
+
source: text
|
|
549
|
+
}] : [...lead ? [{
|
|
550
|
+
index: 0,
|
|
551
|
+
source: lead
|
|
552
|
+
}] : [], ...findUrlRuns(text).filter((url) => url.index >= lead.length)];
|
|
553
|
+
if (urls.length === 0) return null;
|
|
554
|
+
let masked = "";
|
|
555
|
+
let end = 0;
|
|
556
|
+
urls.forEach((url, i) => {
|
|
557
|
+
masked += text.slice(end, url.index) + urlPlaceholder(i);
|
|
558
|
+
end = url.index + url.source.length;
|
|
559
|
+
});
|
|
560
|
+
masked += text.slice(end);
|
|
561
|
+
return exportFormat(node, masked).replace(URL_PLACEHOLDER, (_, i) => urls[Number(i)]?.source ?? "");
|
|
562
|
+
},
|
|
563
|
+
regExp: /(?!)/,
|
|
564
|
+
type: "text-match"
|
|
565
|
+
}, ...CUSTOM_TRANSFORMERS];
|
|
412
566
|
/**
|
|
413
567
|
* No transformers: `singleLine` mode edits plain-text entity names, not markdown.
|
|
414
568
|
*
|
|
@@ -456,6 +610,10 @@ const preprocessMarkdown = (markdown) => {
|
|
|
456
610
|
if (tableLines.length > 0) flushTable();
|
|
457
611
|
return result.join("\n");
|
|
458
612
|
};
|
|
613
|
+
/** Replaces the root with `markdown`: the one import a load, a `value` change and a paste share. */
|
|
614
|
+
const $importMarkdown = (markdown, transformers) => {
|
|
615
|
+
$convertFromMarkdownString(preprocessMarkdown(markdown), transformers);
|
|
616
|
+
};
|
|
459
617
|
/** Unwrap code-fenced tables back to raw GFM table markdown. */
|
|
460
618
|
const postprocessMarkdown = (markdown) => {
|
|
461
619
|
const regex = new RegExp(`\`\`\`${TABLE_FENCE_LANG}\\n([\\s\\S]*?)\\n\`\`\``, "g");
|
|
@@ -464,6 +622,81 @@ const postprocessMarkdown = (markdown) => {
|
|
|
464
622
|
/** Reverses Lexical's export escaping of `* _ ` ~ \`, for `singleLine` plain-text names. */
|
|
465
623
|
const unescapeMarkdownText = (markdown) => markdown.replace(/\\([*_`~\\])/g, "$1");
|
|
466
624
|
//#endregion
|
|
625
|
+
//#region src/blocks/MarkdownEditor/plugins/MarkdownPastePlugin.tsx
|
|
626
|
+
/**
|
|
627
|
+
* `markdown` as the top-level nodes the editor's own import makes of it.
|
|
628
|
+
*
|
|
629
|
+
* - Runs in a scratch editor: `$importMarkdown` replaces the root it imports into,
|
|
630
|
+
* and only nodes attached to a root get their lines merged into one paragraph.
|
|
631
|
+
*/
|
|
632
|
+
const parseMarkdown = (markdown, transformers) => {
|
|
633
|
+
const scratch = createEditor({
|
|
634
|
+
namespace: "MarkdownEditorPaste",
|
|
635
|
+
nodes: EDITOR_NODES,
|
|
636
|
+
onError: (error) => {
|
|
637
|
+
throw error;
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
scratch.update(() => {
|
|
641
|
+
$importMarkdown(markdown, transformers);
|
|
642
|
+
}, { discrete: true });
|
|
643
|
+
return scratch.getEditorState().toJSON().root.children;
|
|
644
|
+
};
|
|
645
|
+
/**
|
|
646
|
+
* Inserts parsed blocks at the selection, keeping a first or last block that is not a paragraph whole.
|
|
647
|
+
*
|
|
648
|
+
* - `insertNodes` merges the first block into a non-empty line and the rest of the line into the
|
|
649
|
+
* last block, which would flatten a fence pasted after `Logs:` into plain text.
|
|
650
|
+
* - An empty paragraph first is never merged, so the block after it goes in whole; it is then removed.
|
|
651
|
+
* - An empty paragraph last takes the rest of the line, and is removed when there was none.
|
|
652
|
+
*/
|
|
653
|
+
const $insertBlocks = (selection, nodes) => {
|
|
654
|
+
const first = nodes[0];
|
|
655
|
+
const last = nodes.at(-1);
|
|
656
|
+
if (!first || !last) return;
|
|
657
|
+
const head = $isParagraphNode(first) ? null : $createParagraphNode();
|
|
658
|
+
const tail = $isParagraphNode(last) ? null : $createParagraphNode();
|
|
659
|
+
selection.insertNodes([
|
|
660
|
+
head,
|
|
661
|
+
...nodes,
|
|
662
|
+
tail
|
|
663
|
+
].filter((node) => node !== null));
|
|
664
|
+
head?.remove();
|
|
665
|
+
if (!tail) return;
|
|
666
|
+
if (tail.isEmpty()) {
|
|
667
|
+
tail.remove();
|
|
668
|
+
last.selectEnd();
|
|
669
|
+
} else tail.selectStart();
|
|
670
|
+
};
|
|
671
|
+
/**
|
|
672
|
+
* Parses a plain-text paste as markdown and inserts the result at the selection.
|
|
673
|
+
*
|
|
674
|
+
* - Only for a clipboard with `text/plain` and neither `text/html` nor files; those keep
|
|
675
|
+
* Lexical's own paste, and files stay for the image uploader or the consumer.
|
|
676
|
+
* - Inside a code block the text goes in literally, through the same fallback.
|
|
677
|
+
* - One undo step: the update is tagged `history-push`.
|
|
678
|
+
*/
|
|
679
|
+
const MarkdownPastePlugin = ({ transformers }) => {
|
|
680
|
+
const [editor] = useLexicalComposerContext();
|
|
681
|
+
useEffect(() => {
|
|
682
|
+
return editor.registerCommand(PASTE_COMMAND, (event) => {
|
|
683
|
+
const data = "clipboardData" in event ? event.clipboardData : null;
|
|
684
|
+
if (!data || data.files.length > 0 || data.types.includes("text/html")) return false;
|
|
685
|
+
const text = data.getData("text/plain").replace(/\r\n?/g, "\n");
|
|
686
|
+
if (!text) return false;
|
|
687
|
+
const selection = $getSelection();
|
|
688
|
+
if (!$isRangeSelection(selection)) return false;
|
|
689
|
+
if ([selection.anchor, selection.focus].some((point) => $findMatchingParent(point.getNode(), $isCodeNode))) return false;
|
|
690
|
+
event.preventDefault();
|
|
691
|
+
$addUpdateTag(PASTE_TAG);
|
|
692
|
+
$addUpdateTag(HISTORY_PUSH_TAG);
|
|
693
|
+
$insertBlocks(selection, parseMarkdown(text, transformers).map($parseSerializedNode));
|
|
694
|
+
return true;
|
|
695
|
+
}, COMMAND_PRIORITY_LOW);
|
|
696
|
+
}, [editor, transformers]);
|
|
697
|
+
return null;
|
|
698
|
+
};
|
|
699
|
+
//#endregion
|
|
467
700
|
//#region src/blocks/MarkdownEditor/plugins/MarkdownSyncPlugin.tsx
|
|
468
701
|
const IMPORT_TAG = "markdown-import";
|
|
469
702
|
const MarkdownSyncPlugin = ({ value, editable, onChange, transformers = CUSTOM_TRANSFORMERS, singleLine = false }) => {
|
|
@@ -481,7 +714,7 @@ const MarkdownSyncPlugin = ({ value, editable, onChange, transformers = CUSTOM_T
|
|
|
481
714
|
if (value === lastExportedRef.current) return;
|
|
482
715
|
lastExportedRef.current = value;
|
|
483
716
|
editor.update(() => {
|
|
484
|
-
$
|
|
717
|
+
$importMarkdown(value, transformersRef.current);
|
|
485
718
|
}, { tag: IMPORT_TAG });
|
|
486
719
|
}, [editor, value]);
|
|
487
720
|
useEffect(() => {
|
|
@@ -523,6 +756,7 @@ const ALIGNMENT_OPTIONS = [
|
|
|
523
756
|
icon: AlignJustify
|
|
524
757
|
}
|
|
525
758
|
];
|
|
759
|
+
const TRIGGER_LABEL = "Text alignment";
|
|
526
760
|
const ALIGNMENT_ICONS = Object.fromEntries(ALIGNMENT_OPTIONS.map(({ value, icon }) => [value, icon]));
|
|
527
761
|
const AlignmentDropdown = ({ editor, alignment }) => {
|
|
528
762
|
const savedSelectionRef = useRef(null);
|
|
@@ -542,13 +776,14 @@ const AlignmentDropdown = ({ editor, alignment }) => {
|
|
|
542
776
|
return /* @__PURE__ */ jsxs(DropdownMenu, {
|
|
543
777
|
onOpenChange: handleOpenChange,
|
|
544
778
|
children: [/* @__PURE__ */ jsx(Tooltip, {
|
|
545
|
-
tooltip:
|
|
779
|
+
tooltip: TRIGGER_LABEL,
|
|
546
780
|
placement: "top",
|
|
547
781
|
triggerAsChild: true,
|
|
548
782
|
children: /* @__PURE__ */ jsx(DropdownMenu.Trigger, {
|
|
549
783
|
asChild: true,
|
|
550
784
|
children: /* @__PURE__ */ jsxs("button", {
|
|
551
785
|
type: "button",
|
|
786
|
+
"aria-label": TRIGGER_LABEL,
|
|
552
787
|
className: cn("tw:flex tw:h-7 tw:cursor-pointer tw:items-center tw:gap-1 tw:rounded tw:border-0 tw:bg-transparent tw:px-1.5 tw:text-xs tw:text-muted-foreground", "tw:hover:bg-accent"),
|
|
553
788
|
children: [/* @__PURE__ */ jsx(CurrentAlignmentIcon, { className: "tw:size-4" }), /* @__PURE__ */ jsx(ChevronDown, { className: "tw:size-3" })]
|
|
554
789
|
})
|
|
@@ -675,12 +910,39 @@ const BlockTypeDropdown = ({ editor, blockType }) => {
|
|
|
675
910
|
};
|
|
676
911
|
//#endregion
|
|
677
912
|
//#region src/blocks/MarkdownEditor/plugins/toolbar/ToolbarButton.tsx
|
|
678
|
-
|
|
679
|
-
|
|
913
|
+
/**
|
|
914
|
+
* The modifier Lexical binds its format keys to: Cmd on Apple platforms, Ctrl elsewhere.
|
|
915
|
+
*
|
|
916
|
+
* - Read from Lexical's own `IS_APPLE`, which counts iPhone and iPad where `isMac` does not.
|
|
917
|
+
*/
|
|
918
|
+
const MOD_KEY = IS_APPLE ? {
|
|
919
|
+
aria: "Meta",
|
|
920
|
+
symbol: "⌘"
|
|
921
|
+
} : {
|
|
922
|
+
aria: "Control",
|
|
923
|
+
symbol: "Ctrl"
|
|
924
|
+
};
|
|
925
|
+
const ToolbarButton = ({ active, disabled, onClick, tooltip, shortcutKey, children, className: extraClassName }) => /* @__PURE__ */ jsx(Tooltip, {
|
|
926
|
+
tooltip: shortcutKey ? /* @__PURE__ */ jsxs("span", {
|
|
927
|
+
className: "tw:inline-flex tw:items-center tw:gap-2",
|
|
928
|
+
children: [tooltip, /* @__PURE__ */ jsxs(Kbd.Group, {
|
|
929
|
+
"aria-hidden": true,
|
|
930
|
+
separator: "+",
|
|
931
|
+
children: [/* @__PURE__ */ jsx(Kbd, {
|
|
932
|
+
variant: "primary",
|
|
933
|
+
children: MOD_KEY.symbol
|
|
934
|
+
}), /* @__PURE__ */ jsx(Kbd, {
|
|
935
|
+
variant: "primary",
|
|
936
|
+
children: shortcutKey
|
|
937
|
+
})]
|
|
938
|
+
})]
|
|
939
|
+
}) : tooltip,
|
|
680
940
|
placement: "top",
|
|
681
941
|
triggerAsChild: true,
|
|
682
942
|
children: /* @__PURE__ */ jsx("button", {
|
|
683
943
|
type: "button",
|
|
944
|
+
"aria-label": tooltip,
|
|
945
|
+
"aria-keyshortcuts": shortcutKey ? `${MOD_KEY.aria}+${shortcutKey}` : void 0,
|
|
684
946
|
disabled,
|
|
685
947
|
onClick,
|
|
686
948
|
className: cn("tw:flex tw:h-7 tw:min-w-7 tw:cursor-pointer tw:items-center tw:justify-center tw:rounded tw:border-0 tw:bg-transparent tw:px-1 tw:text-xs tw:text-muted-foreground", "tw:hover:bg-accent tw:disabled:cursor-not-allowed tw:disabled:opacity-30", active && "tw:bg-secondary-100 tw:text-secondary-500", extraClassName),
|
|
@@ -712,6 +974,8 @@ const LinkPanel = ({ editor, isLink }) => {
|
|
|
712
974
|
const [label, setLabel] = useState("");
|
|
713
975
|
const savedSelectionRef = useRef(null);
|
|
714
976
|
const panelRef = useRef(null);
|
|
977
|
+
const labelId = useId();
|
|
978
|
+
const urlId = useId();
|
|
715
979
|
const handleOpen = () => {
|
|
716
980
|
editor.getEditorState().read(() => {
|
|
717
981
|
const selection = $getSelection();
|
|
@@ -723,13 +987,27 @@ const LinkPanel = ({ editor, isLink }) => {
|
|
|
723
987
|
});
|
|
724
988
|
setOpen(true);
|
|
725
989
|
};
|
|
726
|
-
const
|
|
990
|
+
const resetPanel = () => {
|
|
727
991
|
setOpen(false);
|
|
728
992
|
setUrl("");
|
|
729
993
|
setLabel("");
|
|
730
994
|
savedSelectionRef.current = null;
|
|
995
|
+
};
|
|
996
|
+
const handleClose = () => {
|
|
997
|
+
resetPanel();
|
|
731
998
|
editor.focus();
|
|
732
999
|
};
|
|
1000
|
+
/**
|
|
1001
|
+
* Closes the panel on Escape pressed anywhere while it is open.
|
|
1002
|
+
*
|
|
1003
|
+
* - `preventDefault` claims the key from a surface outside Radix's stack (`FullScreenModal`).
|
|
1004
|
+
* - Focus goes back to the editor only from inside the panel; anywhere else it stays put.
|
|
1005
|
+
*/
|
|
1006
|
+
const handleEscape = (event) => {
|
|
1007
|
+
event.preventDefault();
|
|
1008
|
+
if (panelRef.current?.contains(document.activeElement)) handleClose();
|
|
1009
|
+
else resetPanel();
|
|
1010
|
+
};
|
|
733
1011
|
const handleSubmit = () => {
|
|
734
1012
|
const trimmedUrl = url.trim();
|
|
735
1013
|
if (!trimmedUrl) return;
|
|
@@ -741,10 +1019,6 @@ const LinkPanel = ({ editor, isLink }) => {
|
|
|
741
1019
|
e.preventDefault();
|
|
742
1020
|
handleSubmit();
|
|
743
1021
|
}
|
|
744
|
-
if (e.key === "Escape") {
|
|
745
|
-
e.preventDefault();
|
|
746
|
-
handleClose();
|
|
747
|
-
}
|
|
748
1022
|
};
|
|
749
1023
|
if (isLink) return /* @__PURE__ */ jsx(ToolbarButton, {
|
|
750
1024
|
active: true,
|
|
@@ -759,18 +1033,19 @@ const LinkPanel = ({ editor, isLink }) => {
|
|
|
759
1033
|
onClick: handleOpen,
|
|
760
1034
|
tooltip: "Insert Link",
|
|
761
1035
|
children: /* @__PURE__ */ jsx(Link, { className: "tw:size-4" })
|
|
762
|
-
}), open && /* @__PURE__ */ jsx(
|
|
1036
|
+
}), open && /* @__PURE__ */ jsx(DismissableLayer, {
|
|
763
1037
|
ref: panelRef,
|
|
1038
|
+
onEscapeKeyDown: handleEscape,
|
|
764
1039
|
className: "tw:absolute tw:left-0 tw:top-full tw:z-50 tw:mt-1 tw:w-80 tw:rounded-md tw:border tw:border-border tw:bg-popover tw:p-3 tw:text-popover-foreground tw:shadow-lg",
|
|
765
1040
|
children: /* @__PURE__ */ jsxs("div", {
|
|
766
1041
|
className: "tw:flex tw:flex-col tw:gap-2",
|
|
767
1042
|
children: [
|
|
768
1043
|
/* @__PURE__ */ jsxs(FormGroup, { children: [/* @__PURE__ */ jsx(Label, {
|
|
769
|
-
htmlFor:
|
|
1044
|
+
htmlFor: labelId,
|
|
770
1045
|
className: "tw:mb-1 tw:text-xs",
|
|
771
1046
|
children: "Label"
|
|
772
1047
|
}), /* @__PURE__ */ jsx(TextInput, {
|
|
773
|
-
id:
|
|
1048
|
+
id: labelId,
|
|
774
1049
|
autoFocus: true,
|
|
775
1050
|
placeholder: "Link text",
|
|
776
1051
|
value: label,
|
|
@@ -779,11 +1054,11 @@ const LinkPanel = ({ editor, isLink }) => {
|
|
|
779
1054
|
variant: "secondary"
|
|
780
1055
|
})] }),
|
|
781
1056
|
/* @__PURE__ */ jsxs(FormGroup, { children: [/* @__PURE__ */ jsx(Label, {
|
|
782
|
-
htmlFor:
|
|
1057
|
+
htmlFor: urlId,
|
|
783
1058
|
className: "tw:mb-1 tw:text-xs",
|
|
784
1059
|
children: "URL"
|
|
785
1060
|
}), /* @__PURE__ */ jsx(TextInput, {
|
|
786
|
-
id:
|
|
1061
|
+
id: urlId,
|
|
787
1062
|
placeholder: "https://…",
|
|
788
1063
|
value: url,
|
|
789
1064
|
onValueChange: setUrl,
|
|
@@ -822,7 +1097,17 @@ const ELEMENT_FORMAT_TYPES = [
|
|
|
822
1097
|
];
|
|
823
1098
|
const isElementFormatType = (value) => ELEMENT_FORMAT_TYPES.includes(value);
|
|
824
1099
|
const toElementFormatType = (value) => isElementFormatType(value) ? value : "left";
|
|
825
|
-
const
|
|
1100
|
+
const TOOLBAR_BASE_CLASS = "tw:flex tw:items-center tw:gap-0.5 tw:rounded-lg tw:border tw:border-border tw:px-1.5 tw:py-1";
|
|
1101
|
+
/**
|
|
1102
|
+
* Keeps focus, and the editor selection with it, where it was when a toolbar control is pressed.
|
|
1103
|
+
*
|
|
1104
|
+
* - A link panel field takes focus as usual, so a click there lands the caret in it.
|
|
1105
|
+
*/
|
|
1106
|
+
const keepEditorFocus = (event) => {
|
|
1107
|
+
if (event.target instanceof HTMLInputElement) return;
|
|
1108
|
+
event.preventDefault();
|
|
1109
|
+
};
|
|
1110
|
+
const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
826
1111
|
const [editor] = useLexicalComposerContext();
|
|
827
1112
|
const [isBold, setIsBold] = useState(false);
|
|
828
1113
|
const [isItalic, setIsItalic] = useState(false);
|
|
@@ -837,6 +1122,7 @@ const ToolbarPlugin = () => {
|
|
|
837
1122
|
const toolbarRef = useRef(null);
|
|
838
1123
|
const isMouseDownRef = useRef(false);
|
|
839
1124
|
const hasSelectionRef = useRef(false);
|
|
1125
|
+
const isFloating = mode === "floating";
|
|
840
1126
|
const updatePosition = useCallback(() => {
|
|
841
1127
|
const nativeSelection = window.getSelection();
|
|
842
1128
|
if (!nativeSelection || nativeSelection.rangeCount === 0) return;
|
|
@@ -850,6 +1136,7 @@ const ToolbarPlugin = () => {
|
|
|
850
1136
|
}
|
|
851
1137
|
}, []);
|
|
852
1138
|
useEffect(() => {
|
|
1139
|
+
if (!isFloating) return;
|
|
853
1140
|
const handleMouseDown = () => {
|
|
854
1141
|
isMouseDownRef.current = true;
|
|
855
1142
|
};
|
|
@@ -863,7 +1150,7 @@ const ToolbarPlugin = () => {
|
|
|
863
1150
|
document.removeEventListener("mousedown", handleMouseDown);
|
|
864
1151
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
865
1152
|
};
|
|
866
|
-
}, []);
|
|
1153
|
+
}, [isFloating]);
|
|
867
1154
|
const updateToolbar = useCallback(() => {
|
|
868
1155
|
const selection = $getSelection();
|
|
869
1156
|
if (!$isRangeSelection(selection)) {
|
|
@@ -904,116 +1191,127 @@ const ToolbarPlugin = () => {
|
|
|
904
1191
|
unregisterUpdate();
|
|
905
1192
|
};
|
|
906
1193
|
}, [editor, updateToolbar]);
|
|
907
|
-
if (!isVisible || !position) return null;
|
|
908
|
-
|
|
1194
|
+
if (isFloating && (!isVisible || !position)) return null;
|
|
1195
|
+
const buttons = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1196
|
+
/* @__PURE__ */ jsx(BlockTypeDropdown, {
|
|
1197
|
+
editor,
|
|
1198
|
+
blockType
|
|
1199
|
+
}),
|
|
1200
|
+
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
1201
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1202
|
+
active: isBold,
|
|
1203
|
+
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold"),
|
|
1204
|
+
tooltip: "Bold",
|
|
1205
|
+
shortcutKey: "B",
|
|
1206
|
+
children: /* @__PURE__ */ jsx(Bold, { className: "tw:size-4" })
|
|
1207
|
+
}),
|
|
1208
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1209
|
+
active: isItalic,
|
|
1210
|
+
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic"),
|
|
1211
|
+
tooltip: "Italic",
|
|
1212
|
+
shortcutKey: "I",
|
|
1213
|
+
children: /* @__PURE__ */ jsx(Italic, { className: "tw:size-4" })
|
|
1214
|
+
}),
|
|
1215
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1216
|
+
active: isUnderline,
|
|
1217
|
+
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline"),
|
|
1218
|
+
tooltip: "Underline",
|
|
1219
|
+
shortcutKey: "U",
|
|
1220
|
+
children: /* @__PURE__ */ jsx(Underline, { className: "tw:size-4" })
|
|
1221
|
+
}),
|
|
1222
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1223
|
+
active: isStrikethrough,
|
|
1224
|
+
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough"),
|
|
1225
|
+
tooltip: "Strikethrough",
|
|
1226
|
+
children: /* @__PURE__ */ jsx(Strikethrough, { className: "tw:size-4" })
|
|
1227
|
+
}),
|
|
1228
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1229
|
+
active: isCode,
|
|
1230
|
+
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code"),
|
|
1231
|
+
tooltip: "Inline Code",
|
|
1232
|
+
children: /* @__PURE__ */ jsx(Code, { className: "tw:size-4" })
|
|
1233
|
+
}),
|
|
1234
|
+
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
1235
|
+
/* @__PURE__ */ jsx(LinkPanel, {
|
|
1236
|
+
editor,
|
|
1237
|
+
isLink
|
|
1238
|
+
}),
|
|
1239
|
+
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
1240
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1241
|
+
active: blockType === "bullet",
|
|
1242
|
+
onClick: () => {
|
|
1243
|
+
if (blockType === "bullet") editor.dispatchCommand(REMOVE_LIST_COMMAND, void 0);
|
|
1244
|
+
else editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, void 0);
|
|
1245
|
+
},
|
|
1246
|
+
tooltip: "Bullet List",
|
|
1247
|
+
children: /* @__PURE__ */ jsx(List, { className: "tw:size-4" })
|
|
1248
|
+
}),
|
|
1249
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1250
|
+
active: blockType === "number",
|
|
1251
|
+
onClick: () => {
|
|
1252
|
+
if (blockType === "number") editor.dispatchCommand(REMOVE_LIST_COMMAND, void 0);
|
|
1253
|
+
else editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, void 0);
|
|
1254
|
+
},
|
|
1255
|
+
tooltip: "Numbered List",
|
|
1256
|
+
children: /* @__PURE__ */ jsx(ListOrdered, { className: "tw:size-4" })
|
|
1257
|
+
}),
|
|
1258
|
+
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
1259
|
+
/* @__PURE__ */ jsx(AlignmentDropdown, {
|
|
1260
|
+
editor,
|
|
1261
|
+
alignment
|
|
1262
|
+
}),
|
|
1263
|
+
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
1264
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1265
|
+
active: blockType === "quote",
|
|
1266
|
+
onClick: () => {
|
|
1267
|
+
editor.update(() => {
|
|
1268
|
+
const selection = $getSelection();
|
|
1269
|
+
if (!$isRangeSelection(selection)) return;
|
|
1270
|
+
const anchorNode = selection.anchor.getNode();
|
|
1271
|
+
const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
|
|
1272
|
+
const newNode = blockType === "quote" ? $createParagraphNode() : $createQuoteNode();
|
|
1273
|
+
element.getChildren().forEach((child) => newNode.append(child));
|
|
1274
|
+
element.replace(newNode);
|
|
1275
|
+
});
|
|
1276
|
+
},
|
|
1277
|
+
tooltip: "Quote",
|
|
1278
|
+
children: /* @__PURE__ */ jsx(Quote, { className: "tw:size-4" })
|
|
1279
|
+
}),
|
|
1280
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1281
|
+
active: blockType === "code",
|
|
1282
|
+
onClick: () => {
|
|
1283
|
+
editor.update(() => {
|
|
1284
|
+
const selection = $getSelection();
|
|
1285
|
+
if (!$isRangeSelection(selection)) return;
|
|
1286
|
+
const anchorNode = selection.anchor.getNode();
|
|
1287
|
+
const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
|
|
1288
|
+
const newNode = blockType === "code" ? $createParagraphNode() : $createCodeNode();
|
|
1289
|
+
element.getChildren().forEach((child) => newNode.append(child));
|
|
1290
|
+
element.replace(newNode);
|
|
1291
|
+
});
|
|
1292
|
+
},
|
|
1293
|
+
tooltip: "Code Block",
|
|
1294
|
+
children: /* @__PURE__ */ jsx(SquareCode, { className: "tw:size-4" })
|
|
1295
|
+
})
|
|
1296
|
+
] });
|
|
1297
|
+
if (!isFloating) return /* @__PURE__ */ jsx("div", {
|
|
1298
|
+
role: "toolbar",
|
|
1299
|
+
"aria-label": "Formatting options",
|
|
1300
|
+
onMouseDown: keepEditorFocus,
|
|
1301
|
+
className: cn(TOOLBAR_BASE_CLASS, "tw:mb-1 tw:flex-wrap tw:bg-background"),
|
|
1302
|
+
children: buttons
|
|
1303
|
+
});
|
|
1304
|
+
return createPortal(/* @__PURE__ */ jsx("div", {
|
|
909
1305
|
ref: toolbarRef,
|
|
910
1306
|
role: "toolbar",
|
|
911
1307
|
"aria-label": "Formatting options",
|
|
912
|
-
onMouseDown:
|
|
1308
|
+
onMouseDown: keepEditorFocus,
|
|
913
1309
|
style: {
|
|
914
|
-
top: position
|
|
915
|
-
left: position
|
|
1310
|
+
top: position?.top,
|
|
1311
|
+
left: position?.left
|
|
916
1312
|
},
|
|
917
|
-
className: "tw:absolute tw:z-[1060] tw
|
|
918
|
-
children:
|
|
919
|
-
/* @__PURE__ */ jsx(BlockTypeDropdown, {
|
|
920
|
-
editor,
|
|
921
|
-
blockType
|
|
922
|
-
}),
|
|
923
|
-
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
924
|
-
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
925
|
-
active: isBold,
|
|
926
|
-
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold"),
|
|
927
|
-
tooltip: "Bold (Ctrl+B)",
|
|
928
|
-
children: /* @__PURE__ */ jsx(Bold, { className: "tw:size-4" })
|
|
929
|
-
}),
|
|
930
|
-
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
931
|
-
active: isItalic,
|
|
932
|
-
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic"),
|
|
933
|
-
tooltip: "Italic (Ctrl+I)",
|
|
934
|
-
children: /* @__PURE__ */ jsx(Italic, { className: "tw:size-4" })
|
|
935
|
-
}),
|
|
936
|
-
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
937
|
-
active: isUnderline,
|
|
938
|
-
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline"),
|
|
939
|
-
tooltip: "Underline (Ctrl+U)",
|
|
940
|
-
children: /* @__PURE__ */ jsx(Underline, { className: "tw:size-4" })
|
|
941
|
-
}),
|
|
942
|
-
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
943
|
-
active: isStrikethrough,
|
|
944
|
-
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough"),
|
|
945
|
-
tooltip: "Strikethrough",
|
|
946
|
-
children: /* @__PURE__ */ jsx(Strikethrough, { className: "tw:size-4" })
|
|
947
|
-
}),
|
|
948
|
-
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
949
|
-
active: isCode,
|
|
950
|
-
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code"),
|
|
951
|
-
tooltip: "Inline Code",
|
|
952
|
-
children: /* @__PURE__ */ jsx(Code, { className: "tw:size-4" })
|
|
953
|
-
}),
|
|
954
|
-
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
955
|
-
/* @__PURE__ */ jsx(LinkPanel, {
|
|
956
|
-
editor,
|
|
957
|
-
isLink
|
|
958
|
-
}),
|
|
959
|
-
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
960
|
-
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
961
|
-
active: blockType === "bullet",
|
|
962
|
-
onClick: () => {
|
|
963
|
-
if (blockType === "bullet") editor.dispatchCommand(REMOVE_LIST_COMMAND, void 0);
|
|
964
|
-
else editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, void 0);
|
|
965
|
-
},
|
|
966
|
-
tooltip: "Bullet List",
|
|
967
|
-
children: /* @__PURE__ */ jsx(List, { className: "tw:size-4" })
|
|
968
|
-
}),
|
|
969
|
-
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
970
|
-
active: blockType === "number",
|
|
971
|
-
onClick: () => {
|
|
972
|
-
if (blockType === "number") editor.dispatchCommand(REMOVE_LIST_COMMAND, void 0);
|
|
973
|
-
else editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, void 0);
|
|
974
|
-
},
|
|
975
|
-
tooltip: "Numbered List",
|
|
976
|
-
children: /* @__PURE__ */ jsx(ListOrdered, { className: "tw:size-4" })
|
|
977
|
-
}),
|
|
978
|
-
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
979
|
-
/* @__PURE__ */ jsx(AlignmentDropdown, {
|
|
980
|
-
editor,
|
|
981
|
-
alignment
|
|
982
|
-
}),
|
|
983
|
-
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
984
|
-
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
985
|
-
active: blockType === "quote",
|
|
986
|
-
onClick: () => {
|
|
987
|
-
editor.update(() => {
|
|
988
|
-
const selection = $getSelection();
|
|
989
|
-
if (!$isRangeSelection(selection)) return;
|
|
990
|
-
const anchorNode = selection.anchor.getNode();
|
|
991
|
-
const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
|
|
992
|
-
const newNode = blockType === "quote" ? $createParagraphNode() : $createQuoteNode();
|
|
993
|
-
element.getChildren().forEach((child) => newNode.append(child));
|
|
994
|
-
element.replace(newNode);
|
|
995
|
-
});
|
|
996
|
-
},
|
|
997
|
-
tooltip: "Quote",
|
|
998
|
-
children: /* @__PURE__ */ jsx(Quote, { className: "tw:size-4" })
|
|
999
|
-
}),
|
|
1000
|
-
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1001
|
-
active: blockType === "code",
|
|
1002
|
-
onClick: () => {
|
|
1003
|
-
editor.update(() => {
|
|
1004
|
-
const selection = $getSelection();
|
|
1005
|
-
if (!$isRangeSelection(selection)) return;
|
|
1006
|
-
const anchorNode = selection.anchor.getNode();
|
|
1007
|
-
const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
|
|
1008
|
-
const newNode = blockType === "code" ? $createParagraphNode() : $createCodeNode();
|
|
1009
|
-
element.getChildren().forEach((child) => newNode.append(child));
|
|
1010
|
-
element.replace(newNode);
|
|
1011
|
-
});
|
|
1012
|
-
},
|
|
1013
|
-
tooltip: "Code Block",
|
|
1014
|
-
children: /* @__PURE__ */ jsx(SquareCode, { className: "tw:size-4" })
|
|
1015
|
-
})
|
|
1016
|
-
]
|
|
1313
|
+
className: cn(TOOLBAR_BASE_CLASS, "tw:absolute tw:z-[1060] tw:-translate-y-full tw:bg-popover tw:text-popover-foreground tw:shadow-lg"),
|
|
1314
|
+
children: buttons
|
|
1017
1315
|
}), document.body);
|
|
1018
1316
|
};
|
|
1019
1317
|
//#endregion
|
|
@@ -1057,21 +1355,15 @@ const editorTheme = {
|
|
|
1057
1355
|
};
|
|
1058
1356
|
//#endregion
|
|
1059
1357
|
//#region src/blocks/MarkdownEditor/MarkdownEditor.tsx
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
HeadingNode,
|
|
1064
|
-
QuoteNode,
|
|
1065
|
-
ListNode,
|
|
1066
|
-
ListItemNode,
|
|
1067
|
-
CodeNode,
|
|
1068
|
-
LinkNode,
|
|
1069
|
-
HorizontalRuleNode
|
|
1070
|
-
];
|
|
1071
|
-
const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFocus = false, disabled = false, onSave, onCancel, onImageUpload, className, imageRenderer, showToolbar = false, compact = false, saveOnBlur = false, singleLine = false, clampDisplay = false, minRows }) => {
|
|
1358
|
+
const AUTO_LINK_MATCHERS = [matchBareUrl];
|
|
1359
|
+
const AUTO_LINK_EXCLUDED_PARENTS = [$isCodeNode];
|
|
1360
|
+
const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFocus = false, disabled = false, onSave, onCancel, onImageUpload, className, imageRenderer, showToolbar = false, toolbarMode = "floating", compact = false, saveOnBlur = false, singleLine = false, clampDisplay = false, minRows, autoLink = false, pasteMarkdown = false }) => {
|
|
1072
1361
|
const editable = !readOnly && !disabled;
|
|
1073
|
-
const
|
|
1362
|
+
const linkify = autoLink && !singleLine;
|
|
1363
|
+
const markdownPaste = pasteMarkdown && !singleLine;
|
|
1364
|
+
const transformers = singleLine ? SINGLE_LINE_TRANSFORMERS : linkify ? AUTO_LINK_TRANSFORMERS : CUSTOM_TRANSFORMERS;
|
|
1074
1365
|
const useDefaultMinHeight = !compact && minRows === void 0;
|
|
1366
|
+
const contentPadding = compact ? "tw:px-1.5 tw:py-0.5" : "tw:p-2";
|
|
1075
1367
|
const customMinHeight = !compact && minRows !== void 0 ? { minHeight: `${minRows * 1.75}em` } : void 0;
|
|
1076
1368
|
const [initialConfig] = useState(() => ({
|
|
1077
1369
|
namespace: "MarkdownEditor",
|
|
@@ -1082,7 +1374,7 @@ const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFo
|
|
|
1082
1374
|
},
|
|
1083
1375
|
editable,
|
|
1084
1376
|
editorState: () => {
|
|
1085
|
-
$
|
|
1377
|
+
$importMarkdown(value, transformers);
|
|
1086
1378
|
}
|
|
1087
1379
|
}));
|
|
1088
1380
|
const skipBlurSaveRef = useRef(false);
|
|
@@ -1101,16 +1393,17 @@ const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFo
|
|
|
1101
1393
|
className: cn(!compact && "tw:prose", compact && "tw:[&_p]:m-0 tw:[&_p+p]:mt-2", clampDisplay && "tw:[&:not(:focus-within)]:line-clamp-2", className),
|
|
1102
1394
|
onBlur: handleBlur,
|
|
1103
1395
|
children: [
|
|
1104
|
-
showToolbar && editable && /* @__PURE__ */ jsx(ToolbarPlugin, {}),
|
|
1396
|
+
showToolbar && editable && /* @__PURE__ */ jsx(ToolbarPlugin, { mode: toolbarMode }),
|
|
1105
1397
|
/* @__PURE__ */ jsx("div", {
|
|
1106
|
-
className: cn("tw:relative tw:rounded-md tw:transition-colors", compact
|
|
1398
|
+
className: cn("tw:relative tw:rounded-md tw:transition-colors", contentPadding, compact && "tw:-mx-1.5 tw:-my-0.5", editable && "tw:cursor-text tw:hover:bg-muted tw:focus-within:ring-2 tw:focus-within:ring-ring/40"),
|
|
1107
1399
|
children: /* @__PURE__ */ jsx(RichTextPlugin, {
|
|
1108
1400
|
contentEditable: /* @__PURE__ */ jsx(ContentEditable, {
|
|
1109
|
-
className: cn("tw:outline-hidden", useDefaultMinHeight && "tw:min-h-[100px]"),
|
|
1401
|
+
className: cn("tw:outline-hidden", !compact && "tw:[&>*:first-child]:mt-0 tw:[&>*:last-child]:mb-0", useDefaultMinHeight && "tw:min-h-[100px]"),
|
|
1110
1402
|
style: customMinHeight,
|
|
1111
1403
|
"aria-placeholder": placeholder ?? "",
|
|
1112
1404
|
placeholder: placeholder ? /* @__PURE__ */ jsx("div", {
|
|
1113
|
-
|
|
1405
|
+
"data-testid": "markdown-editor-placeholder",
|
|
1406
|
+
className: cn("tw:pointer-events-none tw:absolute tw:inset-0 tw:select-none tw:text-muted-foreground", contentPadding),
|
|
1114
1407
|
children: placeholder
|
|
1115
1408
|
}) : /* @__PURE__ */ jsx("div", {})
|
|
1116
1409
|
}),
|
|
@@ -1133,6 +1426,11 @@ const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFo
|
|
|
1133
1426
|
singleLine
|
|
1134
1427
|
}),
|
|
1135
1428
|
/* @__PURE__ */ jsx(ImagePastePlugin, { onImageUpload }),
|
|
1429
|
+
linkify && /* @__PURE__ */ jsx(AutoLinkPlugin, {
|
|
1430
|
+
matchers: AUTO_LINK_MATCHERS,
|
|
1431
|
+
excludeParents: AUTO_LINK_EXCLUDED_PARENTS
|
|
1432
|
+
}),
|
|
1433
|
+
markdownPaste && /* @__PURE__ */ jsx(MarkdownPastePlugin, { transformers }),
|
|
1136
1434
|
/* @__PURE__ */ jsx(KeyboardPlugin, {
|
|
1137
1435
|
onSave,
|
|
1138
1436
|
onCancel,
|