@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
package/dist/index.d.cts CHANGED
@@ -3,13 +3,38 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { SerializedEditorState } from 'lexical';
4
4
  export { SerializedEditorState } from 'lexical';
5
5
 
6
+ interface ImageItem {
7
+ fileName: string;
8
+ originalName: string;
9
+ size: number;
10
+ mimeType: string;
11
+ url: string;
12
+ relativePath: string;
13
+ createdAt: number;
14
+ }
15
+ interface FolderNode {
16
+ name: string;
17
+ path: string;
18
+ images: ImageItem[];
19
+ subfolders: FolderNode[];
20
+ }
21
+ interface EditorUploadsContextType {
22
+ isLoading: boolean;
23
+ folderTree?: FolderNode;
24
+ onUploadFile?: (file: File) => Promise<{
25
+ url: string;
26
+ error?: string;
27
+ }>;
28
+ }
29
+
6
30
  interface LexicalEditorProps {
7
31
  value?: unknown;
8
32
  onChange?: (value: SerializedEditorState) => void;
9
33
  readOnly?: boolean;
10
34
  className?: string;
11
35
  placeholder?: string;
36
+ uploadsContext?: EditorUploadsContextType;
12
37
  }
13
- declare function LexicalEditor({ value, onChange, readOnly, className, placeholder, }: LexicalEditorProps): react_jsx_runtime.JSX.Element;
38
+ declare function LexicalEditor({ value, onChange, readOnly, className, placeholder, uploadsContext, }: LexicalEditorProps): react_jsx_runtime.JSX.Element;
14
39
 
15
40
  export { LexicalEditor, type LexicalEditorProps };
package/dist/index.d.ts CHANGED
@@ -3,13 +3,38 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { SerializedEditorState } from 'lexical';
4
4
  export { SerializedEditorState } from 'lexical';
5
5
 
6
+ interface ImageItem {
7
+ fileName: string;
8
+ originalName: string;
9
+ size: number;
10
+ mimeType: string;
11
+ url: string;
12
+ relativePath: string;
13
+ createdAt: number;
14
+ }
15
+ interface FolderNode {
16
+ name: string;
17
+ path: string;
18
+ images: ImageItem[];
19
+ subfolders: FolderNode[];
20
+ }
21
+ interface EditorUploadsContextType {
22
+ isLoading: boolean;
23
+ folderTree?: FolderNode;
24
+ onUploadFile?: (file: File) => Promise<{
25
+ url: string;
26
+ error?: string;
27
+ }>;
28
+ }
29
+
6
30
  interface LexicalEditorProps {
7
31
  value?: unknown;
8
32
  onChange?: (value: SerializedEditorState) => void;
9
33
  readOnly?: boolean;
10
34
  className?: string;
11
35
  placeholder?: string;
36
+ uploadsContext?: EditorUploadsContextType;
12
37
  }
13
- declare function LexicalEditor({ value, onChange, readOnly, className, placeholder, }: LexicalEditorProps): react_jsx_runtime.JSX.Element;
38
+ declare function LexicalEditor({ value, onChange, readOnly, className, placeholder, uploadsContext, }: LexicalEditorProps): react_jsx_runtime.JSX.Element;
14
39
 
15
40
  export { LexicalEditor, type LexicalEditorProps };
package/dist/index.js CHANGED
@@ -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';
@@ -4192,6 +4192,12 @@ var init_tabs = __esm({
4192
4192
  TabsContext = React21.createContext(null);
4193
4193
  }
4194
4194
  });
