parent
7ab0c1aba8
commit
6428b59ccb
@ -0,0 +1,23 @@
|
||||
import { register } from "./register";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { deepCopyElement } from "../element/newElement";
|
||||
import { loadLibrary, saveLibrary } from "../data/localStorage";
|
||||
|
||||
export const actionAddToLibrary = register({
|
||||
name: "addToLibrary",
|
||||
perform: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
|
||||
loadLibrary().then((items) => {
|
||||
saveLibrary([...items, selectedElements.map(deepCopyElement)]);
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
contextMenuOrder: 6,
|
||||
contextItemLabel: "labels.addToLibrary",
|
||||
});
|
@ -0,0 +1,75 @@
|
||||
.library-unit {
|
||||
align-items: center;
|
||||
border: 1px solid #ccc;
|
||||
display: flex;
|
||||
height: 126px; // match width
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 126px; // exactly match the toolbar width when 3 are lined up + padding
|
||||
}
|
||||
|
||||
.library-unit__dragger {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.library-unit__dragger > svg {
|
||||
flex-grow: 1;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.library-unit__removeFromLibrary,
|
||||
.library-unit__removeFromLibrary:hover,
|
||||
.library-unit__removeFromLibrary:active {
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.library-unit__removeFromLibrary > svg {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.library-unit__pulse {
|
||||
transform: scale(1);
|
||||
animation: library-unit__pulse-animation 1s ease-in infinite;
|
||||
}
|
||||
|
||||
.library-unit__adder {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: -10px;
|
||||
margin-top: -10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.library-unit__active {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@keyframes library-unit__pulse-animation {
|
||||
0% {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { exportToSvg } from "../scene/export";
|
||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
||||
import { close } from "../components/icons";
|
||||
|
||||
import "./LibraryUnit.scss";
|
||||
import { t } from "../i18n";
|
||||
|
||||
// fa-plus
|
||||
const PLUS_ICON = (
|
||||
<svg viewBox="0 0 1792 1792">
|
||||
<path d="M1600 736v192q0 40-28 68t-68 28h-416v416q0 40-28 68t-68 28h-192q-40 0-68-28t-28-68v-416h-416q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h416v-416q0-40 28-68t68-28h192q40 0 68 28t28 68v416h416q40 0 68 28t28 68z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const LibraryUnit = ({
|
||||
elements,
|
||||
pendingElements,
|
||||
onRemoveFromLibrary,
|
||||
onClick,
|
||||
}: {
|
||||
elements?: NonDeleted<ExcalidrawElement>[];
|
||||
pendingElements?: NonDeleted<ExcalidrawElement>[];
|
||||
onRemoveFromLibrary: () => void;
|
||||
onClick: () => void;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
const elementsToRender = elements || pendingElements;
|
||||
if (!elementsToRender) {
|
||||
return;
|
||||
}
|
||||
const svg = exportToSvg(elementsToRender, {
|
||||
exportBackground: false,
|
||||
viewBackgroundColor: "#fff",
|
||||
shouldAddWatermark: false,
|
||||
});
|
||||
for (const child of ref.current!.children) {
|
||||
if (child.tagName !== "svg") {
|
||||
continue;
|
||||
}
|
||||
ref.current!.removeChild(child);
|
||||
}
|
||||
ref.current!.appendChild(svg);
|
||||
|
||||
const current = ref.current!;
|
||||
return () => {
|
||||
current.removeChild(svg);
|
||||
};
|
||||
}, [elements, pendingElements]);
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const adder = isHovered && pendingElements && (
|
||||
<div className="library-unit__adder">{PLUS_ICON}</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`library-unit ${
|
||||
elements || pendingElements ? "library-unit__active" : ""
|
||||
}`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div
|
||||
className={`library-unit__dragger ${
|
||||
!!pendingElements ? "library-unit__pulse" : ""
|
||||
}`}
|
||||
ref={ref}
|
||||
draggable={!!elements}
|
||||
onClick={!!elements || !!pendingElements ? onClick : undefined}
|
||||
onDragStart={(event) => {
|
||||
setIsHovered(false);
|
||||
event.dataTransfer.setData(
|
||||
"application/vnd.excalidraw.json",
|
||||
JSON.stringify(elements),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{adder}
|
||||
{elements && isHovered && (
|
||||
<button
|
||||
className="library-unit__removeFromLibrary"
|
||||
aria-label={t("labels.removeFromLibrary")}
|
||||
onClick={onRemoveFromLibrary}
|
||||
>
|
||||
{close}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue