From 5ade8987e4b38ecebe70f4a4400432644e3347ee Mon Sep 17 00:00:00 2001 From: Faustino Kialungila Date: Mon, 6 Jan 2020 20:28:14 +0100 Subject: [PATCH 1/7] Fixes pasting colors in color picker (#215) * improve lozenge dimensions * fix pasting colors in color picker input --- src/components/ColorPicker.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx index 65dfc78b5..a415db92e 100644 --- a/src/components/ColorPicker.tsx +++ b/src/components/ColorPicker.tsx @@ -45,6 +45,7 @@ export function ColorPicker({ type="text" className="swatch-input" value={color || ""} + onPaste={e => onChange(e.clipboardData.getData("text"))} onChange={e => onChange(e.target.value)} /> From b12ea7de3e29c33640e7133edada40b04dd15a55 Mon Sep 17 00:00:00 2001 From: Abhishek Kulshrestha Date: Tue, 7 Jan 2020 01:06:48 +0530 Subject: [PATCH 2/7] paste inside the viewport (#214) --- src/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 32f4e314a..832b3d95c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -319,8 +319,8 @@ class App extends React.Component<{}, AppState> { ) { clearSelection(elements); parsedElements.forEach(parsedElement => { - parsedElement.x += 10; - parsedElement.y += 10; + parsedElement.x = 10 - this.state.scrollX; + parsedElement.y = 10 - this.state.scrollY; parsedElement.seed = randomSeed(); generateDraw(parsedElement); elements.push(parsedElement); From 1443cf1cd59b5efdfd9f8bf3918fdf7d0432b1f2 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Mon, 6 Jan 2020 21:19:21 +0100 Subject: [PATCH 3/7] implement shift+resize for all sides (#210) --- src/index.tsx | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 832b3d95c..90be31e50 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -712,19 +712,33 @@ class App extends React.Component<{}, AppState> { switch (resizeHandle) { case "nw": element.width += element.x - lastX; - element.height += element.y - lastY; element.x = lastX; - element.y = lastY; + if (e.shiftKey) { + element.y += element.height - element.width; + element.height = element.width; + } else { + element.height += element.y - lastY; + element.y = lastY; + } break; case "ne": element.width = lastX - element.x; - element.height += element.y - lastY; - element.y = lastY; + if (e.shiftKey) { + element.y += element.height - element.width; + element.height = element.width; + } else { + element.height += element.y - lastY; + element.y = lastY; + } break; case "sw": element.width += element.x - lastX; element.x = lastX; - element.height = lastY - element.y; + if (e.shiftKey) { + element.height = element.width; + } else { + element.height = lastY - element.y; + } break; case "se": element.width += x - lastX; From 8a91f4fe7bb2f3d6bf84ecdc8962c8abfd0f5078 Mon Sep 17 00:00:00 2001 From: Christopher Chedeau Date: Mon, 6 Jan 2020 13:57:04 -0800 Subject: [PATCH 4/7] One more testimonial (#221) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1e3d1e930..73a258af8 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Go to https://excalidraw.com to start sketching + + ## Run the code From 9305a33dba419ab646cd7d289ee27effbb184eb5 Mon Sep 17 00:00:00 2001 From: Faustino Kialungila Date: Mon, 6 Jan 2020 23:22:48 +0100 Subject: [PATCH 5/7] Copy and paste styles (#219) * copy and paste styles * save copied styles in memory --- src/index.tsx | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/index.tsx b/src/index.tsx index 90be31e50..2cdc5465c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -55,6 +55,8 @@ const KEYS = { BACKSPACE: "Backspace" }; +let COPIED_STYLES: string = "{}"; + function isArrowKey(keyCode: string) { return ( keyCode === KEYS.ARROW_LEFT || @@ -192,7 +194,6 @@ class App extends React.Component<{}, AppState> { } else if (event.metaKey && event.shiftKey && event.code === "KeyF") { this.moveAllRight(); event.preventDefault(); - // Select all: Cmd-A } else if (event.metaKey && event.code === "KeyA") { elements.forEach(element => { @@ -212,6 +213,29 @@ class App extends React.Component<{}, AppState> { } this.forceUpdate(); 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(); } }; From 9fe3fe5091a338371261865a707b5227973e9c1c Mon Sep 17 00:00:00 2001 From: Ben Kraft Date: Mon, 6 Jan 2020 15:42:37 -0800 Subject: [PATCH 6/7] Fix URL in README (#222) Looks like excalidraw.com is an SSL error, but www.excalidraw.com works great. It's possible the github pages config could be changed to make both work, but this seemed easier to fix. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 73a258af8..cb9ebe6a9 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ## Try it now -Go to https://excalidraw.com to start sketching +Go to https://www.excalidraw.com to start sketching ## Testimonials From 257f697a98b3f86cdf87e467a0159a9d4a02c125 Mon Sep 17 00:00:00 2001 From: Timur Khazamov Date: Tue, 7 Jan 2020 07:50:59 +0500 Subject: [PATCH 7/7] Context menu with some commands (#217) --- src/components/ColorPicker.tsx | 6 +- src/components/ContextMenu.css | 34 +++++++++ src/components/ContextMenu.tsx | 85 +++++++++++++++++++++++ src/components/Popover.tsx | 24 +++++++ src/index.tsx | 123 +++++++++++++++++++++++++++------ src/styles.scss | 3 +- 6 files changed, 250 insertions(+), 25 deletions(-) create mode 100644 src/components/ContextMenu.css create mode 100644 src/components/ContextMenu.tsx create mode 100644 src/components/Popover.tsx diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx index a415db92e..0431b7e77 100644 --- a/src/components/ColorPicker.tsx +++ b/src/components/ColorPicker.tsx @@ -1,5 +1,6 @@ import React from "react"; import { TwitterPicker } from "react-color"; +import { Popover } from "./Popover"; export function ColorPicker({ color, @@ -17,8 +18,7 @@ export function ColorPicker({ onClick={() => setActive(!isActive)} /> {isActive ? ( -
-
setActive(false)} /> + setActive(false)}> -
+ ) : null} +
    e.preventDefault()}> + {options.map((option, idx) => ( +
  • + +
  • + ))} +
