@byline/richtext-lexical 6.3.0 → 6.5.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.
Files changed (62) hide show
  1. package/dist/field/context/shared-on-change-context.js +1 -1
  2. package/dist/field/extensions/code-highlight/code-highlight-extension.js +2 -1
  3. package/dist/field/extensions/inline-image/inline-image-extension.js +4 -2
  4. package/dist/field/extensions/inline-image/inline-image-modal.css +67 -0
  5. package/dist/field/extensions/inline-image/inline-image-modal.js +173 -98
  6. package/dist/field/extensions/inline-image/inline-image-node-component.css +37 -0
  7. package/dist/field/extensions/inline-image/inline-image-node-component.d.ts +3 -1
  8. package/dist/field/extensions/inline-image/inline-image-node-component.js +39 -1
  9. package/dist/field/extensions/inline-image/inline-image-node.d.ts +13 -2
  10. package/dist/field/extensions/inline-image/inline-image-node.js +63 -13
  11. package/dist/field/extensions/inline-image/node-types.d.ts +19 -0
  12. package/dist/field/extensions/inline-image/populate.d.ts +22 -0
  13. package/dist/field/extensions/inline-image/populate.js +19 -1
  14. package/dist/field/extensions/inline-image/types.d.ts +7 -0
  15. package/dist/field/extensions/link/link-href.d.ts +30 -0
  16. package/dist/field/extensions/link/link-href.js +14 -0
  17. package/dist/field/extensions/link/link-modal.js +96 -288
  18. package/dist/field/extensions/link/link-target-fields.css +59 -0
  19. package/dist/field/extensions/link/link-target-fields.d.ts +38 -0
  20. package/dist/field/extensions/link/link-target-fields.js +227 -0
  21. package/dist/field/extensions/link/link-target-state.d.ts +54 -0
  22. package/dist/field/extensions/link/link-target-state.js +70 -0
  23. package/dist/field/extensions/link/populate.d.ts +15 -1
  24. package/dist/field/extensions/link/populate.js +55 -50
  25. package/dist/field/markdown/lexical-to-markdown.js +6 -1
  26. package/dist/field/plugins/toolbar-plugin/index.js +2 -1
  27. package/dist/field/utils/getSelectedNode.js +1 -1
  28. package/dist/field/utils/rect.js +1 -2
  29. package/dist/richtext-field.js +1 -1
  30. package/dist/server.d.ts +2 -2
  31. package/dist/server.js +4 -2
  32. package/dist/validate/createEmptyEditorState.d.ts +2 -2
  33. package/dist/validate/hasText.d.ts +2 -2
  34. package/package.json +27 -26
  35. package/src/field/extensions/code-highlight/code-highlight-extension.ts +8 -1
  36. package/src/field/extensions/inline-image/inline-image-extension.tsx +5 -0
  37. package/src/field/extensions/inline-image/inline-image-modal.css +100 -0
  38. package/src/field/extensions/inline-image/inline-image-modal.tsx +178 -85
  39. package/src/field/extensions/inline-image/inline-image-node-component.css +44 -0
  40. package/src/field/extensions/inline-image/inline-image-node-component.tsx +52 -0
  41. package/src/field/extensions/inline-image/inline-image-node.test.tsx +331 -0
  42. package/src/field/extensions/inline-image/inline-image-node.tsx +108 -11
  43. package/src/field/extensions/inline-image/node-types.ts +19 -0
  44. package/src/field/extensions/inline-image/populate-link.test.node.ts +320 -0
  45. package/src/field/extensions/inline-image/populate.ts +44 -0
  46. package/src/field/extensions/inline-image/types.ts +7 -0
  47. package/src/field/extensions/link/link-href.test.node.ts +78 -0
  48. package/src/field/extensions/link/link-href.ts +52 -0
  49. package/src/field/extensions/link/link-modal.tsx +86 -325
  50. package/src/field/extensions/link/link-target-fields.css +81 -0
  51. package/src/field/extensions/link/link-target-fields.tsx +275 -0
  52. package/src/field/extensions/link/link-target-state.test.node.ts +197 -0
  53. package/src/field/extensions/link/link-target-state.ts +158 -0
  54. package/src/field/extensions/link/populate.ts +94 -67
  55. package/src/field/markdown/lexical-to-markdown.test.node.ts +68 -0
  56. package/src/field/markdown/lexical-to-markdown.ts +10 -1
  57. package/src/field/plugins/toolbar-plugin/index.tsx +7 -4
  58. package/src/richtext-field.tsx +1 -1
  59. package/src/server.ts +11 -5
  60. package/src/validate/createEmptyEditorState.ts +2 -7
  61. package/src/validate/hasText.test.ts +1 -2
  62. package/src/validate/hasText.ts +2 -9
