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>
This commit is contained in:
Kostas Bariotis 2020-04-11 17:10:56 +01:00 committed by GitHub
parent d2246bfb30
commit 5e2f164026
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 134 additions and 11 deletions

View File

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

View File

@ -8,6 +8,7 @@ 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,

View File

@ -25,6 +25,7 @@ import {
getElementWithResizeHandler, getElementWithResizeHandler,
canResizeMutlipleElements, canResizeMutlipleElements,
getResizeHandlerFromCoords, getResizeHandlerFromCoords,
isNonDeletedElement,
} from "../element"; } from "../element";
import { import {
deleteSelectedElements, deleteSelectedElements,
@ -269,19 +270,31 @@ export class App extends React.Component<any, AppState> {
if (this.unmounted) { if (this.unmounted) {
return; return;
} }
let editingElement: AppState["editingElement"] | null = null;
if (res.elements) { 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); globalSceneState.replaceAllElements(res.elements);
if (res.commitToHistory) { if (res.commitToHistory) {
history.resumeRecording(); history.resumeRecording();
} }
} }
if (res.appState) { if (res.appState || editingElement) {
if (res.commitToHistory) { if (res.commitToHistory) {
history.resumeRecording(); history.resumeRecording();
} }
this.setState((state) => ({ this.setState((state) => ({
...res.appState, ...res.appState,
editingElement: editingElement || state.editingElement,
isCollaborating: state.isCollaborating, isCollaborating: state.isCollaborating,
collaborators: state.collaborators, 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 = () => { const deleteElement = () => {
globalSceneState.replaceAllElements([ globalSceneState.replaceAllElements([
...globalSceneState.getElementsIncludingDeleted().map((_element) => { ...globalSceneState.getElementsIncludingDeleted().map((_element) => {
@ -1216,7 +1226,7 @@ export class App extends React.Component<any, AppState> {
]); ]);
}; };
textWysiwyg({ const wysiwygElement = textWysiwyg({
x, x,
y, y,
initText: element.text, initText: element.text,
@ -1236,6 +1246,7 @@ export 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,
@ -1255,6 +1266,8 @@ export class App extends React.Component<any, AppState> {
resetSelection(); resetSelection();
}), }),
}); });
// deselect all other elements when inserting text
this.setState({ selectedElementIds: {}, wysiwygElement });
// 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)
@ -1564,6 +1577,9 @@ export 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

View File

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

View File

@ -1,5 +1,6 @@
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { selectNode } from "../utils"; import { selectNode } from "../utils";
import { WysiwigElement } from "./types";
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
@ -40,7 +41,7 @@ export function textWysiwyg({
textAlign, textAlign,
onSubmit, onSubmit,
onCancel, onCancel,
}: TextWysiwygParams) { }: TextWysiwygParams): WysiwigElement {
const editable = document.createElement("div"); const editable = document.createElement("div");
try { try {
editable.contentEditable = "plaintext-only"; editable.contentEditable = "plaintext-only";
@ -120,7 +121,6 @@ export function textWysiwyg({
event.stopPropagation(); event.stopPropagation();
} }
}; };
editable.onblur = handleSubmit;
function stopEvent(event: Event) { function stopEvent(event: Event) {
event.stopPropagation(); event.stopPropagation();
@ -137,7 +137,6 @@ export function textWysiwyg({
function cleanup() { function cleanup() {
// 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;
@ -150,4 +149,12 @@ export function textWysiwyg({
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();
},
};
} }

View File

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

View File

@ -14,6 +14,7 @@ import {
handlerRectangles, handlerRectangles,
getCommonBounds, getCommonBounds,
canResizeMutlipleElements, canResizeMutlipleElements,
isTextElement,
} from "../element"; } from "../element";
import { roundRect } from "./roundRect"; import { roundRect } from "./roundRect";
@ -103,6 +104,18 @@ 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);

View File

@ -41,6 +41,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -240,6 +241,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -358,6 +360,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -633,6 +636,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -793,6 +797,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -993,6 +998,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -1252,6 +1258,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -1603,7 +1610,32 @@ Object {
"cursorX": 0, "cursorX": 0,
"cursorY": 0, "cursorY": 0,
"draggingElement": null, "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, "elementLocked": false,
"elementType": "selection", "elementType": "selection",
"errorMessage": null, "errorMessage": null,
@ -1626,6 +1658,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -2250,6 +2283,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -2368,6 +2402,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -2486,6 +2521,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -2604,6 +2640,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -2744,6 +2781,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -2884,6 +2922,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -3024,6 +3063,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -3142,6 +3182,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -3260,6 +3301,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -3400,6 +3442,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -3518,6 +3561,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -3590,6 +3634,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -4475,6 +4520,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -4899,6 +4945,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -5230,6 +5277,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -5472,6 +5520,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -5645,6 +5694,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -6481,6 +6531,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -7208,6 +7259,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -7830,6 +7882,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -8352,6 +8405,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -8824,6 +8878,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -9201,6 +9256,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -9487,6 +9543,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -9702,6 +9759,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -10594,6 +10652,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -11375,6 +11434,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -12049,6 +12109,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -12616,6 +12677,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -12994,6 +13056,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -13050,6 +13113,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -13106,6 +13170,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;
@ -13402,6 +13467,7 @@ Object {
"showShortcutsDialog": false, "showShortcutsDialog": false,
"username": "", "username": "",
"viewBackgroundColor": "#ffffff", "viewBackgroundColor": "#ffffff",
"wysiwygElement": null,
"zoom": 1, "zoom": 1,
} }
`; `;

View File

@ -4,6 +4,7 @@ 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";
@ -12,6 +13,7 @@ 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;