From 5e2f164026769fe1ab8484f200e0b6d98503ae8c Mon Sep 17 00:00:00 2001 From: Kostas Bariotis Date: Sat, 11 Apr 2020 17:10:56 +0100 Subject: [PATCH] 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 Co-authored-by: dwelle Co-authored-by: Lipis Co-authored-by: Fausto95 --- src/actions/actionProperties.tsx | 5 +- src/appState.ts | 1 + src/components/App.tsx | 26 +++++-- src/element/index.ts | 12 +++- src/element/textWysiwyg.tsx | 13 +++- src/element/types.ts | 5 ++ src/renderer/renderScene.ts | 13 ++++ .../regressionTests.test.tsx.snap | 68 ++++++++++++++++++- src/types.ts | 2 + 9 files changed, 134 insertions(+), 11 deletions(-) diff --git a/src/actions/actionProperties.tsx b/src/actions/actionProperties.tsx index db2e2948..17e11f93 100644 --- a/src/actions/actionProperties.tsx +++ b/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; diff --git a/src/appState.ts b/src/appState.ts index 5087a352..703cad4b 100644 --- a/src/appState.ts +++ b/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, diff --git a/src/components/App.tsx b/src/components/App.tsx index 78e930ce..c0c6d1ee 100644 --- a/src/components/App.tsx +++ b/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 { 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 { }); }; - // 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 { ]); }; - textWysiwyg({ + const wysiwygElement = textWysiwyg({ x, y, initText: element.text, @@ -1236,6 +1246,7 @@ export class App extends React.Component { 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 { 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 { private handleCanvasPointerDown = ( event: React.PointerEvent, ) => { + 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 diff --git a/src/element/index.ts b/src/element/index.ts index 05331c1a..ba8e1a07 100644 --- a/src/element/index.ts +++ b/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( + element: T, +): element is NonDeleted { + return !element.isDeleted; +} diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index 7be88fdd..663646f2 100644 --- a/src/element/textWysiwyg.tsx +++ b/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(); + }, + }; } diff --git a/src/element/types.ts b/src/element/types.ts index 1b1add6c..ef78b06f 100644 --- a/src/element/types.ts +++ b/src/element/types.ts @@ -68,3 +68,8 @@ export type ResizeArrowFnType = ( pointerY: number, perfect: boolean, ) => void; + +export type WysiwigElement = { + submit: () => void; + changeStyle: (style: Record) => void; +}; diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 3ff3f4e6..3cbac058 100644 --- a/src/renderer/renderScene.ts +++ b/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); diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index 5d85183a..899867c4 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/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, } `; diff --git a/src/types.ts b/src/types.ts index 65f2d53a..026b116c 100644 --- a/src/types.ts +++ b/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; export type AppState = { + wysiwygElement: WysiwigElement | null; isLoading: boolean; errorMessage: string | null; draggingElement: NonDeletedExcalidrawElement | null;