Add image component for editor (#57)
* Add image component * More image editor * add more image changes
This commit is contained in:
@@ -3,6 +3,8 @@ import {
|
||||
AlignCenterIcon,
|
||||
AlignLeftIcon,
|
||||
AlignRightIcon,
|
||||
BoxSelectIcon,
|
||||
LinkIcon,
|
||||
ScanIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
@@ -16,7 +18,15 @@ import { Button } from "@unsend/ui/src/button";
|
||||
import { AllowedAlignments, ButtonOptions } from "../types";
|
||||
import { Separator } from "@unsend/ui/src/separator";
|
||||
import { BorderWidth } from "../components/ui/icons/BorderWidth";
|
||||
import { ColorPicker } from "../components/ui/ColorPicker";
|
||||
import { ColorPicker, ColorPickerPopup } from "../components/ui/ColorPicker";
|
||||
import { LinkEditorPanel } from "../components/panels/LinkEditorPanel";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipProvider,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@unsend/ui/src/tooltip";
|
||||
|
||||
const alignments: Array<AllowedAlignments> = ["left", "center", "right"];
|
||||
|
||||
@@ -33,6 +43,10 @@ export function ButtonComponent(props: NodeViewProps) {
|
||||
} = props.node.attrs as ButtonOptions;
|
||||
const { getPos, editor } = props;
|
||||
|
||||
const [editUrlOpen, setEditUrlOpen] = useState(false);
|
||||
|
||||
console.log(props);
|
||||
|
||||
return (
|
||||
<NodeViewWrapper
|
||||
className={`react-component ${
|
||||
@@ -47,8 +61,9 @@ export function ButtonComponent(props: NodeViewProps) {
|
||||
<Popover open={props.selected}>
|
||||
<PopoverTrigger asChild>
|
||||
<div>
|
||||
<button
|
||||
<div
|
||||
className={cn(
|
||||
"cursor-pointer",
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-white transition-colors disabled:pointer-events-none disabled:opacity-50",
|
||||
"h-10 px-4 py-2",
|
||||
"px-[32px] py-[20px] font-semibold no-underline"
|
||||
@@ -68,230 +83,241 @@ export function ButtonComponent(props: NodeViewProps) {
|
||||
editor.commands.setNodeSelection(pos);
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</button>
|
||||
<div className="relative flex max-w-full items-center">
|
||||
{props.selected}
|
||||
<div className="inset-0 flex items-center overflow-hidden ">
|
||||
<span
|
||||
className={cn(
|
||||
" cursor-text",
|
||||
props.selected ? "text-transparent" : ""
|
||||
)}
|
||||
>
|
||||
{text === "" ? "Button text" : text}
|
||||
</span>
|
||||
</div>
|
||||
{props.selected ? (
|
||||
<form className="absolute inset-x-[-4px] inset-y-0 flex items-center justify-center">
|
||||
<input
|
||||
type="text"
|
||||
value={text}
|
||||
onChange={(e) => {
|
||||
props.updateAttributes({
|
||||
text: e.target.value,
|
||||
});
|
||||
}}
|
||||
onBlur={() => {
|
||||
editor.commands.setNodeSelection(getPos());
|
||||
}}
|
||||
autoFocus
|
||||
className="w-full bg-transparent text-center outline-none"
|
||||
/>
|
||||
</form>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
side="top"
|
||||
className="space-y-2 light border-gray-200"
|
||||
className="space-y-2 w-[28rem] light border-gray-200 py-1 px-1"
|
||||
sideOffset={10}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<Input
|
||||
placeholder="Add text here"
|
||||
value={text}
|
||||
onChange={(e) => {
|
||||
props.updateAttributes({
|
||||
text: e.target.value,
|
||||
});
|
||||
}}
|
||||
className="light"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Add link here"
|
||||
value={url}
|
||||
onChange={(e) => {
|
||||
props.updateAttributes({
|
||||
url: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/* <div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="Button text"
|
||||
value={text}
|
||||
onChange={(e) => {
|
||||
props.updateAttributes({
|
||||
text: e.target.value,
|
||||
});
|
||||
}}
|
||||
className="light"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Add link here"
|
||||
value={url}
|
||||
onChange={(e) => {
|
||||
props.updateAttributes({
|
||||
url: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div> */}
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-xs text-gray-500 mt-4">Border</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex items-center border border-transparent focus-within:border-border gap-2 px-1 py-0.5 rounded-md">
|
||||
<ScanIcon className="text-slate-700 h-4 w-4" />
|
||||
<Input
|
||||
value={_radius}
|
||||
onChange={(e) =>
|
||||
props.updateAttributes({
|
||||
borderRadius: e.target.value,
|
||||
})
|
||||
}
|
||||
className="border-0 focus-visible:ring-0 h-6 p-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center border border-transparent focus-within:border-border gap-2 px-1 py-0.5 rounded-md">
|
||||
<BorderWidth className="text-slate-700 h-4 w-4" />
|
||||
<Input
|
||||
value={borderWidth}
|
||||
onChange={(e) =>
|
||||
props.updateAttributes({
|
||||
borderWidth: e.target.value,
|
||||
})
|
||||
}
|
||||
className="border-0 focus-visible:ring-0 h-6 p-0"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<TooltipProvider>
|
||||
<div className="flex gap-1 items-center">
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 mt-4 mb-2">Alignment</div>
|
||||
<div className="flex">
|
||||
{alignments.map((alignment) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className=""
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
props.updateAttributes({
|
||||
alignment,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<AlignmentIcon alignment={alignment} />
|
||||
</Button>
|
||||
<Tooltip key={alignment}>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
key={alignment}
|
||||
className=""
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
props.updateAttributes({
|
||||
alignment,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<AlignmentIcon alignment={alignment} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Align {alignment}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Separator orientation="vertical" className=" h-6 my-auto" />
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 mt-4 mb-2">Colors</div>
|
||||
<div className="flex gap-2">
|
||||
<BorderColorPickerPopup
|
||||
color={borderColor}
|
||||
onChange={(color) => {
|
||||
props.updateAttributes({
|
||||
borderColor: color,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<BackgroundColorPickerPopup
|
||||
color={buttonColor}
|
||||
onChange={(color) => {
|
||||
props.updateAttributes({
|
||||
buttonColor: color,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<TextColorPickerPopup
|
||||
color={textColor}
|
||||
onChange={(color) => {
|
||||
props.updateAttributes({
|
||||
textColor: color,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<div className="flex">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<ColorPickerPopup
|
||||
trigger={
|
||||
<div
|
||||
className="h-4 w-4 rounded border"
|
||||
style={{
|
||||
backgroundColor: buttonColor,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
color={buttonColor}
|
||||
onChange={(color) => {
|
||||
props.updateAttributes({
|
||||
buttonColor: color,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Background color</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<ColorPickerPopup
|
||||
trigger={
|
||||
<div className="flex flex-col items-center justify-center gap-[1px]">
|
||||
<span className="font-bolder font-mono text-xs text-slate-700">
|
||||
A
|
||||
</span>
|
||||
<div
|
||||
className="h-[2px] w-3"
|
||||
style={{ backgroundColor: textColor }}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
color={textColor}
|
||||
onChange={(color) => {
|
||||
props.updateAttributes({
|
||||
textColor: color,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Text color</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<Separator orientation="vertical" className=" h-6 my-auto" />
|
||||
<div>
|
||||
<div className="flex gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex items-center border border-transparent hover:border-border focus-within:border-border gap-1 px-1 py-0.5 rounded-md">
|
||||
<ScanIcon className="text-slate-700 h-4 w-4" />
|
||||
<Input
|
||||
value={_radius}
|
||||
onChange={(e) =>
|
||||
props.updateAttributes({
|
||||
borderRadius: e.target.value,
|
||||
})
|
||||
}
|
||||
className="border-0 focus-visible:ring-0 h-6 p-0 w-5"
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Border radius</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex items-center border border-transparent hover:border-border focus-within:border-border gap-1 px-1 py-0.5 rounded-md">
|
||||
<BorderWidth className="text-slate-700 h-4 w-4" />
|
||||
<Input
|
||||
value={borderWidth}
|
||||
onChange={(e) =>
|
||||
props.updateAttributes({
|
||||
borderWidth: e.target.value,
|
||||
})
|
||||
}
|
||||
className="border-0 focus-visible:ring-0 h-6 p-0 w-5"
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Border width</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<ColorPickerPopup
|
||||
trigger={
|
||||
<BoxSelectIcon
|
||||
className="h-4 w-4"
|
||||
style={{ color: borderColor }}
|
||||
/>
|
||||
}
|
||||
color={borderColor}
|
||||
onChange={(color) => {
|
||||
props.updateAttributes({
|
||||
borderColor: color,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Border color</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<Separator orientation="vertical" className=" h-6 my-auto" />
|
||||
<Popover open={editUrlOpen} onOpenChange={setEditUrlOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="px-2"
|
||||
size="sm"
|
||||
type="button"
|
||||
>
|
||||
Link
|
||||
<LinkIcon className="h-4 w-4 ml-2" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="light border-gray-200 px-4 py-2">
|
||||
<LinkEditorPanel
|
||||
initialUrl={url}
|
||||
onSetLink={(u) => {
|
||||
props.updateAttributes({
|
||||
url: u,
|
||||
});
|
||||
setEditUrlOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
// type ColorPickerProps = {
|
||||
// variant?: AllowedButtonVariant;
|
||||
// color: string;
|
||||
// onChange: (color: string) => void;
|
||||
// };
|
||||
|
||||
function BackgroundColorPickerPopup(props: ColorPickerProps) {
|
||||
const { color, onChange } = props;
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost" className="" size="sm" type="button">
|
||||
<div
|
||||
className="h-4 w-4 rounded border"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full rounded-none border-0 !bg-transparent !p-0 shadow-none drop-shadow-md">
|
||||
<ColorPicker
|
||||
color={color}
|
||||
onChange={(newColor) => {
|
||||
// HACK: This is a workaround for a bug in tiptap
|
||||
// https://github.com/ueberdosis/tiptap/issues/3580
|
||||
//
|
||||
// ERROR: flushSync was called from inside a lifecycle
|
||||
//
|
||||
// To fix this, we need to make sure that the onChange
|
||||
// callback is run after the current execution context.
|
||||
queueMicrotask(() => {
|
||||
onChange(newColor);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function TextColorPickerPopup(props: ColorPickerProps) {
|
||||
const { color, onChange } = props;
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost" size="sm" type="button">
|
||||
<div className="flex flex-col items-center justify-center gap-[1px]">
|
||||
<span className="font-bolder font-mono text-xs text-slate-700">
|
||||
A
|
||||
</span>
|
||||
<div className="h-[2px] w-3" style={{ backgroundColor: color }} />
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full rounded-none border-0 !bg-transparent !p-0 shadow-none drop-shadow-md">
|
||||
<ColorPicker
|
||||
color={color}
|
||||
onChange={(color) => {
|
||||
queueMicrotask(() => {
|
||||
onChange(color);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
type ColorPickerProps = {
|
||||
color: string;
|
||||
onChange: (color: string) => void;
|
||||
};
|
||||
|
||||
function BorderColorPickerPopup(props: ColorPickerProps) {
|
||||
const { color, onChange } = props;
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost" className="" size="sm" type="button">
|
||||
<BorderWidth className="h-4 w-4" style={{ color: color }} />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-full rounded-none border-0 !bg-transparent !p-0 shadow-none drop-shadow-md">
|
||||
<ColorPicker
|
||||
color={color}
|
||||
onChange={(newColor) => {
|
||||
// HACK: This is a workaround for a bug in tiptap
|
||||
// https://github.com/ueberdosis/tiptap/issues/3580
|
||||
//
|
||||
// ERROR: flushSync was called from inside a lifecycle
|
||||
//
|
||||
// To fix this, we need to make sure that the onChange
|
||||
// callback is run after the current execution context.
|
||||
queueMicrotask(() => {
|
||||
onChange(newColor);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
const AlignmentIcon = ({ alignment }: { alignment: AllowedAlignments }) => {
|
||||
if (alignment === "left") {
|
||||
return <AlignLeftIcon className="h-4 w-4" />;
|
||||
|
412
packages/email-editor/src/nodes/image-resize.tsx
Normal file
412
packages/email-editor/src/nodes/image-resize.tsx
Normal file
@@ -0,0 +1,412 @@
|
||||
import { NodeViewProps } from "@tiptap/core";
|
||||
import { CSSProperties, useRef, useState } from "react";
|
||||
import { useEvent } from "../hooks/useEvent";
|
||||
import { NodeViewWrapper } from "@tiptap/react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@unsend/ui/src/popover";
|
||||
import {
|
||||
ExpandIcon,
|
||||
ScanIcon,
|
||||
LinkIcon,
|
||||
ImageIcon,
|
||||
TypeIcon,
|
||||
} from "lucide-react";
|
||||
import { Input } from "@unsend/ui/src/input";
|
||||
import { BorderWidth } from "../components/ui/icons/BorderWidth";
|
||||
import { ColorPickerPopup } from "../components/ui/ColorPicker";
|
||||
import { AllowedAlignments } from "../types";
|
||||
import { Button } from "@unsend/ui/src/button";
|
||||
import { AlignmentIcon } from "../components/ui/icons/AlignmentIcon";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipProvider,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@unsend/ui/src/tooltip";
|
||||
import { Separator } from "@unsend/ui/src/separator";
|
||||
import { LinkEditorPanel } from "../components/panels/LinkEditorPanel";
|
||||
import { TextEditorPanel } from "../components/panels/TextEditorPanel";
|
||||
|
||||
const alignments: Array<AllowedAlignments> = ["left", "center", "right"];
|
||||
|
||||
const MIN_WIDTH = 60;
|
||||
|
||||
export function ResizableImageTemplate(props: NodeViewProps) {
|
||||
const { node, updateAttributes, selected } = props;
|
||||
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
const [resizingStyle, setResizingStyle] = useState<
|
||||
Pick<CSSProperties, "width" | "height"> | undefined
|
||||
>();
|
||||
|
||||
let {
|
||||
alignment = "center",
|
||||
width,
|
||||
height,
|
||||
borderRadius,
|
||||
borderWidth,
|
||||
borderColor,
|
||||
src,
|
||||
} = node.attrs || {};
|
||||
|
||||
const [widthState, setWidthState] = useState<string>(width.toString());
|
||||
const [openLink, setOpenLink] = useState(false);
|
||||
const [openImgSrc, setOpenImgSrc] = useState(false);
|
||||
const [openAltText, setOpenAltText] = useState(false);
|
||||
|
||||
const handleMouseDown = useEvent(
|
||||
(event: React.MouseEvent<HTMLDivElement>) => {
|
||||
const imageParent = document.querySelector(
|
||||
".ProseMirror-selectednode"
|
||||
) as HTMLDivElement;
|
||||
|
||||
if (!imgRef.current || !imageParent || !selected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageParentWidth = imageParent.offsetWidth;
|
||||
|
||||
event.preventDefault();
|
||||
const direction = event.currentTarget.dataset.direction || "--";
|
||||
const initialXPosition = event.clientX;
|
||||
const currentWidth = imgRef.current.width;
|
||||
const currentHeight = imgRef.current.height;
|
||||
let newWidth = currentWidth;
|
||||
let newHeight = currentHeight;
|
||||
const transform = direction === "left" ? -1 : 1;
|
||||
|
||||
const removeListeners = () => {
|
||||
window.removeEventListener("mousemove", mouseMoveHandler);
|
||||
window.removeEventListener("mouseup", removeListeners);
|
||||
updateAttributes({ width: newWidth, height: newHeight });
|
||||
setResizingStyle(undefined);
|
||||
};
|
||||
|
||||
const mouseMoveHandler = (event: MouseEvent) => {
|
||||
newWidth = Math.max(
|
||||
currentWidth + transform * (event.clientX - initialXPosition),
|
||||
MIN_WIDTH
|
||||
);
|
||||
|
||||
if (newWidth > imageParentWidth) {
|
||||
newWidth = imageParentWidth;
|
||||
}
|
||||
|
||||
newHeight = (newWidth / currentWidth) * currentHeight;
|
||||
|
||||
setResizingStyle({ width: newWidth, height: newHeight });
|
||||
setWidthState(newWidth.toString());
|
||||
// If mouse is up, remove event listeners
|
||||
if (!event.buttons) {
|
||||
return removeListeners();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", mouseMoveHandler);
|
||||
window.addEventListener("mouseup", removeListeners);
|
||||
}
|
||||
);
|
||||
|
||||
const updateWidth = (_newWidth: string) => {
|
||||
setWidthState(_newWidth.toString());
|
||||
if (!imgRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageParent = document.querySelector(
|
||||
".ProseMirror-selectednode"
|
||||
) as HTMLDivElement;
|
||||
|
||||
const imageParentWidth = imageParent.offsetWidth;
|
||||
|
||||
const currentWidth = imgRef.current.width;
|
||||
const currentHeight = imgRef.current.height;
|
||||
|
||||
let newWidth = Number(_newWidth);
|
||||
|
||||
newWidth = newWidth > 59 ? newWidth : 60;
|
||||
|
||||
if (newWidth > imageParentWidth) {
|
||||
newWidth = imageParentWidth;
|
||||
}
|
||||
|
||||
const newHeight = (newWidth / currentWidth) * currentHeight;
|
||||
|
||||
setResizingStyle({
|
||||
width: newWidth > 59 ? newWidth : 60,
|
||||
height: newHeight,
|
||||
});
|
||||
};
|
||||
|
||||
function dragButton(direction: "left" | "right") {
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onMouseDown={handleMouseDown}
|
||||
data-direction={direction}
|
||||
className=" bg-white bg-opacity-40 border rounded-3xl"
|
||||
style={{
|
||||
position: "absolute",
|
||||
height: "60px",
|
||||
width: "7px",
|
||||
[direction]: 5,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
cursor: "ew-resize",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
externalLink,
|
||||
alt,
|
||||
borderRadius: _br,
|
||||
borderColor: _bc,
|
||||
...attrs
|
||||
} = node.attrs || {};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper
|
||||
as="div"
|
||||
draggable
|
||||
data-drag-handle
|
||||
style={{
|
||||
width,
|
||||
height,
|
||||
borderRadius: Number(borderRadius),
|
||||
borderWidth: Number(borderWidth),
|
||||
borderColor,
|
||||
...resizingStyle,
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
// Weird! Basically tiptap/prose wraps this in a span and the line height causes an annoying buffer.
|
||||
lineHeight: "0px",
|
||||
display: "block",
|
||||
...({
|
||||
center: { marginLeft: "auto", marginRight: "auto" },
|
||||
left: { marginRight: "auto" },
|
||||
right: { marginLeft: "auto" },
|
||||
}[alignment as string] || {}),
|
||||
}}
|
||||
>
|
||||
<Popover open={props.selected}>
|
||||
<PopoverTrigger>
|
||||
<img
|
||||
{...attrs}
|
||||
ref={imgRef}
|
||||
style={{
|
||||
...resizingStyle,
|
||||
cursor: "default",
|
||||
marginBottom: 0,
|
||||
}}
|
||||
/>
|
||||
{selected && (
|
||||
<>
|
||||
{dragButton("left")}
|
||||
{dragButton("right")}
|
||||
</>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="start"
|
||||
side="top"
|
||||
className="light border-gray-200 px-2 py-2 w-[32rem]"
|
||||
sideOffset={10}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<TooltipProvider>
|
||||
<div className="flex items-center gap-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex items-center border border-transparent focus-within:border-border gap-2 px-1 py-0.5 rounded-md">
|
||||
<ExpandIcon className="text-slate-700 h-4 w-4" />
|
||||
<Input
|
||||
value={widthState}
|
||||
onChange={(e) => updateWidth(e.target.value)}
|
||||
className="border-0 focus-visible:ring-0 h-6 p-0 w-8"
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Width</TooltipContent>
|
||||
</Tooltip>
|
||||
<Separator orientation="vertical" className="h-6 my-auto" />
|
||||
{alignments.map((alignment) => (
|
||||
<Tooltip key={alignment}>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
key={alignment}
|
||||
className=""
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
props.updateAttributes({
|
||||
alignment,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<AlignmentIcon alignment={alignment} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Align {alignment}</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
<Separator orientation="vertical" className="h-6 my-auto" />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex items-center border border-transparent focus-within:border-border gap-2 px-1 py-0.5 rounded-md">
|
||||
<ScanIcon className="text-slate-700 h-4 w-4" />
|
||||
<Input
|
||||
value={borderRadius}
|
||||
onChange={(e) =>
|
||||
props.updateAttributes({
|
||||
borderRadius: e.target.value,
|
||||
})
|
||||
}
|
||||
className="border-0 focus-visible:ring-0 h-6 p-0 w-5"
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Border radius</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="flex items-center border border-transparent focus-within:border-border gap-2 px-1 py-0.5 rounded-md">
|
||||
<BorderWidth className="text-slate-700 h-4 w-4" />
|
||||
<Input
|
||||
value={borderWidth}
|
||||
onChange={(e) =>
|
||||
props.updateAttributes({
|
||||
borderWidth: e.target.value,
|
||||
})
|
||||
}
|
||||
className="border-0 focus-visible:ring-0 h-6 p-0 w-5"
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Border width</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<ColorPickerPopup
|
||||
trigger={
|
||||
<div
|
||||
className="h-4 w-4 rounded border"
|
||||
style={{
|
||||
backgroundColor: borderColor,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
color={borderColor}
|
||||
onChange={(color) => {
|
||||
props.updateAttributes({
|
||||
borderColor: color,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Border color</TooltipContent>
|
||||
</Tooltip>
|
||||
<Separator orientation="vertical" className="h-6 my-auto" />
|
||||
<Popover open={openImgSrc} onOpenChange={setOpenImgSrc}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="px-2"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => setOpenImgSrc(true)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<ImageIcon className="h-4 w-4 " />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Image source</TooltipContent>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="light border-gray-200 px-4 py-2">
|
||||
<LinkEditorPanel
|
||||
initialUrl={src}
|
||||
onSetLink={(u) => {
|
||||
props.updateAttributes({
|
||||
src: u,
|
||||
});
|
||||
setOpenImgSrc(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Popover open={openAltText} onOpenChange={setOpenAltText}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="px-2"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => setOpenAltText(true)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<TypeIcon className="h-4 w-4 " />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Alt text</TooltipContent>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="light border-gray-200 px-4 py-2">
|
||||
<TextEditorPanel
|
||||
initialText={alt}
|
||||
onSetInitialText={(t) => {
|
||||
props.updateAttributes({
|
||||
alt: t,
|
||||
});
|
||||
setOpenAltText(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Popover open={openLink} onOpenChange={setOpenLink}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="px-2"
|
||||
size="sm"
|
||||
type="button"
|
||||
onClick={() => setOpenLink(true)}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<LinkIcon className="h-4 w-4 " />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Link</TooltipContent>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="light border-gray-200 px-4 py-2">
|
||||
<LinkEditorPanel
|
||||
initialUrl={externalLink}
|
||||
onSetLink={(u) => {
|
||||
props.updateAttributes({
|
||||
externalLink: u,
|
||||
});
|
||||
setOpenLink(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user