wip
This commit is contained in:
parent
55be6de5ec
commit
e5a9786af3
@ -25,7 +25,7 @@ VITE_APP_ENABLE_TRACKING=true
|
|||||||
FAST_REFRESH=false
|
FAST_REFRESH=false
|
||||||
|
|
||||||
# The port the run the dev server
|
# The port the run the dev server
|
||||||
VITE_APP_PORT=3000
|
VITE_APP_PORT=3001
|
||||||
|
|
||||||
#Debug flags
|
#Debug flags
|
||||||
|
|
||||||
|
@ -4845,6 +4845,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
) {
|
) {
|
||||||
const elementsMap = this.scene.getElementsMapIncludingDeleted();
|
const elementsMap = this.scene.getElementsMapIncludingDeleted();
|
||||||
|
|
||||||
|
// flushSync(() => {
|
||||||
|
// this.setState({
|
||||||
|
// editingTextElement: element,
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
const updateElement = (nextOriginalText: string, isDeleted: boolean) => {
|
const updateElement = (nextOriginalText: string, isDeleted: boolean) => {
|
||||||
this.scene.replaceAllElements([
|
this.scene.replaceAllElements([
|
||||||
// Not sure why we include deleted elements as well hence using deleted elements map
|
// Not sure why we include deleted elements as well hence using deleted elements map
|
||||||
@ -6278,6 +6284,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
private handleCanvasPointerDown = (
|
private handleCanvasPointerDown = (
|
||||||
event: React.PointerEvent<HTMLElement>,
|
event: React.PointerEvent<HTMLElement>,
|
||||||
) => {
|
) => {
|
||||||
|
console.log("(1)", document.activeElement);
|
||||||
|
console.time();
|
||||||
|
this.focusContainer();
|
||||||
|
console.timeEnd();
|
||||||
|
console.log("(2)", document.activeElement);
|
||||||
this.maybeCleanupAfterMissingPointerUp(event.nativeEvent);
|
this.maybeCleanupAfterMissingPointerUp(event.nativeEvent);
|
||||||
this.maybeUnfollowRemoteUser();
|
this.maybeUnfollowRemoteUser();
|
||||||
|
|
||||||
@ -6722,17 +6733,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
isPanning = true;
|
isPanning = true;
|
||||||
|
|
||||||
// due to event.preventDefault below, container wouldn't get focus
|
|
||||||
// automatically
|
|
||||||
this.focusContainer();
|
|
||||||
|
|
||||||
// preventing defualt while text editing messes with cursor/focus
|
// preventing defualt while text editing messes with cursor/focus
|
||||||
if (!this.state.editingTextElement) {
|
if (!this.state.editingTextElement) {
|
||||||
// necessary to prevent browser from scrolling the page if excalidraw
|
// necessary to prevent browser from scrolling the page if excalidraw
|
||||||
// not full-page #4489
|
// not full-page #4489
|
||||||
//
|
//
|
||||||
// as such, the above is broken when panning canvas while in wysiwyg
|
// note, this fix won't work when panning canvas while in wysiwyg since
|
||||||
|
// we don't execute it while in wysiwyg
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
// focus explicitly due to the event.preventDefault above
|
||||||
|
this.focusContainer();
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextPastePrevented = false;
|
let nextPastePrevented = false;
|
||||||
|
@ -19,6 +19,7 @@ import { jotaiScope } from "../../jotai";
|
|||||||
import { ColorInput } from "./ColorInput";
|
import { ColorInput } from "./ColorInput";
|
||||||
import { activeEyeDropperAtom } from "../EyeDropper";
|
import { activeEyeDropperAtom } from "../EyeDropper";
|
||||||
import { PropertiesPopover } from "../PropertiesPopover";
|
import { PropertiesPopover } from "../PropertiesPopover";
|
||||||
|
import { CLASSES } from "../../constants";
|
||||||
|
|
||||||
import "./ColorPicker.scss";
|
import "./ColorPicker.scss";
|
||||||
|
|
||||||
@ -186,9 +187,13 @@ const ColorPickerTrigger = ({
|
|||||||
return (
|
return (
|
||||||
<Popover.Trigger
|
<Popover.Trigger
|
||||||
type="button"
|
type="button"
|
||||||
className={clsx("color-picker__button active-color properties-trigger", {
|
className={clsx(
|
||||||
|
"color-picker__button active-color",
|
||||||
|
CLASSES.PROPERTIES_POPOVER_TRIGGER,
|
||||||
|
{
|
||||||
"is-transparent": color === "transparent" || !color,
|
"is-transparent": color === "transparent" || !color,
|
||||||
})}
|
},
|
||||||
|
)}
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
style={color ? { "--swatch-color": color } : undefined}
|
style={color ? { "--swatch-color": color } : undefined}
|
||||||
title={
|
title={
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom } from "jotai";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
import { useUIAppState } from "../../context/ui-appState";
|
||||||
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
|
import { activeColorPickerSectionAtom } from "./colorPickerUtils";
|
||||||
import HotkeyLabel from "./HotkeyLabel";
|
import HotkeyLabel from "./HotkeyLabel";
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ export const CustomColorList = ({
|
|||||||
onChange,
|
onChange,
|
||||||
label,
|
label,
|
||||||
}: CustomColorListProps) => {
|
}: CustomColorListProps) => {
|
||||||
|
const appState = useUIAppState();
|
||||||
const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(
|
const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(
|
||||||
activeColorPickerSectionAtom,
|
activeColorPickerSectionAtom,
|
||||||
);
|
);
|
||||||
@ -54,7 +56,9 @@ export const CustomColorList = ({
|
|||||||
key={i}
|
key={i}
|
||||||
>
|
>
|
||||||
<div className="color-picker__button-outline" />
|
<div className="color-picker__button-outline" />
|
||||||
|
{!appState.editingTextElement && (
|
||||||
<HotkeyLabel color={c} keyLabel={i + 1} isCustomColor />
|
<HotkeyLabel color={c} keyLabel={i + 1} isCustomColor />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -10,6 +10,7 @@ import HotkeyLabel from "./HotkeyLabel";
|
|||||||
import type { ColorPaletteCustom } from "../../colors";
|
import type { ColorPaletteCustom } from "../../colors";
|
||||||
import type { TranslationKeys } from "../../i18n";
|
import type { TranslationKeys } from "../../i18n";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
|
import { useUIAppState } from "../../context/ui-appState";
|
||||||
|
|
||||||
interface PickerColorListProps {
|
interface PickerColorListProps {
|
||||||
palette: ColorPaletteCustom;
|
palette: ColorPaletteCustom;
|
||||||
@ -26,6 +27,8 @@ const PickerColorList = ({
|
|||||||
label,
|
label,
|
||||||
activeShade,
|
activeShade,
|
||||||
}: PickerColorListProps) => {
|
}: PickerColorListProps) => {
|
||||||
|
const appState = useUIAppState();
|
||||||
|
|
||||||
const colorObj = getColorNameAndShadeFromColor({
|
const colorObj = getColorNameAndShadeFromColor({
|
||||||
color: color || "transparent",
|
color: color || "transparent",
|
||||||
palette,
|
palette,
|
||||||
@ -80,7 +83,9 @@ const PickerColorList = ({
|
|||||||
key={key}
|
key={key}
|
||||||
>
|
>
|
||||||
<div className="color-picker__button-outline" />
|
<div className="color-picker__button-outline" />
|
||||||
|
{!appState.editingTextElement && (
|
||||||
<HotkeyLabel color={color} keyLabel={keybinding} />
|
<HotkeyLabel color={color} keyLabel={keybinding} />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
import HotkeyLabel from "./HotkeyLabel";
|
import HotkeyLabel from "./HotkeyLabel";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import type { ColorPaletteCustom } from "../../colors";
|
import type { ColorPaletteCustom } from "../../colors";
|
||||||
|
import { useUIAppState } from "../../context/ui-appState";
|
||||||
|
|
||||||
interface ShadeListProps {
|
interface ShadeListProps {
|
||||||
hex: string;
|
hex: string;
|
||||||
@ -16,6 +17,8 @@ interface ShadeListProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ShadeList = ({ hex, onChange, palette }: ShadeListProps) => {
|
export const ShadeList = ({ hex, onChange, palette }: ShadeListProps) => {
|
||||||
|
const appState = useUIAppState();
|
||||||
|
|
||||||
const colorObj = getColorNameAndShadeFromColor({
|
const colorObj = getColorNameAndShadeFromColor({
|
||||||
color: hex || "transparent",
|
color: hex || "transparent",
|
||||||
palette,
|
palette,
|
||||||
@ -31,7 +34,7 @@ export const ShadeList = ({ hex, onChange, palette }: ShadeListProps) => {
|
|||||||
if (btnRef.current && activeColorPickerSection === "shades") {
|
if (btnRef.current && activeColorPickerSection === "shades") {
|
||||||
btnRef.current.focus();
|
btnRef.current.focus();
|
||||||
}
|
}
|
||||||
}, [colorObj, activeColorPickerSection]);
|
}, [colorObj?.colorName, colorObj?.shade, activeColorPickerSection]);
|
||||||
|
|
||||||
if (colorObj) {
|
if (colorObj) {
|
||||||
const { colorName, shade } = colorObj;
|
const { colorName, shade } = colorObj;
|
||||||
@ -64,7 +67,9 @@ export const ShadeList = ({ hex, onChange, palette }: ShadeListProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="color-picker__button-outline" />
|
<div className="color-picker__button-outline" />
|
||||||
|
{!appState.editingTextElement && (
|
||||||
<HotkeyLabel color={color} keyLabel={i + 1} isShade />
|
<HotkeyLabel color={color} keyLabel={i + 1} isShade />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,6 +56,7 @@ export const TopPicks = ({
|
|||||||
title={color}
|
title={color}
|
||||||
onClick={() => onChange(color)}
|
onClick={() => onChange(color)}
|
||||||
data-testid={`color-top-pick-${color}`}
|
data-testid={`color-top-pick-${color}`}
|
||||||
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<div className="color-picker__button-outline" />
|
<div className="color-picker__button-outline" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -250,6 +250,10 @@ export const FontPickerList = React.memo(
|
|||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onPointerLeave={onLeave}
|
onPointerLeave={onLeave}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
|
onFocusOutside={(event) => {
|
||||||
|
// so we don't close when refocusing wysiwyg while editing
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<QuickSearch
|
<QuickSearch
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
@ -5,6 +5,7 @@ import { TextIcon } from "../icons";
|
|||||||
import type { FontFamilyValues } from "../../element/types";
|
import type { FontFamilyValues } from "../../element/types";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import { isDefaultFont } from "./FontPicker";
|
import { isDefaultFont } from "./FontPicker";
|
||||||
|
import { CLASSES } from "../../constants";
|
||||||
|
|
||||||
interface FontPickerTriggerProps {
|
interface FontPickerTriggerProps {
|
||||||
selectedFontFamily: FontFamilyValues | null;
|
selectedFontFamily: FontFamilyValues | null;
|
||||||
@ -26,7 +27,7 @@ export const FontPickerTrigger = ({
|
|||||||
standalone
|
standalone
|
||||||
icon={TextIcon}
|
icon={TextIcon}
|
||||||
title={t("labels.showFonts")}
|
title={t("labels.showFonts")}
|
||||||
className="properties-trigger"
|
className={CLASSES.PROPERTIES_POPOVER_TRIGGER}
|
||||||
testId={"font-family-show-fonts"}
|
testId={"font-family-show-fonts"}
|
||||||
active={isTriggerActive}
|
active={isTriggerActive}
|
||||||
// no-op
|
// no-op
|
||||||
|
@ -5,6 +5,7 @@ import * as Popover from "@radix-ui/react-popover";
|
|||||||
import { useDevice } from "./App";
|
import { useDevice } from "./App";
|
||||||
import { Island } from "./Island";
|
import { Island } from "./Island";
|
||||||
import { isInteractive } from "../utils";
|
import { isInteractive } from "../utils";
|
||||||
|
import { CLASSES } from "../constants";
|
||||||
|
|
||||||
interface PropertiesPopoverProps {
|
interface PropertiesPopoverProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -42,7 +43,11 @@ export const PropertiesPopover = React.forwardRef<
|
|||||||
<Popover.Portal container={container}>
|
<Popover.Portal container={container}>
|
||||||
<Popover.Content
|
<Popover.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clsx("focus-visible-none", className)}
|
className={clsx(
|
||||||
|
"focus-visible-none",
|
||||||
|
CLASSES.PROPERTIES_POPOVER,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
data-prevent-outside-click
|
data-prevent-outside-click
|
||||||
side={
|
side={
|
||||||
device.editor.isMobile && !device.viewport.isLandscape
|
device.editor.isMobile && !device.viewport.isLandscape
|
||||||
|
@ -115,6 +115,8 @@ export const CLASSES = {
|
|||||||
SHAPE_ACTIONS_MENU: "App-menu__left",
|
SHAPE_ACTIONS_MENU: "App-menu__left",
|
||||||
ZOOM_ACTIONS: "zoom-actions",
|
ZOOM_ACTIONS: "zoom-actions",
|
||||||
SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
|
SEARCH_MENU_INPUT_WRAPPER: "layer-ui__search-inputWrapper",
|
||||||
|
PROPERTIES_POPOVER: "properties-popover",
|
||||||
|
PROPERTIES_POPOVER_TRIGGER: "properties-popover-trigger",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CJK_HAND_DRAWN_FALLBACK_FONT = "Xiaolai";
|
export const CJK_HAND_DRAWN_FALLBACK_FONT = "Xiaolai";
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
isBoundToContainer,
|
isBoundToContainer,
|
||||||
isTextElement,
|
isTextElement,
|
||||||
} from "./typeChecks";
|
} from "./typeChecks";
|
||||||
import { CLASSES, POINTER_BUTTON } from "../constants";
|
import { CLASSES, EVENT, isSafari, POINTER_BUTTON } from "../constants";
|
||||||
import type {
|
import type {
|
||||||
ExcalidrawElement,
|
ExcalidrawElement,
|
||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
@ -50,6 +50,8 @@ import {
|
|||||||
originalContainerCache,
|
originalContainerCache,
|
||||||
updateOriginalContainerCache,
|
updateOriginalContainerCache,
|
||||||
} from "./containerCache";
|
} from "./containerCache";
|
||||||
|
import { activeEyeDropperAtom } from "../components/EyeDropper";
|
||||||
|
import { jotaiStore } from "../jotai";
|
||||||
|
|
||||||
const getTransform = (
|
const getTransform = (
|
||||||
width: number,
|
width: number,
|
||||||
@ -524,6 +526,7 @@ export const textWysiwyg = ({
|
|||||||
// so that we don't need to create separate a callback for event handlers
|
// so that we don't need to create separate a callback for event handlers
|
||||||
let submittedViaKeyboard = false;
|
let submittedViaKeyboard = false;
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
|
console.warn("handleSubmit");
|
||||||
// prevent double submit
|
// prevent double submit
|
||||||
if (isDestroyed) {
|
if (isDestroyed) {
|
||||||
return;
|
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 = () => {
|
const cleanup = () => {
|
||||||
// remove events to ensure they don't late-fire
|
// remove events to ensure they don't late-fire
|
||||||
editable.onblur = null;
|
editable.onblur = null;
|
||||||
editable.oninput = null;
|
editable.oninput = null;
|
||||||
editable.onkeydown = null;
|
editable.onkeydown = null;
|
||||||
|
editable.onpointerdown = null;
|
||||||
|
|
||||||
if (observer) {
|
if (observer) {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.removeEventListener("resize", updateWysiwygStyle);
|
window.removeEventListener(EVENT.RESIZE, updateWysiwygStyle);
|
||||||
window.removeEventListener("wheel", stopEvent, true);
|
window.removeEventListener(EVENT.WHEEL, stopEvent, true);
|
||||||
window.removeEventListener("pointerdown", onPointerDown);
|
window.removeEventListener(EVENT.POINTER_DOWN, onPointerDown, {
|
||||||
window.removeEventListener("pointerup", bindBlurEvent);
|
capture: true,
|
||||||
window.removeEventListener("blur", handleSubmit);
|
});
|
||||||
window.removeEventListener("beforeunload", handleSubmit);
|
window.removeEventListener(EVENT.POINTER_UP, onPointerUp);
|
||||||
|
window.removeEventListener(EVENT.BLUR, onBlur);
|
||||||
|
window.removeEventListener(EVENT.BEFORE_UNLOAD, handleSubmit);
|
||||||
unbindUpdate();
|
unbindUpdate();
|
||||||
unbindOnScroll();
|
unbindOnScroll();
|
||||||
|
|
||||||
editable.remove();
|
editable.remove();
|
||||||
};
|
};
|
||||||
|
|
||||||
const bindBlurEvent = (event?: MouseEvent) => {
|
const focusEditable = (event: MouseEvent | FocusEvent | null) => {
|
||||||
window.removeEventListener("pointerup", bindBlurEvent);
|
const target = event?.target;
|
||||||
|
|
||||||
|
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)));
|
||||||
|
|
||||||
|
if (!shouldSkipRefocus) {
|
||||||
// Deferred so that the pointerdown that initiates the wysiwyg doesn't
|
// Deferred so that the pointerdown that initiates the wysiwyg doesn't
|
||||||
// trigger the blur on ensuing pointerup.
|
// trigger the blur on ensuing pointerup.
|
||||||
// Also to handle cases such as picking a color which would trigger a blur
|
// Also to handle cases such as picking a color which would trigger a blur
|
||||||
// in that same tick.
|
// in that same tick.
|
||||||
const target = event?.target;
|
|
||||||
|
|
||||||
const isPropertiesTrigger =
|
|
||||||
target instanceof HTMLElement &&
|
|
||||||
target.classList.contains("properties-trigger");
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
editable.onblur = handleSubmit;
|
// double deferred because on onUpdate/color picker shennanings
|
||||||
|
setTimeout(() => {
|
||||||
// case: clicking on the same property → no change → no update → no focus
|
|
||||||
if (!isPropertiesTrigger) {
|
|
||||||
editable.focus();
|
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;
|
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
|
// handle edge-case where pointerup doesn't fire e.g. due to user
|
||||||
// alt-tabbing away
|
// alt-tabbing away
|
||||||
window.addEventListener("blur", handleSubmit);
|
window.addEventListener(EVENT.FOCUS, onPointerUp);
|
||||||
};
|
};
|
||||||
|
|
||||||
// prevent blur when changing properties from the menu
|
// prevent blur when changing properties from the menu
|
||||||
const onPointerDown = (event: MouseEvent) => {
|
const onPointerDown = (event: MouseEvent) => {
|
||||||
const target = event?.target;
|
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
|
// panning canvas
|
||||||
if (event.button === POINTER_BUTTON.WHEEL) {
|
if (event.button === POINTER_BUTTON.WHEEL) {
|
||||||
// trying to pan by clicking inside text area itself -> handle here
|
// trying to pan by clicking inside text area itself -> handle here
|
||||||
@ -644,24 +681,18 @@ export const textWysiwyg = ({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
app.handleCanvasPanUsingWheelOrSpaceDrag(event);
|
app.handleCanvasPanUsingWheelOrSpaceDrag(event);
|
||||||
}
|
}
|
||||||
temporarilyDisableSubmit();
|
disableBlurUntilNextPointerUp();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPropertiesTrigger =
|
|
||||||
target instanceof HTMLElement &&
|
|
||||||
target.classList.contains("properties-trigger");
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
((event.target instanceof HTMLElement ||
|
(event.target instanceof HTMLElement ||
|
||||||
event.target instanceof SVGElement) &&
|
event.target instanceof SVGElement) &&
|
||||||
event.target.closest(
|
event.target.closest(
|
||||||
`.${CLASSES.SHAPE_ACTIONS_MENU}, .${CLASSES.ZOOM_ACTIONS}`,
|
`.${CLASSES.SHAPE_ACTIONS_MENU}, .${CLASSES.ZOOM_ACTIONS}, .${CLASSES.PROPERTIES_POPOVER}`,
|
||||||
) &&
|
)
|
||||||
!isWritableElement(event.target)) ||
|
|
||||||
isPropertiesTrigger
|
|
||||||
) {
|
) {
|
||||||
temporarilyDisableSubmit();
|
disableBlurUntilNextPointerUp();
|
||||||
} else if (
|
} else if (
|
||||||
event.target instanceof HTMLCanvasElement &&
|
event.target instanceof HTMLCanvasElement &&
|
||||||
// Vitest simply ignores stopPropagation, capture-mode, or rAF
|
// Vitest simply ignores stopPropagation, capture-mode, or rAF
|
||||||
@ -684,9 +715,11 @@ export const textWysiwyg = ({
|
|||||||
const unbindUpdate = app.scene.onUpdate(() => {
|
const unbindUpdate = app.scene.onUpdate(() => {
|
||||||
updateWysiwygStyle();
|
updateWysiwygStyle();
|
||||||
const isPopupOpened = !!document.activeElement?.closest(
|
const isPopupOpened = !!document.activeElement?.closest(
|
||||||
".properties-content",
|
CLASSES.PROPERTIES_POPOVER,
|
||||||
);
|
);
|
||||||
if (!isPopupOpened) {
|
if (!isPopupOpened) {
|
||||||
|
// we need to keep this code path for safari (iPadOS) bs reasons
|
||||||
|
// (also Vitest)
|
||||||
editable.focus();
|
editable.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -704,8 +737,11 @@ export const textWysiwyg = ({
|
|||||||
// because we need it to happen *after* the blur event from `pointerdown`)
|
// because we need it to happen *after* the blur event from `pointerdown`)
|
||||||
editable.select();
|
editable.select();
|
||||||
}
|
}
|
||||||
bindBlurEvent();
|
focusEditable(null);
|
||||||
|
setTimeout(() => {
|
||||||
|
editable.onblur = onBlur;
|
||||||
|
});
|
||||||
|
console.log(">>>>>>>>", app.state.editingTextElement);
|
||||||
// reposition wysiwyg in case of canvas is resized. Using ResizeObserver
|
// reposition wysiwyg in case of canvas is resized. Using ResizeObserver
|
||||||
// is preferred so we catch changes from host, where window may not resize.
|
// is preferred so we catch changes from host, where window may not resize.
|
||||||
let observer: ResizeObserver | null = null;
|
let observer: ResizeObserver | null = null;
|
||||||
@ -715,7 +751,7 @@ export const textWysiwyg = ({
|
|||||||
});
|
});
|
||||||
observer.observe(canvas);
|
observer.observe(canvas);
|
||||||
} else {
|
} else {
|
||||||
window.addEventListener("resize", updateWysiwygStyle);
|
window.addEventListener(EVENT.RESIZE, updateWysiwygStyle);
|
||||||
}
|
}
|
||||||
|
|
||||||
editable.onpointerdown = (event) => event.stopPropagation();
|
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
|
// rAF (+ capture to by doubly sure) so we don't catch te pointerdown that
|
||||||
// triggered the wysiwyg
|
// triggered the wysiwyg
|
||||||
requestAnimationFrame(() => {
|
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
|
excalidrawContainer
|
||||||
?.querySelector(".excalidraw-textEditorContainer")!
|
?.querySelector(".excalidraw-textEditorContainer")!
|
||||||
.appendChild(editable);
|
.appendChild(editable);
|
||||||
|
@ -635,7 +635,7 @@ exports[`<Excalidraw/> > Test UIOptions prop > Test canvasActions > should rende
|
|||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
aria-label="Canvas background"
|
aria-label="Canvas background"
|
||||||
class="color-picker__button active-color properties-trigger"
|
class="color-picker__button active-color properties-popover-trigger"
|
||||||
data-state="closed"
|
data-state="closed"
|
||||||
style="--swatch-color: #ffffff;"
|
style="--swatch-color: #ffffff;"
|
||||||
title="Show background color picker"
|
title="Show background color picker"
|
||||||
|
@ -17,6 +17,7 @@ exports[`Test Linear Elements > Test bound text element > should match styles fo
|
|||||||
class="excalidraw-wysiwyg"
|
class="excalidraw-wysiwyg"
|
||||||
data-type="wysiwyg"
|
data-type="wysiwyg"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
|
placeholder=" "
|
||||||
style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 26.25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: 992.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Excalifont, Xiaolai, Segoe UI Emoji;"
|
style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 26.25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: 992.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Excalifont, Xiaolai, Segoe UI Emoji;"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
wrap="off"
|
wrap="off"
|
||||||
|
@ -38,6 +38,8 @@ import { pointFrom, pointRotateRads } from "../../../math";
|
|||||||
import { cropElement } from "../../element/cropElement";
|
import { cropElement } from "../../element/cropElement";
|
||||||
import type { ToolType } from "../../types";
|
import type { ToolType } from "../../types";
|
||||||
|
|
||||||
|
const TEXT_EDITOR_SELECTOR = ".excalidraw-textEditorContainer > textarea";
|
||||||
|
|
||||||
// so that window.h is available when App.tsx is not imported as well.
|
// so that window.h is available when App.tsx is not imported as well.
|
||||||
createTestHook();
|
createTestHook();
|
||||||
|
|
||||||
@ -477,12 +479,20 @@ export class UI {
|
|||||||
pointFrom(0, 0),
|
pointFrom(0, 0),
|
||||||
pointFrom(width, height),
|
pointFrom(width, height),
|
||||||
];
|
];
|
||||||
|
|
||||||
UI.clickTool(type);
|
UI.clickTool(type);
|
||||||
|
|
||||||
if (type === "text") {
|
if (type === "text") {
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
mouse.click(x, y);
|
mouse.click(x, y);
|
||||||
|
|
||||||
|
const openedEditor =
|
||||||
|
document.querySelector<HTMLTextAreaElement>(TEXT_EDITOR_SELECTOR);
|
||||||
|
|
||||||
|
// NOTE this is a hack to make sure the editor is focused on edit
|
||||||
|
// which for some reason doesn't work in tests after latest changes.
|
||||||
|
// This means that a regression in wysiwyg editor might not be caught
|
||||||
|
// tests.
|
||||||
|
openedEditor?.focus();
|
||||||
} else if ((type === "line" || type === "arrow") && points.length > 2) {
|
} else if ((type === "line" || type === "arrow") && points.length > 2) {
|
||||||
points.forEach((point) => {
|
points.forEach((point) => {
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
@ -518,20 +528,25 @@ export class UI {
|
|||||||
static async editText<
|
static async editText<
|
||||||
T extends ExcalidrawTextElement | ExcalidrawTextContainer,
|
T extends ExcalidrawTextElement | ExcalidrawTextContainer,
|
||||||
>(element: T, text: string) {
|
>(element: T, text: string) {
|
||||||
const textEditorSelector = ".excalidraw-textEditorContainer > textarea";
|
|
||||||
const openedEditor =
|
const openedEditor =
|
||||||
document.querySelector<HTMLTextAreaElement>(textEditorSelector);
|
document.querySelector<HTMLTextAreaElement>(TEXT_EDITOR_SELECTOR);
|
||||||
|
|
||||||
if (!openedEditor) {
|
if (!openedEditor) {
|
||||||
mouse.select(element);
|
mouse.select(element);
|
||||||
Keyboard.keyPress(KEYS.ENTER);
|
Keyboard.keyPress(KEYS.ENTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
const editor = await getTextEditor(textEditorSelector);
|
const editor = await getTextEditor();
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
throw new Error("Can't find wysiwyg text editor in the dom");
|
throw new Error("Can't find wysiwyg text editor in the dom");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE this is a hack to make sure the editor is focused on edit
|
||||||
|
// which for some reason doesn't work in tests after latest changes.
|
||||||
|
// This means that a regression in wysiwyg editor might not be caught
|
||||||
|
// tests.
|
||||||
|
editor.focus();
|
||||||
|
|
||||||
fireEvent.input(editor, { target: { value: text } });
|
fireEvent.input(editor, { target: { value: text } });
|
||||||
act(() => {
|
act(() => {
|
||||||
editor.blur();
|
editor.blur();
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { waitFor } from "@testing-library/dom";
|
import { waitFor } from "@testing-library/dom";
|
||||||
import { fireEvent } from "@testing-library/react";
|
import { fireEvent } from "@testing-library/react";
|
||||||
|
|
||||||
export const getTextEditor = async (selector: string, waitForEditor = true) => {
|
export const getTextEditor = async (
|
||||||
|
selector = ".excalidraw-textEditorContainer > textarea",
|
||||||
|
waitForEditor = true,
|
||||||
|
) => {
|
||||||
const query = () => document.querySelector(selector) as HTMLTextAreaElement;
|
const query = () => document.querySelector(selector) as HTMLTextAreaElement;
|
||||||
if (waitForEditor) {
|
if (waitForEditor) {
|
||||||
await waitFor(() => expect(query()).not.toBe(null));
|
await waitFor(() => expect(query()).not.toBe(null));
|
||||||
|
@ -1126,7 +1126,7 @@ describe("multiple selection", () => {
|
|||||||
expect(bottomArrowLabel.fontSize).toBeCloseTo(28 * scale);
|
expect(bottomArrowLabel.fontSize).toBeCloseTo(28 * scale);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resizes with text elements", async () => {
|
it.only("resizes with text elements", async () => {
|
||||||
const topText = UI.createElement("text", { position: 0 });
|
const topText = UI.createElement("text", { position: 0 });
|
||||||
await UI.editText(topText, "lorem ipsum");
|
await UI.editText(topText, "lorem ipsum");
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user