65be7973be
* 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>
135 lines
3.2 KiB
TypeScript
135 lines
3.2 KiB
TypeScript
import { KEYS } from "../keys";
|
|
import { selectNode } from "../utils";
|
|
|
|
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 = {
|
|
initText: string;
|
|
x: number;
|
|
y: number;
|
|
strokeColor: string;
|
|
font: string;
|
|
opacity: number;
|
|
zoom: number;
|
|
angle: number;
|
|
onSubmit: (text: string) => void;
|
|
onCancel: () => void;
|
|
};
|
|
|
|
export function textWysiwyg({
|
|
initText,
|
|
x,
|
|
y,
|
|
strokeColor,
|
|
font,
|
|
opacity,
|
|
zoom,
|
|
angle,
|
|
onSubmit,
|
|
onCancel,
|
|
}: TextWysiwygParams) {
|
|
const editable = document.createElement("div");
|
|
try {
|
|
editable.contentEditable = "plaintext-only";
|
|
} catch {
|
|
editable.contentEditable = "true";
|
|
}
|
|
editable.tabIndex = 0;
|
|
editable.innerText = initText;
|
|
editable.dataset.type = "wysiwyg";
|
|
|
|
const degree = (180 * angle) / Math.PI;
|
|
|
|
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: "left",
|
|
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",
|
|
minHeight: "1em",
|
|
backfaceVisibility: "hidden",
|
|
});
|
|
|
|
editable.onpaste = (ev) => {
|
|
try {
|
|
const selection = window.getSelection();
|
|
if (!selection?.rangeCount) {
|
|
return;
|
|
}
|
|
selection.deleteFromDocument();
|
|
|
|
const text = ev.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);
|
|
|
|
ev.preventDefault();
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
};
|
|
|
|
editable.onkeydown = (ev) => {
|
|
if (ev.key === KEYS.ESCAPE) {
|
|
ev.preventDefault();
|
|
handleSubmit();
|
|
}
|
|
if (ev.key === KEYS.ENTER && !ev.shiftKey) {
|
|
ev.stopPropagation();
|
|
}
|
|
};
|
|
editable.onblur = handleSubmit;
|
|
|
|
function stopEvent(ev: Event) {
|
|
ev.stopPropagation();
|
|
}
|
|
|
|
function handleSubmit() {
|
|
if (editable.innerText) {
|
|
onSubmit(trimText(editable.innerText));
|
|
} else {
|
|
onCancel();
|
|
}
|
|
cleanup();
|
|
}
|
|
|
|
function cleanup() {
|
|
editable.onblur = null;
|
|
editable.onkeydown = null;
|
|
editable.onpaste = null;
|
|
window.removeEventListener("wheel", stopEvent, true);
|
|
document.body.removeChild(editable);
|
|
}
|
|
|
|
window.addEventListener("wheel", stopEvent, true);
|
|
document.body.appendChild(editable);
|
|
editable.focus();
|
|
selectNode(editable);
|
|
}
|