@haklex/rich-renderer-image 0.58.0 → 0.59.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/EditMetaPopover.d.ts +23 -1
- package/dist/EditMetaPopover.d.ts.map +1 -1
- package/dist/ImageActionsMenu.d.ts +6 -0
- package/dist/ImageActionsMenu.d.ts.map +1 -0
- package/dist/ImageEditContext.d.ts.map +1 -1
- package/dist/ImageEditToolbar.d.ts.map +1 -1
- package/dist/ImageLayoutControls.d.ts +7 -0
- package/dist/ImageLayoutControls.d.ts.map +1 -1
- package/dist/{ImageRenderer-B3TgOJEU.js → ImageRenderer-BwCsVAhc.js} +22 -36
- package/dist/ImageSizeControl.d.ts +12 -0
- package/dist/ImageSizeControl.d.ts.map +1 -1
- package/dist/ReplacePanel.d.ts +2 -1
- package/dist/ReplacePanel.d.ts.map +1 -1
- package/dist/ReplacePopover.d.ts.map +1 -1
- package/dist/ToolbarButton.d.ts +10 -0
- package/dist/ToolbarButton.d.ts.map +1 -0
- package/dist/atoms.d.ts +0 -34
- package/dist/atoms.d.ts.map +1 -1
- package/dist/index.mjs +571 -651
- package/dist/rich-renderer-image.css +1 -1
- package/dist/static.mjs +1 -1
- package/dist/styles.css.d.ts +16 -30
- package/dist/styles.css.d.ts.map +1 -1
- package/dist/useImageActions.d.ts +0 -4
- package/dist/useImageActions.d.ts.map +1 -1
- package/dist/useImageSelection.d.ts +2 -0
- package/dist/useImageSelection.d.ts.map +1 -0
- package/package.json +4 -4
- package/dist/history-shortcuts.d.ts +0 -13
- package/dist/history-shortcuts.d.ts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as textButton, C as resizeHandleRight, D as sizeButton, E as semanticClassNames, F as widthRow, M as toolbarSeparator, O as sizeChevron, S as resizeHandleLeft, T as root, _ as inspectorLabel, a as editToolbarVisible, c as footerNote, d as image, f as imageDragging, g as inspectorField, h as inspector, i as editToolbar, j as toolbarGroup, l as frame, m as imageVisible, o as editTrigger, p as imageState, r as editPlaceholder, t as ImageRenderer, u as frameEditMode, w as resizeHandleVisible, x as resizeHandle } from "./ImageRenderer-BwCsVAhc.js";
|
|
2
2
|
import { $createImageNode, $isImageNode, computeImageDisplayPixels, convertImageDisplaySize, imageDisplayCssVars, imageDisplayDataAttr, imageDisplaySizeAfterResize, resolveImageDisplaySize, sanitizeImageDisplayWidth, sanitizeImageFixedPx } from "@haklex/rich-editor/nodes";
|
|
3
3
|
import { useRendererMode } from "@haklex/rich-editor/static";
|
|
4
4
|
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
5
5
|
import { Provider, atom, createStore, useAtom, useAtomValue, useSetAtom, useStore } from "jotai";
|
|
6
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { computeImageMeta, decodeThumbHash } from "@haklex/rich-editor/renderers";
|
|
8
|
+
import { $createNodeSelection, $getNearestNodeFromDOMNode, $getNodeByKey, $getSelection, $isNodeSelection, $setSelection, COMMAND_PRIORITY_HIGH, DRAGOVER_COMMAND, DRAGSTART_COMMAND, DROP_COMMAND, HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG, UNDO_COMMAND } from "lexical";
|
|
8
9
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
10
|
+
import { AlignCenterVertical, AlignEndVertical, AlignStartVertical, ArrowLeftToLine, ArrowRightToLine, ChevronDown, CopyPlus, Download, Ellipsis, ExternalLink, ImageIcon, Replace, Trash2 } from "lucide-react";
|
|
11
|
+
import { ActionButton, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Field, Kbd, Popover, PopoverBody, PopoverFooter, PopoverHeader, PopoverPanel, PopoverTrigger, SegmentedControl, TextInput, TooltipContent, TooltipRoot, TooltipTrigger, toast, updateToast, useLiveEdit } from "@haklex/rich-editor-ui";
|
|
12
|
+
import { defaultImageUpload, getImageEditor, presentImageDialog, resolvePreprocessTargets, useImagePreprocess, useImageUpload } from "@haklex/rich-editor/plugins";
|
|
13
|
+
import { usePortalTheme } from "@haklex/rich-style-token";
|
|
13
14
|
//#region src/atoms.ts
|
|
14
15
|
function getCaptionText(altText, caption) {
|
|
15
16
|
if (caption) return caption;
|
|
@@ -36,15 +37,6 @@ var layoutOpenAtom = atom(false);
|
|
|
36
37
|
var resizingAtom = atom(false);
|
|
37
38
|
var toolbarVisibleAtom = atom((get) => get(hoveringAtom) || get(metaOpenAtom) || get(replaceOpenAtom) || get(sizeOpenAtom) || get(layoutOpenAtom) || get(resizingAtom));
|
|
38
39
|
var focusCaptionOnOpenAtom = atom(false);
|
|
39
|
-
var editSrcAtom = atom("");
|
|
40
|
-
var editAltTextAtom = atom("");
|
|
41
|
-
var editCaptionAtom = atom("");
|
|
42
|
-
var replaceModeAtom = atom("upload");
|
|
43
|
-
var replaceUrlAtom = atom("");
|
|
44
|
-
var replacePreviewAtom = atom(null);
|
|
45
|
-
var replaceMetaAtom = atom(null);
|
|
46
|
-
var replaceLoadingAtom = atom(false);
|
|
47
|
-
var replaceErrorAtom = atom(null);
|
|
48
40
|
var wrapperRefAtom = atom({ current: null });
|
|
49
41
|
var fileInputRefAtom = atom({ current: null });
|
|
50
42
|
var placeholderUrlAtom = atom((get) => {
|
|
@@ -75,6 +67,7 @@ var frameStyleAtom = atom((get) => {
|
|
|
75
67
|
//#endregion
|
|
76
68
|
//#region src/ImageEditContext.tsx
|
|
77
69
|
function ImageEditProvider({ props, children }) {
|
|
70
|
+
const [editor] = useLexicalComposerContext();
|
|
78
71
|
const wrapperRef = useRef(null);
|
|
79
72
|
const fileInputRef = useRef(null);
|
|
80
73
|
const [store] = useState(() => {
|
|
@@ -90,13 +83,10 @@ function ImageEditProvider({ props, children }) {
|
|
|
90
83
|
s.set(fixedWidthAtom, props.fixedWidth);
|
|
91
84
|
s.set(fixedHeightAtom, props.fixedHeight);
|
|
92
85
|
s.set(layoutAtom, props.layout);
|
|
93
|
-
s.set(editSrcAtom, props.src);
|
|
94
|
-
s.set(editAltTextAtom, props.altText);
|
|
95
|
-
s.set(editCaptionAtom, props.caption || "");
|
|
96
86
|
s.set(wrapperRefAtom, wrapperRef);
|
|
97
87
|
s.set(fileInputRefAtom, fileInputRef);
|
|
98
88
|
s.set(loadStateAtom, props.src ? "loading" : "error");
|
|
99
|
-
s.set(replaceOpenAtom, !props.src);
|
|
89
|
+
s.set(replaceOpenAtom, !props.src && !!props.autoOpenOnMount);
|
|
100
90
|
return s;
|
|
101
91
|
});
|
|
102
92
|
useEffect(() => {
|
|
@@ -125,56 +115,49 @@ function ImageEditProvider({ props, children }) {
|
|
|
125
115
|
props.fixedHeight,
|
|
126
116
|
props.layout
|
|
127
117
|
]);
|
|
128
|
-
useEffect(() => {
|
|
129
|
-
store.set(editSrcAtom, props.src);
|
|
130
|
-
store.set(editAltTextAtom, props.altText);
|
|
131
|
-
store.set(editCaptionAtom, props.caption || "");
|
|
132
|
-
}, [
|
|
133
|
-
store,
|
|
134
|
-
props.src,
|
|
135
|
-
props.altText,
|
|
136
|
-
props.caption
|
|
137
|
-
]);
|
|
138
118
|
useEffect(() => {
|
|
139
119
|
store.set(loadStateAtom, props.src ? "loading" : "error");
|
|
140
|
-
if (!props.src) store.set(replaceOpenAtom, true);
|
|
141
120
|
}, [store, props.src]);
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!props.autoOpenOnMount) return;
|
|
123
|
+
const wrapper = wrapperRef.current;
|
|
124
|
+
if (!wrapper) return;
|
|
125
|
+
editor.update(() => {
|
|
126
|
+
const node = $getNearestNodeFromDOMNode(wrapper);
|
|
127
|
+
if ($isImageNode(node)) node.setShouldAutoOpenOnMount(false);
|
|
128
|
+
}, { tag: [HISTORY_MERGE_TAG, SKIP_SCROLL_INTO_VIEW_TAG] });
|
|
129
|
+
}, [editor, props.autoOpenOnMount]);
|
|
142
130
|
return /* @__PURE__ */ jsx(Provider, {
|
|
143
131
|
store,
|
|
144
132
|
children
|
|
145
133
|
});
|
|
146
134
|
}
|
|
147
135
|
//#endregion
|
|
148
|
-
//#region src/
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
});
|
|
168
|
-
};
|
|
169
|
-
image.onerror = () => reject(/* @__PURE__ */ new Error("Image load failed"));
|
|
170
|
-
image.src = src;
|
|
171
|
-
});
|
|
136
|
+
//#region src/ToolbarButton.tsx
|
|
137
|
+
var stopMouseDown = (event) => {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
event.stopPropagation();
|
|
140
|
+
};
|
|
141
|
+
function ToolbarButton({ children, label, icon = true, ...props }) {
|
|
142
|
+
return /* @__PURE__ */ jsxs(TooltipRoot, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
143
|
+
render: /* @__PURE__ */ jsx(ActionButton, {
|
|
144
|
+
"aria-label": label,
|
|
145
|
+
icon,
|
|
146
|
+
size: "md",
|
|
147
|
+
onMouseDown: stopMouseDown,
|
|
148
|
+
...props
|
|
149
|
+
}),
|
|
150
|
+
children
|
|
151
|
+
}), /* @__PURE__ */ jsx(TooltipContent, {
|
|
152
|
+
side: "top",
|
|
153
|
+
children: label
|
|
154
|
+
})] });
|
|
172
155
|
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/useImageActions.ts
|
|
173
158
|
function useImageActions() {
|
|
174
159
|
const store = useStore();
|
|
175
160
|
const [editor] = useLexicalComposerContext();
|
|
176
|
-
const uploadImage = useImageUpload();
|
|
177
|
-
const preprocess = useImagePreprocess();
|
|
178
161
|
const withImageNode = useCallback((updater) => {
|
|
179
162
|
const wrapperRef = store.get(wrapperRefAtom);
|
|
180
163
|
if (!wrapperRef.current) return;
|
|
@@ -185,23 +168,7 @@ function useImageActions() {
|
|
|
185
168
|
}, [editor, store]);
|
|
186
169
|
const closeReplacePanel = useCallback(() => {
|
|
187
170
|
store.set(replaceOpenAtom, false);
|
|
188
|
-
store.set(replaceModeAtom, "upload");
|
|
189
|
-
store.set(replaceUrlAtom, "");
|
|
190
|
-
store.set(replacePreviewAtom, null);
|
|
191
|
-
store.set(replaceMetaAtom, null);
|
|
192
|
-
store.set(replaceLoadingAtom, false);
|
|
193
|
-
store.set(replaceErrorAtom, null);
|
|
194
171
|
}, [store]);
|
|
195
|
-
const commitMeta = useCallback(() => {
|
|
196
|
-
const editSrc = store.get(editSrcAtom).trim();
|
|
197
|
-
if (!editSrc || !isSafeImageSrc(editSrc)) return;
|
|
198
|
-
withImageNode((node) => {
|
|
199
|
-
node.setSrc(editSrc);
|
|
200
|
-
node.setAltText(store.get(editAltTextAtom).trim());
|
|
201
|
-
node.setCaption(store.get(editCaptionAtom).trim() || void 0);
|
|
202
|
-
});
|
|
203
|
-
store.set(metaOpenAtom, false);
|
|
204
|
-
}, [store, withImageNode]);
|
|
205
172
|
const handleDelete = useCallback(() => {
|
|
206
173
|
const wrapperRef = store.get(wrapperRefAtom);
|
|
207
174
|
if (!wrapperRef.current) return;
|
|
@@ -257,75 +224,7 @@ function useImageActions() {
|
|
|
257
224
|
});
|
|
258
225
|
}, [withImageNode]);
|
|
259
226
|
return {
|
|
260
|
-
commitMeta,
|
|
261
227
|
closeReplacePanel,
|
|
262
|
-
handleReplaceFile: useCallback(async (picked) => {
|
|
263
|
-
if (!picked || !picked.type.startsWith("image/")) return;
|
|
264
|
-
const [file] = await resolvePreprocessTargets([picked], "replace", preprocess?.ref.current ?? null);
|
|
265
|
-
if (!file) return;
|
|
266
|
-
store.set(replaceLoadingAtom, true);
|
|
267
|
-
store.set(replaceErrorAtom, null);
|
|
268
|
-
try {
|
|
269
|
-
const fallbackUploader = async (nextFile) => ({
|
|
270
|
-
src: await readAsDataUrl(nextFile),
|
|
271
|
-
altText: nextFile.name
|
|
272
|
-
});
|
|
273
|
-
const uploader = uploadImage ?? fallbackUploader;
|
|
274
|
-
const [result, meta] = await Promise.all([uploader(file), computeImageMeta(file)]);
|
|
275
|
-
withImageNode((node) => {
|
|
276
|
-
node.setSrc(result.src);
|
|
277
|
-
node.setAltText(result.altText ?? file.name);
|
|
278
|
-
node.setDimensions(result.width ?? meta.width, result.height ?? meta.height);
|
|
279
|
-
node.setThumbhash(result.thumbhash ?? meta.thumbhash);
|
|
280
|
-
});
|
|
281
|
-
closeReplacePanel();
|
|
282
|
-
} catch {
|
|
283
|
-
store.set(replaceErrorAtom, "Failed to replace image");
|
|
284
|
-
} finally {
|
|
285
|
-
store.set(replaceLoadingAtom, false);
|
|
286
|
-
}
|
|
287
|
-
}, [
|
|
288
|
-
closeReplacePanel,
|
|
289
|
-
preprocess,
|
|
290
|
-
store,
|
|
291
|
-
uploadImage,
|
|
292
|
-
withImageNode
|
|
293
|
-
]),
|
|
294
|
-
handlePreviewUrl: useCallback(async () => {
|
|
295
|
-
const url = store.get(replaceUrlAtom).trim();
|
|
296
|
-
if (!url || !isSafeImageSrc(url)) {
|
|
297
|
-
store.set(replaceErrorAtom, "Invalid image URL");
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
store.set(replaceLoadingAtom, true);
|
|
301
|
-
store.set(replaceErrorAtom, null);
|
|
302
|
-
try {
|
|
303
|
-
const meta = await loadImageByUrl(url);
|
|
304
|
-
store.set(replacePreviewAtom, url);
|
|
305
|
-
store.set(replaceMetaAtom, meta);
|
|
306
|
-
} catch {
|
|
307
|
-
store.set(replacePreviewAtom, null);
|
|
308
|
-
store.set(replaceMetaAtom, null);
|
|
309
|
-
store.set(replaceErrorAtom, "Image URL could not be loaded");
|
|
310
|
-
} finally {
|
|
311
|
-
store.set(replaceLoadingAtom, false);
|
|
312
|
-
}
|
|
313
|
-
}, [store]),
|
|
314
|
-
handleReplaceByUrl: useCallback(() => {
|
|
315
|
-
const preview = store.get(replacePreviewAtom);
|
|
316
|
-
if (!preview || !isSafeImageSrc(preview)) return;
|
|
317
|
-
const meta = store.get(replaceMetaAtom);
|
|
318
|
-
withImageNode((node) => {
|
|
319
|
-
node.setSrc(preview);
|
|
320
|
-
node.setDimensions(meta?.width, meta?.height);
|
|
321
|
-
node.setThumbhash(void 0);
|
|
322
|
-
});
|
|
323
|
-
closeReplacePanel();
|
|
324
|
-
}, [
|
|
325
|
-
closeReplacePanel,
|
|
326
|
-
store,
|
|
327
|
-
withImageNode
|
|
328
|
-
]),
|
|
329
228
|
handleReplaceOpenChange: useCallback((nextOpen) => {
|
|
330
229
|
store.set(replaceOpenAtom, nextOpen);
|
|
331
230
|
if (!nextOpen) closeReplacePanel();
|
|
@@ -339,87 +238,158 @@ function useImageActions() {
|
|
|
339
238
|
};
|
|
340
239
|
}
|
|
341
240
|
//#endregion
|
|
342
|
-
//#region src/
|
|
343
|
-
function
|
|
344
|
-
const
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
children:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
children: [/* @__PURE__ */ jsx(Captions, {
|
|
392
|
-
className: `${editFieldIcon} ${semanticClassNames.editFieldIcon}`,
|
|
393
|
-
size: 14
|
|
394
|
-
}), /* @__PURE__ */ jsx("input", {
|
|
395
|
-
className: `${editInput} ${semanticClassNames.editInput}`,
|
|
396
|
-
placeholder: "Caption (optional)",
|
|
397
|
-
ref: captionInputRef,
|
|
398
|
-
type: "text",
|
|
399
|
-
value: editCaption,
|
|
400
|
-
onChange: (event) => setEditCaption(event.target.value)
|
|
401
|
-
})]
|
|
402
|
-
}),
|
|
403
|
-
/* @__PURE__ */ jsxs(ActionBar, { children: [/* @__PURE__ */ jsx(ActionButton, {
|
|
404
|
-
onClick: () => setMetaOpen(false),
|
|
405
|
-
children: "Close"
|
|
406
|
-
}), /* @__PURE__ */ jsx(ActionButton, {
|
|
407
|
-
variant: "accent",
|
|
408
|
-
onClick: commitMeta,
|
|
409
|
-
children: "Save"
|
|
410
|
-
})] })
|
|
411
|
-
] });
|
|
241
|
+
//#region src/ImageActionsMenu.tsx
|
|
242
|
+
function ImageActionsMenu({ onOpenChange }) {
|
|
243
|
+
const [editor] = useLexicalComposerContext();
|
|
244
|
+
const { handleDuplicate, handleOpen, handleDownload, handleDelete } = useImageActions();
|
|
245
|
+
const deleteImage = () => {
|
|
246
|
+
handleDelete();
|
|
247
|
+
toast({
|
|
248
|
+
title: "Image deleted",
|
|
249
|
+
action: {
|
|
250
|
+
label: "Undo",
|
|
251
|
+
onClick: () => editor.dispatchCommand(UNDO_COMMAND, void 0)
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
};
|
|
255
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, {
|
|
256
|
+
onOpenChange: (open) => onOpenChange?.(open),
|
|
257
|
+
children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
258
|
+
render: /* @__PURE__ */ jsx(ActionButton, {
|
|
259
|
+
icon: true,
|
|
260
|
+
"aria-label": "More actions",
|
|
261
|
+
size: "md",
|
|
262
|
+
onMouseDown: stopMouseDown
|
|
263
|
+
}),
|
|
264
|
+
children: /* @__PURE__ */ jsx(Ellipsis, {})
|
|
265
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
266
|
+
align: "end",
|
|
267
|
+
children: [
|
|
268
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
269
|
+
onClick: handleDuplicate,
|
|
270
|
+
children: [/* @__PURE__ */ jsx(CopyPlus, {}), "Duplicate"]
|
|
271
|
+
}),
|
|
272
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
273
|
+
onClick: handleOpen,
|
|
274
|
+
children: [/* @__PURE__ */ jsx(ExternalLink, {}), "Open original"]
|
|
275
|
+
}),
|
|
276
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
277
|
+
onClick: handleDownload,
|
|
278
|
+
children: [/* @__PURE__ */ jsx(Download, {}), "Download"]
|
|
279
|
+
}),
|
|
280
|
+
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
281
|
+
/* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
282
|
+
shortcut: "Backspace",
|
|
283
|
+
variant: "destructive",
|
|
284
|
+
onClick: deleteImage,
|
|
285
|
+
children: [/* @__PURE__ */ jsx(Trash2, {}), "Delete"]
|
|
286
|
+
})
|
|
287
|
+
]
|
|
288
|
+
})]
|
|
289
|
+
});
|
|
412
290
|
}
|
|
413
291
|
//#endregion
|
|
414
|
-
//#region src/
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
292
|
+
//#region src/EditMetaPopover.tsx
|
|
293
|
+
var UNSAFE_SRC_RE = /^(?:javascript\s*:|vbscript\s*:|data\s*:(?!image\/))/i;
|
|
294
|
+
function isSafeImageSrc(src) {
|
|
295
|
+
return src.trim() !== "" && !UNSAFE_SRC_RE.test(src.trim());
|
|
296
|
+
}
|
|
297
|
+
function ImageInspector({ focus, menu, onApply, onKeyDown, onOpenChange, open, renderWidth, trigger, values }) {
|
|
298
|
+
const id = useId();
|
|
299
|
+
const altRef = useRef(null);
|
|
300
|
+
const captionRef = useRef(null);
|
|
301
|
+
const [altText, setAltText] = useState(values.altText);
|
|
302
|
+
const [caption, setCaption] = useState(values.caption);
|
|
303
|
+
const [src, setSrc] = useState(values.src);
|
|
304
|
+
const { apply, revert } = useLiveEdit(values, open, onApply);
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
if (!open) return;
|
|
307
|
+
setAltText(values.altText);
|
|
308
|
+
setCaption(values.caption);
|
|
309
|
+
setSrc(values.src);
|
|
310
|
+
}, [open]);
|
|
311
|
+
const srcInvalid = !isSafeImageSrc(src);
|
|
312
|
+
const initialFocus = focus === "caption" ? captionRef : focus === "alt" ? altRef : void 0;
|
|
313
|
+
return /* @__PURE__ */ jsxs(Popover, {
|
|
314
|
+
open,
|
|
315
|
+
onOpenChange: (nextOpen, details) => {
|
|
316
|
+
if (!nextOpen && details?.reason === "escape-key") revert();
|
|
317
|
+
onOpenChange(nextOpen);
|
|
318
|
+
},
|
|
319
|
+
children: [/* @__PURE__ */ jsx(PopoverTrigger, { render: trigger }), /* @__PURE__ */ jsxs(PopoverPanel, {
|
|
320
|
+
flush: true,
|
|
321
|
+
className: `${inspector} ${semanticClassNames.inspector}`,
|
|
322
|
+
initialFocus,
|
|
323
|
+
side: "bottom",
|
|
324
|
+
size: "md",
|
|
325
|
+
onKeyDown,
|
|
326
|
+
children: [
|
|
327
|
+
/* @__PURE__ */ jsx(PopoverHeader, {
|
|
328
|
+
actions: menu,
|
|
329
|
+
icon: /* @__PURE__ */ jsx(ImageIcon, {}),
|
|
330
|
+
title: "Image"
|
|
331
|
+
}),
|
|
332
|
+
/* @__PURE__ */ jsxs(PopoverBody, { children: [
|
|
333
|
+
renderWidth?.((size) => apply({ size })),
|
|
334
|
+
/* @__PURE__ */ jsx(Field, {
|
|
335
|
+
id: `${id}-alt`,
|
|
336
|
+
label: "Alt text",
|
|
337
|
+
children: /* @__PURE__ */ jsx(TextInput, {
|
|
338
|
+
id: `${id}-alt`,
|
|
339
|
+
placeholder: "Describe the image",
|
|
340
|
+
ref: altRef,
|
|
341
|
+
value: altText,
|
|
342
|
+
onChange: (event) => {
|
|
343
|
+
setAltText(event.target.value);
|
|
344
|
+
apply({ altText: event.target.value });
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
}),
|
|
348
|
+
/* @__PURE__ */ jsx(Field, {
|
|
349
|
+
hint: "Optional",
|
|
350
|
+
id: `${id}-caption`,
|
|
351
|
+
label: "Caption",
|
|
352
|
+
children: /* @__PURE__ */ jsx(TextInput, {
|
|
353
|
+
id: `${id}-caption`,
|
|
354
|
+
ref: captionRef,
|
|
355
|
+
value: caption,
|
|
356
|
+
onChange: (event) => {
|
|
357
|
+
setCaption(event.target.value);
|
|
358
|
+
apply({ caption: event.target.value });
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
}),
|
|
362
|
+
/* @__PURE__ */ jsx(Field, {
|
|
363
|
+
error: srcInvalid ? "Enter an image URL" : void 0,
|
|
364
|
+
id: `${id}-src`,
|
|
365
|
+
label: "Source URL",
|
|
366
|
+
children: /* @__PURE__ */ jsx(TextInput, {
|
|
367
|
+
"aria-describedby": srcInvalid ? `${id}-src-error` : void 0,
|
|
368
|
+
id: `${id}-src`,
|
|
369
|
+
invalid: srcInvalid,
|
|
370
|
+
type: "url",
|
|
371
|
+
value: src,
|
|
372
|
+
onChange: (event) => {
|
|
373
|
+
setSrc(event.target.value);
|
|
374
|
+
if (isSafeImageSrc(event.target.value)) apply({ src: event.target.value.trim() });
|
|
375
|
+
}
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
] }),
|
|
379
|
+
/* @__PURE__ */ jsxs(PopoverFooter, {
|
|
380
|
+
hint: "Changes apply as you type",
|
|
381
|
+
children: [/* @__PURE__ */ jsx(Kbd, {
|
|
382
|
+
keys: "Esc",
|
|
383
|
+
size: "sm",
|
|
384
|
+
tone: "muted"
|
|
385
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
386
|
+
className: footerNote,
|
|
387
|
+
children: "revert"
|
|
388
|
+
})]
|
|
389
|
+
})
|
|
390
|
+
]
|
|
391
|
+
})]
|
|
392
|
+
});
|
|
423
393
|
}
|
|
424
394
|
//#endregion
|
|
425
395
|
//#region src/image-metrics.ts
|
|
@@ -472,6 +442,7 @@ function applyImageSizePreview(trigger, layout, next) {
|
|
|
472
442
|
}
|
|
473
443
|
//#endregion
|
|
474
444
|
//#region src/ImageSizeControl.tsx
|
|
445
|
+
var inspectorFocusAtom = atom(null);
|
|
475
446
|
var SIZE_PRESETS = [
|
|
476
447
|
25,
|
|
477
448
|
50,
|
|
@@ -481,219 +452,238 @@ var SIZE_PRESETS = [
|
|
|
481
452
|
var SIZE_MIN = 10;
|
|
482
453
|
var SIZE_MAX = 100;
|
|
483
454
|
var SNAP_THRESHOLD = 3;
|
|
484
|
-
var
|
|
485
|
-
|
|
486
|
-
"
|
|
487
|
-
"
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
"
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
"H",
|
|
498
|
-
"Fixed height"
|
|
499
|
-
]
|
|
455
|
+
var WIDTH_MODES = [
|
|
456
|
+
{
|
|
457
|
+
value: "auto",
|
|
458
|
+
label: "Auto"
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
value: "percent",
|
|
462
|
+
label: "%"
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
value: "px",
|
|
466
|
+
label: "px"
|
|
467
|
+
}
|
|
500
468
|
];
|
|
501
469
|
function snapToPreset(value) {
|
|
502
470
|
for (const preset of SIZE_PRESETS) if (Math.abs(value - preset) <= SNAP_THRESHOLD) return preset;
|
|
503
471
|
return value;
|
|
504
472
|
}
|
|
505
|
-
function
|
|
506
|
-
|
|
473
|
+
function formatImageSize(size) {
|
|
474
|
+
switch (size.mode) {
|
|
475
|
+
case "percent": return `${size.value}%`;
|
|
476
|
+
case "fixed-width":
|
|
477
|
+
case "fixed-height": return `${size.px}px`;
|
|
478
|
+
default: return "Auto";
|
|
479
|
+
}
|
|
507
480
|
}
|
|
508
|
-
|
|
509
|
-
var stopMouseDown$1 = (e) => {
|
|
510
|
-
e.preventDefault();
|
|
511
|
-
e.stopPropagation();
|
|
512
|
-
};
|
|
513
|
-
function ImageSizeControl() {
|
|
514
|
-
const store = useStore();
|
|
515
|
-
const [sizeOpen, setSizeOpen] = useAtom(sizeOpenAtom);
|
|
481
|
+
function useImageDisplaySize() {
|
|
516
482
|
const displayWidth = useAtomValue(displayWidthAtom);
|
|
517
483
|
const fixedWidth = useAtomValue(fixedWidthAtom);
|
|
518
484
|
const fixedHeight = useAtomValue(fixedHeightAtom);
|
|
519
|
-
|
|
520
|
-
const intrinsicHeight = useAtomValue(heightAtom);
|
|
521
|
-
const { handleSetDisplaySize } = useImageActions();
|
|
522
|
-
const [dragValue, setDragValue] = useState(null);
|
|
523
|
-
const [fixedDraft, setFixedDraft] = useState(null);
|
|
524
|
-
const size = resolveImageDisplaySize({
|
|
485
|
+
return useMemo(() => resolveImageDisplaySize({
|
|
525
486
|
displayWidth,
|
|
526
487
|
fixedWidth,
|
|
527
488
|
fixedHeight
|
|
528
|
-
})
|
|
529
|
-
useEffect(() => {
|
|
530
|
-
setDragValue(null);
|
|
531
|
-
setFixedDraft(null);
|
|
532
|
-
}, [
|
|
489
|
+
}), [
|
|
533
490
|
displayWidth,
|
|
534
491
|
fixedWidth,
|
|
535
492
|
fixedHeight
|
|
536
493
|
]);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
494
|
+
}
|
|
495
|
+
function useImageInspectorApply() {
|
|
496
|
+
const [editor] = useLexicalComposerContext();
|
|
497
|
+
const store = useStore();
|
|
498
|
+
return useCallback((patch, merge) => {
|
|
499
|
+
const wrapper = store.get(wrapperRefAtom).current;
|
|
500
|
+
if (!wrapper) return;
|
|
501
|
+
editor.update(() => {
|
|
502
|
+
const node = $getNearestNodeFromDOMNode(wrapper);
|
|
503
|
+
if (!$isImageNode(node)) return;
|
|
504
|
+
if (patch.src !== void 0) node.setSrc(patch.src);
|
|
505
|
+
if (patch.altText !== void 0) node.setAltText(patch.altText);
|
|
506
|
+
if (patch.caption !== void 0) node.setCaption(patch.caption || void 0);
|
|
507
|
+
if (patch.size) node.applyDisplaySize(patch.size);
|
|
508
|
+
}, { tag: merge ? [HISTORY_MERGE_TAG, "skip-scroll-into-view"] : "skip-scroll-into-view" });
|
|
509
|
+
}, [editor, store]);
|
|
510
|
+
}
|
|
511
|
+
function ImageWidthControl({ size, onChange }) {
|
|
512
|
+
const id = useId();
|
|
513
|
+
const store = useStore();
|
|
514
|
+
const intrinsicWidth = useAtomValue(widthAtom);
|
|
515
|
+
const intrinsicHeight = useAtomValue(heightAtom);
|
|
516
|
+
const [dragValue, setDragValue] = useState(null);
|
|
517
|
+
const [pxDraft, setPxDraft] = useState(null);
|
|
518
|
+
useEffect(() => {
|
|
519
|
+
setDragValue(null);
|
|
520
|
+
setPxDraft(null);
|
|
521
|
+
}, [size]);
|
|
522
|
+
const readContext = () => readImageDisplayContext(store.get(wrapperRefAtom).current, intrinsicWidth, intrinsicHeight);
|
|
523
|
+
const mode = size.mode === "auto" ? "auto" : size.mode === "percent" ? "percent" : "px";
|
|
557
524
|
const switchMode = (to) => {
|
|
558
|
-
if (to ===
|
|
525
|
+
if (to === mode) return;
|
|
559
526
|
if (to === "auto") {
|
|
560
|
-
|
|
527
|
+
onChange({ mode: "auto" });
|
|
561
528
|
return;
|
|
562
529
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
const commitSlider = () => {
|
|
567
|
-
if (dragValue === null) return;
|
|
568
|
-
if (size.mode === "fixed-width" || size.mode === "fixed-height") {
|
|
569
|
-
if (dragValue === committedPercent || !ctx) return;
|
|
570
|
-
handleSetDisplaySize(convertImageDisplaySize({
|
|
530
|
+
const ctx = readContext();
|
|
531
|
+
if (to === "percent") {
|
|
532
|
+
onChange(ctx ? convertImageDisplaySize(size, "percent", ctx) : {
|
|
571
533
|
mode: "percent",
|
|
572
|
-
value:
|
|
573
|
-
}
|
|
534
|
+
value: 100
|
|
535
|
+
});
|
|
574
536
|
return;
|
|
575
537
|
}
|
|
576
|
-
if (
|
|
577
|
-
|
|
578
|
-
mode: "percent",
|
|
579
|
-
value: dragValue
|
|
580
|
-
});
|
|
581
|
-
};
|
|
582
|
-
const handleSliderChange = (event) => {
|
|
583
|
-
const next = snapToPreset(Number(event.target.value));
|
|
584
|
-
setDragValue(next);
|
|
585
|
-
if (size.mode === "fixed-width" || size.mode === "fixed-height") {
|
|
586
|
-
if (!ctx) return;
|
|
587
|
-
applyPreview(convertImageDisplaySize({
|
|
588
|
-
mode: "percent",
|
|
589
|
-
value: next
|
|
590
|
-
}, size.mode, ctx));
|
|
538
|
+
if (ctx) {
|
|
539
|
+
onChange(convertImageDisplaySize(size, "fixed-width", ctx));
|
|
591
540
|
return;
|
|
592
541
|
}
|
|
593
|
-
|
|
542
|
+
const px = sanitizeImageFixedPx(intrinsicWidth);
|
|
543
|
+
if (px) onChange({
|
|
544
|
+
mode: "fixed-width",
|
|
545
|
+
px
|
|
546
|
+
});
|
|
547
|
+
};
|
|
548
|
+
const preview = (next) => {
|
|
549
|
+
const wrapper = store.get(wrapperRefAtom).current;
|
|
550
|
+
if (wrapper) applyImageSizePreview(wrapper, store.get(layoutAtom), next);
|
|
551
|
+
};
|
|
552
|
+
const committedPercent = (() => {
|
|
553
|
+
if (size.mode === "percent") return size.value;
|
|
554
|
+
const ctx = readContext();
|
|
555
|
+
if (!ctx) return void 0;
|
|
556
|
+
const { widthPx } = computeImageDisplayPixels(size, ctx);
|
|
557
|
+
return widthPx ? sanitizeImageDisplayWidth(widthPx / ctx.containerWidth * 100) : void 0;
|
|
558
|
+
})();
|
|
559
|
+
const percent = dragValue ?? committedPercent ?? SIZE_MAX;
|
|
560
|
+
const fill = `${(percent - SIZE_MIN) / 90 * 100}%`;
|
|
561
|
+
const commitSlider = () => {
|
|
562
|
+
if (dragValue === null || dragValue === committedPercent) return;
|
|
563
|
+
onChange({
|
|
594
564
|
mode: "percent",
|
|
595
|
-
value:
|
|
565
|
+
value: dragValue
|
|
596
566
|
});
|
|
597
567
|
};
|
|
598
|
-
const value = dragValue ?? committedPercent ?? SIZE_MAX;
|
|
599
|
-
const fill = `${(value - SIZE_MIN) / 90 * 100}%`;
|
|
600
568
|
const fixedPx = size.mode === "fixed-width" || size.mode === "fixed-height" ? size.px : void 0;
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
children: [/* @__PURE__ */ jsx(
|
|
605
|
-
className:
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
className:
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
})]
|
|
668
|
-
})
|
|
669
|
-
]
|
|
670
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
671
|
-
className: sizePanelRow,
|
|
672
|
-
children: /* @__PURE__ */ jsxs("span", {
|
|
673
|
-
className: sizeSliderWrap,
|
|
674
|
-
children: [/* @__PURE__ */ jsx("input", {
|
|
675
|
-
"aria-label": "Image width, percent of content width",
|
|
676
|
-
className: sizeSlider,
|
|
677
|
-
max: SIZE_MAX,
|
|
678
|
-
min: SIZE_MIN,
|
|
679
|
-
step: 1,
|
|
680
|
-
style: { "--fill": fill },
|
|
681
|
-
type: "range",
|
|
682
|
-
value,
|
|
683
|
-
onBlur: commitSlider,
|
|
684
|
-
onChange: handleSliderChange,
|
|
685
|
-
onKeyUp: commitSlider,
|
|
686
|
-
onMouseDown: (e) => e.stopPropagation(),
|
|
687
|
-
onPointerUp: commitSlider
|
|
688
|
-
}), SIZE_PRESETS.map((preset) => /* @__PURE__ */ jsx("span", {
|
|
689
|
-
className: sizeSliderTick,
|
|
690
|
-
style: { left: tickLeft(preset) }
|
|
691
|
-
}, preset))]
|
|
569
|
+
const pxLabel = size.mode === "fixed-height" ? "Height in pixels" : "Width in pixels";
|
|
570
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
571
|
+
className: inspectorField,
|
|
572
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
573
|
+
className: inspectorLabel,
|
|
574
|
+
id: `${id}-label`,
|
|
575
|
+
children: "Width"
|
|
576
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
577
|
+
"aria-labelledby": `${id}-label`,
|
|
578
|
+
className: widthRow,
|
|
579
|
+
role: "group",
|
|
580
|
+
children: [
|
|
581
|
+
/* @__PURE__ */ jsx(SegmentedControl, {
|
|
582
|
+
items: WIDTH_MODES,
|
|
583
|
+
value: mode,
|
|
584
|
+
onChange: switchMode
|
|
585
|
+
}),
|
|
586
|
+
mode === "percent" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("input", {
|
|
587
|
+
"aria-label": "Width, percent of content width",
|
|
588
|
+
className: "_1n94osfu",
|
|
589
|
+
max: SIZE_MAX,
|
|
590
|
+
min: SIZE_MIN,
|
|
591
|
+
step: 1,
|
|
592
|
+
style: { "--fill": fill },
|
|
593
|
+
type: "range",
|
|
594
|
+
value: percent,
|
|
595
|
+
onBlur: commitSlider,
|
|
596
|
+
onKeyUp: commitSlider,
|
|
597
|
+
onPointerUp: commitSlider,
|
|
598
|
+
onChange: (event) => {
|
|
599
|
+
const next = snapToPreset(Number(event.target.value));
|
|
600
|
+
setDragValue(next);
|
|
601
|
+
preview({
|
|
602
|
+
mode: "percent",
|
|
603
|
+
value: next
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
607
|
+
className: "_1n94osf15",
|
|
608
|
+
children: `${percent}%`
|
|
609
|
+
})] }),
|
|
610
|
+
mode === "px" && /* @__PURE__ */ jsxs("span", {
|
|
611
|
+
className: "_1n94osf17",
|
|
612
|
+
children: [/* @__PURE__ */ jsx(TextInput, {
|
|
613
|
+
"aria-label": pxLabel,
|
|
614
|
+
autoComplete: "off",
|
|
615
|
+
inputMode: "numeric",
|
|
616
|
+
invalid: pxDraft !== null && sanitizeImageFixedPx(Number(pxDraft)) === void 0,
|
|
617
|
+
spellCheck: false,
|
|
618
|
+
value: pxDraft ?? String(fixedPx ?? ""),
|
|
619
|
+
onChange: (event) => {
|
|
620
|
+
setPxDraft(event.target.value);
|
|
621
|
+
const px = sanitizeImageFixedPx(Number(event.target.value));
|
|
622
|
+
if (px && (size.mode === "fixed-width" || size.mode === "fixed-height")) onChange({
|
|
623
|
+
mode: size.mode,
|
|
624
|
+
px
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
628
|
+
className: "_1n94osf18",
|
|
629
|
+
children: "px"
|
|
630
|
+
})]
|
|
631
|
+
}),
|
|
632
|
+
mode === "auto" && /* @__PURE__ */ jsx("span", {
|
|
633
|
+
className: "_1n94osf16",
|
|
634
|
+
children: "Natural size"
|
|
692
635
|
})
|
|
693
|
-
|
|
636
|
+
]
|
|
694
637
|
})]
|
|
695
638
|
});
|
|
696
639
|
}
|
|
640
|
+
function ImageSizeControl() {
|
|
641
|
+
const [open, setOpen] = useAtom(metaOpenAtom);
|
|
642
|
+
const [focus, setFocus] = useAtom(inspectorFocusAtom);
|
|
643
|
+
const [focusCaption, setFocusCaption] = useAtom(focusCaptionOnOpenAtom);
|
|
644
|
+
const src = useAtomValue(srcAtom);
|
|
645
|
+
const altText = useAtomValue(altTextAtom);
|
|
646
|
+
const caption = useAtomValue(captionAtom);
|
|
647
|
+
const size = useImageDisplaySize();
|
|
648
|
+
const onApply = useImageInspectorApply();
|
|
649
|
+
const values = useMemo(() => ({
|
|
650
|
+
src,
|
|
651
|
+
altText,
|
|
652
|
+
caption: caption ?? "",
|
|
653
|
+
size
|
|
654
|
+
}), [
|
|
655
|
+
src,
|
|
656
|
+
altText,
|
|
657
|
+
caption,
|
|
658
|
+
size
|
|
659
|
+
]);
|
|
660
|
+
return /* @__PURE__ */ jsx(ImageInspector, {
|
|
661
|
+
focus: focusCaption ? "caption" : focus,
|
|
662
|
+
menu: /* @__PURE__ */ jsx(ImageActionsMenu, {}),
|
|
663
|
+
open,
|
|
664
|
+
renderWidth: (apply) => /* @__PURE__ */ jsx(ImageWidthControl, {
|
|
665
|
+
size,
|
|
666
|
+
onChange: apply
|
|
667
|
+
}),
|
|
668
|
+
values,
|
|
669
|
+
trigger: /* @__PURE__ */ jsxs(ActionButton, {
|
|
670
|
+
"aria-label": `Image size, ${formatImageSize(size)}`,
|
|
671
|
+
className: sizeButton,
|
|
672
|
+
size: "md",
|
|
673
|
+
onClick: () => setFocus(null),
|
|
674
|
+
onMouseDown: stopMouseDown,
|
|
675
|
+
children: [/* @__PURE__ */ jsx("span", { children: formatImageSize(size) }), /* @__PURE__ */ jsx(ChevronDown, { className: sizeChevron })]
|
|
676
|
+
}),
|
|
677
|
+
onApply,
|
|
678
|
+
onOpenChange: (nextOpen) => {
|
|
679
|
+
setOpen(nextOpen);
|
|
680
|
+
if (!nextOpen) {
|
|
681
|
+
setFocus(null);
|
|
682
|
+
setFocusCaption(false);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
697
687
|
//#endregion
|
|
698
688
|
//#region src/ImageLayoutControls.tsx
|
|
699
689
|
var LAYOUT_OPTIONS = [
|
|
@@ -723,286 +713,216 @@ var LAYOUT_OPTIONS = [
|
|
|
723
713
|
Icon: ArrowRightToLine
|
|
724
714
|
}
|
|
725
715
|
];
|
|
726
|
-
var toolbarButtonClass = `${editToolbarButton} ${semanticClassNames.editToolbarButton}`;
|
|
727
|
-
var stopMouseDown = (e) => {
|
|
728
|
-
e.preventDefault();
|
|
729
|
-
e.stopPropagation();
|
|
730
|
-
};
|
|
731
716
|
function ImageLayoutControl() {
|
|
732
|
-
const [layoutOpen, setLayoutOpen] = useAtom(layoutOpenAtom);
|
|
733
717
|
const layout = useAtomValue(layoutAtom);
|
|
734
718
|
const { handleSetLayout } = useImageActions();
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
children:
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
sideOffset: 8,
|
|
748
|
-
children: LAYOUT_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ jsx("button", {
|
|
749
|
-
className: `${toolbarButtonClass} ${layout === value ? editToolbarButtonActive : ""}`.trim(),
|
|
750
|
-
title: label,
|
|
751
|
-
type: "button",
|
|
752
|
-
onMouseDown: stopMouseDown,
|
|
753
|
-
onClick: () => {
|
|
754
|
-
handleSetLayout(value);
|
|
755
|
-
setLayoutOpen(false);
|
|
756
|
-
},
|
|
757
|
-
children: /* @__PURE__ */ jsx(Icon, { size: 14 })
|
|
758
|
-
}, value ?? "center"))
|
|
759
|
-
})]
|
|
719
|
+
return /* @__PURE__ */ jsx("div", {
|
|
720
|
+
"aria-label": "Image layout",
|
|
721
|
+
className: toolbarGroup,
|
|
722
|
+
role: "group",
|
|
723
|
+
children: LAYOUT_OPTIONS.map(({ value, label, Icon }) => /* @__PURE__ */ jsx(ToolbarButton, {
|
|
724
|
+
"aria-pressed": layout === value,
|
|
725
|
+
label,
|
|
726
|
+
onClick: () => {
|
|
727
|
+
if (layout !== value) handleSetLayout(value);
|
|
728
|
+
},
|
|
729
|
+
children: /* @__PURE__ */ jsx(Icon, {})
|
|
730
|
+
}, value ?? "center"))
|
|
760
731
|
});
|
|
761
732
|
}
|
|
762
733
|
//#endregion
|
|
763
734
|
//#region src/ReplacePanel.tsx
|
|
764
|
-
var
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
735
|
+
var ERROR_TOAST_DURATION = 6e3;
|
|
736
|
+
function useReplaceImageDialog() {
|
|
737
|
+
const [editor] = useLexicalComposerContext();
|
|
738
|
+
const store = useStore();
|
|
739
|
+
const uploadImage = useImageUpload();
|
|
740
|
+
const preprocess = useImagePreprocess();
|
|
741
|
+
const portalTheme = usePortalTheme();
|
|
742
|
+
const openRef = useRef(false);
|
|
743
|
+
return useCallback(async () => {
|
|
744
|
+
if (openRef.current) return;
|
|
745
|
+
openRef.current = true;
|
|
746
|
+
const wrapper = store.get(wrapperRefAtom).current;
|
|
747
|
+
const withNode = (updater) => {
|
|
748
|
+
if (!wrapper) return;
|
|
749
|
+
editor.update(() => {
|
|
750
|
+
const node = $getNearestNodeFromDOMNode(wrapper);
|
|
751
|
+
if ($isImageNode(node)) updater(node);
|
|
752
|
+
}, { tag: "skip-scroll-into-view" });
|
|
753
|
+
};
|
|
754
|
+
try {
|
|
755
|
+
const imageEditor = getImageEditor(preprocess);
|
|
756
|
+
const entry = (await presentImageDialog({
|
|
757
|
+
editor: imageEditor,
|
|
758
|
+
mode: "replace",
|
|
759
|
+
portalClassName: portalTheme.className,
|
|
760
|
+
theme: portalTheme.theme
|
|
761
|
+
}))?.[0];
|
|
762
|
+
if (!entry) return;
|
|
763
|
+
if (entry.kind === "url") {
|
|
764
|
+
withNode((node) => {
|
|
765
|
+
node.setSrc(entry.src);
|
|
766
|
+
node.setDimensions(entry.width, entry.height);
|
|
767
|
+
node.setThumbhash(void 0);
|
|
768
|
+
});
|
|
769
|
+
return;
|
|
797
770
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
771
|
+
const [file] = imageEditor ? [entry.file] : await resolvePreprocessTargets([entry.file], "replace", preprocess?.ref.current ?? null);
|
|
772
|
+
if (!file) return;
|
|
773
|
+
const upload = async () => {
|
|
774
|
+
const id = toast({
|
|
775
|
+
title: "Uploading image…",
|
|
776
|
+
tone: "loading"
|
|
777
|
+
});
|
|
778
|
+
try {
|
|
779
|
+
const uploader = uploadImage ?? defaultImageUpload;
|
|
780
|
+
const [result, meta] = await Promise.all([uploader(file), computeImageMeta(file)]);
|
|
781
|
+
withNode((node) => {
|
|
782
|
+
node.setSrc(result.src);
|
|
783
|
+
node.setAltText(result.altText ?? file.name);
|
|
784
|
+
node.setDimensions(result.width ?? meta.width, result.height ?? meta.height);
|
|
785
|
+
node.setThumbhash(result.thumbhash ?? meta.thumbhash);
|
|
786
|
+
});
|
|
787
|
+
updateToast(id, {
|
|
788
|
+
title: "Image replaced",
|
|
789
|
+
tone: "success"
|
|
790
|
+
});
|
|
791
|
+
} catch (err) {
|
|
792
|
+
console.error("[ImageReplace]", err);
|
|
793
|
+
updateToast(id, {
|
|
794
|
+
action: {
|
|
795
|
+
label: "Retry",
|
|
796
|
+
onClick: () => void upload()
|
|
797
|
+
},
|
|
798
|
+
duration: ERROR_TOAST_DURATION,
|
|
799
|
+
title: "Image replace failed",
|
|
800
|
+
tone: "error"
|
|
801
|
+
});
|
|
819
802
|
}
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
803
|
+
};
|
|
804
|
+
await upload();
|
|
805
|
+
} finally {
|
|
806
|
+
openRef.current = false;
|
|
807
|
+
}
|
|
808
|
+
}, [
|
|
809
|
+
editor,
|
|
810
|
+
portalTheme,
|
|
811
|
+
preprocess,
|
|
812
|
+
store,
|
|
813
|
+
uploadImage
|
|
814
|
+
]);
|
|
815
|
+
}
|
|
816
|
+
//#endregion
|
|
817
|
+
//#region src/useImageSelection.ts
|
|
818
|
+
function useImageSelected() {
|
|
819
|
+
const [editor] = useLexicalComposerContext();
|
|
820
|
+
const store = useStore();
|
|
821
|
+
const [selected, setSelected] = useState(false);
|
|
822
|
+
useEffect(() => {
|
|
823
|
+
const wrapper = store.get(wrapperRefAtom).current;
|
|
824
|
+
if (!wrapper) return;
|
|
825
|
+
const readSelected = () => editor.read(() => {
|
|
826
|
+
const selection = $getSelection();
|
|
827
|
+
if (!$isNodeSelection(selection)) return false;
|
|
828
|
+
const node = $getNearestNodeFromDOMNode(wrapper);
|
|
829
|
+
return node !== null && selection.has(node.getKey());
|
|
830
|
+
});
|
|
831
|
+
const sync = () => {
|
|
832
|
+
const next = readSelected();
|
|
833
|
+
setSelected(next);
|
|
834
|
+
if (next) wrapper.dataset.selected = "";
|
|
835
|
+
else delete wrapper.dataset.selected;
|
|
836
|
+
};
|
|
837
|
+
const handleClick = (event) => {
|
|
838
|
+
if (event.target?.closest(`.${semanticClassNames.editToolbar}`)) return;
|
|
839
|
+
editor.update(() => {
|
|
840
|
+
const node = $getNearestNodeFromDOMNode(wrapper);
|
|
841
|
+
if (!node) return;
|
|
842
|
+
const selection = $createNodeSelection();
|
|
843
|
+
selection.add(node.getKey());
|
|
844
|
+
$setSelection(selection);
|
|
845
|
+
}, { tag: "skip-scroll-into-view" });
|
|
846
|
+
};
|
|
847
|
+
sync();
|
|
848
|
+
wrapper.addEventListener("click", handleClick);
|
|
849
|
+
const unregister = editor.registerUpdateListener(sync);
|
|
850
|
+
return () => {
|
|
851
|
+
wrapper.removeEventListener("click", handleClick);
|
|
852
|
+
unregister();
|
|
853
|
+
delete wrapper.dataset.selected;
|
|
854
|
+
};
|
|
855
|
+
}, [editor, store]);
|
|
856
|
+
return selected;
|
|
843
857
|
}
|
|
844
858
|
//#endregion
|
|
845
859
|
//#region src/ImageEditToolbar.tsx
|
|
860
|
+
function ToolbarSeparator() {
|
|
861
|
+
return /* @__PURE__ */ jsx("span", {
|
|
862
|
+
"aria-hidden": true,
|
|
863
|
+
className: toolbarSeparator
|
|
864
|
+
});
|
|
865
|
+
}
|
|
846
866
|
function ImageEditToolbar() {
|
|
847
|
-
const [editor] = useLexicalComposerContext();
|
|
848
867
|
const toolbarVisible = useAtomValue(toolbarVisibleAtom);
|
|
849
|
-
const
|
|
868
|
+
const selected = useImageSelected();
|
|
869
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
850
870
|
const setMetaOpen = useSetAtom(metaOpenAtom);
|
|
851
|
-
const
|
|
852
|
-
const
|
|
853
|
-
const
|
|
854
|
-
const caption = useAtomValue(captionAtom);
|
|
855
|
-
const editSrc = useAtomValue(editSrcAtom);
|
|
856
|
-
const editAltText = useAtomValue(editAltTextAtom);
|
|
857
|
-
const editCaption = useAtomValue(editCaptionAtom);
|
|
858
|
-
const replaceUrl = useAtomValue(replaceUrlAtom);
|
|
859
|
-
const replacePreview = useAtomValue(replacePreviewAtom);
|
|
860
|
-
const { handleReplaceOpenChange, handleOpen, handleDuplicate, handleDownload, handleDelete } = useImageActions();
|
|
861
|
-
const dispatchHistoryShortcut = (shortcut) => {
|
|
862
|
-
editor.dispatchCommand(shortcut === "undo" ? UNDO_COMMAND : REDO_COMMAND, void 0);
|
|
863
|
-
};
|
|
864
|
-
const handleMetaPanelKeyDown = (event) => {
|
|
865
|
-
const shortcut = getEditorHistoryShortcut(event, { isDirty: editSrc !== src || editAltText !== altText || editCaption !== (caption || "") });
|
|
866
|
-
if (!shortcut) return;
|
|
867
|
-
event.preventDefault();
|
|
868
|
-
event.stopPropagation();
|
|
869
|
-
dispatchHistoryShortcut(shortcut);
|
|
870
|
-
};
|
|
871
|
-
const handleReplacePanelKeyDown = (event) => {
|
|
872
|
-
const shortcut = getEditorHistoryShortcut(event, { isDirty: replaceUrl !== "" || replacePreview !== null });
|
|
873
|
-
if (!shortcut) return;
|
|
874
|
-
event.preventDefault();
|
|
875
|
-
event.stopPropagation();
|
|
876
|
-
dispatchHistoryShortcut(shortcut);
|
|
877
|
-
};
|
|
871
|
+
const setInspectorFocus = useSetAtom(inspectorFocusAtom);
|
|
872
|
+
const openReplaceDialog = useReplaceImageDialog();
|
|
873
|
+
const visible = toolbarVisible || selected || menuOpen;
|
|
878
874
|
return /* @__PURE__ */ jsxs("div", {
|
|
879
|
-
|
|
875
|
+
"aria-label": "Image",
|
|
876
|
+
className: `${editToolbar} ${semanticClassNames.editToolbar} ${visible ? `${editToolbarVisible} ${semanticClassNames.editToolbarVisible}` : ""}`,
|
|
877
|
+
role: "toolbar",
|
|
880
878
|
children: [
|
|
881
879
|
/* @__PURE__ */ jsx(ImageSizeControl, {}),
|
|
880
|
+
/* @__PURE__ */ jsx(ToolbarSeparator, {}),
|
|
882
881
|
/* @__PURE__ */ jsx(ImageLayoutControl, {}),
|
|
883
|
-
/* @__PURE__ */
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
title: "Edit details",
|
|
892
|
-
onMouseDown: (e) => {
|
|
893
|
-
e.preventDefault();
|
|
894
|
-
e.stopPropagation();
|
|
895
|
-
},
|
|
896
|
-
children: /* @__PURE__ */ jsx(Type, { size: 14 })
|
|
897
|
-
}), /* @__PURE__ */ jsx(PopoverPanel, {
|
|
898
|
-
className: editPanel,
|
|
899
|
-
side: "bottom",
|
|
900
|
-
sideOffset: 8,
|
|
901
|
-
onKeyDown: handleMetaPanelKeyDown,
|
|
902
|
-
children: /* @__PURE__ */ jsx(EditMetaPopover, {})
|
|
903
|
-
})]
|
|
904
|
-
}),
|
|
905
|
-
/* @__PURE__ */ jsxs(Popover, {
|
|
906
|
-
open: replaceOpen,
|
|
907
|
-
onOpenChange: (nextOpen) => {
|
|
908
|
-
handleReplaceOpenChange(nextOpen);
|
|
909
|
-
if (nextOpen) setMetaOpen(false);
|
|
910
|
-
},
|
|
911
|
-
children: [/* @__PURE__ */ jsx(PopoverTrigger, {
|
|
912
|
-
className: `${editToolbarButton} ${semanticClassNames.editToolbarButton}`,
|
|
913
|
-
title: "Replace image",
|
|
914
|
-
onMouseDown: (e) => {
|
|
915
|
-
e.preventDefault();
|
|
916
|
-
e.stopPropagation();
|
|
917
|
-
},
|
|
918
|
-
children: /* @__PURE__ */ jsx(Replace, { size: 14 })
|
|
919
|
-
}), /* @__PURE__ */ jsx(PopoverPanel, {
|
|
920
|
-
className: editPanel,
|
|
921
|
-
side: "bottom",
|
|
922
|
-
sideOffset: 8,
|
|
923
|
-
onKeyDown: handleReplacePanelKeyDown,
|
|
924
|
-
children: /* @__PURE__ */ jsx(ReplacePanel, {})
|
|
925
|
-
})]
|
|
926
|
-
}),
|
|
927
|
-
/* @__PURE__ */ jsx("button", {
|
|
928
|
-
className: `${editToolbarButton} ${semanticClassNames.editToolbarButton}`,
|
|
929
|
-
title: "Duplicate",
|
|
930
|
-
type: "button",
|
|
931
|
-
onClick: handleDuplicate,
|
|
932
|
-
onMouseDown: (e) => {
|
|
933
|
-
e.preventDefault();
|
|
934
|
-
e.stopPropagation();
|
|
935
|
-
},
|
|
936
|
-
children: /* @__PURE__ */ jsx(CopyPlus, { size: 14 })
|
|
937
|
-
}),
|
|
938
|
-
/* @__PURE__ */ jsx("button", {
|
|
939
|
-
className: `${editToolbarButton} ${semanticClassNames.editToolbarButton}`,
|
|
940
|
-
title: "Open source",
|
|
941
|
-
type: "button",
|
|
942
|
-
onClick: handleOpen,
|
|
943
|
-
onMouseDown: (e) => {
|
|
944
|
-
e.preventDefault();
|
|
945
|
-
e.stopPropagation();
|
|
882
|
+
/* @__PURE__ */ jsx(ToolbarSeparator, {}),
|
|
883
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
884
|
+
className: textButton,
|
|
885
|
+
icon: false,
|
|
886
|
+
label: "Alt text",
|
|
887
|
+
onClick: () => {
|
|
888
|
+
setInspectorFocus("alt");
|
|
889
|
+
setMetaOpen(true);
|
|
946
890
|
},
|
|
947
|
-
children:
|
|
891
|
+
children: "Alt"
|
|
948
892
|
}),
|
|
949
|
-
/* @__PURE__ */ jsx(
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
onClick: handleDownload,
|
|
954
|
-
onMouseDown: (e) => {
|
|
955
|
-
e.preventDefault();
|
|
956
|
-
e.stopPropagation();
|
|
957
|
-
},
|
|
958
|
-
children: /* @__PURE__ */ jsx(Download, { size: 14 })
|
|
893
|
+
/* @__PURE__ */ jsx(ToolbarButton, {
|
|
894
|
+
label: "Replace image",
|
|
895
|
+
onClick: () => void openReplaceDialog(),
|
|
896
|
+
children: /* @__PURE__ */ jsx(Replace, {})
|
|
959
897
|
}),
|
|
960
|
-
/* @__PURE__ */ jsx(
|
|
961
|
-
|
|
962
|
-
title: "Remove image",
|
|
963
|
-
type: "button",
|
|
964
|
-
onClick: handleDelete,
|
|
965
|
-
onMouseDown: (e) => {
|
|
966
|
-
e.preventDefault();
|
|
967
|
-
e.stopPropagation();
|
|
968
|
-
},
|
|
969
|
-
children: /* @__PURE__ */ jsx(Trash2, { size: 14 })
|
|
970
|
-
})
|
|
898
|
+
/* @__PURE__ */ jsx(ToolbarSeparator, {}),
|
|
899
|
+
/* @__PURE__ */ jsx(ImageActionsMenu, { onOpenChange: setMenuOpen })
|
|
971
900
|
]
|
|
972
901
|
});
|
|
973
902
|
}
|
|
974
903
|
//#endregion
|
|
975
904
|
//#region src/ReplacePopover.tsx
|
|
976
905
|
function ReplacePopover() {
|
|
977
|
-
const [
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
},
|
|
998
|
-
children: [/* @__PURE__ */ jsx(ImageIcon, { size: 24 }), /* @__PURE__ */ jsx("span", { children: "Click to add image" })]
|
|
999
|
-
}), /* @__PURE__ */ jsx(PopoverPanel, {
|
|
1000
|
-
className: editPanel,
|
|
1001
|
-
side: "bottom",
|
|
1002
|
-
sideOffset: 8,
|
|
1003
|
-
onKeyDown: handlePanelKeyDown,
|
|
1004
|
-
children: /* @__PURE__ */ jsx(ReplacePanel, {})
|
|
1005
|
-
})]
|
|
906
|
+
const [replaceOpen, setReplaceOpen] = useAtom(replaceOpenAtom);
|
|
907
|
+
const openReplaceDialog = useReplaceImageDialog();
|
|
908
|
+
useEffect(() => {
|
|
909
|
+
if (!replaceOpen) return;
|
|
910
|
+
setReplaceOpen(false);
|
|
911
|
+
openReplaceDialog();
|
|
912
|
+
}, [
|
|
913
|
+
openReplaceDialog,
|
|
914
|
+
replaceOpen,
|
|
915
|
+
setReplaceOpen
|
|
916
|
+
]);
|
|
917
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
918
|
+
className: `${editPlaceholder} ${semanticClassNames.editPlaceholder}`,
|
|
919
|
+
type: "button",
|
|
920
|
+
onClick: () => void openReplaceDialog(),
|
|
921
|
+
onMouseDown: (e) => {
|
|
922
|
+
e.preventDefault();
|
|
923
|
+
e.stopPropagation();
|
|
924
|
+
},
|
|
925
|
+
children: [/* @__PURE__ */ jsx(ImageIcon, { size: 24 }), /* @__PURE__ */ jsx("span", { children: "Click to add image" })]
|
|
1006
926
|
});
|
|
1007
927
|
}
|
|
1008
928
|
//#endregion
|