4195
+ function EditorUploadsProvider({
4196
+ children,
4197
+ value
4198
+ }) {
4199
+ return /* @__PURE__ */ jsx(EditorUploadsContext.Provider, { value, children });
4200
+ }
4195
4201
  function useEditorUploads() {
4196
4202
  const context = useContext(EditorUploadsContext);
4197
4203
  if (context === void 0) {
@@ -5069,12 +5075,14 @@ var init_image_placeholder = __esm({
5069
5075
  }
5070
5076
  });
5071
5077
  function ContentEditable({
5072
- placeholder,
5078
+ placeholder = "",
5073
5079
  className,
5074
5080
  placeholderClassName,
5075
5081
  placeholderDefaults = true
5076
5082
  }) {
5077
5083
  const isReadOnlyOrReview = className?.includes("--readonly") || className?.includes("--review");
5084
+ const text = placeholder.trim();
5085
+ const showLexicalPlaceholder = text.length > 0;
5078
5086
  return /* @__PURE__ */ jsx(
5079
5087
  ContentEditable$1,
5080
5088
  {
@@ -5083,7 +5091,17 @@ function ContentEditable({
5083
5091
  !isReadOnlyOrReview && "min-h-72 px-8 py-4",
5084
5092
  className
5085
5093
  ),
5086
- "aria-label": "Editor n\u1ED9i dung"
5094
+ "aria-label": "Editor n\u1ED9i dung",
5095
+ ...showLexicalPlaceholder ? {
5096
+ "aria-placeholder": text,
5097
+ placeholder: /* @__PURE__ */ jsx(
5098
+ "div",
5099
+ {
5100
+ className: cn(placeholderDefaults && "editor-placeholder", placeholderClassName),
5101
+ children: text
5102
+ }
5103
+ )
5104
+ } : { placeholder: null }
5087
5105
  }
5088
5106
  );
5089
5107
  }
@@ -6219,6 +6237,86 @@ var init_list_with_color_node = __esm({
6219
6237
  };
6220
6238
  }
6221
6239
  });
