ソースを参照

Center element on paste (#248)

* Center element on paste

* paste on cursor position

* correctly center elements

* rename vars
Faustino Kialungila 5 年 前
コミット
1ea72e9134
4 ファイル変更75 行追加29 行削除
  1. 0 1
      README.md
  2. 72 25
      src/index.tsx
  3. 1 3
      src/scene/comparisons.ts
  4. 2 0
      src/types.ts

+ 0 - 1
README.md

@@ -21,7 +21,6 @@ Go to https://www.excalidraw.com to start sketching
 
 <a href="https://twitter.com/lucasazzola/status/1215126440330416128"><img width="429" src="https://user-images.githubusercontent.com/197597/72039003-48e99580-3258-11ea-8daa-85dd055f2a82.png">
 
-
 ## Run the code
 
 ### Code Sandbox

+ 72 - 25
src/index.tsx

@@ -10,7 +10,8 @@ import {
   duplicateElement,
   resizeTest,
   isTextElement,
-  textWysiwyg
+  textWysiwyg,
+  getElementAbsoluteCoords
 } from "./element";
 import {
   clearSelection,
@@ -115,6 +116,7 @@ export class App extends React.Component<{}, AppState> {
 
   public componentDidMount() {
     document.addEventListener("keydown", this.onKeyDown, false);
+    document.addEventListener("mousemove", this.getCurrentCursorPosition);
     window.addEventListener("resize", this.onResize, false);
 
     const savedState = restoreFromLocalStorage(elements);
@@ -125,6 +127,11 @@ export class App extends React.Component<{}, AppState> {
 
   public componentWillUnmount() {
     document.removeEventListener("keydown", this.onKeyDown, false);
+    document.removeEventListener(
+      "mousemove",
+      this.getCurrentCursorPosition,
+      false
+    );
     window.removeEventListener("resize", this.onResize, false);
   }
 
@@ -139,6 +146,8 @@ export class App extends React.Component<{}, AppState> {
     viewBackgroundColor: "#ffffff",
     scrollX: 0,
     scrollY: 0,
+    cursorX: 0,
+    cursorY: 0,
     name: DEFAULT_PROJECT_NAME
   };
 
@@ -146,6 +155,10 @@ export class App extends React.Component<{}, AppState> {
     this.forceUpdate();
   };
 
+  private getCurrentCursorPosition = (e: MouseEvent) => {
+    this.setState({ cursorX: e.x, cursorY: e.y });
+  };
+
   private onKeyDown = (event: KeyboardEvent) => {
     if (isInputLike(event.target)) return;
 
@@ -263,7 +276,7 @@ export class App extends React.Component<{}, AppState> {
         element.fillStyle = pastedElement?.fillStyle;
         element.opacity = pastedElement?.opacity;
         element.roughness = pastedElement?.roughness;
-        if(isTextElement(element)) {
+        if (isTextElement(element)) {
           element.font = pastedElement?.font;
           this.redrawTextBoundingBox(element);
         }
@@ -331,11 +344,11 @@ export class App extends React.Component<{}, AppState> {
     }
   };
 
-  private pasteFromClipboard = (x?: number, y?: number) => {
+  private pasteFromClipboard = () => {
     if (navigator.clipboard) {
       navigator.clipboard
         .readText()
-        .then(text => this.addElementsFromPaste(text, x, y));
+        .then(text => this.addElementsFromPaste(text));
     }
   };
 
@@ -345,7 +358,7 @@ export class App extends React.Component<{}, AppState> {
     element.height = metrics.height;
     element.baseline = metrics.baseline;
     this.forceUpdate();
-  }
+  };
 
   public render() {
     const canvasWidth = window.innerWidth - CANVAS_WINDOW_OFFSET_LEFT;
@@ -480,39 +493,45 @@ export class App extends React.Component<{}, AppState> {
                 <h5>Font size</h5>
                 <ButtonSelect
                   options={[
-                    { value: 16, text: "Small" },                    
+                    { value: 16, text: "Small" },
                     { value: 20, text: "Medium" },
                     { value: 28, text: "Large" },
                     { value: 36, text: "Very Large" }
                   ]}
                   value={getSelectedAttribute(
                     elements,
-                    element => isTextElement(element) && +element.font.split("px ")[0]
+                    element =>
+                      isTextElement(element) && +element.font.split("px ")[0]
                   )}
                   onChange={value =>
                     this.changeProperty(element => {
-                      if(isTextElement(element)) {
-                        element.font = `${value}px ${element.font.split("px ")[1]}`;
+                      if (isTextElement(element)) {
+                        element.font = `${value}px ${
+                          element.font.split("px ")[1]
+                        }`;
                         this.redrawTextBoundingBox(element);
                       }
                     })
                   }
                 />
                 <h5>Font familly</h5>
-                <ButtonSelect 
+                <ButtonSelect
                   options={[
-                    {value: "Virgil", text: "Virgil"},
-                    {value: "Helvetica", text: "Helvetica"},
-                    {value: "Courier", text: "Courier"},
+                    { value: "Virgil", text: "Virgil" },
+                    { value: "Helvetica", text: "Helvetica" },
+                    { value: "Courier", text: "Courier" }
                   ]}
                   value={getSelectedAttribute(
                     elements,
-                    element => isTextElement(element) && element.font.split("px ")[1]
+                    element =>
+                      isTextElement(element) && element.font.split("px ")[1]
                   )}
                   onChange={value =>
                     this.changeProperty(element => {
-                      if(isTextElement(element)) {
-                        element.font = `${element.font.split("px ")[0]}px ${value}`;
+                      if (isTextElement(element)) {
+                        element.font = `${
+                          element.font.split("px ")[0]
+                        }px ${value}`;
                         this.redrawTextBoundingBox(element);
                       }
                     })
@@ -609,7 +628,7 @@ export class App extends React.Component<{}, AppState> {
                 options: [
                   navigator.clipboard && {
                     label: "Paste",
-                    action: () => this.pasteFromClipboard(x, y)
+                    action: () => this.pasteFromClipboard()
                   }
                 ],
                 top: e.clientY,
@@ -632,7 +651,7 @@ export class App extends React.Component<{}, AppState> {
                 },
                 navigator.clipboard && {
                   label: "Paste",
-                  action: () => this.pasteFromClipboard(x, y)
+                  action: () => this.pasteFromClipboard()
                 },
                 { label: "Copy Styles", action: this.copyStyles },
                 { label: "Paste Styles", action: this.pasteStyles },
@@ -1105,7 +1124,7 @@ export class App extends React.Component<{}, AppState> {
     }));
   };
 
-  private addElementsFromPaste = (paste: string, x?: number, y?: number) => {
+  private addElementsFromPaste = (paste: string) => {
     let parsedElements;
     try {
       parsedElements = JSON.parse(paste);
@@ -1117,19 +1136,47 @@ export class App extends React.Component<{}, AppState> {
     ) {
       clearSelection(elements);
 
-      if (x == null) x = 10 - this.state.scrollX;
-      if (y == null) y = 10 - this.state.scrollY;
+      let subCanvasX1 = Infinity;
+      let subCanvasX2 = 0;
+      let subCanvasY1 = Infinity;
+      let subCanvasY2 = 0;
+
       const minX = Math.min(...parsedElements.map(element => element.x));
       const minY = Math.min(...parsedElements.map(element => element.y));
-      const dx = x - minX;
-      const dy = y - minY;
+
+      const distance = (x: number, y: number) => {
+        return Math.abs(x > y ? x - y : y - x);
+      };
+
+      parsedElements.forEach(parsedElement => {
+        const [x1, y1, x2, y2] = getElementAbsoluteCoords(parsedElement);
+        subCanvasX1 = Math.min(subCanvasX1, x1);
+        subCanvasY1 = Math.min(subCanvasY1, y1);
+        subCanvasX2 = Math.max(subCanvasX2, x2);
+        subCanvasY2 = Math.max(subCanvasY2, y2);
+      });
+
+      const elementsCenterX = distance(subCanvasX1, subCanvasX2) / 2;
+      const elementsCenterY = distance(subCanvasY1, subCanvasY2) / 2;
+
+      const dx =
+        this.state.cursorX -
+        this.state.scrollX -
+        CANVAS_WINDOW_OFFSET_LEFT -
+        elementsCenterX;
+      const dy =
+        this.state.cursorY -
+        this.state.scrollY -
+        CANVAS_WINDOW_OFFSET_TOP -
+        elementsCenterY;
 
       parsedElements.forEach(parsedElement => {
         const duplicate = duplicateElement(parsedElement);
-        duplicate.x += dx;
-        duplicate.y += dy;
+        duplicate.x += dx - minX;
+        duplicate.y += dy - minY;
         elements.push(duplicate);
       });
+
       this.forceUpdate();
     }
   };

+ 1 - 3
src/scene/comparisons.ts

@@ -22,9 +22,7 @@ export const hasStroke = (elements: ExcalidrawElement[]) =>
   );
 
 export const hasText = (elements: ExcalidrawElement[]) =>
-  elements.some(
-    element => element.isSelected && element.type === "text"
-  );
+  elements.some(element => element.isSelected && element.type === "text");
 
 export function getElementAtPosition(
   elements: ExcalidrawElement[],

+ 2 - 0
src/types.ts

@@ -11,5 +11,7 @@ export type AppState = {
   viewBackgroundColor: string;
   scrollX: number;
   scrollY: number;
+  cursorX: number;
+  cursorY: number;
   name: string;
 };