@thangph2146/lexical-editor 0.0.11 → 0.0.12

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 (45) 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 -3
  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 -3
  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/plugins/_floating-link-editor.scss +28 -2
  24. package/src/themes/ui-components/_button.scss +1 -1
  25. package/src/themes/ui-components/_flex.scss +1 -0
  26. package/src/ui/button-group.tsx +10 -10
  27. package/src/ui/button.tsx +38 -38
  28. package/src/ui/collapsible.tsx +67 -67
  29. package/src/ui/command.tsx +48 -48
  30. package/src/ui/dialog.tsx +146 -146
  31. package/src/ui/flex.tsx +45 -45
  32. package/src/ui/input.tsx +20 -20
  33. package/src/ui/label.tsx +20 -20
  34. package/src/ui/number-input.tsx +104 -104
  35. package/src/ui/popover.tsx +128 -128
  36. package/src/ui/scroll-area.tsx +17 -17
  37. package/src/ui/select.tsx +171 -171
  38. package/src/ui/separator.tsx +20 -20
  39. package/src/ui/slider.tsx +14 -14
  40. package/src/ui/slot.tsx +3 -3
  41. package/src/ui/tabs.tsx +87 -87
  42. package/src/ui/toggle-group.tsx +109 -109
  43. package/src/ui/toggle.tsx +28 -28
  44. package/src/ui/tooltip.tsx +28 -28
  45. package/src/ui/typography.tsx +44 -44
package/README.md CHANGED
@@ -37,7 +37,7 @@ function MyEditor() {
37
37
  <LexicalEditor
38
38
  placeholder="Nhập nội dung tại đây..."
39
39
  onChange={handleChange}
40
- />
40
+ />gioi-thieu:team-section
41
41
  );
42
42
  }
43
43
  ```
@@ -51,5 +51,6 @@ MIT
51
51
  ```bash
52
52
  pnpm build
53
53
  npm whoami
54
+ npm login
54
55
  npm publish --access public
55
56
  ```
@@ -20,9 +20,9 @@ var LexicalHistoryPlugin = require('@lexical/react/LexicalHistoryPlugin');
20
20
  var LexicalRichTextPlugin = require('@lexical/react/LexicalRichTextPlugin');
21
21
  var LexicalErrorBoundary = require('@lexical/react/LexicalErrorBoundary');
22
22
  var list = require('@lexical/list');
23
+ var link = require('@lexical/link');
23
24
  var code = require('@lexical/code');
24
25
  var hashtag = require('@lexical/hashtag');
25
- var link = require('@lexical/link');
26
26
  var overflow = require('@lexical/overflow');
27
27
  var LexicalHorizontalRuleNode = require('@lexical/react/LexicalHorizontalRuleNode');
28
28
  var richText = require('@lexical/rich-text');
@@ -5095,12 +5095,14 @@ var init_image_placeholder = __esm({
5095
5095
  }
5096
5096
  });
5097
5097
  function ContentEditable({
5098
- placeholder,
5098
+ placeholder = "",
5099
5099
  className,
5100
5100
  placeholderClassName,
5101
5101
  placeholderDefaults = true
5102
5102
  }) {
5103
5103
  const isReadOnlyOrReview = className?.includes("--readonly") || className?.includes("--review");
5104
+ const text = placeholder.trim();
5105
+ const showLexicalPlaceholder = text.length > 0;
5104
5106
  return /* @__PURE__ */ jsxRuntime.jsx(
5105
5107
  LexicalContentEditable.ContentEditable,
5106
5108
  {
@@ -5109,7 +5111,17 @@ function ContentEditable({
5109
5111
  !isReadOnlyOrReview && "min-h-72 px-8 py-4",
5110
5112
  className
5111
5113
  ),
5112
- "aria-label": "Editor n\u1ED9i dung"
5114
+ "aria-label": "Editor n\u1ED9i dung",
5115
+ ...showLexicalPlaceholder ? {
5116
+ "aria-placeholder": text,
5117
+ placeholder: /* @__PURE__ */ jsxRuntime.jsx(
5118
+ "div",
5119
+ {
5120
+ className: cn(placeholderDefaults && "editor-placeholder", placeholderClassName),
5121
+ children: text
5122
+ }
5123
+ )
5124
+ } : { placeholder: null }
5113
5125
  }
5114
5126
  );
5115
5127
  }
@@ -6245,6 +6257,86 @@ var init_list_with_color_node = __esm({
6245
6257
  };
6246
6258
  }
6247
6259
  });
6260
+ function $createDownloadLinkNode(url, download = null, attributes) {
6261
+ return new DownloadLinkNode(url, download, attributes);
6262
+ }
6263
+ function $isDownloadLinkNode(node) {
6264
+ return node instanceof DownloadLinkNode;
6265
+ }
6266
+ var DownloadLinkNode;
6267
+ var init_download_link_node = __esm({
6268
+ "src/nodes/download-link-node.tsx"() {
6269
+ DownloadLinkNode = class _DownloadLinkNode extends link.LinkNode {
6270
+ __download;
6271
+ static getType() {
6272
+ return "download-link";
6273
+ }
6274
+ static clone(node) {
6275
+ return new _DownloadLinkNode(
6276
+ node.getURL(),
6277
+ node.__download,
6278
+ {
6279
+ rel: node.getRel(),
6280
+ target: node.getTarget(),
6281
+ title: node.getTitle()
6282
+ },
6283
+ node.__key
6284
+ );
6285
+ }
6286
+ constructor(url, download = null, attributes, key) {
6287
+ super(url, attributes, key);
6288
+ this.__download = download;
6289
+ }
6290
+ getDownload() {
6291
+ return this.__download;
6292
+ }
6293
+ setDownload(download) {
6294
+ const writable = this.getWritable();
6295
+ writable.__download = download;
6296
+ return this;
6297
+ }
6298
+ createDOM(config) {
6299
+ const dom = super.createDOM(config);
6300
+ this.applyDownloadDOM(dom);
6301
+ return dom;
6302
+ }
6303
+ updateLinkDOM(prevNode, anchorElem, config) {
6304
+ super.updateLinkDOM(prevNode, anchorElem, config);
6305
+ this.applyDownloadDOM(anchorElem);
6306
+ }
6307
+ exportJSON() {
6308
+ return {
6309
+ ...super.exportJSON(),
6310
+ type: _DownloadLinkNode.getType(),
6311
+ version: 1,
6312
+ download: this.__download
6313
+ };
6314
+ }
6315
+ static importJSON(serializedNode) {
6316
+ const node = new _DownloadLinkNode(
6317
+ serializedNode.url,
6318
+ serializedNode.download,
6319
+ {
6320
+ rel: serializedNode.rel ?? null,
6321
+ target: serializedNode.target ?? null,
6322
+ title: serializedNode.title ?? null
6323
+ },
6324
+ serializedNode.key
6325
+ );
6326
+ return node;
6327
+ }
6328
+ applyDownloadDOM(dom) {
6329
+ if (dom instanceof HTMLAnchorElement) {
6330
+ if (this.__download === null) {
6331
+ dom.removeAttribute("download");
6332
+ } else {
6333
+ dom.setAttribute("download", this.__download);
6334
+ }
6335
+ }
6336
+ }
6337
+ };
6338
+ }
6339
+ });
6248
6340
  function $convertMentionElement(domNode) {
6249
6341
  const textContent = domNode.textContent;
6250
6342
  if (textContent !== null) {
@@ -6357,6 +6449,7 @@ var init_nodes = __esm({
6357
6449
  init_layout_container_node();
6358
6450
  init_layout_item_node();
6359
6451
  init_list_with_color_node();
6452
+ init_download_link_node();
6360
6453
  init_mention_node();
6361
6454
  nodes = [
6362
6455
  richText.HeadingNode,
@@ -6372,6 +6465,7 @@ var init_nodes = __esm({
6372
6465
  ListWithColorNode,
6373
6466
  list.ListItemNode,
6374
6467
  link.LinkNode,
6468
+ DownloadLinkNode,
6375
6469
  overflow.OverflowNode,
6376
6470
  hashtag.HashtagNode,
6377
6471
  table.TableNode,
@@ -28024,6 +28118,44 @@ var init_url = __esm({
28024
28118
  );
28025
28119
  }
28026
28120
  });
28121
+ function shouldTreatUrlAsDownload(url) {
28122
+ if (typeof url !== "string") return false;
28123
+ const u = url.toLowerCase();
28124
+ if (u.includes("/api/uploads/") || u.includes("/uploads/") || u.includes("/api/admin/uploads/") || u.includes("/admin/uploads/"))
28125
+ return true;
28126
+ return /\.(pdf|doc|docx|xls|xlsx|csv|zip|rar|7z|txt|rtf|png|jpg|jpeg|gif|webp|mp3|wav|mp4|mov|avi)(\?.*)?$/.test(
28127
+ u
28128
+ );
28129
+ }
28130
+ function inferDownloadFileName(url) {
28131
+ try {
28132
+ const path = url.split("?")[0] ?? "";
28133
+ const last = path.split("/").filter(Boolean).pop();
28134
+ return last ? decodeURIComponent(last) : "download";
28135
+ } catch {
28136
+ return "download";
28137
+ }
28138
+ }
28139
+ function getCookieValue(name) {
28140
+ if (typeof document === "undefined") return null;
28141
+ const row = document.cookie.split("; ").find((item) => item.startsWith(`${name}=`));
28142
+ if (!row) return null;
28143
+ return row.split("=").slice(1).join("=") || null;
28144
+ }
28145
+ function buildHrefFromJsDownloadArg(jsArg) {
28146
+ const firstSegment = typeof window !== "undefined" ? window.location.pathname.split("/").filter(Boolean)[0] ?? "" : "";
28147
+ const serveBase = firstSegment === "admin" ? "/api/admin/uploads/serve" : "/api/uploads/serve";
28148
+ const arg = jsArg.trim();
28149
+ if (!arg) return "about:blank";
28150
+ if (/^https?:\/\//i.test(arg)) return arg;
28151
+ if (arg.startsWith("/api/")) return arg;
28152
+ if (arg.startsWith("images/") || arg.startsWith("files/")) {
28153
+ return `${serveBase}/${arg}`;
28154
+ }
28155
+ const m = arg.match(/(images|files)\/.+/i);
28156
+ if (m?.[0]) return `${serveBase}/${m[0]}`;
28157
+ return "about:blank";
28158
+ }
28027
28159
  function FloatingLinkEditor({
28028
28160
  editor,
28029
28161
  isLink,
@@ -28037,6 +28169,9 @@ function FloatingLinkEditor({
28037
28169
  const [linkUrl, setLinkUrl] = React21.useState("");
28038
28170
  const [editedLinkUrl, setEditedLinkUrl] = React21.useState("https://");
28039
28171
  const [lastSelection, setLastSelection] = React21.useState(null);
28172
+ const [isUploadingFile, setIsUploadingFile] = React21.useState(false);
28173
+ const fileInputRef = React21.useRef(null);
28174
+ const { onUploadFile } = useEditorUploads();
28040
28175
  const $updateLinkEditor = React21.useCallback(() => {
28041
28176
  const selection = lexical.$getSelection();
28042
28177
  let linkNode = null;
@@ -28230,9 +28365,11 @@ function FloatingLinkEditor({
28230
28365
  setIsLinkEditMode(false);
28231
28366
  }
28232
28367
  };
28233
- const handleLinkSubmission = () => {
28234
- const url = sanitizeUrl(editedLinkUrl);
28235
- if (url && url !== "https://" && url !== "http://") {
28368
+ const handleLinkSubmission = (submittedUrl, originalFileName) => {
28369
+ const rawUrl = typeof submittedUrl === "string" ? submittedUrl : editedLinkUrl;
28370
+ const url = sanitizeUrl(rawUrl);
28371
+ const downloadFileName = originalFileName || (shouldTreatUrlAsDownload(url) ? inferDownloadFileName(url) : null);
28372
+ if (url && url !== "about:blank" && url !== "https://" && url !== "http://") {
28236
28373
  editor.update(() => {
28237
28374
  let selection = lexical.$getSelection();
28238
28375
  if (!selection && lastSelection !== null) {
@@ -28257,8 +28394,11 @@ function FloatingLinkEditor({
28257
28394
  const existingLinkNode = utils.$findMatchingParent(node, link.$isLinkNode) || (link.$isLinkNode(node.getParent()) ? node.getParent() : null);
28258
28395
  if (existingLinkNode) {
28259
28396
  existingLinkNode.setURL(url);
28397
+ if (downloadFileName && $isDownloadLinkNode(existingLinkNode)) {
28398
+ existingLinkNode.setDownload(downloadFileName);
28399
+ }
28260
28400
  } else {
28261
- const linkNode = link.$createLinkNode(url);
28401
+ const linkNode = downloadFileName ? $createDownloadLinkNode(url, downloadFileName) : link.$createLinkNode(url);
28262
28402
  utils.$wrapNodeInElement(node, () => linkNode);
28263
28403
  }
28264
28404
  }
@@ -28267,25 +28407,114 @@ function FloatingLinkEditor({
28267
28407
  editor.dispatchCommand(link.TOGGLE_LINK_COMMAND, url);
28268
28408
  const parent = getSelectedNode(selection).getParent();
28269
28409
  if (link.$isAutoLinkNode(parent)) {
28270
- const linkNode = link.$createLinkNode(parent.getURL(), {
28410
+ const linkNode = downloadFileName ? $createDownloadLinkNode(parent.getURL(), downloadFileName, {
28411
+ rel: parent.__rel,
28412
+ target: parent.__target,
28413
+ title: parent.__title
28414
+ }) : link.$createLinkNode(parent.getURL(), {
28271
28415
  rel: parent.__rel,
28272
28416
  target: parent.__target,
28273
28417
  title: parent.__title
28274
28418
  });
28275
28419
  parent.replace(linkNode, true);
28276
28420
  }
28421
+ if (downloadFileName) {
28422
+ const selectedNode = getSelectedNode(selection);
28423
+ const linkNode = utils.$findMatchingParent(selectedNode, link.$isLinkNode) || (link.$isLinkNode(selectedNode) ? selectedNode : null);
28424
+ if (linkNode) {
28425
+ const currentText = linkNode.getTextContent();
28426
+ const targetText = originalFileName || downloadFileName;
28427
+ const downloadLinkNode = $createDownloadLinkNode(url, downloadFileName, {
28428
+ rel: linkNode.getRel(),
28429
+ target: linkNode.getTarget(),
28430
+ title: linkNode.getTitle()
28431
+ });
28432
+ if (currentText === url && targetText) {
28433
+ downloadLinkNode.append(lexical.$createTextNode(targetText));
28434
+ } else {
28435
+ const children = linkNode.getChildren();
28436
+ if (children.length > 0) {
28437
+ downloadLinkNode.append(...children);
28438
+ }
28439
+ }
28440
+ linkNode.replace(downloadLinkNode);
28441
+ }
28442
+ }
28277
28443
  }
28278
28444
  });
28279
28445
  setEditedLinkUrl("https://");
28280
28446
  setIsLinkEditMode(false);
28281
28447
  }
28282
28448
  };
28449
+ const handlePickLocalFile = () => {
28450
+ if (isUploadingFile) return;
28451
+ fileInputRef.current?.click();
28452
+ };
28453
+ const handleUploadLocalFile = async (event) => {
28454
+ const file = event.target.files?.[0];
28455
+ if (!file) return;
28456
+ try {
28457
+ setIsUploadingFile(true);
28458
+ let uploadedUrl = void 0;
28459
+ if (onUploadFile) {
28460
+ const result = await onUploadFile(file);
28461
+ if (result.error) throw new Error(result.error);
28462
+ uploadedUrl = result.url;
28463
+ } else {
28464
+ const formData = new FormData();
28465
+ formData.append("file", file);
28466
+ const firstSegment = typeof window !== "undefined" ? window.location.pathname.split("/").filter(Boolean)[0] ?? "" : "";
28467
+ const pathPart = firstSegment === "admin" ? "/admin/uploads" : "/uploads";
28468
+ const endpoint = firstSegment === "admin" ? `/admin/api${pathPart}` : `/api${pathPart}`;
28469
+ const userId = getCookieValue("app_user_id");
28470
+ const authToken = getCookieValue("auth-token");
28471
+ const headers = {};
28472
+ if (userId) headers["X-User-Id"] = userId;
28473
+ if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
28474
+ const res = await fetch(endpoint, {
28475
+ method: "POST",
28476
+ credentials: "include",
28477
+ headers: Object.keys(headers).length > 0 ? headers : void 0,
28478
+ body: formData
28479
+ });
28480
+ if (!res.ok) {
28481
+ throw new Error(`Upload failed: HTTP ${res.status}`);
28482
+ }
28483
+ const payload = await res.json();
28484
+ uploadedUrl = payload?.data?.url;
28485
+ if (!payload?.success || !uploadedUrl) {
28486
+ throw new Error(payload?.message || payload?.error || "Upload failed");
28487
+ }
28488
+ }
28489
+ if (uploadedUrl) {
28490
+ setEditedLinkUrl(uploadedUrl);
28491
+ handleLinkSubmission(uploadedUrl, file.name);
28492
+ }
28493
+ } catch (error) {
28494
+ console.error("[FloatingLinkEditor] Upload local file failed:", error);
28495
+ } finally {
28496
+ setIsUploadingFile(false);
28497
+ if (fileInputRef.current) {
28498
+ fileInputRef.current.value = "";
28499
+ }
28500
+ }
28501
+ };
28283
28502
  return /* @__PURE__ */ jsxRuntime.jsx(
28284
28503
  "div",
28285
28504
  {
28286
28505
  ref: editorRef,
28287
28506
  className: "editor-floating-link-editor",
28288
- children: isLinkEditMode || isLink ? isLinkEditMode ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "editor-floating-link-editor__input-container", children: [
28507
+ children: isLinkEditMode || isLink ? isLinkEditMode ? /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "editor-floating-link-editor__input-container", children: [
28508
+ /* @__PURE__ */ jsxRuntime.jsx(
28509
+ "input",
28510
+ {
28511
+ ref: fileInputRef,
28512
+ type: "file",
28513
+ className: "hidden",
28514
+ onChange: handleUploadLocalFile,
28515
+ 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"
28516
+ }
28517
+ ),
28289
28518
  /* @__PURE__ */ jsxRuntime.jsx(
28290
28519
  Input,
28291
28520
  {
@@ -28296,6 +28525,18 @@ function FloatingLinkEditor({
28296
28525
  className: "editor-flex-grow"
28297
28526
  }
28298
28527
  ),
28528
+ /* @__PURE__ */ jsxRuntime.jsx(
28529
+ Button,
28530
+ {
28531
+ size: "icon",
28532
+ variant: "ghost",
28533
+ onClick: handlePickLocalFile,
28534
+ className: "editor-shrink-0",
28535
+ disabled: isUploadingFile,
28536
+ title: isUploadingFile ? "Uploading..." : "Upload file t\u1EEB thi\u1EBFt b\u1ECB",
28537
+ children: isUploadingFile ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Loader2, { className: "editor-icon-sm animate-spin" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Upload, { className: "editor-icon-sm" })
28538
+ }
28539
+ ),
28299
28540
  /* @__PURE__ */ jsxRuntime.jsx(
28300
28541
  Button,
28301
28542
  {
@@ -28313,22 +28554,39 @@ function FloatingLinkEditor({
28313
28554
  Button,
28314
28555
  {
28315
28556
  size: "icon",
28316
- onClick: handleLinkSubmission,
28557
+ onClick: () => handleLinkSubmission(),
28317
28558
  className: "editor-shrink-0",
28318
28559
  children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "editor-icon-sm" })
28319
28560
  }
28320
28561
  )
28321
- ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "editor-floating-link-editor__view-container", children: [
28322
- /* @__PURE__ */ jsxRuntime.jsx(
28323
- "a",
28324
- {
28325
- href: sanitizeUrl(linkUrl),
28326
- target: "_blank",
28327
- rel: "noopener noreferrer",
28328
- className: "editor-floating-link-editor__link",
28329
- children: /* @__PURE__ */ jsxRuntime.jsx(TypographyPSmall, { className: "editor-truncate", children: linkUrl })
28562
+ ] }) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "editor-floating-link-editor__view-container", children: [
28563
+ (() => {
28564
+ let href = sanitizeUrl(linkUrl);
28565
+ const jsDownloadMatch = typeof linkUrl === "string" ? linkUrl.match(/^javascript:download\(\s*(['"])(.*?)\1\s*\)\s*$/i) : null;
28566
+ let downloadAttr;
28567
+ if (jsDownloadMatch) {
28568
+ const jsArg = jsDownloadMatch[2] ?? "";
28569
+ href = buildHrefFromJsDownloadArg(jsArg);
28570
+ if (href !== "about:blank") {
28571
+ downloadAttr = inferDownloadFileName(href);
28572
+ }
28573
+ } else if (shouldTreatUrlAsDownload(href)) {
28574
+ downloadAttr = inferDownloadFileName(href);
28330
28575
  }
28331
- ),
28576
+ const isDownload = typeof downloadAttr === "string" && downloadAttr.length > 0;
28577
+ const text = jsDownloadMatch ? "Download" : shouldTreatUrlAsDownload(href) ? inferDownloadFileName(href) : href === "about:blank" ? "Invalid URL" : linkUrl;
28578
+ return /* @__PURE__ */ jsxRuntime.jsx(
28579
+ "a",
28580
+ {
28581
+ href,
28582
+ download: downloadAttr,
28583
+ target: isDownload ? "_self" : "_blank",
28584
+ rel: isDownload ? void 0 : "noopener noreferrer",
28585
+ className: "editor-floating-link-editor__link",
28586
+ children: /* @__PURE__ */ jsxRuntime.jsx(TypographyPSmall, { className: "editor-truncate", children: text })
28587
+ }
28588
+ );
28589
+ })(),
28332
28590
  /* @__PURE__ */ jsxRuntime.jsxs(Flex, { gap: 0, className: "editor-shrink-0", children: [
28333
28591
  /* @__PURE__ */ jsxRuntime.jsx(
28334
28592
  Button,
@@ -28642,6 +28900,8 @@ var init_floating_link_editor_plugin = __esm({
28642
28900
  init_flex();
28643
28901
  init_typography();
28644
28902
  init_image_node();
28903
+ init_download_link_node();
28904
+ init_uploads_context();
28645
28905
  }
28646
28906
  });
28647
28907