mirror of
				https://github.com/excalidraw/excalidraw.git
				synced 2025-10-31 19:04:35 +01:00 
			
		
		
		
	fix
This commit is contained in:
		| @@ -22,3 +22,8 @@ REACT_APP_DEV_ENABLE_SW= | ||||
| REACT_APP_DEV_DISABLE_LIVE_RELOAD= | ||||
|  | ||||
| FAST_REFRESH=false | ||||
|  | ||||
| #Debug flags | ||||
|  | ||||
| # To enable bounding box for text containers | ||||
| REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX= | ||||
|   | ||||
| @@ -1785,9 +1785,9 @@ | ||||
|     "@hapi/hoek" "^9.0.0" | ||||
|  | ||||
| "@sideway/formula@^3.0.0": | ||||
|   version "3.0.0" | ||||
|   resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c" | ||||
|   integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg== | ||||
|   version "3.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f" | ||||
|   integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg== | ||||
|  | ||||
| "@sideway/pinpoint@^2.0.0": | ||||
|   version "2.0.0" | ||||
| @@ -4376,9 +4376,9 @@ htmlparser2@^8.0.1: | ||||
|     entities "^4.3.0" | ||||
|  | ||||
| http-cache-semantics@^4.0.0: | ||||
|   version "4.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" | ||||
|   integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== | ||||
|   version "4.1.1" | ||||
|   resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" | ||||
|   integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== | ||||
|  | ||||
| http-deceiver@^1.2.7: | ||||
|   version "1.2.7" | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| import { VERTICAL_ALIGN } from "../constants"; | ||||
| import { getNonDeletedElements, isTextElement } from "../element"; | ||||
| import { BOUND_TEXT_PADDING, ROUNDNESS, VERTICAL_ALIGN } from "../constants"; | ||||
| import { getNonDeletedElements, isTextElement, newElement } from "../element"; | ||||
| import { mutateElement } from "../element/mutateElement"; | ||||
| import { | ||||
|   computeContainerDimensionForBoundText, | ||||
|   getBoundTextElement, | ||||
|   measureText, | ||||
|   redrawTextBoundingBox, | ||||
| @@ -13,8 +14,11 @@ import { | ||||
| import { | ||||
|   hasBoundTextElement, | ||||
|   isTextBindableContainer, | ||||
|   isUsingAdaptiveRadius, | ||||
| } from "../element/typeChecks"; | ||||
| import { | ||||
|   ExcalidrawElement, | ||||
|   ExcalidrawLinearElement, | ||||
|   ExcalidrawTextContainer, | ||||
|   ExcalidrawTextElement, | ||||
| } from "../element/types"; | ||||
| @@ -129,19 +133,151 @@ export const actionBindText = register({ | ||||
|       }), | ||||
|     }); | ||||
|     redrawTextBoundingBox(textElement, container); | ||||
|     const updatedElements = elements.slice(); | ||||
|     const textElementIndex = updatedElements.findIndex( | ||||
|       (ele) => ele.id === textElement.id, | ||||
|     ); | ||||
|     updatedElements.splice(textElementIndex, 1); | ||||
|     const containerIndex = updatedElements.findIndex( | ||||
|       (ele) => ele.id === container.id, | ||||
|     ); | ||||
|     updatedElements.splice(containerIndex + 1, 0, textElement); | ||||
|  | ||||
|     return { | ||||
|       elements: updatedElements, | ||||
|       elements: pushTextAboveContainer(elements, container, textElement), | ||||
|       appState: { ...appState, selectedElementIds: { [container.id]: true } }, | ||||
|       commitToHistory: true, | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| const pushTextAboveContainer = ( | ||||
|   elements: readonly ExcalidrawElement[], | ||||
|   container: ExcalidrawElement, | ||||
|   textElement: ExcalidrawTextElement, | ||||
| ) => { | ||||
|   const updatedElements = elements.slice(); | ||||
|   const textElementIndex = updatedElements.findIndex( | ||||
|     (ele) => ele.id === textElement.id, | ||||
|   ); | ||||
|   updatedElements.splice(textElementIndex, 1); | ||||
|  | ||||
|   const containerIndex = updatedElements.findIndex( | ||||
|     (ele) => ele.id === container.id, | ||||
|   ); | ||||
|   updatedElements.splice(containerIndex + 1, 0, textElement); | ||||
|   return updatedElements; | ||||
| }; | ||||
|  | ||||
| const pushContainerBelowText = ( | ||||
|   elements: readonly ExcalidrawElement[], | ||||
|   container: ExcalidrawElement, | ||||
|   textElement: ExcalidrawTextElement, | ||||
| ) => { | ||||
|   const updatedElements = elements.slice(); | ||||
|   const containerIndex = updatedElements.findIndex( | ||||
|     (ele) => ele.id === container.id, | ||||
|   ); | ||||
|   updatedElements.splice(containerIndex, 1); | ||||
|  | ||||
|   const textElementIndex = updatedElements.findIndex( | ||||
|     (ele) => ele.id === textElement.id, | ||||
|   ); | ||||
|   updatedElements.splice(textElementIndex, 0, container); | ||||
|   return updatedElements; | ||||
| }; | ||||
|  | ||||
| export const actionCreateContainerFromText = register({ | ||||
|   name: "createContainerFromText", | ||||
|   contextItemLabel: "labels.createContainerFromText", | ||||
|   trackEvent: { category: "element" }, | ||||
|   predicate: (elements, appState) => { | ||||
|     const selectedElements = getSelectedElements(elements, appState); | ||||
|     return selectedElements.length === 1 && isTextElement(selectedElements[0]); | ||||
|   }, | ||||
|   perform: (elements, appState) => { | ||||
|     const selectedElements = getSelectedElements( | ||||
|       getNonDeletedElements(elements), | ||||
|       appState, | ||||
|     ); | ||||
|     const updatedElements = elements.slice(); | ||||
|     if (selectedElements.length === 1 && isTextElement(selectedElements[0])) { | ||||
|       const textElement = selectedElements[0]; | ||||
|       const container = newElement({ | ||||
|         type: "rectangle", | ||||
|         backgroundColor: appState.currentItemBackgroundColor, | ||||
|         boundElements: [ | ||||
|           ...(textElement.boundElements || []), | ||||
|           { id: textElement.id, type: "text" }, | ||||
|         ], | ||||
|         angle: textElement.angle, | ||||
|         fillStyle: appState.currentItemFillStyle, | ||||
|         strokeColor: appState.currentItemStrokeColor, | ||||
|         roughness: appState.currentItemRoughness, | ||||
|         strokeWidth: appState.currentItemStrokeWidth, | ||||
|         strokeStyle: appState.currentItemStrokeStyle, | ||||
|         roundness: | ||||
|           appState.currentItemRoundness === "round" | ||||
|             ? { | ||||
|                 type: isUsingAdaptiveRadius("rectangle") | ||||
|                   ? ROUNDNESS.ADAPTIVE_RADIUS | ||||
|                   : ROUNDNESS.PROPORTIONAL_RADIUS, | ||||
|               } | ||||
|             : null, | ||||
|         opacity: 100, | ||||
|         locked: false, | ||||
|         x: textElement.x - BOUND_TEXT_PADDING, | ||||
|         y: textElement.y - BOUND_TEXT_PADDING, | ||||
|         width: computeContainerDimensionForBoundText( | ||||
|           textElement.width, | ||||
|           "rectangle", | ||||
|         ), | ||||
|         height: computeContainerDimensionForBoundText( | ||||
|           textElement.height, | ||||
|           "rectangle", | ||||
|         ), | ||||
|         groupIds: textElement.groupIds, | ||||
|       }); | ||||
|  | ||||
|       // update bindings | ||||
|       if (textElement.boundElements?.length) { | ||||
|         const linearElementIds = textElement.boundElements | ||||
|           .filter((ele) => ele.type === "arrow") | ||||
|           .map((el) => el.id); | ||||
|         const linearElements = updatedElements.filter((ele) => | ||||
|           linearElementIds.includes(ele.id), | ||||
|         ) as ExcalidrawLinearElement[]; | ||||
|         linearElements.forEach((ele) => { | ||||
|           let startBinding = null; | ||||
|           let endBinding = null; | ||||
|           if (ele.startBinding) { | ||||
|             startBinding = { ...ele.startBinding, elementId: container.id }; | ||||
|           } | ||||
|           if (ele.endBinding) { | ||||
|             endBinding = { ...ele.endBinding, elementId: container.id }; | ||||
|           } | ||||
|           mutateElement(ele, { startBinding, endBinding }); | ||||
|         }); | ||||
|       } | ||||
|  | ||||
|       mutateElement(textElement, { | ||||
|         containerId: container.id, | ||||
|         verticalAlign: VERTICAL_ALIGN.MIDDLE, | ||||
|         boundElements: null, | ||||
|       }); | ||||
|       redrawTextBoundingBox(textElement, container); | ||||
|  | ||||
|       return { | ||||
|         elements: pushContainerBelowText( | ||||
|           [...elements, container], | ||||
|           container, | ||||
|           textElement, | ||||
|         ), | ||||
|         appState: { | ||||
|           ...appState, | ||||
|           selectedElementIds: { | ||||
|             [container.id]: true, | ||||
|             [textElement.id]: false, | ||||
|           }, | ||||
|         }, | ||||
|         commitToHistory: true, | ||||
|       }; | ||||
|     } | ||||
|     return { | ||||
|       elements: updatedElements, | ||||
|       appState, | ||||
|       commitToHistory: true, | ||||
|     }; | ||||
|   }, | ||||
| }); | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { isDarwin } from "../constants"; | ||||
| import { t } from "../i18n"; | ||||
| import { SubtypeOf } from "../utility-types"; | ||||
| import { getShortcutKey } from "../utils"; | ||||
| import { ActionName } from "./types"; | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import { | ||||
|   ExcalidrawProps, | ||||
|   BinaryFiles, | ||||
| } from "../types"; | ||||
| import { MarkOptional } from "../utility-types"; | ||||
|  | ||||
| export type ActionSource = "ui" | "keyboard" | "contextMenu" | "api"; | ||||
|  | ||||
| @@ -113,7 +114,8 @@ export type ActionName = | ||||
|   | "toggleLock" | ||||
|   | "toggleLinearEditor" | ||||
|   | "toggleEraserTool" | ||||
|   | "toggleHandTool"; | ||||
|   | "toggleHandTool" | ||||
|   | "createContainerFromText"; | ||||
|  | ||||
| export type PanelComponentProps = { | ||||
|   elements: readonly ExcalidrawElement[]; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { atom, useAtom } from "jotai"; | ||||
| import { actionClearCanvas } from "../actions"; | ||||
| import { t } from "../i18n"; | ||||
| import { jotaiScope } from "../jotai"; | ||||
| import { useExcalidrawActionManager } from "./App"; | ||||
| import ConfirmDialog from "./ConfirmDialog"; | ||||
|  | ||||
| @@ -9,6 +10,7 @@ export const activeConfirmDialogAtom = atom<"clearCanvas" | null>(null); | ||||
| export const ActiveConfirmDialog = () => { | ||||
|   const [activeConfirmDialog, setActiveConfirmDialog] = useAtom( | ||||
|     activeConfirmDialogAtom, | ||||
|     jotaiScope, | ||||
|   ); | ||||
|   const actionManager = useExcalidrawActionManager(); | ||||
|  | ||||
|   | ||||
| @@ -284,6 +284,7 @@ import { actionPaste } from "../actions/actionClipboard"; | ||||
| import { actionToggleHandTool } from "../actions/actionCanvas"; | ||||
| import { jotaiStore } from "../jotai"; | ||||
| import { activeConfirmDialogAtom } from "./ActiveConfirmDialog"; | ||||
| import { actionCreateContainerFromText } from "../actions/actionBoundText"; | ||||
|  | ||||
| const deviceContextInitialValue = { | ||||
|   isSmScreen: false, | ||||
| @@ -2767,7 +2768,6 @@ class App extends React.Component<AppProps, AppState> { | ||||
|       ); | ||||
|       if (container) { | ||||
|         if ( | ||||
|           isArrowElement(container) || | ||||
|           hasBoundTextElement(container) || | ||||
|           !isTransparent(container.backgroundColor) || | ||||
|           isHittingElementNotConsideringBoundingBox(container, this.state, [ | ||||
| @@ -6238,6 +6238,7 @@ class App extends React.Component<AppProps, AppState> { | ||||
|       actionGroup, | ||||
|       actionUnbindText, | ||||
|       actionBindText, | ||||
|       actionCreateContainerFromText, | ||||
|       actionUngroup, | ||||
|       CONTEXT_MENU_SEPARATOR, | ||||
|       actionAddToLibrary, | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import DialogActionButton from "./DialogActionButton"; | ||||
| import { useSetAtom } from "jotai"; | ||||
| import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent"; | ||||
| import { useExcalidrawSetAppState } from "./App"; | ||||
| import { jotaiScope } from "../jotai"; | ||||
|  | ||||
| interface Props extends Omit<DialogProps, "onCloseRequest"> { | ||||
|   onConfirm: () => void; | ||||
| @@ -24,7 +25,7 @@ const ConfirmDialog = (props: Props) => { | ||||
|     ...rest | ||||
|   } = props; | ||||
|   const setAppState = useExcalidrawSetAppState(); | ||||
|   const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom); | ||||
|   const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope); | ||||
|  | ||||
|   return ( | ||||
|     <Dialog | ||||
|   | ||||
| @@ -16,6 +16,7 @@ import { AppState } from "../types"; | ||||
| import { queryFocusableElements } from "../utils"; | ||||
| import { useSetAtom } from "jotai"; | ||||
| import { isLibraryMenuOpenAtom } from "./LibraryMenuHeaderContent"; | ||||
| import { jotaiScope } from "../jotai"; | ||||
|  | ||||
| export interface DialogProps { | ||||
|   children: React.ReactNode; | ||||
| @@ -72,7 +73,7 @@ export const Dialog = (props: DialogProps) => { | ||||
|   }, [islandNode, props.autofocus]); | ||||
|  | ||||
|   const setAppState = useExcalidrawSetAppState(); | ||||
|   const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom); | ||||
|   const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom, jotaiScope); | ||||
|  | ||||
|   const onClose = () => { | ||||
|     setAppState({ openMenu: null }); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { t } from "../i18n"; | ||||
| import { HelpIcon } from "./icons"; | ||||
|  | ||||
| type HelpButtonProps = { | ||||
|   title?: string; | ||||
|   name?: string; | ||||
|   id?: string; | ||||
|   onClick?(): void; | ||||
| @@ -12,8 +12,8 @@ export const HelpButton = (props: HelpButtonProps) => ( | ||||
|     className="help-icon" | ||||
|     onClick={props.onClick} | ||||
|     type="button" | ||||
|     title={`${props.title} — ?`} | ||||
|     aria-label={props.title} | ||||
|     title={`${t("helpDialog.title")} — ?`} | ||||
|     aria-label={t("helpDialog.title")} | ||||
|   > | ||||
|     {HelpIcon} | ||||
|   </button> | ||||
|   | ||||
| @@ -48,6 +48,7 @@ export const LibraryMenuHeader: React.FC<{ | ||||
|   const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope); | ||||
|   const [isLibraryMenuOpen, setIsLibraryMenuOpen] = useAtom( | ||||
|     isLibraryMenuOpenAtom, | ||||
|     jotaiScope, | ||||
|   ); | ||||
|   const renderRemoveLibAlert = useCallback(() => { | ||||
|     const content = selectedItems.length | ||||
|   | ||||
| @@ -31,6 +31,7 @@ import "./DefaultItems.scss"; | ||||
| import clsx from "clsx"; | ||||
| import { useSetAtom } from "jotai"; | ||||
| import { activeConfirmDialogAtom } from "../ActiveConfirmDialog"; | ||||
| import { jotaiScope } from "../../jotai"; | ||||
|  | ||||
| export const LoadScene = () => { | ||||
|   const { t } = useI18n(); | ||||
| @@ -113,7 +114,10 @@ Help.displayName = "Help"; | ||||
| export const ClearCanvas = () => { | ||||
|   const { t } = useI18n(); | ||||
|  | ||||
|   const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom); | ||||
|   const setActiveConfirmDialog = useSetAtom( | ||||
|     activeConfirmDialogAtom, | ||||
|     jotaiScope, | ||||
|   ); | ||||
|   const actionManager = useExcalidrawActionManager(); | ||||
|  | ||||
|   if (!actionManager.isActionEnabled(actionClearCanvas)) { | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import { CanvasError } from "../errors"; | ||||
| import { t } from "../i18n"; | ||||
| import { calculateScrollCenter } from "../scene"; | ||||
| import { AppState, DataURL, LibraryItem } from "../types"; | ||||
| import { ValueOf } from "../utility-types"; | ||||
| import { bytesToHexString } from "../utils"; | ||||
| import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem"; | ||||
| import { isValidExcalidrawData, isValidLibrary } from "./json"; | ||||
|   | ||||
| @@ -34,6 +34,7 @@ import { bumpVersion } from "../element/mutateElement"; | ||||
| import { getUpdatedTimestamp, updateActiveTool } from "../utils"; | ||||
| import { arrayToMap } from "../utils"; | ||||
| import oc from "open-color"; | ||||
| import { MarkOptional, Mutable } from "../utility-types"; | ||||
|  | ||||
| type RestoredAppState = Omit< | ||||
|   AppState, | ||||
|   | ||||
| @@ -23,6 +23,7 @@ import { | ||||
| import { rescalePoints } from "../points"; | ||||
| import { getBoundTextElement, getContainerElement } from "./textElement"; | ||||
| import { LinearElementEditor } from "./linearElementEditor"; | ||||
| import { Mutable } from "../utility-types"; | ||||
|  | ||||
| // x and y position of top left corner, x and y position of bottom right corner | ||||
| export type Bounds = readonly [number, number, number, number]; | ||||
|   | ||||
| @@ -38,6 +38,7 @@ import { isTextElement } from "."; | ||||
| import { isTransparent } from "../utils"; | ||||
| import { shouldShowBoundingBox } from "./transformHandles"; | ||||
| import { getBoundTextElement } from "./textElement"; | ||||
| import { Mutable } from "../utility-types"; | ||||
|  | ||||
| const isElementDraggableFromInside = ( | ||||
|   element: NonDeletedExcalidrawElement, | ||||
|   | ||||
| @@ -41,6 +41,7 @@ import { shouldRotateWithDiscreteAngle } from "../keys"; | ||||
| import { getBoundTextElement, handleBindTextResize } from "./textElement"; | ||||
| import { getShapeForElement } from "../renderer/renderElement"; | ||||
| import { DRAGGING_THRESHOLD } from "../constants"; | ||||
| import { Mutable } from "../utility-types"; | ||||
|  | ||||
| const editorMidPointsCache: { | ||||
|   version: number | null; | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import { getSizeFromPoints } from "../points"; | ||||
| import { randomInteger } from "../random"; | ||||
| import { Point } from "../types"; | ||||
| import { getUpdatedTimestamp } from "../utils"; | ||||
| import { Mutable } from "../utility-types"; | ||||
|  | ||||
| type ElementUpdate<TElement extends ExcalidrawElement> = Omit< | ||||
|   Partial<TElement>, | ||||
|   | ||||
| @@ -32,6 +32,7 @@ import { | ||||
| } from "./textElement"; | ||||
| import { VERTICAL_ALIGN } from "../constants"; | ||||
| import { isArrowElement } from "./typeChecks"; | ||||
| import { MarkOptional, Merge, Mutable } from "../utility-types"; | ||||
|  | ||||
| type ElementConstructorOpts = MarkOptional< | ||||
|   Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">, | ||||
|   | ||||
| @@ -693,7 +693,9 @@ const resizeMultipleElements = ( | ||||
|       }; | ||||
|       const fontSize = measureFontSizeFromWidth( | ||||
|         boundTextElement ?? (element.orig as ExcalidrawTextElement), | ||||
|         getContainerMaxWidth(updatedElement), | ||||
|         boundTextElement | ||||
|           ? getContainerMaxWidth(updatedElement) | ||||
|           : updatedElement.width, | ||||
|       ); | ||||
|  | ||||
|       if (!fontSize) { | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { BOUND_TEXT_PADDING } from "../constants"; | ||||
| import { API } from "../tests/helpers/api"; | ||||
| import { | ||||
|   computeContainerHeightForBoundText, | ||||
|   computeContainerDimensionForBoundText, | ||||
|   getContainerCoords, | ||||
|   getContainerMaxWidth, | ||||
|   getContainerMaxHeight, | ||||
| @@ -35,10 +35,11 @@ describe("Test wrapText", () => { | ||||
|  | ||||
|   describe("When text doesn't contain new lines", () => { | ||||
|     const text = "Hello whats up"; | ||||
|  | ||||
|     [ | ||||
|       { | ||||
|         desc: "break all words when width of each word is less than container width", | ||||
|         width: 90, | ||||
|         width: 80, | ||||
|         res: `Hello  | ||||
| whats  | ||||
| up`, | ||||
| @@ -62,7 +63,7 @@ p`, | ||||
|       { | ||||
|         desc: "break words as per the width", | ||||
|  | ||||
|         width: 150, | ||||
|         width: 140, | ||||
|         res: `Hello whats  | ||||
| up`, | ||||
|       }, | ||||
| @@ -93,7 +94,7 @@ whats up`; | ||||
|     [ | ||||
|       { | ||||
|         desc: "break all words when width of each word is less than container width", | ||||
|         width: 90, | ||||
|         width: 80, | ||||
|         res: `Hello | ||||
| whats  | ||||
| up`, | ||||
| @@ -214,7 +215,7 @@ describe("Test measureText", () => { | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe("Test computeContainerHeightForBoundText", () => { | ||||
|   describe("Test computeContainerDimensionForBoundText", () => { | ||||
|     const params = { | ||||
|       width: 178, | ||||
|       height: 194, | ||||
| @@ -225,7 +226,9 @@ describe("Test measureText", () => { | ||||
|         type: "rectangle", | ||||
|         ...params, | ||||
|       }); | ||||
|       expect(computeContainerHeightForBoundText(element, 150)).toEqual(160); | ||||
|       expect(computeContainerDimensionForBoundText(150, element.type)).toEqual( | ||||
|         160, | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it("should compute container height correctly for ellipse", () => { | ||||
| @@ -233,7 +236,9 @@ describe("Test measureText", () => { | ||||
|         type: "ellipse", | ||||
|         ...params, | ||||
|       }); | ||||
|       expect(computeContainerHeightForBoundText(element, 150)).toEqual(226); | ||||
|       expect(computeContainerDimensionForBoundText(150, element.type)).toEqual( | ||||
|         226, | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     it("should compute container height correctly for diamond", () => { | ||||
| @@ -241,7 +246,9 @@ describe("Test measureText", () => { | ||||
|         type: "diamond", | ||||
|         ...params, | ||||
|       }); | ||||
|       expect(computeContainerHeightForBoundText(element, 150)).toEqual(320); | ||||
|       expect(computeContainerDimensionForBoundText(150, element.type)).toEqual( | ||||
|         320, | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   | ||||
| @@ -12,11 +12,7 @@ import { BOUND_TEXT_PADDING, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants"; | ||||
| import { MaybeTransformHandleType } from "./transformHandles"; | ||||
| import Scene from "../scene/Scene"; | ||||
| import { isTextElement } from "."; | ||||
| import { | ||||
|   isBoundToContainer, | ||||
|   isImageElement, | ||||
|   isArrowElement, | ||||
| } from "./typeChecks"; | ||||
| import { isBoundToContainer, isArrowElement } from "./typeChecks"; | ||||
| import { LinearElementEditor } from "./linearElementEditor"; | ||||
| import { AppState } from "../types"; | ||||
| import { isTextBindableContainer } from "./typeChecks"; | ||||
| @@ -27,6 +23,7 @@ import { | ||||
|   resetOriginalContainerCache, | ||||
|   updateOriginalContainerCache, | ||||
| } from "./textWysiwyg"; | ||||
| import { ExtractSetType } from "../utility-types"; | ||||
|  | ||||
| export const normalizeText = (text: string) => { | ||||
|   return ( | ||||
| @@ -78,9 +75,9 @@ export const redrawTextBoundingBox = ( | ||||
|     let nextHeight = containerDims.height; | ||||
|  | ||||
|     if (metrics.height > maxContainerHeight) { | ||||
|       nextHeight = computeContainerHeightForBoundText( | ||||
|         container, | ||||
|       nextHeight = computeContainerDimensionForBoundText( | ||||
|         metrics.height, | ||||
|         container.type, | ||||
|       ); | ||||
|       mutateElement(container, { height: nextHeight }); | ||||
|       updateOriginalContainerCache(container.id, nextHeight); | ||||
| @@ -184,9 +181,9 @@ export const handleBindTextResize = ( | ||||
|     } | ||||
|     // increase height in case text element height exceeds | ||||
|     if (nextHeight > maxHeight) { | ||||
|       containerHeight = computeContainerHeightForBoundText( | ||||
|         container, | ||||
|       containerHeight = computeContainerDimensionForBoundText( | ||||
|         nextHeight, | ||||
|         container.type, | ||||
|       ); | ||||
|  | ||||
|       const diff = containerHeight - containerDims.height; | ||||
| @@ -326,7 +323,6 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => { | ||||
|   const lines: Array<string> = []; | ||||
|   const originalLines = text.split("\n"); | ||||
|   const spaceWidth = getLineWidth(" ", font); | ||||
|  | ||||
|   const push = (str: string) => { | ||||
|     if (str.trim()) { | ||||
|       lines.push(str); | ||||
| @@ -400,7 +396,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => { | ||||
|           const word = words[index]; | ||||
|           currentLineWidthTillNow = getLineWidth(currentLine + word, font); | ||||
|  | ||||
|           if (currentLineWidthTillNow >= maxWidth) { | ||||
|           if (currentLineWidthTillNow > maxWidth) { | ||||
|             push(currentLine); | ||||
|             currentLineWidthTillNow = 0; | ||||
|             currentLine = ""; | ||||
| @@ -704,32 +700,34 @@ export const getTextBindableContainerAtPosition = ( | ||||
|   return isTextBindableContainer(hitElement, false) ? hitElement : null; | ||||
| }; | ||||
|  | ||||
| export const isValidTextContainer = (element: ExcalidrawElement) => { | ||||
|   return ( | ||||
|     element.type === "rectangle" || | ||||
|     element.type === "ellipse" || | ||||
|     element.type === "diamond" || | ||||
|     isImageElement(element) || | ||||
|     isArrowElement(element) | ||||
|   ); | ||||
| }; | ||||
| const VALID_CONTAINER_TYPES = new Set([ | ||||
|   "rectangle", | ||||
|   "ellipse", | ||||
|   "diamond", | ||||
|   "image", | ||||
|   "arrow", | ||||
| ]); | ||||
|  | ||||
| export const computeContainerHeightForBoundText = ( | ||||
|   container: NonDeletedExcalidrawElement, | ||||
|   boundTextElementHeight: number, | ||||
| export const isValidTextContainer = (element: ExcalidrawElement) => | ||||
|   VALID_CONTAINER_TYPES.has(element.type); | ||||
|  | ||||
| export const computeContainerDimensionForBoundText = ( | ||||
|   dimension: number, | ||||
|   containerType: ExtractSetType<typeof VALID_CONTAINER_TYPES>, | ||||
| ) => { | ||||
|   if (container.type === "ellipse") { | ||||
|     return Math.round( | ||||
|       ((boundTextElementHeight + BOUND_TEXT_PADDING * 2) / Math.sqrt(2)) * 2, | ||||
|     ); | ||||
|   dimension = Math.ceil(dimension); | ||||
|   const padding = BOUND_TEXT_PADDING * 2; | ||||
|  | ||||
|   if (containerType === "ellipse") { | ||||
|     return Math.round(((dimension + padding) / Math.sqrt(2)) * 2); | ||||
|   } | ||||
|   if (isArrowElement(container)) { | ||||
|     return boundTextElementHeight + BOUND_TEXT_PADDING * 8 * 2; | ||||
|   if (containerType === "arrow") { | ||||
|     return dimension + padding * 8; | ||||
|   } | ||||
|   if (container.type === "diamond") { | ||||
|     return 2 * (boundTextElementHeight + BOUND_TEXT_PADDING * 2); | ||||
|   if (containerType === "diamond") { | ||||
|     return 2 * (dimension + padding); | ||||
|   } | ||||
|   return boundTextElementHeight + BOUND_TEXT_PADDING * 2; | ||||
|   return dimension + padding; | ||||
| }; | ||||
|  | ||||
| export const getContainerMaxWidth = (container: ExcalidrawElement) => { | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import { | ||||
| } from "../tests/test-utils"; | ||||
| import { queryByText } from "@testing-library/react"; | ||||
|  | ||||
| import { FONT_FAMILY } from "../constants"; | ||||
| import { FONT_FAMILY, TEXT_ALIGN, VERTICAL_ALIGN } from "../constants"; | ||||
| import { | ||||
|   ExcalidrawTextElement, | ||||
|   ExcalidrawTextElementWithContainer, | ||||
| @@ -19,6 +19,7 @@ import { API } from "../tests/helpers/api"; | ||||
| import { mutateElement } from "./mutateElement"; | ||||
| import { resize } from "../tests/utils"; | ||||
| import { getOriginalContainerHeightFromCache } from "./textWysiwyg"; | ||||
|  | ||||
| // Unmount ReactDOM from root | ||||
| ReactDOM.unmountComponentAtNode(document.getElementById("root")!); | ||||
|  | ||||
| @@ -1307,5 +1308,91 @@ describe("textWysiwyg", () => { | ||||
|         `); | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it("should wrap text in a container when wrap text in container triggered from context menu", async () => { | ||||
|       UI.clickTool("text"); | ||||
|       mouse.clickAt(20, 30); | ||||
|       const editor = document.querySelector( | ||||
|         ".excalidraw-textEditorContainer > textarea", | ||||
|       ) as HTMLTextAreaElement; | ||||
|  | ||||
|       fireEvent.change(editor, { | ||||
|         target: { | ||||
|           value: "Excalidraw is an opensource virtual collaborative whiteboard", | ||||
|         }, | ||||
|       }); | ||||
|  | ||||
|       editor.dispatchEvent(new Event("input")); | ||||
|       await new Promise((cb) => setTimeout(cb, 0)); | ||||
|  | ||||
|       editor.select(); | ||||
|       fireEvent.click(screen.getByTitle("Left")); | ||||
|       await new Promise((r) => setTimeout(r, 0)); | ||||
|  | ||||
|       editor.blur(); | ||||
|  | ||||
|       const textElement = h.elements[1] as ExcalidrawTextElement; | ||||
|       expect(textElement.width).toBe(600); | ||||
|       expect(textElement.height).toBe(24); | ||||
|       expect(textElement.textAlign).toBe(TEXT_ALIGN.LEFT); | ||||
|       expect((textElement as ExcalidrawTextElement).text).toBe( | ||||
|         "Excalidraw is an opensource virtual collaborative whiteboard", | ||||
|       ); | ||||
|  | ||||
|       API.setSelectedElements([textElement]); | ||||
|  | ||||
|       fireEvent.contextMenu(GlobalTestState.canvas, { | ||||
|         button: 2, | ||||
|         clientX: 20, | ||||
|         clientY: 30, | ||||
|       }); | ||||
|  | ||||
|       const contextMenu = document.querySelector(".context-menu"); | ||||
|       fireEvent.click( | ||||
|         queryByText(contextMenu as HTMLElement, "Wrap text in a container")!, | ||||
|       ); | ||||
|       expect(h.elements.length).toBe(3); | ||||
|  | ||||
|       expect(h.elements[1]).toEqual( | ||||
|         expect.objectContaining({ | ||||
|           angle: 0, | ||||
|           backgroundColor: "transparent", | ||||
|           boundElements: [ | ||||
|             { | ||||
|               id: h.elements[2].id, | ||||
|               type: "text", | ||||
|             }, | ||||
|           ], | ||||
|           fillStyle: "hachure", | ||||
|           groupIds: [], | ||||
|           height: 34, | ||||
|           isDeleted: false, | ||||
|           link: null, | ||||
|           locked: false, | ||||
|           opacity: 100, | ||||
|           roughness: 1, | ||||
|           roundness: { | ||||
|             type: 3, | ||||
|           }, | ||||
|           strokeColor: "#000000", | ||||
|           strokeStyle: "solid", | ||||
|           strokeWidth: 1, | ||||
|           type: "rectangle", | ||||
|           updated: 1, | ||||
|           version: 1, | ||||
|           width: 610, | ||||
|           x: 15, | ||||
|           y: 25, | ||||
|         }), | ||||
|       ); | ||||
|       expect(h.elements[2] as ExcalidrawTextElement).toEqual( | ||||
|         expect.objectContaining({ | ||||
|           text: "Excalidraw is an opensource virtual collaborative whiteboard", | ||||
|           verticalAlign: VERTICAL_ALIGN.MIDDLE, | ||||
|           textAlign: TEXT_ALIGN.LEFT, | ||||
|           boundElements: null, | ||||
|         }), | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import { | ||||
|   isBoundToContainer, | ||||
|   isTextElement, | ||||
| } from "./typeChecks"; | ||||
| import { CLASSES, isFirefox, isSafari } from "../constants"; | ||||
| import { CLASSES } from "../constants"; | ||||
| import { | ||||
|   ExcalidrawElement, | ||||
|   ExcalidrawLinearElement, | ||||
| @@ -252,8 +252,7 @@ export const textWysiwyg = ({ | ||||
|       if (!container) { | ||||
|         maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value; | ||||
|         textElementWidth = Math.min(textElementWidth, maxWidth); | ||||
|       } else if (isFirefox || isSafari) { | ||||
|         // As firefox, Safari needs little higher dimensions on DOM | ||||
|       } else { | ||||
|         textElementWidth += 0.5; | ||||
|       } | ||||
|       // Make sure text editor height doesn't go beyond viewport | ||||
|   | ||||
							
								
								
									
										66
									
								
								src/element/typeChecks.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/element/typeChecks.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| import { API } from "../tests/helpers/api"; | ||||
| import { hasBoundTextElement } from "./typeChecks"; | ||||
|  | ||||
| describe("Test TypeChecks", () => { | ||||
|   describe("Test hasBoundTextElement", () => { | ||||
|     it("should return true for text bindable containers with bound text", () => { | ||||
|       expect( | ||||
|         hasBoundTextElement( | ||||
|           API.createElement({ | ||||
|             type: "rectangle", | ||||
|             boundElements: [{ type: "text", id: "text-id" }], | ||||
|           }), | ||||
|         ), | ||||
|       ).toBeTruthy(); | ||||
|  | ||||
|       expect( | ||||
|         hasBoundTextElement( | ||||
|           API.createElement({ | ||||
|             type: "ellipse", | ||||
|             boundElements: [{ type: "text", id: "text-id" }], | ||||
|           }), | ||||
|         ), | ||||
|       ).toBeTruthy(); | ||||
|  | ||||
|       expect( | ||||
|         hasBoundTextElement( | ||||
|           API.createElement({ | ||||
|             type: "arrow", | ||||
|             boundElements: [{ type: "text", id: "text-id" }], | ||||
|           }), | ||||
|         ), | ||||
|       ).toBeTruthy(); | ||||
|  | ||||
|       expect( | ||||
|         hasBoundTextElement( | ||||
|           API.createElement({ | ||||
|             type: "image", | ||||
|             boundElements: [{ type: "text", id: "text-id" }], | ||||
|           }), | ||||
|         ), | ||||
|       ).toBeTruthy(); | ||||
|     }); | ||||
|  | ||||
|     it("should return false for text bindable containers without bound text", () => { | ||||
|       expect( | ||||
|         hasBoundTextElement( | ||||
|           API.createElement({ | ||||
|             type: "freedraw", | ||||
|             boundElements: [{ type: "arrow", id: "arrow-id" }], | ||||
|           }), | ||||
|         ), | ||||
|       ).toBeFalsy(); | ||||
|     }); | ||||
|  | ||||
|     it("should return false for non text bindable containers", () => { | ||||
|       expect( | ||||
|         hasBoundTextElement( | ||||
|           API.createElement({ | ||||
|             type: "freedraw", | ||||
|             boundElements: [{ type: "text", id: "text-id" }], | ||||
|           }), | ||||
|         ), | ||||
|       ).toBeFalsy(); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { ROUNDNESS } from "../constants"; | ||||
| import { AppState } from "../types"; | ||||
| import { MarkNonNullable } from "../utility-types"; | ||||
| import { | ||||
|   ExcalidrawElement, | ||||
|   ExcalidrawTextElement, | ||||
| @@ -139,7 +140,7 @@ export const hasBoundTextElement = ( | ||||
|   element: ExcalidrawElement | null, | ||||
| ): element is MarkNonNullable<ExcalidrawBindableElement, "boundElements"> => { | ||||
|   return ( | ||||
|     isBindableElement(element) && | ||||
|     isTextBindableContainer(element) && | ||||
|     !!element.boundElements?.some(({ type }) => type === "text") | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import { | ||||
|   THEME, | ||||
|   VERTICAL_ALIGN, | ||||
| } from "../constants"; | ||||
| import { MarkNonNullable, ValueOf } from "../utility-types"; | ||||
|  | ||||
| export type ChartType = "bar" | "line"; | ||||
| export type FillStyle = "hachure" | "cross-hatch" | "solid"; | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import { encryptData, decryptData } from "../../data/encryption"; | ||||
| import { MIME_TYPES } from "../../constants"; | ||||
| import { reconcileElements } from "../collab/reconciliation"; | ||||
| import { getSyncableElements, SyncableExcalidrawElement } from "."; | ||||
| import { ResolutionType } from "../../utility-types"; | ||||
|  | ||||
| // private | ||||
| // ----------------------------------------------------------------------------- | ||||
|   | ||||
| @@ -85,6 +85,7 @@ import { useAtomWithInitialValue } from "../jotai"; | ||||
| import { appJotaiStore } from "./app-jotai"; | ||||
|  | ||||
| import "./index.scss"; | ||||
| import { ResolutionType } from "../utility-types"; | ||||
|  | ||||
| polyfill(); | ||||
|  | ||||
|   | ||||
							
								
								
									
										47
									
								
								src/global.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										47
									
								
								src/global.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -50,36 +50,6 @@ interface Clipboard extends EventTarget { | ||||
|   write(data: any[]): Promise<void>; | ||||
| } | ||||
|  | ||||
| type Mutable<T> = { | ||||
|   -readonly [P in keyof T]: T[P]; | ||||
| }; | ||||
|  | ||||
| type ValueOf<T> = T[keyof T]; | ||||
|  | ||||
| type Merge<M, N> = Omit<M, keyof N> & N; | ||||
|  | ||||
| /** utility type to assert that the second type is a subtype of the first type. | ||||
|  * Returns the subtype. */ | ||||
| type SubtypeOf<Supertype, Subtype extends Supertype> = Subtype; | ||||
|  | ||||
| type ResolutionType<T extends (...args: any) => any> = T extends ( | ||||
|   ...args: any | ||||
| ) => Promise<infer R> | ||||
|   ? R | ||||
|   : any; | ||||
|  | ||||
| // https://github.com/krzkaczor/ts-essentials | ||||
| type MarkOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; | ||||
|  | ||||
| type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> & | ||||
|   Required<Pick<T, RK>>; | ||||
|  | ||||
| type MarkNonNullable<T, K extends keyof T> = { | ||||
|   [P in K]-?: P extends K ? NonNullable<T[P]> : T[P]; | ||||
| } & { [P in keyof T]: T[P] }; | ||||
|  | ||||
| type NonOptional<T> = Exclude<T, undefined>; | ||||
|  | ||||
| // PNG encoding/decoding | ||||
| // ----------------------------------------------------------------------------- | ||||
| type TEXtChunk = { name: "tEXt"; data: Uint8Array }; | ||||
| @@ -101,23 +71,6 @@ declare module "png-chunks-extract" { | ||||
| } | ||||
| // ----------------------------------------------------------------------------- | ||||
|  | ||||
| // ----------------------------------------------------------------------------- | ||||
| // type getter for interface's callable type | ||||
| // src: https://stackoverflow.com/a/58658851/927631 | ||||
| // ----------------------------------------------------------------------------- | ||||
| type SignatureType<T> = T extends (...args: infer R) => any ? R : never; | ||||
| type CallableType<T extends (...args: any[]) => any> = ( | ||||
|   ...args: SignatureType<T> | ||||
| ) => ReturnType<T>; | ||||
| // --------------------------------------------------------------------------— | ||||
|  | ||||
| // Type for React.forwardRef --- supply only the first generic argument T | ||||
| type ForwardRef<T, P = any> = Parameters< | ||||
|   CallableType<React.ForwardRefRenderFunction<T, P>> | ||||
| >[1]; | ||||
|  | ||||
| // --------------------------------------------------------------------------— | ||||
|  | ||||
| interface Blob { | ||||
|   handle?: import("browser-fs-acces").FileSystemHandle; | ||||
|   name?: string; | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { AppState } from "./types"; | ||||
| import { ExcalidrawElement } from "./element/types"; | ||||
| import { isLinearElement } from "./element/typeChecks"; | ||||
| import { deepCopyElement } from "./element/newElement"; | ||||
| import { Mutable } from "./utility-types"; | ||||
|  | ||||
| export interface HistoryEntry { | ||||
|   appState: ReturnType<typeof clearAppStatePropertiesForHistory>; | ||||
|   | ||||
| @@ -110,6 +110,7 @@ | ||||
|     "increaseFontSize": "Increase font size", | ||||
|     "unbindText": "Unbind text", | ||||
|     "bindText": "Bind text to the container", | ||||
|     "createContainerFromText": "Wrap text in a container", | ||||
|     "link": { | ||||
|       "edit": "Edit link", | ||||
|       "create": "Create link", | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import { | ||||
| } from "./element/types"; | ||||
| import { getShapeForElement } from "./renderer/renderElement"; | ||||
| import { getCurvePathOps } from "./element/bounds"; | ||||
| import { Mutable } from "./utility-types"; | ||||
|  | ||||
| export const rotate = ( | ||||
|   x1: number, | ||||
|   | ||||
| @@ -2032,9 +2032,9 @@ dns-equal@^1.0.0: | ||||
|   integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= | ||||
|  | ||||
| dns-packet@^5.2.2: | ||||
|   version "5.3.1" | ||||
|   resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.3.1.tgz#eb94413789daec0f0ebe2fcc230bdc9d7c91b43d" | ||||
|   integrity sha512-spBwIj0TK0Ey3666GwIdWVfUpLyubpU53BTCu8iPn4r4oXd9O14Hjg3EHw3ts2oed77/SeckunUYCyRlSngqHw== | ||||
|   version "5.4.0" | ||||
|   resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.4.0.tgz#1f88477cf9f27e78a213fb6d118ae38e759a879b" | ||||
|   integrity sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g== | ||||
|   dependencies: | ||||
|     "@leichtgewicht/ip-codec" "^2.0.1" | ||||
|  | ||||
|   | ||||
| @@ -14,6 +14,7 @@ import { | ||||
|   isFreeDrawElement, | ||||
|   isInitializedImageElement, | ||||
|   isArrowElement, | ||||
|   hasBoundTextElement, | ||||
| } from "../element/typeChecks"; | ||||
| import { | ||||
|   getDiamondPoints, | ||||
| @@ -41,7 +42,10 @@ import { getStroke, StrokeOptions } from "perfect-freehand"; | ||||
| import { | ||||
|   getLineHeight, | ||||
|   getBoundTextElement, | ||||
|   getContainerCoords, | ||||
|   getContainerElement, | ||||
|   getContainerMaxHeight, | ||||
|   getContainerMaxWidth, | ||||
| } from "../element/textElement"; | ||||
| import { LinearElementEditor } from "../element/linearElementEditor"; | ||||
|  | ||||
| @@ -811,6 +815,24 @@ const drawElementFromCanvas = ( | ||||
|       elementWithCanvas.canvas!.width / elementWithCanvas.canvasZoom, | ||||
|       elementWithCanvas.canvas!.height / elementWithCanvas.canvasZoom, | ||||
|     ); | ||||
|  | ||||
|     if ( | ||||
|       process.env.REACT_APP_DEBUG_ENABLE_TEXT_CONTAINER_BOUNDING_BOX && | ||||
|       hasBoundTextElement(element) | ||||
|     ) { | ||||
|       const textElement = getBoundTextElement( | ||||
|         element, | ||||
|       ) as ExcalidrawTextElementWithContainer; | ||||
|       const coords = getContainerCoords(element); | ||||
|       context.strokeStyle = "#c92a2a"; | ||||
|       context.lineWidth = 3; | ||||
|       context.strokeRect( | ||||
|         (coords.x + renderConfig.scrollX) * window.devicePixelRatio, | ||||
|         (coords.y + renderConfig.scrollY) * window.devicePixelRatio, | ||||
|         getContainerMaxWidth(element) * window.devicePixelRatio, | ||||
|         getContainerMaxHeight(element, textElement) * window.devicePixelRatio, | ||||
|       ); | ||||
|     } | ||||
|   } | ||||
|   context.restore(); | ||||
|  | ||||
|   | ||||
| @@ -119,6 +119,15 @@ Object { | ||||
|           "category": "element", | ||||
|         }, | ||||
|       }, | ||||
|       Object { | ||||
|         "contextItemLabel": "labels.createContainerFromText", | ||||
|         "name": "createContainerFromText", | ||||
|         "perform": [Function], | ||||
|         "predicate": [Function], | ||||
|         "trackEvent": Object { | ||||
|           "category": "element", | ||||
|         }, | ||||
|       }, | ||||
|       Object { | ||||
|         "PanelComponent": [Function], | ||||
|         "contextItemLabel": "labels.ungroup", | ||||
| @@ -4507,6 +4516,15 @@ Object { | ||||
|           "category": "element", | ||||
|         }, | ||||
|       }, | ||||
|       Object { | ||||
|         "contextItemLabel": "labels.createContainerFromText", | ||||
|         "name": "createContainerFromText", | ||||
|         "perform": [Function], | ||||
|         "predicate": [Function], | ||||
|         "trackEvent": Object { | ||||
|           "category": "element", | ||||
|         }, | ||||
|       }, | ||||
|       Object { | ||||
|         "PanelComponent": [Function], | ||||
|         "contextItemLabel": "labels.ungroup", | ||||
| @@ -5048,6 +5066,15 @@ Object { | ||||
|           "category": "element", | ||||
|         }, | ||||
|       }, | ||||
|       Object { | ||||
|         "contextItemLabel": "labels.createContainerFromText", | ||||
|         "name": "createContainerFromText", | ||||
|         "perform": [Function], | ||||
|         "predicate": [Function], | ||||
|         "trackEvent": Object { | ||||
|           "category": "element", | ||||
|         }, | ||||
|       }, | ||||
|       Object { | ||||
|         "PanelComponent": [Function], | ||||
|         "contextItemLabel": "labels.ungroup", | ||||
| @@ -5888,6 +5915,15 @@ Object { | ||||
|           "category": "element", | ||||
|         }, | ||||
|       }, | ||||
|       Object { | ||||
|         "contextItemLabel": "labels.createContainerFromText", | ||||
|         "name": "createContainerFromText", | ||||
|         "perform": [Function], | ||||
|         "predicate": [Function], | ||||
|         "trackEvent": Object { | ||||
|           "category": "element", | ||||
|         }, | ||||
|       }, | ||||
|       Object { | ||||
|         "PanelComponent": [Function], | ||||
|         "contextItemLabel": "labels.ungroup", | ||||
| @@ -6225,6 +6261,15 @@ Object { | ||||
|           "category": "element", | ||||
|         }, | ||||
|       }, | ||||
|       Object { | ||||
|         "contextItemLabel": "labels.createContainerFromText", | ||||
|         "name": "createContainerFromText", | ||||
|         "perform": [Function], | ||||
|         "predicate": [Function], | ||||
|         "trackEvent": Object { | ||||
|           "category": "element", | ||||
|         }, | ||||
|       }, | ||||
|       Object { | ||||
|         "PanelComponent": [Function], | ||||
|         "contextItemLabel": "labels.ungroup", | ||||
|   | ||||
| @@ -5,7 +5,7 @@ exports[`Test Linear Elements Test bound text element should match styles for te | ||||
|   class="excalidraw-wysiwyg" | ||||
|   data-type="wysiwyg" | ||||
|   dir="auto" | ||||
|   style="position: absolute; display: inline-block; min-height: 1em; 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: 10px; height: 24px; left: 35px; top: 8px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -8px; font: Emoji 20px 20px; line-height: 24px; font-family: Virgil, Segoe UI Emoji;" | ||||
|   style="position: absolute; display: inline-block; min-height: 1em; 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: 24px; left: 35px; top: 8px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(0, 0, 0); opacity: 1; filter: var(--theme-filter); max-height: -8px; font: Emoji 20px 20px; line-height: 24px; font-family: Virgil, Segoe UI Emoji;" | ||||
|   tabindex="0" | ||||
|   wrap="off" | ||||
| /> | ||||
|   | ||||
| @@ -19,6 +19,7 @@ import { newFreeDrawElement, newImageElement } from "../../element/newElement"; | ||||
| import { Point } from "../../types"; | ||||
| import { getSelectedElements } from "../../scene/selection"; | ||||
| import { isLinearElementType } from "../../element/typeChecks"; | ||||
| import { Mutable } from "../../utility-types"; | ||||
|  | ||||
| const readFile = util.promisify(fs.readFile); | ||||
|  | ||||
|   | ||||
							
								
								
									
										30
									
								
								src/tests/shortcuts.test.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/tests/shortcuts.test.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import { KEYS } from "../keys"; | ||||
| import { Excalidraw } from "../packages/excalidraw/entry"; | ||||
| import { API } from "./helpers/api"; | ||||
| import { Keyboard } from "./helpers/ui"; | ||||
| import { fireEvent, render, waitFor } from "./test-utils"; | ||||
|  | ||||
| describe("shortcuts", () => { | ||||
|   it("Clear canvas shortcut should display confirm dialog", async () => { | ||||
|     await render( | ||||
|       <Excalidraw | ||||
|         initialData={{ elements: [API.createElement({ type: "rectangle" })] }} | ||||
|         handleKeyboardGlobally | ||||
|       />, | ||||
|     ); | ||||
|  | ||||
|     expect(window.h.elements.length).toBe(1); | ||||
|  | ||||
|     Keyboard.withModifierKeys({ ctrl: true }, () => { | ||||
|       Keyboard.keyDown(KEYS.DELETE); | ||||
|     }); | ||||
|     const confirmDialog = document.querySelector(".confirm-dialog")!; | ||||
|     expect(confirmDialog).not.toBe(null); | ||||
|  | ||||
|     fireEvent.click(confirmDialog.querySelector('[aria-label="Confirm"]')!); | ||||
|  | ||||
|     await waitFor(() => { | ||||
|       expect(window.h.elements[0].isDeleted).toBe(true); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
| @@ -31,6 +31,7 @@ import Library from "./data/library"; | ||||
| import type { FileSystemHandle } from "./data/filesystem"; | ||||
| import type { ALLOWED_IMAGE_MIME_TYPES, MIME_TYPES } from "./constants"; | ||||
| import { ContextMenuItems } from "./components/ContextMenu"; | ||||
| import { Merge, ForwardRef } from "./utility-types"; | ||||
|  | ||||
| export type Point = Readonly<RoughPoint>; | ||||
|  | ||||
|   | ||||
							
								
								
									
										49
									
								
								src/utility-types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/utility-types.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| export type Mutable<T> = { | ||||
|   -readonly [P in keyof T]: T[P]; | ||||
| }; | ||||
|  | ||||
| export type ValueOf<T> = T[keyof T]; | ||||
|  | ||||
| export type Merge<M, N> = Omit<M, keyof N> & N; | ||||
|  | ||||
| /** utility type to assert that the second type is a subtype of the first type. | ||||
|  * Returns the subtype. */ | ||||
| export type SubtypeOf<Supertype, Subtype extends Supertype> = Subtype; | ||||
|  | ||||
| export type ResolutionType<T extends (...args: any) => any> = T extends ( | ||||
|   ...args: any | ||||
| ) => Promise<infer R> | ||||
|   ? R | ||||
|   : any; | ||||
|  | ||||
| // https://github.com/krzkaczor/ts-essentials | ||||
| export type MarkOptional<T, K extends keyof T> = Omit<T, K> & | ||||
|   Partial<Pick<T, K>>; | ||||
|  | ||||
| export type MarkRequired<T, RK extends keyof T> = Exclude<T, RK> & | ||||
|   Required<Pick<T, RK>>; | ||||
|  | ||||
| export type MarkNonNullable<T, K extends keyof T> = { | ||||
|   [P in K]-?: P extends K ? NonNullable<T[P]> : T[P]; | ||||
| } & { [P in keyof T]: T[P] }; | ||||
|  | ||||
| export type NonOptional<T> = Exclude<T, undefined>; | ||||
|  | ||||
| // ----------------------------------------------------------------------------- | ||||
| // type getter for interface's callable type | ||||
| // src: https://stackoverflow.com/a/58658851/927631 | ||||
| // ----------------------------------------------------------------------------- | ||||
| export type SignatureType<T> = T extends (...args: infer R) => any ? R : never; | ||||
| export type CallableType<T extends (...args: any[]) => any> = ( | ||||
|   ...args: SignatureType<T> | ||||
| ) => ReturnType<T>; | ||||
| // --------------------------------------------------------------------------— | ||||
|  | ||||
| // Type for React.forwardRef --- supply only the first generic argument T | ||||
| export type ForwardRef<T, P = any> = Parameters< | ||||
|   CallableType<React.ForwardRefRenderFunction<T, P>> | ||||
| >[1]; | ||||
|  | ||||
| export type ExtractSetType<T extends Set<any>> = T extends Set<infer U> | ||||
|   ? U | ||||
|   : never; | ||||
| @@ -16,6 +16,7 @@ import { AppState, DataURL, LastActiveTool, Zoom } from "./types"; | ||||
| import { unstable_batchedUpdates } from "react-dom"; | ||||
| import { SHAPES } from "./shapes"; | ||||
| import { isEraserActive, isHandToolActive } from "./appState"; | ||||
| import { ResolutionType } from "./utility-types"; | ||||
|  | ||||
| let mockDateTime: string | null = null; | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Aakansha Doshi
					Aakansha Doshi