@byline/richtext-lexical 6.4.0 → 6.6.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.
- package/dist/field/extensions/code-highlight/code-highlight-extension.js +2 -1
- package/dist/field/extensions/inline-image/inline-image-extension.js +4 -2
- package/dist/field/extensions/inline-image/inline-image-modal.css +67 -0
- package/dist/field/extensions/inline-image/inline-image-modal.js +173 -98
- package/dist/field/extensions/inline-image/inline-image-node-component.css +37 -0
- package/dist/field/extensions/inline-image/inline-image-node-component.d.ts +3 -1
- package/dist/field/extensions/inline-image/inline-image-node-component.js +39 -1
- package/dist/field/extensions/inline-image/inline-image-node.d.ts +13 -2
- package/dist/field/extensions/inline-image/inline-image-node.js +63 -13
- package/dist/field/extensions/inline-image/node-types.d.ts +19 -0
- package/dist/field/extensions/inline-image/populate.d.ts +22 -0
- package/dist/field/extensions/inline-image/populate.js +19 -1
- package/dist/field/extensions/inline-image/types.d.ts +7 -0
- package/dist/field/extensions/link/link-href.d.ts +30 -0
- package/dist/field/extensions/link/link-href.js +14 -0
- package/dist/field/extensions/link/link-modal.js +96 -288
- package/dist/field/extensions/link/link-target-fields.css +59 -0
- package/dist/field/extensions/link/link-target-fields.d.ts +38 -0
- package/dist/field/extensions/link/link-target-fields.js +227 -0
- package/dist/field/extensions/link/link-target-state.d.ts +54 -0
- package/dist/field/extensions/link/link-target-state.js +70 -0
- package/dist/field/extensions/link/populate.d.ts +15 -1
- package/dist/field/extensions/link/populate.js +55 -50
- package/dist/field/markdown/lexical-to-markdown.js +6 -1
- package/dist/field/plugins/toolbar-plugin/index.js +2 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +4 -2
- package/dist/validate/createEmptyEditorState.d.ts +2 -2
- package/dist/validate/hasText.d.ts +2 -2
- package/package.json +25 -24
- package/src/field/extensions/code-highlight/code-highlight-extension.ts +8 -1
- package/src/field/extensions/inline-image/inline-image-extension.tsx +5 -0
- package/src/field/extensions/inline-image/inline-image-modal.css +100 -0
- package/src/field/extensions/inline-image/inline-image-modal.tsx +178 -85
- package/src/field/extensions/inline-image/inline-image-node-component.css +44 -0
- package/src/field/extensions/inline-image/inline-image-node-component.tsx +52 -0
- package/src/field/extensions/inline-image/inline-image-node.test.tsx +331 -0
- package/src/field/extensions/inline-image/inline-image-node.tsx +108 -11
- package/src/field/extensions/inline-image/node-types.ts +19 -0
- package/src/field/extensions/inline-image/populate-link.test.node.ts +320 -0
- package/src/field/extensions/inline-image/populate.ts +44 -0
- package/src/field/extensions/inline-image/types.ts +7 -0
- package/src/field/extensions/link/link-href.test.node.ts +78 -0
- package/src/field/extensions/link/link-href.ts +52 -0
- package/src/field/extensions/link/link-modal.tsx +86 -325
- package/src/field/extensions/link/link-target-fields.css +81 -0
- package/src/field/extensions/link/link-target-fields.tsx +275 -0
- package/src/field/extensions/link/link-target-state.test.node.ts +197 -0
- package/src/field/extensions/link/link-target-state.ts +158 -0
- package/src/field/extensions/link/populate.ts +94 -67
- package/src/field/markdown/lexical-to-markdown.test.node.ts +68 -0
- package/src/field/markdown/lexical-to-markdown.ts +10 -1
- package/src/field/plugins/toolbar-plugin/index.tsx +7 -4
- package/src/server.ts +11 -5
- package/src/validate/createEmptyEditorState.ts +2 -7
- package/src/validate/hasText.test.ts +1 -2
- package/src/validate/hasText.ts +2 -9
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { CodeHighlightNode, CodeNode
|
|
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
|
-
|
|
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 [
|
|
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)
|
|
103
|
-
|
|
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(
|
|
151
|
-
className: "
|
|
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(
|
|
154
|
-
className: "
|
|
182
|
+
/*#__PURE__*/ jsxs(Tabs.List, {
|
|
183
|
+
className: "mb-4",
|
|
155
184
|
children: [
|
|
156
|
-
/*#__PURE__*/ jsx(
|
|
157
|
-
|
|
185
|
+
/*#__PURE__*/ jsx(Tabs.Trigger, {
|
|
186
|
+
value: "image",
|
|
158
187
|
children: "Image"
|
|
159
188
|
}),
|
|
160
|
-
/*#__PURE__*/
|
|
161
|
-
|
|
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(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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(
|
|
215
|
-
|
|
301
|
+
/*#__PURE__*/ jsxs(Tabs.Content, {
|
|
302
|
+
value: "link",
|
|
303
|
+
className: "inline-image-modal-link-panel",
|
|
216
304
|
children: [
|
|
217
|
-
/*#__PURE__*/ jsx(
|
|
218
|
-
|
|
219
|
-
|
|
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(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
232
|
-
}))
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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;
|