@@ -16,6 +16,6 @@ const SharedOnChangeContext = ({ children, onChange })=>{
16
16
  const useSharedOnChange = ()=>{
17
17
  const context = useContext(Context);
18
18
  if (void 0 !== context) return context;
19
- throw new Error('useSharedOnChange must be used within an SharedOnChangeProvider');
19
+ else throw new Error('useSharedOnChange must be used within an SharedOnChangeProvider');
20
20
  };
21
21
  export { SharedOnChangeContext, useSharedOnChange };
@@ -1,4 +1,5 @@
1
- import { CodeHighlightNode, CodeNode, registerCodeHighlighting } from "@lexical/code";
1
+ import { CodeHighlightNode, CodeNode } from "@lexical/code";
2
+ import { registerCodeHighlighting } from "@lexical/code-prism";
2
3
  import { defineExtension } from "lexical";
3
4
  const CodeHighlightExtension = defineExtension({
4
5
  name: '@byline/richtext-lexical/CodeHighlight',
@@ -30,7 +30,8 @@ function readNodeAsInitialData(editor, nodeKey) {
30
30
  src: node.getSrc(),
31
31
  altText: node.getAltText(),
32
32
  position: node.getPosition(),
33
- showCaption: node.getShowCaption()
33
+ showCaption: node.getShowCaption(),
34
+ link: node.getLink()
34
35
  };
35
36
  });
36
37
  return data;
@@ -87,7 +88,8 @@ function InlineImagePlugin() {
87
88
  position: data.position,
88
89
  width: data.width,
89
90
  height: data.height,
90
- showCaption: data.showCaption
91
+ showCaption: data.showCaption,
92
+ link: data.link
91
93
  };
92
94
  if ('edit' === modalState.mode && null != modalState.nodeKey) editor.dispatchCommand(UPDATE_INLINE_IMAGE_COMMAND, {
93
95
  nodeKey: modalState.nodeKey,
@@ -49,3 +49,70 @@
49
49
  overflow: hidden;
50
50
  }
51
51
 
52
+ .inline-image-modal-tabs.byline-tabs-root {
53
+ border: none;
54
+ border-radius: 0;
55
+ gap: 0;
56
+ padding: 0;
57
+ }
58
+
59
+ .inline-image-modal-tabs .byline-tabs-list {
60
+ border: none;
61
+ border-bottom: 1px solid var(--border-color);
62
+ border-radius: 0;
63
+ gap: 1.25rem;
64
+ padding: 0;
65
+ }
66
+
67
+ .inline-image-modal-tabs .byline-tabs-trigger {
68
+ width: auto;
69
+ color: var(--gray-500);
70
+ font-size: 1rem;
71
+ font-weight: var(--font-weight-medium);
72
+ cursor: pointer;
73
+ background: none;
74
+ border: none;
75
+ border-bottom: 2px solid #0000;
76
+ flex: none;
77
+ justify-content: flex-start;
78
+ margin-bottom: -1px;
79
+ padding: .5rem 0;
80
+ transition: color .15s, border-color .15s;
81
+ display: inline-flex;
82
+ position: relative;
83
+ }
84
+
85
+ .inline-image-modal-tabs .byline-tabs-trigger:hover {
86
+ color: var(--gray-800);
87
+ border-bottom-color: var(--gray-300);
88
+ }
89
+
90
+ .inline-image-modal-tabs .byline-tabs-trigger:focus-visible {
91
+ box-shadow: inset 0 0 0 2px var(--blue-500);
92
+ border-radius: var(--radius-sm, 3px);
93
+ outline: none;
94
+ }
95
+
96
+ .inline-image-modal-tabs .byline-tabs-trigger[data-active], .inline-image-modal-tabs .byline-tabs-trigger[data-active]:hover {
97
+ color: var(--primary-600);
98
+ border-bottom-color: var(--primary-400);
99
+ }
100
+
101
+ .inline-image-modal-tabs .inline-image-modal-link-panel {
102
+ min-height: 20rem;
103
+ }
104
+
105
+ :is([data-theme="dark"], .dark) .inline-image-modal-tabs .byline-tabs-trigger {
106
+ color: var(--gray-400);
107
+ }
108
+
109
+ :is([data-theme="dark"], .dark) .inline-image-modal-tabs .byline-tabs-trigger:hover {
110
+ color: var(--gray-200);
111
+ border-bottom-color: var(--gray-600);
112
+ }
113
+
114
+ :is([data-theme="dark"], .dark) .inline-image-modal-tabs .byline-tabs-trigger[data-active], :is([data-theme="dark"], .dark) .inline-image-modal-tabs .byline-tabs-trigger[data-active]:hover {
115
+ color: var(--primary-200);
116
+ border-bottom-color: var(--primary-400);
117
+ }
118
+
@@ -2,27 +2,34 @@
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useMemo, useState } from "react";
4
4
  import { RelationPicker } from "@byline/admin/react";
5
- import { getCollectionDefinition, isSingleton } from "@byline/core";
6
- import { Button, Checkbox, CloseIcon, ErrorText, IconButton, Input, Label, Modal, RadioGroup, RadioGroupItem } from "@byline/ui/react";
5
+ import { getAdminConfig, getCollectionDefinition, isSingleton } from "@byline/core";
6
+ import { Button, Checkbox, CloseIcon, ErrorText, IconButton, Input, Label, Modal, RadioGroup, RadioGroupItem, Tabs } from "@byline/ui/react";
7
7
  import { useModalFormState } from "../../shared/useModalFormState.js";
8
+ import { LinkTargetFields } from "../link/link-target-fields.js";
9
+ import { fromLinkAttributes, toLinkAttributes, validateLinkTarget } from "../link/link-target-state.js";
8
10
  import { isAltTextValid, positionOptions } from "./fields.js";
9
11
  import { deriveImageSizes, getPreferredSize } from "./utils.js";
10
12
  import "./inline-image-modal.css";
11
- function emptyState() {
13
+ const LINK_TARGET_OPTIONS = {
14
+ allowNone: true
15
+ };
16
+ function emptyState(linkable) {
12
17
  return {
13
18
  documentRelation: null,
14
19
  altText: '',
15
20
  position: 'full',
16
- showCaption: false
21
+ showCaption: false,
22
+ target: fromLinkAttributes(void 0, linkable, LINK_TARGET_OPTIONS)
17
23
  };
18
24
  }
19
- function fromInlineImageData(data) {
20
- if (!data) return emptyState();
25
+ function fromInlineImageData(data, linkable) {
26
+ if (!data) return emptyState(linkable);
21
27
  return {
22
28
  documentRelation: data.documentRelation ?? null,
23
29
  altText: data.altText ?? '',
24
30
  position: data.position ?? 'full',
25
- showCaption: data.showCaption ?? false
31
+ showCaption: data.showCaption ?? false,
32
+ target: fromLinkAttributes(data.link, linkable, LINK_TARGET_OPTIONS)
26
33
  };
27
34
  }
28
35
  const LABEL_FIELD_KEYS = [
@@ -45,9 +52,14 @@ const InlineImageModal = ({ isOpen, collection, data: dataFromProps, onSubmit, o
45
52
  const [pickerOpen, setPickerOpen] = useState(false);
46
53
  const [altError, setAltError] = useState(null);
47
54
  const [imageError, setImageError] = useState(null);
48
- const [state, setState] = useModalFormState(isOpen, ()=>fromInlineImageData(dataFromProps), ()=>{
55
+ const [linkError, setLinkError] = useState(null);
56
+ const [activeTab, setActiveTab] = useState('image');
57
+ const linkable = useMemo(()=>getAdminConfig().collections.filter((collection)=>!isSingleton(collection) && true === collection.linksInEditor), []);
58
+ const [state, setState] = useModalFormState(isOpen, ()=>fromInlineImageData(dataFromProps, linkable), ()=>{
49
59
  setAltError(null);
50
60
  setImageError(null);
61
+ setLinkError(null);
62
+ setActiveTab('image');
51
63
  });
52
64
  const targetDefinition = getCollectionDefinition(collection);
53
65
  const targetDef = null == targetDefinition || isSingleton(targetDefinition) ? null : targetDefinition;
@@ -99,8 +111,22 @@ const InlineImageModal = ({ isOpen, collection, data: dataFromProps, onSubmit, o
99
111
  setImageError(null);
100
112
  };
101
113
  const handleSave = ()=>{
102
- if (!state.documentRelation || !pickedImage) return void setImageError('Pick an image');
103
- if (!isAltTextValid(state.altText)) return void setAltError('Alt text is required');
114
+ if (!state.documentRelation || !pickedImage) {
115
+ setImageError('Pick an image');
116
+ setActiveTab('image');
117
+ return;
118
+ }
119
+ if (!isAltTextValid(state.altText)) {
120
+ setAltError('Alt text is required');
121
+ setActiveTab('image');
122
+ return;
123
+ }
124
+ const linkProblem = validateLinkTarget(state.target);
125
+ if (null != linkProblem) {
126
+ setLinkError(linkProblem);
127
+ setActiveTab('link');
128
+ return;
129
+ }
104
130
  const preferred = getPreferredSize(state.position, pickedImage);
105
131
  const data = {
106
132
  documentRelation: state.documentRelation,
@@ -109,7 +135,8 @@ const InlineImageModal = ({ isOpen, collection, data: dataFromProps, onSubmit, o
109
135
  position: state.position,
110
136
  width: preferred?.width,
111
137
  height: preferred?.height,
112
- showCaption: state.showCaption
138
+ showCaption: state.showCaption,
139
+ link: toLinkAttributes(state.target)
113
140
  };
114
141
  onSubmit(data);
115
142
  onClose();
@@ -147,106 +174,154 @@ const InlineImageModal = ({ isOpen, collection, data: dataFromProps, onSubmit, o
147
174
  ]
148
175
  }),
149
176
  /*#__PURE__*/ jsx(Modal.Content, {
150
- children: /*#__PURE__*/ jsxs("div", {
151
- className: "flex flex-col gap-4",
177
+ children: /*#__PURE__*/ jsxs(Tabs, {
178
+ className: "inline-image-modal-tabs",
179
+ value: activeTab,
180
+ onValueChange: (value)=>setActiveTab(value),
152
181
  children: [
153
- /*#__PURE__*/ jsxs("div", {
154
- className: "flex flex-col gap-2",
182
+ /*#__PURE__*/ jsxs(Tabs.List, {
183
+ className: "mb-4",
155
184
  children: [
156
- /*#__PURE__*/ jsx("span", {
157
- className: "text-sm font-medium",
185
+ /*#__PURE__*/ jsx(Tabs.Trigger, {
186
+ value: "image",
158
187
  children: "Image"
159
188
  }),
160
- /*#__PURE__*/ jsxs("div", {
161
- className: "inline-image-modal-picker",
162
- children: [
163
- pickedThumbUrl ? /*#__PURE__*/ jsx("img", {
164
- src: pickedThumbUrl,
165
- alt: pickedTitle ?? '',
166
- className: "inline-image-modal-thumb"
167
- }) : /*#__PURE__*/ jsx("div", {
168
- className: "inline-image-modal-thumb-placeholder",
169
- children: "—"
170
- }),
171
- /*#__PURE__*/ jsxs("div", {
172
- className: "inline-image-modal-picker-details",
173
- children: [
174
- pickedTitle && /*#__PURE__*/ jsx("span", {
175
- className: "inline-image-modal-title",
176
- children: pickedTitle
177
- }),
178
- /*#__PURE__*/ jsx(Button, {
179
- size: "sm",
180
- className: "inline-image-modal-change-btn",
181
- variant: "outlined",
182
- intent: "noeffect",
183
- type: "button",
184
- onClick: ()=>setPickerOpen(true),
185
- children: state.documentRelation ? 'Change image…' : `Pick ${targetDef?.labels.singular ?? 'image'}…`
186
- })
187
- ]
188
- })
189
- ]
190
- }),
191
- imageError && /*#__PURE__*/ jsx(ErrorText, {
192
- id: "image-error",
193
- text: imageError
189
+ /*#__PURE__*/ jsx(Tabs.Trigger, {
190
+ value: "link",
191
+ children: "Link"
194
192
  })
195
193
  ]
196
194
  }),
197
- /*#__PURE__*/ jsx(Input, {
198
- id: "inline-image-alt",
199
- name: "altText",
200
- label: "Alt text",
201
- required: true,
202
- placeholder: "Describe the image for screen readers",
203
- value: state.altText,
204
- error: null != altError,
205
- errorText: altError ?? void 0,
206
- onChange: (e)=>{
207
- setAltError(null);
208
- setState((s)=>({
209
- ...s,
210
- altText: e.target.value
211
- }));
212
- }
195
+ /*#__PURE__*/ jsx(Tabs.Content, {
196
+ value: "image",
197
+ children: /*#__PURE__*/ jsxs("div", {
198
+ className: "flex flex-col gap-4",
199
+ children: [
200
+ /*#__PURE__*/ jsxs("div", {
201
+ className: "flex flex-col gap-2",
202
+ children: [
203
+ /*#__PURE__*/ jsx("span", {
204
+ className: "text-sm font-medium",
205
+ children: "Image"
206
+ }),
207
+ /*#__PURE__*/ jsxs("div", {
208
+ className: "inline-image-modal-picker",
209
+ children: [
210
+ pickedThumbUrl ? /*#__PURE__*/ jsx("img", {
211
+ src: pickedThumbUrl,
212
+ alt: pickedTitle ?? '',
213
+ className: "inline-image-modal-thumb"
214
+ }) : /*#__PURE__*/ jsx("div", {
215
+ className: "inline-image-modal-thumb-placeholder",
216
+ children: "—"
217
+ }),
218
+ /*#__PURE__*/ jsxs("div", {
219
+ className: "inline-image-modal-picker-details",
220
+ children: [
221
+ pickedTitle && /*#__PURE__*/ jsx("span", {
222
+ className: "inline-image-modal-title",
223
+ children: pickedTitle
224
+ }),
225
+ /*#__PURE__*/ jsx(Button, {
226
+ size: "sm",
227
+ className: "inline-image-modal-change-btn",
228
+ variant: "outlined",
229
+ intent: "noeffect",
230
+ type: "button",
231
+ onClick: ()=>setPickerOpen(true),
232
+ children: state.documentRelation ? 'Change image…' : `Pick ${targetDef?.labels.singular ?? 'image'}…`
233
+ })
234
+ ]
235
+ })
236
+ ]
237
+ }),
238
+ imageError && /*#__PURE__*/ jsx(ErrorText, {
239
+ id: "image-error",
240
+ text: imageError
241
+ })
242
+ ]
243
+ }),
244
+ /*#__PURE__*/ jsx(Input, {
245
+ id: "inline-image-alt",
246
+ name: "altText",
247
+ label: "Alt text",
248
+ required: true,
249
+ placeholder: "Describe the image for screen readers",
250
+ value: state.altText,
251
+ error: null != altError,
252
+ errorText: altError ?? void 0,
253
+ onChange: (e)=>{
254
+ setAltError(null);
255
+ setState((s)=>({
256
+ ...s,
257
+ altText: e.target.value
258
+ }));
259
+ }
260
+ }),
261
+ /*#__PURE__*/ jsxs("div", {
262
+ className: "flex flex-col gap-2 mb-3",
263
+ children: [
264
+ /*#__PURE__*/ jsx(Label, {
265
+ htmlFor: "inline-image-position",
266
+ id: "inline-image-position-label",
267
+ className: "text-sm font-medium",
268
+ label: "Position"
269
+ }),
270
+ /*#__PURE__*/ jsx(RadioGroup, {
271
+ id: "inline-image-position",
272
+ name: "position",
273
+ "aria-labelledby": "inline-image-position-label",
274
+ direction: "row",
275
+ value: state.position ?? 'full',
276
+ onValueChange: (value)=>setState((s)=>({
277
+ ...s,
278
+ position: value
279
+ })),
280
+ children: positionOptions.map((opt)=>/*#__PURE__*/ jsx(RadioGroupItem, {
281
+ id: `inline-image-position-${opt.value}`,
282
+ value: String(opt.value),
283
+ label: opt.label
284
+ }, String(opt.value)))
285
+ })
286
+ ]
287
+ }),
288
+ /*#__PURE__*/ jsx(Checkbox, {
289
+ id: "inline-image-caption",
290
+ name: "showCaption",
291
+ label: "Show caption",
292
+ checked: state.showCaption,
293
+ onCheckedChange: (checked)=>setState((s)=>({
294
+ ...s,
295
+ showCaption: true === checked
296
+ }))
297
+ })
298
+ ]
299
+ })
213
300
  }),
214
- /*#__PURE__*/ jsxs("div", {
215
- className: "flex flex-col gap-2 mb-3",
301
+ /*#__PURE__*/ jsxs(Tabs.Content, {
302
+ value: "link",
303
+ className: "inline-image-modal-link-panel",
216
304
  children: [
217
- /*#__PURE__*/ jsx(Label, {
218
- htmlFor: "inline-image-position",
219
- id: "inline-image-position-label",
220
- className: "text-sm font-medium",
221
- label: "Position"
305
+ /*#__PURE__*/ jsx("p", {
306
+ className: "mt-0 mb-3 text-sm text-accent-400",
307
+ children: "Give the image a click target. Leave this as “No link” for a plain image."
222
308
  }),
223
- /*#__PURE__*/ jsx(RadioGroup, {
224
- id: "inline-image-position",
225
- name: "position",
226
- "aria-labelledby": "inline-image-position-label",
227
- direction: "row",
228
- value: state.position ?? 'full',
229
- onValueChange: (value)=>setState((s)=>({
309
+ /*#__PURE__*/ jsx(LinkTargetFields, {
310
+ idPrefix: "inline-image-link",
311
+ state: state.target,
312
+ onChange: (target)=>{
313
+ setLinkError(null);
314
+ setState((s)=>({
230
315
  ...s,
231
- position: value
232
- })),
233
- children: positionOptions.map((opt)=>/*#__PURE__*/ jsx(RadioGroupItem, {
234
- id: `inline-image-position-${opt.value}`,
235
- value: String(opt.value),
236
- label: opt.label
237
- }, String(opt.value)))
316
+ target
317
+ }));
318
+ },
319
+ linkable: linkable,
320
+ allowNone: true,
321
+ error: linkError,
322
+ legend: "Link to"
238
323
  })
239
324
  ]
240
- }),
241
- /*#__PURE__*/ jsx(Checkbox, {
242
- id: "inline-image-caption",
243
- name: "showCaption",
244
- label: "Show caption",
245
- checked: state.showCaption,
246
- onCheckedChange: (checked)=>setState((s)=>({
247
- ...s,
248
- showCaption: true === checked
249
- }))
250
325
  })
251
326
  ]
252
327
  })
@@ -10,6 +10,14 @@
10
10
  max-width: 100%;
11
11
  }
12
12
 
13
+ .LexicalEditor__inline-image.has-link img {
14
+ cursor: pointer;
15
+ }
16
+
17
+ .LexicalEditor__inline-image.has-link img.focused.draggable {
18
+ cursor: grab;
19
+ }
20
+
13
21
  .LexicalEditor__inline-image img.focused {
14
22
  outline: 2px solid #3c84f4;
15
23
  }
@@ -65,6 +73,35 @@
65
73
  background-color: #3c84f480;
66
74
  }
67
75
 
76
+ .LexicalEditor__inline-image .image-link-badge {
77
+ color: #fff;
78
+ cursor: pointer;
79
+ -webkit-user-select: none;
80
+ user-select: none;
81
+ background-color: #00000080;
82
+ border: 1px solid #ffffff4d;
83
+ border-radius: 5px;
84
+ align-items: center;
85
+ gap: 4px;
86
+ max-width: calc(100% - 90px);
87
+ padding: 1px 6px;
88
+ font-size: .8rem;
89
+ display: flex;
90
+ position: absolute;
91
+ top: 12px;
92
+ left: 12px;
93
+ }
94
+
95
+ .LexicalEditor__inline-image .image-link-badge:hover {
96
+ background-color: #3c84f480;
97
+ }
98
+
99
+ .LexicalEditor__inline-image .image-link-badge__label {
100
+ text-overflow: ellipsis;
101
+ white-space: nowrap;
102
+ overflow: hidden;
103
+ }
104
+
68
105
  .LexicalEditor__inline-image .InlineImageNode__caption_container {
69
106
  color: #000;
70
107
  background-color: #f4f4f4;
@@ -8,9 +8,10 @@
8
8
  import type * as React from 'react';
9
9
  import type { LexicalEditor, NodeKey } from 'lexical';
10
10
  import type { DocumentRelation } from '../../nodes/document-relation.js';
11
+ import type { LinkAttributes } from '../link/index.js';
11
12
  import type { Position } from './node-types.js';
12
13
  import './inline-image-node-component.css';
13
- export default function InlineImageComponent({ relation, src, position, altText, width, height, showCaption, caption, nodeKey, }: {
14
+ export default function InlineImageComponent({ relation, src, position, altText, width, height, showCaption, caption, link, nodeKey, }: {
14
15
  relation: DocumentRelation;
15
16
  src: string;
16
17
  position: Position;
@@ -19,5 +20,6 @@ export default function InlineImageComponent({ relation, src, position, altText,
19
20
  width?: number | string;
20
21
  showCaption: boolean;
21
22
  caption: LexicalEditor;
23
+ link?: LinkAttributes;
22
24
  nodeKey: NodeKey;
23
25
  }): React.JSX.Element;
@@ -17,6 +17,7 @@ import placeholder_inline from "../../ui/placeholder-inline.js";
17
17
  import { FloatingTextFormatToolbarPlugin } from "../floating-text-format/index.js";
18
18
  import { FloatingLinkEditorPlugin } from "../link/floating-link-editor.js";
19
19
  import { LinkPlugin } from "../link/link-extension.js";
20
+ import { resolveLinkHref } from "../link/link-href.js";
20
21
  import { OPEN_INLINE_IMAGE_MODAL_COMMAND } from "./inline-image-extension.js";
21
22
  import { $isInlineImageNode } from "./inline-image-node.js";
22
23
  import "./inline-image-node-component.css";
@@ -49,7 +50,7 @@ function LazyImage({ id, collection, src, position, altText, className, imageRef
49
50
  draggable: "false"
50
51
  });
51
52
  }
52
- function InlineImageComponent({ relation, src, position, altText, width, height, showCaption, caption, nodeKey }) {
53
+ function InlineImageComponent({ relation, src, position, altText, width, height, showCaption, caption, link, nodeKey }) {
53
54
  const { targetDocumentId: id, targetCollectionPath: collection } = relation;
54
55
  const [editor] = useLexicalComposerContext();
55
56
  const { onChange } = useSharedOnChange();
@@ -59,6 +60,9 @@ function InlineImageComponent({ relation, src, position, altText, width, height,
59
60
  const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey);
60
61
  const [selection, setSelection] = useState(null);
61
62
  const activeEditorRef = useRef(null);
63
+ const linkHref = resolveLinkHref(link);
64
+ const linkHrefRef = useRef(linkHref);
65
+ linkHrefRef.current = linkHref;
62
66
  const onDelete = useCallback((payload)=>{
63
67
  if (isSelected && $isNodeSelection($getSelection())) {
64
68
  const event = payload;
@@ -120,6 +124,12 @@ function InlineImageComponent({ relation, src, position, altText, width, height,
120
124
  }, COMMAND_PRIORITY_LOW), editor.registerCommand(CLICK_COMMAND, (payload)=>{
121
125
  const event = payload;
122
126
  if (event.target === imageRef.current) {
127
+ const href = linkHrefRef.current;
128
+ if (null != href && (event.metaKey || event.ctrlKey)) {
129
+ event.preventDefault();
130
+ window.open(href, '_blank', 'noopener,noreferrer');
131
+ return true;
132
+ }
123
133
  if (event.shiftKey) setSelected(!isSelected);
124
134
  else {
125
135
  clearSelection();
@@ -150,6 +160,18 @@ function InlineImageComponent({ relation, src, position, altText, width, height,
150
160
  ]);
151
161
  const draggable = isSelected && $isNodeSelection(selection);
152
162
  const isFocused = isSelected;
163
+ const linkBadgeLabel = (()=>{
164
+ if (null == link) return '';
165
+ if ('internal' === link.linkType) {
166
+ if (link.document?._resolved === false) return 'Missing target';
167
+ return link.document?.title ?? link.targetCollectionPath;
168
+ }
169
+ try {
170
+ return new URL(link.url ?? '').hostname;
171
+ } catch {
172
+ return link.url ?? '';
173
+ }
174
+ })();
153
175
  const handleToggleModal = ()=>{
154
176
  editor.dispatchCommand(OPEN_INLINE_IMAGE_MODAL_COMMAND, {
155
177
  nodeKey
@@ -173,6 +195,22 @@ function InlineImageComponent({ relation, src, position, altText, width, height,
173
195
  onClick: handleToggleModal,
174
196
  children: "Edit"
175
197
  }),
198
+ null != link && /*#__PURE__*/ jsxs("button", {
199
+ type: "button",
200
+ className: "image-link-badge",
201
+ title: null != linkHref ? `Links to ${linkHref} — ⌘/Ctrl-click the image to open` : 'Link target could not be resolved',
202
+ onClick: handleToggleModal,
203
+ children: [
204
+ /*#__PURE__*/ jsx("span", {
205
+ "aria-hidden": "true",
206
+ children: "\uD83D\uDD17"
207
+ }),
208
+ /*#__PURE__*/ jsx("span", {
209
+ className: "image-link-badge__label",
210
+ children: linkBadgeLabel
211
+ })
212
+ ]
213
+ }),
176
214
  /*#__PURE__*/ jsx(LazyImage, {
177
215
  id: id,
178
216
  collection: collection,
@@ -9,6 +9,7 @@ import * as React from 'react';
9
9
  import type { DOMConversionMap, DOMExportOutput, EditorConfig, LexicalEditor, LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical';
10
10
  import { DecoratorNode } from 'lexical';
11
11
  import type { DocumentRelation } from '../../nodes/document-relation.js';
12
+ import type { LinkAttributes } from '../link/index.js';
12
13
  import type { InlineImageAttributes, Position, SerializedInlineImageNode } from './node-types.js';
13
14
  export declare class InlineImageNode extends DecoratorNode<React.JSX.Element> {
14
15
  __relation: DocumentRelation;
@@ -19,11 +20,12 @@ export declare class InlineImageNode extends DecoratorNode<React.JSX.Element> {
19
20
  __height: number | string | undefined;
20
21
  __showCaption: boolean;
21
22
  __caption: LexicalEditor;
23
+ __link: LinkAttributes | undefined;
22
24
  static getType(): string;
23
25
  static clone(node: InlineImageNode): InlineImageNode;
24
26
  static importJSON(serializedNode: SerializedLexicalNode): InlineImageNode;
25
27
  static importDOM(): DOMConversionMap | null;
26
- constructor(relation: DocumentRelation, src: string, position: Position, altText?: string, width?: number | string, height?: number | string, showCaption?: boolean, caption?: LexicalEditor, key?: NodeKey);
28
+ constructor(relation: DocumentRelation, src: string, position: Position, altText?: string, width?: number | string, height?: number | string, showCaption?: boolean, caption?: LexicalEditor, link?: LinkAttributes, key?: NodeKey);
27
29
  exportDOM(): DOMExportOutput;
28
30
  exportJSON(): SerializedInlineImageNode;
29
31
  getRelation(): DocumentRelation;
@@ -33,12 +35,21 @@ export declare class InlineImageNode extends DecoratorNode<React.JSX.Element> {
33
35
  setWidthAndHeight(width: number | string, height: number | string): void;
34
36
  getShowCaption(): boolean;
35
37
  setShowCaption(showCaption: boolean): void;
38
+ getLink(): LinkAttributes | undefined;
39
+ setLink(link: LinkAttributes | undefined): void;
36
40
  getPosition(): Position;
37
41
  setPosition(position: Position): void;
38
42
  update(payload: InlineImageAttributes): void;
43
+ /**
44
+ * `has-link` drives the editor's cursor affordance — see
45
+ * `inline-image-node-component.css`. It is a class rather than a
46
+ * decorator-side style so the hover state is correct before React has
47
+ * hydrated the decorator.
48
+ */
49
+ private buildClassName;
39
50
  createDOM(config: EditorConfig): HTMLElement;
40
51
  updateDOM(prevNode: InlineImageNode, dom: HTMLElement, config: EditorConfig): boolean;
41
52
  decorate(): React.JSX.Element;
42
53
  }
43
- export declare function $createInlineImageNode({ targetDocumentId, targetCollectionId, targetCollectionPath, document, src, position, altText, height, width, showCaption, caption, key, }: InlineImageAttributes): InlineImageNode;
54
+ export declare function $createInlineImageNode({ targetDocumentId, targetCollectionId, targetCollectionPath, document, src, position, altText, height, width, showCaption, caption, link, key, }: InlineImageAttributes): InlineImageNode;
44
55
  export declare function $isInlineImageNode(node: LexicalNode | null | undefined): node is InlineImageNode;