@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/index.mjs CHANGED
@@ -1,15 +1,16 @@
1
- import { A as root, B as sizeSliderWrap, D as resizeHandleLeft, E as resizeHandle, F as sizePanelRow, I as sizePxInput, L as sizeReadout, M as sizeEquiv, N as sizeOption, O as resizeHandleRight, P as sizePanel, R as sizeSlider, T as replaceUploadArea, V as sizeValueField, _ as frameEditMode, a as editFieldIcon, b as imageState, c as editPlaceholder, d as editToolbarButtonActive, f as editToolbarButtonDanger, g as frame, i as editField, j as semanticClassNames, k as resizeHandleVisible, l as editToolbar, m as editTrigger, o as editInput, p as editToolbarVisible, r as controlPanel, s as editPanel, t as ImageRenderer, u as editToolbarButton, v as image, x as imageVisible, y as imageDragging, z as sizeSliderTick } from "./ImageRenderer-B3TgOJEU.js";
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 { ActionBar, ActionButton, Popover, PopoverPanel, PopoverTrigger, SegmentedControl } from "@haklex/rich-editor-ui";
10
- import { $getNearestNodeFromDOMNode, $getNodeByKey, COMMAND_PRIORITY_HIGH, DRAGOVER_COMMAND, DRAGSTART_COMMAND, DROP_COMMAND, REDO_COMMAND, UNDO_COMMAND } from "lexical";
11
- import { AlignCenterVertical, AlignEndVertical, AlignStartVertical, ArrowLeftToLine, ArrowRightToLine, Captions, CopyPlus, Download, ExternalLink, ImageIcon, Loader2, Replace, Scaling, Trash2, Type, Upload } from "lucide-react";
12
- import { resolvePreprocessTargets, useImagePreprocess, useImageUpload } from "@haklex/rich-editor/plugins";
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/useImageActions.ts
149
- function isSafeImageSrc(src) {
150
- return !/^(?:javascript\s*:|vbscript\s*:|data\s*:(?!image\/))/i.test(src);
151
- }
152
- function readAsDataUrl(file) {
153
- return new Promise((resolve, reject) => {
154
- const reader = new FileReader();
155
- reader.onload = () => resolve(String(reader.result));
156
- reader.onerror = () => reject(reader.error ?? /* @__PURE__ */ new Error("File read failed"));
157
- reader.readAsDataURL(file);
158
- });
159
- }
160
- function loadImageByUrl(src) {
161
- return new Promise((resolve, reject) => {
162
- const image = new Image();
163
- image.onload = () => {
164
- resolve({
165
- width: image.naturalWidth || image.width,
166
- height: image.naturalHeight || image.height
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/EditMetaPopover.tsx
343
- function EditMetaPopover() {
344
- const editSrc = useAtomValue(editSrcAtom);
345
- const setEditSrc = useSetAtom(editSrcAtom);
346
- const editAltText = useAtomValue(editAltTextAtom);
347
- const setEditAltText = useSetAtom(editAltTextAtom);
348
- const editCaption = useAtomValue(editCaptionAtom);
349
- const setEditCaption = useSetAtom(editCaptionAtom);
350
- const setMetaOpen = useSetAtom(metaOpenAtom);
351
- const { commitMeta } = useImageActions();
352
- const captionInputRef = useRef(null);
353
- const focusCaptionOnOpen = useAtomValue(focusCaptionOnOpenAtom);
354
- const setFocusCaptionOnOpen = useSetAtom(focusCaptionOnOpenAtom);
355
- useEffect(() => {
356
- if (focusCaptionOnOpen && captionInputRef.current) {
357
- captionInputRef.current.focus();
358
- captionInputRef.current.select();
359
- setFocusCaptionOnOpen(false);
360
- }
361
- }, [focusCaptionOnOpen, setFocusCaptionOnOpen]);
362
- return /* @__PURE__ */ jsxs(Fragment, { children: [
363
- /* @__PURE__ */ jsxs("div", {
364
- className: `${editField} ${semanticClassNames.editField}`,
365
- children: [/* @__PURE__ */ jsx(ImageIcon, {
366
- className: `${editFieldIcon} ${semanticClassNames.editFieldIcon}`,
367
- size: 14
368
- }), /* @__PURE__ */ jsx("input", {
369
- className: `${editInput} ${semanticClassNames.editInput}`,
370
- placeholder: "Image URL",
371
- type: "url",
372
- value: editSrc,
373
- onChange: (event) => setEditSrc(event.target.value)
374
- })]
375
- }),
376
- /* @__PURE__ */ jsxs("div", {
377
- className: `${editField} ${semanticClassNames.editField}`,
378
- children: [/* @__PURE__ */ jsx(Type, {
379
- className: `${editFieldIcon} ${semanticClassNames.editFieldIcon}`,
380
- size: 14
381
- }), /* @__PURE__ */ jsx("input", {
382
- className: `${editInput} ${semanticClassNames.editInput}`,
383
- placeholder: "Alt text",
384
- type: "text",
385
- value: editAltText,
386
- onChange: (event) => setEditAltText(event.target.value)
387
- })]
388
- }),
389
- /* @__PURE__ */ jsxs("div", {
390
- className: `${editField} ${semanticClassNames.editField}`,
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/history-shortcuts.ts
415
- function getEditorHistoryShortcut(event, options = {}) {
416
- if (options.isDirty) return null;
417
- if (event.altKey) return null;
418
- if (!event.metaKey && !event.ctrlKey) return null;
419
- const key = event.key.toLowerCase();
420
- if (key === "z") return event.shiftKey ? "redo" : "undo";
421
- if (key === "y" && event.ctrlKey && !event.metaKey && !event.shiftKey) return "redo";
422
- return null;
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 MODE_BUTTONS = [
485
- [
486
- "percent",
487
- "%",
488
- "Percent of content width"
489
- ],
490
- [
491
- "fixed-width",
492
- "W",
493
- "Fixed width"
494
- ],
495
- [
496
- "fixed-height",
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 tickLeft(preset) {
506
- return `calc(6px + ${(preset - SIZE_MIN) / 90} * (100% - 12px))`;
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
- var toolbarButtonClass$1 = `${editToolbarButton} ${semanticClassNames.editToolbarButton}`;
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
- const intrinsicWidth = useAtomValue(widthAtom);
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
- const ctx = readImageDisplayContext(store.get(wrapperRefAtom).current, intrinsicWidth, intrinsicHeight);
538
- const committedPixels = ctx ? computeImageDisplayPixels(size, ctx) : {};
539
- const committedPercent = size.mode === "percent" ? size.value : ctx && committedPixels.widthPx ? sanitizeImageDisplayWidth(committedPixels.widthPx / ctx.containerWidth * 100) : void 0;
540
- const previewSize = dragValue === null ? size : size.mode === "fixed-width" || size.mode === "fixed-height" ? ctx ? convertImageDisplaySize({
541
- mode: "percent",
542
- value: dragValue
543
- }, size.mode, ctx) : size : {
544
- mode: "percent",
545
- value: dragValue
546
- };
547
- const previewPixels = ctx ? computeImageDisplayPixels(previewSize, {
548
- ...ctx,
549
- renderedWidth: dragValue !== null ? void 0 : ctx.renderedWidth,
550
- renderedHeight: dragValue !== null ? void 0 : ctx.renderedHeight
551
- }) : {};
552
- const previewPercent = dragValue ?? (ctx && previewPixels.widthPx ? sanitizeImageDisplayWidth(previewPixels.widthPx / ctx.containerWidth * 100) : committedPercent);
553
- const applyPreview = (next) => {
554
- const trigger = store.get(wrapperRefAtom).current;
555
- if (trigger) applyImageSizePreview(trigger, store.get(layoutAtom), next);
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 === size.mode) return;
525
+ if (to === mode) return;
559
526
  if (to === "auto") {
560
- handleSetDisplaySize({ mode: "auto" });
527
+ onChange({ mode: "auto" });
561
528
  return;
562
529
  }
563
- if (!ctx) return;
564
- handleSetDisplaySize(convertImageDisplaySize(size, to, ctx));
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: dragValue
573
- }, size.mode, ctx));
534
+ value: 100
535
+ });
574
536
  return;
575
537
  }
576
- if (dragValue === (displayWidth ?? SIZE_MAX)) return;
577
- handleSetDisplaySize({
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
- applyPreview({
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: next
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
- return /* @__PURE__ */ jsxs(Popover, {
602
- open: sizeOpen,
603
- onOpenChange: setSizeOpen,
604
- children: [/* @__PURE__ */ jsx(PopoverTrigger, {
605
- className: toolbarButtonClass$1,
606
- title: "Image size",
607
- onMouseDown: stopMouseDown$1,
608
- children: /* @__PURE__ */ jsx(Scaling, { size: 14 })
609
- }), /* @__PURE__ */ jsxs(PopoverPanel, {
610
- className: sizePanel,
611
- side: "bottom",
612
- sideOffset: 8,
613
- children: [/* @__PURE__ */ jsxs("div", {
614
- className: sizePanelRow,
615
- children: [
616
- /* @__PURE__ */ jsx("button", {
617
- className: `${sizeOption} ${size.mode === "auto" ? editToolbarButtonActive : ""}`.trim(),
618
- title: "Natural size",
619
- type: "button",
620
- onMouseDown: stopMouseDown$1,
621
- onClick: () => {
622
- handleSetDisplaySize({ mode: "auto" });
623
- setSizeOpen(false);
624
- },
625
- children: "Auto"
626
- }),
627
- MODE_BUTTONS.map(([mode, label, title]) => /* @__PURE__ */ jsx("button", {
628
- className: `${sizeOption} ${size.mode === mode ? editToolbarButtonActive : ""}`.trim(),
629
- title,
630
- type: "button",
631
- onClick: () => switchMode(mode),
632
- onMouseDown: stopMouseDown$1,
633
- children: label
634
- }, mode)),
635
- /* @__PURE__ */ jsxs("span", {
636
- className: sizeReadout,
637
- children: [size.mode === "fixed-width" || size.mode === "fixed-height" ? /* @__PURE__ */ jsx("input", {
638
- autoComplete: "off",
639
- className: `${sizeValueField} ${sizePxInput}`,
640
- inputMode: "numeric",
641
- spellCheck: false,
642
- "aria-label": size.mode === "fixed-width" ? "Image width in pixels" : "Image height in pixels",
643
- value: fixedDraft ?? (previewSize.mode === "fixed-width" || previewSize.mode === "fixed-height" ? String(previewSize.px) : String(fixedPx ?? "")),
644
- onMouseDown: (e) => e.stopPropagation(),
645
- onBlur: () => {
646
- const px = sanitizeImageFixedPx(Number(fixedDraft ?? fixedPx));
647
- if (px && (size.mode === "fixed-width" || size.mode === "fixed-height")) handleSetDisplaySize({
648
- mode: size.mode,
649
- px
650
- });
651
- setFixedDraft(null);
652
- },
653
- onChange: (event) => {
654
- setFixedDraft(event.target.value);
655
- const px = sanitizeImageFixedPx(Number(event.target.value));
656
- if (px && (size.mode === "fixed-width" || size.mode === "fixed-height")) applyPreview({
657
- mode: size.mode,
658
- px
659
- });
660
- }
661
- }) : /* @__PURE__ */ jsx("span", {
662
- className: sizeValueField,
663
- children: previewSize.mode === "percent" ? `${previewSize.value}%` : "Auto"
664
- }), /* @__PURE__ */ jsx("span", {
665
- className: sizeEquiv,
666
- children: previewSize.mode === "percent" ? previewPixels.widthPx ? `${previewPixels.widthPx}px` : "\xA0" : previewSize.mode === "auto" ? "\xA0" : previewPercent ? `${previewPercent}%` : "\xA0"
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
- const current = LAYOUT_OPTIONS.find((option) => option.value === layout) ?? LAYOUT_OPTIONS[0];
736
- return /* @__PURE__ */ jsxs(Popover, {
737
- open: layoutOpen,
738
- onOpenChange: setLayoutOpen,
739
- children: [/* @__PURE__ */ jsx(PopoverTrigger, {
740
- className: toolbarButtonClass,
741
- title: "Image layout",
742
- onMouseDown: stopMouseDown,
743
- children: /* @__PURE__ */ jsx(current.Icon, { size: 14 })
744
- }), /* @__PURE__ */ jsx(PopoverPanel, {
745
- className: controlPanel,
746
- side: "bottom",
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 replaceModeItems = [{
765
- value: "upload",
766
- label: "Upload"
767
- }, {
768
- value: "url",
769
- label: "URL"
770
- }];
771
- function ReplacePanel() {
772
- const replaceMode = useAtomValue(replaceModeAtom);
773
- const setReplaceMode = useSetAtom(replaceModeAtom);
774
- const replaceUrl = useAtomValue(replaceUrlAtom);
775
- const setReplaceUrl = useSetAtom(replaceUrlAtom);
776
- const replacePreview$1 = useAtomValue(replacePreviewAtom);
777
- const replaceError = useAtomValue(replaceErrorAtom);
778
- const replaceLoading = useAtomValue(replaceLoadingAtom);
779
- const fileInputRef = useAtomValue(fileInputRefAtom);
780
- const { handleReplaceFile, handlePreviewUrl, handleReplaceByUrl } = useImageActions();
781
- return /* @__PURE__ */ jsxs(Fragment, { children: [
782
- /* @__PURE__ */ jsx(SegmentedControl, {
783
- fullWidth: true,
784
- items: replaceModeItems,
785
- value: replaceMode,
786
- onChange: setReplaceMode
787
- }),
788
- replaceMode === "upload" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("input", {
789
- accept: "image/*",
790
- ref: fileInputRef,
791
- style: { display: "none" },
792
- type: "file",
793
- onChange: (event) => {
794
- const file = event.currentTarget.files?.[0] ?? null;
795
- handleReplaceFile(file);
796
- event.currentTarget.value = "";
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
- }), /* @__PURE__ */ jsx("button", {
799
- className: `${replaceUploadArea} ${semanticClassNames.replaceUploadArea}`,
800
- type: "button",
801
- onClick: () => fileInputRef.current?.click(),
802
- children: replaceLoading ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Loader2, { size: 16 }), /* @__PURE__ */ jsx("span", { children: "Uploading image..." })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Upload, { size: 16 }), /* @__PURE__ */ jsx("span", { children: "Click to upload image" })] })
803
- })] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
804
- className: `${editField} ${semanticClassNames.editField}`,
805
- children: [/* @__PURE__ */ jsx(ImageIcon, {
806
- className: `${editFieldIcon} ${semanticClassNames.editFieldIcon}`,
807
- size: 14
808
- }), /* @__PURE__ */ jsx("input", {
809
- className: `${editInput} ${semanticClassNames.editInput}`,
810
- placeholder: "https://example.com/image.jpg",
811
- type: "url",
812
- value: replaceUrl,
813
- onChange: (event) => setReplaceUrl(event.target.value),
814
- onKeyDown: (event) => {
815
- if (event.key === "Enter") {
816
- event.preventDefault();
817
- handlePreviewUrl();
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
- }), /* @__PURE__ */ jsxs(ActionBar, { children: [/* @__PURE__ */ jsx(ActionButton, {
822
- disabled: replaceLoading || !replaceUrl.trim(),
823
- onClick: handlePreviewUrl,
824
- children: "Preview"
825
- }), /* @__PURE__ */ jsx(ActionButton, {
826
- disabled: !replacePreview$1,
827
- variant: "accent",
828
- onClick: handleReplaceByUrl,
829
- children: "Apply"
830
- })] })] }),
831
- replaceError && /* @__PURE__ */ jsx("span", {
832
- className: `_1n94osfy ${semanticClassNames.panelHint}`,
833
- children: replaceError
834
- }),
835
- replacePreview$1 && /* @__PURE__ */ jsx("div", {
836
- className: `_1n94osfx ${semanticClassNames.replacePreview}`,
837
- children: /* @__PURE__ */ jsx("img", {
838
- alt: "Replace preview",
839
- src: replacePreview$1
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 metaOpen = useAtomValue(metaOpenAtom);
868
+ const selected = useImageSelected();
869
+ const [menuOpen, setMenuOpen] = useState(false);
850
870
  const setMetaOpen = useSetAtom(metaOpenAtom);
851
- const replaceOpen = useAtomValue(replaceOpenAtom);
852
- const src = useAtomValue(srcAtom);
853
- const altText = useAtomValue(altTextAtom);
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
- className: `${editToolbar} ${semanticClassNames.editToolbar} ${toolbarVisible ? `${editToolbarVisible} ${semanticClassNames.editToolbarVisible}` : ""}`,
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__ */ jsxs(Popover, {
884
- open: metaOpen,
885
- onOpenChange: (nextOpen) => {
886
- setMetaOpen(nextOpen);
887
- if (nextOpen) handleReplaceOpenChange(false);
888
- },
889
- children: [/* @__PURE__ */ jsx(PopoverTrigger, {
890
- className: `${editToolbarButton} ${semanticClassNames.editToolbarButton}`,
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: /* @__PURE__ */ jsx(ExternalLink, { size: 14 })
891
+ children: "Alt"
948
892
  }),
949
- /* @__PURE__ */ jsx("button", {
950
- className: `${editToolbarButton} ${semanticClassNames.editToolbarButton}`,
951
- title: "Download",
952
- type: "button",
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("button", {
961
- className: `${editToolbarButton} ${semanticClassNames.editToolbarButton} ${editToolbarButtonDanger} ${semanticClassNames.editToolbarButtonDanger}`,
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 [editor] = useLexicalComposerContext();
978
- const replaceOpen = useAtomValue(replaceOpenAtom);
979
- const replaceUrl = useAtomValue(replaceUrlAtom);
980
- const replacePreview = useAtomValue(replacePreviewAtom);
981
- const { handleReplaceOpenChange } = useImageActions();
982
- const handlePanelKeyDown = (event) => {
983
- const shortcut = getEditorHistoryShortcut(event, { isDirty: replaceUrl !== "" || replacePreview !== null });
984
- if (!shortcut) return;
985
- event.preventDefault();
986
- event.stopPropagation();
987
- editor.dispatchCommand(shortcut === "undo" ? UNDO_COMMAND : REDO_COMMAND, void 0);
988
- };
989
- return /* @__PURE__ */ jsxs(Popover, {
990
- open: replaceOpen,
991
- onOpenChange: handleReplaceOpenChange,
992
- children: [/* @__PURE__ */ jsxs(PopoverTrigger, {
993
- className: `${editPlaceholder} ${semanticClassNames.editPlaceholder}`,
994
- onMouseDown: (e) => {
995
- e.preventDefault();
996
- e.stopPropagation();
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