This commit is contained in:
dwelle
2025-01-02 16:59:14 +01:00
parent 55be6de5ec
commit e5a9786af3
18 changed files with 167 additions and 68 deletions
+83 -45
View File
@@ -11,7 +11,7 @@ import {
isBoundToContainer,
isTextElement,
} from "./typeChecks";
import { CLASSES, POINTER_BUTTON } from "../constants";
import { CLASSES, EVENT, isSafari, POINTER_BUTTON } from "../constants";
import type {
ExcalidrawElement,
ExcalidrawLinearElement,
@@ -50,6 +50,8 @@ import {
originalContainerCache,
updateOriginalContainerCache,
} from "./containerCache";
import { activeEyeDropperAtom } from "../components/EyeDropper";
import { jotaiStore } from "../jotai";
const getTransform = (
width: number,
@@ -524,6 +526,7 @@ export const textWysiwyg = ({
// so that we don't need to create separate a callback for event handlers
let submittedViaKeyboard = false;
const handleSubmit = () => {
console.warn("handleSubmit");
// prevent double submit
if (isDestroyed) {
return;
@@ -581,62 +584,96 @@ export const textWysiwyg = ({
});
};
const onBlur = () => {
console.warn("onBlur", document.activeElement);
const isColorPicking = jotaiStore.get(activeEyeDropperAtom);
if (isColorPicking) {
focusEditable(null);
} else if (document.activeElement !== editable) {
handleSubmit();
}
};
const cleanup = () => {
// remove events to ensure they don't late-fire
editable.onblur = null;
editable.oninput = null;
editable.onkeydown = null;
editable.onpointerdown = null;
if (observer) {
observer.disconnect();
}
window.removeEventListener("resize", updateWysiwygStyle);
window.removeEventListener("wheel", stopEvent, true);
window.removeEventListener("pointerdown", onPointerDown);
window.removeEventListener("pointerup", bindBlurEvent);
window.removeEventListener("blur", handleSubmit);
window.removeEventListener("beforeunload", handleSubmit);
window.removeEventListener(EVENT.RESIZE, updateWysiwygStyle);
window.removeEventListener(EVENT.WHEEL, stopEvent, true);
window.removeEventListener(EVENT.POINTER_DOWN, onPointerDown, {
capture: true,
});
window.removeEventListener(EVENT.POINTER_UP, onPointerUp);
window.removeEventListener(EVENT.BLUR, onBlur);
window.removeEventListener(EVENT.BEFORE_UNLOAD, handleSubmit);
unbindUpdate();
unbindOnScroll();
editable.remove();
};
const bindBlurEvent = (event?: MouseEvent) => {
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.
const focusEditable = (event: MouseEvent | FocusEvent | null) => {
const target = event?.target;
const isPropertiesTrigger =
target instanceof HTMLElement &&
target.classList.contains("properties-trigger");
const shouldSkipRefocus =
target &&
// don't steal focus if user is focusing an input such as HEX input
((isWritableElement(target) && document.activeElement !== editable) ||
// refocusing while clicking on popver breaks safari
(isSafari &&
target instanceof HTMLElement &&
target.classList.contains(CLASSES.PROPERTIES_POPOVER_TRIGGER)));
setTimeout(() => {
editable.onblur = handleSubmit;
// case: clicking on the same property → no change → no update → no focus
if (!isPropertiesTrigger) {
editable.focus();
}
});
if (!shouldSkipRefocus) {
// 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.
setTimeout(() => {
// double deferred because on onUpdate/color picker shennanings
setTimeout(() => {
editable.focus();
});
});
}
};
const temporarilyDisableSubmit = () => {
const onPointerUp = (event: PointerEvent | FocusEvent) => {
window.removeEventListener(EVENT.POINTER_UP, onPointerUp);
window.removeEventListener(EVENT.FOCUS, onPointerUp);
// needs to be deferred due to Safari
setTimeout(() => {
editable.onblur = onBlur;
});
focusEditable(event);
};
const disableBlurUntilNextPointerUp = () => {
editable.onblur = null;
window.addEventListener("pointerup", bindBlurEvent);
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
// handle edge-case where pointerup doesn't fire e.g. due to user
// alt-tabbing away
window.addEventListener("blur", handleSubmit);
window.addEventListener(EVENT.FOCUS, onPointerUp);
};
// prevent blur when changing properties from the menu
const onPointerDown = (event: MouseEvent) => {
const target = event?.target;
// ugly hack to close popups such as color picker when clicking back
// into the wysiwyg editor (it won't autoclose as blur won't trigger
// since we perpetually keep focus inside the wysiwyg)
if (target === editable && app.state.openPopup) {
app.setState({ openPopup: null });
}
// panning canvas
if (event.button === POINTER_BUTTON.WHEEL) {
// trying to pan by clicking inside text area itself -> handle here
@@ -644,24 +681,18 @@ export const textWysiwyg = ({
event.preventDefault();
app.handleCanvasPanUsingWheelOrSpaceDrag(event);
}
temporarilyDisableSubmit();
disableBlurUntilNextPointerUp();
return;
}
const isPropertiesTrigger =
target instanceof HTMLElement &&
target.classList.contains("properties-trigger");
if (
((event.target instanceof HTMLElement ||
(event.target instanceof HTMLElement ||
event.target instanceof SVGElement) &&
event.target.closest(
`.${CLASSES.SHAPE_ACTIONS_MENU}, .${CLASSES.ZOOM_ACTIONS}`,
) &&
!isWritableElement(event.target)) ||
isPropertiesTrigger
event.target.closest(
`.${CLASSES.SHAPE_ACTIONS_MENU}, .${CLASSES.ZOOM_ACTIONS}, .${CLASSES.PROPERTIES_POPOVER}`,
)
) {
temporarilyDisableSubmit();
disableBlurUntilNextPointerUp();
} else if (
event.target instanceof HTMLCanvasElement &&
// Vitest simply ignores stopPropagation, capture-mode, or rAF
@@ -684,9 +715,11 @@ export const textWysiwyg = ({
const unbindUpdate = app.scene.onUpdate(() => {
updateWysiwygStyle();
const isPopupOpened = !!document.activeElement?.closest(
".properties-content",
CLASSES.PROPERTIES_POPOVER,
);
if (!isPopupOpened) {
// we need to keep this code path for safari (iPadOS) bs reasons
// (also Vitest)
editable.focus();
}
});
@@ -704,8 +737,11 @@ export const textWysiwyg = ({
// because we need it to happen *after* the blur event from `pointerdown`)
editable.select();
}
bindBlurEvent();
focusEditable(null);
setTimeout(() => {
editable.onblur = onBlur;
});
console.log(">>>>>>>>", app.state.editingTextElement);
// 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;
@@ -715,7 +751,7 @@ export const textWysiwyg = ({
});
observer.observe(canvas);
} else {
window.addEventListener("resize", updateWysiwygStyle);
window.addEventListener(EVENT.RESIZE, updateWysiwygStyle);
}
editable.onpointerdown = (event) => event.stopPropagation();
@@ -723,9 +759,11 @@ export const textWysiwyg = ({
// rAF (+ capture to by doubly sure) so we don't catch te pointerdown that
// triggered the wysiwyg
requestAnimationFrame(() => {
window.addEventListener("pointerdown", onPointerDown, { capture: true });
window.addEventListener(EVENT.POINTER_DOWN, onPointerDown, {
capture: true,
});
});
window.addEventListener("beforeunload", handleSubmit);
window.addEventListener(EVENT.BEFORE_UNLOAD, handleSubmit);
excalidrawContainer
?.querySelector(".excalidraw-textEditorContainer")!
.appendChild(editable);