mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Merge branch 'master' into save-state-in-url
This commit is contained in:
commit
4a9c601eec
9 changed files with 375 additions and 61 deletions
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
## Try it now
|
## Try it now
|
||||||
|
|
||||||
Go to https://excalidraw.com to start sketching
|
Go to https://www.excalidraw.com to start sketching
|
||||||
|
|
||||||
## Testimonials
|
## Testimonials
|
||||||
|
|
||||||
|
@ -15,6 +15,8 @@ Go to https://excalidraw.com to start sketching
|
||||||
|
|
||||||
<a href="https://twitter.com/dan_abramov/status/1213762494428262400"><img width="398" src="https://user-images.githubusercontent.com/197597/71783990-4d395880-2fa3-11ea-9ad7-186138db5003.png"></a>
|
<a href="https://twitter.com/dan_abramov/status/1213762494428262400"><img width="398" src="https://user-images.githubusercontent.com/197597/71783990-4d395880-2fa3-11ea-9ad7-186138db5003.png"></a>
|
||||||
|
|
||||||
|
<a href="https://twitter.com/kyehohenberger/status/1214288572037025792"><img width="423" src="https://user-images.githubusercontent.com/197597/71851802-34f13880-308c-11ea-9416-191099e6349c.png"></a>
|
||||||
|
|
||||||
|
|
||||||
## Run the code
|
## Run the code
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { TwitterPicker } from "react-color";
|
import { TwitterPicker } from "react-color";
|
||||||
|
import { Popover } from "./Popover";
|
||||||
|
|
||||||
export function ColorPicker({
|
export function ColorPicker({
|
||||||
color,
|
color,
|
||||||
|
@ -17,8 +18,7 @@ export function ColorPicker({
|
||||||
onClick={() => setActive(!isActive)}
|
onClick={() => setActive(!isActive)}
|
||||||
/>
|
/>
|
||||||
{isActive ? (
|
{isActive ? (
|
||||||
<div className="popover">
|
<Popover onCloseRequest={() => setActive(false)}>
|
||||||
<div className="cover" onClick={() => setActive(false)} />
|
|
||||||
<TwitterPicker
|
<TwitterPicker
|
||||||
colors={[
|
colors={[
|
||||||
"#000000",
|
"#000000",
|
||||||
|
@ -39,7 +39,7 @@ export function ColorPicker({
|
||||||
onChange(changedColor.hex);
|
onChange(changedColor.hex);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Popover>
|
||||||
) : null}
|
) : null}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
34
src/components/ContextMenu.css
Normal file
34
src/components/ContextMenu.css
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
.context-menu {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0px 3px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu__option {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-option {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-option:focus {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu__option:first-child .context-menu-option {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu__option:last-child .context-menu-option {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
}
|
85
src/components/ContextMenu.tsx
Normal file
85
src/components/ContextMenu.tsx
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import React from "react";
|
||||||
|
import { Popover } from "./Popover";
|
||||||
|
import { render, unmountComponentAtNode } from "react-dom";
|
||||||
|
|
||||||
|
import "./ContextMenu.css";
|
||||||
|
|
||||||
|
type ContextMenuOption = {
|
||||||
|
label: string;
|
||||||
|
action(): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
options: ContextMenuOption[];
|
||||||
|
onCloseRequest?(): void;
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ContextMenu({ options, onCloseRequest, top, left }: Props) {
|
||||||
|
return (
|
||||||
|
<Popover onCloseRequest={onCloseRequest} top={top} left={left}>
|
||||||
|
<ul className="context-menu" onContextMenu={e => e.preventDefault()}>
|
||||||
|
{options.map((option, idx) => (
|
||||||
|
<li
|
||||||
|
key={idx}
|
||||||
|
className="context-menu__option"
|
||||||
|
onClick={onCloseRequest}
|
||||||
|
>
|
||||||
|
<ContextMenuOption {...option} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextMenuOption({ label, action }: ContextMenuOption) {
|
||||||
|
return (
|
||||||
|
<button className="context-menu-option" onClick={action}>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let contextMenuNode: HTMLDivElement;
|
||||||
|
function getContextMenuNode(): HTMLDivElement {
|
||||||
|
if (contextMenuNode) {
|
||||||
|
return contextMenuNode;
|
||||||
|
}
|
||||||
|
const div = document.createElement("div");
|
||||||
|
document.body.appendChild(div);
|
||||||
|
return (contextMenuNode = div);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContextMenuParams = {
|
||||||
|
options: (ContextMenuOption | false | null | undefined)[];
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
unmountComponentAtNode(getContextMenuNode());
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
push(params: ContextMenuParams) {
|
||||||
|
const options = Array.of<ContextMenuOption>();
|
||||||
|
params.options.forEach(option => {
|
||||||
|
if (option) {
|
||||||
|
options.push(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (options.length) {
|
||||||
|
render(
|
||||||
|
<ContextMenu
|
||||||
|
top={params.top}
|
||||||
|
left={params.left}
|
||||||
|
options={options}
|
||||||
|
onCloseRequest={handleClose}
|
||||||
|
/>,
|
||||||
|
getContextMenuNode()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
24
src/components/Popover.tsx
Normal file
24
src/components/Popover.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
top?: number;
|
||||||
|
left?: number;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
onCloseRequest?(): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Popover({ children, left, onCloseRequest, top }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="popover" style={{ top: top, left: left }}>
|
||||||
|
<div
|
||||||
|
className="cover"
|
||||||
|
onClick={onCloseRequest}
|
||||||
|
onContextMenu={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (onCloseRequest) onCloseRequest();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -13,3 +13,4 @@ export { hitTest } from "./collision";
|
||||||
export { resizeTest } from "./resizeTest";
|
export { resizeTest } from "./resizeTest";
|
||||||
export { generateDraw } from "./generateDraw";
|
export { generateDraw } from "./generateDraw";
|
||||||
export { isTextElement } from "./typeChecks";
|
export { isTextElement } from "./typeChecks";
|
||||||
|
export { textWysiwyg } from "./textWysiwyg";
|
||||||
|
|
54
src/element/textWysiwyg.tsx
Normal file
54
src/element/textWysiwyg.tsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { KEYS } from "../index";
|
||||||
|
|
||||||
|
export function textWysiwyg(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
onSubmit: (text: string) => void
|
||||||
|
) {
|
||||||
|
const input = document.createElement("input");
|
||||||
|
|
||||||
|
Object.assign(input.style, {
|
||||||
|
position: "absolute",
|
||||||
|
top: y - 8 + "px",
|
||||||
|
left: x + "px",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
boxShadow: "none",
|
||||||
|
textAlign: "center",
|
||||||
|
width: (window.innerWidth - x) * 2 + "px",
|
||||||
|
fontSize: "20px",
|
||||||
|
fontFamily: "Virgil",
|
||||||
|
border: "none",
|
||||||
|
background: "transparent"
|
||||||
|
});
|
||||||
|
|
||||||
|
input.onkeydown = ev => {
|
||||||
|
if (ev.key === KEYS.ESCAPE) {
|
||||||
|
cleanup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ev.key === KEYS.ENTER) {
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.onblur = handleSubmit;
|
||||||
|
|
||||||
|
function stopEvent(ev: Event) {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSubmit() {
|
||||||
|
if (input.value) {
|
||||||
|
onSubmit(input.value);
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
window.removeEventListener("wheel", stopEvent, true);
|
||||||
|
document.body.removeChild(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("wheel", stopEvent, true);
|
||||||
|
document.body.appendChild(input);
|
||||||
|
input.focus();
|
||||||
|
}
|
225
src/index.tsx
225
src/index.tsx
|
@ -4,7 +4,13 @@ import rough from "roughjs/bin/wrappers/rough";
|
||||||
|
|
||||||
import { moveOneLeft, moveAllLeft, moveOneRight, moveAllRight } from "./zindex";
|
import { moveOneLeft, moveAllLeft, moveOneRight, moveAllRight } from "./zindex";
|
||||||
import { randomSeed } from "./random";
|
import { randomSeed } from "./random";
|
||||||
import { newElement, resizeTest, generateDraw, isTextElement } from "./element";
|
import {
|
||||||
|
newElement,
|
||||||
|
resizeTest,
|
||||||
|
generateDraw,
|
||||||
|
isTextElement,
|
||||||
|
textWysiwyg
|
||||||
|
} from "./element";
|
||||||
import {
|
import {
|
||||||
renderScene,
|
renderScene,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
|
@ -38,6 +44,7 @@ import { SHAPES, findShapeByKey, shapesShortcutKeys } from "./shapes";
|
||||||
import { createHistory } from "./history";
|
import { createHistory } from "./history";
|
||||||
|
|
||||||
import "./styles.scss";
|
import "./styles.scss";
|
||||||
|
import ContextMenu from "./components/ContextMenu";
|
||||||
|
|
||||||
const { elements } = createScene();
|
const { elements } = createScene();
|
||||||
const { history } = createHistory();
|
const { history } = createHistory();
|
||||||
|
@ -47,16 +54,23 @@ const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`;
|
||||||
const CANVAS_WINDOW_OFFSET_LEFT = 250;
|
const CANVAS_WINDOW_OFFSET_LEFT = 250;
|
||||||
const CANVAS_WINDOW_OFFSET_TOP = 0;
|
const CANVAS_WINDOW_OFFSET_TOP = 0;
|
||||||
|
|
||||||
const KEYS = {
|
export const KEYS = {
|
||||||
ARROW_LEFT: "ArrowLeft",
|
ARROW_LEFT: "ArrowLeft",
|
||||||
ARROW_RIGHT: "ArrowRight",
|
ARROW_RIGHT: "ArrowRight",
|
||||||
ARROW_DOWN: "ArrowDown",
|
ARROW_DOWN: "ArrowDown",
|
||||||
ARROW_UP: "ArrowUp",
|
ARROW_UP: "ArrowUp",
|
||||||
ESCAPE: "Escape",
|
ESCAPE: "Escape",
|
||||||
|
ENTER: "Enter",
|
||||||
DELETE: "Delete",
|
DELETE: "Delete",
|
||||||
BACKSPACE: "Backspace"
|
BACKSPACE: "Backspace"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const META_KEY = /Mac|iPod|iPhone|iPad/.test(window.navigator.platform)
|
||||||
|
? "metaKey"
|
||||||
|
: "ctrlKey";
|
||||||
|
|
||||||
|
let COPIED_STYLES: string = "{}";
|
||||||
|
|
||||||
function isArrowKey(keyCode: string) {
|
function isArrowKey(keyCode: string) {
|
||||||
return (
|
return (
|
||||||
keyCode === KEYS.ARROW_LEFT ||
|
keyCode === KEYS.ARROW_LEFT ||
|
||||||
|
@ -70,9 +84,8 @@ function resetCursor() {
|
||||||
document.documentElement.style.cursor = "";
|
document.documentElement.style.cursor = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function addTextElement(element: ExcalidrawTextElement) {
|
function addTextElement(element: ExcalidrawTextElement, text = "") {
|
||||||
resetCursor();
|
resetCursor();
|
||||||
const text = prompt("What text do you want?");
|
|
||||||
if (text === null || text === "") {
|
if (text === null || text === "") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -148,8 +161,7 @@ class App extends React.Component<{}, AppState> {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) {
|
} else if (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) {
|
||||||
deleteSelectedElements(elements);
|
this.deleteSelectedElements();
|
||||||
this.forceUpdate();
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (isArrowKey(event.key)) {
|
} else if (isArrowKey(event.key)) {
|
||||||
const step = event.shiftKey
|
const step = event.shiftKey
|
||||||
|
@ -168,7 +180,7 @@ class App extends React.Component<{}, AppState> {
|
||||||
|
|
||||||
// Send backward: Cmd-Shift-Alt-B
|
// Send backward: Cmd-Shift-Alt-B
|
||||||
} else if (
|
} else if (
|
||||||
event.metaKey &&
|
event[META_KEY] &&
|
||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
event.altKey &&
|
event.altKey &&
|
||||||
event.code === "KeyB"
|
event.code === "KeyB"
|
||||||
|
@ -177,13 +189,13 @@ class App extends React.Component<{}, AppState> {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Send to back: Cmd-Shift-B
|
// Send to back: Cmd-Shift-B
|
||||||
} else if (event.metaKey && event.shiftKey && event.code === "KeyB") {
|
} else if (event[META_KEY] && event.shiftKey && event.code === "KeyB") {
|
||||||
this.moveAllLeft();
|
this.moveAllLeft();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Bring forward: Cmd-Shift-Alt-F
|
// Bring forward: Cmd-Shift-Alt-F
|
||||||
} else if (
|
} else if (
|
||||||
event.metaKey &&
|
event[META_KEY] &&
|
||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
event.altKey &&
|
event.altKey &&
|
||||||
event.code === "KeyF"
|
event.code === "KeyF"
|
||||||
|
@ -192,12 +204,11 @@ class App extends React.Component<{}, AppState> {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Bring to front: Cmd-Shift-F
|
// Bring to front: Cmd-Shift-F
|
||||||
} else if (event.metaKey && event.shiftKey && event.code === "KeyF") {
|
} else if (event[META_KEY] && event.shiftKey && event.code === "KeyF") {
|
||||||
this.moveAllRight();
|
this.moveAllRight();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Select all: Cmd-A
|
// Select all: Cmd-A
|
||||||
} else if (event.metaKey && event.code === "KeyA") {
|
} else if (event[META_KEY] && event.code === "KeyA") {
|
||||||
elements.forEach(element => {
|
elements.forEach(element => {
|
||||||
element.isSelected = true;
|
element.isSelected = true;
|
||||||
});
|
});
|
||||||
|
@ -205,7 +216,7 @@ class App extends React.Component<{}, AppState> {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (shapesShortcutKeys.includes(event.key.toLowerCase())) {
|
} else if (shapesShortcutKeys.includes(event.key.toLowerCase())) {
|
||||||
this.setState({ elementType: findShapeByKey(event.key) });
|
this.setState({ elementType: findShapeByKey(event.key) });
|
||||||
} else if (event.metaKey && event.code === "KeyZ") {
|
} else if (event[META_KEY] && event.code === "KeyZ") {
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
// Redo action
|
// Redo action
|
||||||
history.redoOnce(elements);
|
history.redoOnce(elements);
|
||||||
|
@ -215,6 +226,29 @@ class App extends React.Component<{}, AppState> {
|
||||||
}
|
}
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
// Copy Styles: Cmd-Shift-C
|
||||||
|
} else if (event.metaKey && event.shiftKey && event.code === "KeyC") {
|
||||||
|
const element = elements.find(el => el.isSelected);
|
||||||
|
if (element) {
|
||||||
|
COPIED_STYLES = JSON.stringify(element);
|
||||||
|
}
|
||||||
|
// Paste Styles: Cmd-Shift-V
|
||||||
|
} else if (event.metaKey && event.shiftKey && event.code === "KeyV") {
|
||||||
|
const pastedElement = JSON.parse(COPIED_STYLES);
|
||||||
|
if (pastedElement.type) {
|
||||||
|
elements.forEach(element => {
|
||||||
|
if (element.isSelected) {
|
||||||
|
element.backgroundColor = pastedElement?.backgroundColor;
|
||||||
|
element.strokeWidth = pastedElement?.strokeWidth;
|
||||||
|
element.strokeColor = pastedElement?.strokeColor;
|
||||||
|
element.fillStyle = pastedElement?.fillStyle;
|
||||||
|
element.opacity = pastedElement?.opacity;
|
||||||
|
generateDraw(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.forceUpdate();
|
||||||
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -286,6 +320,23 @@ class App extends React.Component<{}, AppState> {
|
||||||
this.setState({ currentItemBackgroundColor: color });
|
this.setState({ currentItemBackgroundColor: color });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private copyToClipboard = () => {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
const text = JSON.stringify(
|
||||||
|
elements.filter(element => element.isSelected)
|
||||||
|
);
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private pasteFromClipboard = (x?: number, y?: number) => {
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard
|
||||||
|
.readText()
|
||||||
|
.then(text => this.addElementsFromPaste(text, x, y));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const canvasWidth = window.innerWidth - CANVAS_WINDOW_OFFSET_LEFT;
|
const canvasWidth = window.innerWidth - CANVAS_WINDOW_OFFSET_LEFT;
|
||||||
const canvasHeight = window.innerHeight - CANVAS_WINDOW_OFFSET_TOP;
|
const canvasHeight = window.innerHeight - CANVAS_WINDOW_OFFSET_TOP;
|
||||||
|
@ -311,25 +362,7 @@ class App extends React.Component<{}, AppState> {
|
||||||
}}
|
}}
|
||||||
onPaste={e => {
|
onPaste={e => {
|
||||||
const paste = e.clipboardData.getData("text");
|
const paste = e.clipboardData.getData("text");
|
||||||
let parsedElements;
|
this.addElementsFromPaste(paste);
|
||||||
try {
|
|
||||||
parsedElements = JSON.parse(paste);
|
|
||||||
} catch (e) {}
|
|
||||||
if (
|
|
||||||
Array.isArray(parsedElements) &&
|
|
||||||
parsedElements.length > 0 &&
|
|
||||||
parsedElements[0].type // need to implement a better check here...
|
|
||||||
) {
|
|
||||||
clearSelection(elements);
|
|
||||||
parsedElements.forEach(parsedElement => {
|
|
||||||
parsedElement.x = 10 - this.state.scrollX;
|
|
||||||
parsedElement.y = 10 - this.state.scrollY;
|
|
||||||
parsedElement.seed = randomSeed();
|
|
||||||
generateDraw(parsedElement);
|
|
||||||
elements.push(parsedElement);
|
|
||||||
});
|
|
||||||
this.forceUpdate();
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -556,6 +589,54 @@ class App extends React.Component<{}, AppState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onContextMenu={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const x =
|
||||||
|
e.clientX - CANVAS_WINDOW_OFFSET_LEFT - this.state.scrollX;
|
||||||
|
const y = e.clientY - CANVAS_WINDOW_OFFSET_TOP - this.state.scrollY;
|
||||||
|
|
||||||
|
const element = getElementAtPosition(elements, x, y);
|
||||||
|
if (!element) {
|
||||||
|
ContextMenu.push({
|
||||||
|
options: [
|
||||||
|
navigator.clipboard && {
|
||||||
|
label: "Paste",
|
||||||
|
action: () => this.pasteFromClipboard(x, y)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
top: e.clientY,
|
||||||
|
left: e.clientX
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!element.isSelected) {
|
||||||
|
clearSelection(elements);
|
||||||
|
element.isSelected = true;
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextMenu.push({
|
||||||
|
options: [
|
||||||
|
navigator.clipboard && {
|
||||||
|
label: "Copy",
|
||||||
|
action: this.copyToClipboard
|
||||||
|
},
|
||||||
|
navigator.clipboard && {
|
||||||
|
label: "Paste",
|
||||||
|
action: () => this.pasteFromClipboard(x, y)
|
||||||
|
},
|
||||||
|
{ label: "Delete", action: this.deleteSelectedElements },
|
||||||
|
{ label: "Move Forward", action: this.moveOneRight },
|
||||||
|
{ label: "Send to Front", action: this.moveAllRight },
|
||||||
|
{ label: "Move Backwards", action: this.moveOneLeft },
|
||||||
|
{ label: "Send to Back", action: this.moveAllLeft }
|
||||||
|
],
|
||||||
|
top: e.clientY,
|
||||||
|
left: e.clientX
|
||||||
|
});
|
||||||
|
}}
|
||||||
onMouseDown={e => {
|
onMouseDown={e => {
|
||||||
if (lastMouseUp !== null) {
|
if (lastMouseUp !== null) {
|
||||||
// Unfortunately, sometimes we don't get a mouseup after a mousedown,
|
// Unfortunately, sometimes we don't get a mouseup after a mousedown,
|
||||||
|
@ -656,22 +737,23 @@ class App extends React.Component<{}, AppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
if (!addTextElement(element)) {
|
textWysiwyg(e.clientX, e.clientY, text => {
|
||||||
return;
|
addTextElement(element, text);
|
||||||
}
|
generateDraw(element);
|
||||||
|
elements.push(element);
|
||||||
|
element.isSelected = true;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
draggingElement: null,
|
||||||
|
elementType: "selection"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateDraw(element);
|
generateDraw(element);
|
||||||
elements.push(element);
|
elements.push(element);
|
||||||
if (this.state.elementType === "text") {
|
this.setState({ draggingElement: element });
|
||||||
this.setState({
|
|
||||||
draggingElement: null,
|
|
||||||
elementType: "selection"
|
|
||||||
});
|
|
||||||
element.isSelected = true;
|
|
||||||
} else {
|
|
||||||
this.setState({ draggingElement: element });
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastX = x;
|
let lastX = x;
|
||||||
let lastY = y;
|
let lastY = y;
|
||||||
|
@ -892,20 +974,17 @@ class App extends React.Component<{}, AppState> {
|
||||||
100
|
100
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!addTextElement(element as ExcalidrawTextElement)) {
|
textWysiwyg(e.clientX, e.clientY, text => {
|
||||||
return;
|
addTextElement(element as ExcalidrawTextElement, text);
|
||||||
}
|
generateDraw(element);
|
||||||
|
elements.push(element);
|
||||||
|
element.isSelected = true;
|
||||||
|
|
||||||
generateDraw(element);
|
this.setState({
|
||||||
elements.push(element);
|
draggingElement: null,
|
||||||
|
elementType: "selection"
|
||||||
this.setState({
|
});
|
||||||
draggingElement: null,
|
|
||||||
elementType: "selection"
|
|
||||||
});
|
});
|
||||||
element.isSelected = true;
|
|
||||||
|
|
||||||
this.forceUpdate();
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -926,6 +1005,40 @@ class App extends React.Component<{}, AppState> {
|
||||||
saveToURL(elements, this.state);
|
saveToURL(elements, this.state);
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
|
private addElementsFromPaste = (paste: string, x?: number, y?: number) => {
|
||||||
|
let parsedElements;
|
||||||
|
try {
|
||||||
|
parsedElements = JSON.parse(paste);
|
||||||
|
} catch (e) {}
|
||||||
|
if (
|
||||||
|
Array.isArray(parsedElements) &&
|
||||||
|
parsedElements.length > 0 &&
|
||||||
|
parsedElements[0].type // need to implement a better check here...
|
||||||
|
) {
|
||||||
|
clearSelection(elements);
|
||||||
|
|
||||||
|
let dx: number;
|
||||||
|
let dy: number;
|
||||||
|
if (x) {
|
||||||
|
let minX = Math.min(...parsedElements.map(element => element.x));
|
||||||
|
dx = x - minX;
|
||||||
|
}
|
||||||
|
if (y) {
|
||||||
|
let minY = Math.min(...parsedElements.map(element => element.y));
|
||||||
|
dy = y - minY;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedElements.forEach(parsedElement => {
|
||||||
|
parsedElement.x = dx ? parsedElement.x + dx : 10 - this.state.scrollX;
|
||||||
|
parsedElement.y = dy ? parsedElement.y + dy : 10 - this.state.scrollY;
|
||||||
|
parsedElement.seed = randomSeed();
|
||||||
|
generateDraw(parsedElement);
|
||||||
|
elements.push(parsedElement);
|
||||||
|
});
|
||||||
|
this.forceUpdate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
renderScene(elements, rc, canvas, {
|
renderScene(elements, rc, canvas, {
|
||||||
scrollX: this.state.scrollX,
|
scrollX: this.state.scrollX,
|
||||||
|
|
|
@ -149,7 +149,8 @@ button {
|
||||||
border-color: #d6d4d4;
|
border-color: #d6d4d4;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active, &.active {
|
&:active,
|
||||||
|
&.active {
|
||||||
background-color: #bdbebc;
|
background-color: #bdbebc;
|
||||||
border-color: #bdbebc;
|
border-color: #bdbebc;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue