camox 0.31.4 → 0.32.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.
@@ -1,35 +1,70 @@
1
- import "react";
1
+ import { getPageIdFromTextLinkTarget, isHttpTextLinkTarget, isValidTextLinkTarget, resolveTextLinkHref, shouldOpenTextLinkInNewTab } from "./textLinks.js";
2
+ import * as React from "react";
2
3
  import { Fragment, jsx } from "react/jsx-runtime";
3
4
 
4
5
  //#region src/core/lib/lexicalReact.tsx
5
6
  /**
6
- * Parse a markdown string with **bold** and *italic* into React nodes.
7
+ * Parse a markdown string with **bold**, *italic*, and text links into React nodes.
7
8
  * Falls back to rendering the raw string if it's not a string value.
8
9
  */
9
- function markdownToReactNodes(value) {
10
+ function markdownToReactNodes(value, options = {}) {
10
11
  if (typeof value !== "string") return null;
11
12
  if (!value) return null;
12
- const regex = /(\*{1,3})((?:(?!\1).)+)\1/g;
13
13
  const parts = [];
14
- let lastIndex = 0;
15
- let match;
16
14
  let key = 0;
15
+ const fallbackHref = options.fallbackHref ?? "#";
17
16
  const pushWithLineBreaks = (text) => {
18
17
  text.split("\n").forEach((line, i) => {
19
18
  if (i > 0) parts.push(/* @__PURE__ */ jsx("br", {}, key++));
20
19
  if (line) parts.push(line);
21
20
  });
22
21
  };
23
- while ((match = regex.exec(value)) !== null) {
24
- if (match.index > lastIndex) pushWithLineBreaks(value.slice(lastIndex, match.index));
25
- const stars = match[1].length;
26
- const content = match[2];
27
- if (stars === 3) parts.push(/* @__PURE__ */ jsx("strong", { children: /* @__PURE__ */ jsx("em", { children: content }) }, key++));
28
- else if (stars === 2) parts.push(/* @__PURE__ */ jsx("strong", { children: content }, key++));
29
- else parts.push(/* @__PURE__ */ jsx("em", { children: content }, key++));
30
- lastIndex = match.index + match[0].length;
22
+ const pushFormatted = (text) => {
23
+ const regex = /(\*{1,3})((?:(?!\1).)+)\1/g;
24
+ let lastIndex = 0;
25
+ let match;
26
+ while ((match = regex.exec(text)) !== null) {
27
+ if (match.index > lastIndex) pushWithLineBreaks(text.slice(lastIndex, match.index));
28
+ const stars = match[1].length;
29
+ const content = match[2];
30
+ if (stars === 3) {
31
+ const emphasis = options.components?.emphasis?.({ children: content }) ?? /* @__PURE__ */ jsx("em", { children: content });
32
+ parts.push(/* @__PURE__ */ jsx(React.Fragment, { children: options.components?.strong?.({ children: emphasis }) ?? /* @__PURE__ */ jsx("strong", { children: emphasis }) }, key++));
33
+ } else if (stars === 2) parts.push(/* @__PURE__ */ jsx(React.Fragment, { children: options.components?.strong?.({ children: content }) ?? /* @__PURE__ */ jsx("strong", { children: content }) }, key++));
34
+ else parts.push(/* @__PURE__ */ jsx(React.Fragment, { children: options.components?.emphasis?.({ children: content }) ?? /* @__PURE__ */ jsx("em", { children: content }) }, key++));
35
+ lastIndex = match.index + match[0].length;
36
+ }
37
+ if (lastIndex < text.length) pushWithLineBreaks(text.slice(lastIndex));
38
+ };
39
+ const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
40
+ let lastLinkIndex = 0;
41
+ let linkMatch;
42
+ while ((linkMatch = linkRegex.exec(value)) !== null) {
43
+ if (linkMatch.index > lastLinkIndex) pushFormatted(value.slice(lastLinkIndex, linkMatch.index));
44
+ const [, label, target] = linkMatch;
45
+ const href = isValidTextLinkTarget(target) ? resolveTextLinkHref(target, options.pages, fallbackHref) : null;
46
+ if (href) {
47
+ const openInNewTab = shouldOpenTextLinkInNewTab(target);
48
+ const linkProps = {
49
+ href,
50
+ target: openInNewTab ? "_blank" : void 0,
51
+ rel: openInNewTab ? "noreferrer" : void 0,
52
+ children: label
53
+ };
54
+ const linkData = {
55
+ target,
56
+ href,
57
+ external: isHttpTextLinkTarget(target),
58
+ pageId: getPageIdFromTextLinkTarget(target) ?? void 0
59
+ };
60
+ parts.push(/* @__PURE__ */ jsx(React.Fragment, { children: options.components?.link?.(linkProps, linkData) ?? /* @__PURE__ */ jsx("a", {
61
+ ...linkProps,
62
+ style: { textDecorationLine: "underline" }
63
+ }) }, key++));
64
+ } else pushFormatted(label);
65
+ lastLinkIndex = linkMatch.index + linkMatch[0].length;
31
66
  }
32
- if (lastIndex < value.length) pushWithLineBreaks(value.slice(lastIndex));
67
+ if (lastLinkIndex < value.length) pushFormatted(value.slice(lastLinkIndex));
33
68
  if (parts.length === 0) return value;
34
69
  return /* @__PURE__ */ jsx(Fragment, { children: parts });
35
70
  }
@@ -1,4 +1,5 @@
1
1
  import { FORMAT_FLAGS, lexicalTextToMarkdown } from "./modifierFormats.js";
2
+ import { isValidTextLinkTarget } from "./textLinks.js";
2
3
 
3
4
  //#region src/core/lib/lexicalState.ts
4
5
  function isLexicalState(value) {
@@ -19,6 +20,12 @@ function lexicalStateToMarkdown(serialized) {
19
20
  }
20
21
  function extractMarkdownFromNode(node) {
21
22
  if (node.type === "text") return lexicalTextToMarkdown(node.text ?? "", node.format ?? 0);
23
+ if (node.type === "link") {
24
+ const text = (node.children ?? []).map(extractMarkdownFromNode).join("");
25
+ const url = typeof node.url === "string" ? node.url : "";
26
+ if (!url) return text;
27
+ return `[${text}](${url})`;
28
+ }
22
29
  if (node.type === "linebreak") return "\n";
23
30
  if (!node.children) return "";
24
31
  const parts = [];
@@ -67,6 +74,33 @@ function parseParagraphWithLineBreaks(para) {
67
74
  return nodes;
68
75
  }
69
76
  function parseInlineMarkdown(text) {
77
+ const nodes = [];
78
+ const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
79
+ let lastLinkIndex = 0;
80
+ let linkMatch;
81
+ while ((linkMatch = linkRegex.exec(text)) !== null) {
82
+ if (linkMatch.index > lastLinkIndex) nodes.push(...parseFormattedText(text.slice(lastLinkIndex, linkMatch.index)));
83
+ const [, label, target] = linkMatch;
84
+ if (isValidTextLinkTarget(target)) nodes.push({
85
+ children: parseFormattedText(label),
86
+ direction: "ltr",
87
+ format: "",
88
+ indent: 0,
89
+ rel: null,
90
+ target: null,
91
+ title: null,
92
+ type: "link",
93
+ url: target,
94
+ version: 1
95
+ });
96
+ else nodes.push(...parseFormattedText(label));
97
+ lastLinkIndex = linkMatch.index + linkMatch[0].length;
98
+ }
99
+ if (lastLinkIndex < text.length) nodes.push(...parseFormattedText(text.slice(lastLinkIndex)));
100
+ if (nodes.length === 0) return parseFormattedText(text);
101
+ return nodes;
102
+ }
103
+ function parseFormattedText(text) {
70
104
  const segments = [];
71
105
  const regex = /(\*{1,3})((?:(?!\1).)+)\1/g;
72
106
  let lastIndex = 0;
@@ -0,0 +1,28 @@
1
+ //#region src/core/lib/textLinks.ts
2
+ const PAGE_TEXT_LINK_PREFIX = "camox:page:";
3
+ function getPageIdFromTextLinkTarget(target) {
4
+ if (!target.startsWith("camox:page:")) return null;
5
+ const pageId = target.slice(11);
6
+ if (!pageId) return null;
7
+ return pageId;
8
+ }
9
+ function createPageTextLinkTarget(pageId) {
10
+ return `${PAGE_TEXT_LINK_PREFIX}${pageId}`;
11
+ }
12
+ function isHttpTextLinkTarget(target) {
13
+ return /^https?:\/\//i.test(target);
14
+ }
15
+ function isValidTextLinkTarget(target) {
16
+ return getPageIdFromTextLinkTarget(target) != null || isHttpTextLinkTarget(target);
17
+ }
18
+ function resolveTextLinkHref(target, pages, fallbackHref) {
19
+ const pageId = getPageIdFromTextLinkTarget(target);
20
+ if (pageId == null) return isHttpTextLinkTarget(target) ? target : null;
21
+ return (pages?.find((p) => String(p.id) === pageId))?.fullPath ?? fallbackHref;
22
+ }
23
+ function shouldOpenTextLinkInNewTab(target) {
24
+ return isHttpTextLinkTarget(target);
25
+ }
26
+
27
+ //#endregion
28
+ export { createPageTextLinkTarget, getPageIdFromTextLinkTarget, isHttpTextLinkTarget, isValidTextLinkTarget, resolveTextLinkHref, shouldOpenTextLinkInNewTab };
@@ -3,6 +3,7 @@ import { useIsPreviewSheetOpen } from "./PreviewSideSheet.js";
3
3
  import { cn, formatShortcut } from "../../../lib/utils.js";
4
4
  import { isOverlayMessage } from "../overlayMessages.js";
5
5
  import { FORMAT_FLAGS } from "../../../core/lib/modifierFormats.js";
6
+ import { TextLinkPopover } from "../../../core/components/lexical/TextLinkPopover.js";
6
7
  import { useCurrentItemActions } from "./useRepeatableItemActions.js";
7
8
  import { formatFieldName } from "./ItemFieldsEditor.js";
8
9
  import { c } from "react/compiler-runtime";
@@ -32,25 +33,39 @@ const FORMAT_BUTTONS = [{
32
33
  shortcut: "⌘ I"
33
34
  }];
34
35
  const FieldToolbar = () => {
35
- const $ = c(28);
36
- if ($[0] !== "a34e7a229eb426498bf253b287fe77f34bd074a248fe59749809701f61b66f8c") {
37
- for (let $i = 0; $i < 28; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
38
- $[0] = "a34e7a229eb426498bf253b287fe77f34bd074a248fe59749809701f61b66f8c";
36
+ const $ = c(38);
37
+ if ($[0] !== "8a2b2d9ef81221d4535d05947c07828197adc9a5cdfe31ce4861c6c95ece3e43") {
38
+ for (let $i = 0; $i < 38; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
39
+ $[0] = "8a2b2d9ef81221d4535d05947c07828197adc9a5cdfe31ce4861c6c95ece3e43";
39
40
  }
40
41
  const iframeElement = useSelector(previewStore, _temp);
41
42
  const selection = useSelector(previewStore, _temp2);
42
43
  const isAnySideSheetOpen = useIsPreviewSheetOpen();
43
44
  const [hasSelection, setHasSelection] = React.useState(false);
44
45
  const [activeFormats, setActiveFormats] = React.useState(0);
46
+ const [linkTarget, setLinkTarget] = React.useState(null);
47
+ const [selectedText, setSelectedText] = React.useState("");
48
+ const [linkPopoverOpen, setLinkPopoverOpen] = React.useState(false);
45
49
  let t0;
46
50
  let t1;
47
51
  if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
48
52
  t0 = () => {
49
53
  const handleMessage = (event) => {
50
54
  const data = event.data;
51
- if (!isOverlayMessage(data) || data.type !== "CAMOX_TEXT_SELECTION_STATE") return;
52
- setHasSelection(data.hasSelection);
53
- setActiveFormats(data.activeFormats);
55
+ if (!isOverlayMessage(data)) return;
56
+ if (data.type === "CAMOX_TEXT_SELECTION_STATE") {
57
+ setHasSelection(data.hasSelection);
58
+ setActiveFormats(data.activeFormats);
59
+ setLinkTarget(data.linkTarget);
60
+ setSelectedText(data.selectedText);
61
+ return;
62
+ }
63
+ if (data.type === "CAMOX_OPEN_TEXT_LINK_POPOVER") {
64
+ setHasSelection(true);
65
+ setLinkTarget(data.target);
66
+ setSelectedText(data.text);
67
+ setLinkPopoverOpen(true);
68
+ }
54
69
  };
55
70
  window.addEventListener("message", handleMessage);
56
71
  return () => window.removeEventListener("message", handleMessage);
@@ -75,51 +90,83 @@ const FieldToolbar = () => {
75
90
  $[4] = t2;
76
91
  } else t2 = $[4];
77
92
  const sendFormat = t2;
93
+ let t3;
94
+ if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
95
+ t3 = (open) => {
96
+ setLinkPopoverOpen(open);
97
+ };
98
+ $[5] = t3;
99
+ } else t3 = $[5];
100
+ const handleLinkPopoverOpenChange = t3;
101
+ let t4;
102
+ if ($[6] !== iframeElement?.contentWindow) {
103
+ t4 = (target, text) => {
104
+ iframeElement?.contentWindow?.postMessage({
105
+ type: "CAMOX_TOGGLE_TEXT_LINK",
106
+ target,
107
+ text
108
+ }, "*");
109
+ setLinkPopoverOpen(false);
110
+ };
111
+ $[6] = iframeElement?.contentWindow;
112
+ $[7] = t4;
113
+ } else t4 = $[7];
114
+ const sendTextLink = t4;
115
+ let t5;
116
+ if ($[8] !== sendTextLink) {
117
+ t5 = () => {
118
+ sendTextLink(null);
119
+ };
120
+ $[8] = sendTextLink;
121
+ $[9] = t5;
122
+ } else t5 = $[9];
123
+ const unlinkText = t5;
78
124
  const isOnField = selection?.type === "block-field" || selection?.type === "item-field";
79
125
  const isOnItemField = selection?.type === "item-field";
80
126
  const isVisible = isOnField && !isAnySideSheetOpen;
81
127
  const { canAdd, addItem, canRemove, removeItem, currentItem } = useCurrentItemActions(isOnItemField ? selection.blockId : null, isOnItemField ? selection.itemId : null);
82
- let t3;
83
- if ($[5] !== selection) {
84
- t3 = () => {
128
+ let t6;
129
+ if ($[10] !== selection) {
130
+ t6 = () => {
85
131
  if (!selection) return;
86
132
  previewStore.send({
87
133
  type: "openBlockContentSheet",
88
134
  blockId: selection.blockId
89
135
  });
90
136
  };
91
- $[5] = selection;
92
- $[6] = t3;
93
- } else t3 = $[6];
94
- const handleEditInForm = t3;
95
- const t4 = isVisible ? "opacity-100 translate-y-0" : "opacity-0 pointer-events-none translate-y-2";
96
- let t5;
97
- if ($[7] !== t4) {
98
- t5 = cn("bottom-17 gap-2", t4);
99
- $[7] = t4;
100
- $[8] = t5;
101
- } else t5 = $[8];
102
- let t6;
103
- if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
104
- t6 = formatShortcut({
137
+ $[10] = selection;
138
+ $[11] = t6;
139
+ } else t6 = $[11];
140
+ const handleEditInForm = t6;
141
+ const handleToolbarMouseDown = _temp3;
142
+ const t7 = isVisible ? "opacity-100 translate-y-0" : "opacity-0 pointer-events-none translate-y-2";
143
+ let t8;
144
+ if ($[12] !== t7) {
145
+ t8 = cn("bottom-17 gap-2", t7);
146
+ $[12] = t7;
147
+ $[13] = t8;
148
+ } else t8 = $[13];
149
+ let t9;
150
+ if ($[14] === Symbol.for("react.memo_cache_sentinel")) {
151
+ t9 = formatShortcut({
105
152
  key: "j",
106
153
  withAlt: true
107
154
  });
108
- $[9] = t6;
109
- } else t6 = $[9];
110
- let t7;
111
- if ($[10] !== handleEditInForm) {
112
- t7 = /* @__PURE__ */ jsxs(Button, {
155
+ $[14] = t9;
156
+ } else t9 = $[14];
157
+ let t10;
158
+ if ($[15] !== handleEditInForm) {
159
+ t10 = /* @__PURE__ */ jsxs(Button, {
113
160
  variant: "outline",
114
161
  onClick: handleEditInForm,
115
- children: ["Edit in form", t6]
162
+ children: ["Edit in form", t9]
116
163
  });
117
- $[10] = handleEditInForm;
118
- $[11] = t7;
119
- } else t7 = $[11];
120
- let t8;
121
- if ($[12] !== addItem || $[13] !== canAdd || $[14] !== canRemove || $[15] !== currentItem || $[16] !== isOnItemField || $[17] !== removeItem) {
122
- t8 = isOnItemField && currentItem && (canAdd || canRemove) && /* @__PURE__ */ jsx(Tooltip$1.TooltipProvider, {
164
+ $[15] = handleEditInForm;
165
+ $[16] = t10;
166
+ } else t10 = $[16];
167
+ let t11;
168
+ if ($[17] !== addItem || $[18] !== canAdd || $[19] !== canRemove || $[20] !== currentItem || $[21] !== isOnItemField || $[22] !== removeItem) {
169
+ t11 = isOnItemField && currentItem && (canAdd || canRemove) && /* @__PURE__ */ jsx(Tooltip$1.TooltipProvider, {
123
170
  delay: 0,
124
171
  children: /* @__PURE__ */ jsxs(ButtonGroup, { children: [canAdd && /* @__PURE__ */ jsxs(Tooltip$1.Tooltip, { children: [/* @__PURE__ */ jsx(Tooltip$1.TooltipTrigger, {
125
172
  render: /* @__PURE__ */ jsx(Button, {
@@ -137,18 +184,18 @@ const FieldToolbar = () => {
137
184
  children: /* @__PURE__ */ jsx(CircleMinus, {})
138
185
  }), /* @__PURE__ */ jsxs(Tooltip$1.TooltipContent, { children: ["Remove item from ", formatFieldName(currentItem.fieldName)] })] })] })
139
186
  });
140
- $[12] = addItem;
141
- $[13] = canAdd;
142
- $[14] = canRemove;
143
- $[15] = currentItem;
144
- $[16] = isOnItemField;
145
- $[17] = removeItem;
146
- $[18] = t8;
147
- } else t8 = $[18];
148
- let t9;
149
- if ($[19] !== activeFormats || $[20] !== hasSelection || $[21] !== sendFormat) {
150
- t9 = hasSelection && /* @__PURE__ */ jsx(ButtonGroup, { children: FORMAT_BUTTONS.map((t10) => {
151
- const { key, flag, icon: Icon, label, shortcut } = t10;
187
+ $[17] = addItem;
188
+ $[18] = canAdd;
189
+ $[19] = canRemove;
190
+ $[20] = currentItem;
191
+ $[21] = isOnItemField;
192
+ $[22] = removeItem;
193
+ $[23] = t11;
194
+ } else t11 = $[23];
195
+ let t12;
196
+ if ($[24] !== activeFormats || $[25] !== hasSelection || $[26] !== linkPopoverOpen || $[27] !== linkTarget || $[28] !== selectedText || $[29] !== sendFormat || $[30] !== sendTextLink || $[31] !== unlinkText) {
197
+ t12 = hasSelection && /* @__PURE__ */ jsxs(ButtonGroup, { children: [FORMAT_BUTTONS.map((t13) => {
198
+ const { key, flag, icon: Icon, label, shortcut } = t13;
152
199
  const isActive = !!(activeFormats & flag);
153
200
  return /* @__PURE__ */ jsxs(Tooltip$1.Tooltip, { children: [/* @__PURE__ */ jsx(Tooltip$1.TooltipTrigger, {
154
201
  render: /* @__PURE__ */ jsx(Toggle, {
@@ -163,30 +210,47 @@ const FieldToolbar = () => {
163
210
  " ",
164
211
  /* @__PURE__ */ jsx(Kbd, { children: shortcut })
165
212
  ] })] }, key);
166
- }) });
167
- $[19] = activeFormats;
168
- $[20] = hasSelection;
169
- $[21] = sendFormat;
170
- $[22] = t9;
171
- } else t9 = $[22];
172
- let t10;
173
- if ($[23] !== t5 || $[24] !== t7 || $[25] !== t8 || $[26] !== t9) {
174
- t10 = /* @__PURE__ */ jsxs(FloatingToolbar, {
175
- onMouseDown: _temp3,
176
- className: t5,
213
+ }), /* @__PURE__ */ jsx(TextLinkPopover, {
214
+ open: linkPopoverOpen,
215
+ onOpenChange: handleLinkPopoverOpenChange,
216
+ trigger: /* @__PURE__ */ jsx(Button, {
217
+ variant: "outline",
218
+ size: "icon",
219
+ "aria-label": "Add link"
220
+ }),
221
+ text: selectedText,
222
+ target: linkTarget,
223
+ onSave: sendTextLink,
224
+ onUnlink: unlinkText
225
+ })] });
226
+ $[24] = activeFormats;
227
+ $[25] = hasSelection;
228
+ $[26] = linkPopoverOpen;
229
+ $[27] = linkTarget;
230
+ $[28] = selectedText;
231
+ $[29] = sendFormat;
232
+ $[30] = sendTextLink;
233
+ $[31] = unlinkText;
234
+ $[32] = t12;
235
+ } else t12 = $[32];
236
+ let t13;
237
+ if ($[33] !== t10 || $[34] !== t11 || $[35] !== t12 || $[36] !== t8) {
238
+ t13 = /* @__PURE__ */ jsxs(FloatingToolbar, {
239
+ onMouseDown: handleToolbarMouseDown,
240
+ className: t8,
177
241
  children: [
178
- t7,
179
- t8,
180
- t9
242
+ t10,
243
+ t11,
244
+ t12
181
245
  ]
182
246
  });
183
- $[23] = t5;
184
- $[24] = t7;
185
- $[25] = t8;
186
- $[26] = t9;
187
- $[27] = t10;
188
- } else t10 = $[27];
189
- return t10;
247
+ $[33] = t10;
248
+ $[34] = t11;
249
+ $[35] = t12;
250
+ $[36] = t8;
251
+ $[37] = t13;
252
+ } else t13 = $[37];
253
+ return t13;
190
254
  };
191
255
  function _temp(state) {
192
256
  return state.context.iframeElement;
@@ -194,8 +258,14 @@ function _temp(state) {
194
258
  function _temp2(state_0) {
195
259
  return state_0.context.selection;
196
260
  }
197
- function _temp3(e) {
198
- return e.preventDefault();
261
+ function _temp3(event_0) {
262
+ const target_0 = event_0.target;
263
+ if (!(target_0 instanceof HTMLElement)) {
264
+ event_0.preventDefault();
265
+ return;
266
+ }
267
+ if (target_0.closest("input, select, textarea, button, [role='combobox']")) return;
268
+ event_0.preventDefault();
199
269
  }
200
270
  function _temp4() {
201
271
  return previewStore.send({ type: "selectParent" });
@@ -142,8 +142,10 @@ function camox(options) {
142
142
  "camox > @lexical/react/LexicalComposerContext",
143
143
  "camox > @lexical/react/LexicalContentEditable",
144
144
  "camox > @lexical/react/LexicalHistoryPlugin",
145
+ "camox > @lexical/react/LexicalLinkPlugin",
145
146
  "camox > @lexical/react/LexicalOnChangePlugin",
146
147
  "camox > @lexical/react/LexicalRichTextPlugin",
148
+ "camox > @lexical/link",
147
149
  "camox > @orpc/client",
148
150
  "camox > @orpc/client/fetch",
149
151
  "camox > @orpc/tanstack-query",