@thangph2146/lexical-editor 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +2 -1
  2. package/dist/editor-x/editor.cjs +280 -20
  3. package/dist/editor-x/editor.cjs.map +1 -1
  4. package/dist/editor-x/editor.css +27 -4
  5. package/dist/editor-x/editor.css.map +1 -1
  6. package/dist/editor-x/editor.js +281 -21
  7. package/dist/editor-x/editor.js.map +1 -1
  8. package/dist/index.cjs +292 -23
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.css +27 -4
  11. package/dist/index.css.map +1 -1
  12. package/dist/index.d.cts +26 -1
  13. package/dist/index.d.ts +26 -1
  14. package/dist/index.js +293 -24
  15. package/dist/index.js.map +1 -1
  16. package/package.json +1 -1
  17. package/src/components/lexical-editor.tsx +19 -6
  18. package/src/context/uploads-context.tsx +1 -0
  19. package/src/editor-ui/content-editable.tsx +18 -2
  20. package/src/editor-x/nodes.ts +2 -0
  21. package/src/nodes/download-link-node.tsx +118 -0
  22. package/src/plugins/floating-link-editor-plugin.tsx +338 -91
  23. package/src/themes/core/_tables.scss +0 -1
  24. package/src/themes/plugins/_floating-link-editor.scss +28 -2
  25. package/src/themes/ui-components/_button.scss +1 -1
  26. package/src/themes/ui-components/_flex.scss +1 -0
  27. package/src/ui/button-group.tsx +10 -10
  28. package/src/ui/button.tsx +38 -38
  29. package/src/ui/collapsible.tsx +67 -67
  30. package/src/ui/command.tsx +48 -48
  31. package/src/ui/dialog.tsx +146 -146
  32. package/src/ui/flex.tsx +45 -45
  33. package/src/ui/input.tsx +20 -20
  34. package/src/ui/label.tsx +20 -20
  35. package/src/ui/number-input.tsx +104 -104
  36. package/src/ui/popover.tsx +128 -128
  37. package/src/ui/scroll-area.tsx +17 -17
  38. package/src/ui/select.tsx +171 -171
  39. package/src/ui/separator.tsx +20 -20
  40. package/src/ui/slider.tsx +14 -14
  41. package/src/ui/slot.tsx +3 -3
  42. package/src/ui/tabs.tsx +87 -87
  43. package/src/ui/toggle-group.tsx +109 -109
  44. package/src/ui/toggle.tsx +28 -28
  45. package/src/ui/tooltip.tsx +28 -28
  46. package/src/ui/typography.tsx +44 -44
