2021-04-13 16:23:46 +02:00
|
|
|
import { CODES, KEYS } from "../keys";
|
2021-12-16 21:14:03 +05:30
|
|
|
import {
|
|
|
|
isWritableElement,
|
|
|
|
getFontString,
|
|
|
|
getFontFamilyString,
|
|
|
|
} from "../utils";
|
2020-07-30 14:50:59 +05:30
|
|
|
import Scene from "../scene/Scene";
|
2021-12-17 15:24:23 +05:30
|
|
|
import { isBoundToContainer, isTextElement } from "./typeChecks";
|
2021-12-16 21:14:03 +05:30
|
|
|
import { CLASSES, PADDING } from "../constants";
|
|
|
|
import {
|
|
|
|
ExcalidrawBindableElement,
|
|
|
|
ExcalidrawElement,
|
|
|
|
ExcalidrawTextElement,
|
|
|
|
} from "./types";
|
2020-07-27 17:18:49 +05:30
|
|
|
import { AppState } from "../types";
|
2021-12-16 21:14:03 +05:30
|
|
|
import { mutateElement } from "./mutateElement";
|
|
|
|
import {
|
|
|
|
getApproxLineHeight,
|
|
|
|
getBoundTextElementId,
|
|
|
|
wrapText,
|
|
|
|
} from "./textElement";
|
2020-06-25 21:21:27 +02:00
|
|
|
|
|
|
|
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")
|
|
|
|
);
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-03-18 13:01:33 +01:00
|
|
|
|
2020-06-25 21:21:27 +02:00
|
|
|
const getTransform = (
|
|
|
|
width: number,
|
|
|
|
height: number,
|
|
|
|
angle: number,
|
2020-07-27 17:18:49 +05:30
|
|
|
appState: AppState,
|
2021-03-12 02:38:50 +05:30
|
|
|
maxWidth: number,
|
2021-12-21 17:13:11 +05:30
|
|
|
maxHeight: number,
|
2020-06-25 21:21:27 +02:00
|
|
|
) => {
|
2020-07-27 17:18:49 +05:30
|
|
|
const { zoom, offsetTop, offsetLeft } = appState;
|
2020-06-25 21:21:27 +02:00
|
|
|
const degree = (180 * angle) / Math.PI;
|
2020-07-27 17:18:49 +05:30
|
|
|
// offsets must be multiplied by 2 to account for the division by 2 of
|
2020-11-05 19:06:18 +02:00
|
|
|
// the whole expression afterwards
|
2021-03-12 02:38:50 +05:30
|
|
|
let translateX = ((width - offsetLeft * 2) * (zoom.value - 1)) / 2;
|
2021-12-21 17:13:11 +05:30
|
|
|
let translateY = ((height - offsetTop * 2) * (zoom.value - 1)) / 2;
|
2021-03-12 02:38:50 +05:30
|
|
|
if (width > maxWidth && zoom.value !== 1) {
|
|
|
|
translateX = (maxWidth / 2) * (zoom.value - 1);
|
|
|
|
}
|
2021-12-21 17:13:11 +05:30
|
|
|
if (height > maxHeight && zoom.value !== 1) {
|
|
|
|
translateY = ((maxHeight - offsetTop * 2) * (zoom.value - 1)) / 2;
|
|
|
|
}
|
2021-03-12 02:38:50 +05:30
|
|
|
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
2020-01-07 22:21:05 +05:00
|
|
|
};
|
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
export const textWysiwyg = ({
|
2020-04-12 15:57:57 +02:00
|
|
|
id,
|
2020-07-27 17:18:49 +05:30
|
|
|
appState,
|
2020-04-03 14:16:14 +02:00
|
|
|
onChange,
|
2020-01-24 12:04:54 +02:00
|
|
|
onSubmit,
|
2020-06-25 21:21:27 +02:00
|
|
|
getViewportCoords,
|
2020-07-30 14:50:59 +05:30
|
|
|
element,
|
2021-02-11 12:24:26 +01:00
|
|
|
canvas,
|
2021-04-23 21:11:18 +05:30
|
|
|
excalidrawContainer,
|
2020-06-25 21:21:27 +02:00
|
|
|
}: {
|
|
|
|
id: ExcalidrawElement["id"];
|
2020-07-27 17:18:49 +05:30
|
|
|
appState: AppState;
|
2020-06-25 21:21:27 +02:00
|
|
|
onChange?: (text: string) => void;
|
2021-12-16 21:14:03 +05:30
|
|
|
onSubmit: (data: {
|
|
|
|
text: string;
|
|
|
|
viaKeyboard: boolean;
|
|
|
|
originalText: string;
|
|
|
|
}) => void;
|
2020-06-25 21:21:27 +02:00
|
|
|
getViewportCoords: (x: number, y: number) => [number, number];
|
2020-07-30 14:50:59 +05:30
|
|
|
element: ExcalidrawElement;
|
2021-02-11 12:24:26 +01:00
|
|
|
canvas: HTMLCanvasElement | null;
|
2021-04-23 21:11:18 +05:30
|
|
|
excalidrawContainer: HTMLDivElement | null;
|
2020-06-25 21:21:27 +02:00
|
|
|
}) => {
|
2021-12-16 21:14:03 +05:30
|
|
|
const textPropertiesUpdated = (
|
|
|
|
updatedElement: ExcalidrawTextElement,
|
|
|
|
editable: HTMLTextAreaElement,
|
|
|
|
) => {
|
|
|
|
const currentFont = editable.style.fontFamily.replaceAll('"', "");
|
|
|
|
if (
|
|
|
|
getFontFamilyString({ fontFamily: updatedElement.fontFamily }) !==
|
|
|
|
currentFont
|
|
|
|
) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (`${updatedElement.fontSize}px` !== editable.style.fontSize) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
let originalContainerHeight: number;
|
|
|
|
let approxLineHeight = isTextElement(element)
|
|
|
|
? getApproxLineHeight(getFontString(element))
|
|
|
|
: 0;
|
|
|
|
|
2020-11-06 22:06:39 +02:00
|
|
|
const updateWysiwygStyle = () => {
|
2020-07-30 14:50:59 +05:30
|
|
|
const updatedElement = Scene.getScene(element)?.getElement(id);
|
|
|
|
if (updatedElement && isTextElement(updatedElement)) {
|
2021-12-16 21:14:03 +05:30
|
|
|
let coordX = updatedElement.x;
|
|
|
|
let coordY = updatedElement.y;
|
|
|
|
let container = updatedElement?.containerId
|
|
|
|
? Scene.getScene(updatedElement)!.getElement(updatedElement.containerId)
|
|
|
|
: null;
|
|
|
|
let maxWidth = updatedElement.width;
|
|
|
|
|
|
|
|
let maxHeight = updatedElement.height;
|
|
|
|
let width = updatedElement.width;
|
2021-12-21 17:13:11 +05:30
|
|
|
const height = Math.max(editable.scrollHeight, updatedElement.height);
|
2021-12-16 21:14:03 +05:30
|
|
|
if (container && updatedElement.containerId) {
|
|
|
|
const propertiesUpdated = textPropertiesUpdated(
|
|
|
|
updatedElement,
|
|
|
|
editable,
|
2021-03-12 02:38:50 +05:30
|
|
|
);
|
2021-12-21 17:13:11 +05:30
|
|
|
|
2021-12-16 21:14:03 +05:30
|
|
|
if (propertiesUpdated) {
|
|
|
|
const currentContainer = Scene.getScene(updatedElement)?.getElement(
|
|
|
|
updatedElement.containerId,
|
|
|
|
) as ExcalidrawBindableElement;
|
|
|
|
approxLineHeight = isTextElement(updatedElement)
|
|
|
|
? getApproxLineHeight(getFontString(updatedElement))
|
|
|
|
: 0;
|
|
|
|
if (updatedElement.height > currentContainer.height - PADDING * 2) {
|
|
|
|
const nextHeight = updatedElement.height + PADDING * 2;
|
|
|
|
originalContainerHeight = nextHeight;
|
|
|
|
mutateElement(container, { height: nextHeight });
|
|
|
|
container = { ...container, height: nextHeight };
|
|
|
|
}
|
|
|
|
editable.style.height = `${updatedElement.height}px`;
|
|
|
|
}
|
|
|
|
if (!originalContainerHeight) {
|
|
|
|
originalContainerHeight = container.height;
|
|
|
|
}
|
|
|
|
maxWidth = container.width - PADDING * 2;
|
|
|
|
maxHeight = container.height - PADDING * 2;
|
|
|
|
width = maxWidth;
|
|
|
|
// The coordinates of text box set a distance of
|
|
|
|
// 30px to preserve padding
|
|
|
|
coordX = container.x + PADDING;
|
|
|
|
|
|
|
|
// autogrow container height if text exceeds
|
2021-12-21 17:13:11 +05:30
|
|
|
if (editable.scrollHeight > maxHeight) {
|
2021-12-16 21:14:03 +05:30
|
|
|
const diff = Math.min(
|
2021-12-21 17:13:11 +05:30
|
|
|
editable.scrollHeight - maxHeight,
|
2021-12-16 21:14:03 +05:30
|
|
|
approxLineHeight,
|
|
|
|
);
|
|
|
|
mutateElement(container, { height: container.height + diff });
|
|
|
|
return;
|
|
|
|
} else if (
|
|
|
|
// autoshrink container height until original container height
|
|
|
|
// is reached when text is removed
|
|
|
|
container.height > originalContainerHeight &&
|
2021-12-21 17:13:11 +05:30
|
|
|
editable.scrollHeight < maxHeight
|
2021-12-16 21:14:03 +05:30
|
|
|
) {
|
|
|
|
const diff = Math.min(
|
2021-12-21 17:13:11 +05:30
|
|
|
maxHeight - editable.scrollHeight,
|
2021-12-16 21:14:03 +05:30
|
|
|
approxLineHeight,
|
|
|
|
);
|
|
|
|
mutateElement(container, { height: container.height - diff });
|
|
|
|
}
|
|
|
|
// Start pushing text upward until a diff of 30px (padding)
|
|
|
|
// is reached
|
|
|
|
else {
|
2021-12-21 17:13:11 +05:30
|
|
|
const lines = editable.scrollHeight / approxLineHeight;
|
2021-12-16 21:14:03 +05:30
|
|
|
// For some reason the scrollHeight gets set to twice the lineHeight
|
|
|
|
// when you start typing for first time and thus line count is 2
|
|
|
|
// hence this check
|
|
|
|
if (lines > 2 || propertiesUpdated) {
|
|
|
|
// vertically center align the text
|
|
|
|
coordY =
|
2021-12-21 17:13:11 +05:30
|
|
|
container.y + container.height / 2 - editable.scrollHeight / 2;
|
2021-12-16 21:14:03 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
|
|
|
|
const { textAlign, angle } = updatedElement;
|
2020-06-25 21:21:27 +02:00
|
|
|
|
2021-12-16 21:14:03 +05:30
|
|
|
editable.value = updatedElement.originalText || updatedElement.text;
|
|
|
|
const lines = updatedElement.originalText.split("\n");
|
|
|
|
const lineHeight = updatedElement.containerId
|
|
|
|
? approxLineHeight
|
|
|
|
: updatedElement.height / lines.length;
|
|
|
|
if (!container) {
|
|
|
|
maxWidth =
|
|
|
|
(appState.offsetLeft + appState.width - viewportX - 8) /
|
|
|
|
appState.zoom.value -
|
|
|
|
// margin-right of parent if any
|
|
|
|
Number(
|
|
|
|
getComputedStyle(
|
|
|
|
excalidrawContainer?.parentNode as Element,
|
|
|
|
).marginRight.slice(0, -2),
|
|
|
|
);
|
|
|
|
}
|
2021-12-21 17:13:11 +05:30
|
|
|
|
2021-12-16 21:14:03 +05:30
|
|
|
// Make sure text editor height doesn't go beyond viewport
|
|
|
|
const editorMaxHeight =
|
2021-12-21 17:13:11 +05:30
|
|
|
(appState.height -
|
|
|
|
viewportY -
|
|
|
|
// There is a ~14px difference which keeps on increasing
|
|
|
|
// with every zoom step when offset present hence I am subtracting it here
|
|
|
|
// However this is not the best fix and breaks in
|
|
|
|
// few scenarios
|
|
|
|
(appState.offsetTop
|
|
|
|
? ((appState.zoom.value * 100 - 100) / 10) * 14
|
|
|
|
: 0)) /
|
2021-12-16 21:14:03 +05:30
|
|
|
appState.zoom.value;
|
2020-06-25 21:21:27 +02:00
|
|
|
Object.assign(editable.style, {
|
|
|
|
font: getFontString(updatedElement),
|
|
|
|
// must be defined *after* font ¯\_(ツ)_/¯
|
|
|
|
lineHeight: `${lineHeight}px`,
|
2021-12-16 21:14:03 +05:30
|
|
|
width: `${width}px`,
|
2021-12-21 17:13:11 +05:30
|
|
|
height: `${height}px`,
|
2020-06-25 21:21:27 +02:00
|
|
|
left: `${viewportX}px`,
|
|
|
|
top: `${viewportY}px`,
|
2021-12-21 17:13:11 +05:30
|
|
|
transform: getTransform(
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
angle,
|
|
|
|
appState,
|
|
|
|
maxWidth,
|
|
|
|
editorMaxHeight,
|
|
|
|
),
|
2020-11-29 18:32:51 +02:00
|
|
|
textAlign,
|
2020-06-25 21:21:27 +02:00
|
|
|
color: updatedElement.strokeColor,
|
|
|
|
opacity: updatedElement.opacity / 100,
|
2021-03-13 18:58:06 +05:30
|
|
|
filter: "var(--theme-filter)",
|
2021-03-12 02:38:50 +05:30
|
|
|
maxWidth: `${maxWidth}px`,
|
2021-12-16 21:14:03 +05:30
|
|
|
maxHeight: `${editorMaxHeight}px`,
|
2020-06-25 21:21:27 +02:00
|
|
|
});
|
|
|
|
}
|
2020-11-06 22:06:39 +02:00
|
|
|
};
|
2020-06-25 21:21:27 +02:00
|
|
|
|
|
|
|
const editable = document.createElement("textarea");
|
|
|
|
|
2020-04-02 12:21:19 -04:00
|
|
|
editable.dir = "auto";
|
2020-01-09 02:04:53 +05:00
|
|
|
editable.tabIndex = 0;
|
|
|
|
editable.dataset.type = "wysiwyg";
|
2020-06-25 21:21:27 +02:00
|
|
|
// prevent line wrapping on Safari
|
|
|
|
editable.wrap = "off";
|
2021-11-17 23:53:43 +05:30
|
|
|
editable.classList.add("excalidraw-wysiwyg");
|
2020-04-02 17:40:26 +09:00
|
|
|
|
2021-12-16 21:14:03 +05:30
|
|
|
let whiteSpace = "pre";
|
2021-12-17 15:24:23 +05:30
|
|
|
let wordBreak = "normal";
|
|
|
|
|
|
|
|
if (isBoundToContainer(element)) {
|
|
|
|
whiteSpace = "pre-wrap";
|
|
|
|
wordBreak = "break-word";
|
2021-12-16 21:14:03 +05:30
|
|
|
}
|
2020-01-09 02:04:53 +05:00
|
|
|
Object.assign(editable.style, {
|
2021-03-07 21:12:10 +05:30
|
|
|
position: "absolute",
|
2020-01-09 02:04:53 +05:00
|
|
|
display: "inline-block",
|
2020-01-24 12:04:54 +02:00
|
|
|
minHeight: "1em",
|
2020-02-07 18:42:24 +01:00
|
|
|
backfaceVisibility: "hidden",
|
2020-06-25 21:21:27 +02:00
|
|
|
margin: 0,
|
|
|
|
padding: 0,
|
|
|
|
border: 0,
|
|
|
|
outline: 0,
|
|
|
|
resize: "none",
|
|
|
|
background: "transparent",
|
|
|
|
overflow: "hidden",
|
2021-02-04 16:21:48 +01:00
|
|
|
// must be specified because in dark mode canvas creates a stacking context
|
|
|
|
zIndex: "var(--zIndex-wysiwyg)",
|
2021-12-17 15:24:23 +05:30
|
|
|
wordBreak,
|
2021-12-16 21:14:03 +05:30
|
|
|
// prevent line wrapping (`whitespace: nowrap` doesn't work on FF)
|
|
|
|
whiteSpace,
|
|
|
|
overflowWrap: "break-word",
|
2020-01-07 22:21:05 +05:00
|
|
|
});
|
2020-06-25 21:21:27 +02:00
|
|
|
updateWysiwygStyle();
|
2020-02-07 18:42:24 +01:00
|
|
|
|
2020-04-03 14:16:14 +02:00
|
|
|
if (onChange) {
|
|
|
|
editable.oninput = () => {
|
2021-12-17 15:24:23 +05:30
|
|
|
if (isBoundToContainer(element)) {
|
|
|
|
editable.style.height = "auto";
|
|
|
|
editable.style.height = `${editable.scrollHeight}px`;
|
|
|
|
}
|
2020-06-25 21:21:27 +02:00
|
|
|
onChange(normalizeText(editable.value));
|
2020-04-03 14:16:14 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-04-04 11:41:54 +03:00
|
|
|
editable.onkeydown = (event) => {
|
2021-04-13 16:23:46 +02:00
|
|
|
event.stopPropagation();
|
2020-04-04 11:41:54 +03:00
|
|
|
if (event.key === KEYS.ESCAPE) {
|
|
|
|
event.preventDefault();
|
2021-03-16 18:04:53 +01:00
|
|
|
submittedViaKeyboard = true;
|
2020-03-12 18:04:56 +01:00
|
|
|
handleSubmit();
|
2020-04-08 20:56:27 +02:00
|
|
|
} else if (event.key === KEYS.ENTER && event[KEYS.CTRL_OR_CMD]) {
|
2020-04-04 11:41:54 +03:00
|
|
|
event.preventDefault();
|
|
|
|
if (event.isComposing || event.keyCode === 229) {
|
2020-04-03 13:41:32 +01:00
|
|
|
return;
|
|
|
|
}
|
2021-03-16 18:04:53 +01:00
|
|
|
submittedViaKeyboard = true;
|
2020-04-03 13:41:32 +01:00
|
|
|
handleSubmit();
|
2021-04-13 16:23:46 +02:00
|
|
|
} else if (
|
|
|
|
event.key === KEYS.TAB ||
|
|
|
|
(event[KEYS.CTRL_OR_CMD] &&
|
|
|
|
(event.code === CODES.BRACKET_LEFT ||
|
|
|
|
event.code === CODES.BRACKET_RIGHT))
|
|
|
|
) {
|
|
|
|
event.preventDefault();
|
|
|
|
if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
|
|
|
|
outdent();
|
|
|
|
} else {
|
|
|
|
indent();
|
|
|
|
}
|
|
|
|
// We must send an input event to resize the element
|
|
|
|
editable.dispatchEvent(new Event("input"));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const TAB_SIZE = 4;
|
|
|
|
const TAB = " ".repeat(TAB_SIZE);
|
|
|
|
const RE_LEADING_TAB = new RegExp(`^ {1,${TAB_SIZE}}`);
|
|
|
|
const indent = () => {
|
|
|
|
const { selectionStart, selectionEnd } = editable;
|
|
|
|
const linesStartIndices = getSelectedLinesStartIndices();
|
|
|
|
|
|
|
|
let value = editable.value;
|
2021-12-16 21:14:03 +05:30
|
|
|
linesStartIndices.forEach((startIndex: number) => {
|
2021-04-13 16:23:46 +02:00
|
|
|
const startValue = value.slice(0, startIndex);
|
|
|
|
const endValue = value.slice(startIndex);
|
|
|
|
|
|
|
|
value = `${startValue}${TAB}${endValue}`;
|
|
|
|
});
|
|
|
|
|
|
|
|
editable.value = value;
|
|
|
|
|
|
|
|
editable.selectionStart = selectionStart + TAB_SIZE;
|
|
|
|
editable.selectionEnd = selectionEnd + TAB_SIZE * linesStartIndices.length;
|
|
|
|
};
|
|
|
|
|
|
|
|
const outdent = () => {
|
|
|
|
const { selectionStart, selectionEnd } = editable;
|
|
|
|
const linesStartIndices = getSelectedLinesStartIndices();
|
|
|
|
const removedTabs: number[] = [];
|
|
|
|
|
|
|
|
let value = editable.value;
|
|
|
|
linesStartIndices.forEach((startIndex) => {
|
|
|
|
const tabMatch = value
|
|
|
|
.slice(startIndex, startIndex + TAB_SIZE)
|
|
|
|
.match(RE_LEADING_TAB);
|
|
|
|
|
|
|
|
if (tabMatch) {
|
|
|
|
const startValue = value.slice(0, startIndex);
|
|
|
|
const endValue = value.slice(startIndex + tabMatch[0].length);
|
|
|
|
|
|
|
|
// Delete a tab from the line
|
|
|
|
value = `${startValue}${endValue}`;
|
|
|
|
removedTabs.push(startIndex);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
editable.value = value;
|
|
|
|
|
|
|
|
if (removedTabs.length) {
|
|
|
|
if (selectionStart > removedTabs[removedTabs.length - 1]) {
|
|
|
|
editable.selectionStart = Math.max(
|
|
|
|
selectionStart - TAB_SIZE,
|
|
|
|
removedTabs[removedTabs.length - 1],
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
// If the cursor is before the first tab removed, ex:
|
|
|
|
// Line| #1
|
|
|
|
// Line #2
|
|
|
|
// Lin|e #3
|
|
|
|
// we should reset the selectionStart to his initial value.
|
|
|
|
editable.selectionStart = selectionStart;
|
|
|
|
}
|
|
|
|
editable.selectionEnd = Math.max(
|
|
|
|
editable.selectionStart,
|
|
|
|
selectionEnd - TAB_SIZE * removedTabs.length,
|
|
|
|
);
|
2020-01-07 22:21:05 +05:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-04-13 16:23:46 +02:00
|
|
|
/**
|
|
|
|
* @returns indeces of start positions of selected lines, in reverse order
|
|
|
|
*/
|
|
|
|
const getSelectedLinesStartIndices = () => {
|
|
|
|
let { selectionStart, selectionEnd, value } = editable;
|
|
|
|
|
|
|
|
// chars before selectionStart on the same line
|
|
|
|
const startOffset = value.slice(0, selectionStart).match(/[^\n]*$/)![0]
|
|
|
|
.length;
|
|
|
|
// put caret at the start of the line
|
|
|
|
selectionStart = selectionStart - startOffset;
|
|
|
|
|
|
|
|
const selected = value.slice(selectionStart, selectionEnd);
|
|
|
|
|
|
|
|
return selected
|
|
|
|
.split("\n")
|
|
|
|
.reduce(
|
|
|
|
(startIndices, line, idx, lines) =>
|
|
|
|
startIndices.concat(
|
|
|
|
idx
|
|
|
|
? // curr line index is prev line's start + prev line's length + \n
|
|
|
|
startIndices[idx - 1] + lines[idx - 1].length + 1
|
|
|
|
: // first selected line
|
|
|
|
selectionStart,
|
|
|
|
),
|
|
|
|
[] as number[],
|
|
|
|
)
|
|
|
|
.reverse();
|
|
|
|
};
|
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
const stopEvent = (event: Event) => {
|
2020-08-25 02:38:03 -07:00
|
|
|
event.preventDefault();
|
2020-04-04 11:41:54 +03:00
|
|
|
event.stopPropagation();
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-01-07 22:21:05 +05:00
|
|
|
|
2021-03-16 18:04:53 +01:00
|
|
|
// using a state variable instead of passing it to the handleSubmit callback
|
|
|
|
// so that we don't need to create separate a callback for event handlers
|
|
|
|
let submittedViaKeyboard = false;
|
2020-05-20 16:21:37 +03:00
|
|
|
const handleSubmit = () => {
|
2021-04-13 01:29:25 +05:30
|
|
|
// cleanup must be run before onSubmit otherwise when app blurs the wysiwyg
|
|
|
|
// it'd get stuck in an infinite loop of blur→onSubmit after we re-focus the
|
|
|
|
// wysiwyg on update
|
|
|
|
cleanup();
|
2021-12-16 21:14:03 +05:30
|
|
|
const updateElement = Scene.getScene(element)?.getElement(element.id);
|
|
|
|
if (!updateElement) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let wrappedText = "";
|
|
|
|
if (isTextElement(updateElement) && updateElement?.containerId) {
|
|
|
|
const container = Scene.getScene(updateElement)!.getElement(
|
|
|
|
updateElement.containerId,
|
|
|
|
) as ExcalidrawBindableElement;
|
|
|
|
|
|
|
|
if (container) {
|
|
|
|
wrappedText = wrapText(
|
|
|
|
editable.value,
|
|
|
|
getFontString(updateElement),
|
|
|
|
container.width,
|
|
|
|
);
|
|
|
|
if (isTextElement(updateElement) && updateElement.containerId) {
|
2021-12-21 17:13:11 +05:30
|
|
|
const editorHeight = Number(editable.style.height.slice(0, -2));
|
2021-12-16 21:14:03 +05:30
|
|
|
if (editable.value) {
|
|
|
|
mutateElement(updateElement, {
|
2021-12-21 17:13:11 +05:30
|
|
|
// vertically center align
|
|
|
|
y: container.y + container.height / 2 - editorHeight / 2,
|
|
|
|
height: editorHeight,
|
2021-12-16 21:14:03 +05:30
|
|
|
width: Number(editable.style.width.slice(0, -2)),
|
2021-12-21 17:13:11 +05:30
|
|
|
// preserve padding
|
|
|
|
x: container.x + PADDING,
|
2021-12-16 21:14:03 +05:30
|
|
|
});
|
|
|
|
const boundTextElementId = getBoundTextElementId(container);
|
|
|
|
if (!boundTextElementId || boundTextElementId !== element.id) {
|
|
|
|
mutateElement(container, {
|
|
|
|
boundElements: (container.boundElements || []).concat({
|
|
|
|
type: "text",
|
|
|
|
id: element.id,
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
mutateElement(container, {
|
|
|
|
boundElements: container.boundElements?.filter(
|
|
|
|
(ele) => ele.type !== "text",
|
|
|
|
),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
wrappedText = editable.value;
|
|
|
|
}
|
|
|
|
|
2021-03-16 18:04:53 +01:00
|
|
|
onSubmit({
|
2021-12-16 21:14:03 +05:30
|
|
|
text: normalizeText(wrappedText),
|
2021-03-16 18:04:53 +01:00
|
|
|
viaKeyboard: submittedViaKeyboard,
|
2021-12-16 21:14:03 +05:30
|
|
|
originalText: editable.value,
|
2021-03-16 18:04:53 +01:00
|
|
|
});
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-01-07 22:21:05 +05:00
|
|
|
|
2020-05-20 16:21:37 +03:00
|
|
|
const cleanup = () => {
|
2020-04-12 15:57:57 +02:00
|
|
|
if (isDestroyed) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
isDestroyed = true;
|
2020-04-05 22:31:59 +02:00
|
|
|
// remove events to ensure they don't late-fire
|
2020-04-12 15:57:57 +02:00
|
|
|
editable.onblur = null;
|
2020-04-05 22:31:59 +02:00
|
|
|
editable.oninput = null;
|
|
|
|
editable.onkeydown = null;
|
|
|
|
|
2021-02-11 12:24:26 +01:00
|
|
|
if (observer) {
|
|
|
|
observer.disconnect();
|
|
|
|
}
|
|
|
|
|
2020-06-25 21:21:27 +02:00
|
|
|
window.removeEventListener("resize", updateWysiwygStyle);
|
2020-01-07 22:21:05 +05:00
|
|
|
window.removeEventListener("wheel", stopEvent, true);
|
2020-04-12 15:57:57 +02:00
|
|
|
window.removeEventListener("pointerdown", onPointerDown);
|
2021-03-16 18:04:53 +01:00
|
|
|
window.removeEventListener("pointerup", bindBlurEvent);
|
2020-04-12 15:57:57 +02:00
|
|
|
window.removeEventListener("blur", handleSubmit);
|
|
|
|
|
|
|
|
unbindUpdate();
|
|
|
|
|
2021-02-04 14:54:00 +01:00
|
|
|
editable.remove();
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|
2020-01-07 22:21:05 +05:00
|
|
|
|
2021-12-17 20:15:22 +05:30
|
|
|
const bindBlurEvent = (event?: MouseEvent) => {
|
2021-03-16 18:04:53 +01:00
|
|
|
window.removeEventListener("pointerup", bindBlurEvent);
|
|
|
|
// Deferred so that the pointerdown that initiates the wysiwyg doesn't
|
|
|
|
// trigger the blur on ensuing pointerup.
|
|
|
|
// Also to handle cases such as picking a color which would trigger a blur
|
|
|
|
// in that same tick.
|
2021-12-17 20:15:22 +05:30
|
|
|
const target = event?.target;
|
|
|
|
|
|
|
|
const isTargetColorPicker =
|
|
|
|
target instanceof HTMLInputElement &&
|
|
|
|
target.closest(".color-picker-input") &&
|
|
|
|
isWritableElement(target);
|
|
|
|
|
2020-04-12 15:57:57 +02:00
|
|
|
setTimeout(() => {
|
|
|
|
editable.onblur = handleSubmit;
|
2021-12-17 20:15:22 +05:30
|
|
|
if (target && isTargetColorPicker) {
|
|
|
|
target.onblur = () => {
|
|
|
|
editable.focus();
|
|
|
|
};
|
|
|
|
}
|
2020-04-12 15:57:57 +02:00
|
|
|
// case: clicking on the same property → no change → no update → no focus
|
2021-12-17 20:15:22 +05:30
|
|
|
if (!isTargetColorPicker) {
|
|
|
|
editable.focus();
|
|
|
|
}
|
2020-04-12 15:57:57 +02:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
// prevent blur when changing properties from the menu
|
|
|
|
const onPointerDown = (event: MouseEvent) => {
|
2021-12-17 20:15:22 +05:30
|
|
|
const isTargetColorPicker =
|
|
|
|
event.target instanceof HTMLInputElement &&
|
|
|
|
event.target.closest(".color-picker-input") &&
|
|
|
|
isWritableElement(event.target);
|
2020-04-12 15:57:57 +02:00
|
|
|
if (
|
2021-12-17 20:15:22 +05:30
|
|
|
((event.target instanceof HTMLElement ||
|
2021-03-22 18:56:35 +05:30
|
|
|
event.target instanceof SVGElement) &&
|
2021-12-17 20:15:22 +05:30
|
|
|
event.target.closest(`.${CLASSES.SHAPE_ACTIONS_MENU}`) &&
|
|
|
|
!isWritableElement(event.target)) ||
|
|
|
|
isTargetColorPicker
|
2020-04-12 15:57:57 +02:00
|
|
|
) {
|
|
|
|
editable.onblur = null;
|
2021-03-16 18:04:53 +01:00
|
|
|
window.addEventListener("pointerup", bindBlurEvent);
|
2020-04-12 15:57:57 +02:00
|
|
|
// handle edge-case where pointerup doesn't fire e.g. due to user
|
2020-11-05 19:06:18 +02:00
|
|
|
// alt-tabbing away
|
2020-04-12 15:57:57 +02:00
|
|
|
window.addEventListener("blur", handleSubmit);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
// handle updates of textElement properties of editing element
|
2020-07-30 14:50:59 +05:30
|
|
|
const unbindUpdate = Scene.getScene(element)!.addCallback(() => {
|
2020-06-25 21:21:27 +02:00
|
|
|
updateWysiwygStyle();
|
2021-12-17 20:15:22 +05:30
|
|
|
const isColorPickerActive = !!document.activeElement?.closest(
|
|
|
|
".color-picker-input",
|
|
|
|
);
|
|
|
|
if (!isColorPickerActive) {
|
|
|
|
editable.focus();
|
|
|
|
}
|
2020-04-12 15:57:57 +02:00
|
|
|
});
|
|
|
|
|
2021-03-16 18:04:53 +01:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
2020-04-12 15:57:57 +02:00
|
|
|
let isDestroyed = false;
|
|
|
|
|
2021-03-16 18:04:53 +01:00
|
|
|
// select on init (focusing is done separately inside the bindBlurEvent()
|
|
|
|
// because we need it to happen *after* the blur event from `pointerdown`)
|
|
|
|
editable.select();
|
|
|
|
bindBlurEvent();
|
2021-02-11 12:24:26 +01:00
|
|
|
|
|
|
|
// reposition wysiwyg in case of canvas is resized. Using ResizeObserver
|
|
|
|
// is preferred so we catch changes from host, where window may not resize.
|
|
|
|
let observer: ResizeObserver | null = null;
|
|
|
|
if (canvas && "ResizeObserver" in window) {
|
|
|
|
observer = new window.ResizeObserver(() => {
|
|
|
|
updateWysiwygStyle();
|
|
|
|
});
|
|
|
|
observer.observe(canvas);
|
|
|
|
} else {
|
|
|
|
window.addEventListener("resize", updateWysiwygStyle);
|
|
|
|
}
|
|
|
|
|
2020-04-12 15:57:57 +02:00
|
|
|
window.addEventListener("pointerdown", onPointerDown);
|
2020-08-25 02:38:03 -07:00
|
|
|
window.addEventListener("wheel", stopEvent, {
|
|
|
|
passive: false,
|
|
|
|
capture: true,
|
|
|
|
});
|
2021-04-23 21:11:18 +05:30
|
|
|
excalidrawContainer
|
|
|
|
?.querySelector(".excalidraw-textEditorContainer")!
|
2021-02-04 14:54:00 +01:00
|
|
|
.appendChild(editable);
|
2020-05-20 16:21:37 +03:00
|
|
|
};
|