+ + ); +} + +function ContextMenuOption({ label, action }: ContextMenuOption) { + return ( + + ); +} + +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(); + params.options.forEach(option => { + if (option) { + options.push(option); + } + }); + if (options.length) { + render( + , + getContextMenuNode() + ); + } + } +}; diff --git a/src/components/Popover.tsx b/src/components/Popover.tsx new file mode 100644 index 000000000..522f7b351 --- /dev/null +++ b/src/components/Popover.tsx @@ -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 ( +
+
{ + e.preventDefault(); + if (onCloseRequest) onCloseRequest(); + }} + /> + {children} +
+ ); +} diff --git a/src/index.tsx b/src/index.tsx index 2cdc5465c..f16a59cc2 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -36,6 +36,7 @@ import { SHAPES, findShapeByKey, shapesShortcutKeys } from "./shapes"; import { createHistory } from "./history"; import "./styles.scss"; +import ContextMenu from "./components/ContextMenu"; const { elements } = createScene(); const { history } = createHistory(); @@ -147,8 +148,7 @@ class App extends React.Component<{}, AppState> { this.forceUpdate(); event.preventDefault(); } else if (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) { - deleteSelectedElements(elements); - this.forceUpdate(); + this.deleteSelectedElements(); event.preventDefault(); } else if (isArrowKey(event.key)) { const step = event.shiftKey @@ -307,6 +307,23 @@ class App extends React.Component<{}, AppState> { 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() { const canvasWidth = window.innerWidth - CANVAS_WINDOW_OFFSET_LEFT; const canvasHeight = window.innerHeight - CANVAS_WINDOW_OFFSET_TOP; @@ -332,25 +349,7 @@ class App extends React.Component<{}, AppState> { }} onPaste={e => { const paste = e.clipboardData.getData("text"); - 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); - 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(); - } + this.addElementsFromPaste(paste); e.preventDefault(); }} > @@ -577,6 +576,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 => { if (lastMouseUp !== null) { // Unfortunately, sometimes we don't get a mouseup after a mousedown, @@ -942,6 +989,40 @@ class App extends React.Component<{}, AppState> { })); }; + 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() { renderScene(elements, rc, canvas, { scrollX: this.state.scrollX, diff --git a/src/styles.scss b/src/styles.scss index 43f157f1c..969174ea9 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -149,7 +149,8 @@ button { border-color: #d6d4d4; } - &:active, &.active { + &:active, + &.active { background-color: #bdbebc; border-color: #bdbebc; }