From 10955f8bb0d19b225060e53c48b079b4d9f59bd5 Mon Sep 17 00:00:00 2001 From: Timur Khazamov Date: Tue, 7 Jan 2020 22:21:05 +0500 Subject: [PATCH] Wysiwyg text 2.0 (#238) * Fixed cleaning handlers after cleanup * Double click to edit text * Preserve text styles on change --- src/element/index.ts | 1 + src/element/textWysiwyg.tsx | 71 +++++++++++++++++++++++++ src/index.tsx | 100 +++++++++++++++++++++++++++--------- src/types.ts | 1 + 4 files changed, 148 insertions(+), 25 deletions(-) create mode 100644 src/element/textWysiwyg.tsx diff --git a/src/element/index.ts b/src/element/index.ts index 71c0cbc9..ca53c437 100644 --- a/src/element/index.ts +++ b/src/element/index.ts @@ -9,3 +9,4 @@ export { handlerRectangles } from "./handlerRectangles"; export { hitTest } from "./collision"; export { resizeTest } from "./resizeTest"; export { isTextElement } from "./typeChecks"; +export { textWysiwyg } from "./textWysiwyg"; diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx new file mode 100644 index 00000000..4935754f --- /dev/null +++ b/src/element/textWysiwyg.tsx @@ -0,0 +1,71 @@ +import { KEYS } from "../index"; + +type TextWysiwygParams = { + initText: string; + x: number; + y: number; + strokeColor: string; + font: string; + onSubmit: (text: string) => void; +}; + +export function textWysiwyg({ + initText, + x, + y, + strokeColor, + font, + onSubmit +}: TextWysiwygParams) { + const input = document.createElement("input"); + input.value = initText; + Object.assign(input.style, { + color: strokeColor, + position: "absolute", + top: y - 8 + "px", + left: x + "px", + transform: "translate(-50%, -50%)", + boxShadow: "none", + textAlign: "center", + width: (window.innerWidth - x) * 2 + "px", + font: font, + border: "none", + background: "transparent" + }); + + input.onkeydown = ev => { + if (ev.key === KEYS.ESCAPE) { + ev.preventDefault(); + cleanup(); + return; + } + if (ev.key === KEYS.ENTER) { + ev.preventDefault(); + handleSubmit(); + } + }; + input.onblur = handleSubmit; + + function stopEvent(ev: Event) { + ev.stopPropagation(); + } + + function handleSubmit() { + if (input.value) { + onSubmit(input.value); + } + cleanup(); + } + + function cleanup() { + input.onblur = null; + input.onkeydown = null; + window.removeEventListener("wheel", stopEvent, true); + document.body.removeChild(input); + } + + window.addEventListener("wheel", stopEvent, true); + document.body.appendChild(input); + input.focus(); + input.select(); +} diff --git a/src/index.tsx b/src/index.tsx index 2642b929..1ab5c975 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,7 +4,7 @@ import rough from "roughjs/bin/wrappers/rough"; import { moveOneLeft, moveAllLeft, moveOneRight, moveAllRight } from "./zindex"; import { randomSeed } from "./random"; -import { newElement, resizeTest, isTextElement } from "./element"; +import { newElement, resizeTest, isTextElement, textWysiwyg } from "./element"; import { clearSelection, getSelectedIndices, @@ -50,11 +50,12 @@ const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`; const CANVAS_WINDOW_OFFSET_LEFT = 250; const CANVAS_WINDOW_OFFSET_TOP = 0; -const KEYS = { +export const KEYS = { ARROW_LEFT: "ArrowLeft", ARROW_RIGHT: "ArrowRight", ARROW_DOWN: "ArrowDown", ARROW_UP: "ArrowUp", + ENTER: "Enter", ESCAPE: "Escape", DELETE: "Delete", BACKSPACE: "Backspace" @@ -79,24 +80,26 @@ function resetCursor() { document.documentElement.style.cursor = ""; } -function addTextElement(element: ExcalidrawTextElement) { +function addTextElement( + element: ExcalidrawTextElement, + text: string, + font: string +) { resetCursor(); - const text = prompt("What text do you want?"); if (text === null || text === "") { return false; } - const fontSize = 20; element.text = text; - element.font = `${fontSize}px Virgil`; - const font = context.font; + element.font = font; + const currentFont = context.font; context.font = element.font; const textMeasure = context.measureText(element.text); const width = textMeasure.width; const actualBoundingBoxAscent = - textMeasure.actualBoundingBoxAscent || fontSize; + textMeasure.actualBoundingBoxAscent || parseInt(font); const actualBoundingBoxDescent = textMeasure.actualBoundingBoxDescent || 0; element.actualBoundingBoxAscent = actualBoundingBoxAscent; - context.font = font; + context.font = currentFont; const height = actualBoundingBoxAscent + actualBoundingBoxDescent; // Center the text element.x -= width / 2; @@ -138,6 +141,7 @@ class App extends React.Component<{}, AppState> { exportBackground: true, currentItemStrokeColor: "#000000", currentItemBackgroundColor: "#ffffff", + currentItemFont: "20px Virgil", viewBackgroundColor: "#ffffff", scrollX: 0, scrollY: 0, @@ -688,9 +692,23 @@ class App extends React.Component<{}, AppState> { } if (isTextElement(element)) { - if (!addTextElement(element)) { - return; - } + textWysiwyg({ + initText: "", + x: e.clientX, + y: e.clientY, + strokeColor: this.state.currentItemStrokeColor, + font: this.state.currentItemFont, + onSubmit: text => { + addTextElement(element, text, this.state.currentItemFont); + elements.push(element); + element.isSelected = true; + this.setState({ + draggingElement: null, + elementType: "selection" + }); + } + }); + return; } elements.push(element); @@ -903,9 +921,12 @@ class App extends React.Component<{}, AppState> { const x = e.clientX - CANVAS_WINDOW_OFFSET_LEFT - this.state.scrollX; const y = e.clientY - CANVAS_WINDOW_OFFSET_TOP - this.state.scrollY; - - if (getElementAtPosition(elements, x, y)) { + const elementAtPosition = getElementAtPosition(elements, x, y); + if (elementAtPosition && !isTextElement(elementAtPosition)) { return; + } else if (elementAtPosition) { + elements.splice(elements.indexOf(elementAtPosition), 1); + this.forceUpdate(); } const element = newElement( @@ -918,21 +939,50 @@ class App extends React.Component<{}, AppState> { 1, 1, 100 - ); + ) as ExcalidrawTextElement; - if (!addTextElement(element as ExcalidrawTextElement)) { - return; + let initText = ""; + let textX = e.clientX; + let textY = e.clientY; + if (elementAtPosition) { + Object.assign(element, elementAtPosition); + // x and y will change after calling addTextElement function + element.x = elementAtPosition.x + elementAtPosition.width / 2; + element.y = + elementAtPosition.y + elementAtPosition.actualBoundingBoxAscent; + initText = elementAtPosition.text; + textX = + this.state.scrollX + + elementAtPosition.x + + CANVAS_WINDOW_OFFSET_LEFT + + elementAtPosition.width / 2; + textY = + this.state.scrollY + + elementAtPosition.y + + CANVAS_WINDOW_OFFSET_TOP + + elementAtPosition.actualBoundingBoxAscent; } - elements.push(element); - - this.setState({ - draggingElement: null, - elementType: "selection" + textWysiwyg({ + initText, + x: textX, + y: textY, + strokeColor: element.strokeColor, + font: element.font || this.state.currentItemFont, + onSubmit: text => { + addTextElement( + element, + text, + element.font || this.state.currentItemFont + ); + elements.push(element); + element.isSelected = true; + this.setState({ + draggingElement: null, + elementType: "selection" + }); + } }); - element.isSelected = true; - - this.forceUpdate(); }} /> diff --git a/src/types.ts b/src/types.ts index 2fa153a7..ea9af670 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,6 +7,7 @@ export type AppState = { exportBackground: boolean; currentItemStrokeColor: string; currentItemBackgroundColor: string; + currentItemFont: string; viewBackgroundColor: string; scrollX: number; scrollY: number;