6240
+ function $createDownloadLinkNode(url, download = null, attributes) {
6241
+ return new DownloadLinkNode(url, download, attributes);
6242
+ }
6243
+ function $isDownloadLinkNode(node) {
6244
+ return node instanceof DownloadLinkNode;
6245
+ }
6246
+ var DownloadLinkNode;
6247
+ var init_download_link_node = __esm({
6248
+ "src/nodes/download-link-node.tsx"() {
6249
+ DownloadLinkNode = class _DownloadLinkNode extends LinkNode {
6250
+ __download;
6251
+ static getType() {
6252
+ return "download-link";
6253
+ }
6254
+ static clone(node) {
6255
+ return new _DownloadLinkNode(
6256
+ node.getURL(),
6257
+ node.__download,
6258
+ {
6259
+ rel: node.getRel(),
6260
+ target: node.getTarget(),
6261
+ title: node.getTitle()
6262
+ },
6263
+ node.__key
6264
+ );
6265
+ }
6266
+ constructor(url, download = null, attributes, key) {
6267
+ super(url, attributes, key);
6268
+ this.__download = download;
6269
+ }
6270
+ getDownload() {
6271
+ return this.__download;
6272
+ }
6273
+ setDownload(download) {
6274
+ const writable = this.getWritable();
6275
+ writable.__download = download;
6276
+ return this;
6277
+ }
6278
+ createDOM(config) {
6279
+ const dom = super.createDOM(config);
6280
+ this.applyDownloadDOM(dom);
6281
+ return dom;
6282
+ }
6283
+ updateLinkDOM(prevNode, anchorElem, config) {
6284
+ super.updateLinkDOM(prevNode, anchorElem, config);
6285
+ this.applyDownloadDOM(anchorElem);
6286
+ }
6287
+ exportJSON() {
6288
+ return {
6289
+ ...super.exportJSON(),
6290
+ type: _DownloadLinkNode.getType(),
6291
+ version: 1,
6292
+ download: this.__download
6293
+ };
6294
+ }
6295
+ static importJSON(serializedNode) {
6296
+ const node = new _DownloadLinkNode(
6297
+ serializedNode.url,
6298
+ serializedNode.download,
6299
+ {
6300
+ rel: serializedNode.rel ?? null,
6301
+ target: serializedNode.target ?? null,
6302
+ title: serializedNode.title ?? null
6303
+ },
6304
+ serializedNode.key
6305
+ );
6306
+ return node;
6307
+ }
6308
+ applyDownloadDOM(dom) {
6309
+ if (dom instanceof HTMLAnchorElement) {
6310
+ if (this.__download === null) {
6311
+ dom.removeAttribute("download");
6312
+ } else {
6313
+ dom.setAttribute("download", this.__download);
6314
+ }
6315
+ }
6316
+ }
6317
+ };
6318
+ }
6319
+ });
6222
6320
  function $convertMentionElement(domNode) {
6223
6321
  const textContent = domNode.textContent;
6224
6322
  if (textContent !== null) {
@@ -6331,6 +6429,7 @@ var init_nodes = __esm({
6331
6429
  init_layout_container_node();
6332
6430
  init_layout_item_node();
6333
6431
  init_list_with_color_node();
6432
+ init_download_link_node();
6334
6433
  init_mention_node();
6335
6434
  nodes = [
6336
6435
  HeadingNode,
@@ -6346,6 +6445,7 @@ var init_nodes = __esm({
6346
6445
  ListWithColorNode,
6347
6446
  ListItemNode,
6348
6447
  LinkNode,
6448
+ DownloadLinkNode,
6349
6449
  OverflowNode,
6350
6450
  HashtagNode,
6351
6451
  TableNode,
@@ -27998,6 +28098,44 @@ var init_url = __esm({
27998
28098
  );
27999
28099
  }
28000
28100
  });
28101
+ function shouldTreatUrlAsDownload(url) {
28102
+ if (typeof url !== "string") return false;
28103
+ const u = url.toLowerCase();
28104
+ if (u.includes("/api/uploads/") || u.includes("/uploads/") || u.includes("/api/admin/uploads/") || u.includes("/admin/uploads/"))
28105
+ return true;
28106
+ return /\.(pdf|doc|docx|xls|xlsx|csv|zip|rar|7z|txt|rtf|png|jpg|jpeg|gif|webp|mp3|wav|mp4|mov|avi)(\?.*)?$/.test(
28107
+ u
28108
+ );
28109
+ }
28110
+ function inferDownloadFileName(url) {
28111
+ try {
28112
+ const path = url.split("?")[0] ?? "";
28113
+ const last = path.split("/").filter(Boolean).pop();
28114
+ return last ? decodeURIComponent(last) : "download";
28115
+ } catch {
28116
+ return "download";
28117
+ }
28118
+ }
28119
+ function getCookieValue(name) {
28120
+ if (typeof document === "undefined") return null;
28121
+ const row = document.cookie.split("; ").find((item) => item.startsWith(`${name}=`));
28122
+ if (!row) return null;
28123
+ return row.split("=").slice(1).join("=") || null;
28124
+ }
28125
+ function buildHrefFromJsDownloadArg(jsArg) {
28126
+ const firstSegment = typeof window !== "undefined" ? window.location.pathname.split("/").filter(Boolean)[0] ?? "" : "";
28127
+ const serveBase = firstSegment === "admin" ? "/api/admin/uploads/serve" : "/api/uploads/serve";
28128
+ const arg = jsArg.trim();
28129
+ if (!arg) return "about:blank";
28130
+ if (/^https?:\/\//i.test(arg)) return arg;
28131
+ if (arg.startsWith("/api/")) return arg;
28132
+ if (arg.startsWith("images/") || arg.startsWith("files/")) {
28133
+ return `${serveBase}/${arg}`;
28134
+ }
28135
+ const m = arg.match(/(images|files)\/.+/i);
28136
+ if (m?.[0]) return `${serveBase}/${m[0]}`;
28137
+ return "about:blank";
28138
+ }
28001
28139
  function FloatingLinkEditor({
28002
28140
  editor,
28003
28141
  isLink,
@@ -28011,6 +28149,9 @@ function FloatingLinkEditor({
28011
28149
  const [linkUrl, setLinkUrl] = useState("");
28012
28150
  const [editedLinkUrl, setEditedLinkUrl] = useState("https://");
28013
28151
  const [lastSelection, setLastSelection] = useState(null);
28152
+ const [isUploadingFile, setIsUploadingFile] = useState(false);
28153
+ const fileInputRef = useRef(null);
28154
+ const { onUploadFile } = useEditorUploads();
28014
28155
  const $updateLinkEditor = useCallback(() => {
28015
28156
  const selection = $getSelection();
28016
28157
  let linkNode = null;
@@ -28204,9 +28345,11 @@ function FloatingLinkEditor({
28204
28345
  setIsLinkEditMode(false);
28205
28346
  }
28206
28347
  };
28207
- const handleLinkSubmission = () => {
28208
- const url = sanitizeUrl(editedLinkUrl);
28209
- if (url && url !== "https://" && url !== "http://") {
28348
+ const handleLinkSubmission = (submittedUrl, originalFileName) => {
28349
+ const rawUrl = typeof submittedUrl === "string" ? submittedUrl : editedLinkUrl;
28350
+ const url = sanitizeUrl(rawUrl);
28351
+ const downloadFileName = originalFileName || (shouldTreatUrlAsDownload(url) ? inferDownloadFileName(url) : null);
28352
+ if (url && url !== "about:blank" && url !== "https://" && url !== "http://") {
28210
28353
  editor.update(() => {
28211
28354
  let selection = $getSelection();
28212
28355
  if (!selection && lastSelection !== null) {
@@ -28231,8 +28374,11 @@ function FloatingLinkEditor({
28231
28374
  const existingLinkNode = $findMatchingParent(node, $isLinkNode) || ($isLinkNode(node.getParent()) ? node.getParent() : null);
28232
28375
  if (existingLinkNode) {
28233
28376
  existingLinkNode.setURL(url);
28377
+ if (downloadFileName && $isDownloadLinkNode(existingLinkNode)) {
28378
+ existingLinkNode.setDownload(downloadFileName);
28379
+ }
28234
28380
  } else {
28235
- const linkNode = $createLinkNode(url);
28381
+ const linkNode = downloadFileName ? $createDownloadLinkNode(url, downloadFileName) : $createLinkNode(url);
28236
28382
  $wrapNodeInElement(node, () => linkNode);
28237
28383
  }
28238
28384
  }
@@ -28241,25 +28387,114 @@ function FloatingLinkEditor({
28241
28387
  editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
28242
28388
  const parent = getSelectedNode(selection).getParent();
28243
28389
  if ($isAutoLinkNode(parent)) {
28244
- const linkNode = $createLinkNode(parent.getURL(), {
28390
+ const linkNode = downloadFileName ? $createDownloadLinkNode(parent.getURL(), downloadFileName, {
28391
+ rel: parent.__rel,
28392
+ target: parent.__target,
28393
+ title: parent.__title
28394
+ }) : $createLinkNode(parent.getURL(), {
28245
28395
  rel: parent.__rel,
28246
28396
  target: parent.__target,
28247
28397
  title: parent.__title
28248
28398
  });
28249
28399
  parent.replace(linkNode, true);
28250
28400
  }
28401
+ if (downloadFileName) {
28402
+ const selectedNode = getSelectedNode(selection);
28403
+ const linkNode = $findMatchingParent(selectedNode, $isLinkNode) || ($isLinkNode(selectedNode) ? selectedNode : null);
28404
+ if (linkNode) {
28405
+ const currentText = linkNode.getTextContent();
28406
+ const targetText = originalFileName || downloadFileName;
28407
+ const downloadLinkNode = $createDownloadLinkNode(url, downloadFileName, {
28408
+ rel: linkNode.getRel(),
28409
+ target: linkNode.getTarget(),
28410
+ title: linkNode.getTitle()
28411
+ });
28412
+ if (currentText === url && targetText) {
28413
+ downloadLinkNode.append($createTextNode(targetText));
28414
+ } else {
28415
+ const children = linkNode.getChildren();
28416
+ if (children.length > 0) {
28417
+ downloadLinkNode.append(...children);
28418
+ }
28419
+ }
28420
+ linkNode.replace(downloadLinkNode);
28421
+ }
28422
+ }
28251
28423
  }
28252
28424
  });
28253
28425
  setEditedLinkUrl("https://");
28254
28426
  setIsLinkEditMode(false);
28255
28427
  }
28256
28428
  };
28429
+ const handlePickLocalFile = () => {
28430
+ if (isUploadingFile) return;
28431
+ fileInputRef.current?.click();
28432
+ };
28433
+ const handleUploadLocalFile = async (event) => {
28434
+ const file = event.target.files?.[0];
28435
+ if (!file) return;
28436
+ try {
28437
+ setIsUploadingFile(true);
28438
+ let uploadedUrl = void 0;
28439
+ if (onUploadFile) {
28440
+ const result = await onUploadFile(file);
28441
+ if (result.error) throw new Error(result.error);
28442
+ uploadedUrl = result.url;
28443
+ } else {
28444
+ const formData = new FormData();
28445
+ formData.append("file", file);
28446
+ const firstSegment = typeof window !== "undefined" ? window.location.pathname.split("/").filter(Boolean)[0] ?? "" : "";
28447
+ const pathPart = firstSegment === "admin" ? "/admin/uploads" : "/uploads";
28448
+ const endpoint = firstSegment === "admin" ? `/admin/api${pathPart}` : `/api${pathPart}`;
28449
+ const userId = getCookieValue("app_user_id");
28450
+ const authToken = getCookieValue("auth-token");
28451
+ const headers = {};
28452
+ if (userId) headers["X-User-Id"] = userId;
28453
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
28454
+ const res = await fetch(endpoint, {
28455
+ method: "POST",
28456
+ credentials: "include",
28457
+ headers: Object.keys(headers).length > 0 ? headers : void 0,
28458
+ body: formData
28459
+ });
28460
+ if (!res.ok) {
28461
+ throw new Error(`Upload failed: HTTP ${res.status}`);
28462
+ }
28463
+ const payload = await res.json();
28464
+ uploadedUrl = payload?.data?.url;
28465
+ if (!payload?.success || !uploadedUrl) {
28466
+ throw new Error(payload?.message || payload?.error || "Upload failed");
28467
+ }
28468
+ }
28469
+ if (uploadedUrl) {
28470
+ setEditedLinkUrl(uploadedUrl);
28471
+ handleLinkSubmission(uploadedUrl, file.name);
28472
+ }
28473
+ } catch (error) {
28474
+ console.error("[FloatingLinkEditor] Upload local file failed:", error);
28475
+ } finally {
28476
+ setIsUploadingFile(false);
28477
+ if (fileInputRef.current) {
28478
+ fileInputRef.current.value = "";
28479
+ }
28480
+ }
28481
+ };
28257
28482
  return /* @__PURE__ */ jsx(
28258
28483
  "div",
28259
28484
  {
28260
28485
  ref: editorRef,
28261
28486
  className: "editor-floating-link-editor",
28262
- children: isLinkEditMode || isLink ? isLinkEditMode ? /* @__PURE__ */ jsxs("div", { className: "editor-floating-link-editor__input-container", children: [
28487
+ children: isLinkEditMode || isLink ? isLinkEditMode ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", { className: "editor-floating-link-editor__input-container", children: [
28488
+ /* @__PURE__ */ jsx(
28489
+ "input",
28490
+ {
28491
+ ref: fileInputRef,
28492
+ type: "file",
28493
+ className: "hidden",
28494
+ onChange: handleUploadLocalFile,
28495
+ 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"
28496
+ }
28497
+ ),
28263
28498
  /* @__PURE__ */ jsx(
28264
28499
  Input,
28265
28500
  {
@@ -28270,6 +28505,18 @@ function FloatingLinkEditor({
28270
28505
  className: "editor-flex-grow"
28271
28506
  }
28272
28507
  ),
28508
+ /* @__PURE__ */ jsx(
28509
+ Button,
28510
+ {
28511
+ size: "icon",
28512
+ variant: "ghost",
28513
+ onClick: handlePickLocalFile,
28514
+ className: "editor-shrink-0",
28515
+ disabled: isUploadingFile,
28516
+ title: isUploadingFile ? "Uploading..." : "Upload file t\u1EEB thi\u1EBFt b\u1ECB",
28517
+ children: isUploadingFile ? /* @__PURE__ */ jsx(Loader2, { className: "editor-icon-sm animate-spin" }) : /* @__PURE__ */ jsx(Upload, { className: "editor-icon-sm" })
28518
+ }
28519
+ ),
28273
28520
  /* @__PURE__ */ jsx(
28274
28521
  Button,
28275
28522
  {
@@ -28287,22 +28534,39 @@ function FloatingLinkEditor({
28287
28534
  Button,
28288
28535
  {
28289
28536
  size: "icon",
28290
- onClick: handleLinkSubmission,
28537
+ onClick: () => handleLinkSubmission(),
28291
28538
  className: "editor-shrink-0",
28292
28539
  children: /* @__PURE__ */ jsx(Check, { className: "editor-icon-sm" })
28293
28540
  }
28294
28541
  )
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 })
28542
+ ] }) }) : /* @__PURE__ */ jsxs("div", { className: "editor-floating-link-editor__view-container", children: [
28543
+ (() => {
28544
+ let href = sanitizeUrl(linkUrl);
28545
+ const jsDownloadMatch = typeof linkUrl === "string" ? linkUrl.match(/^javascript:download\(\s*(['"])(.*?)\1\s*\)\s*$/i) : null;
28546
+ let downloadAttr;
28547
+ if (jsDownloadMatch) {
28548
+ const jsArg = jsDownloadMatch[2] ?? "";
28549
+ href = buildHrefFromJsDownloadArg(jsArg);
28550
+ if (href !== "about:blank") {
28551
+ downloadAttr = inferDownloadFileName(href);
28552
+ }
28553
+ } else if (shouldTreatUrlAsDownload(href)) {
28554
+ downloadAttr = inferDownloadFileName(href);
28304
28555
  }
28305
- ),
28556
+ const isDownload = typeof downloadAttr === "string" && downloadAttr.length > 0;
28557
+ const text = jsDownloadMatch ? "Download" : shouldTreatUrlAsDownload(href) ? inferDownloadFileName(href) : href === "about:blank" ? "Invalid URL" : linkUrl;
28558
+ return /* @__PURE__ */ jsx(
28559
+ "a",
28560
+ {
28561
+ href,
28562
+ download: downloadAttr,
28563
+ target: isDownload ? "_self" : "_blank",
28564
+ rel: isDownload ? void 0 : "noopener noreferrer",
28565
+ className: "editor-floating-link-editor__link",
28566
+ children: /* @__PURE__ */ jsx(TypographyPSmall, { className: "editor-truncate", children: text })
28567
+ }
28568
+ );
28569
+ })(),
28306
28570
  /* @__PURE__ */ jsxs(Flex, { gap: 0, className: "editor-shrink-0", children: [
28307
28571
  /* @__PURE__ */ jsx(
28308
28572
  Button,
@@ -28616,6 +28880,8 @@ var init_floating_link_editor_plugin = __esm({
28616
28880
  init_flex();
28617
28881
  init_typography();
28618
28882
  init_image_node();
28883
+ init_download_link_node();
28884
+ init_uploads_context();
28619
28885
  }
28620
28886
  });
28621
28887
 
@@ -33503,6 +33769,7 @@ function Editor({
33503
33769
  );
33504
33770
  }
33505
33771
  init_logger();
33772
+ init_uploads_context();
33506
33773
  function isValidSerializedEditorState(value) {
33507
33774
  return value !== null && typeof value === "object" && "root" in value && value.root !== null && typeof value.root === "object" && "type" in value.root && value.root.type === "root";
33508
33775
  }
@@ -33511,7 +33778,8 @@ function LexicalEditor11({
33511
33778
  onChange,
33512
33779
  readOnly = false,
33513
33780
  className,
33514
- placeholder = ""
33781
+ placeholder = "",
33782
+ uploadsContext
33515
33783
  }) {
33516
33784
  const [editorState, setEditorState] = useState(() => {
33517
33785
  if (value && typeof value === "object" && value !== null) {
@@ -33581,7 +33849,7 @@ function LexicalEditor11({
33581
33849
  onChange(newState);
33582
33850
  }
33583
33851
  };
33584
- return /* @__PURE__ */ jsx("div", { className, children: /* @__PURE__ */ jsx(
33852
+ const editorContent = /* @__PURE__ */ jsx(
33585
33853
  Editor,
33586
33854
  {
33587
33855
  editorSerializedState: editorState,
@@ -33589,7 +33857,8 @@ function LexicalEditor11({
33589
33857
  readOnly,
33590
33858
  placeholder
33591
33859
  }
33592
- ) });
33860
+ );
33861
+ return /* @__PURE__ */ jsx("div", { className, children: uploadsContext ? /* @__PURE__ */ jsx(EditorUploadsProvider, { value: uploadsContext, children: editorContent }) : editorContent });
33593
33862
  }
33594
33863
 
33595
33864
  export { Editor, LexicalEditor11 as LexicalEditor };