From 257f697a98b3f86cdf87e467a0159a9d4a02c125 Mon Sep 17 00:00:00 2001 From: Timur Khazamov Date: Tue, 7 Jan 2020 07:50:59 +0500 Subject: [PATCH] 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 a415db92..0431b7e7 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} + + + ); +} + +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 00000000..522f7b35 --- /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 2cdc5465..f16a59cc 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 43f157f1..969174ea 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; }