excalidraw/src/element/textWysiwyg.tsx

214 lines
5.6 KiB
TypeScript
Raw Normal View History

import { KEYS } from "../keys";
import { selectNode, isWritableElement } from "../utils";
import { globalSceneState } from "../scene";
import { isTextElement } from "./typeChecks";
import { CLASSES } from "../constants";
function 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, "");
}
type TextWysiwygParams = {
id: string;
initText: string;
x: number;
y: number;
strokeColor: string;
font: string;
opacity: number;
zoom: number;
Rotation support (#1099) * rotate rectanble with fixed angle * rotate dashed rectangle with fixed angle * fix rotate handler rect * fix canvas size with rotation * angle in element base * fix bug in calculating canvas size * trial only for rectangle * hitTest for rectangle rotation * properly resize rotated rectangle * fix canvas size calculation * giving up... workaround for now * **experimental** handler to rotate rectangle * remove rotation on copy for debugging * update snapshots * better rotation handler with atan2 * rotate when drawImage * add rotation handler * hitTest for any shapes * fix hitTest for curved lines * rotate text element * rotation locking * hint messaage for rotating * show proper handlers on mobile (a workaround, there should be a better way) * refactor hitTest * support exporting png * support exporting svg * fix rotating curved line * refactor drawElementFromCanvas with getElementAbsoluteCoords * fix export png and svg * adjust resize positions for lines (N, E, S, W) * do not make handlers big on mobile * Update src/locales/en.json Alright! Co-Authored-By: Lipis <lipiridis@gmail.com> * do not show rotation/resizing hints on mobile * proper calculation for N and W positions * simplify calculation * use "rotation" as property name for clarification (may increase bundle size) * update snapshots excluding rotation handle * refactor with adjustPositionWithRotation * refactor with adjustXYWithRotation * forgot to rename rotation * rename internal function * initialize element angle on restore * rotate wysiwyg editor * fix shift-rotate around 270deg * improve rotation locking * refactor adjustXYWithRotation * avoid rotation degree becomes >=360 * refactor with generateHandler Co-authored-by: Lipis <lipiridis@gmail.com> Co-authored-by: dwelle <luzar.david@gmail.com>
2020-04-02 17:40:26 +09:00
angle: number;
textAlign: string;
onChange?: (text: string) => void;
onSubmit: (text: string) => void;
onCancel: () => void;
};
export function textWysiwyg({
id,
initText,
x,
y,
strokeColor,
font,
opacity,
zoom,
Rotation support (#1099) * rotate rectanble with fixed angle * rotate dashed rectangle with fixed angle * fix rotate handler rect * fix canvas size with rotation * angle in element base * fix bug in calculating canvas size * trial only for rectangle * hitTest for rectangle rotation * properly resize rotated rectangle * fix canvas size calculation * giving up... workaround for now * **experimental** handler to rotate rectangle * remove rotation on copy for debugging * update snapshots * better rotation handler with atan2 * rotate when drawImage * add rotation handler * hitTest for any shapes * fix hitTest for curved lines * rotate text element * rotation locking * hint messaage for rotating * show proper handlers on mobile (a workaround, there should be a better way) * refactor hitTest * support exporting png * support exporting svg * fix rotating curved line * refactor drawElementFromCanvas with getElementAbsoluteCoords * fix export png and svg * adjust resize positions for lines (N, E, S, W) * do not make handlers big on mobile * Update src/locales/en.json Alright! Co-Authored-By: Lipis <lipiridis@gmail.com> * do not show rotation/resizing hints on mobile * proper calculation for N and W positions * simplify calculation * use "rotation" as property name for clarification (may increase bundle size) * update snapshots excluding rotation handle * refactor with adjustPositionWithRotation * refactor with adjustXYWithRotation * forgot to rename rotation * rename internal function * initialize element angle on restore * rotate wysiwyg editor * fix shift-rotate around 270deg * improve rotation locking * refactor adjustXYWithRotation * avoid rotation degree becomes >=360 * refactor with generateHandler Co-authored-by: Lipis <lipiridis@gmail.com> Co-authored-by: dwelle <luzar.david@gmail.com>
2020-04-02 17:40:26 +09:00
angle,
onChange,
textAlign,
2020-01-24 12:04:54 +02:00
onSubmit,
onCancel,
}: TextWysiwygParams) {
const editable = document.createElement("div");
2020-02-21 22:51:34 -05:00
try {
editable.contentEditable = "plaintext-only";
} catch {
editable.contentEditable = "true";
}
editable.dir = "auto";
editable.tabIndex = 0;
editable.innerText = initText;
editable.dataset.type = "wysiwyg";
Rotation support (#1099) * rotate rectanble with fixed angle * rotate dashed rectangle with fixed angle * fix rotate handler rect * fix canvas size with rotation * angle in element base * fix bug in calculating canvas size * trial only for rectangle * hitTest for rectangle rotation * properly resize rotated rectangle * fix canvas size calculation * giving up... workaround for now * **experimental** handler to rotate rectangle * remove rotation on copy for debugging * update snapshots * better rotation handler with atan2 * rotate when drawImage * add rotation handler * hitTest for any shapes * fix hitTest for curved lines * rotate text element * rotation locking * hint messaage for rotating * show proper handlers on mobile (a workaround, there should be a better way) * refactor hitTest * support exporting png * support exporting svg * fix rotating curved line * refactor drawElementFromCanvas with getElementAbsoluteCoords * fix export png and svg * adjust resize positions for lines (N, E, S, W) * do not make handlers big on mobile * Update src/locales/en.json Alright! Co-Authored-By: Lipis <lipiridis@gmail.com> * do not show rotation/resizing hints on mobile * proper calculation for N and W positions * simplify calculation * use "rotation" as property name for clarification (may increase bundle size) * update snapshots excluding rotation handle * refactor with adjustPositionWithRotation * refactor with adjustXYWithRotation * forgot to rename rotation * rename internal function * initialize element angle on restore * rotate wysiwyg editor * fix shift-rotate around 270deg * improve rotation locking * refactor adjustXYWithRotation * avoid rotation degree becomes >=360 * refactor with generateHandler Co-authored-by: Lipis <lipiridis@gmail.com> Co-authored-by: dwelle <luzar.david@gmail.com>
2020-04-02 17:40:26 +09:00
const degree = (180 * angle) / Math.PI;
Object.assign(editable.style, {
color: strokeColor,
position: "fixed",
opacity: opacity / 100,
top: `${y}px`,
left: `${x}px`,
Rotation support (#1099) * rotate rectanble with fixed angle * rotate dashed rectangle with fixed angle * fix rotate handler rect * fix canvas size with rotation * angle in element base * fix bug in calculating canvas size * trial only for rectangle * hitTest for rectangle rotation * properly resize rotated rectangle * fix canvas size calculation * giving up... workaround for now * **experimental** handler to rotate rectangle * remove rotation on copy for debugging * update snapshots * better rotation handler with atan2 * rotate when drawImage * add rotation handler * hitTest for any shapes * fix hitTest for curved lines * rotate text element * rotation locking * hint messaage for rotating * show proper handlers on mobile (a workaround, there should be a better way) * refactor hitTest * support exporting png * support exporting svg * fix rotating curved line * refactor drawElementFromCanvas with getElementAbsoluteCoords * fix export png and svg * adjust resize positions for lines (N, E, S, W) * do not make handlers big on mobile * Update src/locales/en.json Alright! Co-Authored-By: Lipis <lipiridis@gmail.com> * do not show rotation/resizing hints on mobile * proper calculation for N and W positions * simplify calculation * use "rotation" as property name for clarification (may increase bundle size) * update snapshots excluding rotation handle * refactor with adjustPositionWithRotation * refactor with adjustXYWithRotation * forgot to rename rotation * rename internal function * initialize element angle on restore * rotate wysiwyg editor * fix shift-rotate around 270deg * improve rotation locking * refactor adjustXYWithRotation * avoid rotation degree becomes >=360 * refactor with generateHandler Co-authored-by: Lipis <lipiridis@gmail.com> Co-authored-by: dwelle <luzar.david@gmail.com>
2020-04-02 17:40:26 +09:00
transform: `translate(-50%, -50%) scale(${zoom}) rotate(${degree}deg)`,
textAlign: textAlign,
display: "inline-block",
font: font,
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",
2020-01-24 12:04:54 +02:00
minHeight: "1em",
backfaceVisibility: "hidden",
});
2020-04-04 11:41:54 +03:00
editable.onpaste = (event) => {
try {
const selection = window.getSelection();
if (!selection?.rangeCount) {
return;
}
selection.deleteFromDocument();
2020-04-04 11:41:54 +03:00
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);
2020-04-04 11:41:54 +03:00
event.preventDefault();
} catch (error) {
console.error(error);
}
};
if (onChange) {
editable.oninput = () => {
onChange(trimText(editable.innerText));
};
}
2020-04-04 11:41:54 +03:00
editable.onkeydown = (event) => {
if (event.key === KEYS.ESCAPE) {
event.preventDefault();
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) {
return;
}
handleSubmit();
2020-04-08 20:56:27 +02:00
} else if (event.key === KEYS.ENTER && !event.altKey) {
2020-04-04 11:41:54 +03:00
event.stopPropagation();
}
};
2020-04-04 11:41:54 +03:00
function stopEvent(event: Event) {
event.stopPropagation();
}
function handleSubmit() {
if (editable.innerText) {
onSubmit(trimText(editable.innerText));
} else {
onCancel();
}
cleanup();
}
function cleanup() {
if (isDestroyed) {
return;
}
isDestroyed = true;
2020-04-05 22:31:59 +02:00
// remove events to ensure they don't late-fire
editable.onblur = null;
2020-04-05 22:31:59 +02:00
editable.onpaste = null;
editable.oninput = null;
editable.onkeydown = null;
window.removeEventListener("wheel", stopEvent, true);
window.removeEventListener("pointerdown", onPointerDown);
window.removeEventListener("pointerup", rebindBlur);
window.removeEventListener("blur", handleSubmit);
unbindUpdate();
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);
document.body.appendChild(editable);
editable.focus();
selectNode(editable);
}