@keboola/design 13.0.1 → 13.1.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/AGENTS.md +2 -0
- package/CHANGELOG.md +31 -0
- package/README.md +2 -2
- package/dist/{AIKit-VB7CXeQk.js → AIKit-BVCWP9Ks.js} +261 -69
- package/dist/AIKit-BVCWP9Ks.js.map +1 -0
- package/dist/{CodeEditor-jZMw1ioj.js → CodeEditor-Df7VVx08.js} +2 -2
- package/dist/{CodeEditor-jZMw1ioj.js.map → CodeEditor-Df7VVx08.js.map} +1 -1
- package/dist/{ColorModeSwitch-BBzn1D0u.js → ColorModeSwitch-D8Cki137.js} +2 -2
- package/dist/{ColorModeSwitch-BBzn1D0u.js.map → ColorModeSwitch-D8Cki137.js.map} +1 -1
- package/dist/{DropdownMenu-C5FLPkvR.js → DropdownMenu-BJGLjpVc.js} +3 -3
- package/dist/{DropdownMenu-C5FLPkvR.js.map → DropdownMenu-BJGLjpVc.js.map} +1 -1
- package/dist/{SectionNav-BTBiitiQ.js → SectionNav-I2p2uXTd.js} +2 -2
- package/dist/{SectionNav-BTBiitiQ.js.map → SectionNav-I2p2uXTd.js.map} +1 -1
- package/dist/{Sidebar-BTv636Cq.js → Sidebar-D9ytIs8V.js} +2 -2
- package/dist/{Sidebar-BTv636Cq.js.map → Sidebar-D9ytIs8V.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 +9 -8
- package/dist/internal/blocks/AIKit.js +1 -1
- package/dist/internal/blocks/ColorModeSwitch.js +1 -1
- package/dist/internal/blocks/ExportMenu.js +1 -1
- package/dist/internal/blocks/SectionNav.js +1 -1
- package/dist/internal/blocks/SplitButton.js +1 -1
- package/dist/internal/components/DropdownMenu.js +1 -1
- package/dist/internal/components/Sidebar.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 +443 -201
- 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/AIKit-VB7CXeQk.js.map +0 -1
- package/dist/Toast-PfQf3vj-.js +0 -49
- package/dist/Toast-PfQf3vj-.js.map +0 -1
package/dist/markdown-editor.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Tooltip } from "./utils-Bd28bWgm.js";
|
|
|
3
3
|
import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold, ChevronDown, Code, Heading1, Heading2, Heading3, Italic, Link, List, ListOrdered, Quote, SquareCode, Strikethrough, Type, Underline } from "./icons-wcA70iQx.js";
|
|
4
4
|
import { Button } from "./Button-B21oVcPU.js";
|
|
5
5
|
import { ButtonGroup } from "./internal/components/ButtonGroup.js";
|
|
6
|
-
import { DropdownMenu } from "./DropdownMenu-
|
|
6
|
+
import { DropdownMenu } from "./DropdownMenu-BJGLjpVc.js";
|
|
7
7
|
import { Label } from "./internal/components/Label.js";
|
|
8
8
|
import { useStableCallback } from "./internal/hooks/useStableCallback.js";
|
|
9
9
|
import { TextInput } from "./TextInput-BWYXbfs4.js";
|
|
@@ -14,27 +14,28 @@ import { Suspense, createContext, lazy, use, useCallback, useEffect, useRef, use
|
|
|
14
14
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
15
15
|
import { createPortal } from "react-dom";
|
|
16
16
|
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
17
|
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
|
|
18
|
+
import { AutoLinkPlugin } from "@lexical/react/LexicalAutoLinkPlugin";
|
|
21
19
|
import { CheckListPlugin } from "@lexical/react/LexicalCheckListPlugin";
|
|
22
20
|
import { ClickableLinkPlugin } from "@lexical/react/LexicalClickableLinkPlugin";
|
|
23
21
|
import { LexicalComposer } from "@lexical/react/LexicalComposer";
|
|
24
22
|
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
|
|
25
23
|
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
|
|
26
24
|
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
|
|
27
|
-
import { $createHorizontalRuleNode, $isHorizontalRuleNode, HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode";
|
|
28
25
|
import { HorizontalRulePlugin } from "@lexical/react/LexicalHorizontalRulePlugin";
|
|
29
26
|
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
|
|
30
27
|
import { ListPlugin } from "@lexical/react/LexicalListPlugin";
|
|
31
28
|
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin";
|
|
32
29
|
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
|
33
30
|
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
31
|
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
37
|
-
import { $
|
|
32
|
+
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";
|
|
33
|
+
import { $findMatchingParent, $getNearestBlockElementAncestorOrThrow } from "@lexical/utils";
|
|
34
|
+
import { $createLinkNode, $isAutoLinkNode, $isLinkNode, AutoLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
|
|
35
|
+
import { $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, ListNode, REMOVE_LIST_COMMAND } from "@lexical/list";
|
|
36
|
+
import { $createHorizontalRuleNode, $isHorizontalRuleNode, HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode";
|
|
37
|
+
import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode, HeadingNode, QuoteNode } from "@lexical/rich-text";
|
|
38
|
+
import { $convertFromMarkdownString, $convertToMarkdownString, CHECK_LIST, TRANSFORMERS } from "@lexical/markdown";
|
|
38
39
|
//#region src/blocks/MarkdownEditor/context.ts
|
|
39
40
|
const ImageRendererContext = createContext(null);
|
|
40
41
|
const ImageRendererProvider = ImageRendererContext;
|
|
@@ -140,73 +141,6 @@ const $isImageNode = (node) => {
|
|
|
140
141
|
return node instanceof ImageNode;
|
|
141
142
|
};
|
|
142
143
|
//#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
144
|
//#region src/blocks/MarkdownEditor/plugins/ImagePastePlugin.tsx
|
|
211
145
|
const ACCEPTED_IMAGE_TYPES = [
|
|
212
146
|
"image/png",
|
|
@@ -339,6 +273,186 @@ const KeyboardPlugin = ({ onSave, onCancel, singleLine, blurOnEscape, skipBlurSa
|
|
|
339
273
|
]);
|
|
340
274
|
return null;
|
|
341
275
|
};
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/blocks/MarkdownEditor/nodes/MermaidNodeComponent.tsx
|
|
278
|
+
const MermaidDiagram = lazy(() => import("./MermaidDiagram-B30x2yV7.js").then((n) => n.MermaidDiagram_exports).then((m) => ({ default: m.MermaidDiagram }), (error) => {
|
|
279
|
+
console.error("[MarkdownEditor] failed to load MermaidDiagram chunk", error);
|
|
280
|
+
return { default: ({ code }) => /* @__PURE__ */ jsx("pre", { children: /* @__PURE__ */ jsx("code", { children: code }) }) };
|
|
281
|
+
}));
|
|
282
|
+
const MermaidNodeComponent = ({ code }) => {
|
|
283
|
+
return /* @__PURE__ */ jsx(Suspense, {
|
|
284
|
+
fallback: /* @__PURE__ */ jsx("div", {
|
|
285
|
+
className: "tw:my-3 tw:overflow-hidden tw:rounded-card tw:border tw:border-neutral-150 tw:bg-neutral-50 tw:p-6",
|
|
286
|
+
children: /* @__PURE__ */ jsx(Skeleton, { className: "tw:h-40" })
|
|
287
|
+
}),
|
|
288
|
+
children: /* @__PURE__ */ jsx(MermaidDiagram, { code })
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region src/blocks/MarkdownEditor/nodes/MermaidNode.tsx
|
|
293
|
+
var MermaidNode = class MermaidNode extends DecoratorNode {
|
|
294
|
+
__code;
|
|
295
|
+
static getType() {
|
|
296
|
+
return "mermaid";
|
|
297
|
+
}
|
|
298
|
+
static clone(node) {
|
|
299
|
+
return new MermaidNode(node.__code, node.__key);
|
|
300
|
+
}
|
|
301
|
+
constructor(code, key) {
|
|
302
|
+
super(key);
|
|
303
|
+
this.__code = code;
|
|
304
|
+
}
|
|
305
|
+
getCode() {
|
|
306
|
+
return this.__code;
|
|
307
|
+
}
|
|
308
|
+
setCode(code) {
|
|
309
|
+
const writable = this.getWritable();
|
|
310
|
+
writable.__code = code;
|
|
311
|
+
}
|
|
312
|
+
createDOM() {
|
|
313
|
+
const div = document.createElement("div");
|
|
314
|
+
div.setAttribute("data-lexical-mermaid", "true");
|
|
315
|
+
return div;
|
|
316
|
+
}
|
|
317
|
+
updateDOM() {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
decorate() {
|
|
321
|
+
return /* @__PURE__ */ jsx(MermaidNodeComponent, { code: this.__code });
|
|
322
|
+
}
|
|
323
|
+
isInline() {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
static importJSON(serializedNode) {
|
|
327
|
+
return $createMermaidNode(serializedNode.code);
|
|
328
|
+
}
|
|
329
|
+
exportJSON() {
|
|
330
|
+
return {
|
|
331
|
+
type: "mermaid",
|
|
332
|
+
version: 1,
|
|
333
|
+
code: this.__code
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
const $createMermaidNode = (code) => {
|
|
338
|
+
return new MermaidNode(code);
|
|
339
|
+
};
|
|
340
|
+
const $isMermaidNode = (node) => {
|
|
341
|
+
return node instanceof MermaidNode;
|
|
342
|
+
};
|
|
343
|
+
//#endregion
|
|
344
|
+
//#region src/blocks/MarkdownEditor/editorNodes.ts
|
|
345
|
+
/**
|
|
346
|
+
* Every node the editor registers, shared by the composer and the paste parser.
|
|
347
|
+
*
|
|
348
|
+
* - `AutoLinkNode` is registered always; only `autoLink` mounts the plugin that creates one.
|
|
349
|
+
*/
|
|
350
|
+
const EDITOR_NODES = [
|
|
351
|
+
ImageNode,
|
|
352
|
+
MermaidNode,
|
|
353
|
+
HeadingNode,
|
|
354
|
+
QuoteNode,
|
|
355
|
+
ListNode,
|
|
356
|
+
ListItemNode,
|
|
357
|
+
CodeNode,
|
|
358
|
+
LinkNode,
|
|
359
|
+
AutoLinkNode,
|
|
360
|
+
HorizontalRuleNode
|
|
361
|
+
];
|
|
362
|
+
//#endregion
|
|
363
|
+
//#region src/blocks/MarkdownEditor/bareUrls.ts
|
|
364
|
+
const DOMAIN_CHAR = String.raw`(?:[^\s\p{P}\p{S}]|-)`;
|
|
365
|
+
const DOMAIN = String.raw`${DOMAIN_CHAR}+(?:[._]${DOMAIN_CHAR}+)*`;
|
|
366
|
+
const URL_START = String.raw`(?:[hH][tT][tT][pP][sS]?:\/\/|[wW][wW][wW]\.)`;
|
|
367
|
+
const URL_BODY = String.raw`${URL_START}${DOMAIN}[^\s<]*`;
|
|
368
|
+
const EMAIL_BODY = String.raw`[\w.+-]+@[\w-]+(?:\.[\w-]+)+`;
|
|
369
|
+
const CANDIDATE = new RegExp(String.raw`(?<![a-zA-Z])(?=[hH])${URL_BODY}|(?<=^|[\s(*_[\]~])(?=[wW])${URL_BODY}|(?<![\w.+/-])${EMAIL_BODY}`, "gu");
|
|
370
|
+
const URL_DOMAIN = new RegExp(String.raw`^${URL_START}(${DOMAIN})`, "u");
|
|
371
|
+
const TRAILING_PUNCTUATION = /[!"'*,.:;?_~\]]$/;
|
|
372
|
+
const TRAILING_ENTITY = /&[a-zA-Z][a-zA-Z0-9]*;$/;
|
|
373
|
+
const count = (text, char) => text.split(char).length - 1;
|
|
374
|
+
const trimTrailing = (candidate) => {
|
|
375
|
+
let text = candidate;
|
|
376
|
+
for (;;) {
|
|
377
|
+
const entity = TRAILING_ENTITY.exec(text);
|
|
378
|
+
if (entity) text = text.slice(0, -entity[0].length);
|
|
379
|
+
else if (TRAILING_PUNCTUATION.test(text)) text = text.slice(0, -1);
|
|
380
|
+
else if (text.endsWith(")") && count(text, ")") > count(text, "(")) text = text.slice(0, -1);
|
|
381
|
+
else return text;
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
const hasLinkableDomain = (url) => {
|
|
385
|
+
const domain = URL_DOMAIN.exec(url)?.[1];
|
|
386
|
+
return domain !== void 0 && !domain.split(".").slice(-2).some((part) => part.includes("_"));
|
|
387
|
+
};
|
|
388
|
+
/**
|
|
389
|
+
* The bare URLs and email addresses in `text`, as GFM's autolink extension finds them.
|
|
390
|
+
*
|
|
391
|
+
* - `http://`, `https://` or `www.` and a domain, then anything up to whitespace or `<`.
|
|
392
|
+
* - Trailing `!"'*,.:;?_~]`, an unbalanced `)` and an entity like `&` stay outside.
|
|
393
|
+
* - `http(s)://` starts after anything but a letter, `www.` only after whitespace or `(*_[]~`.
|
|
394
|
+
* - An email ending in `-` or `_` is not one.
|
|
395
|
+
*/
|
|
396
|
+
const findBareUrls = (text, limit = Infinity) => {
|
|
397
|
+
const found = [];
|
|
398
|
+
for (const match of text.matchAll(CANDIDATE)) {
|
|
399
|
+
const source = match[0];
|
|
400
|
+
if (/^(?:https?:\/\/|www\.)/i.test(source)) {
|
|
401
|
+
const trimmed = trimTrailing(source);
|
|
402
|
+
if (!hasLinkableDomain(trimmed)) continue;
|
|
403
|
+
const url = /^www\./i.test(trimmed) ? `https://${trimmed}` : trimmed;
|
|
404
|
+
found.push({
|
|
405
|
+
index: match.index,
|
|
406
|
+
text: trimmed,
|
|
407
|
+
url
|
|
408
|
+
});
|
|
409
|
+
} else if (!/[-_]$/.test(source)) found.push({
|
|
410
|
+
index: match.index,
|
|
411
|
+
text: source,
|
|
412
|
+
url: `mailto:${source}`
|
|
413
|
+
});
|
|
414
|
+
if (found.length >= limit) break;
|
|
415
|
+
}
|
|
416
|
+
return found;
|
|
417
|
+
};
|
|
418
|
+
/**
|
|
419
|
+
* Every stretch of `text` GFM starts scanning as a URL or email, linked in the end or not.
|
|
420
|
+
*
|
|
421
|
+
* - Export writes these raw: GFM scans the run before trimming its end, and an escape moves where
|
|
422
|
+
* it ends a domain, so `https://a\_b.c\_d` is linked with its backslashes, `https://a_b.c_d` is not.
|
|
423
|
+
*/
|
|
424
|
+
const findUrlRuns = (text) => Array.from(text.matchAll(CANDIDATE), (match) => ({
|
|
425
|
+
index: match.index,
|
|
426
|
+
source: match[0]
|
|
427
|
+
}));
|
|
428
|
+
/**
|
|
429
|
+
* A `RegExp` whose `String#match` is the first URL `findBareUrls` finds, for Lexical's `importRegExp`.
|
|
430
|
+
*
|
|
431
|
+
* - Lexical's markdown import reads `importRegExp` only through `text.match(importRegExp)`.
|
|
432
|
+
* - So a candidate GFM rejects (`https://a_b.c_d`) never hides a valid URL after it.
|
|
433
|
+
*/
|
|
434
|
+
var FirstBareUrl = class extends RegExp {
|
|
435
|
+
[Symbol.match](text) {
|
|
436
|
+
const [url] = findBareUrls(text, 1);
|
|
437
|
+
if (!url) return null;
|
|
438
|
+
const match = [url.text];
|
|
439
|
+
return Object.assign(match, {
|
|
440
|
+
index: url.index,
|
|
441
|
+
input: text
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
const FIRST_BARE_URL = new FirstBareUrl(CANDIDATE.source, "u");
|
|
446
|
+
/** Lexical `AutoLinkPlugin` matcher: the first bare URL in the text, as `findBareUrls` reads it. */
|
|
447
|
+
const matchBareUrl = (text) => {
|
|
448
|
+
const [first] = findBareUrls(text, 1);
|
|
449
|
+
return first ? {
|
|
450
|
+
index: first.index,
|
|
451
|
+
length: first.text.length,
|
|
452
|
+
text: first.text,
|
|
453
|
+
url: first.url
|
|
454
|
+
} : null;
|
|
455
|
+
};
|
|
342
456
|
/**
|
|
343
457
|
* Our transformers plus Lexical's built-ins; order matters.
|
|
344
458
|
*
|
|
@@ -409,6 +523,44 @@ const CUSTOM_TRANSFORMERS = [
|
|
|
409
523
|
},
|
|
410
524
|
...TRANSFORMERS
|
|
411
525
|
];
|
|
526
|
+
const urlPlaceholder = (index) => `\uE000${index}\uE001`;
|
|
527
|
+
const URL_PLACEHOLDER = /\uE000(\d+)\uE001/g;
|
|
528
|
+
/** `CUSTOM_TRANSFORMERS` plus bare URLs, for `autoLink`. */
|
|
529
|
+
const AUTO_LINK_TRANSFORMERS = [{
|
|
530
|
+
dependencies: [],
|
|
531
|
+
importRegExp: FIRST_BARE_URL,
|
|
532
|
+
replace: (node) => {
|
|
533
|
+
const literal = $createTextNode(unescapeMarkdownText(node.getTextContent()));
|
|
534
|
+
literal.setFormat(node.getFormat());
|
|
535
|
+
node.replace(literal);
|
|
536
|
+
},
|
|
537
|
+
export: (node, _exportChildren, exportFormat) => {
|
|
538
|
+
if (!$isTextNode(node) || node.hasFormat("code")) return null;
|
|
539
|
+
const parent = node.getParent();
|
|
540
|
+
if ($isLinkNode(parent) && !$isAutoLinkNode(parent)) return null;
|
|
541
|
+
const text = node.getTextContent();
|
|
542
|
+
if (text.includes("")) return null;
|
|
543
|
+
const lead = $isAutoLinkNode(node.getPreviousSibling()) ? /^[^\s<]+/.exec(text)?.[0] ?? "" : "";
|
|
544
|
+
const urls = $isAutoLinkNode(parent) ? [{
|
|
545
|
+
index: 0,
|
|
546
|
+
source: text
|
|
547
|
+
}] : [...lead ? [{
|
|
548
|
+
index: 0,
|
|
549
|
+
source: lead
|
|
550
|
+
}] : [], ...findUrlRuns(text).filter((url) => url.index >= lead.length)];
|
|
551
|
+
if (urls.length === 0) return null;
|
|
552
|
+
let masked = "";
|
|
553
|
+
let end = 0;
|
|
554
|
+
urls.forEach((url, i) => {
|
|
555
|
+
masked += text.slice(end, url.index) + urlPlaceholder(i);
|
|
556
|
+
end = url.index + url.source.length;
|
|
557
|
+
});
|
|
558
|
+
masked += text.slice(end);
|
|
559
|
+
return exportFormat(node, masked).replace(URL_PLACEHOLDER, (_, i) => urls[Number(i)]?.source ?? "");
|
|
560
|
+
},
|
|
561
|
+
regExp: /(?!)/,
|
|
562
|
+
type: "text-match"
|
|
563
|
+
}, ...CUSTOM_TRANSFORMERS];
|
|
412
564
|
/**
|
|
413
565
|
* No transformers: `singleLine` mode edits plain-text entity names, not markdown.
|
|
414
566
|
*
|
|
@@ -456,6 +608,10 @@ const preprocessMarkdown = (markdown) => {
|
|
|
456
608
|
if (tableLines.length > 0) flushTable();
|
|
457
609
|
return result.join("\n");
|
|
458
610
|
};
|
|
611
|
+
/** Replaces the root with `markdown`: the one import a load, a `value` change and a paste share. */
|
|
612
|
+
const $importMarkdown = (markdown, transformers) => {
|
|
613
|
+
$convertFromMarkdownString(preprocessMarkdown(markdown), transformers);
|
|
614
|
+
};
|
|
459
615
|
/** Unwrap code-fenced tables back to raw GFM table markdown. */
|
|
460
616
|
const postprocessMarkdown = (markdown) => {
|
|
461
617
|
const regex = new RegExp(`\`\`\`${TABLE_FENCE_LANG}\\n([\\s\\S]*?)\\n\`\`\``, "g");
|
|
@@ -464,6 +620,81 @@ const postprocessMarkdown = (markdown) => {
|
|
|
464
620
|
/** Reverses Lexical's export escaping of `* _ ` ~ \`, for `singleLine` plain-text names. */
|
|
465
621
|
const unescapeMarkdownText = (markdown) => markdown.replace(/\\([*_`~\\])/g, "$1");
|
|
466
622
|
//#endregion
|
|
623
|
+
//#region src/blocks/MarkdownEditor/plugins/MarkdownPastePlugin.tsx
|
|
624
|
+
/**
|
|
625
|
+
* `markdown` as the top-level nodes the editor's own import makes of it.
|
|
626
|
+
*
|
|
627
|
+
* - Runs in a scratch editor: `$importMarkdown` replaces the root it imports into,
|
|
628
|
+
* and only nodes attached to a root get their lines merged into one paragraph.
|
|
629
|
+
*/
|
|
630
|
+
const parseMarkdown = (markdown, transformers) => {
|
|
631
|
+
const scratch = createEditor({
|
|
632
|
+
namespace: "MarkdownEditorPaste",
|
|
633
|
+
nodes: EDITOR_NODES,
|
|
634
|
+
onError: (error) => {
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
scratch.update(() => {
|
|
639
|
+
$importMarkdown(markdown, transformers);
|
|
640
|
+
}, { discrete: true });
|
|
641
|
+
return scratch.getEditorState().toJSON().root.children;
|
|
642
|
+
};
|
|
643
|
+
/**
|
|
644
|
+
* Inserts parsed blocks at the selection, keeping a first or last block that is not a paragraph whole.
|
|
645
|
+
*
|
|
646
|
+
* - `insertNodes` merges the first block into a non-empty line and the rest of the line into the
|
|
647
|
+
* last block, which would flatten a fence pasted after `Logs:` into plain text.
|
|
648
|
+
* - An empty paragraph first is never merged, so the block after it goes in whole; it is then removed.
|
|
649
|
+
* - An empty paragraph last takes the rest of the line, and is removed when there was none.
|
|
650
|
+
*/
|
|
651
|
+
const $insertBlocks = (selection, nodes) => {
|
|
652
|
+
const first = nodes[0];
|
|
653
|
+
const last = nodes.at(-1);
|
|
654
|
+
if (!first || !last) return;
|
|
655
|
+
const head = $isParagraphNode(first) ? null : $createParagraphNode();
|
|
656
|
+
const tail = $isParagraphNode(last) ? null : $createParagraphNode();
|
|
657
|
+
selection.insertNodes([
|
|
658
|
+
head,
|
|
659
|
+
...nodes,
|
|
660
|
+
tail
|
|
661
|
+
].filter((node) => node !== null));
|
|
662
|
+
head?.remove();
|
|
663
|
+
if (!tail) return;
|
|
664
|
+
if (tail.isEmpty()) {
|
|
665
|
+
tail.remove();
|
|
666
|
+
last.selectEnd();
|
|
667
|
+
} else tail.selectStart();
|
|
668
|
+
};
|
|
669
|
+
/**
|
|
670
|
+
* Parses a plain-text paste as markdown and inserts the result at the selection.
|
|
671
|
+
*
|
|
672
|
+
* - Only for a clipboard with `text/plain` and neither `text/html` nor files; those keep
|
|
673
|
+
* Lexical's own paste, and files stay for the image uploader or the consumer.
|
|
674
|
+
* - Inside a code block the text goes in literally, through the same fallback.
|
|
675
|
+
* - One undo step: the update is tagged `history-push`.
|
|
676
|
+
*/
|
|
677
|
+
const MarkdownPastePlugin = ({ transformers }) => {
|
|
678
|
+
const [editor] = useLexicalComposerContext();
|
|
679
|
+
useEffect(() => {
|
|
680
|
+
return editor.registerCommand(PASTE_COMMAND, (event) => {
|
|
681
|
+
const data = "clipboardData" in event ? event.clipboardData : null;
|
|
682
|
+
if (!data || data.files.length > 0 || data.types.includes("text/html")) return false;
|
|
683
|
+
const text = data.getData("text/plain").replace(/\r\n?/g, "\n");
|
|
684
|
+
if (!text) return false;
|
|
685
|
+
const selection = $getSelection();
|
|
686
|
+
if (!$isRangeSelection(selection)) return false;
|
|
687
|
+
if ([selection.anchor, selection.focus].some((point) => $findMatchingParent(point.getNode(), $isCodeNode))) return false;
|
|
688
|
+
event.preventDefault();
|
|
689
|
+
$addUpdateTag(PASTE_TAG);
|
|
690
|
+
$addUpdateTag(HISTORY_PUSH_TAG);
|
|
691
|
+
$insertBlocks(selection, parseMarkdown(text, transformers).map($parseSerializedNode));
|
|
692
|
+
return true;
|
|
693
|
+
}, COMMAND_PRIORITY_LOW);
|
|
694
|
+
}, [editor, transformers]);
|
|
695
|
+
return null;
|
|
696
|
+
};
|
|
697
|
+
//#endregion
|
|
467
698
|
//#region src/blocks/MarkdownEditor/plugins/MarkdownSyncPlugin.tsx
|
|
468
699
|
const IMPORT_TAG = "markdown-import";
|
|
469
700
|
const MarkdownSyncPlugin = ({ value, editable, onChange, transformers = CUSTOM_TRANSFORMERS, singleLine = false }) => {
|
|
@@ -481,7 +712,7 @@ const MarkdownSyncPlugin = ({ value, editable, onChange, transformers = CUSTOM_T
|
|
|
481
712
|
if (value === lastExportedRef.current) return;
|
|
482
713
|
lastExportedRef.current = value;
|
|
483
714
|
editor.update(() => {
|
|
484
|
-
$
|
|
715
|
+
$importMarkdown(value, transformersRef.current);
|
|
485
716
|
}, { tag: IMPORT_TAG });
|
|
486
717
|
}, [editor, value]);
|
|
487
718
|
useEffect(() => {
|
|
@@ -822,7 +1053,8 @@ const ELEMENT_FORMAT_TYPES = [
|
|
|
822
1053
|
];
|
|
823
1054
|
const isElementFormatType = (value) => ELEMENT_FORMAT_TYPES.includes(value);
|
|
824
1055
|
const toElementFormatType = (value) => isElementFormatType(value) ? value : "left";
|
|
825
|
-
const
|
|
1056
|
+
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";
|
|
1057
|
+
const ToolbarPlugin = ({ mode = "floating" }) => {
|
|
826
1058
|
const [editor] = useLexicalComposerContext();
|
|
827
1059
|
const [isBold, setIsBold] = useState(false);
|
|
828
1060
|
const [isItalic, setIsItalic] = useState(false);
|
|
@@ -837,6 +1069,7 @@ const ToolbarPlugin = () => {
|
|
|
837
1069
|
const toolbarRef = useRef(null);
|
|
838
1070
|
const isMouseDownRef = useRef(false);
|
|
839
1071
|
const hasSelectionRef = useRef(false);
|
|
1072
|
+
const isFloating = mode === "floating";
|
|
840
1073
|
const updatePosition = useCallback(() => {
|
|
841
1074
|
const nativeSelection = window.getSelection();
|
|
842
1075
|
if (!nativeSelection || nativeSelection.rangeCount === 0) return;
|
|
@@ -850,6 +1083,7 @@ const ToolbarPlugin = () => {
|
|
|
850
1083
|
}
|
|
851
1084
|
}, []);
|
|
852
1085
|
useEffect(() => {
|
|
1086
|
+
if (!isFloating) return;
|
|
853
1087
|
const handleMouseDown = () => {
|
|
854
1088
|
isMouseDownRef.current = true;
|
|
855
1089
|
};
|
|
@@ -863,7 +1097,7 @@ const ToolbarPlugin = () => {
|
|
|
863
1097
|
document.removeEventListener("mousedown", handleMouseDown);
|
|
864
1098
|
document.removeEventListener("mouseup", handleMouseUp);
|
|
865
1099
|
};
|
|
866
|
-
}, []);
|
|
1100
|
+
}, [isFloating]);
|
|
867
1101
|
const updateToolbar = useCallback(() => {
|
|
868
1102
|
const selection = $getSelection();
|
|
869
1103
|
if (!$isRangeSelection(selection)) {
|
|
@@ -904,116 +1138,124 @@ const ToolbarPlugin = () => {
|
|
|
904
1138
|
unregisterUpdate();
|
|
905
1139
|
};
|
|
906
1140
|
}, [editor, updateToolbar]);
|
|
907
|
-
if (!isVisible || !position) return null;
|
|
908
|
-
|
|
1141
|
+
if (isFloating && (!isVisible || !position)) return null;
|
|
1142
|
+
const buttons = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1143
|
+
/* @__PURE__ */ jsx(BlockTypeDropdown, {
|
|
1144
|
+
editor,
|
|
1145
|
+
blockType
|
|
1146
|
+
}),
|
|
1147
|
+
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
1148
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1149
|
+
active: isBold,
|
|
1150
|
+
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold"),
|
|
1151
|
+
tooltip: "Bold (Ctrl+B)",
|
|
1152
|
+
children: /* @__PURE__ */ jsx(Bold, { className: "tw:size-4" })
|
|
1153
|
+
}),
|
|
1154
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1155
|
+
active: isItalic,
|
|
1156
|
+
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic"),
|
|
1157
|
+
tooltip: "Italic (Ctrl+I)",
|
|
1158
|
+
children: /* @__PURE__ */ jsx(Italic, { className: "tw:size-4" })
|
|
1159
|
+
}),
|
|
1160
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1161
|
+
active: isUnderline,
|
|
1162
|
+
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline"),
|
|
1163
|
+
tooltip: "Underline (Ctrl+U)",
|
|
1164
|
+
children: /* @__PURE__ */ jsx(Underline, { className: "tw:size-4" })
|
|
1165
|
+
}),
|
|
1166
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1167
|
+
active: isStrikethrough,
|
|
1168
|
+
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough"),
|
|
1169
|
+
tooltip: "Strikethrough",
|
|
1170
|
+
children: /* @__PURE__ */ jsx(Strikethrough, { className: "tw:size-4" })
|
|
1171
|
+
}),
|
|
1172
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1173
|
+
active: isCode,
|
|
1174
|
+
onClick: () => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code"),
|
|
1175
|
+
tooltip: "Inline Code",
|
|
1176
|
+
children: /* @__PURE__ */ jsx(Code, { className: "tw:size-4" })
|
|
1177
|
+
}),
|
|
1178
|
+
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
1179
|
+
/* @__PURE__ */ jsx(LinkPanel, {
|
|
1180
|
+
editor,
|
|
1181
|
+
isLink
|
|
1182
|
+
}),
|
|
1183
|
+
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
1184
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1185
|
+
active: blockType === "bullet",
|
|
1186
|
+
onClick: () => {
|
|
1187
|
+
if (blockType === "bullet") editor.dispatchCommand(REMOVE_LIST_COMMAND, void 0);
|
|
1188
|
+
else editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, void 0);
|
|
1189
|
+
},
|
|
1190
|
+
tooltip: "Bullet List",
|
|
1191
|
+
children: /* @__PURE__ */ jsx(List, { className: "tw:size-4" })
|
|
1192
|
+
}),
|
|
1193
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1194
|
+
active: blockType === "number",
|
|
1195
|
+
onClick: () => {
|
|
1196
|
+
if (blockType === "number") editor.dispatchCommand(REMOVE_LIST_COMMAND, void 0);
|
|
1197
|
+
else editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, void 0);
|
|
1198
|
+
},
|
|
1199
|
+
tooltip: "Numbered List",
|
|
1200
|
+
children: /* @__PURE__ */ jsx(ListOrdered, { className: "tw:size-4" })
|
|
1201
|
+
}),
|
|
1202
|
+
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
1203
|
+
/* @__PURE__ */ jsx(AlignmentDropdown, {
|
|
1204
|
+
editor,
|
|
1205
|
+
alignment
|
|
1206
|
+
}),
|
|
1207
|
+
/* @__PURE__ */ jsx(ToolbarDivider, {}),
|
|
1208
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1209
|
+
active: blockType === "quote",
|
|
1210
|
+
onClick: () => {
|
|
1211
|
+
editor.update(() => {
|
|
1212
|
+
const selection = $getSelection();
|
|
1213
|
+
if (!$isRangeSelection(selection)) return;
|
|
1214
|
+
const anchorNode = selection.anchor.getNode();
|
|
1215
|
+
const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
|
|
1216
|
+
const newNode = blockType === "quote" ? $createParagraphNode() : $createQuoteNode();
|
|
1217
|
+
element.getChildren().forEach((child) => newNode.append(child));
|
|
1218
|
+
element.replace(newNode);
|
|
1219
|
+
});
|
|
1220
|
+
},
|
|
1221
|
+
tooltip: "Quote",
|
|
1222
|
+
children: /* @__PURE__ */ jsx(Quote, { className: "tw:size-4" })
|
|
1223
|
+
}),
|
|
1224
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
1225
|
+
active: blockType === "code",
|
|
1226
|
+
onClick: () => {
|
|
1227
|
+
editor.update(() => {
|
|
1228
|
+
const selection = $getSelection();
|
|
1229
|
+
if (!$isRangeSelection(selection)) return;
|
|
1230
|
+
const anchorNode = selection.anchor.getNode();
|
|
1231
|
+
const element = $getNearestBlockElementAncestorOrThrow(anchorNode);
|
|
1232
|
+
const newNode = blockType === "code" ? $createParagraphNode() : $createCodeNode();
|
|
1233
|
+
element.getChildren().forEach((child) => newNode.append(child));
|
|
1234
|
+
element.replace(newNode);
|
|
1235
|
+
});
|
|
1236
|
+
},
|
|
1237
|
+
tooltip: "Code Block",
|
|
1238
|
+
children: /* @__PURE__ */ jsx(SquareCode, { className: "tw:size-4" })
|
|
1239
|
+
})
|
|
1240
|
+
] });
|
|
1241
|
+
if (!isFloating) return /* @__PURE__ */ jsx("div", {
|
|
1242
|
+
role: "toolbar",
|
|
1243
|
+
"aria-label": "Formatting options",
|
|
1244
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
1245
|
+
className: cn(TOOLBAR_BASE_CLASS, "tw:mb-1 tw:flex-wrap tw:bg-background"),
|
|
1246
|
+
children: buttons
|
|
1247
|
+
});
|
|
1248
|
+
return createPortal(/* @__PURE__ */ jsx("div", {
|
|
909
1249
|
ref: toolbarRef,
|
|
910
1250
|
role: "toolbar",
|
|
911
1251
|
"aria-label": "Formatting options",
|
|
912
|
-
onMouseDown: (
|
|
1252
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
913
1253
|
style: {
|
|
914
|
-
top: position
|
|
915
|
-
left: position
|
|
1254
|
+
top: position?.top,
|
|
1255
|
+
left: position?.left
|
|
916
1256
|
},
|
|
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
|
-
]
|
|
1257
|
+
className: cn(TOOLBAR_BASE_CLASS, "tw:absolute tw:z-[1060] tw:-translate-y-full tw:bg-popover tw:text-popover-foreground tw:shadow-lg"),
|
|
1258
|
+
children: buttons
|
|
1017
1259
|
}), document.body);
|
|
1018
1260
|
};
|
|
1019
1261
|
//#endregion
|
|
@@ -1057,21 +1299,15 @@ const editorTheme = {
|
|
|
1057
1299
|
};
|
|
1058
1300
|
//#endregion
|
|
1059
1301
|
//#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 }) => {
|
|
1302
|
+
const AUTO_LINK_MATCHERS = [matchBareUrl];
|
|
1303
|
+
const AUTO_LINK_EXCLUDED_PARENTS = [$isCodeNode];
|
|
1304
|
+
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
1305
|
const editable = !readOnly && !disabled;
|
|
1073
|
-
const
|
|
1306
|
+
const linkify = autoLink && !singleLine;
|
|
1307
|
+
const markdownPaste = pasteMarkdown && !singleLine;
|
|
1308
|
+
const transformers = singleLine ? SINGLE_LINE_TRANSFORMERS : linkify ? AUTO_LINK_TRANSFORMERS : CUSTOM_TRANSFORMERS;
|
|
1074
1309
|
const useDefaultMinHeight = !compact && minRows === void 0;
|
|
1310
|
+
const contentPadding = compact ? "tw:px-1.5 tw:py-0.5" : "tw:p-2";
|
|
1075
1311
|
const customMinHeight = !compact && minRows !== void 0 ? { minHeight: `${minRows * 1.75}em` } : void 0;
|
|
1076
1312
|
const [initialConfig] = useState(() => ({
|
|
1077
1313
|
namespace: "MarkdownEditor",
|
|
@@ -1082,7 +1318,7 @@ const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFo
|
|
|
1082
1318
|
},
|
|
1083
1319
|
editable,
|
|
1084
1320
|
editorState: () => {
|
|
1085
|
-
$
|
|
1321
|
+
$importMarkdown(value, transformers);
|
|
1086
1322
|
}
|
|
1087
1323
|
}));
|
|
1088
1324
|
const skipBlurSaveRef = useRef(false);
|
|
@@ -1101,16 +1337,17 @@ const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFo
|
|
|
1101
1337
|
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
1338
|
onBlur: handleBlur,
|
|
1103
1339
|
children: [
|
|
1104
|
-
showToolbar && editable && /* @__PURE__ */ jsx(ToolbarPlugin, {}),
|
|
1340
|
+
showToolbar && editable && /* @__PURE__ */ jsx(ToolbarPlugin, { mode: toolbarMode }),
|
|
1105
1341
|
/* @__PURE__ */ jsx("div", {
|
|
1106
|
-
className: cn("tw:relative tw:rounded-md tw:transition-colors", compact
|
|
1342
|
+
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
1343
|
children: /* @__PURE__ */ jsx(RichTextPlugin, {
|
|
1108
1344
|
contentEditable: /* @__PURE__ */ jsx(ContentEditable, {
|
|
1109
|
-
className: cn("tw:outline-hidden", useDefaultMinHeight && "tw:min-h-[100px]"),
|
|
1345
|
+
className: cn("tw:outline-hidden", !compact && "tw:[&>*:first-child]:mt-0 tw:[&>*:last-child]:mb-0", useDefaultMinHeight && "tw:min-h-[100px]"),
|
|
1110
1346
|
style: customMinHeight,
|
|
1111
1347
|
"aria-placeholder": placeholder ?? "",
|
|
1112
1348
|
placeholder: placeholder ? /* @__PURE__ */ jsx("div", {
|
|
1113
|
-
|
|
1349
|
+
"data-testid": "markdown-editor-placeholder",
|
|
1350
|
+
className: cn("tw:pointer-events-none tw:absolute tw:inset-0 tw:select-none tw:text-muted-foreground", contentPadding),
|
|
1114
1351
|
children: placeholder
|
|
1115
1352
|
}) : /* @__PURE__ */ jsx("div", {})
|
|
1116
1353
|
}),
|
|
@@ -1133,6 +1370,11 @@ const MarkdownEditor = ({ value, onChange, readOnly = false, placeholder, autoFo
|
|
|
1133
1370
|
singleLine
|
|
1134
1371
|
}),
|
|
1135
1372
|
/* @__PURE__ */ jsx(ImagePastePlugin, { onImageUpload }),
|
|
1373
|
+
linkify && /* @__PURE__ */ jsx(AutoLinkPlugin, {
|
|
1374
|
+
matchers: AUTO_LINK_MATCHERS,
|
|
1375
|
+
excludeParents: AUTO_LINK_EXCLUDED_PARENTS
|
|
1376
|
+
}),
|
|
1377
|
+
markdownPaste && /* @__PURE__ */ jsx(MarkdownPastePlugin, { transformers }),
|
|
1136
1378
|
/* @__PURE__ */ jsx(KeyboardPlugin, {
|
|
1137
1379
|
onSave,
|
|
1138
1380
|
onCancel,
|