Browse Source

PoC: Expose wysiwyg element to manipulate from outside (#1356)

* expose wysiwyg element to manipulate from outside

* keep focus after changing style

* update editingElement correctly

* remove mistake

* update text only

* proper check for element

* udpate snapshots

* add error log

* remove try catch handler

* remove blur event

* add proper types

* merge if condition

* simplify if condition

Co-Authored-By: Lipis <lipiridis@gmail.com>

Co-authored-by: dwelle <luzar.david@gmail.com>
Co-authored-by: Lipis <lipiridis@gmail.com>
Co-authored-by: Fausto95 <faustino.kialungila@gmail.com>
Kostas Bariotis 5 years ago
parent
commit
5e2f164026

+ 4 - 1
src/actions/actionProperties.tsx

@@ -27,7 +27,10 @@ const changeProperty = (
   callback: (element: ExcalidrawElement) => ExcalidrawElement,
 ) => {
   return elements.map((element) => {
-    if (appState.selectedElementIds[element.id]) {
+    if (
+      appState.selectedElementIds[element.id] ||
+      element.id === appState.editingElement?.id
+    ) {
       return callback(element);
     }
     return element;

+ 1 - 0
src/appState.ts

@@ -8,6 +8,7 @@ export const DEFAULT_TEXT_ALIGN = "left";
 
 export function getDefaultAppState(): AppState {
   return {
+    wysiwygElement: null,
     isLoading: false,
     errorMessage: null,
     draggingElement: null,

+ 21 - 5
src/components/App.tsx

@@ -25,6 +25,7 @@ import {
   getElementWithResizeHandler,
   canResizeMutlipleElements,
   getResizeHandlerFromCoords,
+  isNonDeletedElement,
 } from "../element";
 import {
   deleteSelectedElements,
@@ -269,19 +270,31 @@ export class App extends React.Component<any, AppState> {
     if (this.unmounted) {
       return;
     }
+
+    let editingElement: AppState["editingElement"] | null = null;
     if (res.elements) {
+      res.elements.forEach((element) => {
+        if (
+          this.state.editingElement?.id === element.id &&
+          this.state.editingElement !== element &&
+          isNonDeletedElement(element)
+        ) {
+          editingElement = element;
+        }
+      });
       globalSceneState.replaceAllElements(res.elements);
       if (res.commitToHistory) {
         history.resumeRecording();
       }
     }
 
-    if (res.appState) {
+    if (res.appState || editingElement) {
       if (res.commitToHistory) {
         history.resumeRecording();
       }
       this.setState((state) => ({
         ...res.appState,
+        editingElement: editingElement || state.editingElement,
         isCollaborating: state.isCollaborating,
         collaborators: state.collaborators,
       }));
@@ -1186,9 +1199,6 @@ export class App extends React.Component<any, AppState> {
       });
     };
 
-    // deselect all other elements when inserting text
-    this.setState({ selectedElementIds: {} });
-
     const deleteElement = () => {
       globalSceneState.replaceAllElements([
         ...globalSceneState.getElementsIncludingDeleted().map((_element) => {
@@ -1216,7 +1226,7 @@ export class App extends React.Component<any, AppState> {
       ]);
     };
 
-    textWysiwyg({
+    const wysiwygElement = textWysiwyg({
       x,
       y,
       initText: element.text,
@@ -1236,6 +1246,7 @@ export class App extends React.Component<any, AppState> {
       onSubmit: withBatchedUpdates((text) => {
         updateElement(text);
         this.setState((prevState) => ({
+          wysiwygElement: null,
           selectedElementIds: {
             ...prevState.selectedElementIds,
             [element.id]: true,
@@ -1255,6 +1266,8 @@ export class App extends React.Component<any, AppState> {
         resetSelection();
       }),
     });
+    // deselect all other elements when inserting text
+    this.setState({ selectedElementIds: {}, wysiwygElement });
 
     // do an initial update to re-initialize element position since we were
     //  modifying element's x/y for sake of editor (case: syncing to remote)
@@ -1564,6 +1577,9 @@ export class App extends React.Component<any, AppState> {
   private handleCanvasPointerDown = (
     event: React.PointerEvent<HTMLCanvasElement>,
   ) => {
+    if (this.state.wysiwygElement && this.state.wysiwygElement.submit) {
+      this.state.wysiwygElement.submit();
+    }
     if (lastPointerUp !== null) {
       // Unfortunately, sometimes we don't get a pointerup after a pointerdown,
       // this can happen when a contextual menu or alert is triggered. In order to avoid

+ 11 - 1
src/element/index.ts

@@ -1,4 +1,8 @@
-import { ExcalidrawElement, NonDeletedExcalidrawElement } from "./types";
+import {
+  ExcalidrawElement,
+  NonDeletedExcalidrawElement,
+  NonDeleted,
+} from "./types";
 import { isInvisiblySmallElement } from "./sizeHelpers";
 
 export {
@@ -68,3 +72,9 @@ export function getNonDeletedElements(elements: readonly ExcalidrawElement[]) {
     readonly NonDeletedExcalidrawElement[]
   );
 }
+
+export function isNonDeletedElement<T extends ExcalidrawElement>(
+  element: T,
+): element is NonDeleted<T> {
+  return !element.isDeleted;
+}

+ 10 - 3
src/element/textWysiwyg.tsx

@@ -1,5 +1,6 @@
 import { KEYS } from "../keys";
 import { selectNode } from "../utils";
+import { WysiwigElement } from "./types";
 
 function trimText(text: string) {
   // whitespace only → trim all because we'd end up inserting invisible element
@@ -40,7 +41,7 @@ export function textWysiwyg({
   textAlign,
   onSubmit,
   onCancel,
-}: TextWysiwygParams) {
+}: TextWysiwygParams): WysiwigElement {
   const editable = document.createElement("div");
   try {
     editable.contentEditable = "plaintext-only";
@@ -120,7 +121,6 @@ export function textWysiwyg({
       event.stopPropagation();
     }
   };
-  editable.onblur = handleSubmit;
 
   function stopEvent(event: Event) {
     event.stopPropagation();
@@ -137,7 +137,6 @@ export function textWysiwyg({
 
   function cleanup() {
     // remove events to ensure they don't late-fire
-    editable.onblur = null;
     editable.onpaste = null;
     editable.oninput = null;
     editable.onkeydown = null;
@@ -150,4 +149,12 @@ export function textWysiwyg({
   document.body.appendChild(editable);
   editable.focus();
   selectNode(editable);
+
+  return {
+    submit: handleSubmit,
+    changeStyle: (style: any) => {
+      Object.assign(editable.style, style);
+      editable.focus();
+    },
+  };
 }

+ 5 - 0
src/element/types.ts

@@ -68,3 +68,8 @@ export type ResizeArrowFnType = (
   pointerY: number,
   perfect: boolean,
 ) => void;
+
+export type WysiwigElement = {
+  submit: () => void;
+  changeStyle: (style: Record<string, any>) => void;
+};

+ 13 - 0
src/renderer/renderScene.ts

@@ -14,6 +14,7 @@ import {
   handlerRectangles,
   getCommonBounds,
   canResizeMutlipleElements,
+  isTextElement,
 } from "../element";
 
 import { roundRect } from "./roundRect";
@@ -103,6 +104,18 @@ export function renderScene(
     return { atLeastOneVisibleElement: false };
   }
 
+  if (
+    appState.wysiwygElement?.changeStyle &&
+    isTextElement(appState.editingElement)
+  ) {
+    appState.wysiwygElement.changeStyle({
+      font: appState.editingElement.font,
+      textAlign: appState.editingElement.textAlign,
+      color: appState.editingElement.strokeColor,
+      opacity: appState.editingElement.opacity,
+    });
+  }
+
   const context = canvas.getContext("2d")!;
   context.scale(scale, scale);
 

+ 67 - 1
src/tests/__snapshots__/regressionTests.test.tsx.snap

@@ -41,6 +41,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -240,6 +241,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -358,6 +360,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -633,6 +636,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -793,6 +797,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -993,6 +998,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -1252,6 +1258,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -1603,7 +1610,32 @@ Object {
   "cursorX": 0,
   "cursorY": 0,
   "draggingElement": null,
-  "editingElement": null,
+  "editingElement": Object {
+    "angle": 0,
+    "backgroundColor": "transparent",
+    "fillStyle": "hachure",
+    "height": 0,
+    "id": "id6",
+    "isDeleted": false,
+    "lastCommittedPoint": null,
+    "opacity": 100,
+    "points": Array [
+      Array [
+        0,
+        0,
+      ],
+    ],
+    "roughness": 1,
+    "seed": 845789479,
+    "strokeColor": "#000000",
+    "strokeWidth": 1,
+    "type": "line",
+    "version": 6,
+    "versionNonce": 745419401,
+    "width": 0,
+    "x": 30,
+    "y": 30,
+  },
   "elementLocked": false,
   "elementType": "selection",
   "errorMessage": null,
@@ -1626,6 +1658,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -2250,6 +2283,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -2368,6 +2402,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -2486,6 +2521,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -2604,6 +2640,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -2744,6 +2781,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -2884,6 +2922,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -3024,6 +3063,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -3142,6 +3182,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -3260,6 +3301,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -3400,6 +3442,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -3518,6 +3561,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -3590,6 +3634,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -4475,6 +4520,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -4899,6 +4945,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -5230,6 +5277,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -5472,6 +5520,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -5645,6 +5694,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -6481,6 +6531,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -7208,6 +7259,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -7830,6 +7882,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -8352,6 +8405,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -8824,6 +8878,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -9201,6 +9256,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -9487,6 +9543,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -9702,6 +9759,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -10594,6 +10652,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -11375,6 +11434,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -12049,6 +12109,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -12616,6 +12677,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -12994,6 +13056,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -13050,6 +13113,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -13106,6 +13170,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;
@@ -13402,6 +13467,7 @@ Object {
   "showShortcutsDialog": false,
   "username": "",
   "viewBackgroundColor": "#ffffff",
+  "wysiwygElement": null,
   "zoom": 1,
 }
 `;

+ 2 - 0
src/types.ts

@@ -4,6 +4,7 @@ import {
   NonDeletedExcalidrawElement,
   NonDeleted,
   TextAlign,
+  WysiwigElement,
 } from "./element/types";
 import { SHAPES } from "./shapes";
 import { Point as RoughPoint } from "roughjs/bin/geometry";
@@ -12,6 +13,7 @@ export type FlooredNumber = number & { _brand: "FlooredNumber" };
 export type Point = Readonly<RoughPoint>;
 
 export type AppState = {
+  wysiwygElement: WysiwigElement | null;
   isLoading: boolean;
   errorMessage: string | null;
   draggingElement: NonDeletedExcalidrawElement | null;