rewrite wysiwyg property updating (#1387)

* rewrite wysiwyg property updating

* reuse existing class

* fix case of focus being stolen by other UIs

* revert mistake csp removal

* ensure we don't run cleanup twice

* fix opacity updating

* add shape actions menu class to constants
pull/1402/head
David Luzar 5 years ago committed by GitHub
parent d79c859cd9
commit 6771b505ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

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

@ -1228,7 +1228,8 @@ class App extends React.Component<any, AppState> {
]); ]);
}; };
const wysiwygElement = textWysiwyg({ textWysiwyg({
id: element.id,
x, x,
y, y,
initText: element.text, initText: element.text,
@ -1248,7 +1249,6 @@ class App extends React.Component<any, AppState> {
onSubmit: withBatchedUpdates((text) => { onSubmit: withBatchedUpdates((text) => {
updateElement(text); updateElement(text);
this.setState((prevState) => ({ this.setState((prevState) => ({
wysiwygElement: null,
selectedElementIds: { selectedElementIds: {
...prevState.selectedElementIds, ...prevState.selectedElementIds,
[element.id]: true, [element.id]: true,
@ -1269,7 +1269,7 @@ class App extends React.Component<any, AppState> {
}), }),
}); });
// deselect all other elements when inserting text // deselect all other elements when inserting text
this.setState({ selectedElementIds: {}, wysiwygElement }); this.setState({ selectedElementIds: {} });
// do an initial update to re-initialize element position since we were // 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) // modifying element's x/y for sake of editor (case: syncing to remote)
@ -1579,9 +1579,6 @@ class App extends React.Component<any, AppState> {
private handleCanvasPointerDown = ( private handleCanvasPointerDown = (
event: React.PointerEvent<HTMLCanvasElement>, event: React.PointerEvent<HTMLCanvasElement>,
) => { ) => {
if (this.state.wysiwygElement && this.state.wysiwygElement.submit) {
this.state.wysiwygElement.submit();
}
if (lastPointerUp !== null) { if (lastPointerUp !== null) {
// Unfortunately, sometimes we don't get a pointerup after a pointerdown, // 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 // this can happen when a contextual menu or alert is triggered. In order to avoid

@ -26,6 +26,7 @@ import { ErrorDialog } from "./ErrorDialog";
import { ShortcutsDialog } from "./ShortcutsDialog"; import { ShortcutsDialog } from "./ShortcutsDialog";
import { LoadingMessage } from "./LoadingMessage"; import { LoadingMessage } from "./LoadingMessage";
import { GitHubCorner } from "./GitHubCorner"; import { GitHubCorner } from "./GitHubCorner";
import { CLASSES } from "../constants";
interface LayerUIProps { interface LayerUIProps {
actionManager: ActionManager; actionManager: ActionManager;
@ -146,7 +147,7 @@ export const LayerUI = React.memo(
</Section> </Section>
{showSelectedShapeActions(appState, elements) && ( {showSelectedShapeActions(appState, elements) && (
<Section heading="selectedShapeActions"> <Section heading="selectedShapeActions">
<Island className="App-menu__left" padding={4}> <Island className={CLASSES.SHAPE_ACTIONS_MENU} padding={4}>
<SelectedShapeActions <SelectedShapeActions
appState={appState} appState={appState}
elements={elements} elements={elements}

@ -53,3 +53,7 @@ export const BROADCAST = {
SERVER_VOLATILE: "server-volatile-broadcast", SERVER_VOLATILE: "server-volatile-broadcast",
SERVER: "server-broadcast", SERVER: "server-broadcast",
}; };
export const CLASSES = {
SHAPE_ACTIONS_MENU: "App-menu__left",
};

@ -1,6 +1,8 @@
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { selectNode } from "../utils"; import { selectNode, isWritableElement } from "../utils";
import { WysiwigElement } from "./types"; import { globalSceneState } from "../scene";
import { isTextElement } from "./typeChecks";
import { CLASSES } from "../constants";
function trimText(text: string) { function trimText(text: string) {
// whitespace only → trim all because we'd end up inserting invisible element // whitespace only → trim all because we'd end up inserting invisible element
@ -14,6 +16,7 @@ function trimText(text: string) {
} }
type TextWysiwygParams = { type TextWysiwygParams = {
id: string;
initText: string; initText: string;
x: number; x: number;
y: number; y: number;
@ -29,6 +32,7 @@ type TextWysiwygParams = {
}; };
export function textWysiwyg({ export function textWysiwyg({
id,
initText, initText,
x, x,
y, y,
@ -41,7 +45,7 @@ export function textWysiwyg({
textAlign, textAlign,
onSubmit, onSubmit,
onCancel, onCancel,
}: TextWysiwygParams): WysiwigElement { }: TextWysiwygParams) {
const editable = document.createElement("div"); const editable = document.createElement("div");
try { try {
editable.contentEditable = "plaintext-only"; editable.contentEditable = "plaintext-only";
@ -136,25 +140,74 @@ export function textWysiwyg({
} }
function cleanup() { function cleanup() {
if (isDestroyed) {
return;
}
isDestroyed = true;
// remove events to ensure they don't late-fire // remove events to ensure they don't late-fire
editable.onblur = null;
editable.onpaste = null; editable.onpaste = null;
editable.oninput = null; editable.oninput = null;
editable.onkeydown = null; editable.onkeydown = null;
window.removeEventListener("wheel", stopEvent, true); window.removeEventListener("wheel", stopEvent, true);
window.removeEventListener("pointerdown", onPointerDown);
window.removeEventListener("pointerup", rebindBlur);
window.removeEventListener("blur", handleSubmit);
unbindUpdate();
document.body.removeChild(editable); document.body.removeChild(editable);
} }
const rebindBlur = () => {
window.removeEventListener("pointerup", rebindBlur);
// deferred to guard against focus traps on various UIs that steal focus
// upon pointerUp
setTimeout(() => {
editable.onblur = handleSubmit;
// case: clicking on the same property → no change → no update → no focus
editable.focus();
});
};
// prevent blur when changing properties from the menu
const onPointerDown = (event: MouseEvent) => {
if (
event.target instanceof HTMLElement &&
event.target.closest(CLASSES.SHAPE_ACTIONS_MENU) &&
!isWritableElement(event.target)
) {
editable.onblur = null;
window.addEventListener("pointerup", rebindBlur);
// handle edge-case where pointerup doesn't fire e.g. due to user
// alt-tabbing away
window.addEventListener("blur", handleSubmit);
}
};
// handle updates of textElement properties of editing element
const unbindUpdate = globalSceneState.addCallback(() => {
const editingElement = globalSceneState
.getElementsIncludingDeleted()
.find((element) => element.id === id);
if (editingElement && isTextElement(editingElement)) {
Object.assign(editable.style, {
font: editingElement.font,
textAlign: editingElement.textAlign,
color: editingElement.strokeColor,
opacity: editingElement.opacity / 100,
});
}
editable.focus();
});
let isDestroyed = false;
editable.onblur = handleSubmit;
window.addEventListener("pointerdown", onPointerDown);
window.addEventListener("wheel", stopEvent, true); window.addEventListener("wheel", stopEvent, true);
document.body.appendChild(editable); document.body.appendChild(editable);
editable.focus(); editable.focus();
selectNode(editable); selectNode(editable);
return {
submit: handleSubmit,
changeStyle: (style: any) => {
Object.assign(editable.style, style);
editable.focus();
},
};
} }

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

@ -14,7 +14,6 @@ import {
handlerRectangles, handlerRectangles,
getCommonBounds, getCommonBounds,
canResizeMutlipleElements, canResizeMutlipleElements,
isTextElement,
} from "../element"; } from "../element";
import { roundRect } from "./roundRect"; import { roundRect } from "./roundRect";
@ -104,18 +103,6 @@ export function renderScene(
return { atLeastOneVisibleElement: false }; 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")!; const context = canvas.getContext("2d")!;
context.scale(scale, scale); context.scale(scale, scale);

@ -41,7 +41,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -241,7 +240,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -360,7 +358,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -636,7 +633,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -797,7 +793,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -998,7 +993,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -1258,7 +1252,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -1658,7 +1651,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -2283,7 +2275,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -2402,7 +2393,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -2521,7 +2511,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -2640,7 +2629,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -2781,7 +2769,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -2922,7 +2909,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -3063,7 +3049,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -3182,7 +3167,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -3301,7 +3285,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -3442,7 +3425,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -3561,7 +3543,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -3634,7 +3615,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -4520,7 +4500,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -4945,7 +4924,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -5277,7 +5255,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -5520,7 +5497,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -5694,7 +5670,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -6531,7 +6506,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -7259,7 +7233,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -7882,7 +7855,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -8405,7 +8377,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -8878,7 +8849,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -9256,7 +9226,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -9543,7 +9512,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -9759,7 +9727,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -10652,7 +10619,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -11434,7 +11400,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -12109,7 +12074,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -12677,7 +12641,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -13056,7 +13019,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -13113,7 +13075,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -13170,7 +13131,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -13467,7 +13427,6 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;

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

Loading…
Cancel
Save