@@ -7,7 +7,7 @@ import { mergeRegister, IS_APPLE, $findMatchingParent, $getNearestNodeOfType, $g
7
7
  import { $getNodeByKey, FORMAT_ELEMENT_COMMAND, $isNodeSelection, $getRoot, $getSelection, $setSelection, $isRangeSelection, SELECTION_CHANGE_COMMAND, COMMAND_PRIORITY_LOW, CLICK_COMMAND, DRAGSTART_COMMAND, KEY_DELETE_COMMAND, KEY_BACKSPACE_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, COMMAND_PRIORITY_CRITICAL, CAN_UNDO_COMMAND, CAN_REDO_COMMAND, UNDO_COMMAND, REDO_COMMAND, $isRootOrShadowRoot, FORMAT_TEXT_COMMAND, KEY_MODIFIER_COMMAND, COMMAND_PRIORITY_NORMAL, $isTextNode, $createParagraphNode, $isElementNode, COMMAND_PRIORITY_EDITOR, $getNearestNodeFromDOMNode, $insertNodes, COMMAND_PRIORITY_HIGH, DRAGOVER_COMMAND, DROP_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_RIGHT_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ARROW_LEFT_COMMAND, FOCUS_COMMAND, KEY_TAB_COMMAND, CUT_COMMAND, COPY_COMMAND, PASTE_COMMAND, $isDecoratorNode, $createTextNode, INDENT_CONTENT_COMMAND, CLEAR_HISTORY_COMMAND, CLEAR_EDITOR_COMMAND, ParagraphNode, TextNode, OUTDENT_CONTENT_COMMAND, $applyNodeReplacement, $createRangeSelection, $isParagraphNode, DecoratorNode, createEditor, RootNode, ElementNode, createCommand, HISTORY_MERGE_TAG, $isLineBreakNode, isHTMLElement, $addUpdateTag } from 'lexical';
8
8
  import { BlockWithAlignableContents } from '@lexical/react/LexicalBlockWithAlignableContents';
9
9
  import { $isDecoratorBlockNode, DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode';
10
- import { UndoIcon, RedoIcon, PlusIcon, ScissorsIcon, ImageIcon, TableIcon, Columns3Icon, TypeIcon, Minus, Plus, SubscriptIcon, SuperscriptIcon, LinkIcon, EraserIcon, BaselineIcon, PaintBucketIcon, IndentDecreaseIcon, IndentIncreaseIcon, CircleUserRoundIcon, GripVerticalIcon, TextIcon, ListTodoIcon, ListOrderedIcon, ListIcon, QuoteIcon, CodeIcon, MinusIcon, Combine, SplitSquareVertical, Columns3, Rows3, LayoutGrid, Palette, Hash, Link2Off, Scissors, Copy, Clipboard, ClipboardType, Trash2, SendIcon, UploadIcon, DownloadIcon, FileTextIcon, LockIcon, UnlockIcon, Trash2Icon, NotebookPenIcon, Loader2, PipetteIcon, Heading3Icon, Heading2Icon, Heading1Icon, AlignJustifyIcon, AlignRightIcon, AlignCenterIcon, AlignLeftIcon, YoutubeIcon, TwitterIcon, BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, ChevronRight, Folder, CircleCheckIcon, CopyIcon, X, Check, Pencil, Trash, MicIcon, ListPlusIcon, ListMinusIcon, CaseSensitiveIcon, AlignLeft, AlignCenter, AlignRight, Minimize2, Maximize2, ImageMinus, ImagePlus } from 'lucide-react';
10
+ import { UndoIcon, RedoIcon, PlusIcon, ScissorsIcon, ImageIcon, TableIcon, Columns3Icon, TypeIcon, Minus, Plus, SubscriptIcon, SuperscriptIcon, LinkIcon, EraserIcon, BaselineIcon, PaintBucketIcon, IndentDecreaseIcon, IndentIncreaseIcon, CircleUserRoundIcon, GripVerticalIcon, TextIcon, ListTodoIcon, ListOrderedIcon, ListIcon, QuoteIcon, CodeIcon, MinusIcon, Combine, SplitSquareVertical, Columns3, Rows3, LayoutGrid, Palette, Hash, Link2Off, Scissors, Copy, Clipboard, ClipboardType, Trash2, SendIcon, UploadIcon, DownloadIcon, FileTextIcon, LockIcon, UnlockIcon, Trash2Icon, NotebookPenIcon, Loader2, PipetteIcon, Heading3Icon, Heading2Icon, Heading1Icon, AlignJustifyIcon, AlignRightIcon, AlignCenterIcon, AlignLeftIcon, YoutubeIcon, TwitterIcon, BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, ChevronRight, Folder, CircleCheckIcon, CopyIcon, Upload, X, Check, Pencil, Trash, MicIcon, ListPlusIcon, ListMinusIcon, CaseSensitiveIcon, AlignLeft, AlignCenter, AlignRight, Minimize2, Maximize2, ImageMinus, ImagePlus } from 'lucide-react';
11
11
  import { useLexicalEditable } from '@lexical/react/useLexicalEditable';
12
12
  import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
13
13
  import { createPortal } from 'react-dom';
@@ -19,9 +19,9 @@ import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
19
19
  import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
20
20
  import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
21
21
  import { $isListNode, ListNode, INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, ListItemNode, $getListDepth, $isListItemNode, $createListItemNode } from '@lexical/list';
22
+ import { TOGGLE_LINK_COMMAND, $isLinkNode, LinkNode, AutoLinkNode, $createLinkNode, $isAutoLinkNode } from '@lexical/link';
22
23
  import { $isCodeNode, getLanguageFriendlyName, registerCodeHighlighting, $createCodeNode, CodeNode, CodeHighlightNode, $isCodeHighlightNode, CODE_LANGUAGE_FRIENDLY_NAME_MAP, CODE_LANGUAGE_MAP } from '@lexical/code';
23
24
  import { HashtagNode } from '@lexical/hashtag';
24
- import { TOGGLE_LINK_COMMAND, $isLinkNode, LinkNode, AutoLinkNode, $createLinkNode, $isAutoLinkNode } from '@lexical/link';
25
25
  import { OverflowNode } from '@lexical/overflow';
26
26
  import { INSERT_HORIZONTAL_RULE_COMMAND, HorizontalRuleNode, $createHorizontalRuleNode, $isHorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
27
27
  import { $isHeadingNode, $isQuoteNode, $createHeadingNode, $createQuoteNode, DRAG_DROP_PASTE, HeadingNode, QuoteNode } from '@lexical/rich-text';
@@ -5069,12 +5069,14 @@ var init_image_placeholder = __esm({
5069
5069
  }
5070
5070
  });
5071
5071
  function ContentEditable({
5072
- placeholder,
5072
+ placeholder = "",
5073
5073
  className,
5074
5074
  placeholderClassName,
5075
5075
  placeholderDefaults = true
5076
5076
  }) {
5077
5077
  const isReadOnlyOrReview = className?.includes("--readonly") || className?.includes("--review");
5078
+ const text = placeholder.trim();
5079
+ const showLexicalPlaceholder = text.length > 0;
5078
5080
  return /* @__PURE__ */ jsx(
5079
5081
  ContentEditable$1,
5080
5082
  {
@@ -5083,7 +5085,17 @@ function ContentEditable({
5083
5085
  !isReadOnlyOrReview && "min-h-72 px-8 py-4",
5084
5086
  className
5085
5087
  ),
5086
- "aria-label": "Editor n\u1ED9i dung"
5088
+ "aria-label": "Editor n\u1ED9i dung",
5089
+ ...showLexicalPlaceholder ? {
5090
+ "aria-placeholder": text,
5091
+ placeholder: /* @__PURE__ */ jsx(
5092
+ "div",
5093
+ {
5094
+ className: cn(placeholderDefaults && "editor-placeholder", placeholderClassName),
5095
+ children: text
5096
+ }
5097
+ )
5098
+ } : { placeholder: null }
5087
5099
  }
5088
5100
  );
5089
5101
  }
@@ -6219,6 +6231,86 @@ var init_list_with_color_node = __esm({
6219
6231
  };
6220
6232
  }
6221
6233
  });
6234
+ function $createDownloadLinkNode(url, download = null, attributes) {
6235
+ return new DownloadLinkNode(url, download, attributes);
6236
+ }
6237
+ function $isDownloadLinkNode(node) {
6238
+ return node instanceof DownloadLinkNode;
6239
+ }
6240
+ var DownloadLinkNode;
6241
+ var init_download_link_node = __esm({
6242
+ "src/nodes/download-link-node.tsx"() {
6243
+ DownloadLinkNode = class _DownloadLinkNode extends LinkNode {
6244
+ __download;
6245
+ static getType() {
6246
+ return "download-link";
6247
+ }
6248
+ static clone(node) {
6249
+ return new _DownloadLinkNode(
6250
+ node.getURL(),
6251
+ node.__download,
6252
+ {
6253
+ rel: node.getRel(),
6254
+ target: node.getTarget(),
6255
+ title: node.getTitle()
6256
+ },
6257
+ node.__key
6258
+ );
6259
+ }
6260
+ constructor(url, download = null, attributes, key) {
6261
+ super(url, attributes, key);
6262
+ this.__download = download;
6263
+ }
6264
+ getDownload() {
6265
+ return this.__download;
6266
+ }
6267
+ setDownload(download) {
6268
+ const writable = this.getWritable();
6269
+ writable.__download = download;
6270
+ return this;
6271
+ }
6272
+ createDOM(config) {
6273
+ const dom = super.createDOM(config);
6274
+ this.applyDownloadDOM(dom);
6275
+ return dom;
6276
+ }
6277
+ updateLinkDOM(prevNode, anchorElem, config) {
6278
+ super.updateLinkDOM(prevNode, anchorElem, config);
6279
+ this.applyDownloadDOM(anchorElem);
6280
+ }
6281
+ exportJSON() {
6282
+ return {
6283
+ ...super.exportJSON(),
6284
+ type: _DownloadLinkNode.getType(),
6285
+ version: 1,
6286
+ download: this.__download
6287
+ };
6288
+ }
6289
+ static importJSON(serializedNode) {
6290
+ const node = new _DownloadLinkNode(
6291
+ serializedNode.url,
6292
+ serializedNode.download,
6293
+ {
6294
+ rel: serializedNode.rel ?? null,
6295
+ target: serializedNode.target ?? null,
6296
+ title: serializedNode.title ?? null
6297
+ },
6298
+ serializedNode.key
6299
+ );
6300
+ return node;
6301
+ }
6302
+ applyDownloadDOM(dom) {
6303
+ if (dom instanceof HTMLAnchorElement) {
6304
+ if (this.__download === null) {
6305
+ dom.removeAttribute("download");
6306
+ } else {
6307
+ dom.setAttribute("download", this.__download);
6308
+ }
6309
+ }
6310
+ }
6311
+ };
6312
+ }
6313
+ });
6222
6314
  function $convertMentionElement(domNode) {
6223
6315
  const textContent = domNode.textContent;
6224
6316
  if (textContent !== null) {
@@ -6331,6 +6423,7 @@ var init_nodes = __esm({
6331
6423
  init_layout_container_node();
6332
6424
  init_layout_item_node();
6333
6425
  init_list_with_color_node();
6426
+ init_download_link_node();
6334
6427
  init_mention_node();
6335
6428
  nodes = [
6336
6429
  HeadingNode,
@@ -6346,6 +6439,7 @@ var init_nodes = __esm({
6346
6439
  ListWithColorNode,
6347
6440
  ListItemNode,
6348
6441
  LinkNode,
6442
+ DownloadLinkNode,
6349
6443
  OverflowNode,
6350
6444
  HashtagNode,
6351
6445
  TableNode,
@@ -27998,6 +28092,44 @@ var init_url = __esm({
27998
28092
  );
27999
28093
  }
28000
28094
  });
28095
+ function shouldTreatUrlAsDownload(url) {
28096
+ if (typeof url !== "string") return false;
28097
+ const u = url.toLowerCase();
28098
+ if (u.includes("/api/uploads/") || u.includes("/uploads/") || u.includes("/api/admin/uploads/") || u.includes("/admin/uploads/"))
28099
+ return true;
28100
+ return /\.(pdf|doc|docx|xls|xlsx|csv|zip|rar|7z|txt|rtf|png|jpg|jpeg|gif|webp|mp3|wav|mp4|mov|avi)(\?.*)?$/.test(
28101
+ u
28102
+ );
28103
+ }
28104
+ function inferDownloadFileName(url) {
28105
+ try {
28106
+ const path = url.split("?")[0] ?? "";
28107
+ const last = path.split("/").filter(Boolean).pop();
28108
+ return last ? decodeURIComponent(last) : "download";
28109
+ } catch {
28110
+ return "download";
28111
+ }
28112
+ }
28113
+ function getCookieValue(name) {
28114
+ if (typeof document === "undefined") return null;
28115
+ const row = document.cookie.split("; ").find((item) => item.startsWith(`${name}=`));
28116
+ if (!row) return null;
28117
+ return row.split("=").slice(1).join("=") || null;
28118
+ }
28119
+ function buildHrefFromJsDownloadArg(jsArg) {
28120
+ const firstSegment = typeof window !== "undefined" ? window.location.pathname.split("/").filter(Boolean)[0] ?? "" : "";
28121
+ const serveBase = firstSegment === "admin" ? "/api/admin/uploads/serve" : "/api/uploads/serve";
28122
+ const arg = jsArg.trim();
28123
+ if (!arg) return "about:blank";
28124
+ if (/^https?:\/\//i.test(arg)) return arg;
28125
+ if (arg.startsWith("/api/")) return arg;
28126
+ if (arg.startsWith("images/") || arg.startsWith("files/")) {
28127
+ return `${serveBase}/${arg}`;
28128
+ }
28129
+ const m = arg.match(/(images|files)\/.+/i);
28130
+ if (m?.[0]) return `${serveBase}/${m[0]}`;
28131
+ return "about:blank";
28132
+ }
28001
28133
  function FloatingLinkEditor({
28002
28134
  editor,
28003
28135
  isLink,
@@ -28011,6 +28143,9 @@ function FloatingLinkEditor({
28011
28143
  const [linkUrl, setLinkUrl] = useState("");
28012
28144
  const [editedLinkUrl, setEditedLinkUrl] = useState("https://");
28013
28145
  const [lastSelection, setLastSelection] = useState(null);
28146
+ const [isUploadingFile, setIsUploadingFile] = useState(false);
28147
+ const fileInputRef = useRef(null);
28148
+ const { onUploadFile } = useEditorUploads();
28014
28149
  const $updateLinkEditor = useCallback(() => {
28015
28150
  const selection = $getSelection();
28016
28151
  let linkNode = null;
@@ -28204,9 +28339,11 @@ function FloatingLinkEditor({
28204
28339
  setIsLinkEditMode(false);
28205
28340
  }
28206
28341
  };
28207
- const handleLinkSubmission = () => {
28208
- const url = sanitizeUrl(editedLinkUrl);
28209
- if (url && url !== "https://" && url !== "http://") {
28342
+ const handleLinkSubmission = (submittedUrl, originalFileName) => {
28343
+ const rawUrl = typeof submittedUrl === "string" ? submittedUrl : editedLinkUrl;
28344
+ const url = sanitizeUrl(rawUrl);
28345
+ const downloadFileName = originalFileName || (shouldTreatUrlAsDownload(url) ? inferDownloadFileName(url) : null);
28346
+ if (url && url !== "about:blank" && url !== "https://" && url !== "http://") {
28210
28347
  editor.update(() => {
28211
28348
  let selection = $getSelection();
28212
28349
  if (!selection && lastSelection !== null) {
@@ -28231,8 +28368,11 @@ function FloatingLinkEditor({
28231
28368
  const existingLinkNode = $findMatchingParent(node, $isLinkNode) || ($isLinkNode(node.getParent()) ? node.getParent() : null);
28232
28369
  if (existingLinkNode) {
28233
28370
  existingLinkNode.setURL(url);
28371
+ if (downloadFileName && $isDownloadLinkNode(existingLinkNode)) {
28372
+ existingLinkNode.setDownload(downloadFileName);
28373
+ }
28234
28374
  } else {
28235
- const linkNode = $createLinkNode(url);
28375
+ const linkNode = downloadFileName ? $createDownloadLinkNode(url, downloadFileName) : $createLinkNode(url);
28236
28376
  $wrapNodeInElement(node, () => linkNode);
28237
28377
  }
28238
28378
  }
@@ -28241,25 +28381,114 @@ function FloatingLinkEditor({
28241
28381
  editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
28242
28382
  const parent = getSelectedNode(selection).getParent();
28243
28383
  if ($isAutoLinkNode(parent)) {
28244
- const linkNode = $createLinkNode(parent.getURL(), {
28384
+ const linkNode = downloadFileName ? $createDownloadLinkNode(parent.getURL(), downloadFileName, {
28385
+ rel: parent.__rel,
28386
+ target: parent.__target,
28387
+ title: parent.__title
28388
+ }) : $createLinkNode(parent.getURL(), {
28245
28389
  rel: parent.__rel,
28246
28390
  target: parent.__target,
28247
28391
  title: parent.__title
28248
28392
  });
28249
28393
  parent.replace(linkNode, true);
28250
28394
  }
28395
+ if (downloadFileName) {
28396
+ const selectedNode = getSelectedNode(selection);
28397
+ const linkNode = $findMatchingParent(selectedNode, $isLinkNode) || ($isLinkNode(selectedNode) ? selectedNode : null);
28398
+ if (linkNode) {
28399
+ const currentText = linkNode.getTextContent();
28400
+ const targetText = originalFileName || downloadFileName;
28401
+ const downloadLinkNode = $createDownloadLinkNode(url, downloadFileName, {
28402
+ rel: linkNode.getRel(),
28403
+ target: linkNode.getTarget(),
28404
+ title: linkNode.getTitle()
28405
+ });
28406
+ if (currentText === url && targetText) {
28407
+ downloadLinkNode.append($createTextNode(targetText));
28408
+ } else {
28409
+ const children = linkNode.getChildren();
28410
+ if (children.length > 0) {
28411
+ downloadLinkNode.append(...children);
28412
+ }
28413
+ }
28414
+ linkNode.replace(downloadLinkNode);
28415
+ }
28416
+ }
28251
28417
  }
28252
28418
  });
28253
28419
  setEditedLinkUrl("https://");
28254
28420
  setIsLinkEditMode(false);
28255
28421
  }
28256
28422
  };
28423
+ const handlePickLocalFile = () => {
28424
+ if (isUploadingFile) return;
28425
+ fileInputRef.current?.click();
28426
+ };
28427
+ const handleUploadLocalFile = async (event) => {
28428
+ const file = event.target.files?.[0];
28429
+ if (!file) return;
28430
+ try {
28431
+ setIsUploadingFile(true);
28432
+ let uploadedUrl = void 0;
28433
+ if (onUploadFile) {
28434
+ const result = await onUploadFile(file);
28435
+ if (result.error) throw new Error(result.error);
28436
+ uploadedUrl = result.url;
28437
+ } else {
28438
+ const formData = new FormData();
28439
+ formData.append("file", file);
28440
+ const firstSegment = typeof window !== "undefined" ? window.location.pathname.split("/").filter(Boolean)[0] ?? "" : "";
28441
+ const pathPart = firstSegment === "admin" ? "/admin/uploads" : "/uploads";
28442
+ const endpoint = firstSegment === "admin" ? `/admin/api${pathPart}` : `/api${pathPart}`;
28443
+ const userId = getCookieValue("app_user_id");
28444
+ const authToken = getCookieValue("auth-token");
28445
+ const headers = {};
28446
+ if (userId) headers["X-User-Id"] = userId;
28447
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
28448
+ const res = await fetch(endpoint, {
28449
+ method: "POST",
28450
+ credentials: "include",
28451
+ headers: Object.keys(headers).length > 0 ? headers : void 0,
28452
+ body: formData
28453
+ });
28454
+ if (!res.ok) {
28455
+ throw new Error(`Upload failed: HTTP ${res.status}`);
28456
+ }
28457
+ const payload = await res.json();
28458
+ uploadedUrl = payload?.data?.url;
28459
+ if (!payload?.success || !uploadedUrl) {
28460
+ throw new Error(payload?.message || payload?.error || "Upload failed");
28461
+ }
28462
+ }
28463
+ if (uploadedUrl) {
28464
+ setEditedLinkUrl(uploadedUrl);
28465
+ handleLinkSubmission(uploadedUrl, file.name);
28466
+ }
28467
+ } catch (error) {
28468
+ console.error("[FloatingLinkEditor] Upload local file failed:", error);
28469
+ } finally {
28470
+ setIsUploadingFile(false);
28471
+ if (fileInputRef.current) {
28472
+ fileInputRef.current.value = "";
28473
+ }
28474
+ }
28475
+ };
28257
28476
  return /* @__PURE__ */ jsx(
28258
28477
  "div",
28259
28478
  {
28260
28479
  ref: editorRef,
28261
28480
  className: "editor-floating-link-editor",
28262
- children: isLinkEditMode || isLink ? isLinkEditMode ? /* @__PURE__ */ jsxs("div", { className: "editor-floating-link-editor__input-container", children: [
28481
+ children: isLinkEditMode || isLink ? isLinkEditMode ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", { className: "editor-floating-link-editor__input-container", children: [
28482
+ /* @__PURE__ */ jsx(
28483
+ "input",
28484
+ {
28485
+ ref: fileInputRef,
28486
+ type: "file",
28487
+ className: "hidden",
28488
+ onChange: handleUploadLocalFile,
28489
+ accept: ".pdf,.doc,.docx,.xls,.xlsx,.csv,.rtf,.txt,.zip,.rar,.7z,.ppt,.pptx,.jpg,.jpeg,.png,.gif,.webp,.svg,.mp3,.wav,.mp4,.mov,.avi,.webm"
28490
+ }
28491
+ ),
28263
28492
  /* @__PURE__ */ jsx(
28264
28493
  Input,
28265
28494
  {
@@ -28270,6 +28499,18 @@ function FloatingLinkEditor({
28270
28499
  className: "editor-flex-grow"
28271
28500
  }
28272
28501
  ),
28502
+ /* @__PURE__ */ jsx(
28503
+ Button,
28504
+ {
28505
+ size: "icon",
28506
+ variant: "ghost",
28507
+ onClick: handlePickLocalFile,
28508
+ className: "editor-shrink-0",
28509
+ disabled: isUploadingFile,
28510
+ title: isUploadingFile ? "Uploading..." : "Upload file t\u1EEB thi\u1EBFt b\u1ECB",
28511
+ children: isUploadingFile ? /* @__PURE__ */ jsx(Loader2, { className: "editor-icon-sm animate-spin" }) : /* @__PURE__ */ jsx(Upload, { className: "editor-icon-sm" })
28512
+ }
28513
+ ),
28273
28514
  /* @__PURE__ */ jsx(
28274
28515
  Button,
28275
28516
  {
@@ -28287,22 +28528,39 @@ function FloatingLinkEditor({
28287
28528
  Button,
28288
28529
  {
28289
28530
  size: "icon",
28290
- onClick: handleLinkSubmission,
28531
+ onClick: () => handleLinkSubmission(),
28291
28532
  className: "editor-shrink-0",
28292
28533
  children: /* @__PURE__ */ jsx(Check, { className: "editor-icon-sm" })
28293
28534
  }
28294
28535
  )
28295
- ] }) : /* @__PURE__ */ jsxs("div", { className: "editor-floating-link-editor__view-container", children: [
28296
- /* @__PURE__ */ jsx(
28297
- "a",
28298
- {
28299
- href: sanitizeUrl(linkUrl),
28300
- target: "_blank",
28301
- rel: "noopener noreferrer",
28302
- className: "editor-floating-link-editor__link",
28303
- children: /* @__PURE__ */ jsx(TypographyPSmall, { className: "editor-truncate", children: linkUrl })
28536
+ ] }) }) : /* @__PURE__ */ jsxs("div", { className: "editor-floating-link-editor__view-container", children: [
28537
+ (() => {
28538
+ let href = sanitizeUrl(linkUrl);
28539
+ const jsDownloadMatch = typeof linkUrl === "string" ? linkUrl.match(/^javascript:download\(\s*(['"])(.*?)\1\s*\)\s*$/i) : null;
28540
+ let downloadAttr;
28541
+ if (jsDownloadMatch) {
28542
+ const jsArg = jsDownloadMatch[2] ?? "";
28543
+ href = buildHrefFromJsDownloadArg(jsArg);
28544
+ if (href !== "about:blank") {
28545
+ downloadAttr = inferDownloadFileName(href);
28546
+ }
28547
+ } else if (shouldTreatUrlAsDownload(href)) {
28548
+ downloadAttr = inferDownloadFileName(href);
28304
28549
  }
28305
- ),
28550
+ const isDownload = typeof downloadAttr === "string" && downloadAttr.length > 0;
28551
+ const text = jsDownloadMatch ? "Download" : shouldTreatUrlAsDownload(href) ? inferDownloadFileName(href) : href === "about:blank" ? "Invalid URL" : linkUrl;
28552
+ return /* @__PURE__ */ jsx(
28553
+ "a",
28554
+ {
28555
+ href,
28556
+ download: downloadAttr,
28557
+ target: isDownload ? "_self" : "_blank",
28558
+ rel: isDownload ? void 0 : "noopener noreferrer",
28559
+ className: "editor-floating-link-editor__link",
28560
+ children: /* @__PURE__ */ jsx(TypographyPSmall, { className: "editor-truncate", children: text })
28561
+ }
28562
+ );
28563
+ })(),
28306
28564
  /* @__PURE__ */ jsxs(Flex, { gap: 0, className: "editor-shrink-0", children: [
28307
28565
  /* @__PURE__ */ jsx(
28308
28566
  Button,
@@ -28616,6 +28874,8 @@ var init_floating_link_editor_plugin = __esm({
28616
28874
  init_flex();
28617
28875
  init_typography();
28618
28876
  init_image_node();
28877
+ init_download_link_node();
28878
+ init_uploads_context();
28619
28879
  }
28620
28880
  });
28621
28881