do not center text when not applicable (#1783)

This commit is contained in:
David Luzar 2020-06-25 21:21:27 +02:00 committed by GitHub
parent 9c89504b6f
commit cd87bd6901
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 418 additions and 321 deletions

View file

@ -1,116 +1,111 @@
import { KEYS } from "../keys";
import { selectNode, isWritableElement, getFontString } from "../utils";
import { isWritableElement, getFontString } from "../utils";
import { globalSceneState } from "../scene";
import { isTextElement } from "./typeChecks";
import { CLASSES } from "../constants";
import { FontFamily } from "./types";
import { ExcalidrawElement } from "./types";
const trimText = (text: string) => {
// whitespace only → trim all because we'd end up inserting invisible element
if (!text.trim()) {
return "";
}
// replace leading/trailing newlines (only) otherwise it messes up bounding
// box calculation (there's also a bug in FF which inserts trailing newline
// for multiline texts)
return text.replace(/^\n+|\n+$/g, "");
const normalizeText = (text: string) => {
return (
text
// replace tabs with spaces so they render and measure correctly
.replace(/\t/g, " ")
// normalize newlines
.replace(/\r?\n|\r/g, "\n")
);
};
type TextWysiwygParams = {
id: string;
initText: string;
x: number;
y: number;
strokeColor: string;
fontSize: number;
fontFamily: FontFamily;
opacity: number;
zoom: number;
angle: number;
textAlign: string;
onChange?: (text: string) => void;
onSubmit: (text: string) => void;
onCancel: () => void;
const getTransform = (
width: number,
height: number,
angle: number,
zoom: number,
) => {
const degree = (180 * angle) / Math.PI;
return `translate(${(width * (zoom - 1)) / 2}px, ${
(height * (zoom - 1)) / 2
}px) scale(${zoom}) rotate(${degree}deg)`;
};
export const textWysiwyg = ({
id,
initText,
x,
y,
strokeColor,
fontSize,
fontFamily,
opacity,
zoom,
angle,
onChange,
textAlign,
onSubmit,
onCancel,
}: TextWysiwygParams) => {
const editable = document.createElement("div");
try {
editable.contentEditable = "plaintext-only";
} catch {
editable.contentEditable = "true";
getViewportCoords,
}: {
id: ExcalidrawElement["id"];
zoom: number;
onChange?: (text: string) => void;
onSubmit: (text: string) => void;
onCancel: () => void;
getViewportCoords: (x: number, y: number) => [number, number];
}) => {
function updateWysiwygStyle() {
const updatedElement = globalSceneState.getElement(id);
if (isTextElement(updatedElement)) {
const [viewportX, viewportY] = getViewportCoords(
updatedElement.x,
updatedElement.y,
);
const { textAlign, angle } = updatedElement;
editable.value = updatedElement.text;
const lines = updatedElement.text.replace(/\r\n?/g, "\n").split("\n");
const lineHeight = updatedElement.height / lines.length;
Object.assign(editable.style, {
font: getFontString(updatedElement),
// must be defined *after* font ¯\_(ツ)_/¯
lineHeight: `${lineHeight}px`,
width: `${updatedElement.width}px`,
height: `${updatedElement.height}px`,
left: `${viewportX}px`,
top: `${viewportY}px`,
transform: getTransform(
updatedElement.width,
updatedElement.height,
angle,
zoom,
),
textAlign: textAlign,
color: updatedElement.strokeColor,
opacity: updatedElement.opacity / 100,
});
}
}
const editable = document.createElement("textarea");
editable.dir = "auto";
editable.tabIndex = 0;
editable.innerText = initText;
editable.dataset.type = "wysiwyg";
const degree = (180 * angle) / Math.PI;
// prevent line wrapping on Safari
editable.wrap = "off";
Object.assign(editable.style, {
color: strokeColor,
position: "fixed",
opacity: opacity / 100,
top: `${y}px`,
left: `${x}px`,
transform: `translate(-50%, -50%) scale(${zoom}) rotate(${degree}deg)`,
textAlign: textAlign,
display: "inline-block",
font: getFontString({ fontSize, fontFamily }),
padding: "4px",
// This needs to have "1px solid" otherwise the carret doesn't show up
// the first time on Safari and Chrome!
outline: "1px solid transparent",
whiteSpace: "nowrap",
minHeight: "1em",
backfaceVisibility: "hidden",
margin: 0,
padding: 0,
border: 0,
outline: 0,
resize: "none",
background: "transparent",
overflow: "hidden",
// prevent line wrapping (`whitespace: nowrap` doesn't work on FF)
whiteSpace: "pre",
});
editable.onpaste = (event) => {
try {
const selection = window.getSelection();
if (!selection?.rangeCount) {
return;
}
selection.deleteFromDocument();
const text = event.clipboardData!.getData("text").replace(/\r\n?/g, "\n");
const span = document.createElement("span");
span.innerText = text;
const range = selection.getRangeAt(0);
range.insertNode(span);
// deselect
window.getSelection()!.removeAllRanges();
range.setStart(span, span.childNodes.length);
range.setEnd(span, span.childNodes.length);
selection.addRange(range);
event.preventDefault();
} catch (error) {
console.error(error);
}
};
updateWysiwygStyle();
if (onChange) {
editable.oninput = () => {
onChange(trimText(editable.innerText));
onChange(normalizeText(editable.value));
};
}
@ -134,8 +129,8 @@ export const textWysiwyg = ({
};
const handleSubmit = () => {
if (editable.innerText) {
onSubmit(trimText(editable.innerText));
if (editable.value) {
onSubmit(normalizeText(editable.value));
} else {
onCancel();
}
@ -149,10 +144,10 @@ export const textWysiwyg = ({
isDestroyed = true;
// remove events to ensure they don't late-fire
editable.onblur = null;
editable.onpaste = null;
editable.oninput = null;
editable.onkeydown = null;
window.removeEventListener("resize", updateWysiwygStyle);
window.removeEventListener("wheel", stopEvent, true);
window.removeEventListener("pointerdown", onPointerDown);
window.removeEventListener("pointerup", rebindBlur);
@ -191,26 +186,19 @@ export const textWysiwyg = ({
// 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: getFontString(editingElement),
textAlign: editingElement.textAlign,
color: editingElement.strokeColor,
opacity: editingElement.opacity / 100,
});
}
updateWysiwygStyle();
editable.focus();
});
let isDestroyed = false;
editable.onblur = handleSubmit;
// reposition wysiwyg in case of window resize. Happens on mobile when
// device keyboard is opened.
window.addEventListener("resize", updateWysiwygStyle);
window.addEventListener("pointerdown", onPointerDown);
window.addEventListener("wheel", stopEvent, true);
document.body.appendChild(editable);
editable.focus();
selectNode(editable);
editable.select();
};